Focus story visualisation on insights
This commit is contained in:
parent
049628b295
commit
588a223b44
@ -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.");
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user