Focus story visualisation on insights

This commit is contained in:
Nick Beckley 2026-07-26 15:21:06 +00:00
parent 049628b295
commit 588a223b44
4 changed files with 27 additions and 242 deletions

View File

@ -1179,6 +1179,10 @@ static void Phase21RSceneBrowserAndPanelsAreReplayFriendly()
Assert(css.Contains("scrollbar-width: none", StringComparison.Ordinal), "Side-panel native scrollbar tracks should be suppressed.");
Assert(css.Contains(".story-exp-rebuild {\n position: fixed;", StringComparison.Ordinal), "Replay rebuild controls should be fixed so they do not resize the whole screen.");
Assert(css.Contains(".story-exp-panel:has(.story-exp-insight-list:empty)", StringComparison.Ordinal), "Empty side panels should collapse instead of reserving large blank space.");
Assert(css.Contains("grid-template-columns: minmax(180px, 0.42fr) minmax(660px, 1.55fr) minmax(380px, 0.84fr);", StringComparison.Ordinal), "Right insight rail should be widened now that assets are not stage nodes.");
Assert(css.Contains("--story-side-body-font-size: 0.72rem;", StringComparison.Ordinal), "Left and right side panels should share body text sizing.");
Assert(!view.Contains("data-assets", StringComparison.Ordinal), "Asset visualisation lane should not be rendered on the main stage.");
Assert(script.Contains("Assets are recorded in insights/diagnostics but not rendered as stage nodes.", StringComparison.Ordinal), "Asset data should remain available without drawing asset stage nodes.");
Assert(!css.Contains("grid-template-rows: minmax(218px, 1.35fr) minmax(185px, 1.08fr) auto", StringComparison.Ordinal), "Right rail must not use fixed-height panel tracks.");
Assert(!css.Contains("grid-template-rows: auto auto minmax(170px, 1fr) minmax(96px, 0.56fr)", StringComparison.Ordinal), "Left rail must not use fixed-height panel tracks.");
Assert(script.Contains("shouldUseCharacterPortrait", StringComparison.Ordinal), "Characters should use evidence cards until portrait evidence is strong enough.");

View File

@ -197,10 +197,6 @@
<p class="story-exp-zone-label">Location</p>
<div class="story-exp-location" data-location></div>
</div>
<div class="story-exp-zone story-exp-zone--assets" aria-label="Significant assets">
<p class="story-exp-zone-label">Assets</p>
<div class="story-exp-assets" data-assets></div>
</div>
</div>
<section class="story-exp-ribbon-wrap" aria-label="Recent scenes and timeline">

View File

@ -16,6 +16,8 @@
--story-rose: #e38ca8;
--story-radius: 18px;
--story-shadow: 0 28px 84px rgba(0, 0, 0, 0.32);
--story-side-body-font-size: 0.72rem;
--story-side-title-font-size: 0.76rem;
--progress-value: 0deg;
}
@ -341,7 +343,7 @@ input:focus-visible {
.story-exp-shell {
display: grid;
grid-template-columns: minmax(180px, 0.46fr) minmax(820px, 2.72fr) minmax(188px, 0.48fr);
grid-template-columns: minmax(180px, 0.42fr) minmax(660px, 1.55fr) minmax(380px, 0.84fr);
gap: 12px;
min-height: 0;
padding: 12px 18px 58px;
@ -538,7 +540,7 @@ input:focus-visible {
.story-exp-panel h2 {
margin: 0 0 5px;
font-size: 0.82rem;
font-size: var(--story-side-title-font-size);
letter-spacing: 0.01em;
}
@ -561,6 +563,7 @@ input:focus-visible {
padding: 6px 7px;
background: rgba(255, 255, 255, 0.035);
color: var(--story-soft);
font-size: var(--story-side-body-font-size);
min-width: 0;
overflow-wrap: anywhere;
}
@ -778,11 +781,6 @@ input:focus-visible {
top: 22px;
}
.story-exp-zone--assets .story-exp-zone-label {
right: 8%;
top: 22px;
}
.story-exp-characters {
position: relative;
z-index: 120;
@ -811,8 +809,7 @@ input:focus-visible {
}
.story-character__image,
.story-location__image,
.story-asset__image {
.story-location__image {
position: relative;
z-index: 1;
display: block;
@ -829,8 +826,7 @@ input:focus-visible {
}
.story-character__image img,
.story-location__image img,
.story-asset__image img {
.story-location__image img {
width: 100%;
height: 100%;
object-fit: cover;
@ -1027,108 +1023,6 @@ input:focus-visible {
white-space: nowrap;
}
.story-exp-assets {
position: relative;
width: 100%;
height: 100%;
}
.story-asset {
position: absolute;
display: grid;
justify-items: center;
gap: 6px;
width: max(var(--node-size, 78px), 104px);
opacity: var(--opacity, 1);
transform: translate(var(--x, 0), var(--y, 0)) scale(var(--scale, 1));
z-index: calc(var(--z, 3) + 30);
pointer-events: auto;
transition: transform 860ms cubic-bezier(.16, .84, .28, 1), opacity 620ms ease;
}
.story-asset__image {
width: var(--node-size, 78px);
height: var(--node-size, 78px);
border-radius: 50%;
border-color: rgba(215, 168, 93, 0.68);
box-shadow: 0 0 0 4px rgba(215, 168, 93, 0.08), 0 18px 38px rgba(0, 0, 0, 0.28), 0 0 24px rgba(215, 168, 93, 0.13);
}
.story-asset__text {
position: relative;
z-index: 80;
display: grid;
justify-items: center;
gap: 2px;
width: min(140px, calc(var(--node-size, 78px) + 52px));
border: 1px solid rgba(215, 168, 93, 0.11);
border-radius: 10px;
padding: 4px 7px 5px;
background: rgba(4, 10, 20, 0.6);
box-shadow: 0 12px 26px rgba(0, 0, 0, 0.22);
backdrop-filter: blur(8px);
text-align: center;
}
.story-asset.is-evidence-card {
width: var(--node-size, 78px);
height: var(--node-size, 78px);
}
.story-asset.is-evidence-card .story-asset__image {
position: absolute;
inset: 0;
opacity: 0;
pointer-events: none;
}
.story-asset.is-evidence-card .story-asset__text {
position: absolute;
inset: 0;
align-content: center;
justify-items: start;
width: 100%;
min-height: 100%;
border-color: rgba(215, 168, 93, 0.18);
padding: 7px 8px;
background:
linear-gradient(180deg, rgba(34, 29, 18, 0.88), rgba(7, 13, 21, 0.84));
text-align: left;
}
.story-asset.is-evidence-card .story-asset__text strong,
.story-asset.is-evidence-card .story-asset__text span {
text-align: left;
}
.story-asset strong,
.story-asset span {
display: block;
overflow: hidden;
max-width: 100%;
line-height: 1.08;
text-overflow: ellipsis;
white-space: nowrap;
}
.story-asset strong {
font-size: 0.78rem;
}
.story-asset span {
color: var(--story-muted);
font-size: 0.64rem;
}
.story-asset .story-entity-facts span {
display: inline-flex;
width: auto;
max-width: 100%;
border-radius: 999px;
line-height: 1.2;
text-overflow: ellipsis;
}
.story-exp-ribbon-wrap {
display: grid;
grid-template-rows: auto minmax(104px, auto) 56px;
@ -1342,7 +1236,7 @@ input:focus-visible {
display: -webkit-box;
overflow: hidden;
margin: 0;
font-size: 0.72rem;
font-size: var(--story-side-body-font-size);
line-height: 1.16;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
@ -1359,7 +1253,7 @@ input:focus-visible {
.story-exp-insight strong {
color: var(--story-text);
font-size: 0.76rem;
font-size: var(--story-side-title-font-size);
line-height: 1.12;
}
@ -1518,8 +1412,7 @@ input:focus-visible {
transform: translateY(12px) scale(0.96);
}
.story-character.is-entering,
.story-asset.is-entering {
.story-character.is-entering {
opacity: 0;
transform: translate(var(--x, 0), var(--y, 0)) scale(0.82);
}

View File

@ -110,7 +110,7 @@
const layout = sceneLayout(current);
reconcileCharacters(current, previous, layout);
reconcileLocation(current, previous);
reconcileAssets(current, previous, layout);
clearAssetStage();
reconcileRelationships(current);
updateKnowledgeThreads(current);
updateSceneRibbon(current);
@ -381,7 +381,7 @@
return [
scene.location?.imagePath,
...(scene.characters || []).map((character) => character.imagePath),
...(scene.assets || []).map((asset) => asset.imagePath)
// Assets are recorded in insights/diagnostics but not rendered as stage nodes.
].filter(Boolean);
}
@ -490,43 +490,8 @@
node.title = `${scene.location.name}: ${scene.location.label}`;
}
function reconcileAssets(scene, previous, layout) {
const activeIds = new Set(scene.assets.map((asset) => asset.id));
scene.assets.forEach((asset, index) => {
let node = dom.assets.querySelector(`[data-asset-id="${cssEscape(asset.id)}"]`);
if (!node) {
node = createAssetNode(asset);
node.classList.add("is-entering");
dom.assets.append(node);
settleEntering(node);
}
const position = layout.assets.get(asset.id) || assetPosition(asset, index, scene.assets.length);
node.style.setProperty("--node-size", `${position.size}px`);
node.style.setProperty("--x", `${position.x}px`);
node.style.setProperty("--y", `${position.y}px`);
node.style.setProperty("--scale", String(position.scale));
node.style.setProperty("--z", String(position.z));
node.style.setProperty("--opacity", "1");
const assetImageReady = shouldUseAssetImage(asset);
node.classList.toggle("is-evidence-card", !assetImageReady);
setEntityImage(node.querySelector("img"), assetImageReady ? asset.imagePath : "", asset.imageResolution, `${asset.name} prototype asset`);
text(node.querySelector("[data-asset-name]"), asset.name);
text(node.querySelector("[data-asset-label]"), conciseAssetLabel(asset));
renderFactChips(node.querySelector("[data-asset-facts]"), assetFacts(asset));
node.title = `${asset.name}: ${asset.label}`;
});
dom.assets.querySelectorAll("[data-asset-id]").forEach((node) => {
if (!activeIds.has(node.dataset.assetId)) {
node.style.setProperty("--opacity", "0");
node.style.setProperty("--scale", "0.8");
window.setTimeout(() => {
const currentIds = new Set(loadSceneState(state.index).assets.map((asset) => asset.id));
if (!currentIds.has(node.dataset.assetId)) node.remove();
}, reducedMotion ? 0 : 520);
}
});
function clearAssetStage() {
dom.assets?.replaceChildren();
}
function reconcileRelationships(scene) {
@ -750,14 +715,6 @@
}
});
scene.assets.forEach((asset, index) => {
const assetNode = dom.visualStage.querySelector(`[data-asset-id="${cssEscape(asset.id)}"]`);
if (!assetNode || !location) return;
const id = `asset-${location.dataset.locationId}-${asset.id}`;
activeIds.add(id);
drawn.push(drawConnection(id, connectionPoint(location, stageRect, assetNode), connectionPoint(assetNode, stageRect, location), "evidence", asset.weight, index === 0 ? assetConnectionLabel(asset) : "", drawn.length, connectionObstacles(stageRect, location, assetNode), occupiedRects));
});
removeInactiveSvgConnections(activeIds);
}
@ -835,27 +792,12 @@
return node;
}
function createAssetNode(asset) {
const node = document.createElement("article");
node.className = "story-asset";
node.dataset.assetId = asset.id;
node.innerHTML = `
<div class="story-asset__image"><img loading="eager" /></div>
<div class="story-asset__text">
<strong data-asset-name></strong>
<span data-asset-label></span>
<div class="story-entity-facts" data-asset-facts></div>
</div>`;
return node;
}
function sceneLayout(scene) {
const bounds = dom.visualStage?.getBoundingClientRect();
const width = Math.max(760, bounds?.width || 1040);
const height = Math.max(500, bounds?.height || 560);
return {
characters: characterLayout(scene, width, height),
assets: assetLayout(scene, width, height)
characters: characterLayout(scene, width, height)
};
}
@ -971,56 +913,6 @@
};
}
function assetPosition(asset, index, total) {
const bounds = dom.visualStage?.getBoundingClientRect();
const width = Math.max(820, bounds?.width || 1040);
const height = Math.max(500, bounds?.height || 560);
const anchors = [
{ x: 0.83, y: 0.26 },
{ x: 0.84, y: 0.51 },
{ x: 0.82, y: 0.74 },
{ x: 0.72, y: 0.19 }
];
const anchor = anchors[index % anchors.length];
const size = Math.min(104, Math.max(68, (64 + Math.min(asset.weight, 100) * 0.42) * 0.9));
return {
x: Math.round(width * anchor.x - size / 2),
y: Math.round(height * anchor.y - size / 2),
size: Math.round(size),
scale: 1
};
}
function assetLayout(scene, width, height) {
const positions = new Map();
const total = scene.assets.length;
const anchorsByTotal = {
1: [{ x: 0.83, y: 0.48 }],
2: [{ x: 0.83, y: 0.34 }, { x: 0.83, y: 0.62 }],
3: [{ x: 0.82, y: 0.25 }, { x: 0.85, y: 0.50 }, { x: 0.81, y: 0.75 }],
4: [{ x: 0.78, y: 0.16 }, { x: 0.90, y: 0.37 }, { x: 0.86, y: 0.62 }, { x: 0.73, y: 0.82 }],
5: [{ x: 0.76, y: 0.13 }, { x: 0.91, y: 0.30 }, { x: 0.80, y: 0.48 }, { x: 0.92, y: 0.66 }, { x: 0.72, y: 0.84 }]
};
const anchors = anchorsByTotal[Math.min(5, Math.max(1, total))] || anchorsByTotal[5];
const placed = [];
scene.assets.forEach((asset, index) => {
const anchor = anchors[index] || anchors[anchors.length - 1];
const important = index === 0 || asset.weight >= 90;
const size = Math.round(important
? Math.min(104, Math.max(83, (70 + Math.min(asset.weight, 100) * 0.42) * 0.9))
: Math.min(88, Math.max(68, (58 + Math.min(asset.weight, 100) * 0.32) * 0.9)));
const desired = {
x: width * anchor.x - size / 2,
y: height * anchor.y - size / 2
};
const resolved = avoidNodeCollisions(desired, size, placed, width, height);
const position = { x: resolved.x, y: resolved.y, size, scale: 1, z: important ? 4 : 3 };
placed.push(position);
positions.set(asset.id, position);
});
return positions;
}
function avoidNodeCollisions(desired, size, placed, width, height) {
const padding = Math.max(16, size * 0.12);
const candidates = [
@ -1066,11 +958,11 @@
}
function connectionPoint(node, parentRect, targetNode) {
const image = node.querySelector(".story-character__image, .story-location__image, .story-asset__image") || node;
const image = node.querySelector(".story-character__image, .story-location__image") || node;
const centre = centrePoint(image, parentRect);
const rect = image.getBoundingClientRect();
const target = targetNode ? centrePoint(targetNode, parentRect) : centre;
const targetImage = targetNode?.querySelector?.(".story-character__image, .story-location__image, .story-asset__image");
const targetImage = targetNode?.querySelector?.(".story-character__image, .story-location__image");
const targetCentre = targetImage ? centrePoint(targetImage, parentRect) : target;
const radius = Math.min(rect.width, rect.height) * 0.5;
const angle = Math.atan2(targetCentre.y - centre.y, targetCentre.x - centre.x);
@ -1121,14 +1013,14 @@
}
function layoutRects(parentRect) {
return [...dom.visualStage.querySelectorAll(".story-character__image, .story-location__image, .story-asset__image, .story-character__text, .story-location__text, .story-asset__text")]
return [...dom.visualStage.querySelectorAll(".story-character__image, .story-location__image, .story-character__text, .story-location__text")]
.map((node) => rectRelativeTo(node, parentRect, node.className.includes("__text") ? 8 : 10));
}
function connectionObstacles(parentRect, source, target) {
const sourceImage = source.querySelector(".story-character__image, .story-location__image, .story-asset__image") || source;
const targetImage = target.querySelector(".story-character__image, .story-location__image, .story-asset__image") || target;
return [...dom.visualStage.querySelectorAll(".story-character__image, .story-location__image, .story-asset__image, .story-character__text, .story-location__text, .story-asset__text")]
const sourceImage = source.querySelector(".story-character__image, .story-location__image") || source;
const targetImage = target.querySelector(".story-character__image, .story-location__image") || target;
return [...dom.visualStage.querySelectorAll(".story-character__image, .story-location__image, .story-character__text, .story-location__text")]
.filter((node) => node !== sourceImage && node !== targetImage)
.map((node) => rectRelativeTo(node, parentRect, node.className.includes("__text") ? 8 : 12));
}
@ -1659,7 +1551,7 @@
const layout = sceneLayout(current);
reconcileCharacters(current, current, layout);
reconcileLocation(current, current);
reconcileAssets(current, current, layout);
clearAssetStage();
scheduleConnectionDraw();
});
@ -1669,7 +1561,7 @@
const current = loadSceneState(state.index);
const layout = sceneLayout(current);
reconcileCharacters(current, current, layout);
reconcileAssets(current, current, layout);
clearAssetStage();
scheduleConnectionDraw();
});
observer.observe(dom.visualStage);