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