lecodes-cli 0.1.5 → 0.1.7
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/dist/index.js +300 -149
- package/package.json +5 -3
- package/runtime/sdk/animate/animate.ts +238 -0
- package/runtime/sdk/animate/bezier.ts +138 -0
- package/runtime/sdk/animate/easings.ts +126 -0
- package/runtime/sdk/core/color.ts +66 -0
- package/runtime/sdk/core/events.ts +38 -0
- package/runtime/sdk/core/registry.ts +23 -0
- package/runtime/sdk/g2/Camera2D.ts +73 -0
- package/runtime/sdk/g2/Node2D.ts +104 -0
- package/runtime/sdk/g2/Scene2D.ts +73 -0
- package/runtime/sdk/g2/Sprite.ts +164 -0
- package/runtime/sdk/g2/Texture2D.ts +37 -0
- package/runtime/sdk/g2/Tilemap.ts +41 -0
- package/runtime/sdk/g2/loop.ts +22 -0
- package/runtime/sdk/gl/Body.ts +101 -0
- package/runtime/sdk/gl/Camera.ts +40 -0
- package/runtime/sdk/gl/Geometry.ts +218 -0
- package/runtime/sdk/gl/Light.ts +37 -0
- package/runtime/sdk/gl/Material.ts +124 -0
- package/runtime/sdk/gl/Mesh.ts +123 -0
- package/runtime/sdk/gl/Node.ts +307 -0
- package/runtime/sdk/gl/Noise.ts +30 -0
- package/runtime/sdk/gl/Particles.ts +185 -0
- package/runtime/sdk/gl/Plane.ts +53 -0
- package/runtime/sdk/gl/Ray.ts +16 -0
- package/runtime/sdk/gl/Scene.ts +179 -0
- package/runtime/sdk/gl/Texture.ts +35 -0
- package/runtime/sdk/gl/controls.ts +95 -0
- package/runtime/sdk/gl/state.ts +6 -0
- package/runtime/sdk/gl/touch.ts +56 -0
- package/runtime/sdk/inject.ts +72 -0
- package/runtime/sdk/math/Mathf.ts +118 -0
- package/runtime/sdk/math/mat4.ts +278 -0
- package/runtime/sdk/math/quat.ts +232 -0
- package/runtime/sdk/math/vec.ts +255 -0
- package/runtime/sdk/plugins/qr.ts +36 -0
- package/runtime/sdk/runtime/device.ts +33 -0
- package/runtime/sdk/runtime/fetch.ts +71 -0
- package/runtime/sdk/runtime/files.ts +23 -0
- package/runtime/sdk/runtime/input.ts +12 -0
- package/runtime/sdk/runtime/media.ts +111 -0
- package/runtime/sdk/runtime/misc.ts +12 -0
- package/runtime/sdk/runtime/net.ts +36 -0
- package/runtime/sdk/runtime/storage.ts +13 -0
- package/runtime/sdk/runtime/touch.ts +44 -0
- package/runtime/sdk/ui/UI.ts +19 -0
- package/runtime/sdk/ui/UIButton.ts +84 -0
- package/runtime/sdk/ui/UIContainer.ts +100 -0
- package/runtime/sdk/ui/UIImage.ts +68 -0
- package/runtime/sdk/ui/UIInput.ts +78 -0
- package/runtime/sdk/ui/UINode.ts +342 -0
- package/runtime/sdk/ui/UIScreen.ts +122 -0
- package/runtime/sdk/ui/UIScrollable.ts +70 -0
- package/runtime/sdk/ui/UISpacer.ts +14 -0
- package/runtime/sdk/ui/UIText.ts +40 -0
- package/runtime/sdk/ui/UIVideo.ts +38 -0
- package/runtime/sdk/ui/UIVirtualizedList.ts +222 -0
- package/runtime/sdk/ui/UIWidget.ts +97 -0
- package/runtime/sdk/ui/fonts.ts +10 -0
- package/runtime/sdk/ui/router.ts +73 -0
- package/runtime/gl-matrix/common.js +0 -54
- package/runtime/gl-matrix/header.js +0 -12
- package/runtime/gl-matrix/index.js +0 -11
- package/runtime/gl-matrix/mat2.js +0 -425
- package/runtime/gl-matrix/mat2d.js +0 -476
- package/runtime/gl-matrix/mat3.js +0 -765
- package/runtime/gl-matrix/mat4.js +0 -1947
- package/runtime/gl-matrix/quat.js +0 -737
- package/runtime/gl-matrix/quat2.js +0 -822
- package/runtime/gl-matrix/vec2.js +0 -620
- package/runtime/gl-matrix/vec3.js +0 -791
- package/runtime/gl-matrix/vec4.js +0 -652
- package/runtime/worker/animate.js +0 -304
- package/runtime/worker/chunk-4ZN32GYS.js +0 -34
- package/runtime/worker/chunk-G5ZA3LKB.js +0 -52
- package/runtime/worker/chunk-MI3UXE5H.js +0 -1395
- package/runtime/worker/creator.js +0 -930
- package/runtime/worker/globals.js +0 -208
- package/runtime/worker/ui.js +0 -763
- package/runtime/worker/utils.js +0 -16
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Orthographic camera for a Scene2D. Position/zoom/rotation live natively; `follow` adds a
|
|
2
|
+
// render-synced per-frame update that tracks a node.
|
|
3
|
+
|
|
4
|
+
import { Vec2, cx, cy, type Vec2Like } from "../math/vec"
|
|
5
|
+
import type { Node2D } from "./Node2D"
|
|
6
|
+
import { offUpdate, onUpdate } from "./loop"
|
|
7
|
+
|
|
8
|
+
export type FollowOptions = {
|
|
9
|
+
/** World-space offset added to the target's position. */
|
|
10
|
+
offset?: Vec2Like
|
|
11
|
+
/** 0 = snap (default). >0 = exponential smoothing time constant in seconds (smaller = snappier). */
|
|
12
|
+
smooth?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class Camera2D {
|
|
16
|
+
private _x = 0
|
|
17
|
+
private _y = 0
|
|
18
|
+
private _zoom = 1
|
|
19
|
+
private _rotation = 0
|
|
20
|
+
|
|
21
|
+
private _followTarget: Node2D | null = null
|
|
22
|
+
private _followOpts: FollowOptions = {}
|
|
23
|
+
private _followCb: ((dt: number) => void) | null = null
|
|
24
|
+
|
|
25
|
+
private readonly sceneId: number
|
|
26
|
+
constructor(sceneId: number) { this.sceneId = sceneId }
|
|
27
|
+
|
|
28
|
+
get position(): Vec2 { return new Vec2(this._x, this._y) }
|
|
29
|
+
set position(v: Vec2Like) { this._x = cx(v); this._y = cy(v); _creator2d.cameraSetPosition(this.sceneId, this._x, this._y) }
|
|
30
|
+
|
|
31
|
+
get zoom(): number { return this._zoom }
|
|
32
|
+
set zoom(z: number) { this._zoom = z; _creator2d.cameraSetZoom(this.sceneId, z) }
|
|
33
|
+
|
|
34
|
+
get rotation(): number { return this._rotation }
|
|
35
|
+
set rotation(deg: number) { this._rotation = deg; _creator2d.cameraSetRotation(this.sceneId, deg) }
|
|
36
|
+
|
|
37
|
+
screenToWorld(screenX: number, screenY: number): Vec2 {
|
|
38
|
+
const o = _creator2d.cameraScreenToWorld(this.sceneId, screenX, screenY)
|
|
39
|
+
return new Vec2(o[0], o[1])
|
|
40
|
+
}
|
|
41
|
+
worldToScreen(worldX: number, worldY: number): Vec2 {
|
|
42
|
+
const o = _creator2d.cameraWorldToScreen(this.sceneId, worldX, worldY)
|
|
43
|
+
return new Vec2(o[0], o[1])
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Track a node each frame (snaps by default; pass `smooth` for eased follow). */
|
|
47
|
+
follow(node: Node2D, options: FollowOptions = {}): this {
|
|
48
|
+
this._followTarget = node
|
|
49
|
+
this._followOpts = options
|
|
50
|
+
if (this._followCb) return this
|
|
51
|
+
this._followCb = (dt: number) => {
|
|
52
|
+
const t = this._followTarget
|
|
53
|
+
if (!t) return
|
|
54
|
+
const [ ox, oy ] = this._followOpts.offset ?? [ 0, 0 ]
|
|
55
|
+
const tx = t.x + ox
|
|
56
|
+
const ty = t.y + oy
|
|
57
|
+
const s = this._followOpts.smooth ?? 0
|
|
58
|
+
if (s > 0) {
|
|
59
|
+
const k = 1 - Math.exp(-dt / s)
|
|
60
|
+
this.position = [ this._x + (tx - this._x) * k, this._y + (ty - this._y) * k ]
|
|
61
|
+
} else {
|
|
62
|
+
this.position = [ tx, ty ]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
onUpdate(this._followCb)
|
|
66
|
+
return this
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
unfollow(): void {
|
|
70
|
+
this._followTarget = null
|
|
71
|
+
if (this._followCb) { offUpdate(this._followCb); this._followCb = null }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Base 2D scene node — an empty transform. Sprite and Tilemap extend it, so every node operation
|
|
2
|
+
// (position, layer, visibility, events) works on them. Native (creator-2d) owns the authoritative
|
|
3
|
+
// transform; JS is the sole writer (no 2D physics yet) so a local cache makes reads free.
|
|
4
|
+
//
|
|
5
|
+
// Empty Node2Ds are useful as: grouping parents, world-space markers/spawn points, and trigger /
|
|
6
|
+
// logic objects you attach a .collider() to.
|
|
7
|
+
|
|
8
|
+
import { Emitter } from "../core/events"
|
|
9
|
+
import { Registry } from "../core/registry"
|
|
10
|
+
import { Vec2, cx, cy, type Vec2Like } from "../math/vec"
|
|
11
|
+
|
|
12
|
+
// re-exported so existing `import { Vec2 } from "./Node2D"` sites keep working; the canonical home is math/.
|
|
13
|
+
export { Vec2, type Vec2Like }
|
|
14
|
+
|
|
15
|
+
export type Node2DEvents = {
|
|
16
|
+
/** A looping animation clip wrapped around (fires each loop). */
|
|
17
|
+
loopReached: (clip: string) => void
|
|
18
|
+
/** A non-looping animation clip finished. */
|
|
19
|
+
completed: (clip: string) => void
|
|
20
|
+
/** A collider overlap began / ended (deferred — native 2D triggers are not built yet). */
|
|
21
|
+
enter: (other: Node2D) => void
|
|
22
|
+
exit: (other: Node2D) => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** id → Node2D, so host callbacks (animation events) route back to the owning JS object. */
|
|
26
|
+
export const node2dRegistry = new Registry<Node2D>()
|
|
27
|
+
|
|
28
|
+
export type ColliderOptions = {
|
|
29
|
+
/** Box half-extents (world units). Defaults to the sprite size when omitted. */
|
|
30
|
+
size?: Vec2Like
|
|
31
|
+
/** Center offset. */
|
|
32
|
+
offset?: Vec2Like
|
|
33
|
+
/** Report overlaps via 'enter'/'exit' instead of blocking (always true for now). */
|
|
34
|
+
trigger?: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class Node2D extends Emitter<Node2DEvents> {
|
|
38
|
+
/** Native entity handle. */
|
|
39
|
+
readonly id: number
|
|
40
|
+
|
|
41
|
+
protected _x = 0
|
|
42
|
+
protected _y = 0
|
|
43
|
+
protected _rotation = 0
|
|
44
|
+
protected _sx = 1
|
|
45
|
+
protected _sy = 1
|
|
46
|
+
protected _layer = 0
|
|
47
|
+
protected _z = 0
|
|
48
|
+
protected _visible = true
|
|
49
|
+
|
|
50
|
+
constructor(internalId?: number) {
|
|
51
|
+
super()
|
|
52
|
+
this.id = internalId ?? _creator2d.createEntity()
|
|
53
|
+
node2dRegistry.set(this.id, this)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// --- transform (Y-up world; at zoom 1, units == pixels) ---
|
|
57
|
+
get x(): number { return this._x }
|
|
58
|
+
set x(v: number) { this._x = v; _creator2d.setPosition(this.id, v, this._y) }
|
|
59
|
+
get y(): number { return this._y }
|
|
60
|
+
set y(v: number) { this._y = v; _creator2d.setPosition(this.id, this._x, v) }
|
|
61
|
+
|
|
62
|
+
get position(): Vec2 { return new Vec2(this._x, this._y) }
|
|
63
|
+
set position(v: Vec2Like) { this._x = cx(v); this._y = cy(v); _creator2d.setPosition(this.id, this._x, this._y) }
|
|
64
|
+
|
|
65
|
+
/** Rotation in degrees (CCW). */
|
|
66
|
+
get rotation(): number { return this._rotation }
|
|
67
|
+
set rotation(deg: number) { this._rotation = deg; _creator2d.setRotation(this.id, deg) }
|
|
68
|
+
|
|
69
|
+
get scale(): Vec2 { return new Vec2(this._sx, this._sy) }
|
|
70
|
+
set scale(v: Vec2Like | number) {
|
|
71
|
+
const sx = typeof v === "number" ? v : cx(v)
|
|
72
|
+
const sy = typeof v === "number" ? v : cy(v)
|
|
73
|
+
this._sx = sx; this._sy = sy
|
|
74
|
+
_creator2d.setScale(this.id, sx, sy)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Draw layer (int). Higher layers render on top. */
|
|
78
|
+
get layer(): number { return this._layer }
|
|
79
|
+
set layer(v: number) { this._layer = v; _creator2d.setLayer(this.id, v) }
|
|
80
|
+
|
|
81
|
+
/** Intra-layer depth for non-Y-sorted layers (higher z = on top). */
|
|
82
|
+
get z(): number { return this._z }
|
|
83
|
+
set z(v: number) { this._z = v; _creator2d.setZ(this.id, v) }
|
|
84
|
+
|
|
85
|
+
get visible(): boolean { return this._visible }
|
|
86
|
+
set visible(v: boolean) { this._visible = v; _creator2d.setVisible(this.id, v) }
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Attach a collider for overlap detection. DEFERRED: native 2D physics/triggers are not built yet,
|
|
90
|
+
* so this records intent and is a no-op until the engine lands — the 'enter'/'exit' events won't
|
|
91
|
+
* fire. The API is here so game code that uses it compiles and stays stable.
|
|
92
|
+
*/
|
|
93
|
+
collider(_options: ColliderOptions = {}): this {
|
|
94
|
+
return this
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** @internal — overridden by Sprite to translate native clip ids into named animation events. */
|
|
98
|
+
_dispatchAnimEvent(_clipId: number, _type: number): void {}
|
|
99
|
+
|
|
100
|
+
destroy(): void {
|
|
101
|
+
node2dRegistry.delete(this.id)
|
|
102
|
+
_creator2d.destroyEntity(this.id)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// A 2D scene: a draw set + an orthographic Camera2D. Opening a scene asks the host to bring up the
|
|
2
|
+
// 2D engine and starts its RAF loop; only the active scene renders. Grown from the Phase 0 seed.
|
|
3
|
+
|
|
4
|
+
import { Color, type ColorInput } from "../core/color"
|
|
5
|
+
import { Camera2D } from "./Camera2D"
|
|
6
|
+
import type { Node2D } from "./Node2D"
|
|
7
|
+
|
|
8
|
+
export type Scene2DOptions = {
|
|
9
|
+
/** Background color — '#10131a', 0x10131a, or [r,g,b]/[r,g,b,a] in 0..1. */
|
|
10
|
+
background?: ColorInput
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Returned by scene2d.layer(n); chain .ySort() to enable top-down depth sorting for that layer. */
|
|
14
|
+
class LayerHandle {
|
|
15
|
+
private readonly sceneId: number
|
|
16
|
+
private readonly layer: number
|
|
17
|
+
constructor(sceneId: number, layer: number) { this.sceneId = sceneId; this.layer = layer }
|
|
18
|
+
/** In this layer, lower-on-screen sprites overlap higher ones (top-down feet sorting). */
|
|
19
|
+
ySort(enabled = true): this {
|
|
20
|
+
_creator2d.setLayerYSort(this.sceneId, this.layer, enabled)
|
|
21
|
+
return this
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class Scene2D {
|
|
26
|
+
/** Native scene handle. */
|
|
27
|
+
readonly id: number
|
|
28
|
+
readonly camera: Camera2D
|
|
29
|
+
|
|
30
|
+
private static _active: Scene2D | null = null
|
|
31
|
+
static get active(): Scene2D | null { return Scene2D._active }
|
|
32
|
+
|
|
33
|
+
constructor(options: Scene2DOptions = {}) {
|
|
34
|
+
this.id = _creator2d.createScene()
|
|
35
|
+
this.camera = new Camera2D(this.id)
|
|
36
|
+
if (options.background !== undefined) this.background = options.background
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
set background(color: ColorInput) {
|
|
40
|
+
const [ r, g, b, a ] = Color.toRgba01(color)
|
|
41
|
+
_creator2d.sceneSetClearColor(this.id, r, g, b, a)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Address a draw layer to configure it, e.g. scene2d.layer(1).ySort(). */
|
|
45
|
+
layer(n: number): LayerHandle {
|
|
46
|
+
return new LayerHandle(this.id, n)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
add(...nodes: Node2D[]): this {
|
|
50
|
+
for (const n of nodes) _creator2d.addEntityToScene(this.id, n.id)
|
|
51
|
+
return this
|
|
52
|
+
}
|
|
53
|
+
remove(...nodes: Node2D[]): this {
|
|
54
|
+
for (const n of nodes) _creator2d.removeEntityFromScene(this.id, n.id)
|
|
55
|
+
return this
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Make this the active scene (brings up the 2D engine on the canvas; closes any other). */
|
|
59
|
+
open(): this {
|
|
60
|
+
_creator2d.openScene(this.id)
|
|
61
|
+
Scene2D._active = this
|
|
62
|
+
return this
|
|
63
|
+
}
|
|
64
|
+
close(): this {
|
|
65
|
+
_creator2d.closeScene()
|
|
66
|
+
if (Scene2D._active === this) Scene2D._active = null
|
|
67
|
+
return this
|
|
68
|
+
}
|
|
69
|
+
destroy(): void {
|
|
70
|
+
this.close()
|
|
71
|
+
_creator2d.destroyScene(this.id)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// A textured 2D node. Cheap setters proxy straight to the native sprite; the data lives in
|
|
2
|
+
// creator-2d. Sprite-sheet animation is built in as chainable methods (no separate component):
|
|
3
|
+
// new Sprite({ texture }).clips({ size: [32, 48], idle: [0,1,2,3], walk: [4,5,6,7] }).play('idle')
|
|
4
|
+
|
|
5
|
+
import { Color, type ColorInput } from "../core/color"
|
|
6
|
+
import { Vec2, type Vec2Like } from "../math/vec"
|
|
7
|
+
import { ensureAnimEvents } from "./loop"
|
|
8
|
+
import { Node2D } from "./Node2D"
|
|
9
|
+
import type { Texture2D } from "./Texture2D"
|
|
10
|
+
|
|
11
|
+
export type SpriteOptions = {
|
|
12
|
+
texture?: Texture2D
|
|
13
|
+
/** Pivot, [0,0] = top-left .. [1,1] = bottom-right. Use [0.5, 1] (feet) for Y-sorted depth. */
|
|
14
|
+
anchor?: Vec2Like
|
|
15
|
+
/** World size; defaults to the texture's pixel dimensions (or the clip frame size once set). */
|
|
16
|
+
size?: Vec2Like
|
|
17
|
+
/** Normalized UV sub-rect [u0,v0,u1,v1]. */
|
|
18
|
+
frame?: [number, number, number, number]
|
|
19
|
+
/** Tint multiplied with the texture. */
|
|
20
|
+
color?: ColorInput
|
|
21
|
+
opacity?: number
|
|
22
|
+
layer?: number
|
|
23
|
+
position?: Vec2Like
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Per-clip frame list, or an object for per-clip fps/loop overrides. */
|
|
27
|
+
export type Clip = number[] | { frames: number[], fps?: number, loop?: boolean }
|
|
28
|
+
|
|
29
|
+
/** Animation definition: the grid frame size + named clips (+ optional shared fps/loop defaults). */
|
|
30
|
+
export type SpriteClips = {
|
|
31
|
+
/** Grid cell size in texture pixels. */
|
|
32
|
+
size: [number, number]
|
|
33
|
+
/** Default fps for clips that don't override it (default 12). */
|
|
34
|
+
fps?: number
|
|
35
|
+
/** Default loop for clips that don't override it (default true). */
|
|
36
|
+
loop?: boolean
|
|
37
|
+
[clip: string]: Clip | [number, number] | number | boolean | undefined
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class Sprite extends Node2D {
|
|
41
|
+
private _texture?: Texture2D
|
|
42
|
+
private _anchor = new Vec2(0.5, 0.5)
|
|
43
|
+
private _color: ColorInput = 0xffffff
|
|
44
|
+
private _opacity = 1
|
|
45
|
+
private _flipX = false
|
|
46
|
+
private _flipY = false
|
|
47
|
+
private _size: Vec2 | null = null
|
|
48
|
+
|
|
49
|
+
private _clipIds: Record<string, number> = {}
|
|
50
|
+
private _clipNames: Record<number, string> = {}
|
|
51
|
+
private _current: string | null = null
|
|
52
|
+
|
|
53
|
+
constructor(options: SpriteOptions = {}) {
|
|
54
|
+
super()
|
|
55
|
+
if (options.texture) this.texture = options.texture
|
|
56
|
+
if (options.size) this.size = options.size
|
|
57
|
+
if (options.anchor) this.anchor = options.anchor
|
|
58
|
+
if (options.frame) this.frame = options.frame
|
|
59
|
+
if (options.color !== undefined) this.color = options.color
|
|
60
|
+
if (options.opacity !== undefined) this.opacity = options.opacity
|
|
61
|
+
if (options.layer !== undefined) this.layer = options.layer
|
|
62
|
+
if (options.position) this.position = options.position
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get texture(): Texture2D | undefined { return this._texture }
|
|
66
|
+
set texture(t: Texture2D | undefined) {
|
|
67
|
+
this._texture = t
|
|
68
|
+
if (!t) return
|
|
69
|
+
_creator2d.setSprite(this.id, t.id) // native resets UV + size to the full texture
|
|
70
|
+
if (this._size) _creator2d.setSpriteSize(this.id, this._size.x, this._size.y)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Current world size: explicit override if set, else the texture's pixel dimensions. */
|
|
74
|
+
get size(): Vec2 { return this._size ?? (this._texture ? new Vec2(this._texture.width, this._texture.height) : new Vec2(0, 0)) }
|
|
75
|
+
set size(v: Vec2Like) { this._size = new Vec2(v); _creator2d.setSpriteSize(this.id, this._size.x, this._size.y) }
|
|
76
|
+
|
|
77
|
+
get anchor(): Vec2 { return this._anchor.clone() }
|
|
78
|
+
set anchor(v: Vec2Like) { this._anchor = new Vec2(v); _creator2d.setSpriteAnchor(this.id, this._anchor.x, this._anchor.y) }
|
|
79
|
+
|
|
80
|
+
/** Set the UV sub-rect (normalized). */
|
|
81
|
+
set frame(uv: [number, number, number, number]) { _creator2d.setSpriteFrame(this.id, uv[0], uv[1], uv[2], uv[3]) }
|
|
82
|
+
/** Set the sub-rect in texture pixels. */
|
|
83
|
+
setFramePx(px: number, py: number, pw: number, ph: number): this {
|
|
84
|
+
_creator2d.setSpriteFramePx(this.id, px, py, pw, ph)
|
|
85
|
+
return this
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
get color(): ColorInput { return this._color }
|
|
89
|
+
set color(c: ColorInput) { this._color = c; const [ r, g, b ] = Color.toRgb01(c); _creator2d.setSpriteColor(this.id, r, g, b) }
|
|
90
|
+
|
|
91
|
+
get opacity(): number { return this._opacity }
|
|
92
|
+
set opacity(a: number) { this._opacity = a; _creator2d.setSpriteOpacity(this.id, a) }
|
|
93
|
+
|
|
94
|
+
get flipX(): boolean { return this._flipX }
|
|
95
|
+
set flipX(v: boolean) { this._flipX = v; _creator2d.setSpriteFlip(this.id, v, this._flipY) }
|
|
96
|
+
get flipY(): boolean { return this._flipY }
|
|
97
|
+
set flipY(v: boolean) { this._flipY = v; _creator2d.setSpriteFlip(this.id, this._flipX, v) }
|
|
98
|
+
|
|
99
|
+
// --- animation (chainable) ---
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Define named sprite-sheet clips from a uniform grid. Frame indices are row-major into the grid.
|
|
103
|
+
* Sets the sprite's display size to one cell. Once defined, clips advance entirely in native code.
|
|
104
|
+
*/
|
|
105
|
+
clips(def: SpriteClips): this {
|
|
106
|
+
const tex = this._texture
|
|
107
|
+
if (!tex) throw new Error("Sprite.clips(): set a texture before defining clips")
|
|
108
|
+
const [ fw, fh ] = def.size
|
|
109
|
+
const defFps = typeof def.fps === "number" ? def.fps : 12
|
|
110
|
+
const defLoop = typeof def.loop === "boolean" ? def.loop : true
|
|
111
|
+
const cols = Math.max(1, Math.floor(tex.width / fw))
|
|
112
|
+
|
|
113
|
+
for (const name of Object.keys(def)) {
|
|
114
|
+
if (name === "size" || name === "fps" || name === "loop") continue
|
|
115
|
+
const value = def[name] as Clip
|
|
116
|
+
const indices = Array.isArray(value) ? value : value.frames
|
|
117
|
+
const fps = Array.isArray(value) ? defFps : (value.fps ?? defFps)
|
|
118
|
+
const loop = Array.isArray(value) ? defLoop : (value.loop ?? defLoop)
|
|
119
|
+
|
|
120
|
+
const frames = new Float32Array(indices.length * 4)
|
|
121
|
+
indices.forEach((idx, i) => {
|
|
122
|
+
const cx = (idx % cols) * fw
|
|
123
|
+
const cy = Math.floor(idx / cols) * fh
|
|
124
|
+
frames[i * 4 + 0] = cx / tex.width
|
|
125
|
+
frames[i * 4 + 1] = cy / tex.height
|
|
126
|
+
frames[i * 4 + 2] = (cx + fw) / tex.width
|
|
127
|
+
frames[i * 4 + 3] = (cy + fh) / tex.height
|
|
128
|
+
})
|
|
129
|
+
const clipId = _creator2d.defineAnimation(this.id, frames, fps, loop)
|
|
130
|
+
this._clipIds[name] = clipId
|
|
131
|
+
this._clipNames[clipId] = name
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!this._size) _creator2d.setSpriteSize(this.id, fw, fh)
|
|
135
|
+
ensureAnimEvents()
|
|
136
|
+
return this
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Play a clip by name. Re-playing the active clip is a no-op (keeps it running smoothly). */
|
|
140
|
+
play(name: string): this {
|
|
141
|
+
const id = this._clipIds[name]
|
|
142
|
+
if (id === undefined) throw new Error(`Sprite.play(): unknown clip "${name}"`)
|
|
143
|
+
if (this._current === name) return this
|
|
144
|
+
this._current = name
|
|
145
|
+
_creator2d.playAnimation(this.id, id)
|
|
146
|
+
return this
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
stop(): this {
|
|
150
|
+
this._current = null
|
|
151
|
+
_creator2d.stopAnimation(this.id)
|
|
152
|
+
return this
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
set animationSpeed(value: number) { _creator2d.setAnimationSpeed(this.id, value) }
|
|
156
|
+
get currentClip(): string | null { return this._current }
|
|
157
|
+
get animationFrame(): number { return _creator2d.getAnimationFrame(this.id) }
|
|
158
|
+
|
|
159
|
+
/** @internal — translate a native clip id back to its name and emit the animation event. */
|
|
160
|
+
override _dispatchAnimEvent(clipId: number, type: number): void {
|
|
161
|
+
const name = this._clipNames[clipId] ?? ""
|
|
162
|
+
this.dispatch(type === 1 ? "completed" : "loopReached", name)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// A GPU texture decoded natively by creator-2d (stb_image). Load from a URL (or an already-fetched
|
|
2
|
+
// response); the raw file bytes are handed to the engine, which decodes + uploads them.
|
|
3
|
+
|
|
4
|
+
// Internal SDK code imports injected globals explicitly (the bare-global inject is only for USER
|
|
5
|
+
// code; here `fetch` would otherwise resolve to the node/bun ambient one).
|
|
6
|
+
import { fetch, type FetchResponse } from "../runtime/fetch"
|
|
7
|
+
|
|
8
|
+
export class Texture2D {
|
|
9
|
+
/** Native texture handle. */
|
|
10
|
+
readonly id: number
|
|
11
|
+
readonly width: number
|
|
12
|
+
readonly height: number
|
|
13
|
+
|
|
14
|
+
constructor(id: number, width: number, height: number) {
|
|
15
|
+
this.id = id
|
|
16
|
+
this.width = width
|
|
17
|
+
this.height = height
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static load(source: string | FetchResponse): Promise<Texture2D> {
|
|
21
|
+
if (typeof source === "string") {
|
|
22
|
+
return fetch(source, { useOnce: true }).then((resp) => {
|
|
23
|
+
if (resp.status >= 400) {
|
|
24
|
+
return Promise.reject(new Error(`Failed to fetch texture from ${source}. HTTP ${resp.status}`))
|
|
25
|
+
}
|
|
26
|
+
return Texture2D.load(resp)
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
return new Promise<Texture2D>((resolve, reject) => {
|
|
30
|
+
_creator2d.createTexture((source as any).id ?? (source as any)._id, (id, w, h) => resolve(new Texture2D(id, w, h)), reject)
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
destroy(): void {
|
|
35
|
+
_creator2d.destroyTexture(this.id)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// A tilemap node. The grid is uploaded once and rendered natively with per-frame view culling; the
|
|
2
|
+
// node's position is the bottom-left of the map. Edit cells with setTile.
|
|
3
|
+
|
|
4
|
+
import { Node2D } from "./Node2D"
|
|
5
|
+
import type { Texture2D } from "./Texture2D"
|
|
6
|
+
|
|
7
|
+
export type TilemapOptions = {
|
|
8
|
+
texture: Texture2D
|
|
9
|
+
/** Map grid size (in tiles). */
|
|
10
|
+
cols: number
|
|
11
|
+
rows: number
|
|
12
|
+
/** On-screen cell size in world units — a single number (square) or [w, h]. */
|
|
13
|
+
tile: number | [number, number]
|
|
14
|
+
/** Atlas grid: how the texture is sliced into tiles, [atlasCols, atlasRows]. */
|
|
15
|
+
atlas: [number, number]
|
|
16
|
+
/** Row-major tile indices into the atlas (-1 = empty), length cols*rows. Row 0 is the top row. */
|
|
17
|
+
data: number[] | Int32Array
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class Tilemap extends Node2D {
|
|
21
|
+
readonly cols: number
|
|
22
|
+
readonly rows: number
|
|
23
|
+
|
|
24
|
+
constructor(options: TilemapOptions) {
|
|
25
|
+
const data = options.data instanceof Int32Array ? options.data : Int32Array.from(options.data)
|
|
26
|
+
const tileW = typeof options.tile === "number" ? options.tile : options.tile[0]
|
|
27
|
+
const tileH = typeof options.tile === "number" ? options.tile : options.tile[1]
|
|
28
|
+
const [ atlasCols, atlasRows ] = options.atlas
|
|
29
|
+
const id = _creator2d.createTilemap(
|
|
30
|
+
options.texture.id, options.cols, options.rows, tileW, tileH, atlasCols, atlasRows, data,
|
|
31
|
+
)
|
|
32
|
+
super(id) // wrap the native tilemap entity
|
|
33
|
+
this.cols = options.cols
|
|
34
|
+
this.rows = options.rows
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
setTile(x: number, y: number, index: number): this {
|
|
38
|
+
_creator2d.setTile(this.id, x, y, index)
|
|
39
|
+
return this
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Internal per-frame plumbing for the 2D engine. NOT user-facing — user game logic uses the host
|
|
2
|
+
// `setLoop` global. `onUpdate` taps the engine's render-synced pre-frame hook (_creator2d.onUpdate),
|
|
3
|
+
// used by camera.follow so the camera moves in lockstep with the native render (no 1-frame lag).
|
|
4
|
+
//
|
|
5
|
+
// Both registrations are lazy (first use) so a 2D project that never follows a camera or animates a
|
|
6
|
+
// sprite pulls none of this in.
|
|
7
|
+
|
|
8
|
+
import { node2dRegistry } from "./Node2D"
|
|
9
|
+
|
|
10
|
+
export const onUpdate = (callback: (dt: number) => void): void => _creator2d.onUpdate(callback)
|
|
11
|
+
export const offUpdate = (callback: (dt: number) => void): void => _creator2d.offUpdate(callback)
|
|
12
|
+
|
|
13
|
+
// Route native animation events (queued in C++, drained once per frame) to the owning node, which
|
|
14
|
+
// translates the native clip id to a clip name and emits 'loopReached' / 'completed'.
|
|
15
|
+
let animEventsRegistered = false
|
|
16
|
+
export const ensureAnimEvents = (): void => {
|
|
17
|
+
if (animEventsRegistered) return
|
|
18
|
+
animEventsRegistered = true
|
|
19
|
+
_creator2d.setOnAnimEvent(({ entityId, clipId, type }) => {
|
|
20
|
+
node2dRegistry.get(entityId)?._dispatchAnimEvent(clipId, type)
|
|
21
|
+
})
|
|
22
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Physics handle for a Node, backed by a JoltPhysics body in creator-gl. Created by node.physics();
|
|
2
|
+
// reached as node.body. Ported from worker/src/components/RigidBody.ts.
|
|
3
|
+
//
|
|
4
|
+
// For a `dynamic` body, physics OWNS the transform — do NOT set node.position per frame; drive it
|
|
5
|
+
// via velocity / applyImpulse. Reading node.position is free (native already wrote it). Kinematic
|
|
6
|
+
// bodies are driven with moveTo.
|
|
7
|
+
|
|
8
|
+
import { Vec3, cx, cy, cz, type Vec3Like } from "../math/vec"
|
|
9
|
+
import type { Node } from "./Node"
|
|
10
|
+
|
|
11
|
+
export type MotionType = "static" | "dynamic" | "kinematic"
|
|
12
|
+
const MOTION: Record<MotionType, number> = { static: 0, kinematic: 1, dynamic: 2 }
|
|
13
|
+
|
|
14
|
+
export type BodyShape =
|
|
15
|
+
| { type: "auto" }
|
|
16
|
+
| { type: "box", halfExtents: Vec3Like }
|
|
17
|
+
| { type: "sphere", radius: number }
|
|
18
|
+
| { type: "cylinder", halfHeight: number, radius: number }
|
|
19
|
+
|
|
20
|
+
export type PhysicsOptions = {
|
|
21
|
+
/** Default "dynamic". */
|
|
22
|
+
motion?: MotionType
|
|
23
|
+
/** Mass in kg for dynamic bodies (default 1). Ignored for static/kinematic. */
|
|
24
|
+
mass?: number
|
|
25
|
+
/** Default { type: "auto" } — a box from the mesh AABB × world scale. */
|
|
26
|
+
shape?: BodyShape
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class Body {
|
|
30
|
+
private _bodyId = 0
|
|
31
|
+
private readonly _motion: MotionType
|
|
32
|
+
|
|
33
|
+
constructor(node: Node, options: PhysicsOptions = {}) {
|
|
34
|
+
this._motion = options.motion ?? "dynamic"
|
|
35
|
+
const mass = options.mass ?? 1
|
|
36
|
+
|
|
37
|
+
// No physics in this build (CREATOR_GL_PHYSICS=OFF) → bodyId stays 0; all methods no-op.
|
|
38
|
+
if (!_creator.physicsHasSupport || !_creator.physicsHasSupport()) return
|
|
39
|
+
|
|
40
|
+
const motion = MOTION[this._motion]
|
|
41
|
+
const shape = resolveShape(node, options.shape ?? { type: "auto" })
|
|
42
|
+
if (shape.type === "sphere") {
|
|
43
|
+
this._bodyId = _creator.physicsCreateSphere(node.id, shape.radius, motion, mass)
|
|
44
|
+
} else if (shape.type === "cylinder") {
|
|
45
|
+
this._bodyId = _creator.physicsCreateCylinder(node.id, shape.halfHeight, shape.radius, motion, mass)
|
|
46
|
+
} else {
|
|
47
|
+
this._bodyId = _creator.physicsCreateBox(node.id, cx(shape.halfExtents), cy(shape.halfExtents), cz(shape.halfExtents), motion, mass)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Native Jolt body id. 0 until assigned, or if the build has no physics support. */
|
|
52
|
+
get id(): number { return this._bodyId }
|
|
53
|
+
get motion(): MotionType { return this._motion }
|
|
54
|
+
|
|
55
|
+
/** Linear velocity in world units/second (fresh Vec3 on read). */
|
|
56
|
+
get velocity(): Vec3 {
|
|
57
|
+
const out = new Float32Array(3)
|
|
58
|
+
if (this._bodyId) _creator.physicsGetLinearVelocity(this._bodyId, out)
|
|
59
|
+
return new Vec3(out[0], out[1], out[2])
|
|
60
|
+
}
|
|
61
|
+
set velocity(v: Vec3Like) {
|
|
62
|
+
if (this._bodyId) _creator.physicsSetLinearVelocity(this._bodyId, cx(v), cy(v), cz(v))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Apply an instantaneous impulse (kg·m/s) and wake the body. */
|
|
66
|
+
applyImpulse(v: Vec3Like): this {
|
|
67
|
+
if (this._bodyId) _creator.physicsApplyImpulse(this._bodyId, cx(v), cy(v), cz(v))
|
|
68
|
+
return this
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Teleport / drive a kinematic (or dynamic) body to a world position. */
|
|
72
|
+
moveTo(p: Vec3Like): this {
|
|
73
|
+
if (this._bodyId) _creator.physicsSetBodyPosition(this._bodyId, cx(p), cy(p), cz(p))
|
|
74
|
+
return this
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Remove the body from the simulation. */
|
|
78
|
+
remove(): void {
|
|
79
|
+
if (this._bodyId) { _creator.physicsRemoveBody(this._bodyId); this._bodyId = 0 }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Resolve "auto" to a box from the mesh AABB × world scale (so the physical box matches the rendered
|
|
84
|
+
// size). Falls back to a unit box if there is no geometry.
|
|
85
|
+
const resolveShape = (node: Node, shape: BodyShape): Exclude<BodyShape, { type: "auto" }> => {
|
|
86
|
+
if (shape.type !== "auto") return shape
|
|
87
|
+
const scale = node.worldScale
|
|
88
|
+
let hx = 0.5, hy = 0.5, hz = 0.5
|
|
89
|
+
const geom = node.geometry
|
|
90
|
+
if (geom) {
|
|
91
|
+
const v = geom.vertices
|
|
92
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity, maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity
|
|
93
|
+
for (let i = 0; i + 2 < v.length; i += 3) {
|
|
94
|
+
if (v[i] < minX) minX = v[i]; if (v[i] > maxX) maxX = v[i]
|
|
95
|
+
if (v[i + 1] < minY) minY = v[i + 1]; if (v[i + 1] > maxY) maxY = v[i + 1]
|
|
96
|
+
if (v[i + 2] < minZ) minZ = v[i + 2]; if (v[i + 2] > maxZ) maxZ = v[i + 2]
|
|
97
|
+
}
|
|
98
|
+
hx = (maxX - minX) * 0.5; hy = (maxY - minY) * 0.5; hz = (maxZ - minZ) * 0.5
|
|
99
|
+
}
|
|
100
|
+
return { type: "box", halfExtents: [ hx * scale.x, hy * scale.y, hz * scale.z ] }
|
|
101
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// The scene camera — a Node (so position / lookAt / transform all work) plus projection reads and
|
|
2
|
+
// screen↔world ray helpers. Created and attached by Scene. Ported from worker/src/components/Camera.ts.
|
|
3
|
+
|
|
4
|
+
import { Vec3 } from "../math/vec"
|
|
5
|
+
import { Node } from "./Node"
|
|
6
|
+
import { Ray } from "./Ray"
|
|
7
|
+
|
|
8
|
+
export class Camera extends Node {
|
|
9
|
+
private _sceneId = -1
|
|
10
|
+
/** Live projection matrix (updated by the host when the viewport/fov changes). */
|
|
11
|
+
readonly projectionMatrix = new Float32Array(16)
|
|
12
|
+
|
|
13
|
+
/** @internal — Scene wires the camera to its native scene id + projection callback. */
|
|
14
|
+
_attach(sceneId: number): void {
|
|
15
|
+
this._sceneId = sceneId
|
|
16
|
+
_creator.attachCameraToAR(sceneId, this.id, (mat) => {
|
|
17
|
+
for (let i = 0; i < 16; i++) this.projectionMatrix[i] = mat[i]
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get fov(): number { return _creator.getCameraFov(this._sceneId, 0) }
|
|
22
|
+
get horizontalFov(): number { return _creator.getCameraFov(this._sceneId, 1) }
|
|
23
|
+
get displaySize(): [number, number] {
|
|
24
|
+
const s = _creator.getDisplaySize()
|
|
25
|
+
return [ s[0], s[1] ]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** World-space ray direction through a screen pixel. */
|
|
29
|
+
getViewDirection(screenX: number, screenY: number): Vec3 {
|
|
30
|
+
if (this._sceneId < 0) throw new Error("Camera is not attached to a scene")
|
|
31
|
+
const v = new Float32Array(3)
|
|
32
|
+
_creator.getCameraViewDirection(this._sceneId, screenX, screenY, v)
|
|
33
|
+
return new Vec3(v[0], v[1], v[2])
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A pick ray from the camera through a screen pixel. */
|
|
37
|
+
getRay(screenX: number, screenY: number): Ray {
|
|
38
|
+
return new Ray(this.worldPosition, this.getViewDirection(screenX, screenY))
|
|
39
|
+
}
|
|
40
|
+
}
|