@chromatic-coherence/generative-charts-3d 0.12.1 → 0.13.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/index.d.ts CHANGED
@@ -61,6 +61,11 @@ export type WorldOpts = {
61
61
  parallax?: number;
62
62
  controls?: boolean;
63
63
  shadows?: boolean;
64
+ /** v0.13 — THE INK EDITION: theme "light" renders the same recipe on paper instead of night.
65
+ * One rule, applied everywhere: lightness inverts (the dark ground becomes paper, bright
66
+ * light becomes ink), the light pass blends normally instead of additively, and fog fades
67
+ * toward the page instead of toward black. Recipes need no changes. Default "dark". */
68
+ theme?: "dark" | "light";
64
69
  camera?: {
65
70
  target?: V3;
66
71
  dist?: number;
@@ -104,6 +109,7 @@ export declare class World {
104
109
  private disposed;
105
110
  private cleanup;
106
111
  private fLight;
112
+ private ink;
107
113
  cam: {
108
114
  target: V3;
109
115
  dist: number;
@@ -158,6 +164,8 @@ export declare class World {
158
164
  constructor(o: WorldOpts);
159
165
  private wireControls;
160
166
  private resize;
167
+ private col;
168
+ private parseCol;
161
169
  private pushFace;
162
170
  /** v0.2 — pass per-vertex normals (na, nb, nc) and shading interpolates SMOOTHLY across
163
171
  * the face: the facets vanish. Omit them for flat shading, exactly as before. */
package/dist/index.js CHANGED
@@ -101,7 +101,7 @@ uniform vec3 uLdir; uniform float uAmb; uniform float uDif; uniform float uSigne
101
101
  uniform vec3 uLp[4]; uniform float uLi[4]; uniform float uLf[4]; uniform int uLn;
102
102
  uniform vec3 uCam; uniform float uFogK; uniform float uSpec;
103
103
  uniform vec3 uSkyC; uniform vec3 uGroundC; uniform float uFilm; uniform float uGrade;
104
- uniform float uShadowOn; uniform sampler2D uShadow; uniform float uPulse;
104
+ uniform float uShadowOn; uniform sampler2D uShadow; uniform float uPulse; uniform float uInk;
105
105
  varying vec4 vSh; varying float vGA; varying float vBlood;
106
106
  float unpack(vec4 e){ return dot(e, vec4(1., 1./255., 1./65025., 1./16581375.)); }
107
107
  float h3(vec3 p){ return fract(sin(dot(p, vec3(12.9898, 78.233, 37.719))) * 43758.5453); }
@@ -174,7 +174,10 @@ void main(){
174
174
  float fres = pow(1.0 - clamp(dot(n, V), 0.0, 1.0), 3.0);
175
175
  lit += vec3(0.55, 0.09, 0.10) * fres * skin * (0.28 + 0.5 * b);
176
176
  float fog = clamp(uFogK / vZ, 0.08, 1.0);
177
- vec3 outC = (lit + vec3(1.0) * spec * uSpec * vCol.a) * fog * vGA;
177
+ vec3 outC = lit + vec3(1.0) * spec * uSpec * vCol.a;
178
+ // ink edition: fog fades toward the paper, not toward black (the page shows through)
179
+ if (uInk > 0.5) outC = mix(vec3(0.90, 0.92, 0.95), outC, fog * vGA);
180
+ else outC = outC * fog * vGA;
178
181
  if (uFilm > 0.5) { // v0.7 — the film look: highlights roll off instead of clipping
179
182
  outC = (outC * (2.51 * outC + 0.03)) / (outC * (2.43 * outC + 0.59) + 0.14);
180
183
  }
@@ -191,8 +194,11 @@ uniform mat4 uVP; uniform float uGA[8];
191
194
  varying vec4 vCol; varying float vZ; varying float vGA;
192
195
  void main(){ vCol=aCol; vGA=uGA[int(aGrp+0.5)]; vec4 p=uVP*vec4(aPos,1.0); vZ=p.w; gl_Position=p; }`;
193
196
  const LINE_FS = `
194
- precision mediump float; varying vec4 vCol; varying float vZ; varying float vGA; uniform float uFogK;
195
- void main(){ float fog = clamp(uFogK / vZ, 0.08, 1.0); gl_FragColor = vec4(vCol.rgb * fog, vCol.a * fog * vGA); }`;
197
+ precision mediump float; varying vec4 vCol; varying float vZ; varying float vGA; uniform float uFogK; uniform float uInk;
198
+ void main(){ float fog = clamp(uFogK / vZ, 0.08, 1.0);
199
+ // ink edition: colour holds, alpha fades — the line washes toward the paper behind it
200
+ if (uInk > 0.5) gl_FragColor = vec4(vCol.rgb, vCol.a * fog * vGA);
201
+ else gl_FragColor = vec4(vCol.rgb * fog, vCol.a * fog * vGA); }`;
196
202
  const PT_VS = `
197
203
  attribute vec3 aPos; attribute float aSize; attribute vec4 aCol; attribute float aGrp;
198
204
  uniform mat4 uVP; uniform float uFocal; uniform float uGA[8];
@@ -200,12 +206,15 @@ varying vec4 vCol; varying float vZ; varying float vGA;
200
206
  void main(){ vCol=aCol; vGA=uGA[int(aGrp+0.5)]; vec4 p=uVP*vec4(aPos,1.0); vZ=p.w;
201
207
  gl_PointSize = clamp(aSize * uFocal / p.w, 1.0, 256.0); gl_Position=p; }`;
202
208
  const PT_FS = `
203
- precision mediump float; varying vec4 vCol; varying float vZ; varying float vGA; uniform float uFogK;
209
+ precision mediump float; varying vec4 vCol; varying float vZ; varying float vGA; uniform float uFogK; uniform float uInk;
204
210
  void main(){
205
211
  float r = length(gl_PointCoord - 0.5) * 2.0;
206
212
  float a = exp(-3.2 * r * r) * (1.0 - smoothstep(0.85, 1.0, r));
207
213
  float fog = clamp(uFogK / vZ, 0.08, 1.0);
208
- gl_FragColor = vec4(vCol.rgb, 1.0) * (vCol.a * a * fog * vGA);
214
+ float A = vCol.a * a * fog * vGA;
215
+ // ink edition: an ink blot blended normally, not a premultiplied additive glow
216
+ if (uInk > 0.5) gl_FragColor = vec4(vCol.rgb, A);
217
+ else gl_FragColor = vec4(vCol.rgb, 1.0) * A;
209
218
  }`;
210
219
  function makeProg(gl, vs, fs) {
211
220
  const sh = (type, src) => {
@@ -224,11 +233,6 @@ function makeProg(gl, vs, fs) {
224
233
  throw new Error("dark-math-3d link: " + gl.getProgramInfoLog(p));
225
234
  return p;
226
235
  }
227
- /** Parse a css hsl(...) string the way the 2d examples write them. */
228
- function parseHsl(c) {
229
- const m = /hsl\(\s*([\d.]+)[deg\s]*[, ]\s*([\d.]+)%[, ]\s*([\d.]+)%/.exec(c);
230
- return m ? hsl(+m[1], +m[2], +m[3]) : [0.9, 0.9, 1];
231
- }
232
236
  export class World {
233
237
  idxFor(name) {
234
238
  const n = name ?? "default";
@@ -260,6 +264,7 @@ export class World {
260
264
  this.disposed = false;
261
265
  this.cleanup = [];
262
266
  this.fLight = [];
267
+ this.ink = false; // v0.13 — theme "light": ink on paper instead of light on black
263
268
  this.cam = { target: v3(0, 0, 0), dist: 700, pitch: 0.5, yawBase: 0, yawUser: 0, pitchUser: 0, distT: 700, fov: 1.15 };
264
269
  this.light = { dir: v3(0, 1, 0), ambient: 0.55, diffuse: 0.45, signed: false };
265
270
  this.lights = [];
@@ -300,6 +305,7 @@ export class World {
300
305
  if (!gl)
301
306
  throw new Error("dark-math-3d: no webgl context");
302
307
  this.gl = gl;
308
+ this.ink = o.theme === "light";
303
309
  this.opts = {
304
310
  maxDpr: o.maxDpr ?? 2,
305
311
  drift: o.drift ?? 0.045,
@@ -416,10 +422,18 @@ export class World {
416
422
  this.og.setTransform(dpr, 0, 0, dpr, 0, 0);
417
423
  this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
418
424
  }
425
+ // ── the ink edition's one rule: lightness inverts; recipe colours pass through unchanged when dark ──
426
+ col(h, s, l) {
427
+ return hsl(h, s, this.ink ? 100 - l : l);
428
+ }
429
+ parseCol(c) {
430
+ const m = /hsl\(\s*([\d.]+)[deg\s]*[, ]\s*([\d.]+)%[, ]\s*([\d.]+)%/.exec(c);
431
+ return m ? this.col(+m[1], +m[2], +m[3]) : (this.ink ? [0.18, 0.18, 0.28] : [0.9, 0.9, 1]);
432
+ }
419
433
  // ── building the static world (same verbs as 2d) ──
420
434
  pushFace(store, a, b, c, o, na, nb, nc) {
421
435
  const n = na ? undefined : norm(cross(sub(b, a), sub(c, a)));
422
- const [r, g, bl] = hsl(o.hue, o.sat ?? 45, o.light ?? 30);
436
+ const [r, g, bl] = this.col(o.hue, o.sat ?? 45, o.light ?? 30);
423
437
  const al = clamp(o.alpha ?? 1, 0, 1);
424
438
  const gi = this.idxFor(o.group);
425
439
  // faces are opaque (blend off), so the colour's alpha slot carries the GLOSS — documented
@@ -437,18 +451,21 @@ export class World {
437
451
  this.staticDirty = true;
438
452
  }
439
453
  line(a, b, o) {
440
- const [r, g, bl] = hsl(o.hue, o.sat ?? 80, o.light ?? 55);
454
+ const [r, g, bl] = this.col(o.hue, o.sat ?? 80, o.light ?? 55);
441
455
  const al = clamp(o.alpha, 0, 1), gi = this.idxFor(o.group);
442
456
  this.sLine.push(a.x, a.y, a.z, r, g, bl, al, gi, b.x, b.y, b.z, r, g, bl, al, gi);
443
457
  this.staticDirty = true;
444
458
  }
445
459
  glow(p, o) {
446
- const [r, g, bl] = o.white ? [1, 1, 1] : hsl(o.hue ?? 0, 85, 62);
460
+ const [r, g, bl] = o.white ? (this.ink ? [0.16, 0.15, 0.24] : [1, 1, 1]) : this.col(o.hue ?? 0, 85, 62);
447
461
  this.sPt.push(p.x, p.y, p.z, o.size, r, g, bl, clamp(o.alpha, 0, 1), this.idxFor(o.group));
448
462
  this.staticDirty = true;
449
463
  }
450
464
  dust(p, alpha) {
451
- this.sPt.push(p.x, p.y, p.z, 1.6, 0.87, 0.89, 1, clamp(alpha * 0.6, 0, 1), 0);
465
+ if (this.ink)
466
+ this.sPt.push(p.x, p.y, p.z, 1.6, 0.29, 0.31, 0.45, clamp(alpha * 0.6, 0, 1), 0);
467
+ else
468
+ this.sPt.push(p.x, p.y, p.z, 1.6, 0.87, 0.89, 1, clamp(alpha * 0.6, 0, 1), 0);
452
469
  this.staticDirty = true;
453
470
  }
454
471
  grow(fn) { this.hooks.push(fn); }
@@ -529,15 +546,15 @@ export class World {
529
546
  continue;
530
547
  }
531
548
  if (rec.k === "glow") {
532
- const [r, g, b] = rec.hue == null ? [1, 1, 1] : hsl(rec.hue, 85, 62);
549
+ const [r, g, b] = rec.hue == null ? (this.ink ? [0.16, 0.15, 0.24] : [1, 1, 1]) : this.col(rec.hue, 85, 62);
533
550
  this.dPtA.push(rec.p.x, rec.p.y, rec.p.z, rec.size, r, g, b, clamp(rec.alpha, 0, 1), 0);
534
551
  }
535
552
  else if (rec.k === "line") {
536
- const [r, g, b] = parseHsl(rec.color);
553
+ const [r, g, b] = this.parseCol(rec.color);
537
554
  this.dLineA.push(rec.a.x, rec.a.y, rec.a.z, r, g, b, rec.alpha, 0, rec.b.x, rec.b.y, rec.b.z, r, g, b, rec.alpha, 0);
538
555
  }
539
556
  else if (rec.k === "poly") {
540
- const [r, g, b] = parseHsl(rec.color);
557
+ const [r, g, b] = this.parseCol(rec.color);
541
558
  for (let i = 1; i < rec.pts.length; i++) {
542
559
  const a = rec.pts[i - 1].p, q = rec.pts[i].p;
543
560
  this.dLineA.push(a.x, a.y, a.z, r, g, b, rec.alpha, 0, q.x, q.y, q.z, r, g, b, rec.alpha, 0);
@@ -594,9 +611,13 @@ export class World {
594
611
  if (this.dFaceA.length)
595
612
  this.drawFaces(this.bFaceD, this.dFaceA.length / 12, null);
596
613
  // the light — additive, depth-TESTED but not written: the dark occludes the light, exactly
614
+ // (ink edition: normal alpha blending — ink layers over paper instead of light adding up)
597
615
  gl.depthMask(false);
598
616
  gl.enable(gl.BLEND);
599
- gl.blendFunc(gl.ONE, gl.ONE);
617
+ if (this.ink)
618
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
619
+ else
620
+ gl.blendFunc(gl.ONE, gl.ONE);
600
621
  gl.useProgram(this.pLine);
601
622
  this.setVP(this.pLine);
602
623
  this.setFog(this.pLine, fogK);
@@ -630,7 +651,7 @@ export class World {
630
651
  const g = this.og, W = this.W, H = this.H;
631
652
  const grd = g.createRadialGradient(W / 2, H * 0.46, Math.min(W, H) * 0.28, W / 2, H / 2, Math.max(W, H) * 0.72);
632
653
  grd.addColorStop(0, "rgba(0,0,0,0)");
633
- grd.addColorStop(1, `rgba(6,4,14,${this.vignette})`);
654
+ grd.addColorStop(1, this.ink ? `rgba(52,48,76,${this.vignette * 0.3})` : `rgba(6,4,14,${this.vignette})`);
634
655
  g.fillStyle = grd;
635
656
  g.fillRect(0, 0, W, H);
636
657
  }
@@ -641,12 +662,14 @@ export class World {
641
662
  }
642
663
  setFog(p, fogK) {
643
664
  this.gl.uniform1f(this.gl.getUniformLocation(p, "uFogK"), fogK);
665
+ this.gl.uniform1f(this.gl.getUniformLocation(p, "uInk"), this.ink ? 1 : 0);
644
666
  }
645
667
  setLights(p, fogK) {
646
668
  const gl = this.gl, L = norm(this.light.dir);
647
669
  gl.uniform3f(gl.getUniformLocation(p, "uLdir"), L.x, L.y, L.z);
648
- gl.uniform1f(gl.getUniformLocation(p, "uAmb"), this.light.ambient);
649
- gl.uniform1f(gl.getUniformLocation(p, "uDif"), this.light.diffuse);
670
+ // ink edition: paper wants gentler shading — lift the ambient, soften the diffuse swing
671
+ gl.uniform1f(gl.getUniformLocation(p, "uAmb"), this.ink ? Math.min(1, this.light.ambient + 0.22) : this.light.ambient);
672
+ gl.uniform1f(gl.getUniformLocation(p, "uDif"), this.ink ? this.light.diffuse * 0.55 : this.light.diffuse);
650
673
  gl.uniform1f(gl.getUniformLocation(p, "uSigned"), this.light.signed ? 1 : 0);
651
674
  gl.uniform3f(gl.getUniformLocation(p, "uCam"), this.cx.x, this.cx.y, this.cx.z);
652
675
  gl.uniform1f(gl.getUniformLocation(p, "uSpec"), this.specular);
@@ -654,7 +677,8 @@ export class World {
654
677
  gl.uniform1f(gl.getUniformLocation(p, "uGrade"), this.grade ? 1 : 0);
655
678
  gl.uniform3f(gl.getUniformLocation(p, "uSkyC"), this.ambientSky.x, this.ambientSky.y, this.ambientSky.z);
656
679
  gl.uniform3f(gl.getUniformLocation(p, "uGroundC"), this.ambientGround.x, this.ambientGround.y, this.ambientGround.z);
657
- gl.uniform1f(gl.getUniformLocation(p, "uFilm"), this.filmic ? 1 : 0);
680
+ // ink edition: the filmic curve would grey the paper itself — tone mapping is a night look
681
+ gl.uniform1f(gl.getUniformLocation(p, "uFilm"), this.filmic && !this.ink ? 1 : 0);
658
682
  gl.uniformMatrix4fv(gl.getUniformLocation(p, "uSVP"), false, this.svp);
659
683
  gl.uniform1f(gl.getUniformLocation(p, "uShadowOn"), this.shadowsOn && this.shadowTex && this.lights.length ? 1 : 0);
660
684
  if (this.shadowTex) {
@@ -766,6 +790,11 @@ export class World {
766
790
  c();
767
791
  this.cleanup = [];
768
792
  this.overlay.remove();
793
+ // Release the GL context NOW — browsers cap live WebGL contexts per page (~16) and only
794
+ // reclaim them at GC, so a page that builds and disposes many worlds otherwise exhausts
795
+ // the pool and every later createWorld fails. A lost context is permanently dead on its
796
+ // canvas: mount a fresh canvas to build again.
797
+ this.gl.getExtension("WEBGL_lose_context")?.loseContext();
769
798
  }
770
799
  }
771
800
  export function createWorld(o) { return new World(o); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chromatic-coherence/generative-charts-3d",
3
- "version": "0.12.1",
3
+ "version": "0.13.0",
4
4
  "description": "The second generative-charts engine — the same API as generative-charts-2d, rendered on WebGL: a real depth buffer (occlusion exact, always on, for solids and light) and per-pixel Lambert + point lights. Zero dependencies.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",