@anvpx/motion-scenes 0.0.0-stage → 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 (221) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +39 -2
  3. package/dist/catalog.d.ts +17 -0
  4. package/dist/catalog.js +51 -0
  5. package/dist/custom.d.ts +86 -0
  6. package/dist/custom.js +176 -0
  7. package/dist/define-scene.d.ts +31 -0
  8. package/dist/define-scene.js +128 -0
  9. package/dist/html.d.ts +5 -0
  10. package/dist/html.js +14 -0
  11. package/dist/index.d.ts +11 -0
  12. package/dist/index.js +11 -0
  13. package/dist/position.d.ts +21 -0
  14. package/dist/position.js +16 -0
  15. package/dist/registry.d.ts +4 -0
  16. package/dist/registry.js +68 -0
  17. package/dist/runtime/compose.d.ts +21 -0
  18. package/dist/runtime/compose.js +43 -0
  19. package/dist/runtime/easing.d.ts +14 -0
  20. package/dist/runtime/easing.js +45 -0
  21. package/dist/runtime/mount.d.ts +50 -0
  22. package/dist/runtime/mount.js +153 -0
  23. package/dist/runtime/timeline.d.ts +147 -0
  24. package/dist/runtime/timeline.js +199 -0
  25. package/dist/runtime-files.d.ts +4 -0
  26. package/dist/runtime-files.js +18 -0
  27. package/dist/scene-contract.d.ts +7 -0
  28. package/dist/scene-contract.js +104 -0
  29. package/dist/scene3d/post.d.ts +12 -0
  30. package/dist/scene3d/post.js +16 -0
  31. package/dist/scene3d/reflector.d.ts +9 -0
  32. package/dist/scene3d/reflector.js +17 -0
  33. package/dist/scene3d/textures.d.ts +9 -0
  34. package/dist/scene3d/textures.js +11 -0
  35. package/dist/scene3d/volumetric.d.ts +9 -0
  36. package/dist/scene3d/volumetric.js +16 -0
  37. package/dist/scenes/bug/index.d.ts +1 -0
  38. package/dist/scenes/bug/index.js +45 -0
  39. package/dist/scenes/bug/scene.d.ts +2 -0
  40. package/dist/scenes/bug/scene.js +6 -0
  41. package/dist/scenes/bug/style.css +30 -0
  42. package/dist/scenes/bullets/index.d.ts +1 -0
  43. package/dist/scenes/bullets/index.js +128 -0
  44. package/dist/scenes/bullets/scene.d.ts +2 -0
  45. package/dist/scenes/bullets/scene.js +6 -0
  46. package/dist/scenes/bullets/style.css +91 -0
  47. package/dist/scenes/callout/index.d.ts +9 -0
  48. package/dist/scenes/callout/index.js +95 -0
  49. package/dist/scenes/callout/scene.d.ts +2 -0
  50. package/dist/scenes/callout/scene.js +43 -0
  51. package/dist/scenes/callout/style.css +97 -0
  52. package/dist/scenes/caption/index.d.ts +1 -0
  53. package/dist/scenes/caption/index.js +72 -0
  54. package/dist/scenes/caption/scene.d.ts +11 -0
  55. package/dist/scenes/caption/scene.js +33 -0
  56. package/dist/scenes/caption/style.css +33 -0
  57. package/dist/scenes/chapter/index.d.ts +1 -0
  58. package/dist/scenes/chapter/index.js +29 -0
  59. package/dist/scenes/chapter/scene.d.ts +2 -0
  60. package/dist/scenes/chapter/scene.js +6 -0
  61. package/dist/scenes/chapter/style.css +25 -0
  62. package/dist/scenes/chart/index.d.ts +1 -0
  63. package/dist/scenes/chart/index.js +24 -0
  64. package/dist/scenes/chart/render.d.ts +4 -0
  65. package/dist/scenes/chart/render.js +257 -0
  66. package/dist/scenes/chart/scale.d.ts +11 -0
  67. package/dist/scenes/chart/scale.js +53 -0
  68. package/dist/scenes/chart/scene.d.ts +2 -0
  69. package/dist/scenes/chart/scene.js +73 -0
  70. package/dist/scenes/chart/schema.d.ts +36 -0
  71. package/dist/scenes/chart/schema.js +109 -0
  72. package/dist/scenes/chart/style.css +425 -0
  73. package/dist/scenes/code/index.d.ts +1 -0
  74. package/dist/scenes/code/index.js +109 -0
  75. package/dist/scenes/code/scene.d.ts +2 -0
  76. package/dist/scenes/code/scene.js +108 -0
  77. package/dist/scenes/code/style.css +155 -0
  78. package/dist/scenes/code/tokenize.d.ts +8 -0
  79. package/dist/scenes/code/tokenize.js +145 -0
  80. package/dist/scenes/compare/index.d.ts +1 -0
  81. package/dist/scenes/compare/index.js +128 -0
  82. package/dist/scenes/compare/scene.d.ts +2 -0
  83. package/dist/scenes/compare/scene.js +58 -0
  84. package/dist/scenes/compare/style.css +187 -0
  85. package/dist/scenes/countdown/index.d.ts +1 -0
  86. package/dist/scenes/countdown/index.js +46 -0
  87. package/dist/scenes/countdown/scene.d.ts +2 -0
  88. package/dist/scenes/countdown/scene.js +21 -0
  89. package/dist/scenes/countdown/style.css +32 -0
  90. package/dist/scenes/flow/index.d.ts +1 -0
  91. package/dist/scenes/flow/index.js +66 -0
  92. package/dist/scenes/flow/inspect.d.ts +26 -0
  93. package/dist/scenes/flow/inspect.js +25 -0
  94. package/dist/scenes/flow/layout.d.ts +83 -0
  95. package/dist/scenes/flow/layout.js +222 -0
  96. package/dist/scenes/flow/measure.d.ts +15 -0
  97. package/dist/scenes/flow/measure.js +33 -0
  98. package/dist/scenes/flow/plan.d.ts +42 -0
  99. package/dist/scenes/flow/plan.js +117 -0
  100. package/dist/scenes/flow/scene.d.ts +2 -0
  101. package/dist/scenes/flow/scene.js +6 -0
  102. package/dist/scenes/flow/schema.d.ts +49 -0
  103. package/dist/scenes/flow/schema.js +60 -0
  104. package/dist/scenes/flow/style.css +54 -0
  105. package/dist/scenes/hero/index.d.ts +3 -0
  106. package/dist/scenes/hero/index.js +77 -0
  107. package/dist/scenes/hero/scene.d.ts +2 -0
  108. package/dist/scenes/hero/scene.js +6 -0
  109. package/dist/scenes/hero/style.css +143 -0
  110. package/dist/scenes/icon-grid/index.d.ts +1 -0
  111. package/dist/scenes/icon-grid/index.js +69 -0
  112. package/dist/scenes/icon-grid/scene.d.ts +2 -0
  113. package/dist/scenes/icon-grid/scene.js +6 -0
  114. package/dist/scenes/icon-grid/style.css +74 -0
  115. package/dist/scenes/kinetic/index.d.ts +1 -0
  116. package/dist/scenes/kinetic/index.js +62 -0
  117. package/dist/scenes/kinetic/scene.d.ts +2 -0
  118. package/dist/scenes/kinetic/scene.js +98 -0
  119. package/dist/scenes/kinetic/style.css +94 -0
  120. package/dist/scenes/kpi-grid/index.d.ts +1 -0
  121. package/dist/scenes/kpi-grid/index.js +161 -0
  122. package/dist/scenes/kpi-grid/scene.d.ts +2 -0
  123. package/dist/scenes/kpi-grid/scene.js +6 -0
  124. package/dist/scenes/kpi-grid/style.css +141 -0
  125. package/dist/scenes/lower-third/index.d.ts +1 -0
  126. package/dist/scenes/lower-third/index.js +33 -0
  127. package/dist/scenes/lower-third/scene.d.ts +2 -0
  128. package/dist/scenes/lower-third/scene.js +6 -0
  129. package/dist/scenes/lower-third/style.css +45 -0
  130. package/dist/scenes/media-frame/index.d.ts +1 -0
  131. package/dist/scenes/media-frame/index.js +99 -0
  132. package/dist/scenes/media-frame/scene.d.ts +2 -0
  133. package/dist/scenes/media-frame/scene.js +85 -0
  134. package/dist/scenes/media-frame/style.css +275 -0
  135. package/dist/scenes/outro/index.d.ts +1 -0
  136. package/dist/scenes/outro/index.js +54 -0
  137. package/dist/scenes/outro/scene.d.ts +2 -0
  138. package/dist/scenes/outro/scene.js +6 -0
  139. package/dist/scenes/outro/style.css +92 -0
  140. package/dist/scenes/poster/index.d.ts +3 -0
  141. package/dist/scenes/poster/index.js +62 -0
  142. package/dist/scenes/poster/scene.d.ts +2 -0
  143. package/dist/scenes/poster/scene.js +6 -0
  144. package/dist/scenes/poster/style.css +102 -0
  145. package/dist/scenes/progress/index.d.ts +1 -0
  146. package/dist/scenes/progress/index.js +40 -0
  147. package/dist/scenes/progress/scene.d.ts +2 -0
  148. package/dist/scenes/progress/scene.js +17 -0
  149. package/dist/scenes/progress/style.css +31 -0
  150. package/dist/scenes/quote/index.d.ts +3 -0
  151. package/dist/scenes/quote/index.js +69 -0
  152. package/dist/scenes/quote/scene.d.ts +2 -0
  153. package/dist/scenes/quote/scene.js +6 -0
  154. package/dist/scenes/quote/style.css +86 -0
  155. package/dist/scenes/scene3d/index.d.ts +1 -0
  156. package/dist/scenes/scene3d/index.js +59 -0
  157. package/dist/scenes/scene3d/scene.d.ts +2 -0
  158. package/dist/scenes/scene3d/scene.js +436 -0
  159. package/dist/scenes/scene3d/style.css +43 -0
  160. package/dist/scenes/stat/index.d.ts +4 -0
  161. package/dist/scenes/stat/index.js +72 -0
  162. package/dist/scenes/stat/scene.d.ts +2 -0
  163. package/dist/scenes/stat/scene.js +6 -0
  164. package/dist/scenes/stat/style.css +95 -0
  165. package/dist/scenes/steps/index.d.ts +1 -0
  166. package/dist/scenes/steps/index.js +147 -0
  167. package/dist/scenes/steps/scene.d.ts +2 -0
  168. package/dist/scenes/steps/scene.js +6 -0
  169. package/dist/scenes/steps/style.css +190 -0
  170. package/dist/scenes/sticker/index.d.ts +1 -0
  171. package/dist/scenes/sticker/index.js +48 -0
  172. package/dist/scenes/sticker/scene.d.ts +2 -0
  173. package/dist/scenes/sticker/scene.js +57 -0
  174. package/dist/scenes/sticker/style.css +88 -0
  175. package/dist/scenes/table/index.d.ts +1 -0
  176. package/dist/scenes/table/index.js +131 -0
  177. package/dist/scenes/table/scene.d.ts +2 -0
  178. package/dist/scenes/table/scene.js +6 -0
  179. package/dist/scenes/table/style.css +102 -0
  180. package/dist/scenes/timeline/index.d.ts +1 -0
  181. package/dist/scenes/timeline/index.js +68 -0
  182. package/dist/scenes/timeline/scene.d.ts +2 -0
  183. package/dist/scenes/timeline/scene.js +6 -0
  184. package/dist/scenes/timeline/style.css +161 -0
  185. package/dist/scenes/title/index.d.ts +1 -0
  186. package/dist/scenes/title/index.js +52 -0
  187. package/dist/scenes/title/scene.d.ts +2 -0
  188. package/dist/scenes/title/scene.js +6 -0
  189. package/dist/scenes/title/style.css +80 -0
  190. package/dist/scenes/waveform/index.d.ts +1 -0
  191. package/dist/scenes/waveform/index.js +39 -0
  192. package/dist/scenes/waveform/scene.d.ts +2 -0
  193. package/dist/scenes/waveform/scene.js +35 -0
  194. package/dist/scenes/waveform/style.css +44 -0
  195. package/dist/shared/fit-text.d.ts +24 -0
  196. package/dist/shared/fit-text.js +82 -0
  197. package/dist/shared/format.d.ts +24 -0
  198. package/dist/shared/format.js +59 -0
  199. package/dist/shared/frame.d.ts +14 -0
  200. package/dist/shared/frame.js +21 -0
  201. package/dist/shared/icon-search.d.ts +9 -0
  202. package/dist/shared/icon-search.js +26 -0
  203. package/dist/shared/icon.d.ts +15 -0
  204. package/dist/shared/icon.js +28 -0
  205. package/dist/shared/index.d.ts +11 -0
  206. package/dist/shared/index.js +7 -0
  207. package/dist/shared/markup.d.ts +12 -0
  208. package/dist/shared/markup.js +43 -0
  209. package/dist/shared/measure.d.ts +7 -0
  210. package/dist/shared/measure.js +34 -0
  211. package/dist/shared/motion.d.ts +94 -0
  212. package/dist/shared/motion.js +117 -0
  213. package/dist/source-hash.d.ts +4 -0
  214. package/dist/source-hash.js +26 -0
  215. package/dist/test-fixtures.d.ts +9 -0
  216. package/dist/test-fixtures.js +22 -0
  217. package/dist/text-file.d.ts +1 -0
  218. package/dist/text-file.js +3 -0
  219. package/dist/words.d.ts +11 -0
  220. package/dist/words.js +93 -0
  221. package/package.json +36 -3
@@ -0,0 +1,199 @@
1
+ // @ts-check
2
+ // Pure timeline core: every value is a function of (inputs, t) only.
3
+ import { clamp01, ease } from "./easing.js";
4
+ /** @typedef {{ from: number, to: number, start: number, end: number, ease: string }} Track */
5
+ /** @typedef {{ baseDelay: number, staggerFrames: number, rise: number }} Plan */
6
+ /** @typedef {{ opacity: number, y: number, dash: number, scale: number }} EntranceState */
7
+ export const ENTRANCE_SECONDS = 0.6;
8
+ export const MIN_STAGGER_FRAMES = 3;
9
+ export const MAX_STAGGER_FRAMES = 6;
10
+ export const COUNT_SECONDS = 1.4;
11
+ /** Share of the scene a count-up may take, and its ceiling in seconds. */
12
+ export const COUNT_SHARE = 0.25;
13
+ export const MAX_COUNT_SECONDS = 3;
14
+ /** Opacity of a "rise" element at progress 0, so no scene opens on a blank frame. */
15
+ export const RISE_FLOOR = 0.45;
16
+ /** Size of a counting number at progress 0, relative to its final size. */
17
+ export const COUNT_SCALE_FROM = 0.7;
18
+ /** Fade-in and fade-out length of an overlay window, in seconds. */
19
+ export const ENVELOPE_SECONDS = 0.4;
20
+ /** Extra size of a countdown number at the start of each step. */
21
+ export const PULSE_SCALE = 0.25;
22
+ const PULSE_SHARE = 0.4;
23
+ const DRIFT_RATE = 0.35;
24
+ const GROUP_SIZE = /\B(?=(\d{3})+(?!\d))/g;
25
+ const LEADING_NUMBER = /^(\D*?)(\d[\d,]*(?:\.\d+)?)(\D*)$/;
26
+ const DIM_OPACITY = 0.3;
27
+ const MOVE_DELTA = 0.04;
28
+ /**
29
+ * Value of a single-segment track at time t (seconds).
30
+ * @param {Track} track
31
+ * @param {number} t
32
+ * @returns {number}
33
+ */
34
+ export const sample = (track, t) => {
35
+ const span = track.end - track.start;
36
+ const progress = span > 0 ? clamp01((t - track.start) / span) : Number(t >= track.end);
37
+ return track.from + (track.to - track.from) * ease(track.ease, progress);
38
+ };
39
+ /**
40
+ * Stagger between consecutive steps, in seconds, from a frame count.
41
+ * @param {number} frames 3-6
42
+ * @param {number} fps
43
+ * @returns {number}
44
+ */
45
+ export const staggerSeconds = (frames, fps) => {
46
+ if (frames < MIN_STAGGER_FRAMES || frames > MAX_STAGGER_FRAMES) {
47
+ throw new RangeError(`stagger must be ${MIN_STAGGER_FRAMES}-${MAX_STAGGER_FRAMES} frames`);
48
+ }
49
+ return frames / fps;
50
+ };
51
+ /**
52
+ * Progress track (0 -> 1) of the entrance of step `step`.
53
+ * @param {number} step
54
+ * @param {Plan} plan
55
+ * @param {number} fps
56
+ * @param {string} easing
57
+ * @returns {Track}
58
+ */
59
+ export const entranceTrack = (step, plan, fps, easing) => {
60
+ const start = plan.baseDelay + step * staggerSeconds(plan.staggerFrames, fps);
61
+ return { from: 0, to: 1, start, end: start + ENTRANCE_SECONDS, ease: easing };
62
+ };
63
+ /**
64
+ * @param {string} kind
65
+ * @param {number} p
66
+ * @returns {number}
67
+ */
68
+ const entranceOpacity = (kind, p) => {
69
+ if (kind === "draw")
70
+ return 1;
71
+ return kind === "rise" ? RISE_FLOOR + (1 - RISE_FLOOR) * clamp01(p) : clamp01(p);
72
+ };
73
+ /**
74
+ * Visual state of an element kind at entrance progress p.
75
+ * @param {string} kind rise | fade | draw | grow
76
+ * @param {number} p progress; may overshoot for springs
77
+ * @param {number} rise rise distance in px
78
+ * @returns {EntranceState}
79
+ */
80
+ export const entranceState = (kind, p, rise) => ({
81
+ opacity: entranceOpacity(kind, p),
82
+ y: kind === "rise" ? (1 - p) * rise : 0,
83
+ dash: 1 - p,
84
+ scale: kind === "grow" ? clamp01(p) : 1,
85
+ });
86
+ /**
87
+ * Count-up length: COUNT_SECONDS, stretched to a quarter of a long scene (max MAX_COUNT_SECONDS).
88
+ * @param {number} duration scene seconds; 0 or unknown keeps COUNT_SECONDS
89
+ * @returns {number}
90
+ */
91
+ export const countSeconds = (duration) => Math.min(MAX_COUNT_SECONDS, Math.max(COUNT_SECONDS, duration * COUNT_SHARE));
92
+ /**
93
+ * Track of a count-up that starts with the entrance of `step`.
94
+ * @param {number} step
95
+ * @param {Plan} plan
96
+ * @param {number} fps
97
+ * @param {string} easing
98
+ * @param {number} [duration] scene seconds
99
+ * @returns {Track}
100
+ */
101
+ export const countTrack = (step, plan, fps, easing, duration = 0) => {
102
+ const { start } = entranceTrack(step, plan, fps, easing);
103
+ return { from: 0, to: 1, start, end: start + countSeconds(duration), ease: easing };
104
+ };
105
+ /**
106
+ * Scale of a counting number at count progress p: grows from COUNT_SCALE_FROM to 1.
107
+ * @param {number} p
108
+ * @returns {number}
109
+ */
110
+ export const countScale = (p) => COUNT_SCALE_FROM + (1 - COUNT_SCALE_FROM) * clamp01(p);
111
+ /** @param {string} digits */
112
+ const withGrouping = (digits) => digits.replace(GROUP_SIZE, ",");
113
+ /**
114
+ * The first number of `text` scaled to progress p, keeping prefix, suffix, decimals and
115
+ * thousands separators; p >= 1 and text without one leading number return `text` verbatim.
116
+ * @param {string} text
117
+ * @param {number} p
118
+ * @returns {string}
119
+ */
120
+ export const countText = (text, p) => {
121
+ const match = LEADING_NUMBER.exec(text);
122
+ if (!match || p >= 1)
123
+ return text;
124
+ const [, prefix = "", number = "", suffix = ""] = match;
125
+ const decimals = number.includes(".") ? number.length - number.indexOf(".") - 1 : 0;
126
+ const [whole = "", fraction] = (parseFloat(number.replaceAll(",", "")) * clamp01(p))
127
+ .toFixed(decimals)
128
+ .split(".");
129
+ const grouped = number.includes(",") ? withGrouping(whole) : whole;
130
+ return `${prefix}${fraction === undefined ? grouped : `${grouped}.${fraction}`}${suffix}`;
131
+ };
132
+ /**
133
+ * Slow background drift in [-1, 1]: one cycle every ~18 s.
134
+ * @param {number} t seconds
135
+ * @returns {number}
136
+ */
137
+ export const driftOf = (t) => Math.sin(t * DRIFT_RATE);
138
+ /**
139
+ * Root scale of the supported camera moves; other moves are applied by the build.
140
+ * @param {string} move
141
+ * @param {number} t
142
+ * @param {number} duration
143
+ * @returns {number}
144
+ */
145
+ export const cameraScale = (move, t, duration) => {
146
+ const progress = duration > 0 ? clamp01(t / duration) : 0;
147
+ if (move === "push-in")
148
+ return 1 + MOVE_DELTA * progress;
149
+ if (move === "pull-out")
150
+ return 1 + MOVE_DELTA * (1 - progress);
151
+ return 1;
152
+ };
153
+ /**
154
+ * Opacity multiplier of `focus`: elements outside the focus dim.
155
+ * @param {string} focus focused key; empty means no focus
156
+ * @param {string} keys space-separated keys of the element; empty means never dim
157
+ * @returns {number}
158
+ */
159
+ export const dimOpacity = (focus, keys) => !focus || !keys || keys.split(" ").includes(focus) ? 1 : DIM_OPACITY;
160
+ /**
161
+ * Opacity of an overlay at local time t: eased fade in, hold, eased fade out.
162
+ * @param {number} t seconds from the start of the overlay window
163
+ * @param {number} duration window length in seconds
164
+ * @param {string} easing
165
+ * @returns {number}
166
+ */
167
+ export const overlayEnvelope = (t, duration, easing) => {
168
+ const edge = Math.min(ENVELOPE_SECONDS, duration / 2);
169
+ if (!(edge > 0))
170
+ return 1;
171
+ return clamp01(Math.min(ease(easing, t / edge), ease(easing, (duration - t) / edge)));
172
+ };
173
+ /**
174
+ * Countdown value and pulse scale at local time t: one equal step per integer from -> to.
175
+ * @param {number} t
176
+ * @param {number} duration
177
+ * @param {number} from
178
+ * @param {number} to
179
+ * @returns {{ value: number, scale: number }}
180
+ */
181
+ export const countdownState = (t, duration, from, to) => {
182
+ const steps = Math.abs(to - from) + 1;
183
+ const length = duration / steps;
184
+ const index = Math.min(steps - 1, Math.floor(clamp01(t / duration) * steps));
185
+ const local = length > 0 ? (t - index * length) / length : 1;
186
+ return {
187
+ value: from + Math.sign(to - from) * index,
188
+ scale: 1 + PULSE_SCALE * (1 - ease("out", local / PULSE_SHARE)),
189
+ };
190
+ };
191
+ /**
192
+ * Linear value from -> to across the duration (progress bars).
193
+ * @param {number} t
194
+ * @param {number} duration
195
+ * @param {number} from
196
+ * @param {number} to
197
+ * @returns {number}
198
+ */
199
+ export const spanProgress = (t, duration, from, to) => from + (to - from) * (duration > 0 ? clamp01(t / duration) : 1);
@@ -0,0 +1,4 @@
1
+ /** True only for a browser module that this package emits; does not read disk. */
2
+ export declare const isSharedModulePath: (path: string) => boolean;
3
+ /** Shared browser modules; the build emits them once under `runtime/` and `shared/`. */
4
+ export declare const runtimeFiles: () => Map<string, string>;
@@ -0,0 +1,18 @@
1
+ import { readText } from "./text-file.js";
2
+ const RUNTIME_MODULES = ["easing.js", "timeline.js", "mount.js", "compose.js"];
3
+ /** Pure helpers shared with emit time; they import `../runtime/easing.js`, so they sit beside it. */
4
+ const SHARED_MODULES = ["motion.js", "format.js"];
5
+ /** True only for a browser module that this package emits; does not read disk. */
6
+ export const isSharedModulePath = (path) => RUNTIME_MODULES.some((name) => path === `runtime/${name}`) ||
7
+ SHARED_MODULES.some((name) => path === `shared/${name}`);
8
+ /** Shared browser modules; the build emits them once under `runtime/` and `shared/`. */
9
+ export const runtimeFiles = () => new Map([
10
+ ...RUNTIME_MODULES.map((name) => [
11
+ `runtime/${name}`,
12
+ readText(`./runtime/${name}`, import.meta.url),
13
+ ]),
14
+ ...SHARED_MODULES.map((name) => [
15
+ `shared/${name}`,
16
+ readText(`./shared/${name}`, import.meta.url),
17
+ ]),
18
+ ]);
@@ -0,0 +1,7 @@
1
+ import type { ScenePlugin } from "@anvpx/motion-schema";
2
+ export interface ContractFixture {
3
+ readonly valid: Record<string, unknown>;
4
+ /** Valid props whose every text field contains markup and quotes. */
5
+ readonly hostile: Record<string, unknown>;
6
+ }
7
+ export declare const describeSceneContract: (plugin: ScenePlugin, fixture: ContractFixture) => void;
@@ -0,0 +1,104 @@
1
+ import { resolveTheme, THEME_NAMES, toCssVars } from "@anvpx/motion-themes";
2
+ import { describe, expect, it } from "vitest";
3
+ import { runtimeFiles } from "./runtime-files.js";
4
+ import { emitContext, entryOf } from "./test-fixtures.js";
5
+ const MIN_TEXT_PX = 28;
6
+ const HEX_COLOR = /#[0-9a-fA-F]{3,8}\b/;
7
+ const FUNCTION_COLOR = /\b(?:rgb|rgba|hsl|hsla)\(/;
8
+ const files = (plugin, props, extra = {}) => {
9
+ const id = `${plugin.type}-1`;
10
+ const out = plugin.emit(entryOf(plugin.type, props, extra), emitContext());
11
+ const get = (name) => out.get(`scenes/${id}/${name}`) ?? "";
12
+ return { id, out, html: get("fragment.html"), css: get("style.css"), js: get("scene.js") };
13
+ };
14
+ const declared = (css) => new Set([...css.matchAll(/(--mk-[\w-]+):/g)].map((m) => m[1] ?? ""));
15
+ const referenced = (css) => [...css.matchAll(/var\((--mk-[\w-]+)\)/g)].map((m) => m[1] ?? "");
16
+ const themeVars = (name) => declared(toCssVars(resolveTheme(name)));
17
+ export const describeSceneContract = (plugin, fixture) => {
18
+ describe(`${plugin.type} emit contract`, () => {
19
+ it("emits fragment, style and script under scenes/<id>/", () => {
20
+ const { out, id } = files(plugin, fixture.valid);
21
+ expect([...out.keys()]).toEqual([
22
+ `scenes/${id}/fragment.html`,
23
+ `scenes/${id}/style.css`,
24
+ `scenes/${id}/scene.js`,
25
+ ]);
26
+ });
27
+ it("fragment is one root element with id scene-<id> and no clip class", () => {
28
+ const { html, id } = files(plugin, fixture.valid);
29
+ expect(html.startsWith(`<section id="scene-${id}" `)).toBe(true);
30
+ expect(html.trimEnd().endsWith("</section>")).toBe(true);
31
+ expect(html.match(/<section/g)).toHaveLength(1);
32
+ expect(html).not.toMatch(/class="[^"]*\bclip\b/);
33
+ });
34
+ it("matches the golden output", () => {
35
+ const { out } = files(plugin, fixture.valid, { move: "push-in", focus: "title" });
36
+ const text = [...out].map(([path, body]) => `=== ${path}\n${body}`).join("\n");
37
+ expect(text).toMatchSnapshot();
38
+ });
39
+ it("is byte-identical when run twice", () => {
40
+ expect([...files(plugin, fixture.valid).out]).toEqual([...files(plugin, fixture.valid).out]);
41
+ });
42
+ it("escapes all prop text", () => {
43
+ const { html } = files(plugin, fixture.hostile);
44
+ expect(html).not.toContain("<script");
45
+ expect(html).toContain("&lt;script&gt;");
46
+ expect(html).toContain("&quot;");
47
+ });
48
+ it("has no inline handlers or external urls", () => {
49
+ const { html, css, js } = files(plugin, fixture.valid);
50
+ expect(html + css + js).not.toMatch(/\son[a-z]+\s*=|https?:\/\/|javascript:/);
51
+ });
52
+ it("css uses only theme custom properties and no hard-coded colours", () => {
53
+ const { css } = files(plugin, fixture.valid);
54
+ expect(css).not.toMatch(HEX_COLOR);
55
+ expect(css).not.toMatch(FUNCTION_COLOR);
56
+ for (const theme of THEME_NAMES) {
57
+ const defined = themeVars(theme);
58
+ for (const name of referenced(css))
59
+ expect(defined.has(name), `${theme} ${name}`).toBe(true);
60
+ }
61
+ });
62
+ it("text is >= 28px and uses one theme font family", () => {
63
+ const { css } = files(plugin, fixture.valid);
64
+ const sizes = [...css.matchAll(/(?<![-\w])font-size:\s*([^;]+);/g)].map((m) => m[1] ?? "");
65
+ expect(sizes.length).toBeGreaterThan(0);
66
+ for (const size of sizes) {
67
+ const step = /^var\(--mk-font-size-(\w+)\)$/.exec(size)?.[1] ?? "";
68
+ const px = emitContext().theme.fontSizes[step] ?? 0;
69
+ expect(px, size).toBeGreaterThanOrEqual(MIN_TEXT_PX);
70
+ }
71
+ const families = [...css.matchAll(/font-family:\s*([^;]+);/g)].map((m) => m[1]);
72
+ // Sans everywhere; code-like scenes may add the theme mono stack.
73
+ const allowed = new Set([
74
+ "var(--mk-font-sans)",
75
+ "var(--mk-font-mono)",
76
+ "var(--mk-font-display, var(--mk-font-sans))",
77
+ ]);
78
+ expect(families.every((family) => allowed.has(family ?? ""))).toBe(true);
79
+ expect(families).toContain("var(--mk-font-sans)");
80
+ });
81
+ it("keeps the safe margin from the context size", () => {
82
+ expect(files(plugin, fixture.valid).css).toContain("padding: 54px 96px;");
83
+ });
84
+ it("carries ease (default out), fps and duration on the root", () => {
85
+ const { html } = files(plugin, fixture.valid);
86
+ expect(html).toContain('data-ease="out"');
87
+ expect(html).toContain('data-fps="30"');
88
+ expect(html).toContain('data-duration="6"');
89
+ expect(files(plugin, fixture.valid, { ease: "in-out" }).html).toContain('data-ease="in-out"');
90
+ });
91
+ it("script is an ES module importing only emitted runtime files", () => {
92
+ const { js } = files(plugin, fixture.valid);
93
+ expect(js).toMatch(/export function createScene\(root, config, ctx\)/);
94
+ const runtime = runtimeFiles();
95
+ const imports = [...js.matchAll(/from "\.\.\/\.\.\/(runtime\/[\w-]+\.js)"/g)];
96
+ expect(imports.length).toBeGreaterThan(0);
97
+ for (const match of imports)
98
+ expect(runtime.has(match[1] ?? "")).toBe(true);
99
+ });
100
+ it("rejects invalid props on emit", () => {
101
+ expect(() => plugin.emit(entryOf(plugin.type, { bogus: 1 }), emitContext())).toThrow();
102
+ });
103
+ });
104
+ };
@@ -0,0 +1,12 @@
1
+ export declare const POST_PRESETS: readonly ["clean", "bloom", "cinematic", "vintage"];
2
+ export type PostPreset = (typeof POST_PRESETS)[number];
3
+ export interface PostParams {
4
+ readonly bloom: number;
5
+ readonly grain: number;
6
+ readonly vignette: number;
7
+ readonly toneMapping: "none" | "agx";
8
+ }
9
+ export declare const isPostPreset: (value: unknown) => value is PostPreset;
10
+ export declare const resolvePost: (preset: PostPreset) => PostParams;
11
+ /** Seeded film-grain frame: pure function of t (30 fps lattice, spike receipt). */
12
+ export declare const grainSeed: (t: number) => number;
@@ -0,0 +1,16 @@
1
+ // Clean-room post chain for the scene3d base scene.
2
+ // MIT attribution: three.js (MIT), postprocessing (MIT). Behaviour reimplemented
3
+ // from projects/spike-scene3d/src/main.js (do not copy); diorama concept follows
4
+ // MIT-licensed three.js examples (e.g. RoomEnvironment). No CDN, no copied code.
5
+ // Curated post presets: small, spike-proven combos (bloom / AgX / vignette / grain).
6
+ export const POST_PRESETS = ["clean", "bloom", "cinematic", "vintage"];
7
+ const POST_TABLE = {
8
+ clean: { bloom: 0, grain: 0.01, vignette: 0, toneMapping: "none" },
9
+ bloom: { bloom: 1.1, grain: 0.02, vignette: 0.3, toneMapping: "none" },
10
+ cinematic: { bloom: 1.1, grain: 0.035, vignette: 0.55, toneMapping: "agx" },
11
+ vintage: { bloom: 0.6, grain: 0.06, vignette: 0.7, toneMapping: "agx" },
12
+ };
13
+ export const isPostPreset = (value) => typeof value === "string" && POST_PRESETS.includes(value);
14
+ export const resolvePost = (preset) => POST_TABLE[preset];
15
+ /** Seeded film-grain frame: pure function of t (30 fps lattice, spike receipt). */
16
+ export const grainSeed = (t) => Math.floor(t * 30) % 1000;
@@ -0,0 +1,9 @@
1
+ export declare const REFLECTOR_PRESETS: readonly ["off", "gloss", "mirror"];
2
+ export type ReflectorPreset = (typeof REFLECTOR_PRESETS)[number];
3
+ export interface ReflectorParams {
4
+ readonly reflectivity: number;
5
+ readonly roughness: number;
6
+ }
7
+ export declare const resolveReflector: (preset: ReflectorPreset) => ReflectorParams;
8
+ /** Fresnel-ish mix: 0 when off, otherwise reflectivity faded by surface roughness. */
9
+ export declare const reflectorMix: (roughness: number, preset: ReflectorPreset) => number;
@@ -0,0 +1,17 @@
1
+ // Clean-room planar reflector preset for scene3d (MIT: three.js).
2
+ // Pure math; no DOM, no clock.
3
+ export const REFLECTOR_PRESETS = ["off", "gloss", "mirror"];
4
+ const TABLE = {
5
+ off: { reflectivity: 0, roughness: 1 },
6
+ gloss: { reflectivity: 0.4, roughness: 0.35 },
7
+ mirror: { reflectivity: 0.9, roughness: 0.05 },
8
+ };
9
+ export const resolveReflector = (preset) => TABLE[preset];
10
+ /** Fresnel-ish mix: 0 when off, otherwise reflectivity faded by surface roughness. */
11
+ export const reflectorMix = (roughness, preset) => {
12
+ const { reflectivity } = TABLE[preset];
13
+ if (reflectivity === 0)
14
+ return 0;
15
+ const clamped = Math.min(1, Math.max(0, roughness));
16
+ return Math.min(1, reflectivity * (1 - clamped));
17
+ };
@@ -0,0 +1,9 @@
1
+ export declare const HDRI_PRESETS: readonly ["neutral", "studio", "sunset", "night"];
2
+ export type HdriPreset = (typeof HDRI_PRESETS)[number];
3
+ export interface HdriParams {
4
+ readonly intensity: number;
5
+ readonly background: number;
6
+ readonly exposure: number;
7
+ }
8
+ export declare const resolveHdri: (preset: HdriPreset) => HdriParams;
9
+ export declare const hdriEnv: (preset: HdriPreset) => HdriParams;
@@ -0,0 +1,11 @@
1
+ // Clean-room procedural environment for scene3d (MIT: three.js incl. RoomEnvironment).
2
+ // No fetches, no HDRI downloads: curated studio gradients resolved offline.
3
+ export const HDRI_PRESETS = ["neutral", "studio", "sunset", "night"];
4
+ const TABLE = {
5
+ neutral: { intensity: 0.6, background: 0x0a0c12, exposure: 1.0 },
6
+ studio: { intensity: 1.2, background: 0x111318, exposure: 1.1 },
7
+ sunset: { intensity: 0.9, background: 0x1a1210, exposure: 1.0 },
8
+ night: { intensity: 0.4, background: 0x05070c, exposure: 0.9 },
9
+ };
10
+ export const resolveHdri = (preset) => TABLE[preset];
11
+ export const hdriEnv = (preset) => TABLE[preset];
@@ -0,0 +1,9 @@
1
+ export declare const VOLUMETRIC_PRESETS: readonly ["off", "soft", "dense"];
2
+ export type VolumetricPreset = (typeof VOLUMETRIC_PRESETS)[number];
3
+ export interface VolumetricParams {
4
+ readonly density: number;
5
+ readonly steps: number;
6
+ }
7
+ export declare const resolveVolumetric: (preset: VolumetricPreset) => VolumetricParams;
8
+ /** Gentle breathing of the shaft: deterministic in t, zero when off. */
9
+ export declare const volumetricIntensity: (t: number, preset: VolumetricPreset) => number;
@@ -0,0 +1,16 @@
1
+ // Clean-room volumetric preset for scene3d (MIT: three.js, postprocessing).
2
+ // Pure function of t; no clock, no random.
3
+ export const VOLUMETRIC_PRESETS = ["off", "soft", "dense"];
4
+ const TABLE = {
5
+ off: { density: 0, steps: 0 },
6
+ soft: { density: 0.35, steps: 32 },
7
+ dense: { density: 0.7, steps: 64 },
8
+ };
9
+ export const resolveVolumetric = (preset) => TABLE[preset];
10
+ /** Gentle breathing of the shaft: deterministic in t, zero when off. */
11
+ export const volumetricIntensity = (t, preset) => {
12
+ const { density } = TABLE[preset];
13
+ if (density === 0)
14
+ return 0;
15
+ return density * (0.9 + 0.1 * Math.cos(t));
16
+ };
@@ -0,0 +1 @@
1
+ export declare const bugScene: import("@anvpx/motion-schema").ScenePlugin;
@@ -0,0 +1,45 @@
1
+ import { z } from "zod";
2
+ import { defineScene } from "../../define-scene.js";
3
+ import { animAttrs, esc } from "../../html.js";
4
+ import { cornerSchema, positionClass, positionCss } from "../../position.js";
5
+ import { readText } from "../../text-file.js";
6
+ import { text } from "../../words.js";
7
+ const configSchema = z
8
+ .strictObject({
9
+ text: text.max(32).optional(),
10
+ image: text.optional(),
11
+ position: cornerSchema.default("top-right"),
12
+ opacity: z.enum(["subtle", "medium", "full"]).default("medium"),
13
+ })
14
+ .superRefine((props, ctx) => {
15
+ if ((props.text === undefined) === (props.image === undefined)) {
16
+ ctx.addIssue({
17
+ code: "custom",
18
+ message: "give exactly one of text or image",
19
+ params: { suggestion: "set text for a word mark, or image for a ledger asset" },
20
+ });
21
+ }
22
+ });
23
+ const mark = (props, resolve) => props.image === undefined
24
+ ? `<span class="mk-text">${esc(props.text ?? "")}</span>`
25
+ : `<img class="mk-image" src="${esc(resolve(props.image))}" alt="" />`;
26
+ const render = (props, resolve) => [
27
+ `<div class="mk-op-${props.opacity} ${positionClass(props.position)}">`,
28
+ `<div class="mk-bug" ${animAttrs("fade", 0)}>${mark(props, resolve)}</div>`,
29
+ "</div>",
30
+ ].join("\n");
31
+ export const bugScene = defineScene({
32
+ type: "bug",
33
+ layer: "overlay",
34
+ configSchema,
35
+ meta: {
36
+ purpose: "Corner word mark or ledger image; give exactly one of text or image.",
37
+ example: { text: "ACME" },
38
+ },
39
+ style: readText("./style.css", import.meta.url),
40
+ script: readText("./scene.js", import.meta.url),
41
+ render: (props, ctx, root) => ({
42
+ body: render(props, ctx.assetResolver),
43
+ css: positionCss(root),
44
+ }),
45
+ });
@@ -0,0 +1,2 @@
1
+ /** @type {import("../../runtime/mount.js").CreateScene} */
2
+ export declare function createScene(root: HTMLElement, config: import("../../runtime/mount.js").SceneConfig, ctx: import("../../runtime/mount.js").RuntimeContext): import("../../runtime/mount.js").Runtime;
@@ -0,0 +1,6 @@
1
+ // @ts-check
2
+ import { mountOverlay } from "../../runtime/mount.js";
3
+ /** @type {import("../../runtime/mount.js").CreateScene} */
4
+ export function createScene(root, config, ctx) {
5
+ return mountOverlay(root, config, ctx, { baseDelay: 0.1, staggerFrames: 4, rise: 0 });
6
+ }
@@ -0,0 +1,30 @@
1
+ #scene-root .mk-op-subtle {
2
+ opacity: var(--mk-opacity-subtle);
3
+ }
4
+ #scene-root .mk-op-medium {
5
+ opacity: var(--mk-opacity-medium);
6
+ }
7
+ #scene-root .mk-op-full {
8
+ opacity: var(--mk-opacity-full);
9
+ }
10
+ #scene-root .mk-bug {
11
+ display: flex;
12
+ align-items: center;
13
+ padding: var(--mk-space-sm) var(--mk-space-lg);
14
+ border-radius: var(--mk-radius-full);
15
+ border: var(--mk-stroke-card, 2px) solid var(--mk-color-divider);
16
+ background: var(--mk-color-card);
17
+ }
18
+ #scene-root .mk-text {
19
+ font-size: var(--mk-font-size-sm);
20
+ font-weight: 700;
21
+ letter-spacing: 0.04em;
22
+ text-transform: uppercase;
23
+ color: var(--mk-color-fg);
24
+ }
25
+ #scene-root .mk-image {
26
+ display: block;
27
+ height: var(--mk-space-xl);
28
+ max-width: 360px;
29
+ object-fit: contain;
30
+ }
@@ -0,0 +1 @@
1
+ export declare const bulletsScene: import("@anvpx/motion-schema").ScenePlugin;