feat(client): pointer-event drag controller for the phantom layer
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,82 @@
|
|||||||
|
import type { PieceType, Square } from '@blind-chess/shared';
|
||||||
|
import { phantoms } from './phantoms.svelte.js';
|
||||||
|
import { game } from './game.svelte.js';
|
||||||
|
|
||||||
|
export type DragSource =
|
||||||
|
| { kind: 'palette'; type: PieceType }
|
||||||
|
| { kind: 'board'; from: Square; type: PieceType };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pointer-event drag controller for the phantom layer. A drag past THRESHOLD
|
||||||
|
* px places/moves/removes a phantom; a sub-threshold press is a tap and is
|
||||||
|
* left for the board's normal click handler. Real moves are unaffected.
|
||||||
|
*/
|
||||||
|
function makeDrag() {
|
||||||
|
const state = $state<{
|
||||||
|
active: DragSource | null;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
moved: boolean;
|
||||||
|
}>({ active: null, x: 0, y: 0, moved: false });
|
||||||
|
|
||||||
|
let startX = 0;
|
||||||
|
let startY = 0;
|
||||||
|
// Set only when a board-phantom drag ends back on its origin square — the
|
||||||
|
// browser then fires a spurious `click` on that square's button which must
|
||||||
|
// be swallowed so it doesn't trigger a real move.
|
||||||
|
let suppressClickOn: Square | null = null;
|
||||||
|
const THRESHOLD = 6;
|
||||||
|
|
||||||
|
function onMove(e: PointerEvent) {
|
||||||
|
if (!state.active) return;
|
||||||
|
state.x = e.clientX;
|
||||||
|
state.y = e.clientY;
|
||||||
|
if (!state.moved && Math.hypot(e.clientX - startX, e.clientY - startY) > THRESHOLD) {
|
||||||
|
state.moved = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUp(e: PointerEvent) {
|
||||||
|
window.removeEventListener('pointermove', onMove);
|
||||||
|
window.removeEventListener('pointerup', onUp);
|
||||||
|
const src = state.active;
|
||||||
|
const wasDrag = state.moved;
|
||||||
|
state.active = null;
|
||||||
|
state.moved = false;
|
||||||
|
if (!src || !wasDrag) return; // a tap — the board click handler deals with it
|
||||||
|
|
||||||
|
const el = document.elementFromPoint(e.clientX, e.clientY);
|
||||||
|
const sqEl = el?.closest('[data-square]') as HTMLElement | null;
|
||||||
|
const target = sqEl?.dataset.square as Square | undefined;
|
||||||
|
|
||||||
|
if (src.kind === 'board') {
|
||||||
|
if (target === src.from) { suppressClickOn = src.from; return; }
|
||||||
|
if (!target) { phantoms.remove(src.from); return; } // dropped off the board
|
||||||
|
if (game.state.view?.pieces[target]) return; // your own real piece — reject
|
||||||
|
phantoms.move(src.from, target);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// palette → board
|
||||||
|
if (target && !game.state.view?.pieces[target]) phantoms.place(target, src.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
function start(src: DragSource, e: PointerEvent) {
|
||||||
|
suppressClickOn = null;
|
||||||
|
state.active = src;
|
||||||
|
state.x = startX = e.clientX;
|
||||||
|
state.y = startY = e.clientY;
|
||||||
|
state.moved = false;
|
||||||
|
window.addEventListener('pointermove', onMove);
|
||||||
|
window.addEventListener('pointerup', onUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The board calls this first in its square-click handler. */
|
||||||
|
function shouldSuppressClick(sq: Square): boolean {
|
||||||
|
if (suppressClickOn === sq) { suppressClickOn = null; return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { state, start, shouldSuppressClick };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const phantomDrag = makeDrag();
|
||||||
Reference in New Issue
Block a user