@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.
Files changed (152) hide show
  1. package/dist/studio.js +920 -329
  2. package/package.json +1 -1
  3. package/release.json +32 -32
  4. package/runtime/apps/native/tools/compile.mjs +10 -2
  5. package/runtime/packages/scene/src/assets.ts +27 -0
  6. package/runtime/packages/scene/src/elements.ts +29 -13
  7. package/runtime/packages/scene/src/evaluate.ts +58 -21
  8. package/runtime/packages/scene/src/index.ts +4 -0
  9. package/runtime/packages/scene/src/jsx-runtime.ts +9 -3
  10. package/runtime/packages/scene/src/knob-schema.ts +5 -2
  11. package/runtime/packages/scene/src/knobs.ts +50 -10
  12. package/runtime/packages/scene/src/motion-sampling.d.mts +1 -0
  13. package/runtime/packages/scene/src/motion-sampling.mjs +33 -0
  14. package/runtime/packages/scene/src/motion.ts +5 -5
  15. package/runtime/packages/scene/src/text-animation.ts +239 -0
  16. package/studio/src/app.css +1 -1
  17. package/studio/src/app.js +207 -198
  18. package/studio/src/engine_bg.wasm +0 -0
  19. package/third-party/inventory.json +1061 -78
  20. package/third-party/npm/@phosphor-icons__react@2.1.10/LICENSE +21 -0
  21. package/third-party/npm/@phosphor-icons__react@2.1.10/package.json +113 -0
  22. package/third-party/npm/@radix-ui__number@1.1.1/package.json +62 -0
  23. package/third-party/npm/@radix-ui__number@1.1.1/radix-ui-primitives-ee4e3197bffe-LICENSE +21 -0
  24. package/third-party/npm/@radix-ui__primitive@1.1.3/LICENSE +21 -0
  25. package/third-party/npm/@radix-ui__primitive@1.1.3/package.json +47 -0
  26. package/third-party/npm/@radix-ui__react-accessible-icon@1.1.7/LICENSE +21 -0
  27. package/third-party/npm/@radix-ui__react-accessible-icon@1.1.7/package.json +68 -0
  28. package/third-party/npm/@radix-ui__react-accordion@1.2.12/LICENSE +21 -0
  29. package/third-party/npm/@radix-ui__react-accordion@1.2.12/package.json +76 -0
  30. package/third-party/npm/@radix-ui__react-alert-dialog@1.1.15/LICENSE +21 -0
  31. package/third-party/npm/@radix-ui__react-alert-dialog@1.1.15/package.json +73 -0
  32. package/third-party/npm/@radix-ui__react-arrow@1.1.7/LICENSE +21 -0
  33. package/third-party/npm/@radix-ui__react-arrow@1.1.7/package.json +68 -0
  34. package/third-party/npm/@radix-ui__react-aspect-ratio@1.1.7/LICENSE +21 -0
  35. package/third-party/npm/@radix-ui__react-aspect-ratio@1.1.7/package.json +68 -0
  36. package/third-party/npm/@radix-ui__react-avatar@1.1.10/LICENSE +21 -0
  37. package/third-party/npm/@radix-ui__react-avatar@1.1.10/package.json +72 -0
  38. package/third-party/npm/@radix-ui__react-checkbox@1.3.3/LICENSE +21 -0
  39. package/third-party/npm/@radix-ui__react-checkbox@1.3.3/package.json +75 -0
  40. package/third-party/npm/@radix-ui__react-collapsible@1.1.12/LICENSE +21 -0
  41. package/third-party/npm/@radix-ui__react-collapsible@1.1.12/package.json +75 -0
  42. package/third-party/npm/@radix-ui__react-collection@1.1.7/LICENSE +21 -0
  43. package/third-party/npm/@radix-ui__react-collection@1.1.7/package.json +71 -0
  44. package/third-party/npm/@radix-ui__react-compose-refs@1.1.2/package.json +75 -0
  45. package/third-party/npm/@radix-ui__react-compose-refs@1.1.2/radix-ui-primitives-ee4e3197bffe-LICENSE +21 -0
  46. package/third-party/npm/@radix-ui__react-context-menu@2.2.16/LICENSE +21 -0
  47. package/third-party/npm/@radix-ui__react-context-menu@2.2.16/package.json +74 -0
  48. package/third-party/npm/@radix-ui__react-context@1.1.2/package.json +75 -0
  49. package/third-party/npm/@radix-ui__react-context@1.1.2/radix-ui-primitives-ee4e3197bffe-LICENSE +21 -0
  50. package/third-party/npm/@radix-ui__react-dialog@1.1.15/LICENSE +21 -0
  51. package/third-party/npm/@radix-ui__react-dialog@1.1.15/package.json +81 -0
  52. package/third-party/npm/@radix-ui__react-direction@1.1.1/package.json +75 -0
  53. package/third-party/npm/@radix-ui__react-direction@1.1.1/radix-ui-primitives-ee4e3197bffe-LICENSE +21 -0
  54. package/third-party/npm/@radix-ui__react-dismissable-layer@1.1.11/LICENSE +21 -0
  55. package/third-party/npm/@radix-ui__react-dismissable-layer@1.1.11/package.json +73 -0
  56. package/third-party/npm/@radix-ui__react-dropdown-menu@2.1.16/LICENSE +21 -0
  57. package/third-party/npm/@radix-ui__react-dropdown-menu@2.1.16/package.json +75 -0
  58. package/third-party/npm/@radix-ui__react-focus-guards@1.1.3/LICENSE +21 -0
  59. package/third-party/npm/@radix-ui__react-focus-guards@1.1.3/package.json +60 -0
  60. package/third-party/npm/@radix-ui__react-focus-scope@1.1.7/LICENSE +21 -0
  61. package/third-party/npm/@radix-ui__react-focus-scope@1.1.7/package.json +70 -0
  62. package/third-party/npm/@radix-ui__react-form@0.1.8/LICENSE +21 -0
  63. package/third-party/npm/@radix-ui__react-form@0.1.8/package.json +73 -0
  64. package/third-party/npm/@radix-ui__react-hover-card@1.1.15/LICENSE +21 -0
  65. package/third-party/npm/@radix-ui__react-hover-card@1.1.15/package.json +76 -0
  66. package/third-party/npm/@radix-ui__react-id@1.1.1/package.json +78 -0
  67. package/third-party/npm/@radix-ui__react-id@1.1.1/radix-ui-primitives-ee4e3197bffe-LICENSE +21 -0
  68. package/third-party/npm/@radix-ui__react-label@2.1.7/LICENSE +21 -0
  69. package/third-party/npm/@radix-ui__react-label@2.1.7/package.json +68 -0
  70. package/third-party/npm/@radix-ui__react-menu@2.1.16/LICENSE +21 -0
  71. package/third-party/npm/@radix-ui__react-menu@2.1.16/package.json +86 -0
  72. package/third-party/npm/@radix-ui__react-menubar@1.1.16/LICENSE +21 -0
  73. package/third-party/npm/@radix-ui__react-menubar@1.1.16/package.json +78 -0
  74. package/third-party/npm/@radix-ui__react-navigation-menu@1.2.14/LICENSE +21 -0
  75. package/third-party/npm/@radix-ui__react-navigation-menu@1.2.14/package.json +81 -0
  76. package/third-party/npm/@radix-ui__react-one-time-password-field@0.1.8/LICENSE +21 -0
  77. package/third-party/npm/@radix-ui__react-one-time-password-field@0.1.8/package.json +80 -0
  78. package/third-party/npm/@radix-ui__react-password-toggle-field@0.1.3/LICENSE +21 -0
  79. package/third-party/npm/@radix-ui__react-password-toggle-field@0.1.3/package.json +76 -0
  80. package/third-party/npm/@radix-ui__react-popover@1.1.15/LICENSE +21 -0
  81. package/third-party/npm/@radix-ui__react-popover@1.1.15/package.json +82 -0
  82. package/third-party/npm/@radix-ui__react-popper@1.2.8/LICENSE +21 -0
  83. package/third-party/npm/@radix-ui__react-popper@1.2.8/package.json +77 -0
  84. package/third-party/npm/@radix-ui__react-portal@1.1.9/LICENSE +21 -0
  85. package/third-party/npm/@radix-ui__react-portal@1.1.9/package.json +69 -0
  86. package/third-party/npm/@radix-ui__react-presence@1.1.5/LICENSE +21 -0
  87. package/third-party/npm/@radix-ui__react-presence@1.1.5/package.json +69 -0
  88. package/third-party/npm/@radix-ui__react-primitive@2.1.3/LICENSE +21 -0
  89. package/third-party/npm/@radix-ui__react-primitive@2.1.3/package.json +68 -0
  90. package/third-party/npm/@radix-ui__react-progress@1.1.7/LICENSE +21 -0
  91. package/third-party/npm/@radix-ui__react-progress@1.1.7/package.json +69 -0
  92. package/third-party/npm/@radix-ui__react-radio-group@1.3.8/LICENSE +21 -0
  93. package/third-party/npm/@radix-ui__react-radio-group@1.3.8/package.json +77 -0
  94. package/third-party/npm/@radix-ui__react-roving-focus@1.1.11/LICENSE +21 -0
  95. package/third-party/npm/@radix-ui__react-roving-focus@1.1.11/package.json +76 -0
  96. package/third-party/npm/@radix-ui__react-scroll-area@1.2.10/LICENSE +21 -0
  97. package/third-party/npm/@radix-ui__react-scroll-area@1.2.10/package.json +76 -0
  98. package/third-party/npm/@radix-ui__react-select@2.2.6/LICENSE +21 -0
  99. package/third-party/npm/@radix-ui__react-select@2.2.6/package.json +89 -0
  100. package/third-party/npm/@radix-ui__react-separator@1.1.7/LICENSE +21 -0
  101. package/third-party/npm/@radix-ui__react-separator@1.1.7/package.json +68 -0
  102. package/third-party/npm/@radix-ui__react-slider@1.3.6/LICENSE +21 -0
  103. package/third-party/npm/@radix-ui__react-slider@1.3.6/package.json +78 -0
  104. package/third-party/npm/@radix-ui__react-slot@1.2.3/LICENSE +21 -0
  105. package/third-party/npm/@radix-ui__react-slot@1.2.3/package.json +63 -0
  106. package/third-party/npm/@radix-ui__react-switch@1.2.6/LICENSE +21 -0
  107. package/third-party/npm/@radix-ui__react-switch@1.2.6/package.json +74 -0
  108. package/third-party/npm/@radix-ui__react-tabs@1.1.13/LICENSE +21 -0
  109. package/third-party/npm/@radix-ui__react-tabs@1.1.13/package.json +75 -0
  110. package/third-party/npm/@radix-ui__react-toast@1.2.15/LICENSE +21 -0
  111. package/third-party/npm/@radix-ui__react-toast@1.2.15/package.json +79 -0
  112. package/third-party/npm/@radix-ui__react-toggle-group@1.1.11/LICENSE +21 -0
  113. package/third-party/npm/@radix-ui__react-toggle-group@1.1.11/package.json +74 -0
  114. package/third-party/npm/@radix-ui__react-toggle@1.1.10/LICENSE +21 -0
  115. package/third-party/npm/@radix-ui__react-toggle@1.1.10/package.json +70 -0
  116. package/third-party/npm/@radix-ui__react-toolbar@1.1.11/LICENSE +21 -0
  117. package/third-party/npm/@radix-ui__react-toolbar@1.1.11/package.json +74 -0
  118. package/third-party/npm/@radix-ui__react-tooltip@1.2.8/LICENSE +21 -0
  119. package/third-party/npm/@radix-ui__react-tooltip@1.2.8/package.json +79 -0
  120. package/third-party/npm/@radix-ui__react-use-callback-ref@1.1.1/package.json +75 -0
  121. package/third-party/npm/@radix-ui__react-use-callback-ref@1.1.1/radix-ui-primitives-ee4e3197bffe-LICENSE +21 -0
  122. package/third-party/npm/@radix-ui__react-use-controllable-state@1.2.2/LICENSE +21 -0
  123. package/third-party/npm/@radix-ui__react-use-controllable-state@1.2.2/package.json +64 -0
  124. package/third-party/npm/@radix-ui__react-use-effect-event@0.0.2/LICENSE +21 -0
  125. package/third-party/npm/@radix-ui__react-use-effect-event@0.0.2/package.json +65 -0
  126. package/third-party/npm/@radix-ui__react-use-escape-keydown@1.1.1/package.json +78 -0
  127. package/third-party/npm/@radix-ui__react-use-escape-keydown@1.1.1/radix-ui-primitives-ee4e3197bffe-LICENSE +21 -0
  128. package/third-party/npm/@radix-ui__react-use-is-hydrated@0.1.0/LICENSE +21 -0
  129. package/third-party/npm/@radix-ui__react-use-is-hydrated@0.1.0/package.json +65 -0
  130. package/third-party/npm/@radix-ui__react-use-layout-effect@1.1.1/package.json +75 -0
  131. package/third-party/npm/@radix-ui__react-use-layout-effect@1.1.1/radix-ui-primitives-ee4e3197bffe-LICENSE +21 -0
  132. package/third-party/npm/@radix-ui__react-use-previous@1.1.1/package.json +75 -0
  133. package/third-party/npm/@radix-ui__react-use-previous@1.1.1/radix-ui-primitives-ee4e3197bffe-LICENSE +21 -0
  134. package/third-party/npm/@radix-ui__react-use-size@1.1.1/package.json +79 -0
  135. package/third-party/npm/@radix-ui__react-use-size@1.1.1/radix-ui-primitives-ee4e3197bffe-LICENSE +21 -0
  136. package/third-party/npm/@radix-ui__react-visually-hidden@1.2.3/LICENSE +21 -0
  137. package/third-party/npm/@radix-ui__react-visually-hidden@1.2.3/package.json +68 -0
  138. package/third-party/npm/color-convert@3.1.3/LICENSE +22 -0
  139. package/third-party/npm/color-convert@3.1.3/package.json +58 -0
  140. package/third-party/npm/color-name@2.1.0/LICENSE +8 -0
  141. package/third-party/npm/color-name@2.1.0/package.json +32 -0
  142. package/third-party/npm/color-string@2.1.4/LICENSE +21 -0
  143. package/third-party/npm/color-string@2.1.4/package.json +47 -0
  144. package/third-party/npm/color@5.0.3/LICENSE +21 -0
  145. package/third-party/npm/color@5.0.3/package.json +52 -0
  146. package/third-party/npm/radix-ui@1.4.3/LICENSE +21 -0
  147. package/third-party/npm/radix-ui@1.4.3/package.json +136 -0
  148. package/third-party/npm/use-sync-external-store@1.6.0/LICENSE +21 -0
  149. package/third-party/npm/use-sync-external-store@1.6.0/package.json +43 -0
  150. package/vendor/osmo-lite +0 -0
  151. package/vendor/osmo-scene-0.1.0.tgz +0 -0
  152. package/vendor/platform.json +1 -1
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.19",
2
+ "version": "0.1.20",
3
3
  "type": "module",
4
4
  "files": [
5
5
  "dist",
package/release.json CHANGED
@@ -1,36 +1,36 @@
1
1
  {
2
- "version": "0.1.19",
2
+ "version": "0.1.20",
3
3
  "package": "@osmo.inc/cli-linux-arm64",
4
4
  "platform": "linux-arm64",
5
5
  "channel": "latest",
6
- "sourceCommit": "8256a0d29c67b090c37504207a379b0924c735e7",
6
+ "sourceCommit": "d884ae69ee0ce8f9171efbfcef5c5f9feb43c7e6",
7
7
  "sourceDirty": false,
8
8
  "signing": "unsigned",
9
9
  "licenseFileIncluded": false,
10
- "dependencyCount": 232,
10
+ "dependencyCount": 297,
11
11
  "nativeDefaultFeatures": false,
12
12
  "contents": {
13
- "totalBytes": 53140218,
14
- "fileCount": 947,
13
+ "totalBytes": 53814153,
14
+ "fileCount": 1079,
15
15
  "groups": {
16
- "vendor": 18350716,
17
- "studio": 17122088,
16
+ "vendor": 18416740,
17
+ "studio": 17474729,
18
18
  "node_modules/typescript": 9913238,
19
- "third-party": 4722136,
19
+ "third-party": 4936088,
20
20
  "src": 2622428,
21
- "dist": 269721,
22
- "runtime": 137987,
21
+ "dist": 293259,
22
+ "runtime": 155767,
23
23
  "THIRD_PARTY_NOTICES.md": 1090,
24
24
  "package.json": 814
25
25
  },
26
26
  "largestFiles": [
27
27
  {
28
28
  "path": "vendor/osmo-lite",
29
- "bytes": 18314952
29
+ "bytes": 18376392
30
30
  },
31
31
  {
32
32
  "path": "studio/src/engine_bg.wasm",
33
- "bytes": 12010921
33
+ "bytes": 12038924
34
34
  },
35
35
  {
36
36
  "path": "node_modules/typescript/lib/typescript.js",
@@ -38,7 +38,7 @@
38
38
  },
39
39
  {
40
40
  "path": "studio/src/app.js",
41
- "bytes": 4767135
41
+ "bytes": 5055327
42
42
  },
43
43
  {
44
44
  "path": "node_modules/typescript/lib/lib.dom.d.ts",
@@ -65,12 +65,12 @@
65
65
  "bytes": 370430
66
66
  },
67
67
  {
68
- "path": "third-party/rust-standard-library/COPYRIGHT-library.html",
69
- "bytes": 279302
68
+ "path": "dist/studio.js",
69
+ "bytes": 293259
70
70
  },
71
71
  {
72
- "path": "dist/studio.js",
73
- "bytes": 269721
72
+ "path": "third-party/rust-standard-library/COPYRIGHT-library.html",
73
+ "bytes": 279302
74
74
  },
75
75
  {
76
76
  "path": "node_modules/typescript/lib/lib.es5.d.ts",
@@ -80,10 +80,18 @@
80
80
  "path": "src/starter/animations/osmo/shape34-0.tsx",
81
81
  "bytes": 139902
82
82
  },
83
+ {
84
+ "path": "studio/src/app.css",
85
+ "bytes": 131513
86
+ },
83
87
  {
84
88
  "path": "third-party/npm/mediabunny@1.52.2/source/src/isobmff/isobmff-demuxer.ts",
85
89
  "bytes": 118682
86
90
  },
91
+ {
92
+ "path": "third-party/inventory.json",
93
+ "bytes": 115089
94
+ },
87
95
  {
88
96
  "path": "src/starter/animations/osmo/shape8-83.tsx",
89
97
  "bytes": 112489
@@ -95,27 +103,19 @@
95
103
  {
96
104
  "path": "third-party/npm/mediabunny@1.52.2/source/src/media-source.ts",
97
105
  "bytes": 97606
98
- },
99
- {
100
- "path": "studio/src/app.css",
101
- "bytes": 95067
102
- },
103
- {
104
- "path": "third-party/npm/mediabunny@1.52.2/source/src/codec-data.ts",
105
- "bytes": 92683
106
106
  }
107
107
  ]
108
108
  },
109
109
  "hashes": {
110
- "dist/studio.js": "be67ed4c321a22a87a446d893f89b4bd342f30290bf519a85c1aea784b612acd",
111
- "vendor/osmo-lite": "4a81a51e3c3121597e64c3b2d9b06d72d471fd993a79af61086cebcb70fe3bf8",
112
- "studio/src/engine_bg.wasm": "889927bd262b27f34e2495a90810fae3295adcfda80c7fda7b2dbd7d13eb8ee1",
113
- "studio/src/app.js": "ca23cc9220cb68e3d23eb3c771c80b1221a971c1c1ace207ebf2acc4e1f24272",
114
- "studio/src/app.css": "aa53a4e2206280c169981d9940f042539e70b2c665895a3e041830c6f41d8ffe",
115
- "third-party/inventory.json": "fe5ca3c8784ad803bf5ecdc227071301115eec6ea2d220fe96aa2024f59902e6"
110
+ "dist/studio.js": "ae0b993f2af7d91678570c9b64b99b62f6c581982b56f2d7305e56516424ed2a",
111
+ "vendor/osmo-lite": "e2cfb28ea87859434bb19a6c381b7dba0767d2961c38182c21174f0b5011a35d",
112
+ "studio/src/engine_bg.wasm": "1a2f493e6ec26f178658c7d3f251b4f6e6ba62104da7735a346058754687eb2a",
113
+ "studio/src/app.js": "44a2284edc365d197450e286aaeda4dfcfa7e6f97385d7f0c98d7c32da9816df",
114
+ "studio/src/app.css": "1f57360a850bbbf2b51ebb0920c5b54c649b8e5d3bea7a6537b67befaa233906",
115
+ "third-party/inventory.json": "d0813172f555bb9547e7a23b1b23197b8264f1cd22e1fd75a73fa0ad73e847e0"
116
116
  },
117
117
  "lockfiles": {
118
- "package-lock.json": "a7fb6b84762aeb4f3ce90166df48f53c2853d2684282ed1c019502f840602e28",
118
+ "package-lock.json": "e762e69bcb0bc3d88b7dc7b8ff81736790e2e05e992f42230378e5ef8373dc68",
119
119
  "apps/native/Cargo.lock": "cf31d67cf167c5f32438abfeb42159eb50da61d36e4e482bae50ed3d71241373",
120
120
  "packages/render-engine/Cargo.lock": "a1a68051e1eea30ef0fa46bf17cd0685bb2c6a327defed0405e0d63dd61017a7"
121
121
  }
@@ -33,14 +33,15 @@ const result = await build({
33
33
  const overridden = Object.hasOwn(sources, file);
34
34
  const text = overridden ? sources[file] : await readFile(file, "utf8");
35
35
  const loader = /tsx$/.test(file) ? "tsx" : /jsx$/.test(file) ? "jsx" : /[cm]?ts$/.test(file) ? "ts" : "js";
36
- if (!text.includes("defineEffect")) return overridden ? {contents:text,loader} : undefined;
36
+ if (!text.includes("defineEffect") && !text.includes("defineKnobs")) return overridden ? {contents:text,loader} : undefined;
37
37
  const ast = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
38
- const names = new Set(), namespaces = new Set();
38
+ const names = new Set(), knobNames = new Set(), namespaces = new Set();
39
39
  for (const statement of ast.statements) {
40
40
  if (!ts.isImportDeclaration(statement) || statement.moduleSpecifier.text !== "@osmo/scene") continue;
41
41
  const imports = statement.importClause?.namedBindings;
42
42
  if (imports && ts.isNamedImports(imports)) for (const item of imports.elements) {
43
43
  if ((item.propertyName ?? item.name).text === "defineEffect") names.add(item.name.text);
44
+ if ((item.propertyName ?? item.name).text === "defineKnobs") knobNames.add(item.name.text);
44
45
  }
45
46
  if (imports && ts.isNamespaceImport(imports)) namespaces.add(imports.name.text);
46
47
  }
@@ -51,6 +52,12 @@ const result = await build({
51
52
  ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && namespaces.has(node.expression.expression.text) && node.expression.name.text === "defineEffect")) {
52
53
  return ts.factory.updateCallExpression(node, node.expression, node.typeArguments, [ts.visitNode(node.arguments[0], visit), ts.factory.createStringLiteral(file)]);
53
54
  }
55
+ if (ts.isCallExpression(node) && node.arguments.length <= 2 &&
56
+ (ts.isIdentifier(node.expression) && knobNames.has(node.expression.text) ||
57
+ ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && namespaces.has(node.expression.expression.text) && node.expression.name.text === "defineKnobs")) {
58
+ return ts.factory.updateCallExpression(node, node.expression, node.typeArguments,
59
+ [node.arguments[0], node.arguments[1] ?? ts.factory.createObjectLiteralExpression(), ts.factory.createStringLiteral(file)]);
60
+ }
54
61
  return ts.visitEachChild(node, visit, context);
55
62
  };
56
63
  return ts.visitNode(root, visit);
@@ -128,6 +135,7 @@ async function collectEffects(target) {
128
135
  async function collectFonts(nodes) {
129
136
  for (const node of nodes) {
130
137
  await collectEffects(node);
138
+ for (const state of node.animate?.states ?? []) await collectEffects(state);
131
139
  if(node.type === "composition") { await collectEffects(node.scene); await collectFonts(node.scene.children); }
132
140
  if (["group","model","surface"].includes(node.type)) await collectFonts(node.children);
133
141
  if (["image", "video", "audio", "model"].includes(node.type)) {
@@ -0,0 +1,27 @@
1
+ /** Local asset contracts shared by authoring, evaluation and the editor. */
2
+ export type AssetKind = "image" | "video" | "audio" | "font" | "model";
3
+ /** Plain paths remain valid; typed knob references retain their asset kind. */
4
+ export type AssetPath<K extends AssetKind> = string & {
5
+ readonly __assetKind?: K;
6
+ };
7
+ export const assetExtensions: Record<AssetKind, readonly string[]> = {
8
+ image: ["png", "jpg", "jpeg", "webp", "gif", "svg"],
9
+ video: ["mp4", "webm", "mov", "m4v"],
10
+ audio: ["wav", "mp3", "m4a", "aac", "ogg", "flac"],
11
+ font: ["ttf", "otf"],
12
+ model: ["glb"],
13
+ };
14
+ export function validAssetPath(
15
+ kind: AssetKind,
16
+ value: unknown,
17
+ ): value is string {
18
+ return (
19
+ Object.hasOwn(assetExtensions, kind) &&
20
+ typeof value === "string" &&
21
+ value.length > 0 &&
22
+ value.length <= 4096 &&
23
+ value === value.trim() &&
24
+ !/^[a-z][a-z0-9+.-]*:|^[/\\]|[\x00-\x1f?#\\]/i.test(value) &&
25
+ assetExtensions[kind].includes(value.split(".").at(-1)!.toLowerCase())
26
+ );
27
+ }
@@ -1,3 +1,5 @@
1
+ import type { AssetPath } from "./assets.js";
2
+ import type { TextAnimation } from "./text-animation.js";
1
3
  import type { EffectInstance } from "./effects.js";
2
4
  import type { KnobRef, KnobSet, KnobBindings } from "./knobs.js";
3
5
  import type { Knobs } from "./knob-schema.js";
@@ -13,7 +15,7 @@ export type Input<T> = KnobRef<T> | (T extends number
13
15
  ? T | Motion<T>
14
16
  : T extends readonly number[]
15
17
  ? T | Motion<T> | { readonly [K in keyof T]: Input<T[K]> }
16
- : T extends object
18
+ : T extends string ? T : T extends object
17
19
  ? { readonly [K in keyof T]: Input<T[K]> }
18
20
  : T);
19
21
  export type InputProps<P> = {
@@ -69,14 +71,14 @@ export interface TransformProps extends ClipProps {
69
71
  z?: number;
70
72
  /** Clockwise degrees around origin. Default: 0. */
71
73
  rotation?: number;
72
- /** Uniform factor or [x, y] factors. Default: 1. */
73
- scale?: number | Vec2;
74
+ /** Uniform factor applied to both axes. Default: 1. */
75
+ scale?: number;
74
76
  /** Local pivot in pixels, not a fraction. Default: [0, 0]. */
75
77
  origin?: Vec2;
76
78
  /** 0–1. Group opacity composites its children together. Default: 1. */
77
79
  opacity?: number;
78
80
  }
79
- export interface SceneProps {
81
+ export interface SceneStyleProps {
80
82
  /** Optional lane metadata keyed by track number (0–99). Labels are static, one word maximum. Does not change paint order. */
81
83
  tracks?: Readonly<Record<number, TrackMetadata>>;
82
84
  /** Filter the complete viewport, including 3D surfaces; array order matters. */
@@ -90,9 +92,14 @@ export interface SceneProps {
90
92
  /** Positive frames per second; static. Default: 30. */
91
93
  fps?: number;
92
94
  /** Fixed viewport backdrop, independent of camera and layout. Default: transparent. */
93
- background?: Color;
95
+ background?: Input<Color>;
94
96
  children?: Child;
95
97
  }
98
+ /** Scene controls follow the same ownership and instance rules as Groups. */
99
+ export type SceneProps<K extends Knobs = Knobs> = SceneStyleProps & (
100
+ | { knobs: KnobSet<K>; values?: KnobBindings<NoInfer<K>> }
101
+ | { knobs?: never; values?: never }
102
+ );
96
103
  /** Timeline lane metadata; independent of the clips occupying the lane. */
97
104
  export interface TrackMetadata {
98
105
  /** Optional static display name, one word maximum, e.g. "Titles". */
@@ -179,12 +186,16 @@ export type SceneInstanceProps = InputProps<TransformProps & {
179
186
  width?: number;
180
187
  /** Viewport height in pixels; defaults to the source scene height. Does not scale content. */
181
188
  height?: number;
182
- }> & { label?: string; hidden?: boolean; solo?: boolean; trimIn?: number };
189
+ }> & {
190
+ label?: string; hidden?: boolean; solo?: boolean; trimIn?: number;
191
+ /** Overrides for the controls declared by the source Scene. */
192
+ values?: Readonly<Record<string, MotionValue | Motion | KnobRef>>;
193
+ };
183
194
  /** JSX attribute contract for imported static Group documents (legacy) and Scenes. */
184
- export type GroupInstance = (props: GroupInstanceProps) => Element;
195
+ export type GroupInstance = (props: GroupInstanceProps | SceneInstanceProps) => Element;
185
196
  export interface ImageProps extends TransformProps {
186
197
  /** Local PNG/JPEG/WebP/SVG path, relative to this TSX file; no remote URLs. */
187
- src: string;
198
+ src: AssetPath<"image">;
188
199
  /** Positive box width in pixels. */
189
200
  width: number;
190
201
  /** Positive box height in pixels. */
@@ -214,7 +225,7 @@ export interface ShaderProps extends TransformProps {
214
225
  export type ModelSelector = string | number;
215
226
  export interface ModelProps extends ClipProps {
216
227
  /** Local .glb path. Uses the default GLB scene; inspect node/material/clip names before targeting them. */
217
- src: string;
228
+ src: AssetPath<"model">;
218
229
  /** Composition [x, y] in pixels. Default: scene center. */
219
230
  position?: Vec2;
220
231
  /** Depth in pixels; positive toward the viewer. Default: 0. */
@@ -269,10 +280,10 @@ export interface MediaTiming extends ClipProps {
269
280
  /** Gain from 0–1; animatable in JSX. Use 0 to mute video audio. Default: 1. */
270
281
  volume?: number;
271
282
  }
272
- export interface VideoProps extends ImageProps, MediaTiming {}
283
+ export interface VideoProps extends Omit<ImageProps, "src">, MediaTiming { src: AssetPath<"video"> }
273
284
  export interface AudioProps extends MediaTiming {
274
285
  /** Local audio file path, relative to this TSX file. */
275
- src: string;
286
+ src: AssetPath<"audio">;
276
287
  }
277
288
 
278
289
  /** SVG-style stroke painted along the contour, centered on its geometry. */
@@ -338,7 +349,7 @@ export interface TextStyleProps extends TransformProps {
338
349
  /** Whole string or number; the renderer handles shaping, kerning and glyph placement. */
339
350
  children: string | number;
340
351
  /** Bundled Inter or a local .ttf/.otf path relative to this TSX file. Default: Inter. */
341
- font?: string;
352
+ font?: AssetPath<"font">;
342
353
  /** Positive size in pixels. Default: 48. */
343
354
  fontSize?: number;
344
355
  /** Requested font weight; omitted uses the selected face’s default. */
@@ -351,6 +362,8 @@ export interface TextStyleProps extends TransformProps {
351
362
  export type TextProps = TextStyleProps & (
352
363
  | {
353
364
  path?: never;
365
+ /** Staggered post-layout character, word or visual-line animation. */
366
+ animate?: TextAnimation;
354
367
  /** Positive wrapping width in pixels; omitted uses natural text width. */
355
368
  width?: number;
356
369
  /** Positive text-box height. Overflow remains visible; use a clipping Group to crop. */
@@ -367,6 +380,7 @@ export type TextProps = TextStyleProps & (
367
380
  | {
368
381
  /** Move a whole string along a curve by animating progress. No width, align, lineHeight or line breaks. */
369
382
  path: TextPath;
383
+ animate?: never;
370
384
  width?: never;
371
385
  height?: never;
372
386
  verticalAlign?: never;
@@ -402,6 +416,8 @@ export interface Element {
402
416
  readonly props: Readonly<Record<string, unknown>>;
403
417
  readonly key?: string | number;
404
418
  readonly sourceFile?: string;
419
+ /** File supplying each literal instance override. */
420
+ readonly valueSources?: Readonly<Record<string, string>>;
405
421
  }
406
422
  export function element(
407
423
  type: Element["type"],
@@ -418,7 +434,7 @@ export function element(
418
434
  /** Root composition with fixed dimensions, duration and FPS.
419
435
  * @example <Scene width={1080} height={1080} duration={4} fps={30} background="#141414"><Text track={0}>Hello</Text></Scene> */
420
436
  export const Scene = Object.assign(
421
- (props: SceneProps): Element => element("scene", props),
437
+ <const K extends Knobs = Knobs>(props: SceneProps<K>): Element => element("scene", props),
422
438
  { sceneRoot: true as const },
423
439
  );
424
440
  /** Layout, transform and style children together; also draws boxes, ellipses and thin lines.
@@ -1,7 +1,8 @@
1
+ import { evaluateTextAnimation, type EvaluatedTextAnimation } from "./text-animation.js";
1
2
  import { readMask, maskSourceIndex } from "./mask.mjs";
2
3
  import { clipFields, clipTiming, isClipPrimitive } from "./clip.mjs";
3
4
  import { readGroupLabel, readGroupPlayback, readSceneTracks, trackPlayback } from "./metadata.mjs";
4
- import { isKnobRef, isKnobSet, resolveKnob, evaluateKnobs, type KnobScope, type KnobDescription } from "./knobs.js";
5
+ import { isKnobRef, isKnobSet, resolveKnob, resolveKnobAsset, evaluateKnobs, type KnobScope, type KnobDescription } from "./knobs.js";
5
6
  import { evaluateEffects, type EvaluatedEffect } from "./effects.js";
6
7
  import {
7
8
  Scene, Model, Surface,
@@ -40,6 +41,7 @@ interface BaseNode {
40
41
  key?: string | number;
41
42
  }
42
43
  export interface CompositionNode extends BaseNode {
44
+ controls?: KnobDescription;
43
45
  type: "composition";
44
46
  width: number;
45
47
  height: number;
@@ -86,6 +88,7 @@ export interface PathNode extends BaseNode {
86
88
  stroke?: { color: Color; width: number; cap: "butt" | "round" | "square"; join: "miter" | "round" | "bevel"; miterLimit: number; trimStart: number; trimEnd: number };
87
89
  }
88
90
  export interface TextNode extends BaseNode {
91
+ animate?: EvaluatedTextAnimation;
89
92
  height?: number;
90
93
  verticalAlign?: "top" | "center" | "bottom";
91
94
  alignBounds?: "lineBox" | "glyphs";
@@ -145,6 +148,7 @@ export interface EvaluatedCamera {
145
148
  fov: number;
146
149
  }
147
150
  export interface EvaluatedScene extends FrameContext {
151
+ controls?: KnobDescription;
148
152
  effects?: EvaluatedEffect[];
149
153
  camera?: EvaluatedCamera;
150
154
  type: "scene";
@@ -179,17 +183,14 @@ function color(value: unknown, label: string): Color {
179
183
  return value as Color;
180
184
  }
181
185
  function transform(props: Readonly<Record<string, unknown>>): Transform {
182
- const scale = props.scale ?? 1;
186
+ const scale = number(props.scale ?? 1, "scale (one uniform number)");
183
187
  const opacity = number(props.opacity ?? 1, "opacity", 0);
184
188
  if (opacity > 1) throw new Error("opacity: must be <= 1");
185
189
  return {
186
190
  position: pair(props.position ?? [0, 0], "position"),
187
191
  z: number(props.z ?? 0, "z"),
188
192
  rotation: number(props.rotation ?? 0, "rotation"),
189
- scale:
190
- typeof scale === "number"
191
- ? [number(scale, "scale"), scale]
192
- : pair(scale, "scale"),
193
+ scale: [scale, scale],
193
194
  origin: pair(props.origin ?? [0, 0], "origin"),
194
195
  opacity,
195
196
  };
@@ -223,7 +224,13 @@ function isElement(value: unknown): value is Element {
223
224
  export function evaluateFrame(root: Element, frame: number): EvaluatedScene {
224
225
  return evaluateScene(root, frame);
225
226
  }
226
- function evaluateScene(root: Element, frame: number, clock?: Partial<ClockContext>, depth = 0): EvaluatedScene {
227
+ interface SceneBindings {
228
+ values?: Record<string, unknown>;
229
+ scope: KnobScope;
230
+ time: number;
231
+ sourceFile?: string;
232
+ }
233
+ function evaluateScene(root: Element, frame: number, clock?: Partial<ClockContext>, depth = 0, instance?: SceneBindings): EvaluatedScene {
227
234
  if (depth > 32) throw new Error("Scene nesting exceeds 32 levels");
228
235
  if (!isElement(root) || (root.type !== Scene && root.type !== "scene")) {
229
236
  throw new Error(
@@ -234,7 +241,7 @@ function evaluateScene(root: Element, frame: number, clock?: Partial<ClockContex
234
241
  checkProps(
235
242
  p,
236
243
  "Scene",
237
- ["width", "height", "duration", "fps", "background", "children", "effects", "tracks"],
244
+ ["width", "height", "duration", "fps", "background", "children", "effects", "tracks", "knobs", "values"],
238
245
  false,
239
246
  );
240
247
  const tracks = readSceneTracks(p.tracks);
@@ -258,6 +265,24 @@ function evaluateScene(root: Element, frame: number, clock?: Partial<ClockContex
258
265
  duration,
259
266
  ...clock,
260
267
  });
268
+ for (const values of [p.values, instance?.values])
269
+ if (values !== undefined && (!values || typeof values !== "object" || Array.isArray(values)))
270
+ throw new Error("Scene.values must be named knob bindings");
271
+ let scope: KnobScope = [];
272
+ let controls: KnobDescription | undefined;
273
+ if (p.knobs !== undefined) {
274
+ const overrides = instance?.values ?? {};
275
+ controls = evaluateKnobs(p.knobs as import("./knobs.js").KnobSet,
276
+ {...(p.values as object ?? {}), ...overrides}, context.time, instance?.scope ?? [], {
277
+ sourceFile: root.sourceFile,
278
+ valueSources: {...root.valueSources, ...Object.fromEntries(Object.keys(overrides)
279
+ .filter(() => instance?.sourceFile).map(k => [k, instance!.sourceFile!]))},
280
+ bindingTimes: Object.fromEntries(Object.keys(overrides).map(k => [k, instance!.time])),
281
+ });
282
+ scope = [{set: p.knobs as import("./knobs.js").KnobSet, description: controls}];
283
+ } else if (p.values !== undefined || instance?.values !== undefined) {
284
+ throw new Error("Scene.values requires Scene.knobs");
285
+ }
261
286
  let camera: EvaluatedCamera | undefined;
262
287
  const children: unknown[] = [];
263
288
  function collect(child: unknown, depth = 0): void {
@@ -269,14 +294,14 @@ function evaluateScene(root: Element, frame: number, clock?: Partial<ClockContex
269
294
  ) {
270
295
  if (camera) throw new Error("Scene accepts only one Camera");
271
296
  camera = evaluateCamera(
272
- resolve(child.props, context.time) as Record<string, unknown>, context,
297
+ resolve(child.props, context.time, scope) as Record<string, unknown>, context,
273
298
  );
274
299
  } else {
275
300
  children.push(child);
276
301
  }
277
302
  }
278
303
  collect(p.children);
279
- const evaluatedChildren = isolateGroups(visit(children, context, depth));
304
+ const evaluatedChildren = isolateGroups(visit(children, context, depth, scope));
280
305
  function checkPlacement(nodes: SceneNode[], parent: string) {
281
306
  for (const n of nodes) {
282
307
  if (n.track !== undefined && parent !== "scene") throw new Error(`${n.type}.track is only supported on root timeline clips`);
@@ -297,8 +322,9 @@ function evaluateScene(root: Element, frame: number, clock?: Partial<ClockContex
297
322
  ...(camera ? { camera } : {}),
298
323
  ...context,
299
324
  totalFrames,
300
- background: color(p.background ?? "transparent", "Scene.background"),
301
- ...(p.effects === undefined ? {} : { effects: evaluateEffects(p.effects, context.time, root.sourceFile) }),
325
+ background: color(resolve(p.background ?? "transparent", context.time, scope), "Scene.background"),
326
+ ...(controls ? {controls} : {}),
327
+ ...(p.effects === undefined ? {} : { effects: evaluateEffects(p.effects, context.time, root.sourceFile, v => isKnobRef(v) ? resolveKnob(v, scope) : v) }),
302
328
  children: evaluatedChildren,
303
329
  };
304
330
  }
@@ -404,7 +430,7 @@ function visit(
404
430
  const source = child.props.scene as Element;
405
431
  const timing = clipTiming(child.props, context.duration, context.fps, 'Scene instance');
406
432
  const localTime = Math.max(0,Math.min(timing.duration-1e-9,context.time-timing.start));
407
- const p = Object.fromEntries(Object.entries(child.props).filter(([k])=>k!=='scene').map(([k,v])=>[k,k==='effects'?v:resolve(v,localTime,scope)]));
433
+ const p = Object.fromEntries(Object.entries(child.props).filter(([k])=>k!=='scene').map(([k,v])=>[k,k==='effects'||k==='values'?v:resolve(v,localTime,scope)]));
408
434
  const trim = number(p.trimIn ?? 0, 'Scene instance.trimIn', 0);
409
435
  const sourceDuration = positive(source.props.duration, 'Scene.duration');
410
436
  if (trim >= sourceDuration) throw new Error('Scene instance.trimIn must be before the source end');
@@ -422,13 +448,14 @@ function visit(
422
448
  return Math.round(value);
423
449
  };
424
450
  const viewport = {...source,props:{...source.props,width:dimension('width'),height:dimension('height')}};
425
- const scene = evaluateScene(viewport, 0, {time, frame:time*context.fps, fps:context.fps, offset, windowStart:start, windowEnd:end}, depth+1);
451
+ const scene = evaluateScene(viewport, 0, {time, frame:time*context.fps, fps:context.fps, offset, windowStart:start, windowEnd:end}, depth+1, {values:p.values as Record<string,unknown> | undefined, scope, time:localTime, sourceFile:child.sourceFile});
426
452
  const active = context.time >= timing.start && context.time < timing.start + Math.min(timing.duration, sourceDuration-trim);
427
453
  readGroupLabel(p.label); readGroupPlayback(p);
428
454
  const node: CompositionNode = {type:'composition',width:scene.width,height:scene.height,scene,
455
+ ...(scene.controls ? {controls:scene.controls} : {}),
429
456
  ...(timing.track === undefined ? {} : {track:timing.track}),
430
457
  transform:transform({...p,opacity:active && !p.hidden ? p.opacity : 0}),
431
- ...(p.effects === undefined ? {} : {effects:evaluateEffects(p.effects,localTime,child.sourceFile)})};
458
+ ...(p.effects === undefined ? {} : {effects:evaluateEffects(p.effects,localTime,child.sourceFile,v => isKnobRef(v) ? resolveKnob(v,scope) : v)})};
432
459
  if (p.solo) soloGroups.add(node);
433
460
  return [node];
434
461
  }
@@ -452,10 +479,18 @@ function visit(
452
479
  readGroupPlayback(child.props);
453
480
  if (child.props.knobs !== undefined) {
454
481
  if (!isKnobSet(child.props.knobs)) throw new Error("Group.knobs must be a defineKnobs declaration");
455
- controls = evaluateKnobs(child.props.knobs, child.props.values, context.time, scope);
482
+ controls = evaluateKnobs(child.props.knobs, child.props.values, context.time, scope, {sourceFile:child.sourceFile, valueSources:child.valueSources});
456
483
  scope = [...scope, { set: child.props.knobs, description: controls }];
457
484
  } else if (child.props.values !== undefined) throw new Error("Group.values requires Group.knobs");
458
485
  }
486
+ // Asset controls carry their authoring file through all forwarding scopes.
487
+ const assetSource = (prop: "src" | "font") => {
488
+ const ref = child.props[prop];
489
+ const asset = isKnobRef(ref) ? resolveKnobAsset(ref, scope) : undefined;
490
+ if (asset && asset.kind !== (prop === "font" ? "font" : type))
491
+ throw new Error(`${type}.${prop}: asset knob ${asset.name} has incompatible kind ${asset.kind}`);
492
+ return asset?.sourceFile ?? child.sourceFile;
493
+ };
459
494
  const props = Object.fromEntries(Object.entries(child.props).map(([name, value]) => [name,
460
495
  (name === "children" && !isKnobRef(value)) || name === "effects" || (type === "group" && (name === "knobs" || name === "values"))
461
496
  ? value : resolve(value, context.time, scope),
@@ -500,7 +535,7 @@ function visit(
500
535
  }
501
536
  const children=visit(props.children,context,depth+1,scope);
502
537
  if(children.some(n=>n.type!=="surface"))throw new Error("Model accepts Surface children only");
503
- return [{type:"model",src:props.src,...(child.sourceFile?{sourceFile:child.sourceFile}:{}),
538
+ return [{type:"model",src:props.src,...(assetSource("src")?{sourceFile:assetSource("src")}:{}),
504
539
  ...(child.key===undefined?{}:{key:child.key}),
505
540
  ...(timing?.track === undefined ? {} : {track: timing.track}),
506
541
  transform:transform({...props,opacity:active ? props.opacity : 0,position:props.position??[context.width/2,context.height/2],rotation:0,scale:positive(props.scale??1,"Model.scale")}),
@@ -683,7 +718,7 @@ function visit(
683
718
  Object.defineProperty(uniforms, name, { value, enumerable: true });
684
719
  }
685
720
  return [{ type: "shader", ...base, src: props.src, backdrop: props.backdrop ?? false,
686
- ...(child.sourceFile ? { sourceFile: child.sourceFile } : {}),
721
+ ...(assetSource("src") ? { sourceFile: assetSource("src") } : {}),
687
722
  width: positive(props.width, "Shader.width"), height: positive(props.height, "Shader.height"),
688
723
  rounding: number(props.rounding ?? 0, "Shader.rounding", 0), uniforms }];
689
724
  }
@@ -712,7 +747,7 @@ function visit(
712
747
  type,
713
748
  ...base,
714
749
  src: props.src,
715
- ...(child.sourceFile ? { sourceFile: child.sourceFile } : {}),
750
+ ...(assetSource("src") ? { sourceFile: assetSource("src") } : {}),
716
751
  };
717
752
  if (visual)
718
753
  Object.assign(node, {
@@ -805,7 +840,7 @@ function visit(
805
840
  ];
806
841
  }
807
842
  if (type === "text") {
808
- checkProps(props, "Text", ["height", "verticalAlign", "alignBounds",
843
+ checkProps(props, "Text", ["animate", "height", "verticalAlign", "alignBounds",
809
844
  "children",
810
845
  "font",
811
846
  "fontSize",
@@ -817,6 +852,7 @@ function visit(
817
852
  "letterSpacing",
818
853
  "path",
819
854
  ]);
855
+ if (props.animate !== undefined && props.path !== undefined) throw new Error("Text.animate cannot be combined with Text.path");
820
856
  let path: TextPath | undefined;
821
857
  if (props.path !== undefined) {
822
858
  if (!props.path || typeof props.path !== "object" || Array.isArray(props.path))
@@ -863,8 +899,9 @@ function visit(
863
899
  ...base, ...box,
864
900
  ...(path ? { path } : {}),
865
901
  text: String(props.children),
902
+ ...(props.animate === undefined ? {} : {animate: evaluateTextAnimation(props.animate, String(props.children), context.time, color(props.color ?? "#ffffff", "Text.color"))}),
866
903
  font,
867
- ...(child.sourceFile ? { fontSource: child.sourceFile } : {}),
904
+ ...(assetSource("font") ? { fontSource: assetSource("font") } : {}),
868
905
  ...(props.fontWeight === undefined
869
906
  ? {}
870
907
  : {
@@ -67,3 +67,7 @@ export type { EffectPadding, Knob, KnobGroup, Knobs, KnobValues, EffectDefinitio
67
67
 
68
68
  export { defineKnobs, describeKnobs } from "./knobs.js";
69
69
  export type { KnobRef, KnobSet, KnobBindings, KnobDescription } from "./knobs.js";
70
+
71
+ export type { TextAnimation, TextAnimationStyle } from "./text-animation.js";
72
+
73
+ export type { AssetKind, AssetPath } from "./assets.js";
@@ -2,7 +2,6 @@ import {
2
2
  element,
3
3
  type Element as SceneElement,
4
4
  type InputProps,
5
- type SceneProps,
6
5
  type Component,
7
6
  type GroupInstance,
8
7
  Scene,
@@ -18,9 +17,11 @@ export function jsx(
18
17
  if (typeof type === "object" && type !== null) {
19
18
  if (type.type === Scene || type.type === "scene") {
20
19
  const overrides = (props ?? {}) as Record<string, unknown>;
21
- const allowed = new Set(['id','label','track','start','duration','trimIn','width','height','position','z','rotation','scale','origin','opacity','effects','hidden','solo']);
20
+ const allowed = new Set(['id','label','track','start','duration','trimIn','width','height','position','z','rotation','scale','origin','opacity','effects','hidden','solo','values']);
22
21
  for (const name of Object.keys(overrides)) if (!allowed.has(name))
23
22
  throw new Error(`Scene instance.${name}: edit scene settings in the source file`);
23
+ if (overrides.values !== undefined && (!overrides.values || typeof overrides.values !== "object" || Array.isArray(overrides.values)))
24
+ throw new Error("Scene instance.values must be named knob bindings");
24
25
  return element("composition", {duration:type.props.duration, ...overrides, scene:type}, key);
25
26
  }
26
27
  if (type.type !== Group && type.type !== "group")
@@ -36,6 +37,7 @@ export function jsx(
36
37
  ...(values === undefined ? {} : { values: { ...(type.props.values as object ?? {}), ...values } }),
37
38
  }, key),
38
39
  ...(type.sourceFile ? { sourceFile: type.sourceFile } : {}),
40
+ ...(type.valueSources ? { valueSources: type.valueSources } : {}),
39
41
  });
40
42
  }
41
43
  return element(type, props ?? {}, key);
@@ -50,6 +52,10 @@ export function jsxDEV(
50
52
  ): SceneElement {
51
53
  return Object.freeze({
52
54
  ...jsx(type, props, key),
55
+ ...(source?.fileName && props && "values" in props && props.values && typeof props.values === "object" ? {
56
+ valueSources: {...(typeof type === "object" ? type.valueSources : {}),
57
+ ...Object.fromEntries(Object.keys(props.values).map(k => [k, source.fileName!]))},
58
+ } : {}),
53
59
  ...(typeof type === "object" && type?.sourceFile && type.type!==Scene && type.type!=="scene" ? { sourceFile: type.sourceFile }
54
60
  : source?.fileName ? { sourceFile: source.fileName } : {}),
55
61
  });
@@ -68,6 +74,6 @@ export namespace JSX {
68
74
  }
69
75
  export interface IntrinsicElements {}
70
76
  export type LibraryManagedAttributes<C, P> = C extends typeof Scene
71
- ? SceneProps
77
+ ? P
72
78
  : InputProps<P>;
73
79
  }
@@ -1,3 +1,4 @@
1
+ import { validAssetPath, type AssetKind } from "./assets.js";
1
2
  import { parseColor } from "./color.js";
2
3
  type When = Readonly<Record<string, string | number | boolean>>;
3
4
  interface Control {
@@ -23,6 +24,7 @@ export type Knob = Control & (
23
24
  max?: number;
24
25
  }
25
26
  | { type: "color" | "text"; default: string }
27
+ | { type: "asset"; kind: AssetKind; default: string }
26
28
  | { type: "boolean"; default: boolean }
27
29
  | { type: "select"; default: string; options: Readonly<Record<string, string>> }
28
30
  | { type: "multiselect"; default: readonly string[]; options: Readonly<Record<string, string>> }
@@ -59,6 +61,7 @@ export function validateKnobValue(knob: Knob, value: unknown, name: string) {
59
61
  switch (knob.type) {
60
62
  case "number": if (!finite(value)) fail(`${name}: expected a finite number`); break;
61
63
  case "boolean": if (typeof value !== "boolean") fail(`${name}: expected a boolean`); break;
64
+ case "asset": if (!validAssetPath(knob.kind, value)) fail(`${name}: expected a local ${knob.kind} asset path`); break;
62
65
  case "text": if (typeof value !== "string") fail(`${name}: expected text`); break;
63
66
  case "color": if (!parseColor(value)) fail(`${name}: expected a hex color or transparent`); break;
64
67
  case "select": if (typeof value !== "string" || !Object.hasOwn(knob.options, value)) fail(`${name}: unknown choice`); break;
@@ -97,9 +100,9 @@ export function validateKnobSchema(knobs: Knobs, inputGroups: Readonly<Record<st
97
100
  }
98
101
  for (const [name, knob] of Object.entries(knobs)) {
99
102
  if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(name) || !record(knob)) fail(`invalid knob ${name}`);
100
- const extras = knob.type === "number" ? ["min", "max", "step"] : ["select", "multiselect"].includes(knob.type) ? ["options"] : [];
103
+ const extras = knob.type === "number" ? ["min", "max", "step"] : knob.type === "asset" ? ["kind"] : ["select", "multiselect"].includes(knob.type) ? ["options"] : [];
101
104
  keys(knob, ["type", "default", "label", "group", "visibleWhen", ...extras], name);
102
- if (!["number", "color", "text", "boolean", "select", "multiselect"].includes(knob.type)) fail(`${name}: unknown knob type`);
105
+ if (!["number", "color", "text", "boolean", "select", "multiselect", "asset"].includes(knob.type)) fail(`${name}: unknown knob type`);
103
106
  if (knob.label !== undefined && typeof knob.label !== "string") fail(`${name}.label: expected text`);
104
107
  if (knob.group !== undefined && !Object.hasOwn(groups, knob.group)) fail(`${name}: unknown group ${knob.group}`);
105
108
  if (knob.type === "number") {