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:
1 parent
cf92c9bd27
commit
6be076e3fc
18 files changed
+62
-13
No files matched your search
@@ -21,8 +21,16 @@
|
||||
/** 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 onGlass = $state(false);
|
||||
let fetching = $state(false);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -211,7 +219,7 @@
|
||||
{#if room.error}<p class="warning">{room.error}</p>{/if}
|
||||
|
||||
{#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 onGlass && Glass}
|
||||
<Glass {g} {mySide} {selected} {targets} {reach} {shownSquares} {shownCaptured} {checkSquare} {calm} {tap} />
|
||||
|
||||
@@ -36,6 +36,8 @@
|
||||
/** Air between the sheets, in squares: enough for a Dragon's wings. */
|
||||
const GAP = 4.2;
|
||||
const CELL = 1;
|
||||
/** The sheet's rim beyond the outer squares, in squares. */
|
||||
const RIM = 0.3;
|
||||
|
||||
const levelY = (level: number) => (level - 1) * GAP;
|
||||
/** Where a square's centre sits: Gold's side towards +z, file a at -x. */
|
||||
@@ -49,6 +51,8 @@
|
||||
let frame: HTMLDivElement;
|
||||
let canvas: HTMLCanvasElement;
|
||||
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');
|
||||
|
||||
type Livery = Record<Side, { plain: THREE.Material; held: THREE.Material }>;
|
||||
@@ -58,7 +62,7 @@
|
||||
const figures = new Map<number, THREE.Group>();
|
||||
/** How many of the game's moves the figures have played through. */
|
||||
let played = -1;
|
||||
let ready = false;
|
||||
let ready = $state(false);
|
||||
|
||||
onMount(() => {
|
||||
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
||||
@@ -89,18 +93,22 @@
|
||||
// The sheets, tinted by level, and the posts that hold them.
|
||||
const slabs: THREE.Mesh[] = [];
|
||||
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');
|
||||
c.width = FILES * 32;
|
||||
c.height = RANKS * 32;
|
||||
c.width = (FILES + 2 * RIM) * PX;
|
||||
c.height = (RANKS + 2 * RIM) * PX;
|
||||
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++) {
|
||||
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);
|
||||
map.colorSpace = THREE.SRGBColorSpace;
|
||||
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 })
|
||||
);
|
||||
slab.position.set(0, levelY(level) - 0.06, 0);
|
||||
@@ -346,9 +354,16 @@
|
||||
look(view);
|
||||
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(
|
||||
(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(() => {
|
||||
ready = true;
|
||||
@@ -388,7 +403,12 @@
|
||||
|
||||
<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>
|
||||
{#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">
|
||||
<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> ·
|
||||
@@ -413,9 +433,22 @@
|
||||
|
||||
.status {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
left: 1rem;
|
||||
inset: 0 0 auto;
|
||||
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 {
|
||||
|
||||
@@ -12,8 +12,16 @@
|
||||
/** The glass board, loaded the first time it is asked for. */
|
||||
let Glass = $state<typeof import('$lib/components/Glass.svelte').default | null>(null);
|
||||
let onGlass = $state(false);
|
||||
let fetching = $state(false);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -72,7 +80,7 @@
|
||||
<section class="stage">
|
||||
<div class="boards">
|
||||
{#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 onGlass && Glass}
|
||||
<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.
Reference in new issue
Block a user