odori 0.0.2 → 0.0.4

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/dist/preview.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { Component, ReactNode, ErrorInfo, ComponentType } from 'react';
3
- import { f as CueDefinition, D as Duration, B as Brand } from './brand-D71KbhAe.js';
3
+ import { f as CueDefinition, D as Duration, B as Brand } from './brand-B-Mv4S_Z.js';
4
4
  import { F as FieldDescriptor } from './schema-DXxRezrk.js';
5
5
  import * as react_jsx_runtime from 'react/jsx-runtime';
6
6
 
@@ -24,6 +24,28 @@ type CueWaveProps = {
24
24
  */
25
25
  declare const CueWave: ({ cue, label, columns }: CueWaveProps) => react_jsx_runtime.JSX.Element;
26
26
 
27
+ type SampleWaveProps = {
28
+ /** Peak per column, 0 to 1, measured from the file when it was published. */
29
+ peaks: number[];
30
+ /** Shown under the wave. The cue name a project would register it as. */
31
+ label: string;
32
+ /** Seconds the recording runs, for the caption. */
33
+ seconds?: number;
34
+ /** Whether the file is cut to repeat. */
35
+ loops?: boolean;
36
+ };
37
+ /**
38
+ * A recording drawn from measured peaks, with a playhead crossing it in time.
39
+ *
40
+ * The sibling of `CueWave`, for sound that is a file rather than a score.
41
+ * A score can be rendered here and now, so `CueWave` draws the real samples; a
42
+ * recording would have to be fetched and decoded, which a frame cannot wait
43
+ * for and a still cannot do at all. So the peaks are measured once when the
44
+ * asset is published and travel in the registry with it: the same picture,
45
+ * deterministic, and no network inside a render.
46
+ */
47
+ declare const SampleWave: ({ peaks, label, seconds, loops }: SampleWaveProps) => react_jsx_runtime.JSX.Element;
48
+
27
49
  type PreviewBoundaryProps = {
28
50
  children: ReactNode;
29
51
  /**
@@ -62,7 +84,22 @@ declare class PreviewBoundary extends Component<PreviewBoundaryProps, State> {
62
84
  type ComponentPreview<Props extends object = Record<string, unknown>> = {
63
85
  readonly kind: "odori-component-preview";
64
86
  title: string;
65
- category: string;
87
+ /**
88
+ * Where this sits, slash separated: `Interface/Controls`.
89
+ *
90
+ * The first level is the family, which is what a catalog filters by; the
91
+ * rest are groups inside it, which is how a library stays navigable once
92
+ * one family has twenty components in it. A category with no slash is a
93
+ * family on its own, which is all most projects ever need.
94
+ *
95
+ * Optional, because the directory already says this. A component at
96
+ * `videos/components/interface/controls/combobox/` is in Interface/Controls
97
+ * without being told, so a project organises by moving files and cannot
98
+ * misspell a group into existence. Declaring it is for source that travels:
99
+ * a registry component is copied into somebody else's tree, lands wherever
100
+ * they put it, and would otherwise arrive filed under nothing.
101
+ */
102
+ category?: string;
66
103
  description?: string;
67
104
  component: ComponentType<Props>;
68
105
  canvas: {
@@ -83,10 +120,47 @@ type ComponentPreviewInput<Props extends object> = Omit<ComponentPreview<Props>,
83
120
  props: Partial<Props>;
84
121
  }>;
85
122
  };
123
+ /**
124
+ * The category a file's own location implies.
125
+ *
126
+ * Everything between the components directory and the component's own folder,
127
+ * so `videos/components/interface/controls/combobox/combobox.preview.tsx` is
128
+ * `Interface/Controls`, and a component sitting directly in the components
129
+ * directory belongs to no group at all. Names are read as words: a directory
130
+ * called `developer-proof` is the family "Developer proof", which keeps the
131
+ * shell honest and the heading readable.
132
+ */
133
+ declare const categoryFromPath: (relativeFile: string, componentsDir?: string,
134
+ /**
135
+ * What a directory calls itself, given its path under the components
136
+ * directory. This is where a `category.json` gets a say: a filesystem
137
+ * cannot tell you that `product-ui` reads "Product UI", and reading the
138
+ * name as words cannot either, because the same rule has to leave
139
+ * "Developer proof" alone.
140
+ */
141
+ labelOf?: (path: string) => string | undefined) => string;
142
+ /**
143
+ * The levels of a category, outermost first.
144
+ *
145
+ * Everything that organises components needs this - Studio, the catalog, the
146
+ * registry - and each of them had grown its own `split("/")`, which is the
147
+ * same fact in six places waiting to disagree about whitespace. It lives here
148
+ * because the convention is the framework's, not any one reader's.
149
+ */
150
+ declare const categoryPath: (category: string) => string[];
151
+ /** The family: the first level, and what a catalog filters by. */
152
+ declare const familyOf: (category: string) => string;
153
+ /** The levels inside the family, joined, or "" for a component at its top. */
154
+ declare const groupOf: (category: string) => string;
86
155
  /**
87
156
  * A development-only fixture. Production bundles never import preview modules,
88
157
  * so this contract can be as rich as Studio needs.
158
+ *
159
+ * The category is normalised here rather than by every reader: ` Interface /
160
+ * Controls ` and `Interface/Controls` are the same place, and a fixture that
161
+ * names no place at all is a mistake worth hearing about at the point it is
162
+ * written rather than seeing as a card filed under nothing.
89
163
  */
90
164
  declare const defineComponentPreview: <Props extends object>(preview: ComponentPreviewInput<Props>) => ComponentPreview<Props>;
91
165
 
92
- export { type ComponentPreview, type ComponentPreviewInput, CueWave, type CueWaveProps, PreviewBoundary, type PreviewBoundaryProps, defineComponentPreview };
166
+ export { type ComponentPreview, type ComponentPreviewInput, CueWave, type CueWaveProps, PreviewBoundary, type PreviewBoundaryProps, SampleWave, type SampleWaveProps, categoryFromPath, categoryPath, defineComponentPreview, familyOf, groupOf };
package/dist/preview.js CHANGED
@@ -7,8 +7,8 @@ import {
7
7
  useDesignScale,
8
8
  useFrame,
9
9
  useVideo
10
- } from "./chunk-2SROZSZM.js";
11
- import "./chunk-PXG45HYV.js";
10
+ } from "./chunk-XN6DA264.js";
11
+ import "./chunk-V34N2I4A.js";
12
12
 
13
13
  // src/cue-wave.tsx
14
14
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -50,9 +50,39 @@ var CueWave = ({ cue, label, columns = 120 }) => {
50
50
  ] });
51
51
  };
52
52
 
53
+ // src/sample-wave.tsx
54
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
55
+ var SampleWave = ({ peaks, label, seconds, loops }) => {
56
+ const frame = useFrame();
57
+ const brand = useBrand();
58
+ const scale = useDesignScale();
59
+ const { durationInFrames } = useVideo();
60
+ const progress = durationInFrames <= 1 ? 0 : frame / (durationInFrames - 1);
61
+ return /* @__PURE__ */ jsxs2(Fill, { style: { alignItems: "center", gap: 34 * scale, justifyContent: "center", padding: 120 * scale }, children: [
62
+ /* @__PURE__ */ jsx2("div", { style: { alignItems: "center", display: "flex", gap: 4 * scale, height: 260 * scale }, children: peaks.map((value, index) => /* @__PURE__ */ jsx2(
63
+ "span",
64
+ {
65
+ style: {
66
+ background: index / peaks.length <= progress ? brand.colors.accent : brand.colors.border,
67
+ borderRadius: 999,
68
+ height: Math.max(2 * scale, value * 260 * scale),
69
+ width: 6 * scale
70
+ }
71
+ },
72
+ index
73
+ )) }),
74
+ /* @__PURE__ */ jsxs2("div", { style: { color: brand.colors.muted, fontFamily: brand.typography.mono, fontSize: 28 * scale }, children: [
75
+ label,
76
+ seconds === void 0 ? "" : ` \xB7 ${seconds}s`,
77
+ loops ? " \xB7 loops" : "",
78
+ " \xB7 recorded"
79
+ ] })
80
+ ] });
81
+ };
82
+
53
83
  // src/preview-boundary.tsx
54
84
  import { Component } from "react";
55
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
85
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
56
86
  var sourceFrame = (stack) => {
57
87
  if (!stack) return null;
58
88
  for (const line of stack.split("\n")) {
@@ -68,7 +98,7 @@ var openInEditor = (location) => {
68
98
  var PreviewErrorPanel = ({ error, componentStack, label }) => {
69
99
  const location = sourceFrame(componentStack ?? null);
70
100
  const [file] = location ? location.split(":") : [null];
71
- return /* @__PURE__ */ jsxs2(
101
+ return /* @__PURE__ */ jsxs3(
72
102
  "div",
73
103
  {
74
104
  "data-odori-preview-error": true,
@@ -92,9 +122,9 @@ var PreviewErrorPanel = ({ error, componentStack, label }) => {
92
122
  textAlign: "left"
93
123
  },
94
124
  children: [
95
- /* @__PURE__ */ jsx2("strong", { style: { color: "#ff9b91", fontSize: 12, letterSpacing: "0.08em", textTransform: "uppercase" }, children: label ?? "This composition threw" }),
96
- /* @__PURE__ */ jsx2("span", { style: { fontSize: 15 }, children: error.message }),
97
- location ? /* @__PURE__ */ jsxs2(
125
+ /* @__PURE__ */ jsx3("strong", { style: { color: "#ff9b91", fontSize: 12, letterSpacing: "0.08em", textTransform: "uppercase" }, children: label ?? "This composition threw" }),
126
+ /* @__PURE__ */ jsx3("span", { style: { fontSize: 15 }, children: error.message }),
127
+ location ? /* @__PURE__ */ jsxs3(
98
128
  "button",
99
129
  {
100
130
  type: "button",
@@ -115,8 +145,8 @@ var PreviewErrorPanel = ({ error, componentStack, label }) => {
115
145
  ]
116
146
  }
117
147
  ) : null,
118
- componentStack ? /* @__PURE__ */ jsx2("pre", { style: { color: "#c9b6b4", fontSize: 11, margin: 0, whiteSpace: "pre-wrap" }, children: componentStack.trim().split("\n").slice(0, 6).join("\n") }) : null,
119
- /* @__PURE__ */ jsx2("span", { style: { color: "#c9b6b4", fontSize: 11 }, children: "Fix the file and save. The preview recovers on reload." })
148
+ componentStack ? /* @__PURE__ */ jsx3("pre", { style: { color: "#c9b6b4", fontSize: 11, margin: 0, whiteSpace: "pre-wrap" }, children: componentStack.trim().split("\n").slice(0, 6).join("\n") }) : null,
149
+ /* @__PURE__ */ jsx3("span", { style: { color: "#c9b6b4", fontSize: 11 }, children: "Fix the file and save. The preview recovers on reload." })
120
150
  ]
121
151
  }
122
152
  );
@@ -138,19 +168,40 @@ var PreviewBoundary = class extends Component {
138
168
  render() {
139
169
  const { error, componentStack } = this.state;
140
170
  if (!error) return this.props.children;
141
- return /* @__PURE__ */ jsx2(PreviewErrorPanel, { error, componentStack, label: this.props.label });
171
+ return /* @__PURE__ */ jsx3(PreviewErrorPanel, { error, componentStack, label: this.props.label });
142
172
  }
143
173
  };
144
174
 
145
175
  // src/preview.ts
146
- var defineComponentPreview = (preview) => ({
147
- kind: "odori-component-preview",
148
- examples: [{ name: "Default", props: {} }],
149
- ...preview
150
- });
176
+ var categoryFromPath = (relativeFile, componentsDir = "components", labelOf) => {
177
+ const parts = relativeFile.split("/").filter(Boolean);
178
+ const start = parts.lastIndexOf(componentsDir);
179
+ if (start === -1) return "";
180
+ const between = parts.slice(start + 1, -2);
181
+ return between.map(
182
+ (part, index) => labelOf?.(between.slice(0, index + 1).join("/")) ?? part.replace(/-/g, " ").replace(/^./, (letter) => letter.toUpperCase())
183
+ ).join("/");
184
+ };
185
+ var categoryPath = (category) => category.split("/").map((part) => part.trim()).filter(Boolean);
186
+ var familyOf = (category) => categoryPath(category)[0] ?? "";
187
+ var groupOf = (category) => categoryPath(category).slice(1).join("/");
188
+ var defineComponentPreview = (preview) => {
189
+ const category = preview.category === void 0 ? void 0 : categoryPath(preview.category).join("/");
190
+ return {
191
+ kind: "odori-component-preview",
192
+ examples: [{ name: "Default", props: {} }],
193
+ ...preview,
194
+ ...category === void 0 ? {} : { category }
195
+ };
196
+ };
151
197
  export {
152
198
  CueWave,
153
199
  PreviewBoundary,
154
- defineComponentPreview
200
+ SampleWave,
201
+ categoryFromPath,
202
+ categoryPath,
203
+ defineComponentPreview,
204
+ familyOf,
205
+ groupOf
155
206
  };
156
207
  //# sourceMappingURL=preview.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cue-wave.tsx","../src/preview-boundary.tsx","../src/preview.ts"],"sourcesContent":["import {Fill} from \"./runtime\";\nimport {useBrand, useDesignScale, useFrame, useVideo} from \"./context\";\nimport {cueSamples, type CueDefinition} from \"./cue\";\nimport {SAMPLE_RATE} from \"./synth\";\n\nexport type CueWaveProps = {\n /** The score to draw. Its samples are rendered, not approximated. */\n cue: CueDefinition;\n /** Shown under the wave. Defaults to the name the cue registers. */\n label?: string;\n /** Bars across the canvas. More is finer, and slower to render. */\n columns?: number;\n};\n\n/**\n * A cue drawn from its own samples, with a playhead crossing it in time.\n *\n * A sound needs a picture on a page that cannot make noise until it is\n * clicked, and the honest picture is the waveform the score actually produces:\n * rendering is deterministic, so this is the sound rather than a decorative\n * squiggle. It lives in `odori/preview` because it is preview furniture — a\n * project owns the score it installed, not the chart of it, and fifteen cues\n * should not carry fifteen copies of this loop.\n */\nexport const CueWave = ({cue, label, columns = 120}: CueWaveProps) => {\n const frame = useFrame();\n const brand = useBrand();\n const scale = useDesignScale();\n const {fps, durationInFrames} = useVideo();\n\n const channel = cue.render({samples: cueSamples(cue, fps), sampleRate: SAMPLE_RATE}).channels[0];\n const width = Math.max(1, Math.floor(channel.length / columns));\n const peaks = Array.from({length: columns}, (_, index) => {\n let highest = 0;\n for (let sample = index * width; sample < Math.min(channel.length, (index + 1) * width); sample += 1) {\n highest = Math.max(highest, Math.abs(channel[sample]));\n }\n return highest;\n });\n\n const progress = durationInFrames <= 1 ? 0 : frame / (durationInFrames - 1);\n\n return (\n <Fill style={{alignItems: \"center\", gap: 34 * scale, justifyContent: \"center\", padding: 120 * scale}}>\n <div style={{alignItems: \"center\", display: \"flex\", gap: 4 * scale, height: 260 * scale}}>\n {peaks.map((value, index) => (\n <span\n key={index}\n style={{\n background: index / columns <= progress ? brand.colors.accent : brand.colors.border,\n borderRadius: 999,\n height: Math.max(2 * scale, value * 260 * scale),\n width: 6 * scale,\n }}\n />\n ))}\n </div>\n <div style={{color: brand.colors.muted, fontFamily: brand.typography.mono, fontSize: 28 * scale}}>\n {label ?? cue.name} · {cue.durationInFrames} frames{cue.loops ? \" · loops\" : \"\"}\n </div>\n </Fill>\n );\n};\n","\"use client\";\n\nimport {Component, type ErrorInfo, type ReactNode} from \"react\";\n\nexport type PreviewBoundaryProps = {\n children: ReactNode;\n /**\n * Changing this clears a caught error. Pass whatever identifies the thing\n * being previewed — an entry id, a format, a brand — so switching away from\n * a broken composition shows the next one instead of its predecessor's\n * error. A module edit clears it on its own, through the HMR hook below.\n */\n resetKey?: string | number;\n /** Shown above the message. Defaults to naming the composition generically. */\n label?: string;\n};\n\ntype State = {error: Error | null; componentStack: string | null};\n\n/** The first project file in a component stack, which is where an author looks. */\nexport const sourceFrame = (stack: string | null): string | null => {\n if (!stack) return null;\n for (const line of stack.split(\"\\n\")) {\n const match = /\\(?((?:\\/|[A-Za-z]:\\\\)[^):]+\\.(?:tsx|ts|jsx|js)):(\\d+):(\\d+)\\)?/.exec(line);\n if (match && !match[1].includes(\"node_modules\")) return `${match[1]}:${match[2]}:${match[3]}`;\n }\n return null;\n};\n\n/**\n * Vite's dev server exposes an editor hook. Studio and the docs site are both\n * Vite, so a file reference in this panel is a link to the line rather than a\n * path to copy by hand. It fails silently: an editor that will not open is not\n * worth a second error on top of the first.\n */\nconst openInEditor = (location: string) => {\n void fetch(`/__open-in-editor?file=${encodeURIComponent(location)}`).catch(() => {});\n};\n\nexport type PreviewErrorPanelProps = {\n error: Error;\n componentStack?: string | null;\n label?: string;\n};\n\n/** What a caught error looks like. Separated so it can be rendered and read. */\nexport const PreviewErrorPanel = ({error, componentStack, label}: PreviewErrorPanelProps) => {\n const location = sourceFrame(componentStack ?? null);\n const [file] = location ? location.split(\":\") : [null];\n\n return (\n <div\n data-odori-preview-error\n style={{\n alignItems: \"flex-start\",\n background: \"#120d0d\",\n border: \"1px solid #532\",\n borderRadius: 12,\n color: \"#f5eceb\",\n display: \"flex\",\n flexDirection: \"column\",\n fontFamily: \"ui-monospace, SFMono-Regular, Menlo, monospace\",\n fontSize: 13,\n gap: 12,\n inset: 0,\n justifyContent: \"center\",\n lineHeight: 1.6,\n overflow: \"auto\",\n padding: 24,\n position: \"absolute\",\n textAlign: \"left\",\n }}\n >\n <strong style={{color: \"#ff9b91\", fontSize: 12, letterSpacing: \"0.08em\", textTransform: \"uppercase\"}}>\n {label ?? \"This composition threw\"}\n </strong>\n <span style={{fontSize: 15}}>{error.message}</span>\n {location ? (\n <button\n type=\"button\"\n onClick={() => openInEditor(location)}\n style={{\n background: \"transparent\",\n border: \"1px solid #644\",\n borderRadius: 8,\n color: \"#f5eceb\",\n cursor: \"pointer\",\n fontFamily: \"inherit\",\n fontSize: 12,\n padding: \"6px 10px\",\n }}\n >\n {file?.split(\"/\").slice(-2).join(\"/\")}\n {location.slice(file?.length ?? 0)}\n </button>\n ) : null}\n {componentStack ? (\n <pre style={{color: \"#c9b6b4\", fontSize: 11, margin: 0, whiteSpace: \"pre-wrap\"}}>\n {componentStack.trim().split(\"\\n\").slice(0, 6).join(\"\\n\")}\n </pre>\n ) : null}\n <span style={{color: \"#c9b6b4\", fontSize: 11}}>Fix the file and save. The preview recovers on reload.</span>\n </div>\n );\n};\n\n/**\n * The designed failure for a composition that throws.\n *\n * Preview only. A render worker must fail loudly and exit non-zero rather than\n * paint an apology into an exported frame, which is why this lives in\n * `odori/preview` and no export path imports it.\n *\n * Without it, one bad component takes the whole surface down and leaves\n * whatever React last painted, with the real message in a console the author\n * may not have open.\n */\nexport class PreviewBoundary extends Component<PreviewBoundaryProps, State> {\n state: State = {error: null, componentStack: null};\n\n static getDerivedStateFromError(error: Error): Partial<State> {\n return {error};\n }\n\n componentDidUpdate(previous: PreviewBoundaryProps) {\n if (previous.resetKey !== this.props.resetKey && this.state.error) {\n this.setState({error: null, componentStack: null});\n }\n }\n\n componentDidCatch(error: Error, info: ErrorInfo) {\n this.setState({componentStack: info.componentStack ?? null});\n // Still report it: the panel is for the person watching, the console is\n // for the person debugging, and they are not always looking at the same\n // thing.\n console.error(error);\n }\n\n render() {\n const {error, componentStack} = this.state;\n if (!error) return this.props.children;\n return <PreviewErrorPanel error={error} componentStack={componentStack} label={this.props.label} />;\n }\n}\n","import {type ComponentType} from \"react\";\nimport {type Duration} from \"./time\";\nimport {type FieldDescriptor} from \"./schema\";\nimport {type Brand} from \"./brand\";\n\nexport type ComponentPreview<Props extends object = Record<string, unknown>> = {\n readonly kind: \"odori-component-preview\";\n title: string;\n category: string;\n description?: string;\n component: ComponentType<Props>;\n canvas: {width: number; height: number; duration: Duration};\n controls?: Record<string, FieldDescriptor>;\n brands?: Brand[];\n examples: Array<{name: string; props: Partial<Props>}>;\n};\n\nexport type ComponentPreviewInput<Props extends object> = Omit<ComponentPreview<Props>, \"kind\" | \"examples\"> & {\n examples?: Array<{name: string; props: Partial<Props>}>;\n};\n\n/**\n * A development-only fixture. Production bundles never import preview modules,\n * so this contract can be as rich as Studio needs.\n */\nexport const defineComponentPreview = <Props extends object>(\n preview: ComponentPreviewInput<Props>,\n): ComponentPreview<Props> => ({\n kind: \"odori-component-preview\",\n examples: [{name: \"Default\", props: {} as Partial<Props>}],\n ...preview,\n});\n\nexport {CueWave, type CueWaveProps} from \"./cue-wave\";\nexport {PreviewBoundary, type PreviewBoundaryProps} from \"./preview-boundary\";\n"],"mappings":"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","names":["jsx","jsxs"]}
1
+ {"version":3,"sources":["../src/cue-wave.tsx","../src/sample-wave.tsx","../src/preview-boundary.tsx","../src/preview.ts"],"sourcesContent":["import {Fill} from \"./runtime\";\nimport {useBrand, useDesignScale, useFrame, useVideo} from \"./context\";\nimport {cueSamples, type CueDefinition} from \"./cue\";\nimport {SAMPLE_RATE} from \"./synth\";\n\nexport type CueWaveProps = {\n /** The score to draw. Its samples are rendered, not approximated. */\n cue: CueDefinition;\n /** Shown under the wave. Defaults to the name the cue registers. */\n label?: string;\n /** Bars across the canvas. More is finer, and slower to render. */\n columns?: number;\n};\n\n/**\n * A cue drawn from its own samples, with a playhead crossing it in time.\n *\n * A sound needs a picture on a page that cannot make noise until it is\n * clicked, and the honest picture is the waveform the score actually produces:\n * rendering is deterministic, so this is the sound rather than a decorative\n * squiggle. It lives in `odori/preview` because it is preview furniture — a\n * project owns the score it installed, not the chart of it, and fifteen cues\n * should not carry fifteen copies of this loop.\n */\nexport const CueWave = ({cue, label, columns = 120}: CueWaveProps) => {\n const frame = useFrame();\n const brand = useBrand();\n const scale = useDesignScale();\n const {fps, durationInFrames} = useVideo();\n\n const channel = cue.render({samples: cueSamples(cue, fps), sampleRate: SAMPLE_RATE}).channels[0];\n const width = Math.max(1, Math.floor(channel.length / columns));\n const peaks = Array.from({length: columns}, (_, index) => {\n let highest = 0;\n for (let sample = index * width; sample < Math.min(channel.length, (index + 1) * width); sample += 1) {\n highest = Math.max(highest, Math.abs(channel[sample]));\n }\n return highest;\n });\n\n const progress = durationInFrames <= 1 ? 0 : frame / (durationInFrames - 1);\n\n return (\n <Fill style={{alignItems: \"center\", gap: 34 * scale, justifyContent: \"center\", padding: 120 * scale}}>\n <div style={{alignItems: \"center\", display: \"flex\", gap: 4 * scale, height: 260 * scale}}>\n {peaks.map((value, index) => (\n <span\n key={index}\n style={{\n background: index / columns <= progress ? brand.colors.accent : brand.colors.border,\n borderRadius: 999,\n height: Math.max(2 * scale, value * 260 * scale),\n width: 6 * scale,\n }}\n />\n ))}\n </div>\n <div style={{color: brand.colors.muted, fontFamily: brand.typography.mono, fontSize: 28 * scale}}>\n {label ?? cue.name} · {cue.durationInFrames} frames{cue.loops ? \" · loops\" : \"\"}\n </div>\n </Fill>\n );\n};\n","import {Fill} from \"./runtime\";\nimport {useBrand, useDesignScale, useFrame, useVideo} from \"./context\";\n\nexport type SampleWaveProps = {\n /** Peak per column, 0 to 1, measured from the file when it was published. */\n peaks: number[];\n /** Shown under the wave. The cue name a project would register it as. */\n label: string;\n /** Seconds the recording runs, for the caption. */\n seconds?: number;\n /** Whether the file is cut to repeat. */\n loops?: boolean;\n};\n\n/**\n * A recording drawn from measured peaks, with a playhead crossing it in time.\n *\n * The sibling of `CueWave`, for sound that is a file rather than a score.\n * A score can be rendered here and now, so `CueWave` draws the real samples; a\n * recording would have to be fetched and decoded, which a frame cannot wait\n * for and a still cannot do at all. So the peaks are measured once when the\n * asset is published and travel in the registry with it: the same picture,\n * deterministic, and no network inside a render.\n */\nexport const SampleWave = ({peaks, label, seconds, loops}: SampleWaveProps) => {\n const frame = useFrame();\n const brand = useBrand();\n const scale = useDesignScale();\n const {durationInFrames} = useVideo();\n const progress = durationInFrames <= 1 ? 0 : frame / (durationInFrames - 1);\n\n return (\n <Fill style={{alignItems: \"center\", gap: 34 * scale, justifyContent: \"center\", padding: 120 * scale}}>\n <div style={{alignItems: \"center\", display: \"flex\", gap: 4 * scale, height: 260 * scale}}>\n {peaks.map((value, index) => (\n <span\n key={index}\n style={{\n background: index / peaks.length <= progress ? brand.colors.accent : brand.colors.border,\n borderRadius: 999,\n height: Math.max(2 * scale, value * 260 * scale),\n width: 6 * scale,\n }}\n />\n ))}\n </div>\n <div style={{color: brand.colors.muted, fontFamily: brand.typography.mono, fontSize: 28 * scale}}>\n {label}\n {seconds === undefined ? \"\" : ` · ${seconds}s`}\n {loops ? \" · loops\" : \"\"} · recorded\n </div>\n </Fill>\n );\n};\n","\"use client\";\n\nimport {Component, type ErrorInfo, type ReactNode} from \"react\";\n\nexport type PreviewBoundaryProps = {\n children: ReactNode;\n /**\n * Changing this clears a caught error. Pass whatever identifies the thing\n * being previewed — an entry id, a format, a brand — so switching away from\n * a broken composition shows the next one instead of its predecessor's\n * error. A module edit clears it on its own, through the HMR hook below.\n */\n resetKey?: string | number;\n /** Shown above the message. Defaults to naming the composition generically. */\n label?: string;\n};\n\ntype State = {error: Error | null; componentStack: string | null};\n\n/** The first project file in a component stack, which is where an author looks. */\nexport const sourceFrame = (stack: string | null): string | null => {\n if (!stack) return null;\n for (const line of stack.split(\"\\n\")) {\n const match = /\\(?((?:\\/|[A-Za-z]:\\\\)[^):]+\\.(?:tsx|ts|jsx|js)):(\\d+):(\\d+)\\)?/.exec(line);\n if (match && !match[1].includes(\"node_modules\")) return `${match[1]}:${match[2]}:${match[3]}`;\n }\n return null;\n};\n\n/**\n * Vite's dev server exposes an editor hook. Studio and the docs site are both\n * Vite, so a file reference in this panel is a link to the line rather than a\n * path to copy by hand. It fails silently: an editor that will not open is not\n * worth a second error on top of the first.\n */\nconst openInEditor = (location: string) => {\n void fetch(`/__open-in-editor?file=${encodeURIComponent(location)}`).catch(() => {});\n};\n\nexport type PreviewErrorPanelProps = {\n error: Error;\n componentStack?: string | null;\n label?: string;\n};\n\n/** What a caught error looks like. Separated so it can be rendered and read. */\nexport const PreviewErrorPanel = ({error, componentStack, label}: PreviewErrorPanelProps) => {\n const location = sourceFrame(componentStack ?? null);\n const [file] = location ? location.split(\":\") : [null];\n\n return (\n <div\n data-odori-preview-error\n style={{\n alignItems: \"flex-start\",\n background: \"#120d0d\",\n border: \"1px solid #532\",\n borderRadius: 12,\n color: \"#f5eceb\",\n display: \"flex\",\n flexDirection: \"column\",\n fontFamily: \"ui-monospace, SFMono-Regular, Menlo, monospace\",\n fontSize: 13,\n gap: 12,\n inset: 0,\n justifyContent: \"center\",\n lineHeight: 1.6,\n overflow: \"auto\",\n padding: 24,\n position: \"absolute\",\n textAlign: \"left\",\n }}\n >\n <strong style={{color: \"#ff9b91\", fontSize: 12, letterSpacing: \"0.08em\", textTransform: \"uppercase\"}}>\n {label ?? \"This composition threw\"}\n </strong>\n <span style={{fontSize: 15}}>{error.message}</span>\n {location ? (\n <button\n type=\"button\"\n onClick={() => openInEditor(location)}\n style={{\n background: \"transparent\",\n border: \"1px solid #644\",\n borderRadius: 8,\n color: \"#f5eceb\",\n cursor: \"pointer\",\n fontFamily: \"inherit\",\n fontSize: 12,\n padding: \"6px 10px\",\n }}\n >\n {file?.split(\"/\").slice(-2).join(\"/\")}\n {location.slice(file?.length ?? 0)}\n </button>\n ) : null}\n {componentStack ? (\n <pre style={{color: \"#c9b6b4\", fontSize: 11, margin: 0, whiteSpace: \"pre-wrap\"}}>\n {componentStack.trim().split(\"\\n\").slice(0, 6).join(\"\\n\")}\n </pre>\n ) : null}\n <span style={{color: \"#c9b6b4\", fontSize: 11}}>Fix the file and save. The preview recovers on reload.</span>\n </div>\n );\n};\n\n/**\n * The designed failure for a composition that throws.\n *\n * Preview only. A render worker must fail loudly and exit non-zero rather than\n * paint an apology into an exported frame, which is why this lives in\n * `odori/preview` and no export path imports it.\n *\n * Without it, one bad component takes the whole surface down and leaves\n * whatever React last painted, with the real message in a console the author\n * may not have open.\n */\nexport class PreviewBoundary extends Component<PreviewBoundaryProps, State> {\n state: State = {error: null, componentStack: null};\n\n static getDerivedStateFromError(error: Error): Partial<State> {\n return {error};\n }\n\n componentDidUpdate(previous: PreviewBoundaryProps) {\n if (previous.resetKey !== this.props.resetKey && this.state.error) {\n this.setState({error: null, componentStack: null});\n }\n }\n\n componentDidCatch(error: Error, info: ErrorInfo) {\n this.setState({componentStack: info.componentStack ?? null});\n // Still report it: the panel is for the person watching, the console is\n // for the person debugging, and they are not always looking at the same\n // thing.\n console.error(error);\n }\n\n render() {\n const {error, componentStack} = this.state;\n if (!error) return this.props.children;\n return <PreviewErrorPanel error={error} componentStack={componentStack} label={this.props.label} />;\n }\n}\n","import {type ComponentType} from \"react\";\nimport {type Duration} from \"./time\";\nimport {type FieldDescriptor} from \"./schema\";\nimport {type Brand} from \"./brand\";\n\nexport type ComponentPreview<Props extends object = Record<string, unknown>> = {\n readonly kind: \"odori-component-preview\";\n title: string;\n /**\n * Where this sits, slash separated: `Interface/Controls`.\n *\n * The first level is the family, which is what a catalog filters by; the\n * rest are groups inside it, which is how a library stays navigable once\n * one family has twenty components in it. A category with no slash is a\n * family on its own, which is all most projects ever need.\n *\n * Optional, because the directory already says this. A component at\n * `videos/components/interface/controls/combobox/` is in Interface/Controls\n * without being told, so a project organises by moving files and cannot\n * misspell a group into existence. Declaring it is for source that travels:\n * a registry component is copied into somebody else's tree, lands wherever\n * they put it, and would otherwise arrive filed under nothing.\n */\n category?: string;\n description?: string;\n component: ComponentType<Props>;\n canvas: {width: number; height: number; duration: Duration};\n controls?: Record<string, FieldDescriptor>;\n brands?: Brand[];\n examples: Array<{name: string; props: Partial<Props>}>;\n};\n\nexport type ComponentPreviewInput<Props extends object> = Omit<ComponentPreview<Props>, \"kind\" | \"examples\"> & {\n examples?: Array<{name: string; props: Partial<Props>}>;\n};\n\n/**\n * The category a file's own location implies.\n *\n * Everything between the components directory and the component's own folder,\n * so `videos/components/interface/controls/combobox/combobox.preview.tsx` is\n * `Interface/Controls`, and a component sitting directly in the components\n * directory belongs to no group at all. Names are read as words: a directory\n * called `developer-proof` is the family \"Developer proof\", which keeps the\n * shell honest and the heading readable.\n */\nexport const categoryFromPath = (\n relativeFile: string,\n componentsDir = \"components\",\n /**\n * What a directory calls itself, given its path under the components\n * directory. This is where a `category.json` gets a say: a filesystem\n * cannot tell you that `product-ui` reads \"Product UI\", and reading the\n * name as words cannot either, because the same rule has to leave\n * \"Developer proof\" alone.\n */\n labelOf?: (path: string) => string | undefined,\n): string => {\n const parts = relativeFile.split(\"/\").filter(Boolean);\n const start = parts.lastIndexOf(componentsDir);\n if (start === -1) return \"\";\n // Drop the components directory, the component's own folder, and the file.\n const between = parts.slice(start + 1, -2);\n return between\n .map(\n (part, index) =>\n labelOf?.(between.slice(0, index + 1).join(\"/\")) ??\n part.replace(/-/g, \" \").replace(/^./, (letter) => letter.toUpperCase()),\n )\n .join(\"/\");\n};\n\n/**\n * The levels of a category, outermost first.\n *\n * Everything that organises components needs this - Studio, the catalog, the\n * registry - and each of them had grown its own `split(\"/\")`, which is the\n * same fact in six places waiting to disagree about whitespace. It lives here\n * because the convention is the framework's, not any one reader's.\n */\nexport const categoryPath = (category: string): string[] =>\n category\n .split(\"/\")\n .map((part) => part.trim())\n .filter(Boolean);\n\n/** The family: the first level, and what a catalog filters by. */\nexport const familyOf = (category: string): string => categoryPath(category)[0] ?? \"\";\n\n/** The levels inside the family, joined, or \"\" for a component at its top. */\nexport const groupOf = (category: string): string => categoryPath(category).slice(1).join(\"/\");\n\n/**\n * A development-only fixture. Production bundles never import preview modules,\n * so this contract can be as rich as Studio needs.\n *\n * The category is normalised here rather than by every reader: ` Interface /\n * Controls ` and `Interface/Controls` are the same place, and a fixture that\n * names no place at all is a mistake worth hearing about at the point it is\n * written rather than seeing as a card filed under nothing.\n */\nexport const defineComponentPreview = <Props extends object>(\n preview: ComponentPreviewInput<Props>,\n): ComponentPreview<Props> => {\n // Undeclared is the ordinary case now, and the reader supplies it from where\n // the file sits. What is normalised here is a category somebody wrote.\n const category = preview.category === undefined ? undefined : categoryPath(preview.category).join(\"/\");\n\n return {\n kind: \"odori-component-preview\",\n examples: [{name: \"Default\", props: {} as Partial<Props>}],\n ...preview,\n ...(category === undefined ? {} : {category}),\n };\n};\n\nexport {CueWave, type CueWaveProps} from \"./cue-wave\";\nexport {SampleWave, type SampleWaveProps} from \"./sample-wave\";\nexport {PreviewBoundary, type PreviewBoundaryProps} from \"./preview-boundary\";\n"],"mappings":"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","names":["jsx","jsxs","jsx","jsxs"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "odori",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "type": "module",
5
5
  "description": "An independent React video framework with a first-class videos source root, deterministic frame runtime, timeline compiler, player, and render manifest.",
6
6
  "license": "Apache-2.0",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/context.ts","../src/audio.ts","../src/synth.ts","../src/cue.ts","../src/easing.ts","../src/brand.ts","../src/layout.ts","../src/runtime.tsx"],"sourcesContent":["\"use client\";\n\nimport {createContext, useContext} from \"react\";\nimport {type AudioCue} from \"./audio\";\nimport {type Brand} from \"./brand\";\nimport {type VideoLayout} from \"./layout\";\n\nexport type FrameState = {frame: number; fps: number; width: number; height: number; durationInFrames: number};\nexport type SceneState = {id: string; name?: string; index: number; start: number; durationInFrames: number};\n\nexport type AssetRegistry = {\n resolve(reference: string): string;\n list(): Array<{reference: string; url: string}>;\n ready: boolean;\n};\n\nexport const FrameContext = createContext<FrameState | null>(null);\nexport const SceneContext = createContext<SceneState | null>(null);\nexport const BrandContext = createContext<Brand | null>(null);\nexport const LayoutContext = createContext<VideoLayout | null>(null);\nexport const AssetContext = createContext<AssetRegistry | null>(null);\n\nexport type AudioSink = {register(cue: AudioCue): void};\n\n/** Present only during the audio collection pass. */\nexport const AudioSinkContext = createContext<AudioSink | null>(null);\n\n/** Forces scenes to mount so their audio can be collected without seeking. */\nexport const ForceMountContext = createContext(false);\n\nexport type Readiness = {\n /** Hold the frame open. Call the returned function when the work is done. */\n hold(): () => void;\n};\n\nexport const ReadinessContext = createContext<Readiness | null>(null);\n\n/**\n * Block the frame until asynchronous work finishes.\n *\n * The driver waits for the frame attribute, and the runtime only writes it once\n * every held handle is released. Without this a component that decodes an image\n * would be screenshotted before it had anything to show.\n */\nexport const useReadiness = (): Readiness => {\n const readiness = useContext(ReadinessContext);\n // Outside a runtime, holding is a no-op rather than an error, so a component\n // stays usable in a test or a storybook-style harness.\n return readiness ?? {hold: () => () => undefined};\n};\n\nconst required = <Value,>(value: Value | null, hook: string): Value => {\n if (value === null) throw new Error(`${hook} must be called inside a <Video> tree.`);\n return value;\n};\n\n/** The frame index local to the nearest scene, or the video frame at the top level. */\nexport const useFrame = (): number => required(useContext(FrameContext), \"useFrame()\").frame;\n\nexport const useVideo = (): Omit<FrameState, \"frame\"> => {\n const state = required(useContext(FrameContext), \"useVideo()\");\n return {\n fps: state.fps,\n width: state.width,\n height: state.height,\n durationInFrames: state.durationInFrames,\n };\n};\n\n/**\n * A single number components multiply their design values by.\n *\n * Sizing from the shorter side keeps type and spacing readable when the same\n * component is composed into a 16:9, 9:16, or 1:1 frame. A width-derived scale\n * would shrink a vertical cut into unreadable text.\n */\nexport const useDesignScale = (reference = 1080): number => {\n const state = required(useContext(FrameContext), \"useDesignScale()\");\n return Math.min(state.width, state.height) / reference;\n};\n\nexport const useBrand = (): Brand => required(useContext(BrandContext), \"useBrand()\");\nexport const useLayout = (): VideoLayout => required(useContext(LayoutContext), \"useLayout()\");\nexport const useScene = (): SceneState => required(useContext(SceneContext), \"useScene()\");\n\nexport const useAssets = (): AssetRegistry => {\n const registry = useContext(AssetContext);\n return registry ?? {resolve: (reference: string) => reference, list: () => [], ready: true};\n};\n\nexport const createAssetRegistry = (entries: Array<{reference: string; url: string}>): AssetRegistry => {\n const table = new Map(entries.map((entry) => [entry.reference, entry.url]));\n return {\n ready: true,\n list: () => entries,\n resolve(reference) {\n const url = table.get(reference);\n if (!url) throw new Error(`Unknown asset reference: ${reference}. Declare it in prepare.ts or odori.config.ts.`);\n return url;\n },\n };\n};\n","import {type Duration} from \"./time\";\n\nexport type AudioCue = {\n /** Stable identity, derived from the source and its placement. */\n id: string;\n src: string;\n fromFrame: number;\n durationInFrames: number;\n /** Linear gain. 1 is unchanged. */\n gain: number;\n /**\n * An authored curve, sampled from a `gain` function at compile time. A\n * function cannot enter a manifest or an FFmpeg filter; its samples can.\n */\n gainPoints?: EnvelopePoint[];\n fadeInFrames: number;\n fadeOutFrames: number;\n /** Seconds skipped at the head of the source file. */\n trimStartSeconds: number;\n loop: boolean;\n /** Ducked cues are attenuated while a louder cue overlaps them. */\n duckUnder: boolean;\n};\n\nexport type AudioTrack = {cues: AudioCue[]; durationInFrames: number};\n\nexport type AudioProps = {\n src: string;\n /** Offset from the enclosing scene, or from the video when used at the top level. */\n from?: Duration;\n duration?: Duration;\n /**\n * A number, or a curve. The function is evaluated once per frame of the\n * cue's window when the timeline compiles, so preview and export read the\n * same points rather than running your code twice.\n */\n gain?: number | ((frame: number) => number);\n fadeIn?: Duration;\n fadeOut?: Duration;\n trimStart?: Duration;\n loop?: boolean;\n duckUnder?: boolean;\n};\n\nexport const cueId = (src: string, fromFrame: number): string => `${src}@${fromFrame}`;\n\n/**\n * A source is either a URL the dev server and the render worker both serve, or\n * a reference declared in `odori.config.ts`. Anything that is not obviously a\n * path is treated as a reference, so `<Audio src=\"score\" />` and\n * `useAssets().resolve(\"score\")` name the same file.\n */\nexport const isAssetReference = (src: string): boolean => !/^(https?:\\/\\/|\\/|\\.\\/|\\.\\.\\/|data:|blob:)/.test(src);\n\n/** Deterministic ordering so a manifest hash does not depend on mount order. */\nexport const sortCues = (cues: AudioCue[]): AudioCue[] =>\n [...cues].sort((left, right) => left.fromFrame - right.fromFrame || left.id.localeCompare(right.id));\n\nexport const trackDuration = (cues: AudioCue[]): number =>\n cues.reduce((total, cue) => Math.max(total, cue.fromFrame + cue.durationInFrames), 0);\n\n/**\n * Linear gain at one frame, including fades. The renderer applies the same\n * curve through FFmpeg, so preview and export agree on shape.\n */\nexport const gainAtFrame = (cue: AudioCue, frame: number): number => {\n const local = frame - cue.fromFrame;\n if (local < 0 || local >= cue.durationInFrames) return 0;\n const fadeIn = cue.fadeInFrames > 0 ? Math.min(1, local / cue.fadeInFrames) : 1;\n const remaining = cue.durationInFrames - local;\n const fadeOut = cue.fadeOutFrames > 0 ? Math.min(1, remaining / cue.fadeOutFrames) : 1;\n const authored = cue.gainPoints ? envelopeAtFrame(cue.gainPoints, frame) : 1;\n return cue.gain * authored * fadeIn * fadeOut;\n};\n\n/**\n * Sample a gain function across a window, then drop the points a straight line\n * already describes. One point per frame would compile into a filter\n * expression hundreds of branches deep for a curve the ear cannot tell from a\n * dozen segments.\n */\nexport const sampleGainCurve = (\n curve: (frame: number) => number,\n fromFrame: number,\n durationInFrames: number,\n tolerance = 0.01,\n): EnvelopePoint[] => {\n const raw: EnvelopePoint[] = [];\n for (let frame = fromFrame; frame < fromFrame + durationInFrames; frame += 1) {\n const value = curve(frame - fromFrame);\n raw.push({frame, value: Number.isFinite(value) ? value : 1});\n }\n if (raw.length <= 2) return raw;\n\n const kept: EnvelopePoint[] = [raw[0]];\n for (let index = 1; index < raw.length - 1; index += 1) {\n const previous = kept[kept.length - 1];\n const next = raw[index + 1];\n const span = next.frame - previous.frame;\n const predicted =\n span === 0\n ? previous.value\n : previous.value + ((next.value - previous.value) * (raw[index].frame - previous.frame)) / span;\n if (Math.abs(raw[index].value - predicted) > tolerance) kept.push(raw[index]);\n }\n kept.push(raw[raw.length - 1]);\n return kept;\n};\n\n/** How far a ducked cue drops while a non-ducked cue is sounding. */\nexport const DUCK_GAIN = 0.35;\n\n/** Ramp into and out of a duck, so the drop is heard as mixing, not as a cut. */\nexport const DUCK_RAMP_FRAMES = 6;\n\nexport type EnvelopePoint = {frame: number; value: number};\n\nconst clampFrame = (frame: number, durationInFrames: number) => Math.max(0, Math.min(durationInFrames, frame));\n\n/**\n * The duck envelope for one cue against the rest of the track.\n *\n * A duck lasts only while a non-ducked cue overlaps, rather than for the whole\n * file: a one second confirmation should not hold a music bed down for the\n * length of the video. Preview evaluates these points directly and the encoder\n * compiles them into a volume expression, so both paths hear one envelope.\n */\nexport const duckEnvelope = (cue: AudioCue, cues: AudioCue[]): EnvelopePoint[] => {\n const end = cue.fromFrame + cue.durationInFrames;\n if (!cue.duckUnder) return [{frame: cue.fromFrame, value: 1}, {frame: end, value: 1}];\n\n const windows = cues\n .filter((other) => other.id !== cue.id && !other.duckUnder)\n .map((other) => ({\n start: Math.max(cue.fromFrame, other.fromFrame),\n end: Math.min(end, other.fromFrame + other.durationInFrames),\n }))\n .filter((window) => window.end > window.start)\n .sort((left, right) => left.start - right.start);\n\n // Overlapping windows duck once, not once per cue.\n const merged: Array<{start: number; end: number}> = [];\n for (const window of windows) {\n const last = merged[merged.length - 1];\n if (last && window.start <= last.end) last.end = Math.max(last.end, window.end);\n else merged.push({...window});\n }\n\n const points: EnvelopePoint[] = [{frame: cue.fromFrame, value: 1}];\n for (const window of merged) {\n points.push(\n {frame: clampFrame(window.start - DUCK_RAMP_FRAMES, end), value: 1},\n {frame: clampFrame(window.start, end), value: DUCK_GAIN},\n {frame: clampFrame(window.end, end), value: DUCK_GAIN},\n {frame: clampFrame(window.end + DUCK_RAMP_FRAMES, end), value: 1},\n );\n }\n points.push({frame: end, value: 1});\n return simplify(points.sort((left, right) => left.frame - right.frame));\n};\n\n/**\n * Two points at one frame are a step, and three points at one level are a\n * straight line. Collapsing both keeps the envelope, and the volume expression\n * compiled from it, as short as the shape actually requires.\n */\nconst simplify = (points: EnvelopePoint[]): EnvelopePoint[] => {\n const stepped: EnvelopePoint[] = [];\n for (const point of points) {\n const last = stepped[stepped.length - 1];\n if (last && last.frame === point.frame) stepped[stepped.length - 1] = point;\n else stepped.push(point);\n }\n\n return stepped.filter((point, index) => {\n const previous = stepped[index - 1];\n const next = stepped[index + 1];\n return !previous || !next || previous.value !== point.value || next.value !== point.value;\n });\n};\n\n/** Linear interpolation between envelope points. */\nexport const envelopeAtFrame = (points: EnvelopePoint[], frame: number): number => {\n if (points.length === 0) return 1;\n if (frame <= points[0].frame) return points[0].value;\n for (let index = 1; index < points.length; index += 1) {\n const previous = points[index - 1];\n const current = points[index];\n if (frame > current.frame) continue;\n const span = current.frame - previous.frame;\n if (span <= 0) return current.value;\n return previous.value + ((current.value - previous.value) * (frame - previous.frame)) / span;\n }\n return points[points.length - 1].value;\n};\n\n/**\n * Everything that shapes a cue's level at one frame: its own gain and fades,\n * and the duck the rest of the track imposes on it.\n */\nexport const trackGainAtFrame = (cue: AudioCue, cues: AudioCue[], frame: number): number =>\n gainAtFrame(cue, frame) * envelopeAtFrame(duckEnvelope(cue, cues), frame);\n","/**\n * Deterministic synthesis. A cue is code that produces samples, so a sound\n * diffs, reviews, and upgrades like every other piece of source, and preview\n * and export can run the identical function.\n *\n * The rules are the frame runtime's rules: no wall clock, no unseeded\n * randomness, one sample rate. Same score in, same bytes out, on any machine.\n */\n\n/** Every cue renders at this rate, so a score never depends on the host. */\nexport const SAMPLE_RATE = 48000;\n\nexport type Signal = {\n /** One array per channel. Mono is one; the mix upmixes when it needs to. */\n channels: Float32Array[];\n sampleRate: number;\n};\n\nexport type Envelope = {\n /** Seconds from the start of the cue to full level. */\n attack?: number;\n /** Seconds from full level to the sustain level. */\n decay?: number;\n /** Level held after the decay, 0 to 1. */\n sustain?: number;\n /** Seconds to fall from the sustain level to silence. */\n release?: number;\n};\n\nconst clamp = (value: number, low: number, high: number) => Math.max(low, Math.min(high, value));\n\n/**\n * A seeded generator, so noise is reproducible. Mulberry32: small, fast, and\n * good enough for audio noise, where the requirement is \"no audible pattern\"\n * rather than cryptographic quality.\n */\nexport const seeded = (seed: number): (() => number) => {\n let state = seed >>> 0;\n return () => {\n state = (state + 0x6d2b79f5) >>> 0;\n let t = Math.imul(state ^ (state >>> 15), 1 | state);\n t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;\n return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n };\n};\n\nexport const silence = (samples: number, channels = 1): Signal => ({\n channels: Array.from({length: channels}, () => new Float32Array(samples)),\n sampleRate: SAMPLE_RATE,\n});\n\n/** A sine partial. The building block for tones, ticks, and sub weight. */\nexport const sine = (samples: number, frequency: number, phase = 0): Signal => {\n const data = new Float32Array(samples);\n const increment = (Math.PI * 2 * frequency) / SAMPLE_RATE;\n for (let index = 0; index < samples; index += 1) data[index] = Math.sin(phase + increment * index);\n return {channels: [data], sampleRate: SAMPLE_RATE};\n};\n\n/** A triangle partial: softer than a square, brighter than a sine. */\nexport const triangle = (samples: number, frequency: number): Signal => {\n const data = new Float32Array(samples);\n const period = SAMPLE_RATE / frequency;\n for (let index = 0; index < samples; index += 1) {\n const position = (index % period) / period;\n data[index] = 4 * Math.abs(position - 0.5) - 1;\n }\n return {channels: [data], sampleRate: SAMPLE_RATE};\n};\n\n/** White noise from a seeded generator, so a click is the same click twice. */\nexport const noise = (samples: number, seed = 1): Signal => {\n const random = seeded(seed);\n const data = new Float32Array(samples);\n for (let index = 0; index < samples; index += 1) data[index] = random() * 2 - 1;\n return {channels: [data], sampleRate: SAMPLE_RATE};\n};\n\n/**\n * A frequency sweep, which is what a riser is. Linear in frequency rather than\n * in pitch, because the shape is easier to reason about when writing one.\n */\nexport const sweep = (samples: number, from: number, to: number): Signal => {\n const data = new Float32Array(samples);\n let phase = 0;\n for (let index = 0; index < samples; index += 1) {\n const position = samples <= 1 ? 1 : index / (samples - 1);\n const frequency = from + (to - from) * position;\n phase += (Math.PI * 2 * frequency) / SAMPLE_RATE;\n data[index] = Math.sin(phase);\n }\n return {channels: [data], sampleRate: SAMPLE_RATE};\n};\n\n/** Apply an ADSR shape. Anything omitted is skipped rather than defaulted. */\nexport const shape = (signal: Signal, envelope: Envelope): Signal => {\n const {attack = 0, decay = 0, sustain = 1, release = 0} = envelope;\n const length = signal.channels[0]?.length ?? 0;\n const attackEnd = Math.round(attack * SAMPLE_RATE);\n const decayEnd = attackEnd + Math.round(decay * SAMPLE_RATE);\n const releaseStart = Math.max(decayEnd, length - Math.round(release * SAMPLE_RATE));\n\n const level = (index: number): number => {\n if (index < attackEnd) return attackEnd === 0 ? 1 : index / attackEnd;\n if (index < decayEnd) {\n const position = (index - attackEnd) / Math.max(1, decayEnd - attackEnd);\n return 1 + (sustain - 1) * position;\n }\n if (index < releaseStart) return sustain;\n const remaining = length - releaseStart;\n return remaining <= 0 ? 0 : sustain * (1 - (index - releaseStart) / remaining);\n };\n\n return {\n sampleRate: signal.sampleRate,\n channels: signal.channels.map((channel) => {\n const out = new Float32Array(channel.length);\n for (let index = 0; index < channel.length; index += 1) out[index] = channel[index] * level(index);\n return out;\n }),\n };\n};\n\nexport const gain = (signal: Signal, amount: number): Signal => ({\n sampleRate: signal.sampleRate,\n channels: signal.channels.map((channel) => {\n const out = new Float32Array(channel.length);\n for (let index = 0; index < channel.length; index += 1) out[index] = channel[index] * amount;\n return out;\n }),\n});\n\n/** Sum signals, padding to the longest. Levels are the caller's business. */\nexport const mix = (...signals: Signal[]): Signal => {\n const length = Math.max(0, ...signals.map((signal) => signal.channels[0]?.length ?? 0));\n const width = Math.max(1, ...signals.map((signal) => signal.channels.length));\n const channels = Array.from({length: width}, () => new Float32Array(length));\n\n for (const signal of signals) {\n for (let channel = 0; channel < width; channel += 1) {\n const source = signal.channels[channel] ?? signal.channels[0];\n if (!source) continue;\n const target = channels[channel];\n for (let index = 0; index < source.length; index += 1) target[index] += source[index];\n }\n }\n\n return {channels, sampleRate: SAMPLE_RATE};\n};\n\n/**\n * One pole filters. Enough to shape a click or take the top off a pad, and\n * cheap enough to stay readable next to the sound it is making.\n */\nexport const lowPass = (signal: Signal, frequency: number): Signal => {\n const coefficient = clamp(1 - Math.exp((-2 * Math.PI * frequency) / SAMPLE_RATE), 0, 1);\n return {\n sampleRate: signal.sampleRate,\n channels: signal.channels.map((channel) => {\n const out = new Float32Array(channel.length);\n let previous = 0;\n for (let index = 0; index < channel.length; index += 1) {\n previous += coefficient * (channel[index] - previous);\n out[index] = previous;\n }\n return out;\n }),\n };\n};\n\nexport const highPass = (signal: Signal, frequency: number): Signal => {\n const low = lowPass(signal, frequency);\n return {\n sampleRate: signal.sampleRate,\n channels: signal.channels.map((channel, index) => {\n const out = new Float32Array(channel.length);\n const removed = low.channels[index];\n for (let sample = 0; sample < channel.length; sample += 1) out[sample] = channel[sample] - removed[sample];\n return out;\n }),\n };\n};\n\n/**\n * Ramp the first and last few milliseconds to zero.\n *\n * A buffer that starts or ends on a non-zero sample is a step change, and a\n * step change is a click. Every cue gets this, because the alternative is a\n * score that sounds correct in isolation and glitches at its edges.\n */\nexport const declick = (signal: Signal, milliseconds = 3): Signal => {\n const ramp = Math.max(1, Math.round((milliseconds / 1000) * SAMPLE_RATE));\n return {\n sampleRate: signal.sampleRate,\n channels: signal.channels.map((channel) => {\n const out = Float32Array.from(channel);\n const span = Math.min(ramp, Math.floor(out.length / 2));\n for (let index = 0; index < span; index += 1) {\n const level = index / span;\n out[index] *= level;\n out[out.length - 1 - index] *= level;\n }\n return out;\n }),\n };\n};\n\n/**\n * Join a signal's tail to its own head, so repeating it is inaudible.\n *\n * `declick` is wrong for a loop: ramping both edges to zero puts a hole at\n * every seam, once per bar for the length of the video. Instead the tail is\n * crossfaded over the head, which is the same trick a sampler uses — the last\n * few milliseconds fade out while the copy of them under the first few\n * milliseconds fades in, so the two ends already agree when they meet.\n *\n * The crossfade eats `milliseconds` from the end, which is why a looping score\n * should be written a hair long rather than exactly one bar.\n */\nexport const seamless = (signal: Signal, milliseconds = 12): Signal => {\n const fade = Math.max(1, Math.round((milliseconds / 1000) * SAMPLE_RATE));\n return {\n sampleRate: signal.sampleRate,\n channels: signal.channels.map((channel) => {\n const span = Math.min(fade, Math.floor(channel.length / 2));\n if (span <= 1) return Float32Array.from(channel);\n // The loop body is everything but the tail the crossfade consumes.\n const out = Float32Array.from(channel.subarray(0, channel.length - span));\n for (let index = 0; index < span; index += 1) {\n const level = index / span;\n out[index] = out[index] * level + channel[channel.length - span + index] * (1 - level);\n }\n return out;\n }),\n };\n};\n\n/** Peak normalize, so a cue lands predictably before the mix's loudness pass. */\nexport const normalize = (signal: Signal, peak = 0.9): Signal => {\n let highest = 0;\n for (const channel of signal.channels) {\n for (let index = 0; index < channel.length; index += 1) {\n const value = Math.abs(channel[index]);\n if (value > highest) highest = value;\n }\n }\n return highest === 0 ? signal : gain(signal, peak / highest);\n};\n\n/** Repeat a signal until it fills `samples`, for beds under a long scene. */\nexport const loop = (signal: Signal, samples: number): Signal => ({\n sampleRate: signal.sampleRate,\n channels: signal.channels.map((channel) => {\n const out = new Float32Array(samples);\n if (channel.length === 0) return out;\n for (let index = 0; index < samples; index += 1) out[index] = channel[index % channel.length];\n return out;\n }),\n});\n\n/** Seconds to samples at the fixed rate, so scores read in musical time. */\nexport const seconds = (value: number): number => Math.max(0, Math.round(value * SAMPLE_RATE));\n\n/**\n * The musical layer.\n *\n * Everything above is a waveform. A bed is not a waveform, it is a phrase: the\n * same voice at different pitches, placed on a grid. These four keep a score\n * readable at that level, so a bed reads as \"these notes, this tempo\" rather\n * than as arithmetic over sample indices — and so two beds written by two\n * people share a vocabulary instead of each inventing one.\n *\n * The rules do not change: pure functions, seeded randomness, one sample rate.\n */\n\nconst SEMITONES = {c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11} as const;\n\n/**\n * Scientific pitch to frequency: `a4` is 440 Hz, `c#3` and `db3` are the same\n * key. A score names notes because \"a2\" survives transposition and reading,\n * and 110 does neither.\n */\nexport const note = (name: string): number => {\n const match = /^([a-gA-G])([#b]?)(-?\\d+)$/.exec(name.trim());\n if (!match) throw new Error(`Not a note name: \"${name}\". Use scientific pitch, for example \"a4\" or \"c#3\".`);\n const [, letter, accidental, octave] = match;\n const semitone =\n SEMITONES[letter.toLowerCase() as keyof typeof SEMITONES] +\n (accidental === \"#\" ? 1 : accidental === \"b\" ? -1 : 0) +\n (Number(octave) + 1) * 12;\n // MIDI 69 is a4 at 440 Hz; equal temperament from there.\n return 440 * 2 ** ((semitone - 69) / 12);\n};\n\n/** Samples in one step of a grid, so a score can size its own voices. */\nexport const step = (bpm: number, division = 4): number =>\n Math.max(1, Math.round((60 / bpm / (division / 4)) * SAMPLE_RATE));\n\nexport type Step = {\n /** Grid position, in steps from the start. Fractions are allowed. */\n at: number;\n /** The voice to place there, built at whatever length it needs. */\n play: (samples: number) => Signal;\n /** Length in steps. Defaults to one step. */\n length?: number;\n /** Linear gain for this hit. */\n gain?: number;\n};\n\n/**\n * Place voices on a tempo grid.\n *\n * A step's `play` receives the samples its length works out to, so one voice\n * definition serves a sixteenth and a whole bar. Swing delays every odd step\n * by a fraction of a step, which is the difference between a drum machine and\n * a groove; it is off by default because a bed under narration should not\n * shuffle unless it was asked to.\n */\nexport const sequence = (\n steps: Step[],\n options: {bpm: number; division?: number; swing?: number; samples?: number},\n): Signal => {\n const {bpm, division = 4, swing = 0, samples} = options;\n const width = step(bpm, division);\n const placed = steps.map((item) => {\n const length = Math.max(1, Math.round((item.length ?? 1) * width));\n const shuffle = swing > 0 && Math.round(item.at) % 2 === 1 ? swing * width : 0;\n const offset = Math.round(item.at * width + shuffle);\n return {offset, signal: item.gain === undefined ? item.play(length) : gain(item.play(length), item.gain)};\n });\n\n const total =\n samples ?? Math.max(0, ...placed.map(({offset, signal}) => offset + (signal.channels[0]?.length ?? 0)));\n const channelCount = Math.max(1, ...placed.map(({signal}) => signal.channels.length));\n const channels = Array.from({length: channelCount}, () => new Float32Array(total));\n\n for (const {offset, signal} of placed) {\n for (let channel = 0; channel < channelCount; channel += 1) {\n const source = signal.channels[channel] ?? signal.channels[0];\n if (!source) continue;\n const target = channels[channel];\n // A voice that runs past the end is cut, not wrapped: a phrase that\n // needs its tail should be written long enough to hold it.\n for (let index = 0; index < source.length && offset + index < total; index += 1) {\n target[offset + index] += source[index];\n }\n }\n }\n\n return {channels, sampleRate: SAMPLE_RATE};\n};\n\n/** Intervals from the root, in semitones. Enough colour for product scoring. */\nconst QUALITIES = {\n major: [0, 4, 7],\n minor: [0, 3, 7],\n sus2: [0, 2, 7],\n sus4: [0, 5, 7],\n major7: [0, 4, 7, 11],\n minor7: [0, 3, 7, 10],\n add9: [0, 4, 7, 14],\n} as const;\n\nexport type ChordQuality = keyof typeof QUALITIES;\n\n/**\n * The frequencies of a chord, root first. Voicing is left to the caller: what\n * makes a pad sound like a pad is which of these you hand to which oscillator,\n * and at what level.\n */\nexport const chord = (root: string | number, quality: ChordQuality = \"major\"): number[] => {\n const base = typeof root === \"number\" ? root : note(root);\n return QUALITIES[quality].map((interval) => base * 2 ** (interval / 12));\n};\n\n/**\n * Sustained tones under one envelope: the sound of a bed that is holding\n * rather than playing. Each voice is detuned by a few cents against the last\n * so the stack beats slowly instead of standing still, which is what separates\n * a pad from a test tone.\n */\nexport const pad = (\n frequencies: number[],\n samples: number,\n options: {envelope?: Envelope; detuneCents?: number; level?: number} = {},\n): Signal => {\n const {envelope = {attack: 0.4, sustain: 1, release: 0.6}, detuneCents = 4, level = 1 / Math.max(1, frequencies.length)} =\n options;\n const voices = frequencies.flatMap((frequency, index) => {\n const detune = 2 ** ((detuneCents * (index % 2 === 0 ? 1 : -1)) / 1200);\n return [gain(sine(samples, frequency), level), gain(sine(samples, frequency * detune), level * 0.6)];\n });\n return shape(mix(...voices), envelope);\n};\n","import {hashValue} from \"./hash\";\nimport {declick, SAMPLE_RATE, seamless, type Signal} from \"./synth\";\n\nexport type CueContext = {\n /** How many samples the cue must fill. */\n samples: number;\n sampleRate: number;\n};\n\nexport type CueDefinition = {\n readonly kind: \"odori-cue\";\n /** The name a brand registers and a video asks for. */\n name: string;\n /** Length at 30fps, which is how a contract states it. */\n durationInFrames: number;\n /**\n * Whether this score is one repeatable phrase rather than a single event.\n *\n * A looping cue renders its phrase once, at `durationInFrames`, and the\n * placement repeats it for as long as the window runs: one render, cached by\n * content, however long the scene. It also changes how the edges are\n * treated — see `defineCue` — and makes `<Audio>` loop by default, so a bed\n * fills its scene without the author restating what the cue already knows.\n */\n loops: boolean;\n /**\n * Anything the score reads. Held separately from the function so the\n * manifest can hash what a cue depends on: two cues that differ only in a\n * parameter are two different sounds and must not share a cached render.\n */\n params: Record<string, number | string | boolean>;\n render(context: CueContext): Signal;\n};\n\nexport type CueInput = Omit<CueDefinition, \"kind\" | \"loops\" | \"params\"> & {\n loops?: boolean;\n params?: Record<string, number | string | boolean>;\n};\n\n/**\n * A sound as source. The function runs unchanged in the browser for preview and\n * in Node for export, so the cue a reviewer approves is the cue that ships.\n */\nexport const defineCue = (input: CueInput): CueDefinition => ({\n kind: \"odori-cue\",\n loops: false,\n params: {},\n ...input,\n // A score is about the sound, not about remembering what a buffer does at\n // its edges. A one shot is declicked, because a buffer that stops\n // mid-waveform is a click. A loop is made seamless instead, because\n // declicking it would put that same silence at every seam.\n render: (context) => (input.loops ? seamless(input.render(context)) : declick(input.render(context))),\n});\n\nexport const isCueDefinition = (value: unknown): value is CueDefinition =>\n typeof value === \"object\" && value !== null && (value as CueDefinition).kind === \"odori-cue\";\n\n/**\n * Identity of a rendered cue. The name alone is not enough, because a project\n * can redefine what a name means; the params and the duration are what change\n * the samples, so they are what the cache and the manifest key on.\n */\nexport const cueSignature = (cue: CueDefinition): string =>\n hashValue({\n name: cue.name,\n durationInFrames: cue.durationInFrames,\n loops: cue.loops,\n params: cue.params,\n }).slice(0, 16);\n\n/** The samples a cue occupies at the given frame rate. */\nexport const cueSamples = (cue: CueDefinition, fps: number): number =>\n Math.max(1, Math.round((cue.durationInFrames / fps) * SAMPLE_RATE));\n\n/**\n * A generated cue resolves to a URL that names its own content, so the render\n * worker can serve it, the mix can find it on disk, and an unchanged cue is\n * never rendered twice.\n */\nexport const cueUrl = (cue: CueDefinition): string => `/__odori/cue/${cue.name}-${cueSignature(cue)}.wav`;\n","export type EasingFunction = (value: number) => number;\n\nconst solveBezierCoordinate = (time: number, a1: number, a2: number) => {\n const inverse = 1 - time;\n return 3 * inverse * inverse * time * a1 + 3 * inverse * time * time * a2 + time ** 3;\n};\n\nconst solveBezierSlope = (time: number, a1: number, a2: number) =>\n 3 * (1 - time) ** 2 * a1 + 6 * (1 - time) * time * (a2 - a1) + 3 * time ** 2 * (1 - a2);\n\nconst cubicBezier =\n (x1: number, y1: number, x2: number, y2: number): EasingFunction =>\n (value) => {\n if (value <= 0 || value >= 1) return value;\n let time = value;\n for (let index = 0; index < 8; index += 1) {\n const error = solveBezierCoordinate(time, x1, x2) - value;\n const slope = solveBezierSlope(time, x1, x2);\n if (Math.abs(error) < 0.000001 || Math.abs(slope) < 0.000001) break;\n time -= error / slope;\n }\n return solveBezierCoordinate(Math.min(1, Math.max(0, time)), y1, y2);\n };\n\nexport const Easing = {\n linear: (value: number) => value,\n bezier: cubicBezier,\n standard: cubicBezier(0.16, 1, 0.3, 1),\n quad: (value: number) => value * value,\n cubic: (value: number) => value ** 3,\n in: (easing: EasingFunction): EasingFunction => easing,\n out:\n (easing: EasingFunction): EasingFunction =>\n (value) =>\n 1 - easing(1 - value),\n inOut:\n (easing: EasingFunction): EasingFunction =>\n (value) =>\n value < 0.5 ? easing(value * 2) / 2 : 1 - easing((1 - value) * 2) / 2,\n};\n\nexport type InterpolateOptions = {\n easing?: EasingFunction;\n extrapolateLeft?: \"extend\" | \"clamp\";\n extrapolateRight?: \"extend\" | \"clamp\";\n};\n\nexport function interpolate(value: number, input: number[], output: number[], options?: InterpolateOptions): number;\nexport function interpolate(value: number, input: number[], output: string[], options?: InterpolateOptions): string;\nexport function interpolate(\n value: number,\n input: number[],\n output: Array<number | string>,\n options: InterpolateOptions = {},\n): number | string {\n if (input.length !== output.length || input.length < 2) {\n throw new Error(\"interpolate() requires matching input and output ranges.\");\n }\n\n let resolved = value;\n if (options.extrapolateLeft !== \"extend\") resolved = Math.max(input[0], resolved);\n if (options.extrapolateRight !== \"extend\") resolved = Math.min(input[input.length - 1], resolved);\n\n let rangeIndex = input.length - 2;\n for (let index = 0; index < input.length - 1; index += 1) {\n if (resolved <= input[index + 1]) {\n rangeIndex = index;\n break;\n }\n }\n\n const inputStart = input[rangeIndex];\n const inputEnd = input[rangeIndex + 1];\n const progress = inputEnd === inputStart ? 0 : (resolved - inputStart) / (inputEnd - inputStart);\n const eased = (options.easing ?? Easing.linear)(progress);\n const startValue = output[rangeIndex];\n const endValue = output[rangeIndex + 1];\n\n if (typeof startValue === \"number\" && typeof endValue === \"number\") {\n return startValue + (endValue - startValue) * eased;\n }\n if (typeof startValue === \"string\" && typeof endValue === \"string\") {\n const startNumbers = startValue.match(/-?\\d*\\.?\\d+/g)?.map(Number) ?? [];\n const endNumbers = endValue.match(/-?\\d*\\.?\\d+/g)?.map(Number) ?? [];\n if (startNumbers.length !== endNumbers.length) {\n throw new Error(\"String interpolation requires matching numeric segments.\");\n }\n let segment = 0;\n return startValue.replace(/-?\\d*\\.?\\d+/g, () => {\n const result = startNumbers[segment] + (endNumbers[segment] - startNumbers[segment]) * eased;\n segment += 1;\n return Number(result.toFixed(4)).toString();\n });\n }\n throw new Error(\"interpolate() output values must share a type.\");\n}\n\nexport type SpringOptions = {\n frame: number;\n fps: number;\n from?: number;\n to?: number;\n stiffness?: number;\n damping?: number;\n mass?: number;\n delayInFrames?: number;\n};\n\n/**\n * A deterministic damped-spring solve. The value depends only on the frame\n * index, so preview and render always agree.\n */\nexport const spring = ({\n frame,\n fps,\n from = 0,\n to = 1,\n stiffness = 120,\n damping = 20,\n mass = 1,\n delayInFrames = 0,\n}: SpringOptions): number => {\n const elapsed = Math.max(0, frame - delayInFrames);\n const step = 1 / fps;\n let position = 0;\n let velocity = 0;\n for (let index = 0; index < elapsed; index += 1) {\n const force = -stiffness * (position - 1) - damping * velocity;\n velocity += (force / mass) * step;\n position += velocity * step;\n }\n return from + (to - from) * position;\n};\n","import {type CueDefinition} from \"./cue\";\n\nexport type BrandColors = {\n background: string;\n surface: string;\n foreground: string;\n muted: string;\n accent: string;\n border: string;\n};\n\nexport type BrandTypography = {\n sans: string;\n mono: string;\n display?: string;\n};\n\nexport type BrandFont = {family: string; url: string; weight?: string; integrity?: string};\n\nexport type Brand = {\n readonly kind: \"odori-brand\";\n name: string;\n colors: BrandColors;\n typography: BrandTypography;\n fonts: BrandFont[];\n logos: Record<string, string>;\n motion: {standard: [number, number, number, number]; staggerFrames: number};\n audio: {\n /**\n * Symbolic cue names, so a video says what a sound means and the brand\n * decides which file that is. Values are asset references or URLs.\n */\n cues: Record<string, string | CueDefinition>;\n targetLufs: number;\n };\n};\n\nexport type BrandInput = {\n name?: string;\n colors?: Partial<BrandColors>;\n typography?: Partial<BrandTypography>;\n fonts?: BrandFont[];\n logos?: Record<string, string>;\n motion?: Partial<Brand[\"motion\"]>;\n audio?: Partial<Brand[\"audio\"]>;\n};\n\nexport const defaultBrand: Brand = {\n kind: \"odori-brand\",\n name: \"odori\",\n colors: {\n background: \"#000000\",\n surface: \"#0a0a0a\",\n foreground: \"#ededed\",\n muted: \"#a1a1a1\",\n accent: \"#ffffff\",\n border: \"#1f1f1f\",\n },\n /**\n * System faces, deliberately.\n *\n * A default that leads with a licensed family it does not ship is a default\n * that renders through a fallback on every machine while claiming not to —\n * and `odori test` is right to fail it, which meant a freshly scaffolded\n * project failed its own contract check the first time it installed a\n * component. A brand that wants Geist names it and adds the file, and the\n * check then protects that choice instead of punishing the default.\n */\n typography: {\n sans: 'system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif',\n mono: 'ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, monospace',\n },\n fonts: [],\n logos: {},\n motion: {standard: [0.16, 1, 0.3, 1], staggerFrames: 4},\n audio: {cues: {}, targetLufs: -14},\n};\n\nexport const defineBrand = (input: BrandInput): Brand => ({\n kind: \"odori-brand\",\n name: input.name ?? defaultBrand.name,\n colors: {...defaultBrand.colors, ...input.colors},\n typography: {...defaultBrand.typography, ...input.typography},\n fonts: input.fonts ?? [],\n logos: {...input.logos},\n motion: {...defaultBrand.motion, ...input.motion},\n audio: {...defaultBrand.audio, ...input.audio, cues: {...defaultBrand.audio.cues, ...input.audio?.cues}},\n});\n\nexport const brandCssVariables = (brand: Brand): Record<string, string> => ({\n \"--odori-background\": brand.colors.background,\n \"--odori-surface\": brand.colors.surface,\n \"--odori-foreground\": brand.colors.foreground,\n \"--odori-muted\": brand.colors.muted,\n \"--odori-accent\": brand.colors.accent,\n \"--odori-border\": brand.colors.border,\n \"--odori-font-sans\": brand.typography.sans,\n \"--odori-font-mono\": brand.typography.mono,\n});\n","import {type Brand, defaultBrand} from \"./brand\";\n\nexport type VideoFormat = {width: number; height: number; fps: number};\nexport type SafeArea = {x: number; y: number};\nexport type MotionPolicy = {enter: [number, number, number, number]; staggerFrames: number; reducedMotion?: boolean};\nexport type AudioPolicy = {palette: string; targetLufs: number};\n\nexport type VideoLayout = {\n readonly kind: \"odori-layout\";\n format: VideoFormat;\n brand: Brand;\n safeArea: SafeArea;\n motion: MotionPolicy;\n audio: AudioPolicy;\n transition: {crossfadeFrames: number};\n};\n\nexport type VideoLayoutInput = {\n extends?: VideoLayout;\n format?: Partial<VideoFormat>;\n brand?: Brand;\n safeArea?: SafeArea;\n motion?: Partial<MotionPolicy>;\n audio?: Partial<AudioPolicy>;\n transition?: {crossfadeFrames: number};\n};\n\nexport const defaultLayout: VideoLayout = {\n kind: \"odori-layout\",\n format: {width: 1920, height: 1080, fps: 30},\n brand: defaultBrand,\n safeArea: {x: 96, y: 72},\n motion: {enter: [0.16, 1, 0.3, 1], staggerFrames: 4},\n audio: {palette: \"product-cinematic\", targetLufs: -14},\n transition: {crossfadeFrames: 8},\n};\n\n/**\n * Merge rules from the docs: scalar format values replace, safe areas replace\n * as a unit, motion and audio policies merge onto the inherited policy, and\n * brand replaces because a brand is itself a resolved policy object.\n */\nexport const defineVideoLayout = (input: VideoLayoutInput = {}): VideoLayout => {\n const base = input.extends ?? defaultLayout;\n return {\n kind: \"odori-layout\",\n format: {...base.format, ...input.format},\n brand: input.brand ?? base.brand,\n safeArea: input.safeArea ?? base.safeArea,\n motion: {...base.motion, ...input.motion},\n audio: {...base.audio, ...input.audio},\n transition: input.transition ?? base.transition,\n };\n};\n","\"use client\";\n\nimport {\n Children,\n cloneElement,\n createContext,\n isValidElement,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n type CSSProperties,\n type ComponentPropsWithoutRef,\n type ReactElement,\n type ReactNode,\n} from \"react\";\nimport {\n AssetContext,\n AudioSinkContext,\n BrandContext,\n ForceMountContext,\n FrameContext,\n LayoutContext,\n ReadinessContext,\n SceneContext,\n createAssetRegistry,\n useAssets,\n useFrame,\n useVideo,\n type Readiness,\n} from \"./context\";\nimport {\n cueId,\n isAssetReference,\n sampleGainCurve,\n sortCues,\n trackDuration,\n type AudioCue,\n type AudioProps,\n type AudioTrack,\n} from \"./audio\";\nimport {cueUrl, isCueDefinition} from \"./cue\";\nimport {Easing, interpolate} from \"./easing\";\nimport {brandCssVariables, defaultBrand, type Brand} from \"./brand\";\nimport {type VideoLayout, defaultLayout} from \"./layout\";\nimport {framesFromDuration, type Duration} from \"./time\";\n\nexport type CompiledScene = {\n id: string;\n name?: string;\n index: number;\n start: number;\n durationInFrames: number;\n /** Frames this scene starts before the previous one ends. Zero is a cut. */\n overlap: number;\n};\nexport type CompiledTimeline = {scenes: CompiledScene[]; durationInFrames: number};\n\ntype TimelineSink = {report(timeline: CompiledTimeline): void};\n\nconst TimelineContext = createContext<TimelineSink | null>(null);\n\nexport const Fill = ({style, ...props}: ComponentPropsWithoutRef<\"div\">) => (\n <div\n {...props}\n style={{\n position: \"absolute\",\n inset: 0,\n width: \"100%\",\n height: \"100%\",\n display: \"flex\",\n flexDirection: \"column\",\n overflow: \"hidden\",\n ...style,\n }}\n />\n);\n\n/** Content inset by the inherited layout safe area. */\nexport const SafeArea = ({children, style}: {children: ReactNode; style?: CSSProperties}) => {\n const layout = useContext(LayoutContext) ?? defaultLayout;\n return (\n <Fill style={{padding: `${layout.safeArea.y}px ${layout.safeArea.x}px`, ...style}}>{children}</Fill>\n );\n};\n\ntype SceneProps = {\n children: ReactNode;\n duration: Duration;\n id?: string;\n name?: string;\n /**\n * Start this many frames before the previous scene ends, so both are on\n * screen together and a transition can span the cut. The video gets shorter\n * by the same amount: an overlap is a join, not an extra beat.\n */\n overlap?: Duration;\n /** Injected by <Video>. Authors never pass these. */\n __start?: number;\n __index?: number;\n __durationInFrames?: number;\n __enterOverlap?: number;\n __exitOverlap?: number;\n __videoFrame?: number;\n};\n\nexport type SceneTransition = {\n /** 0 to 1 while this scene is arriving over its predecessor. */\n entering: number;\n /** 0 to 1 while its successor is arriving over it. */\n leaving: number;\n /** Frames of overlap at each end, for a component that needs the raw counts. */\n enterFrames: number;\n exitFrames: number;\n};\n\nconst SceneTransitionContext = createContext<SceneTransition>({\n entering: 1,\n leaving: 0,\n enterFrames: 0,\n exitFrames: 0,\n});\n\n/**\n * How far this scene is through the joins at its edges.\n *\n * A transition that spans a cut needs to know two things the scene itself\n * cannot see: that it is arriving over something, and that something is\n * arriving over it. Both are numbers rather than a reference to the other\n * scene — a component that could reach into its neighbour's tree would couple\n * two scenes together, and the point of the timeline is that they compose.\n *\n * Outside an overlap this reports a scene fully present and nothing leaving,\n * so a component written against it also works on an ordinary cut.\n */\nexport const useSceneTransition = (): SceneTransition => useContext(SceneTransitionContext);\n\nconst SceneEnvelope = ({children, durationInFrames}: {children: ReactNode; durationInFrames: number}) => {\n const frame = useFrame();\n const edge = Math.max(1, Math.min(10, Math.floor(durationInFrames / 6)));\n const opacity = interpolate(\n frame,\n [0, edge, Math.max(edge + 1, durationInFrames - edge), Math.max(edge + 2, durationInFrames - 1)],\n [0, 1, 1, 0],\n {easing: Easing.standard},\n );\n return <Fill style={{opacity}}>{children}</Fill>;\n};\n\n/**\n * A timeline segment with a local frame clock starting at zero.\n */\nexport const Scene = ({\n children,\n id,\n name,\n __start = 0,\n __index = 0,\n __durationInFrames = 1,\n __enterOverlap = 0,\n __exitOverlap = 0,\n __videoFrame = 0,\n}: SceneProps) => {\n const config = useVideo();\n const forceMount = useContext(ForceMountContext);\n const sceneId = id ?? `scene-${__index + 1}`;\n const localFrame = __videoFrame - __start;\n const active = localFrame >= 0 && localFrame < __durationInFrames;\n if (!active && !forceMount) return null;\n\n const joined = __enterOverlap > 0 || __exitOverlap > 0;\n const transition: SceneTransition = {\n entering: __enterOverlap > 0 ? Math.min(1, Math.max(0, localFrame / __enterOverlap)) : 1,\n leaving:\n __exitOverlap > 0\n ? Math.min(1, Math.max(0, (localFrame - (__durationInFrames - __exitOverlap)) / __exitOverlap))\n : 0,\n enterFrames: __enterOverlap,\n exitFrames: __exitOverlap,\n };\n\n return (\n <SceneContext.Provider value={{id: sceneId, name, index: __index, start: __start, durationInFrames: __durationInFrames}}>\n <FrameContext.Provider\n value={{\n frame: Math.max(0, Math.min(__durationInFrames - 1, localFrame)),\n fps: config.fps,\n width: config.width,\n height: config.height,\n durationInFrames: __durationInFrames,\n }}\n >\n <SceneTransitionContext.Provider value={transition}>\n <Fill data-odori-scene={sceneId}>\n {/* A joined scene owns its own edges: the default envelope would\n fade it out underneath whatever the transition is doing, and\n two treatments of the same cut fight each other. */}\n {joined ? (\n <Fill>{children}</Fill>\n ) : (\n <SceneEnvelope durationInFrames={__durationInFrames}>{children}</SceneEnvelope>\n )}\n </Fill>\n </SceneTransitionContext.Provider>\n </FrameContext.Provider>\n </SceneContext.Provider>\n );\n};\nScene.__odoriScene = true as const;\n\n/** An explicit offset window inside a scene, for staggered layers. */\nexport const Stagger = ({children, from = 0, duration}: {children: ReactNode; from?: Duration; duration?: Duration}) => {\n const config = useVideo();\n const frame = useFrame();\n const start = typeof from === \"number\" && Number.isInteger(from) && from > 1 ? from : framesFromDuration(from, config.fps);\n const window = duration === undefined ? config.durationInFrames : framesFromDuration(duration, config.fps);\n const local = frame - start;\n if (local < 0 || local >= window) return null;\n return (\n <FrameContext.Provider value={{...config, frame: local, durationInFrames: window}}>\n <Fill>{children}</Fill>\n </FrameContext.Provider>\n );\n};\n\n/**\n * Repeat children on a local clock.\n *\n * The frame inside is the frame within one pass, so a component written for a\n * two second window needs no idea it is being repeated. Everything stays a\n * pure function of the frame — the tenth repetition is not the first one\n * mutated ten times, it is the same computation with the same input, which is\n * why scrubbing backwards into a loop lands exactly where playing forwards\n * did.\n */\nexport const Loop = ({\n children,\n duration,\n times,\n}: {\n children: ReactNode;\n /** Length of one pass. */\n duration: Duration;\n /** How many passes before it stops. Unlimited by default. */\n times?: number;\n}) => {\n const config = useVideo();\n const frame = useFrame();\n const window = Math.max(1, framesFromDuration(duration, config.fps));\n const pass = Math.floor(frame / window);\n if (times !== undefined && pass >= times) return null;\n\n return (\n <FrameContext.Provider value={{...config, frame: frame % window, durationInFrames: window}}>\n <Fill>{children}</Fill>\n </FrameContext.Provider>\n );\n};\n\n/**\n * Hold children at one frame.\n *\n * Useful for the still half of a comparison, for a poster frame, and for\n * pausing an animation on the moment that makes the point. It is the frame\n * clock that stops, not the render: children still mount and lay out\n * normally, they simply always see the same number.\n */\nexport const Freeze = ({children, at = 0}: {children: ReactNode; at?: Duration}) => {\n const config = useVideo();\n const held = framesFromDuration(at, config.fps);\n return (\n <FrameContext.Provider value={{...config, frame: Math.max(0, held)}}>\n <Fill>{children}</Fill>\n </FrameContext.Provider>\n );\n};\n\n/**\n * A sound placed on the timeline. Audio is declarative like a scene: it\n * registers a cue rather than starting playback, so the player, a still, and\n * the encoder all read the same track.\n */\nexport const Audio = ({\n src,\n from = 0,\n duration,\n gain = 1,\n fadeIn = 0,\n fadeOut = 0,\n trimStart = 0,\n loop,\n duckUnder = false,\n}: AudioProps) => {\n const config = useVideo();\n const sink = useContext(AudioSinkContext);\n const scene = useContext(SceneContext);\n const assets = useAssets();\n const brand = useContext(BrandContext) ?? defaultBrand;\n const frames = (value: Duration) => framesFromDuration(value, config.fps);\n // A path is a path. A name the brand knows is whatever the brand points it\n // at, which is how one edit re-scores every video. Anything else is an\n // asset reference.\n const named = brand.audio.cues[src];\n // A generated cue names itself by content, so the same score is fetched,\n // decoded, and mixed once however many videos ask for it.\n const source = isCueDefinition(named) ? cueUrl(named) : (named ?? src);\n const url = isAssetReference(source) ? assets.resolve(source) : source;\n // A looping cue is one phrase that repeats, and it says so. Taking the\n // default from the score means a bed fills its scene without every\n // composition restating what the cue already declares; `loop` on the element\n // is still the override when a video wants the other behaviour.\n const repeats = loop ?? (isCueDefinition(named) ? named.loops : false);\n\n if (sink) {\n const offset = (from === 0 ? 0 : frames(from)) + (scene?.start ?? 0);\n const window = duration === undefined ? (scene?.durationInFrames ?? config.durationInFrames) : frames(duration);\n sink.register({\n id: cueId(url, offset),\n src: url,\n fromFrame: offset,\n durationInFrames: window,\n // A curve is sampled here, once, where the window is known. Downstream\n // only ever sees points.\n gain: typeof gain === \"function\" ? 1 : gain,\n gainPoints: typeof gain === \"function\" ? sampleGainCurve(gain, offset, window) : undefined,\n fadeInFrames: fadeIn === 0 ? 0 : frames(fadeIn),\n fadeOutFrames: fadeOut === 0 ? 0 : frames(fadeOut),\n trimStartSeconds: trimStart === 0 ? 0 : frames(trimStart) / config.fps,\n loop: repeats,\n duckUnder,\n });\n }\n\n return null;\n};\nAudio.__odoriAudio = true as const;\n\nconst isSceneElement = (node: ReactNode): node is ReactElement<SceneProps> =>\n isValidElement(node) && (node.type as {__odoriScene?: boolean})?.__odoriScene === true;\n\n/**\n * Provides timeline structure. `<Video>` adds no duration of its own: scene\n * children are laid out in order, other children render for the whole video.\n */\nexport const Video = ({children, style}: {children: ReactNode; style?: CSSProperties}) => {\n const config = useVideo();\n const frame = useFrame();\n const sink = useContext(TimelineContext);\n const reported = useRef(\"\");\n const nodes = Children.toArray(children);\n\n const {laidOut, timeline} = useMemo(() => {\n let cursor = 0;\n let sceneIndex = 0;\n const scenes: CompiledScene[] = [];\n const overlaps: number[] = [];\n\n // Two passes: the first places every scene, because a scene's exit overlap\n // is the next scene's entry overlap and is therefore not known until the\n // next scene has been read.\n const placed = nodes.map((node) => {\n if (!isSceneElement(node)) return null;\n const durationInFrames = Math.max(1, framesFromDuration(node.props.duration, config.fps));\n const requested = node.props.overlap === undefined ? 0 : framesFromDuration(node.props.overlap, config.fps);\n // An overlap cannot be longer than either scene, or the two would cross\n // and the timeline would run backwards.\n const previous = scenes[scenes.length - 1];\n const overlap =\n sceneIndex === 0 ? 0 : Math.max(0, Math.min(requested, durationInFrames, previous?.durationInFrames ?? 0));\n\n const start = Math.max(0, cursor - overlap);\n const index = sceneIndex;\n scenes.push({id: node.props.id ?? `scene-${index + 1}`, name: node.props.name, index, start, durationInFrames, overlap});\n overlaps.push(overlap);\n cursor = start + durationInFrames;\n sceneIndex += 1;\n return {start, index, durationInFrames};\n });\n\n let placedIndex = 0;\n const output = nodes.map((node, key) => {\n if (!isSceneElement(node)) return node;\n const entry = placed[key] ?? placed.find((item) => item !== null);\n const index = placedIndex;\n placedIndex += 1;\n if (!entry) return node;\n return cloneElement(node, {\n key: `odori-scene-${key}`,\n __start: entry.start,\n __index: entry.index,\n __durationInFrames: entry.durationInFrames,\n __enterOverlap: overlaps[index] ?? 0,\n __exitOverlap: overlaps[index + 1] ?? 0,\n });\n });\n return {laidOut: output, timeline: {scenes, durationInFrames: cursor}};\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [children, config.fps]);\n\n // The compiled timeline is recomputed whenever children change identity,\n // which happens on every frame. Report only when it actually differs, so a\n // consumer that stores it in state cannot be driven into a render loop.\n const publish = () => {\n const signature = JSON.stringify(timeline);\n if (signature === reported.current) return;\n reported.current = signature;\n sink?.report(timeline);\n };\n\n // On the server there is no effect phase, so report inline. In the browser\n // the effect keeps the report out of the render pass.\n if (typeof window === \"undefined\") publish();\n useEffect(publish);\n\n return (\n <Fill data-odori-video style={style}>\n {laidOut.map((node) =>\n isSceneElement(node) ? cloneElement(node, {__videoFrame: frame}) : node,\n )}\n </Fill>\n );\n};\n\nexport type VideoEntry = {\n component: (props: any) => ReactNode;\n metadata: {\n id: string;\n title: string;\n description?: string;\n duration?: Duration;\n layout?: VideoLayout;\n schema?: {parse(input: unknown): unknown};\n defaultProps?: Record<string, unknown>;\n tags?: string[];\n thumbnailFrame?: number;\n };\n};\n\nexport type OdoriRuntimeProps = {\n entry: VideoEntry;\n frame: number;\n input?: Record<string, unknown>;\n prepared?: unknown;\n assets?: Array<{reference: string; url: string}>;\n layout?: VideoLayout;\n onTimeline?: (timeline: CompiledTimeline) => void;\n onAudio?: (track: AudioTrack) => void;\n};\n\nexport const resolveEntryLayout = (entry: VideoEntry, override?: VideoLayout): VideoLayout =>\n override ?? entry.metadata.layout ?? defaultLayout;\n\nexport const entryDurationInFrames = (entry: VideoEntry, layout: VideoLayout): number =>\n entry.metadata.duration === undefined ? 0 : framesFromDuration(entry.metadata.duration, layout.format.fps);\n\nconst fontFaceCss = (fonts: Brand[\"fonts\"]) =>\n fonts\n .map(\n (font) =>\n `@font-face{font-family:\"${font.family}\";src:url(\"${font.url}\") format(\"woff2\");font-weight:${\n font.weight ?? \"100 900\"\n };font-display:block;font-style:normal;}`,\n )\n .join(\"\");\n\n/** Faces already installed in this document, so a remount never reloads them. */\nconst installedFonts = new Set<string>();\n\n/**\n * Brand fonts are declared as policy, not imported by components, so preview,\n * stills, and the export worker all load the same faces before the first frame\n * is captured.\n *\n * In the browser the rules are installed once at the document level and never\n * removed. Rendering them inside the tree would drop every face each time a\n * video unmounts, which reloads the fonts and reflows the whole page.\n */\nconst FontFaces = ({fonts}: {fonts: Brand[\"fonts\"]}) => {\n const css = useMemo(() => fontFaceCss(fonts), [fonts]);\n\n useEffect(() => {\n if (!css || installedFonts.has(css)) return;\n installedFonts.add(css);\n const style = document.createElement(\"style\");\n style.dataset.odoriFonts = \"\";\n style.textContent = css;\n document.head.append(style);\n }, [css]);\n\n // Server rendering has no effect phase, so the faces stay inline there.\n return typeof window === \"undefined\" && css ? <style data-odori-fonts>{css}</style> : null;\n};\n\n/**\n * Mounts every scene at once, invisibly, so `<Audio>` cues can be collected\n * without seeking through the whole timeline first.\n */\nconst AudioPass = ({\n children,\n onAudio,\n durationInFrames,\n}: {\n children: ReactNode;\n onAudio: (track: AudioTrack) => void;\n durationInFrames: number;\n}) => {\n const collected = useRef(new Map<string, AudioCue>());\n const reported = useRef(\"\");\n const sink = useMemo(\n () => ({\n register: (cue: AudioCue) => {\n collected.current.set(cue.id, cue);\n },\n }),\n [],\n );\n\n collected.current.clear();\n const tree = (\n <AudioSinkContext.Provider value={sink}>\n <ForceMountContext.Provider value>{children}</ForceMountContext.Provider>\n </AudioSinkContext.Provider>\n );\n\n const publish = () => {\n const cues = sortCues([...collected.current.values()]);\n const signature = JSON.stringify(cues);\n if (signature === reported.current) return;\n reported.current = signature;\n onAudio({cues, durationInFrames: Math.max(durationInFrames, trackDuration(cues))});\n };\n\n useEffect(publish);\n\n return (\n <div aria-hidden data-odori-audio-pass style={{display: \"none\"}}>\n {tree}\n {/* Rendered after the tree, so on the server every cue has registered\n by the time this publishes. */}\n <Publisher publish={publish} />\n </div>\n );\n};\n\nconst Publisher = ({publish}: {publish: () => void}) => {\n if (typeof window === \"undefined\") publish();\n return null;\n};\n\nexport const OdoriRuntime = ({entry, frame, input, prepared, assets, layout, onTimeline, onAudio}: OdoriRuntimeProps) => {\n const resolvedLayout = resolveEntryLayout(entry, layout);\n const {format, brand} = resolvedLayout;\n const registry = useMemo(() => createAssetRegistry(assets ?? []), [assets]);\n const sink = useMemo<TimelineSink>(() => ({report: (timeline) => onTimeline?.(timeline)}), [onTimeline]);\n\n const props = useMemo(() => {\n const merged = {...entry.metadata.defaultProps, ...input};\n return entry.metadata.schema ? (entry.metadata.schema.parse(merged) as Record<string, unknown>) : merged;\n }, [entry, input]);\n\n const durationInFrames = entryDurationInFrames(entry, resolvedLayout);\n const Composition = entry.component;\n\n // The frame attribute is the readiness handshake the render worker waits on,\n // so it is written only once every held handle has been released.\n const [pending, setPending] = useState(0);\n const readiness = useMemo<Readiness>(\n () => ({\n hold: () => {\n setPending((count) => count + 1);\n let released = false;\n return () => {\n if (released) return;\n released = true;\n setPending((count) => Math.max(0, count - 1));\n };\n },\n }),\n [],\n );\n\n return (\n <LayoutContext.Provider value={resolvedLayout}>\n <BrandContext.Provider value={brand}>\n <AssetContext.Provider value={registry}>\n <ReadinessContext.Provider value={readiness}>\n <TimelineContext.Provider value={sink}>\n <FrameContext.Provider\n value={{\n frame,\n fps: format.fps,\n width: format.width,\n height: format.height,\n durationInFrames: durationInFrames || 1,\n }}\n >\n <Fill\n data-odori-frame={pending === 0 ? frame : undefined}\n style={\n {\n backgroundColor: brand.colors.background,\n color: brand.colors.foreground,\n fontFamily: brand.typography.sans,\n ...brandCssVariables(brand),\n } as CSSProperties\n }\n >\n <FontFaces fonts={brand.fonts} />\n <Composition {...props} prepared={prepared} />\n {/* The pass is keyed by composition: it dedupes against what it\n last published, so a consumer that clears its track when the\n selection changes would otherwise never be handed the cues\n again for two videos that share one cue signature. */}\n {onAudio ? (\n <AudioPass key={entry.metadata.id} onAudio={onAudio} durationInFrames={durationInFrames}>\n <Composition {...props} prepared={prepared} />\n </AudioPass>\n ) : null}\n </Fill>\n </FrameContext.Provider>\n </TimelineContext.Provider>\n </ReadinessContext.Provider>\n </AssetContext.Provider>\n </BrandContext.Provider>\n </LayoutContext.Provider>\n );\n};\n"],"mappings":"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","names":["window","step","createContext","useContext","createContext","useContext","window","gain","loop"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/hash.ts","../src/time.ts"],"sourcesContent":["/**\n * Deterministic, dependency-free content hash.\n *\n * The renderer only needs stable identity for cache keys and frozen manifests,\n * so a 128-bit FNV-1a variant over canonical JSON is sufficient and runs\n * identically in Node and the browser.\n */\nexport const canonicalJson = (value: unknown): string => {\n const walk = (input: unknown): unknown => {\n if (input === null || typeof input !== \"object\") return input;\n if (Array.isArray(input)) return input.map(walk);\n const entries = Object.entries(input as Record<string, unknown>)\n .filter(([, item]) => item !== undefined)\n .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));\n return Object.fromEntries(entries.map(([key, item]) => [key, walk(item)]));\n };\n return JSON.stringify(walk(value)) ?? \"null\";\n};\n\nexport const hashString = (input: string): string => {\n let high = 0x811c9dc5;\n let low = 0xcbf29ce4;\n for (let index = 0; index < input.length; index += 1) {\n const code = input.charCodeAt(index);\n low ^= code;\n high ^= code * 31 + index;\n low = Math.imul(low, 0x01000193) >>> 0;\n high = Math.imul(high, 0x01000195) >>> 0;\n }\n const pad = (value: number) => (value >>> 0).toString(16).padStart(8, \"0\");\n return `${pad(high)}${pad(low)}${pad(Math.imul(high ^ low, 0x27220a95))}${pad(input.length * 2654435761)}`;\n};\n\nexport const hashValue = (value: unknown): string => hashString(canonicalJson(value));\n","export type Duration = number | `${number}s` | `${number}ms` | `${number}f` | string;\n\nconst PATTERN = /^(-?\\d*\\.?\\d+)\\s*(s|ms|f|frames?|sec|seconds?)?$/;\n\n/**\n * Convert an authoring duration into whole frames.\n *\n * Numbers are seconds, matching the JSX authoring model where `duration={4}`\n * reads as four seconds. Suffixed strings are explicit.\n */\nexport const framesFromDuration = (duration: Duration, fps: number): number => {\n if (!Number.isFinite(fps) || fps <= 0) {\n throw new Error(`Invalid fps: ${String(fps)}`);\n }\n\n const seconds = (() => {\n if (typeof duration === \"number\") {\n if (!Number.isFinite(duration)) throw new Error(`Invalid duration: ${String(duration)}`);\n return duration;\n }\n\n const match = PATTERN.exec(duration.trim());\n if (!match) throw new Error(`Invalid duration: ${duration}`);\n const value = Number(match[1]);\n const unit = match[2] ?? \"s\";\n if (unit === \"ms\") return value / 1000;\n if (unit === \"f\" || unit.startsWith(\"frame\")) return value / fps;\n return value;\n })();\n\n if (seconds < 0) throw new Error(`Duration cannot be negative: ${String(duration)}`);\n return Math.max(1, Math.round(seconds * fps));\n};\n\nexport const secondsFromFrames = (frames: number, fps: number): number => frames / fps;\n\nexport const formatTimecode = (frames: number, fps: number): string => {\n const totalSeconds = Math.floor(frames / fps);\n const minutes = Math.floor(totalSeconds / 60);\n const seconds = totalSeconds % 60;\n const remainder = Math.round(frames % fps);\n const pad = (value: number, size = 2) => String(value).padStart(size, \"0\");\n return `${pad(minutes)}:${pad(seconds)}.${pad(remainder)}`;\n};\n"],"mappings":";;;AAOO,IAAM,gBAAgB,CAAC,UAA2B;AACvD,QAAM,OAAO,CAAC,UAA4B;AACxC,QAAI,UAAU,QAAQ,OAAO,UAAU,SAAU,QAAO;AACxD,QAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,IAAI,IAAI;AAC/C,UAAM,UAAU,OAAO,QAAQ,KAAgC,EAC5D,OAAO,CAAC,CAAC,EAAE,IAAI,MAAM,SAAS,MAAS,EACvC,KAAK,CAAC,CAAC,IAAI,GAAG,CAAC,KAAK,MAAO,OAAO,QAAQ,KAAK,OAAO,QAAQ,IAAI,CAAE;AACvE,WAAO,OAAO,YAAY,QAAQ,IAAI,CAAC,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC;AAAA,EAC3E;AACA,SAAO,KAAK,UAAU,KAAK,KAAK,CAAC,KAAK;AACxC;AAEO,IAAM,aAAa,CAAC,UAA0B;AACnD,MAAI,OAAO;AACX,MAAI,MAAM;AACV,WAAS,QAAQ,GAAG,QAAQ,MAAM,QAAQ,SAAS,GAAG;AACpD,UAAM,OAAO,MAAM,WAAW,KAAK;AACnC,WAAO;AACP,YAAQ,OAAO,KAAK;AACpB,UAAM,KAAK,KAAK,KAAK,QAAU,MAAM;AACrC,WAAO,KAAK,KAAK,MAAM,QAAU,MAAM;AAAA,EACzC;AACA,QAAM,MAAM,CAAC,WAAmB,UAAU,GAAG,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG;AACzE,SAAO,GAAG,IAAI,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,IAAI,KAAK,KAAK,OAAO,KAAK,SAAU,CAAC,CAAC,GAAG,IAAI,MAAM,SAAS,UAAU,CAAC;AAC1G;AAEO,IAAM,YAAY,CAAC,UAA2B,WAAW,cAAc,KAAK,CAAC;;;AC/BpF,IAAM,UAAU;AAQT,IAAM,qBAAqB,CAAC,UAAoB,QAAwB;AAC7E,MAAI,CAAC,OAAO,SAAS,GAAG,KAAK,OAAO,GAAG;AACrC,UAAM,IAAI,MAAM,gBAAgB,OAAO,GAAG,CAAC,EAAE;AAAA,EAC/C;AAEA,QAAM,WAAW,MAAM;AACrB,QAAI,OAAO,aAAa,UAAU;AAChC,UAAI,CAAC,OAAO,SAAS,QAAQ,EAAG,OAAM,IAAI,MAAM,qBAAqB,OAAO,QAAQ,CAAC,EAAE;AACvF,aAAO;AAAA,IACT;AAEA,UAAM,QAAQ,QAAQ,KAAK,SAAS,KAAK,CAAC;AAC1C,QAAI,CAAC,MAAO,OAAM,IAAI,MAAM,qBAAqB,QAAQ,EAAE;AAC3D,UAAM,QAAQ,OAAO,MAAM,CAAC,CAAC;AAC7B,UAAM,OAAO,MAAM,CAAC,KAAK;AACzB,QAAI,SAAS,KAAM,QAAO,QAAQ;AAClC,QAAI,SAAS,OAAO,KAAK,WAAW,OAAO,EAAG,QAAO,QAAQ;AAC7D,WAAO;AAAA,EACT,GAAG;AAEH,MAAI,UAAU,EAAG,OAAM,IAAI,MAAM,gCAAgC,OAAO,QAAQ,CAAC,EAAE;AACnF,SAAO,KAAK,IAAI,GAAG,KAAK,MAAM,UAAU,GAAG,CAAC;AAC9C;AAEO,IAAM,oBAAoB,CAAC,QAAgB,QAAwB,SAAS;AAE5E,IAAM,iBAAiB,CAAC,QAAgB,QAAwB;AACrE,QAAM,eAAe,KAAK,MAAM,SAAS,GAAG;AAC5C,QAAM,UAAU,KAAK,MAAM,eAAe,EAAE;AAC5C,QAAM,UAAU,eAAe;AAC/B,QAAM,YAAY,KAAK,MAAM,SAAS,GAAG;AACzC,QAAM,MAAM,CAAC,OAAe,OAAO,MAAM,OAAO,KAAK,EAAE,SAAS,MAAM,GAAG;AACzE,SAAO,GAAG,IAAI,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,IAAI,IAAI,SAAS,CAAC;AAC1D;","names":[]}