@chromatic-coherence/generative-engine 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,36 @@
1
+ Chromatic Coherence — generative-engine Licence (2026)
2
+
3
+ Copyright (c) 2026 Chromatic Coherence. All rights reserved.
4
+
5
+ This software ("the Software") is source-available but is NOT open source.
6
+ It is licensed, not sold. Downloading or installing it (for example from npm)
7
+ grants no rights beyond those written below, and does not transfer ownership.
8
+
9
+ 1. FREE LICENCE — no cost, perpetual — granted to:
10
+ * individuals, for personal or non-commercial use;
11
+ * registered charities and non-profit organisations;
12
+ * small businesses — an organisation with fewer than 25 people AND under
13
+ £1,000,000 in annual gross revenue.
14
+ A Free Licence lets you install the Software and use it AS-IS, embedded
15
+ inside your own application, and distribute it only as an inseparable part
16
+ of that application.
17
+
18
+ 2. COMMERCIAL LICENCE — required by any organisation not covered by clause 1
19
+ (an "SME or Enterprise"). A one-off fee of £100 per organisation grants a
20
+ perpetual Commercial Licence covering all of that organisation's products.
21
+ Contact hello@chromaticcoherence.ai.
22
+
23
+ 3. In no case, and under no licence, may you:
24
+ * modify, adapt, translate, or create derivative works of the Software;
25
+ * reverse-engineer, decompile, or disassemble it, except to the extent
26
+ that right cannot lawfully be excluded;
27
+ * resell, relicense, sublicense, rent, or redistribute the Software on its
28
+ own — as a package, fork, component, or competing library;
29
+ * remove or alter this licence or any copyright or authorship notice.
30
+
31
+ 4. NO WARRANTY. The Software is provided "AS IS", without warranty of any kind,
32
+ express or implied. To the fullest extent permitted by law, Chromatic
33
+ Coherence accepts no liability for any claim, damage, or loss arising from
34
+ the Software or its use.
35
+
36
+ For commercial licensing, contact hello@chromaticcoherence.ai.
package/README.md ADDED
@@ -0,0 +1,80 @@
1
+ <div align="center">
2
+
3
+ # generative-engine
4
+
5
+ ### **THE THIRD ENGINE** — the [Generative Charts](https://chromaticcoherence.ai/generative-charts) family grows into a full **WebGL2 graphics engine**.
6
+
7
+ [![licence: source-available](https://img.shields.io/badge/licence-source--available-1e88e5)](./LICENSE)
8
+ [![zero dependencies](https://img.shields.io/badge/dependencies-0-success)](#)
9
+
10
+ **[2D · CPU edition →](https://www.npmjs.com/package/@chromatic-coherence/generative-charts-2d)** · **[3D · GPU charts edition →](https://www.npmjs.com/package/@chromatic-coherence/generative-charts-3d)**
11
+
12
+ </div>
13
+
14
+ ---
15
+
16
+ The two chart editions stay what they are — small, stable, chart-first. **generative-engine** is where
17
+ the family expands: the same API (`createWorld` · `face` · `line` · `glow` · `grow` · `draw*` ·
18
+ `fade`), so chart recipes port forward by changing one import — and underneath it, a real
19
+ graphics engine. Zero dependencies; the browser is the only runtime.
20
+
21
+ ```bash
22
+ npm install @chromatic-coherence/generative-engine
23
+ ```
24
+
25
+ ```js
26
+ import { createWorld, v3, icosphere, heightfield } from "@chromatic-coherence/generative-engine";
27
+
28
+ const w = createWorld({ canvas, controls: true, shadows: true });
29
+ w.lights.push({ p: v3(-160, 320, 120), intensity: 1.2, falloff: 500 });
30
+ w.mesh(heightfield((x, z) => Math.sin(x * 0.01) * 20, 800, 800), { hue: 262, sat: 38, light: 26 });
31
+ w.meshMorph(icosphere(70, 3), myBlob, { hue: 350, sat: 30, light: 42, gloss: 0.5, group: "blob" });
32
+ w.grow((t) => { w.morph("blob", 0.5 + 0.5 * Math.sin(t)); w.setTransform("blob", { rotateY: t * 0.4 }); });
33
+ w.start();
34
+ ```
35
+
36
+ ## What the engine adds over the chart editions
37
+
38
+ | | |
39
+ |---|---|
40
+ | **WebGL2 + HDR post chain** | The scene renders to a floating-point target, then: **bloom** (soft-knee bright pass, half-res gaussian), **SSAO** (depth-based contact shading), one composite pass (ACES filmic, colour grade, vignette), and **FXAA**. All art-directable (`w.bloom`, `w.ssao`, `w.fxaa`), all on by default. `post: false` opts out. |
41
+ | **Real shadow maps** | A depth **texture** with hardware-comparison PCF (`sampler2DShadow`) — soft, exact, 2048px by default. `shadows: { size, softness, bias }`. |
42
+ | **Geometry stdlib** | `plane` · `box` · `icosphere` · `tube` (parallel-transport sweep, taperable) · `lathe` · `extrude` · `heightfield` · `transformMesh` · `mergeMesh` — all pure math, all unit-tested, uploaded in one `w.mesh(geo, opts)` call with smooth normals. |
43
+ | **Ribbons** | `w.ribbon(points, { width })` / `w.drawRibbon(...)` — screen-space-expanded strokes with **real pixel width**, feathered edges, depth-tested. The stroke aesthetic `gl.LINES` never allowed. |
44
+ | **Group transforms** | `w.setTransform(group, { translate, rotateY, scale })` — geometry moves rigidly in the vertex stage, zero CPU re-tessellation, shadow pass included. |
45
+ | **Morph targets** | `w.meshMorph(geoA, geoB, opts)` + `w.morph(group, t)` — blend between two vertex sets in the shader (breathing, becoming). Eased like `fade`. |
46
+ | **8 point lights** | Up from 4; `lights[0]` casts the shadows. HDR intensities welcome — bloom catches what exceeds 1.0. |
47
+ | **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. |
48
+
49
+ **The ink edition** (`theme: "light"`) still renders any recipe on paper instead of night —
50
+ lightness inverts, strokes blend normally, SSAO becomes pencil shading. Bloom stays dark-only.
51
+
52
+ ## Demo
53
+
54
+ `demo/index.html` — serve the package folder (`python -m http.server`) and open
55
+ `/demo/`. Drag to orbit, ctrl+wheel to zoom; `?theme=light`, `?post=0`, `?still`.
56
+
57
+ ## Falling back
58
+
59
+ `createWorld` **throws** (never a blank page) when WebGL2 is missing. Catch it and build the
60
+ same recipe through the chart editions — the API is a subset, so the fallback chain
61
+ engine → charts-3d → charts-2d is three imports of the same code. Browsers cap live GL
62
+ contexts per page: on teardown call `dispose(true)` to release the context immediately, and
63
+ mount a **fresh** canvas if you rebuild (a lost context is dead on its canvas).
64
+
65
+ ## Roadmap
66
+
67
+ Planar mirrors · instancing · depth of field · motion blur. The chart editions stay lean.
68
+
69
+ ## Licence
70
+
71
+ **Source-available, not open source.** Free for individuals, charities and small businesses
72
+ (fewer than 25 people and under £1M annual revenue); a one-off **£100** covers any larger
73
+ organisation. Full terms in [LICENSE](./LICENSE); commercial licensing:
74
+ [hello@chromaticcoherence.ai](mailto:hello@chromaticcoherence.ai).
75
+
76
+ ---
77
+
78
+ <div align="center">
79
+ <sub>© 2026 Chromatic Coherence. All rights reserved.</sub>
80
+ </div>
@@ -0,0 +1,40 @@
1
+ import { V3, M4 } from "./math.js";
2
+ /** Triangle soup: positions/normals are 3 floats per vertex, 3 vertices per triangle. */
3
+ export type Mesh = {
4
+ pos: number[];
5
+ nrm: number[];
6
+ };
7
+ export declare const meshVertCount: (m: Mesh) => number;
8
+ /** Grid helper: builds (nx*nz) quads from a parametric surface, smooth normals from
9
+ * central differences. Used by plane, heightfield and lathe alike. */
10
+ export declare function parametric(f: (u: number, v: number) => V3, nu: number, nv: number, opts?: {
11
+ wrapU?: boolean;
12
+ }): Mesh;
13
+ /** A flat XZ plane centred on the origin, normal +Y. */
14
+ export declare function plane(sizeX: number, sizeZ: number, nx?: number, nz?: number): Mesh;
15
+ /** An axis-aligned box centred on the origin (flat-shaded faces). */
16
+ export declare function box(sx: number, sy: number, sz: number): Mesh;
17
+ /** An icosphere — geodesic subdivision of the icosahedron; normals are exact
18
+ * (radial), so it shades perfectly smooth at any subdivision level. */
19
+ export declare function icosphere(radius: number, subdiv?: number): Mesh;
20
+ /** Sweep a circular cross-section along a polyline path using parallel-transport
21
+ * frames (no twist). `radius` may be a constant or a per-station function of
22
+ * u in 0..1 — taper for free. */
23
+ export declare function tube(path: V3[], radius: number | ((u: number) => number), segments?: number, closed?: boolean): Mesh;
24
+ /** Revolve a 2D profile (x = radius, y = height; x >= 0) around the Y axis. */
25
+ export declare function lathe(profile: {
26
+ x: number;
27
+ y: number;
28
+ }[], segments?: number): Mesh;
29
+ /** Extrude a 2D polygon (CCW, on XZ) straight up by `height` — walls + fan caps. */
30
+ export declare function extrude(shape: {
31
+ x: number;
32
+ z: number;
33
+ }[], height: number): Mesh;
34
+ /** A heightfield surface: y = f(x, z) sampled on an (nx x nz) grid over
35
+ * [-sizeX/2, sizeX/2] x [-sizeZ/2, sizeZ/2], smooth normals. */
36
+ export declare function heightfield(f: (x: number, z: number) => number, sizeX: number, sizeZ: number, nx?: number, nz?: number): Mesh;
37
+ /** Transform a mesh in place by a mat4 (normals via the inverse-transpose). */
38
+ export declare function transformMesh(m: Mesh, mat: M4): Mesh;
39
+ /** Concatenate meshes into one (they must be pre-transformed into a shared space). */
40
+ export declare function mergeMesh(...parts: Mesh[]): Mesh;
@@ -0,0 +1,203 @@
1
+ // generative-engine — geometry.ts: the mesh stdlib. Every builder returns a Mesh —
2
+ // flat triangle soup with per-vertex smooth normals — that World.mesh() uploads
3
+ // in one call. Pure math, no GL here: everything in this file runs under node
4
+ // and is covered by test/run.mjs.
5
+ import { v3, add, sub, scale, cross, norm, dot, len, applyM4, normalMat3, TAU } from "./math.js";
6
+ export const meshVertCount = (m) => m.pos.length / 3;
7
+ function pushTri(m, a, b, c, na, nb, nc) {
8
+ m.pos.push(a.x, a.y, a.z, b.x, b.y, b.z, c.x, c.y, c.z);
9
+ m.nrm.push(na.x, na.y, na.z, nb.x, nb.y, nb.z, nc.x, nc.y, nc.z);
10
+ }
11
+ function flatTri(m, a, b, c) {
12
+ const n = norm(cross(sub(b, a), sub(c, a)));
13
+ pushTri(m, a, b, c, n, n, n);
14
+ }
15
+ /** Grid helper: builds (nx*nz) quads from a parametric surface, smooth normals from
16
+ * central differences. Used by plane, heightfield and lathe alike. */
17
+ export function parametric(f, nu, nv, opts) {
18
+ const m = { pos: [], nrm: [] };
19
+ const P = [], N = [];
20
+ const eps = 1e-3;
21
+ for (let i = 0; i <= nu; i++) {
22
+ P.push([]);
23
+ N.push([]);
24
+ const u = i / nu;
25
+ for (let j = 0; j <= nv; j++) {
26
+ const v = j / nv;
27
+ const p = f(u, v);
28
+ const du = sub(f(Math.min(1, u + eps), v), f(Math.max(0, u - eps), v));
29
+ const dv = sub(f(u, Math.min(1, v + eps)), f(u, Math.max(0, v - eps)));
30
+ const n = norm(cross(dv, du)); // dv x du: +Y on an XZ field, outward on a lathe
31
+ P[i].push(p);
32
+ N[i].push(n);
33
+ }
34
+ }
35
+ if (opts?.wrapU)
36
+ for (let j = 0; j <= nv; j++) {
37
+ P[nu][j] = P[0][j];
38
+ N[nu][j] = N[0][j];
39
+ }
40
+ for (let i = 0; i < nu; i++)
41
+ for (let j = 0; j < nv; j++) {
42
+ pushTri(m, P[i][j], P[i + 1][j], P[i + 1][j + 1], N[i][j], N[i + 1][j], N[i + 1][j + 1]);
43
+ pushTri(m, P[i][j], P[i + 1][j + 1], P[i][j + 1], N[i][j], N[i + 1][j + 1], N[i][j + 1]);
44
+ }
45
+ return m;
46
+ }
47
+ /** A flat XZ plane centred on the origin, normal +Y. */
48
+ export function plane(sizeX, sizeZ, nx = 1, nz = 1) {
49
+ return parametric((u, v) => v3((u - 0.5) * sizeX, 0, (v - 0.5) * sizeZ), nx, nz);
50
+ }
51
+ /** An axis-aligned box centred on the origin (flat-shaded faces). */
52
+ export function box(sx, sy, sz) {
53
+ const m = { pos: [], nrm: [] };
54
+ const x = sx / 2, y = sy / 2, z = sz / 2;
55
+ const q = (a, b, c, d) => { flatTri(m, a, b, c); flatTri(m, a, c, d); };
56
+ q(v3(-x, -y, z), v3(x, -y, z), v3(x, y, z), v3(-x, y, z)); // +Z
57
+ q(v3(x, -y, -z), v3(-x, -y, -z), v3(-x, y, -z), v3(x, y, -z)); // -Z
58
+ q(v3(x, -y, z), v3(x, -y, -z), v3(x, y, -z), v3(x, y, z)); // +X
59
+ q(v3(-x, -y, -z), v3(-x, -y, z), v3(-x, y, z), v3(-x, y, -z)); // -X
60
+ q(v3(-x, y, z), v3(x, y, z), v3(x, y, -z), v3(-x, y, -z)); // +Y
61
+ q(v3(-x, -y, -z), v3(x, -y, -z), v3(x, -y, z), v3(-x, -y, z)); // -Y
62
+ return m;
63
+ }
64
+ /** An icosphere — geodesic subdivision of the icosahedron; normals are exact
65
+ * (radial), so it shades perfectly smooth at any subdivision level. */
66
+ export function icosphere(radius, subdiv = 2) {
67
+ const t = (1 + Math.sqrt(5)) / 2;
68
+ const V = [
69
+ v3(-1, t, 0), v3(1, t, 0), v3(-1, -t, 0), v3(1, -t, 0),
70
+ v3(0, -1, t), v3(0, 1, t), v3(0, -1, -t), v3(0, 1, -t),
71
+ v3(t, 0, -1), v3(t, 0, 1), v3(-t, 0, -1), v3(-t, 0, 1),
72
+ ].map(norm);
73
+ let tris = [
74
+ [V[0], V[11], V[5]], [V[0], V[5], V[1]], [V[0], V[1], V[7]], [V[0], V[7], V[10]], [V[0], V[10], V[11]],
75
+ [V[1], V[5], V[9]], [V[5], V[11], V[4]], [V[11], V[10], V[2]], [V[10], V[7], V[6]], [V[7], V[1], V[8]],
76
+ [V[3], V[9], V[4]], [V[3], V[4], V[2]], [V[3], V[2], V[6]], [V[3], V[6], V[8]], [V[3], V[8], V[9]],
77
+ [V[4], V[9], V[5]], [V[2], V[4], V[11]], [V[6], V[2], V[10]], [V[8], V[6], V[7]], [V[9], V[8], V[1]],
78
+ ];
79
+ for (let s = 0; s < subdiv; s++) {
80
+ const next = [];
81
+ for (const [a, b, c] of tris) {
82
+ const ab = norm(scale(add(a, b), 0.5)), bc = norm(scale(add(b, c), 0.5)), ca = norm(scale(add(c, a), 0.5));
83
+ next.push([a, ab, ca], [ab, b, bc], [ca, bc, c], [ab, bc, ca]);
84
+ }
85
+ tris = next;
86
+ }
87
+ const m = { pos: [], nrm: [] };
88
+ for (const [a, b, c] of tris)
89
+ pushTri(m, scale(a, radius), scale(b, radius), scale(c, radius), a, b, c);
90
+ return m;
91
+ }
92
+ /** Sweep a circular cross-section along a polyline path using parallel-transport
93
+ * frames (no twist). `radius` may be a constant or a per-station function of
94
+ * u in 0..1 — taper for free. */
95
+ export function tube(path, radius, segments = 12, closed = false) {
96
+ const m = { pos: [], nrm: [] };
97
+ const n = path.length;
98
+ if (n < 2)
99
+ return m;
100
+ const rad = typeof radius === "number" ? () => radius : radius;
101
+ // tangents by central difference
102
+ const T = [];
103
+ for (let i = 0; i < n; i++) {
104
+ const a = path[Math.max(0, i - 1)], b = path[Math.min(n - 1, i + 1)];
105
+ T.push(norm(sub(b, a)));
106
+ }
107
+ // parallel transport: seed a normal, rotate minimally between stations
108
+ let N0 = cross(T[0], Math.abs(T[0].y) < 0.95 ? v3(0, 1, 0) : v3(1, 0, 0));
109
+ N0 = norm(N0);
110
+ const rings = [], ringN = [];
111
+ let N = N0;
112
+ for (let i = 0; i < n; i++) {
113
+ if (i > 0) {
114
+ // rotate N so it stays perpendicular to the new tangent (Gram-Schmidt re-project)
115
+ N = norm(sub(N, scale(T[i], dot(N, T[i]))));
116
+ if (!isFinite(N.x) || len(N) < 1e-6)
117
+ N = norm(cross(T[i], v3(0, 1, 0)));
118
+ }
119
+ const B = cross(T[i], N);
120
+ const r = rad(i / (n - 1));
121
+ const ring = [], rn = [];
122
+ for (let s = 0; s < segments; s++) {
123
+ const a = (s / segments) * TAU;
124
+ const dir = add(scale(N, Math.cos(a)), scale(B, Math.sin(a)));
125
+ ring.push(add(path[i], scale(dir, r)));
126
+ rn.push(dir);
127
+ }
128
+ rings.push(ring);
129
+ ringN.push(rn);
130
+ }
131
+ const last = closed ? n : n - 1;
132
+ for (let i = 0; i < last; i++) {
133
+ const j = (i + 1) % n;
134
+ for (let s = 0; s < segments; s++) {
135
+ const s2 = (s + 1) % segments;
136
+ pushTri(m, rings[i][s], rings[j][s], rings[j][s2], ringN[i][s], ringN[j][s], ringN[j][s2]);
137
+ pushTri(m, rings[i][s], rings[j][s2], rings[i][s2], ringN[i][s], ringN[j][s2], ringN[i][s2]);
138
+ }
139
+ }
140
+ return m;
141
+ }
142
+ /** Revolve a 2D profile (x = radius, y = height; x >= 0) around the Y axis. */
143
+ export function lathe(profile, segments = 24) {
144
+ const np = profile.length;
145
+ return parametric((u, v) => {
146
+ const a = u * TAU;
147
+ const i = Math.min(np - 1.001, v * (np - 1));
148
+ const i0 = Math.floor(i), fr = i - i0;
149
+ const x = profile[i0].x + (profile[Math.min(np - 1, i0 + 1)].x - profile[i0].x) * fr;
150
+ const y = profile[i0].y + (profile[Math.min(np - 1, i0 + 1)].y - profile[i0].y) * fr;
151
+ return v3(Math.cos(a) * x, y, Math.sin(a) * x);
152
+ }, segments, (np - 1) * 2, { wrapU: true });
153
+ }
154
+ /** Extrude a 2D polygon (CCW, on XZ) straight up by `height` — walls + fan caps. */
155
+ export function extrude(shape, height) {
156
+ const m = { pos: [], nrm: [] };
157
+ const n = shape.length;
158
+ const lo = (p) => v3(p.x, 0, p.z);
159
+ const hi = (p) => v3(p.x, height, p.z);
160
+ for (let i = 0; i < n; i++) {
161
+ const a = shape[i], b = shape[(i + 1) % n];
162
+ flatTri(m, lo(a), lo(b), hi(b));
163
+ flatTri(m, lo(a), hi(b), hi(a));
164
+ }
165
+ for (let i = 1; i < n - 1; i++) { // caps (convex fan)
166
+ flatTri(m, hi(shape[0]), hi(shape[i]), hi(shape[i + 1]));
167
+ flatTri(m, lo(shape[0]), lo(shape[i + 1]), lo(shape[i]));
168
+ }
169
+ return m;
170
+ }
171
+ /** A heightfield surface: y = f(x, z) sampled on an (nx x nz) grid over
172
+ * [-sizeX/2, sizeX/2] x [-sizeZ/2, sizeZ/2], smooth normals. */
173
+ export function heightfield(f, sizeX, sizeZ, nx = 48, nz = 48) {
174
+ return parametric((u, v) => {
175
+ const x = (u - 0.5) * sizeX, z = (v - 0.5) * sizeZ;
176
+ return v3(x, f(x, z), z);
177
+ }, nx, nz);
178
+ }
179
+ /** Transform a mesh in place by a mat4 (normals via the inverse-transpose). */
180
+ export function transformMesh(m, mat) {
181
+ const nm = normalMat3(mat);
182
+ for (let i = 0; i < m.pos.length; i += 3) {
183
+ const p = applyM4(mat, v3(m.pos[i], m.pos[i + 1], m.pos[i + 2]));
184
+ m.pos[i] = p.x;
185
+ m.pos[i + 1] = p.y;
186
+ m.pos[i + 2] = p.z;
187
+ const x = m.nrm[i], y = m.nrm[i + 1], z = m.nrm[i + 2];
188
+ const n = norm(v3(nm[0] * x + nm[3] * y + nm[6] * z, nm[1] * x + nm[4] * y + nm[7] * z, nm[2] * x + nm[5] * y + nm[8] * z));
189
+ m.nrm[i] = n.x;
190
+ m.nrm[i + 1] = n.y;
191
+ m.nrm[i + 2] = n.z;
192
+ }
193
+ return m;
194
+ }
195
+ /** Concatenate meshes into one (they must be pre-transformed into a shared space). */
196
+ export function mergeMesh(...parts) {
197
+ const m = { pos: [], nrm: [] };
198
+ for (const p of parts) {
199
+ m.pos.push(...p.pos);
200
+ m.nrm.push(...p.nrm);
201
+ }
202
+ return m;
203
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./math.js";
2
+ export * from "./geometry.js";
3
+ export * from "./world.js";
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ // generative-engine — THE THIRD ENGINE. Public surface: the whole math kit, the geometry
2
+ // stdlib, and the World. The chart editions' API is a strict subset — recipes port
3
+ // forward by changing only the import.
4
+ export * from "./math.js";
5
+ export * from "./geometry.js";
6
+ export * from "./world.js";
package/dist/math.d.ts ADDED
@@ -0,0 +1,45 @@
1
+ export type V3 = {
2
+ x: number;
3
+ y: number;
4
+ z: number;
5
+ };
6
+ export declare const v3: (x?: number, y?: number, z?: number) => V3;
7
+ export declare const add: (a: V3, b: V3) => V3;
8
+ export declare const sub: (a: V3, b: V3) => V3;
9
+ export declare const scale: (a: V3, s: number) => V3;
10
+ export declare const dot: (a: V3, b: V3) => number;
11
+ export declare const cross: (a: V3, b: V3) => V3;
12
+ export declare const len: (a: V3) => number;
13
+ export declare const norm: (a: V3) => V3;
14
+ export declare const mix3: (a: V3, b: V3, u: number) => V3;
15
+ export declare const TAU: number;
16
+ export declare const clamp: (v: number, lo: number, hi: number) => number;
17
+ export declare const lerp: (a: number, b: number, u: number) => number;
18
+ export declare const easeInOut: (u: number) => number;
19
+ /** Deterministic hash noise — identical to the chart editions, so recipes port. */
20
+ export declare function rnd(i: number, k: number): number;
21
+ export type M4 = Float32Array;
22
+ export declare const mIdent: () => M4;
23
+ export declare function mPerspective(fovy: number, aspect: number, near: number, far: number): M4;
24
+ export declare function mLookAt(eye: V3, at: V3, up: V3): M4;
25
+ export declare function mMul(a: M4, b: M4): M4;
26
+ export declare const mTranslate: (t: V3) => M4;
27
+ export declare const mScale: (s: V3) => M4;
28
+ export declare function mRotX(a: number): M4;
29
+ export declare function mRotY(a: number): M4;
30
+ export declare function mRotZ(a: number): M4;
31
+ /** Compose T*Ry*Rx*Rz*S from a plain-english spec — the group-transform helper. */
32
+ export type TransformSpec = {
33
+ translate?: V3;
34
+ rotateX?: number;
35
+ rotateY?: number;
36
+ rotateZ?: number;
37
+ scale?: number | V3;
38
+ };
39
+ export declare function mCompose(spec: TransformSpec): M4;
40
+ export declare function mInvert(m: M4): M4;
41
+ export declare function mTranspose(m: M4): M4;
42
+ /** Upper-3x3 inverse-transpose of a mat4 — the normal matrix (handles non-uniform scale). */
43
+ export declare function normalMat3(m: M4): Float32Array;
44
+ export declare function applyM4(m: M4, p: V3): V3;
45
+ export declare function hsl(h: number, s: number, l: number): [number, number, number];
package/dist/math.js ADDED
@@ -0,0 +1,151 @@
1
+ // generative-engine — THE THIRD ENGINE. math.ts: vectors, matrices, easing, colour.
2
+ // Grown from dark-math-3d's maths, extended with the mat4 compose/invert set the
3
+ // group-transform and post pipelines need. Zero third-party imports, on purpose.
4
+ export const v3 = (x = 0, y = 0, z = 0) => ({ x, y, z });
5
+ export const add = (a, b) => v3(a.x + b.x, a.y + b.y, a.z + b.z);
6
+ export const sub = (a, b) => v3(a.x - b.x, a.y - b.y, a.z - b.z);
7
+ export const scale = (a, s) => v3(a.x * s, a.y * s, a.z * s);
8
+ export const dot = (a, b) => a.x * b.x + a.y * b.y + a.z * b.z;
9
+ export const cross = (a, b) => v3(a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x);
10
+ export const len = (a) => Math.hypot(a.x, a.y, a.z);
11
+ export const norm = (a) => { const l = len(a) || 1; return v3(a.x / l, a.y / l, a.z / l); };
12
+ export const mix3 = (a, b, u) => v3(a.x + (b.x - a.x) * u, a.y + (b.y - a.y) * u, a.z + (b.z - a.z) * u);
13
+ export const TAU = Math.PI * 2;
14
+ export const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
15
+ export const lerp = (a, b, u) => a + (b - a) * u;
16
+ export const easeInOut = (u) => (u < 0.5 ? 4 * u * u * u : 1 - Math.pow(-2 * u + 2, 3) / 2);
17
+ /** Deterministic hash noise — identical to the chart editions, so recipes port. */
18
+ export function rnd(i, k) {
19
+ const x = Math.sin(i * 12.9898 + k * 78.233) * 43758.5453;
20
+ return x - Math.floor(x);
21
+ }
22
+ export const mIdent = () => new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
23
+ export function mPerspective(fovy, aspect, near, far) {
24
+ const f = 1 / Math.tan(fovy / 2), nf = 1 / (near - far);
25
+ const m = new Float32Array(16);
26
+ m[0] = f / aspect;
27
+ m[5] = f;
28
+ m[10] = (far + near) * nf;
29
+ m[11] = -1;
30
+ m[14] = 2 * far * near * nf;
31
+ return m;
32
+ }
33
+ export function mLookAt(eye, at, up) {
34
+ const zc = norm(sub(eye, at)), xc = norm(cross(up, zc)), yc = cross(zc, xc);
35
+ const m = new Float32Array(16);
36
+ m[0] = xc.x;
37
+ m[1] = yc.x;
38
+ m[2] = zc.x;
39
+ m[4] = xc.y;
40
+ m[5] = yc.y;
41
+ m[6] = zc.y;
42
+ m[8] = xc.z;
43
+ m[9] = yc.z;
44
+ m[10] = zc.z;
45
+ m[12] = -dot(xc, eye);
46
+ m[13] = -dot(yc, eye);
47
+ m[14] = -dot(zc, eye);
48
+ m[15] = 1;
49
+ return m;
50
+ }
51
+ export function mMul(a, b) {
52
+ const o = new Float32Array(16);
53
+ for (let c = 0; c < 4; c++)
54
+ for (let r = 0; r < 4; r++)
55
+ o[c * 4 + r] = a[r] * b[c * 4] + a[4 + r] * b[c * 4 + 1] + a[8 + r] * b[c * 4 + 2] + a[12 + r] * b[c * 4 + 3];
56
+ return o;
57
+ }
58
+ export const mTranslate = (t) => new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, t.x, t.y, t.z, 1]);
59
+ export const mScale = (s) => new Float32Array([s.x, 0, 0, 0, 0, s.y, 0, 0, 0, 0, s.z, 0, 0, 0, 0, 1]);
60
+ export function mRotX(a) {
61
+ const c = Math.cos(a), s = Math.sin(a);
62
+ return new Float32Array([1, 0, 0, 0, 0, c, s, 0, 0, -s, c, 0, 0, 0, 0, 1]);
63
+ }
64
+ export function mRotY(a) {
65
+ const c = Math.cos(a), s = Math.sin(a);
66
+ return new Float32Array([c, 0, -s, 0, 0, 1, 0, 0, s, 0, c, 0, 0, 0, 0, 1]);
67
+ }
68
+ export function mRotZ(a) {
69
+ const c = Math.cos(a), s = Math.sin(a);
70
+ return new Float32Array([c, s, 0, 0, -s, c, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
71
+ }
72
+ export function mCompose(spec) {
73
+ let m = mIdent();
74
+ if (spec.scale != null) {
75
+ const s = typeof spec.scale === "number" ? v3(spec.scale, spec.scale, spec.scale) : spec.scale;
76
+ m = mMul(mScale(s), m);
77
+ }
78
+ if (spec.rotateZ)
79
+ m = mMul(mRotZ(spec.rotateZ), m);
80
+ if (spec.rotateX)
81
+ m = mMul(mRotX(spec.rotateX), m);
82
+ if (spec.rotateY)
83
+ m = mMul(mRotY(spec.rotateY), m);
84
+ if (spec.translate)
85
+ m = mMul(mTranslate(spec.translate), m);
86
+ return m;
87
+ }
88
+ export function mInvert(m) {
89
+ // general 4x4 inverse (cofactors) — the post chain needs inverse-projection,
90
+ // the normal matrix needs inverse-transpose. Falls back to identity if singular.
91
+ const inv = new Float32Array(16);
92
+ inv[0] = m[5] * m[10] * m[15] - m[5] * m[11] * m[14] - m[9] * m[6] * m[15] + m[9] * m[7] * m[14] + m[13] * m[6] * m[11] - m[13] * m[7] * m[10];
93
+ inv[4] = -m[4] * m[10] * m[15] + m[4] * m[11] * m[14] + m[8] * m[6] * m[15] - m[8] * m[7] * m[14] - m[12] * m[6] * m[11] + m[12] * m[7] * m[10];
94
+ inv[8] = m[4] * m[9] * m[15] - m[4] * m[11] * m[13] - m[8] * m[5] * m[15] + m[8] * m[7] * m[13] + m[12] * m[5] * m[11] - m[12] * m[7] * m[9];
95
+ inv[12] = -m[4] * m[9] * m[14] + m[4] * m[10] * m[13] + m[8] * m[5] * m[14] - m[8] * m[6] * m[13] - m[12] * m[5] * m[10] + m[12] * m[6] * m[9];
96
+ inv[1] = -m[1] * m[10] * m[15] + m[1] * m[11] * m[14] + m[9] * m[2] * m[15] - m[9] * m[3] * m[14] - m[13] * m[2] * m[11] + m[13] * m[3] * m[10];
97
+ inv[5] = m[0] * m[10] * m[15] - m[0] * m[11] * m[14] - m[8] * m[2] * m[15] + m[8] * m[3] * m[14] + m[12] * m[2] * m[11] - m[12] * m[3] * m[10];
98
+ inv[9] = -m[0] * m[9] * m[15] + m[0] * m[11] * m[13] + m[8] * m[1] * m[15] - m[8] * m[3] * m[13] - m[12] * m[1] * m[11] + m[12] * m[3] * m[9];
99
+ inv[13] = m[0] * m[9] * m[14] - m[0] * m[10] * m[13] - m[8] * m[1] * m[14] + m[8] * m[2] * m[13] + m[12] * m[1] * m[10] - m[12] * m[2] * m[9];
100
+ inv[2] = m[1] * m[6] * m[15] - m[1] * m[7] * m[14] - m[5] * m[2] * m[15] + m[5] * m[3] * m[14] + m[13] * m[2] * m[7] - m[13] * m[3] * m[6];
101
+ inv[6] = -m[0] * m[6] * m[15] + m[0] * m[7] * m[14] + m[4] * m[2] * m[15] - m[4] * m[3] * m[14] - m[12] * m[2] * m[7] + m[12] * m[3] * m[6];
102
+ inv[10] = m[0] * m[5] * m[15] - m[0] * m[7] * m[13] - m[4] * m[1] * m[15] + m[4] * m[3] * m[13] + m[12] * m[1] * m[7] - m[12] * m[3] * m[5];
103
+ inv[14] = -m[0] * m[5] * m[14] + m[0] * m[6] * m[13] + m[4] * m[1] * m[14] - m[4] * m[2] * m[13] - m[12] * m[1] * m[6] + m[12] * m[2] * m[5];
104
+ inv[3] = -m[1] * m[6] * m[11] + m[1] * m[7] * m[10] + m[5] * m[2] * m[11] - m[5] * m[3] * m[10] - m[9] * m[2] * m[7] + m[9] * m[3] * m[6];
105
+ inv[7] = m[0] * m[6] * m[11] - m[0] * m[7] * m[10] - m[4] * m[2] * m[11] + m[4] * m[3] * m[10] + m[8] * m[2] * m[7] - m[8] * m[3] * m[6];
106
+ inv[11] = -m[0] * m[5] * m[11] + m[0] * m[7] * m[9] + m[4] * m[1] * m[11] - m[4] * m[3] * m[9] - m[8] * m[1] * m[7] + m[8] * m[3] * m[5];
107
+ inv[15] = m[0] * m[5] * m[10] - m[0] * m[6] * m[9] - m[4] * m[1] * m[10] + m[4] * m[2] * m[9] + m[8] * m[1] * m[6] - m[8] * m[2] * m[5];
108
+ const det = m[0] * inv[0] + m[1] * inv[4] + m[2] * inv[8] + m[3] * inv[12];
109
+ if (!det)
110
+ return mIdent();
111
+ const d = 1 / det;
112
+ for (let i = 0; i < 16; i++)
113
+ inv[i] *= d;
114
+ return inv;
115
+ }
116
+ export function mTranspose(m) {
117
+ const o = new Float32Array(16);
118
+ for (let c = 0; c < 4; c++)
119
+ for (let r = 0; r < 4; r++)
120
+ o[c * 4 + r] = m[r * 4 + c];
121
+ return o;
122
+ }
123
+ /** Upper-3x3 inverse-transpose of a mat4 — the normal matrix (handles non-uniform scale). */
124
+ export function normalMat3(m) {
125
+ const it = mTranspose(mInvert(m));
126
+ return new Float32Array([it[0], it[1], it[2], it[4], it[5], it[6], it[8], it[9], it[10]]);
127
+ }
128
+ export function applyM4(m, p) {
129
+ const w = m[3] * p.x + m[7] * p.y + m[11] * p.z + m[15] || 1;
130
+ return v3((m[0] * p.x + m[4] * p.y + m[8] * p.z + m[12]) / w, (m[1] * p.x + m[5] * p.y + m[9] * p.z + m[13]) / w, (m[2] * p.x + m[6] * p.y + m[10] * p.z + m[14]) / w);
131
+ }
132
+ // ── colour: hsl -> rgb (0..1), CPU-side — colours ride the vertex buffers ──
133
+ export function hsl(h, s, l) {
134
+ h = (((h % 360) + 360) % 360) / 360;
135
+ s = clamp(s, 0, 100) / 100;
136
+ l = clamp(l, 0, 100) / 100;
137
+ if (s === 0)
138
+ return [l, l, l];
139
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s, p = 2 * l - q;
140
+ const f = (t) => {
141
+ t = ((t % 1) + 1) % 1;
142
+ if (t < 1 / 6)
143
+ return p + (q - p) * 6 * t;
144
+ if (t < 1 / 2)
145
+ return q;
146
+ if (t < 2 / 3)
147
+ return p + (q - p) * (2 / 3 - t) * 6;
148
+ return p;
149
+ };
150
+ return [f(h + 1 / 3), f(h), f(h - 1 / 3)];
151
+ }
package/dist/post.d.ts ADDED
@@ -0,0 +1,57 @@
1
+ export declare function makeProg(gl: WebGL2RenderingContext, vs: string, fs: string): WebGLProgram;
2
+ /** A program plus a uniform-location cache — getUniformLocation once, ever. */
3
+ export declare class PInfo {
4
+ private gl;
5
+ prog: WebGLProgram;
6
+ private locs;
7
+ constructor(gl: WebGL2RenderingContext, vs: string, fs: string);
8
+ u(name: string): WebGLUniformLocation | null;
9
+ use(): void;
10
+ }
11
+ export type PostParams = {
12
+ bloomOn: boolean;
13
+ bloomThreshold: number;
14
+ bloomKnee: number;
15
+ bloomStrength: number;
16
+ ssaoOn: boolean;
17
+ ssaoStrength: number;
18
+ ssaoRadius: number;
19
+ filmic: boolean;
20
+ grade: boolean;
21
+ vignette: number;
22
+ ink: boolean;
23
+ fxaa: boolean;
24
+ near: number;
25
+ far: number;
26
+ };
27
+ export declare class PostChain {
28
+ readonly hdr: boolean;
29
+ private gl;
30
+ private pBright;
31
+ private pBlur;
32
+ private pSsao;
33
+ private pAoBlur;
34
+ private pComposite;
35
+ private pFxaa;
36
+ private scene;
37
+ sceneDepth: WebGLTexture | null;
38
+ private bloomA;
39
+ private bloomB;
40
+ private aoA;
41
+ private aoB;
42
+ private ldr;
43
+ private W;
44
+ private H;
45
+ constructor(gl: WebGL2RenderingContext);
46
+ private makeTarget;
47
+ private drop;
48
+ /** (Re)allocate every target at the given DEVICE-pixel size. */
49
+ resize(w: number, h: number): void;
50
+ /** Bind the scene target — the world's render pass draws into this. */
51
+ begin(): void;
52
+ private fst;
53
+ private bindTex;
54
+ /** Run the chain: bloom + ssao + composite (+ fxaa) to the canvas. */
55
+ end(o: PostParams): void;
56
+ dispose(): void;
57
+ }