@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,436 @@
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
+ * Hero geometry per `model` preset. These are procedural shapes, not loaded
48
+ * GLB files: the scene stays offline and deterministic. `gem` is a faceted
49
+ * low-poly stone; unknown names fall back to the knot.
50
+ * @param {string} name
51
+ * @returns {any}
52
+ */
53
+ const modelGeometry = (name) => {
54
+ switch (name) {
55
+ case "gem":
56
+ return new THREE.OctahedronGeometry(1.28, 0);
57
+ case "sphere":
58
+ return new THREE.SphereGeometry(0.95, 48, 32);
59
+ case "torus":
60
+ return new THREE.TorusGeometry(0.95, 0.28, 32, 96);
61
+ default:
62
+ return new THREE.TorusKnotGeometry(0.82, 0.27, 160, 32);
63
+ }
64
+ };
65
+ /**
66
+ * @param {Record<string, any>} table
67
+ * @param {string | undefined} name
68
+ * @returns {any}
69
+ */
70
+ const pick = (table, name) => (name !== undefined && name in table ? table[name] : null);
71
+ /**
72
+ * @param {number} t
73
+ * @returns {number}
74
+ */
75
+ const seedOf = (t) => Math.floor(t * 30) % 1000;
76
+ /**
77
+ * @param {number} x
78
+ * @param {number} y
79
+ * @param {number} s
80
+ * @returns {number}
81
+ */
82
+ const hash2 = (x, y, s) => {
83
+ const h = Math.sin(x * 12.9898 + y * 78.233 + s * 43.7585) * 43758.5453;
84
+ return h - Math.floor(h);
85
+ };
86
+ /**
87
+ * @param {number} t
88
+ * @param {number} duration
89
+ * @returns {number}
90
+ */
91
+ const rotOf = (t, duration) => ((t / duration) * TAU) % TAU;
92
+ /* ---------- 2D fallback: a flat wireframe diorama, no WebGL required ---------- */
93
+ /**
94
+ * @param {readonly [number, number, number]} p
95
+ * @param {number} ay
96
+ * @param {number} tilt
97
+ * @param {number} cx
98
+ * @param {number} cy
99
+ * @param {number} s
100
+ * @returns {[number, number]}
101
+ */
102
+ const project = (p, ay, tilt, cx, cy, s) => {
103
+ const c = Math.cos(ay);
104
+ const sY = Math.sin(ay);
105
+ const x1 = p[0] * c + p[2] * sY;
106
+ const z1 = -p[0] * sY + p[2] * c;
107
+ const y1 = p[1] * Math.cos(tilt) - z1 * Math.sin(tilt);
108
+ return [cx + x1 * s, cy - y1 * s];
109
+ };
110
+ /** @type {Array<[number, number, number]>} */
111
+ const CUBE = [
112
+ [-0.5, -0.5, -0.5],
113
+ [0.5, -0.5, -0.5],
114
+ [0.5, 0.5, -0.5],
115
+ [-0.5, 0.5, -0.5],
116
+ [-0.5, -0.5, 0.5],
117
+ [0.5, -0.5, 0.5],
118
+ [0.5, 0.5, 0.5],
119
+ [-0.5, 0.5, 0.5],
120
+ ];
121
+ /** @type {Array<[number, number]>} */
122
+ const EDGES = [
123
+ [0, 1],
124
+ [1, 2],
125
+ [2, 3],
126
+ [3, 0],
127
+ [4, 5],
128
+ [5, 6],
129
+ [6, 7],
130
+ [7, 4],
131
+ [0, 4],
132
+ [1, 5],
133
+ [2, 6],
134
+ [3, 7],
135
+ ];
136
+ /**
137
+ * @param {CanvasRenderingContext2D} g
138
+ * @param {number} a
139
+ * @param {number} cx
140
+ * @param {number} cy
141
+ * @param {number} s
142
+ */
143
+ const paintCube = (g, a, cx, cy, s) => {
144
+ const tilt = 0.42;
145
+ g.lineWidth = Math.max(2, cx / 320);
146
+ g.strokeStyle = "#e8ecf4";
147
+ for (const edge of EDGES) {
148
+ const a0 = CUBE[edge[0]] ?? CUBE[0];
149
+ const b0 = CUBE[edge[1]] ?? CUBE[0];
150
+ const p = project(/** @type {[number, number, number]} */ (a0), a, tilt, cx, cy, s);
151
+ const q = project(/** @type {[number, number, number]} */ (b0), a, tilt, cx, cy, s);
152
+ g.beginPath();
153
+ g.moveTo(p[0], p[1]);
154
+ g.lineTo(q[0], q[1]);
155
+ g.stroke();
156
+ }
157
+ };
158
+ /**
159
+ * @param {CanvasRenderingContext2D} g
160
+ * @param {number} a
161
+ * @param {number} cx
162
+ * @param {number} cy
163
+ * @param {number} s
164
+ * @param {{ bloom: number }} post
165
+ * @param {{ orb: string }} light
166
+ */
167
+ const paintOrb = (g, a, cx, cy, s, post, light) => {
168
+ const ox = cx + Math.cos(a) * s * 1.6;
169
+ const oy = cy - 60 - Math.sin(a) * s * 0.5;
170
+ const glowR = s * (0.5 + post.bloom * 0.35);
171
+ const glow = g.createRadialGradient(ox, oy, 0, ox, oy, glowR);
172
+ glow.addColorStop(0, light.orb);
173
+ glow.addColorStop(1, "rgba(0,0,0,0)");
174
+ g.fillStyle = glow;
175
+ g.beginPath();
176
+ g.arc(ox, oy, glowR, 0, TAU);
177
+ g.fill();
178
+ g.fillStyle = light.orb;
179
+ g.beginPath();
180
+ g.arc(ox, oy, Math.max(6, s * 0.12), 0, TAU);
181
+ g.fill();
182
+ };
183
+ /**
184
+ * @param {CanvasRenderingContext2D} g
185
+ * @param {number} w
186
+ * @param {number} h
187
+ * @param {number} seed
188
+ * @param {number} amount
189
+ */
190
+ const paintGrain = (g, w, h, seed, amount) => {
191
+ const step = 8;
192
+ g.fillStyle = `rgba(255,255,255,${String(amount)})`;
193
+ for (let y = 0; y < h; y += step) {
194
+ for (let x = 0; x < w; x += step) {
195
+ if (hash2(x, y, seed) > 0.5)
196
+ g.fillRect(x, y, 2, 2);
197
+ }
198
+ }
199
+ };
200
+ /**
201
+ * @param {CanvasRenderingContext2D} g
202
+ * @param {number} w
203
+ * @param {number} h
204
+ * @param {number} cx
205
+ * @param {number} cy
206
+ * @param {number} amount
207
+ */
208
+ const paintVignette = (g, w, h, cx, cy, amount) => {
209
+ if (amount <= 0)
210
+ return;
211
+ const v = g.createRadialGradient(cx, cy, Math.min(w, h) * 0.35, cx, cy, Math.max(w, h) * 0.75);
212
+ v.addColorStop(0, "rgba(0,0,0,0)");
213
+ v.addColorStop(1, `rgba(0,0,0,${String(amount)})`);
214
+ g.fillStyle = v;
215
+ g.fillRect(0, 0, w, h);
216
+ };
217
+ /**
218
+ * @param {CanvasRenderingContext2D} g
219
+ * @param {number} w
220
+ * @param {number} h
221
+ * @param {number} t
222
+ * @param {number} duration
223
+ */
224
+ const paintFade = (g, w, h, t, duration) => {
225
+ const fade = progress(t, duration - 0.5, duration);
226
+ if (fade <= 0)
227
+ return;
228
+ g.fillStyle = `rgba(0,0,0,${String(fade * 0.35)})`;
229
+ g.fillRect(0, 0, w, h);
230
+ };
231
+ /**
232
+ * @param {HTMLCanvasElement} canvas
233
+ * @param {{ post: { bloom: number, grain: number, vignette: number }, light: { orb: string }, bg: string }} presets
234
+ * @param {number} duration
235
+ */
236
+ const flatStage = (canvas, presets, duration) => {
237
+ const g = canvas.getContext("2d");
238
+ if (g === null)
239
+ return null;
240
+ /** @param {number} t */
241
+ const render = (t) => {
242
+ const w = canvas.width;
243
+ const h = canvas.height;
244
+ const a = rotOf(t, duration);
245
+ g.save();
246
+ g.fillStyle = presets.bg;
247
+ g.fillRect(0, 0, w, h);
248
+ paintCube(g, a, w * 0.5, h * 0.58, Math.min(w, h) * 0.22);
249
+ paintOrb(g, a, w * 0.5, h * 0.58, Math.min(w, h) * 0.22, presets.post, presets.light);
250
+ paintGrain(g, w, h, seedOf(t), presets.post.grain);
251
+ paintVignette(g, w, h, w * 0.5, h * 0.58, presets.post.vignette);
252
+ paintFade(g, w, h, t, duration);
253
+ g.restore();
254
+ };
255
+ return { render, dispose: () => g.clearRect(0, 0, canvas.width, canvas.height) };
256
+ };
257
+ /* ---------- WebGL diorama ---------- */
258
+ /**
259
+ * Builds the three.js stage. Any failure (no WebGL, a driver quirk, an API change in a vendored
260
+ * library) returns null so the caller falls back to the 2D diorama instead of a black frame.
261
+ * @param {HTMLCanvasElement} canvas
262
+ * @param {HTMLElement} root
263
+ * @param {{ post: any, light: any, material: any, model?: string, env: any }} presets
264
+ * @param {number} duration
265
+ */
266
+ const glStage = (canvas, root, presets, duration) => {
267
+ const width = Math.max(1, Math.round(canvas.clientWidth || canvas.width));
268
+ const height = Math.max(1, Math.round(canvas.clientHeight || canvas.height));
269
+ const renderer = new THREE.WebGLRenderer({
270
+ canvas,
271
+ antialias: true,
272
+ preserveDrawingBuffer: true,
273
+ powerPreference: "high-performance",
274
+ });
275
+ renderer.setPixelRatio(1);
276
+ renderer.setSize(width, height, false);
277
+ renderer.shadowMap.enabled = true;
278
+ renderer.shadowMap.type = THREE.PCFShadowMap;
279
+ /**
280
+ * @param {string} name
281
+ * @param {string} fallback
282
+ * @returns {string}
283
+ */
284
+ const cssVar = (name, fallback) => {
285
+ const value = getComputedStyle(root).getPropertyValue(name).trim();
286
+ return value === "" ? fallback : value;
287
+ };
288
+ const accent = cssVar("--mk-color-accent", "#7cc4ff");
289
+ const accent2 = cssVar("--mk-color-accent2", "#b58cff");
290
+ const scene = new THREE.Scene();
291
+ scene.background = new THREE.Color(presets.env.bg);
292
+ const camera = new THREE.PerspectiveCamera(38, width / height, 0.1, 100);
293
+ camera.position.set(0, presets.env.height, presets.env.dist);
294
+ camera.lookAt(0, 0.1, 0);
295
+ scene.add(new THREE.HemisphereLight(presets.env.ambient, presets.env.ground, 0.85));
296
+ const sun = new THREE.DirectionalLight(presets.light.key, presets.light.sun * 2.4);
297
+ sun.position.set(3.4, 5.2, 2.6);
298
+ sun.castShadow = true;
299
+ sun.shadow.mapSize.set(1024, 1024);
300
+ sun.shadow.camera.near = 1;
301
+ sun.shadow.camera.far = 18;
302
+ sun.shadow.camera.left = -5;
303
+ sun.shadow.camera.right = 5;
304
+ sun.shadow.camera.top = 5;
305
+ sun.shadow.camera.bottom = -5;
306
+ sun.shadow.bias = -0.0012;
307
+ scene.add(sun);
308
+ // Shadow-only floor: the theme background shows through, so the diorama sits in the card.
309
+ const floor = new THREE.Mesh(new THREE.PlaneGeometry(24, 24), new THREE.ShadowMaterial({ opacity: 0.42 }));
310
+ floor.rotation.x = -Math.PI / 2;
311
+ floor.position.y = -1.25;
312
+ floor.receiveShadow = true;
313
+ scene.add(floor);
314
+ const surface = presets.material;
315
+ const modelName = presets.model ?? "knot";
316
+ const geometry = modelGeometry(modelName);
317
+ const heroMaterial = new THREE.MeshStandardMaterial({
318
+ color: new THREE.Color(accent),
319
+ roughness: surface.roughness,
320
+ metalness: surface.metalness,
321
+ emissive: new THREE.Color(accent2),
322
+ emissiveIntensity: surface.emissiveIntensity * 0.35,
323
+ flatShading: modelName === "gem",
324
+ });
325
+ const hero = new THREE.Mesh(geometry, heroMaterial);
326
+ hero.position.y = 0.12;
327
+ hero.castShadow = true;
328
+ scene.add(hero);
329
+ const satelliteMaterial = new THREE.MeshStandardMaterial({
330
+ color: new THREE.Color(accent2),
331
+ roughness: 0.22,
332
+ metalness: 0.3,
333
+ emissive: new THREE.Color(accent2),
334
+ emissiveIntensity: 0.9,
335
+ });
336
+ const satellite = new THREE.Mesh(new THREE.SphereGeometry(0.13, 32, 24), satelliteMaterial);
337
+ satellite.castShadow = true;
338
+ scene.add(satellite);
339
+ // The orbit path itself: a thin ring the satellite travels along, so the motion reads as intended.
340
+ const ring = new THREE.Mesh(new THREE.TorusGeometry(1.95, 0.007, 6, 160), new THREE.MeshBasicMaterial({
341
+ color: new THREE.Color(accent),
342
+ transparent: true,
343
+ opacity: 0.32,
344
+ }));
345
+ ring.rotation.x = -Math.PI / 2;
346
+ ring.position.y = 0.1;
347
+ scene.add(ring);
348
+ const orbColor = new THREE.Color(presets.light.orb);
349
+ // Over-bright (HDR) so the bloom pass picks the orb up as a light source, not a flat disc.
350
+ const orb = new THREE.Mesh(new THREE.SphereGeometry(0.1, 32, 24), new THREE.MeshBasicMaterial({ color: orbColor.clone().multiplyScalar(2.6) }));
351
+ scene.add(orb);
352
+ const orbLight = new THREE.PointLight(orbColor, 14, 14, 2);
353
+ scene.add(orbLight);
354
+ const composer = new EffectComposer(renderer);
355
+ composer.addPass(new RenderPass(scene, camera));
356
+ /** @type {any[]} */
357
+ const effects = [];
358
+ if (presets.post.bloomIntensity > 0) {
359
+ effects.push(new BloomEffect({
360
+ intensity: presets.post.bloomIntensity,
361
+ luminanceThreshold: 0.52,
362
+ luminanceSmoothing: 0.28,
363
+ mipmapBlur: true,
364
+ }));
365
+ }
366
+ if (presets.post.vignetteDarkness > 0) {
367
+ effects.push(new VignetteEffect({ darkness: presets.post.vignetteDarkness, offset: 0.32 }));
368
+ }
369
+ effects.push(new ToneMappingEffect());
370
+ composer.addPass(new EffectPass(camera, ...effects));
371
+ composer.setSize(width, height);
372
+ /** @param {number} t */
373
+ const render = (t) => {
374
+ const a = rotOf(t, duration);
375
+ hero.rotation.set(0.32 + Math.sin(a * 0.5) * 0.12, a, 0);
376
+ hero.position.y = 0.12 + Math.sin(a) * 0.06;
377
+ satellite.position.set(Math.cos(a) * 1.95, 0.1, Math.sin(a) * 1.95);
378
+ satellite.rotation.set(a * 1.6, a, 0);
379
+ orb.position.set(Math.cos(-a) * 1.55, 1.35, Math.sin(-a) * 1.55);
380
+ orbLight.position.copy(orb.position);
381
+ camera.position.set(Math.sin(a * 0.35) * 0.4, presets.env.height, presets.env.dist);
382
+ camera.lookAt(0, 0.1, 0);
383
+ composer.render(0);
384
+ };
385
+ const dispose = () => {
386
+ composer.dispose();
387
+ for (const effect of effects)
388
+ effect.dispose?.();
389
+ for (const object of [hero, satellite, orb, ring]) {
390
+ object.geometry.dispose();
391
+ /** @type {any} */ (object.material).dispose();
392
+ }
393
+ floor.geometry.dispose();
394
+ /** @type {any} */ (floor.material).dispose();
395
+ renderer.dispose();
396
+ renderer.forceContextLoss();
397
+ };
398
+ return { render, dispose };
399
+ };
400
+ /** @type {import("../../runtime/mount.js").CreateScene} */
401
+ export function createScene(root, config, ctx) {
402
+ const inner = mountTimeline(root, config, ctx, { baseDelay: 0.2, staggerFrames: 5, rise: 36 });
403
+ const box = /** @type {HTMLElement | null} */ (root.querySelector(".mk-scene3d"));
404
+ const canvas = /** @type {HTMLCanvasElement | null} */ (root.querySelector("canvas.mk-gl"));
405
+ const duration = ctx.duration || Number(root.dataset.duration) || DEFAULT_DURATION;
406
+ const env = pick(ENV, box?.dataset.hdri) ?? ENV.neutral;
407
+ const presets = {
408
+ post: pick(POST, box?.dataset.post) ?? POST.cinematic,
409
+ light: pick(LIGHT, box?.dataset.light) ?? LIGHT.studio,
410
+ material: pick(MATERIAL, box?.dataset.material) ?? MATERIAL.clay,
411
+ model: box?.dataset.model ?? "knot",
412
+ env,
413
+ bg: env.bg,
414
+ };
415
+ /** @type {{ render: (t: number) => void, dispose: () => void } | null} */
416
+ let stage = null;
417
+ if (canvas !== null) {
418
+ try {
419
+ stage = glStage(canvas, root, presets, duration);
420
+ }
421
+ catch {
422
+ stage = flatStage(canvas, presets, duration);
423
+ }
424
+ }
425
+ /** @param {number} t */
426
+ const seek = (t) => {
427
+ inner.seek(t);
428
+ stage?.render(t);
429
+ };
430
+ const dispose = () => {
431
+ inner.dispose();
432
+ stage?.dispose();
433
+ };
434
+ seek(0);
435
+ return { seek, dispose, ready: inner.ready };
436
+ }
@@ -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;