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} />
|
||||
|
||||
Reference in new issue
Block a user