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.
- package/README.md +5 -2
- package/dist/editor.d.ts +12 -0
- package/dist/global.d.ts +4 -8
- package/dist/host.d.ts +2 -3
- package/dist/types/animate/tween/Animation.d.ts +0 -3
- package/dist/types/animate/tween/animateValue.d.ts +4 -2
- package/dist/types/animate/tween/easing.d.ts +8 -0
- package/dist/types/animate/tween/spec.d.ts +15 -7
- package/dist/types/audio/audio.d.ts +2 -1
- package/dist/types/canvas/Canvas.d.ts +40 -108
- package/dist/types/canvas/gen/cssColor.d.ts +17 -0
- package/dist/types/canvas/gen/recorder.d.ts +118 -0
- package/dist/types/canvas/gen/spec.d.ts +144 -0
- package/dist/types/core/color.d.ts +3 -1
- package/dist/types/core/pins.d.ts +18 -0
- package/dist/types/g2/Node2D.d.ts +5 -8
- package/dist/types/g2/Scene2D.d.ts +6 -2
- package/dist/types/gl/Foliage.d.ts +30 -7
- package/dist/types/gl/Light.d.ts +11 -0
- package/dist/types/gl/Lightmap.d.ts +13 -2
- package/dist/types/gl/Material.d.ts +17 -3
- package/dist/types/gl/Model.d.ts +6 -2
- package/dist/types/gl/Node.d.ts +3 -6
- package/dist/types/gl/Particles.d.ts +15 -0
- package/dist/types/gl/Scene.d.ts +41 -13
- package/dist/types/gl/Texture.d.ts +1 -1
- package/dist/types/gl/animation/Locomotion.d.ts +59 -3
- package/dist/types/gl/terrain/Terrain.d.ts +8 -0
- package/dist/types/inject.d.ts +11 -11
- package/dist/types/inject.editor.d.ts +1 -0
- package/dist/types/net/core.d.ts +7 -0
- package/dist/types/plugin.d.ts +76 -0
- package/dist/types/plugins/gen/camera/sdk/camera.d.ts +24 -0
- package/dist/types/plugins/gen/camera/sdk/camera.gen.d.ts +25 -0
- package/dist/types/plugins/{geolocation.d.ts → gen/geolocation/sdk/geolocation.d.ts} +2 -20
- package/dist/types/plugins/gen/geolocation/sdk/geolocation.gen.d.ts +31 -0
- package/dist/types/plugins/{map.d.ts → gen/map/sdk/map.d.ts} +9 -50
- package/dist/types/plugins/gen/map/sdk/map.gen.d.ts +53 -0
- package/dist/types/plugins/gen/push/sdk/push.d.ts +23 -0
- package/dist/types/plugins/gen/push/sdk/push.gen.d.ts +35 -0
- package/dist/types/plugins/{qr.d.ts → gen/qr-scanner/sdk/qr-scanner.d.ts} +2 -3
- package/dist/types/plugins/gen/qr-scanner/sdk/qr-scanner.gen.d.ts +15 -0
- package/dist/types/runtime/app.d.ts +9 -2
- package/dist/types/runtime/fetch.d.ts +2 -0
- package/dist/types/runtime/input.d.ts +8 -1
- package/dist/types/runtime/media.d.ts +6 -10
- package/dist/types/runtime/misc.d.ts +4 -1
- package/dist/types/runtime/net.d.ts +3 -2
- package/dist/types/runtime/touch.d.ts +32 -0
- package/dist/types/scene/defineScene.d.ts +43 -2
- package/dist/types/scene/editor.d.ts +52 -0
- package/dist/types/scene/gizmos.d.ts +7 -4
- package/dist/types/ui/NativeView.d.ts +6 -4
- package/dist/types/ui/UI.d.ts +1 -1
- package/dist/types/ui/UIBottomSheet.d.ts +6 -12
- package/dist/types/ui/UIButton.d.ts +12 -14
- package/dist/types/ui/UIContainer.d.ts +0 -6
- package/dist/types/ui/UIImage.d.ts +1 -4
- package/dist/types/ui/UIInput.d.ts +8 -24
- package/dist/types/ui/UIModal.d.ts +0 -2
- package/dist/types/ui/UINode.d.ts +70 -56
- package/dist/types/ui/UIPager.d.ts +28 -27
- package/dist/types/ui/UIPopover.d.ts +0 -2
- package/dist/types/ui/UIScreen.d.ts +12 -17
- package/dist/types/ui/UIScrollable.d.ts +1 -4
- package/dist/types/ui/UIText.d.ts +0 -2
- package/dist/types/ui/UIVideo.d.ts +3 -5
- package/dist/types/ui/UIVirtualizedList.d.ts +14 -16
- package/dist/types/ui/UIWidget.d.ts +10 -9
- package/dist/types/ui/colorKeys.gen.d.ts +9 -0
- package/dist/types/ui/presentable.d.ts +46 -32
- package/dist/types/ui/router.d.ts +18 -7
- package/dist/types/ui/styleColor.d.ts +1 -0
- package/dist/types/ui/transitions.d.ts +18 -0
- package/dist/types/ui/tree.d.ts +75 -0
- package/dist/types/version.d.ts +10 -0
- package/dist/types.json +1 -1
- package/package.json +12 -3
- package/prompts/2d.md +2 -6
- package/prompts/3d.md +1 -5
- package/prompts/README.md +142 -142
- package/prompts/canvas.md +9 -8
- package/prompts/compose.ts +1 -1
- package/prompts/core.md +3 -3
- package/prompts/design.md +1 -1
- package/prompts/dist/2d-game.md +77 -54
- package/prompts/dist/3d-app.md +75 -52
- package/prompts/dist/ar-app.md +74 -51
- package/prompts/dist/design.md +30 -8
- package/prompts/dist/ui-app.md +73 -46
- package/prompts/ui-design.md +2 -3
- package/prompts/ui.md +45 -37
- package/src/animate/tween/Animation.ts +262 -378
- package/src/animate/tween/animateValue.ts +103 -100
- package/src/animate/tween/easing.ts +10 -3
- package/src/animate/tween/spec.ts +505 -479
- package/src/audio/Sound.ts +3 -3
- package/src/audio/audio.ts +162 -161
- package/src/bridges/2d.d.ts +317 -0
- package/src/bridges/app.d.ts +91 -0
- package/src/bridges/audio.d.ts +97 -0
- package/src/bridges/canvas.d.ts +79 -0
- package/src/bridges/device.d.ts +72 -0
- package/src/bridges/fetch.d.ts +80 -0
- package/src/bridges/files.d.ts +70 -0
- package/src/bridges/gl.d.ts +1133 -0
- package/src/bridges/input.d.ts +72 -0
- package/src/bridges/media.d.ts +55 -0
- package/src/bridges/nav.d.ts +71 -0
- package/src/bridges/net.d.ts +52 -0
- package/src/bridges/service.d.ts +52 -0
- package/src/bridges/socket.d.ts +31 -0
- package/src/bridges/storage.d.ts +33 -0
- package/src/bridges/tree.d.ts +301 -0
- package/src/bridges/types.d.ts +49 -0
- package/src/canvas/Canvas.ts +114 -159
- package/src/canvas/gen/cssColor.ts +224 -0
- package/src/canvas/gen/recorder.ts +212 -0
- package/src/canvas/gen/spec.ts +201 -0
- package/src/chisel.ts +193 -0
- package/src/compile/assetMacro.ts +1 -1
- package/src/compile/bundler.ts +11 -2
- package/src/compile/compileProject.ts +43 -4
- package/src/compile/fontMacro.ts +3 -4
- package/src/compile/header.ts +26 -5
- package/src/compile/index.ts +3 -1
- package/src/compile/liteMaterial.ts +1 -1
- package/src/compile/sceneEditor.ts +11 -26
- package/src/core/color.ts +73 -30
- package/src/core/pins.ts +51 -0
- package/src/core/signals.ts +8 -1
- package/src/g2/CharacterController2D.ts +3 -3
- package/src/g2/Node2D.ts +57 -39
- package/src/g2/Physics2D.ts +2 -2
- package/src/g2/Scene2D.ts +35 -23
- package/src/g2/Texture2D.ts +1 -1
- package/src/g2/loop.ts +4 -4
- package/src/gl/Foliage.ts +72 -17
- package/src/gl/Geometry.ts +1 -2
- package/src/gl/Light.ts +13 -0
- package/src/gl/Lightmap.ts +45 -29
- package/src/gl/Material.ts +95 -51
- package/src/gl/Model.ts +172 -167
- package/src/gl/Node.ts +91 -24
- package/src/gl/Particles.ts +20 -1
- package/src/gl/Scene.ts +100 -44
- package/src/gl/Texture.ts +8 -7
- package/src/gl/animation/AnimationClip.ts +1 -1
- package/src/gl/animation/Locomotion.ts +77 -8
- package/src/gl/nav/NavMesh.ts +3 -4
- package/src/gl/physics/Physics.ts +2 -2
- package/src/gl/physics/physicsEvents.ts +3 -3
- package/src/gl/terrain/Terrain.ts +33 -5
- package/src/gl/touch.ts +14 -15
- package/src/host.d.ts +2 -3
- package/src/inject.editor.ts +7 -0
- package/src/inject.ts +233 -236
- package/src/net/core.ts +6 -5
- package/src/net/index.ts +1 -1
- package/src/net/replication.ts +1 -1
- package/src/plugin.ts +191 -0
- package/src/plugins/gen/camera/contract.d.ts +27 -0
- package/src/plugins/gen/camera/sdk/camera.gen.ts +46 -0
- package/src/plugins/gen/camera/sdk/camera.ts +57 -0
- package/src/plugins/gen/geolocation/contract.d.ts +50 -0
- package/src/plugins/gen/geolocation/sdk/geolocation.gen.ts +54 -0
- package/src/plugins/{geolocation.ts → gen/geolocation/sdk/geolocation.ts} +22 -43
- package/src/plugins/gen/map/contract.d.ts +144 -0
- package/src/plugins/gen/map/sdk/map.gen.ts +88 -0
- package/src/plugins/{map.ts → gen/map/sdk/map.ts} +68 -102
- package/src/plugins/gen/push/contract.d.ts +61 -0
- package/src/plugins/gen/push/sdk/push.gen.ts +60 -0
- package/src/plugins/gen/push/sdk/push.ts +105 -0
- package/src/plugins/gen/qr-scanner/contract.d.ts +16 -0
- package/src/plugins/gen/qr-scanner/sdk/qr-scanner.gen.ts +29 -0
- package/src/plugins/gen/qr-scanner/sdk/qr-scanner.ts +52 -0
- package/src/plugins/permission.ts +5 -4
- package/src/runtime/app.ts +20 -9
- package/src/runtime/appEvents.ts +5 -4
- package/src/runtime/channel.ts +18 -15
- package/src/runtime/clipboard.ts +4 -3
- package/src/runtime/datetime.ts +2 -1
- package/src/runtime/device.ts +17 -15
- package/src/runtime/fetch.ts +30 -20
- package/src/runtime/files.ts +16 -15
- package/src/runtime/input.ts +21 -8
- package/src/runtime/media.ts +50 -46
- package/src/runtime/misc.ts +7 -3
- package/src/runtime/net.ts +8 -7
- package/src/runtime/rpc.ts +1 -3
- package/src/runtime/service.ts +19 -14
- package/src/runtime/share.ts +4 -3
- package/src/runtime/storage.ts +6 -4
- package/src/runtime/touch.ts +32 -0
- package/src/scene/defineScene.ts +61 -365
- package/src/scene/editor.ts +408 -0
- package/src/scene/editorPlugins.ts +3 -3
- package/src/scene/gizmos.ts +154 -148
- package/src/server/db/marci/query.ts +1 -1
- package/src/server/host.ts +1 -1
- package/src/server/runtime.ts +1 -1
- package/src/ui/NativeView.ts +72 -25
- package/src/ui/UI.ts +3 -3
- package/src/ui/UIBottomSheet.ts +16 -17
- package/src/ui/UIButton.ts +54 -16
- package/src/ui/UIContainer.ts +0 -6
- package/src/ui/UIImage.ts +34 -30
- package/src/ui/UIInput.ts +29 -37
- package/src/ui/UIModal.ts +1 -3
- package/src/ui/UINode.ts +347 -297
- package/src/ui/UIPager.ts +93 -78
- package/src/ui/UIPopover.ts +0 -2
- package/src/ui/UIScreen.ts +41 -36
- package/src/ui/UIScrollable.ts +19 -13
- package/src/ui/UISpacer.ts +1 -1
- package/src/ui/UITabs.ts +8 -6
- package/src/ui/UIText.ts +7 -17
- package/src/ui/UIVideo.ts +30 -27
- package/src/ui/UIVirtualizedList.ts +58 -59
- package/src/ui/UIWidget.ts +38 -18
- package/src/ui/colorKeys.gen.ts +37 -0
- package/src/ui/fonts.ts +2 -2
- package/src/ui/presentable.ts +59 -42
- package/src/ui/router.ts +52 -35
- package/src/ui/styleColor.ts +56 -0
- package/src/ui/theme.ts +7 -6
- package/src/ui/transitions.ts +249 -0
- package/src/ui/tree.ts +346 -0
- package/src/version.ts +24 -0
- package/tests/helpers/engineWorld.ts +11 -0
- package/tests/helpers/fakeTree.ts +353 -0
- package/tests/helpers/hostStubs.ts +31 -0
- package/tests/helpers/index.ts +14 -0
- package/tests/helpers/memoryMarci.ts +124 -0
- package/tests/helpers/phases.ts +23 -0
- package/tests/helpers/preload.ts +18 -0
- package/tests/helpers/stubApp.ts +2 -0
- package/tests/helpers/stubDevice.ts +2 -0
- package/tests/helpers/stubFetch.ts +2 -0
- package/tests/helpers/stubInput.ts +2 -0
- package/dist/inject.js +0 -4730
- package/dist/types/core/registry.d.ts +0 -7
- package/dist/types/plugins/camera.d.ts +0 -25
- package/dist/types/plugins/push.d.ts +0 -46
- package/src/bridges.d.ts +0 -1760
- package/src/compile/__tests__/assetIconMacro.test.ts +0 -219
- package/src/compile/__tests__/assetMacro.test.ts +0 -100
- package/src/compile/__tests__/assetName.test.ts +0 -55
- package/src/compile/__tests__/compile.test.ts +0 -310
- package/src/compile/__tests__/detectEntry.test.ts +0 -151
- package/src/compile/__tests__/fontMacro.test.ts +0 -199
- package/src/compile/__tests__/serverSplit.test.ts +0 -27
- package/src/core/__tests__/stateMachine.test.ts +0 -132
- package/src/core/registry.ts +0 -23
- package/src/plugins/camera.ts +0 -81
- package/src/plugins/push.ts +0 -132
- package/src/plugins/qr.ts +0 -73
package/src/ui/presentable.ts
CHANGED
|
@@ -4,45 +4,61 @@
|
|
|
4
4
|
// longer overlays a scene, it replaces it (with a transition); UI over a scene goes through
|
|
5
5
|
// `UIWidget.attachTo(scene)`.
|
|
6
6
|
//
|
|
7
|
+
// A destination is addressed by (kind, id): a screen / video by its node id, a scene by its
|
|
8
|
+
// scene id, a native view by its viewId (`_dest()`). The runtime reports the lifecycle back through
|
|
9
|
+
// the tree emitter (TREE_EVENT_OPEN / CLOSE / BACK) — see ui/tree.ts resolveDest.
|
|
10
|
+
//
|
|
7
11
|
// This module is dependency-free on purpose: gl/ and g2/ import it, so it must not pull any UI
|
|
8
12
|
// element code into engine-only bundles.
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
|
|
14
|
+
import type { EasingInput } from "../animate/tween/easing"
|
|
15
|
+
|
|
16
|
+
/** The built-in transitions (ui/transitions.ts holds what each one is). `push` / `pop` are the
|
|
17
|
+
* stacked-navigation pair — the incoming screen slides over the other one, which drifts and dims;
|
|
18
|
+
* the rest are two-screen moves. */
|
|
12
19
|
export type TransitionName =
|
|
13
20
|
| "push" | "pop"
|
|
14
21
|
| "slide-from-left" | "slide-from-right" | "slide-from-top" | "slide-from-bottom"
|
|
15
22
|
| "zoom" | "zoom-in" | "zoom-out" | "fade" | "none"
|
|
16
23
|
|
|
17
|
-
/**
|
|
18
|
-
*
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
/**
|
|
25
|
+
* One screen's pose in a transition — the keys of an `animateTo` bag a transition can move, with
|
|
26
|
+
* the side's own timing. An array is a list of keyframes, as there.
|
|
27
|
+
*/
|
|
28
|
+
export type TransitionPose = {
|
|
29
|
+
/** A transform list, as in a style — and here a `%` in `translate` is a percent of the screen's
|
|
30
|
+
* own box: `"translateX(100%)"` is one width to the right. */
|
|
31
|
+
transform?: string | string[]
|
|
32
|
+
opacity?: number | number[]
|
|
33
|
+
/** Black over this screen, 0..1: how dark the screen UNDER the other one gets. */
|
|
34
|
+
dim?: number | number[]
|
|
35
|
+
/** ms (default 300). */
|
|
36
|
+
duration?: number
|
|
37
|
+
/** ms to wait; the screen holds its first pose through it. */
|
|
38
|
+
delay?: number
|
|
39
|
+
/** Any curve `animateTo` takes (default `ease-in-out`). */
|
|
40
|
+
easing?: EasingInput
|
|
24
41
|
}
|
|
25
42
|
|
|
26
43
|
/**
|
|
27
|
-
* A
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
44
|
+
* A transition between two destinations, described by where each side is when it is NOT at rest:
|
|
45
|
+
* the incoming one comes FROM `enter`, the outgoing one goes TO `exit`. A side left out stays put.
|
|
46
|
+
*
|
|
47
|
+
* ```ts
|
|
48
|
+
* Router.push(page, { transition: { enter: { transform: "translateY(100%)", duration: 400 }, exit: { dim: 0.3 } } })
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* A screen's own `transform` / `opacity` are not seen while a transition moves it (put a lasting
|
|
52
|
+
* effect on a child). A scene, a native view, a video never move: next to one of them only the
|
|
53
|
+
* screen's side plays, above it.
|
|
31
54
|
*/
|
|
32
55
|
export interface TransitionSpec {
|
|
33
|
-
/**
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
/** The leaving destination animates from identity TO this pose. */
|
|
40
|
-
outgoing?: { to?: TransitionTransform }
|
|
41
|
-
/** Scrim alpha (0..1) painted over whichever destination is underneath. */
|
|
42
|
-
dim?: number
|
|
43
|
-
/** Which side stacks on top during the animation (default "incoming"). On Android, transitions
|
|
44
|
-
* involving a scene always animate the screen side on top — the scene view itself never moves. */
|
|
45
|
-
onTop?: "incoming" | "outgoing"
|
|
56
|
+
/** The incoming destination's start: it moves from this pose to rest. */
|
|
57
|
+
enter?: TransitionPose
|
|
58
|
+
/** The outgoing destination's end: it moves from rest to this pose. */
|
|
59
|
+
exit?: TransitionPose
|
|
60
|
+
/** Which side is above the other while they move (default `"enter"`). */
|
|
61
|
+
onTop?: "enter" | "exit"
|
|
46
62
|
}
|
|
47
63
|
|
|
48
64
|
export type Transition = TransitionName | TransitionSpec
|
|
@@ -54,6 +70,14 @@ export type PresentOptions = {
|
|
|
54
70
|
transition?: Transition
|
|
55
71
|
}
|
|
56
72
|
|
|
73
|
+
/** `close()` / `Router.hide()`: the transition the destination leaves with (default "none"). */
|
|
74
|
+
export type DismissOptions = {
|
|
75
|
+
transition?: Transition
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** @internal The (kind, id, viewName, paramsJson) tuple the tree bridge addresses a destination by. */
|
|
79
|
+
export type DestTuple = [kind: number, id: number, viewName: string, paramsJson: string]
|
|
80
|
+
|
|
57
81
|
/**
|
|
58
82
|
* Anything that can be shown as the app's current destination: a `UIScreen`, a `Scene` /
|
|
59
83
|
* `ARScene`, a `Scene2D`, a `NativeView`, or a `UIVideo`. One Presentable is visible at a time;
|
|
@@ -66,21 +90,26 @@ export type PresentOptions = {
|
|
|
66
90
|
export interface Presentable {
|
|
67
91
|
// Return type covers the implementers: UIScreen → void, Scene2D → this, ARScene → Promise.
|
|
68
92
|
open(options?: PresentOptions): void | this | Promise<void>
|
|
69
|
-
|
|
93
|
+
/** Close this destination if it is the one on screen (nothing takes its place). One that is not
|
|
94
|
+
* on screen — already replaced by a `Router.init/push/replace` or another `open()` — is left as
|
|
95
|
+
* it is, so `Router.init(home); scene.close()` keeps `home` visible. */
|
|
96
|
+
close(options?: DismissOptions): void
|
|
70
97
|
onOpen(callback: () => void): this
|
|
71
98
|
onClose(callback: () => void): this
|
|
72
|
-
|
|
99
|
+
onBack(callback: () => void): this
|
|
73
100
|
/** @internal Async work that must finish BEFORE this destination replaces the current one
|
|
74
101
|
* (camera permission, warm render, session launch — ARScene). The previous destination —
|
|
75
102
|
* typically a loading screen — stays visible while it runs; `open()` and `Router.push/replace`
|
|
76
103
|
* await it and skip the swap entirely if it rejects (permission denied, or superseded by
|
|
77
104
|
* another navigation that happened while preparing). */
|
|
78
105
|
_prepare?(): Promise<void>
|
|
106
|
+
/** @internal The destination tuple (see DestTuple). */
|
|
107
|
+
_dest(): DestTuple
|
|
79
108
|
}
|
|
80
109
|
|
|
81
110
|
// The destination visible right now. Set optimistically by every successful open()/router
|
|
82
|
-
// navigation, cleared by close(), and corrected by router onChange (
|
|
83
|
-
//
|
|
111
|
+
// navigation, cleared by close(), and corrected by router onChange (the runtime reports the new
|
|
112
|
+
// top on every stack change, including back gestures).
|
|
84
113
|
let current: Presentable | null = null
|
|
85
114
|
/** @internal Call whenever the visible destination changes. */
|
|
86
115
|
export const _setCurrent = (p: Presentable | null): void => { current = p }
|
|
@@ -103,15 +132,3 @@ let navEpoch = 0
|
|
|
103
132
|
export const _navEpoch = (): number => navEpoch
|
|
104
133
|
/** @internal Call on every action that changes the visible destination. */
|
|
105
134
|
export const _bumpNavEpoch = (): void => { navEpoch++ }
|
|
106
|
-
|
|
107
|
-
/** @internal Does this host implement the Presentable navigation surface? The SDK falls back to
|
|
108
|
-
* the legacy openScreen/openScene bridges when it doesn't (screens keep working; scene/native
|
|
109
|
-
* destinations in the Router then throw). */
|
|
110
|
-
export const _navSupported = (): boolean =>
|
|
111
|
-
typeof _creatorUI !== "undefined" && typeof _creatorUI.openView === "function"
|
|
112
|
-
|
|
113
|
-
/** @internal The wire descriptor for a Presentable. Screens (and anything without `_viewDesc`)
|
|
114
|
-
* travel as themselves; scenes/native views/video provide a stable descriptor object carrying
|
|
115
|
-
* `_p` (the SDK instance) so hosts can hand it back through router onChange / lifecycle. */
|
|
116
|
-
export const _descOf = (p: Presentable): object =>
|
|
117
|
-
typeof (p as any)._viewDesc === "function" ? (p as any)._viewDesc() : (p as object)
|
package/src/ui/router.ts
CHANGED
|
@@ -1,13 +1,27 @@
|
|
|
1
|
-
import { _bumpNavEpoch,
|
|
1
|
+
import { _bumpNavEpoch, _setCurrent, Presentable, type DismissOptions, type PresentOptions, type Transition } from "./presentable"
|
|
2
|
+
import { _transitionBack, _transitionId, TRANSITION_NONE, TRANSITION_UNSET } from "./transitions"
|
|
2
3
|
import { _navDefaults } from "./theme"
|
|
4
|
+
import { ensureEmitter, pin, resolveDest, setRouterChangeHandler, tree } from "./tree"
|
|
3
5
|
|
|
4
6
|
type RouterOptions = {
|
|
5
7
|
showDefaultBackButton?: boolean
|
|
6
8
|
}
|
|
7
9
|
|
|
8
|
-
type NavigateOptions = {
|
|
9
|
-
/** Transition to play. Defaults: `"push"` for push
|
|
10
|
-
*
|
|
10
|
+
export type NavigateOptions = {
|
|
11
|
+
/** Transition to play. Defaults: `"push"` for push; replace follows the theme's
|
|
12
|
+
* `replaceTransition` (`"none"` out of the box); a pop plays the way back the destination it
|
|
13
|
+
* pops remembers. */
|
|
14
|
+
transition?: Transition
|
|
15
|
+
/** The way BACK from the destination — what `Router.pop()` without a transition, the edge swipe
|
|
16
|
+
* and the system's back play when they take it off the stack. Default: the transition it came
|
|
17
|
+
* with, the other way round (a push goes back with a pop, a slide from the bottom leaves
|
|
18
|
+
* downwards; a zoom goes back with a fade). A `replace` that names none keeps the way back of
|
|
19
|
+
* the destination it replaces: it belongs to the place in the stack. */
|
|
20
|
+
popTransition?: Transition
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type PopOptions = {
|
|
24
|
+
/** Transition to play instead of the way back the popped destination remembers. */
|
|
11
25
|
transition?: Transition
|
|
12
26
|
}
|
|
13
27
|
|
|
@@ -29,12 +43,12 @@ type Router = {
|
|
|
29
43
|
replace(page: Presentable, opts?: NavigateOptions): void | Promise<void>
|
|
30
44
|
/** Pop the top destination (the programmatic twin of the back gesture); `to` unwinds to a
|
|
31
45
|
* stack index in one step. */
|
|
32
|
-
pop(to?: number, opts?:
|
|
46
|
+
pop(to?: number, opts?: PopOptions): void
|
|
33
47
|
/** Blank the router's page, keeping the stack — `restore()` brings it back. A destination
|
|
34
48
|
* opened directly stays visible. */
|
|
35
|
-
hide(): void
|
|
49
|
+
hide(opts?: DismissOptions): void
|
|
36
50
|
/** Re-present the stack top after `hide()` or a direct `open()` suspended the router. */
|
|
37
|
-
restore(): void
|
|
51
|
+
restore(opts?: PresentOptions): void
|
|
38
52
|
|
|
39
53
|
/** `"change"` fires on every stack change — pushes, pops, and native back gestures — with the
|
|
40
54
|
* new top destination. */
|
|
@@ -43,42 +57,44 @@ type Router = {
|
|
|
43
57
|
}
|
|
44
58
|
|
|
45
59
|
let currentPage: Presentable | null = null
|
|
46
|
-
let
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
//
|
|
50
|
-
|
|
60
|
+
let hooked = false
|
|
61
|
+
const onChange = (kind: number, id: number) => {
|
|
62
|
+
// The runtime reports the new top on every stack change (pushes, pops, back gestures): resolve
|
|
63
|
+
// it to the SDK instance and keep Presentable.current in sync.
|
|
64
|
+
const page = resolveDest(kind, id) as unknown as Presentable | undefined
|
|
65
|
+
if (!page) return
|
|
66
|
+
currentPage = page
|
|
51
67
|
_setCurrent(currentPage)
|
|
52
68
|
for (let callback of (Router as any)._changeListeners) {
|
|
53
69
|
callback(currentPage)
|
|
54
70
|
}
|
|
55
71
|
}
|
|
56
72
|
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
const
|
|
60
|
-
const desc = _descOf(page)
|
|
61
|
-
if (desc !== page && !_navSupported()) {
|
|
62
|
-
throw new Error("This host does not support scene/native-view navigation (requires _creatorUI.openView)")
|
|
63
|
-
}
|
|
64
|
-
return desc
|
|
65
|
-
}
|
|
73
|
+
// A screen the router holds is attached (pinned) until the runtime retires it (TREE_EVENT_DETACHED).
|
|
74
|
+
// Scenes / native views are not tree elements: their destination registry holds them (`_dest()`).
|
|
75
|
+
const hold = (page: Presentable): void => { if ((page as any)._h) pin(page as any) }
|
|
66
76
|
|
|
67
77
|
export const Router: Router = /*#__PURE__*/ Object.freeze({
|
|
68
78
|
|
|
69
79
|
init(homePage: Presentable, opts: RouterOptions = {}): void {
|
|
80
|
+
ensureEmitter()
|
|
81
|
+
if (!hooked) { hooked = true; setRouterChangeHandler(onChange) }
|
|
70
82
|
_bumpNavEpoch()
|
|
71
|
-
|
|
83
|
+
const [kind, id, name, params] = homePage._dest()
|
|
84
|
+
hold(homePage)
|
|
85
|
+
tree().routerOpen(kind, id, name, params, opts.showDefaultBackButton ?? false)
|
|
72
86
|
currentPage = homePage
|
|
73
87
|
_setCurrent(homePage)
|
|
74
88
|
},
|
|
75
89
|
push(page: Presentable, opts?: NavigateOptions): void | Promise<void> {
|
|
76
|
-
const desc = wireDesc(page) // throws early on unsupported hosts, before any async work
|
|
77
90
|
const go = () => {
|
|
78
91
|
_bumpNavEpoch()
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
92
|
+
const [kind, id, name, params] = page._dest()
|
|
93
|
+
hold(page)
|
|
94
|
+
const transition = opts?.transition ?? "push"
|
|
95
|
+
const back = opts?.popTransition !== undefined ? _transitionId(opts.popTransition) : _transitionBack(transition)
|
|
96
|
+
tree().routerPush(kind, id, name, params, _transitionId(transition), back)
|
|
97
|
+
_setCurrent(page) // optimistic — the runtime's change event confirms
|
|
82
98
|
}
|
|
83
99
|
// A preparing destination (ARScene) keeps the current page visible until it's ready; a
|
|
84
100
|
// rejection (camera denied / superseded) propagates to the caller and nothing is pushed.
|
|
@@ -86,28 +102,29 @@ export const Router: Router = /*#__PURE__*/ Object.freeze({
|
|
|
86
102
|
return prep ? prep.then(go) : go()
|
|
87
103
|
},
|
|
88
104
|
replace(page: Presentable, opts?: NavigateOptions): void | Promise<void> {
|
|
89
|
-
const desc = wireDesc(page)
|
|
90
105
|
const go = () => {
|
|
91
106
|
_bumpNavEpoch()
|
|
92
|
-
|
|
93
|
-
|
|
107
|
+
const [kind, id, name, params] = page._dest()
|
|
108
|
+
hold(page)
|
|
109
|
+
tree().routerReplace(kind, id, name, params, _transitionId(opts?.transition ?? _navDefaults.replace), _transitionId(opts?.popTransition))
|
|
110
|
+
_setCurrent(page) // optimistic — the runtime's change event confirms
|
|
94
111
|
}
|
|
95
112
|
const prep = page._prepare?.()
|
|
96
113
|
return prep ? prep.then(go) : go()
|
|
97
114
|
},
|
|
98
|
-
pop(to?: number, opts?:
|
|
115
|
+
pop(to?: number, opts?: PopOptions): void {
|
|
99
116
|
_bumpNavEpoch()
|
|
100
|
-
|
|
117
|
+
tree().routerPop(to ?? -1, _transitionId(opts?.transition, TRANSITION_UNSET))
|
|
101
118
|
},
|
|
102
|
-
hide() {
|
|
119
|
+
hide(opts?: DismissOptions) {
|
|
103
120
|
_bumpNavEpoch()
|
|
104
|
-
|
|
121
|
+
tree().routerHide(_transitionId(opts?.transition, TRANSITION_NONE))
|
|
105
122
|
// hide() only blanks the router's own page — a directly-opened destination stays visible.
|
|
106
123
|
if (Presentable.current === currentPage) _setCurrent(null)
|
|
107
124
|
},
|
|
108
|
-
restore() {
|
|
125
|
+
restore(opts?: PresentOptions) {
|
|
109
126
|
_bumpNavEpoch()
|
|
110
|
-
|
|
127
|
+
tree().routerRestore(_transitionId(opts?.transition, TRANSITION_NONE))
|
|
111
128
|
_setCurrent(currentPage) // the stack top is visible again
|
|
112
129
|
},
|
|
113
130
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// The UI color wire. The UI core parses CSS colors itself (engines/ui color.h over AnyCanvas's
|
|
2
|
+
// css_color.h — the parser the SDK's `Color` is a twin of), so a color STRING goes over as written:
|
|
3
|
+
// the same grammar, and CSS semantics for junk (an unparseable color is transparent, never an
|
|
4
|
+
// error). The SDK converts only what the core cannot read — a packed `0xRRGGBB` number and
|
|
5
|
+
// `[r, g, b(, a)]` components — to '#rrggbbaa', and a non-color there throws naming the key.
|
|
6
|
+
// The element keeps the app's own value in `_style` (reads answer what the app set); only the wire
|
|
7
|
+
// is converted. Kept out of UINode.ts: its `type Color` would clash with the `Color` value.
|
|
8
|
+
// Bundle rule: this module must reach no CSS parser (numericColorToHex, never `Color`) — the name
|
|
9
|
+
// table would otherwise ride along in every app with a styled color.
|
|
10
|
+
|
|
11
|
+
import { numericColorToHex } from "../core/color"
|
|
12
|
+
import { COLOR_KEYS, COLOR_SHORTHAND_KEYS } from "./colorKeys.gen"
|
|
13
|
+
|
|
14
|
+
/** @internal One color value → its wire form. Pass-throughs: null / undefined (a reset) and every
|
|
15
|
+
* string (CSS colors, `"default"`, `var(…)` — the core reads them all). A number or an array goes
|
|
16
|
+
* through {@link numericColorToHex} and throws, naming the key and the value, when it is not a
|
|
17
|
+
* color. */
|
|
18
|
+
export const wireColor = (key: string, v: unknown): unknown => {
|
|
19
|
+
if (v == null || typeof v === "string") return v
|
|
20
|
+
try {
|
|
21
|
+
return numericColorToHex(v)
|
|
22
|
+
} catch (e) {
|
|
23
|
+
if (e instanceof Error) e.message = `${key}: ${e.message}`
|
|
24
|
+
throw e
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** @internal One style write → its wire value: a color key through {@link wireColor}. A shorthand
|
|
29
|
+
* (`background`, `border*`) is a color in one of its forms only: a number or an array under
|
|
30
|
+
* `background` is one; a string stays (the core's shorthand tells a color from a width, an image,
|
|
31
|
+
* a gradient — same grammar) and a number under `border*` is a width. Every other key as is. */
|
|
32
|
+
export const wireValue = (key: string, v: unknown): unknown => {
|
|
33
|
+
if (COLOR_SHORTHAND_KEYS.has(key)) {
|
|
34
|
+
return key === "background" && (typeof v === "number" || Array.isArray(v)) ? wireColor(key, v) : v
|
|
35
|
+
}
|
|
36
|
+
return COLOR_KEYS.has(key) ? wireColor(key, v) : v
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// A nested state block (a `$class`: `$pressed`, `$landscape`, `$checked`, …): a plain object that is
|
|
40
|
+
// not a host handle — a backgroundImage FetchResponse / File carries `_h` (the runtime's
|
|
41
|
+
// own rule, presentation/tree.cpp mergeStyleLayer).
|
|
42
|
+
const isBlock = (v: unknown): v is Record<string, unknown> =>
|
|
43
|
+
typeof v === "object" && v !== null && !Array.isArray(v) && !("_h" in v)
|
|
44
|
+
|
|
45
|
+
/** @internal A style bag → its wire form: {@link wireValue} per key, nested state blocks
|
|
46
|
+
* recursively. Copy on write: the SAME object comes back when nothing changed. */
|
|
47
|
+
export const wireStyle = <T extends object>(bag: T): T => {
|
|
48
|
+
const src = bag as Record<string, unknown>
|
|
49
|
+
let out: Record<string, unknown> | null = null
|
|
50
|
+
for (const key of Object.keys(src)) {
|
|
51
|
+
const v = src[key]
|
|
52
|
+
const w = isBlock(v) ? wireStyle(v) : wireValue(key, v)
|
|
53
|
+
if (w !== v) (out ??= { ...src })[key] = w
|
|
54
|
+
}
|
|
55
|
+
return (out ?? bag) as T
|
|
56
|
+
}
|
package/src/ui/theme.ts
CHANGED
|
@@ -27,9 +27,10 @@ const RESERVED = ["safe-top", "safe-bottom", "safe-left", "safe-right", "vw", "v
|
|
|
27
27
|
* card.style({ bgColor: T.cardBg }) // T.cardBg === "var(--cardBg)"
|
|
28
28
|
* ```
|
|
29
29
|
*
|
|
30
|
-
* Strings pass through (colors
|
|
31
|
-
*
|
|
32
|
-
* (`
|
|
30
|
+
* Strings pass through as written (colors — the core parses CSS —, fonts, expressions like
|
|
31
|
+
* `"max(safe-top, 24px)"`); numbers are lengths (px), so a theme COLOR is always a
|
|
32
|
+
* string (`"#0A84FF"`, `"red"`, never `0x0a84ff`); `null` removes a key (comfort knobs reset to
|
|
33
|
+
* their defaults). Env names (`safe-*`, `vw`/`vh`/…) are host-owned — skipped with a warning. One special key:
|
|
33
34
|
* `replaceTransition` — the app-wide default transition for `Router.replace` (consumed SDK-side,
|
|
34
35
|
* never sent to the host table).
|
|
35
36
|
*/
|
|
@@ -39,8 +40,9 @@ const setTheme = <T extends Record<string, string | number | Transition | null>>
|
|
|
39
40
|
): ThemeAccessors<Omit<T, "replaceTransition">> => {
|
|
40
41
|
const accessors: Record<string, string> = {}
|
|
41
42
|
let send = values as ThemeValues
|
|
43
|
+
const own = () => { if (send === values) send = { ...values } as ThemeValues }
|
|
42
44
|
const strip = (key: string) => {
|
|
43
|
-
|
|
45
|
+
own()
|
|
44
46
|
delete send[key]
|
|
45
47
|
}
|
|
46
48
|
for (const key of Object.keys(values)) {
|
|
@@ -56,8 +58,7 @@ const setTheme = <T extends Record<string, string | number | Transition | null>>
|
|
|
56
58
|
}
|
|
57
59
|
accessors[key] = `var(--${key})`
|
|
58
60
|
}
|
|
59
|
-
|
|
60
|
-
if (Object.keys(send).length > 0) _creatorUI.setTheme?.(send)
|
|
61
|
+
if (Object.keys(send).length > 0) _creatorTree.setTheme(send)
|
|
61
62
|
return accessors as ThemeAccessors<Omit<T, "replaceTransition">>
|
|
62
63
|
}
|
|
63
64
|
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// Screen transitions (docs/plans/transitions-plan.md). Every transition — the built-in ones too —
|
|
2
|
+
// is DATA in the format of presentable.ts and is written HERE: the runtime holds no catalog and
|
|
3
|
+
// knows no name, a host animates nothing of its own. A transition becomes a tween blob whose
|
|
4
|
+
// targets are ROLES, registered once per world (`_creatorTree.defineTransition`); a navigation call
|
|
5
|
+
// carries the id, and the runtime binds the roles to the two screen roots when it plays it — as
|
|
6
|
+
// ordinary tracks on their `transform` / `opacity` / `dim`, which never commit.
|
|
7
|
+
//
|
|
8
|
+
// Depends on the tween builder alone: gl/ and g2/ reach this through their open().
|
|
9
|
+
|
|
10
|
+
import type { Easing } from "../animate/easings"
|
|
11
|
+
import { DEFAULT_EASING, parseEasing, type EasingSpec } from "../animate/tween/easing"
|
|
12
|
+
import { CLOCK_UI, DOM_UI, KIND_FLOAT, KIND_TRANSFORM, buildBlob, parseTransformList, transformLanes, warnOnce, type Keyframe, type Track, type TweenTarget } from "../animate/tween/spec"
|
|
13
|
+
import type { Transition, TransitionName, TransitionPose, TransitionSpec } from "./presentable"
|
|
14
|
+
|
|
15
|
+
/** Wire ids: a registered transition is > 0. */
|
|
16
|
+
export const TRANSITION_NONE = 0
|
|
17
|
+
/** "The caller names none": the runtime plays what the stack entry remembers (a pop), or keeps
|
|
18
|
+
* what the replaced entry remembered (a replace's way back). */
|
|
19
|
+
export const TRANSITION_UNSET = -1
|
|
20
|
+
|
|
21
|
+
/** Who a track moves. The ALONE roles play next to a destination that cannot move (a scene, a
|
|
22
|
+
* native view, a video) or next to nothing (close(), the first open): only the screen plays, above. */
|
|
23
|
+
export const ROLE_ENTER = 0
|
|
24
|
+
export const ROLE_EXIT = 1
|
|
25
|
+
export const ROLE_ENTER_ALONE = 2
|
|
26
|
+
export const ROLE_EXIT_ALONE = 3
|
|
27
|
+
|
|
28
|
+
/** `defineTransition` flags. */
|
|
29
|
+
export const TRANSITION_ENTER_ON_TOP = 1
|
|
30
|
+
|
|
31
|
+
const DEFAULT_DURATION_MS = 300
|
|
32
|
+
|
|
33
|
+
// ---- the catalog ---------------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
// A spring as a curve over 0..1 (the response of a unit step, `omega` = how soon it settles inside
|
|
36
|
+
// the span): what UIKit's `dampingRatio` animators and the old Android SpringInterpolator eased with.
|
|
37
|
+
const spring = (damping: number, omega: number): Easing => (t) => {
|
|
38
|
+
if (t >= 1) return 1
|
|
39
|
+
if (damping < 1) {
|
|
40
|
+
const wd = omega * Math.sqrt(1 - damping * damping)
|
|
41
|
+
return 1 - Math.exp(-damping * omega * t) * (Math.cos(wd * t) + ((damping * omega) / wd) * Math.sin(wd * t))
|
|
42
|
+
}
|
|
43
|
+
const w = omega * damping
|
|
44
|
+
return 1 - Math.exp(-w * t) * (1 + w * t)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// A soft ease-out that settles with a slight overshoot: the slides and the base zoom.
|
|
48
|
+
const SETTLE = [0.3, 1.1, 0.5, 1] as const
|
|
49
|
+
|
|
50
|
+
const slide = (enter: string, exit: string): TransitionSpec => ({
|
|
51
|
+
enter: { transform: enter, duration: 400, easing: SETTLE },
|
|
52
|
+
exit: { transform: exit, duration: 400, easing: SETTLE },
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// The stacked pair: the top screen travels its whole width while the one under it drifts a part
|
|
56
|
+
// of it and dims. The two phone platforms keep their own feel — the numbers of one table.
|
|
57
|
+
const push = (): TransitionSpec => {
|
|
58
|
+
if (_creatorDevice.platform === "android") {
|
|
59
|
+
const t = { duration: 350, easing: [0.2, 0, 0, 1] as const }
|
|
60
|
+
return { enter: { transform: "translateX(100%)", ...t }, exit: { transform: "translateX(-25%)", dim: 0.3, ...t } }
|
|
61
|
+
}
|
|
62
|
+
const t = { duration: 380, easing: spring(1, 9) }
|
|
63
|
+
return { enter: { transform: "translateX(100%)", ...t }, exit: { transform: "translateX(-30%)", dim: 0.12, ...t } }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** What a name is. `none` has no spec. */
|
|
67
|
+
const named = (name: TransitionName): TransitionSpec | null => {
|
|
68
|
+
switch (name) {
|
|
69
|
+
case "push": return push()
|
|
70
|
+
case "pop": return mirrored(push())
|
|
71
|
+
case "slide-from-left": return slide("translateX(-100%)", "translateX(100%)")
|
|
72
|
+
case "slide-from-right": return slide("translateX(100%)", "translateX(-100%)")
|
|
73
|
+
case "slide-from-top": return slide("translateY(-100%)", "translateY(100%)")
|
|
74
|
+
case "slide-from-bottom": return slide("translateY(100%)", "translateY(-100%)")
|
|
75
|
+
// the incoming grows into place above the other one, which only fills the thin edge
|
|
76
|
+
case "zoom":
|
|
77
|
+
case "zoom-in": return { enter: { transform: "scale(0.985)", duration: 400, easing: SETTLE } }
|
|
78
|
+
// the outgoing recedes and dissolves above; the incoming settles from slightly enlarged, never
|
|
79
|
+
// below 1, so no edge is exposed
|
|
80
|
+
case "zoom-out": {
|
|
81
|
+
const t = { duration: 420, easing: spring(0.82, 10) }
|
|
82
|
+
return { onTop: "exit", enter: { transform: "scale(1.06)", ...t }, exit: { transform: "scale(0.92)", opacity: 0, ...t } }
|
|
83
|
+
}
|
|
84
|
+
// a fade-THROUGH: out, then in — two half-transparent screens never show through each other
|
|
85
|
+
case "fade": return {
|
|
86
|
+
onTop: "exit",
|
|
87
|
+
exit: { opacity: 0, duration: 120, easing: [0.42, 0, 1, 1] },
|
|
88
|
+
enter: { opacity: 0, delay: 120, duration: 200, easing: [0, 0, 0.58, 1] },
|
|
89
|
+
}
|
|
90
|
+
default: return null
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The way back of a name. A slide and the stacked pair go back the way they came; a zoom's mirror
|
|
95
|
+
* would end with the top screen vanishing at its start scale, so a zoom goes back with a fade. */
|
|
96
|
+
const BACK: Record<TransitionName, TransitionName> = {
|
|
97
|
+
"push": "pop", "pop": "push",
|
|
98
|
+
"slide-from-left": "slide-from-right", "slide-from-right": "slide-from-left",
|
|
99
|
+
"slide-from-top": "slide-from-bottom", "slide-from-bottom": "slide-from-top",
|
|
100
|
+
"zoom": "fade", "zoom-in": "fade", "zoom-out": "fade", "fade": "fade", "none": "none",
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---- the mirror ----------------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
const backwards = <T>(v: T | T[] | undefined): T | T[] | undefined => (Array.isArray(v) ? v.slice().reverse() : v)
|
|
106
|
+
|
|
107
|
+
const reversedPose = (p: TransitionPose | undefined): TransitionPose | undefined =>
|
|
108
|
+
p && { ...p, transform: backwards(p.transform), opacity: backwards(p.opacity), dim: backwards(p.dim) }
|
|
109
|
+
|
|
110
|
+
/** The way back of a spec: the ROLES swap — the screen that came leaves into its `enter` pose, the
|
|
111
|
+
* one that left comes back from its `exit` pose, the same screen stays on top — and the timing and
|
|
112
|
+
* the curves stay as written. Not a rewind: that would turn an ease-out into a slow start. */
|
|
113
|
+
export const mirrored = (s: TransitionSpec): TransitionSpec => ({
|
|
114
|
+
enter: reversedPose(s.exit),
|
|
115
|
+
exit: reversedPose(s.enter),
|
|
116
|
+
onTop: (s.onTop ?? "enter") === "enter" ? "exit" : "enter",
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
// ---- a spec → tracks ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
/** One prop of a side: its keys in the order the side plays them (an incoming side ends at rest
|
|
122
|
+
* after them, an outgoing one starts at rest before them), and what rest is. */
|
|
123
|
+
type Lane = { prop: string, kind: number, sig?: string, keys: number[][], rest: number[] }
|
|
124
|
+
type Side = { lanes: Lane[], atMs: number, durMs: number, easing: EasingSpec }
|
|
125
|
+
|
|
126
|
+
const list = <T>(v: T | T[]): T[] => (Array.isArray(v) ? v : [v])
|
|
127
|
+
|
|
128
|
+
const transformLane = (value: string | string[]): Lane | null => {
|
|
129
|
+
const parsed = list(value).map((v) => parseTransformList(v, true))
|
|
130
|
+
const first = parsed[0]
|
|
131
|
+
if (!first || parsed.some((p) => p === null || p.sig !== first.sig)) {
|
|
132
|
+
warnOnce("transition:transform", "a transition's transform must be a list of translate / scale / rotate, the same functions in every keyframe — ignored")
|
|
133
|
+
return null
|
|
134
|
+
}
|
|
135
|
+
if (first.sig === "") return null
|
|
136
|
+
const rest: number[] = []
|
|
137
|
+
for (const fn of first.sig.split(",")) for (let i = transformLanes(fn); i > 0; i--) rest.push(fn.startsWith("scale") ? 1 : 0)
|
|
138
|
+
return { prop: "transform", kind: KIND_TRANSFORM, sig: first.sig, keys: parsed.map((p) => p!.lanes), rest }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const floatLane = (prop: string, value: number | number[], rest: number): Lane =>
|
|
142
|
+
({ prop, kind: KIND_FLOAT, keys: list(value).map((v) => [v]), rest: [rest] })
|
|
143
|
+
|
|
144
|
+
const sideOf = (pose: TransitionPose | undefined): Side | null => {
|
|
145
|
+
if (!pose) return null
|
|
146
|
+
const lanes: Lane[] = []
|
|
147
|
+
if (pose.transform !== undefined) { const l = transformLane(pose.transform); if (l) lanes.push(l) }
|
|
148
|
+
if (pose.opacity !== undefined) lanes.push(floatLane("opacity", pose.opacity, 1))
|
|
149
|
+
if (pose.dim !== undefined) lanes.push(floatLane("dim", pose.dim, 0))
|
|
150
|
+
if (lanes.length === 0) return null
|
|
151
|
+
return {
|
|
152
|
+
lanes,
|
|
153
|
+
atMs: Math.max(0, pose.delay ?? 0),
|
|
154
|
+
durMs: Math.max(0, pose.duration ?? DEFAULT_DURATION_MS),
|
|
155
|
+
easing: pose.easing === undefined ? DEFAULT_EASING : parseEasing(pose.easing),
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The OTHER side's pose as this screen's own, for a screen that plays alone: it takes the pose of
|
|
160
|
+
* the side that is on top (the one made to cover and to uncover — the side under it only drifts),
|
|
161
|
+
* travelling the other way, so a push still moves everything to the left; its keys run backwards,
|
|
162
|
+
* the direction being the opposite one. No dim: nothing is over it. */
|
|
163
|
+
const borrowed = (side: Side | null): Side | null => {
|
|
164
|
+
if (!side) return null
|
|
165
|
+
const lanes = side.lanes.filter((l) => l.prop !== "dim").map((l) => {
|
|
166
|
+
if (l.kind !== KIND_TRANSFORM) return { ...l, keys: l.keys.slice().reverse() }
|
|
167
|
+
const sign: number[] = []
|
|
168
|
+
for (const fn of l.sig!.split(",")) for (let i = transformLanes(fn); i > 0; i--) sign.push(fn.startsWith("translate") ? -1 : 1)
|
|
169
|
+
return { ...l, keys: l.keys.map((k) => k.map((v, i) => v * sign[i]!)).reverse() }
|
|
170
|
+
})
|
|
171
|
+
return lanes.length > 0 ? { ...side, lanes } : null
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const roleTarget = {} as TweenTarget // a track's target is its role: nothing of a Track reads this
|
|
175
|
+
|
|
176
|
+
/** `incoming` = the screen moves from its pose to rest; otherwise from rest to its pose. */
|
|
177
|
+
const tracksOf = (side: Side | null, role: number, incoming: boolean): Track[] => {
|
|
178
|
+
if (!side) return []
|
|
179
|
+
const channel = { domain: DOM_UI, id: () => role + 1, value: () => null }
|
|
180
|
+
return side.lanes.map((l) => {
|
|
181
|
+
// explicit keys at both ends: a captured one that fails to read would leave a screen in its pose
|
|
182
|
+
const values = incoming ? [...l.keys, l.rest] : [l.rest, ...l.keys]
|
|
183
|
+
const keys: Keyframe[] = values.map((lanes, i) => ({ t: i / (values.length - 1), easing: side.easing, value: { lanes } }))
|
|
184
|
+
return { target: roleTarget, prop: l.prop, channel, kind: l.kind, lanes: l.rest.length, sig: l.sig, atMs: side.atMs, durMs: side.durMs, commit: false, keys }
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** @internal The blob of a spec: `roles[i]` is the role of the blob's target `i`. Null = the spec
|
|
189
|
+
* moves nothing. */
|
|
190
|
+
export const _transitionBlob = (spec: TransitionSpec): { data: Float32Array, strings: string[], roles: number[], flags: number } | null => {
|
|
191
|
+
const enter = sideOf(spec.enter), exit = sideOf(spec.exit)
|
|
192
|
+
const enterOnTop = (spec.onTop ?? "enter") === "enter"
|
|
193
|
+
const tracks = [
|
|
194
|
+
...tracksOf(enter, ROLE_ENTER, true),
|
|
195
|
+
...tracksOf(exit, ROLE_EXIT, false),
|
|
196
|
+
...tracksOf(enterOnTop ? enter : borrowed(exit), ROLE_ENTER_ALONE, true),
|
|
197
|
+
...tracksOf(enterOnTop ? borrowed(enter) : exit, ROLE_EXIT_ALONE, false),
|
|
198
|
+
]
|
|
199
|
+
if (tracks.length === 0) return null
|
|
200
|
+
let durationMs = 0
|
|
201
|
+
for (const t of tracks) durationMs = Math.max(durationMs, t.atMs + t.durMs)
|
|
202
|
+
const blob = buildBlob({ clock: CLOCK_UI, durationMs, delayMs: 0, iterations: 1, pingPong: false, rate: 1, tracks, calls: [] })
|
|
203
|
+
return { data: blob.data, strings: blob.strings, roles: blob.targets.map((t) => (t as number) - 1), flags: enterOnTop ? TRANSITION_ENTER_ON_TOP : 0 }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ---- the registry (one per world, like the runtime's) -------------------------------------------
|
|
207
|
+
|
|
208
|
+
type Entry = { id: number, back?: number, spec: TransitionSpec | null }
|
|
209
|
+
let byName = new Map<string, Entry>()
|
|
210
|
+
let bySpec = new WeakMap<TransitionSpec, Entry>()
|
|
211
|
+
|
|
212
|
+
const define = (spec: TransitionSpec | null): Entry => {
|
|
213
|
+
const blob = spec && _transitionBlob(spec)
|
|
214
|
+
return { id: blob ? _creatorTree.defineTransition(blob.data, blob.strings, blob.roles, blob.flags) : TRANSITION_NONE, spec }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const entryOf = (t: Transition): Entry => {
|
|
218
|
+
if (typeof t === "string") {
|
|
219
|
+
let e = byName.get(t)
|
|
220
|
+
if (!e) {
|
|
221
|
+
if (!(t in BACK)) warnOnce(`transition:${t}`, `"${t}" is not a transition — none plays`)
|
|
222
|
+
byName.set(t, e = define(named(t)))
|
|
223
|
+
}
|
|
224
|
+
return e
|
|
225
|
+
}
|
|
226
|
+
let e = bySpec.get(t)
|
|
227
|
+
if (!e) bySpec.set(t, e = define(t))
|
|
228
|
+
return e
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** @internal The wire id of a transition (registered on its first use); nothing named = `unset`. */
|
|
232
|
+
export const _transitionId = (t: Transition | undefined, unset: number = TRANSITION_UNSET): number =>
|
|
233
|
+
t === undefined ? unset : entryOf(t).id
|
|
234
|
+
|
|
235
|
+
/** @internal The wire id of the way back of `t`: the catalog's for a name, the mirror of a spec. */
|
|
236
|
+
export const _transitionBack = (t: Transition): number => {
|
|
237
|
+
const e = entryOf(t)
|
|
238
|
+
if (e.back === undefined) {
|
|
239
|
+
e.back = typeof t === "string" ? entryOf(BACK[t as TransitionName] ?? "none").id
|
|
240
|
+
: e.spec ? define(mirrored(e.spec)).id : TRANSITION_NONE
|
|
241
|
+
}
|
|
242
|
+
return e.back
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** @internal Tests: a new fake runtime has no transition of the previous one. */
|
|
246
|
+
export const _resetTransitions = (): void => {
|
|
247
|
+
byName = new Map()
|
|
248
|
+
bySpec = new WeakMap()
|
|
249
|
+
}
|