lecodes-sdk 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. package/README.md +5 -2
  2. package/dist/editor.d.ts +12 -0
  3. package/dist/global.d.ts +4 -8
  4. package/dist/host.d.ts +2 -3
  5. package/dist/types/animate/tween/Animation.d.ts +0 -3
  6. package/dist/types/animate/tween/animateValue.d.ts +4 -2
  7. package/dist/types/animate/tween/easing.d.ts +8 -0
  8. package/dist/types/animate/tween/spec.d.ts +15 -7
  9. package/dist/types/audio/audio.d.ts +2 -1
  10. package/dist/types/canvas/Canvas.d.ts +40 -108
  11. package/dist/types/canvas/gen/cssColor.d.ts +17 -0
  12. package/dist/types/canvas/gen/recorder.d.ts +118 -0
  13. package/dist/types/canvas/gen/spec.d.ts +144 -0
  14. package/dist/types/core/color.d.ts +3 -1
  15. package/dist/types/core/pins.d.ts +18 -0
  16. package/dist/types/g2/Node2D.d.ts +5 -8
  17. package/dist/types/g2/Scene2D.d.ts +6 -2
  18. package/dist/types/gl/Foliage.d.ts +30 -7
  19. package/dist/types/gl/Light.d.ts +11 -0
  20. package/dist/types/gl/Lightmap.d.ts +13 -2
  21. package/dist/types/gl/Material.d.ts +17 -3
  22. package/dist/types/gl/Model.d.ts +6 -2
  23. package/dist/types/gl/Node.d.ts +3 -6
  24. package/dist/types/gl/Particles.d.ts +15 -0
  25. package/dist/types/gl/Scene.d.ts +41 -13
  26. package/dist/types/gl/Texture.d.ts +1 -1
  27. package/dist/types/gl/animation/Locomotion.d.ts +59 -3
  28. package/dist/types/gl/terrain/Terrain.d.ts +8 -0
  29. package/dist/types/inject.d.ts +11 -11
  30. package/dist/types/inject.editor.d.ts +1 -0
  31. package/dist/types/net/core.d.ts +7 -0
  32. package/dist/types/plugin.d.ts +76 -0
  33. package/dist/types/plugins/gen/camera/sdk/camera.d.ts +24 -0
  34. package/dist/types/plugins/gen/camera/sdk/camera.gen.d.ts +25 -0
  35. package/dist/types/plugins/{geolocation.d.ts → gen/geolocation/sdk/geolocation.d.ts} +2 -20
  36. package/dist/types/plugins/gen/geolocation/sdk/geolocation.gen.d.ts +31 -0
  37. package/dist/types/plugins/{map.d.ts → gen/map/sdk/map.d.ts} +9 -50
  38. package/dist/types/plugins/gen/map/sdk/map.gen.d.ts +53 -0
  39. package/dist/types/plugins/gen/push/sdk/push.d.ts +23 -0
  40. package/dist/types/plugins/gen/push/sdk/push.gen.d.ts +35 -0
  41. package/dist/types/plugins/{qr.d.ts → gen/qr-scanner/sdk/qr-scanner.d.ts} +2 -3
  42. package/dist/types/plugins/gen/qr-scanner/sdk/qr-scanner.gen.d.ts +15 -0
  43. package/dist/types/runtime/app.d.ts +9 -2
  44. package/dist/types/runtime/fetch.d.ts +2 -0
  45. package/dist/types/runtime/input.d.ts +8 -1
  46. package/dist/types/runtime/media.d.ts +6 -10
  47. package/dist/types/runtime/misc.d.ts +4 -1
  48. package/dist/types/runtime/net.d.ts +3 -2
  49. package/dist/types/runtime/touch.d.ts +32 -0
  50. package/dist/types/scene/defineScene.d.ts +43 -2
  51. package/dist/types/scene/editor.d.ts +52 -0
  52. package/dist/types/scene/gizmos.d.ts +7 -4
  53. package/dist/types/ui/NativeView.d.ts +6 -4
  54. package/dist/types/ui/UI.d.ts +1 -1
  55. package/dist/types/ui/UIBottomSheet.d.ts +6 -12
  56. package/dist/types/ui/UIButton.d.ts +12 -14
  57. package/dist/types/ui/UIContainer.d.ts +0 -6
  58. package/dist/types/ui/UIImage.d.ts +1 -4
  59. package/dist/types/ui/UIInput.d.ts +8 -24
  60. package/dist/types/ui/UIModal.d.ts +0 -2
  61. package/dist/types/ui/UINode.d.ts +70 -56
  62. package/dist/types/ui/UIPager.d.ts +28 -27
  63. package/dist/types/ui/UIPopover.d.ts +0 -2
  64. package/dist/types/ui/UIScreen.d.ts +12 -17
  65. package/dist/types/ui/UIScrollable.d.ts +1 -4
  66. package/dist/types/ui/UIText.d.ts +0 -2
  67. package/dist/types/ui/UIVideo.d.ts +3 -5
  68. package/dist/types/ui/UIVirtualizedList.d.ts +14 -16
  69. package/dist/types/ui/UIWidget.d.ts +10 -9
  70. package/dist/types/ui/colorKeys.gen.d.ts +9 -0
  71. package/dist/types/ui/presentable.d.ts +46 -32
  72. package/dist/types/ui/router.d.ts +18 -7
  73. package/dist/types/ui/styleColor.d.ts +1 -0
  74. package/dist/types/ui/transitions.d.ts +18 -0
  75. package/dist/types/ui/tree.d.ts +75 -0
  76. package/dist/types/version.d.ts +10 -0
  77. package/dist/types.json +1 -1
  78. package/package.json +12 -3
  79. package/prompts/2d.md +2 -6
  80. package/prompts/3d.md +1 -5
  81. package/prompts/README.md +142 -142
  82. package/prompts/canvas.md +9 -8
  83. package/prompts/compose.ts +1 -1
  84. package/prompts/core.md +3 -3
  85. package/prompts/design.md +1 -1
  86. package/prompts/dist/2d-game.md +77 -54
  87. package/prompts/dist/3d-app.md +75 -52
  88. package/prompts/dist/ar-app.md +74 -51
  89. package/prompts/dist/design.md +30 -8
  90. package/prompts/dist/ui-app.md +73 -46
  91. package/prompts/ui-design.md +2 -3
  92. package/prompts/ui.md +45 -37
  93. package/src/animate/tween/Animation.ts +262 -378
  94. package/src/animate/tween/animateValue.ts +103 -100
  95. package/src/animate/tween/easing.ts +10 -3
  96. package/src/animate/tween/spec.ts +505 -479
  97. package/src/audio/Sound.ts +3 -3
  98. package/src/audio/audio.ts +162 -161
  99. package/src/bridges/2d.d.ts +317 -0
  100. package/src/bridges/app.d.ts +91 -0
  101. package/src/bridges/audio.d.ts +97 -0
  102. package/src/bridges/canvas.d.ts +79 -0
  103. package/src/bridges/device.d.ts +72 -0
  104. package/src/bridges/fetch.d.ts +80 -0
  105. package/src/bridges/files.d.ts +70 -0
  106. package/src/bridges/gl.d.ts +1133 -0
  107. package/src/bridges/input.d.ts +72 -0
  108. package/src/bridges/media.d.ts +55 -0
  109. package/src/bridges/nav.d.ts +71 -0
  110. package/src/bridges/net.d.ts +52 -0
  111. package/src/bridges/service.d.ts +52 -0
  112. package/src/bridges/socket.d.ts +31 -0
  113. package/src/bridges/storage.d.ts +33 -0
  114. package/src/bridges/tree.d.ts +301 -0
  115. package/src/bridges/types.d.ts +49 -0
  116. package/src/canvas/Canvas.ts +114 -159
  117. package/src/canvas/gen/cssColor.ts +224 -0
  118. package/src/canvas/gen/recorder.ts +212 -0
  119. package/src/canvas/gen/spec.ts +201 -0
  120. package/src/chisel.ts +193 -0
  121. package/src/compile/assetMacro.ts +1 -1
  122. package/src/compile/bundler.ts +11 -2
  123. package/src/compile/compileProject.ts +43 -4
  124. package/src/compile/fontMacro.ts +3 -4
  125. package/src/compile/header.ts +26 -5
  126. package/src/compile/index.ts +3 -1
  127. package/src/compile/liteMaterial.ts +1 -1
  128. package/src/compile/sceneEditor.ts +11 -26
  129. package/src/core/color.ts +73 -30
  130. package/src/core/pins.ts +51 -0
  131. package/src/core/signals.ts +8 -1
  132. package/src/g2/CharacterController2D.ts +3 -3
  133. package/src/g2/Node2D.ts +57 -39
  134. package/src/g2/Physics2D.ts +2 -2
  135. package/src/g2/Scene2D.ts +35 -23
  136. package/src/g2/Texture2D.ts +1 -1
  137. package/src/g2/loop.ts +4 -4
  138. package/src/gl/Foliage.ts +72 -17
  139. package/src/gl/Geometry.ts +1 -2
  140. package/src/gl/Light.ts +13 -0
  141. package/src/gl/Lightmap.ts +45 -29
  142. package/src/gl/Material.ts +95 -51
  143. package/src/gl/Model.ts +172 -167
  144. package/src/gl/Node.ts +91 -24
  145. package/src/gl/Particles.ts +20 -1
  146. package/src/gl/Scene.ts +100 -44
  147. package/src/gl/Texture.ts +8 -7
  148. package/src/gl/animation/AnimationClip.ts +1 -1
  149. package/src/gl/animation/Locomotion.ts +77 -8
  150. package/src/gl/nav/NavMesh.ts +3 -4
  151. package/src/gl/physics/Physics.ts +2 -2
  152. package/src/gl/physics/physicsEvents.ts +3 -3
  153. package/src/gl/terrain/Terrain.ts +33 -5
  154. package/src/gl/touch.ts +14 -15
  155. package/src/host.d.ts +2 -3
  156. package/src/inject.editor.ts +7 -0
  157. package/src/inject.ts +233 -236
  158. package/src/net/core.ts +6 -5
  159. package/src/net/index.ts +1 -1
  160. package/src/net/replication.ts +1 -1
  161. package/src/plugin.ts +191 -0
  162. package/src/plugins/gen/camera/contract.d.ts +27 -0
  163. package/src/plugins/gen/camera/sdk/camera.gen.ts +46 -0
  164. package/src/plugins/gen/camera/sdk/camera.ts +57 -0
  165. package/src/plugins/gen/geolocation/contract.d.ts +50 -0
  166. package/src/plugins/gen/geolocation/sdk/geolocation.gen.ts +54 -0
  167. package/src/plugins/{geolocation.ts → gen/geolocation/sdk/geolocation.ts} +22 -43
  168. package/src/plugins/gen/map/contract.d.ts +144 -0
  169. package/src/plugins/gen/map/sdk/map.gen.ts +88 -0
  170. package/src/plugins/{map.ts → gen/map/sdk/map.ts} +68 -102
  171. package/src/plugins/gen/push/contract.d.ts +61 -0
  172. package/src/plugins/gen/push/sdk/push.gen.ts +60 -0
  173. package/src/plugins/gen/push/sdk/push.ts +105 -0
  174. package/src/plugins/gen/qr-scanner/contract.d.ts +16 -0
  175. package/src/plugins/gen/qr-scanner/sdk/qr-scanner.gen.ts +29 -0
  176. package/src/plugins/gen/qr-scanner/sdk/qr-scanner.ts +52 -0
  177. package/src/plugins/permission.ts +5 -4
  178. package/src/runtime/app.ts +20 -9
  179. package/src/runtime/appEvents.ts +5 -4
  180. package/src/runtime/channel.ts +18 -15
  181. package/src/runtime/clipboard.ts +4 -3
  182. package/src/runtime/datetime.ts +2 -1
  183. package/src/runtime/device.ts +17 -15
  184. package/src/runtime/fetch.ts +30 -20
  185. package/src/runtime/files.ts +16 -15
  186. package/src/runtime/input.ts +21 -8
  187. package/src/runtime/media.ts +50 -46
  188. package/src/runtime/misc.ts +7 -3
  189. package/src/runtime/net.ts +8 -7
  190. package/src/runtime/rpc.ts +1 -3
  191. package/src/runtime/service.ts +19 -14
  192. package/src/runtime/share.ts +4 -3
  193. package/src/runtime/storage.ts +6 -4
  194. package/src/runtime/touch.ts +32 -0
  195. package/src/scene/defineScene.ts +61 -365
  196. package/src/scene/editor.ts +408 -0
  197. package/src/scene/editorPlugins.ts +3 -3
  198. package/src/scene/gizmos.ts +154 -148
  199. package/src/server/db/marci/query.ts +1 -1
  200. package/src/server/host.ts +1 -1
  201. package/src/server/runtime.ts +1 -1
  202. package/src/ui/NativeView.ts +72 -25
  203. package/src/ui/UI.ts +3 -3
  204. package/src/ui/UIBottomSheet.ts +16 -17
  205. package/src/ui/UIButton.ts +54 -16
  206. package/src/ui/UIContainer.ts +0 -6
  207. package/src/ui/UIImage.ts +34 -30
  208. package/src/ui/UIInput.ts +29 -37
  209. package/src/ui/UIModal.ts +1 -3
  210. package/src/ui/UINode.ts +347 -297
  211. package/src/ui/UIPager.ts +93 -78
  212. package/src/ui/UIPopover.ts +0 -2
  213. package/src/ui/UIScreen.ts +41 -36
  214. package/src/ui/UIScrollable.ts +19 -13
  215. package/src/ui/UISpacer.ts +1 -1
  216. package/src/ui/UITabs.ts +8 -6
  217. package/src/ui/UIText.ts +7 -17
  218. package/src/ui/UIVideo.ts +30 -27
  219. package/src/ui/UIVirtualizedList.ts +58 -59
  220. package/src/ui/UIWidget.ts +38 -18
  221. package/src/ui/colorKeys.gen.ts +37 -0
  222. package/src/ui/fonts.ts +2 -2
  223. package/src/ui/presentable.ts +59 -42
  224. package/src/ui/router.ts +52 -35
  225. package/src/ui/styleColor.ts +56 -0
  226. package/src/ui/theme.ts +7 -6
  227. package/src/ui/transitions.ts +249 -0
  228. package/src/ui/tree.ts +346 -0
  229. package/src/version.ts +24 -0
  230. package/tests/helpers/engineWorld.ts +11 -0
  231. package/tests/helpers/fakeTree.ts +353 -0
  232. package/tests/helpers/hostStubs.ts +31 -0
  233. package/tests/helpers/index.ts +14 -0
  234. package/tests/helpers/memoryMarci.ts +124 -0
  235. package/tests/helpers/phases.ts +23 -0
  236. package/tests/helpers/preload.ts +18 -0
  237. package/tests/helpers/stubApp.ts +2 -0
  238. package/tests/helpers/stubDevice.ts +2 -0
  239. package/tests/helpers/stubFetch.ts +2 -0
  240. package/tests/helpers/stubInput.ts +2 -0
  241. package/dist/inject.js +0 -4730
  242. package/dist/types/core/registry.d.ts +0 -7
  243. package/dist/types/plugins/camera.d.ts +0 -25
  244. package/dist/types/plugins/push.d.ts +0 -46
  245. package/src/bridges.d.ts +0 -1760
  246. package/src/compile/__tests__/assetIconMacro.test.ts +0 -219
  247. package/src/compile/__tests__/assetMacro.test.ts +0 -100
  248. package/src/compile/__tests__/assetName.test.ts +0 -55
  249. package/src/compile/__tests__/compile.test.ts +0 -310
  250. package/src/compile/__tests__/detectEntry.test.ts +0 -151
  251. package/src/compile/__tests__/fontMacro.test.ts +0 -199
  252. package/src/compile/__tests__/serverSplit.test.ts +0 -27
  253. package/src/core/__tests__/stateMachine.test.ts +0 -132
  254. package/src/core/registry.ts +0 -23
  255. package/src/plugins/camera.ts +0 -81
  256. package/src/plugins/push.ts +0 -132
  257. package/src/plugins/qr.ts +0 -73
@@ -1,479 +1,505 @@
1
- // The keyframe animation SPEC (docs/timeline-plan.md §4) and its wire form.
2
- //
3
- // An animation is a list of TRACKS — (target, prop) → keyframes — plus a clock, iteration and call
4
- // (time-callback) metadata. The SDK builds the spec from the flat options bags (`animateTo`,
5
- // `Timeline.add`), and `buildBlob` flattens it into ONE Float32Array + a string table at play time
6
- // (target ids are only known then: UI nodes get their id at mount, screens re-assign it on every
7
- // open). creator-pkg parses the blob into creator-tween's structures; the layout below is the
8
- // contract (creator-tween/include/creator-tween/blob.h mirrors it).
9
- //
10
- // Blob layout (all floats; ids and indices are exact below 2^24):
11
- // header [version=1, clock, durationMs, delayMs, iterations, pingPong, rate, nTracks, nCalls, nTables]
12
- // calls nCalls × [tMs]
13
- // tables nTables × EASE_TABLE_SIZE samples
14
- // tracks nTracks × ( [domain, targetIdx, propStr, kind, lanes, atMs, durMs, commit, nKeys, sigStr]
15
- // + nKeys × [t01, easeKind, e0, e1, e2, e3, has, lanes × value] )
16
- // `targetIdx` indexes the `targets` list handed over next to the floats (a host node handle is an
17
- // opaque object on creator-pkg hosts, a number elsewhere — it can't ride in a Float32Array).
18
- // A key with has = 0 carries no value: the evaluator CAPTURES the target's current value when the
19
- // animation first plays (the implicit first key of `animateTo`, the implicit last of `animateFrom`).
20
- // String-valued lanes (DISCRETE / STRING / TRANSFORM_MATRIX keys) hold a string-table index.
21
-
22
- import { EASE_TABLE_SIZE, evalEasing, parseEasing, type EasingInput, type EasingSpec } from "./easing"
23
- import { Color } from "../../core/color"
24
- import { Mat4 } from "../../math/mat4"
25
-
26
- export const BLOB_VERSION = 1
27
-
28
- // ---- domains (who the target is; creator-pkg maps each to a writer) ----------------------------
29
- export const DOM_UI = 0 // targetId = the element's host node id
30
- export const DOM_GL_NODE = 1 // targetId = entity: position / quaternion / eulerAngles / scale
31
- export const DOM_GL_LIGHT = 2 // targetId = entity: intensity / color
32
- export const DOM_GL_CAMERA = 3 // targetId = scene id: fov / near / far
33
- export const DOM_NODE2D = 4 // targetId = entity: x / y / position / rotation / scale
34
- export const DOM_SPRITE2D = 5 // targetId = entity: opacity / color
35
- export const DOM_VALUE = 6 // no engine target: the host hands the lanes back to JS — animate() / Timeline.animate; targetId = slot + 1
36
-
37
- // ---- value kinds ------------------------------------------------------------------------------
38
- export const KIND_FLOAT = 0 // 1 lane
39
- export const KIND_FLOAT_UNIT = 1 // 1 lane, sig = the unit ("px" | "%" | "vw" | "vh" | "deg" | "em")
40
- export const KIND_COLOR = 2 // 4 lanes r g b a in 0..1
41
- export const KIND_TRANSFORM = 3 // n lanes = the function list's args; sig = "translate,rotate,scale,…"
42
- export const KIND_TRANSFORM_MATRIX = 4 // 1 lane = string index of the raw transform; the host decomposes
43
- export const KIND_VEC3 = 5 // 3 lanes
44
- export const KIND_QUAT = 6 // 4 lanes, slerp
45
- export const KIND_EULER = 7 // 3 lanes, degrees, no shortest arc
46
- export const KIND_DISCRETE = 8 // 1 lane = string index, steps at the key
47
- export const KIND_STRING = 9 // 1 lane = string index; the UI host resolves: color prop → COLOR, else DISCRETE
48
- export const KIND_VEC2 = 10 // 2 lanes
49
- export const KIND_MAT4 = 11 // 10 lanes: position 3, quaternion 4 (slerp), scale 3 — a decomposed 4x4
50
-
51
- export const CLOCK_UI = 0
52
- export const CLOCK_GAME = 1
53
-
54
- /** Lane counts per kind (TRANSFORM is per-signature, see `transformLanes`). */
55
- const LANES: Record<number, number> = {
56
- [KIND_FLOAT]: 1, [KIND_FLOAT_UNIT]: 1, [KIND_COLOR]: 4, [KIND_TRANSFORM_MATRIX]: 1,
57
- [KIND_VEC3]: 3, [KIND_QUAT]: 4, [KIND_EULER]: 3, [KIND_DISCRETE]: 1, [KIND_STRING]: 1, [KIND_VEC2]: 2, [KIND_MAT4]: 10,
58
- }
59
- export const lanesOf = (kind: number): number => LANES[kind] ?? 1
60
-
61
- /** One normalized value: numeric lanes, or a string (string-lane kinds). */
62
- export type TweenValue = { lanes: number[] } | { str: string }
63
-
64
- /** A target's answer to "can you animate `prop`?": the writer domain, the id resolver, the value
65
- * normalizer, and the commit hook (UI writes the last key into its stored style). */
66
- export type TweenChannel = {
67
- domain: number,
68
- /** Host handle at play time — a number (entity, web node id) or the host's opaque node object
69
- * (creator-pkg hands `_id` out as one); falsy = not addressable right now (unmounted), the
70
- * track is skipped. Handed to the host in the blob's `targets` list, never in the float data. */
71
- id: () => unknown,
72
- /** Normalize one keyframe value; `null` = not animatable (a warning, the value is committed only). */
73
- value: (v: unknown) => ({ kind: number, sig?: string } & TweenValue) | null,
74
- commit?: (v: unknown) => void,
75
- }
76
-
77
- /** Implemented by every animatable class (Element, Node, Node2D, Sprite, Light, Camera). */
78
- export interface TweenTarget {
79
- /** @internal */
80
- _tweenChannel(prop: string): TweenChannel | null
81
- /** @internal Default clock for `animateTo` on this target. */
82
- readonly _tweenClock: number
83
- }
84
-
85
- export type Keyframe = {
86
- /** 0..1 within the track. */
87
- t: number,
88
- easing: EasingSpec,
89
- /** `undefined` = capture at play. */
90
- value?: TweenValue,
91
- /** The bag value as written — what the old-host path forwards verbatim. */
92
- raw?: unknown,
93
- }
94
-
95
- export type Track = {
96
- target: TweenTarget,
97
- prop: string,
98
- channel: TweenChannel,
99
- kind: number,
100
- lanes: number,
101
- sig?: string,
102
- atMs: number,
103
- durMs: number,
104
- commit: boolean,
105
- keys: Keyframe[],
106
- }
107
-
108
- export type TweenSpec = {
109
- clock: number,
110
- /** One iteration, ms — the last track's end (holds included). */
111
- durationMs: number,
112
- delayMs: number,
113
- /** -1 = forever. */
114
- iterations: number,
115
- pingPong: boolean,
116
- rate: number,
117
- tracks: Track[],
118
- /** Time-callbacks, ms; index = the event index the host reports. */
119
- calls: number[],
120
- }
121
-
122
- // ---- the options bag -----------------------------------------------------------------------------
123
-
124
- /** Meta keys of an `animateTo` / `Timeline.add` bag — everything that is not a prop target. */
125
- export const TWEEN_META = new Set(["duration", "delay", "easing", "times", "loop", "loopMode", "commit", "clock", "layer", "at", "stagger"])
126
-
127
- export type TweenMeta = {
128
- /** Tween length in **milliseconds** (default 225). */
129
- duration?: number,
130
- /** Wait before starting, **milliseconds**. The first keyframe holds through the wait. A loop delays once. */
131
- delay?: number,
132
- /** A curve for every prop, or a per-prop map (`{ width: 'outExpo', opacity: 'linear' }`). Names
133
- * (`outCubic`, `inOutSine`, `outBack`, …), `cubic-bezier(…)` / a `[x1, y1, x2, y2]` tuple,
134
- * `steps(n)`, or any `Easing` function. Default linear. */
135
- easing?: EasingInput | Record<string, EasingInput>,
136
- /** Keyframe offsets 0..1 for array-valued props (default: evenly spaced). */
137
- times?: number[],
138
- /** `true` = forever, a number = that many cycles. A loop never commits. */
139
- loop?: boolean | number,
140
- /** `"ping-pong"` (default) there and back each cycle; `"restart"` snaps back and replays. */
141
- loopMode?: "restart" | "ping-pong",
142
- /** `false` = animate without writing the last keyframe into the element's style. */
143
- commit?: boolean,
144
- /** `'ui'` = wall time (menus animate while the game is paused); `'game'` follows `Time.scale`.
145
- * Default: `'ui'` on UI elements, `'game'` on 3D / 2D nodes. */
146
- clock?: "ui" | "game",
147
- /** Legacy escape hatch (old hosts only). */
148
- layer?: number,
149
- }
150
-
151
- export const DEFAULT_DURATION_MS = 225
152
-
153
- /** Warn once per (prop) about a value that cannot be animated — the bag is committed anyway. */
154
- const warned = new Set<string>()
155
- export const warnOnce = (key: string, msg: string): void => {
156
- if (warned.has(key)) return
157
- warned.add(key)
158
- console.warn(`[tween] ${msg}`)
159
- }
160
-
161
- /** Resolve the per-prop easing from the bag. */
162
- export const easingFor = (easing: TweenMeta["easing"], prop: string): EasingSpec => {
163
- if (easing !== undefined && typeof easing === "object" && !Array.isArray(easing) && typeof easing !== "function") {
164
- return parseEasing((easing as Record<string, EasingInput>)[prop])
165
- }
166
- return parseEasing(easing as EasingInput | undefined)
167
- }
168
-
169
- /** Iteration count from `loop`: 1 = one-shot, -1 = forever, n = cycles. */
170
- export const iterationsOf = (loop: TweenMeta["loop"]): number => {
171
- if (loop === true) return -1
172
- if (typeof loop === "number" && loop > 0) return Math.floor(loop)
173
- return 1
174
- }
175
-
176
- // ---- value normalizers shared by the domains ----------------------------------------------------
177
-
178
- const UNIT_RE = /^\s*(-?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?)\s*(px|%|vw|vh|vmin|vmax|deg|rad|em|rem)?\s*$/i
179
-
180
- /** A UI style value: number → FLOAT; "12px" / "50%" → FLOAT_UNIT; other strings → STRING (the host
181
- * decides color vs discrete); booleans → DISCRETE. */
182
- export const uiValue = (v: unknown): ({ kind: number, sig?: string } & TweenValue) | null => {
183
- if (typeof v === "number") return { kind: KIND_FLOAT, lanes: [v] }
184
- if (typeof v === "boolean") return { kind: KIND_DISCRETE, str: v ? "true" : "false" }
185
- if (typeof v === "string") {
186
- const m = UNIT_RE.exec(v)
187
- if (m) {
188
- const unit = (m[2] ?? "").toLowerCase()
189
- return unit === "" || unit === "px"
190
- ? { kind: KIND_FLOAT, lanes: [parseFloat(m[1]!)] }
191
- : { kind: KIND_FLOAT_UNIT, sig: unit, lanes: [parseFloat(m[1]!)] }
192
- }
193
- return { kind: KIND_STRING, str: v }
194
- }
195
- return null
196
- }
197
-
198
- /** Per-function argument counts of a UI transform list (degrees / px are the SDK's units). */
199
- const TRANSFORM_FNS: Record<string, number> = {
200
- translate: 2, translateX: 1, translateY: 1, rotate: 1, scale: 2, scaleX: 1, scaleY: 1, skewX: 1, skewY: 1,
201
- }
202
-
203
- /** Parse `"translate(10px, 20px) rotate(45deg) scale(1.2)"` into a signature + lanes, or null when a
204
- * function is unknown (the caller falls back to matrix decomposition on the host). `scale(2)` and
205
- * `translate(5px)` complete their second argument like CSS. */
206
- export const parseTransformList = (s: string): { sig: string, lanes: number[] } | null => {
207
- const sig: string[] = []
208
- const lanes: number[] = []
209
- const re = /([a-zA-Z]+)\s*\(([^)]*)\)/g
210
- let m: RegExpExecArray | null
211
- let consumed = 0
212
- const src = s.trim()
213
- if (src === "" || src === "none") return { sig: "", lanes: [] }
214
- while ((m = re.exec(src)) !== null) {
215
- const fn = m[1]!
216
- const n = TRANSFORM_FNS[fn]
217
- if (n === undefined) return null
218
- const args = m[2]!.split(",").map((a) => {
219
- const u = UNIT_RE.exec(a)
220
- return u ? parseFloat(u[1]!) : NaN
221
- })
222
- if (args.some((a) => Number.isNaN(a)) || args.length === 0 || args.length > n) return null
223
- if (args.length < n) args.push(fn === "translate" ? 0 : args[0]!)
224
- sig.push(fn)
225
- lanes.push(...args)
226
- consumed = re.lastIndex
227
- }
228
- if (src.slice(consumed).trim() !== "") return null
229
- return { sig: sig.join(","), lanes }
230
- }
231
-
232
- export const transformLanes = (sig: string): number => (sig === "" ? 0 : sig.split(",").reduce((n, fn) => n + (TRANSFORM_FNS[fn] ?? 0), 0))
233
-
234
- const num3 = (v: unknown): number[] | null => {
235
- if (typeof v === "number") return [v, v, v]
236
- if (v && typeof v === "object") {
237
- const o = v as any
238
- if (typeof o.length === "number" && o.length >= 3) return [+o[0], +o[1], +o[2]]
239
- if (typeof o.x === "number") return [o.x, o.y, o.z]
240
- }
241
- return null
242
- }
243
-
244
- const num4 = (v: unknown): number[] | null => {
245
- if (v && typeof v === "object") {
246
- const o = v as any
247
- if (typeof o.length === "number" && o.length >= 4) return [+o[0], +o[1], +o[2], +o[3]]
248
- if (typeof o.x === "number" && typeof o.w === "number") return [o.x, o.y, o.z, o.w]
249
- }
250
- return null
251
- }
252
-
253
- const num2 = (v: unknown): number[] | null => {
254
- if (typeof v === "number") return [v, v]
255
- if (v && typeof v === "object") {
256
- const o = v as any
257
- if (typeof o.length === "number" && o.length >= 2) return [+o[0], +o[1]]
258
- if (typeof o.x === "number") return [o.x, o.y]
259
- }
260
- return null
261
- }
262
-
263
- export const vec3Value = (v: unknown) => { const l = num3(v); return l ? { kind: KIND_VEC3, lanes: l } : null }
264
- export const quatValue = (v: unknown) => { const l = num4(v); return l ? { kind: KIND_QUAT, lanes: l } : null }
265
- export const eulerValue = (v: unknown) => { const l = num3(v); return l ? { kind: KIND_EULER, lanes: l } : null }
266
- export const vec2Value = (v: unknown) => { const l = num2(v); return l ? { kind: KIND_VEC2, lanes: l } : null }
267
- export const floatValue = (v: unknown) => (typeof v === "number" ? { kind: KIND_FLOAT, lanes: [v] } : null)
268
- export const colorValue = (v: unknown, toRgba: (c: any) => [number, number, number, number]) => {
269
- if (typeof v === "string" || typeof v === "number" || Array.isArray(v)) return { kind: KIND_COLOR, lanes: toRgba(v) }
270
- return null
271
- }
272
-
273
- /** A free value for animate(): number → FLOAT, a color string → COLOR, arrays / vectors by length
274
- * (2 → VEC2, 3 → VEC3, 4 → QUAT, 16 → MAT4 decomposed into position · quaternion · scale). */
275
- export const valueValue = (v: unknown): ({ kind: number } & TweenValue) | null => {
276
- if (typeof v === "number") return { kind: KIND_FLOAT, lanes: [v] }
277
- if (typeof v === "string") return { kind: KIND_COLOR, lanes: Color.toRgba01(v) }
278
- if (v && typeof v === "object") {
279
- const o = v as any
280
- const arr: ArrayLike<number> | undefined = typeof o.length === "number" ? o : Array.isArray(o.m) && o.m.length === 16 ? o.m : undefined
281
- if (arr) {
282
- if (arr.length === 2) return { kind: KIND_VEC2, lanes: [+arr[0]!, +arr[1]!] }
283
- if (arr.length === 3) return { kind: KIND_VEC3, lanes: [+arr[0]!, +arr[1]!, +arr[2]!] }
284
- if (arr.length === 4) return { kind: KIND_QUAT, lanes: [+arr[0]!, +arr[1]!, +arr[2]!, +arr[3]!] }
285
- if (arr.length === 16) {
286
- const d = new Mat4(arr as any).decompose()
287
- return { kind: KIND_MAT4, lanes: [d.position.x, d.position.y, d.position.z, d.rotation.x, d.rotation.y, d.rotation.z, d.rotation.w, d.scale.x, d.scale.y, d.scale.z] }
288
- }
289
- return null
290
- }
291
- if (typeof o.x === "number") {
292
- if (typeof o.w === "number") return { kind: KIND_QUAT, lanes: [o.x, o.y, o.z, o.w] }
293
- if (typeof o.z === "number") return { kind: KIND_VEC3, lanes: [o.x, o.y, o.z] }
294
- return { kind: KIND_VEC2, lanes: [o.x, o.y] }
295
- }
296
- }
297
- return null
298
- }
299
-
300
- /** Is this bag value a KEYFRAME ARRAY for a vector prop, as opposed to one vector? A vector value
301
- * is an array of NUMBERS (`[0, 2, 0]`); anything else in the array is a list of keyframes —
302
- * `[[0,0,0], [0,2,0]]`, `[new Vec3(), …]`, and the UI's `color: ['#fff', '#e29a22']`. */
303
- export const isVectorKeyframes = (v: unknown): boolean =>
304
- Array.isArray(v) && v.length > 0 && typeof v[0] !== "number"
305
-
306
- // ---- track construction ------------------------------------------------------------------------
307
-
308
- /** Build one track's keyframes from a bag value: a single value = one explicit key (the other end
309
- * captured), an array = explicit keys spaced by `times` or evenly. `fromCurrent` says which end is
310
- * implicit (animateTo: the first; animateFrom: the last). Returns null when a value can't be
311
- * animated (already warned). */
312
- export const makeTrack = (
313
- target: TweenTarget, prop: string, raw: unknown, meta: TweenMeta, atMs: number, fromCurrent: boolean, vectorProp: boolean,
314
- ): Track | null => {
315
- const channel = target._tweenChannel(prop)
316
- if (!channel) {
317
- warnOnce(`prop:${prop}`, `"${prop}" is not animatable on this target — set it directly`)
318
- return null
319
- }
320
- const values: unknown[] = Array.isArray(raw) && (!vectorProp || isVectorKeyframes(raw)) ? raw : [raw]
321
- const easing = easingFor(meta.easing, prop)
322
- const norm = values.map((v) => channel.value(v))
323
- if (norm.some((n) => n === null)) {
324
- warnOnce(`value:${prop}`, `"${prop}" got a value that cannot be animated — set it directly`)
325
- return null
326
- }
327
- const ns = norm as NonNullable<(typeof norm)[number]>[]
328
-
329
- // One kind per track. Mismatched units / transform signatures degrade to DISCRETE (a flip at the
330
- // key, like CSS), never to garbage interpolation.
331
- let kind = ns[0]!.kind
332
- let sig = ns[0]!.sig
333
- for (const n of ns) {
334
- if (n.kind !== kind || n.sig !== sig) {
335
- // FLOAT vs FLOAT_UNIT(px) can't happen (px normalizes to FLOAT); anything else is discrete
336
- kind = KIND_DISCRETE
337
- sig = undefined
338
- break
339
- }
340
- }
341
- const lanes = kind === KIND_TRANSFORM ? transformLanes(sig ?? "") : LANES[kind]!
342
- const toValue = (n: NonNullable<(typeof norm)[number]>): TweenValue =>
343
- kind === KIND_DISCRETE && "lanes" in n ? { str: String(n.lanes[0]) } : ("str" in n ? { str: n.str } : { lanes: n.lanes })
344
-
345
- const explicit = ns.length
346
- const total = explicit + (explicit === 1 ? 1 : 0) // a single value pairs with a captured end
347
- const keys: Keyframe[] = []
348
- const times = meta.times
349
- for (let i = 0; i < total; i++) {
350
- let t: number
351
- if (times && times.length === total) t = times[i]!
352
- else t = total === 1 ? 1 : i / (total - 1)
353
- let value: TweenValue | undefined
354
- let raw: unknown
355
- if (explicit === 1) {
356
- // animateTo: [captured, v]; animateFrom: [v, captured]
357
- const isExplicit = fromCurrent ? i === 1 : i === 0
358
- value = isExplicit ? toValue(ns[0]!) : undefined
359
- raw = isExplicit ? values[0] : undefined
360
- } else {
361
- value = toValue(ns[i]!)
362
- raw = values[i]
363
- }
364
- keys.push({ t, easing, value, raw })
365
- }
366
- // animateFrom with an explicit array: the element's own style is the implicit LAST key
367
- if (!fromCurrent && explicit > 1) {
368
- for (const k of keys) k.t *= (total) / (total + 1)
369
- keys.push({ t: 1, easing, value: undefined })
370
- }
371
-
372
- return {
373
- target, prop, channel, kind, lanes, sig,
374
- atMs: atMs + (meta.delay ?? 0),
375
- durMs: Math.max(0, meta.duration ?? DEFAULT_DURATION_MS),
376
- // animateFrom never commits (its last key is the element's own state); loops never commit
377
- commit: fromCurrent && meta.commit !== false && iterationsOf(meta.loop) === 1,
378
- keys,
379
- }
380
- }
381
-
382
- /** Every (prop → value) entry of a bag, meta keys stripped. */
383
- export const bagProps = (bag: Record<string, unknown>): [string, unknown][] =>
384
- Object.entries(bag).filter(([k, v]) => !TWEEN_META.has(k) && v !== undefined)
385
-
386
- // ---- JS evaluation (the old-host path of VALUE tracks; the C evaluator is the reference) ----------
387
-
388
- const slerpInto = (out: number[], o: number, a: number[], ai: number, b: number[], bi: number, s: number): void => {
389
- let bx = b[bi]!, by = b[bi + 1]!, bz = b[bi + 2]!, bw = b[bi + 3]!
390
- let cosom = a[ai]! * bx + a[ai + 1]! * by + a[ai + 2]! * bz + a[ai + 3]! * bw
391
- if (cosom < 0) { cosom = -cosom; bx = -bx; by = -by; bz = -bz; bw = -bw }
392
- let k0: number, k1: number
393
- if (1 - cosom > 1e-6) {
394
- const omega = Math.acos(Math.min(1, cosom)), sinom = Math.sin(omega)
395
- k0 = Math.sin((1 - s) * omega) / sinom
396
- k1 = Math.sin(s * omega) / sinom
397
- } else { k0 = 1 - s; k1 = s }
398
- out[o] = k0 * a[ai]! + k1 * bx; out[o + 1] = k0 * a[ai + 1]! + k1 * by; out[o + 2] = k0 * a[ai + 2]! + k1 * bz; out[o + 3] = k0 * a[ai + 3]! + k1 * bw
399
- }
400
-
401
- /** The value of a numeric track at local time `ms` (the C evaluator's rule: first key holds before,
402
- * last after; eased segment between). `out` receives `track.lanes` numbers. */
403
- export const evaluateTrack = (track: Track, ms: number, out: number[]): void => {
404
- const u = track.durMs <= 0 ? (ms >= track.atMs ? 1 : 0) : Math.min(1, Math.max(0, (ms - track.atMs) / track.durMs))
405
- const keys = track.keys
406
- const lanesOfKey = (k: Keyframe): number[] => (k.value && "lanes" in k.value ? k.value.lanes : [])
407
- if (keys.length === 0) return
408
- if (u <= keys[0]!.t || keys.length === 1) { const l = lanesOfKey(keys[0]!); for (let i = 0; i < track.lanes; i++) out[i] = l[i] ?? 0; return }
409
- const last = keys[keys.length - 1]!
410
- if (u >= last.t) { const l = lanesOfKey(last); for (let i = 0; i < track.lanes; i++) out[i] = l[i] ?? 0; return }
411
- let i = 0
412
- while (i + 1 < keys.length && keys[i + 1]!.t <= u) i++
413
- const a = keys[i]!, b = keys[i + 1]!
414
- const span = b.t - a.t
415
- const s = evalEasing(a.easing, span > 0 ? (u - a.t) / span : 1)
416
- const la = lanesOfKey(a), lb = lanesOfKey(b)
417
- if (track.kind === KIND_QUAT) { slerpInto(out, 0, la, 0, lb, 0, s); return }
418
- if (track.kind === KIND_MAT4) {
419
- for (let k = 0; k < 3; k++) out[k] = la[k]! + (lb[k]! - la[k]!) * s
420
- slerpInto(out, 3, la, 3, lb, 3, s)
421
- for (let k = 7; k < 10; k++) out[k] = la[k]! + (lb[k]! - la[k]!) * s
422
- return
423
- }
424
- for (let k = 0; k < track.lanes; k++) out[k] = (la[k] ?? 0) + ((lb[k] ?? 0) - (la[k] ?? 0)) * s
425
- }
426
-
427
- // ---- the blob ----------------------------------------------------------------------------------
428
-
429
- export type TweenBlob = { data: Float32Array, strings: string[], targets: unknown[] }
430
-
431
- /** Flatten a spec for `_creatorUI.tweenCreate`. Tracks whose target has no host id right now are
432
- * dropped (the animation still runs its clock, calls and finish). */
433
- export const buildBlob = (spec: TweenSpec): TweenBlob => {
434
- const strings: string[] = []
435
- const strIndex = new Map<string, number>()
436
- const str = (s: string): number => {
437
- let i = strIndex.get(s)
438
- if (i === undefined) { i = strings.length; strings.push(s); strIndex.set(s, i) }
439
- return i
440
- }
441
- const tables: Float32Array[] = []
442
- const tableIndex = (e: EasingSpec): number => {
443
- const i = tables.indexOf(e.table!)
444
- if (i >= 0) return i
445
- tables.push(e.table!)
446
- return tables.length - 1
447
- }
448
-
449
- const tracks = spec.tracks.map((t) => ({ t, id: t.channel.id() })).filter((x) => !!x.id)
450
- const targets: unknown[] = []
451
- const targetIndex = (id: unknown): number => {
452
- let i = targets.indexOf(id)
453
- if (i < 0) { i = targets.length; targets.push(id) }
454
- return i
455
- }
456
- const out: number[] = []
457
- out.push(BLOB_VERSION, spec.clock, spec.durationMs, spec.delayMs, spec.iterations, spec.pingPong ? 1 : 0, spec.rate,
458
- tracks.length, spec.calls.length, 0 /* nTables, patched */)
459
- for (const c of spec.calls) out.push(c)
460
- const tablesAt = out.length
461
- for (const { t, id } of tracks) {
462
- out.push(t.channel.domain, targetIndex(id), str(t.prop), t.kind, t.lanes, t.atMs, t.durMs, t.commit ? 1 : 0, t.keys.length, t.sig !== undefined ? str(t.sig) : -1)
463
- for (const k of t.keys) {
464
- const e = k.easing
465
- const p: number[] = [e.p[0], e.p[1], e.p[2], e.p[3]]
466
- if (e.table) p[0] = tableIndex(e)
467
- out.push(k.t, e.kind, p[0]!, p[1]!, p[2]!, p[3]!, k.value ? 1 : 0)
468
- if (!k.value) { for (let i = 0; i < t.lanes; i++) out.push(0) }
469
- else if ("str" in k.value) { out.push(str(k.value.str)); for (let i = 1; i < t.lanes; i++) out.push(0) }
470
- else { for (let i = 0; i < t.lanes; i++) out.push(k.value.lanes[i] ?? 0) }
471
- }
472
- }
473
- // splice the tables in after the calls
474
- const tableData: number[] = []
475
- for (const tb of tables) for (let i = 0; i < EASE_TABLE_SIZE; i++) tableData.push(tb[i]!)
476
- out.splice(tablesAt, 0, ...tableData)
477
- out[9] = tables.length
478
- return { data: Float32Array.from(out), strings, targets }
479
- }
1
+ // The keyframe animation SPEC (docs/timeline-plan.md §4) and its wire form.
2
+ //
3
+ // An animation is a list of TRACKS — (target, prop) → keyframes — plus a clock, iteration and call
4
+ // (time-callback) metadata. The SDK builds the spec from the flat options bags (`animateTo`,
5
+ // `Timeline.add`), and `buildBlob` flattens it into ONE Float32Array + a string table at play time
6
+ // (target ids are only known then: UI nodes get their id at mount, screens re-assign it on every
7
+ // open). creator-pkg parses the blob into creator-tween's structures; the layout below is the
8
+ // contract (creator-tween/include/creator-tween/blob.h mirrors it).
9
+ //
10
+ // Blob layout (all floats; ids and indices are exact below 2^24):
11
+ // header [version=1, clock, durationMs, delayMs, iterations, pingPong, rate, nTracks, nCalls, nTables]
12
+ // calls nCalls × [tMs]
13
+ // tables nTables × EASE_TABLE_SIZE samples
14
+ // tracks nTracks × ( [domain, targetIdx, propStr, kind, lanes, atMs, durMs, commit, nKeys, sigStr]
15
+ // + nKeys × [t01, easeKind, e0, e1, e2, e3, has, lanes × value] )
16
+ // `targetIdx` indexes the `targets` list handed over next to the floats (a host node handle is an
17
+ // opaque object on creator-pkg hosts, a number elsewhere — it can't ride in a Float32Array).
18
+ // A key with has = 0 carries no value: the evaluator CAPTURES the target's current value when the
19
+ // animation first plays (the implicit first key of `animateTo`, the implicit last of `animateFrom`).
20
+ // String-valued lanes (DISCRETE / STRING / TRANSFORM_MATRIX keys) hold a string-table index.
21
+
22
+ import { DEFAULT_EASING, EASE_TABLE_SIZE, evalEasing, parseEasing, type EasingInput, type EasingSpec } from "./easing"
23
+ import { Color } from "../../core/color"
24
+ import { Mat4 } from "../../math/mat4"
25
+
26
+ export const BLOB_VERSION = 1
27
+
28
+ // ---- domains (who the target is; creator-pkg maps each to a writer) ----------------------------
29
+ export const DOM_UI = 0 // targetId = the element's host node id
30
+ export const DOM_GL_NODE = 1 // targetId = entity: position / quaternion / eulerAngles / scale
31
+ export const DOM_GL_LIGHT = 2 // targetId = entity: intensity / color
32
+ export const DOM_GL_CAMERA = 3 // targetId = scene id: fov / near / far
33
+ export const DOM_NODE2D = 4 // targetId = entity: x / y / position / rotation / scale
34
+ export const DOM_SPRITE2D = 5 // targetId = entity: opacity / color
35
+ export const DOM_VALUE = 6 // no engine target: the host hands the lanes back to JS — animate() / Timeline.animate; targetId = slot + 1
36
+
37
+ // ---- value kinds ------------------------------------------------------------------------------
38
+ export const KIND_FLOAT = 0 // 1 lane
39
+ export const KIND_FLOAT_UNIT = 1 // 1 lane, sig = the unit ("px" | "%" | "vw" | "vh" | "deg" | "em")
40
+ export const KIND_COLOR = 2 // 4 lanes r g b a in 0..1
41
+ export const KIND_TRANSFORM = 3 // n lanes = the function list's args; sig = "translate,rotate,scale,…"
42
+ export const KIND_TRANSFORM_MATRIX = 4 // 1 lane = string index of the raw transform; the host decomposes
43
+ export const KIND_VEC3 = 5 // 3 lanes
44
+ export const KIND_QUAT = 6 // 4 lanes, slerp
45
+ export const KIND_EULER = 7 // 3 lanes, degrees, no shortest arc
46
+ export const KIND_DISCRETE = 8 // 1 lane = string index, steps at the key
47
+ export const KIND_STRING = 9 // 1 lane = string index; the UI host resolves: color prop → COLOR, else DISCRETE
48
+ export const KIND_VEC2 = 10 // 2 lanes
49
+ export const KIND_MAT4 = 11 // 10 lanes: position 3, quaternion 4 (slerp), scale 3 — a decomposed 4x4
50
+
51
+ export const CLOCK_UI = 0
52
+ export const CLOCK_GAME = 1
53
+
54
+ /** Lane counts per kind (TRANSFORM is per-signature, see `transformLanes`). */
55
+ const LANES: Record<number, number> = {
56
+ [KIND_FLOAT]: 1, [KIND_FLOAT_UNIT]: 1, [KIND_COLOR]: 4, [KIND_TRANSFORM_MATRIX]: 1,
57
+ [KIND_VEC3]: 3, [KIND_QUAT]: 4, [KIND_EULER]: 3, [KIND_DISCRETE]: 1, [KIND_STRING]: 1, [KIND_VEC2]: 2, [KIND_MAT4]: 10,
58
+ }
59
+ export const lanesOf = (kind: number): number => LANES[kind] ?? 1
60
+
61
+ /** One normalized value: numeric lanes, or a string (string-lane kinds). */
62
+ export type TweenValue = { lanes: number[] } | { str: string }
63
+
64
+ /** A target's answer to "can you animate `prop`?": the writer domain, the id resolver, the value
65
+ * normalizer, and the commit hook (UI writes the last key into its stored style). */
66
+ export type TweenChannel = {
67
+ domain: number,
68
+ /** Host handle at play time — a number (entity, web node id) or the host's opaque node object
69
+ * (creator-pkg hands `_id` out as one); falsy = not addressable right now (unmounted), the
70
+ * track is skipped. Handed to the host in the blob's `targets` list, never in the float data. */
71
+ id: () => unknown,
72
+ /** Normalize one keyframe value; `null` = not animatable (a warning, the value is committed only). */
73
+ value: (v: unknown) => ({ kind: number, sig?: string } & TweenValue) | null,
74
+ commit?: (v: unknown) => void,
75
+ }
76
+
77
+ /** Implemented by every animatable class (Element, Node, Node2D, Sprite, Light, Camera). */
78
+ export interface TweenTarget {
79
+ /** @internal */
80
+ _tweenChannel(prop: string): TweenChannel | null
81
+ /** @internal Default clock for `animateTo` on this target. */
82
+ readonly _tweenClock: number
83
+ }
84
+
85
+ export type Keyframe = {
86
+ /** 0..1 within the track. */
87
+ t: number,
88
+ easing: EasingSpec,
89
+ /** `undefined` = capture at play. */
90
+ value?: TweenValue,
91
+ /** The bag value as written — what the old-host path forwards verbatim. */
92
+ raw?: unknown,
93
+ }
94
+
95
+ export type Track = {
96
+ target: TweenTarget,
97
+ prop: string,
98
+ channel: TweenChannel,
99
+ kind: number,
100
+ lanes: number,
101
+ sig?: string,
102
+ atMs: number,
103
+ durMs: number,
104
+ commit: boolean,
105
+ keys: Keyframe[],
106
+ }
107
+
108
+ export type TweenSpec = {
109
+ clock: number,
110
+ /** One iteration, ms — the last track's end (holds included). */
111
+ durationMs: number,
112
+ delayMs: number,
113
+ /** -1 = forever. */
114
+ iterations: number,
115
+ pingPong: boolean,
116
+ rate: number,
117
+ tracks: Track[],
118
+ /** Time-callbacks, ms; index = the event index the host reports. */
119
+ calls: number[],
120
+ }
121
+
122
+ // ---- the options bag -----------------------------------------------------------------------------
123
+
124
+ /** Meta keys of an `animateTo` / `Timeline.add` bag — everything that is not a prop target. */
125
+ export const TWEEN_META = new Set(["duration", "delay", "easing", "times", "loop", "loopMode", "commit", "clock", "layer", "at", "stagger"])
126
+
127
+ export type TweenMeta = {
128
+ /** Tween length in **milliseconds** (default 225). */
129
+ duration?: number,
130
+ /** Wait before starting, **milliseconds**. The first keyframe holds through the wait. A loop delays once. */
131
+ delay?: number,
132
+ /** A curve for every prop, or a per-prop map (`{ width: 'outExpo', opacity: 'linear' }`). Names
133
+ * (`outCubic`, `inOutSine`, `outBack`, …), `cubic-bezier(…)` / a `[x1, y1, x2, y2]` tuple,
134
+ * `steps(n)`, or any `Easing` function. Default `ease-in-out` (cubic-bezier(0.42, 0, 0.58, 1),
135
+ * the curve UIKit and CSS transitions ease with); `"linear"` for uniform motion. */
136
+ easing?: EasingInput | Record<string, EasingInput>,
137
+ /** Keyframe offsets 0..1 for array-valued props (default: evenly spaced). */
138
+ times?: number[],
139
+ /** `true` = forever, a number = that many cycles. A loop never commits. */
140
+ loop?: boolean | number,
141
+ /** `"ping-pong"` (default) there and back each cycle; `"restart"` snaps back and replays. */
142
+ loopMode?: "restart" | "ping-pong",
143
+ /** `false` = animate without writing the last keyframe into the element's style. */
144
+ commit?: boolean,
145
+ /** `'ui'` = wall time (menus animate while the game is paused); `'game'` follows `Time.scale`.
146
+ * Default: `'ui'` on UI elements, `'game'` on 3D / 2D nodes. */
147
+ clock?: "ui" | "game",
148
+ /** Legacy escape hatch (old hosts only). */
149
+ layer?: number,
150
+ }
151
+
152
+ export const DEFAULT_DURATION_MS = 225
153
+
154
+ /** Warn once per (prop) about a value that cannot be animated — the bag is committed anyway. */
155
+ const warned = new Set<string>()
156
+ export const warnOnce = (key: string, msg: string): void => {
157
+ if (warned.has(key)) return
158
+ warned.add(key)
159
+ console.warn(`[tween] ${msg}`)
160
+ }
161
+
162
+ /** Resolve the per-prop easing from the bag; nothing given (or no entry for `prop` in a per-prop
163
+ * map) = `fallback` — the tracks' DEFAULT_EASING, linear for a value tween. */
164
+ export const easingFor = (easing: TweenMeta["easing"], prop: string, fallback: EasingSpec = DEFAULT_EASING): EasingSpec => {
165
+ const one = easing !== undefined && typeof easing === "object" && !Array.isArray(easing) && typeof easing !== "function"
166
+ ? (easing as Record<string, EasingInput>)[prop]
167
+ : easing as EasingInput | undefined
168
+ return one === undefined ? fallback : parseEasing(one)
169
+ }
170
+
171
+ /** Iteration count from `loop`: 1 = one-shot, -1 = forever, n = cycles. */
172
+ export const iterationsOf = (loop: TweenMeta["loop"]): number => {
173
+ if (loop === true) return -1
174
+ if (typeof loop === "number" && loop > 0) return Math.floor(loop)
175
+ return 1
176
+ }
177
+
178
+ // ---- value normalizers shared by the domains ----------------------------------------------------
179
+
180
+ const UNIT_RE = /^\s*(-?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?)\s*(px|%|vw|vh|vmin|vmax|deg|rad|em|rem)?\s*$/i
181
+
182
+ /** A UI style value: number → FLOAT; "12px" / "50%" → FLOAT_UNIT; other strings → STRING (the host
183
+ * decides color vs discrete); booleans → DISCRETE. The element hands over the WIRE value
184
+ * (UINode._tweenChannel): a color key arrives as a string, never a packed number. */
185
+ export const uiValue = (v: unknown): ({ kind: number, sig?: string } & TweenValue) | null => {
186
+ if (typeof v === "number") return { kind: KIND_FLOAT, lanes: [v] }
187
+ if (typeof v === "boolean") return { kind: KIND_DISCRETE, str: v ? "true" : "false" }
188
+ if (typeof v === "string") {
189
+ const m = UNIT_RE.exec(v)
190
+ if (m) {
191
+ const unit = (m[2] ?? "").toLowerCase()
192
+ return unit === "" || unit === "px"
193
+ ? { kind: KIND_FLOAT, lanes: [parseFloat(m[1]!)] }
194
+ : { kind: KIND_FLOAT_UNIT, sig: unit, lanes: [parseFloat(m[1]!)] }
195
+ }
196
+ return { kind: KIND_STRING, str: v }
197
+ }
198
+ return null
199
+ }
200
+
201
+ /** Per-function argument counts of a UI transform list (degrees / px are the SDK's units). A
202
+ * translate named with a trailing `%` holds PERCENTS of the target's own box — a screen
203
+ * transition's pose (ui/transitions.ts), the only place that writes one. */
204
+ const TRANSFORM_FNS: Record<string, number> = {
205
+ translate: 2, translateX: 1, translateY: 1, rotate: 1, scale: 2, scaleX: 1, scaleY: 1, skewX: 1, skewY: 1,
206
+ "translate%": 2, "translateX%": 1, "translateY%": 1,
207
+ }
208
+
209
+ /** Parse `"translate(10px, 20px) rotate(45deg) scale(1.2)"` into a signature + lanes, or null when a
210
+ * function is unknown (the caller falls back to matrix decomposition on the host). `scale(2)` and
211
+ * `translate(5px)` complete their second argument like CSS.
212
+ *
213
+ * `percent`: a `%` in a translate is a percent of the target's box (`translateX(100%)` →
214
+ * `translateX%` [100]; a translate mixing the units splits into its two axes). Without it a `%`
215
+ * reads as px, as the style's own parser reads it — with a warning: only a screen transition
216
+ * measures a translate in percents. */
217
+ export const parseTransformList = (s: string, percent = false): { sig: string, lanes: number[] } | null => {
218
+ const sig: string[] = []
219
+ const lanes: number[] = []
220
+ const re = /([a-zA-Z]+)\s*\(([^)]*)\)/g
221
+ let m: RegExpExecArray | null
222
+ let consumed = 0
223
+ const src = s.trim()
224
+ if (src === "" || src === "none") return { sig: "", lanes: [] }
225
+ while ((m = re.exec(src)) !== null) {
226
+ const fn = m[1]!
227
+ const n = TRANSFORM_FNS[fn]
228
+ if (n === undefined) return null
229
+ const units: string[] = []
230
+ const args = m[2]!.split(",").map((a) => {
231
+ const u = UNIT_RE.exec(a)
232
+ units.push(u ? (u[2] ?? "") : "")
233
+ return u ? parseFloat(u[1]!) : NaN
234
+ })
235
+ if (args.some((a) => Number.isNaN(a)) || args.length === 0 || args.length > n) return null
236
+ if (args.length < n) { args.push(fn === "translate" ? 0 : args[0]!); units.push(units[0]!) }
237
+ consumed = re.lastIndex
238
+ if (fn.startsWith("translate") && units.includes("%")) {
239
+ if (!percent) {
240
+ warnOnce("transform:%", `transform: a % in ${fn}() is read as px — percents of the box work in a screen transition only`)
241
+ } else if (fn === "translate" && units[0] !== units[1]) {
242
+ sig.push(units[0] === "%" ? "translateX%" : "translateX", units[1] === "%" ? "translateY%" : "translateY")
243
+ lanes.push(...args)
244
+ continue
245
+ } else {
246
+ sig.push(fn + "%")
247
+ lanes.push(...args)
248
+ continue
249
+ }
250
+ }
251
+ sig.push(fn)
252
+ lanes.push(...args)
253
+ }
254
+ if (src.slice(consumed).trim() !== "") return null
255
+ return { sig: sig.join(","), lanes }
256
+ }
257
+
258
+ export const transformLanes = (sig: string): number => (sig === "" ? 0 : sig.split(",").reduce((n, fn) => n + (TRANSFORM_FNS[fn] ?? 0), 0))
259
+
260
+ const num3 = (v: unknown): number[] | null => {
261
+ if (typeof v === "number") return [v, v, v]
262
+ if (v && typeof v === "object") {
263
+ const o = v as any
264
+ if (typeof o.length === "number" && o.length >= 3) return [+o[0], +o[1], +o[2]]
265
+ if (typeof o.x === "number") return [o.x, o.y, o.z]
266
+ }
267
+ return null
268
+ }
269
+
270
+ const num4 = (v: unknown): number[] | null => {
271
+ if (v && typeof v === "object") {
272
+ const o = v as any
273
+ if (typeof o.length === "number" && o.length >= 4) return [+o[0], +o[1], +o[2], +o[3]]
274
+ if (typeof o.x === "number" && typeof o.w === "number") return [o.x, o.y, o.z, o.w]
275
+ }
276
+ return null
277
+ }
278
+
279
+ const num2 = (v: unknown): number[] | null => {
280
+ if (typeof v === "number") return [v, v]
281
+ if (v && typeof v === "object") {
282
+ const o = v as any
283
+ if (typeof o.length === "number" && o.length >= 2) return [+o[0], +o[1]]
284
+ if (typeof o.x === "number") return [o.x, o.y]
285
+ }
286
+ return null
287
+ }
288
+
289
+ export const vec3Value = (v: unknown) => { const l = num3(v); return l ? { kind: KIND_VEC3, lanes: l } : null }
290
+ export const quatValue = (v: unknown) => { const l = num4(v); return l ? { kind: KIND_QUAT, lanes: l } : null }
291
+ export const eulerValue = (v: unknown) => { const l = num3(v); return l ? { kind: KIND_EULER, lanes: l } : null }
292
+ export const vec2Value = (v: unknown) => { const l = num2(v); return l ? { kind: KIND_VEC2, lanes: l } : null }
293
+ export const floatValue = (v: unknown) => (typeof v === "number" ? { kind: KIND_FLOAT, lanes: [v] } : null)
294
+ export const colorValue = (v: unknown, toRgba: (c: any) => [number, number, number, number]) => {
295
+ if (typeof v === "string" || typeof v === "number" || Array.isArray(v)) return { kind: KIND_COLOR, lanes: toRgba(v) }
296
+ return null
297
+ }
298
+
299
+ /** A free value for animate(): number → FLOAT, a color string → COLOR, arrays / vectors by length
300
+ * (2 → VEC2, 3 → VEC3, 4 → QUAT, 16 → MAT4 decomposed into position · quaternion · scale). */
301
+ export const valueValue = (v: unknown): ({ kind: number } & TweenValue) | null => {
302
+ if (typeof v === "number") return { kind: KIND_FLOAT, lanes: [v] }
303
+ if (typeof v === "string") return { kind: KIND_COLOR, lanes: Color.toRgba01(v) }
304
+ if (v && typeof v === "object") {
305
+ const o = v as any
306
+ const arr: ArrayLike<number> | undefined = typeof o.length === "number" ? o : Array.isArray(o.m) && o.m.length === 16 ? o.m : undefined
307
+ if (arr) {
308
+ if (arr.length === 2) return { kind: KIND_VEC2, lanes: [+arr[0]!, +arr[1]!] }
309
+ if (arr.length === 3) return { kind: KIND_VEC3, lanes: [+arr[0]!, +arr[1]!, +arr[2]!] }
310
+ if (arr.length === 4) return { kind: KIND_QUAT, lanes: [+arr[0]!, +arr[1]!, +arr[2]!, +arr[3]!] }
311
+ if (arr.length === 16) {
312
+ const d = new Mat4(arr as any).decompose()
313
+ return { kind: KIND_MAT4, lanes: [d.position.x, d.position.y, d.position.z, d.rotation.x, d.rotation.y, d.rotation.z, d.rotation.w, d.scale.x, d.scale.y, d.scale.z] }
314
+ }
315
+ return null
316
+ }
317
+ if (typeof o.x === "number") {
318
+ if (typeof o.w === "number") return { kind: KIND_QUAT, lanes: [o.x, o.y, o.z, o.w] }
319
+ if (typeof o.z === "number") return { kind: KIND_VEC3, lanes: [o.x, o.y, o.z] }
320
+ return { kind: KIND_VEC2, lanes: [o.x, o.y] }
321
+ }
322
+ }
323
+ return null
324
+ }
325
+
326
+ /** Is this bag value a KEYFRAME ARRAY for a vector prop, as opposed to one vector? A vector value
327
+ * is an array of NUMBERS (`[0, 2, 0]`); anything else in the array is a list of keyframes —
328
+ * `[[0,0,0], [0,2,0]]`, `[new Vec3(), …]`, and the UI's `color: ['#fff', '#e29a22']`. */
329
+ export const isVectorKeyframes = (v: unknown): boolean =>
330
+ Array.isArray(v) && v.length > 0 && typeof v[0] !== "number"
331
+
332
+ // ---- track construction ------------------------------------------------------------------------
333
+
334
+ /** Build one track's keyframes from a bag value: a single value = one explicit key (the other end
335
+ * captured), an array = explicit keys spaced by `times` or evenly. `fromCurrent` says which end is
336
+ * implicit (animateTo: the first; animateFrom: the last). Returns null when a value can't be
337
+ * animated (already warned). */
338
+ export const makeTrack = (
339
+ target: TweenTarget, prop: string, raw: unknown, meta: TweenMeta, atMs: number, fromCurrent: boolean, vectorProp: boolean,
340
+ ): Track | null => {
341
+ const channel = target._tweenChannel(prop)
342
+ if (!channel) {
343
+ warnOnce(`prop:${prop}`, `"${prop}" is not animatable on this target — set it directly`)
344
+ return null
345
+ }
346
+ const values: unknown[] = Array.isArray(raw) && (!vectorProp || isVectorKeyframes(raw)) ? raw : [raw]
347
+ const easing = easingFor(meta.easing, prop)
348
+ const norm = values.map((v) => channel.value(v))
349
+ if (norm.some((n) => n === null)) {
350
+ warnOnce(`value:${prop}`, `"${prop}" got a value that cannot be animated — set it directly`)
351
+ return null
352
+ }
353
+ const ns = norm as NonNullable<(typeof norm)[number]>[]
354
+
355
+ // One kind per track. Mismatched units / transform signatures degrade to DISCRETE (a flip at the
356
+ // key, like CSS), never to garbage interpolation.
357
+ let kind = ns[0]!.kind
358
+ let sig = ns[0]!.sig
359
+ for (const n of ns) {
360
+ if (n.kind !== kind || n.sig !== sig) {
361
+ // FLOAT vs FLOAT_UNIT(px) can't happen (px normalizes to FLOAT); anything else is discrete
362
+ kind = KIND_DISCRETE
363
+ sig = undefined
364
+ break
365
+ }
366
+ }
367
+ const lanes = kind === KIND_TRANSFORM ? transformLanes(sig ?? "") : LANES[kind]!
368
+ const toValue = (n: NonNullable<(typeof norm)[number]>): TweenValue =>
369
+ kind === KIND_DISCRETE && "lanes" in n ? { str: String(n.lanes[0]) } : ("str" in n ? { str: n.str } : { lanes: n.lanes })
370
+
371
+ const explicit = ns.length
372
+ const total = explicit + (explicit === 1 ? 1 : 0) // a single value pairs with a captured end
373
+ const keys: Keyframe[] = []
374
+ const times = meta.times
375
+ for (let i = 0; i < total; i++) {
376
+ let t: number
377
+ if (times && times.length === total) t = times[i]!
378
+ else t = total === 1 ? 1 : i / (total - 1)
379
+ let value: TweenValue | undefined
380
+ let raw: unknown
381
+ if (explicit === 1) {
382
+ // animateTo: [captured, v]; animateFrom: [v, captured]
383
+ const isExplicit = fromCurrent ? i === 1 : i === 0
384
+ value = isExplicit ? toValue(ns[0]!) : undefined
385
+ raw = isExplicit ? values[0] : undefined
386
+ } else {
387
+ value = toValue(ns[i]!)
388
+ raw = values[i]
389
+ }
390
+ keys.push({ t, easing, value, raw })
391
+ }
392
+ // animateFrom with an explicit array: the element's own style is the implicit LAST key
393
+ if (!fromCurrent && explicit > 1) {
394
+ for (const k of keys) k.t *= (total) / (total + 1)
395
+ keys.push({ t: 1, easing, value: undefined })
396
+ }
397
+
398
+ return {
399
+ target, prop, channel, kind, lanes, sig,
400
+ atMs: atMs + (meta.delay ?? 0),
401
+ durMs: Math.max(0, meta.duration ?? DEFAULT_DURATION_MS),
402
+ // animateFrom never commits (its last key is the element's own state); loops never commit
403
+ commit: fromCurrent && meta.commit !== false && iterationsOf(meta.loop) === 1,
404
+ keys,
405
+ }
406
+ }
407
+
408
+ /** Every (prop → value) entry of a bag, meta keys stripped. */
409
+ export const bagProps = (bag: Record<string, unknown>): [string, unknown][] =>
410
+ Object.entries(bag).filter(([k, v]) => !TWEEN_META.has(k) && v !== undefined)
411
+
412
+ // ---- JS evaluation (the old-host path of VALUE tracks; the C evaluator is the reference) ----------
413
+
414
+ const slerpInto = (out: number[], o: number, a: number[], ai: number, b: number[], bi: number, s: number): void => {
415
+ let bx = b[bi]!, by = b[bi + 1]!, bz = b[bi + 2]!, bw = b[bi + 3]!
416
+ let cosom = a[ai]! * bx + a[ai + 1]! * by + a[ai + 2]! * bz + a[ai + 3]! * bw
417
+ if (cosom < 0) { cosom = -cosom; bx = -bx; by = -by; bz = -bz; bw = -bw }
418
+ let k0: number, k1: number
419
+ if (1 - cosom > 1e-6) {
420
+ const omega = Math.acos(Math.min(1, cosom)), sinom = Math.sin(omega)
421
+ k0 = Math.sin((1 - s) * omega) / sinom
422
+ k1 = Math.sin(s * omega) / sinom
423
+ } else { k0 = 1 - s; k1 = s }
424
+ out[o] = k0 * a[ai]! + k1 * bx; out[o + 1] = k0 * a[ai + 1]! + k1 * by; out[o + 2] = k0 * a[ai + 2]! + k1 * bz; out[o + 3] = k0 * a[ai + 3]! + k1 * bw
425
+ }
426
+
427
+ /** The value of a numeric track at local time `ms` (the C evaluator's rule: first key holds before,
428
+ * last after; eased segment between). `out` receives `track.lanes` numbers. */
429
+ export const evaluateTrack = (track: Track, ms: number, out: number[]): void => {
430
+ const u = track.durMs <= 0 ? (ms >= track.atMs ? 1 : 0) : Math.min(1, Math.max(0, (ms - track.atMs) / track.durMs))
431
+ const keys = track.keys
432
+ const lanesOfKey = (k: Keyframe): number[] => (k.value && "lanes" in k.value ? k.value.lanes : [])
433
+ if (keys.length === 0) return
434
+ if (u <= keys[0]!.t || keys.length === 1) { const l = lanesOfKey(keys[0]!); for (let i = 0; i < track.lanes; i++) out[i] = l[i] ?? 0; return }
435
+ const last = keys[keys.length - 1]!
436
+ if (u >= last.t) { const l = lanesOfKey(last); for (let i = 0; i < track.lanes; i++) out[i] = l[i] ?? 0; return }
437
+ let i = 0
438
+ while (i + 1 < keys.length && keys[i + 1]!.t <= u) i++
439
+ const a = keys[i]!, b = keys[i + 1]!
440
+ const span = b.t - a.t
441
+ const s = evalEasing(a.easing, span > 0 ? (u - a.t) / span : 1)
442
+ const la = lanesOfKey(a), lb = lanesOfKey(b)
443
+ if (track.kind === KIND_QUAT) { slerpInto(out, 0, la, 0, lb, 0, s); return }
444
+ if (track.kind === KIND_MAT4) {
445
+ for (let k = 0; k < 3; k++) out[k] = la[k]! + (lb[k]! - la[k]!) * s
446
+ slerpInto(out, 3, la, 3, lb, 3, s)
447
+ for (let k = 7; k < 10; k++) out[k] = la[k]! + (lb[k]! - la[k]!) * s
448
+ return
449
+ }
450
+ for (let k = 0; k < track.lanes; k++) out[k] = (la[k] ?? 0) + ((lb[k] ?? 0) - (la[k] ?? 0)) * s
451
+ }
452
+
453
+ // ---- the blob ----------------------------------------------------------------------------------
454
+
455
+ export type TweenBlob = { data: Float32Array, strings: string[], targets: unknown[] }
456
+
457
+ /** Flatten a spec for `_creatorTree.tweenCreate`. Tracks whose target has no host id right now are
458
+ * dropped (the animation still runs its clock, calls and finish). */
459
+ export const buildBlob = (spec: TweenSpec): TweenBlob => {
460
+ const strings: string[] = []
461
+ const strIndex = new Map<string, number>()
462
+ const str = (s: string): number => {
463
+ let i = strIndex.get(s)
464
+ if (i === undefined) { i = strings.length; strings.push(s); strIndex.set(s, i) }
465
+ return i
466
+ }
467
+ const tables: Float32Array[] = []
468
+ const tableIndex = (e: EasingSpec): number => {
469
+ const i = tables.indexOf(e.table!)
470
+ if (i >= 0) return i
471
+ tables.push(e.table!)
472
+ return tables.length - 1
473
+ }
474
+
475
+ const tracks = spec.tracks.map((t) => ({ t, id: t.channel.id() })).filter((x) => !!x.id)
476
+ const targets: unknown[] = []
477
+ const targetIndex = (id: unknown): number => {
478
+ let i = targets.indexOf(id)
479
+ if (i < 0) { i = targets.length; targets.push(id) }
480
+ return i
481
+ }
482
+ const out: number[] = []
483
+ out.push(BLOB_VERSION, spec.clock, spec.durationMs, spec.delayMs, spec.iterations, spec.pingPong ? 1 : 0, spec.rate,
484
+ tracks.length, spec.calls.length, 0 /* nTables, patched */)
485
+ for (const c of spec.calls) out.push(c)
486
+ const tablesAt = out.length
487
+ for (const { t, id } of tracks) {
488
+ out.push(t.channel.domain, targetIndex(id), str(t.prop), t.kind, t.lanes, t.atMs, t.durMs, t.commit ? 1 : 0, t.keys.length, t.sig !== undefined ? str(t.sig) : -1)
489
+ for (const k of t.keys) {
490
+ const e = k.easing
491
+ const p: number[] = [e.p[0], e.p[1], e.p[2], e.p[3]]
492
+ if (e.table) p[0] = tableIndex(e)
493
+ out.push(k.t, e.kind, p[0]!, p[1]!, p[2]!, p[3]!, k.value ? 1 : 0)
494
+ if (!k.value) { for (let i = 0; i < t.lanes; i++) out.push(0) }
495
+ else if ("str" in k.value) { out.push(str(k.value.str)); for (let i = 1; i < t.lanes; i++) out.push(0) }
496
+ else { for (let i = 0; i < t.lanes; i++) out.push(k.value.lanes[i] ?? 0) }
497
+ }
498
+ }
499
+ // splice the tables in after the calls
500
+ const tableData: number[] = []
501
+ for (const tb of tables) for (let i = 0; i < EASE_TABLE_SIZE; i++) tableData.push(tb[i]!)
502
+ out.splice(tablesAt, 0, ...tableData)
503
+ out[9] = tables.length
504
+ return { data: Float32Array.from(out), strings, targets }
505
+ }