lecodes-sdk 2.4.9 → 2.5.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/dist/global.d.ts +35 -0
- package/dist/types/core/env.d.ts +25 -0
- package/dist/types/core/particlesConfig.d.ts +196 -0
- package/dist/types/g2/Font2D.d.ts +19 -0
- package/dist/types/g2/Material2D.d.ts +24 -0
- package/dist/types/g2/Node2D.d.ts +6 -5
- package/dist/types/g2/Particles2D.d.ts +169 -0
- package/dist/types/g2/Path2D.d.ts +95 -0
- package/dist/types/g2/Physics2D.d.ts +1 -1
- package/dist/types/g2/Scene2D.d.ts +15 -17
- package/dist/types/g2/Sprite.d.ts +12 -1
- package/dist/types/g2/SpriteSheet.d.ts +4 -1
- package/dist/types/g2/Text2D.d.ts +96 -0
- package/dist/types/g2/Tilemap.d.ts +8 -0
- package/dist/types/g2/defineScene2d.d.ts +69 -11
- package/dist/types/gl/Foliage.d.ts +0 -7
- package/dist/types/gl/Lightmap.d.ts +101 -2
- package/dist/types/gl/Material.d.ts +21 -15
- package/dist/types/gl/Model.d.ts +8 -3
- package/dist/types/gl/Particles.d.ts +3 -135
- package/dist/types/gl/Statics.d.ts +56 -0
- package/dist/types/gl/staticsFormat.d.ts +101 -0
- package/dist/types/inject.d.ts +11 -3
- package/dist/types/runtime/env.d.ts +10 -0
- package/dist/types/runtime/envValues.d.ts +8 -0
- package/dist/types/scene/defineScene.d.ts +17 -12
- package/dist/types/server/env.d.ts +14 -0
- package/dist/types/server/inject.d.ts +1 -0
- package/dist/types/ui/fonts.d.ts +4 -0
- package/dist/types/version.d.ts +1 -1
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/src/bridges/2d.d.ts +65 -5
- package/src/bridges/gl.d.ts +34 -15
- package/src/compile/bakeMacro.ts +3 -0
- package/src/compile/compileProject.ts +25 -4
- package/src/compile/envCompile.ts +74 -0
- package/src/compile/fontMacro.ts +11 -6
- package/src/compile/index.ts +6 -0
- package/src/compile/resourceSlots.ts +1 -0
- package/src/compile/serverSplit.ts +47 -4
- package/src/core/env.ts +47 -0
- package/src/core/particlesConfig.ts +422 -0
- package/src/g2/Font2D.ts +87 -0
- package/src/g2/Material2D.ts +64 -0
- package/src/g2/Node2D.ts +10 -6
- package/src/g2/Particles2D.ts +337 -0
- package/src/g2/Path2D.ts +215 -0
- package/src/g2/Physics2D.ts +1 -1
- package/src/g2/Scene2D.ts +16 -22
- package/src/g2/Sprite.ts +23 -4
- package/src/g2/SpriteSheet.ts +9 -5
- package/src/g2/Text2D.ts +178 -0
- package/src/g2/Tilemap.ts +10 -0
- package/src/g2/defineScene2d.ts +108 -21
- package/src/g2/groups2d.ts +2 -2
- package/src/gl/Foliage.ts +1 -16
- package/src/gl/Lightmap.ts +79 -26
- package/src/gl/Material.ts +51 -21
- package/src/gl/Model.ts +19 -5
- package/src/gl/Particles.ts +22 -361
- package/src/gl/Statics.ts +301 -0
- package/src/gl/nav/NavMesh.ts +4 -1
- package/src/gl/staticsFormat.ts +240 -0
- package/src/inject.ts +18 -3
- package/src/runtime/env.ts +13 -0
- package/src/runtime/envValues.ts +8 -0
- package/src/runtime/fetch.ts +18 -0
- package/src/scene/defineScene.ts +107 -27
- package/src/server/env.ts +25 -0
- package/src/server/host.ts +6 -0
- package/src/server/inject.ts +2 -1
- package/src/ui/fonts.ts +46 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
// The particle config vocabulary — the ONE language of the 3D `Particles` / `Trail` nodes
|
|
2
|
+
// (gl/Particles.ts) and the 2D `Particles2D` node (g2/Particles2D.ts). Every engine runs the same
|
|
3
|
+
// simulation (creator-particles), so every node speaks it the same way: this module holds the record
|
|
4
|
+
// tags, the curve builders, the engine-neutral option types and the record writers; the nodes add only
|
|
5
|
+
// what is theirs (a 3D material / mesh / light, a 2D sheet texture and Material2D blend).
|
|
6
|
+
//
|
|
7
|
+
// The format: all emitter/curve config crosses the bridge as ONE Float32Array of
|
|
8
|
+
// [tag, payloadLen, ...payload] records — `_creator.setParticleSystemConfig` in 3D,
|
|
9
|
+
// `_creator2d.particlesConfig` in 2D. A constructor batches every option into a single call, a live
|
|
10
|
+
// setter sends a one-record buffer; unknown tags are skipped by length natively. The tag values mirror
|
|
11
|
+
// CPART_TAG_* in engines/particles/include/creator-particles/creator-particles.h (guarded by
|
|
12
|
+
// tests/gl/particles-tags.test.ts).
|
|
13
|
+
//
|
|
14
|
+
// Curves over a particle's lifetime are built with curve()/colorCurve() chains — see the builders
|
|
15
|
+
// below. A builder's whole state is the flat `_data` array (the exact record payload), so the
|
|
16
|
+
// chisel compiler can constant-fold literal chains into the buffer at compile time.
|
|
17
|
+
|
|
18
|
+
import { Color, type ColorInput } from "./color"
|
|
19
|
+
import { Vec3, cx, cy, cz, type Vec3Like } from "../math/vec"
|
|
20
|
+
|
|
21
|
+
export type Range<T> = T | { min: T, max: T }
|
|
22
|
+
|
|
23
|
+
// --- config record tags (keep in sync with CPART_TAG_* in creator-particles.h) ------------------
|
|
24
|
+
|
|
25
|
+
export const TAG_SHAPE = 1
|
|
26
|
+
export const TAG_LIFETIME = 2
|
|
27
|
+
export const TAG_RATE = 3
|
|
28
|
+
export const TAG_VELOCITY = 4
|
|
29
|
+
export const TAG_VELOCITY_RND = 5
|
|
30
|
+
export const TAG_ACCELERATION = 6
|
|
31
|
+
export const TAG_DRAG = 7
|
|
32
|
+
export const TAG_NOISE = 8
|
|
33
|
+
export const TAG_SEED = 9
|
|
34
|
+
export const TAG_PARAM_CURVE = 10
|
|
35
|
+
export const TAG_COLOR_CURVE = 11
|
|
36
|
+
export const TAG_SPACE = 12
|
|
37
|
+
export const TAG_INHERIT_VELOCITY = 13
|
|
38
|
+
export const TAG_RATE_DISTANCE = 14
|
|
39
|
+
export const TAG_RENDER_MODE = 15
|
|
40
|
+
export const TAG_MESH_GEOMETRY = 16
|
|
41
|
+
export const TAG_ANGULAR_VELOCITY = 17
|
|
42
|
+
export const TAG_GROUND = 18
|
|
43
|
+
export const TAG_VELOCITY_LIFE = 19
|
|
44
|
+
export const TAG_ORDER = 20
|
|
45
|
+
export const TAG_SMOOTH_PATH = 21
|
|
46
|
+
export const TAG_RIBBON_ORIENT = 22
|
|
47
|
+
export const TAG_LIT = 23
|
|
48
|
+
/** 0 / 1 — an XY-plane system; the 2D engine sets it itself at creation (CPART_TAG_PLANAR), the SDK never writes it. */
|
|
49
|
+
export const TAG_PLANAR = 24
|
|
50
|
+
|
|
51
|
+
/** RENDER_MODE payloads (CPART_MODE_* in creator-particles.h). */
|
|
52
|
+
export const MODE_POINTS = 0
|
|
53
|
+
export const MODE_QUADS = 1
|
|
54
|
+
export const MODE_RIBBON = 2
|
|
55
|
+
export const MODE_MESH = 3
|
|
56
|
+
|
|
57
|
+
/** The PARAM_CURVE slots the named params alias (custom[0..3]). */
|
|
58
|
+
export const SLOT_SIZE = 0
|
|
59
|
+
export const SLOT_ROTATION = 1
|
|
60
|
+
export const SLOT_OPACITY = 2
|
|
61
|
+
export const SLOT_FRAME = 3
|
|
62
|
+
|
|
63
|
+
/** @internal — exported for the tag-sync test only. */
|
|
64
|
+
export const _particleTags = {
|
|
65
|
+
SHAPE: TAG_SHAPE, LIFETIME: TAG_LIFETIME, RATE: TAG_RATE, VELOCITY: TAG_VELOCITY,
|
|
66
|
+
VELOCITY_RND: TAG_VELOCITY_RND, ACCELERATION: TAG_ACCELERATION, DRAG: TAG_DRAG,
|
|
67
|
+
NOISE: TAG_NOISE, SEED: TAG_SEED, PARAM_CURVE: TAG_PARAM_CURVE, COLOR_CURVE: TAG_COLOR_CURVE,
|
|
68
|
+
SPACE: TAG_SPACE, INHERIT_VELOCITY: TAG_INHERIT_VELOCITY, RATE_DISTANCE: TAG_RATE_DISTANCE,
|
|
69
|
+
RENDER_MODE: TAG_RENDER_MODE, MESH_GEOMETRY: TAG_MESH_GEOMETRY,
|
|
70
|
+
ANGULAR_VELOCITY: TAG_ANGULAR_VELOCITY, GROUND: TAG_GROUND, VELOCITY_LIFE: TAG_VELOCITY_LIFE,
|
|
71
|
+
ORDER: TAG_ORDER, SMOOTH_PATH: TAG_SMOOTH_PATH, RIBBON_ORIENT: TAG_RIBBON_ORIENT,
|
|
72
|
+
LIT: TAG_LIT,
|
|
73
|
+
PLANAR: TAG_PLANAR,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// --- curve builders -----------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
// A value curve reads as a journey over the particle's life:
|
|
79
|
+
// curve(0.3).from(0).via(0.15, 1).via(0.6, 1).to(0) // fade in, hold, fade out — or:
|
|
80
|
+
// curve(0.3).fade(0.15, 0.4)
|
|
81
|
+
// The curve multiplies the base value (or adds to it with mode 'add'); the base and any stop can be
|
|
82
|
+
// a { min, max } range, randomized per particle. With no `.from` the curve starts at the identity
|
|
83
|
+
// (1 for multiply, 0 for add); with no `.to` it holds its last value to end of life.
|
|
84
|
+
//
|
|
85
|
+
// chisel constant-folds a literal chain into its `_data` buffer at compile time and validates it
|
|
86
|
+
// (bad stop order, >8 stops, unparsable color → a build diagnostic): chisel-core/src/curve.rs mirrors
|
|
87
|
+
// these method bodies op-for-op, against a verbatim copy of them in chisel's fixtures/curve-sdk/. Keep
|
|
88
|
+
// the two in step — a change here means a change there (chisel's scripts/curve-exact.mjs compares the
|
|
89
|
+
// fused payload against the real builder, buffer for buffer).
|
|
90
|
+
|
|
91
|
+
/** @internal A range as its [lo, hi] pair. */
|
|
92
|
+
export const lohi = (v: Range<number>): [number, number] =>
|
|
93
|
+
typeof v === "number" ? [ v, v ] : [ v.min, v.max ]
|
|
94
|
+
|
|
95
|
+
export class CurveBuilder {
|
|
96
|
+
private _kind: 0 | 1
|
|
97
|
+
private _lo: number
|
|
98
|
+
private _hi: number
|
|
99
|
+
private _stops: number[] = [] // flat (t, lo, hi)
|
|
100
|
+
|
|
101
|
+
constructor(base: Range<number>, mode: "multiply" | "add") {
|
|
102
|
+
this._kind = mode === "add" ? 1 : 0
|
|
103
|
+
;[ this._lo, this._hi ] = lohi(base)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Curve value at birth (t = 0). Must come before any other stop. */
|
|
107
|
+
from(v: Range<number>): this {
|
|
108
|
+
if (this._stops.length > 0) throw new Error("curve: .from() must come first")
|
|
109
|
+
return this.via(0, v)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Curve value at `t` (0..1 of the particle's life). Stops must be added in ascending t order. */
|
|
113
|
+
via(t: number, v: Range<number>): this {
|
|
114
|
+
const [ lo, hi ] = lohi(v)
|
|
115
|
+
this._stops.push(t, lo, hi)
|
|
116
|
+
return this
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Curve value at death (t = 1). */
|
|
120
|
+
to(v: Range<number>): this {
|
|
121
|
+
return this.via(1, v)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The classic opacity envelope: rise over the first `fadeIn`, fall over the last `fadeOut`. */
|
|
125
|
+
fade(fadeIn = 0.15, fadeOut = fadeIn): this {
|
|
126
|
+
const up = Math.min(Math.max(fadeIn, 0), 1)
|
|
127
|
+
const down = Math.min(Math.max(fadeOut, 0), 1 - up)
|
|
128
|
+
if (up > 0) this.via(0, 0).via(up, 1)
|
|
129
|
+
else this.via(0, 1)
|
|
130
|
+
if (down > 0) {
|
|
131
|
+
if (1 - down > up) this.via(1 - down, 1)
|
|
132
|
+
this.via(1, 0)
|
|
133
|
+
}
|
|
134
|
+
return this
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The PARAM_CURVE record payload (sans slot): [kind, baseLo, baseHi, n, (t, lo, hi)×n]. */
|
|
138
|
+
get _data(): Float32Array {
|
|
139
|
+
const stops = this._stops
|
|
140
|
+
const prepend = stops.length > 0 && stops[0] > 0
|
|
141
|
+
const n = stops.length / 3 + (prepend ? 1 : 0)
|
|
142
|
+
const out = new Float32Array(4 + n * 3)
|
|
143
|
+
out[0] = this._kind
|
|
144
|
+
out[1] = this._lo
|
|
145
|
+
out[2] = this._hi
|
|
146
|
+
out[3] = n
|
|
147
|
+
let o = 4
|
|
148
|
+
if (prepend) {
|
|
149
|
+
const identity = this._kind === 1 ? 0 : 1
|
|
150
|
+
out[o + 1] = identity
|
|
151
|
+
out[o + 2] = identity
|
|
152
|
+
o += 3
|
|
153
|
+
}
|
|
154
|
+
out.set(stops, o)
|
|
155
|
+
return out
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export class ColorCurveBuilder {
|
|
160
|
+
private _lo: [number, number, number, number]
|
|
161
|
+
private _hi: [number, number, number, number]
|
|
162
|
+
private _stops: number[] = [] // flat (t, lo rgba, hi rgba)
|
|
163
|
+
|
|
164
|
+
constructor(base: Range<ColorInput>) {
|
|
165
|
+
if (isColorRange(base)) {
|
|
166
|
+
this._lo = Color.toRgba01(base.min)
|
|
167
|
+
this._hi = Color.toRgba01(base.max)
|
|
168
|
+
} else {
|
|
169
|
+
this._lo = Color.toRgba01(base)
|
|
170
|
+
this._hi = this._lo
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Curve color at birth (t = 0). Must come first. The curve multiplies the base color. */
|
|
175
|
+
from(c: Range<ColorInput>): this {
|
|
176
|
+
if (this._stops.length > 0) throw new Error("colorCurve: .from() must come first")
|
|
177
|
+
return this.via(0, c)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Curve color at `t` (0..1 of the particle's life), in ascending t order. */
|
|
181
|
+
via(t: number, c: Range<ColorInput>): this {
|
|
182
|
+
let lo: number[], hi: number[]
|
|
183
|
+
if (isColorRange(c)) {
|
|
184
|
+
lo = Color.toRgba01(c.min)
|
|
185
|
+
hi = Color.toRgba01(c.max)
|
|
186
|
+
} else {
|
|
187
|
+
lo = hi = Color.toRgba01(c)
|
|
188
|
+
}
|
|
189
|
+
this._stops.push(t, ...lo, ...hi)
|
|
190
|
+
return this
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Curve color at death (t = 1). */
|
|
194
|
+
to(c: Range<ColorInput>): this {
|
|
195
|
+
return this.via(1, c)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** The COLOR_CURVE record payload: [baseLo rgba, baseHi rgba, n, (t, lo rgba, hi rgba)×n]. */
|
|
199
|
+
get _data(): Float32Array {
|
|
200
|
+
const stops = this._stops
|
|
201
|
+
const prepend = stops.length > 0 && stops[0] > 0
|
|
202
|
+
const n = stops.length / 9 + (prepend ? 1 : 0)
|
|
203
|
+
const out = new Float32Array(9 + n * 9)
|
|
204
|
+
out.set(this._lo, 0)
|
|
205
|
+
out.set(this._hi, 4)
|
|
206
|
+
out[8] = n
|
|
207
|
+
let o = 9
|
|
208
|
+
if (prepend) {
|
|
209
|
+
for (let i = 1; i < 9; i++) out[o + i] = 1 // identity multiplier: white, t = 0
|
|
210
|
+
o += 9
|
|
211
|
+
}
|
|
212
|
+
out.set(stops, o)
|
|
213
|
+
return out
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** A value curve over each particle's lifetime. Default mode `multiply` (the curve scales the
|
|
218
|
+
* base); `add` offsets it instead. */
|
|
219
|
+
export const curve = (base: Range<number> = 1, mode: "multiply" | "add" = "multiply"): CurveBuilder =>
|
|
220
|
+
new CurveBuilder(base, mode)
|
|
221
|
+
|
|
222
|
+
/** A color curve over each particle's lifetime — always multiplies the base color. */
|
|
223
|
+
export const colorCurve = (base: Range<ColorInput> = "#ffffff"): ColorCurveBuilder =>
|
|
224
|
+
new ColorCurveBuilder(base)
|
|
225
|
+
|
|
226
|
+
// --- option types -------------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
export type ParticleValue = Range<number> | CurveBuilder | { _data: Float32Array }
|
|
229
|
+
export type ParticleColor = ColorInput | { min: ColorInput, max: ColorInput } | ColorCurveBuilder | { _data: Float32Array }
|
|
230
|
+
|
|
231
|
+
/** A start velocity by direction + speed (the object form of `startVelocity`). */
|
|
232
|
+
export type VelocityValue = {
|
|
233
|
+
speed?: Range<number>
|
|
234
|
+
/** Cone half-angle in radians — jitter the launch direction uniformly within it. */
|
|
235
|
+
spread?: number
|
|
236
|
+
/** Asymmetric jitter: two angles (u, v — the x/y components) in radians. */
|
|
237
|
+
randomizeAngle?: { min: Vec3Like, max: Vec3Like }
|
|
238
|
+
} & ({ from: Vec3Like } | { to: Vec3Like } | { dir: Vec3Like } | {})
|
|
239
|
+
|
|
240
|
+
/** Where particles are born, in emitter space. Sphere / hemisphere / cone have an axis of +Y (the
|
|
241
|
+
* hemisphere is the +Y half, the cone opens along +Y from its base disc at `center`) — aim the
|
|
242
|
+
* node with `lookAt(target, 'y')`. `thickness` 0 emits from the surface only, 1 (default) from the
|
|
243
|
+
* whole volume. A sphere or cone also sets the LAUNCH DIRECTION (outward / from the cone's apex)
|
|
244
|
+
* when `startVelocity` gives a speed but no direction, like Unity's shapes. */
|
|
245
|
+
export type ParticleShape =
|
|
246
|
+
| { type: "point", v: Vec3Like }
|
|
247
|
+
| { type: "box", min: Vec3Like, max: Vec3Like }
|
|
248
|
+
| { type: "circle", center: Vec3Like, radius: number }
|
|
249
|
+
| { type: "sphere", center?: Vec3Like, radius: number, thickness?: number }
|
|
250
|
+
| { type: "hemisphere", center?: Vec3Like, radius: number, thickness?: number }
|
|
251
|
+
| { type: "cone", center?: Vec3Like, radius: number, /** half-angle, radians */ angle?: number, /** emit this far up the axis too */ length?: number, thickness?: number }
|
|
252
|
+
|
|
253
|
+
/** Extra velocity over each particle's life — evaluated from the curves every frame and ADDED as a
|
|
254
|
+
* displacement (not accumulated like `acceleration`): `radial` pushes outward along the particle's
|
|
255
|
+
* birth direction (a burst that decays: `curve(4).to(0)`), `x`/`y`/`z` drift in the simulation
|
|
256
|
+
* space (rising smoke: `y: curve(2).from(0).to(1)`). Unity's velocityOverLifetime. */
|
|
257
|
+
export type VelocityOverLife = { radial?: ParticleValue, x?: ParticleValue, y?: ParticleValue, z?: ParticleValue }
|
|
258
|
+
|
|
259
|
+
export type ParticleNoise = {
|
|
260
|
+
strength?: number, frequency?: number, speed?: number,
|
|
261
|
+
/** How much of the displacement a particle earns with AGE (default 0 = all of it from birth).
|
|
262
|
+
* The field is sampled by position, so at 0 every particle born at the same spot is pushed the
|
|
263
|
+
* same way and a plume's root wanders off its emitter by up to `strength` metres; at 1 a particle
|
|
264
|
+
* is born exactly where the emitter put it and drifts into the field over its life — what you
|
|
265
|
+
* want whenever the source is visible (an exhaust pipe, a contact patch, a muzzle). */
|
|
266
|
+
ramp?: number,
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Ground plane for bouncing particles (all render modes). `height` is in the simulation space:
|
|
270
|
+
* world y with `space: 'world'`, emitter-local y otherwise. */
|
|
271
|
+
export type ParticleGround = {
|
|
272
|
+
/** Plane height (default 0). */
|
|
273
|
+
height?: number
|
|
274
|
+
/** Contact radius — particles stop `radius` above the plane (a mesh's half-extent; default 0). */
|
|
275
|
+
radius?: number
|
|
276
|
+
/** Fraction of the vertical speed kept on impact, 0..1 (default 0.3). */
|
|
277
|
+
bounce?: number
|
|
278
|
+
/** Tangential damping while in contact, per second — like `drag` (default 8). */
|
|
279
|
+
friction?: number
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** The mesh a mesh-mode system draws per particle — structurally a `Geometry`. */
|
|
283
|
+
export type ParticleMeshData = {
|
|
284
|
+
readonly vertices: ArrayLike<number>
|
|
285
|
+
readonly normals: ArrayLike<number>
|
|
286
|
+
readonly uv: ArrayLike<number>
|
|
287
|
+
readonly indices: ArrayLike<number>
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// --- record writers -----------------------------------------------------------------------------
|
|
291
|
+
// Each appends its record(s) to `out`; the node turns the whole array into one Float32Array.
|
|
292
|
+
|
|
293
|
+
const isColorRange = (v: unknown): v is { min: ColorInput, max: ColorInput } =>
|
|
294
|
+
typeof v === "object" && v !== null && !Array.isArray(v) && "min" in v
|
|
295
|
+
|
|
296
|
+
const hasData = (v: unknown): v is { _data: Float32Array } =>
|
|
297
|
+
typeof v === "object" && v !== null && "_data" in v
|
|
298
|
+
|
|
299
|
+
/** A PARAM_CURVE record: `slot` 0 size / 1 rotation / 2 opacity / 3 frame. */
|
|
300
|
+
export const pushParam = (out: number[], slot: number, value: ParticleValue): void => {
|
|
301
|
+
if (typeof value === "number") {
|
|
302
|
+
out.push(TAG_PARAM_CURVE, 5, slot, 0, value, value, 0)
|
|
303
|
+
} else if (hasData(value)) {
|
|
304
|
+
const d = value._data
|
|
305
|
+
out.push(TAG_PARAM_CURVE, d.length + 1, slot)
|
|
306
|
+
for (let i = 0; i < d.length; i++) out.push(d[i])
|
|
307
|
+
} else {
|
|
308
|
+
out.push(TAG_PARAM_CURVE, 5, slot, 0, value.min, value.max, 0)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export const pushColor = (out: number[], value: ParticleColor): void => {
|
|
313
|
+
let d: Float32Array
|
|
314
|
+
if (hasData(value)) {
|
|
315
|
+
d = value._data
|
|
316
|
+
} else if (isColorRange(value)) {
|
|
317
|
+
d = new ColorCurveBuilder(value)._data
|
|
318
|
+
} else {
|
|
319
|
+
d = new ColorCurveBuilder(value as ColorInput)._data
|
|
320
|
+
}
|
|
321
|
+
out.push(TAG_COLOR_CURVE, d.length)
|
|
322
|
+
for (let i = 0; i < d.length; i++) out.push(d[i])
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export const pushMesh = (out: number[], g: ParticleMeshData): void => {
|
|
326
|
+
const nv = g.vertices.length / 3
|
|
327
|
+
const ni = g.indices.length
|
|
328
|
+
out.push(TAG_MESH_GEOMETRY, 2 + nv * 8 + ni, nv, ni)
|
|
329
|
+
for (let i = 0; i < nv * 3; i++) out.push(g.vertices[i])
|
|
330
|
+
for (let i = 0; i < nv * 3; i++) out.push(g.normals[i])
|
|
331
|
+
for (let i = 0; i < nv * 2; i++) out.push(g.uv[i])
|
|
332
|
+
for (let i = 0; i < ni; i++) out.push(g.indices[i])
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export const pushAngularVelocity = (out: number[], v: Vec3Like | { min: Vec3Like, max: Vec3Like }): void => {
|
|
336
|
+
const r = v as { min: Vec3Like, max: Vec3Like }
|
|
337
|
+
if (typeof r === "object" && r !== null && !Array.isArray(r) && "min" in r && !(v instanceof Float32Array) && !(v instanceof Vec3)) {
|
|
338
|
+
out.push(TAG_ANGULAR_VELOCITY, 6, cx(r.min), cy(r.min), cz(r.min), cx(r.max), cy(r.max), cz(r.max))
|
|
339
|
+
} else {
|
|
340
|
+
const a = v as Vec3Like
|
|
341
|
+
out.push(TAG_ANGULAR_VELOCITY, 6, cx(a), cy(a), cz(a), cx(a), cy(a), cz(a))
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export const pushGround = (out: number[], g: ParticleGround | null): void => {
|
|
346
|
+
if (g === null) out.push(TAG_GROUND, 5, 0, 0, 0, 0, 0)
|
|
347
|
+
else out.push(TAG_GROUND, 5, 1, g.height ?? 0, g.radius ?? 0, g.bounce ?? 0.3, g.friction ?? 8)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export const pushShape = (out: number[], shape: ParticleShape): void => {
|
|
351
|
+
if (shape.type === "point") out.push(TAG_SHAPE, 4, 0, cx(shape.v), cy(shape.v), cz(shape.v))
|
|
352
|
+
else if (shape.type === "box") out.push(TAG_SHAPE, 7, 1, cx(shape.min), cy(shape.min), cz(shape.min), cx(shape.max), cy(shape.max), cz(shape.max))
|
|
353
|
+
else if (shape.type === "circle") out.push(TAG_SHAPE, 5, 2, cx(shape.center), cy(shape.center), cz(shape.center), shape.radius)
|
|
354
|
+
else {
|
|
355
|
+
const c = shape.center ?? [ 0, 0, 0 ]
|
|
356
|
+
const thickness = shape.thickness ?? 1
|
|
357
|
+
if (shape.type === "cone") out.push(TAG_SHAPE, 8, 5, cx(c), cy(c), cz(c), shape.radius, shape.angle ?? 0, shape.length ?? 0, thickness)
|
|
358
|
+
else out.push(TAG_SHAPE, 6, shape.type === "sphere" ? 3 : 4, cx(c), cy(c), cz(c), shape.radius, thickness)
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export const pushVelocityLife = (out: number[], v: VelocityOverLife): void => {
|
|
363
|
+
const axes: Array<[number, ParticleValue | undefined]> = [ [ 0, v.radial ], [ 1, v.x ], [ 2, v.y ], [ 3, v.z ] ]
|
|
364
|
+
for (const [ axis, value ] of axes) {
|
|
365
|
+
if (value === undefined) continue
|
|
366
|
+
if (typeof value === "number") out.push(TAG_VELOCITY_LIFE, 5, axis, 0, value, value, 0)
|
|
367
|
+
else if (hasData(value)) {
|
|
368
|
+
const d = value._data
|
|
369
|
+
out.push(TAG_VELOCITY_LIFE, d.length + 1, axis)
|
|
370
|
+
for (let i = 0; i < d.length; i++) out.push(d[i])
|
|
371
|
+
} else out.push(TAG_VELOCITY_LIFE, 5, axis, 0, value.min, value.max, 0)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export const pushVelocity = (out: number[], val: Vec3Like | VelocityValue): void => {
|
|
376
|
+
if (Array.isArray(val) || val instanceof Float32Array || val instanceof Vec3) {
|
|
377
|
+
// a plain vector: launch along it, its length is the speed (native derives it)
|
|
378
|
+
out.push(TAG_VELOCITY, 6, 1, cx(val), cy(val), cz(val), 0, 0)
|
|
379
|
+
out.push(TAG_VELOCITY_RND, 5, 0, 0, 0, 0, 0)
|
|
380
|
+
return
|
|
381
|
+
}
|
|
382
|
+
const v = val as VelocityValue
|
|
383
|
+
let mode = 0
|
|
384
|
+
let p: Vec3Like = [ 0, 0, 0 ]
|
|
385
|
+
if ("dir" in v) { mode = 1; p = v.dir }
|
|
386
|
+
else if ("from" in v) { mode = 2; p = v.from }
|
|
387
|
+
else if ("to" in v) { mode = 3; p = v.to }
|
|
388
|
+
let lo = 0, hi = 0
|
|
389
|
+
if (v.speed !== undefined) {
|
|
390
|
+
if (typeof v.speed === "number") lo = hi = v.speed
|
|
391
|
+
else { lo = v.speed.min; hi = v.speed.max }
|
|
392
|
+
}
|
|
393
|
+
out.push(TAG_VELOCITY, 6, mode, cx(p), cy(p), cz(p), lo, hi)
|
|
394
|
+
if (v.spread !== undefined) {
|
|
395
|
+
out.push(TAG_VELOCITY_RND, 5, 1, -v.spread, -v.spread, v.spread, v.spread)
|
|
396
|
+
} else if (v.randomizeAngle) {
|
|
397
|
+
const a = v.randomizeAngle
|
|
398
|
+
out.push(TAG_VELOCITY_RND, 5, 1, cx(a.min), cy(a.min), cx(a.max), cy(a.max))
|
|
399
|
+
} else {
|
|
400
|
+
out.push(TAG_VELOCITY_RND, 5, 0, 0, 0, 0, 0)
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** A NOISE record; `null` turns the field off. */
|
|
405
|
+
export const pushNoise = (out: number[], n: ParticleNoise | null): void => {
|
|
406
|
+
out.push(TAG_NOISE, 4, n ? n.strength ?? 1 : 0, n ? n.frequency ?? 1 : 0, n ? n.speed ?? 1 : 0, n ? n.ramp ?? 0 : 0)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** A LIFETIME record. */
|
|
410
|
+
export const pushLifetime = (out: number[], val: Range<number>): void => {
|
|
411
|
+
const [ lo, hi ] = lohi(val)
|
|
412
|
+
out.push(TAG_LIFETIME, 2, lo, hi)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** The default flipbook: with a sheet of more than one frame and no explicit `frame`, one full sheet
|
|
416
|
+
* cycle over each particle's lifetime, starting at `startFrame` (`'random'` = desynchronized loops). */
|
|
417
|
+
export const pushDefaultFrames = (out: number[], cols: number, rows: number, startFrame: number | "random" = "random"): void => {
|
|
418
|
+
const frames = cols * rows
|
|
419
|
+
if (frames <= 1) return
|
|
420
|
+
const base = startFrame === "random" ? { min: 0, max: frames - 1 } : startFrame
|
|
421
|
+
pushParam(out, SLOT_FRAME, curve(base, "add").from(0).to(frames))
|
|
422
|
+
}
|
package/src/g2/Font2D.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// A font for 2D text (Text2D): a TTF / OTF file parsed natively by creator-2d, whose glyphs are
|
|
2
|
+
// rasterized on demand as signed distance fields into ONE atlas per font — the same atlas serves every
|
|
3
|
+
// size, so a Font2D is loaded once and used at any fontSize.
|
|
4
|
+
//
|
|
5
|
+
// const inter = await Font2D.load(font("inter")) // a family the font() macro registered
|
|
6
|
+
// const bold = await Font2D.load(font("inter"), { weight: 700 })
|
|
7
|
+
// const brand = await Font2D.load(asset('./fonts/Brand.ttf')) // a file of the project, fetched as is
|
|
8
|
+
//
|
|
9
|
+
// A family name resolves through the faces the `font()` compile macro registered (ui/fonts.ts records
|
|
10
|
+
// each face's TTF URL beside its UI registration — the native parser reads TTF / OTF, never the woff2
|
|
11
|
+
// a browser's UI text may use): the exact weight / style, else the nearest registered weight of that
|
|
12
|
+
// style. A name nobody registered fails with an error listing the registered families. Anything that
|
|
13
|
+
// is not a bare name (it has a "/", "." or ":") is a URL / asset path and is fetched as is.
|
|
14
|
+
//
|
|
15
|
+
// Loads are cached per resolved URL, so every `Font2D.load("Inter")` of a project shares one native
|
|
16
|
+
// font (and one atlas). `destroy()` frees it for every holder.
|
|
17
|
+
|
|
18
|
+
import { fetch, type FetchResponse, type File } from "../runtime/fetch"
|
|
19
|
+
import { _fontUrlOf, _registeredFontFamilies } from "../ui/fonts"
|
|
20
|
+
|
|
21
|
+
export type Font2DOptions = {
|
|
22
|
+
/** For a registered family: the face's weight (default 400; the nearest registered one is used). */
|
|
23
|
+
weight?: number
|
|
24
|
+
/** For a registered family: the italic face (default false). */
|
|
25
|
+
italic?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// A bare family name ("Inter", "Brand Sans") — a URL or asset path always carries one of / . :
|
|
29
|
+
const FAMILY_NAME = /^[^/\\.:]+$/
|
|
30
|
+
|
|
31
|
+
let loaded: Map<string, Promise<Font2D>> | undefined // resolved URL → the (pending) font
|
|
32
|
+
const cache = (): Map<string, Promise<Font2D>> => (loaded ??= new Map())
|
|
33
|
+
|
|
34
|
+
const resolveUrl = (source: string, options: Font2DOptions): string => {
|
|
35
|
+
if (!FAMILY_NAME.test(source)) return source
|
|
36
|
+
const italic = options.italic ?? false
|
|
37
|
+
const url = _fontUrlOf(source, options.weight ?? 400, italic)
|
|
38
|
+
if (url !== undefined) return url
|
|
39
|
+
const families = _registeredFontFamilies()
|
|
40
|
+
if (families.some((f) => f.toLowerCase() === source.toLowerCase())) {
|
|
41
|
+
throw new Error(`Font2D.load("${source}"): the family has no ${italic ? "italic" : "upright"} face registered — add it to the font("…") call (e.g. { italic: true }).`)
|
|
42
|
+
}
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Font2D.load("${source}"): no font family "${source}" is registered — declare it with font("…") (a registry id or './fonts/X.ttf'), ` +
|
|
45
|
+
`or pass a URL / asset('./x.ttf'). Registered families: ${families.length > 0 ? families.join(", ") : "none"}.`,
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class Font2D {
|
|
50
|
+
/** Native font handle. */
|
|
51
|
+
readonly id: number
|
|
52
|
+
private _key?: string
|
|
53
|
+
|
|
54
|
+
/** @internal Use {@link Font2D.load}. */
|
|
55
|
+
constructor(id: number) {
|
|
56
|
+
this.id = id
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** A registered family name (what `font("inter")` compiles to, e.g. "Inter") picks the ttf of
|
|
60
|
+
* (weight ?? 400, italic); anything else is a URL / asset path / response / File fetched as is
|
|
61
|
+
* (`asset('./fonts/Brand.ttf')`). Cached per resolved URL: a family loads once. */
|
|
62
|
+
static load(source: string | FetchResponse | File, options: Font2DOptions = {}): Promise<Font2D> {
|
|
63
|
+
if (typeof source !== "string") {
|
|
64
|
+
return new Promise<Font2D>((resolve, reject) => {
|
|
65
|
+
_creator2d.createFont((source as any)._h.id, (id) => resolve(new Font2D(id)), (e) => reject(e instanceof Error ? e : new Error(`Font2D.load: not a TTF / OTF font (${String(e)})`)))
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
let url: string
|
|
69
|
+
try { url = resolveUrl(source, options) } catch (e) { return Promise.reject(e) }
|
|
70
|
+
const hit = cache().get(url)
|
|
71
|
+
if (hit) return hit
|
|
72
|
+
const pending = fetch(url, { useOnce: true }).then((resp) => {
|
|
73
|
+
if (resp.status >= 400) throw new Error(`Failed to fetch font from ${url}. HTTP ${resp.status}`)
|
|
74
|
+
return Font2D.load(resp)
|
|
75
|
+
}).then((font) => { font._key = url; return font })
|
|
76
|
+
pending.catch(() => { if (cache().get(url) === pending) cache().delete(url) })
|
|
77
|
+
cache().set(url, pending)
|
|
78
|
+
return pending
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Free the native font (and its atlas) — for every holder of this cached font; a later load
|
|
82
|
+
* parses it again. Texts still using it stop drawing. */
|
|
83
|
+
destroy(): void {
|
|
84
|
+
if (this._key !== undefined) { cache().delete(this._key); this._key = undefined }
|
|
85
|
+
_creator2d.destroyFont(this.id)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// A 2D material: how a sprite or tilemap blends into what is under it and how its texture is
|
|
2
|
+
// sampled. That is all a material is in 2D — no shader, no uniforms:
|
|
3
|
+
// - blend 'additive' adds the colour instead of covering: glow, fire, sparks, muzzle flashes, light
|
|
4
|
+
// shafts (black is transparent, overlaps brighten).
|
|
5
|
+
// - wrap 'repeat' tiles the texture when a sprite's UV `frame` reaches past [0,1]: a scrolling or
|
|
6
|
+
// parallax background is one sprite with a growing frame offset.
|
|
7
|
+
// - filter per texture: crisp 'nearest' pixel art beside smooth 'linear' hi-res art in one scene.
|
|
8
|
+
// Unset = the renderer default (the Scene2D `filter`).
|
|
9
|
+
//
|
|
10
|
+
// new Sprite({ texture: flare, material: Material2D.additive() })
|
|
11
|
+
// sky.material = Material2D.sprite({ wrap: 'repeat', filter: 'linear' })
|
|
12
|
+
//
|
|
13
|
+
// The shape mirrors the 3D `Material.lit()` / `Material.unlit()` factories, but a Material2D is DATA,
|
|
14
|
+
// not a native handle: the engine takes three ints per node, so one value can be shared by any number
|
|
15
|
+
// of nodes and is immutable — to change a node's material, assign a new one.
|
|
16
|
+
|
|
17
|
+
export type Blend2D = "alpha" | "additive"
|
|
18
|
+
export type Filter2D = "nearest" | "linear"
|
|
19
|
+
export type Wrap2D = "clamp" | "repeat"
|
|
20
|
+
|
|
21
|
+
export type Material2DOptions = {
|
|
22
|
+
/** `'alpha'` (default) covers what is under it by its alpha; `'additive'` adds its colour. */
|
|
23
|
+
blend?: Blend2D
|
|
24
|
+
/** Texture sampling; unset = the renderer default (the Scene2D `filter`). */
|
|
25
|
+
filter?: Filter2D
|
|
26
|
+
/** `'clamp'` (default) stretches the edge texel past [0,1]; `'repeat'` tiles the texture. */
|
|
27
|
+
wrap?: Wrap2D
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class Material2D {
|
|
31
|
+
readonly blend: Blend2D
|
|
32
|
+
/** Undefined = the renderer default (the Scene2D `filter`). */
|
|
33
|
+
readonly filter?: Filter2D
|
|
34
|
+
readonly wrap: Wrap2D
|
|
35
|
+
|
|
36
|
+
private constructor(options: Material2DOptions) {
|
|
37
|
+
this.blend = options.blend ?? "alpha"
|
|
38
|
+
this.filter = options.filter
|
|
39
|
+
this.wrap = options.wrap ?? "clamp"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** An alpha-blended material — what every sprite and tilemap has by default. */
|
|
43
|
+
static sprite(options: Material2DOptions = {}): Material2D {
|
|
44
|
+
return new Material2D(options)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** An additive material: glow, fire, sparks, flashes. */
|
|
48
|
+
static additive(options: Omit<Material2DOptions, "blend"> = {}): Material2D {
|
|
49
|
+
return new Material2D({ ...options, blend: "additive" })
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The default material (alpha blend, renderer-default filter, clamp). */
|
|
53
|
+
static readonly default: Material2D = Material2D.sprite()
|
|
54
|
+
|
|
55
|
+
/** @internal Push this material to an entity — the engine's three ints. */
|
|
56
|
+
_apply(entityId: number): void {
|
|
57
|
+
_creator2d.setMaterial(
|
|
58
|
+
entityId,
|
|
59
|
+
this.blend === "additive" ? 1 : 0,
|
|
60
|
+
this.filter === "nearest" ? 1 : this.filter === "linear" ? 2 : 0,
|
|
61
|
+
this.wrap === "repeat" ? 1 : 0,
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/g2/Node2D.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Base 2D scene node — an empty transform. Sprite and Tilemap extend it, so every node operation
|
|
2
|
-
// (position,
|
|
2
|
+
// (position, draw order, visibility, events) works on them. Native (creator-2d) owns the authoritative
|
|
3
3
|
// transform. JS caches it for free reads (JS is normally the sole writer); the exception is a node
|
|
4
4
|
// driven by a dynamic/kinematic Physics2D body — there the native physics step also writes the
|
|
5
5
|
// transform, so such nodes are flagged `_physicsDriven` and their transform getters sync the cache
|
|
@@ -74,7 +74,7 @@ export class Node2D extends AspectHost<Node2DEvents> implements CompWriter {
|
|
|
74
74
|
protected _rotation = 0
|
|
75
75
|
protected _sx = 1
|
|
76
76
|
protected _sy = 1
|
|
77
|
-
protected
|
|
77
|
+
protected _renderPriority = 4
|
|
78
78
|
protected _z = 0
|
|
79
79
|
protected _visible = true
|
|
80
80
|
|
|
@@ -183,11 +183,15 @@ export class Node2D extends AspectHost<Node2DEvents> implements CompWriter {
|
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
/**
|
|
187
|
-
|
|
188
|
-
|
|
186
|
+
/** Coarse draw order: 0 draws first, 7 last, 4 is the default — the 3D `Mesh.renderPriority`.
|
|
187
|
+
* Within one priority `z` orders, then (with `scene.ySort` on) world Y. Rounded and clamped to 0..7. */
|
|
188
|
+
get renderPriority(): number { return this._renderPriority }
|
|
189
|
+
set renderPriority(v: number) {
|
|
190
|
+
this._renderPriority = Math.max(0, Math.min(7, Math.round(v)))
|
|
191
|
+
_creator2d.setRenderPriority(this.id, this._renderPriority)
|
|
192
|
+
}
|
|
189
193
|
|
|
190
|
-
/**
|
|
194
|
+
/** Draw order within a renderPriority (higher = on top); with `scene.ySort` on, equal z sorts by world Y. */
|
|
191
195
|
get z(): number { return this._z }
|
|
192
196
|
set z(v: number) { this._z = v; _creator2d.setZ(this.id, v) }
|
|
193
197
|
|