@firecms/neat 0.8.0 → 0.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.
@@ -1,5 +1,5 @@
1
1
  import { buildColorFunctions, buildNoise, buildVertUniforms, buildFragUniforms, fragmentShaderSource, vertexShaderSource } from "./shaders";
2
- import { generatePlaneGeometry, OrthographicCamera, updateCamera, Matrix4 } from "./math";
2
+ import { generatePlaneGeometry, generateSphereGeometry, generateTorusGeometry, generateCylinderGeometry, generateRibbonGeometry, OrthographicCamera, updateCamera, Matrix4 } from "./math";
3
3
  console.info("%c🌈 Neat Gradients%c\n\nLicensed under MIT + The Commons Clause.\nFree for personal and commercial use.\nSelling this software or its derivatives is strictly prohibited.\nhttps://neat.firecms.co", "font-weight: bold; font-size: 14px; color: #FF5772;", "color: inherit;");
4
4
  const PLANE_WIDTH = 50;
5
5
  const PLANE_HEIGHT = 80;
@@ -22,6 +22,7 @@ export class NeatGradient {
22
22
  _grainSparsity = -1;
23
23
  _grainSpeed = -1;
24
24
  _colorBlending = -1;
25
+ _resolution = 1;
25
26
  _colors = [];
26
27
  _wireframe = false;
27
28
  _backgroundColor = "#FFFFFF";
@@ -43,6 +44,7 @@ export class NeatGradient {
43
44
  _textureColorBlending = 0.01;
44
45
  _textureSeed = 333;
45
46
  _textureEase = 0.5;
47
+ _transparentTextureVoid = false;
46
48
  // New effects
47
49
  _domainWarpEnabled = false;
48
50
  _domainWarpIntensity = 0.5;
@@ -60,6 +62,32 @@ export class NeatGradient {
60
62
  _bloomIntensity = 0;
61
63
  _bloomThreshold = 0.7;
62
64
  _chromaticAberration = 0;
65
+ _silhouetteFade = 0.25;
66
+ _cylinderFade = 0.08;
67
+ _ribbonFade = 0.05;
68
+ // 3D Shapes config
69
+ _shapeType = 'plane';
70
+ _shapeRotationX = 0;
71
+ _shapeRotationY = 0;
72
+ _shapeRotationZ = 0;
73
+ _shapeAutoRotateSpeedX = 0;
74
+ _shapeAutoRotateSpeedY = 0;
75
+ _sphereRadius = 15;
76
+ _torusRadius = 15;
77
+ _torusTube = 5;
78
+ _cylinderRadius = 10;
79
+ _cylinderHeight = 40;
80
+ _planeBend = 0;
81
+ _planeTwist = 0;
82
+ // Camera settings
83
+ _cameraLock = false;
84
+ _cameraX = 0;
85
+ _cameraY = 0;
86
+ _cameraZ = 0;
87
+ _cameraRotationX = 0;
88
+ _cameraRotationY = 0;
89
+ _cameraRotationZ = 0;
90
+ _cameraZoom = 1.0;
63
91
  _proceduralTexture = null;
64
92
  _proceduralBackgroundColor = "#000000";
65
93
  _textureShapeTriangles = 20;
@@ -75,6 +103,11 @@ export class NeatGradient {
75
103
  _yOffsetWaveMultiplier = 0.004;
76
104
  _yOffsetColorMultiplier = 0.004;
77
105
  _yOffsetFlowMultiplier = 0.004;
106
+ // Cached offscreen canvases for procedural texture generation
107
+ _sourceCanvas = null;
108
+ _sourceCtx = null;
109
+ _maskedCanvas = null;
110
+ _maskedCtx = null;
78
111
  // Performance optimizations
79
112
  _resizeTimeoutId = null;
80
113
  _textureNeedsUpdate = false;
@@ -87,7 +120,11 @@ export class NeatGradient {
87
120
  // Flow field parameters
88
121
  flowDistortionA = 0, flowDistortionB = 0, flowScale = 1.0, flowEase = 0.0, flowEnabled = true,
89
122
  // Texture generation
90
- enableProceduralTexture = false, textureVoidLikelihood = 0.45, textureVoidWidthMin = 200, textureVoidWidthMax = 486, textureBandDensity = 2.15, textureColorBlending = 0.01, textureSeed = 333, textureEase = 0.5, proceduralBackgroundColor = "#000000", textureShapeTriangles = 20, textureShapeCircles = 15, textureShapeBars = 15, textureShapeSquiggles = 10, domainWarpEnabled = false, domainWarpIntensity = 0.5, domainWarpScale = 1.0, vignetteIntensity = 0.5, vignetteRadius = 0.8, fresnelEnabled = false, fresnelPower = 2.0, fresnelIntensity = 0.5, fresnelColor = "#FFFFFF", iridescenceEnabled = false, iridescenceIntensity = 0.5, iridescenceSpeed = 1.0, bloomIntensity = 0.0, bloomThreshold = 0.7, chromaticAberration = 0.0, } = config;
123
+ enableProceduralTexture = false, textureVoidLikelihood = 0.45, textureVoidWidthMin = 200, textureVoidWidthMax = 486, textureBandDensity = 2.15, textureColorBlending = 0.01, textureSeed = 333, textureEase = 0.5, proceduralBackgroundColor = "#000000", transparentTextureVoid = false, textureShapeTriangles = 20, textureShapeCircles = 15, textureShapeBars = 15, textureShapeSquiggles = 10, domainWarpEnabled = false, domainWarpIntensity = 0.5, domainWarpScale = 1.0, vignetteIntensity = 0.0, vignetteRadius = 0.8, fresnelEnabled = false, fresnelPower = 2.0, fresnelIntensity = 0.5, fresnelColor = "#FFFFFF", iridescenceEnabled = false, iridescenceIntensity = 0.5, iridescenceSpeed = 1.0, bloomIntensity = 0.0, bloomThreshold = 0.7, chromaticAberration = 0.0, silhouetteFade = 0.25, cylinderFade = 0.08, ribbonFade = 0.05,
124
+ // Camera configuration
125
+ cameraLock = false, cameraX = 0, cameraY = 0, cameraZ = 0, cameraRotationX = 0, cameraRotationY = 0, cameraRotationZ = 0, cameraZoom = 1.0,
126
+ // 3D shapes default
127
+ shapeType = 'plane', shapeRotationX = 0, shapeRotationY = 0, shapeRotationZ = 0, shapeAutoRotateSpeedX = 0, shapeAutoRotateSpeedY = 0, sphereRadius = 15, torusRadius = 15, torusTube = 5, cylinderRadius = 10, cylinderHeight = 40, planeBend = 0, planeTwist = 0, } = config;
91
128
  this._ref = ref;
92
129
  this.destroy = this.destroy.bind(this);
93
130
  this._initScene = this._initScene.bind(this);
@@ -98,6 +135,7 @@ export class NeatGradient {
98
135
  this.waveFrequencyY = waveFrequencyY;
99
136
  this.waveAmplitude = waveAmplitude;
100
137
  this.colorBlending = colorBlending;
138
+ this._resolution = resolution;
101
139
  this.grainScale = grainScale;
102
140
  this.grainIntensity = grainIntensity;
103
141
  this.grainSparsity = grainSparsity;
@@ -130,6 +168,7 @@ export class NeatGradient {
130
168
  this.textureSeed = textureSeed;
131
169
  this.textureEase = textureEase;
132
170
  this._proceduralBackgroundColor = proceduralBackgroundColor;
171
+ this.transparentTextureVoid = transparentTextureVoid;
133
172
  this._textureShapeTriangles = textureShapeTriangles;
134
173
  this._textureShapeCircles = textureShapeCircles;
135
174
  this._textureShapeBars = textureShapeBars;
@@ -149,6 +188,30 @@ export class NeatGradient {
149
188
  this.bloomIntensity = bloomIntensity;
150
189
  this.bloomThreshold = bloomThreshold;
151
190
  this.chromaticAberration = chromaticAberration;
191
+ this.silhouetteFade = silhouetteFade;
192
+ this.cylinderFade = cylinderFade;
193
+ this.ribbonFade = ribbonFade;
194
+ this._cameraLock = cameraLock;
195
+ this._cameraX = cameraX;
196
+ this._cameraY = cameraY;
197
+ this._cameraZ = cameraZ;
198
+ this._cameraRotationX = cameraRotationX;
199
+ this._cameraRotationY = cameraRotationY;
200
+ this._cameraRotationZ = cameraRotationZ;
201
+ this._cameraZoom = cameraZoom;
202
+ this._shapeType = shapeType;
203
+ this._shapeRotationX = shapeRotationX;
204
+ this._shapeRotationY = shapeRotationY;
205
+ this._shapeRotationZ = shapeRotationZ;
206
+ this._shapeAutoRotateSpeedX = shapeAutoRotateSpeedX;
207
+ this._shapeAutoRotateSpeedY = shapeAutoRotateSpeedY;
208
+ this._sphereRadius = sphereRadius;
209
+ this._torusRadius = torusRadius;
210
+ this._torusTube = torusTube;
211
+ this._cylinderRadius = cylinderRadius;
212
+ this._cylinderHeight = cylinderHeight;
213
+ this._planeBend = planeBend;
214
+ this._planeTwist = planeTwist;
152
215
  this.glState = this._initScene(resolution);
153
216
  injectSEO();
154
217
  let tick = seed !== undefined ? seed : getElapsedSecondsInLastHour();
@@ -169,6 +232,36 @@ export class NeatGradient {
169
232
  lastTime = timeNow;
170
233
  gl.useProgram(program);
171
234
  gl.uniform1f(locations.uniforms['u_time'], tick);
235
+ // Update modelViewMatrix in every frame to support dynamic rotation and auto-rotation
236
+ const camera = this.glState.camera;
237
+ const modelViewMatrix = new Matrix4();
238
+ // 1. Camera translation (default camera distance + displacement)
239
+ modelViewMatrix.translate(-camera.position[0] - this._cameraX, -camera.position[1] - this._cameraY, -camera.position[2] - this._cameraZ);
240
+ modelViewMatrix.translate(0, 0, -1);
241
+ // 2. Camera rotation (revolving around target)
242
+ modelViewMatrix.rotateX(-this._cameraRotationX);
243
+ modelViewMatrix.rotateY(-this._cameraRotationY);
244
+ modelViewMatrix.rotateZ(-this._cameraRotationZ);
245
+ let rx = this._shapeRotationX;
246
+ let ry = this._shapeRotationY;
247
+ let rz = this._shapeRotationZ;
248
+ if (this._shapeAutoRotateSpeedX !== 0) {
249
+ rx += tick * this._shapeAutoRotateSpeedX * 0.1;
250
+ }
251
+ if (this._shapeAutoRotateSpeedY !== 0) {
252
+ ry += tick * this._shapeAutoRotateSpeedY * 0.1;
253
+ }
254
+ if (this._shapeType === 'plane' || this._shapeType === 'ribbon') {
255
+ modelViewMatrix.rotateX(rx - Math.PI / 3.5);
256
+ }
257
+ else {
258
+ modelViewMatrix.rotateX(rx);
259
+ }
260
+ modelViewMatrix.rotateY(ry);
261
+ modelViewMatrix.rotateZ(rz);
262
+ const mvLoc = locations.uniforms["modelViewMatrix"];
263
+ if (mvLoc)
264
+ gl.uniformMatrix4fv(mvLoc, false, modelViewMatrix.elements);
172
265
  // Only upload static uniforms when they've been modified
173
266
  if (this._uniformsDirty) {
174
267
  gl.uniform2f(locations.uniforms['u_resolution'], this._ref.clientWidth, this._ref.clientHeight);
@@ -194,8 +287,19 @@ export class NeatGradient {
194
287
  gl.uniform1f(locations.uniforms['u_flow_scale'], this._flowScale);
195
288
  gl.uniform1f(locations.uniforms['u_flow_ease'], this._flowEase);
196
289
  gl.uniform1f(locations.uniforms['u_flow_enabled'], this._flowEnabled ? 1.0 : 0.0);
290
+ let shapeTypeVal = 0.0;
291
+ if (this._shapeType === 'sphere')
292
+ shapeTypeVal = 1.0;
293
+ else if (this._shapeType === 'torus')
294
+ shapeTypeVal = 2.0;
295
+ else if (this._shapeType === 'cylinder')
296
+ shapeTypeVal = 3.0;
297
+ else if (this._shapeType === 'ribbon')
298
+ shapeTypeVal = 4.0;
299
+ gl.uniform1f(locations.uniforms['u_shape_type'], shapeTypeVal);
197
300
  gl.uniform1f(locations.uniforms['u_enable_procedural_texture'], this._enableProceduralTexture ? 1.0 : 0.0);
198
301
  gl.uniform1f(locations.uniforms['u_texture_ease'], this._textureEase);
302
+ gl.uniform1f(locations.uniforms['u_transparent_texture_void'], this._transparentTextureVoid ? 1.0 : 0.0);
199
303
  gl.uniform1f(locations.uniforms['u_domain_warp_enabled'], this._domainWarpEnabled ? 1.0 : 0.0);
200
304
  gl.uniform1f(locations.uniforms['u_domain_warp_intensity'], this._domainWarpIntensity);
201
305
  gl.uniform1f(locations.uniforms['u_domain_warp_scale'], this._domainWarpScale);
@@ -211,6 +315,9 @@ export class NeatGradient {
211
315
  gl.uniform1f(locations.uniforms['u_bloom_intensity'], this._bloomIntensity);
212
316
  gl.uniform1f(locations.uniforms['u_bloom_threshold'], this._bloomThreshold);
213
317
  gl.uniform1f(locations.uniforms['u_chromatic_aberration'], this._chromaticAberration);
318
+ gl.uniform1f(locations.uniforms['u_silhouette_fade'], this._silhouetteFade);
319
+ gl.uniform1f(locations.uniforms['u_cylinder_fade'], this._cylinderFade);
320
+ gl.uniform1f(locations.uniforms['u_ribbon_fade'], this._ribbonFade);
214
321
  this._uniformsDirty = false;
215
322
  }
216
323
  // Only regenerate procedural texture when needed
@@ -268,11 +375,12 @@ export class NeatGradient {
268
375
  this._ref.width = width;
269
376
  this._ref.height = height;
270
377
  gl.viewport(0, 0, width, height);
271
- updateCamera(camera, width, height);
378
+ updateCamera(camera, width, height, PLANE_WIDTH, PLANE_HEIGHT, this._shapeType, this._cameraZoom);
272
379
  // Recompute projection matrix on resize
273
- const projLoc = gl.getUniformLocation(this.glState.program, "projectionMatrix");
380
+ const projLoc = this.glState.locations.uniforms["projectionMatrix"];
274
381
  gl.useProgram(this.glState.program);
275
- gl.uniformMatrix4fv(projLoc, false, camera.projectionMatrix.elements);
382
+ if (projLoc)
383
+ gl.uniformMatrix4fv(projLoc, false, camera.projectionMatrix.elements);
276
384
  };
277
385
  // Debounce resize to prevent excessive operations
278
386
  this.sizeObserver = new ResizeObserver(() => {
@@ -318,98 +426,292 @@ export class NeatGradient {
318
426
  const dataURL = this._ref.toDataURL("image/png");
319
427
  downloadURI(dataURL, filename);
320
428
  }
429
+ /**
430
+ * Records the canvas animation as a video with a NEAT watermark overlay.
431
+ * @param options.durationMs Recording duration in milliseconds (default 5000).
432
+ * @param options.filename Output file name (default "neat.firecms.co").
433
+ * @param options.width Output video width in pixels (default: current canvas width).
434
+ * @param options.height Output video height in pixels (default: current canvas height).
435
+ * @param options.format Preferred format: 'mp4' or 'webm' (default: best available).
436
+ * @param options.onProgress Callback with progress 0-1.
437
+ * @param options.onComplete Callback when recording finishes.
438
+ * @returns A stop function to end recording early.
439
+ */
440
+ recordVideo(options = {}) {
441
+ const { durationMs = 5000, filename = "neat.firecms.co", format, onProgress, onComplete, } = options;
442
+ const source = this._ref;
443
+ const width = options.width || source.width || source.clientWidth;
444
+ const height = options.height || source.height || source.clientHeight;
445
+ // Offscreen canvas that composites gradient + watermark each frame
446
+ const offscreen = document.createElement("canvas");
447
+ offscreen.width = width;
448
+ offscreen.height = height;
449
+ const ctx = offscreen.getContext("2d");
450
+ // Use captureStream(0) — only captures a frame when we explicitly
451
+ // call requestFrame() on the video track, so every composited frame
452
+ // is guaranteed to be captured.
453
+ const stream = offscreen.captureStream(0);
454
+ const videoTrack = stream.getVideoTracks()[0];
455
+ // Codec candidates ordered by preference
456
+ const mp4Candidates = [
457
+ "video/mp4;codecs=avc1",
458
+ "video/mp4;codecs=avc1,opus",
459
+ "video/mp4",
460
+ ];
461
+ const webmCandidates = [
462
+ "video/webm;codecs=vp9,opus",
463
+ "video/webm;codecs=vp9",
464
+ "video/webm;codecs=vp8,opus",
465
+ "video/webm",
466
+ ];
467
+ // Build candidate list based on preferred format
468
+ let candidates;
469
+ if (format === 'mp4')
470
+ candidates = [...mp4Candidates, ...webmCandidates];
471
+ else if (format === 'webm')
472
+ candidates = [...webmCandidates, ...mp4Candidates];
473
+ else
474
+ candidates = [...mp4Candidates, ...webmCandidates];
475
+ let mimeType = "video/webm";
476
+ for (const candidate of candidates) {
477
+ if (MediaRecorder.isTypeSupported(candidate)) {
478
+ mimeType = candidate;
479
+ break;
480
+ }
481
+ }
482
+ // Scale bitrate with pixel count: 8 Mbps baseline at 720p
483
+ const pixels = width * height;
484
+ const baseBitrate = 8_000_000;
485
+ const basePixels = 1280 * 720;
486
+ const videoBitsPerSecond = Math.round(baseBitrate * Math.max(1, pixels / basePixels));
487
+ const recorder = new MediaRecorder(stream, {
488
+ mimeType,
489
+ videoBitsPerSecond,
490
+ });
491
+ const chunks = [];
492
+ recorder.ondataavailable = (e) => {
493
+ if (e.data.size > 0)
494
+ chunks.push(e.data);
495
+ };
496
+ let stopped = false;
497
+ let rafId;
498
+ const startTime = performance.now();
499
+ let lastProgressTime = 0;
500
+ // Composite loop: draw source canvas + watermark overlay on each frame
501
+ const drawFrame = () => {
502
+ if (stopped)
503
+ return;
504
+ ctx.clearRect(0, 0, width, height);
505
+ ctx.drawImage(source, 0, 0, width, height);
506
+ // Watermark: "NEAT" in bottom-right corner
507
+ const fontSize = Math.max(14, Math.round(height * 0.025));
508
+ ctx.font = `bold ${fontSize}px "Sofia Sans", sans-serif`;
509
+ ctx.textAlign = "right";
510
+ ctx.textBaseline = "bottom";
511
+ ctx.shadowColor = "rgba(0,0,0,0.5)";
512
+ ctx.shadowBlur = 4;
513
+ ctx.shadowOffsetX = 1;
514
+ ctx.shadowOffsetY = 1;
515
+ ctx.fillStyle = "rgba(255,255,255,0.7)";
516
+ ctx.fillText("NEAT", width - fontSize * 0.8, height - fontSize * 0.5);
517
+ ctx.shadowColor = "transparent";
518
+ ctx.shadowBlur = 0;
519
+ ctx.shadowOffsetX = 0;
520
+ ctx.shadowOffsetY = 0;
521
+ // Signal the stream to capture this frame
522
+ // @ts-ignore – requestFrame exists on CanvasCaptureMediaStreamTrack
523
+ if (videoTrack.requestFrame)
524
+ videoTrack.requestFrame();
525
+ // Throttle progress to ~4 updates/sec to avoid flooding React state
526
+ if (onProgress) {
527
+ const now = performance.now();
528
+ if (now - lastProgressTime > 250) {
529
+ lastProgressTime = now;
530
+ onProgress(Math.min(0.99, (now - startTime) / durationMs));
531
+ }
532
+ }
533
+ rafId = requestAnimationFrame(drawFrame);
534
+ };
535
+ recorder.onstop = () => {
536
+ stopped = true;
537
+ cancelAnimationFrame(rafId);
538
+ // Use the correct file extension for the actual format
539
+ const isMP4 = mimeType.startsWith("video/mp4");
540
+ const ext = isMP4 ? ".mp4" : ".webm";
541
+ const blobType = isMP4 ? "video/mp4" : "video/webm";
542
+ const finalFilename = filename + ext;
543
+ const blob = new Blob(chunks, { type: blobType });
544
+ const url = URL.createObjectURL(blob);
545
+ downloadURI(url, finalFilename);
546
+ setTimeout(() => URL.revokeObjectURL(url), 30000);
547
+ onProgress?.(1);
548
+ onComplete?.();
549
+ };
550
+ // Start drawing frames, then start recording
551
+ drawFrame();
552
+ recorder.start(100); // collect data every 100ms
553
+ // Auto-stop after the requested duration
554
+ const timeoutId = window.setTimeout(() => {
555
+ if (recorder.state === "recording") {
556
+ recorder.stop();
557
+ }
558
+ }, durationMs);
559
+ // Return a stop function for early termination
560
+ return () => {
561
+ clearTimeout(timeoutId);
562
+ if (recorder.state === "recording") {
563
+ recorder.stop();
564
+ }
565
+ };
566
+ }
567
+ get speed() {
568
+ return this._speed * 20;
569
+ }
321
570
  set speed(speed) {
322
571
  this._uniformsDirty = true;
323
572
  this._speed = speed / 20;
324
573
  }
574
+ get horizontalPressure() {
575
+ return this._horizontalPressure * 4;
576
+ }
325
577
  set horizontalPressure(horizontalPressure) {
326
578
  this._uniformsDirty = true;
327
579
  this._horizontalPressure = horizontalPressure / 4;
328
580
  }
581
+ get verticalPressure() {
582
+ return this._verticalPressure * 4;
583
+ }
329
584
  set verticalPressure(verticalPressure) {
330
585
  this._uniformsDirty = true;
331
586
  this._verticalPressure = verticalPressure / 4;
332
587
  }
588
+ get waveFrequencyX() {
589
+ return this._waveFrequencyX / 0.04;
590
+ }
333
591
  set waveFrequencyX(waveFrequencyX) {
334
592
  this._uniformsDirty = true;
335
593
  this._waveFrequencyX = waveFrequencyX * 0.04;
336
594
  }
595
+ get waveFrequencyY() {
596
+ return this._waveFrequencyY / 0.04;
597
+ }
337
598
  set waveFrequencyY(waveFrequencyY) {
338
599
  this._uniformsDirty = true;
339
600
  this._waveFrequencyY = waveFrequencyY * 0.04;
340
601
  }
602
+ get waveAmplitude() {
603
+ return this._waveAmplitude / 0.75;
604
+ }
341
605
  set waveAmplitude(waveAmplitude) {
342
606
  this._uniformsDirty = true;
343
607
  this._waveAmplitude = waveAmplitude * .75;
344
608
  }
609
+ get colors() {
610
+ return this._colors;
611
+ }
345
612
  set colors(colors) {
346
613
  this._uniformsDirty = true;
347
614
  this._colors = colors;
348
615
  this._cachedColorRgb = colors.map(c => this._hexToRgb(c.color));
349
616
  this._colorsChanged = true;
350
617
  }
618
+ get highlights() {
619
+ return this._highlights * 100;
620
+ }
351
621
  set highlights(highlights) {
352
622
  this._uniformsDirty = true;
353
623
  this._highlights = highlights / 100;
354
624
  }
625
+ get shadows() {
626
+ return this._shadows * 100;
627
+ }
355
628
  set shadows(shadows) {
356
629
  this._uniformsDirty = true;
357
630
  this._shadows = shadows / 100;
358
631
  }
632
+ get colorSaturation() {
633
+ return this._saturation * 10;
634
+ }
359
635
  set colorSaturation(colorSaturation) {
360
636
  this._uniformsDirty = true;
361
637
  this._saturation = colorSaturation / 10;
362
638
  }
639
+ get colorBrightness() {
640
+ return this._brightness;
641
+ }
363
642
  set colorBrightness(colorBrightness) {
364
643
  this._uniformsDirty = true;
365
644
  this._brightness = colorBrightness;
366
645
  }
646
+ get colorBlending() {
647
+ return this._colorBlending * 10;
648
+ }
367
649
  set colorBlending(colorBlending) {
368
650
  this._uniformsDirty = true;
369
651
  this._colorBlending = colorBlending / 10;
370
652
  }
653
+ get grainScale() {
654
+ return this._grainScale;
655
+ }
371
656
  set grainScale(grainScale) {
372
657
  this._uniformsDirty = true;
373
658
  this._grainScale = grainScale == 0 ? 1 : grainScale;
374
659
  }
660
+ get grainIntensity() {
661
+ return this._grainIntensity;
662
+ }
375
663
  set grainIntensity(grainIntensity) {
376
664
  this._uniformsDirty = true;
377
665
  this._grainIntensity = grainIntensity;
378
666
  }
667
+ get grainSparsity() {
668
+ return this._grainSparsity;
669
+ }
379
670
  set grainSparsity(grainSparsity) {
380
671
  this._uniformsDirty = true;
381
672
  this._grainSparsity = grainSparsity;
382
673
  }
674
+ get grainSpeed() {
675
+ return this._grainSpeed;
676
+ }
383
677
  set grainSpeed(grainSpeed) {
384
678
  this._uniformsDirty = true;
385
679
  this._grainSpeed = grainSpeed;
386
680
  }
681
+ get wireframe() {
682
+ return this._wireframe;
683
+ }
387
684
  set wireframe(wireframe) {
388
685
  this._uniformsDirty = true;
389
686
  this._wireframe = wireframe;
390
687
  }
688
+ get resolution() {
689
+ return this._resolution;
690
+ }
391
691
  set resolution(resolution) {
392
- this._uniformsDirty = true;
393
- if (this.glState) {
394
- const gl = this.glState.gl;
395
- gl.deleteProgram(this.glState.program);
396
- gl.deleteBuffer(this.glState.buffers.position);
397
- gl.deleteBuffer(this.glState.buffers.normal);
398
- gl.deleteBuffer(this.glState.buffers.uv);
399
- gl.deleteBuffer(this.glState.buffers.index);
400
- gl.deleteBuffer(this.glState.buffers.wireframeIndex);
401
- }
402
- this.glState = this._initScene(resolution);
692
+ if (this._resolution === resolution)
693
+ return;
694
+ this._resolution = resolution;
695
+ this._updateGeometry();
696
+ }
697
+ get backgroundColor() {
698
+ return this._backgroundColor;
403
699
  }
404
700
  set backgroundColor(backgroundColor) {
405
701
  this._uniformsDirty = true;
406
702
  this._backgroundColor = backgroundColor;
407
703
  this._backgroundColorRgb = this._hexToRgb(backgroundColor);
408
704
  }
705
+ get backgroundAlpha() {
706
+ return this._backgroundAlpha;
707
+ }
409
708
  set backgroundAlpha(backgroundAlpha) {
410
709
  this._uniformsDirty = true;
411
710
  this._backgroundAlpha = backgroundAlpha;
412
711
  }
712
+ get yOffset() {
713
+ return this._yOffset;
714
+ }
413
715
  set yOffset(yOffset) {
414
716
  this._uniformsDirty = true;
415
717
  this._yOffset = yOffset;
@@ -435,18 +737,30 @@ export class NeatGradient {
435
737
  this._uniformsDirty = true;
436
738
  this._yOffsetFlowMultiplier = value / 1000;
437
739
  }
740
+ get flowDistortionA() {
741
+ return this._flowDistortionA;
742
+ }
438
743
  set flowDistortionA(value) {
439
744
  this._uniformsDirty = true;
440
745
  this._flowDistortionA = value;
441
746
  }
747
+ get flowDistortionB() {
748
+ return this._flowDistortionB;
749
+ }
442
750
  set flowDistortionB(value) {
443
751
  this._uniformsDirty = true;
444
752
  this._flowDistortionB = value;
445
753
  }
754
+ get flowScale() {
755
+ return this._flowScale;
756
+ }
446
757
  set flowScale(value) {
447
758
  this._uniformsDirty = true;
448
759
  this._flowScale = value;
449
760
  }
761
+ get flowEase() {
762
+ return this._flowEase;
763
+ }
450
764
  set flowEase(value) {
451
765
  this._uniformsDirty = true;
452
766
  this._flowEase = value;
@@ -458,6 +772,9 @@ export class NeatGradient {
458
772
  get flowEnabled() {
459
773
  return this._flowEnabled;
460
774
  }
775
+ get enableProceduralTexture() {
776
+ return this._enableProceduralTexture;
777
+ }
461
778
  set enableProceduralTexture(value) {
462
779
  this._uniformsDirty = true;
463
780
  this._enableProceduralTexture = value;
@@ -465,6 +782,9 @@ export class NeatGradient {
465
782
  this._textureNeedsUpdate = true;
466
783
  }
467
784
  }
785
+ get textureVoidLikelihood() {
786
+ return this._textureVoidLikelihood;
787
+ }
468
788
  set textureVoidLikelihood(value) {
469
789
  this._uniformsDirty = true;
470
790
  this._textureVoidLikelihood = value;
@@ -472,6 +792,9 @@ export class NeatGradient {
472
792
  this._textureNeedsUpdate = true;
473
793
  }
474
794
  }
795
+ get textureVoidWidthMin() {
796
+ return this._textureVoidWidthMin;
797
+ }
475
798
  set textureVoidWidthMin(value) {
476
799
  this._uniformsDirty = true;
477
800
  this._textureVoidWidthMin = value;
@@ -479,6 +802,9 @@ export class NeatGradient {
479
802
  this._textureNeedsUpdate = true;
480
803
  }
481
804
  }
805
+ get textureVoidWidthMax() {
806
+ return this._textureVoidWidthMax;
807
+ }
482
808
  set textureVoidWidthMax(value) {
483
809
  this._uniformsDirty = true;
484
810
  this._textureVoidWidthMax = value;
@@ -486,6 +812,9 @@ export class NeatGradient {
486
812
  this._textureNeedsUpdate = true;
487
813
  }
488
814
  }
815
+ get textureBandDensity() {
816
+ return this._textureBandDensity;
817
+ }
489
818
  set textureBandDensity(value) {
490
819
  this._uniformsDirty = true;
491
820
  this._textureBandDensity = value;
@@ -493,6 +822,9 @@ export class NeatGradient {
493
822
  this._textureNeedsUpdate = true;
494
823
  }
495
824
  }
825
+ get textureColorBlending() {
826
+ return this._textureColorBlending;
827
+ }
496
828
  set textureColorBlending(value) {
497
829
  this._uniformsDirty = true;
498
830
  this._textureColorBlending = value;
@@ -500,6 +832,9 @@ export class NeatGradient {
500
832
  this._textureNeedsUpdate = true;
501
833
  }
502
834
  }
835
+ get textureSeed() {
836
+ return this._textureSeed;
837
+ }
503
838
  set textureSeed(value) {
504
839
  this._uniformsDirty = true;
505
840
  this._textureSeed = value;
@@ -514,6 +849,19 @@ export class NeatGradient {
514
849
  this._uniformsDirty = true;
515
850
  this._textureEase = value;
516
851
  }
852
+ get transparentTextureVoid() {
853
+ return this._transparentTextureVoid;
854
+ }
855
+ set transparentTextureVoid(value) {
856
+ this._uniformsDirty = true;
857
+ this._transparentTextureVoid = value;
858
+ if (this._enableProceduralTexture) {
859
+ this._textureNeedsUpdate = true;
860
+ }
861
+ }
862
+ get proceduralBackgroundColor() {
863
+ return this._proceduralBackgroundColor;
864
+ }
517
865
  set proceduralBackgroundColor(value) {
518
866
  this._uniformsDirty = true;
519
867
  this._proceduralBackgroundColor = value;
@@ -521,30 +869,90 @@ export class NeatGradient {
521
869
  this._textureNeedsUpdate = true;
522
870
  }
523
871
  }
872
+ get textureShapeTriangles() {
873
+ return this._textureShapeTriangles;
874
+ }
524
875
  set textureShapeTriangles(value) {
525
876
  this._uniformsDirty = true;
526
877
  this._textureShapeTriangles = value;
527
878
  if (this._enableProceduralTexture)
528
879
  this._textureNeedsUpdate = true;
529
880
  }
881
+ get textureShapeCircles() {
882
+ return this._textureShapeCircles;
883
+ }
530
884
  set textureShapeCircles(value) {
531
885
  this._uniformsDirty = true;
532
886
  this._textureShapeCircles = value;
533
887
  if (this._enableProceduralTexture)
534
888
  this._textureNeedsUpdate = true;
535
889
  }
890
+ get textureShapeBars() {
891
+ return this._textureShapeBars;
892
+ }
536
893
  set textureShapeBars(value) {
537
894
  this._uniformsDirty = true;
538
895
  this._textureShapeBars = value;
539
896
  if (this._enableProceduralTexture)
540
897
  this._textureNeedsUpdate = true;
541
898
  }
899
+ get textureShapeSquiggles() {
900
+ return this._textureShapeSquiggles;
901
+ }
542
902
  set textureShapeSquiggles(value) {
543
903
  this._uniformsDirty = true;
544
904
  this._textureShapeSquiggles = value;
545
905
  if (this._enableProceduralTexture)
546
906
  this._textureNeedsUpdate = true;
547
907
  }
908
+ _updateGeometry() {
909
+ if (!this.glState)
910
+ return;
911
+ const gl = this.glState.gl;
912
+ const resolution = this._resolution || 1;
913
+ let geometry;
914
+ if (this._shapeType === 'sphere') {
915
+ geometry = generateSphereGeometry(this._sphereRadius, 120 * resolution, 120 * resolution);
916
+ }
917
+ else if (this._shapeType === 'torus') {
918
+ geometry = generateTorusGeometry(this._torusRadius, this._torusTube, 120 * resolution, 120 * resolution);
919
+ }
920
+ else if (this._shapeType === 'cylinder') {
921
+ geometry = generateCylinderGeometry(this._cylinderRadius, this._cylinderRadius, this._cylinderHeight, 120 * resolution, 120 * resolution);
922
+ }
923
+ else if (this._shapeType === 'ribbon') {
924
+ geometry = generateRibbonGeometry(PLANE_WIDTH, PLANE_HEIGHT, 240 * resolution, 240 * resolution, this._planeBend, this._planeTwist);
925
+ }
926
+ else {
927
+ geometry = generatePlaneGeometry(PLANE_WIDTH, PLANE_HEIGHT, 240 * resolution, 240 * resolution);
928
+ }
929
+ const { position, normal, uv, index, wireframeIndex } = geometry;
930
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.glState.buffers.position);
931
+ gl.bufferData(gl.ARRAY_BUFFER, position, gl.STATIC_DRAW);
932
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.glState.buffers.normal);
933
+ gl.bufferData(gl.ARRAY_BUFFER, normal, gl.STATIC_DRAW);
934
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.glState.buffers.uv);
935
+ gl.bufferData(gl.ARRAY_BUFFER, uv, gl.STATIC_DRAW);
936
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.glState.buffers.index);
937
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, index, gl.STATIC_DRAW);
938
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.glState.buffers.wireframeIndex);
939
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, wireframeIndex, gl.STATIC_DRAW);
940
+ // Restore default bound element buffer
941
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.glState.buffers.index);
942
+ this.glState.indexCount = index.length;
943
+ this.glState.wireframeIndexCount = wireframeIndex.length;
944
+ this.glState.indexType = (index instanceof Uint32Array) ? gl.UNSIGNED_INT : gl.UNSIGNED_SHORT;
945
+ // Keep camera updated with the new shapeType and dimensions
946
+ const width = this._ref.clientWidth;
947
+ const height = this._ref.clientHeight;
948
+ updateCamera(this.glState.camera, width, height, PLANE_WIDTH, PLANE_HEIGHT, this._shapeType, this._cameraZoom);
949
+ // Recompute projection matrix
950
+ const projLoc = this.glState.locations.uniforms["projectionMatrix"];
951
+ gl.useProgram(this.glState.program);
952
+ if (projLoc)
953
+ gl.uniformMatrix4fv(projLoc, false, this.glState.camera.projectionMatrix.elements);
954
+ this._uniformsDirty = true;
955
+ }
548
956
  _hexToRgb(hex) {
549
957
  const bigint = parseInt(hex.replace('#', ''), 16);
550
958
  return [
@@ -564,8 +972,24 @@ export class NeatGradient {
564
972
  const ext = gl.getExtension("OES_standard_derivatives");
565
973
  gl.getExtension("OES_element_index_uint");
566
974
  gl.viewport(0, 0, width, height);
567
- // Generate plane geometry with Uint32Array for large meshes
568
- const { position, normal, uv, index, wireframeIndex } = generatePlaneGeometry(PLANE_WIDTH, PLANE_HEIGHT, 240 * resolution, 240 * resolution);
975
+ // Generate parametric geometry based on shapeType
976
+ let geometry;
977
+ if (this._shapeType === 'sphere') {
978
+ geometry = generateSphereGeometry(this._sphereRadius, 120 * resolution, 120 * resolution);
979
+ }
980
+ else if (this._shapeType === 'torus') {
981
+ geometry = generateTorusGeometry(this._torusRadius, this._torusTube, 120 * resolution, 120 * resolution);
982
+ }
983
+ else if (this._shapeType === 'cylinder') {
984
+ geometry = generateCylinderGeometry(this._cylinderRadius, this._cylinderRadius, this._cylinderHeight, 120 * resolution, 120 * resolution);
985
+ }
986
+ else if (this._shapeType === 'ribbon') {
987
+ geometry = generateRibbonGeometry(PLANE_WIDTH, PLANE_HEIGHT, 240 * resolution, 240 * resolution, this._planeBend, this._planeTwist);
988
+ }
989
+ else {
990
+ geometry = generatePlaneGeometry(PLANE_WIDTH, PLANE_HEIGHT, 240 * resolution, 240 * resolution);
991
+ }
992
+ const { position, normal, uv, index, wireframeIndex } = geometry;
569
993
  const positionBuffer = gl.createBuffer();
570
994
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
571
995
  gl.bufferData(gl.ARRAY_BUFFER, position, gl.STATIC_DRAW);
@@ -620,7 +1044,7 @@ export class NeatGradient {
620
1044
  gl.useProgram(program);
621
1045
  const camera = new OrthographicCamera(0, 0, 0, 0, 0, 1000);
622
1046
  camera.position = [0, 0, 5];
623
- updateCamera(camera, width, height);
1047
+ updateCamera(camera, width, height, PLANE_WIDTH, PLANE_HEIGHT, this._shapeType, this._cameraZoom);
624
1048
  // Define attributes
625
1049
  const aPosition = gl.getAttribLocation(program, "position");
626
1050
  const aNormal = gl.getAttribLocation(program, "normal");
@@ -635,15 +1059,7 @@ export class NeatGradient {
635
1059
  gl.bindBuffer(gl.ARRAY_BUFFER, uvBuffer);
636
1060
  gl.vertexAttribPointer(aUv, 2, gl.FLOAT, false, 0, 0);
637
1061
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
638
- const modelViewMatrix = new Matrix4();
639
- // The View Matrix is the inverse of the Camera's position
640
- // Camera is at [0, 0, 5], so view matrix translates by [0, 0, -5]
641
- modelViewMatrix.translate(-camera.position[0], -camera.position[1], -camera.position[2]);
642
- // The Model Matrix mimicking: plane.rotation.x = -Math.PI / 3.5; plane.position.z = -1;
643
- modelViewMatrix.translate(0, 0, -1);
644
- modelViewMatrix.rotateX(-Math.PI / 3.5);
645
- const mvLoc = gl.getUniformLocation(program, "modelViewMatrix");
646
- gl.uniformMatrix4fv(mvLoc, false, modelViewMatrix.elements);
1062
+ // modelViewMatrix is set dynamically in the render loop
647
1063
  const projLoc = gl.getUniformLocation(program, "projectionMatrix");
648
1064
  gl.uniformMatrix4fv(projLoc, false, camera.projectionMatrix.elements);
649
1065
  const planeWidthLoc = gl.getUniformLocation(program, "u_plane_width");
@@ -653,17 +1069,19 @@ export class NeatGradient {
653
1069
  const colorsCountLoc = gl.getUniformLocation(program, "u_colors_count");
654
1070
  gl.uniform1i(colorsCountLoc, COLORS_COUNT);
655
1071
  const uniformsList = [
1072
+ "projectionMatrix", "modelViewMatrix",
656
1073
  "u_time", "u_resolution", "u_color_pressure", "u_wave_frequency_x", "u_wave_frequency_y",
657
1074
  "u_wave_amplitude", "u_colors_count", "u_plane_width", "u_plane_height", "u_shadows",
658
1075
  "u_highlights", "u_grain_intensity", "u_grain_sparsity", "u_grain_scale", "u_grain_speed",
659
1076
  "u_flow_distortion_a", "u_flow_distortion_b", "u_flow_scale", "u_flow_ease", "u_flow_enabled",
660
1077
  "u_y_offset", "u_y_offset_wave_multiplier", "u_y_offset_color_multiplier", "u_y_offset_flow_multiplier",
661
- "u_procedural_texture", "u_enable_procedural_texture", "u_texture_ease", "u_saturation", "u_brightness", "u_color_blending",
1078
+ "u_procedural_texture", "u_enable_procedural_texture", "u_texture_ease", "u_transparent_texture_void", "u_saturation", "u_brightness", "u_color_blending",
662
1079
  "u_domain_warp_enabled", "u_domain_warp_intensity", "u_domain_warp_scale",
663
1080
  "u_vignette_intensity", "u_vignette_radius",
664
1081
  "u_fresnel_enabled", "u_fresnel_power", "u_fresnel_intensity", "u_fresnel_color",
665
1082
  "u_iridescence_enabled", "u_iridescence_intensity", "u_iridescence_speed",
666
- "u_bloom_intensity", "u_bloom_threshold", "u_chromatic_aberration"
1083
+ "u_bloom_intensity", "u_bloom_threshold", "u_chromatic_aberration",
1084
+ "u_shape_type", "u_silhouette_fade", "u_cylinder_fade", "u_ribbon_fade"
667
1085
  ];
668
1086
  const locations = {
669
1087
  attributes: { position: aPosition, normal: aNormal, uv: aUv },
@@ -708,10 +1126,14 @@ export class NeatGradient {
708
1126
  // Texture size - 1024 provides good balance between quality and performance
709
1127
  // Reduced from 2048 for better performance
710
1128
  const texSize = 1024;
711
- const sourceCanvas = document.createElement('canvas');
712
- sourceCanvas.width = texSize;
713
- sourceCanvas.height = texSize;
714
- const sCtx = sourceCanvas.getContext('2d', { willReadFrequently: true });
1129
+ if (!this._sourceCanvas) {
1130
+ this._sourceCanvas = document.createElement('canvas');
1131
+ this._sourceCanvas.width = texSize;
1132
+ this._sourceCanvas.height = texSize;
1133
+ this._sourceCtx = this._sourceCanvas.getContext('2d');
1134
+ }
1135
+ const sourceCanvas = this._sourceCanvas;
1136
+ const sCtx = this._sourceCtx;
715
1137
  if (!sCtx)
716
1138
  return null;
717
1139
  let seed = this._textureSeed;
@@ -727,6 +1149,9 @@ export class NeatGradient {
727
1149
  const colors = this._colors.filter(c => c.enabled).map(c => c.color);
728
1150
  if (colors.length === 0)
729
1151
  return null;
1152
+ const shouldTile = this._shapeType !== 'plane';
1153
+ const dxs = shouldTile ? [-1, 0, 1] : [0];
1154
+ const dys = shouldTile ? [-1, 0, 1] : [0];
730
1155
  // Helper functions
731
1156
  function hexToRgb(hex) {
732
1157
  const bigint = parseInt(hex.replace('#', ''), 16);
@@ -763,64 +1188,120 @@ export class NeatGradient {
763
1188
  sCtx.fillRect(0, 0, texSize, texSize);
764
1189
  // Triangles: use configurable count
765
1190
  for (let i = 0; i < this._textureShapeTriangles; i++) {
766
- sCtx.fillStyle = getInterColor();
767
- sCtx.beginPath();
1191
+ const fillStyle = getInterColor();
768
1192
  const x = random() * texSize;
769
1193
  const y = random() * texSize;
770
1194
  const s = 100 + random() * 300;
771
- sCtx.moveTo(x, y);
772
- sCtx.lineTo(x + (random() - 0.5) * s, y + (random() - 0.5) * s);
773
- sCtx.lineTo(x + (random() - 0.5) * s, y + (random() - 0.5) * s);
774
- sCtx.fill();
1195
+ const x1 = (random() - 0.5) * s;
1196
+ const y1 = (random() - 0.5) * s;
1197
+ const x2 = (random() - 0.5) * s;
1198
+ const y2 = (random() - 0.5) * s;
1199
+ for (const dx of dxs) {
1200
+ for (const dy of dys) {
1201
+ sCtx.fillStyle = fillStyle;
1202
+ sCtx.beginPath();
1203
+ const tx = x + dx * texSize;
1204
+ const ty = y + dy * texSize;
1205
+ sCtx.moveTo(tx, ty);
1206
+ sCtx.lineTo(tx + x1, ty + y1);
1207
+ sCtx.lineTo(tx + x2, ty + y2);
1208
+ sCtx.fill();
1209
+ }
1210
+ }
775
1211
  }
776
1212
  // Circles / rings: use configurable count
777
1213
  for (let i = 0; i < this._textureShapeCircles; i++) {
778
- sCtx.strokeStyle = getInterColor();
779
- sCtx.lineWidth = 10 + random() * 50;
780
- sCtx.beginPath();
1214
+ const strokeStyle = getInterColor();
1215
+ const lineWidth = 10 + random() * 50;
781
1216
  const x = random() * texSize;
782
1217
  const y = random() * texSize;
783
1218
  const r = 50 + random() * 150;
784
- sCtx.arc(x, y, r, 0, Math.PI * 2);
785
- sCtx.stroke();
1219
+ for (const dx of dxs) {
1220
+ for (const dy of dys) {
1221
+ sCtx.strokeStyle = strokeStyle;
1222
+ sCtx.lineWidth = lineWidth;
1223
+ sCtx.beginPath();
1224
+ sCtx.arc(x + dx * texSize, y + dy * texSize, r, 0, Math.PI * 2);
1225
+ sCtx.stroke();
1226
+ }
1227
+ }
786
1228
  }
787
1229
  // Bars: use configurable count
788
1230
  for (let i = 0; i < this._textureShapeBars; i++) {
789
- sCtx.fillStyle = getInterColor();
790
- sCtx.save();
791
- sCtx.translate(random() * texSize, random() * texSize);
792
- sCtx.rotate(random() * Math.PI);
793
- sCtx.fillRect(-150, -25, 300, 50);
794
- sCtx.restore();
1231
+ const fillStyle = getInterColor();
1232
+ const x = random() * texSize;
1233
+ const y = random() * texSize;
1234
+ const rot = random() * Math.PI;
1235
+ for (const dx of dxs) {
1236
+ for (const dy of dys) {
1237
+ sCtx.fillStyle = fillStyle;
1238
+ sCtx.save();
1239
+ sCtx.translate(x + dx * texSize, y + dy * texSize);
1240
+ sCtx.rotate(rot);
1241
+ sCtx.fillRect(-150, -25, 300, 50);
1242
+ sCtx.restore();
1243
+ }
1244
+ }
795
1245
  }
796
1246
  // Squiggles: use configurable count
797
1247
  sCtx.lineWidth = 15;
798
1248
  sCtx.lineCap = 'round';
799
1249
  for (let i = 0; i < this._textureShapeSquiggles; i++) {
800
- sCtx.strokeStyle = getInterColor();
801
- sCtx.beginPath();
802
- let x = random() * texSize;
803
- let y = random() * texSize;
804
- sCtx.moveTo(x, y);
1250
+ const strokeStyle = getInterColor();
1251
+ const x = random() * texSize;
1252
+ const y = random() * texSize;
1253
+ const curves = [];
1254
+ let cx = 0;
1255
+ let cy = 0;
805
1256
  for (let j = 0; j < 4; j++) {
806
- sCtx.bezierCurveTo(x + (random() - 0.5) * 300, y + (random() - 0.5) * 300, x + (random() - 0.5) * 300, y + (random() - 0.5) * 300, x + (random() - 0.5) * 300, y + (random() - 0.5) * 300);
807
- x += (random() - 0.5) * 300;
808
- y += (random() - 0.5) * 300;
1257
+ const ex = cx + (random() - 0.5) * 300;
1258
+ const ey = cy + (random() - 0.5) * 300;
1259
+ curves.push({
1260
+ cx1: cx + (random() - 0.5) * 300,
1261
+ cy1: cy + (random() - 0.5) * 300,
1262
+ cx2: cx + (random() - 0.5) * 300,
1263
+ cy2: cy + (random() - 0.5) * 300,
1264
+ ex: ex,
1265
+ ey: ey
1266
+ });
1267
+ cx = ex;
1268
+ cy = ey;
1269
+ }
1270
+ for (const dx of dxs) {
1271
+ for (const dy of dys) {
1272
+ sCtx.strokeStyle = strokeStyle;
1273
+ sCtx.beginPath();
1274
+ const tx = x + dx * texSize;
1275
+ const ty = y + dy * texSize;
1276
+ sCtx.moveTo(tx, ty);
1277
+ for (const curve of curves) {
1278
+ sCtx.bezierCurveTo(tx + curve.cx1, ty + curve.cy1, tx + curve.cx2, ty + curve.cy2, tx + curve.ex, ty + curve.ey);
1279
+ }
1280
+ sCtx.stroke();
1281
+ }
809
1282
  }
810
- sCtx.stroke();
811
1283
  }
812
1284
  // === MASKED CANVAS ===
813
1285
  // Masking: Seed isolation
814
1286
  setSeed(50000);
815
- const canvas = document.createElement('canvas');
816
- canvas.width = texSize;
817
- canvas.height = texSize;
818
- const ctx = canvas.getContext('2d', { willReadFrequently: true });
1287
+ if (!this._maskedCanvas) {
1288
+ this._maskedCanvas = document.createElement('canvas');
1289
+ this._maskedCanvas.width = texSize;
1290
+ this._maskedCanvas.height = texSize;
1291
+ this._maskedCtx = this._maskedCanvas.getContext('2d');
1292
+ }
1293
+ const canvas = this._maskedCanvas;
1294
+ const ctx = this._maskedCtx;
819
1295
  if (!ctx)
820
1296
  return null;
821
1297
  // Start filled with the chosen void color so gaps show that color
822
- ctx.fillStyle = baseColor;
823
- ctx.fillRect(0, 0, texSize, texSize);
1298
+ if (this._transparentTextureVoid) {
1299
+ ctx.clearRect(0, 0, texSize, texSize);
1300
+ }
1301
+ else {
1302
+ ctx.fillStyle = baseColor;
1303
+ ctx.fillRect(0, 0, texSize, texSize);
1304
+ }
824
1305
  // Determine layout segments (matter vs void)
825
1306
  let layoutHead = 0;
826
1307
  const segments = [];
@@ -869,54 +1350,108 @@ export class NeatGradient {
869
1350
  }
870
1351
  return tex;
871
1352
  }
1353
+ get silhouetteFade() {
1354
+ return this._silhouetteFade;
1355
+ }
1356
+ set silhouetteFade(value) {
1357
+ if (this._silhouetteFade !== value) {
1358
+ this._silhouetteFade = value;
1359
+ this._uniformsDirty = true;
1360
+ }
1361
+ }
1362
+ get cylinderFade() {
1363
+ return this._cylinderFade;
1364
+ }
1365
+ set cylinderFade(value) {
1366
+ if (this._cylinderFade !== value) {
1367
+ this._cylinderFade = value;
1368
+ this._uniformsDirty = true;
1369
+ }
1370
+ }
1371
+ get ribbonFade() {
1372
+ return this._ribbonFade;
1373
+ }
1374
+ set ribbonFade(value) {
1375
+ if (this._ribbonFade !== value) {
1376
+ this._ribbonFade = value;
1377
+ this._uniformsDirty = true;
1378
+ }
1379
+ }
1380
+ get domainWarpEnabled() {
1381
+ return this._domainWarpEnabled;
1382
+ }
872
1383
  set domainWarpEnabled(enabled) {
873
1384
  if (this._domainWarpEnabled !== enabled) {
874
1385
  this._domainWarpEnabled = enabled;
875
1386
  this._uniformsDirty = true;
876
1387
  }
877
1388
  }
1389
+ get domainWarpIntensity() {
1390
+ return this._domainWarpIntensity;
1391
+ }
878
1392
  set domainWarpIntensity(intensity) {
879
1393
  if (this._domainWarpIntensity !== intensity) {
880
1394
  this._domainWarpIntensity = intensity;
881
1395
  this._uniformsDirty = true;
882
1396
  }
883
1397
  }
1398
+ get domainWarpScale() {
1399
+ return this._domainWarpScale;
1400
+ }
884
1401
  set domainWarpScale(scale) {
885
1402
  if (this._domainWarpScale !== scale) {
886
1403
  this._domainWarpScale = scale;
887
1404
  this._uniformsDirty = true;
888
1405
  }
889
1406
  }
1407
+ get vignetteIntensity() {
1408
+ return this._vignetteIntensity;
1409
+ }
890
1410
  set vignetteIntensity(intensity) {
891
1411
  if (this._vignetteIntensity !== intensity) {
892
1412
  this._vignetteIntensity = intensity;
893
1413
  this._uniformsDirty = true;
894
1414
  }
895
1415
  }
1416
+ get vignetteRadius() {
1417
+ return this._vignetteRadius;
1418
+ }
896
1419
  set vignetteRadius(radius) {
897
1420
  if (this._vignetteRadius !== radius) {
898
1421
  this._vignetteRadius = radius;
899
1422
  this._uniformsDirty = true;
900
1423
  }
901
1424
  }
1425
+ get fresnelEnabled() {
1426
+ return this._fresnelEnabled;
1427
+ }
902
1428
  set fresnelEnabled(enabled) {
903
1429
  if (this._fresnelEnabled !== enabled) {
904
1430
  this._fresnelEnabled = enabled;
905
1431
  this._uniformsDirty = true;
906
1432
  }
907
1433
  }
1434
+ get fresnelPower() {
1435
+ return this._fresnelPower;
1436
+ }
908
1437
  set fresnelPower(power) {
909
1438
  if (this._fresnelPower !== power) {
910
1439
  this._fresnelPower = power;
911
1440
  this._uniformsDirty = true;
912
1441
  }
913
1442
  }
1443
+ get fresnelIntensity() {
1444
+ return this._fresnelIntensity;
1445
+ }
914
1446
  set fresnelIntensity(intensity) {
915
1447
  if (this._fresnelIntensity !== intensity) {
916
1448
  this._fresnelIntensity = intensity;
917
1449
  this._uniformsDirty = true;
918
1450
  }
919
1451
  }
1452
+ get fresnelColor() {
1453
+ return this._fresnelColor;
1454
+ }
920
1455
  set fresnelColor(fresnelColor) {
921
1456
  if (this._fresnelColor !== fresnelColor) {
922
1457
  this._fresnelColor = fresnelColor;
@@ -924,42 +1459,199 @@ export class NeatGradient {
924
1459
  this._uniformsDirty = true;
925
1460
  }
926
1461
  }
1462
+ get iridescenceEnabled() {
1463
+ return this._iridescenceEnabled;
1464
+ }
927
1465
  set iridescenceEnabled(enabled) {
928
1466
  if (this._iridescenceEnabled !== enabled) {
929
1467
  this._iridescenceEnabled = enabled;
930
1468
  this._uniformsDirty = true;
931
1469
  }
932
1470
  }
1471
+ get iridescenceIntensity() {
1472
+ return this._iridescenceIntensity;
1473
+ }
933
1474
  set iridescenceIntensity(intensity) {
934
1475
  if (this._iridescenceIntensity !== intensity) {
935
1476
  this._iridescenceIntensity = intensity;
936
1477
  this._uniformsDirty = true;
937
1478
  }
938
1479
  }
1480
+ get iridescenceSpeed() {
1481
+ return this._iridescenceSpeed;
1482
+ }
939
1483
  set iridescenceSpeed(speed) {
940
1484
  if (this._iridescenceSpeed !== speed) {
941
1485
  this._iridescenceSpeed = speed;
942
1486
  this._uniformsDirty = true;
943
1487
  }
944
1488
  }
1489
+ get bloomIntensity() {
1490
+ return this._bloomIntensity;
1491
+ }
945
1492
  set bloomIntensity(intensity) {
946
1493
  if (this._bloomIntensity !== intensity) {
947
1494
  this._bloomIntensity = intensity;
948
1495
  this._uniformsDirty = true;
949
1496
  }
950
1497
  }
1498
+ get bloomThreshold() {
1499
+ return this._bloomThreshold;
1500
+ }
951
1501
  set bloomThreshold(threshold) {
952
1502
  if (this._bloomThreshold !== threshold) {
953
1503
  this._bloomThreshold = threshold;
954
1504
  this._uniformsDirty = true;
955
1505
  }
956
1506
  }
1507
+ get chromaticAberration() {
1508
+ return this._chromaticAberration;
1509
+ }
957
1510
  set chromaticAberration(aberration) {
958
1511
  if (this._chromaticAberration !== aberration) {
959
1512
  this._chromaticAberration = aberration;
960
1513
  this._uniformsDirty = true;
961
1514
  }
962
1515
  }
1516
+ // Getters and Setters for 3D Shapes
1517
+ get shapeType() {
1518
+ return this._shapeType;
1519
+ }
1520
+ set shapeType(val) {
1521
+ if (this._shapeType !== val) {
1522
+ this._shapeType = val;
1523
+ this._updateGeometry();
1524
+ }
1525
+ }
1526
+ get shapeRotationX() { return this._shapeRotationX; }
1527
+ set shapeRotationX(val) {
1528
+ this._shapeRotationX = val;
1529
+ this._uniformsDirty = true;
1530
+ }
1531
+ get shapeRotationY() { return this._shapeRotationY; }
1532
+ set shapeRotationY(val) {
1533
+ this._shapeRotationY = val;
1534
+ this._uniformsDirty = true;
1535
+ }
1536
+ get shapeRotationZ() { return this._shapeRotationZ; }
1537
+ set shapeRotationZ(val) {
1538
+ this._shapeRotationZ = val;
1539
+ this._uniformsDirty = true;
1540
+ }
1541
+ get shapeAutoRotateSpeedX() { return this._shapeAutoRotateSpeedX; }
1542
+ set shapeAutoRotateSpeedX(val) {
1543
+ this._shapeAutoRotateSpeedX = val;
1544
+ this._uniformsDirty = true;
1545
+ }
1546
+ get shapeAutoRotateSpeedY() { return this._shapeAutoRotateSpeedY; }
1547
+ set shapeAutoRotateSpeedY(val) {
1548
+ this._shapeAutoRotateSpeedY = val;
1549
+ this._uniformsDirty = true;
1550
+ }
1551
+ get sphereRadius() { return this._sphereRadius; }
1552
+ set sphereRadius(val) {
1553
+ if (this._sphereRadius !== val) {
1554
+ this._sphereRadius = val;
1555
+ this._updateGeometry();
1556
+ }
1557
+ }
1558
+ get torusRadius() { return this._torusRadius; }
1559
+ set torusRadius(val) {
1560
+ if (this._torusRadius !== val) {
1561
+ this._torusRadius = val;
1562
+ this._updateGeometry();
1563
+ }
1564
+ }
1565
+ get torusTube() { return this._torusTube; }
1566
+ set torusTube(val) {
1567
+ if (this._torusTube !== val) {
1568
+ this._torusTube = val;
1569
+ this._updateGeometry();
1570
+ }
1571
+ }
1572
+ get cylinderRadius() { return this._cylinderRadius; }
1573
+ set cylinderRadius(val) {
1574
+ if (this._cylinderRadius !== val) {
1575
+ this._cylinderRadius = val;
1576
+ this._updateGeometry();
1577
+ }
1578
+ }
1579
+ get cylinderHeight() { return this._cylinderHeight; }
1580
+ set cylinderHeight(val) {
1581
+ if (this._cylinderHeight !== val) {
1582
+ this._cylinderHeight = val;
1583
+ this._updateGeometry();
1584
+ }
1585
+ }
1586
+ get planeBend() { return this._planeBend; }
1587
+ set planeBend(val) {
1588
+ if (this._planeBend !== val) {
1589
+ this._planeBend = val;
1590
+ this._updateGeometry();
1591
+ }
1592
+ }
1593
+ get planeTwist() { return this._planeTwist; }
1594
+ set planeTwist(val) {
1595
+ if (this._planeTwist !== val) {
1596
+ this._planeTwist = val;
1597
+ this._updateGeometry();
1598
+ }
1599
+ }
1600
+ // Camera Getters and Setters
1601
+ get cameraLock() { return this._cameraLock; }
1602
+ set cameraLock(val) {
1603
+ this._cameraLock = val;
1604
+ }
1605
+ get cameraX() { return this._cameraX; }
1606
+ set cameraX(val) {
1607
+ this._cameraX = val;
1608
+ this._uniformsDirty = true;
1609
+ }
1610
+ get cameraY() { return this._cameraY; }
1611
+ set cameraY(val) {
1612
+ this._cameraY = val;
1613
+ this._uniformsDirty = true;
1614
+ }
1615
+ get cameraZ() { return this._cameraZ; }
1616
+ set cameraZ(val) {
1617
+ this._cameraZ = val;
1618
+ this._uniformsDirty = true;
1619
+ }
1620
+ get cameraRotationX() { return this._cameraRotationX; }
1621
+ set cameraRotationX(val) {
1622
+ this._cameraRotationX = val;
1623
+ this._uniformsDirty = true;
1624
+ }
1625
+ get cameraRotationY() { return this._cameraRotationY; }
1626
+ set cameraRotationY(val) {
1627
+ this._cameraRotationY = val;
1628
+ this._uniformsDirty = true;
1629
+ }
1630
+ get cameraRotationZ() { return this._cameraRotationZ; }
1631
+ set cameraRotationZ(val) {
1632
+ this._cameraRotationZ = val;
1633
+ this._uniformsDirty = true;
1634
+ }
1635
+ get cameraZoom() { return this._cameraZoom; }
1636
+ set cameraZoom(val) {
1637
+ if (this._cameraZoom !== val) {
1638
+ this._cameraZoom = val;
1639
+ this._updateCameraFrustum();
1640
+ }
1641
+ }
1642
+ _updateCameraFrustum() {
1643
+ if (!this.glState)
1644
+ return;
1645
+ const gl = this.glState.gl;
1646
+ const width = this._ref.clientWidth;
1647
+ const height = this._ref.clientHeight;
1648
+ updateCamera(this.glState.camera, width, height, PLANE_WIDTH, PLANE_HEIGHT, this._shapeType, this._cameraZoom);
1649
+ const projLoc = this.glState.locations.uniforms["projectionMatrix"];
1650
+ gl.useProgram(this.glState.program);
1651
+ if (projLoc)
1652
+ gl.uniformMatrix4fv(projLoc, false, this.glState.camera.projectionMatrix.elements);
1653
+ this._uniformsDirty = true;
1654
+ }
963
1655
  }
964
1656
  const setLinkStyles = (link) => {
965
1657
  link.id = LINK_ID;