Files
newbury-exhibit/public/jitter.html
T
2026-06-28 12:29:26 +10:00

150 lines
7.7 KiB
HTML

<!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 Exhibit — pose jitter meter</title>
<style>
:root {
--red:#f65151; --yellow:#fff35d; --blue:#529eff; --cyan:#51eaf1;
--ink:#e8eaf2; --dim:#8a93ad; --panel:rgba(10,12,20,0.86);
--good:#51eaf1; --warn:#fff35d; --bad:#f65151;
}
* { box-sizing:border-box; }
html,body { margin:0; height:100%; background:#05060a; color:var(--ink);
font-family:'Segoe UI',system-ui,-apple-system,sans-serif; overflow:hidden;
-webkit-user-select:none; user-select:none; }
#stage { position:fixed; inset:0; }
#video,#overlay { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); }
#overlay { pointer-events:none; }
.card { background:var(--panel); border:1px solid rgba(255,255,255,0.08);
border-radius:14px; backdrop-filter:blur(10px); }
#hud { position:fixed; left:10px; right:10px; top:calc(8px + env(safe-area-inset-top));
padding:10px 12px; font-size:12.5px; line-height:1.45; }
#hud .top { display:flex; align-items:center; gap:8px; margin-bottom:6px; }
#hud .title { font-weight:700; font-size:13px; letter-spacing:-0.01em; }
.tag { padding:1px 8px; border-radius:999px; font-size:11px; font-weight:600; }
.tag.on { background:rgba(81,234,241,0.16); color:var(--cyan); }
.tag.off { background:rgba(246,81,81,0.16); color:var(--bad); }
.grid { display:grid; grid-template-columns:repeat(4,1fr); gap:6px; margin-top:4px; }
.stat { background:rgba(255,255,255,0.04); border-radius:9px; padding:6px 8px; }
.stat .k { color:var(--dim); font-size:10px; text-transform:uppercase; letter-spacing:0.04em; }
.stat .v { font-size:16px; font-weight:700; font-variant-numeric:tabular-nums; }
.stat .u { color:var(--dim); font-size:10px; font-weight:400; }
.jitter-good { color:var(--good); } .jitter-warn { color:var(--warn); } .jitter-bad { color:var(--bad); }
#markersList { margin-top:6px; font-size:11px; color:var(--dim); min-height:14px; }
#markersList b { color:var(--blue); }
/* bottom controls */
#controls { position:fixed; left:10px; right:10px; bottom:calc(10px + env(safe-area-inset-bottom));
padding:10px 12px; display:flex; flex-direction:column; gap:8px; }
#controls .row { display:flex; gap:8px; align-items:center; }
#note { flex:1; background:rgba(255,255,255,0.06); border:1px solid rgba(255,255,255,0.1);
color:var(--ink); border-radius:9px; padding:8px 10px; font-size:13px; font-family:inherit; }
#note::placeholder { color:var(--dim); }
button { appearance:none; border:0; cursor:pointer; font-family:inherit; font-weight:600;
border-radius:9px; padding:9px 12px; font-size:13px; }
.btn-rec { background:linear-gradient(135deg,var(--red),#ff2678); color:#fff; }
.btn-rec.recording { background:linear-gradient(135deg,#2a2f44,#1a1d2b); color:var(--bad);
box-shadow:0 0 0 1px var(--bad) inset; }
.btn-ghost { background:rgba(255,255,255,0.08); color:var(--ink); }
.btn-go { background:linear-gradient(135deg,var(--blue),var(--cyan)); color:#06121f; }
.counter { font-variant-numeric:tabular-nums; font-weight:700; color:var(--cyan); min-width:54px; text-align:center; }
#recBadge { position:fixed; top:calc(8px + env(safe-area-inset-top)); right:14px;
display:none; align-items:center; gap:6px; z-index:5; font-size:12px; font-weight:700; color:var(--bad); }
#recBadge.show { display:flex; }
#recDot { width:9px; height:9px; border-radius:50%; background:var(--bad); animation:pulse 1s infinite; }
@keyframes pulse { 0%,100%{opacity:1;} 50%{opacity:0.3;} }
#start { position:fixed; inset:0; display:flex; flex-direction:column; align-items:center;
justify-content:center; gap:18px; text-align:center; padding:24px;
background:radial-gradient(circle at 50% 38%,#161a2b,#05060a 70%); }
#start h1 { font-size:24px; margin:0; }
#start h1 span { background:linear-gradient(90deg,var(--red),var(--yellow),var(--blue));
-webkit-background-clip:text; background-clip:text; color:transparent; }
#start p { color:var(--dim); max-width:32ch; margin:0; line-height:1.6; font-size:14px; }
#err { color:var(--bad); font-size:12.5px; max-width:34ch; }
.hidden { display:none !important; }
/* export modal */
#modal { position:fixed; inset:0; background:rgba(2,3,8,0.8); z-index:20;
display:none; align-items:center; justify-content:center; padding:18px; }
#modal.show { display:flex; }
#modal .card { width:100%; max-width:520px; max-height:80vh; padding:16px; display:flex; flex-direction:column; }
#modal h2 { margin:0 0 8px; font-size:16px; }
#modal .summary { font-size:12.5px; color:var(--dim); line-height:1.6; margin-bottom:10px;
white-space:pre-wrap; font-family:ui-monospace,Menlo,Consolas,monospace; background:rgba(255,255,255,0.04);
border-radius:8px; padding:10px; overflow:auto; }
#modal .row { display:flex; gap:8px; }
</style>
</head>
<body>
<div id="stage">
<video id="video" playsinline muted autoplay></video>
<canvas id="overlay"></canvas>
</div>
<div id="recBadge"><span id="recDot"></span>REC <span id="recTime">0.0s</span></div>
<div id="hud" class="card hidden">
<div class="top">
<span class="title">Pose jitter</span>
<span id="lock" class="tag off">no marker</span>
<span style="margin-left:auto;color:var(--dim);font-size:11px">fps <b id="fps" style="color:var(--ink)">0</b></span>
</div>
<div class="grid">
<div class="stat"><div class="k">Pos jitter</div><div class="v"><span id="posJit"></span><span class="u"> mm</span></div></div>
<div class="stat"><div class="k">Ang jitter</div><div class="v"><span id="angJit"></span><span class="u"> °</span></div></div>
<div class="stat"><div class="k">Distance</div><div class="v"><span id="dist"></span><span class="u"> mm</span></div></div>
<div class="stat"><div class="k">View angle</div><div class="v"><span id="ang"></span><span class="u"> °</span></div></div>
</div>
<div id="markersList">point at a crest…</div>
</div>
<div id="controls" class="card hidden">
<div class="row">
<input id="note" type="text" placeholder="label this run (e.g. '1 marker, 30cm, room light)" />
</div>
<div class="row">
<button id="rec" class="btn-rec">● Record</button>
<span class="counter" id="sampleCount">0</span>
<button id="markPoint" class="btn-ghost" title="tag a moment in the log">Flag</button>
<button id="export" class="btn-go" style="margin-left:auto">Export ▼</button>
</div>
</div>
<div id="modal">
<div class="card">
<h2>Session results</h2>
<div class="summary" id="summary"></div>
<div class="row">
<button class="btn-go" id="downloadJson" style="flex:1">Download JSON</button>
<button class="btn-ghost" id="copyText" style="flex:1">Copy summary</button>
</div>
<div class="row" style="margin-top:8px">
<button class="btn-ghost" id="closeModal" style="flex:1">Close</button>
<button class="btn-rec" id="clearAll" style="flex:1">Clear all runs</button>
</div>
</div>
</div>
<div id="start">
<h1>Newbury <span>jitter meter</span></h1>
<p>Hold the phone steady on a crest, hit Record for ~10s, then move closer / further / to a sharper angle and record again. Export and send me the file.</p>
<button id="go" class="btn-go" style="padding:14px 30px;border-radius:999px;font-size:16px">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 src="js/jitter.js"></script>
</body>
</html>