feat(client): render and drag phantom pieces on the board
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ALL_SQUARES, FILES, RANKS, geometricMoves, type Color, type Piece, type Square } from '@blind-chess/shared';
|
import { ALL_SQUARES, FILES, RANKS, geometricMoves, type Color, type Piece, type Square } from '@blind-chess/shared';
|
||||||
import { pieceGlyph } from './pieces.js';
|
import { pieceGlyph } from './pieces.js';
|
||||||
|
import { phantomDrag } from './stores/phantom-drag.svelte.js';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
pieces: Partial<Record<Square, Piece>>;
|
pieces: Partial<Record<Square, Piece>>;
|
||||||
@@ -8,6 +9,8 @@
|
|||||||
toMove: Color;
|
toMove: Color;
|
||||||
mode: 'blind' | 'vanilla';
|
mode: 'blind' | 'vanilla';
|
||||||
highlightingEnabled: boolean;
|
highlightingEnabled: boolean;
|
||||||
|
phantoms?: Partial<Record<Square, Piece>>;
|
||||||
|
phantomsEnabled?: boolean;
|
||||||
armedSquare: Square | null; // local visual arm (pre-commit)
|
armedSquare: Square | null; // local visual arm (pre-commit)
|
||||||
touchedSquare: Square | null; // server-authoritative touch
|
touchedSquare: Square | null; // server-authoritative touch
|
||||||
onArm: (sq: Square | null) => void;
|
onArm: (sq: Square | null) => void;
|
||||||
@@ -16,6 +19,7 @@
|
|||||||
|
|
||||||
let {
|
let {
|
||||||
pieces, you, toMove, mode, highlightingEnabled,
|
pieces, you, toMove, mode, highlightingEnabled,
|
||||||
|
phantoms = {}, phantomsEnabled = false,
|
||||||
armedSquare, touchedSquare, onArm, onCommit,
|
armedSquare, touchedSquare, onArm, onCommit,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
@@ -43,6 +47,14 @@
|
|||||||
return new Set(moves);
|
return new Set(moves);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The board square a phantom is currently being dragged out of (so it can
|
||||||
|
// be dimmed while the drag ghost is shown). Bind `active` to a local first
|
||||||
|
// so TypeScript narrows the discriminated union reliably.
|
||||||
|
const dragOrigin = $derived.by(() => {
|
||||||
|
const a = phantomDrag.state.active;
|
||||||
|
return a?.kind === 'board' && phantomDrag.state.moved ? a.from : null;
|
||||||
|
});
|
||||||
|
|
||||||
function squareColor(sq: Square): 'light' | 'dark' {
|
function squareColor(sq: Square): 'light' | 'dark' {
|
||||||
const f = sq.charCodeAt(0) - 'a'.charCodeAt(0);
|
const f = sq.charCodeAt(0) - 'a'.charCodeAt(0);
|
||||||
const r = parseInt(sq[1]!, 10) - 1;
|
const r = parseInt(sq[1]!, 10) - 1;
|
||||||
@@ -50,6 +62,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onSquareClick(sq: Square) {
|
function onSquareClick(sq: Square) {
|
||||||
|
if (phantomDrag.shouldSuppressClick(sq)) return;
|
||||||
const piece = pieces[sq];
|
const piece = pieces[sq];
|
||||||
|
|
||||||
// If a piece is touched (server-authoritative), only commits to that square's destinations are valid.
|
// If a piece is touched (server-authoritative), only commits to that square's destinations are valid.
|
||||||
@@ -82,6 +95,7 @@
|
|||||||
{#each filesDisplay as f (f)}
|
{#each filesDisplay as f (f)}
|
||||||
{@const sq = `${f}${r}` as Square}
|
{@const sq = `${f}${r}` as Square}
|
||||||
{@const piece = pieces[sq]}
|
{@const piece = pieces[sq]}
|
||||||
|
{@const ph = phantomsEnabled ? phantoms[sq] : undefined}
|
||||||
{@const sc = squareColor(sq)}
|
{@const sc = squareColor(sq)}
|
||||||
{@const isArmed = sq === armedSquare}
|
{@const isArmed = sq === armedSquare}
|
||||||
{@const isTouched = sq === touchedSquare}
|
{@const isTouched = sq === touchedSquare}
|
||||||
@@ -96,6 +110,7 @@
|
|||||||
class:hl-cap={isHighlightCap}
|
class:hl-cap={isHighlightCap}
|
||||||
onclick={() => onSquareClick(sq)}
|
onclick={() => onSquareClick(sq)}
|
||||||
aria-label={sq}
|
aria-label={sq}
|
||||||
|
data-square={sq}
|
||||||
>
|
>
|
||||||
{#if r === (you === 'w' ? '1' : '8') && f === filesDisplay[0]}
|
{#if r === (you === 'w' ? '1' : '8') && f === filesDisplay[0]}
|
||||||
<span class="coord coord-rank">{r}</span>
|
<span class="coord coord-rank">{r}</span>
|
||||||
@@ -106,6 +121,13 @@
|
|||||||
{#if piece}
|
{#if piece}
|
||||||
<span class="piece piece-{piece.color}">{pieceGlyph(piece)}</span>
|
<span class="piece piece-{piece.color}">{pieceGlyph(piece)}</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if ph && !piece}
|
||||||
|
<span
|
||||||
|
class="phantom phantom-{ph.color}"
|
||||||
|
class:dragging={sq === dragOrigin}
|
||||||
|
onpointerdown={(e) => { e.preventDefault(); phantomDrag.start({ kind: 'board', from: sq, type: ph.type }, e); }}
|
||||||
|
>{pieceGlyph(ph)}</span>
|
||||||
|
{/if}
|
||||||
{#if isHighlight && !piece}
|
{#if isHighlight && !piece}
|
||||||
<span class="dot"></span>
|
<span class="dot"></span>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -154,6 +176,23 @@
|
|||||||
.piece-w { color: #fafafa; }
|
.piece-w { color: #fafafa; }
|
||||||
.piece-b { color: #1a1a1a; }
|
.piece-b { color: #1a1a1a; }
|
||||||
|
|
||||||
|
.phantom {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
pointer-events: auto;
|
||||||
|
touch-action: none;
|
||||||
|
cursor: grab;
|
||||||
|
opacity: 0.4;
|
||||||
|
outline: 2px dashed var(--text-dim);
|
||||||
|
outline-offset: -5px;
|
||||||
|
}
|
||||||
|
.phantom-w { color: #fafafa; }
|
||||||
|
.phantom-b { color: #1a1a1a; }
|
||||||
|
.phantom.dragging { opacity: 0.12; }
|
||||||
|
|
||||||
.armed { box-shadow: inset 0 0 0 4px var(--armed); }
|
.armed { box-shadow: inset 0 0 0 4px var(--armed); }
|
||||||
.touched { box-shadow: inset 0 0 0 4px var(--touched); }
|
.touched { box-shadow: inset 0 0 0 4px var(--touched); }
|
||||||
.hl::before {
|
.hl::before {
|
||||||
|
|||||||
Reference in New Issue
Block a user