@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,128 @@
1
+ import { contrastRatio } from "@anvpx/motion-themes";
2
+ import { z } from "zod";
3
+ import { defineScene } from "../../define-scene.js";
4
+ import { animAttrs, esc } from "../../html.js";
5
+ import { frameCss, frameOf } from "../../shared/frame.js";
6
+ import { sceneIcon } from "../../shared/icon.js";
7
+ import { readText } from "../../text-file.js";
8
+ import { limitWords, present, text } from "../../words.js";
9
+ const COUNT_MESSAGE = "need 1-6 items";
10
+ const MARKERS = ["dot", "check", "cross", "number"];
11
+ const ICON_PX = 48;
12
+ const ICON_OF = { check: "check", cross: "x" };
13
+ const configSchema = limitWords(z.strictObject({
14
+ title: text.optional(),
15
+ items: z.array(text).min(1, COUNT_MESSAGE).max(6, COUNT_MESSAGE),
16
+ marker: z.enum(MARKERS).optional(),
17
+ }), (props) => [...present(props.title), ...props.items]);
18
+ /** Marker-specific rules; absent marker keeps the original numbered look and adds nothing. */
19
+ const MIN_CONTRAST = 4.5;
20
+ /** First token (chart colour, then accent, then text) with 4.5:1 on the card; light themes keep their chart colour. */
21
+ const markerColour = (ctx, chart, chartVar) => {
22
+ const card = ctx.theme.colors.card ?? "#ffffff";
23
+ const candidates = [
24
+ [ctx.theme.chart[chart], chartVar],
25
+ [ctx.theme.colors.accent, "var(--mk-color-accent)"],
26
+ [ctx.theme.colors.fg, "var(--mk-color-fg)"],
27
+ ];
28
+ const pick = candidates.find(([hex]) => hex !== undefined && contrastRatio(hex, card) >= MIN_CONTRAST);
29
+ return (pick ?? candidates[0])[1];
30
+ };
31
+ /** Tall frames: the heading sits above the list instead of beside it. */
32
+ const TALL_CSS = `#scene-root { flex-direction: column; align-items: stretch; justify-content: center; gap: var(--mk-space-xl); }
33
+ #scene-root .mk-head { flex: none; }
34
+ #scene-root .mk-list { flex: none; }`;
35
+ /** Portrait: a long list gets roomier rows so it fills the tall frame instead of hugging the top. */
36
+ const PORTRAIT_MANY_PX = 56;
37
+ const portraitCss = (ctx) => [
38
+ "#scene-root .mk-list { gap: var(--mk-space-xl); }",
39
+ "#scene-root .mk-big .mk-item { padding-block: calc(var(--mk-space-xl) * 1.6); }",
40
+ "#scene-root .mk-list.mk-many { gap: var(--mk-space-lg); }",
41
+ `#scene-root .mk-list.mk-many .mk-item { grid-template-columns: 72px 1fr; padding: var(--mk-space-lg); font-size: var(--mk-font-size-${stepWithin(ctx, ["xl", "lg", "md"], PORTRAIT_MANY_PX)}); }`,
42
+ "#scene-root .mk-list.mk-many .mk-item::before, #scene-root .mk-list.mk-many .mk-item .mk-mark { width: 72px; height: 72px; }",
43
+ ].join("\n");
44
+ /** Square: heading over list leaves little height, so big rows keep regular spacing. */
45
+ const SQUARE_CSS = `#scene-root .mk-big { gap: var(--mk-space-lg); }
46
+ #scene-root .mk-big .mk-item { padding: var(--mk-space-lg); }`;
47
+ const markerCss = (root, marker, ctx) => {
48
+ const item = `${root} .mk-marker-${marker} .mk-item`;
49
+ if (marker === "number")
50
+ return `${item}::before { content: counter(item); }\n`;
51
+ if (marker === "dot") {
52
+ return `${item}::before { content: ""; width: 32px; height: 32px; justify-self: center; }\n`;
53
+ }
54
+ const colour = marker === "check"
55
+ ? markerColour(ctx, 2, "var(--mk-chart-3)")
56
+ : markerColour(ctx, 4, "var(--mk-chart-5)");
57
+ return [
58
+ `${item}::before { content: none; }`,
59
+ `${item} .mk-mark { display: grid; place-items: center; width: 88px; height: 88px; border-radius: var(--mk-radius-full); background: var(--mk-color-card); border: 4px solid ${colour}; color: ${colour}; }`,
60
+ "",
61
+ ].join("\n");
62
+ };
63
+ const markHtml = (props, index) => {
64
+ const marker = props.marker;
65
+ if (marker !== "check" && marker !== "cross")
66
+ return "";
67
+ const icon = sceneIcon(ICON_OF[marker], { at: `props.items[${index}]`, size: ICON_PX });
68
+ return `<span class="mk-mark" aria-hidden="true">${icon}</span>`;
69
+ };
70
+ /** Up to 3 brief points read as a big list; longer lists or text keep the regular size. */
71
+ const BIG_MAX_ITEMS = 3;
72
+ const BIG_MAX_CHARS = 84;
73
+ /** From this many items the rows compress so the list never leaves the frame. */
74
+ const MANY_ITEMS = 5;
75
+ /** Largest of the given type steps that stays within `maxPx` for this theme (themes scale their steps differently). */
76
+ const stepWithin = (ctx, steps, maxPx) => steps.find((step) => (ctx.theme.fontSizes[step] ?? 0) <= maxPx) ??
77
+ steps[steps.length - 1] ??
78
+ "md";
79
+ const BIG_MAX_PX = 80;
80
+ /** A square frame stacks heading over list in 1080px: big rows must stay smaller. */
81
+ const SQUARE_BIG_MAX_PX = 52;
82
+ const MANY_MAX_PX = 52;
83
+ const sizeCss = (root, ctx) => [
84
+ `${root} .mk-big .mk-item { font-size: var(--mk-font-size-${stepWithin(ctx, ["xl", "lg", "md"], frameOf(ctx) === "square" ? SQUARE_BIG_MAX_PX : BIG_MAX_PX)}); }`,
85
+ `${root} .mk-list.mk-many .mk-item { font-size: var(--mk-font-size-${stepWithin(ctx, ["lg", "md"], MANY_MAX_PX)}); }`,
86
+ "",
87
+ ].join("\n");
88
+ const isBig = (props) => props.items.length <= BIG_MAX_ITEMS &&
89
+ props.items.reduce((sum, item) => sum + item.length, 0) <= BIG_MAX_CHARS;
90
+ const render = (props) => {
91
+ const offset = props.title ? 1 : 0;
92
+ const head = props.title
93
+ ? `<div class="mk-head">
94
+ <h1 class="mk-heading" ${animAttrs("rise", 0, "title")}>${esc(props.title)}</h1>
95
+ <span class="mk-rule" ${animAttrs("grow", 0, "title")}></span>
96
+ </div>
97
+ `
98
+ : "";
99
+ const items = props.items.map((item, index) => {
100
+ const attrs = animAttrs("rise", index + offset, `item-${index + 1}`);
101
+ return `<li class="mk-item" ${attrs}>${markHtml(props, index)}${esc(item)}</li>`;
102
+ });
103
+ const listClass = [
104
+ "mk-list",
105
+ props.marker ? `mk-marker-${props.marker}` : "",
106
+ isBig(props) ? "mk-big" : "",
107
+ props.items.length >= MANY_ITEMS ? "mk-many" : "",
108
+ ]
109
+ .filter((name) => name !== "")
110
+ .join(" ");
111
+ return `${head}<ul class="${listClass}">\n${items.join("\n")}\n</ul>`;
112
+ };
113
+ export const bulletsScene = defineScene({
114
+ type: "bullets",
115
+ configSchema,
116
+ meta: {
117
+ purpose: "Short list of 1-6 points with an optional heading.",
118
+ example: { items: ["One", "Two"] },
119
+ },
120
+ style: readText("./style.css", import.meta.url),
121
+ script: readText("./scene.js", import.meta.url),
122
+ render: (props, ctx, root) => ({
123
+ body: render(props),
124
+ css: (props.marker ? markerCss(root, props.marker, ctx) : "") +
125
+ sizeCss(root, ctx) +
126
+ frameCss(ctx, root, { tall: TALL_CSS, portrait: portraitCss(ctx), square: SQUARE_CSS }),
127
+ }),
128
+ });
@@ -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: 5, rise: 32 });
6
+ }
@@ -0,0 +1,91 @@
1
+ #scene-root {
2
+ flex-direction: row;
3
+ align-items: center;
4
+ gap: var(--mk-space-xxl);
5
+ }
6
+ #scene-root .mk-head {
7
+ flex: 0 0 28%;
8
+ display: flex;
9
+ flex-direction: column;
10
+ align-items: flex-start;
11
+ gap: var(--mk-space-lg);
12
+ }
13
+ #scene-root .mk-heading {
14
+ font-family: var(--mk-font-display, var(--mk-font-sans));
15
+ font-size: var(--mk-font-size-title);
16
+ font-weight: 800;
17
+ line-height: 1.04;
18
+ letter-spacing: -0.03em;
19
+ text-wrap: balance;
20
+ }
21
+ #scene-root .mk-list {
22
+ flex: 1;
23
+ list-style: none;
24
+ padding: 0;
25
+ display: flex;
26
+ flex-direction: column;
27
+ gap: var(--mk-space-lg);
28
+ counter-reset: item;
29
+ }
30
+ #scene-root .mk-item {
31
+ counter-increment: item;
32
+ display: grid;
33
+ grid-template-columns: 88px 1fr;
34
+ align-items: center;
35
+ column-gap: var(--mk-space-lg);
36
+ padding: var(--mk-space-lg) var(--mk-space-xl) var(--mk-space-lg) var(--mk-space-lg);
37
+ font-size: var(--mk-font-size-lg);
38
+ font-weight: 600;
39
+ line-height: 1.25;
40
+ background: var(--mk-color-card);
41
+ border: var(--mk-stroke-card, 2px) solid var(--mk-color-divider);
42
+ border-radius: var(--mk-radius-xl);
43
+ box-shadow: var(
44
+ --mk-shadow-card,
45
+ 0 24px 56px -28px color-mix(in srgb, var(--mk-color-fg) 40%, transparent)
46
+ );
47
+ }
48
+ #scene-root .mk-item::before {
49
+ content: counter(item, decimal-leading-zero);
50
+ display: grid;
51
+ place-items: center;
52
+ width: 88px;
53
+ height: 88px;
54
+ border-radius: var(--mk-radius-full);
55
+ background: var(--mk-color-accent);
56
+ color: var(--mk-color-onAccent);
57
+ font-size: var(--mk-font-size-md);
58
+ font-weight: 700;
59
+ font-variant-numeric: var(--mk-numeric, tabular-nums);
60
+ }
61
+
62
+ /* short lists: larger type and air so 1-3 points fill the frame */
63
+ #scene-root .mk-big {
64
+ gap: var(--mk-space-xl);
65
+ }
66
+ #scene-root .mk-big .mk-item {
67
+ line-height: 1.15;
68
+ padding: var(--mk-space-xl) var(--mk-space-xl) var(--mk-space-xl) var(--mk-space-lg);
69
+ }
70
+
71
+ /* 5-6 points: tighter rows so long text still fits the frame */
72
+ #scene-root .mk-list.mk-many {
73
+ gap: var(--mk-space-md);
74
+ }
75
+ #scene-root .mk-list.mk-many .mk-item {
76
+ grid-template-columns: 64px 1fr;
77
+ padding: var(--mk-space-md) var(--mk-space-lg) var(--mk-space-md) var(--mk-space-md);
78
+ line-height: 1.15;
79
+ }
80
+ #scene-root .mk-list.mk-many .mk-item::before {
81
+ width: 64px;
82
+ height: 64px;
83
+ }
84
+ #scene-root .mk-list.mk-many .mk-item .mk-mark {
85
+ width: 64px;
86
+ height: 64px;
87
+ }
88
+ #scene-root .mk-list.mk-many .mk-item .mk-mark svg {
89
+ width: 36px;
90
+ height: 36px;
91
+ }
@@ -0,0 +1,9 @@
1
+ import { type AccentSegment } from "../../shared/markup.ts";
2
+ export interface Token {
3
+ readonly pieces: readonly AccentSegment[];
4
+ /** A bare operator such as `+`, `=` or `×`; dimmed so the terms carry the reading. */
5
+ readonly op: boolean;
6
+ }
7
+ /** Words of accent markup as tokens: split at spaces, adjacent pieces (`[c]²`) stay together. */
8
+ export declare const tokenize: (source: string) => Token[];
9
+ export declare const calloutScene: import("@anvpx/motion-schema").ScenePlugin;
@@ -0,0 +1,95 @@
1
+ import { z } from "zod";
2
+ import { defineScene } from "../../define-scene.js";
3
+ import { animAttrs, esc } from "../../html.js";
4
+ import { sceneIcon } from "../../shared/icon.js";
5
+ import { accentSegments, stripAccent } from "../../shared/markup.js";
6
+ import { readText } from "../../text-file.js";
7
+ import { limitWords, present, text } from "../../words.js";
8
+ const MAX_CHARS = 120;
9
+ const ICON_SIZE = 48;
10
+ const KINDS = ["formula", "statement", "definition", "warning", "tip"];
11
+ const KIND_ICON = {
12
+ formula: "sigma",
13
+ statement: "quote",
14
+ definition: "book-open",
15
+ warning: "triangle-alert",
16
+ tip: "lightbulb",
17
+ };
18
+ /** Text of at most this many characters (accent brackets excluded) gets the next size up. */
19
+ const SIZE_STEPS = {
20
+ formula: [16, 40],
21
+ statement: [28, 70],
22
+ definition: [28, 70],
23
+ warning: [28, 70],
24
+ tip: [28, 70],
25
+ };
26
+ const capped = (max) => text.max(max, `max ${max} characters`);
27
+ const configSchema = limitWords(z.strictObject({
28
+ text: capped(MAX_CHARS),
29
+ kind: z.enum(KINDS).default("statement"),
30
+ label: capped(30).optional(),
31
+ note: capped(80).optional(),
32
+ }), (props) => [...present(stripAccent(props.text), props.label, props.note)]);
33
+ const OPERATOR = /^[+\-−–×÷*/=<>≈≠≤≥·^%→⇒±∑∏√]+$/;
34
+ /** Words of accent markup as tokens: split at spaces, adjacent pieces (`[c]²`) stay together. */
35
+ export const tokenize = (source) => {
36
+ const tokens = [];
37
+ let pieces = [];
38
+ const flush = () => {
39
+ if (pieces.length === 0)
40
+ return;
41
+ const plain = pieces.every((p) => !p.accent);
42
+ const joined = pieces.map((p) => p.text).join("");
43
+ tokens.push({ pieces, op: plain && OPERATOR.test(joined) });
44
+ pieces = [];
45
+ };
46
+ for (const segment of accentSegments(source)) {
47
+ segment.text.split(/(\s+)/).forEach((part, index) => {
48
+ if (index % 2 === 1)
49
+ flush();
50
+ else if (part !== "")
51
+ pieces.push({ text: part, accent: segment.accent });
52
+ });
53
+ }
54
+ flush();
55
+ return tokens;
56
+ };
57
+ const termHtml = (token, index) => {
58
+ const inner = token.pieces
59
+ .map((p) => (p.accent ? `<span class="accent">${esc(p.text)}</span>` : esc(p.text)))
60
+ .join("");
61
+ return `<span class="mk-term${token.op ? " mk-op" : ""}" data-term="${index}">${inner}</span>`;
62
+ };
63
+ /** Size class by visible length so the text fills the card without crowding it. */
64
+ const sizeClass = (kind, visible) => {
65
+ const [big, mid] = SIZE_STEPS[kind];
66
+ if (visible <= big)
67
+ return "mk-size-1";
68
+ return visible <= mid ? "mk-size-2" : "mk-size-3";
69
+ };
70
+ const render = (props) => {
71
+ const plain = stripAccent(props.text);
72
+ const terms = tokenize(props.text).map(termHtml).join(" ");
73
+ const icon = sceneIcon(KIND_ICON[props.kind], { at: "props.kind", size: ICON_SIZE });
74
+ const label = props.label ? `<span class="mk-label">${esc(props.label)}</span>` : "";
75
+ const note = props.note
76
+ ? `<p class="mk-note" ${animAttrs("rise", 6, "note")}>${esc(props.note)}</p>`
77
+ : "";
78
+ return `<div class="mk-card mk-${props.kind} ${sizeClass(props.kind, plain.length)}">
79
+ <div class="mk-kind" ${animAttrs("rise", 0, "label")}><span class="mk-icon">${icon}</span>${label}</div>
80
+ <p class="mk-text" aria-label="${esc(plain)}">${terms}</p>
81
+ <span class="mk-rule" data-rule></span>
82
+ ${note}
83
+ </div>`;
84
+ };
85
+ export const calloutScene = defineScene({
86
+ type: "callout",
87
+ configSchema,
88
+ meta: {
89
+ purpose: "One formula, rule, definition, warning or tip made big; [x] marks accent terms, revealed term by term.",
90
+ example: { text: "[Profit] = [Revenue] − [Costs]", kind: "formula" },
91
+ },
92
+ style: readText("./style.css", import.meta.url),
93
+ script: readText("./scene.js", import.meta.url),
94
+ render: (props) => ({ body: render(props) }),
95
+ });
@@ -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,43 @@
1
+ // @ts-check
2
+ import { ease } from "../../runtime/easing.js";
3
+ import { mountTimeline } from "../../runtime/mount.js";
4
+ import { progress, revealProgress } from "../../shared/motion.js";
5
+ const DEFAULT_DURATION = 6;
6
+ const START = 0.25;
7
+ const MAX_SPAN = 2.4;
8
+ const SPAN_SHARE = 0.4;
9
+ /** Opacity of a term before its reveal, so the frame is never blank. */
10
+ const FLOOR = 0.12;
11
+ const RISE_PX = 24;
12
+ /** @type {import("../../runtime/mount.js").CreateScene} */
13
+ export function createScene(root, config, ctx) {
14
+ const inner = mountTimeline(root, config, ctx, { baseDelay: 0.1, staggerFrames: 5, rise: 28 });
15
+ const duration = ctx.duration || Number(root.dataset.duration) || DEFAULT_DURATION;
16
+ const span = Math.min(MAX_SPAN, duration * SPAN_SHARE);
17
+ const terms = /** @type {HTMLElement[]} */ (Array.from(root.querySelectorAll("[data-term]")));
18
+ const rule = /** @type {HTMLElement | null} */ (root.querySelector("[data-rule]"));
19
+ const dimmed = terms.map((el) => el.classList.contains("mk-op"));
20
+ /** @param {number} t */
21
+ const seek = (t) => {
22
+ inner.seek(t);
23
+ const shown = revealProgress(terms.length, Math.max(0, t - START), span);
24
+ terms.forEach((el, i) => {
25
+ const p = shown[i] ?? 0;
26
+ const target = dimmed[i] ? 0.45 : 1;
27
+ el.style.opacity = String(FLOOR + (target - FLOOR) * p);
28
+ el.style.transform = `translateY(${(1 - p) * RISE_PX}px)`;
29
+ });
30
+ if (rule)
31
+ rule.style.transform = `scaleX(${ease("out", progress(t, START, START + span))})`;
32
+ };
33
+ const dispose = () => {
34
+ inner.dispose();
35
+ for (const el of terms) {
36
+ el.style.removeProperty("opacity");
37
+ el.style.removeProperty("transform");
38
+ }
39
+ rule?.style.removeProperty("transform");
40
+ };
41
+ seek(0);
42
+ return { seek, dispose, ready: inner.ready };
43
+ }
@@ -0,0 +1,97 @@
1
+ #scene-root .mk-card {
2
+ --k: var(--mk-color-accent);
3
+ position: relative;
4
+ display: flex;
5
+ flex-direction: column;
6
+ align-items: flex-start;
7
+ gap: var(--mk-space-lg);
8
+ padding: var(--mk-space-xl) var(--mk-space-xxl);
9
+ background: var(--mk-color-card);
10
+ border: var(--mk-stroke-card, 2px) solid var(--mk-color-divider);
11
+ border-radius: var(--mk-radius-xl);
12
+ box-shadow:
13
+ inset 16px 0 0 var(--k),
14
+ 0 24px 56px -28px color-mix(in srgb, var(--mk-color-fg) 40%, transparent);
15
+ }
16
+ #scene-root .mk-warning {
17
+ --k: var(--mk-chart-2);
18
+ }
19
+ #scene-root .mk-tip {
20
+ --k: var(--mk-color-accent2);
21
+ }
22
+ #scene-root .mk-formula {
23
+ align-items: center;
24
+ text-align: center;
25
+ box-shadow:
26
+ inset 0 16px 0 var(--k),
27
+ 0 24px 56px -28px color-mix(in srgb, var(--mk-color-fg) 40%, transparent);
28
+ background: color-mix(in srgb, var(--mk-color-accentSoft) 45%, var(--mk-color-card));
29
+ }
30
+ #scene-root .mk-kind {
31
+ display: flex;
32
+ align-items: center;
33
+ gap: var(--mk-space-md);
34
+ color: var(--k);
35
+ }
36
+ #scene-root .mk-icon {
37
+ display: grid;
38
+ place-items: center;
39
+ width: 80px;
40
+ height: 80px;
41
+ border-radius: var(--mk-radius-full);
42
+ background: color-mix(in srgb, var(--k) 16%, var(--mk-color-card));
43
+ }
44
+ #scene-root .mk-label {
45
+ font-size: var(--mk-font-size-md);
46
+ font-weight: 800;
47
+ letter-spacing: 0.08em;
48
+ text-transform: uppercase;
49
+ }
50
+ #scene-root .mk-text {
51
+ font-family: var(--mk-font-display, var(--mk-font-sans));
52
+ font-weight: 800;
53
+ line-height: 1.12;
54
+ letter-spacing: -0.02em;
55
+ text-wrap: balance;
56
+ font-variant-numeric: var(--mk-numeric, tabular-nums);
57
+ }
58
+ #scene-root .mk-formula .mk-text {
59
+ font-weight: 700;
60
+ }
61
+ #scene-root .mk-definition .mk-text,
62
+ #scene-root .mk-tip .mk-text {
63
+ font-weight: 600;
64
+ }
65
+ #scene-root .mk-size-1 .mk-text {
66
+ font-size: var(--mk-font-size-display);
67
+ }
68
+ #scene-root .mk-size-2 .mk-text {
69
+ font-size: var(--mk-font-size-title);
70
+ }
71
+ #scene-root .mk-size-3 .mk-text {
72
+ font-size: var(--mk-font-size-xl);
73
+ }
74
+ #scene-root .mk-term {
75
+ display: inline-block;
76
+ }
77
+ #scene-root .mk-op {
78
+ color: var(--mk-color-muted);
79
+ font-weight: 500;
80
+ }
81
+ #scene-root .accent {
82
+ color: var(--k);
83
+ }
84
+ #scene-root .mk-rule {
85
+ background: linear-gradient(90deg, var(--k), var(--mk-color-accent2));
86
+ transform: scaleX(0);
87
+ }
88
+ #scene-root .mk-formula .mk-rule {
89
+ transform-origin: center;
90
+ }
91
+ #scene-root .mk-note {
92
+ font-size: var(--mk-font-size-md);
93
+ font-weight: 500;
94
+ line-height: 1.3;
95
+ color: var(--mk-color-muted);
96
+ max-width: 90%;
97
+ }
@@ -0,0 +1 @@
1
+ export declare const captionScene: 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, text } from "../../words.js";
6
+ /**
7
+ * One caption word on the clip clock (seconds). Same shape as the canonical
8
+ * `WordTiming` (`packages/schema/src/audio.ts`): `kit.reveal(words, t)` and
9
+ * voice word timings plug in with zero scene changes.
10
+ */
11
+ const captionWordSchema = z
12
+ .strictObject({
13
+ w: text.max(40),
14
+ start: z.number().min(0),
15
+ end: z.number().positive(),
16
+ })
17
+ .refine((word) => word.end > word.start, "end must be after start");
18
+ const captionWordsSchema = z
19
+ .array(captionWordSchema)
20
+ .min(1)
21
+ .max(40)
22
+ .superRefine((words, ctx) => {
23
+ words.forEach((word, index) => {
24
+ const prev = words[index - 1];
25
+ if (prev && word.start < prev.end) {
26
+ ctx.addIssue({
27
+ code: "custom",
28
+ path: [index, "start"],
29
+ message: `word "${word.w}" overlaps "${prev.w}"`,
30
+ });
31
+ }
32
+ });
33
+ });
34
+ const configSchema = limitWords(z
35
+ .strictObject({
36
+ text: text.max(140).optional(),
37
+ words: captionWordsSchema.optional(),
38
+ })
39
+ .superRefine((props, ctx) => {
40
+ if (props.text === undefined && props.words === undefined) {
41
+ ctx.addIssue({ code: "custom", message: "give text, words, or both" });
42
+ }
43
+ }), (props) => [...(props.text ? [props.text] : []), ...(props.words?.map((word) => word.w) ?? [])]);
44
+ const render = (props) => {
45
+ const line = props.words
46
+ ? props.words
47
+ .map((word, index) => `<span class="mk-word" data-i="${index}" data-start="${word.start}" data-end="${word.end}">${esc(word.w)}</span>`)
48
+ .join(" ")
49
+ : `<span class="mk-full">${esc(props.text ?? "")}</span>`;
50
+ return [
51
+ `<div class="mk-caption" ${animAttrs("fade", 0)}>`,
52
+ `<p class="mk-line">${line}</p>`,
53
+ "</div>",
54
+ ].join("\n");
55
+ };
56
+ export const captionScene = defineScene({
57
+ type: "caption",
58
+ layer: "overlay",
59
+ configSchema,
60
+ meta: {
61
+ purpose: "Voice word timings as karaoke captions; falls back to a full-text card.",
62
+ example: {
63
+ words: [
64
+ { w: "Hello", start: 0, end: 0.4 },
65
+ { w: "world", start: 0.4, end: 0.9 },
66
+ ],
67
+ },
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,11 @@
1
+ /**
2
+ * Karaoke captions: each `.mk-word` carries its clip-clock window in
3
+ * `data-start`/`data-end` (kit.reveal-compatible `WordTiming` shape).
4
+ * `seek(t)` is a pure function of `t`: the live word gets `mk-now`,
5
+ * spoken words keep `mk-on`, everything else is dimmed by CSS.
6
+ * Without `words` the fragment holds a static `.mk-full` card and
7
+ * seek is a no-op envelope fade.
8
+ *
9
+ * @type {import("../../runtime/mount.js").CreateScene}
10
+ */
11
+ 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,33 @@
1
+ // @ts-check
2
+ import { mountOverlay } from "../../runtime/mount.js";
3
+ /**
4
+ * Karaoke captions: each `.mk-word` carries its clip-clock window in
5
+ * `data-start`/`data-end` (kit.reveal-compatible `WordTiming` shape).
6
+ * `seek(t)` is a pure function of `t`: the live word gets `mk-now`,
7
+ * spoken words keep `mk-on`, everything else is dimmed by CSS.
8
+ * Without `words` the fragment holds a static `.mk-full` card and
9
+ * seek is a no-op envelope fade.
10
+ *
11
+ * @type {import("../../runtime/mount.js").CreateScene}
12
+ */
13
+ export function createScene(root, config, ctx) {
14
+ const words = /** @type {HTMLElement[]} */ (Array.from(root.querySelectorAll(".mk-word")));
15
+ const times = words.map((el) => ({
16
+ el,
17
+ start: Number(el.dataset.start),
18
+ end: Number(el.dataset.end),
19
+ }));
20
+ return mountOverlay(root, config, ctx, { baseDelay: 0.1, staggerFrames: 4, rise: 0 }, {
21
+ /** @param {number} t */
22
+ update: (t) => {
23
+ for (const { el, start, end } of times) {
24
+ el.classList.toggle("mk-now", t >= start && t < end);
25
+ el.classList.toggle("mk-on", t >= end);
26
+ }
27
+ },
28
+ reset: () => {
29
+ for (const { el } of times)
30
+ el.classList.remove("mk-now", "mk-on");
31
+ },
32
+ });
33
+ }
@@ -0,0 +1,33 @@
1
+ #scene-root {
2
+ justify-content: flex-end;
3
+ align-items: center;
4
+ padding-bottom: calc(var(--mk-space-xxl) + var(--mk-space-lg));
5
+ }
6
+ #scene-root .mk-caption {
7
+ align-self: center;
8
+ max-width: min(88ch, 100%);
9
+ padding: var(--mk-space-md) var(--mk-space-lg);
10
+ border-radius: var(--mk-radius-lg);
11
+ border: var(--mk-stroke-card, 2px) solid var(--mk-color-divider);
12
+ background: var(--mk-color-card);
13
+ text-align: center;
14
+ }
15
+ #scene-root .mk-line {
16
+ font-family: var(--mk-font-sans);
17
+ font-size: var(--mk-font-size-lg);
18
+ font-weight: 600;
19
+ line-height: 1.4;
20
+ text-wrap: balance;
21
+ }
22
+ #scene-root .mk-word {
23
+ color: var(--mk-color-muted);
24
+ }
25
+ #scene-root .mk-word.mk-on {
26
+ color: var(--mk-color-fg);
27
+ }
28
+ #scene-root .mk-word.mk-now {
29
+ color: var(--mk-color-accent);
30
+ }
31
+ #scene-root .mk-full {
32
+ color: var(--mk-color-fg);
33
+ }
@@ -0,0 +1 @@
1
+ export declare const chapterScene: import("@anvpx/motion-schema").ScenePlugin;
@@ -0,0 +1,29 @@
1
+ import { z } from "zod";
2
+ import { defineScene } from "../../define-scene.js";
3
+ import { animAttrs, esc } from "../../html.js";
4
+ import { cornerSchema, positionClass, positionCss } from "../../position.js";
5
+ import { readText } from "../../text-file.js";
6
+ import { limitWords, present, text } from "../../words.js";
7
+ const configSchema = limitWords(z.strictObject({
8
+ number: z.number().int().min(0).max(99),
9
+ label: text,
10
+ position: cornerSchema.default("top-left"),
11
+ }), (props) => present(props.number, props.label));
12
+ const render = (props) => [
13
+ `<div class="mk-chip ${positionClass(props.position)}" ${animAttrs("fade", 0)}>`,
14
+ `<span class="mk-number">${props.number}</span>`,
15
+ `<span class="mk-label">${esc(props.label)}</span>`,
16
+ "</div>",
17
+ ].join("\n");
18
+ export const chapterScene = defineScene({
19
+ type: "chapter",
20
+ layer: "overlay",
21
+ configSchema,
22
+ meta: {
23
+ purpose: "Numbered chapter chip in a corner.",
24
+ example: { number: 1, label: "Intro" },
25
+ },
26
+ style: readText("./style.css", import.meta.url),
27
+ script: readText("./scene.js", import.meta.url),
28
+ render: (props, _ctx, root) => ({ body: render(props), css: positionCss(root) }),
29
+ });
@@ -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;