lecodes-sdk 1.0.0 → 1.2.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 (94) hide show
  1. package/dist/global.d.ts +18 -4
  2. package/dist/inject.js +260 -361
  3. package/dist/types/animate/tween/Animation.d.ts +69 -0
  4. package/dist/types/animate/tween/Timeline.d.ts +55 -0
  5. package/dist/types/animate/tween/animateValue.d.ts +27 -0
  6. package/dist/types/animate/tween/easing.d.ts +29 -0
  7. package/dist/types/animate/tween/spec.d.ts +178 -0
  8. package/dist/types/g2/Node2D.d.ts +16 -0
  9. package/dist/types/g2/Sprite.d.ts +11 -1
  10. package/dist/types/gl/Camera.d.ts +15 -1
  11. package/dist/types/gl/Foliage.d.ts +47 -0
  12. package/dist/types/gl/Geometry.d.ts +24 -0
  13. package/dist/types/gl/Light.d.ts +28 -7
  14. package/dist/types/gl/Lightmap.d.ts +90 -60
  15. package/dist/types/gl/Material.d.ts +28 -20
  16. package/dist/types/gl/Model.d.ts +7 -5
  17. package/dist/types/gl/Node.d.ts +18 -0
  18. package/dist/types/gl/Particles.d.ts +55 -1
  19. package/dist/types/gl/Scene.d.ts +20 -0
  20. package/dist/types/gl/animation/AnimationClip.d.ts +19 -0
  21. package/dist/types/gl/animation/Animator.d.ts +27 -0
  22. package/dist/types/gl/animation/DynamicBone.d.ts +19 -8
  23. package/dist/types/gl/animation/IK.d.ts +86 -30
  24. package/dist/types/gl/animation/Locomotion.d.ts +52 -3
  25. package/dist/types/gl/animation/Warp.d.ts +2 -1
  26. package/dist/types/gl/animation/core.d.ts +35 -4
  27. package/dist/types/gl/physics/Ragdoll.d.ts +87 -12
  28. package/dist/types/gl/terrain/Terrain.d.ts +12 -2
  29. package/dist/types/inject.d.ts +8 -2
  30. package/dist/types/runtime/input.d.ts +7 -0
  31. package/dist/types/scene/defineScene.d.ts +44 -32
  32. package/dist/types/ui/UIButton.d.ts +3 -1
  33. package/dist/types/ui/UIInput.d.ts +5 -1
  34. package/dist/types/ui/UINode.d.ts +24 -24
  35. package/dist/types.json +1 -1
  36. package/package.json +1 -1
  37. package/prompts/README.md +142 -142
  38. package/prompts/core-design.md +27 -4
  39. package/prompts/core.md +35 -6
  40. package/prompts/dist/2d-game.md +197 -408
  41. package/prompts/dist/3d-app.md +166 -491
  42. package/prompts/dist/ar-app.md +163 -373
  43. package/prompts/dist/design.md +87 -83
  44. package/prompts/dist/ui-app.md +136 -325
  45. package/prompts/select.ts +19 -4
  46. package/src/animate/tween/Animation.ts +378 -0
  47. package/src/animate/tween/Timeline.ts +175 -0
  48. package/src/animate/tween/animateValue.ts +100 -0
  49. package/src/animate/tween/easing.ts +172 -0
  50. package/src/animate/tween/spec.ts +479 -0
  51. package/src/audio/audio.ts +161 -161
  52. package/src/bridges.d.ts +235 -65
  53. package/src/compile/__tests__/assetMacro.test.ts +26 -0
  54. package/src/compile/__tests__/detectEntry.test.ts +19 -0
  55. package/src/compile/__tests__/serverSplit.test.ts +27 -0
  56. package/src/compile/bundler.ts +34 -4
  57. package/src/compile/compileProject.ts +31 -1
  58. package/src/compile/detectEntry.ts +8 -3
  59. package/src/compile/index.ts +2 -0
  60. package/src/compile/serverSplit.ts +9 -3
  61. package/src/g2/Node2D.ts +38 -0
  62. package/src/g2/Sprite.ts +20 -1
  63. package/src/gl/Camera.ts +34 -1
  64. package/src/gl/CameraPlace.ts +52 -52
  65. package/src/gl/Foliage.ts +102 -0
  66. package/src/gl/Geometry.ts +393 -348
  67. package/src/gl/Light.ts +49 -16
  68. package/src/gl/Lightmap.ts +439 -275
  69. package/src/gl/Material.ts +59 -47
  70. package/src/gl/Mesh.ts +120 -120
  71. package/src/gl/Model.ts +23 -12
  72. package/src/gl/Node.ts +39 -0
  73. package/src/gl/Particles.ts +80 -2
  74. package/src/gl/Scene.ts +34 -1
  75. package/src/gl/animation/AnimationClip.ts +52 -0
  76. package/src/gl/animation/Animator.ts +42 -2
  77. package/src/gl/animation/DynamicBone.ts +35 -12
  78. package/src/gl/animation/IK.ts +173 -152
  79. package/src/gl/animation/Locomotion.ts +72 -8
  80. package/src/gl/animation/Playback.ts +5 -4
  81. package/src/gl/animation/Warp.ts +5 -2
  82. package/src/gl/animation/core.ts +65 -4
  83. package/src/gl/physics/Ragdoll.ts +451 -272
  84. package/src/gl/scenarios.ts +291 -291
  85. package/src/gl/terrain/Terrain.ts +33 -2
  86. package/src/inject.ts +236 -226
  87. package/src/runtime/input.ts +11 -0
  88. package/src/scene/defineScene.ts +72 -62
  89. package/src/scene/gizmos.ts +148 -148
  90. package/src/ui/UIButton.ts +2 -2
  91. package/src/ui/UIInput.ts +3 -3
  92. package/src/ui/UINode.ts +61 -36
  93. package/dist/types/animate/animate.d.ts +0 -20
  94. package/src/animate/animate.ts +0 -238
@@ -0,0 +1,172 @@
1
+ // Easing vocabulary of the keyframe animation core (docs/timeline-plan.md §2.1). Every form the
2
+ // options bag accepts is normalized here into an `EasingSpec` the native evaluator (creator-tween)
3
+ // understands: linear, a NAMED curve (evaluated natively by id — the formulas below are the twin,
4
+ // used for the old-host path and tests), a cubic bezier, `steps(n)`, or a sampled TABLE for an
5
+ // arbitrary `Easing` function (65 points, linear between them — the only way a JS function can
6
+ // cross the bridge).
7
+
8
+ import type { Easing } from "../easings"
9
+
10
+ export const EASE_LINEAR = 0
11
+ export const EASE_NAMED = 1
12
+ export const EASE_BEZIER = 2
13
+ export const EASE_STEPS = 3
14
+ export const EASE_TABLE = 4
15
+
16
+ /** Samples per table (t = i / 64). Interpolation error for the smooth built-ins is < 1e-3. */
17
+ export const EASE_TABLE_SIZE = 65
18
+
19
+ export type EasingSpec = {
20
+ kind: number,
21
+ /** kind-specific parameters: NAMED [id], BEZIER [x1, y1, x2, y2], STEPS [n, jumpAtStart], TABLE [tableIndex] */
22
+ p: [number, number, number, number],
23
+ /** TABLE only — the samples, appended to the blob's table section by the builder. */
24
+ table?: Float32Array,
25
+ }
26
+
27
+ /** A curve in the options bag: a name, `"cubic-bezier(x1,y1,x2,y2)"`, `"steps(n)"`, a bezier tuple,
28
+ * or any `Easing` function. */
29
+ export type EasingInput = string | readonly [number, number, number, number] | Easing
30
+
31
+ // ---- named curves (ids are the native contract — append, never renumber) ------------------------
32
+
33
+ const c1 = 1.70158
34
+ const c2 = c1 * 1.525
35
+ const c3 = c1 + 1
36
+ const c4 = (2 * Math.PI) / 3
37
+ const c5 = (2 * Math.PI) / 4.5
38
+
39
+ const outBounce = (x: number): number => {
40
+ const n1 = 7.5625, d1 = 2.75
41
+ if (x < 1 / d1) return n1 * x * x
42
+ if (x < 2 / d1) return n1 * (x -= 1.5 / d1) * x + 0.75
43
+ if (x < 2.5 / d1) return n1 * (x -= 2.25 / d1) * x + 0.9375
44
+ return n1 * (x -= 2.625 / d1) * x + 0.984375
45
+ }
46
+
47
+ /** id → (name, formula). The formulas are the standard easings.net set; creator-tween's
48
+ * `ctweenEaseNamed` implements the same list by id. */
49
+ export const NAMED_EASINGS: ReadonlyArray<readonly [string, Easing]> = [
50
+ ["linear", (x) => x], // 0
51
+ ["inSine", (x) => 1 - Math.cos((x * Math.PI) / 2)], // 1
52
+ ["outSine", (x) => Math.sin((x * Math.PI) / 2)], // 2
53
+ ["inOutSine", (x) => -(Math.cos(Math.PI * x) - 1) / 2], // 3
54
+ ["inQuad", (x) => x * x], // 4
55
+ ["outQuad", (x) => 1 - (1 - x) * (1 - x)], // 5
56
+ ["inOutQuad", (x) => (x < 0.5 ? 2 * x * x : 1 - (-2 * x + 2) ** 2 / 2)], // 6
57
+ ["inCubic", (x) => x * x * x], // 7
58
+ ["outCubic", (x) => 1 - (1 - x) ** 3], // 8
59
+ ["inOutCubic", (x) => (x < 0.5 ? 4 * x * x * x : 1 - (-2 * x + 2) ** 3 / 2)], // 9
60
+ ["inQuart", (x) => x ** 4], // 10
61
+ ["outQuart", (x) => 1 - (1 - x) ** 4], // 11
62
+ ["inOutQuart", (x) => (x < 0.5 ? 8 * x ** 4 : 1 - (-2 * x + 2) ** 4 / 2)], // 12
63
+ ["inExpo", (x) => (x === 0 ? 0 : 2 ** (10 * x - 10))], // 13
64
+ ["outExpo", (x) => (x === 1 ? 1 : 1 - 2 ** (-10 * x))], // 14
65
+ ["inOutExpo", (x) => (x === 0 ? 0 : x === 1 ? 1 : x < 0.5 ? 2 ** (20 * x - 10) / 2 : (2 - 2 ** (-20 * x + 10)) / 2)], // 15
66
+ ["inCirc", (x) => 1 - Math.sqrt(1 - x * x)], // 16
67
+ ["outCirc", (x) => Math.sqrt(1 - (x - 1) ** 2)], // 17
68
+ ["inOutCirc", (x) => (x < 0.5 ? (1 - Math.sqrt(1 - (2 * x) ** 2)) / 2 : (Math.sqrt(1 - (-2 * x + 2) ** 2) + 1) / 2)], // 18
69
+ ["inBack", (x) => c3 * x * x * x - c1 * x * x], // 19
70
+ ["outBack", (x) => 1 + c3 * (x - 1) ** 3 + c1 * (x - 1) ** 2], // 20
71
+ ["inOutBack", (x) => (x < 0.5 ? ((2 * x) ** 2 * ((c2 + 1) * 2 * x - c2)) / 2 : ((2 * x - 2) ** 2 * ((c2 + 1) * (x * 2 - 2) + c2) + 2) / 2)], // 21
72
+ ["inElastic", (x) => (x === 0 ? 0 : x === 1 ? 1 : -(2 ** (10 * x - 10)) * Math.sin((x * 10 - 10.75) * c4))], // 22
73
+ ["outElastic", (x) => (x === 0 ? 0 : x === 1 ? 1 : 2 ** (-10 * x) * Math.sin((x * 10 - 0.75) * c4) + 1)], // 23
74
+ ["inOutElastic", (x) => (x === 0 ? 0 : x === 1 ? 1 : x < 0.5 ? -(2 ** (20 * x - 10) * Math.sin((20 * x - 11.125) * c5)) / 2 : (2 ** (-20 * x + 10) * Math.sin((20 * x - 11.125) * c5)) / 2 + 1)], // 24
75
+ ["inBounce", (x) => 1 - outBounce(1 - x)], // 25
76
+ ["outBounce", outBounce], // 26
77
+ ["inOutBounce", (x) => (x < 0.5 ? (1 - outBounce(1 - 2 * x)) / 2 : (1 + outBounce(2 * x - 1)) / 2)], // 27
78
+ ["smoothstep", (x) => x * x * (3 - 2 * x)], // 28 — the $class / onPressed default
79
+ ]
80
+
81
+ /** Short spellings → canonical names. `in`/`out`/`inOut` are the quadratics, matching the SDK's
82
+ * `easeIn`/`easeOut`/`easeInOut` functions. */
83
+ const ALIASES: Record<string, string> = {
84
+ in: "inQuad", out: "outQuad", inOut: "inOutQuad",
85
+ easeIn: "inQuad", easeOut: "outQuad", easeInOut: "inOutQuad",
86
+ "ease-in": "inQuad", "ease-out": "outQuad", "ease-in-out": "inOutQuad",
87
+ }
88
+
89
+ const namedId = (name: string): number => {
90
+ const canon = ALIASES[name] ?? name
91
+ for (let i = 0; i < NAMED_EASINGS.length; i++) if (NAMED_EASINGS[i]![0] === canon) return i
92
+ return -1
93
+ }
94
+
95
+ /** The CSS `ease` curve — what a bare `"ease"` means. */
96
+ const CSS_EASE: [number, number, number, number] = [0.25, 0.1, 0.25, 1]
97
+
98
+ const LINEAR: EasingSpec = { kind: EASE_LINEAR, p: [0, 0, 0, 0] }
99
+
100
+ /** Sample an `Easing` function into a table. Endpoints are forced exact so a curve that is
101
+ * numerically 0.9999 at 1 still lands on the keyframe. */
102
+ export const sampleEasing = (fn: Easing): Float32Array => {
103
+ const t = new Float32Array(EASE_TABLE_SIZE)
104
+ for (let i = 0; i < EASE_TABLE_SIZE; i++) t[i] = fn(i / (EASE_TABLE_SIZE - 1))
105
+ t[0] = 0
106
+ t[EASE_TABLE_SIZE - 1] = 1
107
+ return t
108
+ }
109
+
110
+ /** Normalize one easing input. Unknown strings fall back to linear with a console warning (a typo
111
+ * should be visible, not a silent snap). */
112
+ export const parseEasing = (input: EasingInput | undefined): EasingSpec => {
113
+ if (input === undefined) return LINEAR
114
+ if (typeof input === "function") return { kind: EASE_TABLE, p: [0, 0, 0, 0], table: sampleEasing(input) }
115
+ if (Array.isArray(input)) {
116
+ const [x1, y1, x2, y2] = input as readonly number[]
117
+ return { kind: EASE_BEZIER, p: [x1!, y1!, x2!, y2!] }
118
+ }
119
+ const s = (input as string).trim()
120
+ if (s === "linear") return LINEAR
121
+ if (s === "ease") return { kind: EASE_BEZIER, p: CSS_EASE }
122
+ if (s === "steps") return { kind: EASE_STEPS, p: [1, 0, 0, 0] }
123
+ let m = /^steps\(\s*(\d+)\s*(?:,\s*(start|end|jump-start|jump-end)\s*)?\)$/.exec(s)
124
+ if (m) return { kind: EASE_STEPS, p: [Math.max(1, parseInt(m[1]!, 10)), m[2] === "start" || m[2] === "jump-start" ? 1 : 0, 0, 0] }
125
+ m = /^cubic-bezier\(\s*([-\d.e]+)\s*,\s*([-\d.e]+)\s*,\s*([-\d.e]+)\s*,\s*([-\d.e]+)\s*\)$/.exec(s)
126
+ if (m) return { kind: EASE_BEZIER, p: [parseFloat(m[1]!), parseFloat(m[2]!), parseFloat(m[3]!), parseFloat(m[4]!)] }
127
+ const id = namedId(s)
128
+ if (id === 0) return LINEAR
129
+ if (id > 0) return { kind: EASE_NAMED, p: [id, 0, 0, 0] }
130
+ console.warn(`[tween] unknown easing "${s}" — using linear`)
131
+ return LINEAR
132
+ }
133
+
134
+ // ---- evaluation twin (old-host path + tests) ---------------------------------------------------
135
+
136
+ const bezierY = (x: number, x1: number, y1: number, x2: number, y2: number): number => {
137
+ // Newton on the x polynomial, then y at that t — the same method creator-tween uses.
138
+ const ax = 1 - 3 * x2 + 3 * x1, bx = 3 * x2 - 6 * x1, cx = 3 * x1
139
+ const ay = 1 - 3 * y2 + 3 * y1, by = 3 * y2 - 6 * y1, cy = 3 * y1
140
+ let t = x
141
+ for (let i = 0; i < 8; i++) {
142
+ const xt = ((ax * t + bx) * t + cx) * t - x
143
+ const dx = (3 * ax * t + 2 * bx) * t + cx
144
+ if (Math.abs(xt) < 1e-6) break
145
+ if (Math.abs(dx) < 1e-6) break
146
+ t -= xt / dx
147
+ }
148
+ t = Math.min(1, Math.max(0, t))
149
+ return ((ay * t + by) * t + cy) * t
150
+ }
151
+
152
+ /** Evaluate an `EasingSpec` at `u` ∈ [0, 1] — the JS twin of `ctweenEase`. */
153
+ export const evalEasing = (e: EasingSpec, u: number): number => {
154
+ if (u <= 0) return e.kind === EASE_STEPS && e.p[1] === 1 ? 1 / e.p[0] : 0
155
+ if (u >= 1) return 1
156
+ switch (e.kind) {
157
+ case EASE_NAMED: return NAMED_EASINGS[e.p[0]]![1](u)
158
+ case EASE_BEZIER: return bezierY(u, e.p[0], e.p[1], e.p[2], e.p[3])
159
+ case EASE_STEPS: {
160
+ const n = e.p[0]
161
+ return e.p[1] === 1 ? Math.min(1, Math.ceil(u * n) / n) : Math.floor(u * n) / n
162
+ }
163
+ case EASE_TABLE: {
164
+ const t = e.table!
165
+ const f = u * (EASE_TABLE_SIZE - 1)
166
+ const i = Math.floor(f)
167
+ const a = t[i]!, b = t[Math.min(i + 1, EASE_TABLE_SIZE - 1)]!
168
+ return a + (b - a) * (f - i)
169
+ }
170
+ default: return u
171
+ }
172
+ }
@@ -0,0 +1,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 { 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
+ }