versioncam 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +196 -46
  2. package/README.md +486 -243
  3. package/bin/versioncam.js +1 -1
  4. package/dist/app-server.js +2 -2
  5. package/dist/app-server.js.map +1 -1
  6. package/dist/cli/commands/doctor.js +41 -4
  7. package/dist/cli/commands/doctor.js.map +1 -1
  8. package/dist/cli/commands/init.js +26 -1
  9. package/dist/cli/commands/init.js.map +1 -1
  10. package/dist/cli/commands/preview.js +10 -1
  11. package/dist/cli/commands/preview.js.map +1 -1
  12. package/dist/cli/commands/record.js +3 -2
  13. package/dist/cli/commands/record.js.map +1 -1
  14. package/dist/cli/commands/render.d.ts +17 -0
  15. package/dist/cli/commands/render.js +79 -22
  16. package/dist/cli/commands/render.js.map +1 -1
  17. package/dist/cli/commands/stability.js +4 -4
  18. package/dist/cli/commands/stability.js.map +1 -1
  19. package/dist/cli/cwd.d.ts +46 -0
  20. package/dist/cli/cwd.js +102 -0
  21. package/dist/cli/cwd.js.map +1 -0
  22. package/dist/cli/main.js +11 -2
  23. package/dist/cli/main.js.map +1 -1
  24. package/dist/cli/usage.d.ts +10 -5
  25. package/dist/cli/usage.js +42 -24
  26. package/dist/cli/usage.js.map +1 -1
  27. package/dist/config.d.ts +24 -0
  28. package/dist/config.js +25 -0
  29. package/dist/config.js.map +1 -1
  30. package/dist/core/camera-track.d.ts +63 -0
  31. package/dist/core/camera-track.js +230 -0
  32. package/dist/core/camera-track.js.map +1 -0
  33. package/dist/core/camera.d.ts +14 -1
  34. package/dist/core/camera.js +18 -2
  35. package/dist/core/camera.js.map +1 -1
  36. package/dist/core/follow.d.ts +129 -0
  37. package/dist/core/follow.js +151 -0
  38. package/dist/core/follow.js.map +1 -0
  39. package/dist/core/motion-defaults.d.ts +29 -0
  40. package/dist/core/motion-defaults.js +29 -0
  41. package/dist/core/motion-defaults.js.map +1 -1
  42. package/dist/core/timeline.d.ts +32 -2
  43. package/dist/core/timeline.js +10 -0
  44. package/dist/core/timeline.js.map +1 -1
  45. package/dist/driver/clip.d.ts +4 -1
  46. package/dist/driver/clip.js +9 -1
  47. package/dist/driver/clip.js.map +1 -1
  48. package/dist/driver/session.d.ts +35 -1
  49. package/dist/driver/session.js +81 -12
  50. package/dist/driver/session.js.map +1 -1
  51. package/dist/driver/settle.js +2 -2
  52. package/dist/driver/settle.js.map +1 -1
  53. package/dist/index.d.ts +2 -2
  54. package/dist/index.js +1 -1
  55. package/dist/index.js.map +1 -1
  56. package/dist/inspect/inspect.js +1 -1
  57. package/dist/loader.d.ts +7 -1
  58. package/dist/loader.js +23 -8
  59. package/dist/loader.js.map +1 -1
  60. package/dist/page/assets/index-fZtacWyp.js +2 -0
  61. package/dist/page/index.html +2 -2
  62. package/dist/render/encode.d.ts +12 -1
  63. package/dist/render/encode.js +94 -35
  64. package/dist/render/encode.js.map +1 -1
  65. package/dist/render/presentation.d.ts +12 -0
  66. package/dist/render/presentation.js +15 -0
  67. package/dist/render/presentation.js.map +1 -1
  68. package/dist/render/render.js +19 -2
  69. package/dist/render/render.js.map +1 -1
  70. package/dist/render/sampling.d.ts +13 -0
  71. package/dist/render/sampling.js +48 -20
  72. package/dist/render/sampling.js.map +1 -1
  73. package/dist/render/scenes.d.ts +16 -0
  74. package/dist/render/scenes.js +108 -0
  75. package/dist/render/scenes.js.map +1 -0
  76. package/dist/render/sequence.d.ts +18 -0
  77. package/dist/render/sequence.js +67 -13
  78. package/dist/render/sequence.js.map +1 -1
  79. package/dist/review/review.js +48 -23
  80. package/dist/review/review.js.map +1 -1
  81. package/dsl.md +267 -92
  82. package/package.json +2 -2
  83. package/plugin/README.md +58 -47
  84. package/plugin/agents/versioncam-reviewer.md +3 -2
  85. package/plugin/skills/versioncam/SKILL.md +60 -14
  86. package/plugin/skills/versioncam/authoring.md +6 -1
  87. package/plugin/skills/versioncam/onboarding.md +17 -17
  88. package/dist/page/assets/index-DUom5amc.js +0 -1
@@ -32,19 +32,63 @@ Follow these steps in order. Two more files sit beside this one, in this
32
32
  skill's directory: `onboarding.md` and `authoring.md`. They are briefs, read
33
33
  when a step says to.
34
34
 
35
- ## 0. Which phase
36
-
37
- First, check versioncam is installed where Node will find it:
38
- `ls -d node_modules/versioncam`, then the same in each parent directory up to
39
- the repository root (`../node_modules/versioncam`, …) — a workspace hoists it.
40
- If none has it, **stop** and say it must be installed first
41
- (`npm i -D versioncam`, then `npx versioncam install`). Every step below runs
42
- `npx versioncam`, and `npx` with nothing installed downloads the latest
43
- published version — not necessarily the one this repository will pin, and
35
+ ## 0. Where, and which phase
36
+
37
+ Everything below runs in **the app's directory**: the one that holds
38
+ `versioncam.config.ts` (or `.js`, `.mjs`). That need not be the working
39
+ directory. In a monorepo, a session started at the repository root can run
40
+ this skill from an app's `.claude/skills/` further down, and one started
41
+ inside an app can run a skill installed above it. So before deciding
42
+ anything, find the config, in the whole repository rather than only here.
43
+
44
+ **Find it.** Glob `**/versioncam.config.{ts,js,mjs}` from the top of the
45
+ repository (the directory holding `.git`), and leave out anything under
46
+ `node_modules/`, `.versioncam/` or `.claude/worktrees/`, which is another
47
+ checkout. Files on disk are what count, committed or not: a first run's
48
+ config may not be committed yet, and a deleted one is gone.
49
+
50
+ - **One** → its directory is the app's directory.
51
+ - **Several** → ask which app with `AskUserQuestion`, listing their
52
+ directories, unless the request already names one. With no one to ask,
53
+ stop and list them.
54
+ - **None** → this is the first run, and the config goes where
55
+ `versioncam init` put this skill. This skill's directory is
56
+ `${CLAUDE_SKILL_DIR}` (if that is not a path, it is the directory you read
57
+ this file from). If it is `<dir>/.claude/skills/versioncam` inside this
58
+ repository, the app's directory is `<dir>`. Otherwise it is the working
59
+ directory.
60
+
61
+ **Check versioncam is installed** where the app will find it: `ls -d
62
+ <the app's directory>/node_modules/versioncam`, then the same in each
63
+ directory above it up to the repository root, since a workspace hoists it.
64
+ If none has it, **stop** and say it must be installed in the app's directory
65
+ first (`npm i -D versioncam`, then `npx versioncam install`). Every step below
66
+ runs `npx versioncam`, and `npx` with nothing installed downloads the latest
67
+ published version: not necessarily the one this repository will pin, and
44
68
  with no Chromium to record with.
45
69
 
46
- Then: is there a `versioncam.config.ts` (or `.js`, `.mjs`) in the working
47
- directory?
70
+ **Go there**, if the app's directory is not the working directory, and say in
71
+ one line which directory it is:
72
+
73
+ - **Below where this session started** (a session at the repository root):
74
+ `cd` into it by its absolute path. The shell stays there for every later
75
+ command, and a subagent you spawn starts there too. Do not reach it with
76
+ `--cwd` from above: `npx` looks for versioncam in the directory it runs in
77
+ and in those above it, never below, so it would not run the app's own.
78
+ - **Anywhere else** (a session started inside the app, below its config): a
79
+ session cannot `cd` out of the directory it started in. Put `--cwd <the
80
+ app's directory>` on the end of every `npx versioncam` command instead.
81
+ `npx` then finds the app's versioncam by looking up from where you are,
82
+ and each command works as if run in the app's directory, so a relative path
83
+ you give it, or it prints, is that directory's. Your own `mkdir`, `cp` and
84
+ files take absolute paths.
85
+
86
+ If a `cd` does not stay (a later `pwd` prints where you started), begin each
87
+ command with `cd <the app's directory> && ` instead. Either way, tell each
88
+ subagent you spawn the app's directory and how you are running commands
89
+ there.
90
+
91
+ Then, did you find a config?
48
92
 
49
93
  - **No** → this is the repository's first run. Read `onboarding.md` and do all
50
94
  of it, then continue with step 1.
@@ -91,7 +135,8 @@ directories; do not summarise it away. Copy `beats.json` beside it.
91
135
  host makes subagents cheap, hand the authoring to a subagent instead of doing
92
136
  it here. Spawn a plain one (general-purpose) whose prompt is the full text of
93
137
  `authoring.md`, followed by the intent, the clip id, the clips directory (the
94
- config's `clips`), the `inspect /` output inline, and the beats path. **Keep the
138
+ config's `clips`), the `inspect /` output inline, the beats path, and the
139
+ app's directory, with the `--cwd` flag if step 0 had you use it. **Keep the
95
140
  agent id it returns**: that id is how you continue the same author in later
96
141
  rounds, with `SendMessage`, everything it learned about the page still in its
97
142
  context — spawning a new one would start from nothing and repeat the same
@@ -114,8 +159,9 @@ The reviewer is given only:
114
159
  - the sheet path,
115
160
  - **the frame numbers `sheet --at-marks` just printed**, in order, one per tile,
116
161
  - the contents of `.versioncam/author/<id>/beats.json`,
117
- - the clip id (so it can call `npx versioncam frame <id> <n>` — from this
118
- directory, which you name if a subagent on your host starts somewhere else).
162
+ - the clip id, so it can call `npx versioncam frame <id> <n>`: from this
163
+ directory, which you name if a subagent on your host starts somewhere
164
+ else, and with the `--cwd` flag on the end if step 0 had you use one.
119
165
 
120
166
  The frame numbers are not optional. A reviewer given only the id assumed tile
121
167
  *n* was frame *n*, pulled frames 1 to 16 — every one of them before the beat
@@ -9,7 +9,9 @@ each thing it claims to show.
9
9
  You have: the intent, the clip id, the clips directory, and the output of
10
10
  `npx versioncam inspect /` for the start page — whether you are the session
11
11
  running the skill or a subagent it handed this brief to. Work from the
12
- directory that holds `versioncam.config.ts`.
12
+ directory that holds `versioncam.config.ts`. If you were given a `--cwd <dir>`
13
+ to use instead, put it on the end of every `npx versioncam` command, and write
14
+ files by their absolute paths.
13
15
 
14
16
  ## Look before you write
15
17
 
@@ -196,6 +198,8 @@ happened again the first time an agent used it.
196
198
  | drag or lasso selected nothing | `measure`, re-author geometry as fractions of the real box |
197
199
  | duration drift | adjust holds, never the beats |
198
200
  | tooltip / menu open in a frame it should not be | move the cursor start; add a hold before the next action |
201
+ | the caption covers something a beat points at | `theme.presentation.captionInset: { right: 40 }` in the config moves it to the bottom-right corner; it moves every clip's caption, so say so in what you write down |
202
+ | the camera zooms in and out, or stops and starts between targets | one `s.camera.follow()` where the clip starts moving around the page, instead of a `zoom` before each thing the cursor uses; `s.camera.reset()` or `release()` where it should stop |
199
203
 
200
204
  Two more, from the tools rather than the app:
201
205
 
@@ -203,6 +207,7 @@ Two more, from the tools rather than the app:
203
207
  |---|---|
204
208
  | a locator "matched N elements" | the text appears in more than one place — use `byRole` with a name, or a test id |
205
209
  | review warns that a track never rendered | you wrote a camera, highlight or caption with no frames after it; add a hold |
210
+ | review warns that a click is outside what the camera shows | the camera was framing something else when the button went down: zoom to what the click is on before it, or follow the cursor |
206
211
 
207
212
  ## What you write down
208
213
 
@@ -1,11 +1,11 @@
1
1
  # The first run: setting a repository up
2
2
 
3
- There is no `versioncam.config.ts` here, so before any clip can be written the
4
- repository has to be taught three things: how its app starts, whether it signs
5
- in, and what "the page is done" means. Then one question has to be answered
6
- before anything is authored: **does this app record the same way twice?** A
7
- clip is only as deterministic as the app it films, and one written against an
8
- app that is not fails CI for reasons nobody wrote.
3
+ There is no `versioncam.config.ts` in this repository, so before any clip can
4
+ be written the repository has to be taught three things: how its app starts,
5
+ whether it signs in, and what "the page is done" means. Then one question has
6
+ to be answered before anything is authored: **does this app record the same
7
+ way twice?** A clip is only as deterministic as the app it films, and one
8
+ written against an app that is not fails CI for reasons nobody wrote.
9
9
 
10
10
  You do not run the app yourself — not with `npm run dev`, not in the
11
11
  background, not ever in this phase. You write what you learn into the config,
@@ -13,11 +13,10 @@ and `versioncam` starts the app from it, every time, and stops it again. If
13
13
  you find yourself wanting to start a server by hand, the config is wrong;
14
14
  fix the config.
15
15
 
16
- Work in the directory the skill was run from; the config goes here. If there
17
- is no config here but `Glob` finds a `versioncam.config.*` further down, stop
18
- and say to run the skill from that directory instead: every `versioncam`
19
- command reads the config from the directory it runs in, and a second config
20
- would split the repository in two.
16
+ Work in the app's directory, the one step 0 of the skill chose: the config
17
+ goes there, and every `versioncam` command reads it from there. Step 0 found
18
+ no `versioncam.config.*` anywhere in the repository, so this is the only one.
19
+ A second would split the repository in two.
21
20
 
22
21
  ## 1. How the app runs
23
22
 
@@ -82,7 +81,7 @@ say what the person needs to do: nothing below works on a sign-in screen.
82
81
 
83
82
  ## 3. Write the config
84
83
 
85
- `versioncam.config.ts` in this directory, with only what the app needs:
84
+ `versioncam.config.ts` in the app's directory, with only what the app needs:
86
85
 
87
86
  ```ts
88
87
  import { defineRecorder } from "versioncam";
@@ -101,11 +100,12 @@ for most apps, and a field nobody needed is a question the next person has to
101
100
  answer. Say in a comment above each field you did write why it has the value
102
101
  it has — the port came from the dev script, the command from the README.
103
102
 
104
- Then: create `demos/clips/`, and make sure `.versioncam/` is ignored —
105
- recordings, renders and a saved sign-in go there. If a `.gitignore` here, or
106
- in a directory above this one up to the repository root, already says so,
107
- leave it; otherwise add the line to the nearest one, creating one here if
108
- there is none, and leave the rest of the file alone.
103
+ Then: create `demos/clips/` in the app's directory, and make sure
104
+ `.versioncam/` is ignored — recordings, renders and a saved sign-in go there.
105
+ If a `.gitignore` in the app's directory, or in a directory above it up to
106
+ the repository root, already says so, leave it; otherwise add the line to the
107
+ nearest one, creating one in the app's directory if there is none, and leave
108
+ the rest of the file alone.
109
109
 
110
110
  ## 4. `npx versioncam doctor`
111
111
 
@@ -1 +0,0 @@
1
- (function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),t.credentials=e.crossOrigin===`use-credentials`?`include`:e.crossOrigin===`anonymous`?`omit`:`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();function e(e){let n=t(e);return n*n*n*(10+n*(-15+6*n))}function t(e){return e<=0?0:e>=1?1:e}function n(e,t,n){return e+(t-e)*n}function r(e,t,r){let i=e.x+e.w/2,a=e.y+e.h/2,o=t.x+t.w/2,s=t.y+t.h/2,c=e.w*(t.w/e.w)**+r,l=e.h*(t.h/e.h)**+r,u=n(i,o,r),d=n(a,s,r);return{x:u-c/2,y:d-l/2,w:c,h:l}}function i(t,n){if(t.length===0)throw Error(`evalCamera called with no keys`);if(n<=t[0].t)return t[0].rect;for(let i=1;i<t.length;i+=1){let a=t[i-1],o=t[i];if(n>=o.t)continue;if(o.ease===`hold`)return a.rect;let s=o.t-a.t,c=s<=0?1:(n-a.t)/s;return r(a.rect,o.rect,e(c))}return t[t.length-1].rect}function a(e,t,n,r){let i=1-r,a=i*i,o=2*i*r,s=r*r;return[a*e[0]+o*t[0]+s*n[0],a*e[1]+o*t[1]+s*n[1]]}function o(t,n){if(t.length===0)return[0,0];let r=t[0];if(n<=r.t0)return r.kind===`move`?r.from:r.at;for(let r of t){if(n>r.t1)continue;if(r.kind===`hold`)return r.at;let t=r.t1-r.t0,i=t<=0?1:(n-r.t0)/t;return a(r.from,r.ctrl,r.to,e(i))}let i=t[t.length-1];return i.kind===`move`?i.to:i.at}function s(e,t,n){let r=n.w/t.w;return[n.x+(e[0]-t.x)*r,n.y+(e[1]-t.y)*r]}function c(e,t,n){let r=n.w/t.w;return{x:n.x+(e.x-t.x)*r,y:n.y+(e.y-t.y)*r,w:e.w*r,h:e.h*r}}function l(e,t){return{x:e.x*t,y:e.y*t,w:e.w*t,h:e.h*t}}function u(e,t,n){let r=Math.min(e.width,e.height)*n,i=e.width-r*2,a=e.height-r*2,o=t.width/t.height,s=i,c=s/o;return c>a&&(c=a,s=c*o),{x:(e.width-s)/2,y:(e.height-c)/2,w:s,h:c}}function d(e,n,r){return r<=0?1:t((e-n+1)/r)}function f(e,n,r,i){return e<=n||e>=r?0:i<=0?1:Math.min(t((e-n)/i),t((r-e)/i))}function p(e,t){return e/t}function m(e,t,n){let r=n?.padding??0;return!n||r<=0?{x:0,y:0,w:e.width,h:e.height}:u(e,t,r)}function h(e){let t=e?.caption??`auto`;return t===`none`?`none`:t===`below`||t===`overlay`?t:(e?.padding??0)>0?`below`:`overlay`}var g={radius:16,hairline:`rgba(255, 255, 255, 0.16)`,shadow:`rgba(0, 0, 0, 0.45)`,shadowBlur:60,shadowOffsetY:18,captionFont:`600 30px "Helvetica Neue", Helvetica, Arial, "Liberation Sans", sans-serif`,captionColor:`rgba(255, 255, 255, 0.94)`,captionShadow:`rgba(0, 0, 0, 0.5)`,captionScrim:`rgba(0, 0, 0, 0.45)`,captionScrimHeight:86,cursorFill:`#ffffff`,cursorStroke:`rgba(0, 0, 0, 0.55)`,cursorShadow:`rgba(0, 0, 0, 0.35)`,ringWidth:3,ringRadius:10,spotlightDim:.55,spotlightTint:`6, 2, 16`};function _(e,t){let n=/^#([0-9a-f]{6})$/i.exec(e.trim());if(!n)return e;let r=Number.parseInt(n[1],16);return`rgba(${r>>16&255}, ${r>>8&255}, ${r&255}, ${t})`}function v(e,t,n){let r=Math.min(n,t.w/2,t.h/2);e.beginPath(),e.moveTo(t.x+r,t.y),e.arcTo(t.x+t.w,t.y,t.x+t.w,t.y+t.h,r),e.arcTo(t.x+t.w,t.y+t.h,t.x,t.y+t.h,r),e.arcTo(t.x,t.y+t.h,t.x,t.y,r),e.arcTo(t.x,t.y,t.x+t.w,t.y,r),e.closePath()}function y(e,t,n){if(typeof n==`string`)e.fillStyle=n;else{let r=e.createLinearGradient(0,0,t.width,t.height);r.addColorStop(0,n[0]),r.addColorStop(1,n[1]),e.fillStyle=r}e.fillRect(0,0,t.width,t.height)}function b(e,t,n){for(let r of e.cuts)if(t>r&&t<=r+n)return{current:e.frames[t],previous:e.frames[r],alpha:d(t,r+1,n)};return{current:e.frames[t],previous:null,alpha:1}}function x(e,t,n,r,i,a,o){let s=l(i,t.viewport.dpr),{current:c,previous:u,alpha:d}=b(t,r,o.motion.crossfadeFrames),f=o.theme.presentation?.frame??!1;e.save(),f&&(v(e,a,g.radius),e.clip());let p=(t,r)=>{let i=n.images[t];i&&(e.globalAlpha=r,e.drawImage(i,s.x,s.y,s.w,s.h,a.x,a.y,a.w,a.h))};u!==null&&u!==c&&p(u,1),p(c,u===null?1:d),e.globalAlpha=1,e.restore()}function S(e,t){e.save(),e.shadowColor=g.shadow,e.shadowBlur=g.shadowBlur,e.shadowOffsetY=g.shadowOffsetY,e.fillStyle=`rgba(0,0,0,1)`,v(e,t,g.radius),e.fill(),e.restore()}function C(e,t){e.save(),e.strokeStyle=g.hairline,e.lineWidth=1,v(e,t,g.radius),e.stroke(),e.restore()}function w(e,t){return{x:e.x-t,y:e.y-t,w:e.w+t*2,h:e.h+t*2}}function T(e,t,n,r,i,a){let o=a.theme.presentation?.frame??!1;for(let s of t.highlights){let t=f(n,s.t0,s.t1,.25);if(t<=0)continue;let l=c(s.rect,r,i);e.save(),e.globalAlpha=t,s.style===`spotlight`?(e.save(),v(e,i,o?g.radius:0),e.clip(),e.fillStyle=`rgba(${g.spotlightTint}, ${g.spotlightDim})`,e.fillRect(i.x,i.y,i.w,i.h),e.globalCompositeOperation=`destination-out`,v(e,w(l,8),g.ringRadius),e.fill(),e.restore()):(e.strokeStyle=a.theme.accent,e.lineWidth=g.ringWidth,e.shadowColor=_(a.theme.accent,.35),e.shadowBlur=18,v(e,w(l,6),g.ringRadius),e.stroke()),e.restore()}}function E(e,t,n){let r=!1,i=[],a=0;for(let o of e.pointer){if(o.t>t)break;if(o.kind===`down`)r=!0,a=o.t;else{r=!1;let e=t-o.t;e>=0&&e<=n.motion.rippleMs/1e3&&i.push({at:o.at,age:e})}}let o=r?t-a:0,s=r?Math.min(1,o/(n.motion.pressScaleMs/1e3)):0;return{scale:1-(1-n.motion.pressScale)*s,ripples:i}}function D(e,t,n,r,i){for(let a of t){let t=a.age/(i.motion.rippleMs/1e3),[o,c]=s(a.at,n,r);e.save(),e.globalAlpha=(1-t)*.55,e.strokeStyle=i.theme.accent,e.lineWidth=2.5,e.beginPath(),e.arc(o,c,6+t*i.motion.rippleRadius,0,Math.PI*2),e.stroke(),e.restore()}}function O(e,t,n,r){let i=r*n;e.save(),e.translate(t[0],t[1]),e.scale(i,i),e.shadowColor=g.cursorShadow,e.shadowBlur=6/i,e.shadowOffsetY=2/i,e.beginPath(),e.moveTo(0,0),e.lineTo(0,16.6),e.lineTo(4.1,12.9),e.lineTo(6.9,18.9),e.lineTo(9.6,17.6),e.lineTo(6.9,11.8),e.lineTo(12.1,11.5),e.closePath(),e.fillStyle=g.cursorFill,e.fill(),e.shadowColor=`transparent`,e.strokeStyle=g.cursorStroke,e.lineWidth=1/i,e.stroke(),e.restore()}function k(e,t,n,r,i,a){let o=h(a.theme.presentation);if(o!==`none`)for(let s of t.captions){let t=f(n,s.t0,s.t1,a.motion.captionFadeS);if(t<=0)continue;if(e.save(),e.globalAlpha=t,o===`overlay`){let t=g.captionScrimHeight,n=e.createLinearGradient(0,i.height-t,0,i.height);n.addColorStop(0,`rgba(0, 0, 0, 0)`),n.addColorStop(1,g.captionScrim),e.fillStyle=n,e.fillRect(0,i.height-t,i.width,t)}e.font=g.captionFont,e.fillStyle=g.captionColor,e.textBaseline=`middle`,e.shadowColor=g.captionShadow,e.shadowBlur=8;let c=o===`overlay`?40:r.x,l=o===`overlay`?i.height-g.captionScrimHeight/2+4:(r.y+r.h+i.height)/2;e.fillText(s.text,c,l),e.restore()}}function A(e,t,n,r,a,c){let l=p(r,t.fps),u=i(t.camera,l),d=c.theme.presentation,f=m(a,t.viewport,d);d?.backdrop&&y(e,a,d.backdrop),d?.frame&&S(e,f),x(e,t,n,r,u,f,c),d?.frame&&C(e,f),T(e,t,l,u,f,c);let{scale:h,ripples:g}=E(t,l,c);D(e,g,u,f,c),O(e,s(o(t.cursor,l),u,f),h,c.theme.cursorScale),k(e,t,l,f,a,c)}var j=`modulepreload`,M=function(e,t){return new URL(e,t).href},N={},P=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}function s(e){return import.meta.resolve?import.meta.resolve(e):new URL(e,import.meta.url).href}r=o(t.map(t=>{if(t=M(t,n),t=s(t),t in N)return;N[t]=!0;let r=t.endsWith(`.css`);for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}let i=document.createElement(`link`);if(i.rel=r?`stylesheet`:j,r||(i.as=`script`),i.crossOrigin=``,i.href=t,a&&i.setAttribute(`nonce`,a),document.head.appendChild(i),r)return new Promise((e,n)=>{i.addEventListener(`load`,e),i.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}).filter(e=>e!==void 0))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},F=new URLSearchParams(location.search),I=F.has(`recording`),L=F.get(`scene`),R=F.get(`headless`)===`1`;R&&document.body.classList.add(`headless`);var z=document.getElementById(`stage`),B=document.getElementById(`scrub`),V=document.getElementById(`status`),H=document.getElementById(`play`),U=document.getElementById(`error`);function W(e){throw U.textContent=e,Error(e)}async function G(e,t){let n=await fetch(e);return n.ok||W(`Could not load ${t} (${e}): ${n.status}`),await n.json()}async function K(e){let t=await fetch(e);return t.ok||W(`Could not load ${e}: ${t.status}`),createImageBitmap(await t.blob())}function q(e){z.width=e.output.width,z.height=e.output.height,R?(z.style.width=`${e.output.width}px`,z.style.height=`${e.output.height}px`):(z.style.width=`min(100%, 1280px)`,z.style.height=`auto`);let t=z.getContext(`2d`,{alpha:!1});return t||W(`No 2D context`),t.imageSmoothingEnabled=!0,t.imageSmoothingQuality=`high`,t}function J(e,t,n,r){let i=!1;H.addEventListener(`click`,()=>{i=!i,H.textContent=i?`Pause`:`Play`;let a=performance.now(),o=s=>{i&&(s-a>=1e3/e&&(a=s,r((n()+1)%t)),requestAnimationFrame(o))};requestAnimationFrame(o)})}function Y(e,t,n,r){e.durationFrames=t,e.fps=n,e.setFrame=e=>new Promise(t=>{r(e),requestAnimationFrame(()=>requestAnimationFrame(()=>t()))})}async function X(e,t){let n=await G(`/recording/timeline.json`,`the recording`),r=q(t),i={images:await Promise.all(n.states.map(e=>K(`/recording/states/${e}`)))};await document.fonts.ready;let a=-1,o=e=>{let o=Math.max(0,Math.min(n.durationFrames-1,e));A(r,n,i,o,z,t),a=o,R||(B.value=String(o),V.textContent=`${o+1} / ${n.durationFrames} · ${(o/n.fps).toFixed(2)}s · state ${n.frames[o]}`)};B.max=String(n.durationFrames-1),B.addEventListener(`input`,()=>o(Number(B.value))),J(n.fps,n.durationFrames,()=>a,o),o(Number(F.get(`frame`)??0)),Y(e,n.durationFrames,n.fps,o)}async function Z(e,t,n){let r=(await P(()=>import(`/scenes.js`),[],import.meta.url)).default,i=r?.[n];i||W(`No scene "${n}". The scenes module exports: ${Object.keys(r??{}).join(`, `)||`(nothing)`}.`);let a=q(t);await document.fonts.ready;let o=-1,s=e=>{let t=Math.max(0,Math.min(i.durationFrames-1,e));i.draw(a,t,z),o=t,R||(B.value=String(t),V.textContent=`${t+1} / ${i.durationFrames} · ${(t/i.fps).toFixed(2)}s`)};B.max=String(i.durationFrames-1),B.addEventListener(`input`,()=>s(Number(B.value))),J(i.fps,i.durationFrames,()=>o,s),s(Number(F.get(`frame`)??0)),Y(e,i.durationFrames,i.fps,s)}async function Q(e){let t=await G(`/render-config.json`,`the render config`);if(L)return Z(e,t,L);if(I)return X(e,t);W(`Pass ?recording or ?scene=<id>.`)}var $={ready:Promise.resolve(),setFrame:async()=>{},durationFrames:0,fps:60};window.__render=$,$.ready=Q($);