lecodes-cli 0.1.6 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +7 -1
  2. package/dist/index.js +202 -84
  3. package/package.json +4 -4
  4. package/runtime/sdk/animate/animate.ts +238 -0
  5. package/runtime/sdk/animate/bezier.ts +138 -0
  6. package/runtime/sdk/animate/easings.ts +126 -0
  7. package/runtime/sdk/core/Aspect.ts +151 -0
  8. package/runtime/sdk/core/color.ts +66 -0
  9. package/runtime/sdk/core/events.ts +38 -0
  10. package/runtime/sdk/core/registry.ts +23 -0
  11. package/runtime/sdk/g2/Camera2D.ts +33 -0
  12. package/runtime/sdk/g2/CharacterController2D.ts +75 -0
  13. package/runtime/sdk/g2/Node2D.ts +194 -0
  14. package/runtime/sdk/g2/Physics2D.ts +126 -0
  15. package/runtime/sdk/g2/Scene2D.ts +101 -0
  16. package/runtime/sdk/g2/Shape.ts +63 -0
  17. package/runtime/sdk/g2/Sprite.ts +79 -0
  18. package/runtime/sdk/g2/SpriteAnimation.ts +90 -0
  19. package/runtime/sdk/g2/Texture2D.ts +37 -0
  20. package/runtime/sdk/g2/Tilemap.ts +41 -0
  21. package/runtime/sdk/g2/Trigger.ts +47 -0
  22. package/runtime/sdk/g2/loop.ts +39 -0
  23. package/runtime/sdk/g2/touch.ts +83 -0
  24. package/runtime/sdk/gl/Camera.ts +40 -0
  25. package/runtime/sdk/gl/Collider.ts +29 -0
  26. package/runtime/sdk/gl/Geometry.ts +218 -0
  27. package/runtime/sdk/gl/Light.ts +37 -0
  28. package/runtime/sdk/gl/Material.ts +124 -0
  29. package/runtime/sdk/gl/Mesh.ts +100 -0
  30. package/runtime/sdk/gl/Model.ts +40 -0
  31. package/runtime/sdk/gl/ModelAnimation.ts +64 -0
  32. package/runtime/sdk/gl/Node.ts +234 -0
  33. package/runtime/sdk/gl/Noise.ts +30 -0
  34. package/runtime/sdk/gl/Particles.ts +185 -0
  35. package/runtime/sdk/gl/Physics.ts +95 -0
  36. package/runtime/sdk/gl/Plane.ts +53 -0
  37. package/runtime/sdk/gl/Ray.ts +16 -0
  38. package/runtime/sdk/gl/Scene.ts +179 -0
  39. package/runtime/sdk/gl/Texture.ts +35 -0
  40. package/runtime/sdk/gl/controls.ts +95 -0
  41. package/runtime/sdk/gl/state.ts +6 -0
  42. package/runtime/sdk/gl/touch.ts +56 -0
  43. package/runtime/sdk/inject.ts +85 -0
  44. package/runtime/sdk/math/Mathf.ts +118 -0
  45. package/runtime/sdk/math/mat4.ts +278 -0
  46. package/runtime/sdk/math/quat.ts +232 -0
  47. package/runtime/sdk/math/vec.ts +255 -0
  48. package/runtime/sdk/plugins/qr.ts +36 -0
  49. package/runtime/sdk/runtime/device.ts +33 -0
  50. package/runtime/sdk/runtime/fetch.ts +71 -0
  51. package/runtime/sdk/runtime/files.ts +23 -0
  52. package/runtime/sdk/runtime/input.ts +12 -0
  53. package/runtime/sdk/runtime/media.ts +111 -0
  54. package/runtime/sdk/runtime/misc.ts +12 -0
  55. package/runtime/sdk/runtime/net.ts +36 -0
  56. package/runtime/sdk/runtime/storage.ts +13 -0
  57. package/runtime/sdk/runtime/touch.ts +50 -0
  58. package/runtime/sdk/ui/UI.ts +19 -0
  59. package/runtime/sdk/ui/UIButton.ts +84 -0
  60. package/runtime/sdk/ui/UIContainer.ts +100 -0
  61. package/runtime/sdk/ui/UIImage.ts +68 -0
  62. package/runtime/sdk/ui/UIInput.ts +78 -0
  63. package/runtime/sdk/ui/UINode.ts +342 -0
  64. package/runtime/sdk/ui/UIScreen.ts +122 -0
  65. package/runtime/sdk/ui/UIScrollable.ts +70 -0
  66. package/runtime/sdk/ui/UISpacer.ts +14 -0
  67. package/runtime/sdk/ui/UIText.ts +40 -0
  68. package/runtime/sdk/ui/UIVideo.ts +38 -0
  69. package/runtime/sdk/ui/UIVirtualizedList.ts +222 -0
  70. package/runtime/sdk/ui/UIWidget.ts +97 -0
  71. package/runtime/sdk/ui/fonts.ts +10 -0
  72. package/runtime/sdk/ui/router.ts +73 -0
  73. package/runtime/inject.js +0 -4629
@@ -0,0 +1,179 @@
1
+ // A 3D scene: a draw set, a Camera, lights and environment. Opening a scene asks the host to bring up
2
+ // the 3D engine; only the active scene renders. Grown from the Phase 0 seed. Ported from
3
+ // worker/src/Scene.ts (Scene + ARScene), with the camera exposed as scene.camera.
4
+ //
5
+ // Lights are explicit: a scene gets image-based ambient (IBL) by default; add a Light.sun() for
6
+ // direct light + shadows.
7
+
8
+ import { Color, type ColorInput } from "../core/color"
9
+ import type { ClickEvent, TouchStartEvent } from "../runtime/touch"
10
+ import type { FetchResponse } from "../runtime/fetch"
11
+ import { Camera } from "./Camera"
12
+ import { attachControls, type ControlsHandle, type ControlsOptions } from "./controls"
13
+ import { Material } from "./Material"
14
+ import { Node, nodeRegistry } from "./Node"
15
+ import { glState } from "./state"
16
+ import { registerTouchEndEvent, registerTouchStartEvent } from "./touch"
17
+
18
+ export type SceneOptions = {
19
+ /** Image-based ambient lighting (default on). */
20
+ ibl?: boolean
21
+ /** IBL intensity (default 20000). */
22
+ environmentIntensity?: number
23
+ /** Bloom post-processing. */
24
+ bloom?: boolean
25
+ bloomIntensity?: number
26
+ /** Skybox / clear color. */
27
+ skybox?: ColorInput
28
+ /** Multisample anti-aliasing. */
29
+ antialias?: boolean
30
+ }
31
+
32
+ export class Scene {
33
+ /** @internal native scene handle. */
34
+ readonly _id: number
35
+ readonly camera: Camera
36
+
37
+ /** @internal touch listeners, read by the dispatch system. */
38
+ readonly _clickListeners: Array<(ev: ClickEvent<Node | null>) => void> = []
39
+ readonly _touchStartListeners: Array<(ev: TouchStartEvent<Node | null>) => void> = []
40
+
41
+ private _material?: Material
42
+
43
+ private static _active: Scene | null = null
44
+ static get active(): Scene | null { return Scene._active }
45
+
46
+ constructor(options: SceneOptions = {}) {
47
+ const parent = (options as any)._parent as Scene | undefined
48
+ if (parent) {
49
+ this.camera = parent.camera
50
+ this._id = _creator.createOverlayScene(parent._id)
51
+ } else {
52
+ this.camera = new Camera()
53
+ this._id = _creator.createScene()
54
+ }
55
+ this.camera._attach(this._id)
56
+
57
+ if (options.ibl !== false) _creator.setDefaultIbl(this._id, options.environmentIntensity ?? 20000)
58
+ if (options.bloom) _creator.setBloomOptions(this._id, true, options.bloomIntensity ?? 0.2, 1)
59
+ if (options.skybox !== undefined) _creator.setSkybox(this._id, Color.toPackedRgb(options.skybox))
60
+ if (options.antialias) _creator.setSceneMultiSampleAntiAliasing(this._id, true, 4)
61
+ }
62
+
63
+ /** Scene occlusion material. */
64
+ get occlusionMaterial(): Material {
65
+ if (!this._material) this._material = new Material(_creator.getSceneMaterial(this._id))
66
+ return this._material
67
+ }
68
+
69
+ set skybox(color: ColorInput) { _creator.setSkybox(this._id, Color.toPackedRgb(color)) }
70
+
71
+ setMaterialGlobalParameter(i: number, x: number, y: number, z: number, w: number): void {
72
+ _creator.setMaterialGlobalParameter(this._id, i, x, y, z, w)
73
+ }
74
+ setAntialias(enabled: boolean, scale = 4): void {
75
+ _creator.setSceneMultiSampleAntiAliasing(this._id, enabled, scale)
76
+ }
77
+
78
+ add(...nodes: Node[]): this {
79
+ for (const n of nodes) _creator.addEntityToScene(this._id, n.id)
80
+ return this
81
+ }
82
+ remove(...nodes: Node[]): this {
83
+ for (const n of nodes) _creator.removeEntityFromScene(this._id, n.id)
84
+ return this
85
+ }
86
+
87
+ createOverlay(options: SceneOptions = {}): Scene {
88
+ return new Scene({ ...options, _parent: this } as any)
89
+ }
90
+
91
+ warmRender(): Promise<void> {
92
+ return new Promise<void>((res) => _creator.warmRender(this._id, res))
93
+ }
94
+
95
+ /** Make this the active scene (brings up the 3D engine; closes any other). */
96
+ open(): void {
97
+ _creator.openScene(this._id)
98
+ Scene._active = this
99
+ glState.activeScene = this
100
+ }
101
+ close(): void {
102
+ _creator.closeScene()
103
+ if (Scene._active === this) Scene._active = null
104
+ if (glState.activeScene === this) glState.activeScene = null
105
+ }
106
+
107
+ addEventListener(channel: "click", callback: (ev: ClickEvent<Node | null>) => void): void
108
+ addEventListener(channel: "touchstart", callback: (ev: TouchStartEvent<Node | null>) => void): void
109
+ addEventListener(channel: "click" | "touchstart", callback: (ev: any) => void): void {
110
+ if (channel === "click") {
111
+ if (!this._clickListeners.includes(callback)) { this._clickListeners.push(callback); registerTouchEndEvent() }
112
+ } else if (channel === "touchstart") {
113
+ if (!this._touchStartListeners.includes(callback)) { this._touchStartListeners.push(callback); registerTouchStartEvent() }
114
+ }
115
+ }
116
+ removeEventListener(channel: "click" | "touchstart", callback: (ev: any) => void): void {
117
+ const list = channel === "click" ? this._clickListeners : this._touchStartListeners
118
+ const i = list.indexOf(callback)
119
+ if (i >= 0) list.splice(i, 1)
120
+ }
121
+ }
122
+
123
+ // --- AR ---
124
+
125
+ export type ARMode = "default" | "markers" | "arcore" | "detached"
126
+
127
+ const onTrackStateChange = (entityId: number, track: boolean): void => {
128
+ const node = nodeRegistry.get(entityId)
129
+ if (!node) return
130
+ node.isTracked = track
131
+ if (track) node._dispatchEvent("track")
132
+ else node._dispatchEvent("untrack")
133
+ }
134
+
135
+ export class ARScene<T extends ARMode = "default"> extends Scene {
136
+ readonly mode: T
137
+ useWarmRender = true
138
+ private _root?: Node
139
+
140
+ constructor(options: SceneOptions & { mode?: T } = {}) {
141
+ super(options)
142
+ this.mode = options.mode ?? ("default" as T)
143
+ _creator.createARController(this._id, this.camera.id, this.mode, onTrackStateChange)
144
+ }
145
+
146
+ /** Root anchor node (not available in marker mode). */
147
+ get root(): Node {
148
+ if (this._root) return this._root
149
+ this._root = new Node(_creator.createRootAnchor(this._id))
150
+ return this._root
151
+ }
152
+
153
+ /** Create an anchor from a tracking source (e.g. an image target). */
154
+ createAnchor(source: FetchResponse, physicalWidth = 0.2): Node {
155
+ return new Node(_creator.createAnchor(this._id, physicalWidth, (source as any)._id))
156
+ }
157
+
158
+ /**
159
+ * Let the user place & manipulate a node with touch gestures — the staple AR object-placement
160
+ * interaction: ONE finger drags it across the floor (relative to where the camera looks), TWO fingers
161
+ * pinch to scale and twist to rotate. Returns a handle; call `.remove()` to detach. Tunable via
162
+ * options (min/max scale, and toggles for `pan` / `pinch` / `twist`).
163
+ */
164
+ addControls(target: Node, options: ControlsOptions = {}): ControlsHandle {
165
+ return attachControls(this, target, options)
166
+ }
167
+
168
+ override async open(): Promise<void> {
169
+ await new Promise<void>((res, rej) => _creator.requestCamera(res, rej))
170
+ if (this.useWarmRender) { _creator.createGLView(); await this.warmRender() }
171
+ await new Promise<void>((res, rej) => _creator.launchAR(this._id, res, rej))
172
+ super.open()
173
+ }
174
+
175
+ override close(): void {
176
+ _creator.stopAR()
177
+ super.close()
178
+ }
179
+ }
@@ -0,0 +1,35 @@
1
+ // A 3D GPU texture, decoded natively by creator-gl. Load from a URL or an already-fetched response.
2
+
3
+ import { fetch, type FetchResponse } from "../runtime/fetch"
4
+
5
+ export class Texture {
6
+ /** @internal native texture handle. */
7
+ readonly _id: number
8
+ readonly width: number
9
+ readonly height: number
10
+
11
+ /** Horizontal wrap mode (U). */
12
+ wrapS = 1
13
+ /** Vertical wrap mode (V). */
14
+ wrapT = 1
15
+
16
+ constructor(width: number, height: number, id: number) {
17
+ this.width = width
18
+ this.height = height
19
+ this._id = id
20
+ }
21
+
22
+ static load(source: string | FetchResponse): Promise<Texture> {
23
+ if (typeof source === "string") {
24
+ return fetch(source, { useOnce: true }).then((resp) => {
25
+ if (resp.status >= 400) {
26
+ return Promise.reject(new Error(`Failed to fetch texture from ${source}. HTTP ${resp.status}`))
27
+ }
28
+ return Texture.load(resp)
29
+ })
30
+ }
31
+ return new Promise<Texture>((resolve, reject) => {
32
+ _creator.createTexture((source as any)._id, (id, w, h) => resolve(new Texture(w, h, id)), reject)
33
+ })
34
+ }
35
+ }
@@ -0,0 +1,95 @@
1
+ // AR object-placement controls — the classic "drop a model in the room and manipulate it" gesture
2
+ // recipe: ONE finger drags the target across the ground plane (camera-relative), TWO fingers pinch to
3
+ // scale and twist to rotate (yaw). Lives on ARScene (see Scene.ts) because it orchestrates the scene's
4
+ // camera + touch input against a target node; it's built entirely on the public API (no native calls
5
+ // the user couldn't make), so it's a convenience, not a primitive.
6
+
7
+ import { Vec2, Vec3 } from "../math/vec"
8
+ import { Quat } from "../math/quat"
9
+ import type { Node } from "./Node"
10
+ import type { Scene } from "./Scene"
11
+ import type { TouchStartEvent } from "../runtime/touch"
12
+
13
+ export type ControlsOptions = {
14
+ /** Smallest uniform scale the pinch gesture allows (default 0.08). */
15
+ minScale?: number
16
+ /** Largest uniform scale the pinch gesture allows (default 2). */
17
+ maxScale?: number
18
+ /** One-finger drag moves the target across the ground plane (default true). */
19
+ pan?: boolean
20
+ /** Two-finger pinch scales the target (default true). */
21
+ pinch?: boolean
22
+ /** Two-finger twist rotates the target around its Y axis (default true). */
23
+ twist?: boolean
24
+ }
25
+
26
+ /** Returned by `addControls` — call `remove()` to detach the gesture listeners. */
27
+ export type ControlsHandle = { remove(): void }
28
+
29
+ type ActiveTouch = { pointerId: number, x: number, y: number }
30
+
31
+ const clamp = (v: number, lo: number, hi: number): number => (v < lo ? lo : v > hi ? hi : v)
32
+
33
+ export const attachControls = (scene: Scene, target: Node, options: ControlsOptions = {}): ControlsHandle => {
34
+ const minScale = options.minScale ?? 0.08
35
+ const maxScale = options.maxScale ?? 2
36
+ const pan = options.pan ?? true
37
+ const pinch = options.pinch ?? true
38
+ const twist = options.twist ?? true
39
+
40
+ const active: ActiveTouch[] = []
41
+
42
+ const onTouchStart = (e: TouchStartEvent<Node | null>): void => {
43
+ const touch: ActiveTouch = { pointerId: e.pointerId, x: e.clientX, y: e.clientY }
44
+ active.push(touch)
45
+ e.track({
46
+ onMove(m) {
47
+ if (active.length === 2) {
48
+ // --- two-finger: pinch-scale + twist-rotate about the gesture axis ---
49
+ const other = active.find((t) => t !== touch)
50
+ if (other) {
51
+ const axis = new Vec2(other.x - touch.x, other.y - touch.y)
52
+ const span = axis.length()
53
+ if (span > 1e-3) {
54
+ const dir = axis.scale(1 / span)
55
+ const delta = new Vec2(m.deltaX, m.deltaY)
56
+ if (pinch) {
57
+ const along = -dir.dot(delta) // movement along the pinch axis
58
+ target.scale = clamp(target.scale.x * (span + along) / span, minScale * 0.4, maxScale * 1.5)
59
+ }
60
+ if (twist) {
61
+ const tangent = dir.perp().dot(delta) // movement across the axis
62
+ target.quaternion = target.quaternion.mul(Quat.fromAxisAngle(Vec3.up, Math.atan2(tangent, span)))
63
+ }
64
+ }
65
+ }
66
+ } else if (active.length === 1 && pan) {
67
+ // --- one-finger: drag across the ground plane, relative to where the camera looks ---
68
+ let dir = scene.camera.forward
69
+ const parent = target.parent
70
+ if (parent) dir = dir.rotate(parent.quaternion.invert()) // into the parent's local frame
71
+ dir = dir.withY(0)
72
+ const k = Math.hypot(dir.x, dir.z)
73
+ const screen = Math.max(...scene.camera.displaySize)
74
+ if (k > 1e-4 && screen > 0) {
75
+ const dist = target.worldPosition.distanceTo(scene.camera.worldPosition)
76
+ const right = new Vec3(-dir.z, 0, dir.x).scale(m.deltaX / k / screen * dist)
77
+ const fwd = dir.scale(-m.deltaY / k / screen * dist)
78
+ const move = right.add(fwd)
79
+ const p = target.position
80
+ target.position = new Vec3(p.x + move.x, p.y, p.z + move.z) // keep height fixed
81
+ }
82
+ }
83
+ touch.x = m.clientX
84
+ touch.y = m.clientY
85
+ },
86
+ onEnd() {
87
+ const i = active.indexOf(touch)
88
+ if (i >= 0) active.splice(i, 1)
89
+ },
90
+ })
91
+ }
92
+
93
+ scene.addEventListener("touchstart", onTouchStart)
94
+ return { remove() { scene.removeEventListener("touchstart", onTouchStart); active.length = 0 } }
95
+ }
@@ -0,0 +1,6 @@
1
+ // Tiny shared module to break the Node ↔ Scene ↔ touch dependency cycle. The active 3D scene is set
2
+ // here by Scene.open()/close() and read by the touch dispatch system.
3
+
4
+ import type { Scene } from "./Scene"
5
+
6
+ export const glState: { activeScene: Scene | null } = { activeScene: null }
@@ -0,0 +1,56 @@
1
+ // 3D pointer dispatch. Registers the host touch callbacks lazily (first listener) and routes hits to
2
+ // the struck Node and to the active scene's listeners. Ported from worker/src/TouchEventSystem.ts.
3
+
4
+ import { ClickEvent, TouchStartEvent, type TrackHandler } from "../runtime/touch"
5
+ import { nodeRegistry } from "./Node"
6
+ import { glState } from "./state"
7
+
8
+ const onTouchEnd = (pointerId: number, clientX: number, clientY: number, entityId: number): TrackHandler | null => {
9
+ const scene = glState.activeScene
10
+ if (!scene) return null
11
+ const ev = new ClickEvent<any>(clientX, clientY, pointerId)
12
+ if (entityId > 0) {
13
+ const node = nodeRegistry.get(entityId) ?? null
14
+ ev.target = node
15
+ if (node) node._dispatchEvent("click", ev)
16
+ } else {
17
+ ev.target = null
18
+ }
19
+ for (const listener of scene._clickListeners.slice()) listener(ev)
20
+ return null
21
+ }
22
+
23
+ const onTouchStart = (pointerId: number, clientX: number, clientY: number, entityId: number): TrackHandler | null => {
24
+ const scene = glState.activeScene
25
+ if (!scene) return null
26
+ const ev = new TouchStartEvent<any>(clientX, clientY, pointerId)
27
+ if (entityId > 0) {
28
+ const node = nodeRegistry.get(entityId) ?? null
29
+ ev.target = node
30
+ if (node) {
31
+ node._dispatchEvent("touchstart", ev)
32
+ if (ev._trackHandler) return ev._trackHandler
33
+ }
34
+ } else {
35
+ ev.target = null
36
+ }
37
+ for (const listener of scene._touchStartListeners.slice()) {
38
+ listener(ev)
39
+ if (ev._trackHandler) return ev._trackHandler
40
+ }
41
+ return null
42
+ }
43
+
44
+ let touchStartRegistered = false
45
+ export const registerTouchStartEvent = (): void => {
46
+ if (touchStartRegistered) return
47
+ touchStartRegistered = true
48
+ _creatorUI.registerTouchStartEvent(onTouchStart)
49
+ }
50
+
51
+ let touchEndRegistered = false
52
+ export const registerTouchEndEvent = (): void => {
53
+ if (touchEndRegistered) return
54
+ touchEndRegistered = true
55
+ _creatorUI.registerTouchEndEvent(onTouchEnd)
56
+ }
@@ -0,0 +1,85 @@
1
+ // THE aggregation point. Every public global the SDK provides is re-exported here under the exact
2
+ // name user code uses it as. The bundler passes this file to esbuild's `inject:` option, so user
3
+ // code references `Scene`, `Sprite`, `toast`, … with no imports; esbuild tree-shakes away whatever
4
+ // a given project does not use (so a pure-2D project drops all 3D classes, and vice versa).
5
+ //
6
+ // Rule: this is the ONLY file that `export`s public names. Everything else is internal and imported
7
+ // by it. Host-provided globals (setLoop, console, setTimeout, DEG2RAD, asset, …) are NOT here —
8
+ // they live in src/host.d.ts as declarations and stay as free references at runtime.
9
+
10
+ // ---- math ----
11
+ export { Mathf } from "./math/Mathf"
12
+ export { Vec2, Vec3 } from "./math/vec"
13
+ export { Quat } from "./math/quat"
14
+ export { Mat4 } from "./math/mat4"
15
+ export { Color } from "./core/color"
16
+
17
+ // ---- aspects (attachable node capabilities) ----
18
+ export { Aspect, type With } from "./core/Aspect"
19
+
20
+ // ---- platform runtime (injected globals; the host provides the lower-level _creator* bridges) ----
21
+ export { toast, SvgSource } from "./runtime/misc"
22
+ export { fetch, fetchLocal, FormData, File } from "./runtime/fetch"
23
+ export { localStorage } from "./runtime/storage"
24
+ export { device } from "./runtime/device"
25
+ export { Input } from "./runtime/input"
26
+ export { WebSocket } from "./runtime/net"
27
+ export { AudioPlayer, VideoPlayer } from "./runtime/media"
28
+ export { openFilePicker } from "./runtime/files"
29
+ export { ClickEvent, TouchStartEvent } from "./runtime/touch"
30
+
31
+ // ---- 2D engine (creator-2d) ----
32
+ export { Scene2D } from "./g2/Scene2D"
33
+ export { Node2D } from "./g2/Node2D"
34
+ export { Camera2D } from "./g2/Camera2D"
35
+ export { Sprite } from "./g2/Sprite"
36
+ export { SpriteAnimation } from "./g2/SpriteAnimation"
37
+ export { Shape } from "./g2/Shape"
38
+ export { Physics2D } from "./g2/Physics2D"
39
+ export { Trigger } from "./g2/Trigger"
40
+ export { CharacterController2D } from "./g2/CharacterController2D"
41
+ export { Tilemap } from "./g2/Tilemap"
42
+ export { Texture2D } from "./g2/Texture2D"
43
+
44
+ // ---- UI (creator-ui) ----
45
+ export {
46
+ UIScreen, type UIScreenStyle,
47
+ UIRow, UIColumn, UIBox, type UIContainerStyle,
48
+ UIText, type UITextStyle,
49
+ UIButton, type UIButtonStyle,
50
+ UIImage, type UIImageStyle,
51
+ UIVideo, type UIVideoStyle,
52
+ UIInput, UITextArea, type UIInputStyle,
53
+ UIScrollable, type UIScrollableStyle,
54
+ UIWidget, type UIWidgetStyle,
55
+ UISpacer, type UISpacerStyle,
56
+ UIVirtualizedList,
57
+ Router,
58
+ registerFont,
59
+ } from "./ui/UI"
60
+
61
+ // ---- animate ----
62
+ export { animate, animateMat4, stopAnimation, pauseAnimation, resumeAnimation } from "./animate/animate"
63
+ export { cubicBezier } from "./animate/bezier"
64
+ export { easeIn, easeOut, easeInOut } from "./animate/easings"
65
+
66
+ // ---- plugins (optional, host-provided capabilities — see src/plugins/) ----
67
+ export { QRScanner } from "./plugins/qr"
68
+
69
+ // ---- 3D engine (creator-gl) ----
70
+ export { Scene, ARScene } from "./gl/Scene"
71
+ export { Node } from "./gl/Node"
72
+ export { Mesh } from "./gl/Mesh"
73
+ export { Model } from "./gl/Model"
74
+ export { Physics } from "./gl/Physics"
75
+ export { Collider } from "./gl/Collider"
76
+ export { ModelAnimation } from "./gl/ModelAnimation"
77
+ export { Light } from "./gl/Light"
78
+ export { Material } from "./gl/Material"
79
+ export { Geometry } from "./gl/Geometry"
80
+ export { Texture } from "./gl/Texture"
81
+ export { Camera } from "./gl/Camera"
82
+ export { Ray } from "./gl/Ray"
83
+ export { Plane } from "./gl/Plane"
84
+ export { Noise } from "./gl/Noise"
85
+ export { Particles, dynamic, dynamicColor } from "./gl/Particles"
@@ -0,0 +1,118 @@
1
+ // Scalar / general math helpers, exposed to user code as the `Mathf` global. Vector & quaternion
2
+ // helpers live with the engine that uses them (vec2 in g2/, vec3+quat in gl/). `smoothDamp` works on
3
+ // both numbers and numeric arrays here (no gl-matrix), which covers 2D (vec2) and 3D position damping.
4
+
5
+ export type Damper<T extends number | number[]> = ((target: T, dt: number) => T) & {
6
+ /** Snap the damper's internal current value (e.g. on teleport). */
7
+ setValue(value: T): void
8
+ }
9
+
10
+ const clamp = (v: number, min: number, max: number): number => (v < min ? min : v > max ? max : v)
11
+
12
+ const smoothDampNumber = (initial: number, smoothTime: number, maxSpeed: number): Damper<number> => {
13
+ let current = initial
14
+ let velocity = 0
15
+ const damp = ((target: number, dt: number): number => {
16
+ smoothTime = Math.max(0.0001, smoothTime)
17
+ const omega = 2 / smoothTime
18
+ const x = omega * dt
19
+ const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x)
20
+ const maxDelta = maxSpeed * smoothTime
21
+ let delta = clamp(current - target, -maxDelta, maxDelta)
22
+ const newTarget = current - delta
23
+ const temp = (velocity + omega * delta) * dt
24
+ velocity = (velocity - omega * temp) * exp
25
+ current = newTarget + (delta + temp) * exp
26
+ return current
27
+ }) as Damper<number>
28
+ damp.setValue = (v) => { current = v; velocity = 0 }
29
+ return damp
30
+ }
31
+
32
+ function smoothDamp(initial: number, smoothTime: number, maxSpeed?: number): Damper<number>
33
+ function smoothDamp(initial: number[], smoothTime: number, maxSpeed?: number): Damper<number[]>
34
+ function smoothDamp(initial: any, smoothTime: number, maxSpeed = Infinity): any {
35
+ return typeof initial === "number"
36
+ ? smoothDampNumber(initial, smoothTime, maxSpeed)
37
+ : smoothDampArray(initial, smoothTime, maxSpeed)
38
+ }
39
+
40
+ const smoothDampArray = (initial: number[], smoothTime: number, maxSpeed: number): Damper<number[]> => {
41
+ const n = initial.length
42
+ let current = initial.slice()
43
+ const velocity = new Array(n).fill(0)
44
+ const damp = ((target: number[], dt: number): number[] => {
45
+ smoothTime = Math.max(0.0001, smoothTime)
46
+ const omega = 2 / smoothTime
47
+ const x = omega * dt
48
+ const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x)
49
+ const maxDelta = maxSpeed * smoothTime
50
+
51
+ const delta = new Array(n)
52
+ let mag = 0
53
+ for (let i = 0; i < n; i++) { delta[i] = current[i] - target[i]; mag += delta[i] * delta[i] }
54
+ mag = Math.sqrt(mag)
55
+ if (mag > maxDelta && mag > 0) {
56
+ const s = maxDelta / mag
57
+ for (let i = 0; i < n; i++) delta[i] *= s
58
+ }
59
+
60
+ const out = new Array(n)
61
+ for (let i = 0; i < n; i++) {
62
+ const newTarget = current[i] - delta[i]
63
+ const temp = (velocity[i] + omega * delta[i]) * dt
64
+ velocity[i] = (velocity[i] - omega * temp) * exp
65
+ out[i] = newTarget + (delta[i] + temp) * exp
66
+ }
67
+ current = out
68
+ return out.slice()
69
+ }) as Damper<number[]>
70
+ damp.setValue = (v) => { current = v.slice(); velocity.fill(0) }
71
+ return damp
72
+ }
73
+
74
+ export const Mathf = {
75
+ PI: Math.PI,
76
+ TAU: Math.PI * 2,
77
+ EPSILON: 1e-6,
78
+ DEG2RAD: Math.PI / 180,
79
+ RAD2DEG: 180 / Math.PI,
80
+
81
+ deg2rad: (deg: number): number => deg * (Math.PI / 180),
82
+ rad2deg: (rad: number): number => rad * (180 / Math.PI),
83
+
84
+ clamp,
85
+ clamp01: (v: number): number => clamp(v, 0, 1),
86
+ lerp: (a: number, b: number, t: number): number => a + (b - a) * t,
87
+ inverseLerp: (a: number, b: number, v: number): number => (a === b ? 0 : (v - a) / (b - a)),
88
+ remap: (v: number, inMin: number, inMax: number, outMin: number, outMax: number): number =>
89
+ outMin + ((v - inMin) / (inMax - inMin)) * (outMax - outMin),
90
+
91
+ /** Move `a` toward `b` by at most `maxDelta`. */
92
+ moveTowards: (a: number, b: number, maxDelta: number): number => {
93
+ const d = b - a
94
+ return Math.abs(d) <= maxDelta ? b : a + Math.sign(d) * maxDelta
95
+ },
96
+ /** Frame-rate-independent exponential approach (good default: lambda 5..15). */
97
+ damp: (a: number, b: number, lambda: number, dt: number): number => a + (b - a) * (1 - Math.exp(-lambda * dt)),
98
+
99
+ sign: Math.sign,
100
+ /** Loop a value into [0, length). */
101
+ repeat: (t: number, length: number): number => clamp(t - Math.floor(t / length) * length, 0, length),
102
+ /** Ping-pong a value in [0, length]. */
103
+ pingPong: (t: number, length: number): number => {
104
+ const r = clamp(t - Math.floor(t / (length * 2)) * (length * 2), 0, length * 2)
105
+ return length - Math.abs(r - length)
106
+ },
107
+
108
+ random: (min = 0, max = 1): number => min + Math.random() * (max - min),
109
+ randomInt: (min: number, max: number): number => Math.floor(min + Math.random() * (max - min + 1)),
110
+
111
+ /**
112
+ * Critically-damped follow (Unity SmoothDamp). Returns a stateful damper you call every frame:
113
+ * const d = Mathf.smoothDamp([x, y], 0.15)
114
+ * pos = d(targetPos, dt)
115
+ * Works on a number or a numeric array (vec2 / vec3).
116
+ */
117
+ smoothDamp,
118
+ }