@chromatic-coherence/generative-engine 1.7.1 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/collision.d.ts +19 -0
- package/dist/collision.js +58 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/world.d.ts +59 -0
- package/dist/world.js +109 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,6 +48,8 @@ w.start();
|
|
|
48
48
|
| **Real water** | `w.water` — the scene renders a second time through the reflected camera (flocks included) under a wave-displaced fresnel surface. Carve a basin, get a lake. |
|
|
49
49
|
| **Skeletal figures** | `humanoid()` / `quadruped()` / your own rig + `w.figure()` — bones grown from code, bent in the vertex stage, with `walkPose` / `trotPose` procedural cycles. |
|
|
50
50
|
| **Objectives** | `attachObjectives(w)` — give a scene a goal and it drives itself there; a live movement meter tells you what is moving and when it has settled. |
|
|
51
|
+
| **Collision control** | `mesh(geo, { collider: true })` registers the mesh's bounding sphere; `addCollider(p, r, group?)` places invisible ones. Query with `collide(p, r)` (push-out normal + overlap depth), `collideAll`, and `rayHit(o, dir, maxDist?, r?)` — pass `r` to sweep a moving sphere. Colliders ride their group's `setTransform`. The pure kit (`sphereHit` · `raySphere` · `boundingSphere`) is exported and node-tested. |
|
|
52
|
+
| **Poster frames** | `w.poster(t?)` renders the window's first frame as a still — HTML video's `poster`, for worlds. `capturePoster(opts, build, width, height, t?)` renders it off-screen and returns a PNG data URL with the GL context released: paint the poster at page load, build the live world only when the window is watched. |
|
|
51
53
|
| **Cinema + flight** | Depth of field, camera motion blur, filmic grade — and `controls: "fly"`: drag to look, scroll to fly. |
|
|
52
54
|
| **The carried light model** | Everything the chart edition learned rides along: GGX + Fresnel specular, procedural micro-relief, the perfusion term (`blood`, `bloodPulse`), pore noise, subsurface rim, coloured hemispheric ambient, fog — now resolving toward a real background colour the engine owns. |
|
|
53
55
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type V3 } from "./math.js";
|
|
2
|
+
import type { Mesh } from "./geometry.js";
|
|
3
|
+
export type Sphere = {
|
|
4
|
+
p: V3;
|
|
5
|
+
r: number;
|
|
6
|
+
};
|
|
7
|
+
/** Overlap between two spheres — null when apart; otherwise the contact:
|
|
8
|
+
* `n` points from b to a (the push-out direction for a), `depth` the overlap. */
|
|
9
|
+
export declare function sphereHit(a: Sphere, b: Sphere): {
|
|
10
|
+
n: V3;
|
|
11
|
+
depth: number;
|
|
12
|
+
} | null;
|
|
13
|
+
/** Ray vs sphere: nearest hit distance t ≥ 0 along a UNIT dir, or null.
|
|
14
|
+
* Pass `r` to sweep a moving sphere of that radius instead of a point;
|
|
15
|
+
* a ray starting inside reports the exit hit. */
|
|
16
|
+
export declare function raySphere(o: V3, dir: V3, s: Sphere, r?: number, maxDist?: number): number | null;
|
|
17
|
+
/** The bounding sphere of a mesh — vertex centroid + max distance. Not the
|
|
18
|
+
* minimal sphere, but stable and cheap; grown once at registration. */
|
|
19
|
+
export declare function boundingSphere(m: Mesh): Sphere;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// generative-engine — COLLISION CONTROL, the pure kit: bounding spheres grown
|
|
2
|
+
// from geometry, sphere/sphere overlap, and ray / swept-sphere casts. The World
|
|
3
|
+
// rides on top (addCollider / collide / rayHit) applying group transforms;
|
|
4
|
+
// this file stays DOM-free and node-testable, like the rest of the math kit.
|
|
5
|
+
import { v3, sub, dot, len, scale } from "./math.js";
|
|
6
|
+
/** Overlap between two spheres — null when apart; otherwise the contact:
|
|
7
|
+
* `n` points from b to a (the push-out direction for a), `depth` the overlap. */
|
|
8
|
+
export function sphereHit(a, b) {
|
|
9
|
+
const d = sub(a.p, b.p);
|
|
10
|
+
const dist = len(d);
|
|
11
|
+
const depth = a.r + b.r - dist;
|
|
12
|
+
if (depth <= 0)
|
|
13
|
+
return null;
|
|
14
|
+
return { n: dist > 1e-9 ? scale(d, 1 / dist) : v3(0, 1, 0), depth };
|
|
15
|
+
}
|
|
16
|
+
/** Ray vs sphere: nearest hit distance t ≥ 0 along a UNIT dir, or null.
|
|
17
|
+
* Pass `r` to sweep a moving sphere of that radius instead of a point;
|
|
18
|
+
* a ray starting inside reports the exit hit. */
|
|
19
|
+
export function raySphere(o, dir, s, r = 0, maxDist = Infinity) {
|
|
20
|
+
const R = s.r + r;
|
|
21
|
+
const oc = sub(o, s.p);
|
|
22
|
+
const b = dot(oc, dir);
|
|
23
|
+
const c = dot(oc, oc) - R * R;
|
|
24
|
+
const disc = b * b - c; // unit dir → a = 1
|
|
25
|
+
if (disc < 0)
|
|
26
|
+
return null;
|
|
27
|
+
const sq = Math.sqrt(disc);
|
|
28
|
+
let t = -b - sq;
|
|
29
|
+
if (t < 0)
|
|
30
|
+
t = -b + sq;
|
|
31
|
+
if (t < 0 || t > maxDist)
|
|
32
|
+
return null;
|
|
33
|
+
return t;
|
|
34
|
+
}
|
|
35
|
+
/** The bounding sphere of a mesh — vertex centroid + max distance. Not the
|
|
36
|
+
* minimal sphere, but stable and cheap; grown once at registration. */
|
|
37
|
+
export function boundingSphere(m) {
|
|
38
|
+
const n = m.pos.length / 3;
|
|
39
|
+
if (n === 0)
|
|
40
|
+
return { p: v3(), r: 0 };
|
|
41
|
+
let cx = 0, cy = 0, cz = 0;
|
|
42
|
+
for (let i = 0; i < m.pos.length; i += 3) {
|
|
43
|
+
cx += m.pos[i];
|
|
44
|
+
cy += m.pos[i + 1];
|
|
45
|
+
cz += m.pos[i + 2];
|
|
46
|
+
}
|
|
47
|
+
cx /= n;
|
|
48
|
+
cy /= n;
|
|
49
|
+
cz /= n;
|
|
50
|
+
let r2 = 0;
|
|
51
|
+
for (let i = 0; i < m.pos.length; i += 3) {
|
|
52
|
+
const dx = m.pos[i] - cx, dy = m.pos[i + 1] - cy, dz = m.pos[i + 2] - cz;
|
|
53
|
+
const d2 = dx * dx + dy * dy + dz * dz;
|
|
54
|
+
if (d2 > r2)
|
|
55
|
+
r2 = d2;
|
|
56
|
+
}
|
|
57
|
+
return { p: v3(cx, cy, cz), r: Math.sqrt(r2) };
|
|
58
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/world.d.ts
CHANGED
|
@@ -25,6 +25,29 @@ export type FaceOpts = {
|
|
|
25
25
|
gloss?: number;
|
|
26
26
|
/** per-face perfusion 0..1: blood near the surface (ruddiness, subsurface bleed, pulse) */
|
|
27
27
|
blood?: number;
|
|
28
|
+
/** COLLISION CONTROL: register this mesh's bounding sphere as a collider.
|
|
29
|
+
* Attached to the mesh's group, so setTransform moves it with the geometry.
|
|
30
|
+
* (Static meshes only — instanced crowds are not auto-registered.) */
|
|
31
|
+
collider?: boolean;
|
|
32
|
+
};
|
|
33
|
+
/** COLLISION CONTROL — a registered sphere collider (world space at rest;
|
|
34
|
+
* its group's transform is applied at query time). */
|
|
35
|
+
export type Collider = {
|
|
36
|
+
id: number;
|
|
37
|
+
p: V3;
|
|
38
|
+
r: number;
|
|
39
|
+
gi: number;
|
|
40
|
+
};
|
|
41
|
+
export type CollisionHit = {
|
|
42
|
+
collider: Collider;
|
|
43
|
+
p: V3;
|
|
44
|
+
n: V3;
|
|
45
|
+
depth: number;
|
|
46
|
+
};
|
|
47
|
+
export type RayHitResult = {
|
|
48
|
+
collider: Collider;
|
|
49
|
+
t: number;
|
|
50
|
+
p: V3;
|
|
28
51
|
};
|
|
29
52
|
export type RibbonOpts = {
|
|
30
53
|
hue: number;
|
|
@@ -92,6 +115,9 @@ export declare class World {
|
|
|
92
115
|
private keys;
|
|
93
116
|
/** W13 fly speed, units/second (shift = 3x). */
|
|
94
117
|
flySpeed: number;
|
|
118
|
+
/** Fog reference distance — by default fog tracks the orbit distance, which is wrong
|
|
119
|
+
* for fly mode (a close camera would fog out the world). Set to see that far. */
|
|
120
|
+
fogDist: number | null;
|
|
95
121
|
private pFace;
|
|
96
122
|
private pLine;
|
|
97
123
|
private pPt;
|
|
@@ -245,6 +271,23 @@ export declare class World {
|
|
|
245
271
|
/** Set a named group's model transform — a mat4 or a {translate, rotateY, scale...} spec.
|
|
246
272
|
* Geometry in the group moves rigidly with NO CPU re-tessellation. */
|
|
247
273
|
setTransform(name: string, m: M4 | TransformSpec): void;
|
|
274
|
+
private colliders;
|
|
275
|
+
private colliderSeq;
|
|
276
|
+
/** Register an invisible sphere collider. Attach it to a named group and it
|
|
277
|
+
* rides that group's setTransform exactly like the geometry. Returns an id. */
|
|
278
|
+
addCollider(p: V3, r: number, group?: string): number;
|
|
279
|
+
removeCollider(id: number): void;
|
|
280
|
+
/** A collider as it stands RIGHT NOW — group transform applied to its centre,
|
|
281
|
+
* the matrix's largest axis scale applied to its radius. */
|
|
282
|
+
private colliderNow;
|
|
283
|
+
/** Deepest overlap between a probe sphere and the colliders, or null.
|
|
284
|
+
* `n` is the push-out direction for the probe; `p` the collider's centre. */
|
|
285
|
+
collide(p: V3, r?: number): CollisionHit | null;
|
|
286
|
+
/** Every collider the probe sphere currently overlaps. */
|
|
287
|
+
collideAll(p: V3, r?: number): CollisionHit[];
|
|
288
|
+
/** Nearest ray hit against the colliders (dir need not be unit). Pass `r`
|
|
289
|
+
* to sweep a moving sphere of that radius instead of a point. */
|
|
290
|
+
rayHit(o: V3, dir: V3, maxDist?: number, r?: number): RayHitResult | null;
|
|
248
291
|
W: number;
|
|
249
292
|
H: number;
|
|
250
293
|
private mx;
|
|
@@ -356,6 +399,12 @@ export declare class World {
|
|
|
356
399
|
resume(): void;
|
|
357
400
|
/** Render one deterministic frame at time t (no loop) — stills, tests, screenshots. */
|
|
358
401
|
settle(t: number): void;
|
|
402
|
+
/** The POSTER FRAME — the still a window shows before its animation starts
|
|
403
|
+
* (HTML video's `poster`, for worlds). Renders the recipe once at `t`
|
|
404
|
+
* (default 0 — the scene's own first frame, its initial background) without
|
|
405
|
+
* starting the loop; a later start()/resume() takes over live. Two passes:
|
|
406
|
+
* the first primes sizing and buffers, the second is the frame that stays. */
|
|
407
|
+
poster(t?: number): void;
|
|
359
408
|
/** Stop the world. `releaseContext: true` also LOSES the GL context immediately —
|
|
360
409
|
* browsers cap live contexts per page and reclaim them only at GC, so many-canvas
|
|
361
410
|
* pages must release on teardown. A lost context is permanently dead on its canvas:
|
|
@@ -363,3 +412,13 @@ export declare class World {
|
|
|
363
412
|
dispose(releaseContext?: boolean): void;
|
|
364
413
|
}
|
|
365
414
|
export declare function createWorld(o: WorldOpts): World;
|
|
415
|
+
/** Render a recipe's poster frame OFF-SCREEN and return it as a PNG data URL —
|
|
416
|
+
* the placeholder-image pattern: paint it into the window (an <img> beneath
|
|
417
|
+
* the live canvas) at page load, and build the live world only when the
|
|
418
|
+
* window scrolls into view. The scratch world renders on a hidden staged
|
|
419
|
+
* canvas (sizing reads clientWidth, so it must be in the DOM) and its GL
|
|
420
|
+
* context is released before returning — a page can poster every window it
|
|
421
|
+
* owns without spending its live-context budget. Overlay labels (drawLabel)
|
|
422
|
+
* are not part of a poster. Returns null when WebGL is unavailable — callers
|
|
423
|
+
* keep their CSS fallback. */
|
|
424
|
+
export declare function capturePoster(o: Omit<WorldOpts, "canvas" | "controls">, build: (w: World) => void, width: number, height: number, t?: number): string | null;
|
package/dist/world.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// WebGL2 renderer: HDR scene target + post chain (bloom, SSAO, composite, FXAA),
|
|
4
4
|
// hardware-PCF depth-texture shadows, per-group transforms + morph targets in the
|
|
5
5
|
// vertex stage, mesh + ribbon primitives. Zero third-party imports, on purpose.
|
|
6
|
-
import { v3, add, sub, scale, cross, norm, clamp, hsl, mIdent, mPerspective, mLookAt, mMul, mCompose, mInvert, normalMat3, } from "./math.js";
|
|
6
|
+
import { v3, add, sub, scale, cross, norm, clamp, hsl, mIdent, mPerspective, mLookAt, mMul, mCompose, mInvert, normalMat3, applyM4, } from "./math.js";
|
|
7
|
+
import { sphereHit, raySphere, boundingSphere } from "./collision.js";
|
|
7
8
|
import { FACE_VS, FACE_FS, DEPTH_VS, DEPTH_FS, LINE_VS, LINE_FS, PT_VS, PT_FS, RIBBON_VS, RIBBON_FS, FACE_INST_VS, DEPTH_INST_VS, WATER_VS, WATER_FS, FACE_SKIN_VS, DEPTH_SKIN_VS, } from "./shaders.js";
|
|
8
9
|
import { composePose } from "./skeletal.js";
|
|
9
10
|
import { PInfo, PostChain } from "./post.js";
|
|
@@ -33,6 +34,64 @@ export class World {
|
|
|
33
34
|
this.groupMat.set(mat, i * 16);
|
|
34
35
|
this.groupNM.set(normalMat3(mat), i * 9);
|
|
35
36
|
}
|
|
37
|
+
/** Register an invisible sphere collider. Attach it to a named group and it
|
|
38
|
+
* rides that group's setTransform exactly like the geometry. Returns an id. */
|
|
39
|
+
addCollider(p, r, group) {
|
|
40
|
+
const id = this.colliderSeq++;
|
|
41
|
+
this.colliders.push({ id, p: v3(p.x, p.y, p.z), r, gi: this.idxFor(group) });
|
|
42
|
+
return id;
|
|
43
|
+
}
|
|
44
|
+
removeCollider(id) {
|
|
45
|
+
const i = this.colliders.findIndex((c) => c.id === id);
|
|
46
|
+
if (i >= 0)
|
|
47
|
+
this.colliders.splice(i, 1);
|
|
48
|
+
}
|
|
49
|
+
/** A collider as it stands RIGHT NOW — group transform applied to its centre,
|
|
50
|
+
* the matrix's largest axis scale applied to its radius. */
|
|
51
|
+
colliderNow(c) {
|
|
52
|
+
const m = this.groupMat.subarray(c.gi * 16, c.gi * 16 + 16);
|
|
53
|
+
const s = Math.max(Math.hypot(m[0], m[1], m[2]), Math.hypot(m[4], m[5], m[6]), Math.hypot(m[8], m[9], m[10]));
|
|
54
|
+
return { p: applyM4(m, c.p), r: c.r * s };
|
|
55
|
+
}
|
|
56
|
+
/** Deepest overlap between a probe sphere and the colliders, or null.
|
|
57
|
+
* `n` is the push-out direction for the probe; `p` the collider's centre. */
|
|
58
|
+
collide(p, r = 0) {
|
|
59
|
+
let best = null;
|
|
60
|
+
for (const c of this.colliders) {
|
|
61
|
+
const s = this.colliderNow(c);
|
|
62
|
+
const h = sphereHit({ p, r }, s);
|
|
63
|
+
if (h && (!best || h.depth > best.depth)) {
|
|
64
|
+
best = { collider: c, p: s.p, n: h.n, depth: h.depth };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return best;
|
|
68
|
+
}
|
|
69
|
+
/** Every collider the probe sphere currently overlaps. */
|
|
70
|
+
collideAll(p, r = 0) {
|
|
71
|
+
const out = [];
|
|
72
|
+
for (const c of this.colliders) {
|
|
73
|
+
const s = this.colliderNow(c);
|
|
74
|
+
const h = sphereHit({ p, r }, s);
|
|
75
|
+
if (h)
|
|
76
|
+
out.push({ collider: c, p: s.p, n: h.n, depth: h.depth });
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
/** Nearest ray hit against the colliders (dir need not be unit). Pass `r`
|
|
81
|
+
* to sweep a moving sphere of that radius instead of a point. */
|
|
82
|
+
rayHit(o, dir, maxDist = Infinity, r = 0) {
|
|
83
|
+
const d = norm(dir);
|
|
84
|
+
let bestT = maxDist;
|
|
85
|
+
let bestC = null;
|
|
86
|
+
for (const c of this.colliders) {
|
|
87
|
+
const t = raySphere(o, d, this.colliderNow(c), r, bestT);
|
|
88
|
+
if (t != null && t <= bestT) {
|
|
89
|
+
bestT = t;
|
|
90
|
+
bestC = c;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return bestC ? { collider: bestC, t: bestT, p: add(o, scale(d, bestT)) } : null;
|
|
94
|
+
}
|
|
36
95
|
constructor(o) {
|
|
37
96
|
this.flyMode = false;
|
|
38
97
|
this.pitchMin = 0.18;
|
|
@@ -40,6 +99,9 @@ export class World {
|
|
|
40
99
|
this.keys = new Set();
|
|
41
100
|
/** W13 fly speed, units/second (shift = 3x). */
|
|
42
101
|
this.flySpeed = 420;
|
|
102
|
+
/** Fog reference distance — by default fog tracks the orbit distance, which is wrong
|
|
103
|
+
* for fly mode (a close camera would fog out the world). Set to see that far. */
|
|
104
|
+
this.fogDist = null;
|
|
43
105
|
this.instanced = [];
|
|
44
106
|
/** W9 — the one wind that blows through every swaying instanced batch */
|
|
45
107
|
this.wind = { dir: v3(1, 0, 0.35), speed: 1 };
|
|
@@ -126,6 +188,9 @@ export class World {
|
|
|
126
188
|
this.groupMorphT = new Float32Array(8);
|
|
127
189
|
this.groupMat = new Float32Array(8 * 16);
|
|
128
190
|
this.groupNM = new Float32Array(8 * 9);
|
|
191
|
+
// ── COLLISION CONTROL — sphere colliders that ride their group's transform ──
|
|
192
|
+
this.colliders = [];
|
|
193
|
+
this.colliderSeq = 1;
|
|
129
194
|
this.W = 0;
|
|
130
195
|
this.H = 0;
|
|
131
196
|
this.mx = 0;
|
|
@@ -371,6 +436,10 @@ export class World {
|
|
|
371
436
|
}
|
|
372
437
|
/** Upload a whole Mesh (geometry.ts builders) as static faces in one call. */
|
|
373
438
|
mesh(geo, o) {
|
|
439
|
+
if (o.collider) { // collision control: the mesh's bounding
|
|
440
|
+
const bs = boundingSphere(geo); // sphere rides the same group transform
|
|
441
|
+
this.addCollider(bs.p, bs.r, o.group);
|
|
442
|
+
}
|
|
374
443
|
const P = geo.pos, N = geo.nrm;
|
|
375
444
|
for (let i = 0; i < P.length; i += 9) {
|
|
376
445
|
this.pushFace(this.sFace, v3(P[i], P[i + 1], P[i + 2]), v3(P[i + 3], P[i + 4], P[i + 5]), v3(P[i + 6], P[i + 7], P[i + 8]), o, v3(N[i], N[i + 1], N[i + 2]), v3(N[i + 3], N[i + 4], N[i + 5]), v3(N[i + 6], N[i + 7], N[i + 8]));
|
|
@@ -913,7 +982,7 @@ export class World {
|
|
|
913
982
|
}
|
|
914
983
|
for (const rb of this.fRib)
|
|
915
984
|
this.pushRibbon(this.dRibA, rb.pts, rb.o);
|
|
916
|
-
const fogK = this.cam.dist * 0.9;
|
|
985
|
+
const fogK = (this.fogDist ?? this.cam.dist) * 0.9;
|
|
917
986
|
for (let i = 0; i < 8; i++) { // eased group alphas + morph weights
|
|
918
987
|
this.groupAlpha[i] += (this.groupTarget[i] - this.groupAlpha[i]) * Math.min(1, dt * 6);
|
|
919
988
|
this.groupMorph[i] += (this.groupMorphT[i] - this.groupMorph[i]) * Math.min(1, dt * 6);
|
|
@@ -1123,6 +1192,12 @@ export class World {
|
|
|
1123
1192
|
}
|
|
1124
1193
|
/** Render one deterministic frame at time t (no loop) — stills, tests, screenshots. */
|
|
1125
1194
|
settle(t) { this.renderFrame(t, 0); }
|
|
1195
|
+
/** The POSTER FRAME — the still a window shows before its animation starts
|
|
1196
|
+
* (HTML video's `poster`, for worlds). Renders the recipe once at `t`
|
|
1197
|
+
* (default 0 — the scene's own first frame, its initial background) without
|
|
1198
|
+
* starting the loop; a later start()/resume() takes over live. Two passes:
|
|
1199
|
+
* the first primes sizing and buffers, the second is the frame that stays. */
|
|
1200
|
+
poster(t = 0) { this.settle(Math.min(0.02, t)); this.settle(t); }
|
|
1126
1201
|
/** Stop the world. `releaseContext: true` also LOSES the GL context immediately —
|
|
1127
1202
|
* browsers cap live contexts per page and reclaim them only at GC, so many-canvas
|
|
1128
1203
|
* pages must release on teardown. A lost context is permanently dead on its canvas:
|
|
@@ -1157,3 +1232,35 @@ export class World {
|
|
|
1157
1232
|
}
|
|
1158
1233
|
}
|
|
1159
1234
|
export function createWorld(o) { return new World(o); }
|
|
1235
|
+
/** Render a recipe's poster frame OFF-SCREEN and return it as a PNG data URL —
|
|
1236
|
+
* the placeholder-image pattern: paint it into the window (an <img> beneath
|
|
1237
|
+
* the live canvas) at page load, and build the live world only when the
|
|
1238
|
+
* window scrolls into view. The scratch world renders on a hidden staged
|
|
1239
|
+
* canvas (sizing reads clientWidth, so it must be in the DOM) and its GL
|
|
1240
|
+
* context is released before returning — a page can poster every window it
|
|
1241
|
+
* owns without spending its live-context budget. Overlay labels (drawLabel)
|
|
1242
|
+
* are not part of a poster. Returns null when WebGL is unavailable — callers
|
|
1243
|
+
* keep their CSS fallback. */
|
|
1244
|
+
export function capturePoster(o, build, width, height, t = 0) {
|
|
1245
|
+
const stage = document.createElement("div");
|
|
1246
|
+
stage.style.cssText = "position:fixed;left:-99999px;top:0;visibility:hidden;pointer-events:none;"
|
|
1247
|
+
+ `width:${Math.max(2, Math.round(width))}px;height:${Math.max(2, Math.round(height))}px;`;
|
|
1248
|
+
const canvas = document.createElement("canvas");
|
|
1249
|
+
canvas.style.cssText = "width:100%;height:100%;display:block;";
|
|
1250
|
+
stage.appendChild(canvas);
|
|
1251
|
+
document.body.appendChild(stage);
|
|
1252
|
+
let w = null;
|
|
1253
|
+
try {
|
|
1254
|
+
w = createWorld({ ...o, canvas, controls: false, maxDpr: 1 });
|
|
1255
|
+
build(w);
|
|
1256
|
+
w.poster(t);
|
|
1257
|
+
return canvas.toDataURL("image/png"); // same task as the render — buffer intact
|
|
1258
|
+
}
|
|
1259
|
+
catch {
|
|
1260
|
+
return null;
|
|
1261
|
+
}
|
|
1262
|
+
finally {
|
|
1263
|
+
w?.dispose(true);
|
|
1264
|
+
stage.remove();
|
|
1265
|
+
}
|
|
1266
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chromatic-coherence/generative-engine",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "A zero-dependency WebGL2 graphics engine where everything is grown from code — no models, no textures, no asset pipeline. Real light and shadow with a cinematic HDR finish (bloom, SSAO, filmic grade, depth of field, motion blur), a geometry stdlib, GPU crowds (instanced wind, ten-thousand-bird flocks), real water with planar reflections, skeletal figures with procedural walk cycles, free-flight controls, and scenes that drive themselves toward objectives. The Generative Charts API rides on top unchanged.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|