@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,33 @@
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
+ const configSchema = limitWords(z.strictObject({
7
+ name: text,
8
+ title: text.optional(),
9
+ align: z.enum(["left", "right"]).default("left"),
10
+ }), (props) => present(props.name, props.title));
11
+ const render = (props) => {
12
+ const title = props.title
13
+ ? `\n<p class="mk-title" ${animAttrs("rise", 2)}>${esc(props.title)}</p>`
14
+ : "";
15
+ return [
16
+ `<div class="mk-panel mk-align-${props.align}" ${animAttrs("fade", 0)}>`,
17
+ `<h2 class="mk-name" ${animAttrs("rise", 1)}>${esc(props.name)}</h2>`,
18
+ `<span class="mk-rule" ${animAttrs("grow", 1)}></span>${title}`,
19
+ "</div>",
20
+ ].join("\n");
21
+ };
22
+ export const lowerThirdScene = defineScene({
23
+ type: "lower-third",
24
+ layer: "overlay",
25
+ configSchema,
26
+ meta: {
27
+ purpose: "Name plate with optional title at the lower edge.",
28
+ example: { name: "Ada Lovelace" },
29
+ },
30
+ style: readText("./style.css", import.meta.url),
31
+ script: readText("./scene.js", import.meta.url),
32
+ render: (props) => ({ body: render(props) }),
33
+ });
@@ -0,0 +1,2 @@
1
+ /** @type {import("../../runtime/mount.js").CreateScene} */
2
+ export declare function createScene(root: HTMLElement, config: import("../../runtime/mount.js").SceneConfig, ctx: import("../../runtime/mount.js").RuntimeContext): import("../../runtime/mount.js").Runtime;
@@ -0,0 +1,6 @@
1
+ // @ts-check
2
+ import { mountOverlay } from "../../runtime/mount.js";
3
+ /** @type {import("../../runtime/mount.js").CreateScene} */
4
+ export function createScene(root, config, ctx) {
5
+ return mountOverlay(root, config, ctx, { baseDelay: 0.1, staggerFrames: 5, rise: 40 });
6
+ }
@@ -0,0 +1,45 @@
1
+ #scene-root {
2
+ justify-content: flex-end;
3
+ padding-bottom: calc(var(--mk-space-xxl) + var(--mk-space-lg));
4
+ }
5
+ #scene-root .mk-panel {
6
+ display: flex;
7
+ flex-direction: column;
8
+ gap: var(--mk-space-sm);
9
+ max-width: 1100px;
10
+ padding: var(--mk-space-md) var(--mk-space-lg);
11
+ border-radius: var(--mk-radius-lg);
12
+ border: var(--mk-stroke-card, 2px) solid var(--mk-color-divider);
13
+ background: var(--mk-color-card);
14
+ color: var(--mk-color-fg);
15
+ }
16
+ #scene-root .mk-align-left {
17
+ align-self: flex-start;
18
+ }
19
+ #scene-root .mk-align-right {
20
+ align-self: flex-end;
21
+ text-align: right;
22
+ }
23
+ #scene-root .mk-name {
24
+ font-family: var(--mk-font-display, var(--mk-font-sans));
25
+ font-size: var(--mk-font-size-lg);
26
+ font-weight: 700;
27
+ letter-spacing: -0.01em;
28
+ }
29
+ #scene-root .mk-rule {
30
+ display: block;
31
+ width: 96px;
32
+ height: 6px;
33
+ border-radius: var(--mk-radius-full);
34
+ background: var(--mk-color-accent);
35
+ transform-origin: left center;
36
+ }
37
+ #scene-root .mk-align-right .mk-rule {
38
+ align-self: flex-end;
39
+ transform-origin: right center;
40
+ }
41
+ #scene-root .mk-title {
42
+ font-size: var(--mk-font-size-md);
43
+ font-weight: 600;
44
+ color: var(--mk-color-muted);
45
+ }
@@ -0,0 +1 @@
1
+ export declare const mediaFrameScene: import("@anvpx/motion-schema").ScenePlugin;
@@ -0,0 +1,99 @@
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
+ const MAX_CALLOUTS = 3;
7
+ /** Past this share of the screen width a label opens toward the left. */
8
+ const FLIP_AT = 55;
9
+ const PHONE_FLIP_AT = 50;
10
+ const calloutSchema = z.strictObject({
11
+ x: z.number().min(0).max(100),
12
+ y: z.number().min(0).max(100),
13
+ label: text.max(24),
14
+ });
15
+ const configSchema = limitWords(z
16
+ .strictObject({
17
+ image: text,
18
+ device: z.enum(["none", "browser", "phone", "laptop"]).default("browser"),
19
+ layout: z.enum(["full", "left", "right"]).default("full"),
20
+ title: text.max(60).optional(),
21
+ caption: text.max(140).optional(),
22
+ motion: z.enum(["none", "kenburns", "tilt"]).default("kenburns"),
23
+ callouts: z.array(calloutSchema).max(MAX_CALLOUTS).optional(),
24
+ })
25
+ .superRefine((props, ctx) => {
26
+ if (props.layout !== "full" && props.title === undefined && props.caption === undefined) {
27
+ ctx.addIssue({
28
+ code: "custom",
29
+ path: ["layout"],
30
+ message: `layout "${props.layout}" puts text beside the image`,
31
+ params: { suggestion: "set title or caption, or use layout full" },
32
+ });
33
+ }
34
+ }), (props) => [
35
+ ...present(props.title, props.caption),
36
+ ...(props.callouts ?? []).map((callout) => callout.label),
37
+ ]);
38
+ const sideOf = (callout, phone) => {
39
+ if (phone)
40
+ return callout.x < PHONE_FLIP_AT ? "below-start" : "below-end";
41
+ return callout.x > FLIP_AT ? "toward-left" : "toward-right";
42
+ };
43
+ const markHtml = (callout, index, phone) => `<div class="mk-mark mk-${sideOf(callout, phone)}" style="left:${callout.x}%;top:${callout.y}%" data-i="${index}">
44
+ <span class="mk-ring"></span><span class="mk-dot">${index + 1}</span><span class="mk-chip">${esc(callout.label)}</span>
45
+ </div>`;
46
+ const copyHtml = (props) => {
47
+ if (props.title === undefined && props.caption === undefined)
48
+ return "";
49
+ const parts = [];
50
+ if (props.title !== undefined) {
51
+ parts.push(`<h1 class="mk-title" ${animAttrs("rise", 0, "title")}>${esc(props.title)}</h1>`);
52
+ parts.push(`<span class="mk-rule" ${animAttrs("grow", 0, "title")}></span>`);
53
+ }
54
+ if (props.caption !== undefined) {
55
+ parts.push(`<p class="mk-caption" ${animAttrs("rise", 1, "caption")}>${esc(props.caption)}</p>`);
56
+ }
57
+ return `<div class="mk-copy">\n${parts.join("\n")}\n</div>\n`;
58
+ };
59
+ const chromeHtml = (device) => device === "browser"
60
+ ? `<div class="mk-chrome" aria-hidden="true"><span class="mk-dots"><i></i><i></i><i></i></span><span class="mk-url"></span></div>`
61
+ : "";
62
+ const baseHtml = (device) => device === "laptop" ? '<div class="mk-base" aria-hidden="true"></div>' : "";
63
+ const render = (props, resolve, portrait) => {
64
+ const src = esc(resolve(props.image));
65
+ const alt = esc(props.title ?? props.caption ?? "");
66
+ const phone = props.device === "phone";
67
+ const marks = (props.callouts ?? []).map((callout, index) => markHtml(callout, index, phone));
68
+ const step = props.title !== undefined || props.caption !== undefined ? 2 : 0;
69
+ const shape = `mk-layout-${props.layout}${portrait ? " mk-portrait" : ""}`;
70
+ return `<div class="mk-wrap ${shape}">
71
+ ${copyHtml(props)}<div class="mk-stage">
72
+ <div class="mk-rig" ${animAttrs("rise", step, "media")}>
73
+ <div class="mk-device mk-dev-${props.device}" data-motion="${props.motion}">
74
+ ${chromeHtml(props.device)}<div class="mk-screen">
75
+ <div class="mk-media"><img class="mk-shot" src="${src}" alt="${alt}" /></div>
76
+ <div class="mk-marks">
77
+ ${marks.join("\n")}
78
+ </div>
79
+ </div>
80
+ ${baseHtml(props.device)}</div>
81
+ </div>
82
+ </div>
83
+ </div>`;
84
+ };
85
+ export const mediaFrameScene = defineScene({
86
+ type: "media-frame",
87
+ configSchema,
88
+ meta: {
89
+ purpose: "Screenshot or photo, bare or in a browser, phone or laptop frame, with optional title, caption and numbered callout dots.",
90
+ example: {
91
+ image: "screenshot.png",
92
+ device: "browser",
93
+ callouts: [{ x: 30, y: 40, label: "Start here" }],
94
+ },
95
+ },
96
+ style: readText("./style.css", import.meta.url),
97
+ script: readText("./scene.js", import.meta.url),
98
+ render: (props, ctx) => ({ body: render(props, ctx.assetResolver, ctx.height > ctx.width) }),
99
+ });
@@ -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,85 @@
1
+ // @ts-check
2
+ import { clamp01, ease } from "../../runtime/easing.js";
3
+ import { mountTimeline } from "../../runtime/mount.js";
4
+ const MARK_START = 0.9;
5
+ const MARK_GAP = 0.4;
6
+ const MARK_POP = 0.4;
7
+ const CHIP_DELAY = 0.12;
8
+ const CHIP_FADE = 0.3;
9
+ const CHIP_SLIDE_PX = 14;
10
+ const PULSE_SECONDS = 1.6;
11
+ const PULSE_GROW = 1.1;
12
+ const PULSE_ALPHA = 0.5;
13
+ const ZOOM_FROM = 1.02;
14
+ const ZOOM_TO = 1.08;
15
+ const PAN_PERCENT = 1.5;
16
+ const TILT_Y = [-9, 3];
17
+ const TILT_X = [4, 1];
18
+ const PERSPECTIVE_PX = 2400;
19
+ /**
20
+ * @param {number} t
21
+ * @param {number} from
22
+ * @param {number} span
23
+ */
24
+ const within = (t, from, span) => clamp01(span > 0 ? (t - from) / span : t >= from ? 1 : 0);
25
+ /**
26
+ * @param {number} a
27
+ * @param {number} b
28
+ * @param {number} p
29
+ */
30
+ const mix = (a, b, p) => a + (b - a) * p;
31
+ /** @type {import("../../runtime/mount.js").CreateScene} */
32
+ export function createScene(root, config, ctx) {
33
+ const inner = mountTimeline(root, config, ctx, { baseDelay: 0.2, staggerFrames: 5, rise: 32 });
34
+ const device = /** @type {HTMLElement} */ (root.querySelector(".mk-device"));
35
+ const shot = /** @type {HTMLElement} */ (root.querySelector(".mk-shot"));
36
+ const motion = device.dataset.motion;
37
+ const duration = ctx.duration ?? (Number(root.dataset.duration) || 6);
38
+ const marks = Array.from(root.querySelectorAll(".mk-mark"), (node) => ({
39
+ dot: /** @type {HTMLElement} */ (node.querySelector(".mk-dot")),
40
+ ring: /** @type {HTMLElement} */ (node.querySelector(".mk-ring")),
41
+ chip: /** @type {HTMLElement} */ (node.querySelector(".mk-chip")),
42
+ flip: node.classList.contains("mk-toward-left") || node.classList.contains("mk-below-end") ? -1 : 1,
43
+ }));
44
+ const first = Math.min(MARK_START, duration * 0.2);
45
+ /** @param {number} t */
46
+ const seek = (t) => {
47
+ inner.seek(t);
48
+ const p = duration > 0 ? clamp01(t / duration) : 1;
49
+ if (motion === "kenburns") {
50
+ const zoom = mix(ZOOM_FROM, ZOOM_TO, p);
51
+ shot.style.transform = `translate(${mix(-PAN_PERCENT, PAN_PERCENT, p)}%, ${mix(PAN_PERCENT / 2, -PAN_PERCENT / 2, p)}%) scale(${zoom})`;
52
+ }
53
+ if (motion === "tilt") {
54
+ const e = ease("in-out", p);
55
+ device.style.transform = `perspective(${PERSPECTIVE_PX}px) rotateY(${mix(TILT_Y[0] ?? 0, TILT_Y[1] ?? 0, e)}deg) rotateX(${mix(TILT_X[0] ?? 0, TILT_X[1] ?? 0, e)}deg)`;
56
+ }
57
+ marks.forEach((mark, i) => {
58
+ const start = first + i * MARK_GAP;
59
+ const pop = within(t, start, MARK_POP);
60
+ const scale = pop > 0 ? ease("spring-snappy", pop) : 0;
61
+ mark.dot.style.opacity = String(clamp01(pop * 4));
62
+ mark.dot.style.transform = `scale(${scale})`;
63
+ const chip = within(t, start + CHIP_DELAY, CHIP_FADE);
64
+ mark.chip.style.opacity = String(ease("out", chip));
65
+ mark.chip.style.transform = `translateX(${(1 - ease("out", chip)) * CHIP_SLIDE_PX * -mark.flip}px)`;
66
+ const since = t - start - MARK_POP;
67
+ const phase = since > 0 ? (since % PULSE_SECONDS) / PULSE_SECONDS : 0;
68
+ mark.ring.style.opacity = since > 0 ? String((1 - phase) * PULSE_ALPHA) : "0";
69
+ mark.ring.style.transform = `scale(${1 + PULSE_GROW * phase})`;
70
+ });
71
+ };
72
+ const dispose = () => {
73
+ inner.dispose();
74
+ shot.style.removeProperty("transform");
75
+ device.style.removeProperty("transform");
76
+ for (const mark of marks) {
77
+ for (const el of [mark.dot, mark.ring, mark.chip]) {
78
+ el.style.removeProperty("opacity");
79
+ el.style.removeProperty("transform");
80
+ }
81
+ }
82
+ };
83
+ seek(0);
84
+ return { seek, dispose, ready: inner.ready };
85
+ }
@@ -0,0 +1,275 @@
1
+ #scene-root {
2
+ align-items: stretch;
3
+ }
4
+ #scene-root .mk-wrap {
5
+ flex: 1;
6
+ min-height: 0;
7
+ display: flex;
8
+ gap: var(--mk-space-xl);
9
+ align-items: stretch;
10
+ }
11
+ #scene-root .mk-layout-full {
12
+ flex-direction: column;
13
+ gap: var(--mk-space-lg);
14
+ }
15
+ #scene-root .mk-layout-left {
16
+ flex-direction: row;
17
+ }
18
+ #scene-root .mk-layout-right {
19
+ flex-direction: row-reverse;
20
+ }
21
+ #scene-root .mk-wrap.mk-portrait {
22
+ flex-direction: column;
23
+ gap: var(--mk-space-lg);
24
+ }
25
+ #scene-root .mk-copy {
26
+ display: flex;
27
+ flex-direction: column;
28
+ justify-content: center;
29
+ gap: var(--mk-space-lg);
30
+ }
31
+ #scene-root .mk-layout-left .mk-copy,
32
+ #scene-root .mk-layout-right .mk-copy {
33
+ flex: 0 0 32%;
34
+ align-items: flex-start;
35
+ }
36
+ #scene-root .mk-layout-full .mk-copy {
37
+ flex: none;
38
+ align-items: center;
39
+ text-align: center;
40
+ gap: var(--mk-space-sm);
41
+ }
42
+ #scene-root .mk-wrap.mk-portrait .mk-copy {
43
+ flex: none;
44
+ }
45
+ #scene-root .mk-layout-full .mk-rule {
46
+ display: none;
47
+ }
48
+ #scene-root .mk-title {
49
+ font-family: var(--mk-font-display, var(--mk-font-sans));
50
+ font-size: var(--mk-font-size-xl);
51
+ font-weight: 800;
52
+ line-height: 1.04;
53
+ letter-spacing: -0.03em;
54
+ text-wrap: balance;
55
+ }
56
+ #scene-root .mk-layout-full .mk-title {
57
+ font-size: var(--mk-font-size-lg);
58
+ }
59
+ #scene-root .mk-caption {
60
+ font-size: var(--mk-font-size-md);
61
+ line-height: 1.3;
62
+ color: var(--mk-color-muted);
63
+ text-wrap: pretty;
64
+ }
65
+ #scene-root .mk-stage {
66
+ flex: 1;
67
+ min-width: 0;
68
+ min-height: 0;
69
+ container-type: size;
70
+ display: grid;
71
+ place-items: center;
72
+ }
73
+ #scene-root .mk-rig {
74
+ display: flex;
75
+ justify-content: center;
76
+ }
77
+ #scene-root .mk-device {
78
+ position: relative;
79
+ width: var(--dw);
80
+ }
81
+ #scene-root .mk-screen {
82
+ position: relative;
83
+ aspect-ratio: 16 / 10;
84
+ }
85
+ #scene-root .mk-media {
86
+ position: absolute;
87
+ inset: 0;
88
+ overflow: hidden;
89
+ background: var(--mk-color-surface);
90
+ }
91
+ #scene-root .mk-shot {
92
+ display: block;
93
+ width: 100%;
94
+ height: 100%;
95
+ object-fit: cover;
96
+ }
97
+ #scene-root .mk-dev-none {
98
+ --dw: min(100cqw, calc(100cqh * 1.6));
99
+ }
100
+ #scene-root .mk-dev-none .mk-screen {
101
+ border-radius: var(--mk-radius-xl);
102
+ box-shadow:
103
+ 0 0 0 2px var(--mk-color-divider),
104
+ 0 40px 80px -32px color-mix(in srgb, var(--mk-color-fg) 50%, transparent);
105
+ }
106
+ #scene-root .mk-dev-none .mk-media {
107
+ border-radius: var(--mk-radius-xl);
108
+ }
109
+ #scene-root .mk-dev-browser {
110
+ --dw: min(100cqw, calc((100cqh - 72px) * 1.6));
111
+ background: var(--mk-color-card);
112
+ border: var(--mk-stroke-card, 2px) solid var(--mk-color-divider);
113
+ border-radius: var(--mk-radius-xl);
114
+ box-shadow: var(
115
+ --mk-shadow-card,
116
+ 0 40px 80px -32px color-mix(in srgb, var(--mk-color-fg) 50%, transparent)
117
+ );
118
+ }
119
+ #scene-root .mk-chrome {
120
+ display: flex;
121
+ align-items: center;
122
+ gap: var(--mk-space-lg);
123
+ height: 72px;
124
+ padding: 0 var(--mk-space-lg);
125
+ background: var(--mk-color-surface);
126
+ border-bottom: 2px solid var(--mk-color-divider);
127
+ border-radius: calc(var(--mk-radius-xl) - 2px) calc(var(--mk-radius-xl) - 2px) 0 0;
128
+ }
129
+ #scene-root .mk-dots {
130
+ display: flex;
131
+ gap: var(--mk-space-md);
132
+ }
133
+ #scene-root .mk-dots i {
134
+ display: block;
135
+ width: 20px;
136
+ height: 20px;
137
+ border-radius: var(--mk-radius-full);
138
+ background: var(--mk-color-divider);
139
+ }
140
+ #scene-root .mk-url {
141
+ flex: 1;
142
+ max-width: 56%;
143
+ height: 36px;
144
+ border-radius: var(--mk-radius-full);
145
+ background: var(--mk-color-card);
146
+ border: 2px solid var(--mk-color-divider);
147
+ }
148
+ #scene-root .mk-dev-browser .mk-media {
149
+ border-radius: 0 0 calc(var(--mk-radius-xl) - 2px) calc(var(--mk-radius-xl) - 2px);
150
+ }
151
+ #scene-root .mk-dev-laptop {
152
+ --dw: min(88cqw, calc((100cqh - 28px) * 1.6));
153
+ }
154
+ #scene-root .mk-dev-laptop .mk-screen {
155
+ border: 18px solid var(--mk-color-deepEnd);
156
+ border-radius: var(--mk-radius-xl) var(--mk-radius-xl) 0 0;
157
+ box-shadow: 0 0 0 2px var(--mk-color-divider);
158
+ }
159
+ #scene-root .mk-dev-laptop .mk-media {
160
+ border-radius: var(--mk-radius-md);
161
+ }
162
+ #scene-root .mk-base {
163
+ width: 112%;
164
+ height: 28px;
165
+ margin-left: -6%;
166
+ border-radius: 0 0 var(--mk-radius-xl) var(--mk-radius-xl);
167
+ background: linear-gradient(var(--mk-color-divider), var(--mk-color-surface));
168
+ border: var(--mk-stroke-card, 2px) solid var(--mk-color-divider);
169
+ box-shadow: var(
170
+ --mk-shadow-card,
171
+ 0 40px 60px -28px color-mix(in srgb, var(--mk-color-fg) 55%, transparent)
172
+ );
173
+ }
174
+ #scene-root .mk-dev-phone {
175
+ --dw: min(100cqw, calc(100cqh * 9 / 19.5));
176
+ }
177
+ #scene-root .mk-dev-phone .mk-screen {
178
+ aspect-ratio: 9 / 19.5;
179
+ border: 16px solid var(--mk-color-deepEnd);
180
+ border-radius: 72px;
181
+ box-shadow:
182
+ 0 0 0 2px var(--mk-color-divider),
183
+ 0 40px 80px -32px color-mix(in srgb, var(--mk-color-fg) 50%, transparent);
184
+ }
185
+ #scene-root .mk-dev-phone .mk-media {
186
+ border-radius: 56px;
187
+ }
188
+ #scene-root .mk-dev-phone .mk-media::after {
189
+ content: "";
190
+ position: absolute;
191
+ top: 18px;
192
+ left: 50%;
193
+ width: 30%;
194
+ height: 30px;
195
+ margin-left: -15%;
196
+ border-radius: var(--mk-radius-full);
197
+ background: var(--mk-color-deepEnd);
198
+ }
199
+ #scene-root .mk-marks {
200
+ position: absolute;
201
+ inset: 0;
202
+ }
203
+ #scene-root .mk-mark {
204
+ position: absolute;
205
+ width: 0;
206
+ height: 0;
207
+ }
208
+ #scene-root .mk-dot,
209
+ #scene-root .mk-ring {
210
+ position: absolute;
211
+ left: 0;
212
+ top: 0;
213
+ width: 56px;
214
+ height: 56px;
215
+ margin: -28px 0 0 -28px;
216
+ border-radius: var(--mk-radius-full);
217
+ }
218
+ #scene-root .mk-dot {
219
+ display: grid;
220
+ place-items: center;
221
+ background: var(--mk-color-accent);
222
+ color: var(--mk-color-onAccent);
223
+ border: 4px solid var(--mk-color-card);
224
+ font-size: var(--mk-font-size-sm);
225
+ font-weight: 800;
226
+ line-height: 1;
227
+ box-shadow: 0 8px 24px -8px color-mix(in srgb, var(--mk-color-fg) 60%, transparent);
228
+ opacity: 0;
229
+ }
230
+ #scene-root .mk-ring {
231
+ border: 4px solid var(--mk-color-accent);
232
+ opacity: 0;
233
+ }
234
+ #scene-root .mk-chip {
235
+ position: absolute;
236
+ top: 0;
237
+ translate: 0 -50%;
238
+ width: max-content;
239
+ max-width: 440px;
240
+ padding: var(--mk-space-sm) var(--mk-space-lg);
241
+ border-radius: var(--mk-radius-full);
242
+ background: var(--mk-color-card);
243
+ color: var(--mk-color-fg);
244
+ border: var(--mk-stroke-card, 2px) solid var(--mk-color-divider);
245
+ font-size: var(--mk-font-size-sm);
246
+ font-weight: 700;
247
+ line-height: 1.2;
248
+ white-space: nowrap;
249
+ box-shadow: var(
250
+ --mk-shadow-card,
251
+ 0 12px 28px -12px color-mix(in srgb, var(--mk-color-fg) 55%, transparent)
252
+ );
253
+ opacity: 0;
254
+ }
255
+ #scene-root .mk-toward-right .mk-chip {
256
+ left: 44px;
257
+ }
258
+ #scene-root .mk-toward-left .mk-chip {
259
+ right: 44px;
260
+ }
261
+ #scene-root .mk-below-start .mk-chip,
262
+ #scene-root .mk-below-end .mk-chip {
263
+ top: 44px;
264
+ translate: none;
265
+ max-width: 300px;
266
+ white-space: normal;
267
+ border-radius: var(--mk-radius-lg);
268
+ text-wrap: balance;
269
+ }
270
+ #scene-root .mk-below-start .mk-chip {
271
+ left: -28px;
272
+ }
273
+ #scene-root .mk-below-end .mk-chip {
274
+ right: -28px;
275
+ }
@@ -0,0 +1 @@
1
+ export declare const outroScene: import("@anvpx/motion-schema").ScenePlugin;
@@ -0,0 +1,54 @@
1
+ import { z } from "zod";
2
+ import { defineScene } from "../../define-scene.js";
3
+ import { animAttrs, esc } from "../../html.js";
4
+ import { frameCss } from "../../shared/frame.js";
5
+ import { readText } from "../../text-file.js";
6
+ import { limitWords, text } from "../../words.js";
7
+ const configSchema = limitWords(z.strictObject({
8
+ title: text,
9
+ lines: z.array(text).max(3, "max 3 lines").optional(),
10
+ credits: z.array(text).max(4, "max 4 credits").optional(),
11
+ }), (props) => [props.title, ...(props.lines ?? []), ...(props.credits ?? [])]);
12
+ /** Tall frames: air around the block; portrait also gets a bigger title and lines so the card is not mostly empty. */
13
+ const TALL_CSS = `#scene-root { padding-block: calc(var(--mk-space-xxl) * 2); }
14
+ #scene-root .mk-main { gap: var(--mk-space-xl); }`;
15
+ const PORTRAIT_CSS = `#scene-root .mk-heading { font-size: calc(var(--mk-font-size-title) * 1.3); }
16
+ #scene-root .mk-line { font-size: var(--mk-font-size-xl); }
17
+ #scene-root .mk-credit { font-size: var(--mk-font-size-md); }`;
18
+ const ORBIT = '<div class="mk-orbit" aria-hidden="true"><i class="mk-orbit-1"></i><i class="mk-orbit-2"></i></div>';
19
+ const mainBlock = (title, lines) => {
20
+ const rows = lines.map((line, index) => {
21
+ const attrs = animAttrs("rise", index + 1, `line-${index + 1}`);
22
+ return `<p class="mk-line" ${attrs}>${esc(line)}</p>`;
23
+ });
24
+ const heading = `<h1 class="mk-heading" ${animAttrs("rise", 0, "title")}>${esc(title)}</h1>`;
25
+ const rule = `<span class="mk-rule" ${animAttrs("grow", 0, "title")}></span>`;
26
+ return `<div class="mk-main">\n${[rule, heading, ...rows].join("\n")}\n</div>`;
27
+ };
28
+ const creditsBlock = (credits, firstStep) => {
29
+ if (credits.length === 0)
30
+ return "";
31
+ const rows = credits.map((credit, index) => {
32
+ const attrs = animAttrs("fade", firstStep + index, "credits");
33
+ return `<li class="mk-credit" ${attrs}>${esc(credit)}</li>`;
34
+ });
35
+ return `\n<ul class="mk-credits">\n${rows.join("\n")}\n</ul>`;
36
+ };
37
+ const render = (props) => {
38
+ const lines = props.lines ?? [];
39
+ return `${ORBIT}\n${mainBlock(props.title, lines)}${creditsBlock(props.credits ?? [], lines.length + 1)}`;
40
+ };
41
+ export const outroScene = defineScene({
42
+ type: "outro",
43
+ configSchema,
44
+ meta: {
45
+ purpose: "Closing card with title, up to 3 lines and up to 4 credits.",
46
+ example: { title: "Thanks" },
47
+ },
48
+ style: readText("./style.css", import.meta.url),
49
+ script: readText("./scene.js", import.meta.url),
50
+ render: (props, ctx, root) => ({
51
+ body: render(props),
52
+ css: frameCss(ctx, root, { tall: TALL_CSS, portrait: PORTRAIT_CSS }),
53
+ }),
54
+ });
@@ -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.3, staggerFrames: 6, rise: 32 });
6
+ }