@chromatic-coherence/generative-charts-3d 0.12.2 → 0.13.1

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,11 @@ export declare class World {
158
164
  constructor(o: WorldOpts);
159
165
  private wireControls;
160
166
  private resize;
167
+ private col;
168
+ private parseCol;
169
+ private inkCss;
170
+ /** Remap a recipe-supplied css colour for the ink edition's overlay labels (identity when dark). */
171
+ private themedCss;
161
172
  private pushFace;
162
173
  /** v0.2 — pass per-vertex normals (na, nb, nc) and shading interpolates SMOOTHLY across
163
174
  * 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 = [];
@@ -295,11 +300,13 @@ export class World {
295
300
  this.fw = v3();
296
301
  this.focal = 0;
297
302
  this.vp = new Float32Array(16);
303
+ this.inkCss = new Map();
298
304
  this.canvas = o.canvas;
299
305
  const gl = o.canvas.getContext("webgl", { alpha: true, antialias: true, premultipliedAlpha: true });
300
306
  if (!gl)
301
307
  throw new Error("dark-math-3d: no webgl context");
302
308
  this.gl = gl;
309
+ this.ink = o.theme === "light";
303
310
  this.opts = {
304
311
  maxDpr: o.maxDpr ?? 2,
305
312
  drift: o.drift ?? 0.045,
@@ -416,10 +423,30 @@ export class World {
416
423
  this.og.setTransform(dpr, 0, 0, dpr, 0, 0);
417
424
  this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
418
425
  }
426
+ // ── the ink edition's one rule: lightness inverts; recipe colours pass through unchanged when dark ──
427
+ col(h, s, l) {
428
+ return hsl(h, s, this.ink ? 100 - l : l);
429
+ }
430
+ parseCol(c) {
431
+ const m = /hsl\(\s*([\d.]+)[deg\s]*[, ]\s*([\d.]+)%[, ]\s*([\d.]+)%/.exec(c);
432
+ return m ? this.col(+m[1], +m[2], +m[3]) : (this.ink ? [0.18, 0.18, 0.28] : [0.9, 0.9, 1]);
433
+ }
434
+ /** Remap a recipe-supplied css colour for the ink edition's overlay labels (identity when dark). */
435
+ themedCss(color) {
436
+ if (!this.ink)
437
+ return color;
438
+ let c = this.inkCss.get(color);
439
+ if (!c) {
440
+ const m = /hsl\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%\s*\)/.exec(color);
441
+ c = m ? `hsl(${m[1]} ${m[2]}% ${clamp(100 - +m[3], 0, 100)}%)` : color;
442
+ this.inkCss.set(color, c);
443
+ }
444
+ return c;
445
+ }
419
446
  // ── building the static world (same verbs as 2d) ──
420
447
  pushFace(store, a, b, c, o, na, nb, nc) {
421
448
  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);
449
+ const [r, g, bl] = this.col(o.hue, o.sat ?? 45, o.light ?? 30);
423
450
  const al = clamp(o.alpha ?? 1, 0, 1);
424
451
  const gi = this.idxFor(o.group);
425
452
  // faces are opaque (blend off), so the colour's alpha slot carries the GLOSS — documented
@@ -437,18 +464,21 @@ export class World {
437
464
  this.staticDirty = true;
438
465
  }
439
466
  line(a, b, o) {
440
- const [r, g, bl] = hsl(o.hue, o.sat ?? 80, o.light ?? 55);
467
+ const [r, g, bl] = this.col(o.hue, o.sat ?? 80, o.light ?? 55);
441
468
  const al = clamp(o.alpha, 0, 1), gi = this.idxFor(o.group);
442
469
  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
470
  this.staticDirty = true;
444
471
  }
445
472
  glow(p, o) {
446
- const [r, g, bl] = o.white ? [1, 1, 1] : hsl(o.hue ?? 0, 85, 62);
473
+ 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
474
  this.sPt.push(p.x, p.y, p.z, o.size, r, g, bl, clamp(o.alpha, 0, 1), this.idxFor(o.group));
448
475
  this.staticDirty = true;
449
476
  }
450
477
  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);
478
+ if (this.ink)
479
+ this.sPt.push(p.x, p.y, p.z, 1.6, 0.29, 0.31, 0.45, clamp(alpha * 0.6, 0, 1), 0);
480
+ else
481
+ this.sPt.push(p.x, p.y, p.z, 1.6, 0.87, 0.89, 1, clamp(alpha * 0.6, 0, 1), 0);
452
482
  this.staticDirty = true;
453
483
  }
454
484
  grow(fn) { this.hooks.push(fn); }
@@ -529,15 +559,15 @@ export class World {
529
559
  continue;
530
560
  }
531
561
  if (rec.k === "glow") {
532
- const [r, g, b] = rec.hue == null ? [1, 1, 1] : hsl(rec.hue, 85, 62);
562
+ 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
563
  this.dPtA.push(rec.p.x, rec.p.y, rec.p.z, rec.size, r, g, b, clamp(rec.alpha, 0, 1), 0);
534
564
  }
535
565
  else if (rec.k === "line") {
536
- const [r, g, b] = parseHsl(rec.color);
566
+ const [r, g, b] = this.parseCol(rec.color);
537
567
  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
568
  }
539
569
  else if (rec.k === "poly") {
540
- const [r, g, b] = parseHsl(rec.color);
570
+ const [r, g, b] = this.parseCol(rec.color);
541
571
  for (let i = 1; i < rec.pts.length; i++) {
542
572
  const a = rec.pts[i - 1].p, q = rec.pts[i].p;
543
573
  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 +624,13 @@ export class World {
594
624
  if (this.dFaceA.length)
595
625
  this.drawFaces(this.bFaceD, this.dFaceA.length / 12, null);
596
626
  // the light — additive, depth-TESTED but not written: the dark occludes the light, exactly
627
+ // (ink edition: normal alpha blending — ink layers over paper instead of light adding up)
597
628
  gl.depthMask(false);
598
629
  gl.enable(gl.BLEND);
599
- gl.blendFunc(gl.ONE, gl.ONE);
630
+ if (this.ink)
631
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
632
+ else
633
+ gl.blendFunc(gl.ONE, gl.ONE);
600
634
  gl.useProgram(this.pLine);
601
635
  this.setVP(this.pLine);
602
636
  this.setFog(this.pLine, fogK);
@@ -622,7 +656,7 @@ export class World {
622
656
  if (!q)
623
657
  continue;
624
658
  this.og.globalAlpha = clamp(l.alpha, 0, 1);
625
- this.og.fillStyle = l.color;
659
+ this.og.fillStyle = this.themedCss(l.color);
626
660
  this.og.fillText(l.text, q.x, q.y + l.dy);
627
661
  }
628
662
  this.og.globalAlpha = 1;
@@ -630,7 +664,7 @@ export class World {
630
664
  const g = this.og, W = this.W, H = this.H;
631
665
  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
666
  grd.addColorStop(0, "rgba(0,0,0,0)");
633
- grd.addColorStop(1, `rgba(6,4,14,${this.vignette})`);
667
+ grd.addColorStop(1, this.ink ? `rgba(52,48,76,${this.vignette * 0.3})` : `rgba(6,4,14,${this.vignette})`);
634
668
  g.fillStyle = grd;
635
669
  g.fillRect(0, 0, W, H);
636
670
  }
@@ -641,12 +675,14 @@ export class World {
641
675
  }
642
676
  setFog(p, fogK) {
643
677
  this.gl.uniform1f(this.gl.getUniformLocation(p, "uFogK"), fogK);
678
+ this.gl.uniform1f(this.gl.getUniformLocation(p, "uInk"), this.ink ? 1 : 0);
644
679
  }
645
680
  setLights(p, fogK) {
646
681
  const gl = this.gl, L = norm(this.light.dir);
647
682
  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);
683
+ // ink edition: paper wants gentler shading — lift the ambient, soften the diffuse swing
684
+ gl.uniform1f(gl.getUniformLocation(p, "uAmb"), this.ink ? Math.min(1, this.light.ambient + 0.22) : this.light.ambient);
685
+ gl.uniform1f(gl.getUniformLocation(p, "uDif"), this.ink ? this.light.diffuse * 0.55 : this.light.diffuse);
650
686
  gl.uniform1f(gl.getUniformLocation(p, "uSigned"), this.light.signed ? 1 : 0);
651
687
  gl.uniform3f(gl.getUniformLocation(p, "uCam"), this.cx.x, this.cx.y, this.cx.z);
652
688
  gl.uniform1f(gl.getUniformLocation(p, "uSpec"), this.specular);
@@ -654,7 +690,8 @@ export class World {
654
690
  gl.uniform1f(gl.getUniformLocation(p, "uGrade"), this.grade ? 1 : 0);
655
691
  gl.uniform3f(gl.getUniformLocation(p, "uSkyC"), this.ambientSky.x, this.ambientSky.y, this.ambientSky.z);
656
692
  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);
693
+ // ink edition: the filmic curve would grey the paper itself — tone mapping is a night look
694
+ gl.uniform1f(gl.getUniformLocation(p, "uFilm"), this.filmic && !this.ink ? 1 : 0);
658
695
  gl.uniformMatrix4fv(gl.getUniformLocation(p, "uSVP"), false, this.svp);
659
696
  gl.uniform1f(gl.getUniformLocation(p, "uShadowOn"), this.shadowsOn && this.shadowTex && this.lights.length ? 1 : 0);
660
697
  if (this.shadowTex) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chromatic-coherence/generative-charts-3d",
3
- "version": "0.12.2",
3
+ "version": "0.13.1",
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",