versioncam 0.1.3 → 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 (74) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/README.md +116 -13
  3. package/dist/cli/commands/doctor.js +39 -2
  4. package/dist/cli/commands/doctor.js.map +1 -1
  5. package/dist/cli/commands/init.js +25 -0
  6. package/dist/cli/commands/init.js.map +1 -1
  7. package/dist/cli/commands/preview.js +10 -1
  8. package/dist/cli/commands/preview.js.map +1 -1
  9. package/dist/cli/commands/render.d.ts +17 -0
  10. package/dist/cli/commands/render.js +78 -21
  11. package/dist/cli/commands/render.js.map +1 -1
  12. package/dist/cli/cwd.d.ts +46 -0
  13. package/dist/cli/cwd.js +102 -0
  14. package/dist/cli/cwd.js.map +1 -0
  15. package/dist/cli/main.js +11 -2
  16. package/dist/cli/main.js.map +1 -1
  17. package/dist/cli/usage.d.ts +3 -2
  18. package/dist/cli/usage.js +9 -4
  19. package/dist/cli/usage.js.map +1 -1
  20. package/dist/config.d.ts +24 -0
  21. package/dist/config.js +25 -0
  22. package/dist/config.js.map +1 -1
  23. package/dist/core/camera-track.d.ts +63 -0
  24. package/dist/core/camera-track.js +230 -0
  25. package/dist/core/camera-track.js.map +1 -0
  26. package/dist/core/camera.d.ts +14 -1
  27. package/dist/core/camera.js +18 -2
  28. package/dist/core/camera.js.map +1 -1
  29. package/dist/core/follow.d.ts +129 -0
  30. package/dist/core/follow.js +151 -0
  31. package/dist/core/follow.js.map +1 -0
  32. package/dist/core/motion-defaults.d.ts +29 -0
  33. package/dist/core/motion-defaults.js +29 -0
  34. package/dist/core/motion-defaults.js.map +1 -1
  35. package/dist/core/timeline.d.ts +10 -2
  36. package/dist/core/timeline.js.map +1 -1
  37. package/dist/driver/session.d.ts +22 -1
  38. package/dist/driver/session.js +48 -12
  39. package/dist/driver/session.js.map +1 -1
  40. package/dist/index.d.ts +2 -2
  41. package/dist/index.js +1 -1
  42. package/dist/index.js.map +1 -1
  43. package/dist/loader.d.ts +7 -1
  44. package/dist/loader.js +23 -8
  45. package/dist/loader.js.map +1 -1
  46. package/dist/page/assets/index-fZtacWyp.js +2 -0
  47. package/dist/page/index.html +1 -1
  48. package/dist/render/encode.d.ts +12 -1
  49. package/dist/render/encode.js +94 -35
  50. package/dist/render/encode.js.map +1 -1
  51. package/dist/render/presentation.d.ts +12 -0
  52. package/dist/render/presentation.js +15 -0
  53. package/dist/render/presentation.js.map +1 -1
  54. package/dist/render/render.js +18 -1
  55. package/dist/render/render.js.map +1 -1
  56. package/dist/render/sampling.d.ts +13 -0
  57. package/dist/render/sampling.js +47 -19
  58. package/dist/render/sampling.js.map +1 -1
  59. package/dist/render/scenes.d.ts +16 -0
  60. package/dist/render/scenes.js +108 -0
  61. package/dist/render/scenes.js.map +1 -0
  62. package/dist/render/sequence.d.ts +18 -0
  63. package/dist/render/sequence.js +67 -13
  64. package/dist/render/sequence.js.map +1 -1
  65. package/dist/review/review.js +46 -21
  66. package/dist/review/review.js.map +1 -1
  67. package/dsl.md +37 -0
  68. package/package.json +1 -1
  69. package/plugin/README.md +7 -2
  70. package/plugin/agents/versioncam-reviewer.md +3 -2
  71. package/plugin/skills/versioncam/SKILL.md +60 -14
  72. package/plugin/skills/versioncam/authoring.md +6 -1
  73. package/plugin/skills/versioncam/onboarding.md +17 -17
  74. package/dist/page/assets/index-DUom5amc.js +0 -1
@@ -0,0 +1,230 @@
1
+ import { interpolateRect } from "./camera.js";
2
+ import { minjerk } from "./easing.js";
3
+ import { followCentres } from "./follow.js";
4
+ import { frameAt, timeAt } from "./timeline.js";
5
+ /**
6
+ * Turn a clip's camera writes into the keys the renderer interpolates.
7
+ *
8
+ * Every write starts from where the camera is, moving or not, and blends
9
+ * from there to where it is going:
10
+ *
11
+ * camera(t) = interpolateRect(before(t), destination(t), minjerk(u))
12
+ *
13
+ * where `before` is the camera as it would have gone on without this write,
14
+ * and `u` runs from 0 to 1 over the write's duration. Min-jerk has no slope
15
+ * at either end, so the camera's velocity is continuous where a blend starts
16
+ * and where it ends: entering a follow from a keyed zoom, leaving it for a
17
+ * reset, or a zoom written while another is still under way. That last one
18
+ * used to wait for the move before it to finish and then run short, because
19
+ * each glide set off from the previous glide's target.
20
+ *
21
+ * A clip that never follows and never writes a move while another is under
22
+ * way gets exactly the keys it always got: a hold where each write starts and
23
+ * a min-jerk key where it ends. Every other clip gets the camera sampled at
24
+ * each frame (the `"linear"` keys), since a blend between two moving framings
25
+ * is no longer a glide between two keys.
26
+ */
27
+ export function cameraTrack(writes, context) {
28
+ const { viewport } = context;
29
+ const full = { x: 0, y: 0, w: viewport.width, h: viewport.height };
30
+ const opening = { t: 0, rect: full, ease: "hold" };
31
+ const effective = effectiveWrites(writes);
32
+ if (glidesOnly(effective))
33
+ return glideKeys(effective, opening);
34
+ return sampledKeys(effective, context, full);
35
+ }
36
+ /**
37
+ * Whether every write is a framing that starts once the one before it has
38
+ * arrived: the only case the old keys describe exactly.
39
+ */
40
+ function glidesOnly(writes) {
41
+ let arrived = 0;
42
+ for (const write of writes) {
43
+ if (write.kind !== "frame" || write.t < arrived)
44
+ return false;
45
+ // The same expression the key's time is computed with below, so the
46
+ // comparison is between the same two doubles the keys will hold.
47
+ arrived = write.t + write.durationMs / 1000;
48
+ }
49
+ return true;
50
+ }
51
+ /**
52
+ * The keys the session wrote before it could follow: hold the current
53
+ * framing until the write, then glide. Kept to the byte, so no recording of
54
+ * a clip that only zooms changes.
55
+ */
56
+ function glideKeys(writes, opening) {
57
+ const keys = [opening];
58
+ for (const write of writes) {
59
+ const last = keys[keys.length - 1];
60
+ if (last.t < write.t) {
61
+ keys.push({ t: write.t, rect: last.rect, ease: "hold" });
62
+ }
63
+ keys.push({
64
+ t: write.t + write.durationMs / 1000,
65
+ rect: write.rect,
66
+ ease: "minjerk",
67
+ });
68
+ }
69
+ return keys;
70
+ }
71
+ /**
72
+ * The writes that take effect.
73
+ *
74
+ * A release with no follow to release is nothing to do. A write replaces an
75
+ * earlier one at the same instant that was still to take time: two moves
76
+ * written together are one intention stated twice, and blending the first
77
+ * under the second would bend the camera towards a framing no frame was
78
+ * meant to show. A cut (duration 0) at the same instant is kept, since it has
79
+ * already happened by the time the next write starts from it.
80
+ */
81
+ function effectiveWrites(writes) {
82
+ const effective = [];
83
+ for (const write of writes) {
84
+ const last = effective[effective.length - 1];
85
+ if (write.kind === "release") {
86
+ if (last?.kind === "follow")
87
+ effective.push(write);
88
+ continue;
89
+ }
90
+ while (effective.length > 0) {
91
+ const previous = effective[effective.length - 1];
92
+ if (previous.kind === "release" ||
93
+ previous.t !== write.t ||
94
+ previous.durationMs <= 0) {
95
+ break;
96
+ }
97
+ effective.pop();
98
+ }
99
+ effective.push(write);
100
+ }
101
+ return effective;
102
+ }
103
+ /** The first frame whose time is at or after `t`. */
104
+ function firstFrameFrom(t, fps) {
105
+ let frame = Math.max(0, Math.ceil(t * fps - 1e-9));
106
+ while (timeAt(frame, fps) < t)
107
+ frame += 1;
108
+ while (frame > 0 && timeAt(frame - 1, fps) >= t)
109
+ frame -= 1;
110
+ return frame;
111
+ }
112
+ /** To a thousandth of a pixel: far below anything a frame can show. */
113
+ const round = (value) => Math.round(value * 1000) / 1000;
114
+ /**
115
+ * A sample, rounded, so a camera at rest compares equal to itself from one
116
+ * frame to the next and its run of samples collapses to two.
117
+ *
118
+ * Rounding may not carry an edge that was inside the page outside it: a
119
+ * frame at the page's right edge is `x + w = width` exactly, and rounding
120
+ * each half up would put a thousandth of a pixel of nothing in the picture.
121
+ */
122
+ function roundRect(rect, viewport) {
123
+ const w = round(rect.w);
124
+ const h = round(rect.h);
125
+ return {
126
+ x: roundInside(rect.x, rect.w, w, viewport.width),
127
+ y: roundInside(rect.y, rect.h, h, viewport.height),
128
+ w,
129
+ h,
130
+ };
131
+ }
132
+ /**
133
+ * One edge's position, rounded, and kept inside `[0, limit]` if it was
134
+ * inside to begin with, give or take the last bit of a double: a frame
135
+ * centred as far right as it goes is `x + w = width` only up to the float
136
+ * arithmetic that centred it. A rectangle a clip asked for outside the page
137
+ * (`camera.to` accepts one) is left where it was asked for.
138
+ */
139
+ function roundInside(start, size, rounded, limit) {
140
+ const slack = 1e-6;
141
+ const at = round(start);
142
+ if (start < -slack || start + size > limit + slack)
143
+ return at;
144
+ if (at < 0)
145
+ return 0;
146
+ if (at + rounded > limit)
147
+ return Math.floor((limit - rounded) * 1000) / 1000;
148
+ return at;
149
+ }
150
+ function sameRect(a, b) {
151
+ return a.x === b.x && a.y === b.y && a.w === b.w && a.h === b.h;
152
+ }
153
+ function sampledKeys(effective, context, full) {
154
+ const { fps, viewport } = context;
155
+ const lastFrame = context.durationFrames - 1;
156
+ // Sampled through the last frame and then on, past it, until every write
157
+ // has arrived: a move still running when the clip ends leaves keys after
158
+ // the end, as a glide's key after the end always has, and that is what
159
+ // `review` warns about.
160
+ let endFrame = lastFrame;
161
+ for (const write of effective) {
162
+ if (write.kind === "release")
163
+ continue;
164
+ endFrame = Math.max(endFrame, frameAt(write.t + write.durationMs / 1000, fps));
165
+ }
166
+ const movers = [];
167
+ effective.forEach((write, index) => {
168
+ if (write.kind === "release")
169
+ return;
170
+ const duration = write.durationMs / 1000;
171
+ if (write.kind === "frame") {
172
+ const rect = write.rect;
173
+ movers.push({ t: write.t, duration, destination: () => rect });
174
+ return;
175
+ }
176
+ // A follow reads the cursor until the next write of any kind, release
177
+ // included.
178
+ const next = effective[index + 1];
179
+ const firstFrame = firstFrameFrom(write.t, fps);
180
+ const centres = followCentres({
181
+ cursor: context.cursor,
182
+ cursorStart: context.cursorStart,
183
+ viewport,
184
+ fps,
185
+ spec: write.spec,
186
+ firstFrame,
187
+ lastFrame: endFrame,
188
+ until: next ? next.t : Number.POSITIVE_INFINITY,
189
+ });
190
+ const w = viewport.width / write.spec.scale;
191
+ const h = viewport.height / write.spec.scale;
192
+ movers.push({
193
+ t: write.t,
194
+ duration,
195
+ destination: (frame) => {
196
+ const [cx, cy] = centres[frame - firstFrame];
197
+ return { x: cx - w / 2, y: cy - h / 2, w, h };
198
+ },
199
+ });
200
+ });
201
+ // The camera at a frame: the latest write that has started, blended with
202
+ // the camera as it was going before it, for as long as the blend lasts.
203
+ const cameraAt = (index, frame, t) => {
204
+ if (index < 0)
205
+ return full;
206
+ const mover = movers[index];
207
+ const destination = mover.destination(frame);
208
+ const u = mover.duration > 0 ? (t - mover.t) / mover.duration : 1;
209
+ if (u >= 1)
210
+ return destination;
211
+ return interpolateRect(cameraAt(index - 1, frame, t), destination, minjerk(u));
212
+ };
213
+ const keys = [];
214
+ let latest = -1;
215
+ for (let frame = 0; frame <= endFrame; frame += 1) {
216
+ const t = timeAt(frame, fps);
217
+ while (latest + 1 < movers.length && movers[latest + 1].t <= t) {
218
+ latest += 1;
219
+ }
220
+ const rect = roundRect(cameraAt(latest, frame, t), viewport);
221
+ keys.push({ t, rect, ease: frame === 0 ? "hold" : "linear" });
222
+ }
223
+ // Where the camera is still, only the two ends of the stillness matter:
224
+ // between two equal samples, every sample is the same.
225
+ return keys.filter((key, i) => i === 0 ||
226
+ i === keys.length - 1 ||
227
+ !sameRect(keys[i - 1].rect, key.rect) ||
228
+ !sameRect(key.rect, keys[i + 1].rect));
229
+ }
230
+ //# sourceMappingURL=camera-track.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"camera-track.js","sourceRoot":"","sources":["../../src/core/camera-track.ts"],"names":[],"mappings":"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"}
@@ -22,9 +22,22 @@ export declare function clampRect(rect: Rect, viewport: Viewport): Rect;
22
22
  export declare function zoomRect(target: Rect, viewport: Viewport, options?: ZoomOptions): Rect;
23
23
  /** The magnification a camera rectangle represents. */
24
24
  export declare function scaleOf(rect: Rect, viewport: Viewport): number;
25
+ /**
26
+ * Interpolate one camera framing towards another.
27
+ *
28
+ * The size moves geometrically and the centre linearly. Interpolating width
29
+ * linearly is the usual reason a zoom feels wrong: going from 1440 wide to 780
30
+ * wide, the first half of the move changes the magnification by 1.22x and the
31
+ * second half by 1.51x, so a zoom that is mathematically even reads as rushing
32
+ * into the target and then crawling. In log space each equal step multiplies
33
+ * the scale by the same factor, which is what the eye actually reads as
34
+ * constant speed.
35
+ */
36
+ export declare function interpolateRect(a: Rect, b: Rect, u: number): Rect;
25
37
  /**
26
38
  * Where the camera is at time `t`. A key's `ease` describes how the camera
27
39
  * reaches it from the previous key: `minjerk` glides, `hold` stays put and
28
- * then cuts.
40
+ * then cuts, `linear` is the next sample of a path the recording computed
41
+ * frame by frame, a follow or a blend.
29
42
  */
30
43
  export declare function evalCamera(keys: CameraKey[], t: number): Rect;
@@ -56,7 +56,7 @@ export function scaleOf(rect, viewport) {
56
56
  * the scale by the same factor, which is what the eye actually reads as
57
57
  * constant speed.
58
58
  */
59
- function interpolateRect(a, b, u) {
59
+ export function interpolateRect(a, b, u) {
60
60
  const cxA = a.x + a.w / 2;
61
61
  const cyA = a.y + a.h / 2;
62
62
  const cxB = b.x + b.w / 2;
@@ -67,10 +67,24 @@ function interpolateRect(a, b, u) {
67
67
  const cy = lerp(cyA, cyB, u);
68
68
  return { x: cx - w / 2, y: cy - h / 2, w, h };
69
69
  }
70
+ /**
71
+ * Straight between two samples of a path, for keys a frame apart. The
72
+ * renderer asks only for frame times, where this lands on a sample exactly;
73
+ * between them, what `review` asks for is off by less than a frame's motion.
74
+ */
75
+ function lerpRect(a, b, u) {
76
+ return {
77
+ x: lerp(a.x, b.x, u),
78
+ y: lerp(a.y, b.y, u),
79
+ w: lerp(a.w, b.w, u),
80
+ h: lerp(a.h, b.h, u),
81
+ };
82
+ }
70
83
  /**
71
84
  * Where the camera is at time `t`. A key's `ease` describes how the camera
72
85
  * reaches it from the previous key: `minjerk` glides, `hold` stays put and
73
- * then cuts.
86
+ * then cuts, `linear` is the next sample of a path the recording computed
87
+ * frame by frame, a follow or a blend.
74
88
  */
75
89
  export function evalCamera(keys, t) {
76
90
  if (keys.length === 0) {
@@ -87,6 +101,8 @@ export function evalCamera(keys, t) {
87
101
  return prev.rect;
88
102
  const span = next.t - prev.t;
89
103
  const u = span <= 0 ? 1 : (t - prev.t) / span;
104
+ if (next.ease === "linear")
105
+ return lerpRect(prev.rect, next.rect, u);
90
106
  return interpolateRect(prev.rect, next.rect, minjerk(u));
91
107
  }
92
108
  return keys[keys.length - 1].rect;
@@ -1 +1 @@
1
- {"version":3,"file":"camera.js","sourceRoot":"","sources":["../../src/core/camera.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"camera.js","sourceRoot":"","sources":["../../src/core/camera.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,129 @@
1
+ import type { Viewport } from "./camera.js";
2
+ import type { CursorTrack, Vec2 } from "./timeline.js";
3
+ /**
4
+ * A camera that follows the cursor.
5
+ *
6
+ * The framing at each frame is a fixed size, `viewport / scale`, centred as
7
+ * near the cursor as three things allow:
8
+ *
9
+ * 1. **A dead zone.** The camera keeps a window centre that moves only when
10
+ * the cursor pushes past the window's edge, the way a platform game's
11
+ * camera follows its character. A cursor moving to the next field of a
12
+ * form stays inside the window, so the camera does not answer it: a
13
+ * camera that answered every movement would be the zooming in and out
14
+ * this mode exists to stop, only smaller.
15
+ * 2. **The page.** The window centre is clamped so the frame never leaves
16
+ * the captured viewport, which has nothing beyond its edge to show.
17
+ * 3. **A critically damped spring, run forward and then backward** over the
18
+ * window's path.
19
+ *
20
+ * Why a spring run both ways, rather than once: a spring that only runs
21
+ * forward trails its target by twice its time constant times the target's
22
+ * speed. At 1.35× on a 1440-wide page, a spring soft enough to look smooth
23
+ * trailed a cursor crossing the page by enough to put the cursor at the
24
+ * frame's edge, and past it. The recording knows the whole cursor path
25
+ * before anything is drawn, so the camera may use the path's future as well
26
+ * as its past, and run backward the spring's lag cancels its own lag. What it
27
+ * costs is a little anticipation: the camera sets off a moment before the
28
+ * cursor does, which is what a camera operator who knew the script would do.
29
+ *
30
+ * The spring's impulse response is never negative, and neither is the
31
+ * reversed one, so every camera position is a weighted average of window
32
+ * positions. Three properties follow from that alone, with no clamping
33
+ * afterwards to break them: the camera never overshoots, because it never
34
+ * goes beyond where the window has been; it stays inside the page, because
35
+ * an average of positions inside an interval is inside it; and it moves
36
+ * smoothly, because a critically damped spring's velocity and acceleration
37
+ * are continuous however sharply its target turns.
38
+ */
39
+ export type FollowSpec = {
40
+ /** Magnification, exactly: the frame is `viewport / scale` the whole time. */
41
+ scale: number;
42
+ /**
43
+ * The share of the frame, around its centre, the cursor can move within
44
+ * without moving the camera: `0.3` is a box 30% of the frame's width by
45
+ * 30% of its height.
46
+ */
47
+ deadZone: number;
48
+ /**
49
+ * How long the camera takes over a move the cursor made at once, from 3%
50
+ * to 97% of the way, in milliseconds. That span is eight of the spring's
51
+ * time constants, so this is eight times the time constant.
52
+ */
53
+ smoothingMs: number;
54
+ };
55
+ /**
56
+ * One step of a critically damped spring, `y'' + 2ωy' + ω²y = ω²u`, from
57
+ * position `y` and velocity `v`, with the target held at `u` for `dt`.
58
+ *
59
+ * Exact for a target held still over the step, rather than an Euler step's
60
+ * approximation, so the path depends on the frame rate only through where the
61
+ * cursor is sampled: a draft at 15 frames a second follows the same curve as
62
+ * a recording at 60.
63
+ */
64
+ export declare function springStep(y: number, v: number, u: number, omega: number, dt: number): [number, number];
65
+ /**
66
+ * A spring run forward and then backward: the same curve with no lag.
67
+ *
68
+ * Holding each sample over the step that ends on it leads the input by half
69
+ * a step on the way forward, and the backward pass trails it by the same
70
+ * half step, so the two cancel as well.
71
+ */
72
+ export declare function zeroPhaseSpring(input: readonly number[], omega: number, dt: number): number[];
73
+ export type FollowPathInput = {
74
+ cursor: CursorTrack;
75
+ /** Where the cursor is when the track is empty: its resting place in frame 0. */
76
+ cursorStart: Vec2;
77
+ viewport: Viewport;
78
+ fps: number;
79
+ spec: FollowSpec;
80
+ /** The first frame the follow is asked for: the first at or after its write. */
81
+ firstFrame: number;
82
+ /** The last frame anything will ask for. */
83
+ lastFrame: number;
84
+ /**
85
+ * When the follow stops reading the cursor, in seconds: the next camera
86
+ * write. From then on it treats the cursor as still where it was, so the
87
+ * camera comes to rest on the framing it was heading for instead of
88
+ * chasing a cursor the clip has stopped following.
89
+ */
90
+ until: number;
91
+ };
92
+ /**
93
+ * The centre of the follow's frame at every frame from `firstFrame` to
94
+ * `lastFrame`, in viewport pixels. The frame itself is `viewport / scale`,
95
+ * centred there.
96
+ *
97
+ * A pure function of its input: the same cursor track and the same numbers
98
+ * make the same path, to the bit, which is what lets a recording that follows
99
+ * the cursor be compared byte for byte with the next one.
100
+ */
101
+ export declare function followCentres(input: FollowPathInput): Vec2[];
102
+ export type FollowOptions = {
103
+ /** Magnification, exactly. */
104
+ scale?: number;
105
+ /** The share of the frame the cursor can move within without moving the camera. */
106
+ deadZone?: number;
107
+ /** How softly the camera follows, in milliseconds. */
108
+ smoothing?: number;
109
+ /** How long the camera takes to get from where it was into the follow. */
110
+ duration?: number;
111
+ };
112
+ export type FollowDefaults = {
113
+ followZoom: number;
114
+ followDeadZone: number;
115
+ followSmoothingMs: number;
116
+ zoomDurationMs: number;
117
+ };
118
+ /**
119
+ * A follow's options, with the config's defaults filled in and each one
120
+ * checked where the clip wrote it.
121
+ *
122
+ * Checked here rather than when the path is computed, because by then the
123
+ * clip has run to the end: a scale of 0.8 found in `finish()` would fail the
124
+ * recording without naming the line that asked for it.
125
+ */
126
+ export declare function resolveFollow(options: FollowOptions, defaults: FollowDefaults): {
127
+ spec: FollowSpec;
128
+ durationMs: number;
129
+ };
@@ -0,0 +1,151 @@
1
+ import { clamp } from "./easing.js";
2
+ import { evalCursor } from "./cursor.js";
3
+ /** Time constants in `smoothingMs`: the span of a jump's response, 3% to 97%. */
4
+ const TIME_CONSTANTS_PER_SMOOTHING = 8;
5
+ /**
6
+ * Time constants of input held still before the backward pass starts.
7
+ *
8
+ * The backward pass has to start from rest, and a spring coming to rest never
9
+ * quite arrives: after thirty time constants the remainder is under a
10
+ * billionth of the distance it had to go, which on any page is far below a
11
+ * thousandth of a pixel.
12
+ */
13
+ const SETTLED_AFTER = 30;
14
+ /**
15
+ * One step of a critically damped spring, `y'' + 2ωy' + ω²y = ω²u`, from
16
+ * position `y` and velocity `v`, with the target held at `u` for `dt`.
17
+ *
18
+ * Exact for a target held still over the step, rather than an Euler step's
19
+ * approximation, so the path depends on the frame rate only through where the
20
+ * cursor is sampled: a draft at 15 frames a second follows the same curve as
21
+ * a recording at 60.
22
+ */
23
+ export function springStep(y, v, u, omega, dt) {
24
+ const e = y - u;
25
+ const k = v + omega * e;
26
+ const decay = Math.exp(-omega * dt);
27
+ return [u + (e + k * dt) * decay, (v - omega * k * dt) * decay];
28
+ }
29
+ /** A spring over a whole sequence, starting at rest on its first value. */
30
+ function spring(input, omega, dt) {
31
+ const out = new Array(input.length);
32
+ let y = input[0];
33
+ let v = 0;
34
+ for (let i = 0; i < input.length; i += 1) {
35
+ if (i > 0)
36
+ [y, v] = springStep(y, v, input[i], omega, dt);
37
+ out[i] = y;
38
+ }
39
+ return out;
40
+ }
41
+ /**
42
+ * A spring run forward and then backward: the same curve with no lag.
43
+ *
44
+ * Holding each sample over the step that ends on it leads the input by half
45
+ * a step on the way forward, and the backward pass trails it by the same
46
+ * half step, so the two cancel as well.
47
+ */
48
+ export function zeroPhaseSpring(input, omega, dt) {
49
+ if (input.length === 0)
50
+ return [];
51
+ const forward = spring(input, omega, dt);
52
+ return spring(forward.reverse(), omega, dt).reverse();
53
+ }
54
+ /**
55
+ * The centre of the follow's frame at every frame from `firstFrame` to
56
+ * `lastFrame`, in viewport pixels. The frame itself is `viewport / scale`,
57
+ * centred there.
58
+ *
59
+ * A pure function of its input: the same cursor track and the same numbers
60
+ * make the same path, to the bit, which is what lets a recording that follows
61
+ * the cursor be compared byte for byte with the next one.
62
+ */
63
+ export function followCentres(input) {
64
+ const { viewport, fps, spec, firstFrame, lastFrame } = input;
65
+ if (lastFrame < firstFrame)
66
+ return [];
67
+ if (!(spec.smoothingMs > 0)) {
68
+ throw new RangeError(`A follow's smoothing has to be more than 0ms, not ${spec.smoothingMs}.`);
69
+ }
70
+ const frameW = viewport.width / spec.scale;
71
+ const frameH = viewport.height / spec.scale;
72
+ // Half the dead zone each side of the window's centre.
73
+ const slackX = (spec.deadZone * frameW) / 2;
74
+ const slackY = (spec.deadZone * frameH) / 2;
75
+ const dt = 1 / fps;
76
+ const timeConstant = spec.smoothingMs / 1000 / TIME_CONSTANTS_PER_SMOOTHING;
77
+ const omega = 1 / timeConstant;
78
+ const cursorAt = (t) => input.cursor.length === 0
79
+ ? input.cursorStart
80
+ : evalCursor(input.cursor, Math.min(t, input.until));
81
+ // Past the last frame anything asks for, the path goes on until the window
82
+ // has been still for long enough that the backward pass can start from
83
+ // rest. The window is still once the cursor is: after the follow stops
84
+ // reading it, or after the track's last step, whichever is first.
85
+ const trackEnd = input.cursor.length === 0 ? 0 : input.cursor[input.cursor.length - 1].t1;
86
+ const stillFrom = Math.ceil(Math.min(input.until, trackEnd) * fps);
87
+ const farFrame = Math.max(lastFrame, stillFrom) +
88
+ Math.ceil(SETTLED_AFTER * timeConstant * fps);
89
+ const windowX = [];
90
+ const windowY = [];
91
+ let x = 0;
92
+ let y = 0;
93
+ for (let frame = firstFrame; frame <= farFrame; frame += 1) {
94
+ const [px, py] = cursorAt(frame / fps);
95
+ if (frame === firstFrame) {
96
+ // A follow starts centred on the cursor, as near as the page allows.
97
+ // The dead zone is for what the cursor does next; starting at its edge
98
+ // would frame the cursor off-centre for no reason anyone could see.
99
+ x = px;
100
+ y = py;
101
+ }
102
+ else {
103
+ x = clamp(x, px - slackX, px + slackX);
104
+ y = clamp(y, py - slackY, py + slackY);
105
+ }
106
+ x = clamp(x, frameW / 2, viewport.width - frameW / 2);
107
+ y = clamp(y, frameH / 2, viewport.height - frameH / 2);
108
+ windowX.push(x);
109
+ windowY.push(y);
110
+ }
111
+ const smoothX = zeroPhaseSpring(windowX, omega, dt);
112
+ const smoothY = zeroPhaseSpring(windowY, omega, dt);
113
+ const centres = [];
114
+ for (let i = 0; i <= lastFrame - firstFrame; i += 1) {
115
+ centres.push([smoothX[i], smoothY[i]]);
116
+ }
117
+ return centres;
118
+ }
119
+ /**
120
+ * A follow's options, with the config's defaults filled in and each one
121
+ * checked where the clip wrote it.
122
+ *
123
+ * Checked here rather than when the path is computed, because by then the
124
+ * clip has run to the end: a scale of 0.8 found in `finish()` would fail the
125
+ * recording without naming the line that asked for it.
126
+ */
127
+ export function resolveFollow(options, defaults) {
128
+ const scale = options.scale ?? defaults.followZoom;
129
+ const deadZone = options.deadZone ?? defaults.followDeadZone;
130
+ const smoothingMs = options.smoothing ?? defaults.followSmoothingMs;
131
+ const durationMs = options.duration ?? defaults.zoomDurationMs;
132
+ if (!Number.isFinite(scale) || scale < 1) {
133
+ throw new RangeError(`camera.follow: scale is ${scale}. It is a magnification, at least 1: ` +
134
+ "1.35 shows the page at 1.35 times its size.");
135
+ }
136
+ if (!Number.isFinite(deadZone) || deadZone < 0 || deadZone >= 1) {
137
+ throw new RangeError(`camera.follow: deadZone is ${deadZone}. It is a share of the frame, ` +
138
+ "from 0 (answer every movement) to below 1: 0.3 lets the cursor " +
139
+ "move across the middle 30% without moving the camera.");
140
+ }
141
+ if (!Number.isFinite(smoothingMs) || smoothingMs <= 0) {
142
+ throw new RangeError(`camera.follow: smoothing is ${smoothingMs}. It is in milliseconds, ` +
143
+ "and more than 0: larger follows more softly.");
144
+ }
145
+ if (!Number.isFinite(durationMs) || durationMs < 0) {
146
+ throw new RangeError(`camera.follow: duration is ${durationMs}. It is in milliseconds, ` +
147
+ "0 or more: 0 cuts straight into the follow.");
148
+ }
149
+ return { spec: { scale, deadZone, smoothingMs }, durationMs };
150
+ }
151
+ //# sourceMappingURL=follow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"follow.js","sourceRoot":"","sources":["../../src/core/follow.ts"],"names":[],"mappings":"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"}
@@ -45,6 +45,35 @@ export declare const MOTION: {
45
45
  readonly maxZoom: 1.65;
46
46
  /** Padding around a zoom target, as a fraction of its size. */
47
47
  readonly zoomPad: 0.12;
48
+ /**
49
+ * How far in a camera that follows the cursor goes. Gentler than a zoom's
50
+ * 1.5, because a zoom frames one thing and stops, while a follow keeps the
51
+ * cursor in frame as it travels and needs more of the page around it to
52
+ * say where it is going. A website review that found clips zooming in and
53
+ * out too much settled on this: in a little, once, and pan.
54
+ */
55
+ readonly followZoom: 1.35;
56
+ /**
57
+ * The share of a following camera's frame, around its centre, that the
58
+ * cursor can move within without the camera answering. At 1.35× on a page
59
+ * 1440 wide that is 160 pixels either side and 100 up or down: a move to
60
+ * the next field of a form stays inside it, a move across the page does
61
+ * not. Much smaller and the camera fidgets with every small movement.
62
+ */
63
+ readonly followDeadZone: 0.3;
64
+ /**
65
+ * How softly a following camera moves: the time it would take over a move
66
+ * the cursor made at once, 3% to 97% of the way. The camera runs over the
67
+ * cursor's whole path, so it has no lag to trade against; larger is
68
+ * smoother, and sets off earlier before each move the cursor makes. At
69
+ * 600, with the dead zone above, it sets off within about a tenth of a
70
+ * second of the cursor and comes to rest about a fifth of a second after
71
+ * the cursor stops: close enough to read as following, soft enough that a
72
+ * pan across the page never lurches. With no dead zone it would lead the
73
+ * cursor by a quarter of a second, which starts to read as a camera that
74
+ * moved first.
75
+ */
76
+ readonly followSmoothingMs: 600;
48
77
  /** How long a highlight stays up if the script does not say. */
49
78
  readonly highlightMs: 1800;
50
79
  /** Click ripple, in the renderer. */