The caption wears the actor's face
Every reel caption now carries the acting wizard's standee at its left — the same token art the board plays with, framed in the caption's own ink — and the burned-in video caption bar gets the portrait too, so a shared clip names its actors by face as well as by name. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
f5151a0c99
commit
252d10711d
@@ -3,6 +3,7 @@
|
||||
import FirstPerson from "./fpv/FirstPerson.svelte";
|
||||
import { untrack } from "svelte";
|
||||
import { humanize } from "./net.svelte";
|
||||
import { tokenArt } from "./art";
|
||||
import { scheduleFx, type BoardFx } from "./fx";
|
||||
import { fpFxForEvents, type FpFx } from "./fpv/fx3d";
|
||||
import { castRay, edgeMid } from "./fpv/raycast";
|
||||
@@ -91,6 +92,12 @@
|
||||
);
|
||||
const atEnd = $derived(idx >= steps.length - 1);
|
||||
|
||||
/** The acting wizard's standee, shown beside their words. */
|
||||
const actorArt = $derived.by(() => {
|
||||
const p = step.view.players.find((x) => x.id === step.actor);
|
||||
return p ? tokenArt(`wizard-${p.colorIndex}`, "players") : null;
|
||||
});
|
||||
|
||||
// The reel draws the same sight line the live table shows for an LOS
|
||||
// attack in progress, so a replay-watcher can see how a spell reached them.
|
||||
const sightTrace = $derived(stackSightTrace(step.view));
|
||||
@@ -517,22 +524,41 @@
|
||||
comp.width = 1280;
|
||||
comp.height = 720;
|
||||
const g = comp.getContext("2d")!;
|
||||
const faces = new Map<string, HTMLImageElement>();
|
||||
const faceFor = (src: string): HTMLImageElement | null => {
|
||||
let img = faces.get(src);
|
||||
if (!img) {
|
||||
img = new Image();
|
||||
img.src = src;
|
||||
faces.set(src, img);
|
||||
}
|
||||
return img.complete && img.naturalWidth > 0 ? img : null;
|
||||
};
|
||||
let compRaf = 0;
|
||||
const drawComp = () => {
|
||||
g.imageSmoothingEnabled = false;
|
||||
g.drawImage(cv, 0, 0, 1280, 720);
|
||||
g.imageSmoothingEnabled = true;
|
||||
// The caption bar: who did what, in the reel's own words.
|
||||
// The caption bar: who did what, in the reel's own words — their
|
||||
// standee at the left.
|
||||
g.fillStyle = "rgba(12, 10, 8, 0.78)";
|
||||
g.fillRect(0, 720 - 96, 1280, 96);
|
||||
const face = actorArt ? faceFor(actorArt) : null;
|
||||
const textX = face ? 110 : 28;
|
||||
if (face) {
|
||||
g.drawImage(face, 24, 720 - 96 + 14, 68, 68);
|
||||
g.strokeStyle = "#43331f";
|
||||
g.lineWidth = 2;
|
||||
g.strokeRect(24, 720 - 96 + 14, 68, 68);
|
||||
}
|
||||
g.fillStyle = "#e0b34a";
|
||||
g.font = "600 22px Oswald, sans-serif";
|
||||
g.fillText(step.actor.toUpperCase(), 28, 720 - 60, 400);
|
||||
g.fillText(step.actor.toUpperCase(), textX, 720 - 60, 400);
|
||||
g.fillStyle = "#efe8d4";
|
||||
g.font = "21px 'Courier Prime', monospace";
|
||||
const said = lines.slice(0, 2);
|
||||
if (said.length === 0) said.push("…considers the maze.");
|
||||
said.forEach((line, i) => g.fillText(line, 28, 720 - 32 + i * 26, 1224));
|
||||
said.forEach((line, i) => g.fillText(line, textX, 720 - 32 + i * 26, 1224 - (textX - 28)));
|
||||
// The colophon corner.
|
||||
g.fillStyle = "rgba(224, 179, 74, 0.6)";
|
||||
g.font = "600 20px Oswald, sans-serif";
|
||||
@@ -611,6 +637,8 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div class="replay-caption">
|
||||
{#if actorArt}<img class="caption-face" src={actorArt} alt={step.actor} />{/if}
|
||||
<div class="caption-lines">
|
||||
<strong>{step.actor}</strong>
|
||||
{#each lines as line, i (i)}<div>{line}</div>{/each}
|
||||
{#if lines.length === 0 && !step.chat?.length}<div>…considers the maze.</div>{/if}
|
||||
@@ -618,6 +646,7 @@
|
||||
<div class="reel-talk">💬 {c.player}: {c.text}</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="replay-controls">
|
||||
<button onclick={() => { playing = false; idx = Math.max(0, idx - 1); }} aria-label="previous move">◀</button>
|
||||
<button class="playpause" onclick={() => (playing = !playing)} aria-label={playing ? "pause" : "play"}>
|
||||
@@ -707,7 +736,19 @@
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.45;
|
||||
min-height: 3.4rem;
|
||||
display: flex;
|
||||
gap: 0.6rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.caption-face {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
object-fit: cover;
|
||||
border-radius: 3px;
|
||||
border: 1.5px solid #43331f;
|
||||
flex: none;
|
||||
}
|
||||
.caption-lines { min-width: 0; }
|
||||
.replay-controls .speed {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.7;
|
||||
|
||||
Reference in New Issue
Block a user