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,378 +1,262 @@
1
- // The animation HANDLE (docs/timeline-plan.md §2.2): what `animateTo` / `animateFrom` / `Timeline`
2
- // return. Owns one spec; every `play()` builds the blob (target ids resolve at play time), hands it
3
- // to the host (`_creatorUI.tweenCreate`) and gets events back (`registerTweenEvent`: time-callbacks
4
- // and finish). On a host without the tween core the UI tracks fall back to the old
5
- // `animateTo` / `animateFrom` bridge pair (single-value tweens only) and the clock is a timer —
6
- // see `fallback` below.
7
-
8
- import { CLOCK_GAME, CLOCK_UI, DOM_UI, DOM_VALUE, buildBlob, evaluateTrack, type Track, type TweenSpec } from "./spec"
9
-
10
- const OP_PLAY = 0
11
- const OP_PAUSE = 1
12
- const OP_RESUME = 2
13
- const OP_CANCEL = 3
14
- const OP_FINISH = 4
15
-
16
- const EV_CALL = 0
17
- const EV_FINISH = 1
18
- const EV_VALUE = 2 // (id, 2, slot, lane0, lane1, …) — a VALUE track's frame, synchronous from the host
19
-
20
- /** Playback control shared by element tweens and timelines. Times are **milliseconds**. */
21
- export interface Animation {
22
- /** One iteration, ms. */
23
- readonly duration: number
24
- /** Position inside the current iteration, ms. Writable = seek. */
25
- time: number
26
- /** 0..1 of the whole animation (iterations included). Writable = seek. */
27
- progress: number
28
- /** Playback rate; negative runs backwards, 0 freezes. */
29
- rate: number
30
- readonly playing: boolean
31
- /** Resolves `true` when the animation reaches its end, `false` when cancelled, replaced or replayed. Never rejects. */
32
- readonly finished: Promise<boolean>
33
- /** Start from t = 0 (a running animation restarts). */
34
- play(): this
35
- pause(): this
36
- resume(): this
37
- /** Jump to `ms`; playback state is unchanged. */
38
- seek(ms: number): this
39
- /** Jump to the end: values land, commits apply, `finished` resolves `true`. */
40
- finish(): this
41
- /** Stop where it is — no commit, `finished` resolves `false`. */
42
- cancel(): this
43
- onFinish(fn: (done: boolean) => void): this
44
- }
45
-
46
- const hasCore = (): boolean => typeof _creatorUI !== "undefined" && typeof (_creatorUI as any).tweenCreate === "function"
47
-
48
- // ---- host events -------------------------------------------------------------------------------
49
-
50
- const live = new Map<number, TweenAnimation>()
51
- let eventsRegistered = false
52
- const ensureEvents = (): void => {
53
- if (eventsRegistered) return
54
- eventsRegistered = true
55
- ;(_creatorUI as any).registerTweenEvent?.(function (this: unknown, id: number, kind: number, index: number) {
56
- const a = live.get(id)
57
- if (!a) return
58
- if (kind === EV_CALL) a._fireCall(index)
59
- else if (kind === EV_FINISH) a._hostFinished()
60
- else if (kind === EV_VALUE) {
61
- // lanes ride as plain number arguments (no per-frame array on either side)
62
- const n = arguments.length - 3
63
- for (let i = 0; i < n; i++) valueScratch[i] = arguments[i + 3] as number
64
- a._fireValue(index, valueScratch)
65
- }
66
- })
67
- }
68
-
69
- /** Reused lane buffer for VALUE deliveries (the host's largest kind is 10 lanes). */
70
- const valueScratch = new Float32Array(16)
71
-
72
- /** The concrete handle — what the SDK instantiates behind `animateTo` / `animateFrom` / `Timeline`.
73
- * User code sees the {@link Animation} interface; the class itself must stay in the public
74
- * declarations because `TimelineImpl` extends it — marking it internal (even mentioning the tag in
75
- * this comment: the strip is a substring match) drops the timeline's playback members. */
76
- export class TweenAnimation implements Animation {
77
- /** @internal */
78
- _spec: TweenSpec
79
- /** @internal Time-callbacks by call index (the spec's `calls[i]`). */
80
- _calls: Array<() => void> = []
81
- /** @internal VALUE-track deliverers by slot (`animate()` / `Timeline.animate`). */
82
- _values = new Map<number, (lanes: Float32Array) => void>()
83
-
84
- private _id = 0 // host id, 0 = not created
85
- private _started = false // play() ran at least once (the first play keeps the constructor's promise)
86
- private _endTime = 0 // `time` after the host freed a finished animation
87
- private _playing = false
88
- private _rate: number
89
- private _finished!: Promise<boolean>
90
- private _resolve!: (done: boolean) => void
91
- private _settled = true
92
- private _listeners: Array<(done: boolean) => void> = []
93
- private _fb?: Fallback
94
-
95
- constructor(spec: TweenSpec) {
96
- this._spec = spec
97
- this._rate = spec.rate
98
- this._newPromise()
99
- }
100
-
101
- private _newPromise(): void {
102
- this._finished = new Promise<boolean>((r) => { this._resolve = r })
103
- this._settled = false
104
- }
105
-
106
- private _settle(done: boolean): void {
107
- if (this._settled) return
108
- this._settled = true
109
- this._playing = false
110
- this._resolve(done)
111
- for (const fn of this._listeners) fn(done)
112
- }
113
-
114
- get duration(): number { return this._spec.durationMs }
115
- get playing(): boolean { return this._playing }
116
- get finished(): Promise<boolean> { return this._finished }
117
-
118
- get rate(): number { return this._rate }
119
- set rate(r: number) {
120
- this._rate = r
121
- if (this._id) (_creatorUI as any).tweenSetRate(this._id, r)
122
- this._fb?.setRate(r)
123
- }
124
-
125
- get time(): number {
126
- if (this._id) return (_creatorUI as any).tweenGetTime?.(this._id) ?? 0
127
- return this._fb?.time ?? this._endTime
128
- }
129
- set time(ms: number) { this.seek(ms) }
130
-
131
- get progress(): number {
132
- const total = this._totalMs()
133
- return total > 0 ? Math.min(1, Math.max(0, this._elapsedMs() / total)) : 1
134
- }
135
- set progress(p: number) {
136
- const total = this._totalMs()
137
- this._seekTotal(Math.min(1, Math.max(0, p)) * total)
138
- }
139
-
140
- private _totalMs(): number {
141
- const s = this._spec
142
- const n = s.iterations < 0 ? 1 : s.iterations
143
- return s.durationMs * n * (s.pingPong && s.iterations !== 1 ? 2 : 1)
144
- }
145
- private _elapsedMs(): number {
146
- if (this._id) return (_creatorUI as any).tweenGetElapsed?.(this._id) ?? this.time
147
- return this._fb?.elapsed ?? (this._endTime > 0 ? this._totalMs() : 0)
148
- }
149
- private _seekTotal(ms: number): void {
150
- if (hasCore()) {
151
- this._ensure()
152
- ;(_creatorUI as any).tweenSeek(this._id, ms)
153
- } else {
154
- this._ensureFallback().seek(ms)
155
- }
156
- }
157
-
158
- /** Create the host animation (paused at t = 0) if it doesn't exist. */
159
- private _ensure(): void {
160
- if (this._id) return
161
- ensureEvents()
162
- const blob = buildBlob(this._spec)
163
- this._id = (_creatorUI as any).tweenCreate(blob.data, blob.strings, blob.targets)
164
- if (this._id) live.set(this._id, this)
165
- if (this._rate !== 1) (_creatorUI as any).tweenSetRate(this._id, this._rate)
166
- }
167
-
168
- private _destroy(): void {
169
- if (this._id) {
170
- live.delete(this._id)
171
- ;(_creatorUI as any).tweenControl(this._id, OP_CANCEL)
172
- this._id = 0
173
- }
174
- this._fb?.cancel()
175
- this._fb = undefined
176
- }
177
-
178
- play(): this {
179
- // A replay: whoever waited on the previous run gets `false` (unless it already ended), the host
180
- // object is rebuilt (ids may have changed — a screen re-mounts its nodes on every open). The
181
- // FIRST play keeps the promise handed out before it — unless a `cancel()` before any play has
182
- // already settled that one, in which case this run gets a fresh promise.
183
- if (this._started) {
184
- this._settle(false)
185
- this._destroy()
186
- }
187
- if (this._settled) this._newPromise()
188
- this._started = true
189
- this._endTime = 0
190
- // Commit = the last keyframe becomes the stored state NOW (as `animateTo` always did), so reads
191
- // and later `.style()` merges see the final state while the host is still tweening.
192
- for (const t of this._spec.tracks) {
193
- if (!t.commit || !t.channel.commit) continue
194
- const last = t.keys[t.keys.length - 1]
195
- if (last && last.value !== undefined) t.channel.commit(last.raw)
196
- }
197
- this._playing = true
198
- if (hasCore()) {
199
- this._ensure()
200
- ;(_creatorUI as any).tweenControl(this._id, OP_PLAY)
201
- } else {
202
- this._ensureFallback().play()
203
- }
204
- return this
205
- }
206
-
207
- pause(): this {
208
- if (hasCore()) { this._ensure(); (_creatorUI as any).tweenControl(this._id, OP_PAUSE) }
209
- else this._ensureFallback().pause()
210
- this._playing = false
211
- return this
212
- }
213
-
214
- resume(): this {
215
- if (this._settled) return this.play()
216
- if (hasCore()) { this._ensure(); (_creatorUI as any).tweenControl(this._id, OP_RESUME) }
217
- else this._ensureFallback().resume()
218
- this._playing = true
219
- return this
220
- }
221
-
222
- seek(ms: number): this {
223
- // `time` is inside the current iteration; a seek by time on a looping animation stays in it.
224
- const s = this._spec
225
- const period = s.durationMs * (s.pingPong && s.iterations !== 1 ? 2 : 1)
226
- const iter = period > 0 ? Math.floor(this._elapsedMs() / period) : 0
227
- this._seekTotal(iter * period + Math.max(0, ms))
228
- return this
229
- }
230
-
231
- finish(): this {
232
- if (hasCore()) {
233
- this._ensure()
234
- ;(_creatorUI as any).tweenControl(this._id, OP_FINISH) // the host answers with EV_FINISH
235
- } else {
236
- this._ensureFallback().finish()
237
- }
238
- return this
239
- }
240
-
241
- cancel(): this {
242
- this._destroy()
243
- this._settle(false)
244
- return this
245
- }
246
-
247
- onFinish(fn: (done: boolean) => void): this {
248
- this._listeners.push(fn)
249
- return this
250
- }
251
-
252
- /** @internal */
253
- _fireCall(index: number): void { this._calls[index]?.() }
254
- /** @internal */
255
- _fireValue(slot: number, lanes: Float32Array): void { this._values.get(slot)?.(lanes) }
256
-
257
- /** @internal The host reached the end (naturally or via finish()) and has freed the animation. */
258
- _hostFinished(): void {
259
- if (this._id) { live.delete(this._id); this._id = 0 }
260
- // the old-host path's timers / value loop must stop with the run, or an interval outlives it
261
- if (this._fb) { this._fb.cancel(); this._fb = undefined }
262
- this._endTime = this._spec.durationMs
263
- this._settle(true)
264
- }
265
-
266
- // ---- old hosts ------------------------------------------------------------------------------
267
-
268
- private _ensureFallback(): Fallback {
269
- if (!this._fb) this._fb = new Fallback(this)
270
- return this._fb
271
- }
272
- }
273
-
274
- /** Old-host path: UI tracks go through the legacy `animateTo` / `animateFrom` pair (one value per
275
- * prop — the last explicit key for a to-tween, the first for a from-tween; no easing, no seek), a
276
- * track on any other domain is dropped with one warning, and the clock is a timer that fires the
277
- * calls and the finish. Good enough for the simple cases on web / Apple until their core lands. */
278
- class Fallback {
279
- time = 0
280
- elapsed = 0
281
- private _timers: ReturnType<typeof setTimeout>[] = []
282
- private _startedAt = 0
283
- private _rate = 1
284
- private _warned = false
285
- private _loop?: ReturnType<typeof setInterval>
286
- private _valueTracks: Track[] = []
287
- private _scratch = new Float32Array(16)
288
- private _lanes: number[] = []
289
-
290
- private a: TweenAnimation
291
- constructor(a: TweenAnimation) { this.a = a }
292
-
293
- setRate(r: number): void { this._rate = r }
294
-
295
- play(): void {
296
- const s = this.a._spec
297
- this.cancel()
298
- this._startedAt = Date.now()
299
- const byTarget = new Map<Track["target"], { to: Record<string, unknown>, from: Record<string, unknown>, at: number, dur: number, commit: boolean }>()
300
- this._valueTracks = []
301
- for (const t of s.tracks) {
302
- if (t.channel.domain === DOM_VALUE) { this._valueTracks.push(t); continue }
303
- if (t.channel.domain !== DOM_UI) {
304
- if (!this._warned) { this._warned = true; console.warn("[tween] this host animates UI elements and values only (no tween core) — 3D / 2D tracks skipped") }
305
- continue
306
- }
307
- const rec = byTarget.get(t.target) ?? { to: {}, from: {}, at: t.atMs, dur: t.durMs, commit: t.commit }
308
- byTarget.set(t.target, rec)
309
- const first = t.keys[0]!, last = t.keys[t.keys.length - 1]!
310
- if (last.value === undefined) rec.from[t.prop] = first.raw // animateFrom shape
311
- else rec.to[t.prop] = last.raw
312
- rec.at = Math.min(rec.at, t.atMs)
313
- rec.dur = Math.max(rec.dur, t.durMs)
314
- }
315
- for (const [target, rec] of byTarget) {
316
- // Sent even for an unmounted element (id 0), exactly as the direct call used to be — the host
317
- // ignores it; a stub host in tests records it.
318
- const id = ((target as any)._id as number) ?? 0
319
- const delay = rec.at + s.delayMs
320
- const meta: Record<string, unknown> = { duration: rec.dur }
321
- if (delay > 0) meta.delay = delay
322
- if (s.iterations !== 1) { meta.loop = s.iterations < 0 ? true : s.iterations; meta.loopMode = s.pingPong ? "ping-pong" : "restart" }
323
- // The host merges its animate layer into base on commit (the SDK committed its own style already)
324
- if (Object.keys(rec.to).length) _creatorUI.animateTo?.(id, { ...rec.to, ...meta, ...(rec.commit ? {} : { commit: false }) })
325
- if (Object.keys(rec.from).length) _creatorUI.animateFrom?.(id, { ...rec.from, ...meta })
326
- }
327
- const scale = this._rate > 0 ? 1 / this._rate : 0
328
- if (scale === 0) return
329
- for (let i = 0; i < s.calls.length; i++) {
330
- this._timers.push(setTimeout(() => this.a._fireCall(i), (s.delayMs + s.calls[i]!) * scale))
331
- }
332
- if (s.iterations === 1) {
333
- this._timers.push(setTimeout(() => { this._applyValues(s.durationMs); this.elapsed = this.time = s.durationMs; this.a._hostFinished() }, (s.delayMs + s.durationMs) * scale))
334
- }
335
- // VALUE tracks are evaluated here in JS (the C evaluator's twin), ~60 times a second
336
- if (this._valueTracks.length) {
337
- this._applyValues(0)
338
- this._loop = setInterval(() => {
339
- const total = (Date.now() - this._startedAt) * this._rate - s.delayMs
340
- if (total < 0) return
341
- const period = s.durationMs * (s.pingPong && s.iterations !== 1 ? 2 : 1)
342
- let local = period > 0 ? total % period : 0
343
- if (s.iterations !== 1 && s.pingPong && local > s.durationMs) local = 2 * s.durationMs - local
344
- if (s.iterations === 1) local = Math.min(total, s.durationMs)
345
- this.elapsed = total
346
- this.time = local
347
- this._applyValues(local)
348
- }, 16)
349
- }
350
- }
351
- private _applyValues(local: number): void {
352
- for (const t of this._valueTracks) {
353
- evaluateTrack(t, local, this._lanes)
354
- for (let i = 0; i < t.lanes; i++) this._scratch[i] = this._lanes[i]!
355
- this.a._fireValue(t.channel.id() as number, this._scratch)
356
- }
357
- }
358
- pause(): void { this.cancel() }
359
- resume(): void { /* no partial resume on the timer clock: the tween already ran on the host */ }
360
- seek(ms: number): void { this.elapsed = ms; this.time = ms % Math.max(1, this.a._spec.durationMs); this._applyValues(this.time) }
361
- finish(): void { this.cancel(); this._applyValues(this.a._spec.durationMs); this.a._hostFinished() }
362
- cancel(): void {
363
- for (const t of this._timers) clearTimeout(t)
364
- this._timers = []
365
- if (this._loop !== undefined) { clearInterval(this._loop); this._loop = undefined }
366
- if (this._startedAt) this.elapsed = this.time = Math.min(this.a._spec.durationMs, (Date.now() - this._startedAt) * this._rate)
367
- }
368
- }
369
-
370
- /** @internal Build a one-bag animation for a single target (the `animateTo` / `animateFrom` path). */
371
- export const singleTargetSpec = (tracks: Track[], clock: number, iterations: number, pingPong: boolean, delayMs: number): TweenSpec => {
372
- let durationMs = 0
373
- for (const t of tracks) durationMs = Math.max(durationMs, t.atMs + t.durMs)
374
- // `delay` lives on the tracks (the first key holds through it, §3.1); the spec delay is 0 here
375
- return { clock, durationMs, delayMs, iterations, pingPong, rate: 1, tracks, calls: [] }
376
- }
377
-
378
- export { CLOCK_UI, CLOCK_GAME }
1
+ // The animation HANDLE (docs/timeline-plan.md §2.2): what `animateTo` / `animateFrom` / `Timeline`
2
+ // return. Owns one spec; every `play()` builds the blob (target ids resolve at play time), hands it
3
+ // to the host (`_creatorTree.tweenCreate`) and gets events back (`registerTweenEvent`: time-callbacks
4
+ // and finish). On a host without the tween core the UI tracks fall back to the old
5
+ // `animateTo` / `animateFrom` bridge pair (single-value tweens only) and the clock is a timer —
6
+ // see `fallback` below.
7
+
8
+ import { CLOCK_GAME, CLOCK_UI, DOM_UI, DOM_VALUE, buildBlob, evaluateTrack, type Track, type TweenSpec } from "./spec"
9
+ import { DEAD_HANDLE, Pins } from "../../core/pins"
10
+
11
+ const OP_PLAY = 0
12
+ const OP_PAUSE = 1
13
+ const OP_RESUME = 2
14
+ const OP_CANCEL = 3
15
+ const OP_FINISH = 4
16
+
17
+ const EV_CALL = 0
18
+ const EV_FINISH = 1
19
+ const EV_VALUE = 2 // (id, 2, slot, lane0, lane1, …) — a VALUE track's frame, synchronous from the host
20
+
21
+ /** Playback control shared by element tweens and timelines. Times are **milliseconds**. */
22
+ export interface Animation {
23
+ /** One iteration, ms. */
24
+ readonly duration: number
25
+ /** Position inside the current iteration, ms. Writable = seek. */
26
+ time: number
27
+ /** 0..1 of the whole animation (iterations included). Writable = seek. */
28
+ progress: number
29
+ /** Playback rate; negative runs backwards, 0 freezes. */
30
+ rate: number
31
+ readonly playing: boolean
32
+ /** Resolves `true` when the animation reaches its end, `false` when cancelled, replaced or replayed. Never rejects. */
33
+ readonly finished: Promise<boolean>
34
+ /** Start from t = 0 (a running animation restarts). */
35
+ play(): this
36
+ pause(): this
37
+ resume(): this
38
+ /** Jump to `ms`; playback state is unchanged. */
39
+ seek(ms: number): this
40
+ /** Jump to the end: values land, commits apply, `finished` resolves `true`. */
41
+ finish(): this
42
+ /** Stop where it is — no commit, `finished` resolves `false`. */
43
+ cancel(): this
44
+ onFinish(fn: (done: boolean) => void): this
45
+ }
46
+
47
+
48
+ // ---- host events -------------------------------------------------------------------------------
49
+
50
+ /** The animations native owns right now (playing), by id — what the host's events resolve to. */
51
+ const tweens = new Pins<TweenAnimation>()
52
+ let eventsRegistered = false
53
+ const ensureEvents = (): void => {
54
+ if (eventsRegistered) return
55
+ eventsRegistered = true
56
+ _creatorTree.registerTweenEvent(function (this: unknown, id: number, kind: number, index: number) {
57
+ const a = tweens.get(id)
58
+ if (!a) return
59
+ if (kind === EV_CALL) a._fireCall(index)
60
+ else if (kind === EV_FINISH) a._hostFinished()
61
+ else if (kind === EV_VALUE) {
62
+ // lanes ride as plain number arguments (no per-frame array on either side)
63
+ const n = arguments.length - 3
64
+ for (let i = 0; i < n; i++) valueScratch[i] = arguments[i + 3] as number
65
+ a._fireValue(index, valueScratch)
66
+ }
67
+ })
68
+ }
69
+
70
+ /** Reused lane buffer for VALUE deliveries (the host's largest kind is 10 lanes). */
71
+ const valueScratch = new Float32Array(16)
72
+
73
+ /** The concrete handle — what the SDK instantiates behind `animateTo` / `animateFrom` / `Timeline`.
74
+ * User code sees the {@link Animation} interface; the class itself must stay in the public
75
+ * declarations because `TimelineImpl` extends it — marking it internal (even mentioning the tag in
76
+ * this comment: the strip is a substring match) drops the timeline's playback members. */
77
+ export class TweenAnimation implements Animation {
78
+ /** @internal */
79
+ _spec: TweenSpec
80
+ /** @internal Time-callbacks by call index (the spec's `calls[i]`). */
81
+ _calls: Array<() => void> = []
82
+ /** @internal VALUE-track deliverers by slot (`animate()` / `Timeline.animate`). */
83
+ _values = new Map<number, (lanes: Float32Array) => void>()
84
+
85
+ /** @internal The host animation's owned handle (DEAD_HANDLE while there is none). */
86
+ _h: Handle = DEAD_HANDLE
87
+ private _started = false // play() ran at least once (the first play keeps the constructor's promise)
88
+ private _endTime = 0 // `time` after the host freed a finished animation
89
+ private _playing = false
90
+ private _rate: number
91
+ private _finished!: Promise<boolean>
92
+ private _resolve!: (done: boolean) => void
93
+ private _settled = true
94
+ private _listeners: Array<(done: boolean) => void> = []
95
+
96
+ constructor(spec: TweenSpec) {
97
+ this._spec = spec
98
+ this._rate = spec.rate
99
+ this._newPromise()
100
+ }
101
+
102
+ private _newPromise(): void {
103
+ this._finished = new Promise<boolean>((r) => { this._resolve = r })
104
+ this._settled = false
105
+ }
106
+
107
+ private _settle(done: boolean): void {
108
+ if (this._settled) return
109
+ this._settled = true
110
+ this._playing = false
111
+ this._resolve(done)
112
+ for (const fn of this._listeners) fn(done)
113
+ }
114
+
115
+ get duration(): number { return this._spec.durationMs }
116
+ get playing(): boolean { return this._playing }
117
+ get finished(): Promise<boolean> { return this._finished }
118
+
119
+ get rate(): number { return this._rate }
120
+ set rate(r: number) {
121
+ this._rate = r
122
+ if (this._h.id) _creatorTree.tweenSetRate(this._h.id, r)
123
+ }
124
+
125
+ get time(): number {
126
+ if (this._h.id) return _creatorTree.tweenGetTime(this._h.id)
127
+ return this._endTime
128
+ }
129
+ set time(ms: number) { this.seek(ms) }
130
+
131
+ get progress(): number {
132
+ const total = this._totalMs()
133
+ return total > 0 ? Math.min(1, Math.max(0, this._elapsedMs() / total)) : 1
134
+ }
135
+ set progress(p: number) {
136
+ const total = this._totalMs()
137
+ this._seekTotal(Math.min(1, Math.max(0, p)) * total)
138
+ }
139
+
140
+ private _totalMs(): number {
141
+ const s = this._spec
142
+ const n = s.iterations < 0 ? 1 : s.iterations
143
+ return s.durationMs * n * (s.pingPong && s.iterations !== 1 ? 2 : 1)
144
+ }
145
+ private _elapsedMs(): number {
146
+ if (this._h.id) return _creatorTree.tweenGetElapsed(this._h.id)
147
+ return this._endTime > 0 ? this._totalMs() : 0
148
+ }
149
+ private _seekTotal(ms: number): void {
150
+ this._ensure()
151
+ _creatorTree.tweenSeek(this._h.id, ms)
152
+ }
153
+
154
+ /** Create the host animation (paused at t = 0) if it doesn't exist. */
155
+ private _ensure(): void {
156
+ if (this._h.id) return
157
+ ensureEvents()
158
+ const blob = buildBlob(this._spec)
159
+ this._h = _creatorTree.tweenCreate(blob.data, blob.strings, blob.targets as number[]) ?? DEAD_HANDLE
160
+ if (this._h.id && this._rate !== 1) _creatorTree.tweenSetRate(this._h.id, this._rate)
161
+ }
162
+
163
+ private _destroy(): void {
164
+ if (this._h.id) {
165
+ tweens.unpin(this._h.id)
166
+ _creatorTree.tweenControl(this._h.id, OP_CANCEL) // the runtime frees it; drop the handle too
167
+ this._h = DEAD_HANDLE
168
+ }
169
+ }
170
+
171
+ play(): this {
172
+ // A replay: whoever waited on the previous run gets `false` (unless it already ended), the host
173
+ // object is rebuilt (ids may have changed — a screen re-mounts its nodes on every open). The
174
+ // FIRST play keeps the promise handed out before it — unless a `cancel()` before any play has
175
+ // already settled that one, in which case this run gets a fresh promise.
176
+ if (this._started) {
177
+ this._settle(false)
178
+ this._destroy()
179
+ }
180
+ if (this._settled) this._newPromise()
181
+ this._started = true
182
+ this._endTime = 0
183
+ // Commit = the last keyframe becomes the stored state NOW (as `animateTo` always did), so reads
184
+ // and later `.style()` merges see the final state while the host is still tweening.
185
+ for (const t of this._spec.tracks) {
186
+ if (!t.commit || !t.channel.commit) continue
187
+ const last = t.keys[t.keys.length - 1]
188
+ if (last && last.value !== undefined) t.channel.commit(last.raw)
189
+ }
190
+ this._playing = true
191
+ this._ensure()
192
+ _creatorTree.tweenControl(this._h.id, OP_PLAY)
193
+ tweens.pin(this) // playing: native owns it, its events must find it
194
+ return this
195
+ }
196
+
197
+ pause(): this {
198
+ this._ensure()
199
+ _creatorTree.tweenControl(this._h.id, OP_PAUSE)
200
+ this._playing = false
201
+ tweens.unpin(this._h.id) // paused: only the app's reference keeps it
202
+ return this
203
+ }
204
+
205
+ resume(): this {
206
+ if (this._settled) return this.play()
207
+ this._ensure()
208
+ _creatorTree.tweenControl(this._h.id, OP_RESUME)
209
+ this._playing = true
210
+ tweens.pin(this)
211
+ return this
212
+ }
213
+
214
+ seek(ms: number): this {
215
+ // `time` is inside the current iteration; a seek by time on a looping animation stays in it.
216
+ const s = this._spec
217
+ const period = s.durationMs * (s.pingPong && s.iterations !== 1 ? 2 : 1)
218
+ const iter = period > 0 ? Math.floor(this._elapsedMs() / period) : 0
219
+ this._seekTotal(iter * period + Math.max(0, ms))
220
+ return this
221
+ }
222
+
223
+ finish(): this {
224
+ this._ensure()
225
+ _creatorTree.tweenControl(this._h.id, OP_FINISH) // the host answers with EV_FINISH
226
+ return this
227
+ }
228
+
229
+ cancel(): this {
230
+ this._destroy()
231
+ this._settle(false)
232
+ return this
233
+ }
234
+
235
+ onFinish(fn: (done: boolean) => void): this {
236
+ this._listeners.push(fn)
237
+ return this
238
+ }
239
+
240
+ /** @internal */
241
+ _fireCall(index: number): void { this._calls[index]?.() }
242
+ /** @internal */
243
+ _fireValue(slot: number, lanes: Float32Array): void { this._values.get(slot)?.(lanes) }
244
+
245
+ /** @internal The host reached the end (naturally or via finish()) and has freed the animation. */
246
+ _hostFinished(): void {
247
+ if (this._h.id) { tweens.unpin(this._h.id); this._h = DEAD_HANDLE } // the runtime freed it
248
+ this._endTime = this._spec.durationMs
249
+ this._settle(true)
250
+ }
251
+
252
+ }
253
+
254
+ /** @internal Build a one-bag animation for a single target (the `animateTo` / `animateFrom` path). */
255
+ export const singleTargetSpec = (tracks: Track[], clock: number, iterations: number, pingPong: boolean, delayMs: number): TweenSpec => {
256
+ let durationMs = 0
257
+ for (const t of tracks) durationMs = Math.max(durationMs, t.atMs + t.durMs)
258
+ // `delay` lives on the tracks (the first key holds through it, §3.1); the spec delay is 0 here
259
+ return { clock, durationMs, delayMs, iterations, pingPong, rate: 1, tracks, calls: [] }
260
+ }
261
+
262
+ export { CLOCK_UI, CLOCK_GAME }