The glass board arrives in seconds: the figures' textures are 256 px (3.8 MB for all fifteen, down from 30), each kind takes its places as its file lands behind a progress bar, the toggle says it is fetching, and the squares sit exactly under the figures now that the checker covers the sheet's rim

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CCLa2PaEeCj8njQZSWGqHx
This commit is contained in:
Eric WagonerandClaude Fable 5.1 committed 2026-10-27 09:39:59 -04:00
1 parent cf92c9bd27
commit 6be076e3fc
18 files changed
+62 -13

No files matched your search

+10 -2
View File
@@ -21,8 +21,16 @@
/** The glass board, loaded the first time it is asked for, so three.js stays out of every other visit. */ /** The glass board, loaded the first time it is asked for, so three.js stays out of every other visit. */
let Glass = $state<typeof import('./Glass.svelte').default | null>(null); let Glass = $state<typeof import('./Glass.svelte').default | null>(null);
let onGlass = $state(false); let onGlass = $state(false);
let fetching = $state(false);
async function seeGlass(open: boolean) { async function seeGlass(open: boolean) {
if (open && !Glass) Glass = (await import('./Glass.svelte')).default; if (open && !Glass) {
fetching = true;
try {
Glass = (await import('./Glass.svelte')).default;
} finally {
fetching = false;
}
}
onGlass = open; onGlass = open;
} }
@@ -211,7 +219,7 @@
{#if room.error}<p class="warning">{room.error}</p>{/if} {#if room.error}<p class="warning">{room.error}</p>{/if}
{#if glass.open} {#if glass.open}
<p class="muted small glass-way"><button type="button" class="link plain" onclick={() => seeGlass(!onGlass)}>{onGlass ? 'Back to the flat boards' : 'See the glass board'}</button></p> <p class="muted small glass-way"><button type="button" class="link plain" disabled={fetching} aria-busy={fetching} onclick={() => seeGlass(!onGlass)}>{fetching ? 'Fetching the glass board…' : onGlass ? 'Back to the flat boards' : 'See the glass board'}</button></p>
{/if} {/if}
{#if onGlass && Glass} {#if onGlass && Glass}
<Glass {g} {mySide} {selected} {targets} {reach} {shownSquares} {shownCaptured} {checkSquare} {calm} {tap} /> <Glass {g} {mySide} {selected} {targets} {reach} {shownSquares} {shownCaptured} {checkSquare} {calm} {tap} />
+42 -9
View File
@@ -36,6 +36,8 @@
/** Air between the sheets, in squares: enough for a Dragon's wings. */ /** Air between the sheets, in squares: enough for a Dragon's wings. */
const GAP = 4.2; const GAP = 4.2;
const CELL = 1; const CELL = 1;
/** The sheet's rim beyond the outer squares, in squares. */
const RIM = 0.3;
const levelY = (level: number) => (level - 1) * GAP; const levelY = (level: number) => (level - 1) * GAP;
/** Where a square's centre sits: Gold's side towards +z, file a at -x. */ /** Where a square's centre sits: Gold's side towards +z, file a at -x. */
@@ -49,6 +51,8 @@
let frame: HTMLDivElement; let frame: HTMLDivElement;
let canvas: HTMLCanvasElement; let canvas: HTMLCanvasElement;
let status = $state('Setting out the figures…'); let status = $state('Setting out the figures…');
/** How many of the fifteen kinds of figure have arrived. */
let arrived = $state(0);
let view = $state<'quarter' | 'seat' | 'above'>('quarter'); let view = $state<'quarter' | 'seat' | 'above'>('quarter');
type Livery = Record<Side, { plain: THREE.Material; held: THREE.Material }>; type Livery = Record<Side, { plain: THREE.Material; held: THREE.Material }>;
@@ -58,7 +62,7 @@
const figures = new Map<number, THREE.Group>(); const figures = new Map<number, THREE.Group>();
/** How many of the game's moves the figures have played through. */ /** How many of the game's moves the figures have played through. */
let played = -1; let played = -1;
let ready = false; let ready = $state(false);
onMount(() => { onMount(() => {
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
@@ -89,18 +93,22 @@
// The sheets, tinted by level, and the posts that hold them. // The sheets, tinted by level, and the posts that hold them.
const slabs: THREE.Mesh[] = []; const slabs: THREE.Mesh[] = [];
for (const level of [1, 2, 3]) { for (const level of [1, 2, 3]) {
// The texture covers the whole sheet, rim included, so that the squares sit exactly under the figures.
const PX = 40;
const c = document.createElement('canvas'); const c = document.createElement('canvas');
c.width = FILES * 32; c.width = (FILES + 2 * RIM) * PX;
c.height = RANKS * 32; c.height = (RANKS + 2 * RIM) * PX;
const ctx = c.getContext('2d')!; const ctx = c.getContext('2d')!;
ctx.fillStyle = SQUARES[level][1];
ctx.fillRect(0, 0, c.width, c.height);
for (let r = 0; r < RANKS; r++) for (let f = 0; f < FILES; f++) { for (let r = 0; r < RANKS; r++) for (let f = 0; f < FILES; f++) {
ctx.fillStyle = SQUARES[level][isLight(idx(level, f, r)) ? 0 : 1]; ctx.fillStyle = SQUARES[level][isLight(idx(level, f, r)) ? 0 : 1];
ctx.fillRect(f * 32, (RANKS - 1 - r) * 32, 32, 32); ctx.fillRect((RIM + f) * PX, (RIM + RANKS - 1 - r) * PX, PX, PX);
} }
const map = new THREE.CanvasTexture(c); const map = new THREE.CanvasTexture(c);
map.colorSpace = THREE.SRGBColorSpace; map.colorSpace = THREE.SRGBColorSpace;
const slab = new THREE.Mesh( const slab = new THREE.Mesh(
new THREE.BoxGeometry(FILES * CELL + 0.6, 0.12, RANKS * CELL + 0.6), new THREE.BoxGeometry((FILES + 2 * RIM) * CELL, 0.12, (RANKS + 2 * RIM) * CELL),
new THREE.MeshPhysicalMaterial({ map, transparent: true, opacity: 0.74, roughness: 0.15, metalness: 0, clearcoat: 1 }) new THREE.MeshPhysicalMaterial({ map, transparent: true, opacity: 0.74, roughness: 0.15, metalness: 0, clearcoat: 1 })
); );
slab.position.set(0, levelY(level) - 0.06, 0); slab.position.set(0, levelY(level) - 0.06, 0);
@@ -346,9 +354,16 @@
look(view); look(view);
controls.addEventListener('change', wake); controls.addEventListener('change', wake);
// Each kind's figures take their places as its file arrives, so the board fills in while the rest are fetched.
Promise.all( Promise.all(
(Object.keys(FIGURE) as Kind[]).map( (Object.keys(FIGURE) as Kind[]).map(
(kind) => new Promise<void>((ok, no) => loader.load(`/figures/${FIGURE[kind]}.glb`, (gltf) => { protos.set(kind, { scene: gltf.scene, livery: liveries(gltf.scene) }); ok(); }, undefined, no)) (kind) => new Promise<void>((ok, no) => loader.load(`/figures/${FIGURE[kind]}.glb`, (gltf) => {
protos.set(kind, { scene: gltf.scene, livery: liveries(gltf.scene) });
arrived++;
for (let i = 0; i < g.cells.length; i++) if (g.cells[i] !== '' && kindOf(g.cells[i]) === kind && !figures.has(i)) figure(i);
wake();
ok();
}, undefined, no))
) )
).then(() => { ).then(() => {
ready = true; ready = true;
@@ -388,7 +403,12 @@
<div class="glass" bind:this={frame}> <div class="glass" bind:this={frame}>
<canvas bind:this={canvas} aria-label="The three boards as sheets of glass with the pieces standing on them. The flat boards beside this view are the place to play by keyboard."></canvas> <canvas bind:this={canvas} aria-label="The three boards as sheets of glass with the pieces standing on them. The flat boards beside this view are the place to play by keyboard."></canvas>
{#if status}<p class="status muted">{status}</p>{/if} {#if status}
<div class="status" role="status">
<p>{status}</p>
{#if !ready}<progress max={Object.keys(FIGURE).length} value={arrived}>{arrived} of {Object.keys(FIGURE).length}</progress>{/if}
</div>
{/if}
<p class="ways muted small"> <p class="ways muted small">
<button type="button" class="link plain" class:current={view === 'quarter'} onclick={() => (view = 'quarter')}>From the corner</button> · <button type="button" class="link plain" class:current={view === 'quarter'} onclick={() => (view = 'quarter')}>From the corner</button> ·
<button type="button" class="link plain" class:current={view === 'seat'} onclick={() => (view = 'seat')}>From your seat</button> · <button type="button" class="link plain" class:current={view === 'seat'} onclick={() => (view = 'seat')}>From your seat</button> ·
@@ -413,9 +433,22 @@
.status { .status {
position: absolute; position: absolute;
top: 1rem; inset: 0 0 auto;
left: 1rem;
margin: 0; margin: 0;
padding: 2rem 1rem 0;
text-align: center;
}
.status p {
margin: 0 0 0.6rem;
font-size: 1.15rem;
color: var(--bone);
}
progress {
width: min(60%, 320px);
height: 6px;
accent-color: var(--ember);
} }
.ways { .ways {
+10 -2
View File
@@ -12,8 +12,16 @@
/** The glass board, loaded the first time it is asked for. */ /** The glass board, loaded the first time it is asked for. */
let Glass = $state<typeof import('$lib/components/Glass.svelte').default | null>(null); let Glass = $state<typeof import('$lib/components/Glass.svelte').default | null>(null);
let onGlass = $state(false); let onGlass = $state(false);
let fetching = $state(false);
async function seeGlass(open: boolean) { async function seeGlass(open: boolean) {
if (open && !Glass) Glass = (await import('$lib/components/Glass.svelte')).default; if (open && !Glass) {
fetching = true;
try {
Glass = (await import('$lib/components/Glass.svelte')).default;
} finally {
fetching = false;
}
}
onGlass = open; onGlass = open;
} }
@@ -72,7 +80,7 @@
<section class="stage"> <section class="stage">
<div class="boards"> <div class="boards">
{#if glass.open} {#if glass.open}
<p class="muted small"><button type="button" class="link plain" onclick={() => seeGlass(!onGlass)}>{onGlass ? 'Back to the flat boards' : 'See the glass board'}</button></p> <p class="muted small"><button type="button" class="link plain" disabled={fetching} aria-busy={fetching} onclick={() => seeGlass(!onGlass)}>{fetching ? 'Fetching the glass board…' : onGlass ? 'Back to the flat boards' : 'See the glass board'}</button></p>
{/if} {/if}
{#if onGlass && Glass} {#if onGlass && Glass}
<Glass {g} {shownSquares} {shownCaptured} {checkSquare} selected={inspected} {reach} {calm} {tap} /> <Glass {g} {shownSquares} {shownCaptured} {checkSquare} selected={inspected} {reach} {calm} {tap} />
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.