animations

This commit is contained in:
ryanfitzpatrickio
2026-08-03 16:04:46 -05:00
parent 8b28e299e6
commit fb1ebeed05
28 changed files with 5246 additions and 105 deletions
+172
View File
@@ -0,0 +1,172 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>tilt — animation studio</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><text y='13' font-size='13'>🎞️</text></svg>">
<style>
:root {
color-scheme:dark; --bg:#080b10; --panel:#0e141d; --panel2:#111a25; --line:#223245;
--muted:#718399; --text:#dce7f0; --accent:#68e0c2; --warm:#f6c85f; --danger:#ef6b73;
}
* { box-sizing:border-box; }
[hidden] { display:none !important; }
html,body { margin:0; min-height:100%; background:var(--bg); color:var(--text);
font-family:Inter,ui-sans-serif,system-ui,-apple-system,sans-serif; }
body { height:100vh; overflow:hidden; display:grid; grid-template-rows:54px minmax(0,1fr) 160px; }
button,input,select { font:inherit; }
button { border:1px solid var(--line); background:#152131; color:var(--text); border-radius:6px;
padding:7px 10px; cursor:pointer; }
button:hover:not(:disabled) { border-color:#4c708f; background:#1a2a3d; }
button:disabled { opacity:.45; cursor:default; }
button.primary { border-color:#338b78; background:#12382f; color:#a8f3df; }
button.danger { color:#ffabb0; }
input,select { min-width:0; border:1px solid var(--line); background:#0b1119; color:var(--text);
border-radius:5px; padding:6px 8px; }
input[type=range] { padding:0; accent-color:var(--accent); }
input[type=checkbox] { accent-color:var(--accent); }
.topbar { min-width:0; display:flex; align-items:center; gap:12px; padding:0 14px; border-bottom:1px solid var(--line);
background:#0b1017; }
.brand { letter-spacing:.22em; font-size:12px; font-weight:800; white-space:nowrap; }
.topbar .brand,.topbar button,.topbar .crumb { flex:0 0 auto; }
.brand span { color:var(--accent); }
.crumb { color:#405064; }
#clipName { width:min(240px,25vw); min-width:120px; flex:0 1 240px; }
.spacer { flex:1; }
.save-state { color:var(--muted); font-size:11px; min-width:74px; max-width:170px; overflow:hidden;
white-space:nowrap; text-overflow:ellipsis; text-align:right; }
.workspace { min-height:0; display:grid; grid-template-columns:minmax(270px,.82fr) minmax(390px,1.45fr) 286px; }
.pane { min-width:0; min-height:0; position:relative; border-right:1px solid var(--line); background:var(--panel); }
.pane-title { height:36px; display:flex; align-items:center; justify-content:space-between; padding:0 11px;
border-bottom:1px solid var(--line); color:#91a4b8; font-size:10px; font-weight:750; letter-spacing:.14em; text-transform:uppercase; }
.video-wrap { position:absolute; inset:36px 0 0; display:grid; place-items:center; overflow:hidden; background:#05070a; }
#referenceVideo { max-width:100%; max-height:100%; width:100%; height:100%; object-fit:contain; }
#poseOverlay { position:absolute; inset:0; width:100%; height:100%; object-fit:contain; pointer-events:none; }
#poseOverlay.picking { pointer-events:auto; cursor:crosshair; }
.video-empty { position:absolute; inset:0; display:grid; place-items:center; padding:24px; text-align:center;
color:var(--muted); font-size:12px; background:radial-gradient(circle at 50% 45%,#111b26,#06080c 72%); }
.video-empty[hidden] { display:none; }
.video-empty strong { display:block; color:var(--text); font-size:17px; margin-bottom:7px; }
.video-empty small { display:block; margin-top:10px; color:#54677b; line-height:1.5; }
.stage-pane { background:#0a0f16; }
#stage { position:absolute; inset:36px 0 0; width:100%; height:calc(100% - 36px); display:block; touch-action:none; }
.stage-hint { position:absolute; left:12px; bottom:10px; color:#587086; font-size:10px; pointer-events:none; }
.confidence { font-variant-numeric:tabular-nums; color:var(--accent); }
.inspector { min-height:0; overflow:auto; background:#0b1119; }
.section { padding:12px; border-bottom:1px solid var(--line); }
.section h2 { margin:0 0 10px; color:#8fa6bb; font-size:10px; letter-spacing:.14em; text-transform:uppercase; }
.stack { display:grid; gap:7px; }
.row { display:flex; gap:7px; align-items:center; }
.row > * { flex:1; }
.field { display:grid; grid-template-columns:58px minmax(0,1fr); gap:7px; align-items:center; color:var(--muted); font-size:11px; }
.field.compact { grid-template-columns:38px minmax(0,1fr) 52px; }
.field input[type=number] { width:52px; padding:5px; text-align:right; }
.toggle { display:flex; align-items:center; gap:7px; color:#91a4b8; font-size:11px; }
#status { min-height:32px; color:var(--muted); font-size:10px; line-height:1.5; }
.hint { color:var(--muted); font-size:10px; line-height:1.5; }
progress { width:100%; height:6px; border:0; accent-color:var(--accent); }
.timeline { min-width:0; display:grid; grid-template-rows:42px 1fr; background:#0a0f16; border-top:1px solid var(--line); }
.transport { display:flex; align-items:center; gap:7px; padding:0 12px; border-bottom:1px solid var(--line); }
.transport button { padding:5px 9px; }
.timecode { width:92px; color:var(--warm); font:600 12px ui-monospace,SFMono-Regular,monospace; }
.timeline-main { position:relative; min-height:0; padding:24px 15px 12px; }
#scrubber { position:absolute; left:15px; right:15px; top:10px; width:calc(100% - 30px); margin:0; z-index:3; }
#markers { position:absolute; left:22px; right:22px; top:49px; height:40px; border-top:1px solid #33475d;
background:repeating-linear-gradient(90deg,transparent 0,transparent calc(5% - 1px),#172536 5%); }
.key-marker { position:absolute; top:-7px; width:12px; height:12px; padding:0; transform:translateX(-50%) rotate(45deg);
border:1px solid #bf953a; border-radius:2px; background:var(--warm); }
.key-marker.current { background:var(--accent); border-color:#b9ffed; box-shadow:0 0 10px #68e0c288; }
.timeline-labels { position:absolute; left:22px; right:22px; top:95px; display:flex; justify-content:space-between;
color:#4c6074; font:10px ui-monospace,SFMono-Regular,monospace; }
#fileInput,#clipInput,#freeMocapInput { display:none; }
@media (max-width:980px) {
body { height:auto; min-height:100vh; overflow:auto; display:block; }
.topbar { height:auto; min-height:54px; flex-wrap:wrap; padding:9px 12px; }
.workspace { grid-template-columns:1fr; grid-template-rows:360px 500px auto; }
.pane { border-right:0; border-bottom:1px solid var(--line); }
.inspector { overflow:visible; }
.timeline { height:160px; position:sticky; bottom:0; z-index:10; }
}
</style>
</head>
<body>
<header class="topbar">
<div class="brand"><span>TILT</span> / ANIMATION</div><span class="crumb"></span>
<input id="clipName" value="reference-motion" aria-label="clip name">
<span id="saveState" class="save-state">new clip</span><span class="spacer"></span>
<button id="loadProject">Open saved</button><button id="saveProject">Save</button>
<button id="importClip">Import</button><button id="exportJson">Export JSON</button>
<button id="exportModule" class="primary">Export pose module</button>
</header>
<main class="workspace">
<section class="pane">
<div class="pane-title"><span>Reference video</span><button id="chooseVideo">Choose video</button></div>
<div class="video-wrap">
<video id="referenceVideo" playsinline muted></video><canvas id="poseOverlay"></canvas>
<div id="videoEmpty" class="video-empty"><div><strong>Drop in movement</strong>Upload a side or ¾ view with the full body visible.<small>MP4, WebM, or MOV supported by your browser.<br>The file stays on this device.</small></div></div>
</div>
</section>
<section class="pane stage-pane">
<div class="pane-title"><span>Tilt rig preview</span><span id="confidence" class="confidence">NO TRACK</span></div>
<canvas id="stage"></canvas><div class="stage-hint">drag background to orbit · click a joint to edit · Q/W rotate gizmo</div>
</section>
<aside class="inspector">
<section class="section"><h2>Capture</h2><div class="stack">
<div class="field"><label for="mocapSource">source</label><select id="mocapSource"><option value="browser">Browser MediaPipe</option><option value="freemocap">Local FreeMoCap</option></select></div>
<div id="browserMocapControls" class="stack">
<div class="field"><label for="captureFps">sample fps</label><input id="captureFps" type="number" min="1" max="30" value="12"></div>
<div class="row"><div class="field"><label for="trimIn">in</label><input id="trimIn" type="number" min="0" step="0.01" value="0"></div><div class="field"><label for="trimOut">out</label><input id="trimOut" type="number" min="0" step="0.01" value="0"></div></div>
<button id="extract" class="primary">Generate keys from video</button>
</div>
<div id="freeMocapControls" class="stack" hidden>
<div class="field"><label for="freeMocapFps">fallback fps</label><input id="freeMocapFps" type="number" min="1" max="240" value="30"></div>
<button id="importFreeMocap" class="primary">Process uploaded video with FreeMoCap</button>
<div id="freeMocapState" class="hint">Checking local worker…</div>
<button id="importFreeMocapCsv">Advanced: import existing XYZ CSV</button>
<input id="freeMocapInput" type="file" accept=".csv,text/csv">
</div>
<label class="toggle"><input id="mirrorPose" type="checkbox"> mirror source pose</label>
<progress id="progress" max="1" value="0"></progress>
<div id="status">Choose a video, set a short in/out range, then generate.</div>
</div></section>
<section class="section"><h2>Stick landmarks</h2><div class="stack">
<div class="field"><label for="shotSide">socket</label><select id="shotSide"><option value="right">right shot · right top hand</option><option value="left">left shot · left top hand</option></select></div>
<label class="toggle"><input id="trackStick" type="checkbox"> bake stick with body keys</label>
<div class="row"><button id="seedStick">Mark both ends at IN</button><button id="clearStick">Clear</button></div>
<button id="correctStick">Correct stick on current key</button>
<button id="applyStick" class="primary">Apply guide to rig + key</button>
<button id="flipStick">Flip baked stick direction</button>
<div id="stickStatus" class="hint">Choose the top-hand socket, then mark both shaft ends in either order.</div>
</div></section>
<section class="section"><h2>Keyframe pose</h2><div class="stack">
<div class="field"><label for="boneSelect">bone</label><select id="boneSelect"></select></div>
<div class="field compact"><label>X</label><input class="rot" data-axis="x" type="range" min="-180" max="180" step="1"><input class="rot-num" data-axis="x" type="number" min="-360" max="360" step="1"></div>
<div class="field compact"><label>Y</label><input class="rot" data-axis="y" type="range" min="-180" max="180" step="1"><input class="rot-num" data-axis="y" type="number" min="-360" max="360" step="1"></div>
<div class="field compact"><label>Z</label><input class="rot" data-axis="z" type="range" min="-180" max="180" step="1"><input class="rot-num" data-axis="z" type="number" min="-360" max="360" step="1"></div>
<div class="row"><button id="resetBone">Reset bone</button><button id="setKey" class="primary">Set / update key</button></div>
<div class="row"><button id="deleteKey" class="danger">Delete key</button><button id="smoothKeys">Smooth imported keys</button></div>
</div></section>
<section class="section"><h2>Clip</h2><div class="stack">
<div class="field"><label for="loopClip">playback</label><select id="loopClip"><option value="loop">loop</option><option value="once">play once</option></select></div>
<div class="field"><span>keys</span><strong id="keyCount">0</strong></div>
<div class="field"><span>duration</span><strong id="durationLabel">0.00 s</strong></div>
<button id="newClip">New empty clip</button>
</div></section>
</aside>
</main>
<footer class="timeline">
<div class="transport"><button id="prevKey" title="previous key">◀|</button><button id="playPause" class="primary">▶ Play</button><button id="nextKey" title="next key">|▶</button><span id="timecode" class="timecode">00:00.000</span><span id="timelineSource" class="save-state">clip</span></div>
<div class="timeline-main"><input id="scrubber" type="range" min="0" max="1" step="0.001" value="0"><div id="markers"></div><div class="timeline-labels"><span>0.00</span><span id="timelineEnd">1.00 s</span></div></div>
</footer>
<input id="fileInput" type="file" accept="video/*"><input id="clipInput" type="file" accept="application/json,.json,.tiltanim">
<script type="module" src="/src/studio/animationStudio.js"></script>
</body>
</html>