lecodes-sdk 1.2.0 → 2.0.1

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
@@ -57,7 +57,7 @@ UIImage(src) // src: string url | SvgSource | imported asset
57
57
  // UIButton — the only TAPPABLE container: a UIRow with children centered on both axes by default.
58
58
  // Anything clickable in the prototype (a card, a list row, an icon) — wrap it in a UIButton.
59
59
  UIButton(...children)
60
- // extra styles: onPressed: { bgColor, opacity, ... } — style while the finger is down (press feedback is
60
+ // extra styles: $pressed: { bgColor, opacity, ... } — style while the finger is down (press feedback is
61
61
  // OPT-IN; set it so the live prototype feels real)
62
62
  // Note: buttons render no chrome of their own — style bgColor/borderRadius/padding yourself, and give a
63
63
  // button an explicit height (on its own it is only as tall as its text). flexDirection: "column" for cards.
@@ -109,8 +109,7 @@ const label: Style<UIText> = { fontSize: 12, fontWeight: 600, letterSpacing: 2 }
109
109
  // Note: calc()/min()/max() do NOT support % ("calc(100% - 20px)" is invalid; use "calc(100vw - 20px)")
110
110
 
111
111
  // --- Colors in UI styles ---
112
- // hex "#f33"/"#f33c"/"#ff3333"/"#ff3333cc" (8 digits = alpha), packed int 0xff3333, "rgb(...)"/"rgba(...)",
113
- // and exactly these names: white black red green blue yellow orange purple gray cyan magenta brown transparent clear
112
+ // any CSS color (hex, rgb()/hsl(), the CSS names — green = #008000, transparent/clear), 0xRRGGBB, [r,g,b(,a)], "var(--x)"
114
113
 
115
114
  // --- Safe areas ---
116
115
  // Env keywords resolving to the device insets (notch, home indicator):
package/prompts/ui.md CHANGED
@@ -27,7 +27,7 @@ UIScreen(...children)
27
27
  // popping back fires onOpen again. Anything started in onOpen (loops, intervals, sockets) MUST be stopped
28
28
  // in onClose (see TIMERS & FRAME LOOP above).
29
29
  // .onTouchStart(cb) — fires for touches anywhere on the screen; use for full-screen gestures (see TOUCH GESTURES above)
30
- // .onBackPressed(cb) — Android hardware/gesture back; typically Router.pop()
30
+ // .onBack(cb) — Android hardware/gesture back; typically Router.pop()
31
31
  // Screens NEVER scroll — the canonical screen is fixed chrome (header, tab bar) + ONE UIScrollable body
32
32
  // with flexGrow: 1: UIScreen(Header(), UIScrollable(content).style({ flexGrow: 1 }))
33
33
  // Note: a screen always fills the device — sizing styles on it (width, height, flexGrow, flexShrink, position) are no-ops
@@ -63,7 +63,7 @@ Router.init(UIScreen(pager, tabBar)) // bar buttons: .onClick(() => pager.sele
63
63
  // UIWidget — floating overlay, independent of screens, always position: fixed in device coordinates
64
64
  // Persists across Router navigation — create ONCE at module scope, reuse; hidden by default.
65
65
  UIWidget(...children)
66
- // .show(), .hide(), .isShow (getter), .onTouchStart(cb), .onBackPressed(cb)
66
+ // .show(), .hide(), .isShown (getter), .onTouchStart(cb), .onBack(cb)
67
67
  // extra style: overlayColor — full-screen scrim BEHIND the widget that blocks taps underneath, turning it into
68
68
  // a modal; null (default) = no layer, "transparent" = invisible but still blocks. A scrim tap fires
69
69
  // .onOverlayTap(cb) — usually () => widget.hide()
@@ -88,7 +88,7 @@ UIModal(...children)
88
88
  UIBottomSheet(...children)
89
89
  // .detents([0.3, 0.6, 1]) — snap positions, ascending fractions of screen height (the map-app model). Call
90
90
  // BEFORE show(); sizes the sheet to the HIGHEST detent — lower detents show the top slice of the content.
91
- // .setDetent(i) (animated), .detent (getter), .onDetentChange(cb(i)) — every settle: finger snap or setDetent()
91
+ // .setDetent(i) (animated), .detent (getter), .onDetent(cb(i)) — every settle: finger snap or setDetent()
92
92
  // .show()/.hide() slide in/out — plus the full UIModal surface (scrim, onOpen/onClose, dismissible).
93
93
  // Dragging below the lowest detent closes it (fires onClose); .dismissible(false) collapses there instead —
94
94
  // the persistent map sheet (pair it with overlayColor: null so the page behind stays interactive).
@@ -174,11 +174,12 @@ player.play() // stop playback in the screen's onClose; .dispose() when gone f
174
174
  // UIButton — the only TAPPABLE container: a UIRow with children centered on both axes by default.
175
175
  // To make anything clickable (a card, a list row, an icon) — wrap it in a UIButton.
176
176
  UIButton(...children)
177
- // .onClick(cb), .onTouchStart(cb), .isPressed() // gestures: see TOUCH GESTURES above
178
- // extra styles: onPressed: { bgColor, opacity, ..., duration? } — style while the finger is down;
177
+ // .onClick(cb), .onTouchStart(cb), .isPressed (getter) // gestures: see TOUCH GESTURES above
178
+ // .onMouseEnter(ev => ev.track({ onMove?, onEnd?, onCancel? })), .isHovered (getter) // mouse only; no leave event — onEnd is the leave
179
+ // extra styles: $pressed: { bgColor, opacity, ..., duration? } — style while the finger is down (reserved class);
179
180
  // rippleColor ("default" or a Color, Android only)
180
181
  // Note: press feedback is OPT-IN — nothing happens visually unless you set it. rippleColor replaces the
181
- // onPressed visual on Android, so rippleColor + onPressed = ripple on Android, onPressed dim on iOS.
182
+ // $pressed visual on Android, so rippleColor + $pressed = ripple on Android, $pressed dim on iOS.
182
183
  // Note: buttons render no chrome of their own — style bgColor/borderRadius/padding yourself, and give a
183
184
  // button an explicit height (on its own it is only as tall as its text). flexDirection: "column" for cards.
184
185
 
@@ -192,7 +193,7 @@ UIInput() / UITextArea()
192
193
  // maxLength: number; autocapitalize: "none"|"words"|"sentences"|"characters"; autocorrect: boolean
193
194
  // keyboardShrink: boolean (default true) — false: keyboard OVERLAYS the UI, no relayout (chat composers)
194
195
  // keyboardDismiss: boolean (default true) — false: taps never dismiss the keyboard (chat composers)
195
- // onFocused: { borderColor, ..., duration? } — style while focused (like onPressed)
196
+ // $focused: { borderColor, ..., duration? } — style while focused (reserved class)
196
197
  // Note: type values are KEYBOARD HINTS, not validators — "number" doesn't block pasted letters. The
197
198
  // phone-pad value is "phone" (there is NO "tel").
198
199
  // Note: "date"/"time" open the native PICKER in the keyboard slot (typing disabled). .value and onChange stay
@@ -240,7 +241,7 @@ UIText("Hi").style({ color: "white" }).style({ fontSize: 20 }).onClick(...)
240
241
  // Older code may pass a style object as the FIRST factory argument — legacy; write new styles with .style().
241
242
 
242
243
  // Mutable CONTENT properties are NOT styles — they live on the element:
243
- // UIText: .text UIInput/UITextArea: .value UIImage: .src UIButton: .isPressed()
244
+ // UIText: .text UIInput/UITextArea: .value UIImage: .src UIButton: .isPressed (read-only)
244
245
  myText.text = "Updated" // re-renders immediately
245
246
 
246
247
  // Style<T> is a global helper type for reusable style objects, no import needed:
@@ -278,20 +279,19 @@ const btn: Style<UIButton> = { bgColor: "#333", borderRadius: 12 }
278
279
  // but do NOT support % ("calc(100% - 20px)" is invalid; use "calc(100vw - 20px)")
279
280
 
280
281
  // --- Colors in UI styles ---
281
- // hex "#f33"/"#f33c"/"#ff3333"/"#ff3333cc", packed int 0xff3333, "rgb(...)"/"rgba(...)", and exactly these names:
282
- // white black red green blue yellow orange purple gray cyan magenta brown transparent clear
283
- // (UI styles only — engine APIs are hex/packed-int only, see COLORS above)
282
+ // any CSS color (hex, rgb()/hsl(), the CSS names — green = #008000, transparent/clear), 0xRRGGBB, [r,g,b(,a)],
283
+ // or "var(--x)" — the same colors as every engine API (see COLORS above); a non-color throws
284
284
 
285
285
  // --- Safe areas & comfort ---
286
286
  // Env keywords resolving to the device insets (notch, home indicator):
287
287
  // "safe-top" | "safe-bottom" | "safe-left" | "safe-right"; "safe-all" — p / m shorthands only
288
288
  // Comfort tokens — where content comfortably starts, never collapse to 0: max(safe-edge, knob) on clearance
289
- // insets (iOS notch/home indicator, gesture nav); past exact-height system BARS (Android status/nav bar) the
290
- // knob ADDS instead. Knob defaults: 12 top, 4 bottom, 16 horizontal (the page gutter):
289
+ // insets (iOS notch/home indicator); past exact-height system BARS (Android status/nav bar) the
290
+ // knob ADDS instead. Knob defaults: 12 top, 8 bottom, 16 horizontal (the page gutter):
291
291
  // "comfort-top" | "comfort-bottom" | "comfort-left" | "comfort-right"
292
292
  // "comfort-x" — px/mx | "comfort-y" — py/my | "comfort-all" — p / m
293
293
  // Accepted on per-side padding & margin, position offsets (top/left/bottom/right), and inside calc()/min()/max():
294
- .style({ pb: "comfort-bottom" }) // bottom bar: 34 over a home indicator, 52 above Android's button bar, 4 on inset-less devices
294
+ .style({ pb: "comfort-bottom" }) // bottom bar: 34 over a home indicator, 56 above Android's button bar, 8 on inset-less devices
295
295
  .style({ px: "comfort-x" }) // page gutter that also clears the notch in landscape
296
296
  .style({ pb: "calc(comfort-bottom + 56px)" }) // scroll content clearing a 56px floating bar
297
297
 
@@ -306,14 +306,9 @@ label.style({ color: T.primaryColor, pl: "var(--comfort-left)" })
306
306
  // Bare comfort-* tokens apply the safe-area formula; "var(--comfort-top)" reads the raw knob.
307
307
  // "var(--name, fallback)" applies the fallback while the key is unset. Env names (safe-*, vw/vh) are not theme keys.
308
308
 
309
- // --- Responsive: onLandscape / onPortrait ---
310
- // Any style can nest orientation overrides, merged on top while the device is in that orientation:
311
- .style({ flexDirection: "column", p: 16, onLandscape: { flexDirection: "row", p: 32 } })
312
- // Keys: onLandscape, onPortrait.
313
-
314
- // --- Style classes ($name) — named states that CASCADE ---
315
- // A $-prefixed key in .style() declares a named style state (like onPressed, but yours: selected,
316
- // checked, expanded); duration/delay inside the block animate the swap. Drive it via el.class:
309
+ // --- Style classes ($name) — the ONLY state mechanism; they CASCADE ---
310
+ // A $-prefixed key in .style() declares a style state (yours: selected, checked, expanded);
311
+ // duration/delay/easing inside the block animate the swap. Drive yours via el.class:
317
312
  const item = UIButton(UIText("Wi-Fi")).style({ bgColor: "#151515", $selected: { bgColor: "#1d2b45", duration: 150 } })
318
313
  item.onClick(ev => ev.target.class.selected = !ev.target.class.selected)
319
314
  // el.class.selected reads/writes a boolean; el.class({ a: true, b: false }) is the chainable batch
@@ -324,11 +319,13 @@ UIButton(
324
319
  UIImage(icon).style({ tintColor: "#888", $selected: { tintColor: "#5b8cff" } }),
325
320
  UIText("Label").style({ color: "#888", $selected: { color: "#5b8cff" } }),
326
321
  ) // .class.selected = true → icon AND label restyle (the cascade stops at hosted screens/widgets)
327
- // $pressed / $focused are RESERVED — the system toggles them (finger down / input focused) and they
328
- // CASCADE, so a button's children can restyle during its press. onPressed/onFocused stay
329
- // own-element-only — use those on nested interactives (a button inside a clickable card):
330
- UIButton(UIText("Buy").style({ $pressed: { color: "#999" } })).style({ $pressed: { transform: "scale(0.97)" } })
331
- // Precedence on the same prop: base < $classes < $pressed/$focused < onPressed/onFocused.
322
+ // RESERVED — the system toggles them, el.class.pressed = … throws:
323
+ // $hovered / $pressed / $focused (mouse over / finger down / input focused) cascade to children but STOP at
324
+ // a nested UIButton — a button inside a pressed card is not pressed;
325
+ // $landscape / $portrait — GLOBAL, from the display size (the swap on rotation is instant).
326
+ UIButton(UIText("Buy").style({ $pressed: { color: "#999" } })).style({ $hovered: { opacity: 0.9 }, $pressed: { transform: "scale(0.97)" } })
327
+ screen.style({ flexDirection: "column", p: 16, $landscape: { flexDirection: "row", p: 32 } })
328
+ // Precedence on the same prop: base < $landscape/$portrait < $classes < $hovered < $pressed < $focused.
332
329
 
333
330
  // --- Defaults that surprise ---
334
331
  // flexShrink: 0 — elements don't shrink to fit (exception: UIScrollable, UIVirtualizedList and UIPager
@@ -411,14 +408,25 @@ UIButton().onLayout(({ width }) => { buttonWidth = width })
411
408
  // ===== ROUTER (multi-page apps) =====
412
409
  // Tabs are UITabs' job, in-tab stacks UIPager's (see UI COMPONENTS) — the Router owns what sits ABOVE the shell.
413
410
  Router.init(homeScreen, opts?: { showDefaultBackButton?: boolean }) // call once in the entry file (default false)
414
- Router.push(screen) // push onto the stack, screen becomes active
415
- Router.pop(to?: number) // default -1 = one back; negative = relative (-2 = back two), 0/positive = absolute
411
+ Router.push(screen, opts?: { transition, popTransition }) // push onto the stack, screen becomes active (default "push")
412
+ Router.pop(to?: number, opts?: { transition })
413
+ // default -1 = one back; negative = relative (-2 = back two), 0/positive = absolute
416
414
  // stack index (0 = home). Popped screens' native trees are destroyed; the UIScreen
417
- // object stays reusable (push remounts it).
418
- Router.replace(screen, opts?: { transition }) // swap the top screen; transition: "slide-from-left" |
419
- // "slide-from-right" | "slide-from-top" | "slide-from-bottom" | "zoom" | "zoom-out" | "zoom-in" | "fade" | "none" (default "fade")
415
+ // object stays reusable (push remounts it). Plays the way back the screen remembers.
416
+ Router.replace(screen, opts?: { transition, popTransition }) // swap the top screen (default "none"; theme({ replaceTransition }))
420
417
  Router.current // the active UIScreen (getter)
421
- Router.hide() / Router.restore() // hide/restore the whole router — stack stays alive; fires the top screen's onClose/onOpen
418
+ Router.hide(opts?) / Router.restore(opts?) // hide/restore the whole router — stack stays alive; fires the top screen's onClose/onOpen
419
+ // TRANSITIONS — every call above, and open() / close() of any destination, takes { transition }:
420
+ // a name: "push" | "pop" | "slide-from-left" | "slide-from-right" | "slide-from-top" | "slide-from-bottom" |
421
+ // "zoom" | "zoom-in" | "zoom-out" | "fade" | "none"
422
+ // or your own: { enter?: pose, exit?: pose, onTop?: "enter" | "exit" } — the incoming screen comes FROM `enter`,
423
+ // the outgoing one goes TO `exit`. pose = { transform?, opacity?, dim?, duration? (ms, 300), delay?, easing? };
424
+ // arrays are keyframes; in a pose a % in translate is a percent of the screen ("translateY(100%)").
425
+ // e.g. Router.push(page, { transition: { enter: { transform: "translateY(100%)", duration: 400 }, exit: { dim: 0.3 } } })
426
+ // popTransition = the way BACK the screen remembers (Router.pop(), the back gesture, the system's back play it).
427
+ // Default: the transition it came with, the other way round (a slide from the bottom leaves downwards;
428
+ // a zoom / a fade go back with a fade). A replace without one keeps the way back of the screen it replaces.
429
+ // A screen's own transform / opacity are not seen while a transition moves it — style a child instead.
422
430
  Router.addEventListener("change", (screen: UIScreen) => ...) / .removeEventListener("change", cb) // after every navigation
423
431
  // Screens BELOW the top stay mounted — element trees and state survive, they're just not rendered.
424
432
  // push fires the outgoing screen's onClose + the incoming one's onOpen; pop fires them in reverse and
@@ -622,7 +630,7 @@ const detailScreen = (name: string) => UIScreen(
622
630
  ).style({ p: 16, pt: "comfort-top", gap: 16, bgColor: "black" })
623
631
 
624
632
  const Item = (name: string) => UIButton(UIText(name).style({ color: "white" }))
625
- .style({ height: 52, px: 16, justifyContent: "flex-start", borderRadius: 12, bgColor: "#151515", onPressed: { opacity: 0.7 } })
633
+ .style({ height: 52, px: 16, justifyContent: "flex-start", borderRadius: 12, bgColor: "#151515", $pressed: { opacity: 0.7 } })
626
634
  .onClick(() => UIPager.push(detailScreen(name))) // in-tab push: tab bar stays, back-swipe pops
627
635
 
628
636
  export const homeScreen = UIScreen(
@@ -658,7 +666,7 @@ Router.init(tabs)
658
666
  const Field = (label: string, style: Style<UIInput> = {}) => {
659
667
  const input = UIInput().style({ height: 50, px: 14, borderRadius: 12, bgColor: "#161A22",
660
668
  border: "1px solid #262C3A", color: "white", placeholderColor: "#5A6272",
661
- onFocused: { borderColor: "#4C8DFF" }, ...style })
669
+ $focused: { borderColor: "#4C8DFF" }, ...style })
662
670
  const message = UIText("").style({ fontSize: 13, minHeight: 18, color: "#FF6B6B" })
663
671
  input.onChange(() => message.text = "")
664
672
  return {
@@ -709,7 +717,7 @@ const screen = UIScreen(
709
717
  UIText("Create account").style({ fontSize: 28, fontWeight: 700, mb: 12 }),
710
718
  name.node, email.node, password.node,
711
719
  UIButton(btnLabel = UIText("Create account").style({ fontSize: 16, fontWeight: 700 }))
712
- .style({ name: "submit", height: 52, borderRadius: 14, bgColor: "#4C8DFF", mt: 8, onPressed: { opacity: 0.85 } })
720
+ .style({ name: "submit", height: 52, borderRadius: 14, bgColor: "#4C8DFF", mt: 8, $pressed: { opacity: 0.85 } })
713
721
  .onClick(() => submit())
714
722
  ).style({ flexGrow: 1, px: 20, pt: "comfort-top", pb: 28, gap: 8 })
715
723
  ).style({ bgColor: "#0C0F14" })
@@ -739,7 +747,7 @@ const sheet = UIBottomSheet(
739
747
  .style({ bgColor: "white", borderRadius: 20, overlayColor: null }) // no scrim — the map stays interactive
740
748
  .detents([0.25, 0.6, 1]) // collapsed / half / full
741
749
  .dismissible(false) // drag below the lowest detent collapses, never closes
742
- .onDetentChange(i => console.log("detent", i))
750
+ .onDetent(i => console.log("detent", i))
743
751
 
744
752
  const mapScreen = UIScreen(
745
753
  // the map / page content behind the sheet
@@ -1,11 +1,12 @@
1
1
  // The animation HANDLE (docs/timeline-plan.md §2.2): what `animateTo` / `animateFrom` / `Timeline`
2
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
3
+ // to the host (`_creatorTree.tweenCreate`) and gets events back (`registerTweenEvent`: time-callbacks
4
4
  // and finish). On a host without the tween core the UI tracks fall back to the old
5
5
  // `animateTo` / `animateFrom` bridge pair (single-value tweens only) and the clock is a timer —
6
6
  // see `fallback` below.
7
7
 
8
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"
9
10
 
10
11
  const OP_PLAY = 0
11
12
  const OP_PAUSE = 1
@@ -43,17 +44,17 @@ export interface Animation {
43
44
  onFinish(fn: (done: boolean) => void): this
44
45
  }
45
46
 
46
- const hasCore = (): boolean => typeof _creatorUI !== "undefined" && typeof (_creatorUI as any).tweenCreate === "function"
47
47
 
48
48
  // ---- host events -------------------------------------------------------------------------------
49
49
 
50
- const live = new Map<number, TweenAnimation>()
50
+ /** The animations native owns right now (playing), by id — what the host's events resolve to. */
51
+ const tweens = new Pins<TweenAnimation>()
51
52
  let eventsRegistered = false
52
53
  const ensureEvents = (): void => {
53
54
  if (eventsRegistered) return
54
55
  eventsRegistered = true
55
- ;(_creatorUI as any).registerTweenEvent?.(function (this: unknown, id: number, kind: number, index: number) {
56
- const a = live.get(id)
56
+ _creatorTree.registerTweenEvent(function (this: unknown, id: number, kind: number, index: number) {
57
+ const a = tweens.get(id)
57
58
  if (!a) return
58
59
  if (kind === EV_CALL) a._fireCall(index)
59
60
  else if (kind === EV_FINISH) a._hostFinished()
@@ -81,7 +82,8 @@ export class TweenAnimation implements Animation {
81
82
  /** @internal VALUE-track deliverers by slot (`animate()` / `Timeline.animate`). */
82
83
  _values = new Map<number, (lanes: Float32Array) => void>()
83
84
 
84
- private _id = 0 // host id, 0 = not created
85
+ /** @internal The host animation's owned handle (DEAD_HANDLE while there is none). */
86
+ _h: Handle = DEAD_HANDLE
85
87
  private _started = false // play() ran at least once (the first play keeps the constructor's promise)
86
88
  private _endTime = 0 // `time` after the host freed a finished animation
87
89
  private _playing = false
@@ -90,7 +92,6 @@ export class TweenAnimation implements Animation {
90
92
  private _resolve!: (done: boolean) => void
91
93
  private _settled = true
92
94
  private _listeners: Array<(done: boolean) => void> = []
93
- private _fb?: Fallback
94
95
 
95
96
  constructor(spec: TweenSpec) {
96
97
  this._spec = spec
@@ -118,13 +119,12 @@ export class TweenAnimation implements Animation {
118
119
  get rate(): number { return this._rate }
119
120
  set rate(r: number) {
120
121
  this._rate = r
121
- if (this._id) (_creatorUI as any).tweenSetRate(this._id, r)
122
- this._fb?.setRate(r)
122
+ if (this._h.id) _creatorTree.tweenSetRate(this._h.id, r)
123
123
  }
124
124
 
125
125
  get time(): number {
126
- if (this._id) return (_creatorUI as any).tweenGetTime?.(this._id) ?? 0
127
- return this._fb?.time ?? this._endTime
126
+ if (this._h.id) return _creatorTree.tweenGetTime(this._h.id)
127
+ return this._endTime
128
128
  }
129
129
  set time(ms: number) { this.seek(ms) }
130
130
 
@@ -143,36 +143,29 @@ export class TweenAnimation implements Animation {
143
143
  return s.durationMs * n * (s.pingPong && s.iterations !== 1 ? 2 : 1)
144
144
  }
145
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)
146
+ if (this._h.id) return _creatorTree.tweenGetElapsed(this._h.id)
147
+ return this._endTime > 0 ? this._totalMs() : 0
148
148
  }
149
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
- }
150
+ this._ensure()
151
+ _creatorTree.tweenSeek(this._h.id, ms)
156
152
  }
157
153
 
158
154
  /** Create the host animation (paused at t = 0) if it doesn't exist. */
159
155
  private _ensure(): void {
160
- if (this._id) return
156
+ if (this._h.id) return
161
157
  ensureEvents()
162
158
  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)
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)
166
161
  }
167
162
 
168
163
  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
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
173
168
  }
174
- this._fb?.cancel()
175
- this._fb = undefined
176
169
  }
177
170
 
178
171
  play(): this {
@@ -195,27 +188,26 @@ export class TweenAnimation implements Animation {
195
188
  if (last && last.value !== undefined) t.channel.commit(last.raw)
196
189
  }
197
190
  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
- }
191
+ this._ensure()
192
+ _creatorTree.tweenControl(this._h.id, OP_PLAY)
193
+ tweens.pin(this) // playing: native owns it, its events must find it
204
194
  return this
205
195
  }
206
196
 
207
197
  pause(): this {
208
- if (hasCore()) { this._ensure(); (_creatorUI as any).tweenControl(this._id, OP_PAUSE) }
209
- else this._ensureFallback().pause()
198
+ this._ensure()
199
+ _creatorTree.tweenControl(this._h.id, OP_PAUSE)
210
200
  this._playing = false
201
+ tweens.unpin(this._h.id) // paused: only the app's reference keeps it
211
202
  return this
212
203
  }
213
204
 
214
205
  resume(): this {
215
206
  if (this._settled) return this.play()
216
- if (hasCore()) { this._ensure(); (_creatorUI as any).tweenControl(this._id, OP_RESUME) }
217
- else this._ensureFallback().resume()
207
+ this._ensure()
208
+ _creatorTree.tweenControl(this._h.id, OP_RESUME)
218
209
  this._playing = true
210
+ tweens.pin(this)
219
211
  return this
220
212
  }
221
213
 
@@ -229,12 +221,8 @@ export class TweenAnimation implements Animation {
229
221
  }
230
222
 
231
223
  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
- }
224
+ this._ensure()
225
+ _creatorTree.tweenControl(this._h.id, OP_FINISH) // the host answers with EV_FINISH
238
226
  return this
239
227
  }
240
228
 
@@ -256,115 +244,11 @@ export class TweenAnimation implements Animation {
256
244
 
257
245
  /** @internal The host reached the end (naturally or via finish()) and has freed the animation. */
258
246
  _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 }
247
+ if (this._h.id) { tweens.unpin(this._h.id); this._h = DEAD_HANDLE } // the runtime freed it
262
248
  this._endTime = this._spec.durationMs
263
249
  this._settle(true)
264
250
  }
265
251
 
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
252
  }
369
253
 
370
254
  /** @internal Build a one-bag animation for a single target (the `animateTo` / `animateFrom` path). */