@firecms/neat 0.5.0 → 0.5.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.
@@ -180,6 +180,15 @@ export class NeatGradient implements NeatController {
180
180
  // For saving/restoring clear color
181
181
  private _tempClearColor = new THREE.Color();
182
182
 
183
+ // Performance optimizations
184
+ private _resizeTimeoutId: number | null = null;
185
+ private _textureNeedsUpdate: boolean = false;
186
+ private _lastColorUpdate: number = 0;
187
+ private _linkCheckCounter: number = 0;
188
+ private _mouseUpdateScheduled: boolean = false;
189
+ private _pendingMousePosition: { x: number; y: number } | null = null;
190
+ private _colorsChanged: boolean = true; // Track if colors need update
191
+
183
192
  constructor(config: NeatConfig & { ref: HTMLCanvasElement, resolution?: number, seed?: number }) {
184
193
 
185
194
  const {
@@ -307,8 +316,10 @@ export class NeatGradient implements NeatController {
307
316
 
308
317
  const { renderer, camera, scene } = this.sceneState;
309
318
 
310
- // Optimization: check if cached link is still valid in DOM, otherwise search
311
- if (Math.floor(tick * 10) % 5 === 0) {
319
+ // Optimization: check if cached link is still valid in DOM less frequently
320
+ this._linkCheckCounter++;
321
+ if (this._linkCheckCounter >= 300) { // Check every ~5 seconds at 60fps
322
+ this._linkCheckCounter = 0;
312
323
  if (!this._linkElement || !document.contains(this._linkElement)) {
313
324
  this._linkElement = addNeatLink(ref);
314
325
  }
@@ -350,30 +361,49 @@ export class NeatGradient implements NeatController {
350
361
  u.u_mouse_distortion_radius.value = this._mouseDistortionRadius;
351
362
  u.u_mouse_darken.value = this._mouseDarken;
352
363
  u.u_enable_procedural_texture.value = this._enableProceduralTexture ? 1.0 : 0.0;
353
- u.u_procedural_texture.value = this._proceduralTexture;
354
- u.u_texture_ease.value = this._textureEase;
355
364
 
356
- // Optimized Color Update: Update the existing array objects instead of recreating array
357
- const shaderColors = u.u_colors.value;
358
- for(let i = 0; i < COLORS_COUNT; i++) {
359
- if (i < this._colors.length) {
360
- const c = this._colors[i];
361
- shaderColors[i].is_active = c.enabled ? 1.0 : 0.0;
362
- shaderColors[i].color.setStyle(c.color, "");
363
- shaderColors[i].influence = c.influence || 0;
364
- } else {
365
- shaderColors[i].is_active = 0.0;
365
+ // Only regenerate procedural texture when needed
366
+ if (this._textureNeedsUpdate && this._enableProceduralTexture) {
367
+ if (this._proceduralTexture) {
368
+ this._proceduralTexture.dispose();
366
369
  }
370
+ this._proceduralTexture = this._createProceduralTexture();
371
+ this._textureNeedsUpdate = false;
367
372
  }
368
373
 
369
- u.u_colors_count.value = COLORS_COUNT;
370
- // Wireframe is a material property, not a uniform
374
+ u.u_procedural_texture.value = this._proceduralTexture;
375
+ u.u_texture_ease.value = this._textureEase;
376
+
377
+ // Wireframe is a material property and must update every frame to avoid artifacts
371
378
  // @ts-ignore - access material safely
372
379
  this.sceneState.meshes[0].material.wireframe = this._wireframe;
380
+
381
+ // Optimized Color Update: Update immediately on change, or throttle to 10 times per second
382
+ const now = Date.now();
383
+ const shouldUpdate = this._colorsChanged || (now - this._lastColorUpdate > 100);
384
+
385
+ if (shouldUpdate) {
386
+ this._lastColorUpdate = now;
387
+ this._colorsChanged = false;
388
+
389
+ const shaderColors = u.u_colors.value;
390
+ for(let i = 0; i < COLORS_COUNT; i++) {
391
+ if (i < this._colors.length) {
392
+ const c = this._colors[i];
393
+ shaderColors[i].is_active = c.enabled ? 1.0 : 0.0;
394
+ shaderColors[i].color.setStyle(c.color, "");
395
+ shaderColors[i].influence = c.influence || 0;
396
+ } else {
397
+ shaderColors[i].is_active = 0.0;
398
+ }
399
+ }
400
+
401
+ u.u_colors_count.value = COLORS_COUNT;
402
+ }
373
403
  }
374
404
 
375
- // Render mouse interaction to FBO
376
- if (this._mouseFBO && this._sceneMouse && this._cameraMouse) {
405
+ // Render mouse interaction to FBO - optimize by only rendering when needed
406
+ if (this._mouseFBO && this._sceneMouse && this._cameraMouse && this._mouseDistortionStrength > 0) {
377
407
  let hasActiveBrushes = false;
378
408
 
379
409
  // Update mouse objects - decay rate controls how fast trails fade
@@ -393,30 +423,27 @@ export class NeatGradient implements NeatController {
393
423
  }
394
424
  }
395
425
 
396
- // FIX 2: Handle FBO Clearing correctly
397
- // Store current clear color (likely the main background color)
398
- renderer.getClearColor(this._tempClearColor);
399
- const oldClearAlpha = renderer.getClearAlpha();
400
-
401
- // Set clear color to Black/Transparent for the FBO.
402
- // Important: If we use the main background color (e.g. White), the FBO
403
- // will be white, causing 100% distortion everywhere.
404
- renderer.setClearColor(0x000000, 0.0);
426
+ // Only render FBO if there are active brushes
427
+ if (hasActiveBrushes) {
428
+ // Store current clear color (likely the main background color)
429
+ renderer.getClearColor(this._tempClearColor);
430
+ const oldClearAlpha = renderer.getClearAlpha();
405
431
 
406
- renderer.setRenderTarget(this._mouseFBO);
407
- renderer.clear();
432
+ // Set clear color to Black/Transparent for the FBO.
433
+ renderer.setClearColor(0x000000, 0.0);
408
434
 
409
- if (hasActiveBrushes) {
435
+ renderer.setRenderTarget(this._mouseFBO);
436
+ renderer.clear();
410
437
  renderer.render(this._sceneMouse, this._cameraMouse);
411
- }
412
- renderer.setRenderTarget(null);
438
+ renderer.setRenderTarget(null);
413
439
 
414
- // Restore main background color for the actual scene render
415
- renderer.setClearColor(this._tempClearColor, oldClearAlpha);
440
+ // Restore main background color for the actual scene render
441
+ renderer.setClearColor(this._tempClearColor, oldClearAlpha);
416
442
 
417
- // Update mouse texture uniform
418
- if (this._cachedUniforms) {
419
- this._cachedUniforms.u_mouse_texture.value = this._mouseFBO.texture;
443
+ // Update mouse texture uniform
444
+ if (this._cachedUniforms) {
445
+ this._cachedUniforms.u_mouse_texture.value = this._mouseFBO.texture;
446
+ }
420
447
  }
421
448
  }
422
449
 
@@ -450,8 +477,15 @@ export class NeatGradient implements NeatController {
450
477
  }
451
478
  };
452
479
 
453
- this.sizeObserver = new ResizeObserver(entries => {
454
- setSize();
480
+ // Debounce resize to prevent excessive operations
481
+ this.sizeObserver = new ResizeObserver(() => {
482
+ if (this._resizeTimeoutId !== null) {
483
+ clearTimeout(this._resizeTimeoutId);
484
+ }
485
+ this._resizeTimeoutId = window.setTimeout(() => {
486
+ setSize();
487
+ this._resizeTimeoutId = null;
488
+ }, 100); // Wait 100ms after last resize event
455
489
  });
456
490
 
457
491
  this.sizeObserver.observe(ref);
@@ -465,6 +499,12 @@ export class NeatGradient implements NeatController {
465
499
  cancelAnimationFrame(this.requestRef);
466
500
  this.sizeObserver.disconnect();
467
501
 
502
+ // Clear resize timeout
503
+ if (this._resizeTimeoutId !== null) {
504
+ clearTimeout(this._resizeTimeoutId);
505
+ this._resizeTimeoutId = null;
506
+ }
507
+
468
508
  // Cleanup WebGL resources
469
509
  if (this.sceneState) {
470
510
  this.sceneState.renderer.dispose();
@@ -512,6 +552,7 @@ export class NeatGradient implements NeatController {
512
552
 
513
553
  set colors(colors: NeatColor[]) {
514
554
  this._colors = colors;
555
+ this._colorsChanged = true; // Flag for immediate update
515
556
  }
516
557
 
517
558
  set highlights(highlights: number) {
@@ -649,49 +690,49 @@ export class NeatGradient implements NeatController {
649
690
  set enableProceduralTexture(value: boolean) {
650
691
  this._enableProceduralTexture = value;
651
692
  if (value && !this._proceduralTexture) {
652
- this._proceduralTexture = this._createProceduralTexture();
693
+ this._textureNeedsUpdate = true;
653
694
  }
654
695
  }
655
696
 
656
697
  set textureVoidLikelihood(value: number) {
657
698
  this._textureVoidLikelihood = value;
658
699
  if (this._enableProceduralTexture) {
659
- this._proceduralTexture = this._createProceduralTexture();
700
+ this._textureNeedsUpdate = true;
660
701
  }
661
702
  }
662
703
 
663
704
  set textureVoidWidthMin(value: number) {
664
705
  this._textureVoidWidthMin = value;
665
706
  if (this._enableProceduralTexture) {
666
- this._proceduralTexture = this._createProceduralTexture();
707
+ this._textureNeedsUpdate = true;
667
708
  }
668
709
  }
669
710
 
670
711
  set textureVoidWidthMax(value: number) {
671
712
  this._textureVoidWidthMax = value;
672
713
  if (this._enableProceduralTexture) {
673
- this._proceduralTexture = this._createProceduralTexture();
714
+ this._textureNeedsUpdate = true;
674
715
  }
675
716
  }
676
717
 
677
718
  set textureBandDensity(value: number) {
678
719
  this._textureBandDensity = value;
679
720
  if (this._enableProceduralTexture) {
680
- this._proceduralTexture = this._createProceduralTexture();
721
+ this._textureNeedsUpdate = true;
681
722
  }
682
723
  }
683
724
 
684
725
  set textureColorBlending(value: number) {
685
726
  this._textureColorBlending = value;
686
727
  if (this._enableProceduralTexture) {
687
- this._proceduralTexture = this._createProceduralTexture();
728
+ this._textureNeedsUpdate = true;
688
729
  }
689
730
  }
690
731
 
691
732
  set textureSeed(value: number) {
692
733
  this._textureSeed = value;
693
734
  if (this._enableProceduralTexture) {
694
- this._proceduralTexture = this._createProceduralTexture();
735
+ this._textureNeedsUpdate = true;
695
736
  }
696
737
  }
697
738
 
@@ -706,25 +747,25 @@ export class NeatGradient implements NeatController {
706
747
  set proceduralBackgroundColor(value: string) {
707
748
  this._proceduralBackgroundColor = value;
708
749
  if (this._enableProceduralTexture) {
709
- this._proceduralTexture = this._createProceduralTexture();
750
+ this._textureNeedsUpdate = true;
710
751
  }
711
752
  }
712
753
 
713
754
  set textureShapeTriangles(value: number) {
714
755
  this._textureShapeTriangles = value;
715
- if (this._enableProceduralTexture) this._proceduralTexture = this._createProceduralTexture();
756
+ if (this._enableProceduralTexture) this._textureNeedsUpdate = true;
716
757
  }
717
758
  set textureShapeCircles(value: number) {
718
759
  this._textureShapeCircles = value;
719
- if (this._enableProceduralTexture) this._proceduralTexture = this._createProceduralTexture();
760
+ if (this._enableProceduralTexture) this._textureNeedsUpdate = true;
720
761
  }
721
762
  set textureShapeBars(value: number) {
722
763
  this._textureShapeBars = value;
723
- if (this._enableProceduralTexture) this._proceduralTexture = this._createProceduralTexture();
764
+ if (this._enableProceduralTexture) this._textureNeedsUpdate = true;
724
765
  }
725
766
  set textureShapeSquiggles(value: number) {
726
767
  this._textureShapeSquiggles = value;
727
- if (this._enableProceduralTexture) this._proceduralTexture = this._createProceduralTexture();
768
+ if (this._enableProceduralTexture) this._textureNeedsUpdate = true;
728
769
  }
729
770
 
730
771
  _initScene(resolution: number): SceneState {
@@ -910,33 +951,52 @@ export class NeatGradient implements NeatController {
910
951
 
911
952
  _onMouseMove(e: MouseEvent) {
912
953
  if (!this._ref || !this._sceneMouse) return;
954
+
913
955
  const rect = this._ref.getBoundingClientRect();
914
956
  const width = this._ref.width;
915
957
  const height = this._ref.height;
916
958
 
917
- this._mouse.x = e.clientX - rect.left - width / 2;
918
- this._mouse.y = -(e.clientY - rect.top - height / 2);
919
-
920
- const brush = this._mouseObjects[this._currentBrush];
921
- brush.mesh.scale.set(this._mouseBrushBaseScale, this._mouseBrushBaseScale, 1.0);
922
- brush.active = true;
923
- brush.mesh.visible = true;
924
- brush.mesh.position.set(this._mouse.x, this._mouse.y, 0);
925
- brush.mesh.rotation.z = Math.random() * Math.PI * 2;
926
- if (brush.mesh.material instanceof THREE.MeshBasicMaterial) {
927
- brush.mesh.material.opacity = 1.0;
959
+ // Store pending mouse position
960
+ this._pendingMousePosition = {
961
+ x: e.clientX - rect.left - width / 2,
962
+ y: -(e.clientY - rect.top - height / 2)
963
+ };
964
+
965
+ // Batch mouse updates using requestAnimationFrame
966
+ if (!this._mouseUpdateScheduled) {
967
+ this._mouseUpdateScheduled = true;
968
+ requestAnimationFrame(() => {
969
+ this._mouseUpdateScheduled = false;
970
+
971
+ if (!this._pendingMousePosition) return;
972
+
973
+ this._mouse.x = this._pendingMousePosition.x;
974
+ this._mouse.y = this._pendingMousePosition.y;
975
+
976
+ const brush = this._mouseObjects[this._currentBrush];
977
+ brush.mesh.scale.set(this._mouseBrushBaseScale, this._mouseBrushBaseScale, 1.0);
978
+ brush.active = true;
979
+ brush.mesh.visible = true;
980
+ brush.mesh.position.set(this._mouse.x, this._mouse.y, 0);
981
+ brush.mesh.rotation.z = Math.random() * Math.PI * 2;
982
+ if (brush.mesh.material instanceof THREE.MeshBasicMaterial) {
983
+ brush.mesh.material.opacity = 1.0;
984
+ }
985
+ this._currentBrush = (this._currentBrush + 1) % this._mouseObjects.length;
986
+
987
+ this._pendingMousePosition = null;
988
+ });
928
989
  }
929
- this._currentBrush = (this._currentBrush + 1) % this._mouseObjects.length;
930
990
  }
931
991
 
932
992
  _createProceduralTexture(): THREE.Texture {
933
993
  // Texture size - 1024 provides good balance between quality and performance
934
- // Can be increased to 2048 for even better quality if needed
994
+ // Reduced from 2048 for better performance
935
995
  const texSize = 1024;
936
996
  const sourceCanvas = document.createElement('canvas');
937
997
  sourceCanvas.width = texSize;
938
998
  sourceCanvas.height = texSize;
939
- const sCtx = sourceCanvas.getContext('2d');
999
+ const sCtx = sourceCanvas.getContext('2d', { willReadFrequently: true });
940
1000
  if (!sCtx) return new THREE.Texture();
941
1001
 
942
1002
  let seed = this._textureSeed;
@@ -1056,7 +1116,7 @@ export class NeatGradient implements NeatController {
1056
1116
  const canvas = document.createElement('canvas');
1057
1117
  canvas.width = texSize;
1058
1118
  canvas.height = texSize;
1059
- const ctx = canvas.getContext('2d');
1119
+ const ctx = canvas.getContext('2d', { willReadFrequently: true });
1060
1120
  if (!ctx) return new THREE.Texture();
1061
1121
 
1062
1122
  // Start filled with the chosen void color so gaps show that color
@@ -1134,11 +1194,26 @@ function updateCamera(camera: THREE.Camera, width: number, height: number) {
1134
1194
  const targetWidth = Math.sqrt(targetPlaneArea * ratio);
1135
1195
  const targetHeight = targetPlaneArea / targetWidth;
1136
1196
 
1137
- const left = -PLANE_WIDTH / 2;
1138
- const right = Math.min((left + targetWidth) / 1.5, PLANE_WIDTH / 2);
1197
+ let left = -PLANE_WIDTH / 2;
1198
+ let right = Math.min((left + targetWidth) / 1.5, PLANE_WIDTH / 2);
1199
+
1200
+ let top = PLANE_HEIGHT / 4;
1201
+ let bottom = Math.max((top - targetHeight) / 2, -PLANE_HEIGHT / 4);
1202
+
1203
+ // Fix for mobile portrait: adjust bounds for proper aspect ratio AND zoom out slightly
1204
+ if (ratio < 1) {
1205
+ // Portrait mode - scale horizontal bounds by aspect ratio to prevent stretching
1206
+ const horizontalScale = ratio;
1207
+ left = left * horizontalScale;
1208
+ right = right * horizontalScale;
1139
1209
 
1140
- const top = PLANE_HEIGHT / 4;
1141
- const bottom = Math.max((top - targetHeight) / 2, -PLANE_HEIGHT / 4);
1210
+ // Zoom out slightly on mobile (1.1 = 10% zoom out)
1211
+ const mobileZoomFactor = 1.05;
1212
+ left = left * mobileZoomFactor;
1213
+ right = right * mobileZoomFactor;
1214
+ top = top * mobileZoomFactor;
1215
+ bottom = bottom * mobileZoomFactor;
1216
+ }
1142
1217
 
1143
1218
  const near = -100;
1144
1219
  const far = 1000;
@@ -1158,8 +1233,13 @@ function updateCamera(camera: THREE.Camera, width: number, height: number) {
1158
1233
  }
1159
1234
 
1160
1235
 
1236
+ // Cache shader strings to avoid repeated concatenation
1237
+ let cachedVertexShader: string | null = null;
1238
+ let cachedFragmentShader: string | null = null;
1239
+
1161
1240
  function buildVertexShader() {
1162
- return `
1241
+ if (cachedVertexShader) return cachedVertexShader;
1242
+ cachedVertexShader = `
1163
1243
  void main() {
1164
1244
  vUv = uv;
1165
1245
 
@@ -1237,10 +1317,12 @@ void main() {
1237
1317
  v_new_position = gl_Position;
1238
1318
  }
1239
1319
  `;
1320
+ return cachedVertexShader;
1240
1321
  }
1241
1322
 
1242
1323
  function buildFragmentShader() {
1243
- return `
1324
+ if (cachedFragmentShader) return cachedFragmentShader;
1325
+ cachedFragmentShader = `
1244
1326
  float random(vec2 p) {
1245
1327
  return fract(sin(dot(p, vec2(12.9898,78.233))) * 43758.5453);
1246
1328
  }
@@ -1323,8 +1405,15 @@ void main() {
1323
1405
  gl_FragColor = vec4(color, 1.0);
1324
1406
  }
1325
1407
  `;
1408
+ return cachedFragmentShader;
1326
1409
  }
1327
- const buildUniforms = () => `
1410
+
1411
+ // Cache uniforms string as well
1412
+ let cachedUniformsShader: string | null = null;
1413
+
1414
+ const buildUniforms = () => {
1415
+ if (cachedUniformsShader) return cachedUniformsShader;
1416
+ cachedUniformsShader = `
1328
1417
  precision highp float;
1329
1418
 
1330
1419
  struct Color {
@@ -1389,8 +1478,15 @@ varying vec3 v_color;
1389
1478
  varying float v_displacement_amount;
1390
1479
 
1391
1480
  `;
1481
+ return cachedUniformsShader;
1482
+ };
1392
1483
 
1393
- const buildNoise = () => `
1484
+ // Cache noise functions as well
1485
+ let cachedNoiseShader: string | null = null;
1486
+
1487
+ const buildNoise = () => {
1488
+ if (cachedNoiseShader) return cachedNoiseShader;
1489
+ cachedNoiseShader = `
1394
1490
 
1395
1491
  // 1. REPLACEMENT PERMUTE:
1396
1492
  // Uses a hash function (fract/sin) instead of a modular lookup table.
@@ -1544,8 +1640,15 @@ float cnoise(vec3 P)
1544
1640
  return 2.2 * n_xyz;
1545
1641
  }
1546
1642
  `;
1643
+ return cachedNoiseShader;
1644
+ };
1547
1645
 
1548
- const buildColorFunctions = () => `
1646
+ // Cache color functions as well
1647
+ let cachedColorFunctionsShader: string | null = null;
1648
+
1649
+ const buildColorFunctions = () => {
1650
+ if (cachedColorFunctionsShader) return cachedColorFunctionsShader;
1651
+ cachedColorFunctionsShader = `
1549
1652
 
1550
1653
  vec3 saturation(vec3 rgb, float adjustment) {
1551
1654
  const vec3 W = vec3(0.2125, 0.7154, 0.0721);
@@ -1589,6 +1692,8 @@ vec3 hsv2rgb(vec3 c)
1589
1692
  return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
1590
1693
  }
1591
1694
  `;
1695
+ return cachedColorFunctionsShader;
1696
+ };
1592
1697
 
1593
1698
  const setLinkStyles = (link: HTMLAnchorElement) => {
1594
1699
  link.id = LINK_ID;