♟ Add BoomRock Chess to your home screen for offline play

♟ BoomRock Chess ♪
Time:
BLACK
10:00
Lawrence Erasmus Peregrine & the Linnean Society

Promote pawn

WHITE
10:00

Status

White to move

Material balance

White 0 pts
Black 0 pts

Piece notes

♙ Pawn — C4 sine♘ Knight — E4 sq. ♗ Bishop — G4 tri.♖ Rook — A3 saw ♕ Queen — C5 sine♔ King — F3 sine

Move log

Chat

Camera & Mic

Requires HTTPS & browser permission.
Works in Chrome, Safari, Firefox.

✋ Pinch to interact · Point at Lawrence on d4
(hit) lawrencePointed = true; } // Controller path: dom-overlay fires pointer events automatically, // so existing pointerenter/leave on the Lawrence square work for free. } // Manage Lawrence tooltip state for hand-tracking if (lawrencePointed && !board[4][3]) { if (!xrLawrenceOn) { xrLawrenceOn = true; const lawrenceSq = document.querySelector('#board .sq:nth-child(36)'); if (lawrenceSq) showLawrenceTip(lawrenceSq); } } else { if (xrLawrenceOn) { xrLawrenceOn = false; hideLawrenceTip(); } } } // ── Project a WebXR world-space position → CSS pixel coords ────────────── function xrProjectToCSS(worldPos, view) { // Column-major 4×4 helpers function mv4(m, v) { return [ m[0]*v[0]+m[4]*v[1]+m[8] *v[2]+m[12]*v[3], m[1]*v[0]+m[5]*v[1]+m[9] *v[2]+m[13]*v[3], m[2]*v[0]+m[6]*v[1]+m[10]*v[2]+m[14]*v[3], m[3]*v[0]+m[7]*v[1]+m[11]*v[2]+m[15]*v[3] ]; } const vm = view.transform.inverse.matrix; const pm = view.projectionMatrix; const vpos = [worldPos.x, worldPos.y, worldPos.z, 1.0]; const cam = mv4(vm, vpos); const clip = mv4(pm, cam); const w = clip[3]; if (Math.abs(w) < 1e-6) return null; const nx = clip[0] / w; // [-1, 1] const ny = clip[1] / w; // [-1, 1] // Convert NDC to CSS pixel space return { x: (nx + 1) * 0.5 * window.innerWidth, y: (1 - (ny + 1) * 0.5) * window.innerHeight }; } // ── Get or create a hand-cursor dot element ──────────────────────────────── function getHandCursor(handedness) { if (!xrHandCursors[handedness]) { const el = document.createElement('div'); el.className = 'xr-hand-cursor'; el.style.borderColor = handedness === 'right' ? '#4aaeff' : '#ff9a3e'; document.body.appendChild(el); xrHandCursors[handedness] = el; } return xrHandCursors[handedness]; } // ── Process one hand input source per frame ──────────────────────────────── function processXRHand(frame, src, view) { const hand = src.hand; const side = src.handedness || 'right'; // Joints we need const indexTipJoint = hand.get('index-finger-tip'); const thumbTipJoint = hand.get('thumb-tip'); if (!indexTipJoint || !thumbTipJoint) return false; const iPose = frame.getJointPose(indexTipJoint, xrRefSpace); const tPose = frame.getJointPose(thumbTipJoint, xrRefSpace); if (!iPose || !tPose) return false; const ip = iPose.transform.position; const tp = tPose.transform.position; // Euclidean distance between fingertips (metres) const pinchDist = Math.sqrt( (ip.x - tp.x) ** 2 + (ip.y - tp.y) ** 2 + (ip.z - tp.z) ** 2 ); const isPinching = pinchDist < 0.025; // 2.5 cm threshold // Project index tip to 2-D screen coords const screen = xrProjectToCSS(ip, view); if (!screen) return false; // Update visual hand-cursor dot const cur = getHandCursor(side); cur.style.left = screen.x + 'px'; cur.style.top = screen.y + 'px'; cur.style.background = isPinching ? 'rgba(120,200,255,.4)' : 'transparent'; cur.style.transform = `translate(-50%,-50%) scale(${isPinching ? 1.6 : 1})`; // Fire synthetic pointer events so every interactive element responds to hands const hitEl = document.elementFromPoint(screen.x, screen.y); if (hitEl) { const wasPin = xrPinching[side]; if (isPinching && !wasPin) { // Pinch start → pointerdown + click hitEl.dispatchEvent(new PointerEvent('pointerenter', { bubbles: true, clientX: screen.x, clientY: screen.y })); hitEl.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, clientX: screen.x, clientY: screen.y, isPrimary: true })); hitEl.dispatchEvent(new PointerEvent('pointerup', { bubbles: true, clientX: screen.x, clientY: screen.y, isPrimary: true })); hitEl.dispatchEvent(new MouseEvent('click', { bubbles: true, clientX: screen.x, clientY: screen.y })); } else if (!isPinching && wasPin) { // Pinch release → pointerleave hitEl.dispatchEvent(new PointerEvent('pointerleave', { bubbles: false })); } else if (!isPinching) { // Hovering — fire pointerenter to let Lawrence tooltip trigger hitEl.dispatchEvent(new PointerEvent('pointerenter', { bubbles: true, clientX: screen.x, clientY: screen.y })); } } xrPinching[side] = isPinching; // Return true if the hand is pointing at the Lawrence square return isOverLawrenceSquare(screen.x, screen.y); } // ── Check whether CSS coords overlap the d4 (Lawrence) square ───────────── function isOverLawrenceSquare(sx, sy) { if (board[4][3]) return false; // occupied — Lawrence not showing // d4 is the 36th child (row 4 × 8 cols + col 3 + 1-based = 36) const sq = document.querySelector('#board .sq:nth-child(36)'); if (!sq) return false; const r = sq.getBoundingClientRect(); return sx >= r.left && sx <= r.right && sy >= r.top && sy <= r.bottom; } // ── LAWRENCE TOOLTIP ──────────────────────────────────────────────────────── function showLawrenceTip(eOrEl) { const el = (eOrEl && eOrEl.currentTarget) ? eOrEl.currentTarget : eOrEl; const tip = document.getElementById('ltip'); const bwR = document.getElementById('bwrap').getBoundingClientRect(); const sqR = el.getBoundingClientRect(); // Prefer showing above the square; flip below if too close to the top of the board const above = sqR.top - bwR.top - 46; if (above >= 0) { tip.style.top = above + 'px'; tip.style.removeProperty('--arrow'); tip.style.setProperty('--flip', '0'); } else { tip.style.top = (sqR.bottom - bwR.top + 8) + 'px'; } tip.style.left = Math.max(0, sqR.left - bwR.left - 8) + 'px'; tip.style.display = 'block'; } function hideLawrenceTip() { document.getElementById('ltip').style.display = 'none'; } // ── NEW GAME ───────────────────────────────────────────────────────────────── function newGame() { clearInterval(clkInt); initBoard(); initClk(); render(); updateScore(); setSt('White to move'); document.getElementById('mlog').innerHTML = ''; document.getElementById('po').style.display = 'none'; setTimeout(() => playSeq(TUNES.start), 200); } newGame();