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 = ``;
+ 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