@osmo.inc/cli-linux-arm64 0.1.19 → 0.1.20
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/studio.js +920 -329
- package/package.json +1 -1
- package/release.json +32 -32
- package/runtime/apps/native/tools/compile.mjs +10 -2
- package/runtime/packages/scene/src/assets.ts +27 -0
- package/runtime/packages/scene/src/elements.ts +29 -13
- package/runtime/packages/scene/src/evaluate.ts +58 -21
- package/runtime/packages/scene/src/index.ts +4 -0
- package/runtime/packages/scene/src/jsx-runtime.ts +9 -3
- package/runtime/packages/scene/src/knob-schema.ts +5 -2
- package/runtime/packages/scene/src/knobs.ts +50 -10
- package/runtime/packages/scene/src/motion-sampling.d.mts +1 -0
- package/runtime/packages/scene/src/motion-sampling.mjs +33 -0
- package/runtime/packages/scene/src/motion.ts +5 -5
- package/runtime/packages/scene/src/text-animation.ts +239 -0
- package/studio/src/app.css +1 -1
- package/studio/src/app.js +207 -198
- package/studio/src/engine_bg.wasm +0 -0
- package/third-party/inventory.json +1061 -78
- package/third-party/npm/@phosphor-icons__react@2.1.10/LICENSE +21 -0
- package/third-party/npm/@phosphor-icons__react@2.1.10/package.json +113 -0
- package/third-party/npm/@radix-ui__number@1.1.1/package.json +62 -0
- package/third-party/npm/@radix-ui__number@1.1.1/radix-ui-primitives-ee4e3197bffe-LICENSE +21 -0
- package/third-party/npm/@radix-ui__primitive@1.1.3/LICENSE +21 -0
- package/third-party/npm/@radix-ui__primitive@1.1.3/package.json +47 -0
- package/third-party/npm/@radix-ui__react-accessible-icon@1.1.7/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-accessible-icon@1.1.7/package.json +68 -0
- package/third-party/npm/@radix-ui__react-accordion@1.2.12/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-accordion@1.2.12/package.json +76 -0
- package/third-party/npm/@radix-ui__react-alert-dialog@1.1.15/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-alert-dialog@1.1.15/package.json +73 -0
- package/third-party/npm/@radix-ui__react-arrow@1.1.7/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-arrow@1.1.7/package.json +68 -0
- package/third-party/npm/@radix-ui__react-aspect-ratio@1.1.7/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-aspect-ratio@1.1.7/package.json +68 -0
- package/third-party/npm/@radix-ui__react-avatar@1.1.10/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-avatar@1.1.10/package.json +72 -0
- package/third-party/npm/@radix-ui__react-checkbox@1.3.3/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-checkbox@1.3.3/package.json +75 -0
- package/third-party/npm/@radix-ui__react-collapsible@1.1.12/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-collapsible@1.1.12/package.json +75 -0
- package/third-party/npm/@radix-ui__react-collection@1.1.7/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-collection@1.1.7/package.json +71 -0
- package/third-party/npm/@radix-ui__react-compose-refs@1.1.2/package.json +75 -0
- package/third-party/npm/@radix-ui__react-compose-refs@1.1.2/radix-ui-primitives-ee4e3197bffe-LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-context-menu@2.2.16/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-context-menu@2.2.16/package.json +74 -0
- package/third-party/npm/@radix-ui__react-context@1.1.2/package.json +75 -0
- package/third-party/npm/@radix-ui__react-context@1.1.2/radix-ui-primitives-ee4e3197bffe-LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-dialog@1.1.15/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-dialog@1.1.15/package.json +81 -0
- package/third-party/npm/@radix-ui__react-direction@1.1.1/package.json +75 -0
- package/third-party/npm/@radix-ui__react-direction@1.1.1/radix-ui-primitives-ee4e3197bffe-LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-dismissable-layer@1.1.11/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-dismissable-layer@1.1.11/package.json +73 -0
- package/third-party/npm/@radix-ui__react-dropdown-menu@2.1.16/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-dropdown-menu@2.1.16/package.json +75 -0
- package/third-party/npm/@radix-ui__react-focus-guards@1.1.3/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-focus-guards@1.1.3/package.json +60 -0
- package/third-party/npm/@radix-ui__react-focus-scope@1.1.7/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-focus-scope@1.1.7/package.json +70 -0
- package/third-party/npm/@radix-ui__react-form@0.1.8/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-form@0.1.8/package.json +73 -0
- package/third-party/npm/@radix-ui__react-hover-card@1.1.15/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-hover-card@1.1.15/package.json +76 -0
- package/third-party/npm/@radix-ui__react-id@1.1.1/package.json +78 -0
- package/third-party/npm/@radix-ui__react-id@1.1.1/radix-ui-primitives-ee4e3197bffe-LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-label@2.1.7/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-label@2.1.7/package.json +68 -0
- package/third-party/npm/@radix-ui__react-menu@2.1.16/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-menu@2.1.16/package.json +86 -0
- package/third-party/npm/@radix-ui__react-menubar@1.1.16/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-menubar@1.1.16/package.json +78 -0
- package/third-party/npm/@radix-ui__react-navigation-menu@1.2.14/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-navigation-menu@1.2.14/package.json +81 -0
- package/third-party/npm/@radix-ui__react-one-time-password-field@0.1.8/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-one-time-password-field@0.1.8/package.json +80 -0
- package/third-party/npm/@radix-ui__react-password-toggle-field@0.1.3/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-password-toggle-field@0.1.3/package.json +76 -0
- package/third-party/npm/@radix-ui__react-popover@1.1.15/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-popover@1.1.15/package.json +82 -0
- package/third-party/npm/@radix-ui__react-popper@1.2.8/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-popper@1.2.8/package.json +77 -0
- package/third-party/npm/@radix-ui__react-portal@1.1.9/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-portal@1.1.9/package.json +69 -0
- package/third-party/npm/@radix-ui__react-presence@1.1.5/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-presence@1.1.5/package.json +69 -0
- package/third-party/npm/@radix-ui__react-primitive@2.1.3/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-primitive@2.1.3/package.json +68 -0
- package/third-party/npm/@radix-ui__react-progress@1.1.7/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-progress@1.1.7/package.json +69 -0
- package/third-party/npm/@radix-ui__react-radio-group@1.3.8/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-radio-group@1.3.8/package.json +77 -0
- package/third-party/npm/@radix-ui__react-roving-focus@1.1.11/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-roving-focus@1.1.11/package.json +76 -0
- package/third-party/npm/@radix-ui__react-scroll-area@1.2.10/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-scroll-area@1.2.10/package.json +76 -0
- package/third-party/npm/@radix-ui__react-select@2.2.6/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-select@2.2.6/package.json +89 -0
- package/third-party/npm/@radix-ui__react-separator@1.1.7/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-separator@1.1.7/package.json +68 -0
- package/third-party/npm/@radix-ui__react-slider@1.3.6/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-slider@1.3.6/package.json +78 -0
- package/third-party/npm/@radix-ui__react-slot@1.2.3/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-slot@1.2.3/package.json +63 -0
- package/third-party/npm/@radix-ui__react-switch@1.2.6/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-switch@1.2.6/package.json +74 -0
- package/third-party/npm/@radix-ui__react-tabs@1.1.13/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-tabs@1.1.13/package.json +75 -0
- package/third-party/npm/@radix-ui__react-toast@1.2.15/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-toast@1.2.15/package.json +79 -0
- package/third-party/npm/@radix-ui__react-toggle-group@1.1.11/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-toggle-group@1.1.11/package.json +74 -0
- package/third-party/npm/@radix-ui__react-toggle@1.1.10/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-toggle@1.1.10/package.json +70 -0
- package/third-party/npm/@radix-ui__react-toolbar@1.1.11/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-toolbar@1.1.11/package.json +74 -0
- package/third-party/npm/@radix-ui__react-tooltip@1.2.8/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-tooltip@1.2.8/package.json +79 -0
- package/third-party/npm/@radix-ui__react-use-callback-ref@1.1.1/package.json +75 -0
- package/third-party/npm/@radix-ui__react-use-callback-ref@1.1.1/radix-ui-primitives-ee4e3197bffe-LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-use-controllable-state@1.2.2/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-use-controllable-state@1.2.2/package.json +64 -0
- package/third-party/npm/@radix-ui__react-use-effect-event@0.0.2/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-use-effect-event@0.0.2/package.json +65 -0
- package/third-party/npm/@radix-ui__react-use-escape-keydown@1.1.1/package.json +78 -0
- package/third-party/npm/@radix-ui__react-use-escape-keydown@1.1.1/radix-ui-primitives-ee4e3197bffe-LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-use-is-hydrated@0.1.0/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-use-is-hydrated@0.1.0/package.json +65 -0
- package/third-party/npm/@radix-ui__react-use-layout-effect@1.1.1/package.json +75 -0
- package/third-party/npm/@radix-ui__react-use-layout-effect@1.1.1/radix-ui-primitives-ee4e3197bffe-LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-use-previous@1.1.1/package.json +75 -0
- package/third-party/npm/@radix-ui__react-use-previous@1.1.1/radix-ui-primitives-ee4e3197bffe-LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-use-size@1.1.1/package.json +79 -0
- package/third-party/npm/@radix-ui__react-use-size@1.1.1/radix-ui-primitives-ee4e3197bffe-LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-visually-hidden@1.2.3/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-visually-hidden@1.2.3/package.json +68 -0
- package/third-party/npm/color-convert@3.1.3/LICENSE +22 -0
- package/third-party/npm/color-convert@3.1.3/package.json +58 -0
- package/third-party/npm/color-name@2.1.0/LICENSE +8 -0
- package/third-party/npm/color-name@2.1.0/package.json +32 -0
- package/third-party/npm/color-string@2.1.4/LICENSE +21 -0
- package/third-party/npm/color-string@2.1.4/package.json +47 -0
- package/third-party/npm/color@5.0.3/LICENSE +21 -0
- package/third-party/npm/color@5.0.3/package.json +52 -0
- package/third-party/npm/radix-ui@1.4.3/LICENSE +21 -0
- package/third-party/npm/radix-ui@1.4.3/package.json +136 -0
- package/third-party/npm/use-sync-external-store@1.6.0/LICENSE +21 -0
- package/third-party/npm/use-sync-external-store@1.6.0/package.json +43 -0
- package/vendor/osmo-lite +0 -0
- package/vendor/osmo-scene-0.1.0.tgz +0 -0
- package/vendor/platform.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AssetKind, AssetPath } from "./assets.js";
|
|
1
2
|
import type { Color } from "./elements.js";
|
|
2
3
|
import { isMotion, sampleMotion, type Motion, type MotionValue, type TrackDefinition } from "./motion.js";
|
|
3
4
|
import { freeze, validateKnobSchema, validateKnobValue, type Knob, type KnobGroup, type Knobs, type KnobValue } from "./knob-schema.js";
|
|
@@ -5,7 +6,8 @@ import { freeze, validateKnobSchema, validateKnobValue, type Knob, type KnobGrou
|
|
|
5
6
|
const SET = Symbol("osmo.knobs");
|
|
6
7
|
const REF = Symbol("osmo.knob.reference");
|
|
7
8
|
const VALUE = Symbol("osmo.knob.value-type");
|
|
8
|
-
type ControlValue<K extends Knob> = K extends { type: "
|
|
9
|
+
type ControlValue<K extends Knob> = K extends { type: "asset"; kind: infer A extends AssetKind } ? AssetPath<A>
|
|
10
|
+
: K extends { type: "color" } ? Color
|
|
9
11
|
: K extends { type: "select"; options: infer O } ? keyof O & string
|
|
10
12
|
: K extends { type: "multiselect"; options: infer O } ? readonly (keyof O & string)[]
|
|
11
13
|
: KnobValue<K>;
|
|
@@ -16,13 +18,16 @@ export interface KnobRef<T = unknown> {
|
|
|
16
18
|
readonly [VALUE]?: T;
|
|
17
19
|
}
|
|
18
20
|
export type KnobSet<K extends Knobs = Knobs> = {
|
|
19
|
-
readonly [SET]: { readonly knobs: K; readonly groups: Readonly<Record<string, KnobGroup
|
|
21
|
+
readonly [SET]: { readonly knobs: K; readonly groups: Readonly<Record<string, KnobGroup>>; readonly sourceFile?: string };
|
|
20
22
|
} & { readonly [P in keyof K]: KnobRef<ControlValue<K[P]>> };
|
|
21
23
|
/** Per-Group overrides. Parent knob references forward values into a child scope; motion tracks animate them. */
|
|
22
24
|
export type KnobBindings<K extends Knobs> = {
|
|
23
|
-
readonly [P in keyof K]?: ControlValue<K[P]> | Motion<KnobValue<K[P]>> | KnobRef<ControlValue<K[P]>>;
|
|
25
|
+
readonly [P in keyof K]?: ControlValue<K[P]> | (K[P] extends { type: "asset" } ? never : Motion<KnobValue<K[P]>>) | KnobRef<ControlValue<K[P]>>;
|
|
24
26
|
};
|
|
27
|
+
export interface AssetBinding { kind: AssetKind; sourceFile?: string; name: string }
|
|
25
28
|
export interface KnobDescription {
|
|
29
|
+
readonly assets?: Readonly<Record<string, AssetBinding>>;
|
|
30
|
+
readonly origins?: Readonly<Record<string, { sourceFile?: string; animated: boolean }>>;
|
|
26
31
|
readonly knobs: Knobs;
|
|
27
32
|
readonly groups: Readonly<Record<string, KnobGroup>>;
|
|
28
33
|
readonly values: Readonly<Record<string, MotionValue>>;
|
|
@@ -39,11 +44,11 @@ export type KnobScope = readonly { set: KnobSet; description: KnobDescription }[
|
|
|
39
44
|
* const style = defineKnobs({ size: { type: "number", default: 48, step: 1 }, ink: { type: "color", default: "#ffffff" } });
|
|
40
45
|
* // <Group knobs={style} values={{ size: 64 }}><Text fontSize={style.size} color={style.ink}>Hello</Text></Group>
|
|
41
46
|
*/
|
|
42
|
-
export function defineKnobs<const K extends Knobs>(knobs: K, options: { groups?: Readonly<Record<string, KnobGroup>> } = {}): KnobSet<K> {
|
|
47
|
+
export function defineKnobs<const K extends Knobs>(knobs: K, options: { groups?: Readonly<Record<string, KnobGroup>> } = {}, sourceFile?: string): KnobSet<K> {
|
|
43
48
|
if (!options || typeof options !== "object" || Array.isArray(options) || Object.keys(options).some(k => k !== "groups"))
|
|
44
49
|
throw new Error("Knobs: options may only contain groups");
|
|
45
50
|
validateKnobSchema(knobs, options.groups);
|
|
46
|
-
const definition = freeze(JSON.parse(JSON.stringify({ knobs, groups: options.groups ?? {} })));
|
|
51
|
+
const definition = freeze(JSON.parse(JSON.stringify({ knobs, groups: options.groups ?? {}, sourceFile })));
|
|
47
52
|
const set = Object.create(null);
|
|
48
53
|
Object.defineProperty(set, SET, { value: definition });
|
|
49
54
|
for (const name of Object.keys(knobs)) {
|
|
@@ -64,13 +69,30 @@ export function resolveKnob(ref: KnobRef, scope: KnobScope): MotionValue {
|
|
|
64
69
|
const { set, name } = ref[REF];
|
|
65
70
|
for (let i = scope.length - 1; i >= 0; i--)
|
|
66
71
|
if (scope[i].set === set) return scope[i].description.values[name];
|
|
67
|
-
throw new Error(`Knob "${name}" has no owning Group in this scope`);
|
|
72
|
+
throw new Error(`Knob "${name}" has no owning Group or Scene in this scope`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function resolveKnobAsset(ref: KnobRef, scope: KnobScope): AssetBinding | undefined {
|
|
76
|
+
const { set, name } = ref[REF];
|
|
77
|
+
for (let i = scope.length - 1; i >= 0; i--)
|
|
78
|
+
if (scope[i].set === set) return scope[i].description.assets?.[name];
|
|
79
|
+
}
|
|
80
|
+
function resolveKnobOrigin(ref: KnobRef, scope: KnobScope) {
|
|
81
|
+
const { set, name } = ref[REF];
|
|
82
|
+
for (let i = scope.length - 1; i >= 0; i--)
|
|
83
|
+
if (scope[i].set === set) return scope[i].description.origins?.[name];
|
|
84
|
+
}
|
|
85
|
+
export interface KnobContext {
|
|
86
|
+
sourceFile?: string;
|
|
87
|
+
valueSources?: Readonly<Record<string, string>>;
|
|
88
|
+
bindingTimes?: Readonly<Record<string, number>>;
|
|
68
89
|
}
|
|
69
90
|
|
|
70
91
|
// Track definitions are immutable. Validate all future keys once per knob,
|
|
71
92
|
// rather than rescanning a long timeline on every rendered frame.
|
|
72
93
|
const validatedTracks = new WeakMap<Motion, WeakSet<Knob>>();
|
|
73
94
|
function validateBinding(knob: Knob, binding: MotionValue | Motion, name: string) {
|
|
95
|
+
if (knob.type === "asset" && isMotion(binding)) throw new Error(`Knob ${name}: asset bindings must be static`);
|
|
74
96
|
if (!isMotion(binding)) { validateKnobValue(knob, binding, name); return; }
|
|
75
97
|
let checked = validatedTracks.get(binding);
|
|
76
98
|
if (checked?.has(knob)) return;
|
|
@@ -83,24 +105,42 @@ function validateBinding(knob: Knob, binding: MotionValue | Motion, name: string
|
|
|
83
105
|
export function describeKnobs<K extends Knobs>(set: KnobSet<K>, bindings: KnobBindings<K> = {}, time = 0): KnobDescription {
|
|
84
106
|
return evaluateKnobs(set, bindings, time, []);
|
|
85
107
|
}
|
|
86
|
-
export function evaluateKnobs(set: KnobSet, bindings: unknown, time: number, parentScope: KnobScope): KnobDescription {
|
|
108
|
+
export function evaluateKnobs(set: KnobSet, bindings: unknown, time: number, parentScope: KnobScope, context: KnobContext = {}): KnobDescription {
|
|
87
109
|
if (!isKnobSet(set)) throw new Error("Group.knobs must be a defineKnobs declaration");
|
|
88
110
|
if (!Number.isFinite(time)) throw new Error("Knob time must be finite");
|
|
89
111
|
const input = bindings === undefined ? {} : bindings;
|
|
90
112
|
if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("Group.values must be named knob bindings");
|
|
91
|
-
const { knobs, groups } = set[SET];
|
|
113
|
+
const { knobs, groups, sourceFile } = set[SET];
|
|
92
114
|
for (const name of Object.keys(input))
|
|
93
115
|
if (!Object.hasOwn(knobs, name)) throw new Error(`Group.values: unknown knob "${name}"`);
|
|
94
116
|
const values: Record<string, MotionValue> = Object.create(null);
|
|
95
117
|
const tracks: Record<string, TrackDefinition> = Object.create(null);
|
|
118
|
+
const assets: Record<string, AssetBinding> = Object.create(null);
|
|
119
|
+
const origins: Record<string, {sourceFile?: string; animated:boolean}> = Object.create(null);
|
|
96
120
|
for (const [name, knob] of Object.entries(knobs)) {
|
|
97
121
|
const supplied = (input as Record<string, unknown>)[name];
|
|
98
122
|
// Explicit null is invalid; only an omitted/undefined binding uses the default.
|
|
99
123
|
let binding = supplied === undefined ? knob.default : supplied;
|
|
124
|
+
origins[name] = (isKnobRef(binding) ? resolveKnobOrigin(binding, parentScope) : undefined) ?? {
|
|
125
|
+
sourceFile: supplied === undefined ? sourceFile ?? context.sourceFile
|
|
126
|
+
: context.valueSources?.[name] ?? context.sourceFile,
|
|
127
|
+
animated: isMotion(binding),
|
|
128
|
+
};
|
|
129
|
+
const forwarded = isKnobRef(binding) ? resolveKnobAsset(binding, parentScope) : undefined;
|
|
130
|
+
if (knob.type === "asset") {
|
|
131
|
+
if (origins[name].animated) throw new Error(`Knob ${name}: asset bindings must be static`);
|
|
132
|
+
if (forwarded && forwarded.kind !== knob.kind)
|
|
133
|
+
throw new Error(`Knob ${name}: cannot bind ${forwarded.kind} to ${knob.kind}`);
|
|
134
|
+
assets[name] = forwarded ?? { kind: knob.kind, name,
|
|
135
|
+
sourceFile: origins[name].sourceFile };
|
|
136
|
+
} else if (forwarded) {
|
|
137
|
+
// Legacy text controls may forward typed paths without losing their origin.
|
|
138
|
+
assets[name] = forwarded;
|
|
139
|
+
}
|
|
100
140
|
if (isKnobRef(binding)) binding = resolveKnob(binding, parentScope);
|
|
101
141
|
validateBinding(knob, binding as MotionValue | Motion, name);
|
|
102
142
|
const value = isMotion(binding)
|
|
103
|
-
? sampleMotion(binding, time, knob.type === "color" ? "color" : knob.type === "number" ? undefined : "discrete")
|
|
143
|
+
? sampleMotion(binding, context.bindingTimes?.[name] ?? time, knob.type === "color" ? "color" : knob.type === "number" ? undefined : "discrete")
|
|
104
144
|
: binding as MotionValue;
|
|
105
145
|
validateKnobValue(knob, value, name);
|
|
106
146
|
values[name] = Array.isArray(value) ? [...value] : value;
|
|
@@ -114,5 +154,5 @@ export function evaluateKnobs(set: KnobSet, bindings: unknown, time: number, par
|
|
|
114
154
|
const visibleKnobs = Object.fromEntries(Object.entries(knobs).map(([name, knob]) => [name,
|
|
115
155
|
visible(knob.visibleWhen) && (!knob.group || visibleGroups[knob.group])
|
|
116
156
|
]));
|
|
117
|
-
return freeze({ knobs, groups, values, tracks, visibleGroups, visibleKnobs });
|
|
157
|
+
return freeze({ knobs, groups, values, tracks, visibleGroups, visibleKnobs, origins, ...(Object.keys(assets).length ? {assets} : {}) });
|
|
118
158
|
}
|
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
import {parseColor, mixColor} from "./color-value.mjs";
|
|
2
|
+
const curves = new Map();
|
|
3
|
+
function curve(value) {
|
|
4
|
+
if (typeof value !== "string") return null;
|
|
5
|
+
if (curves.has(value)) return curves.get(value);
|
|
6
|
+
const match = /^cubic-bezier\(([^)]+)\)$/.exec(value);
|
|
7
|
+
if (!match) return null;
|
|
8
|
+
const parts = match[1].split(",");
|
|
9
|
+
const points = parts.map(Number);
|
|
10
|
+
if (parts.some(p => !p.trim()) || points.length !== 4 || !points.every(Number.isFinite) ||
|
|
11
|
+
points[0] < 0 || points[0] > 1 || points[2] < 0 || points[2] > 1) return null;
|
|
12
|
+
if (curves.size >= 256) curves.clear();
|
|
13
|
+
curves.set(value, points);
|
|
14
|
+
return points;
|
|
15
|
+
}
|
|
16
|
+
export function isEasing(value) {
|
|
17
|
+
return ["linear", "easeIn", "easeOut", "easeInOut", "hold"].includes(value) || curve(value) !== null;
|
|
18
|
+
}
|
|
19
|
+
function bezier(t, a, b) {
|
|
20
|
+
return 3 * (1 - t) ** 2 * t * a + 3 * (1 - t) * t * t * b + t ** 3;
|
|
21
|
+
}
|
|
2
22
|
function ease(t, easing) {
|
|
23
|
+
const points = curve(easing);
|
|
24
|
+
if (points) {
|
|
25
|
+
// Easing is y(x^-1(time)), not y(time). Bisection remains stable
|
|
26
|
+
// with flat tangents, including x1=1/x2=0, and allows y overshoot.
|
|
27
|
+
if (t <= 0 || t >= 1) return t;
|
|
28
|
+
let lo = 0, hi = 1;
|
|
29
|
+
for (let i = 0; i < 32; i++) {
|
|
30
|
+
const u = (lo + hi) / 2;
|
|
31
|
+
if (bezier(u, points[0], points[2]) < t) lo = u;
|
|
32
|
+
else hi = u;
|
|
33
|
+
}
|
|
34
|
+
return bezier((lo + hi) / 2, points[1], points[3]);
|
|
35
|
+
}
|
|
3
36
|
switch (easing) {
|
|
4
37
|
case "linear":
|
|
5
38
|
return t;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { parseColor } from "./color.js";
|
|
2
|
-
import { sampleTrack } from "./motion-sampling.mjs";
|
|
2
|
+
import { sampleTrack, isEasing } from "./motion-sampling.mjs";
|
|
3
3
|
const TRACK = Symbol("osmo.motion.track");
|
|
4
4
|
/** Numbers/vectors interpolate; colors interpolate in linear light; text, booleans and choices switch discretely. */
|
|
5
5
|
export type MotionValue = number | boolean | string | readonly number[] | readonly string[];
|
|
6
|
-
|
|
6
|
+
/** Cubic Bézier x control points must be in 0..1; y may overshoot. */
|
|
7
|
+
export type Easing = "linear" | "easeIn" | "easeOut" | "easeInOut" | "hold"
|
|
8
|
+
| `cubic-bezier(${number},${number},${number},${number})`;
|
|
7
9
|
export interface Keyframe<T extends MotionValue = MotionValue> {
|
|
8
10
|
/** Scene time in seconds; nonnegative and strictly increasing within a track. */
|
|
9
11
|
readonly at: number;
|
|
@@ -73,9 +75,7 @@ export function defineMotion<const D extends MotionDefinition>(
|
|
|
73
75
|
throw new Error(`${label}: expected a color`);
|
|
74
76
|
if (
|
|
75
77
|
key.easing !== undefined &&
|
|
76
|
-
!
|
|
77
|
-
key.easing,
|
|
78
|
-
)
|
|
78
|
+
!isEasing(key.easing)
|
|
79
79
|
) {
|
|
80
80
|
throw new Error(`${label}: unsupported easing ${key.easing}`);
|
|
81
81
|
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import type { Color, Vec2 } from "./elements.js";
|
|
2
|
+
import type { Easing, MotionValue } from "./motion.js";
|
|
3
|
+
import { sampleTrack, isEasing } from "./motion-sampling.mjs";
|
|
4
|
+
import { parseColor } from "./color.js";
|
|
5
|
+
import { blur, evaluateEffects, type EvaluatedEffect } from "./effects.js";
|
|
6
|
+
|
|
7
|
+
/** Post-layout offsets; omitted properties keep their neutral value. */
|
|
8
|
+
export interface TextAnimationStyle {
|
|
9
|
+
position?: Vec2;
|
|
10
|
+
z?: number;
|
|
11
|
+
rotation?: number;
|
|
12
|
+
scale?: number;
|
|
13
|
+
/** Pivot in unit-local pixels; omitted uses the unit's ink center. */
|
|
14
|
+
origin?: Vec2;
|
|
15
|
+
opacity?: number;
|
|
16
|
+
color?: Color;
|
|
17
|
+
/** Gaussian radius in pixels, 0–64. */
|
|
18
|
+
blur?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface TextAnimation {
|
|
21
|
+
unit: "character" | "word" | "line";
|
|
22
|
+
/** Seconds between units, in reading/source order. Default: 0. */
|
|
23
|
+
stagger?: number;
|
|
24
|
+
/** Seconds before the first unit starts. Default: 0. */
|
|
25
|
+
delay?: number;
|
|
26
|
+
/** Seconds per unit. Default: 0.4. Use keyframes for multiple stages. */
|
|
27
|
+
duration?: number;
|
|
28
|
+
easing?: Easing;
|
|
29
|
+
from?: TextAnimationStyle;
|
|
30
|
+
to?: TextAnimationStyle;
|
|
31
|
+
/** Local seconds per unit; begin at 0. Replaces from/to/duration. */
|
|
32
|
+
keyframes?: readonly {
|
|
33
|
+
at: number;
|
|
34
|
+
value: TextAnimationStyle;
|
|
35
|
+
easing?: Easing;
|
|
36
|
+
}[];
|
|
37
|
+
}
|
|
38
|
+
export interface TextAnimationState {
|
|
39
|
+
position: Vec2;
|
|
40
|
+
z: number;
|
|
41
|
+
rotation: number;
|
|
42
|
+
scale: number;
|
|
43
|
+
origin?: Vec2;
|
|
44
|
+
opacity: number;
|
|
45
|
+
color: Color;
|
|
46
|
+
effects: EvaluatedEffect[];
|
|
47
|
+
}
|
|
48
|
+
export interface EvaluatedTextAnimation {
|
|
49
|
+
unit: TextAnimation["unit"];
|
|
50
|
+
/** UTF-8 ranges, matched against shaped clusters. Lines use visual line indices. */
|
|
51
|
+
ranges: readonly (readonly [number, number])[];
|
|
52
|
+
states: TextAnimationState[];
|
|
53
|
+
}
|
|
54
|
+
const properties = [
|
|
55
|
+
"position",
|
|
56
|
+
"z",
|
|
57
|
+
"rotation",
|
|
58
|
+
"scale",
|
|
59
|
+
"origin",
|
|
60
|
+
"opacity",
|
|
61
|
+
"color",
|
|
62
|
+
"blur",
|
|
63
|
+
] as const;
|
|
64
|
+
const fail = (message: string): never => {
|
|
65
|
+
throw new Error(`Text.animate: ${message}`);
|
|
66
|
+
};
|
|
67
|
+
function object(v: unknown): Record<string, unknown> {
|
|
68
|
+
if (!v || typeof v !== "object" || Array.isArray(v))
|
|
69
|
+
return fail("expected an object");
|
|
70
|
+
return v as Record<string, unknown>;
|
|
71
|
+
}
|
|
72
|
+
function finite(v: unknown, name: string, min = -1e6, max = 1e6): number {
|
|
73
|
+
if (typeof v !== "number" || !Number.isFinite(v) || v < min || v > max)
|
|
74
|
+
return fail(`${name} must be a finite number in ${min}..${max}`);
|
|
75
|
+
return v;
|
|
76
|
+
}
|
|
77
|
+
function easing(v: unknown): Easing {
|
|
78
|
+
if (
|
|
79
|
+
!isEasing(v)
|
|
80
|
+
)
|
|
81
|
+
return fail("unsupported easing");
|
|
82
|
+
return v as Easing;
|
|
83
|
+
}
|
|
84
|
+
function style(v: unknown): TextAnimationStyle {
|
|
85
|
+
const value = object(v);
|
|
86
|
+
for (const [key, x] of Object.entries(value)) {
|
|
87
|
+
if (!(properties as readonly string[]).includes(key))
|
|
88
|
+
fail(`unsupported per-unit property ${key}; layout belongs on Text`);
|
|
89
|
+
if (key === "position" || key === "origin") {
|
|
90
|
+
if (!Array.isArray(x) || x.length !== 2) fail(`${key} must be [x, y]`);
|
|
91
|
+
(x as unknown[]).forEach((n) => finite(n, key));
|
|
92
|
+
} else if (key === "color") {
|
|
93
|
+
if (!parseColor(x)) fail("color must be a hex color or transparent");
|
|
94
|
+
} else
|
|
95
|
+
finite(
|
|
96
|
+
x,
|
|
97
|
+
key,
|
|
98
|
+
key === "opacity" || key === "blur" ? 0 : -1e6,
|
|
99
|
+
key === "opacity" ? 1 : key === "blur" ? 64 : 1e6,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return value as TextAnimationStyle;
|
|
103
|
+
}
|
|
104
|
+
const bytes = (s: string) =>
|
|
105
|
+
Array.from(s).reduce(
|
|
106
|
+
(n, c) =>
|
|
107
|
+
n +
|
|
108
|
+
(c.codePointAt(0)! > 0xffff
|
|
109
|
+
? 4
|
|
110
|
+
: c.charCodeAt(0) > 0x7ff
|
|
111
|
+
? 3
|
|
112
|
+
: c.charCodeAt(0) > 0x7f
|
|
113
|
+
? 2
|
|
114
|
+
: 1),
|
|
115
|
+
0,
|
|
116
|
+
);
|
|
117
|
+
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
|
|
118
|
+
export function evaluateTextAnimation(
|
|
119
|
+
input: unknown,
|
|
120
|
+
text: string,
|
|
121
|
+
time: number,
|
|
122
|
+
color: Color,
|
|
123
|
+
): EvaluatedTextAnimation {
|
|
124
|
+
const a = object(input);
|
|
125
|
+
for (const name of Object.keys(a))
|
|
126
|
+
if (
|
|
127
|
+
![
|
|
128
|
+
"unit",
|
|
129
|
+
"stagger",
|
|
130
|
+
"delay",
|
|
131
|
+
"duration",
|
|
132
|
+
"easing",
|
|
133
|
+
"from",
|
|
134
|
+
"to",
|
|
135
|
+
"keyframes",
|
|
136
|
+
].includes(name)
|
|
137
|
+
)
|
|
138
|
+
fail(`unknown option ${name}`);
|
|
139
|
+
if (!["character", "word", "line"].includes(a.unit as string))
|
|
140
|
+
fail("unit must be character, word or line");
|
|
141
|
+
const unit = a.unit as TextAnimation["unit"];
|
|
142
|
+
const stagger = finite(a.stagger ?? 0, "stagger", 0),
|
|
143
|
+
delay = finite(a.delay ?? 0, "delay", 0);
|
|
144
|
+
const ease = easing(a.easing ?? "linear");
|
|
145
|
+
const neutral: TextAnimationStyle = {
|
|
146
|
+
position: [0, 0],
|
|
147
|
+
z: 0,
|
|
148
|
+
rotation: 0,
|
|
149
|
+
scale: 1,
|
|
150
|
+
opacity: 1,
|
|
151
|
+
color,
|
|
152
|
+
blur: 0,
|
|
153
|
+
};
|
|
154
|
+
let keys: { at: number; value: TextAnimationStyle; easing: Easing }[];
|
|
155
|
+
if (a.keyframes !== undefined) {
|
|
156
|
+
if (a.from !== undefined || a.to !== undefined || a.duration !== undefined)
|
|
157
|
+
fail("keyframes replace from/to/duration");
|
|
158
|
+
if (
|
|
159
|
+
!Array.isArray(a.keyframes) ||
|
|
160
|
+
a.keyframes.length < 2 ||
|
|
161
|
+
a.keyframes.length > 128
|
|
162
|
+
)
|
|
163
|
+
fail("expected 2–128 keyframes");
|
|
164
|
+
let previous = -1,
|
|
165
|
+
state = neutral;
|
|
166
|
+
keys = (a.keyframes as unknown[]).map((entry) => {
|
|
167
|
+
const key = object(entry);
|
|
168
|
+
if (Object.keys(key).some((k) => !["at", "value", "easing"].includes(k)))
|
|
169
|
+
fail("unknown keyframe property");
|
|
170
|
+
const at = finite(key.at, "keyframe.at", 0);
|
|
171
|
+
if (at <= previous || (previous === -1 && at !== 0))
|
|
172
|
+
fail("keyframes must start at 0 and increase strictly");
|
|
173
|
+
previous = at;
|
|
174
|
+
state = { ...state, ...style(key.value) };
|
|
175
|
+
return { at, value: state, easing: easing(key.easing ?? ease) };
|
|
176
|
+
});
|
|
177
|
+
} else {
|
|
178
|
+
const duration = finite(a.duration ?? 0.4, "duration", Number.EPSILON);
|
|
179
|
+
keys = [
|
|
180
|
+
{ at: 0, value: { ...neutral, ...style(a.from ?? {}) }, easing: ease },
|
|
181
|
+
{
|
|
182
|
+
at: duration,
|
|
183
|
+
value: { ...neutral, ...style(a.to ?? {}) },
|
|
184
|
+
easing: ease,
|
|
185
|
+
},
|
|
186
|
+
];
|
|
187
|
+
}
|
|
188
|
+
// An explicit origin is constant unless both endpoints supply it; never
|
|
189
|
+
// interpolate a pixel pivot against an unknown layout-dependent center.
|
|
190
|
+
const origin = keys.find((k) => k.value.origin)?.value.origin;
|
|
191
|
+
if (origin)
|
|
192
|
+
keys = keys.map((k) => ({
|
|
193
|
+
...k,
|
|
194
|
+
value: { ...k.value, origin: k.value.origin ?? origin },
|
|
195
|
+
}));
|
|
196
|
+
const graphemes = Array.from(segmenter.segment(text));
|
|
197
|
+
if (graphemes.length > 512) fail("at most 512 graphemes per animated Text");
|
|
198
|
+
const spans =
|
|
199
|
+
unit === "word"
|
|
200
|
+
? Array.from(text.matchAll(/\S+/gu), (m) => ({
|
|
201
|
+
index: m.index!,
|
|
202
|
+
segment: m[0],
|
|
203
|
+
}))
|
|
204
|
+
: graphemes;
|
|
205
|
+
const ranges = spans.map(
|
|
206
|
+
(s) =>
|
|
207
|
+
[
|
|
208
|
+
bytes(text.slice(0, s.index)),
|
|
209
|
+
bytes(text.slice(0, s.index + s.segment.length)),
|
|
210
|
+
] as const,
|
|
211
|
+
);
|
|
212
|
+
const count = unit === "line" ? graphemes.length + 1 : ranges.length;
|
|
213
|
+
const states = Array.from({ length: count }, (_, i) => {
|
|
214
|
+
const value: Record<string, unknown> = {};
|
|
215
|
+
for (const name of properties) {
|
|
216
|
+
if (keys[0].value[name] === undefined) continue;
|
|
217
|
+
value[name] = sampleTrack(
|
|
218
|
+
{
|
|
219
|
+
default: keys[0].value[name],
|
|
220
|
+
keyframes: keys.map((k) => ({
|
|
221
|
+
at: k.at,
|
|
222
|
+
value: k.value[name] as MotionValue,
|
|
223
|
+
easing: k.easing,
|
|
224
|
+
})),
|
|
225
|
+
},
|
|
226
|
+
time - delay - i * stagger,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
const { blur: radius, ...state } = value;
|
|
230
|
+
return {
|
|
231
|
+
...state,
|
|
232
|
+
effects:
|
|
233
|
+
Number(radius) > 0
|
|
234
|
+
? evaluateEffects([blur({ radius: Number(radius) })], time)
|
|
235
|
+
: [],
|
|
236
|
+
} as TextAnimationState;
|
|
237
|
+
});
|
|
238
|
+
return { unit, ranges: unit === "line" ? [] : ranges, states };
|
|
239
|
+
}
|