@anvpx/motion-scenes 0.0.0-stage → 0.1.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 (219) 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 +170 -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/easing.d.ts +14 -0
  18. package/dist/runtime/easing.js +45 -0
  19. package/dist/runtime/mount.d.ts +50 -0
  20. package/dist/runtime/mount.js +153 -0
  21. package/dist/runtime/timeline.d.ts +147 -0
  22. package/dist/runtime/timeline.js +199 -0
  23. package/dist/runtime-files.d.ts +2 -0
  24. package/dist/runtime-files.js +15 -0
  25. package/dist/scene-contract.d.ts +7 -0
  26. package/dist/scene-contract.js +104 -0
  27. package/dist/scene3d/post.d.ts +12 -0
  28. package/dist/scene3d/post.js +16 -0
  29. package/dist/scene3d/reflector.d.ts +9 -0
  30. package/dist/scene3d/reflector.js +17 -0
  31. package/dist/scene3d/textures.d.ts +9 -0
  32. package/dist/scene3d/textures.js +11 -0
  33. package/dist/scene3d/volumetric.d.ts +9 -0
  34. package/dist/scene3d/volumetric.js +16 -0
  35. package/dist/scenes/bug/index.d.ts +1 -0
  36. package/dist/scenes/bug/index.js +45 -0
  37. package/dist/scenes/bug/scene.d.ts +2 -0
  38. package/dist/scenes/bug/scene.js +6 -0
  39. package/dist/scenes/bug/style.css +30 -0
  40. package/dist/scenes/bullets/index.d.ts +1 -0
  41. package/dist/scenes/bullets/index.js +128 -0
  42. package/dist/scenes/bullets/scene.d.ts +2 -0
  43. package/dist/scenes/bullets/scene.js +6 -0
  44. package/dist/scenes/bullets/style.css +91 -0
  45. package/dist/scenes/callout/index.d.ts +9 -0
  46. package/dist/scenes/callout/index.js +95 -0
  47. package/dist/scenes/callout/scene.d.ts +2 -0
  48. package/dist/scenes/callout/scene.js +43 -0
  49. package/dist/scenes/callout/style.css +97 -0
  50. package/dist/scenes/caption/index.d.ts +1 -0
  51. package/dist/scenes/caption/index.js +72 -0
  52. package/dist/scenes/caption/scene.d.ts +11 -0
  53. package/dist/scenes/caption/scene.js +33 -0
  54. package/dist/scenes/caption/style.css +33 -0
  55. package/dist/scenes/chapter/index.d.ts +1 -0
  56. package/dist/scenes/chapter/index.js +29 -0
  57. package/dist/scenes/chapter/scene.d.ts +2 -0
  58. package/dist/scenes/chapter/scene.js +6 -0
  59. package/dist/scenes/chapter/style.css +25 -0
  60. package/dist/scenes/chart/index.d.ts +1 -0
  61. package/dist/scenes/chart/index.js +24 -0
  62. package/dist/scenes/chart/render.d.ts +4 -0
  63. package/dist/scenes/chart/render.js +257 -0
  64. package/dist/scenes/chart/scale.d.ts +11 -0
  65. package/dist/scenes/chart/scale.js +53 -0
  66. package/dist/scenes/chart/scene.d.ts +2 -0
  67. package/dist/scenes/chart/scene.js +73 -0
  68. package/dist/scenes/chart/schema.d.ts +36 -0
  69. package/dist/scenes/chart/schema.js +109 -0
  70. package/dist/scenes/chart/style.css +425 -0
  71. package/dist/scenes/code/index.d.ts +1 -0
  72. package/dist/scenes/code/index.js +109 -0
  73. package/dist/scenes/code/scene.d.ts +2 -0
  74. package/dist/scenes/code/scene.js +108 -0
  75. package/dist/scenes/code/style.css +155 -0
  76. package/dist/scenes/code/tokenize.d.ts +8 -0
  77. package/dist/scenes/code/tokenize.js +145 -0
  78. package/dist/scenes/compare/index.d.ts +1 -0
  79. package/dist/scenes/compare/index.js +128 -0
  80. package/dist/scenes/compare/scene.d.ts +2 -0
  81. package/dist/scenes/compare/scene.js +58 -0
  82. package/dist/scenes/compare/style.css +187 -0
  83. package/dist/scenes/countdown/index.d.ts +1 -0
  84. package/dist/scenes/countdown/index.js +46 -0
  85. package/dist/scenes/countdown/scene.d.ts +2 -0
  86. package/dist/scenes/countdown/scene.js +21 -0
  87. package/dist/scenes/countdown/style.css +32 -0
  88. package/dist/scenes/flow/index.d.ts +1 -0
  89. package/dist/scenes/flow/index.js +66 -0
  90. package/dist/scenes/flow/inspect.d.ts +26 -0
  91. package/dist/scenes/flow/inspect.js +25 -0
  92. package/dist/scenes/flow/layout.d.ts +83 -0
  93. package/dist/scenes/flow/layout.js +222 -0
  94. package/dist/scenes/flow/measure.d.ts +15 -0
  95. package/dist/scenes/flow/measure.js +33 -0
  96. package/dist/scenes/flow/plan.d.ts +42 -0
  97. package/dist/scenes/flow/plan.js +117 -0
  98. package/dist/scenes/flow/scene.d.ts +2 -0
  99. package/dist/scenes/flow/scene.js +6 -0
  100. package/dist/scenes/flow/schema.d.ts +49 -0
  101. package/dist/scenes/flow/schema.js +60 -0
  102. package/dist/scenes/flow/style.css +54 -0
  103. package/dist/scenes/hero/index.d.ts +3 -0
  104. package/dist/scenes/hero/index.js +77 -0
  105. package/dist/scenes/hero/scene.d.ts +2 -0
  106. package/dist/scenes/hero/scene.js +6 -0
  107. package/dist/scenes/hero/style.css +143 -0
  108. package/dist/scenes/icon-grid/index.d.ts +1 -0
  109. package/dist/scenes/icon-grid/index.js +69 -0
  110. package/dist/scenes/icon-grid/scene.d.ts +2 -0
  111. package/dist/scenes/icon-grid/scene.js +6 -0
  112. package/dist/scenes/icon-grid/style.css +74 -0
  113. package/dist/scenes/kinetic/index.d.ts +1 -0
  114. package/dist/scenes/kinetic/index.js +62 -0
  115. package/dist/scenes/kinetic/scene.d.ts +2 -0
  116. package/dist/scenes/kinetic/scene.js +98 -0
  117. package/dist/scenes/kinetic/style.css +94 -0
  118. package/dist/scenes/kpi-grid/index.d.ts +1 -0
  119. package/dist/scenes/kpi-grid/index.js +161 -0
  120. package/dist/scenes/kpi-grid/scene.d.ts +2 -0
  121. package/dist/scenes/kpi-grid/scene.js +6 -0
  122. package/dist/scenes/kpi-grid/style.css +141 -0
  123. package/dist/scenes/lower-third/index.d.ts +1 -0
  124. package/dist/scenes/lower-third/index.js +33 -0
  125. package/dist/scenes/lower-third/scene.d.ts +2 -0
  126. package/dist/scenes/lower-third/scene.js +6 -0
  127. package/dist/scenes/lower-third/style.css +45 -0
  128. package/dist/scenes/media-frame/index.d.ts +1 -0
  129. package/dist/scenes/media-frame/index.js +99 -0
  130. package/dist/scenes/media-frame/scene.d.ts +2 -0
  131. package/dist/scenes/media-frame/scene.js +85 -0
  132. package/dist/scenes/media-frame/style.css +275 -0
  133. package/dist/scenes/outro/index.d.ts +1 -0
  134. package/dist/scenes/outro/index.js +54 -0
  135. package/dist/scenes/outro/scene.d.ts +2 -0
  136. package/dist/scenes/outro/scene.js +6 -0
  137. package/dist/scenes/outro/style.css +92 -0
  138. package/dist/scenes/poster/index.d.ts +3 -0
  139. package/dist/scenes/poster/index.js +62 -0
  140. package/dist/scenes/poster/scene.d.ts +2 -0
  141. package/dist/scenes/poster/scene.js +6 -0
  142. package/dist/scenes/poster/style.css +102 -0
  143. package/dist/scenes/progress/index.d.ts +1 -0
  144. package/dist/scenes/progress/index.js +40 -0
  145. package/dist/scenes/progress/scene.d.ts +2 -0
  146. package/dist/scenes/progress/scene.js +17 -0
  147. package/dist/scenes/progress/style.css +31 -0
  148. package/dist/scenes/quote/index.d.ts +3 -0
  149. package/dist/scenes/quote/index.js +69 -0
  150. package/dist/scenes/quote/scene.d.ts +2 -0
  151. package/dist/scenes/quote/scene.js +6 -0
  152. package/dist/scenes/quote/style.css +86 -0
  153. package/dist/scenes/scene3d/index.d.ts +1 -0
  154. package/dist/scenes/scene3d/index.js +58 -0
  155. package/dist/scenes/scene3d/scene.d.ts +2 -0
  156. package/dist/scenes/scene3d/scene.js +413 -0
  157. package/dist/scenes/scene3d/style.css +43 -0
  158. package/dist/scenes/stat/index.d.ts +4 -0
  159. package/dist/scenes/stat/index.js +72 -0
  160. package/dist/scenes/stat/scene.d.ts +2 -0
  161. package/dist/scenes/stat/scene.js +6 -0
  162. package/dist/scenes/stat/style.css +95 -0
  163. package/dist/scenes/steps/index.d.ts +1 -0
  164. package/dist/scenes/steps/index.js +147 -0
  165. package/dist/scenes/steps/scene.d.ts +2 -0
  166. package/dist/scenes/steps/scene.js +6 -0
  167. package/dist/scenes/steps/style.css +190 -0
  168. package/dist/scenes/sticker/index.d.ts +1 -0
  169. package/dist/scenes/sticker/index.js +48 -0
  170. package/dist/scenes/sticker/scene.d.ts +2 -0
  171. package/dist/scenes/sticker/scene.js +57 -0
  172. package/dist/scenes/sticker/style.css +88 -0
  173. package/dist/scenes/table/index.d.ts +1 -0
  174. package/dist/scenes/table/index.js +131 -0
  175. package/dist/scenes/table/scene.d.ts +2 -0
  176. package/dist/scenes/table/scene.js +6 -0
  177. package/dist/scenes/table/style.css +102 -0
  178. package/dist/scenes/timeline/index.d.ts +1 -0
  179. package/dist/scenes/timeline/index.js +68 -0
  180. package/dist/scenes/timeline/scene.d.ts +2 -0
  181. package/dist/scenes/timeline/scene.js +6 -0
  182. package/dist/scenes/timeline/style.css +161 -0
  183. package/dist/scenes/title/index.d.ts +1 -0
  184. package/dist/scenes/title/index.js +52 -0
  185. package/dist/scenes/title/scene.d.ts +2 -0
  186. package/dist/scenes/title/scene.js +6 -0
  187. package/dist/scenes/title/style.css +80 -0
  188. package/dist/scenes/waveform/index.d.ts +1 -0
  189. package/dist/scenes/waveform/index.js +39 -0
  190. package/dist/scenes/waveform/scene.d.ts +2 -0
  191. package/dist/scenes/waveform/scene.js +35 -0
  192. package/dist/scenes/waveform/style.css +44 -0
  193. package/dist/shared/fit-text.d.ts +24 -0
  194. package/dist/shared/fit-text.js +82 -0
  195. package/dist/shared/format.d.ts +24 -0
  196. package/dist/shared/format.js +59 -0
  197. package/dist/shared/frame.d.ts +14 -0
  198. package/dist/shared/frame.js +21 -0
  199. package/dist/shared/icon-search.d.ts +9 -0
  200. package/dist/shared/icon-search.js +26 -0
  201. package/dist/shared/icon.d.ts +15 -0
  202. package/dist/shared/icon.js +28 -0
  203. package/dist/shared/index.d.ts +11 -0
  204. package/dist/shared/index.js +7 -0
  205. package/dist/shared/markup.d.ts +12 -0
  206. package/dist/shared/markup.js +43 -0
  207. package/dist/shared/measure.d.ts +7 -0
  208. package/dist/shared/measure.js +34 -0
  209. package/dist/shared/motion.d.ts +94 -0
  210. package/dist/shared/motion.js +117 -0
  211. package/dist/source-hash.d.ts +4 -0
  212. package/dist/source-hash.js +26 -0
  213. package/dist/test-fixtures.d.ts +9 -0
  214. package/dist/test-fixtures.js +22 -0
  215. package/dist/text-file.d.ts +1 -0
  216. package/dist/text-file.js +3 -0
  217. package/dist/words.d.ts +11 -0
  218. package/dist/words.js +93 -0
  219. package/package.json +36 -3
@@ -0,0 +1,413 @@
1
+ // @ts-check
2
+ // scene3d browser runtime. WebGL diorama (three.js + postprocessing) with a 2D canvas fallback.
3
+ // MIT attribution: three.js (MIT) and postprocessing (Zlib) are vendored, unmodified, by the build
4
+ // (ADR-0006). Everything here is a pure function of t: rotation is one turn per duration, grain uses
5
+ // floor(t * 30), and no clock, random source or accumulator is read. Render only in seek().
6
+ import { BloomEffect, EffectComposer, EffectPass, RenderPass, ToneMappingEffect, VignetteEffect, } from "postprocessing";
7
+ import * as THREE from "three";
8
+ import { mountTimeline } from "../../runtime/mount.js";
9
+ import { progress } from "../../shared/motion.js";
10
+ const DEFAULT_DURATION = 5;
11
+ const TAU = Math.PI * 2;
12
+ /** Effect strength per post preset; the 2D fallback reads bloom/grain/vignette only. */
13
+ const POST = {
14
+ clean: { bloom: 0, grain: 0.01, vignette: 0, bloomIntensity: 0, vignetteDarkness: 0 },
15
+ bloom: { bloom: 1.1, grain: 0.02, vignette: 0.3, bloomIntensity: 1.05, vignetteDarkness: 0.35 },
16
+ cinematic: {
17
+ bloom: 1.1,
18
+ grain: 0.035,
19
+ vignette: 0.55,
20
+ bloomIntensity: 1.2,
21
+ vignetteDarkness: 0.6,
22
+ },
23
+ vintage: { bloom: 0.6, grain: 0.06, vignette: 0.7, bloomIntensity: 0.7, vignetteDarkness: 0.78 },
24
+ };
25
+ /** Key and fill per light preset; `key` is the 3D sun colour, `orb` the emissive accent. */
26
+ const LIGHT = {
27
+ studio: { orb: "#66ccff", sun: 1.0, key: "#ffffff" },
28
+ sunset: { orb: "#ff9a5c", sun: 0.9, key: "#ffb27a" },
29
+ night: { orb: "#8fb4ff", sun: 0.5, key: "#8fb4ff" },
30
+ soft: { orb: "#bfe8ff", sun: 0.7, key: "#e8f2ff" },
31
+ };
32
+ /** Background and framing per hdri preset: the stage backdrop and camera distance. */
33
+ const ENV = {
34
+ neutral: { bg: "#0a0c12", ambient: "#93a4c8", ground: "#0b0d13", dist: 5.9, height: 1.15 },
35
+ studio: { bg: "#111318", ambient: "#c9d6f0", ground: "#0d1016", dist: 5.5, height: 1.0 },
36
+ sunset: { bg: "#1a1210", ambient: "#ffb489", ground: "#170f0c", dist: 5.8, height: 1.3 },
37
+ night: { bg: "#05070c", ambient: "#7d9bd6", ground: "#05070b", dist: 5.2, height: 1.05 },
38
+ };
39
+ /** Surface response per material preset. */
40
+ const MATERIAL = {
41
+ clay: { roughness: 0.92, metalness: 0.02, emissiveIntensity: 0 },
42
+ gloss: { roughness: 0.16, metalness: 0.06, emissiveIntensity: 0 },
43
+ metal: { roughness: 0.3, metalness: 1, emissiveIntensity: 0 },
44
+ emissive: { roughness: 0.45, metalness: 0, emissiveIntensity: 1.6 },
45
+ };
46
+ /**
47
+ * @param {Record<string, any>} table
48
+ * @param {string | undefined} name
49
+ * @returns {any}
50
+ */
51
+ const pick = (table, name) => (name !== undefined && name in table ? table[name] : null);
52
+ /**
53
+ * @param {number} t
54
+ * @returns {number}
55
+ */
56
+ const seedOf = (t) => Math.floor(t * 30) % 1000;
57
+ /**
58
+ * @param {number} x
59
+ * @param {number} y
60
+ * @param {number} s
61
+ * @returns {number}
62
+ */
63
+ const hash2 = (x, y, s) => {
64
+ const h = Math.sin(x * 12.9898 + y * 78.233 + s * 43.7585) * 43758.5453;
65
+ return h - Math.floor(h);
66
+ };
67
+ /**
68
+ * @param {number} t
69
+ * @param {number} duration
70
+ * @returns {number}
71
+ */
72
+ const rotOf = (t, duration) => ((t / duration) * TAU) % TAU;
73
+ /* ---------- 2D fallback: a flat wireframe diorama, no WebGL required ---------- */
74
+ /**
75
+ * @param {readonly [number, number, number]} p
76
+ * @param {number} ay
77
+ * @param {number} tilt
78
+ * @param {number} cx
79
+ * @param {number} cy
80
+ * @param {number} s
81
+ * @returns {[number, number]}
82
+ */
83
+ const project = (p, ay, tilt, cx, cy, s) => {
84
+ const c = Math.cos(ay);
85
+ const sY = Math.sin(ay);
86
+ const x1 = p[0] * c + p[2] * sY;
87
+ const z1 = -p[0] * sY + p[2] * c;
88
+ const y1 = p[1] * Math.cos(tilt) - z1 * Math.sin(tilt);
89
+ return [cx + x1 * s, cy - y1 * s];
90
+ };
91
+ /** @type {Array<[number, number, number]>} */
92
+ const CUBE = [
93
+ [-0.5, -0.5, -0.5],
94
+ [0.5, -0.5, -0.5],
95
+ [0.5, 0.5, -0.5],
96
+ [-0.5, 0.5, -0.5],
97
+ [-0.5, -0.5, 0.5],
98
+ [0.5, -0.5, 0.5],
99
+ [0.5, 0.5, 0.5],
100
+ [-0.5, 0.5, 0.5],
101
+ ];
102
+ /** @type {Array<[number, number]>} */
103
+ const EDGES = [
104
+ [0, 1],
105
+ [1, 2],
106
+ [2, 3],
107
+ [3, 0],
108
+ [4, 5],
109
+ [5, 6],
110
+ [6, 7],
111
+ [7, 4],
112
+ [0, 4],
113
+ [1, 5],
114
+ [2, 6],
115
+ [3, 7],
116
+ ];
117
+ /**
118
+ * @param {CanvasRenderingContext2D} g
119
+ * @param {number} a
120
+ * @param {number} cx
121
+ * @param {number} cy
122
+ * @param {number} s
123
+ */
124
+ const paintCube = (g, a, cx, cy, s) => {
125
+ const tilt = 0.42;
126
+ g.lineWidth = Math.max(2, cx / 320);
127
+ g.strokeStyle = "#e8ecf4";
128
+ for (const edge of EDGES) {
129
+ const a0 = CUBE[edge[0]] ?? CUBE[0];
130
+ const b0 = CUBE[edge[1]] ?? CUBE[0];
131
+ const p = project(/** @type {[number, number, number]} */ (a0), a, tilt, cx, cy, s);
132
+ const q = project(/** @type {[number, number, number]} */ (b0), a, tilt, cx, cy, s);
133
+ g.beginPath();
134
+ g.moveTo(p[0], p[1]);
135
+ g.lineTo(q[0], q[1]);
136
+ g.stroke();
137
+ }
138
+ };
139
+ /**
140
+ * @param {CanvasRenderingContext2D} g
141
+ * @param {number} a
142
+ * @param {number} cx
143
+ * @param {number} cy
144
+ * @param {number} s
145
+ * @param {{ bloom: number }} post
146
+ * @param {{ orb: string }} light
147
+ */
148
+ const paintOrb = (g, a, cx, cy, s, post, light) => {
149
+ const ox = cx + Math.cos(a) * s * 1.6;
150
+ const oy = cy - 60 - Math.sin(a) * s * 0.5;
151
+ const glowR = s * (0.5 + post.bloom * 0.35);
152
+ const glow = g.createRadialGradient(ox, oy, 0, ox, oy, glowR);
153
+ glow.addColorStop(0, light.orb);
154
+ glow.addColorStop(1, "rgba(0,0,0,0)");
155
+ g.fillStyle = glow;
156
+ g.beginPath();
157
+ g.arc(ox, oy, glowR, 0, TAU);
158
+ g.fill();
159
+ g.fillStyle = light.orb;
160
+ g.beginPath();
161
+ g.arc(ox, oy, Math.max(6, s * 0.12), 0, TAU);
162
+ g.fill();
163
+ };
164
+ /**
165
+ * @param {CanvasRenderingContext2D} g
166
+ * @param {number} w
167
+ * @param {number} h
168
+ * @param {number} seed
169
+ * @param {number} amount
170
+ */
171
+ const paintGrain = (g, w, h, seed, amount) => {
172
+ const step = 8;
173
+ g.fillStyle = `rgba(255,255,255,${String(amount)})`;
174
+ for (let y = 0; y < h; y += step) {
175
+ for (let x = 0; x < w; x += step) {
176
+ if (hash2(x, y, seed) > 0.5)
177
+ g.fillRect(x, y, 2, 2);
178
+ }
179
+ }
180
+ };
181
+ /**
182
+ * @param {CanvasRenderingContext2D} g
183
+ * @param {number} w
184
+ * @param {number} h
185
+ * @param {number} cx
186
+ * @param {number} cy
187
+ * @param {number} amount
188
+ */
189
+ const paintVignette = (g, w, h, cx, cy, amount) => {
190
+ if (amount <= 0)
191
+ return;
192
+ const v = g.createRadialGradient(cx, cy, Math.min(w, h) * 0.35, cx, cy, Math.max(w, h) * 0.75);
193
+ v.addColorStop(0, "rgba(0,0,0,0)");
194
+ v.addColorStop(1, `rgba(0,0,0,${String(amount)})`);
195
+ g.fillStyle = v;
196
+ g.fillRect(0, 0, w, h);
197
+ };
198
+ /**
199
+ * @param {CanvasRenderingContext2D} g
200
+ * @param {number} w
201
+ * @param {number} h
202
+ * @param {number} t
203
+ * @param {number} duration
204
+ */
205
+ const paintFade = (g, w, h, t, duration) => {
206
+ const fade = progress(t, duration - 0.5, duration);
207
+ if (fade <= 0)
208
+ return;
209
+ g.fillStyle = `rgba(0,0,0,${String(fade * 0.35)})`;
210
+ g.fillRect(0, 0, w, h);
211
+ };
212
+ /**
213
+ * @param {HTMLCanvasElement} canvas
214
+ * @param {{ post: { bloom: number, grain: number, vignette: number }, light: { orb: string }, bg: string }} presets
215
+ * @param {number} duration
216
+ */
217
+ const flatStage = (canvas, presets, duration) => {
218
+ const g = canvas.getContext("2d");
219
+ if (g === null)
220
+ return null;
221
+ /** @param {number} t */
222
+ const render = (t) => {
223
+ const w = canvas.width;
224
+ const h = canvas.height;
225
+ const a = rotOf(t, duration);
226
+ g.save();
227
+ g.fillStyle = presets.bg;
228
+ g.fillRect(0, 0, w, h);
229
+ paintCube(g, a, w * 0.5, h * 0.58, Math.min(w, h) * 0.22);
230
+ paintOrb(g, a, w * 0.5, h * 0.58, Math.min(w, h) * 0.22, presets.post, presets.light);
231
+ paintGrain(g, w, h, seedOf(t), presets.post.grain);
232
+ paintVignette(g, w, h, w * 0.5, h * 0.58, presets.post.vignette);
233
+ paintFade(g, w, h, t, duration);
234
+ g.restore();
235
+ };
236
+ return { render, dispose: () => g.clearRect(0, 0, canvas.width, canvas.height) };
237
+ };
238
+ /* ---------- WebGL diorama ---------- */
239
+ /**
240
+ * Builds the three.js stage. Any failure (no WebGL, a driver quirk, an API change in a vendored
241
+ * library) returns null so the caller falls back to the 2D diorama instead of a black frame.
242
+ * @param {HTMLCanvasElement} canvas
243
+ * @param {HTMLElement} root
244
+ * @param {{ post: any, light: any, material: any, env: any }} presets
245
+ * @param {number} duration
246
+ */
247
+ const glStage = (canvas, root, presets, duration) => {
248
+ const width = Math.max(1, Math.round(canvas.clientWidth || canvas.width));
249
+ const height = Math.max(1, Math.round(canvas.clientHeight || canvas.height));
250
+ const renderer = new THREE.WebGLRenderer({
251
+ canvas,
252
+ antialias: true,
253
+ preserveDrawingBuffer: true,
254
+ powerPreference: "high-performance",
255
+ });
256
+ renderer.setPixelRatio(1);
257
+ renderer.setSize(width, height, false);
258
+ renderer.shadowMap.enabled = true;
259
+ renderer.shadowMap.type = THREE.PCFShadowMap;
260
+ /**
261
+ * @param {string} name
262
+ * @param {string} fallback
263
+ * @returns {string}
264
+ */
265
+ const cssVar = (name, fallback) => {
266
+ const value = getComputedStyle(root).getPropertyValue(name).trim();
267
+ return value === "" ? fallback : value;
268
+ };
269
+ const accent = cssVar("--mk-color-accent", "#7cc4ff");
270
+ const accent2 = cssVar("--mk-color-accent2", "#b58cff");
271
+ const scene = new THREE.Scene();
272
+ scene.background = new THREE.Color(presets.env.bg);
273
+ const camera = new THREE.PerspectiveCamera(38, width / height, 0.1, 100);
274
+ camera.position.set(0, presets.env.height, presets.env.dist);
275
+ camera.lookAt(0, 0.1, 0);
276
+ scene.add(new THREE.HemisphereLight(presets.env.ambient, presets.env.ground, 0.85));
277
+ const sun = new THREE.DirectionalLight(presets.light.key, presets.light.sun * 2.4);
278
+ sun.position.set(3.4, 5.2, 2.6);
279
+ sun.castShadow = true;
280
+ sun.shadow.mapSize.set(1024, 1024);
281
+ sun.shadow.camera.near = 1;
282
+ sun.shadow.camera.far = 18;
283
+ sun.shadow.camera.left = -5;
284
+ sun.shadow.camera.right = 5;
285
+ sun.shadow.camera.top = 5;
286
+ sun.shadow.camera.bottom = -5;
287
+ sun.shadow.bias = -0.0012;
288
+ scene.add(sun);
289
+ // Shadow-only floor: the theme background shows through, so the diorama sits in the card.
290
+ const floor = new THREE.Mesh(new THREE.PlaneGeometry(24, 24), new THREE.ShadowMaterial({ opacity: 0.42 }));
291
+ floor.rotation.x = -Math.PI / 2;
292
+ floor.position.y = -1.25;
293
+ floor.receiveShadow = true;
294
+ scene.add(floor);
295
+ const surface = presets.material;
296
+ const heroMaterial = new THREE.MeshStandardMaterial({
297
+ color: new THREE.Color(accent),
298
+ roughness: surface.roughness,
299
+ metalness: surface.metalness,
300
+ emissive: new THREE.Color(accent2),
301
+ emissiveIntensity: surface.emissiveIntensity * 0.35,
302
+ });
303
+ const hero = new THREE.Mesh(new THREE.TorusKnotGeometry(0.82, 0.27, 160, 32), heroMaterial);
304
+ hero.position.y = 0.12;
305
+ hero.castShadow = true;
306
+ scene.add(hero);
307
+ const satelliteMaterial = new THREE.MeshStandardMaterial({
308
+ color: new THREE.Color(accent2),
309
+ roughness: 0.22,
310
+ metalness: 0.3,
311
+ emissive: new THREE.Color(accent2),
312
+ emissiveIntensity: 0.9,
313
+ });
314
+ const satellite = new THREE.Mesh(new THREE.SphereGeometry(0.13, 32, 24), satelliteMaterial);
315
+ satellite.castShadow = true;
316
+ scene.add(satellite);
317
+ // The orbit path itself: a thin ring the satellite travels along, so the motion reads as intended.
318
+ const ring = new THREE.Mesh(new THREE.TorusGeometry(1.95, 0.007, 6, 160), new THREE.MeshBasicMaterial({
319
+ color: new THREE.Color(accent),
320
+ transparent: true,
321
+ opacity: 0.32,
322
+ }));
323
+ ring.rotation.x = -Math.PI / 2;
324
+ ring.position.y = 0.1;
325
+ scene.add(ring);
326
+ const orbColor = new THREE.Color(presets.light.orb);
327
+ // Over-bright (HDR) so the bloom pass picks the orb up as a light source, not a flat disc.
328
+ const orb = new THREE.Mesh(new THREE.SphereGeometry(0.1, 32, 24), new THREE.MeshBasicMaterial({ color: orbColor.clone().multiplyScalar(2.6) }));
329
+ scene.add(orb);
330
+ const orbLight = new THREE.PointLight(orbColor, 14, 14, 2);
331
+ scene.add(orbLight);
332
+ const composer = new EffectComposer(renderer);
333
+ composer.addPass(new RenderPass(scene, camera));
334
+ /** @type {any[]} */
335
+ const effects = [];
336
+ if (presets.post.bloomIntensity > 0) {
337
+ effects.push(new BloomEffect({
338
+ intensity: presets.post.bloomIntensity,
339
+ luminanceThreshold: 0.52,
340
+ luminanceSmoothing: 0.28,
341
+ mipmapBlur: true,
342
+ }));
343
+ }
344
+ if (presets.post.vignetteDarkness > 0) {
345
+ effects.push(new VignetteEffect({ darkness: presets.post.vignetteDarkness, offset: 0.32 }));
346
+ }
347
+ effects.push(new ToneMappingEffect());
348
+ composer.addPass(new EffectPass(camera, ...effects));
349
+ composer.setSize(width, height);
350
+ /** @param {number} t */
351
+ const render = (t) => {
352
+ const a = rotOf(t, duration);
353
+ hero.rotation.set(0.32 + Math.sin(a * 0.5) * 0.12, a, 0);
354
+ hero.position.y = 0.12 + Math.sin(a) * 0.06;
355
+ satellite.position.set(Math.cos(a) * 1.95, 0.1, Math.sin(a) * 1.95);
356
+ satellite.rotation.set(a * 1.6, a, 0);
357
+ orb.position.set(Math.cos(-a) * 1.55, 1.35, Math.sin(-a) * 1.55);
358
+ orbLight.position.copy(orb.position);
359
+ camera.position.set(Math.sin(a * 0.35) * 0.4, presets.env.height, presets.env.dist);
360
+ camera.lookAt(0, 0.1, 0);
361
+ composer.render(0);
362
+ };
363
+ const dispose = () => {
364
+ composer.dispose();
365
+ for (const effect of effects)
366
+ effect.dispose?.();
367
+ for (const object of [hero, satellite, orb, ring]) {
368
+ object.geometry.dispose();
369
+ /** @type {any} */ (object.material).dispose();
370
+ }
371
+ floor.geometry.dispose();
372
+ /** @type {any} */ (floor.material).dispose();
373
+ renderer.dispose();
374
+ renderer.forceContextLoss();
375
+ };
376
+ return { render, dispose };
377
+ };
378
+ /** @type {import("../../runtime/mount.js").CreateScene} */
379
+ export function createScene(root, config, ctx) {
380
+ const inner = mountTimeline(root, config, ctx, { baseDelay: 0.2, staggerFrames: 5, rise: 36 });
381
+ const box = /** @type {HTMLElement | null} */ (root.querySelector(".mk-scene3d"));
382
+ const canvas = /** @type {HTMLCanvasElement | null} */ (root.querySelector("canvas.mk-gl"));
383
+ const duration = ctx.duration || Number(root.dataset.duration) || DEFAULT_DURATION;
384
+ const env = pick(ENV, box?.dataset.hdri) ?? ENV.neutral;
385
+ const presets = {
386
+ post: pick(POST, box?.dataset.post) ?? POST.cinematic,
387
+ light: pick(LIGHT, box?.dataset.light) ?? LIGHT.studio,
388
+ material: pick(MATERIAL, box?.dataset.material) ?? MATERIAL.clay,
389
+ env,
390
+ bg: env.bg,
391
+ };
392
+ /** @type {{ render: (t: number) => void, dispose: () => void } | null} */
393
+ let stage = null;
394
+ if (canvas !== null) {
395
+ try {
396
+ stage = glStage(canvas, root, presets, duration);
397
+ }
398
+ catch {
399
+ stage = flatStage(canvas, presets, duration);
400
+ }
401
+ }
402
+ /** @param {number} t */
403
+ const seek = (t) => {
404
+ inner.seek(t);
405
+ stage?.render(t);
406
+ };
407
+ const dispose = () => {
408
+ inner.dispose();
409
+ stage?.dispose();
410
+ };
411
+ seek(0);
412
+ return { seek, dispose, ready: inner.ready };
413
+ }
@@ -0,0 +1,43 @@
1
+ #scene-root .mk-scene3d {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: var(--mk-space-lg);
5
+ flex: 1;
6
+ min-height: 0;
7
+ }
8
+ #scene-root .mk-copy {
9
+ display: flex;
10
+ flex-direction: column;
11
+ gap: var(--mk-space-md);
12
+ max-width: 56%;
13
+ }
14
+ #scene-root .mk-title {
15
+ font-family: var(--mk-font-sans);
16
+ font-size: var(--mk-font-size-xl);
17
+ font-weight: 800;
18
+ line-height: 1.04;
19
+ letter-spacing: -0.02em;
20
+ color: var(--mk-color-fg);
21
+ }
22
+ #scene-root .mk-caption {
23
+ font-size: var(--mk-font-size-lg);
24
+ line-height: 1.3;
25
+ color: var(--mk-color-muted);
26
+ }
27
+ #scene-root .mk-stage {
28
+ position: relative;
29
+ flex: 1;
30
+ min-height: 320px;
31
+ border-radius: var(--mk-radius-xl);
32
+ overflow: hidden;
33
+ background: var(--mk-color-card);
34
+ border: 2px solid var(--mk-color-divider);
35
+ }
36
+ #scene-root .mk-gl {
37
+ position: absolute;
38
+ inset: 0;
39
+ width: 100%;
40
+ height: 100%;
41
+ display: block;
42
+ background: var(--mk-color-card);
43
+ }
@@ -0,0 +1,4 @@
1
+ /** Doubles hold 15 digits exactly; beyond that the count-up would show wrong digits. */
2
+ export declare const MAX_COUNT_DIGITS = 15;
3
+ export declare const countsExactly: (value: string | number) => boolean;
4
+ export declare const statScene: import("@anvpx/motion-schema").ScenePlugin;
@@ -0,0 +1,72 @@
1
+ import { z } from "zod";
2
+ import { defineScene } from "../../define-scene.js";
3
+ import { animAttrs, esc } from "../../html.js";
4
+ import { readText } from "../../text-file.js";
5
+ import { limitWords, present, text } from "../../words.js";
6
+ /** Doubles hold 15 digits exactly; beyond that the count-up would show wrong digits. */
7
+ export const MAX_COUNT_DIGITS = 15;
8
+ const LEADING_NUMBER = /\d[\d,]*(?:\.\d+)?/;
9
+ export const countsExactly = (value) => {
10
+ if (typeof value === "number")
11
+ return Math.abs(value) < 10 ** MAX_COUNT_DIGITS;
12
+ const digits = LEADING_NUMBER.exec(value)?.[0].replace(/\D/g, "") ?? "";
13
+ return digits.length <= MAX_COUNT_DIGITS;
14
+ };
15
+ const configSchema = limitWords(z
16
+ .strictObject({
17
+ value: z.union([text, z.number()]),
18
+ label: text,
19
+ unit: text.optional(),
20
+ delta: text.optional(),
21
+ context: text.optional(),
22
+ })
23
+ .superRefine((props, ctx) => {
24
+ if (!countsExactly(props.value)) {
25
+ ctx.addIssue({
26
+ code: "custom",
27
+ path: ["value"],
28
+ message: `too large to count up exactly (max ${MAX_COUNT_DIGITS} digits)`,
29
+ });
30
+ }
31
+ }), (props) => present(props.value, props.unit, props.label, props.delta, props.context));
32
+ const SHORT_VALUE = 4;
33
+ const MEDIUM_VALUE = 7;
34
+ /** Hero size class by value length so long values stay inside the frame. */
35
+ const fit = (value) => {
36
+ if (value.length <= SHORT_VALUE)
37
+ return "mk-fit-hero";
38
+ return value.length <= MEDIUM_VALUE ? "mk-fit-md" : "mk-fit-sm";
39
+ };
40
+ /** The live span counts up from 0; the hidden ghost reserves the final width. */
41
+ const figure = (props) => {
42
+ const value = esc(String(props.value));
43
+ const unit = props.unit ? `<span class="mk-unit">${esc(props.unit)}</span>` : "";
44
+ const live = `<span class="mk-live" data-step="0" data-count="${value}">${value}</span>`;
45
+ const ghost = `<span class="mk-ghost" aria-hidden="true">${value}</span>`;
46
+ return `<span class="mk-value ${fit(String(props.value))}">${ghost}${live}</span>${unit}`;
47
+ };
48
+ const render = (props) => {
49
+ const lines = [];
50
+ const add = (tag, cls, inner, key) => {
51
+ const attrs = animAttrs("rise", lines.length, key);
52
+ lines.push(`<${tag} class="${cls}" ${attrs}>${inner}</${tag}>`);
53
+ };
54
+ add("div", "mk-figure", figure(props), "value");
55
+ add("p", "mk-label", esc(props.label), "label");
56
+ if (props.delta)
57
+ add("p", "mk-delta", esc(props.delta), "delta");
58
+ if (props.context)
59
+ add("p", "mk-context", esc(props.context), "context");
60
+ return `<div class="mk-halo" aria-hidden="true"></div>\n${lines.join("\n")}`;
61
+ };
62
+ export const statScene = defineScene({
63
+ type: "stat",
64
+ configSchema,
65
+ meta: {
66
+ purpose: "One hero number with label, unit, delta and context; counts up.",
67
+ example: { value: 42, label: "Users" },
68
+ },
69
+ style: readText("./style.css", import.meta.url),
70
+ script: readText("./scene.js", import.meta.url),
71
+ render: (props) => ({ body: render(props) }),
72
+ });
@@ -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 { mountTimeline } from "../../runtime/mount.js";
3
+ /** @type {import("../../runtime/mount.js").CreateScene} */
4
+ export function createScene(root, config, ctx) {
5
+ return mountTimeline(root, config, ctx, { baseDelay: 0.2, staggerFrames: 4, rise: 56 });
6
+ }
@@ -0,0 +1,95 @@
1
+ #scene-root {
2
+ align-items: center;
3
+ text-align: center;
4
+ }
5
+ #scene-root .mk-halo {
6
+ position: absolute;
7
+ z-index: -1;
8
+ left: 50%;
9
+ top: 50%;
10
+ width: 1280px;
11
+ height: 1280px;
12
+ margin: -640px 0 0 -640px;
13
+ border-radius: 50%;
14
+ border: 2px solid var(--mk-color-divider);
15
+ background: radial-gradient(
16
+ closest-side,
17
+ color-mix(in srgb, var(--mk-color-card) 80%, transparent),
18
+ transparent
19
+ );
20
+ transform: scale(calc(1 + var(--drift, 0) * 0.015));
21
+ }
22
+ #scene-root .mk-halo::after {
23
+ content: "";
24
+ position: absolute;
25
+ inset: 170px;
26
+ border-radius: 50%;
27
+ border: 2px solid var(--mk-color-accent);
28
+ opacity: 0.35;
29
+ }
30
+ #scene-root .mk-figure {
31
+ display: flex;
32
+ align-items: baseline;
33
+ justify-content: center;
34
+ gap: var(--mk-space-lg);
35
+ }
36
+ #scene-root .mk-value {
37
+ font-family: var(--mk-font-display, var(--mk-font-sans));
38
+ display: inline-grid;
39
+ font-weight: 800;
40
+ line-height: 0.9;
41
+ letter-spacing: -0.04em;
42
+ font-variant-numeric: var(--mk-numeric, tabular-nums);
43
+ color: var(--mk-color-accent);
44
+ }
45
+ #scene-root .mk-value > * {
46
+ grid-area: 1 / 1;
47
+ text-align: left;
48
+ }
49
+ #scene-root .mk-live {
50
+ text-align: right;
51
+ transform-origin: right 65%;
52
+ }
53
+ #scene-root .mk-ghost {
54
+ visibility: hidden;
55
+ }
56
+ #scene-root .mk-fit-hero {
57
+ font-size: var(--mk-font-size-hero);
58
+ }
59
+ #scene-root .mk-fit-md {
60
+ font-size: var(--mk-font-size-heroMd);
61
+ }
62
+ #scene-root .mk-fit-sm {
63
+ font-size: var(--mk-font-size-heroSm);
64
+ }
65
+ #scene-root .mk-unit {
66
+ font-family: var(--mk-font-display, var(--mk-font-sans));
67
+ font-size: var(--mk-font-size-title);
68
+ font-weight: 700;
69
+ letter-spacing: -0.02em;
70
+ }
71
+ #scene-root .mk-label {
72
+ margin-top: var(--mk-space-lg);
73
+ max-width: 1200px;
74
+ font-size: var(--mk-font-size-lg);
75
+ font-weight: 600;
76
+ line-height: 1.25;
77
+ }
78
+ #scene-root .mk-delta {
79
+ margin-top: var(--mk-space-lg);
80
+ padding: var(--mk-space-sm) var(--mk-space-lg);
81
+ border-radius: var(--mk-radius-full);
82
+ background: var(--mk-color-accentSoft);
83
+ font-size: var(--mk-font-size-md);
84
+ font-weight: 700;
85
+ letter-spacing: 0.02em;
86
+ color: var(--mk-color-accent);
87
+ }
88
+ #scene-root .mk-context {
89
+ margin-top: var(--mk-space-lg);
90
+ padding-top: var(--mk-space-lg);
91
+ max-width: 1000px;
92
+ border-top: 2px solid var(--mk-color-divider);
93
+ font-size: var(--mk-font-size-md);
94
+ color: var(--mk-color-muted);
95
+ }
@@ -0,0 +1 @@
1
+ export declare const stepsScene: import("@anvpx/motion-schema").ScenePlugin;