versioncam 0.1.1

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 (182) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/LICENSE.md +105 -0
  3. package/README.md +463 -0
  4. package/bin/versioncam.js +29 -0
  5. package/dist/.types-render/render-page/draw.d.ts +28 -0
  6. package/dist/.types-render/render-page/main.d.ts +24 -0
  7. package/dist/.types-render/render-page/theme.d.ts +37 -0
  8. package/dist/app-server.d.ts +59 -0
  9. package/dist/app-server.js +328 -0
  10. package/dist/app-server.js.map +1 -0
  11. package/dist/cli/app.d.ts +13 -0
  12. package/dist/cli/app.js +21 -0
  13. package/dist/cli/app.js.map +1 -0
  14. package/dist/cli/commands/check.d.ts +8 -0
  15. package/dist/cli/commands/check.js +51 -0
  16. package/dist/cli/commands/check.js.map +1 -0
  17. package/dist/cli/commands/doctor.d.ts +8 -0
  18. package/dist/cli/commands/doctor.js +130 -0
  19. package/dist/cli/commands/doctor.js.map +1 -0
  20. package/dist/cli/commands/dsl.d.ts +16 -0
  21. package/dist/cli/commands/dsl.js +22 -0
  22. package/dist/cli/commands/dsl.js.map +1 -0
  23. package/dist/cli/commands/frame.d.ts +8 -0
  24. package/dist/cli/commands/frame.js +72 -0
  25. package/dist/cli/commands/frame.js.map +1 -0
  26. package/dist/cli/commands/init.d.ts +23 -0
  27. package/dist/cli/commands/init.js +109 -0
  28. package/dist/cli/commands/init.js.map +1 -0
  29. package/dist/cli/commands/inspect.d.ts +1 -0
  30. package/dist/cli/commands/inspect.js +32 -0
  31. package/dist/cli/commands/inspect.js.map +1 -0
  32. package/dist/cli/commands/install.d.ts +33 -0
  33. package/dist/cli/commands/install.js +66 -0
  34. package/dist/cli/commands/install.js.map +1 -0
  35. package/dist/cli/commands/login.d.ts +10 -0
  36. package/dist/cli/commands/login.js +49 -0
  37. package/dist/cli/commands/login.js.map +1 -0
  38. package/dist/cli/commands/measure.d.ts +1 -0
  39. package/dist/cli/commands/measure.js +36 -0
  40. package/dist/cli/commands/measure.js.map +1 -0
  41. package/dist/cli/commands/open-app.d.ts +14 -0
  42. package/dist/cli/commands/open-app.js +45 -0
  43. package/dist/cli/commands/open-app.js.map +1 -0
  44. package/dist/cli/commands/preview.d.ts +8 -0
  45. package/dist/cli/commands/preview.js +55 -0
  46. package/dist/cli/commands/preview.js.map +1 -0
  47. package/dist/cli/commands/record.d.ts +10 -0
  48. package/dist/cli/commands/record.js +86 -0
  49. package/dist/cli/commands/record.js.map +1 -0
  50. package/dist/cli/commands/render.d.ts +1 -0
  51. package/dist/cli/commands/render.js +93 -0
  52. package/dist/cli/commands/render.js.map +1 -0
  53. package/dist/cli/commands/review.d.ts +6 -0
  54. package/dist/cli/commands/review.js +89 -0
  55. package/dist/cli/commands/review.js.map +1 -0
  56. package/dist/cli/commands/sheet.d.ts +1 -0
  57. package/dist/cli/commands/sheet.js +48 -0
  58. package/dist/cli/commands/sheet.js.map +1 -0
  59. package/dist/cli/commands/stability.d.ts +14 -0
  60. package/dist/cli/commands/stability.js +110 -0
  61. package/dist/cli/commands/stability.js.map +1 -0
  62. package/dist/cli/main.d.ts +2 -0
  63. package/dist/cli/main.js +69 -0
  64. package/dist/cli/main.js.map +1 -0
  65. package/dist/cli/usage.d.ts +10 -0
  66. package/dist/cli/usage.js +46 -0
  67. package/dist/cli/usage.js.map +1 -0
  68. package/dist/config.d.ts +250 -0
  69. package/dist/config.js +154 -0
  70. package/dist/config.js.map +1 -0
  71. package/dist/core/camera.d.ts +30 -0
  72. package/dist/core/camera.js +94 -0
  73. package/dist/core/camera.js.map +1 -0
  74. package/dist/core/compose.d.ts +38 -0
  75. package/dist/core/compose.js +81 -0
  76. package/dist/core/compose.js.map +1 -0
  77. package/dist/core/cursor.d.ts +38 -0
  78. package/dist/core/cursor.js +103 -0
  79. package/dist/core/cursor.js.map +1 -0
  80. package/dist/core/easing.d.ts +15 -0
  81. package/dist/core/easing.js +33 -0
  82. package/dist/core/easing.js.map +1 -0
  83. package/dist/core/loop.d.ts +30 -0
  84. package/dist/core/loop.js +96 -0
  85. package/dist/core/loop.js.map +1 -0
  86. package/dist/core/motion-defaults.d.ts +61 -0
  87. package/dist/core/motion-defaults.js +62 -0
  88. package/dist/core/motion-defaults.js.map +1 -0
  89. package/dist/core/rng.d.ts +13 -0
  90. package/dist/core/rng.js +27 -0
  91. package/dist/core/rng.js.map +1 -0
  92. package/dist/core/sse.d.ts +15 -0
  93. package/dist/core/sse.js +16 -0
  94. package/dist/core/sse.js.map +1 -0
  95. package/dist/core/timeline.d.ts +146 -0
  96. package/dist/core/timeline.js +81 -0
  97. package/dist/core/timeline.js.map +1 -0
  98. package/dist/core/timing.d.ts +31 -0
  99. package/dist/core/timing.js +29 -0
  100. package/dist/core/timing.js.map +1 -0
  101. package/dist/core/typing.d.ts +12 -0
  102. package/dist/core/typing.js +35 -0
  103. package/dist/core/typing.js.map +1 -0
  104. package/dist/driver/clip.d.ts +71 -0
  105. package/dist/driver/clip.js +120 -0
  106. package/dist/driver/clip.js.map +1 -0
  107. package/dist/driver/compare.d.ts +34 -0
  108. package/dist/driver/compare.js +40 -0
  109. package/dist/driver/compare.js.map +1 -0
  110. package/dist/driver/gate.d.ts +36 -0
  111. package/dist/driver/gate.js +27 -0
  112. package/dist/driver/gate.js.map +1 -0
  113. package/dist/driver/launch.d.ts +43 -0
  114. package/dist/driver/launch.js +47 -0
  115. package/dist/driver/launch.js.map +1 -0
  116. package/dist/driver/page-hooks.d.ts +72 -0
  117. package/dist/driver/page-hooks.js +129 -0
  118. package/dist/driver/page-hooks.js.map +1 -0
  119. package/dist/driver/reports.d.ts +34 -0
  120. package/dist/driver/reports.js +42 -0
  121. package/dist/driver/reports.js.map +1 -0
  122. package/dist/driver/session.d.ts +285 -0
  123. package/dist/driver/session.js +773 -0
  124. package/dist/driver/session.js.map +1 -0
  125. package/dist/driver/settle.d.ts +41 -0
  126. package/dist/driver/settle.js +82 -0
  127. package/dist/driver/settle.js.map +1 -0
  128. package/dist/env.d.ts +11 -0
  129. package/dist/env.js +41 -0
  130. package/dist/env.js.map +1 -0
  131. package/dist/fixtures.d.ts +13 -0
  132. package/dist/fixtures.js +13 -0
  133. package/dist/fixtures.js.map +1 -0
  134. package/dist/index.d.ts +30 -0
  135. package/dist/index.js +16 -0
  136. package/dist/index.js.map +1 -0
  137. package/dist/inspect/inspect.d.ts +70 -0
  138. package/dist/inspect/inspect.js +176 -0
  139. package/dist/inspect/inspect.js.map +1 -0
  140. package/dist/inspect/measure.d.ts +40 -0
  141. package/dist/inspect/measure.js +107 -0
  142. package/dist/inspect/measure.js.map +1 -0
  143. package/dist/loader.d.ts +28 -0
  144. package/dist/loader.js +143 -0
  145. package/dist/loader.js.map +1 -0
  146. package/dist/page/assets/index-DUom5amc.js +1 -0
  147. package/dist/page/index.html +18 -0
  148. package/dist/render/encode.d.ts +40 -0
  149. package/dist/render/encode.js +183 -0
  150. package/dist/render/encode.js.map +1 -0
  151. package/dist/render/ffmpeg.d.ts +13 -0
  152. package/dist/render/ffmpeg.js +72 -0
  153. package/dist/render/ffmpeg.js.map +1 -0
  154. package/dist/render/presentation.d.ts +19 -0
  155. package/dist/render/presentation.js +27 -0
  156. package/dist/render/presentation.js.map +1 -0
  157. package/dist/render/render.d.ts +59 -0
  158. package/dist/render/render.js +144 -0
  159. package/dist/render/render.js.map +1 -0
  160. package/dist/render/sampling.d.ts +47 -0
  161. package/dist/render/sampling.js +129 -0
  162. package/dist/render/sampling.js.map +1 -0
  163. package/dist/render/sequence.d.ts +24 -0
  164. package/dist/render/sequence.js +105 -0
  165. package/dist/render/sequence.js.map +1 -0
  166. package/dist/render/serve.d.ts +35 -0
  167. package/dist/render/serve.js +124 -0
  168. package/dist/render/serve.js.map +1 -0
  169. package/dist/review/review.d.ts +54 -0
  170. package/dist/review/review.js +229 -0
  171. package/dist/review/review.js.map +1 -0
  172. package/dist/scene.d.ts +23 -0
  173. package/dist/scene.js +2 -0
  174. package/dist/scene.js.map +1 -0
  175. package/dsl.md +119 -0
  176. package/package.json +76 -0
  177. package/plugin/.claude-plugin/plugin.json +9 -0
  178. package/plugin/README.md +105 -0
  179. package/plugin/agents/versioncam-reviewer.md +63 -0
  180. package/plugin/skills/versioncam/SKILL.md +235 -0
  181. package/plugin/skills/versioncam/authoring.md +226 -0
  182. package/plugin/skills/versioncam/onboarding.md +199 -0
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The numbers that decide whether a clip looks human or looks generated.
3
+ *
4
+ * Each one is a judgement, not a constant of nature, so each carries the
5
+ * reason it is what it is. Tune them with the render page's scrubber, then
6
+ * write down what was wrong before.
7
+ */
8
+ export declare const MOTION: {
9
+ /**
10
+ * Pause after arriving somewhere before doing anything. Without it the
11
+ * click lands on the same frame the cursor stops, which reads as a machine
12
+ * hitting a coordinate rather than a person deciding.
13
+ */
14
+ readonly hoverDwellMs: 220;
15
+ /** Dwell before a click, drawn from this range so it is never metronomic. */
16
+ readonly clickDwellMinMs: 80;
17
+ readonly clickDwellMaxMs: 150;
18
+ /** How long the button stays down. Below ~60 ms the press is invisible. */
19
+ readonly pressMinMs: 70;
20
+ readonly pressMaxMs: 100;
21
+ /**
22
+ * A lasso stroke. Much faster and the shape reads as a scribble; much
23
+ * slower and the clip has no room for anything else.
24
+ */
25
+ readonly lassoDurationMs: 1400;
26
+ /**
27
+ * Camera moves. Longer than a cursor move on purpose: the camera carries the
28
+ * whole frame, so at 700ms it reads as a snap even with min-jerk easing and
29
+ * geometric interpolation. Around a second is where it stops calling
30
+ * attention to itself.
31
+ */
32
+ readonly zoomDurationMs: 1100;
33
+ /**
34
+ * Default magnification, and the point past which a zoom stops being free.
35
+ *
36
+ * A 1440-wide viewport captured at DPR 2 is 2880 real pixels. The app frame
37
+ * inside a 1920-wide output is about 1750. Reading 2880/Z source pixels into
38
+ * 1750 output pixels stays 1:1 only while Z <= 2880/1750, so 1.65 is where
39
+ * upscaling begins — not 1.85, which was arithmetic done backwards.
40
+ *
41
+ * Deep zooms are therefore not the way to make small UI readable. Use the
42
+ * app's own affordances instead: the chat panel has a "Full view".
43
+ */
44
+ readonly defaultZoom: 1.5;
45
+ readonly maxZoom: 1.65;
46
+ /** Padding around a zoom target, as a fraction of its size. */
47
+ readonly zoomPad: 0.12;
48
+ /** How long a highlight stays up if the script does not say. */
49
+ readonly highlightMs: 1800;
50
+ /** Click ripple, in the renderer. */
51
+ readonly rippleMs: 400;
52
+ /** How far the ripple grows, in output pixels. */
53
+ readonly rippleRadius: 34;
54
+ /** Cursor glyph scale while pressed, and how long it takes to get there. */
55
+ readonly pressScale: 0.9;
56
+ readonly pressScaleMs: 120;
57
+ /** Frames a post-settle crossfade lasts. Six at 60 fps is 100 ms. */
58
+ readonly crossfadeFrames: 6;
59
+ /** Caption fade, in seconds. */
60
+ readonly captionFadeS: 0.3;
61
+ };
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The numbers that decide whether a clip looks human or looks generated.
3
+ *
4
+ * Each one is a judgement, not a constant of nature, so each carries the
5
+ * reason it is what it is. Tune them with the render page's scrubber, then
6
+ * write down what was wrong before.
7
+ */
8
+ export const MOTION = {
9
+ /**
10
+ * Pause after arriving somewhere before doing anything. Without it the
11
+ * click lands on the same frame the cursor stops, which reads as a machine
12
+ * hitting a coordinate rather than a person deciding.
13
+ */
14
+ hoverDwellMs: 220,
15
+ /** Dwell before a click, drawn from this range so it is never metronomic. */
16
+ clickDwellMinMs: 80,
17
+ clickDwellMaxMs: 150,
18
+ /** How long the button stays down. Below ~60 ms the press is invisible. */
19
+ pressMinMs: 70,
20
+ pressMaxMs: 100,
21
+ /**
22
+ * A lasso stroke. Much faster and the shape reads as a scribble; much
23
+ * slower and the clip has no room for anything else.
24
+ */
25
+ lassoDurationMs: 1400,
26
+ /**
27
+ * Camera moves. Longer than a cursor move on purpose: the camera carries the
28
+ * whole frame, so at 700ms it reads as a snap even with min-jerk easing and
29
+ * geometric interpolation. Around a second is where it stops calling
30
+ * attention to itself.
31
+ */
32
+ zoomDurationMs: 1100,
33
+ /**
34
+ * Default magnification, and the point past which a zoom stops being free.
35
+ *
36
+ * A 1440-wide viewport captured at DPR 2 is 2880 real pixels. The app frame
37
+ * inside a 1920-wide output is about 1750. Reading 2880/Z source pixels into
38
+ * 1750 output pixels stays 1:1 only while Z <= 2880/1750, so 1.65 is where
39
+ * upscaling begins — not 1.85, which was arithmetic done backwards.
40
+ *
41
+ * Deep zooms are therefore not the way to make small UI readable. Use the
42
+ * app's own affordances instead: the chat panel has a "Full view".
43
+ */
44
+ defaultZoom: 1.5,
45
+ maxZoom: 1.65,
46
+ /** Padding around a zoom target, as a fraction of its size. */
47
+ zoomPad: 0.12,
48
+ /** How long a highlight stays up if the script does not say. */
49
+ highlightMs: 1800,
50
+ /** Click ripple, in the renderer. */
51
+ rippleMs: 400,
52
+ /** How far the ripple grows, in output pixels. */
53
+ rippleRadius: 34,
54
+ /** Cursor glyph scale while pressed, and how long it takes to get there. */
55
+ pressScale: 0.9,
56
+ pressScaleMs: 120,
57
+ /** Frames a post-settle crossfade lasts. Six at 60 fps is 100 ms. */
58
+ crossfadeFrames: 6,
59
+ /** Caption fade, in seconds. */
60
+ captionFadeS: 0.3,
61
+ };
62
+ //# sourceMappingURL=motion-defaults.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"motion-defaults.js","sourceRoot":"","sources":["../../src/core/motion-defaults.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB;;;;OAIG;IACH,YAAY,EAAE,GAAG;IAEjB,6EAA6E;IAC7E,eAAe,EAAE,EAAE;IACnB,eAAe,EAAE,GAAG;IAEpB,2EAA2E;IAC3E,UAAU,EAAE,EAAE;IACd,UAAU,EAAE,GAAG;IAEf;;;OAGG;IACH,eAAe,EAAE,IAAI;IAErB;;;;;OAKG;IACH,cAAc,EAAE,IAAI;IAEpB;;;;;;;;;;OAUG;IACH,WAAW,EAAE,GAAG;IAChB,OAAO,EAAE,IAAI;IAEb,+DAA+D;IAC/D,OAAO,EAAE,IAAI;IAEb,gEAAgE;IAChE,WAAW,EAAE,IAAI;IAEjB,qCAAqC;IACrC,QAAQ,EAAE,GAAG;IACb,kDAAkD;IAClD,YAAY,EAAE,EAAE;IAEhB,4EAA4E;IAC5E,UAAU,EAAE,GAAG;IACf,YAAY,EAAE,GAAG;IAEjB,qEAAqE;IACrE,eAAe,EAAE,CAAC;IAElB,gCAAgC;IAChC,YAAY,EAAE,GAAG;CACT,CAAC"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * A seeded PRNG, because "looks human" and "identical every run" have to hold
3
+ * at the same time. Every jittered decision in the recorder — arc direction,
4
+ * dwell length, where inside a button the cursor lands, typing cadence —
5
+ * draws from one of these, seeded per clip.
6
+ *
7
+ * mulberry32: small, fast, and good enough for motion jitter.
8
+ */
9
+ export declare function seeded(seed: number): () => number;
10
+ /** A value in [min, max) from `rng`. */
11
+ export declare function between(rng: () => number, min: number, max: number): number;
12
+ /** A symmetric jitter in [-magnitude, +magnitude). */
13
+ export declare function jitter(rng: () => number, magnitude: number): number;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * A seeded PRNG, because "looks human" and "identical every run" have to hold
3
+ * at the same time. Every jittered decision in the recorder — arc direction,
4
+ * dwell length, where inside a button the cursor lands, typing cadence —
5
+ * draws from one of these, seeded per clip.
6
+ *
7
+ * mulberry32: small, fast, and good enough for motion jitter.
8
+ */
9
+ export function seeded(seed) {
10
+ let state = seed >>> 0;
11
+ return function next() {
12
+ state = (state + 0x6d2b79f5) >>> 0;
13
+ let t = state;
14
+ t = Math.imul(t ^ (t >>> 15), t | 1);
15
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
16
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
17
+ };
18
+ }
19
+ /** A value in [min, max) from `rng`. */
20
+ export function between(rng, min, max) {
21
+ return min + rng() * (max - min);
22
+ }
23
+ /** A symmetric jitter in [-magnitude, +magnitude). */
24
+ export function jitter(rng, magnitude) {
25
+ return (rng() * 2 - 1) * magnitude;
26
+ }
27
+ //# sourceMappingURL=rng.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rng.js","sourceRoot":"","sources":["../../src/core/rng.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,UAAU,MAAM,CAAC,IAAY;IACjC,IAAI,KAAK,GAAG,IAAI,KAAK,CAAC,CAAC;IACvB,OAAO,SAAS,IAAI;QAClB,KAAK,GAAG,CAAC,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC;QACnC,IAAI,CAAC,GAAG,KAAK,CAAC;QACd,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QACrC,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC;QAC1C,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,UAAU,CAAC;IAC/C,CAAC,CAAC;AACJ,CAAC;AAED,wCAAwC;AACxC,MAAM,UAAU,OAAO,CAAC,GAAiB,EAAE,GAAW,EAAE,GAAW;IACjE,OAAO,GAAG,GAAG,GAAG,EAAE,GAAG,CAAC,GAAG,GAAG,GAAG,CAAC,CAAC;AACnC,CAAC;AAED,sDAAsD;AACtD,MAAM,UAAU,MAAM,CAAC,GAAiB,EAAE,SAAiB;IACzD,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,SAAS,CAAC;AACrC,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Server-sent-event bodies, for replaying a streamed response.
3
+ *
4
+ * An app that streams — an assistant answering, a job reporting progress —
5
+ * cannot be part of a regenerable recording while it is live: the response
6
+ * would differ every run. The driver intercepts the route and replays a
7
+ * fixture instead. This builds the body; the shape of each event's payload
8
+ * belongs to the app, so it stays in the app's own clip support code, along
9
+ * with a contract test feeding this output back through the app's parser.
10
+ */
11
+ export type SseFixtureEvent = {
12
+ event: string;
13
+ data: unknown;
14
+ };
15
+ export declare function sseBody(events: readonly SseFixtureEvent[]): string;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Server-sent-event bodies, for replaying a streamed response.
3
+ *
4
+ * An app that streams — an assistant answering, a job reporting progress —
5
+ * cannot be part of a regenerable recording while it is live: the response
6
+ * would differ every run. The driver intercepts the route and replays a
7
+ * fixture instead. This builds the body; the shape of each event's payload
8
+ * belongs to the app, so it stays in the app's own clip support code, along
9
+ * with a contract test feeding this output back through the app's parser.
10
+ */
11
+ export function sseBody(events) {
12
+ return events
13
+ .map((e) => `event: ${e.event}\ndata: ${JSON.stringify(e.data)}\n\n`)
14
+ .join("");
15
+ }
16
+ //# sourceMappingURL=sse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sse.js","sourceRoot":"","sources":["../../src/core/sse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,MAAM,UAAU,OAAO,CAAC,MAAkC;IACxD,OAAO,MAAM;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,KAAK,WAAW,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC;SACpE,IAAI,CAAC,EAAE,CAAC,CAAC;AACd,CAAC"}
@@ -0,0 +1,146 @@
1
+ /**
2
+ * The contract between the driver (which captures) and the renderer (which
3
+ * composites). A recording is a directory of state PNGs plus one of these.
4
+ *
5
+ * Coordinates are CSS pixels in viewport space — the same space
6
+ * `locator.boundingBox()` reports — so a timeline is independent of the
7
+ * device pixel ratio it was captured at. The renderer multiplies by
8
+ * `viewport.dpr` when it needs to address pixels inside a state image.
9
+ *
10
+ * Times are seconds from the start of the clip. Durations elsewhere in the
11
+ * recorder are milliseconds, because that is the unit a person authoring a
12
+ * clip thinks in; conversions happen at the boundary.
13
+ */
14
+ export type Vec2 = readonly [number, number];
15
+ export type Rect = {
16
+ x: number;
17
+ y: number;
18
+ w: number;
19
+ h: number;
20
+ };
21
+ export type CursorSegment = {
22
+ kind: "move";
23
+ t0: number;
24
+ t1: number;
25
+ from: Vec2;
26
+ to: Vec2;
27
+ /** Quadratic Bézier control point: the slight arc that reads as human. */
28
+ ctrl: Vec2;
29
+ ease: "minjerk";
30
+ };
31
+ export type CursorHold = {
32
+ kind: "hold";
33
+ t0: number;
34
+ t1: number;
35
+ at: Vec2;
36
+ };
37
+ export type CursorStep = CursorSegment | CursorHold;
38
+ export type CursorTrack = CursorStep[];
39
+ export type PointerAction = {
40
+ t: number;
41
+ kind: "down" | "up";
42
+ at: Vec2;
43
+ button: "left";
44
+ };
45
+ export type CameraKey = {
46
+ t: number;
47
+ rect: Rect;
48
+ /** How to reach this key from the previous one. */
49
+ ease: "minjerk" | "hold";
50
+ };
51
+ export type HighlightStyle = "ring" | "spotlight";
52
+ export type Highlight = {
53
+ t0: number;
54
+ t1: number;
55
+ rect: Rect;
56
+ style: HighlightStyle;
57
+ };
58
+ export type Caption = {
59
+ t0: number;
60
+ t1: number;
61
+ text: string;
62
+ };
63
+ /**
64
+ * A beat, as a frame number: where a labelled `settle()` left the page.
65
+ *
66
+ * The frame recorded is the first one *after* the settle, because a settle
67
+ * captures nothing — so the earliest picture of what a beat produced is the
68
+ * next frame, and that is the frame a reviewer should be shown.
69
+ * `versioncam review` checks a clip's beats against these and
70
+ * `versioncam sheet --at-marks` samples them, which is how the moments a clip
71
+ * was written around reach a reader instead of eight frames spread evenly
72
+ * through it.
73
+ */
74
+ export type Mark = {
75
+ frame: number;
76
+ label: string;
77
+ };
78
+ export type Timeline = {
79
+ version: 1;
80
+ clipId: string;
81
+ fps: number;
82
+ durationFrames: number;
83
+ viewport: {
84
+ width: number;
85
+ height: number;
86
+ dpr: number;
87
+ };
88
+ /** State file names, relative to the recording's `states/` directory. */
89
+ states: string[];
90
+ /** Length `durationFrames`; `frames[f]` indexes into `states`. */
91
+ frames: number[];
92
+ /**
93
+ * Frames where a settle happened, i.e. where the page may have changed a
94
+ * lot at once. The renderer crossfades only here — everywhere else a state
95
+ * change is a real one-frame change and should stay crisp.
96
+ */
97
+ cuts: number[];
98
+ /** Labelled settles, in the order they happened. */
99
+ marks: Mark[];
100
+ cursor: CursorTrack;
101
+ pointer: PointerAction[];
102
+ camera: CameraKey[];
103
+ highlights: Highlight[];
104
+ captions: Caption[];
105
+ meta: {
106
+ title: string;
107
+ recordedAt: string;
108
+ appCommit: string;
109
+ baseUrl: string;
110
+ /**
111
+ * How many frames were screenshotted, as opposed to reusing the previous
112
+ * frame's state. A property of the recording, not of the machine — the
113
+ * same script against the same app skips the same frames — so it belongs
114
+ * here, where wall-clock timings deliberately do not.
115
+ */
116
+ captured?: number;
117
+ /**
118
+ * Recorded in draft: fewer frames, captured at CSS scale. A draft is for
119
+ * looking at while writing a clip, never for publishing.
120
+ */
121
+ draft?: boolean;
122
+ };
123
+ };
124
+ export type TimelineInit = {
125
+ clipId: string;
126
+ fps: number;
127
+ viewport: {
128
+ width: number;
129
+ height: number;
130
+ dpr: number;
131
+ };
132
+ title: string;
133
+ appCommit: string;
134
+ baseUrl: string;
135
+ recordedAt: string;
136
+ };
137
+ export declare function emptyTimeline(init: TimelineInit): Timeline;
138
+ /** The frame a given time falls in. Frame f covers [f/fps, (f+1)/fps). */
139
+ export declare function frameAt(t: number, fps: number): number;
140
+ /** The time a frame starts at. */
141
+ export declare function timeAt(frame: number, fps: number): number;
142
+ /**
143
+ * Structural checks that must hold for any timeline the renderer accepts.
144
+ * Returns the problems found so a caller can report all of them at once.
145
+ */
146
+ export declare function validateTimeline(timeline: Timeline): string[];
@@ -0,0 +1,81 @@
1
+ /**
2
+ * The contract between the driver (which captures) and the renderer (which
3
+ * composites). A recording is a directory of state PNGs plus one of these.
4
+ *
5
+ * Coordinates are CSS pixels in viewport space — the same space
6
+ * `locator.boundingBox()` reports — so a timeline is independent of the
7
+ * device pixel ratio it was captured at. The renderer multiplies by
8
+ * `viewport.dpr` when it needs to address pixels inside a state image.
9
+ *
10
+ * Times are seconds from the start of the clip. Durations elsewhere in the
11
+ * recorder are milliseconds, because that is the unit a person authoring a
12
+ * clip thinks in; conversions happen at the boundary.
13
+ */
14
+ export function emptyTimeline(init) {
15
+ return {
16
+ version: 1,
17
+ clipId: init.clipId,
18
+ fps: init.fps,
19
+ durationFrames: 0,
20
+ viewport: init.viewport,
21
+ states: [],
22
+ frames: [],
23
+ cuts: [],
24
+ marks: [],
25
+ cursor: [],
26
+ pointer: [],
27
+ camera: [
28
+ {
29
+ t: 0,
30
+ rect: { x: 0, y: 0, w: init.viewport.width, h: init.viewport.height },
31
+ ease: "hold",
32
+ },
33
+ ],
34
+ highlights: [],
35
+ captions: [],
36
+ meta: {
37
+ title: init.title,
38
+ recordedAt: init.recordedAt,
39
+ appCommit: init.appCommit,
40
+ baseUrl: init.baseUrl,
41
+ },
42
+ };
43
+ }
44
+ /** The frame a given time falls in. Frame f covers [f/fps, (f+1)/fps). */
45
+ export function frameAt(t, fps) {
46
+ return Math.round(t * fps);
47
+ }
48
+ /** The time a frame starts at. */
49
+ export function timeAt(frame, fps) {
50
+ return frame / fps;
51
+ }
52
+ /**
53
+ * Structural checks that must hold for any timeline the renderer accepts.
54
+ * Returns the problems found so a caller can report all of them at once.
55
+ */
56
+ export function validateTimeline(timeline) {
57
+ const problems = [];
58
+ if (timeline.frames.length !== timeline.durationFrames) {
59
+ problems.push(`frames has ${timeline.frames.length} entries but durationFrames is ${timeline.durationFrames}`);
60
+ }
61
+ for (const [index, state] of timeline.frames.entries()) {
62
+ if (state < 0 || state >= timeline.states.length) {
63
+ problems.push(`frames[${index}] = ${state} is not a valid state index`);
64
+ break;
65
+ }
66
+ }
67
+ if (timeline.camera.length === 0) {
68
+ problems.push("camera has no keys");
69
+ }
70
+ else if (timeline.camera[0].t !== 0) {
71
+ problems.push("camera must open with a key at t=0");
72
+ }
73
+ const cameraOutOfOrder = timeline.camera.some((key, index) => index > 0 && key.t < timeline.camera[index - 1].t);
74
+ if (cameraOutOfOrder)
75
+ problems.push("camera keys are not sorted by time");
76
+ const cursorOutOfOrder = timeline.cursor.some((step, index) => index > 0 && step.t0 < timeline.cursor[index - 1].t0);
77
+ if (cursorOutOfOrder)
78
+ problems.push("cursor steps are not sorted by time");
79
+ return problems;
80
+ }
81
+ //# sourceMappingURL=timeline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timeline.js","sourceRoot":"","sources":["../../src/core/timeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAsHH,MAAM,UAAU,aAAa,CAAC,IAAkB;IAC9C,OAAO;QACL,OAAO,EAAE,CAAC;QACV,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,GAAG,EAAE,IAAI,CAAC,GAAG;QACb,cAAc,EAAE,CAAC;QACjB,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,MAAM,EAAE,EAAE;QACV,MAAM,EAAE,EAAE;QACV,IAAI,EAAE,EAAE;QACR,KAAK,EAAE,EAAE;QACT,MAAM,EAAE,EAAE;QACV,OAAO,EAAE,EAAE;QACX,MAAM,EAAE;YACN;gBACE,CAAC,EAAE,CAAC;gBACJ,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE;gBACrE,IAAI,EAAE,MAAM;aACb;SACF;QACD,UAAU,EAAE,EAAE;QACd,QAAQ,EAAE,EAAE;QACZ,IAAI,EAAE;YACJ,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,UAAU,EAAE,IAAI,CAAC,UAAU;YAC3B,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,OAAO,EAAE,IAAI,CAAC,OAAO;SACtB;KACF,CAAC;AACJ,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,OAAO,CAAC,CAAS,EAAE,GAAW;IAC5C,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC;AAC7B,CAAC;AAED,kCAAkC;AAClC,MAAM,UAAU,MAAM,CAAC,KAAa,EAAE,GAAW;IAC/C,OAAO,KAAK,GAAG,GAAG,CAAC;AACrB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,QAAkB;IACjD,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,KAAK,QAAQ,CAAC,cAAc,EAAE,CAAC;QACvD,QAAQ,CAAC,IAAI,CACX,cAAc,QAAQ,CAAC,MAAM,CAAC,MAAM,kCAAkC,QAAQ,CAAC,cAAc,EAAE,CAChG,CAAC;IACJ,CAAC;IACD,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,CAAC;QACvD,IAAI,KAAK,GAAG,CAAC,IAAI,KAAK,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;YACjD,QAAQ,CAAC,IAAI,CAAC,UAAU,KAAK,OAAO,KAAK,6BAA6B,CAAC,CAAC;YACxE,MAAM;QACR,CAAC;IACH,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACjC,QAAQ,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;IACtC,CAAC;SAAM,IAAI,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;QACtC,QAAQ,CAAC,IAAI,CAAC,oCAAoC,CAAC,CAAC;IACtD,CAAC;IACD,MAAM,gBAAgB,GAAG,QAAQ,CAAC,MAAM,CAAC,IAAI,CAC3C,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,QAAQ,CAAC,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAClE,CAAC;IACF,IAAI,gBAAgB;QAAE,QAAQ,CAAC,IAAI,CAAC,oCAAoC,CAAC,CAAC;IAC1E,MAAM,gBAAgB,GAAG,QAAQ,CAAC,MAAM,CAAC,IAAI,CAC3C,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,GAAG,CAAC,IAAI,IAAI,CAAC,EAAE,GAAG,QAAQ,CAAC,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,EAAE,CACtE,CAAC;IACF,IAAI,gBAAgB;QAAE,QAAQ,CAAC,IAAI,CAAC,qCAAqC,CAAC,CAAC;IAC3E,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Where a recording's real time went.
3
+ *
4
+ * Deliberately not written into the timeline: two recordings of one script
5
+ * must differ in no byte, which is why `recordedAt` is fixed to the epoch. A
6
+ * measurement of how busy this machine was is the opposite of that, so it is
7
+ * reported to whoever asked for the recording and not stored beside it.
8
+ *
9
+ * The buckets do not overlap. `settle()` is called by the clip script between
10
+ * advances, never inside one, so no millisecond is counted twice.
11
+ */
12
+ export type RecordingTiming = {
13
+ /** Navigation and sign-in. */
14
+ openMs: number;
15
+ /** Every `settle()`, including the one inside `open()`. */
16
+ settleMs: number;
17
+ /** The frame loop, captures included. */
18
+ framesMs: number;
19
+ /** Validating and writing the timeline. */
20
+ writeMs: number;
21
+ captured: number;
22
+ skipped: number;
23
+ };
24
+ export declare function emptyTiming(): RecordingTiming;
25
+ /**
26
+ * One line saying where the next second would have to come from.
27
+ *
28
+ * The capture counts ride along with the frame bucket because they explain it:
29
+ * a frame loop is expensive in proportion to how many screenshots it took.
30
+ */
31
+ export declare function formatTiming(timing: RecordingTiming): string;
@@ -0,0 +1,29 @@
1
+ export function emptyTiming() {
2
+ return {
3
+ openMs: 0,
4
+ settleMs: 0,
5
+ framesMs: 0,
6
+ writeMs: 0,
7
+ captured: 0,
8
+ skipped: 0,
9
+ };
10
+ }
11
+ const secs = (ms) => `${(ms / 1000).toFixed(1)}s`;
12
+ /**
13
+ * One line saying where the next second would have to come from.
14
+ *
15
+ * The capture counts ride along with the frame bucket because they explain it:
16
+ * a frame loop is expensive in proportion to how many screenshots it took.
17
+ */
18
+ export function formatTiming(timing) {
19
+ const frames = timing.skipped > 0
20
+ ? `frames ${secs(timing.framesMs)} (${timing.captured} captured, ${timing.skipped} skipped)`
21
+ : `frames ${secs(timing.framesMs)} (${timing.captured} captured)`;
22
+ return [
23
+ `open ${secs(timing.openMs)}`,
24
+ `settles ${secs(timing.settleMs)}`,
25
+ frames,
26
+ `write ${secs(timing.writeMs)}`,
27
+ ].join(" · ");
28
+ }
29
+ //# sourceMappingURL=timing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timing.js","sourceRoot":"","sources":["../../src/core/timing.ts"],"names":[],"mappings":"AAwBA,MAAM,UAAU,WAAW;IACzB,OAAO;QACL,MAAM,EAAE,CAAC;QACT,QAAQ,EAAE,CAAC;QACX,QAAQ,EAAE,CAAC;QACX,OAAO,EAAE,CAAC;QACV,QAAQ,EAAE,CAAC;QACX,OAAO,EAAE,CAAC;KACX,CAAC;AACJ,CAAC;AAED,MAAM,IAAI,GAAG,CAAC,EAAU,EAAU,EAAE,CAAC,GAAG,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC;AAElE;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,MAAuB;IAClD,MAAM,MAAM,GACV,MAAM,CAAC,OAAO,GAAG,CAAC;QAChB,CAAC,CAAC,UAAU,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,MAAM,CAAC,QAAQ,cAAc,MAAM,CAAC,OAAO,WAAW;QAC5F,CAAC,CAAC,UAAU,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,MAAM,CAAC,QAAQ,YAAY,CAAC;IACtE,OAAO;QACL,QAAQ,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE;QAC7B,WAAW,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE;QAClC,MAAM;QACN,SAAS,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE;KAChC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAChB,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * When each character of `text` gets typed, in milliseconds from the start.
3
+ *
4
+ * Constant-rate typing is one of the strongest tells that a demo is scripted.
5
+ * Real typing varies per keystroke and pauses at word and clause boundaries,
6
+ * so the pause goes *after* a space or a comma, where a person's hands
7
+ * actually hesitate. No fake typos: they read as a gimmick on second viewing
8
+ * and make the clip impossible to re-cut.
9
+ */
10
+ export declare function typingSchedule(text: string, rng: () => number): number[];
11
+ /** Total time `typingSchedule` spans, in milliseconds. */
12
+ export declare function typingDuration(times: number[]): number;
@@ -0,0 +1,35 @@
1
+ import { jitter } from "./rng.js";
2
+ const BASE_MS = 55;
3
+ const SPREAD_MS = 25;
4
+ const AFTER_SPACE_MS = 80;
5
+ const AFTER_PUNCTUATION_MS = 160;
6
+ const PUNCTUATION = new Set([".", ",", "?", "!", ":", ";"]);
7
+ /**
8
+ * When each character of `text` gets typed, in milliseconds from the start.
9
+ *
10
+ * Constant-rate typing is one of the strongest tells that a demo is scripted.
11
+ * Real typing varies per keystroke and pauses at word and clause boundaries,
12
+ * so the pause goes *after* a space or a comma, where a person's hands
13
+ * actually hesitate. No fake typos: they read as a gimmick on second viewing
14
+ * and make the clip impossible to re-cut.
15
+ */
16
+ export function typingSchedule(text, rng) {
17
+ const times = [];
18
+ let t = 0;
19
+ for (let i = 0; i < text.length; i += 1) {
20
+ const previous = i > 0 ? text[i - 1] : "";
21
+ let extra = 0;
22
+ if (previous === " ")
23
+ extra = AFTER_SPACE_MS;
24
+ else if (PUNCTUATION.has(previous))
25
+ extra = AFTER_PUNCTUATION_MS;
26
+ t += Math.max(10, BASE_MS + jitter(rng, SPREAD_MS)) + extra;
27
+ times.push(t);
28
+ }
29
+ return times;
30
+ }
31
+ /** Total time `typingSchedule` spans, in milliseconds. */
32
+ export function typingDuration(times) {
33
+ return times.length === 0 ? 0 : times[times.length - 1];
34
+ }
35
+ //# sourceMappingURL=typing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typing.js","sourceRoot":"","sources":["../../src/core/typing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAElC,MAAM,OAAO,GAAG,EAAE,CAAC;AACnB,MAAM,SAAS,GAAG,EAAE,CAAC;AACrB,MAAM,cAAc,GAAG,EAAE,CAAC;AAC1B,MAAM,oBAAoB,GAAG,GAAG,CAAC;AAEjC,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;AAE5D;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,GAAiB;IAC5D,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACxC,MAAM,QAAQ,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1C,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,IAAI,QAAQ,KAAK,GAAG;YAAE,KAAK,GAAG,cAAc,CAAC;aACxC,IAAI,WAAW,CAAC,GAAG,CAAC,QAAQ,CAAC;YAAE,KAAK,GAAG,oBAAoB,CAAC;QAEjE,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,OAAO,GAAG,MAAM,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC,GAAG,KAAK,CAAC;QAC5D,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAChB,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,cAAc,CAAC,KAAe;IAC5C,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AAC1D,CAAC"}
@@ -0,0 +1,71 @@
1
+ import type { Browser } from "playwright";
2
+ import type { CaptureConfig, ResolvedConfig } from "../config.js";
3
+ import type { Timeline } from "../core/timeline.js";
4
+ import type { RecordingTiming } from "../core/timing.js";
5
+ import type { SettleReport } from "./settle.js";
6
+ import { RecordingSession } from "./session.js";
7
+ export type ClipOptions = {
8
+ /** Shown as the clip's caption, unless the body sets its own. */
9
+ title: string;
10
+ /** Per-clip overrides of the config, for a clip that needs a different shape. */
11
+ viewport?: {
12
+ width: number;
13
+ height: number;
14
+ };
15
+ dpr?: number;
16
+ fps?: number;
17
+ /** Seeds the jitter in cursor paths, dwells and typing. */
18
+ seed?: number;
19
+ /**
20
+ * Per-clip capture overrides. A clip that films a canvas sets
21
+ * `forceCaptureEvery: 1` here rather than making every other clip pay.
22
+ */
23
+ capture?: CaptureConfig;
24
+ };
25
+ export type ClipBody = (session: RecordingSession) => Promise<void>;
26
+ export type ClipDefinition = {
27
+ id: string;
28
+ options: ClipOptions;
29
+ body: ClipBody;
30
+ };
31
+ /**
32
+ * Declare a clip.
33
+ *
34
+ * This only describes one — running it is `runClip`, which the CLI does. The
35
+ * prototype registered a Playwright test here instead, which worked but meant
36
+ * every way of recording had to go through a test runner.
37
+ */
38
+ export declare function clip(id: string, options: ClipOptions, body: ClipBody): ClipDefinition;
39
+ export type RunClipResult = {
40
+ timeline: Timeline;
41
+ dir: string;
42
+ /** What it cost in real time. Diagnostics; never part of the recording. */
43
+ cost: RecordingTiming;
44
+ /**
45
+ * Every settle, in order. The same list `settles.json` holds, for a caller
46
+ * that would otherwise read back the file it just caused to be written.
47
+ */
48
+ settles: SettleReport[];
49
+ };
50
+ /**
51
+ * Record one clip.
52
+ *
53
+ * A fresh context each time, with the fake clock paused before any navigation
54
+ * so the app's own boot timers are fake from the first tick.
55
+ */
56
+ export type RunClipOptions = {
57
+ /**
58
+ * Record cheaply, for looking at while writing the clip.
59
+ *
60
+ * Fifteen frames a second instead of sixty, captured at CSS scale instead of
61
+ * device scale — at 1440×900 on a retina context that is 25 ms a frame
62
+ * rather than 67 ms, for a quarter as many frames.
63
+ *
64
+ * What it deliberately does *not* change is the page. The context keeps its
65
+ * device pixel ratio and its viewport, so the app lays out identically and
66
+ * runs the same code; a draft that reduced the DPR could pass while the full
67
+ * capture it stands in for fails on a media query.
68
+ */
69
+ draft?: boolean;
70
+ };
71
+ export declare function runClip(browser: Browser, config: ResolvedConfig, definition: ClipDefinition, runOptions?: RunClipOptions): Promise<RunClipResult>;