diff --git a/lib/folk-shape.ts b/lib/folk-shape.ts index 4bbda8e..6fab2b0 100644 --- a/lib/folk-shape.ts +++ b/lib/folk-shape.ts @@ -422,7 +422,7 @@ export class FolkShape extends FolkElement {
-
Forgotten — right-click to remember or delete
`, +
Forgotten — right-click to remember or forget permanently
`, ); this.#handles = Object.fromEntries( diff --git a/website/canvas.html b/website/canvas.html index ea82d1e..189c088 100644 --- a/website/canvas.html +++ b/website/canvas.html @@ -1148,6 +1148,55 @@ display: none !important; } + /* ── Memory graph mode ── */ + #canvas.memory-mode { + overflow: hidden; + touch-action: none; + } + #canvas.memory-mode #canvas-content { + display: none !important; + } + body:has(#canvas.memory-mode) #toolbar, + body:has(#canvas.memory-mode) #bottom-toolbar { + display: none !important; + } + body:has(#canvas.memory-mode) #canvas-corner-tools #corner-zoom-toggle, + body:has(#canvas.memory-mode) #canvas-corner-tools #zoom-in, + body:has(#canvas.memory-mode) #canvas-corner-tools #zoom-out, + body:has(#canvas.memory-mode) #canvas-corner-tools #reset-view, + body:has(#canvas.memory-mode) #canvas-corner-tools .corner-sep { + display: none !important; + } + #memory-graph-overlay { + position: absolute; + inset: 0; + z-index: 10; + } + #memory-graph-overlay svg { + width: 100%; + height: 100%; + } + .memory-bubble { cursor: pointer; } + .memory-bubble:hover circle { filter: brightness(1.3); } + .memory-bubble text { + fill: var(--rs-text-primary, #e2e8f0); + font-family: system-ui, sans-serif; + pointer-events: none; + text-anchor: middle; + dominant-baseline: central; + } + #memory-graph-title { + position: absolute; + top: 80px; + left: 50%; + transform: translateX(-50%); + color: var(--rs-text-secondary, #94a3b8); + font: italic 0.875rem/1.4 system-ui, sans-serif; + text-align: center; + pointer-events: none; + z-index: 11; + } + /* ── Feed card wrappers ── */ .feed-card { width: min(100%, 600px); @@ -1615,6 +1664,7 @@ } /* When collapsed on mobile, hide feed toggle too — show only zoom icon */ #canvas-corner-tools.collapsed #feed-toggle, + #canvas-corner-tools.collapsed #memory-graph-toggle, #canvas-corner-tools.collapsed .corner-sep { display: none !important; } @@ -2028,6 +2078,9 @@ + @@ -5466,7 +5519,7 @@ Use real coordinates, YYYY-MM-DD dates, ISO currency codes. Ask clarifying quest if (!alreadyForgotten) { html += ``; } - html += ``; + html += ``; } shapeContextMenu.innerHTML = html; @@ -5795,6 +5848,12 @@ Use real coordinates, YYYY-MM-DD dates, ISO currency codes. Ask clarifying quest memoryList.innerHTML = ""; + // ── Explainer blurb ── + const explainer = document.createElement("div"); + explainer.style.cssText = "font-style:italic;color:var(--rs-text-secondary,#94a3b8);font-size:0.75rem;line-height:1.5;padding:8px 12px 12px;opacity:0.85"; + explainer.textContent = "In collective knowledge spaces, forgetting is natural. When you forget a shape, it fades — but persists. The more people who forget it, the fainter it becomes. Shapes everyone still remembers stay vivid. You can always choose to remember what others have forgotten."; + memoryList.appendChild(explainer); + // ── Fading section ── if (faded.length > 0) { const header = document.createElement("div"); @@ -5818,7 +5877,7 @@ Use real coordinates, YYYY-MM-DD dates, ISO currency codes. Ask clarifying quest ${forgetCount > 0 ? `${forgetCount}x` : ''} - + `; item.querySelector(".remember-btn").addEventListener("click", (e) => { @@ -5841,7 +5900,7 @@ Use real coordinates, YYYY-MM-DD dates, ISO currency codes. Ask clarifying quest if (deleted.length > 0) { const header = document.createElement("div"); header.className = "memory-section-header"; - header.textContent = "Deleted"; + header.textContent = "Forgotten permanently"; memoryList.appendChild(header); for (const shape of deleted) { @@ -6379,11 +6438,11 @@ Use real coordinates, YYYY-MM-DD dates, ISO currency codes. Ask clarifying quest dialog.style.cssText = "background:var(--rs-bg-surface,#1e1b4b);border:1px solid var(--rs-border,#334155);border-radius:12px;padding:1.5rem 2rem;max-width:380px;text-align:center;color:var(--rs-text-primary,#e2e8f0);font-family:system-ui,sans-serif;user-select:none"; const btnBase = "padding:0.5rem 1.25rem;border-radius:8px;cursor:pointer;font-size:0.875rem;touch-action:manipulation;user-select:none;-webkit-tap-highlight-color:transparent;transition:filter 0.1s"; dialog.innerHTML = ` -

Delete ${count} elements?

-

You are about to delete a large number of elements. This action cannot be easily undone.

+

Forget ${count} elements?

+

These shapes will fade from your view. Others may still remember them. Shapes forgotten by everyone eventually dissolve.

- +
`; overlay.appendChild(dialog); document.body.appendChild(overlay); @@ -6904,6 +6963,8 @@ Use real coordinates, YYYY-MM-DD dates, ISO currency codes. Ask clarifying quest } function toggleFeedMode() { + // Exit memory mode first if active + if (memoryMode) toggleMemoryGraph(); feedMode = !feedMode; canvas.classList.toggle('feed-mode', feedMode); feedToggleBtn.classList.toggle('active', feedMode); @@ -6949,6 +7010,171 @@ Use real coordinates, YYYY-MM-DD dates, ISO currency codes. Ask clarifying quest if (feedMode) sortFeedShapes(feedSortKey); }); + // ── Collective Memory graph view ── + const memoryGraphBtn = document.getElementById('memory-graph-toggle'); + let memoryMode = false; + + function toggleMemoryGraph() { + // Exit feed mode first if active + if (feedMode) toggleFeedMode(); + memoryMode = !memoryMode; + canvas.classList.toggle('memory-mode', memoryMode); + memoryGraphBtn.classList.toggle('active', memoryMode); + if (memoryMode) { + renderMemoryGraph(); + } else { + const overlay = document.getElementById('memory-graph-overlay'); + if (overlay) overlay.remove(); + const title = document.getElementById('memory-graph-title'); + if (title) title.remove(); + } + } + + function renderMemoryGraph() { + // Remove previous + const old = document.getElementById('memory-graph-overlay'); + if (old) old.remove(); + const oldTitle = document.getElementById('memory-graph-title'); + if (oldTitle) oldTitle.remove(); + + const shapes = sync.doc.shapes || {}; + const members = sync.doc.members || {}; + const totalMembers = Math.max(Object.keys(members).length, 1); + + // Build nodes from non-deleted shapes + const nodes = []; + for (const [id, shape] of Object.entries(shapes)) { + if (shape.deleted) continue; + const fb = shape.forgottenBy; + const forgottenCount = (fb && typeof fb === 'object') ? Object.keys(fb).length : 0; + const ratio = (totalMembers - forgottenCount) / totalMembers; + nodes.push({ + id, + type: shape.type || 'shape', + label: getShapeLabel(shape), + icon: SHAPE_ICONS[shape.type] || '📦', + ratio: Math.max(ratio, 0), + forgottenCount, + totalMembers, + }); + } + + if (nodes.length === 0) { + memoryMode = false; + canvas.classList.remove('memory-mode'); + memoryGraphBtn.classList.remove('active'); + return; + } + + const w = canvas.clientWidth; + const h = canvas.clientHeight; + const cx = w / 2; + const cy = h / 2; + const minR = 12, maxR = 80; + + // Assign radii and initial positions + for (const n of nodes) { + n.r = minR + (maxR - minR) * n.ratio; + n.x = cx + (Math.random() - 0.5) * w * 0.6; + n.y = cy + (Math.random() - 0.5) * h * 0.6; + n.vx = 0; + n.vy = 0; + } + + // Simple force simulation: center gravity + collision + for (let iter = 0; iter < 60; iter++) { + for (const n of nodes) { + // Center gravity — stronger for more-remembered shapes + const dx = cx - n.x; + const dy = cy - n.y; + const pull = 0.02 * n.ratio; + n.vx += dx * pull; + n.vy += dy * pull; + } + // Collision avoidance + for (let i = 0; i < nodes.length; i++) { + for (let j = i + 1; j < nodes.length; j++) { + const a = nodes[i], b = nodes[j]; + let ddx = b.x - a.x; + let ddy = b.y - a.y; + const dist = Math.sqrt(ddx * ddx + ddy * ddy) || 1; + const minDist = a.r + b.r + 4; + if (dist < minDist) { + const push = (minDist - dist) / dist * 0.3; + const px = ddx * push; + const py = ddy * push; + a.vx -= px; a.vy -= py; + b.vx += px; b.vy += py; + } + } + } + // Apply velocity with damping + for (const n of nodes) { + n.x += n.vx; + n.y += n.vy; + n.vx *= 0.6; + n.vy *= 0.6; + // Clamp to viewport + n.x = Math.max(n.r + 8, Math.min(w - n.r - 8, n.x)); + n.y = Math.max(n.r + 100, Math.min(h - n.r - 8, n.y)); + } + } + + // Color: green (120) → amber (40) → red (0) + function ratioToColor(ratio, opacity) { + const hue = ratio * 120; // 0=red, 60=amber, 120=green + return `hsla(${hue}, 70%, 45%, ${opacity})`; + } + + // Render SVG + const overlay = document.createElement('div'); + overlay.id = 'memory-graph-overlay'; + + let svg = ``; + for (const n of nodes) { + const opacity = Math.max(0.2, n.ratio); + const fill = ratioToColor(n.ratio, opacity); + const truncLabel = n.label.length > 16 ? n.label.slice(0, 14) + '…' : n.label; + const fontSize = Math.max(9, Math.min(14, n.r * 0.35)); + const tooltipText = `${n.label} (${n.type}) — forgotten by ${n.forgottenCount} of ${n.totalMembers}`; + svg += ``; + svg += `${escapeHtml(tooltipText)}`; + svg += ``; + svg += `${n.icon}`; + svg += `${escapeHtml(truncLabel)}`; + svg += ``; + } + svg += ``; + overlay.innerHTML = svg; + canvas.appendChild(overlay); + + // Title + const titleEl = document.createElement('div'); + titleEl.id = 'memory-graph-title'; + titleEl.textContent = `Collective Memory — ${nodes.length} shapes across ${totalMembers} member${totalMembers !== 1 ? 's' : ''}`; + canvas.appendChild(titleEl); + + // Click → exit memory mode and pan to shape + overlay.addEventListener('click', (e) => { + const bubble = e.target.closest('.memory-bubble'); + if (!bubble) return; + const shapeId = bubble.dataset.shapeId; + toggleMemoryGraph(); // exit + // Pan canvas to center on the shape + const shapeData = (sync.doc.shapes || {})[shapeId]; + if (shapeData && shapeData.x != null && shapeData.y != null) { + const rect = canvas.getBoundingClientRect(); + const sw = shapeData.width || 300; + const sh = shapeData.height || 200; + panX = rect.width / 2 - (shapeData.x + sw / 2) * scale; + panY = rect.height / 2 - (shapeData.y + sh / 2) * scale; + updateCanvasTransform(); + } + }); + } + + memoryGraphBtn.addEventListener('click', toggleMemoryGraph); + sync.connect(wsUrl); // Debug: expose sync for console inspection