arcadia / royal-chess

Royal Chess

perspective.js1.9 KB
1// Shared "first-person" board view: tilts the canvas toward the player (CSS perspective) and maps
2// pointer input back through the inverse projection, so game code keeps using flat canvas coordinates.
3(() => {
4 const c = document.getElementById("c");
5 if (!c) return;
6 const TILT = 30; // degrees
7 function apply() {
8 const h = c.offsetHeight || 800;
9 c.style.transformOrigin = "50% 100%";
10 c.style.transform = "perspective(" + Math.round(h * 1.3) + "px) rotateX(" + TILT + "deg)";
11 c.style.willChange = "transform";
12 }
13 apply();
14 if (window.ResizeObserver) new ResizeObserver(apply).observe(c);
15 addEventListener("resize", apply);
16 function toLocal(sx, sy) {
17 const w = c.offsetWidth, h = c.offsetHeight;
18 const m = new DOMMatrix(getComputedStyle(c).transform);
19 const dx = sx - (c.offsetLeft + w / 2), dy = sy - (c.offsetTop + h);
20 const a = m.m11 - dx * m.m14, b = m.m21 - dx * m.m24, e = dx * m.m44 - m.m41;
21 const d = m.m12 - dy * m.m14, f = m.m22 - dy * m.m24, g = dy * m.m44 - m.m42;
22 const det = a * f - b * d;
23 if (!det) return null;
24 return { x: (e * f - b * g) / det + w / 2, y: (a * g - e * d) / det + h, w, h };
25 }
26 let busy = false;
27 ["pointerdown", "pointermove", "pointerup"].forEach((type) => {
28 addEventListener(type, (e) => {
29 if (busy || e.target !== c || !e.isTrusted) return;
30 const p = toLocal(e.clientX, e.clientY);
31 if (!p) return;
32 e.stopImmediatePropagation();
33 const r = c.getBoundingClientRect();
34 const ev = new PointerEvent(type, {
35 bubbles: true, cancelable: true, pointerId: e.pointerId, pointerType: e.pointerType, isPrimary: e.isPrimary,
36 button: e.button, buttons: e.buttons, clientX: r.left + (p.x / p.w) * r.width, clientY: r.top + (p.y / p.h) * r.height,
37 });
38 busy = true;
39 try { c.dispatchEvent(ev); } finally { busy = false; }
40 }, true);
41 });
42})();
43