story redesign 2
All checks were successful
Build Authoring Service / build (push) Successful in 10s

This commit is contained in:
2026-05-15 16:21:50 +01:00
parent 1a0352fc3c
commit abf8c65f39

View File

@@ -1355,6 +1355,8 @@ HIDDEN_APP_HTML = r"""<!doctype html>
.memory-node .fragment-star { stroke-width: 1.1; } .memory-node .fragment-star { stroke-width: 1.1; }
.memory-node .fragment-ring { fill: none; stroke: rgba(255, 246, 223, 0.24); stroke-width: 1; stroke-dasharray: 2 7; } .memory-node .fragment-ring { fill: none; stroke: rgba(255, 246, 223, 0.24); stroke-width: 1; stroke-dasharray: 2 7; }
.memory-node .node-breathe, .cluster-node .node-breathe { animation: avatar-breathe 8s ease-in-out infinite; transform-box: fill-box; transform-origin: center; } .memory-node .node-breathe, .cluster-node .node-breathe { animation: avatar-breathe 8s ease-in-out infinite; transform-box: fill-box; transform-origin: center; }
.sequence-badge { fill: rgba(18, 16, 13, 0.88); stroke: rgba(255, 243, 199, 0.68); stroke-width: 1; }
.sequence-number { fill: #ffe7ad; font-size: 10px; font-weight: 850; paint-order: stroke; stroke: rgba(14, 11, 8, 0.72); stroke-width: 2px; }
.selection-halo { fill: rgba(255, 231, 173, 0.1); stroke: rgba(255, 243, 199, 0.95); stroke-width: 3; stroke-dasharray: 4 9; filter: drop-shadow(0 0 22px rgba(255, 231, 173, 0.62)); animation: selection-pulse 2.8s ease-in-out infinite; pointer-events: none; } .selection-halo { fill: rgba(255, 231, 173, 0.1); stroke: rgba(255, 243, 199, 0.95); stroke-width: 3; stroke-dasharray: 4 9; filter: drop-shadow(0 0 22px rgba(255, 231, 173, 0.62)); animation: selection-pulse 2.8s ease-in-out infinite; pointer-events: none; }
.memory-node:hover .node-shell, .memory-node:hover .node-portrait, .memory-node:hover .fragment-star { filter: drop-shadow(0 0 20px var(--node-glow, rgba(255, 232, 177, 0.4))); } .memory-node:hover .node-shell, .memory-node:hover .node-portrait, .memory-node:hover .fragment-star { filter: drop-shadow(0 0 20px var(--node-glow, rgba(255, 232, 177, 0.4))); }
.memory-node.selected .node-shell, .memory-node.selected .node-portrait, .memory-node.selected .fragment-star { stroke: #fff3c7; stroke-width: 3; } .memory-node.selected .node-shell, .memory-node.selected .node-portrait, .memory-node.selected .fragment-star { stroke: #fff3c7; stroke-width: 3; }
@@ -2631,6 +2633,74 @@ HIDDEN_APP_HTML = r"""<!doctype html>
function relationshipPairsForClusters(clusters) { function relationshipPairsForClusters(clusters) {
return []; return [];
} }
function storyLayoutItems(story, route, visibleEntries) {
const visible = new Set(visibleEntries.map((entry) => entry.id));
const routeIds = new Set(route.map((entry) => entry.id));
const items = [];
let sectionIndex = 0;
let sequenceIndex = 0;
storyItems(story).forEach((item) => {
if (item.kind === "section") {
sectionIndex += 1;
return;
}
if (!visible.has(item.id) || !routeIds.has(item.id)) return;
items.push({ id: item.id, sectionIndex, sequenceIndex });
sequenceIndex += 1;
});
return items;
}
function storyConstellationPoint(layout, item) {
const count = layout.count;
if (count <= 6) {
const step = item.sequenceIndex - (count - 1) / 2;
const arc = Math.sin(item.sequenceIndex / Math.max(1, count - 1) * Math.PI) * 58;
return {
x: step * 156,
y: arc + (item.sectionIndex % 2) * 18,
};
}
const row = Math.floor(item.sequenceIndex / layout.columns);
const indexInRow = item.sequenceIndex % layout.columns;
const reversed = row % 2 === 1;
const actualColumns = row === layout.rows - 1 && count % layout.columns ? count % layout.columns : layout.columns;
const col = reversed ? actualColumns - 1 - indexInRow : indexInRow;
const wave = Math.sin((indexInRow / Math.max(1, actualColumns - 1)) * Math.PI) * layout.arc;
const sectionLift = (item.sectionIndex % 3 - 1) * 18;
return {
x: (col - (actualColumns - 1) / 2) * layout.columnGap,
y: (row - (layout.rows - 1) / 2) * layout.rowGap + wave + sectionLift,
};
}
function storyConstellationLayout(story, route, entries, storyIndex, storyCount) {
const angle = (Math.PI * 2 * storyIndex) / Math.max(1, storyCount) + hash(story.id) / 9000;
const baseRadius = 520 + (storyIndex % 4) * 250;
const count = route.length;
const columns = count > 18 ? Math.ceil(Math.sqrt(count * 1.9)) : count > 10 ? Math.ceil(Math.sqrt(count * 1.65)) : Math.max(1, count);
const rows = Math.max(1, Math.ceil(count / Math.max(1, columns)));
const layout = {
count,
columns: Math.max(1, columns),
rows,
columnGap: count > 18 ? 178 : 160,
rowGap: count > 18 ? 134 : 122,
arc: count > 18 ? 38 : 52,
};
const anchorX = WORLD.cx + Math.cos(angle) * baseRadius;
const anchorY = WORLD.cy + Math.sin(angle) * baseRadius * 0.68;
const driftX = Math.cos(angle) * Math.min(130, 30 + count * 4);
const driftY = Math.sin(angle) * Math.min(92, 20 + count * 2.8);
const layoutItems = storyLayoutItems(story, route, entries);
layoutItems.forEach((item) => {
const point = storyConstellationPoint(layout, item);
state.positions.set(item.id, {
x: anchorX + driftX + point.x,
y: anchorY + driftY + point.y,
sequenceIndex: item.sequenceIndex,
});
});
}
function layoutEntries(entries) { function layoutEntries(entries) {
const cx = WORLD.cx; const cx = WORLD.cx;
const cy = WORLD.cy; const cy = WORLD.cy;
@@ -2638,23 +2708,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
const stories = filteredStories().filter((story) => (story.nodes || []).some((id) => entries.some((entry) => entry.id === id))); const stories = filteredStories().filter((story) => (story.nodes || []).some((id) => entries.some((entry) => entry.id === id)));
stories.forEach((story, storyIndex) => { stories.forEach((story, storyIndex) => {
const route = storyEntries(story).filter((entry) => entries.some((item) => item.id === entry.id)); const route = storyEntries(story).filter((entry) => entries.some((item) => item.id === entry.id));
const angle = (Math.PI * 2 * storyIndex) / Math.max(1, stories.length) + hash(story.id) / 9000; storyConstellationLayout(story, route, entries, storyIndex, stories.length);
const baseRadius = 520 + (storyIndex % 4) * 250;
const anchorX = cx + Math.cos(angle) * baseRadius;
const anchorY = cy + Math.sin(angle) * baseRadius * 0.68;
const denseRoute = route.length > 12;
const stepX = denseRoute ? 188 : 150;
const stepY = denseRoute ? 138 : 110;
const curveLift = denseRoute ? 150 : 90;
route.forEach((entry, index) => {
const step = index - (route.length - 1) / 2;
const curve = Math.sin(index / Math.max(1, route.length - 1) * Math.PI) * curveLift;
const direction = angle + Math.PI / 2;
const lane = denseRoute ? ((index % 2) - 0.5) * 70 : 0;
const x = anchorX + Math.cos(direction) * step * stepX + Math.cos(angle) * curve + Math.cos(angle) * lane;
const y = anchorY + Math.sin(direction) * step * stepY + Math.sin(angle) * curve * 0.7 + Math.sin(angle) * lane * 0.55;
state.positions.set(entry.id, { x, y });
});
}); });
const loose = entries.filter((entry) => !state.positions.has(entry.id)); const loose = entries.filter((entry) => !state.positions.has(entry.id));
const byLayer = new Map(); const byLayer = new Map();
@@ -2840,6 +2894,11 @@ HIDDEN_APP_HTML = r"""<!doctype html>
const clipId = `clip-node-${slugify(entry.id)}`; const clipId = `clip-node-${slugify(entry.id)}`;
defs.push(`<clipPath id="${html(clipId)}"><circle r="${size}"></circle></clipPath>`); defs.push(`<clipPath id="${html(clipId)}"><circle r="${size}"></circle></clipPath>`);
const distant = state.zoom === 0; const distant = state.zoom === 0;
const sequenceNumber = Number.isInteger(pos.sequenceIndex) ? pos.sequenceIndex + 1 : 0;
const badgeRadius = sequenceNumber > 99 ? 14 : sequenceNumber > 9 ? 12 : 10;
const sequenceBadge = sequenceNumber && (state.camera.k > 0.26 || activeStoryIds.has(entry.id) || related)
? `<g transform="translate(${Math.max(12, size * 0.62)} ${-Math.max(12, size * 0.62)})"><circle class="sequence-badge" r="${badgeRadius}"></circle><text class="sequence-number" x="0" y="3.5" text-anchor="middle">${sequenceNumber}</text></g>`
: "";
return `<g class="memory-node${isSelected ? " selected" : ""}${related && !isSelected ? " related-node" : ""}${dimmed ? " dimmed" : ""}${entry.enabled === false ? " resting" : ""}${dragClass}" data-id="${html(entry.id)}" tabindex="0" transform="translate(${pos.x} ${pos.y})" style="--node-glow:${html(avatarGlow(character))}"> return `<g class="memory-node${isSelected ? " selected" : ""}${related && !isSelected ? " related-node" : ""}${dimmed ? " dimmed" : ""}${entry.enabled === false ? " resting" : ""}${dragClass}" data-id="${html(entry.id)}" tabindex="0" transform="translate(${pos.x} ${pos.y})" style="--node-glow:${html(avatarGlow(character))}">
<circle class="hit-target" r="${Math.max(34, size + 16)}"></circle> <circle class="hit-target" r="${Math.max(34, size + 16)}"></circle>
${isSelected ? `<circle class="selection-halo" r="${Math.max(38, size + 18)}"></circle>` : ""} ${isSelected ? `<circle class="selection-halo" r="${Math.max(38, size + 18)}"></circle>` : ""}
@@ -2848,6 +2907,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
: distant : distant
? `<circle class="node-marker constellation-shimmer" r="${size}" fill="${color}" opacity="${entry.enabled === false ? 0.32 : 0.78}"></circle><text x="0" y="4" text-anchor="middle">${html(characterSymbol(character))}</text>` ? `<circle class="node-marker constellation-shimmer" r="${size}" fill="${color}" opacity="${entry.enabled === false ? 0.32 : 0.78}"></circle><text x="0" y="4" text-anchor="middle">${html(characterSymbol(character))}</text>`
: svgAvatar(character, size, clipId, avatarExpressionForEntry(entry), state.zoom >= 2 ? "node-breathe" : "")} : svgAvatar(character, size, clipId, avatarExpressionForEntry(entry), state.zoom >= 2 ? "node-breathe" : "")}
${sequenceBadge}
</g>${label}`; </g>${label}`;
}).join(""); }).join("");
graph.innerHTML = `<defs>${defs.join("")}</defs><g id="world" transform="${cameraTransform()}">${rings}${storyRegions}${clusterEdges}${edges}${clusterNodes}${nodes}<g id="dragLayer"></g></g>`; graph.innerHTML = `<defs>${defs.join("")}</defs><g id="world" transform="${cameraTransform()}">${rings}${storyRegions}${clusterEdges}${edges}${clusterNodes}${nodes}<g id="dragLayer"></g></g>`;