@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.
- package/LICENSE +21 -0
- package/README.md +39 -2
- package/dist/catalog.d.ts +17 -0
- package/dist/catalog.js +51 -0
- package/dist/custom.d.ts +86 -0
- package/dist/custom.js +170 -0
- package/dist/define-scene.d.ts +31 -0
- package/dist/define-scene.js +128 -0
- package/dist/html.d.ts +5 -0
- package/dist/html.js +14 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/position.d.ts +21 -0
- package/dist/position.js +16 -0
- package/dist/registry.d.ts +4 -0
- package/dist/registry.js +68 -0
- package/dist/runtime/easing.d.ts +14 -0
- package/dist/runtime/easing.js +45 -0
- package/dist/runtime/mount.d.ts +50 -0
- package/dist/runtime/mount.js +153 -0
- package/dist/runtime/timeline.d.ts +147 -0
- package/dist/runtime/timeline.js +199 -0
- package/dist/runtime-files.d.ts +2 -0
- package/dist/runtime-files.js +15 -0
- package/dist/scene-contract.d.ts +7 -0
- package/dist/scene-contract.js +104 -0
- package/dist/scene3d/post.d.ts +12 -0
- package/dist/scene3d/post.js +16 -0
- package/dist/scene3d/reflector.d.ts +9 -0
- package/dist/scene3d/reflector.js +17 -0
- package/dist/scene3d/textures.d.ts +9 -0
- package/dist/scene3d/textures.js +11 -0
- package/dist/scene3d/volumetric.d.ts +9 -0
- package/dist/scene3d/volumetric.js +16 -0
- package/dist/scenes/bug/index.d.ts +1 -0
- package/dist/scenes/bug/index.js +45 -0
- package/dist/scenes/bug/scene.d.ts +2 -0
- package/dist/scenes/bug/scene.js +6 -0
- package/dist/scenes/bug/style.css +30 -0
- package/dist/scenes/bullets/index.d.ts +1 -0
- package/dist/scenes/bullets/index.js +128 -0
- package/dist/scenes/bullets/scene.d.ts +2 -0
- package/dist/scenes/bullets/scene.js +6 -0
- package/dist/scenes/bullets/style.css +91 -0
- package/dist/scenes/callout/index.d.ts +9 -0
- package/dist/scenes/callout/index.js +95 -0
- package/dist/scenes/callout/scene.d.ts +2 -0
- package/dist/scenes/callout/scene.js +43 -0
- package/dist/scenes/callout/style.css +97 -0
- package/dist/scenes/caption/index.d.ts +1 -0
- package/dist/scenes/caption/index.js +72 -0
- package/dist/scenes/caption/scene.d.ts +11 -0
- package/dist/scenes/caption/scene.js +33 -0
- package/dist/scenes/caption/style.css +33 -0
- package/dist/scenes/chapter/index.d.ts +1 -0
- package/dist/scenes/chapter/index.js +29 -0
- package/dist/scenes/chapter/scene.d.ts +2 -0
- package/dist/scenes/chapter/scene.js +6 -0
- package/dist/scenes/chapter/style.css +25 -0
- package/dist/scenes/chart/index.d.ts +1 -0
- package/dist/scenes/chart/index.js +24 -0
- package/dist/scenes/chart/render.d.ts +4 -0
- package/dist/scenes/chart/render.js +257 -0
- package/dist/scenes/chart/scale.d.ts +11 -0
- package/dist/scenes/chart/scale.js +53 -0
- package/dist/scenes/chart/scene.d.ts +2 -0
- package/dist/scenes/chart/scene.js +73 -0
- package/dist/scenes/chart/schema.d.ts +36 -0
- package/dist/scenes/chart/schema.js +109 -0
- package/dist/scenes/chart/style.css +425 -0
- package/dist/scenes/code/index.d.ts +1 -0
- package/dist/scenes/code/index.js +109 -0
- package/dist/scenes/code/scene.d.ts +2 -0
- package/dist/scenes/code/scene.js +108 -0
- package/dist/scenes/code/style.css +155 -0
- package/dist/scenes/code/tokenize.d.ts +8 -0
- package/dist/scenes/code/tokenize.js +145 -0
- package/dist/scenes/compare/index.d.ts +1 -0
- package/dist/scenes/compare/index.js +128 -0
- package/dist/scenes/compare/scene.d.ts +2 -0
- package/dist/scenes/compare/scene.js +58 -0
- package/dist/scenes/compare/style.css +187 -0
- package/dist/scenes/countdown/index.d.ts +1 -0
- package/dist/scenes/countdown/index.js +46 -0
- package/dist/scenes/countdown/scene.d.ts +2 -0
- package/dist/scenes/countdown/scene.js +21 -0
- package/dist/scenes/countdown/style.css +32 -0
- package/dist/scenes/flow/index.d.ts +1 -0
- package/dist/scenes/flow/index.js +66 -0
- package/dist/scenes/flow/inspect.d.ts +26 -0
- package/dist/scenes/flow/inspect.js +25 -0
- package/dist/scenes/flow/layout.d.ts +83 -0
- package/dist/scenes/flow/layout.js +222 -0
- package/dist/scenes/flow/measure.d.ts +15 -0
- package/dist/scenes/flow/measure.js +33 -0
- package/dist/scenes/flow/plan.d.ts +42 -0
- package/dist/scenes/flow/plan.js +117 -0
- package/dist/scenes/flow/scene.d.ts +2 -0
- package/dist/scenes/flow/scene.js +6 -0
- package/dist/scenes/flow/schema.d.ts +49 -0
- package/dist/scenes/flow/schema.js +60 -0
- package/dist/scenes/flow/style.css +54 -0
- package/dist/scenes/hero/index.d.ts +3 -0
- package/dist/scenes/hero/index.js +77 -0
- package/dist/scenes/hero/scene.d.ts +2 -0
- package/dist/scenes/hero/scene.js +6 -0
- package/dist/scenes/hero/style.css +143 -0
- package/dist/scenes/icon-grid/index.d.ts +1 -0
- package/dist/scenes/icon-grid/index.js +69 -0
- package/dist/scenes/icon-grid/scene.d.ts +2 -0
- package/dist/scenes/icon-grid/scene.js +6 -0
- package/dist/scenes/icon-grid/style.css +74 -0
- package/dist/scenes/kinetic/index.d.ts +1 -0
- package/dist/scenes/kinetic/index.js +62 -0
- package/dist/scenes/kinetic/scene.d.ts +2 -0
- package/dist/scenes/kinetic/scene.js +98 -0
- package/dist/scenes/kinetic/style.css +94 -0
- package/dist/scenes/kpi-grid/index.d.ts +1 -0
- package/dist/scenes/kpi-grid/index.js +161 -0
- package/dist/scenes/kpi-grid/scene.d.ts +2 -0
- package/dist/scenes/kpi-grid/scene.js +6 -0
- package/dist/scenes/kpi-grid/style.css +141 -0
- package/dist/scenes/lower-third/index.d.ts +1 -0
- package/dist/scenes/lower-third/index.js +33 -0
- package/dist/scenes/lower-third/scene.d.ts +2 -0
- package/dist/scenes/lower-third/scene.js +6 -0
- package/dist/scenes/lower-third/style.css +45 -0
- package/dist/scenes/media-frame/index.d.ts +1 -0
- package/dist/scenes/media-frame/index.js +99 -0
- package/dist/scenes/media-frame/scene.d.ts +2 -0
- package/dist/scenes/media-frame/scene.js +85 -0
- package/dist/scenes/media-frame/style.css +275 -0
- package/dist/scenes/outro/index.d.ts +1 -0
- package/dist/scenes/outro/index.js +54 -0
- package/dist/scenes/outro/scene.d.ts +2 -0
- package/dist/scenes/outro/scene.js +6 -0
- package/dist/scenes/outro/style.css +92 -0
- package/dist/scenes/poster/index.d.ts +3 -0
- package/dist/scenes/poster/index.js +62 -0
- package/dist/scenes/poster/scene.d.ts +2 -0
- package/dist/scenes/poster/scene.js +6 -0
- package/dist/scenes/poster/style.css +102 -0
- package/dist/scenes/progress/index.d.ts +1 -0
- package/dist/scenes/progress/index.js +40 -0
- package/dist/scenes/progress/scene.d.ts +2 -0
- package/dist/scenes/progress/scene.js +17 -0
- package/dist/scenes/progress/style.css +31 -0
- package/dist/scenes/quote/index.d.ts +3 -0
- package/dist/scenes/quote/index.js +69 -0
- package/dist/scenes/quote/scene.d.ts +2 -0
- package/dist/scenes/quote/scene.js +6 -0
- package/dist/scenes/quote/style.css +86 -0
- package/dist/scenes/scene3d/index.d.ts +1 -0
- package/dist/scenes/scene3d/index.js +58 -0
- package/dist/scenes/scene3d/scene.d.ts +2 -0
- package/dist/scenes/scene3d/scene.js +413 -0
- package/dist/scenes/scene3d/style.css +43 -0
- package/dist/scenes/stat/index.d.ts +4 -0
- package/dist/scenes/stat/index.js +72 -0
- package/dist/scenes/stat/scene.d.ts +2 -0
- package/dist/scenes/stat/scene.js +6 -0
- package/dist/scenes/stat/style.css +95 -0
- package/dist/scenes/steps/index.d.ts +1 -0
- package/dist/scenes/steps/index.js +147 -0
- package/dist/scenes/steps/scene.d.ts +2 -0
- package/dist/scenes/steps/scene.js +6 -0
- package/dist/scenes/steps/style.css +190 -0
- package/dist/scenes/sticker/index.d.ts +1 -0
- package/dist/scenes/sticker/index.js +48 -0
- package/dist/scenes/sticker/scene.d.ts +2 -0
- package/dist/scenes/sticker/scene.js +57 -0
- package/dist/scenes/sticker/style.css +88 -0
- package/dist/scenes/table/index.d.ts +1 -0
- package/dist/scenes/table/index.js +131 -0
- package/dist/scenes/table/scene.d.ts +2 -0
- package/dist/scenes/table/scene.js +6 -0
- package/dist/scenes/table/style.css +102 -0
- package/dist/scenes/timeline/index.d.ts +1 -0
- package/dist/scenes/timeline/index.js +68 -0
- package/dist/scenes/timeline/scene.d.ts +2 -0
- package/dist/scenes/timeline/scene.js +6 -0
- package/dist/scenes/timeline/style.css +161 -0
- package/dist/scenes/title/index.d.ts +1 -0
- package/dist/scenes/title/index.js +52 -0
- package/dist/scenes/title/scene.d.ts +2 -0
- package/dist/scenes/title/scene.js +6 -0
- package/dist/scenes/title/style.css +80 -0
- package/dist/scenes/waveform/index.d.ts +1 -0
- package/dist/scenes/waveform/index.js +39 -0
- package/dist/scenes/waveform/scene.d.ts +2 -0
- package/dist/scenes/waveform/scene.js +35 -0
- package/dist/scenes/waveform/style.css +44 -0
- package/dist/shared/fit-text.d.ts +24 -0
- package/dist/shared/fit-text.js +82 -0
- package/dist/shared/format.d.ts +24 -0
- package/dist/shared/format.js +59 -0
- package/dist/shared/frame.d.ts +14 -0
- package/dist/shared/frame.js +21 -0
- package/dist/shared/icon-search.d.ts +9 -0
- package/dist/shared/icon-search.js +26 -0
- package/dist/shared/icon.d.ts +15 -0
- package/dist/shared/icon.js +28 -0
- package/dist/shared/index.d.ts +11 -0
- package/dist/shared/index.js +7 -0
- package/dist/shared/markup.d.ts +12 -0
- package/dist/shared/markup.js +43 -0
- package/dist/shared/measure.d.ts +7 -0
- package/dist/shared/measure.js +34 -0
- package/dist/shared/motion.d.ts +94 -0
- package/dist/shared/motion.js +117 -0
- package/dist/source-hash.d.ts +4 -0
- package/dist/source-hash.js +26 -0
- package/dist/test-fixtures.d.ts +9 -0
- package/dist/test-fixtures.js +22 -0
- package/dist/text-file.d.ts +1 -0
- package/dist/text-file.js +3 -0
- package/dist/words.d.ts +11 -0
- package/dist/words.js +93 -0
- package/package.json +36 -3
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { readText } from "./text-file.js";
|
|
2
|
+
const RUNTIME_MODULES = ["easing.js", "timeline.js", "mount.js"];
|
|
3
|
+
/** Pure helpers shared with emit time; they import `../runtime/easing.js`, so they sit beside it. */
|
|
4
|
+
const SHARED_MODULES = ["motion.js", "format.js"];
|
|
5
|
+
/** Shared browser modules; the build emits them once under `runtime/` and `shared/`. */
|
|
6
|
+
export const runtimeFiles = () => new Map([
|
|
7
|
+
...RUNTIME_MODULES.map((name) => [
|
|
8
|
+
`runtime/${name}`,
|
|
9
|
+
readText(`./runtime/${name}`, import.meta.url),
|
|
10
|
+
]),
|
|
11
|
+
...SHARED_MODULES.map((name) => [
|
|
12
|
+
`shared/${name}`,
|
|
13
|
+
readText(`./shared/${name}`, import.meta.url),
|
|
14
|
+
]),
|
|
15
|
+
]);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ScenePlugin } from "@anvpx/motion-schema";
|
|
2
|
+
export interface ContractFixture {
|
|
3
|
+
readonly valid: Record<string, unknown>;
|
|
4
|
+
/** Valid props whose every text field contains markup and quotes. */
|
|
5
|
+
readonly hostile: Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
export declare const describeSceneContract: (plugin: ScenePlugin, fixture: ContractFixture) => void;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { resolveTheme, THEME_NAMES, toCssVars } from "@anvpx/motion-themes";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { runtimeFiles } from "./runtime-files.js";
|
|
4
|
+
import { emitContext, entryOf } from "./test-fixtures.js";
|
|
5
|
+
const MIN_TEXT_PX = 28;
|
|
6
|
+
const HEX_COLOR = /#[0-9a-fA-F]{3,8}\b/;
|
|
7
|
+
const FUNCTION_COLOR = /\b(?:rgb|rgba|hsl|hsla)\(/;
|
|
8
|
+
const files = (plugin, props, extra = {}) => {
|
|
9
|
+
const id = `${plugin.type}-1`;
|
|
10
|
+
const out = plugin.emit(entryOf(plugin.type, props, extra), emitContext());
|
|
11
|
+
const get = (name) => out.get(`scenes/${id}/${name}`) ?? "";
|
|
12
|
+
return { id, out, html: get("fragment.html"), css: get("style.css"), js: get("scene.js") };
|
|
13
|
+
};
|
|
14
|
+
const declared = (css) => new Set([...css.matchAll(/(--mk-[\w-]+):/g)].map((m) => m[1] ?? ""));
|
|
15
|
+
const referenced = (css) => [...css.matchAll(/var\((--mk-[\w-]+)\)/g)].map((m) => m[1] ?? "");
|
|
16
|
+
const themeVars = (name) => declared(toCssVars(resolveTheme(name)));
|
|
17
|
+
export const describeSceneContract = (plugin, fixture) => {
|
|
18
|
+
describe(`${plugin.type} emit contract`, () => {
|
|
19
|
+
it("emits fragment, style and script under scenes/<id>/", () => {
|
|
20
|
+
const { out, id } = files(plugin, fixture.valid);
|
|
21
|
+
expect([...out.keys()]).toEqual([
|
|
22
|
+
`scenes/${id}/fragment.html`,
|
|
23
|
+
`scenes/${id}/style.css`,
|
|
24
|
+
`scenes/${id}/scene.js`,
|
|
25
|
+
]);
|
|
26
|
+
});
|
|
27
|
+
it("fragment is one root element with id scene-<id> and no clip class", () => {
|
|
28
|
+
const { html, id } = files(plugin, fixture.valid);
|
|
29
|
+
expect(html.startsWith(`<section id="scene-${id}" `)).toBe(true);
|
|
30
|
+
expect(html.trimEnd().endsWith("</section>")).toBe(true);
|
|
31
|
+
expect(html.match(/<section/g)).toHaveLength(1);
|
|
32
|
+
expect(html).not.toMatch(/class="[^"]*\bclip\b/);
|
|
33
|
+
});
|
|
34
|
+
it("matches the golden output", () => {
|
|
35
|
+
const { out } = files(plugin, fixture.valid, { move: "push-in", focus: "title" });
|
|
36
|
+
const text = [...out].map(([path, body]) => `=== ${path}\n${body}`).join("\n");
|
|
37
|
+
expect(text).toMatchSnapshot();
|
|
38
|
+
});
|
|
39
|
+
it("is byte-identical when run twice", () => {
|
|
40
|
+
expect([...files(plugin, fixture.valid).out]).toEqual([...files(plugin, fixture.valid).out]);
|
|
41
|
+
});
|
|
42
|
+
it("escapes all prop text", () => {
|
|
43
|
+
const { html } = files(plugin, fixture.hostile);
|
|
44
|
+
expect(html).not.toContain("<script");
|
|
45
|
+
expect(html).toContain("<script>");
|
|
46
|
+
expect(html).toContain(""");
|
|
47
|
+
});
|
|
48
|
+
it("has no inline handlers or external urls", () => {
|
|
49
|
+
const { html, css, js } = files(plugin, fixture.valid);
|
|
50
|
+
expect(html + css + js).not.toMatch(/\son[a-z]+\s*=|https?:\/\/|javascript:/);
|
|
51
|
+
});
|
|
52
|
+
it("css uses only theme custom properties and no hard-coded colours", () => {
|
|
53
|
+
const { css } = files(plugin, fixture.valid);
|
|
54
|
+
expect(css).not.toMatch(HEX_COLOR);
|
|
55
|
+
expect(css).not.toMatch(FUNCTION_COLOR);
|
|
56
|
+
for (const theme of THEME_NAMES) {
|
|
57
|
+
const defined = themeVars(theme);
|
|
58
|
+
for (const name of referenced(css))
|
|
59
|
+
expect(defined.has(name), `${theme} ${name}`).toBe(true);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
it("text is >= 28px and uses one theme font family", () => {
|
|
63
|
+
const { css } = files(plugin, fixture.valid);
|
|
64
|
+
const sizes = [...css.matchAll(/(?<![-\w])font-size:\s*([^;]+);/g)].map((m) => m[1] ?? "");
|
|
65
|
+
expect(sizes.length).toBeGreaterThan(0);
|
|
66
|
+
for (const size of sizes) {
|
|
67
|
+
const step = /^var\(--mk-font-size-(\w+)\)$/.exec(size)?.[1] ?? "";
|
|
68
|
+
const px = emitContext().theme.fontSizes[step] ?? 0;
|
|
69
|
+
expect(px, size).toBeGreaterThanOrEqual(MIN_TEXT_PX);
|
|
70
|
+
}
|
|
71
|
+
const families = [...css.matchAll(/font-family:\s*([^;]+);/g)].map((m) => m[1]);
|
|
72
|
+
// Sans everywhere; code-like scenes may add the theme mono stack.
|
|
73
|
+
const allowed = new Set([
|
|
74
|
+
"var(--mk-font-sans)",
|
|
75
|
+
"var(--mk-font-mono)",
|
|
76
|
+
"var(--mk-font-display, var(--mk-font-sans))",
|
|
77
|
+
]);
|
|
78
|
+
expect(families.every((family) => allowed.has(family ?? ""))).toBe(true);
|
|
79
|
+
expect(families).toContain("var(--mk-font-sans)");
|
|
80
|
+
});
|
|
81
|
+
it("keeps the safe margin from the context size", () => {
|
|
82
|
+
expect(files(plugin, fixture.valid).css).toContain("padding: 54px 96px;");
|
|
83
|
+
});
|
|
84
|
+
it("carries ease (default out), fps and duration on the root", () => {
|
|
85
|
+
const { html } = files(plugin, fixture.valid);
|
|
86
|
+
expect(html).toContain('data-ease="out"');
|
|
87
|
+
expect(html).toContain('data-fps="30"');
|
|
88
|
+
expect(html).toContain('data-duration="6"');
|
|
89
|
+
expect(files(plugin, fixture.valid, { ease: "in-out" }).html).toContain('data-ease="in-out"');
|
|
90
|
+
});
|
|
91
|
+
it("script is an ES module importing only emitted runtime files", () => {
|
|
92
|
+
const { js } = files(plugin, fixture.valid);
|
|
93
|
+
expect(js).toMatch(/export function createScene\(root, config, ctx\)/);
|
|
94
|
+
const runtime = runtimeFiles();
|
|
95
|
+
const imports = [...js.matchAll(/from "\.\.\/\.\.\/(runtime\/[\w-]+\.js)"/g)];
|
|
96
|
+
expect(imports.length).toBeGreaterThan(0);
|
|
97
|
+
for (const match of imports)
|
|
98
|
+
expect(runtime.has(match[1] ?? "")).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
it("rejects invalid props on emit", () => {
|
|
101
|
+
expect(() => plugin.emit(entryOf(plugin.type, { bogus: 1 }), emitContext())).toThrow();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const POST_PRESETS: readonly ["clean", "bloom", "cinematic", "vintage"];
|
|
2
|
+
export type PostPreset = (typeof POST_PRESETS)[number];
|
|
3
|
+
export interface PostParams {
|
|
4
|
+
readonly bloom: number;
|
|
5
|
+
readonly grain: number;
|
|
6
|
+
readonly vignette: number;
|
|
7
|
+
readonly toneMapping: "none" | "agx";
|
|
8
|
+
}
|
|
9
|
+
export declare const isPostPreset: (value: unknown) => value is PostPreset;
|
|
10
|
+
export declare const resolvePost: (preset: PostPreset) => PostParams;
|
|
11
|
+
/** Seeded film-grain frame: pure function of t (30 fps lattice, spike receipt). */
|
|
12
|
+
export declare const grainSeed: (t: number) => number;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Clean-room post chain for the scene3d base scene.
|
|
2
|
+
// MIT attribution: three.js (MIT), postprocessing (MIT). Behaviour reimplemented
|
|
3
|
+
// from projects/spike-scene3d/src/main.js (do not copy); diorama concept follows
|
|
4
|
+
// MIT-licensed three.js examples (e.g. RoomEnvironment). No CDN, no copied code.
|
|
5
|
+
// Curated post presets: small, spike-proven combos (bloom / AgX / vignette / grain).
|
|
6
|
+
export const POST_PRESETS = ["clean", "bloom", "cinematic", "vintage"];
|
|
7
|
+
const POST_TABLE = {
|
|
8
|
+
clean: { bloom: 0, grain: 0.01, vignette: 0, toneMapping: "none" },
|
|
9
|
+
bloom: { bloom: 1.1, grain: 0.02, vignette: 0.3, toneMapping: "none" },
|
|
10
|
+
cinematic: { bloom: 1.1, grain: 0.035, vignette: 0.55, toneMapping: "agx" },
|
|
11
|
+
vintage: { bloom: 0.6, grain: 0.06, vignette: 0.7, toneMapping: "agx" },
|
|
12
|
+
};
|
|
13
|
+
export const isPostPreset = (value) => typeof value === "string" && POST_PRESETS.includes(value);
|
|
14
|
+
export const resolvePost = (preset) => POST_TABLE[preset];
|
|
15
|
+
/** Seeded film-grain frame: pure function of t (30 fps lattice, spike receipt). */
|
|
16
|
+
export const grainSeed = (t) => Math.floor(t * 30) % 1000;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const REFLECTOR_PRESETS: readonly ["off", "gloss", "mirror"];
|
|
2
|
+
export type ReflectorPreset = (typeof REFLECTOR_PRESETS)[number];
|
|
3
|
+
export interface ReflectorParams {
|
|
4
|
+
readonly reflectivity: number;
|
|
5
|
+
readonly roughness: number;
|
|
6
|
+
}
|
|
7
|
+
export declare const resolveReflector: (preset: ReflectorPreset) => ReflectorParams;
|
|
8
|
+
/** Fresnel-ish mix: 0 when off, otherwise reflectivity faded by surface roughness. */
|
|
9
|
+
export declare const reflectorMix: (roughness: number, preset: ReflectorPreset) => number;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Clean-room planar reflector preset for scene3d (MIT: three.js).
|
|
2
|
+
// Pure math; no DOM, no clock.
|
|
3
|
+
export const REFLECTOR_PRESETS = ["off", "gloss", "mirror"];
|
|
4
|
+
const TABLE = {
|
|
5
|
+
off: { reflectivity: 0, roughness: 1 },
|
|
6
|
+
gloss: { reflectivity: 0.4, roughness: 0.35 },
|
|
7
|
+
mirror: { reflectivity: 0.9, roughness: 0.05 },
|
|
8
|
+
};
|
|
9
|
+
export const resolveReflector = (preset) => TABLE[preset];
|
|
10
|
+
/** Fresnel-ish mix: 0 when off, otherwise reflectivity faded by surface roughness. */
|
|
11
|
+
export const reflectorMix = (roughness, preset) => {
|
|
12
|
+
const { reflectivity } = TABLE[preset];
|
|
13
|
+
if (reflectivity === 0)
|
|
14
|
+
return 0;
|
|
15
|
+
const clamped = Math.min(1, Math.max(0, roughness));
|
|
16
|
+
return Math.min(1, reflectivity * (1 - clamped));
|
|
17
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const HDRI_PRESETS: readonly ["neutral", "studio", "sunset", "night"];
|
|
2
|
+
export type HdriPreset = (typeof HDRI_PRESETS)[number];
|
|
3
|
+
export interface HdriParams {
|
|
4
|
+
readonly intensity: number;
|
|
5
|
+
readonly background: number;
|
|
6
|
+
readonly exposure: number;
|
|
7
|
+
}
|
|
8
|
+
export declare const resolveHdri: (preset: HdriPreset) => HdriParams;
|
|
9
|
+
export declare const hdriEnv: (preset: HdriPreset) => HdriParams;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Clean-room procedural environment for scene3d (MIT: three.js incl. RoomEnvironment).
|
|
2
|
+
// No fetches, no HDRI downloads: curated studio gradients resolved offline.
|
|
3
|
+
export const HDRI_PRESETS = ["neutral", "studio", "sunset", "night"];
|
|
4
|
+
const TABLE = {
|
|
5
|
+
neutral: { intensity: 0.6, background: 0x0a0c12, exposure: 1.0 },
|
|
6
|
+
studio: { intensity: 1.2, background: 0x111318, exposure: 1.1 },
|
|
7
|
+
sunset: { intensity: 0.9, background: 0x1a1210, exposure: 1.0 },
|
|
8
|
+
night: { intensity: 0.4, background: 0x05070c, exposure: 0.9 },
|
|
9
|
+
};
|
|
10
|
+
export const resolveHdri = (preset) => TABLE[preset];
|
|
11
|
+
export const hdriEnv = (preset) => TABLE[preset];
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const VOLUMETRIC_PRESETS: readonly ["off", "soft", "dense"];
|
|
2
|
+
export type VolumetricPreset = (typeof VOLUMETRIC_PRESETS)[number];
|
|
3
|
+
export interface VolumetricParams {
|
|
4
|
+
readonly density: number;
|
|
5
|
+
readonly steps: number;
|
|
6
|
+
}
|
|
7
|
+
export declare const resolveVolumetric: (preset: VolumetricPreset) => VolumetricParams;
|
|
8
|
+
/** Gentle breathing of the shaft: deterministic in t, zero when off. */
|
|
9
|
+
export declare const volumetricIntensity: (t: number, preset: VolumetricPreset) => number;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Clean-room volumetric preset for scene3d (MIT: three.js, postprocessing).
|
|
2
|
+
// Pure function of t; no clock, no random.
|
|
3
|
+
export const VOLUMETRIC_PRESETS = ["off", "soft", "dense"];
|
|
4
|
+
const TABLE = {
|
|
5
|
+
off: { density: 0, steps: 0 },
|
|
6
|
+
soft: { density: 0.35, steps: 32 },
|
|
7
|
+
dense: { density: 0.7, steps: 64 },
|
|
8
|
+
};
|
|
9
|
+
export const resolveVolumetric = (preset) => TABLE[preset];
|
|
10
|
+
/** Gentle breathing of the shaft: deterministic in t, zero when off. */
|
|
11
|
+
export const volumetricIntensity = (t, preset) => {
|
|
12
|
+
const { density } = TABLE[preset];
|
|
13
|
+
if (density === 0)
|
|
14
|
+
return 0;
|
|
15
|
+
return density * (0.9 + 0.1 * Math.cos(t));
|
|
16
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const bugScene: import("@anvpx/motion-schema").ScenePlugin;
|
|
@@ -0,0 +1,45 @@
|
|
|
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 { text } from "../../words.js";
|
|
7
|
+
const configSchema = z
|
|
8
|
+
.strictObject({
|
|
9
|
+
text: text.max(32).optional(),
|
|
10
|
+
image: text.optional(),
|
|
11
|
+
position: cornerSchema.default("top-right"),
|
|
12
|
+
opacity: z.enum(["subtle", "medium", "full"]).default("medium"),
|
|
13
|
+
})
|
|
14
|
+
.superRefine((props, ctx) => {
|
|
15
|
+
if ((props.text === undefined) === (props.image === undefined)) {
|
|
16
|
+
ctx.addIssue({
|
|
17
|
+
code: "custom",
|
|
18
|
+
message: "give exactly one of text or image",
|
|
19
|
+
params: { suggestion: "set text for a word mark, or image for a ledger asset" },
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
const mark = (props, resolve) => props.image === undefined
|
|
24
|
+
? `<span class="mk-text">${esc(props.text ?? "")}</span>`
|
|
25
|
+
: `<img class="mk-image" src="${esc(resolve(props.image))}" alt="" />`;
|
|
26
|
+
const render = (props, resolve) => [
|
|
27
|
+
`<div class="mk-op-${props.opacity} ${positionClass(props.position)}">`,
|
|
28
|
+
`<div class="mk-bug" ${animAttrs("fade", 0)}>${mark(props, resolve)}</div>`,
|
|
29
|
+
"</div>",
|
|
30
|
+
].join("\n");
|
|
31
|
+
export const bugScene = defineScene({
|
|
32
|
+
type: "bug",
|
|
33
|
+
layer: "overlay",
|
|
34
|
+
configSchema,
|
|
35
|
+
meta: {
|
|
36
|
+
purpose: "Corner word mark or ledger image; give exactly one of text or image.",
|
|
37
|
+
example: { text: "ACME" },
|
|
38
|
+
},
|
|
39
|
+
style: readText("./style.css", import.meta.url),
|
|
40
|
+
script: readText("./scene.js", import.meta.url),
|
|
41
|
+
render: (props, ctx, root) => ({
|
|
42
|
+
body: render(props, ctx.assetResolver),
|
|
43
|
+
css: positionCss(root),
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
@@ -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: 4, rise: 0 });
|
|
6
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#scene-root .mk-op-subtle {
|
|
2
|
+
opacity: var(--mk-opacity-subtle);
|
|
3
|
+
}
|
|
4
|
+
#scene-root .mk-op-medium {
|
|
5
|
+
opacity: var(--mk-opacity-medium);
|
|
6
|
+
}
|
|
7
|
+
#scene-root .mk-op-full {
|
|
8
|
+
opacity: var(--mk-opacity-full);
|
|
9
|
+
}
|
|
10
|
+
#scene-root .mk-bug {
|
|
11
|
+
display: flex;
|
|
12
|
+
align-items: center;
|
|
13
|
+
padding: var(--mk-space-sm) var(--mk-space-lg);
|
|
14
|
+
border-radius: var(--mk-radius-full);
|
|
15
|
+
border: var(--mk-stroke-card, 2px) solid var(--mk-color-divider);
|
|
16
|
+
background: var(--mk-color-card);
|
|
17
|
+
}
|
|
18
|
+
#scene-root .mk-text {
|
|
19
|
+
font-size: var(--mk-font-size-sm);
|
|
20
|
+
font-weight: 700;
|
|
21
|
+
letter-spacing: 0.04em;
|
|
22
|
+
text-transform: uppercase;
|
|
23
|
+
color: var(--mk-color-fg);
|
|
24
|
+
}
|
|
25
|
+
#scene-root .mk-image {
|
|
26
|
+
display: block;
|
|
27
|
+
height: var(--mk-space-xl);
|
|
28
|
+
max-width: 360px;
|
|
29
|
+
object-fit: contain;
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const bulletsScene: import("@anvpx/motion-schema").ScenePlugin;
|
|
@@ -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,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;
|