lecodes-sdk 1.2.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 (260) 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 +8 -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/Scene.d.ts +41 -13
  25. package/dist/types/gl/Texture.d.ts +1 -1
  26. package/dist/types/gl/animation/Locomotion.d.ts +8 -1
  27. package/dist/types/inject.d.ts +11 -11
  28. package/dist/types/inject.editor.d.ts +1 -0
  29. package/dist/types/net/core.d.ts +7 -0
  30. package/dist/types/plugin.d.ts +76 -0
  31. package/dist/types/plugins/gen/camera/sdk/camera.d.ts +24 -0
  32. package/dist/types/plugins/gen/camera/sdk/camera.gen.d.ts +25 -0
  33. package/dist/types/plugins/{geolocation.d.ts → gen/geolocation/sdk/geolocation.d.ts} +2 -20
  34. package/dist/types/plugins/gen/geolocation/sdk/geolocation.gen.d.ts +31 -0
  35. package/dist/types/plugins/{map.d.ts → gen/map/sdk/map.d.ts} +9 -50
  36. package/dist/types/plugins/gen/map/sdk/map.gen.d.ts +53 -0
  37. package/dist/types/plugins/gen/push/sdk/push.d.ts +23 -0
  38. package/dist/types/plugins/gen/push/sdk/push.gen.d.ts +35 -0
  39. package/dist/types/plugins/{qr.d.ts → gen/qr-scanner/sdk/qr-scanner.d.ts} +2 -3
  40. package/dist/types/plugins/gen/qr-scanner/sdk/qr-scanner.gen.d.ts +15 -0
  41. package/dist/types/runtime/app.d.ts +9 -2
  42. package/dist/types/runtime/fetch.d.ts +2 -0
  43. package/dist/types/runtime/input.d.ts +1 -1
  44. package/dist/types/runtime/media.d.ts +6 -10
  45. package/dist/types/runtime/misc.d.ts +4 -1
  46. package/dist/types/runtime/net.d.ts +3 -2
  47. package/dist/types/runtime/touch.d.ts +32 -0
  48. package/dist/types/scene/defineScene.d.ts +43 -2
  49. package/dist/types/scene/editor.d.ts +52 -0
  50. package/dist/types/scene/gizmos.d.ts +7 -4
  51. package/dist/types/ui/NativeView.d.ts +6 -4
  52. package/dist/types/ui/UI.d.ts +1 -1
  53. package/dist/types/ui/UIBottomSheet.d.ts +6 -12
  54. package/dist/types/ui/UIButton.d.ts +12 -14
  55. package/dist/types/ui/UIContainer.d.ts +0 -6
  56. package/dist/types/ui/UIImage.d.ts +1 -4
  57. package/dist/types/ui/UIInput.d.ts +8 -24
  58. package/dist/types/ui/UIModal.d.ts +0 -2
  59. package/dist/types/ui/UINode.d.ts +70 -56
  60. package/dist/types/ui/UIPager.d.ts +28 -27
  61. package/dist/types/ui/UIPopover.d.ts +0 -2
  62. package/dist/types/ui/UIScreen.d.ts +12 -17
  63. package/dist/types/ui/UIScrollable.d.ts +1 -4
  64. package/dist/types/ui/UIText.d.ts +0 -2
  65. package/dist/types/ui/UIVideo.d.ts +3 -5
  66. package/dist/types/ui/UIVirtualizedList.d.ts +14 -16
  67. package/dist/types/ui/UIWidget.d.ts +10 -9
  68. package/dist/types/ui/colorKeys.gen.d.ts +9 -0
  69. package/dist/types/ui/presentable.d.ts +46 -32
  70. package/dist/types/ui/router.d.ts +18 -7
  71. package/dist/types/ui/styleColor.d.ts +1 -0
  72. package/dist/types/ui/transitions.d.ts +18 -0
  73. package/dist/types/ui/tree.d.ts +75 -0
  74. package/dist/types/version.d.ts +10 -0
  75. package/dist/types.json +1 -1
  76. package/package.json +12 -3
  77. package/prompts/2d.md +2 -6
  78. package/prompts/3d.md +1 -5
  79. package/prompts/README.md +1 -1
  80. package/prompts/canvas.md +9 -8
  81. package/prompts/compose.ts +1 -1
  82. package/prompts/core.md +3 -3
  83. package/prompts/design.md +1 -1
  84. package/prompts/dist/2d-game.md +473 -239
  85. package/prompts/dist/3d-app.md +553 -205
  86. package/prompts/dist/ar-app.md +435 -202
  87. package/prompts/dist/design.md +113 -95
  88. package/prompts/dist/ui-app.md +386 -170
  89. package/prompts/ui-design.md +2 -3
  90. package/prompts/ui.md +45 -37
  91. package/src/animate/tween/Animation.ts +34 -150
  92. package/src/animate/tween/Timeline.ts +175 -175
  93. package/src/animate/tween/animateValue.ts +6 -3
  94. package/src/animate/tween/easing.ts +10 -3
  95. package/src/animate/tween/spec.ts +41 -15
  96. package/src/audio/Sound.ts +3 -3
  97. package/src/audio/audio.ts +2 -1
  98. package/src/bridges/2d.d.ts +317 -0
  99. package/src/bridges/app.d.ts +91 -0
  100. package/src/bridges/audio.d.ts +97 -0
  101. package/src/bridges/canvas.d.ts +79 -0
  102. package/src/bridges/device.d.ts +72 -0
  103. package/src/bridges/fetch.d.ts +80 -0
  104. package/src/bridges/files.d.ts +70 -0
  105. package/src/bridges/gl.d.ts +1133 -0
  106. package/src/bridges/input.d.ts +72 -0
  107. package/src/bridges/media.d.ts +55 -0
  108. package/src/bridges/nav.d.ts +71 -0
  109. package/src/bridges/net.d.ts +52 -0
  110. package/src/bridges/service.d.ts +52 -0
  111. package/src/bridges/socket.d.ts +31 -0
  112. package/src/bridges/storage.d.ts +33 -0
  113. package/src/bridges/tree.d.ts +301 -0
  114. package/src/bridges/types.d.ts +49 -0
  115. package/src/canvas/Canvas.ts +114 -159
  116. package/src/canvas/gen/cssColor.ts +224 -0
  117. package/src/canvas/gen/recorder.ts +212 -0
  118. package/src/canvas/gen/spec.ts +201 -0
  119. package/src/chisel.ts +193 -0
  120. package/src/compile/assetMacro.ts +1 -1
  121. package/src/compile/bundler.ts +11 -2
  122. package/src/compile/compileProject.ts +43 -4
  123. package/src/compile/fontMacro.ts +3 -4
  124. package/src/compile/header.ts +26 -5
  125. package/src/compile/index.ts +3 -1
  126. package/src/compile/liteMaterial.ts +1 -1
  127. package/src/compile/sceneEditor.ts +11 -26
  128. package/src/core/color.ts +73 -30
  129. package/src/core/pins.ts +51 -0
  130. package/src/core/signals.ts +8 -1
  131. package/src/g2/CharacterController2D.ts +3 -3
  132. package/src/g2/Node2D.ts +57 -39
  133. package/src/g2/Physics2D.ts +2 -2
  134. package/src/g2/Scene2D.ts +35 -23
  135. package/src/g2/Texture2D.ts +1 -1
  136. package/src/g2/loop.ts +4 -4
  137. package/src/gl/CameraPlace.ts +52 -52
  138. package/src/gl/Foliage.ts +72 -17
  139. package/src/gl/Geometry.ts +1 -2
  140. package/src/gl/Light.ts +10 -0
  141. package/src/gl/Lightmap.ts +45 -29
  142. package/src/gl/Material.ts +95 -51
  143. package/src/gl/Mesh.ts +120 -120
  144. package/src/gl/Model.ts +21 -16
  145. package/src/gl/Node.ts +91 -24
  146. package/src/gl/Particles.ts +1 -1
  147. package/src/gl/Scene.ts +100 -44
  148. package/src/gl/Texture.ts +8 -7
  149. package/src/gl/animation/AnimationClip.ts +1 -1
  150. package/src/gl/animation/DynamicBone.ts +482 -482
  151. package/src/gl/animation/Locomotion.ts +8 -3
  152. package/src/gl/nav/NavMesh.ts +3 -4
  153. package/src/gl/physics/Physics.ts +2 -2
  154. package/src/gl/physics/physicsEvents.ts +3 -3
  155. package/src/gl/scenarios.ts +291 -291
  156. package/src/gl/terrain/Terrain.ts +4 -5
  157. package/src/gl/touch.ts +14 -15
  158. package/src/host.d.ts +2 -3
  159. package/src/inject.editor.ts +7 -0
  160. package/src/inject.ts +13 -16
  161. package/src/net/core.ts +6 -5
  162. package/src/net/index.ts +1 -1
  163. package/src/net/replication.ts +1 -1
  164. package/src/plugin.ts +191 -0
  165. package/src/plugins/gen/camera/contract.d.ts +27 -0
  166. package/src/plugins/gen/camera/sdk/camera.gen.ts +46 -0
  167. package/src/plugins/gen/camera/sdk/camera.ts +57 -0
  168. package/src/plugins/gen/geolocation/contract.d.ts +50 -0
  169. package/src/plugins/gen/geolocation/sdk/geolocation.gen.ts +54 -0
  170. package/src/plugins/{geolocation.ts → gen/geolocation/sdk/geolocation.ts} +22 -43
  171. package/src/plugins/gen/map/contract.d.ts +144 -0
  172. package/src/plugins/gen/map/sdk/map.gen.ts +88 -0
  173. package/src/plugins/{map.ts → gen/map/sdk/map.ts} +68 -102
  174. package/src/plugins/gen/push/contract.d.ts +61 -0
  175. package/src/plugins/gen/push/sdk/push.gen.ts +60 -0
  176. package/src/plugins/gen/push/sdk/push.ts +105 -0
  177. package/src/plugins/gen/qr-scanner/contract.d.ts +16 -0
  178. package/src/plugins/gen/qr-scanner/sdk/qr-scanner.gen.ts +29 -0
  179. package/src/plugins/gen/qr-scanner/sdk/qr-scanner.ts +52 -0
  180. package/src/plugins/permission.ts +5 -4
  181. package/src/runtime/app.ts +20 -9
  182. package/src/runtime/appEvents.ts +5 -4
  183. package/src/runtime/channel.ts +18 -15
  184. package/src/runtime/clipboard.ts +4 -3
  185. package/src/runtime/datetime.ts +2 -1
  186. package/src/runtime/device.ts +17 -15
  187. package/src/runtime/fetch.ts +30 -20
  188. package/src/runtime/files.ts +16 -15
  189. package/src/runtime/input.ts +12 -10
  190. package/src/runtime/media.ts +50 -46
  191. package/src/runtime/misc.ts +7 -3
  192. package/src/runtime/net.ts +8 -7
  193. package/src/runtime/rpc.ts +1 -3
  194. package/src/runtime/service.ts +19 -14
  195. package/src/runtime/share.ts +4 -3
  196. package/src/runtime/storage.ts +6 -4
  197. package/src/runtime/touch.ts +32 -0
  198. package/src/scene/defineScene.ts +61 -365
  199. package/src/scene/editor.ts +408 -0
  200. package/src/scene/editorPlugins.ts +3 -3
  201. package/src/scene/gizmos.ts +15 -9
  202. package/src/server/db/marci/query.ts +1 -1
  203. package/src/server/host.ts +1 -1
  204. package/src/server/runtime.ts +1 -1
  205. package/src/ui/NativeView.ts +72 -25
  206. package/src/ui/UI.ts +3 -3
  207. package/src/ui/UIBottomSheet.ts +16 -17
  208. package/src/ui/UIButton.ts +54 -16
  209. package/src/ui/UIContainer.ts +0 -6
  210. package/src/ui/UIImage.ts +34 -30
  211. package/src/ui/UIInput.ts +29 -37
  212. package/src/ui/UIModal.ts +1 -3
  213. package/src/ui/UINode.ts +347 -297
  214. package/src/ui/UIPager.ts +93 -78
  215. package/src/ui/UIPopover.ts +0 -2
  216. package/src/ui/UIScreen.ts +41 -36
  217. package/src/ui/UIScrollable.ts +19 -13
  218. package/src/ui/UISpacer.ts +1 -1
  219. package/src/ui/UITabs.ts +8 -6
  220. package/src/ui/UIText.ts +7 -17
  221. package/src/ui/UIVideo.ts +30 -27
  222. package/src/ui/UIVirtualizedList.ts +58 -59
  223. package/src/ui/UIWidget.ts +38 -18
  224. package/src/ui/colorKeys.gen.ts +37 -0
  225. package/src/ui/fonts.ts +2 -2
  226. package/src/ui/presentable.ts +59 -42
  227. package/src/ui/router.ts +52 -35
  228. package/src/ui/styleColor.ts +56 -0
  229. package/src/ui/theme.ts +7 -6
  230. package/src/ui/transitions.ts +249 -0
  231. package/src/ui/tree.ts +346 -0
  232. package/src/version.ts +24 -0
  233. package/tests/helpers/engineWorld.ts +11 -0
  234. package/tests/helpers/fakeTree.ts +353 -0
  235. package/tests/helpers/hostStubs.ts +31 -0
  236. package/tests/helpers/index.ts +14 -0
  237. package/tests/helpers/memoryMarci.ts +124 -0
  238. package/tests/helpers/phases.ts +23 -0
  239. package/tests/helpers/preload.ts +18 -0
  240. package/tests/helpers/stubApp.ts +2 -0
  241. package/tests/helpers/stubDevice.ts +2 -0
  242. package/tests/helpers/stubFetch.ts +2 -0
  243. package/tests/helpers/stubInput.ts +2 -0
  244. package/dist/inject.js +0 -4629
  245. package/dist/types/core/registry.d.ts +0 -7
  246. package/dist/types/plugins/camera.d.ts +0 -25
  247. package/dist/types/plugins/push.d.ts +0 -46
  248. package/src/bridges.d.ts +0 -1769
  249. package/src/compile/__tests__/assetIconMacro.test.ts +0 -219
  250. package/src/compile/__tests__/assetMacro.test.ts +0 -100
  251. package/src/compile/__tests__/assetName.test.ts +0 -55
  252. package/src/compile/__tests__/compile.test.ts +0 -310
  253. package/src/compile/__tests__/detectEntry.test.ts +0 -151
  254. package/src/compile/__tests__/fontMacro.test.ts +0 -199
  255. package/src/compile/__tests__/serverSplit.test.ts +0 -27
  256. package/src/core/__tests__/stateMachine.test.ts +0 -132
  257. package/src/core/registry.ts +0 -23
  258. package/src/plugins/camera.ts +0 -81
  259. package/src/plugins/push.ts +0 -132
  260. package/src/plugins/qr.ts +0 -73
@@ -1,175 +1,175 @@
1
- // `Timeline` (docs/timeline-plan.md §2.4): choreography over the keyframe core. Tracks on any
2
- // number of targets (UI elements, 3D / 2D nodes, lights, cameras) at absolute positions, JS calls
3
- // at times, labels, holds — one clock, one handle. Also the `animateTo` / `animateFrom` bag
4
- // builder every target class delegates to (`tweenBag`).
5
-
6
- import { TweenAnimation, type Animation } from "./Animation"
7
- import { makeValueTrack, type AnimateOptions, type AnimateValue } from "./animateValue"
8
- import { CLOCK_GAME, CLOCK_UI, bagProps, iterationsOf, makeTrack, type Track, type TweenMeta, type TweenSpec, type TweenTarget } from "./spec"
9
-
10
- /** Vector-valued props: an array value is ONE keyframe unless it is an array of vectors. */
11
- const VECTOR_PROPS = new Set(["position", "scale", "quaternion", "eulerAngles", "color"])
12
-
13
- const clockOf = (clock: TweenMeta["clock"], fallback: number): number =>
14
- clock === "game" ? CLOCK_GAME : clock === "ui" ? CLOCK_UI : fallback
15
-
16
- /** @internal Build the tracks of one options bag on one target. `fromCurrent` = animateTo shape. */
17
- export const bagTracks = (target: TweenTarget, bag: Record<string, unknown>, atMs: number, fromCurrent: boolean): Track[] => {
18
- const meta = bag as TweenMeta
19
- const tracks: Track[] = []
20
- for (const [prop, value] of bagProps(bag)) {
21
- const track = makeTrack(target, prop, value, meta, atMs, fromCurrent, VECTOR_PROPS.has(prop))
22
- if (track) tracks.push(track)
23
- }
24
- return tracks
25
- }
26
-
27
- /** @internal The `animateTo` / `animateFrom` implementation: one bag → one animation, played now. */
28
- export const tweenBag = (target: TweenTarget, bag: Record<string, unknown>, fromCurrent: boolean): Animation => {
29
- const meta = bag as TweenMeta
30
- const tracks = bagTracks(target, bag, 0, fromCurrent)
31
- let durationMs = 0
32
- for (const t of tracks) durationMs = Math.max(durationMs, t.atMs + t.durMs)
33
- const spec: TweenSpec = {
34
- clock: clockOf(meta.clock, target._tweenClock),
35
- durationMs, delayMs: 0,
36
- iterations: iterationsOf(meta.loop),
37
- pingPong: meta.loopMode !== "restart",
38
- rate: 1, tracks, calls: [],
39
- }
40
- return new TweenAnimation(spec).play()
41
- }
42
-
43
- export type TimelineOptions = {
44
- /** `'ui'` (default) = wall time; `'game'` follows `Time.scale` and pauses with the game. */
45
- clock?: "ui" | "game",
46
- /** Repeat the whole timeline: `true` = forever, a number = cycles. */
47
- loop?: boolean | number,
48
- loopMode?: "restart" | "ping-pong",
49
- /** Wait before the first play, ms. */
50
- delay?: number,
51
- }
52
-
53
- /** Where a track goes: ms, a label, or `[label, offsetMs]`. Default = the current end (sequencing). */
54
- export type TimelinePosition = number | string | [string, number]
55
-
56
- export type TimelineAddOptions = {
57
- at?: TimelinePosition,
58
- /** With several targets: each starts this many ms after the previous one. */
59
- stagger?: number,
60
- }
61
-
62
- /** A choreography: tracks on many targets at absolute times, JS calls, labels and holds, driven by
63
- * one clock. Build it once, `play()` on every entrance — `play()` restarts from t = 0 and the pose
64
- * at any time is fully defined by the tracks (the first keyframe of a track holds before it
65
- * starts, the last one after it ends). */
66
- export class TimelineImpl extends TweenAnimation {
67
- private _labels = new Map<string, number>()
68
- private _end = 0
69
-
70
- constructor(options: TimelineOptions = {}) {
71
- super({
72
- clock: clockOf(options.clock, CLOCK_UI),
73
- durationMs: 0,
74
- delayMs: options.delay ?? 0,
75
- iterations: iterationsOf(options.loop),
76
- pingPong: options.loopMode !== "restart",
77
- rate: 1, tracks: [], calls: [],
78
- })
79
- }
80
-
81
- private _at(at: TimelinePosition | undefined): number {
82
- if (at === undefined) return this._end
83
- if (typeof at === "number") return at
84
- const name = Array.isArray(at) ? at[0] : at
85
- const offset = Array.isArray(at) ? at[1] : 0
86
- const base = this._labels.get(name)
87
- if (base === undefined) throw new Error(`Timeline: unknown label "${name}"`)
88
- return base + offset
89
- }
90
-
91
- private _grow(endMs: number): void {
92
- if (endMs > this._end) this._end = endMs
93
- this._spec.durationMs = this._end
94
- }
95
-
96
- /** Animate `props` on one target or several (staggered), like `animateTo` — arrays are keyframes,
97
- * `duration` / `easing` / `times` / `commit` apply. The bag's `delay` shifts the track after `at`. */
98
- add(target: TweenTarget | TweenTarget[], props: Record<string, unknown> & TweenMeta, options: TimelineAddOptions = {}): this {
99
- const at = this._at(options.at)
100
- const targets = Array.isArray(target) ? target : [target]
101
- const stagger = options.stagger ?? 0
102
- targets.forEach((t, i) => {
103
- const tracks = bagTracks(t, props, at + i * stagger, true)
104
- for (const tr of tracks) { this._spec.tracks.push(tr); this._grow(tr.atMs + tr.durMs) }
105
- })
106
- return this
107
- }
108
-
109
- /** Like `add` with the `animateFrom` shape: the target's own state is the implicit last keyframe. */
110
- addFrom(target: TweenTarget | TweenTarget[], props: Record<string, unknown> & TweenMeta, options: TimelineAddOptions = {}): this {
111
- const at = this._at(options.at)
112
- const targets = Array.isArray(target) ? target : [target]
113
- const stagger = options.stagger ?? 0
114
- targets.forEach((t, i) => {
115
- const tracks = bagTracks(t, props, at + i * stagger, false)
116
- for (const tr of tracks) { this._spec.tracks.push(tr); this._grow(tr.atMs + tr.durMs) }
117
- })
118
- return this
119
- }
120
-
121
- /** A free-value track (see the `animate()` global): the host evaluates it and calls `onUpdate`
122
- * every frame with the value. */
123
- animate<T extends AnimateValue>(options: AnimateOptions<T>, at?: TimelinePosition): this {
124
- const { track, deliver, slot } = makeValueTrack(options, this._at(at))
125
- this._values.set(slot, deliver)
126
- this._spec.tracks.push(track)
127
- this._grow(track.atMs + track.durMs)
128
- return this
129
- }
130
-
131
- /** Call `fn` when playback crosses `at` (forward or backward), in time order, always before
132
- * finish, never from a previous run. */
133
- call(at: TimelinePosition, fn: () => void): this {
134
- const t = this._at(at)
135
- this._spec.calls.push(t)
136
- this._calls.push(fn)
137
- this._grow(t)
138
- return this
139
- }
140
-
141
- /** Name a time — for `at`, `seek` and callers (`tl.labels.landed`). Default = the current end. */
142
- label(name: string, at?: TimelinePosition): this {
143
- this._labels.set(name, this._at(at))
144
- return this
145
- }
146
-
147
- /** Named times, ms. */
148
- get labels(): Record<string, number> {
149
- const o: Record<string, number> = {}
150
- for (const [k, v] of this._labels) o[k] = v
151
- return o
152
- }
153
-
154
- /** Extend the timeline with stillness after its current end. */
155
- hold(ms: number): this {
156
- this._grow(this._end + Math.max(0, ms))
157
- return this
158
- }
159
-
160
- /** The current end, ms (tracks, calls and holds). */
161
- get end(): number { return this._end }
162
-
163
- /** `seek` also accepts a label. */
164
- override seek(at: number | string): this {
165
- return super.seek(typeof at === "string" ? this._at(at) : at)
166
- }
167
- }
168
-
169
- /** The timeline handle type (see {@link TimelineImpl} for the members). */
170
- export type Timeline = TimelineImpl
171
-
172
- /** Create a timeline — `Timeline()` / `Timeline({ clock: 'game', loop: true })`. */
173
- export function Timeline(options?: TimelineOptions): Timeline {
174
- return new TimelineImpl(options)
175
- }
1
+ // `Timeline` (docs/timeline-plan.md §2.4): choreography over the keyframe core. Tracks on any
2
+ // number of targets (UI elements, 3D / 2D nodes, lights, cameras) at absolute positions, JS calls
3
+ // at times, labels, holds — one clock, one handle. Also the `animateTo` / `animateFrom` bag
4
+ // builder every target class delegates to (`tweenBag`).
5
+
6
+ import { TweenAnimation, type Animation } from "./Animation"
7
+ import { makeValueTrack, type AnimateOptions, type AnimateValue } from "./animateValue"
8
+ import { CLOCK_GAME, CLOCK_UI, bagProps, iterationsOf, makeTrack, type Track, type TweenMeta, type TweenSpec, type TweenTarget } from "./spec"
9
+
10
+ /** Vector-valued props: an array value is ONE keyframe unless it is an array of vectors. */
11
+ const VECTOR_PROPS = new Set(["position", "scale", "quaternion", "eulerAngles", "color"])
12
+
13
+ const clockOf = (clock: TweenMeta["clock"], fallback: number): number =>
14
+ clock === "game" ? CLOCK_GAME : clock === "ui" ? CLOCK_UI : fallback
15
+
16
+ /** @internal Build the tracks of one options bag on one target. `fromCurrent` = animateTo shape. */
17
+ export const bagTracks = (target: TweenTarget, bag: Record<string, unknown>, atMs: number, fromCurrent: boolean): Track[] => {
18
+ const meta = bag as TweenMeta
19
+ const tracks: Track[] = []
20
+ for (const [prop, value] of bagProps(bag)) {
21
+ const track = makeTrack(target, prop, value, meta, atMs, fromCurrent, VECTOR_PROPS.has(prop))
22
+ if (track) tracks.push(track)
23
+ }
24
+ return tracks
25
+ }
26
+
27
+ /** @internal The `animateTo` / `animateFrom` implementation: one bag → one animation, played now. */
28
+ export const tweenBag = (target: TweenTarget, bag: Record<string, unknown>, fromCurrent: boolean): Animation => {
29
+ const meta = bag as TweenMeta
30
+ const tracks = bagTracks(target, bag, 0, fromCurrent)
31
+ let durationMs = 0
32
+ for (const t of tracks) durationMs = Math.max(durationMs, t.atMs + t.durMs)
33
+ const spec: TweenSpec = {
34
+ clock: clockOf(meta.clock, target._tweenClock),
35
+ durationMs, delayMs: 0,
36
+ iterations: iterationsOf(meta.loop),
37
+ pingPong: meta.loopMode !== "restart",
38
+ rate: 1, tracks, calls: [],
39
+ }
40
+ return new TweenAnimation(spec).play()
41
+ }
42
+
43
+ export type TimelineOptions = {
44
+ /** `'ui'` (default) = wall time; `'game'` follows `Time.scale` and pauses with the game. */
45
+ clock?: "ui" | "game",
46
+ /** Repeat the whole timeline: `true` = forever, a number = cycles. */
47
+ loop?: boolean | number,
48
+ loopMode?: "restart" | "ping-pong",
49
+ /** Wait before the first play, ms. */
50
+ delay?: number,
51
+ }
52
+
53
+ /** Where a track goes: ms, a label, or `[label, offsetMs]`. Default = the current end (sequencing). */
54
+ export type TimelinePosition = number | string | [string, number]
55
+
56
+ export type TimelineAddOptions = {
57
+ at?: TimelinePosition,
58
+ /** With several targets: each starts this many ms after the previous one. */
59
+ stagger?: number,
60
+ }
61
+
62
+ /** A choreography: tracks on many targets at absolute times, JS calls, labels and holds, driven by
63
+ * one clock. Build it once, `play()` on every entrance — `play()` restarts from t = 0 and the pose
64
+ * at any time is fully defined by the tracks (the first keyframe of a track holds before it
65
+ * starts, the last one after it ends). */
66
+ export class TimelineImpl extends TweenAnimation {
67
+ private _labels = new Map<string, number>()
68
+ private _end = 0
69
+
70
+ constructor(options: TimelineOptions = {}) {
71
+ super({
72
+ clock: clockOf(options.clock, CLOCK_UI),
73
+ durationMs: 0,
74
+ delayMs: options.delay ?? 0,
75
+ iterations: iterationsOf(options.loop),
76
+ pingPong: options.loopMode !== "restart",
77
+ rate: 1, tracks: [], calls: [],
78
+ })
79
+ }
80
+
81
+ private _at(at: TimelinePosition | undefined): number {
82
+ if (at === undefined) return this._end
83
+ if (typeof at === "number") return at
84
+ const name = Array.isArray(at) ? at[0] : at
85
+ const offset = Array.isArray(at) ? at[1] : 0
86
+ const base = this._labels.get(name)
87
+ if (base === undefined) throw new Error(`Timeline: unknown label "${name}"`)
88
+ return base + offset
89
+ }
90
+
91
+ private _grow(endMs: number): void {
92
+ if (endMs > this._end) this._end = endMs
93
+ this._spec.durationMs = this._end
94
+ }
95
+
96
+ /** Animate `props` on one target or several (staggered), like `animateTo` — arrays are keyframes,
97
+ * `duration` / `easing` / `times` / `commit` apply. The bag's `delay` shifts the track after `at`. */
98
+ add(target: TweenTarget | TweenTarget[], props: Record<string, unknown> & TweenMeta, options: TimelineAddOptions = {}): this {
99
+ const at = this._at(options.at)
100
+ const targets = Array.isArray(target) ? target : [target]
101
+ const stagger = options.stagger ?? 0
102
+ targets.forEach((t, i) => {
103
+ const tracks = bagTracks(t, props, at + i * stagger, true)
104
+ for (const tr of tracks) { this._spec.tracks.push(tr); this._grow(tr.atMs + tr.durMs) }
105
+ })
106
+ return this
107
+ }
108
+
109
+ /** Like `add` with the `animateFrom` shape: the target's own state is the implicit last keyframe. */
110
+ addFrom(target: TweenTarget | TweenTarget[], props: Record<string, unknown> & TweenMeta, options: TimelineAddOptions = {}): this {
111
+ const at = this._at(options.at)
112
+ const targets = Array.isArray(target) ? target : [target]
113
+ const stagger = options.stagger ?? 0
114
+ targets.forEach((t, i) => {
115
+ const tracks = bagTracks(t, props, at + i * stagger, false)
116
+ for (const tr of tracks) { this._spec.tracks.push(tr); this._grow(tr.atMs + tr.durMs) }
117
+ })
118
+ return this
119
+ }
120
+
121
+ /** A free-value track (see the `animate()` global): the host evaluates it and calls `onUpdate`
122
+ * every frame with the value. */
123
+ animate<T extends AnimateValue>(options: AnimateOptions<T>, at?: TimelinePosition): this {
124
+ const { track, deliver, slot } = makeValueTrack(options, this._at(at))
125
+ this._values.set(slot, deliver)
126
+ this._spec.tracks.push(track)
127
+ this._grow(track.atMs + track.durMs)
128
+ return this
129
+ }
130
+
131
+ /** Call `fn` when playback crosses `at` (forward or backward), in time order, always before
132
+ * finish, never from a previous run. */
133
+ call(at: TimelinePosition, fn: () => void): this {
134
+ const t = this._at(at)
135
+ this._spec.calls.push(t)
136
+ this._calls.push(fn)
137
+ this._grow(t)
138
+ return this
139
+ }
140
+
141
+ /** Name a time — for `at`, `seek` and callers (`tl.labels.landed`). Default = the current end. */
142
+ label(name: string, at?: TimelinePosition): this {
143
+ this._labels.set(name, this._at(at))
144
+ return this
145
+ }
146
+
147
+ /** Named times, ms. */
148
+ get labels(): Record<string, number> {
149
+ const o: Record<string, number> = {}
150
+ for (const [k, v] of this._labels) o[k] = v
151
+ return o
152
+ }
153
+
154
+ /** Extend the timeline with stillness after its current end. */
155
+ hold(ms: number): this {
156
+ this._grow(this._end + Math.max(0, ms))
157
+ return this
158
+ }
159
+
160
+ /** The current end, ms (tracks, calls and holds). */
161
+ get end(): number { return this._end }
162
+
163
+ /** `seek` also accepts a label. */
164
+ override seek(at: number | string): this {
165
+ return super.seek(typeof at === "string" ? this._at(at) : at)
166
+ }
167
+ }
168
+
169
+ /** The timeline handle type (see {@link TimelineImpl} for the members). */
170
+ export type Timeline = TimelineImpl
171
+
172
+ /** Create a timeline — `Timeline()` / `Timeline({ clock: 'game', loop: true })`. */
173
+ export function Timeline(options?: TimelineOptions): Timeline {
174
+ return new TimelineImpl(options)
175
+ }
@@ -4,6 +4,7 @@
4
4
  // evaluates natively and hands back to JS once per frame, the same easing vocabulary, keyframes,
5
5
  // loops and clocks, and the same `Animation` handle. `Timeline.animate` puts one into a timeline.
6
6
 
7
+ import { LINEAR } from "./easing"
7
8
  import { TweenAnimation, type Animation } from "./Animation"
8
9
  import { Mat4 } from "../../math/mat4"
9
10
  import type { Vec2Like, Vec3Like } from "../../math/vec"
@@ -15,8 +16,10 @@ import {
15
16
  type Keyframe, type Track, type TweenChannel, type TweenMeta, type TweenSpec, type TweenTarget,
16
17
  } from "./spec"
17
18
 
18
- /** What `animate()` tweens: a number, a color (string / packed / rgb(a) array — delivered as rgba
19
- * 0..1), a 2 / 3-vector, a quaternion (slerp), or a 4x4 matrix (position / rotation / scale). */
19
+ /** What `animate()` tweens: a number (a FLOAT, never a packed color), a color (any CSS color string,
20
+ * through `Color` — delivered as rgba 0..1; a string that is not a color throws), a 2 / 3-vector (a
21
+ * 3-array is a vector, not a color), a quaternion (slerp), or a 4x4 matrix (position / rotation /
22
+ * scale). */
20
23
  export type AnimateValue = number | string | Vec2Like | Vec3Like | QuatLike | Mat4Like | ColorInput
21
24
 
22
25
  /** `onUpdate` receives a number for a number; every other kind arrives as the SAME `Float32Array`
@@ -65,7 +68,7 @@ export const makeValueTrack = (o: AnimateOptions<any>, atMs: number): { track: T
65
68
  const kind = norm[0]!.kind
66
69
  if (norm.some((n) => n!.kind !== kind)) throw new Error("animate: every value must be the same kind")
67
70
  const lanes = lanesOf(kind)
68
- const easing = easingFor(o.easing, "value")
71
+ const easing = easingFor(o.easing, "value", LINEAR) // a raw progress: the app maps it, the core never pre-eases it
69
72
  const keys: Keyframe[] = norm.map((n, i) => ({
70
73
  t: o.times && o.times.length === norm.length ? o.times[i]! : norm.length === 1 ? 1 : i / (norm.length - 1),
71
74
  easing,
@@ -75,7 +75,7 @@ export const NAMED_EASINGS: ReadonlyArray<readonly [string, Easing]> = [
75
75
  ["inBounce", (x) => 1 - outBounce(1 - x)], // 25
76
76
  ["outBounce", outBounce], // 26
77
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
78
+ ["smoothstep", (x) => x * x * (3 - 2 * x)], // 28 — the $class block default
79
79
  ]
80
80
 
81
81
  /** Short spellings → canonical names. `in`/`out`/`inOut` are the quadratics, matching the SDK's
@@ -94,8 +94,15 @@ const namedId = (name: string): number => {
94
94
 
95
95
  /** The CSS `ease` curve — what a bare `"ease"` means. */
96
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] }
97
+ /** The CSS `ease-in-out` curve — also UIKit's default animation curve, and web-lite's transition
98
+ * default: what every host tweened with before the keyframe core. */
99
+ export const CSS_EASE_IN_OUT: [number, number, number, number] = [0.42, 0, 0.58, 1]
100
+
101
+ export const LINEAR: EasingSpec = { kind: EASE_LINEAR, p: [0, 0, 0, 0] }
102
+ /** The default of every track with a TARGET (`animateTo` / `animateFrom` / `Timeline.add` on an
103
+ * element, a 3D / 2D object): `ease-in-out` (2026-09-27; linear before). `animate()` — a raw
104
+ * progress the app maps itself — stays linear. Write `easing: "linear"` for uniform motion. */
105
+ export const DEFAULT_EASING: EasingSpec = { kind: EASE_BEZIER, p: CSS_EASE_IN_OUT }
99
106
 
100
107
  /** Sample an `Easing` function into a table. Endpoints are forced exact so a curve that is
101
108
  * numerically 0.9999 at 1 still lands on the keyframe. */
@@ -19,7 +19,7 @@
19
19
  // animation first plays (the implicit first key of `animateTo`, the implicit last of `animateFrom`).
20
20
  // String-valued lanes (DISCRETE / STRING / TRANSFORM_MATRIX keys) hold a string-table index.
21
21
 
22
- import { EASE_TABLE_SIZE, evalEasing, parseEasing, type EasingInput, type EasingSpec } from "./easing"
22
+ import { DEFAULT_EASING, EASE_TABLE_SIZE, evalEasing, parseEasing, type EasingInput, type EasingSpec } from "./easing"
23
23
  import { Color } from "../../core/color"
24
24
  import { Mat4 } from "../../math/mat4"
25
25
 
@@ -131,7 +131,8 @@ export type TweenMeta = {
131
131
  delay?: number,
132
132
  /** A curve for every prop, or a per-prop map (`{ width: 'outExpo', opacity: 'linear' }`). Names
133
133
  * (`outCubic`, `inOutSine`, `outBack`, …), `cubic-bezier(…)` / a `[x1, y1, x2, y2]` tuple,
134
- * `steps(n)`, or any `Easing` function. Default linear. */
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. */
135
136
  easing?: EasingInput | Record<string, EasingInput>,
136
137
  /** Keyframe offsets 0..1 for array-valued props (default: evenly spaced). */
137
138
  times?: number[],
@@ -158,12 +159,13 @@ export const warnOnce = (key: string, msg: string): void => {
158
159
  console.warn(`[tween] ${msg}`)
159
160
  }
160
161
 
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)
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)
167
169
  }
168
170
 
169
171
  /** Iteration count from `loop`: 1 = one-shot, -1 = forever, n = cycles. */
@@ -178,7 +180,8 @@ export const iterationsOf = (loop: TweenMeta["loop"]): number => {
178
180
  const UNIT_RE = /^\s*(-?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?)\s*(px|%|vw|vh|vmin|vmax|deg|rad|em|rem)?\s*$/i
179
181
 
180
182
  /** A UI style value: number → FLOAT; "12px" / "50%" → FLOAT_UNIT; other strings → STRING (the host
181
- * decides color vs discrete); booleans → DISCRETE. */
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. */
182
185
  export const uiValue = (v: unknown): ({ kind: number, sig?: string } & TweenValue) | null => {
183
186
  if (typeof v === "number") return { kind: KIND_FLOAT, lanes: [v] }
184
187
  if (typeof v === "boolean") return { kind: KIND_DISCRETE, str: v ? "true" : "false" }
@@ -195,15 +198,23 @@ export const uiValue = (v: unknown): ({ kind: number, sig?: string } & TweenValu
195
198
  return null
196
199
  }
197
200
 
198
- /** Per-function argument counts of a UI transform list (degrees / px are the SDK's units). */
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. */
199
204
  const TRANSFORM_FNS: Record<string, number> = {
200
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,
201
207
  }
202
208
 
203
209
  /** Parse `"translate(10px, 20px) rotate(45deg) scale(1.2)"` into a signature + lanes, or null when a
204
210
  * 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 => {
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 => {
207
218
  const sig: string[] = []
208
219
  const lanes: number[] = []
209
220
  const re = /([a-zA-Z]+)\s*\(([^)]*)\)/g
@@ -215,15 +226,30 @@ export const parseTransformList = (s: string): { sig: string, lanes: number[] }
215
226
  const fn = m[1]!
216
227
  const n = TRANSFORM_FNS[fn]
217
228
  if (n === undefined) return null
229
+ const units: string[] = []
218
230
  const args = m[2]!.split(",").map((a) => {
219
231
  const u = UNIT_RE.exec(a)
232
+ units.push(u ? (u[2] ?? "") : "")
220
233
  return u ? parseFloat(u[1]!) : NaN
221
234
  })
222
235
  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]!)
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
+ }
224
251
  sig.push(fn)
225
252
  lanes.push(...args)
226
- consumed = re.lastIndex
227
253
  }
228
254
  if (src.slice(consumed).trim() !== "") return null
229
255
  return { sig: sig.join(","), lanes }
@@ -428,7 +454,7 @@ export const evaluateTrack = (track: Track, ms: number, out: number[]): void =>
428
454
 
429
455
  export type TweenBlob = { data: Float32Array, strings: string[], targets: unknown[] }
430
456
 
431
- /** Flatten a spec for `_creatorUI.tweenCreate`. Tracks whose target has no host id right now are
457
+ /** Flatten a spec for `_creatorTree.tweenCreate`. Tracks whose target has no host id right now are
432
458
  * dropped (the animation still runs its clock, calls and finish). */
433
459
  export const buildBlob = (spec: TweenSpec): TweenBlob => {
434
460
  const strings: string[] = []
@@ -9,7 +9,7 @@
9
9
  // gun.audio.play(shot) // 3D, through an AudioSource aspect
10
10
  //
11
11
  // A Sound is APP-scoped (the menu and the level share one `shot`); the engine drops every clip
12
- // when `_creatorUtils.run()` swaps worlds. Without host support `load` resolves to a silent clip.
12
+ // when `_creatorApp.run()` swaps worlds. Without host support `load` resolves to a silent clip.
13
13
 
14
14
  import { fetch } from "../runtime/fetch"
15
15
  import { audioSupported } from "./support"
@@ -48,7 +48,7 @@ export class Sound {
48
48
  let channels = 1
49
49
  for (const url of urls) {
50
50
  const resp = await fetch(url, { useOnce: true })
51
- const systemId = (resp as unknown as { _id: number })._id
51
+ const systemId = (resp as unknown as { _h: Handle })._h.id
52
52
  try {
53
53
  const [ clipId, duration, ch ] = await new Promise<[number, number, number]>((resolve, reject) => {
54
54
  _creatorAudio.loadClip(systemId, !!options.stereo,
@@ -59,7 +59,7 @@ export class Sound {
59
59
  durations.push(duration)
60
60
  channels = ch
61
61
  } finally {
62
- resp.dispose()
62
+ // the response body goes with `resp` (owned handle)
63
63
  }
64
64
  }
65
65
  return new Sound(urls, clips, durations, channels)
@@ -81,7 +81,8 @@ class AudioSystem {
81
81
 
82
82
  /** HRTF binaural rendering (MIT KEMAR filters on the CPU): sounds get a real up / down / behind in
83
83
  * HEADPHONES. Off by default — on speakers it only smears the image. Applies to the `hrtfVoices`
84
- * nearest 3D voices; the rest keep plain panning. */
84
+ * nearest 3D voices; the rest keep plain panning. A host built without the filters (an iOS App
85
+ * Clip: they are 184 KB) keeps every voice on the panner — `stats.hrtfVoices` stays 0. */
85
86
  get hrtf(): boolean { return this._hrtf }
86
87
  set hrtf(v: boolean) { this._hrtf = v; if (audioSupported) _creatorAudio.setHrtf(v, this._hrtfVoices) }
87
88
  /** `Time.scale` also pitches the sfx bus: slow motion drops every effect's tone, like a film. Default