@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,147 @@
1
+ import { z } from "zod";
2
+ import { defineScene } from "../../define-scene.js";
3
+ import { animAttrs, esc } from "../../html.js";
4
+ import { frameCss, frameOf } from "../../shared/frame.js";
5
+ import { sceneIcon } from "../../shared/icon.js";
6
+ import { textWidth } from "../../shared/measure.js";
7
+ import { readText } from "../../text-file.js";
8
+ import { limitWords, present, text } from "../../words.js";
9
+ const COUNT_MESSAGE = "need 2-6 steps";
10
+ const CHECK_SIZE = 56;
11
+ const stepSchema = z.strictObject({
12
+ label: text,
13
+ value: text.optional(),
14
+ detail: text.optional(),
15
+ });
16
+ const configSchema = limitWords(z
17
+ .strictObject({
18
+ title: text.optional(),
19
+ steps: z.array(stepSchema).min(2, COUNT_MESSAGE).max(6, COUNT_MESSAGE),
20
+ layout: z.enum(["path", "ladder", "stairs"]).default("path"),
21
+ active: z.number().int().min(1).optional(),
22
+ })
23
+ .superRefine((props, ctx) => {
24
+ if (props.active !== undefined && props.active > props.steps.length) {
25
+ ctx.addIssue({
26
+ code: "custom",
27
+ path: ["active"],
28
+ message: `must be 1-${props.steps.length}`,
29
+ });
30
+ }
31
+ }), (props) => props.steps
32
+ .flatMap((step) => present(step.label, step.value, step.detail))
33
+ .concat(present(props.title)));
34
+ /** Progress state of step `index` (0-based) against the 1-based `active` step. */
35
+ const stateOf = (index, active) => {
36
+ if (active === undefined)
37
+ return "on";
38
+ if (index + 1 < active)
39
+ return "done";
40
+ return index + 1 === active ? "active" : "todo";
41
+ };
42
+ const textBlock = (step) => {
43
+ const value = step.value ? `\n<p class="mk-value">${esc(step.value)}</p>` : "";
44
+ const detail = step.detail ? `\n<p class="mk-detail">${esc(step.detail)}</p>` : "";
45
+ return `<p class="mk-label">${esc(step.label)}</p>${value}${detail}`;
46
+ };
47
+ /** Marker of a path step: a check for finished steps, otherwise its number. */
48
+ const marker = (index, state) => state === "done"
49
+ ? sceneIcon("check", { at: `props.steps[${index}]`, size: CHECK_SIZE })
50
+ : String(index + 1);
51
+ const item = (props, step, index, offset) => {
52
+ const state = stateOf(index, props.active);
53
+ const rank = ((index + 1) / props.steps.length).toFixed(3);
54
+ const attrs = animAttrs("rise", index + offset, `step-${index + 1}`);
55
+ const open = `<li class="mk-step mk-${state}" style="--rank:${rank}" ${attrs}>`;
56
+ if (props.layout === "path") {
57
+ return `${open}\n<span class="mk-marker">${marker(index, state)}</span>\n${textBlock(step)}\n</li>`;
58
+ }
59
+ if (props.layout === "stairs") {
60
+ return `${open}\n<div class="mk-tread">${textBlock(step)}</div>\n<span class="mk-block">${index + 1}</span>\n</li>`;
61
+ }
62
+ return `${open}\n${textBlock(step)}\n</li>`;
63
+ };
64
+ /** Tall frames: the ladder becomes ascending rows (label left, value right); words never break. */
65
+ const TALL_CSS = `#scene-root .mk-label, #scene-root .mk-value, #scene-root .mk-detail { overflow-wrap: normal; hyphens: none; }
66
+ #scene-root .mk-ladder { flex-direction: column; align-items: flex-start; justify-content: center; height: auto; gap: var(--mk-space-md); }
67
+ #scene-root .mk-ladder .mk-step { width: calc(62% + 38% * var(--rank)); height: auto; flex: none; display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; column-gap: var(--mk-space-lg); row-gap: var(--mk-space-xs); padding: var(--mk-space-lg) var(--mk-space-xl); border-top-width: var(--mk-stroke-card, 2px); border-left: 10px solid var(--mk-color-accent); }
68
+ #scene-root .mk-ladder .mk-todo { border-left-color: var(--mk-color-divider); }
69
+ #scene-root .mk-ladder .mk-label { grid-column: 1; }
70
+ #scene-root .mk-ladder .mk-value { grid-column: 2; grid-row: 1 / span 2; }
71
+ #scene-root .mk-ladder .mk-detail { grid-column: 1; }`;
72
+ /** Square: four rows must fit the height, so type and padding shrink one step. */
73
+ const SQUARE_CSS = `#scene-root { gap: var(--mk-space-lg); }
74
+ #scene-root .mk-ladder { gap: var(--mk-space-sm); }
75
+ #scene-root .mk-ladder .mk-step { padding: var(--mk-space-md) var(--mk-space-lg); }
76
+ #scene-root .mk-ladder .mk-label { font-size: var(--mk-font-size-md); }
77
+ #scene-root .mk-ladder .mk-value { font-size: var(--mk-font-size-lg); }`;
78
+ /** Landscape has room: type grows as the step count falls (details stay above the 28px floor). */
79
+ const LANDSCAPE_CSS = `#scene-root .mk-n2 .mk-label, #scene-root .mk-n3 .mk-label { font-size: var(--mk-font-size-xl); }
80
+ #scene-root .mk-n2 .mk-value, #scene-root .mk-n3 .mk-value { font-size: var(--mk-font-size-title); }
81
+ #scene-root .mk-n2 .mk-detail, #scene-root .mk-n3 .mk-detail { font-size: var(--mk-font-size-lg); line-height: 1.25; }
82
+ #scene-root .mk-n4 .mk-value { font-size: var(--mk-font-size-title); }
83
+ #scene-root .mk-n4 .mk-detail { font-size: var(--mk-font-size-md); line-height: 1.3; }
84
+ #scene-root .mk-path { margin-top: var(--mk-space-xl); }
85
+ #scene-root .mk-path .mk-step { gap: var(--mk-space-lg); }`;
86
+ const FIT_SIDES = 0.84;
87
+ const FIT_MARGIN = 0.92;
88
+ const FALLBACK_GAP_PX = 32;
89
+ /** Largest step whose longest text fits one column (themes scale their steps differently). */
90
+ const fitStep = (ctx, texts, steps, width) => steps.find((step) => texts.every((text) => textWidth(text, ctx.theme.fontSizes[step] ?? 0) <= width * FIT_MARGIN)) ??
91
+ steps[steps.length - 1] ??
92
+ "sm";
93
+ /** Tall frames with 4+ columns of path/stairs: type steps down until each word fits its column. */
94
+ const columnCss = (props, ctx, root) => {
95
+ const n = props.steps.length;
96
+ if (props.layout === "ladder" || n < 4 || frameOf(ctx) === "landscape")
97
+ return "";
98
+ const gap = props.layout === "stairs" ? 0 : (ctx.theme.spacing.lg ?? FALLBACK_GAP_PX);
99
+ const col = (ctx.width * FIT_SIDES - gap * (n - 1)) / n;
100
+ const label = fitStep(ctx, props.steps.map((step) => step.label), ["lg", "md", "sm"], col);
101
+ const value = fitStep(ctx, props.steps.flatMap((step) => (step.value ? [step.value] : [])), ["xl", "lg", "md", "sm"], col);
102
+ return [
103
+ `${root} .mk-steps .mk-label { font-size: var(--mk-font-size-${label}); }`,
104
+ `${root} .mk-steps .mk-value { font-size: var(--mk-font-size-${value}); }`,
105
+ "",
106
+ ].join("\n");
107
+ };
108
+ const render = (props) => {
109
+ const offset = props.title ? 1 : 0;
110
+ const head = props.title
111
+ ? `<div class="mk-head">
112
+ <h1 class="mk-heading" ${animAttrs("rise", 0, "title")}>${esc(props.title)}</h1>
113
+ <span class="mk-rule" ${animAttrs("grow", 0, "title")}></span>
114
+ </div>
115
+ `
116
+ : "";
117
+ const steps = props.steps.map((step, index) => item(props, step, index, offset));
118
+ const cls = `mk-steps mk-${props.layout} mk-n${props.steps.length}`;
119
+ return `${head}<ol class="${cls}">\n${steps.join("\n")}\n</ol>`;
120
+ };
121
+ export const stepsScene = defineScene({
122
+ type: "steps",
123
+ configSchema,
124
+ meta: {
125
+ purpose: "Ordered progression of 2-6 steps: a process path, or ascending ladder/stairs tiers with value labels (pricing, levels).",
126
+ example: {
127
+ title: "Plans",
128
+ layout: "ladder",
129
+ steps: [
130
+ { label: "Starter", value: "$0" },
131
+ { label: "Team", value: "$29" },
132
+ { label: "Scale", value: "$99" },
133
+ ],
134
+ },
135
+ },
136
+ style: readText("./style.css", import.meta.url),
137
+ script: readText("./scene.js", import.meta.url),
138
+ render: (props, ctx, root) => ({
139
+ body: render(props),
140
+ css: frameOf(ctx) === "landscape"
141
+ ? `
142
+ /* landscape */
143
+ ${LANDSCAPE_CSS.replaceAll("#scene-root", root)}
144
+ `
145
+ : frameCss(ctx, root, { tall: TALL_CSS, square: SQUARE_CSS }) + columnCss(props, ctx, root),
146
+ }),
147
+ });
@@ -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: 36 });
6
+ }
@@ -0,0 +1,190 @@
1
+ #scene-root {
2
+ gap: var(--mk-space-xxl);
3
+ }
4
+ #scene-root .mk-head {
5
+ display: flex;
6
+ flex-direction: column;
7
+ align-items: flex-start;
8
+ gap: var(--mk-space-md);
9
+ }
10
+ #scene-root .mk-heading {
11
+ font-family: var(--mk-font-display, var(--mk-font-sans));
12
+ font-size: var(--mk-font-size-title);
13
+ font-weight: 800;
14
+ line-height: 1.04;
15
+ letter-spacing: -0.03em;
16
+ text-wrap: balance;
17
+ }
18
+ #scene-root .mk-steps {
19
+ list-style: none;
20
+ padding: 0;
21
+ display: flex;
22
+ gap: var(--mk-space-lg);
23
+ }
24
+ #scene-root .mk-label {
25
+ font-size: var(--mk-font-size-lg);
26
+ font-weight: 700;
27
+ line-height: 1.1;
28
+ letter-spacing: -0.01em;
29
+ text-wrap: balance;
30
+ overflow-wrap: break-word;
31
+ }
32
+ #scene-root .mk-value {
33
+ font-family: var(--mk-font-display, var(--mk-font-sans));
34
+ font-size: var(--mk-font-size-xl);
35
+ font-weight: 800;
36
+ line-height: 1;
37
+ letter-spacing: -0.03em;
38
+ font-variant-numeric: var(--mk-numeric, tabular-nums);
39
+ color: var(--mk-color-accent);
40
+ }
41
+ #scene-root .mk-detail {
42
+ font-size: var(--mk-font-size-sm);
43
+ line-height: 1.3;
44
+ color: var(--mk-color-muted);
45
+ text-wrap: pretty;
46
+ }
47
+ #scene-root .mk-n5 .mk-label,
48
+ #scene-root .mk-n6 .mk-label {
49
+ font-size: var(--mk-font-size-md);
50
+ }
51
+ #scene-root .mk-n5 .mk-value,
52
+ #scene-root .mk-n6 .mk-value {
53
+ font-size: var(--mk-font-size-lg);
54
+ }
55
+
56
+ /* path: numbered nodes on a connector */
57
+ #scene-root .mk-path .mk-step {
58
+ position: relative;
59
+ flex: 1;
60
+ min-width: 0;
61
+ display: flex;
62
+ flex-direction: column;
63
+ align-items: center;
64
+ gap: var(--mk-space-md);
65
+ text-align: center;
66
+ }
67
+ #scene-root .mk-path .mk-step::before {
68
+ content: "";
69
+ position: absolute;
70
+ top: 52px;
71
+ right: calc(50% + 80px);
72
+ width: calc(100% + var(--mk-space-lg) - 160px);
73
+ height: 8px;
74
+ border-radius: var(--mk-radius-full);
75
+ background: var(--mk-color-accent);
76
+ }
77
+ #scene-root .mk-path .mk-step:first-child::before {
78
+ display: none;
79
+ }
80
+ #scene-root .mk-path .mk-todo::before {
81
+ background: var(--mk-color-divider);
82
+ }
83
+ #scene-root .mk-path .mk-marker {
84
+ position: relative;
85
+ z-index: 1;
86
+ display: grid;
87
+ place-items: center;
88
+ width: 112px;
89
+ height: 112px;
90
+ margin-bottom: var(--mk-space-md);
91
+ border-radius: var(--mk-radius-full);
92
+ background: var(--mk-color-accent);
93
+ color: var(--mk-color-onAccent);
94
+ font-size: var(--mk-font-size-lg);
95
+ font-weight: 800;
96
+ font-variant-numeric: var(--mk-numeric, tabular-nums);
97
+ }
98
+ #scene-root .mk-path .mk-active .mk-marker {
99
+ box-shadow:
100
+ 0 0 0 10px var(--mk-color-bg),
101
+ 0 0 0 18px var(--mk-color-accent);
102
+ }
103
+ #scene-root .mk-path .mk-todo .mk-marker {
104
+ background: var(--mk-color-card);
105
+ border: 6px solid var(--mk-color-divider);
106
+ color: var(--mk-color-muted);
107
+ }
108
+ #scene-root .mk-path .mk-todo .mk-label {
109
+ color: var(--mk-color-muted);
110
+ }
111
+
112
+ /* ladder: ascending tier cards, value labels */
113
+ #scene-root .mk-ladder {
114
+ align-items: flex-end;
115
+ height: 640px;
116
+ }
117
+ #scene-root .mk-ladder .mk-step {
118
+ flex: 1;
119
+ min-width: 0;
120
+ display: flex;
121
+ flex-direction: column;
122
+ justify-content: flex-start;
123
+ gap: var(--mk-space-md);
124
+ height: calc(34% + 66% * var(--rank));
125
+ padding: var(--mk-space-xl);
126
+ background: var(--mk-color-card);
127
+ border: var(--mk-stroke-card, 2px) solid var(--mk-color-divider);
128
+ border-top: 10px solid var(--mk-color-accent);
129
+ border-radius: var(--mk-radius-xl);
130
+ box-shadow: var(
131
+ --mk-shadow-card,
132
+ 0 24px 56px -28px color-mix(in srgb, var(--mk-color-fg) 40%, transparent)
133
+ );
134
+ }
135
+ #scene-root .mk-ladder .mk-active {
136
+ background: var(--mk-color-accent);
137
+ border-color: var(--mk-color-accent);
138
+ color: var(--mk-color-onAccent);
139
+ }
140
+ #scene-root .mk-ladder .mk-active .mk-value,
141
+ #scene-root .mk-ladder .mk-active .mk-detail {
142
+ color: var(--mk-color-onAccent);
143
+ }
144
+ #scene-root .mk-ladder .mk-todo {
145
+ border-top-color: var(--mk-color-divider);
146
+ }
147
+
148
+ /* stairs: treads with the label on top of each rising block */
149
+ #scene-root .mk-stairs {
150
+ align-items: flex-end;
151
+ gap: 0;
152
+ height: 700px;
153
+ }
154
+ #scene-root .mk-stairs .mk-step {
155
+ flex: 1;
156
+ min-width: 0;
157
+ display: flex;
158
+ flex-direction: column;
159
+ justify-content: flex-end;
160
+ }
161
+ #scene-root .mk-stairs .mk-tread {
162
+ display: flex;
163
+ flex-direction: column;
164
+ gap: var(--mk-space-sm);
165
+ padding: 0 var(--mk-space-md) var(--mk-space-lg);
166
+ }
167
+ #scene-root .mk-stairs .mk-block {
168
+ display: flex;
169
+ align-items: flex-start;
170
+ justify-content: flex-start;
171
+ height: calc(120px + var(--rank) * 280px);
172
+ padding: var(--mk-space-lg);
173
+ border-radius: var(--mk-radius-lg) var(--mk-radius-lg) 0 0;
174
+ background: var(--mk-color-accent);
175
+ color: var(--mk-color-onAccent);
176
+ font-size: var(--mk-font-size-xl);
177
+ font-weight: 800;
178
+ line-height: 1;
179
+ font-variant-numeric: var(--mk-numeric, tabular-nums);
180
+ }
181
+ #scene-root .mk-stairs .mk-active .mk-block {
182
+ background: var(--mk-color-accent2);
183
+ color: var(--mk-color-onAccent2);
184
+ }
185
+ #scene-root .mk-stairs .mk-todo .mk-block {
186
+ background: var(--mk-color-card);
187
+ border: var(--mk-stroke-card, 2px) solid var(--mk-color-divider);
188
+ border-bottom: 0;
189
+ color: var(--mk-color-muted);
190
+ }
@@ -0,0 +1 @@
1
+ export declare const stickerScene: import("@anvpx/motion-schema").ScenePlugin;
@@ -0,0 +1,48 @@
1
+ import { z } from "zod";
2
+ import { defineScene } from "../../define-scene.js";
3
+ import { esc } from "../../html.js";
4
+ import { textWidth } from "../../shared/measure.js";
5
+ import { readText } from "../../text-file.js";
6
+ import { limitWords, text } from "../../words.js";
7
+ const MAX_WORDS = 5;
8
+ const MAX_CHARS = 16;
9
+ /** Share of the frame a sticker may fill. */
10
+ const FILL_WIDTH = 0.86;
11
+ const FILL_HEIGHT = 0.8;
12
+ /** Theme font steps, largest first; every one is >= 28 px. */
13
+ const STEPS = ["hero", "heroMd", "heroSm", "display", "title", "xl", "lg"];
14
+ const configSchema = limitWords(z.strictObject({
15
+ words: z
16
+ .array(text.max(MAX_CHARS))
17
+ .min(1, `need 1-${MAX_WORDS} words`)
18
+ .max(MAX_WORDS, `need 1-${MAX_WORDS} words`),
19
+ burst: z.boolean().default(true),
20
+ }), (props) => props.words);
21
+ const px = (ctx, step) => ctx.theme.fontSizes[step] ?? 0;
22
+ /** Largest step at which every word fits the frame. */
23
+ const largest = (ctx, fits) => STEPS.find((step) => px(ctx, step) > 0 && fits(px(ctx, step))) ?? "lg";
24
+ const render = (props, ctx) => {
25
+ const maxWidth = ctx.width * FILL_WIDTH;
26
+ const step = largest(ctx, (size) => props.words.every((word) => textWidth(word, size) <= maxWidth) &&
27
+ props.words.length * size * 1.2 <= ctx.height * FILL_HEIGHT);
28
+ const stickers = props.words.map((word, index) => {
29
+ const tilt = index % 2 === 0 ? -4 : 3;
30
+ return `<p class="mk-sticker mk-sz-${step}" data-i="${index}" data-tilt="${tilt}">${esc(word)}</p>`;
31
+ });
32
+ const burst = props.burst ? '<div class="mk-burst" aria-hidden="true"></div>' : "";
33
+ return `<div class="mk-stage">
34
+ ${burst}
35
+ ${stickers.join("\n")}
36
+ </div>`;
37
+ };
38
+ export const stickerScene = defineScene({
39
+ type: "sticker",
40
+ configSchema,
41
+ meta: {
42
+ purpose: "Pop-comic sticker slam: 1-5 short phrases slam in with outline type, tilt and a starburst; for hooks, myth-vs-fact and punchy shorts.",
43
+ example: { words: ["Myth", "Busted"], burst: true },
44
+ },
45
+ style: readText("./style.css", import.meta.url),
46
+ script: readText("./scene.js", import.meta.url),
47
+ render: (props, ctx) => ({ body: render(props, ctx) }),
48
+ });
@@ -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,57 @@
1
+ // @ts-check
2
+ import { clamp01, ease } from "../../runtime/easing.js";
3
+ import { mountTimeline } from "../../runtime/mount.js";
4
+ const IN_SECONDS = 0.3;
5
+ const IN_SHARE = 0.45;
6
+ const SLAM_FROM = 1.8;
7
+ const SETTLE_SPIN = 8;
8
+ /**
9
+ * @param {number} t
10
+ * @param {number} from
11
+ * @param {number} span
12
+ */
13
+ const within = (t, from, span) => clamp01(span > 0 ? (t - from) / span : t >= from ? 1 : 0);
14
+ /** @type {import("../../runtime/mount.js").CreateScene} */
15
+ export function createScene(root, config, ctx) {
16
+ const inner = mountTimeline(root, config, ctx, { baseDelay: 0.1, staggerFrames: 4, rise: 0 });
17
+ const words = /** @type {HTMLElement[]} */ (Array.from(root.querySelectorAll(".mk-sticker")));
18
+ const count = words.length;
19
+ const duration = ctx.duration ?? (Number(root.dataset.duration) || 6);
20
+ const beat = duration / Math.max(count, 1);
21
+ const inSpan = Math.min(IN_SECONDS, beat * IN_SHARE);
22
+ /**
23
+ * @param {HTMLElement} el
24
+ * @param {number} i
25
+ * @param {number} t
26
+ */
27
+ const slam = (el, i, t) => {
28
+ if (t < i * beat) {
29
+ el.style.opacity = "0";
30
+ el.style.transform = "";
31
+ return;
32
+ }
33
+ const tilt = Number(el.dataset.tilt ?? 0);
34
+ const local = within(t, i * beat, inSpan);
35
+ const eased = ease("spring-snappy", local);
36
+ const scale = 1 + (SLAM_FROM - 1) * (1 - eased);
37
+ const spin = tilt + SETTLE_SPIN * (1 - eased);
38
+ el.style.opacity = "1";
39
+ el.style.transform = `scale(${scale}) rotate(${spin}deg)`;
40
+ };
41
+ /** @param {number} t */
42
+ const seek = (t) => {
43
+ inner.seek(t);
44
+ words.forEach((el, i) => {
45
+ slam(el, i, t);
46
+ });
47
+ };
48
+ const dispose = () => {
49
+ inner.dispose();
50
+ for (const el of words) {
51
+ el.style.removeProperty("opacity");
52
+ el.style.removeProperty("transform");
53
+ }
54
+ };
55
+ seek(0);
56
+ return { seek, dispose, ready: inner.ready };
57
+ }
@@ -0,0 +1,88 @@
1
+ #scene-root {
2
+ align-items: center;
3
+ text-align: center;
4
+ }
5
+ #scene-root .mk-stage {
6
+ flex: 1;
7
+ width: 100%;
8
+ min-height: 0;
9
+ display: grid;
10
+ place-items: center;
11
+ position: relative;
12
+ overflow: hidden;
13
+ }
14
+ #scene-root .mk-burst {
15
+ grid-area: 1 / 1;
16
+ width: 130vmin;
17
+ height: 130vmin;
18
+ background: var(--mk-color-accentSoft);
19
+ clip-path: polygon(
20
+ 50% 0%,
21
+ 55% 12%,
22
+ 68% 6%,
23
+ 68% 20%,
24
+ 82% 18%,
25
+ 78% 31%,
26
+ 92% 34%,
27
+ 84% 45%,
28
+ 97% 52%,
29
+ 85% 59%,
30
+ 94% 71%,
31
+ 80% 72%,
32
+ 83% 86%,
33
+ 69% 83%,
34
+ 65% 97%,
35
+ 55% 87%,
36
+ 50% 100%,
37
+ 45% 87%,
38
+ 35% 97%,
39
+ 31% 83%,
40
+ 17% 86%,
41
+ 20% 72%,
42
+ 6% 71%,
43
+ 15% 59%,
44
+ 3% 52%,
45
+ 16% 45%,
46
+ 8% 34%,
47
+ 22% 31%,
48
+ 18% 18%,
49
+ 32% 20%,
50
+ 32% 6%,
51
+ 45% 12%
52
+ );
53
+ opacity: 0.35;
54
+ }
55
+ #scene-root .mk-sticker {
56
+ grid-area: 1 / 1;
57
+ font-family: var(--mk-font-display, var(--mk-font-sans));
58
+ font-weight: 900;
59
+ line-height: 1.1;
60
+ letter-spacing: -0.02em;
61
+ white-space: nowrap;
62
+ color: var(--mk-color-fg);
63
+ -webkit-text-stroke: 0.035em var(--mk-color-accent);
64
+ paint-order: stroke fill;
65
+ opacity: 0;
66
+ will-change: transform, opacity;
67
+ }
68
+ #scene-root .mk-sz-hero {
69
+ font-size: var(--mk-font-size-hero);
70
+ }
71
+ #scene-root .mk-sz-heroMd {
72
+ font-size: var(--mk-font-size-heroMd);
73
+ }
74
+ #scene-root .mk-sz-heroSm {
75
+ font-size: var(--mk-font-size-heroSm);
76
+ }
77
+ #scene-root .mk-sz-display {
78
+ font-size: var(--mk-font-size-display);
79
+ }
80
+ #scene-root .mk-sz-title {
81
+ font-size: var(--mk-font-size-title);
82
+ }
83
+ #scene-root .mk-sz-xl {
84
+ font-size: var(--mk-font-size-xl);
85
+ }
86
+ #scene-root .mk-sz-lg {
87
+ font-size: var(--mk-font-size-lg);
88
+ }
@@ -0,0 +1 @@
1
+ export declare const tableScene: import("@anvpx/motion-schema").ScenePlugin;
@@ -0,0 +1,131 @@
1
+ import { z } from "zod";
2
+ import { defineScene } from "../../define-scene.js";
3
+ import { animAttrs, esc } from "../../html.js";
4
+ import { textWidth } from "../../shared/measure.js";
5
+ import { readText } from "../../text-file.js";
6
+ import { limitWords, present, text } from "../../words.js";
7
+ const COLUMNS_MESSAGE = "need 2-5 columns";
8
+ const ROWS_MESSAGE = "need 2-8 rows";
9
+ const HEADER_MAX = 32;
10
+ const CELL_MAX = 40;
11
+ const CELL_PX = 32;
12
+ const RANK_COL_PX = 96;
13
+ const WEIGHT_PAD_PX = 160;
14
+ const ROOMY_PX = 48;
15
+ const ROOMY_MAX_ROWS = 5;
16
+ const COL_GAP_PX = 32;
17
+ const TABLE_PAD_PX = 2 * 64 + 4;
18
+ const SAFE_SIDES = 0.1;
19
+ const cell = z.union([text.max(CELL_MAX), z.number()]);
20
+ const configSchema = limitWords(z
21
+ .strictObject({
22
+ title: text.optional(),
23
+ columns: z.array(text.max(HEADER_MAX)).min(2, COLUMNS_MESSAGE).max(5, COLUMNS_MESSAGE),
24
+ rows: z.array(z.array(cell)).min(2, ROWS_MESSAGE).max(8, ROWS_MESSAGE),
25
+ rank: z.boolean().optional(),
26
+ highlight: z.number().int().min(0).optional(),
27
+ align: z
28
+ .array(z.enum(["l", "r"]))
29
+ .max(5)
30
+ .optional(),
31
+ })
32
+ .superRefine((props, ctx) => {
33
+ props.rows.forEach((row, index) => {
34
+ if (row.length !== props.columns.length) {
35
+ ctx.addIssue({
36
+ code: "custom",
37
+ path: ["rows", index],
38
+ message: `has ${row.length} cells; needs ${props.columns.length} (one per column)`,
39
+ });
40
+ }
41
+ });
42
+ if (props.highlight !== undefined && props.highlight >= props.rows.length) {
43
+ ctx.addIssue({
44
+ code: "custom",
45
+ path: ["highlight"],
46
+ message: `highlight ${props.highlight} is outside rows (0-${props.rows.length - 1})`,
47
+ });
48
+ }
49
+ if ((props.align?.length ?? 0) > props.columns.length) {
50
+ ctx.addIssue({
51
+ code: "custom",
52
+ path: ["align"],
53
+ message: "more align entries than columns",
54
+ });
55
+ }
56
+ }), (props) => [...present(props.title), ...props.columns, ...props.rows.flat().map(String)]);
57
+ /** A number with optional currency, sign, separators and a short suffix ("%", "K", "x"). */
58
+ const NUMERIC = /^[^\p{L}\d]{0,2}\d[\d,.]*\s?(?:%|[KMBkmbx])?$/u;
59
+ const isNumeric = (value) => typeof value === "number" || NUMERIC.test(value);
60
+ /** Per column: right for numbers, left for text, unless `align` says otherwise. */
61
+ const alignments = (props) => props.columns.map((_, col) => {
62
+ const given = props.align?.[col];
63
+ if (given !== undefined)
64
+ return given;
65
+ return props.rows.every((row) => isNumeric(row[col] ?? "")) ? "r" : "l";
66
+ });
67
+ const widest = (props, col, px) => Math.max(textWidth(props.columns[col] ?? "", CELL_PX), ...props.rows.map((row) => textWidth(String(row[col] ?? ""), CELL_PX))) *
68
+ (px / CELL_PX);
69
+ /** Column weights follow the widest text in each column, so wide content gets wide columns. */
70
+ const template = (props) => {
71
+ const cols = props.columns.map((_, col) => `minmax(0, ${Math.round(widest(props, col, CELL_PX) + WEIGHT_PAD_PX)}fr)`);
72
+ return (props.rank ? [`${RANK_COL_PX}px`, ...cols] : cols).join(" ");
73
+ };
74
+ /** Few rows with short text get the larger size, which fills the frame better. */
75
+ const isRoomy = (props, ctx) => {
76
+ if (props.rows.length > ROOMY_MAX_ROWS)
77
+ return false;
78
+ const text = props.columns.reduce((sum, _, col) => sum + widest(props, col, ROOMY_PX), 0);
79
+ const fixed = (props.columns.length - 1) * COL_GAP_PX + TABLE_PAD_PX;
80
+ const rank = props.rank ? RANK_COL_PX + COL_GAP_PX : 0;
81
+ return text + fixed + rank <= ctx.width * (1 - SAFE_SIDES);
82
+ };
83
+ const cellHtml = (value, role, side) => `<span role="${role}" class="mk-cell mk-${side}">${esc(value)}</span>`;
84
+ const render = (props, ctx) => {
85
+ const offset = props.title ? 1 : 0;
86
+ const sides = alignments(props);
87
+ const head = props.title
88
+ ? `<div class="mk-head">
89
+ <h1 class="mk-heading" ${animAttrs("rise", 0, "title")}>${esc(props.title)}</h1>
90
+ <span class="mk-rule" ${animAttrs("grow", 0, "title")}></span>
91
+ </div>
92
+ `
93
+ : "";
94
+ const rankHead = props.rank ? `<span role="columnheader" class="mk-cell mk-rankh">#</span>` : "";
95
+ const header = `<div role="row" class="mk-trow mk-thead" ${animAttrs("fade", offset)}>${rankHead}${props.columns
96
+ .map((column, col) => cellHtml(column, "columnheader", sides[col] ?? "l"))
97
+ .join("")}</div>`;
98
+ const rows = props.rows.map((row, index) => {
99
+ const cls = `mk-trow${index === props.highlight ? " mk-hi" : ""}`;
100
+ const attrs = animAttrs("rise", offset + 1 + index, `row-${index + 1}`);
101
+ const rank = props.rank
102
+ ? `<span role="cell" class="mk-cell mk-rank${index === 0 ? " mk-first" : ""}"><b>${index + 1}</b></span>`
103
+ : "";
104
+ const cells = row.map((value, col) => cellHtml(String(value), "cell", sides[col] ?? "l"));
105
+ return `<div role="row" class="${cls}" ${attrs}>${rank}${cells.join("")}</div>`;
106
+ });
107
+ return `${head}<div role="table" class="mk-table${isRoomy(props, ctx) ? " mk-roomy" : ""}" aria-label="${esc(props.title ?? props.columns.join(", "))}">\n${header}\n${rows.join("\n")}\n</div>`;
108
+ };
109
+ export const tableScene = defineScene({
110
+ type: "table",
111
+ configSchema,
112
+ meta: {
113
+ purpose: "Small data table or ranked list (2-5 columns, 2-8 rows) with row-by-row reveal and one optional highlighted row.",
114
+ example: {
115
+ title: "Top regions",
116
+ columns: ["Region", "Revenue"],
117
+ rows: [
118
+ ["North", "$4.2M"],
119
+ ["South", "$3.1M"],
120
+ ],
121
+ rank: true,
122
+ highlight: 0,
123
+ },
124
+ },
125
+ style: readText("./style.css", import.meta.url),
126
+ script: readText("./scene.js", import.meta.url),
127
+ render: (props, ctx, root) => ({
128
+ body: render(props, ctx),
129
+ css: `${root} .mk-trow { grid-template-columns: ${template(props)}; }\n`,
130
+ }),
131
+ });