Files
newbury-exhibit/public/orient-debug.html
T
2026-07-08 20:24:50 +10:00

293 lines
15 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, maximum-scale=1" />
<title>Newbury — orientation debug</title>
<style>
:root { --ink:#e8eaf2; --dim:#8a93ad; --cyan:#51eaf1; --green:#4ade80; --amber:#fbbf24; --red:#f65151;
--panel:rgba(8,10,16,0.9); }
* { box-sizing:border-box; }
html,body { margin:0; height:100%; background:#05060a; color:var(--ink);
font-family:'Inter',system-ui,sans-serif; overflow:hidden; }
#video { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); }
#three,#overlay { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); }
#overlay { pointer-events:none; }
#hud { position:fixed; left:10px; top:calc(10px + env(safe-area-inset-top)); padding:11px 13px;
background:var(--panel); border:1px solid rgba(255,255,255,0.1); border-radius:12px;
font-family:ui-monospace,monospace; font-size:11px; line-height:1.65; backdrop-filter:blur(8px); max-width:88vw; }
#hud h3 { margin:0 0 6px; font-family:'Inter',sans-serif; font-size:12px; }
#hud .row { display:flex; gap:8px; }
#hud .lab { color:var(--dim); width:66px; display:inline-block; }
#hud .v { font-weight:600; }
.rgb-x{color:#ff6b6b}.rgb-y{color:#7CFF6b}.rgb-z{color:#6b9cff}
#controls { position:fixed; left:50%; bottom:calc(14px + env(safe-area-inset-bottom));
transform:translateX(-50%); display:flex; gap:8px; flex-wrap:wrap; justify-content:center; width:94vw; }
#controls button { border:0; border-radius:10px; padding:12px 14px; font-size:13px; font-weight:600;
cursor:pointer; background:#1a2033; color:var(--ink); flex:1; min-width:88px; max-width:150px; }
#controls button.rec { background:var(--red); color:#fff; }
#controls button.primary { background:linear-gradient(135deg,#529eff,#51eaf1); color:#06121f; }
#count { position:fixed; left:50%; bottom:calc(66px + env(safe-area-inset-bottom)); transform:translateX(-50%);
font-family:ui-monospace,monospace; font-size:12px; color:var(--dim); background:var(--panel);
padding:5px 12px; border-radius:8px; }
#logout { position:fixed; inset:0; background:#080a12; padding:16px; display:none; flex-direction:column; gap:10px; z-index:20; }
#logout.show { display:flex; }
#logout textarea { flex:1; width:100%; background:#0e1220; color:var(--ink); border:1px solid #26304a;
border-radius:10px; padding:12px; font-family:ui-monospace,monospace; font-size:11px; resize:none; }
#logout .row { display:flex; gap:8px; }
#logout button { flex:1; border:0; border-radius:10px; padding:13px; font-size:14px; font-weight:600; cursor:pointer; }
#copyBtn { background:linear-gradient(135deg,#529eff,#51eaf1); color:#06121f; }
#closeBtn { background:#1a2033; color:var(--ink); }
#copied { text-align:center; color:var(--green); font-size:12px; height:14px; }
#start { position:fixed; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center;
gap:16px; text-align:center; padding:26px; background:radial-gradient(circle at 50% 40%,#141a2b,#05060a 72%); }
#start h1 { font-size:22px; margin:0; } #start p { color:var(--dim); max-width:34ch; line-height:1.6; font-size:13.5px; margin:0; }
#go { border:0; cursor:pointer; font-size:16px; font-weight:600; color:#06121f;
background:linear-gradient(135deg,#529eff,#51eaf1); padding:14px 32px; border-radius:999px; }
#err { color:var(--red); font-size:12.5px; max-width:34ch; }
.hidden { display:none !important; }
</style>
</head>
<body>
<video id="video" playsinline muted autoplay></video>
<canvas id="three"></canvas>
<canvas id="overlay"></canvas>
<div id="hud" class="hidden">
<h3>Orientation debug</h3>
<div class="row"><span class="lab">marker</span><span class="v" id="dId"></span></div>
<div class="row"><span class="lab">RAW posit</span><span class="v" id="dRaw"></span></div>
<div class="row"><span class="lab">RENDER</span><span class="v" id="dRender"></span></div>
<div class="row"><span class="lab">gyro</span><span class="v" id="dGyro"></span></div>
<div class="row"><span class="lab" style="color:var(--amber)">watch</span><span id="dHint" style="color:var(--amber)">tilt phone up = pitch should ↑</span></div>
</div>
<div id="count" class="hidden">not recording</div>
<div id="controls" class="hidden">
<button id="recBtn">● Record</button>
<button id="markBtn">Mark</button>
<button id="viewBtn" class="primary">Copy log</button>
</div>
<div id="logout">
<div id="copied"></div>
<textarea id="logText" readonly></textarea>
<div class="row">
<button id="copyBtn">Copy to clipboard</button>
<button id="closeBtn">Back</button>
</div>
</div>
<div id="start">
<h1>Orientation debug</h1>
<p>Shows raw POS-IT angles vs what we render, plus a 3D gizmo on the marker. Tilt & rotate the phone; the RGB axes should stay glued to the marker. Record and send me the log.</p>
<button id="go">Start camera</button>
<div id="err"></div>
</div>
<script src="vendor/cv.js"></script>
<script src="vendor/svd.js"></script>
<script src="vendor/posit1.js"></script>
<script src="vendor/aruco.js"></script>
<script src="vendor/dictionaries/aruco_4x4_1000.js"></script>
<script type="importmap">
{ "imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
} }
</script>
<script type="module">
import * as THREE from 'three';
import { TunedDetector, MarkerTracker } from './js/detect-tuned.js?v=1';
const $ = (id) => document.getElementById(id);
const video = $('video'), canvas = $('three'), overlay = $('overlay');
const octx = overlay.getContext('2d');
const grab = document.createElement('canvas'), gctx = grab.getContext('2d', { willReadFrequently: true });
const MARKER_SIZE_MM = 96;
let detector, tracker, posit, focalPx = 0, running = false;
let recording = false, log = [], frameN = 0;
let gyro = { alpha: null, beta: null, gamma: null };
// ---- three.js: mirror ar-head EXACTLY, but draw an AXIS GIZMO not a ghost ----
const renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true });
renderer.setClearColor(0x000000, 0);
const scene = new THREE.Scene();
scene.add(new THREE.AmbientLight(0xffffff, 1.2));
let camera = new THREE.PerspectiveCamera(50, 1, 1, 5000);
// anchor object driven by pose (exactly like ar-head)
const anchor = new THREE.Object3D();
anchor.matrixAutoUpdate = false;
scene.add(anchor);
// A big RGB axis gizmo + an "UP" arrow, parented to anchor.
const gizmo = new THREE.AxesHelper(MARKER_SIZE_MM * 1.2);
anchor.add(gizmo);
// a distinct arrow pointing along marker normal (local +Z) so we see "up off table"
const arrowMat = new THREE.MeshBasicMaterial({ color: 0x51eaf1 });
const arrow = new THREE.Mesh(new THREE.ConeGeometry(10, 40, 12), arrowMat);
arrow.position.set(0, 0, MARKER_SIZE_MM); // +Z = marker normal
arrow.rotation.x = Math.PI / 2;
anchor.add(arrow);
anchor.visible = false;
const flip = new THREE.Matrix4().makeScale(1, -1, -1);
const poseMatrix = new THREE.Matrix4();
function centredCorners(m, w, h) { return m.corners.map((c) => ({ x: c.x - w / 2, y: h / 2 - c.y })); }
function applyPose(m) {
const pose = posit.pose(centredCorners(m, grab.width, grab.height));
if (!pose) return null;
const R = pose.bestRotation, t = pose.bestTranslation;
// NEW conversion (matches exhibit.js fix): rotation as-is, flip only
// translation Y and Z. Preserves correct orientation.
poseMatrix.set(
R[0][0], R[0][1], R[0][2], t[0],
R[1][0], R[1][1], R[1][2], -t[1],
R[2][0], R[2][1], R[2][2], -t[2],
0, 0, 0, 1
);
anchor.matrix.copy(poseMatrix);
anchor.matrixWorldNeedsUpdate = true;
// raw posit euler (before flip) and rendered euler (after flip)
const rawM = new THREE.Matrix4().set(
R[0][0], R[0][1], R[0][2], 0, R[1][0], R[1][1], R[1][2], 0, R[2][0], R[2][1], R[2][2], 0, 0,0,0,1);
const rawE = new THREE.Euler().setFromRotationMatrix(rawM, 'YXZ');
const renM = poseMatrix.clone(); renM.setPosition(0,0,0);
const renE = new THREE.Euler().setFromRotationMatrix(renM, 'YXZ');
return {
dist: Math.hypot(t[0], t[1], t[2]),
raw: { yaw: deg(rawE.y), pitch: deg(rawE.x), roll: deg(rawE.z) },
ren: { yaw: deg(renE.y), pitch: deg(renE.x), roll: deg(renE.z) },
};
}
const deg = (r) => Math.round(r * 180 / Math.PI);
function sizeAll() {
const vw = video.videoWidth, vh = video.videoHeight; if (!vw) return;
grab.width = vw; grab.height = vh;
const scale = Math.max(innerWidth / vw, innerHeight / vh);
const dw = vw * scale, dh = vh * scale;
for (const el of [video, canvas, overlay]) { el.style.width = dw + 'px'; el.style.height = dh + 'px'; }
renderer.setPixelRatio(Math.min(devicePixelRatio, 2)); renderer.setSize(dw, dh, false);
overlay.width = dw; overlay.height = dh;
focalPx = vw;
const fovY = 2 * Math.atan((vh / 2) / focalPx) * (180 / Math.PI);
camera.fov = fovY; camera.aspect = dw / dh; camera.updateProjectionMatrix();
posit = new POS.Posit(MARKER_SIZE_MM, focalPx);
}
async function start() {
$('err').textContent = '';
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: { ideal: 'environment' }, width: { ideal: 1280 }, height: { ideal: 720 } }, audio: false });
video.srcObject = stream; await video.play();
} catch (e) { $('err').textContent = 'Camera error: ' + (e.message || e.name); return; }
detector = new TunedDetector({ dictionaryName: 'ARUCO_4X4_1000' }).setPreset('forgiving');
tracker = new MarkerTracker({ coastMs: 150, smooth: true });
// device orientation (gyro) — needs permission on iOS
if (typeof DeviceOrientationEvent !== 'undefined' && DeviceOrientationEvent.requestPermission) {
try { await DeviceOrientationEvent.requestPermission(); } catch (_) {}
}
addEventListener('deviceorientation', (e) => { gyro = { alpha: e.alpha, beta: e.beta, gamma: e.gamma }; });
await new Promise((r) => { if (video.readyState >= 2) r(); else video.onloadeddata = () => r(); });
sizeAll(); addEventListener('resize', sizeAll);
$('start').classList.add('hidden');
for (const id of ['hud','count','controls']) $(id).classList.remove('hidden');
running = true; requestAnimationFrame(loop);
}
const sx = () => overlay.width / grab.width, sy = () => overlay.height / grab.height;
function loop(now) {
if (!running) return; requestAnimationFrame(loop);
if (video.readyState < 2 || !grab.width) return;
gctx.drawImage(video, 0, 0, grab.width, grab.height);
const img = gctx.getImageData(0, 0, grab.width, grab.height);
let raw = []; try { raw = detector.detect(img, { width: grab.width, height: grab.height }); } catch (_) {}
const markers = tracker.update(raw, now);
octx.clearRect(0, 0, overlay.width, overlay.height);
let main = markers[0] || null;
if (main) {
// draw outline + corner numbers
octx.strokeStyle = '#51eaf1'; octx.lineWidth = 2; octx.beginPath();
main.corners.forEach((p, i) => { const x = p.x*sx(), y = p.y*sy(); i ? octx.lineTo(x,y) : octx.moveTo(x,y); });
octx.closePath(); octx.stroke();
main.corners.forEach((p, i) => {
const x = p.x*sx(), y = p.y*sy();
octx.fillStyle = ['#fff','#ffd24a','#ff8a3a','#ff5a5a'][i];
octx.beginPath(); octx.arc(x, y, 6, 0, 7); octx.fill();
octx.fillStyle = '#000'; octx.font = 'bold 10px monospace'; octx.fillText(i, x-3, y+3);
});
const res = applyPose(main);
if (res) {
anchor.visible = true;
$('dId').textContent = 'id ' + main.id + (main.coasting ? ' coast' : '');
$('dRaw').innerHTML = fmt(res.raw);
$('dRender').innerHTML = fmt(res.ren);
$('dGyro').textContent = gyro.beta != null
? ${Math.round(gyro.beta)} γ${Math.round(gyro.gamma)} α${Math.round(gyro.alpha)}` : 'n/a';
if (recording) {
frameN++;
if (frameN % 3 === 0) {
log.push({ t: Math.round(now), id: main.id, dist: Math.round(res.dist),
raw: res.raw, ren: res.ren, gyro: { b: r1(gyro.beta), g: r1(gyro.gamma), a: r1(gyro.alpha) } });
$('count').textContent = `● REC · ${log.length}`;
}
}
}
} else {
anchor.visible = false; $('dId').textContent = 'none';
}
renderer.render(scene, camera);
}
function r1(v){ return v==null?null:Math.round(v); }
function fmt(a){ return `<span class="rgb-y">P${a.pitch}</span> <span class="rgb-z">R${a.roll}</span> <span class="rgb-x">Y${a.yaw}</span>`; }
function buildLog() {
const L = [];
L.push('=== NEWBURY ORIENTATION LOG ===');
L.push('conversion: NEW (rotation as-is, flip translation Y/Z) marker:' + MARKER_SIZE_MM + 'mm cam:' + grab.width + 'x' + grab.height);
L.push('cols: t,id,dist, RAW(pitch,roll,yaw), RENDER(pitch,roll,yaw), GYRO(beta,gamma,alpha)');
L.push('gyro beta=front/back tilt, gamma=left/right tilt, alpha=compass');
L.push('samples:' + log.length);
L.push('');
for (const r of log) {
L.push([r.t, r.id, r.dist,
'RAW', r.raw.pitch, r.raw.roll, r.raw.yaw,
'REN', r.ren.pitch, r.ren.roll, r.ren.yaw,
'GYRO', r.gyro.b, r.gyro.g, r.gyro.a].join(','));
}
return L.join('\n');
}
$('recBtn').addEventListener('click', () => {
recording = !recording;
$('recBtn').classList.toggle('rec', recording);
$('recBtn').textContent = recording ? '■ Stop' : '● Record';
if (recording) { log = []; $('count').textContent = '● REC · 0'; }
});
$('markBtn').addEventListener('click', () => { log.push({ t: Math.round(performance.now()), mark: true, raw:{}, ren:{}, gyro:{} }); });
$('viewBtn').addEventListener('click', () => { $('logText').value = buildLog(); $('logout').classList.add('show'); });
$('copyBtn').addEventListener('click', async () => {
try { await navigator.clipboard.writeText($('logText').value); $('copied').textContent = 'Copied!'; }
catch (_) { $('logText').select(); $('copied').textContent = 'Long-press → Copy'; }
});
$('closeBtn').addEventListener('click', () => $('logout').classList.remove('show'));
$('go').addEventListener('click', start);
</script>
</body>
</html>