@firecms/neat 0.6.0 → 0.7.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/README.md +5 -29
- package/dist/NeatGradient.d.ts +29 -43
- package/dist/NeatGradient.js +346 -897
- package/dist/NeatGradient.js.map +1 -1
- package/dist/index.es.js +757 -731
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +103 -120
- package/dist/index.umd.js.map +1 -1
- package/dist/math.d.ts +26 -0
- package/dist/math.js +148 -0
- package/dist/math.js.map +1 -0
- package/dist/shaders.d.ts +6 -0
- package/dist/shaders.js +407 -0
- package/dist/shaders.js.map +1 -0
- package/dist/types.d.ts +52 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +3 -7
- package/src/NeatGradient.ts +410 -967
- package/src/math.ts +162 -0
- package/src/shaders.ts +411 -0
- package/src/types.ts +54 -0
package/src/NeatGradient.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { buildColorFunctions, buildNoise, buildVertUniforms, buildFragUniforms, fragmentShaderSource, vertexShaderSource } from "./shaders";
|
|
2
|
+
import { generatePlaneGeometry, OrthographicCamera, updateCamera, Matrix4 } from "./math";
|
|
2
3
|
|
|
3
4
|
console.info(
|
|
4
5
|
"%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",
|
|
@@ -8,30 +9,32 @@ console.info(
|
|
|
8
9
|
const PLANE_WIDTH = 50;
|
|
9
10
|
const PLANE_HEIGHT = 80;
|
|
10
11
|
|
|
11
|
-
const WIREFRAME = true;
|
|
12
|
-
const COLORS_COUNT = 6;
|
|
13
12
|
|
|
14
|
-
const
|
|
13
|
+
const COLORS_COUNT = 6;
|
|
15
14
|
|
|
16
15
|
const LINK_ID = generateRandomString();
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
export interface WebGLState {
|
|
18
|
+
gl: WebGLRenderingContext | WebGL2RenderingContext;
|
|
19
|
+
program: WebGLProgram;
|
|
20
|
+
buffers: {
|
|
21
|
+
position: WebGLBuffer;
|
|
22
|
+
normal: WebGLBuffer;
|
|
23
|
+
uv: WebGLBuffer;
|
|
24
|
+
index: WebGLBuffer;
|
|
25
|
+
wireframeIndex: WebGLBuffer;
|
|
26
|
+
};
|
|
27
|
+
locations: {
|
|
28
|
+
attributes: Record<string, number>;
|
|
29
|
+
uniforms: Record<string, WebGLUniformLocation | null>;
|
|
30
|
+
};
|
|
31
|
+
camera: OrthographicCamera;
|
|
32
|
+
indexCount: number;
|
|
33
|
+
wireframeIndexCount: number;
|
|
34
|
+
indexType: number;
|
|
24
35
|
}
|
|
25
36
|
|
|
26
|
-
|
|
27
|
-
interface NeatUniforms {
|
|
28
|
-
[key: string]: THREE.IUniform;
|
|
29
|
-
u_time: { value: number };
|
|
30
|
-
u_resolution: { value: THREE.Vector2 };
|
|
31
|
-
u_color_pressure: { value: THREE.Vector2 };
|
|
32
|
-
u_colors: { value: { is_active: number; color: THREE.Color; influence: number }[] };
|
|
33
|
-
u_mouse_texture: { value: THREE.Texture | null };
|
|
34
|
-
}
|
|
37
|
+
|
|
35
38
|
|
|
36
39
|
export type NeatConfig = {
|
|
37
40
|
resolution?: number;
|
|
@@ -64,14 +67,7 @@ export type NeatConfig = {
|
|
|
64
67
|
flowScale?: number;
|
|
65
68
|
flowEase?: number;
|
|
66
69
|
flowEnabled?: boolean;
|
|
67
|
-
|
|
68
|
-
/** Strength of mouse-driven distortion */
|
|
69
|
-
mouseDistortionStrength?: number;
|
|
70
|
-
/** Radius / area of mouse-driven distortion in UV space (0–1-ish) */
|
|
71
|
-
mouseDistortionRadius?: number;
|
|
72
|
-
/** How quickly mouse trails decay/fade (0.9=slow/wobbly, 0.99=fast/sharp) */
|
|
73
|
-
mouseDecayRate?: number;
|
|
74
|
-
mouseDarken?: number;
|
|
70
|
+
|
|
75
71
|
// Texture generation
|
|
76
72
|
enableProceduralTexture?: boolean;
|
|
77
73
|
textureVoidLikelihood?: number;
|
|
@@ -130,6 +126,7 @@ export class NeatGradient implements NeatController {
|
|
|
130
126
|
private _wireframe: boolean = false;
|
|
131
127
|
|
|
132
128
|
private _backgroundColor: string = "#FFFFFF";
|
|
129
|
+
private _backgroundColorRgb: [number, number, number] = [1, 1, 1];
|
|
133
130
|
private _backgroundAlpha: number = 1.0;
|
|
134
131
|
|
|
135
132
|
// Flow field properties
|
|
@@ -139,18 +136,7 @@ export class NeatGradient implements NeatController {
|
|
|
139
136
|
private _flowEase: number = 0.0;
|
|
140
137
|
private _flowEnabled: boolean = true;
|
|
141
138
|
|
|
142
|
-
|
|
143
|
-
private _mouseDistortionStrength: number = 0.0;
|
|
144
|
-
private _mouseDistortionRadius: number = 0.25;
|
|
145
|
-
private _mouseDecayRate: number = 0.96;
|
|
146
|
-
private _mouseDarken: number = 0.0;
|
|
147
|
-
private _mouse: THREE.Vector2 = new THREE.Vector2(-1000, -1000);
|
|
148
|
-
private _mouseFBO: THREE.WebGLRenderTarget | null = null;
|
|
149
|
-
private _sceneMouse: THREE.Scene | null = null;
|
|
150
|
-
private _cameraMouse: THREE.OrthographicCamera | null = null;
|
|
151
|
-
private _mouseObjects: Array<{ mesh: THREE.Mesh, active: boolean }> = [];
|
|
152
|
-
private _currentBrush: number = 0;
|
|
153
|
-
private _mouseBrushBaseScale: number = 1;
|
|
139
|
+
private glState!: WebGLState;
|
|
154
140
|
|
|
155
141
|
// Texture generation properties
|
|
156
142
|
private _enableProceduralTexture: boolean = false;
|
|
@@ -161,7 +147,7 @@ export class NeatGradient implements NeatController {
|
|
|
161
147
|
private _textureColorBlending: number = 0.01;
|
|
162
148
|
private _textureSeed: number = 333;
|
|
163
149
|
private _textureEase: number = 0.5;
|
|
164
|
-
private _proceduralTexture:
|
|
150
|
+
private _proceduralTexture: WebGLTexture | null = null;
|
|
165
151
|
private _proceduralBackgroundColor: string = "#000000";
|
|
166
152
|
|
|
167
153
|
private _textureShapeTriangles: number = 20;
|
|
@@ -171,28 +157,23 @@ export class NeatGradient implements NeatController {
|
|
|
171
157
|
|
|
172
158
|
private requestRef: number = -1;
|
|
173
159
|
private sizeObserver: ResizeObserver;
|
|
174
|
-
private sceneState: SceneState;
|
|
175
160
|
|
|
176
|
-
|
|
177
|
-
private _cachedUniforms: NeatUniforms | null = null;
|
|
161
|
+
private _initialized: boolean = false;
|
|
178
162
|
private _linkElement: HTMLAnchorElement | null = null;
|
|
163
|
+
private _cachedColorRgb: [number, number, number][] = [];
|
|
179
164
|
|
|
180
165
|
private _yOffset: number = 0;
|
|
181
166
|
private _yOffsetWaveMultiplier: number = 0.004;
|
|
182
167
|
private _yOffsetColorMultiplier: number = 0.004;
|
|
183
168
|
private _yOffsetFlowMultiplier: number = 0.004;
|
|
184
169
|
|
|
185
|
-
// For saving/restoring clear color
|
|
186
|
-
private _tempClearColor = new THREE.Color();
|
|
187
|
-
|
|
188
170
|
// Performance optimizations
|
|
189
171
|
private _resizeTimeoutId: number | null = null;
|
|
190
172
|
private _textureNeedsUpdate: boolean = false;
|
|
191
|
-
private _lastColorUpdate: number = 0;
|
|
192
173
|
private _linkCheckCounter: number = 0;
|
|
193
|
-
private
|
|
194
|
-
private
|
|
195
|
-
private
|
|
174
|
+
private _colorsChanged: boolean = true;
|
|
175
|
+
private _uniformsDirty: boolean = true;
|
|
176
|
+
private _textureDirty: boolean = true;
|
|
196
177
|
|
|
197
178
|
constructor(config: NeatConfig & { ref: HTMLCanvasElement, resolution?: number, seed?: number }) {
|
|
198
179
|
|
|
@@ -229,11 +210,7 @@ export class NeatGradient implements NeatController {
|
|
|
229
210
|
flowScale = 1.0,
|
|
230
211
|
flowEase = 0.0,
|
|
231
212
|
flowEnabled = true,
|
|
232
|
-
|
|
233
|
-
mouseDistortionStrength = 0.0,
|
|
234
|
-
mouseDistortionRadius = 0.25,
|
|
235
|
-
mouseDecayRate = 0.96,
|
|
236
|
-
mouseDarken = 0.0,
|
|
213
|
+
|
|
237
214
|
// Texture generation
|
|
238
215
|
enableProceduralTexture = false,
|
|
239
216
|
textureVoidLikelihood = 0.45,
|
|
@@ -255,7 +232,6 @@ export class NeatGradient implements NeatController {
|
|
|
255
232
|
|
|
256
233
|
this.destroy = this.destroy.bind(this);
|
|
257
234
|
this._initScene = this._initScene.bind(this);
|
|
258
|
-
this._buildMaterial = this._buildMaterial.bind(this);
|
|
259
235
|
|
|
260
236
|
this.speed = speed;
|
|
261
237
|
this.horizontalPressure = horizontalPressure;
|
|
@@ -288,11 +264,7 @@ export class NeatGradient implements NeatController {
|
|
|
288
264
|
this.flowEase = flowEase;
|
|
289
265
|
this.flowEnabled = flowEnabled;
|
|
290
266
|
|
|
291
|
-
|
|
292
|
-
this.mouseDistortionStrength = mouseDistortionStrength;
|
|
293
|
-
this.mouseDistortionRadius = mouseDistortionRadius;
|
|
294
|
-
this.mouseDecayRate = mouseDecayRate;
|
|
295
|
-
this.mouseDarken = mouseDarken;
|
|
267
|
+
|
|
296
268
|
|
|
297
269
|
// Texture generation
|
|
298
270
|
this.enableProceduralTexture = enableProceduralTexture;
|
|
@@ -310,18 +282,17 @@ export class NeatGradient implements NeatController {
|
|
|
310
282
|
this._textureShapeBars = textureShapeBars;
|
|
311
283
|
this._textureShapeSquiggles = textureShapeSquiggles;
|
|
312
284
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
this._setupMouseInteraction();
|
|
316
|
-
this.sceneState = this._initScene(resolution);
|
|
285
|
+
|
|
286
|
+
this.glState = this._initScene(resolution);
|
|
317
287
|
|
|
318
288
|
injectSEO();
|
|
319
289
|
|
|
320
290
|
let tick = seed !== undefined ? seed : getElapsedSecondsInLastHour();
|
|
291
|
+
let lastTime = performance.now();
|
|
321
292
|
|
|
322
293
|
const render = () => {
|
|
323
294
|
|
|
324
|
-
const {
|
|
295
|
+
const { gl, program, locations, indexCount, indexType } = this.glState;
|
|
325
296
|
|
|
326
297
|
// Optimization: check if cached link is still valid in DOM less frequently
|
|
327
298
|
this._linkCheckCounter++;
|
|
@@ -332,156 +303,127 @@ export class NeatGradient implements NeatController {
|
|
|
332
303
|
}
|
|
333
304
|
}
|
|
334
305
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
306
|
+
if (this._initialized) {
|
|
307
|
+
const timeNow = performance.now();
|
|
308
|
+
tick += ((timeNow - lastTime) / 1000) * this._speed;
|
|
309
|
+
lastTime = timeNow;
|
|
310
|
+
|
|
311
|
+
gl.useProgram(program);
|
|
312
|
+
|
|
313
|
+
gl.uniform1f(locations.uniforms['u_time'], tick);
|
|
314
|
+
|
|
315
|
+
// Only upload static uniforms when they've been modified
|
|
316
|
+
if (this._uniformsDirty) {
|
|
317
|
+
gl.uniform2f(locations.uniforms['u_resolution'], this._ref.clientWidth, this._ref.clientHeight);
|
|
318
|
+
gl.uniform2f(locations.uniforms['u_color_pressure'], this._horizontalPressure, this._verticalPressure);
|
|
319
|
+
|
|
320
|
+
gl.uniform1f(locations.uniforms['u_wave_frequency_x'], this._waveFrequencyX);
|
|
321
|
+
gl.uniform1f(locations.uniforms['u_wave_frequency_y'], this._waveFrequencyY);
|
|
322
|
+
gl.uniform1f(locations.uniforms['u_wave_amplitude'], this._waveAmplitude);
|
|
323
|
+
gl.uniform1f(locations.uniforms['u_color_blending'], this._colorBlending);
|
|
324
|
+
gl.uniform1f(locations.uniforms['u_shadows'], this._shadows);
|
|
325
|
+
gl.uniform1f(locations.uniforms['u_highlights'], this._highlights);
|
|
326
|
+
gl.uniform1f(locations.uniforms['u_saturation'], this._saturation);
|
|
327
|
+
gl.uniform1f(locations.uniforms['u_brightness'], this._brightness);
|
|
328
|
+
gl.uniform1f(locations.uniforms['u_grain_intensity'], this._grainIntensity);
|
|
329
|
+
gl.uniform1f(locations.uniforms['u_grain_sparsity'], this._grainSparsity);
|
|
330
|
+
gl.uniform1f(locations.uniforms['u_grain_speed'], this._grainSpeed);
|
|
331
|
+
gl.uniform1f(locations.uniforms['u_grain_scale'], this._grainScale);
|
|
332
|
+
gl.uniform1f(locations.uniforms['u_y_offset'], this._yOffset);
|
|
333
|
+
gl.uniform1f(locations.uniforms['u_y_offset_wave_multiplier'], this._yOffsetWaveMultiplier);
|
|
334
|
+
gl.uniform1f(locations.uniforms['u_y_offset_color_multiplier'], this._yOffsetColorMultiplier);
|
|
335
|
+
gl.uniform1f(locations.uniforms['u_y_offset_flow_multiplier'], this._yOffsetFlowMultiplier);
|
|
336
|
+
gl.uniform1f(locations.uniforms['u_flow_distortion_a'], this._flowDistortionA);
|
|
337
|
+
gl.uniform1f(locations.uniforms['u_flow_distortion_b'], this._flowDistortionB);
|
|
338
|
+
gl.uniform1f(locations.uniforms['u_flow_scale'], this._flowScale);
|
|
339
|
+
gl.uniform1f(locations.uniforms['u_flow_ease'], this._flowEase);
|
|
340
|
+
gl.uniform1f(locations.uniforms['u_flow_enabled'], this._flowEnabled ? 1.0 : 0.0);
|
|
341
|
+
|
|
342
|
+
gl.uniform1f(locations.uniforms['u_enable_procedural_texture'], this._enableProceduralTexture ? 1.0 : 0.0);
|
|
343
|
+
gl.uniform1f(locations.uniforms['u_texture_ease'], this._textureEase);
|
|
344
|
+
|
|
345
|
+
this._uniformsDirty = false;
|
|
346
|
+
}
|
|
371
347
|
|
|
372
348
|
// Only regenerate procedural texture when needed
|
|
373
349
|
if (this._textureNeedsUpdate && this._enableProceduralTexture) {
|
|
374
350
|
if (this._proceduralTexture) {
|
|
375
|
-
this._proceduralTexture
|
|
351
|
+
gl.deleteTexture(this._proceduralTexture);
|
|
376
352
|
}
|
|
377
|
-
this._proceduralTexture = this._createProceduralTexture();
|
|
353
|
+
this._proceduralTexture = this._createProceduralTexture(gl);
|
|
378
354
|
this._textureNeedsUpdate = false;
|
|
355
|
+
this._textureDirty = true;
|
|
379
356
|
}
|
|
380
357
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
// Optimized Color Update: Update immediately on change, or throttle to 10 times per second
|
|
389
|
-
const now = Date.now();
|
|
390
|
-
const shouldUpdate = this._colorsChanged || (now - this._lastColorUpdate > 100);
|
|
358
|
+
// Procedural texture binding — only when texture changes
|
|
359
|
+
if (this._textureDirty && this._proceduralTexture) {
|
|
360
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
361
|
+
gl.bindTexture(gl.TEXTURE_2D, this._proceduralTexture);
|
|
362
|
+
gl.uniform1i(locations.uniforms['u_procedural_texture'], 1);
|
|
363
|
+
this._textureDirty = false;
|
|
364
|
+
}
|
|
391
365
|
|
|
392
|
-
|
|
393
|
-
|
|
366
|
+
// Color update — only when colors have changed
|
|
367
|
+
if (this._colorsChanged) {
|
|
394
368
|
this._colorsChanged = false;
|
|
395
369
|
|
|
396
|
-
const shaderColors = u.u_colors.value;
|
|
397
370
|
for (let i = 0; i < COLORS_COUNT; i++) {
|
|
398
371
|
if (i < this._colors.length) {
|
|
399
372
|
const c = this._colors[i];
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
373
|
+
const rgb = this._cachedColorRgb[i] || [0, 0, 0];
|
|
374
|
+
gl.uniform1f(locations.uniforms[`u_colors[${i}].is_active`], c.enabled ? 1.0 : 0.0);
|
|
375
|
+
gl.uniform3fv(locations.uniforms[`u_colors[${i}].color`], rgb);
|
|
376
|
+
gl.uniform1f(locations.uniforms[`u_colors[${i}].influence`], c.influence || 0);
|
|
403
377
|
} else {
|
|
404
|
-
|
|
378
|
+
gl.uniform1f(locations.uniforms[`u_colors[${i}].is_active`], 0.0);
|
|
405
379
|
}
|
|
406
380
|
}
|
|
407
381
|
|
|
408
|
-
|
|
382
|
+
gl.uniform1i(locations.uniforms['u_colors_count'], COLORS_COUNT);
|
|
409
383
|
}
|
|
410
384
|
}
|
|
411
385
|
|
|
412
|
-
// Render mouse interaction to FBO - optimize by only rendering when needed
|
|
413
|
-
if (this._mouseFBO && this._sceneMouse && this._cameraMouse && this._mouseDistortionStrength > 0) {
|
|
414
|
-
let hasActiveBrushes = false;
|
|
415
|
-
|
|
416
|
-
// Update mouse objects - decay rate controls how fast trails fade
|
|
417
|
-
for (let i = 0; i < this._mouseObjects.length; i++) {
|
|
418
|
-
const obj = this._mouseObjects[i];
|
|
419
|
-
if (obj.mesh.visible) {
|
|
420
|
-
hasActiveBrushes = true;
|
|
421
|
-
obj.mesh.rotation.z += 0.01;
|
|
422
|
-
if (obj.mesh.material instanceof THREE.MeshBasicMaterial) {
|
|
423
|
-
// Decay only affects opacity
|
|
424
|
-
obj.mesh.material.opacity *= this._mouseDecayRate;
|
|
425
|
-
|
|
426
|
-
if (obj.mesh.material.opacity < 0.01) {
|
|
427
|
-
obj.mesh.visible = false;
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// Only render FBO if there are active brushes
|
|
434
|
-
if (hasActiveBrushes) {
|
|
435
|
-
// Store current clear color (likely the main background color)
|
|
436
|
-
renderer.getClearColor(this._tempClearColor);
|
|
437
|
-
const oldClearAlpha = renderer.getClearAlpha();
|
|
438
|
-
|
|
439
|
-
// Set clear color to Black/Transparent for the FBO.
|
|
440
|
-
renderer.setClearColor(0x000000, 0.0);
|
|
441
|
-
|
|
442
|
-
renderer.setRenderTarget(this._mouseFBO);
|
|
443
|
-
renderer.clear();
|
|
444
|
-
renderer.render(this._sceneMouse, this._cameraMouse);
|
|
445
|
-
renderer.setRenderTarget(null);
|
|
446
386
|
|
|
447
|
-
|
|
448
|
-
|
|
387
|
+
// Draw scene
|
|
388
|
+
gl.clearColor(
|
|
389
|
+
this._backgroundColorRgb[0],
|
|
390
|
+
this._backgroundColorRgb[1],
|
|
391
|
+
this._backgroundColorRgb[2],
|
|
392
|
+
this._backgroundAlpha
|
|
393
|
+
);
|
|
394
|
+
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
|
449
395
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
396
|
+
if (this._wireframe) {
|
|
397
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.glState.buffers.wireframeIndex);
|
|
398
|
+
gl.drawElements(gl.LINES, this.glState.wireframeIndexCount, indexType, 0);
|
|
399
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.glState.buffers.index);
|
|
400
|
+
} else {
|
|
401
|
+
gl.drawElements(gl.TRIANGLES, indexCount, indexType, 0);
|
|
455
402
|
}
|
|
456
403
|
|
|
457
|
-
// Ensure we set the clear color for the main scene explicitly before rendering
|
|
458
|
-
renderer.setClearColor(this._backgroundColor, this._backgroundAlpha);
|
|
459
|
-
renderer.render(scene, camera);
|
|
460
404
|
this.requestRef = requestAnimationFrame(render);
|
|
461
405
|
};
|
|
462
406
|
|
|
463
407
|
const setSize = () => {
|
|
464
408
|
|
|
465
|
-
const {
|
|
466
|
-
const
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
this.
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
this._cameraMouse.updateProjectionMatrix();
|
|
484
|
-
}
|
|
409
|
+
const { gl, camera } = this.glState;
|
|
410
|
+
const width = this._ref.clientWidth;
|
|
411
|
+
const height = this._ref.clientHeight;
|
|
412
|
+
|
|
413
|
+
// Handle high DPI displays properly without scaling buffer resolution, matching client width
|
|
414
|
+
this._ref.width = width;
|
|
415
|
+
this._ref.height = height;
|
|
416
|
+
|
|
417
|
+
gl.viewport(0, 0, width, height);
|
|
418
|
+
|
|
419
|
+
updateCamera(camera, width, height);
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
// Recompute projection matrix on resize
|
|
424
|
+
const projLoc = gl.getUniformLocation(this.glState.program, "projectionMatrix");
|
|
425
|
+
gl.useProgram(this.glState.program);
|
|
426
|
+
gl.uniformMatrix4fv(projLoc, false, camera.projectionMatrix.elements);
|
|
485
427
|
};
|
|
486
428
|
|
|
487
429
|
// Debounce resize to prevent excessive operations
|
|
@@ -502,119 +444,155 @@ export class NeatGradient implements NeatController {
|
|
|
502
444
|
}
|
|
503
445
|
|
|
504
446
|
destroy() {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
this.sizeObserver.disconnect();
|
|
447
|
+
cancelAnimationFrame(this.requestRef);
|
|
448
|
+
this.sizeObserver.disconnect();
|
|
508
449
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
450
|
+
// Clear resize timeout
|
|
451
|
+
if (this._resizeTimeoutId !== null) {
|
|
452
|
+
clearTimeout(this._resizeTimeoutId);
|
|
453
|
+
this._resizeTimeoutId = null;
|
|
454
|
+
}
|
|
514
455
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
456
|
+
// Remove NEAT link
|
|
457
|
+
if (this._linkElement && this._linkElement.parentElement) {
|
|
458
|
+
this._linkElement.parentElement.removeChild(this._linkElement);
|
|
459
|
+
this._linkElement = null;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Cleanup WebGL resources
|
|
463
|
+
if (this.glState) {
|
|
464
|
+
const gl = this.glState.gl;
|
|
465
|
+
gl.deleteProgram(this.glState.program);
|
|
466
|
+
gl.deleteBuffer(this.glState.buffers.position);
|
|
467
|
+
gl.deleteBuffer(this.glState.buffers.normal);
|
|
468
|
+
gl.deleteBuffer(this.glState.buffers.uv);
|
|
469
|
+
gl.deleteBuffer(this.glState.buffers.index);
|
|
470
|
+
gl.deleteBuffer(this.glState.buffers.wireframeIndex);
|
|
471
|
+
}
|
|
472
|
+
if (this._proceduralTexture && this.glState) {
|
|
473
|
+
this.glState.gl.deleteTexture(this._proceduralTexture);
|
|
526
474
|
}
|
|
527
475
|
}
|
|
528
476
|
|
|
529
477
|
downloadAsPNG(filename = "neat.png") {
|
|
530
|
-
console.log("Downloading as PNG", this._ref);
|
|
531
478
|
const dataURL = this._ref.toDataURL("image/png");
|
|
532
|
-
console.log("data", dataURL);
|
|
533
479
|
downloadURI(dataURL, filename);
|
|
534
480
|
}
|
|
535
481
|
|
|
536
482
|
set speed(speed: number) {
|
|
483
|
+
this._uniformsDirty = true;
|
|
537
484
|
this._speed = speed / 20;
|
|
538
485
|
}
|
|
539
486
|
|
|
540
487
|
set horizontalPressure(horizontalPressure: number) {
|
|
488
|
+
this._uniformsDirty = true;
|
|
541
489
|
this._horizontalPressure = horizontalPressure / 4;
|
|
542
490
|
}
|
|
543
491
|
|
|
544
492
|
set verticalPressure(verticalPressure: number) {
|
|
493
|
+
this._uniformsDirty = true;
|
|
545
494
|
this._verticalPressure = verticalPressure / 4;
|
|
546
495
|
}
|
|
547
496
|
|
|
548
497
|
set waveFrequencyX(waveFrequencyX: number) {
|
|
498
|
+
this._uniformsDirty = true;
|
|
549
499
|
this._waveFrequencyX = waveFrequencyX * 0.04;
|
|
550
500
|
}
|
|
551
501
|
|
|
552
502
|
set waveFrequencyY(waveFrequencyY: number) {
|
|
503
|
+
this._uniformsDirty = true;
|
|
553
504
|
this._waveFrequencyY = waveFrequencyY * 0.04;
|
|
554
505
|
}
|
|
555
506
|
|
|
556
507
|
set waveAmplitude(waveAmplitude: number) {
|
|
508
|
+
this._uniformsDirty = true;
|
|
557
509
|
this._waveAmplitude = waveAmplitude * .75;
|
|
558
510
|
}
|
|
559
511
|
|
|
560
512
|
set colors(colors: NeatColor[]) {
|
|
513
|
+
this._uniformsDirty = true;
|
|
561
514
|
this._colors = colors;
|
|
562
|
-
this.
|
|
515
|
+
this._cachedColorRgb = colors.map(c => this._hexToRgb(c.color));
|
|
516
|
+
this._colorsChanged = true;
|
|
563
517
|
}
|
|
564
518
|
|
|
565
519
|
set highlights(highlights: number) {
|
|
520
|
+
this._uniformsDirty = true;
|
|
566
521
|
this._highlights = highlights / 100;
|
|
567
522
|
}
|
|
568
523
|
|
|
569
524
|
set shadows(shadows: number) {
|
|
525
|
+
this._uniformsDirty = true;
|
|
570
526
|
this._shadows = shadows / 100;
|
|
571
527
|
}
|
|
572
528
|
|
|
573
529
|
set colorSaturation(colorSaturation: number) {
|
|
530
|
+
this._uniformsDirty = true;
|
|
574
531
|
this._saturation = colorSaturation / 10;
|
|
575
532
|
}
|
|
576
533
|
|
|
577
534
|
set colorBrightness(colorBrightness: number) {
|
|
535
|
+
this._uniformsDirty = true;
|
|
578
536
|
this._brightness = colorBrightness;
|
|
579
537
|
}
|
|
580
538
|
|
|
581
539
|
set colorBlending(colorBlending: number) {
|
|
540
|
+
this._uniformsDirty = true;
|
|
582
541
|
this._colorBlending = colorBlending / 10;
|
|
583
542
|
}
|
|
584
543
|
|
|
585
544
|
set grainScale(grainScale: number) {
|
|
545
|
+
this._uniformsDirty = true;
|
|
586
546
|
this._grainScale = grainScale == 0 ? 1 : grainScale;
|
|
587
547
|
}
|
|
588
548
|
|
|
589
549
|
set grainIntensity(grainIntensity: number) {
|
|
550
|
+
this._uniformsDirty = true;
|
|
590
551
|
this._grainIntensity = grainIntensity;
|
|
591
552
|
}
|
|
592
553
|
|
|
593
554
|
set grainSparsity(grainSparsity: number) {
|
|
555
|
+
this._uniformsDirty = true;
|
|
594
556
|
this._grainSparsity = grainSparsity;
|
|
595
557
|
}
|
|
596
558
|
|
|
597
559
|
set grainSpeed(grainSpeed: number) {
|
|
560
|
+
this._uniformsDirty = true;
|
|
598
561
|
this._grainSpeed = grainSpeed;
|
|
599
562
|
}
|
|
600
563
|
|
|
601
564
|
set wireframe(wireframe: boolean) {
|
|
565
|
+
this._uniformsDirty = true;
|
|
602
566
|
this._wireframe = wireframe;
|
|
603
567
|
}
|
|
604
568
|
|
|
605
569
|
set resolution(resolution: number) {
|
|
606
|
-
this.
|
|
570
|
+
this._uniformsDirty = true;
|
|
571
|
+
if (this.glState) {
|
|
572
|
+
const gl = this.glState.gl;
|
|
573
|
+
gl.deleteProgram(this.glState.program);
|
|
574
|
+
gl.deleteBuffer(this.glState.buffers.position);
|
|
575
|
+
gl.deleteBuffer(this.glState.buffers.normal);
|
|
576
|
+
gl.deleteBuffer(this.glState.buffers.uv);
|
|
577
|
+
gl.deleteBuffer(this.glState.buffers.index);
|
|
578
|
+
gl.deleteBuffer(this.glState.buffers.wireframeIndex);
|
|
579
|
+
}
|
|
580
|
+
this.glState = this._initScene(resolution);
|
|
607
581
|
}
|
|
608
582
|
|
|
609
583
|
set backgroundColor(backgroundColor: string) {
|
|
584
|
+
this._uniformsDirty = true;
|
|
610
585
|
this._backgroundColor = backgroundColor;
|
|
586
|
+
this._backgroundColorRgb = this._hexToRgb(backgroundColor);
|
|
611
587
|
}
|
|
612
588
|
|
|
613
589
|
set backgroundAlpha(backgroundAlpha: number) {
|
|
590
|
+
this._uniformsDirty = true;
|
|
614
591
|
this._backgroundAlpha = backgroundAlpha;
|
|
615
592
|
}
|
|
616
593
|
|
|
617
594
|
set yOffset(yOffset: number) {
|
|
595
|
+
this._uniformsDirty = true;
|
|
618
596
|
this._yOffset = yOffset;
|
|
619
597
|
}
|
|
620
598
|
|
|
@@ -623,6 +601,7 @@ export class NeatGradient implements NeatController {
|
|
|
623
601
|
}
|
|
624
602
|
|
|
625
603
|
set yOffsetWaveMultiplier(value: number) {
|
|
604
|
+
this._uniformsDirty = true;
|
|
626
605
|
this._yOffsetWaveMultiplier = value / 1000;
|
|
627
606
|
}
|
|
628
607
|
|
|
@@ -631,6 +610,7 @@ export class NeatGradient implements NeatController {
|
|
|
631
610
|
}
|
|
632
611
|
|
|
633
612
|
set yOffsetColorMultiplier(value: number) {
|
|
613
|
+
this._uniformsDirty = true;
|
|
634
614
|
this._yOffsetColorMultiplier = value / 1000;
|
|
635
615
|
}
|
|
636
616
|
|
|
@@ -639,26 +619,32 @@ export class NeatGradient implements NeatController {
|
|
|
639
619
|
}
|
|
640
620
|
|
|
641
621
|
set yOffsetFlowMultiplier(value: number) {
|
|
622
|
+
this._uniformsDirty = true;
|
|
642
623
|
this._yOffsetFlowMultiplier = value / 1000;
|
|
643
624
|
}
|
|
644
625
|
|
|
645
626
|
set flowDistortionA(value: number) {
|
|
627
|
+
this._uniformsDirty = true;
|
|
646
628
|
this._flowDistortionA = value;
|
|
647
629
|
}
|
|
648
630
|
|
|
649
631
|
set flowDistortionB(value: number) {
|
|
632
|
+
this._uniformsDirty = true;
|
|
650
633
|
this._flowDistortionB = value;
|
|
651
634
|
}
|
|
652
635
|
|
|
653
636
|
set flowScale(value: number) {
|
|
637
|
+
this._uniformsDirty = true;
|
|
654
638
|
this._flowScale = value;
|
|
655
639
|
}
|
|
656
640
|
|
|
657
641
|
set flowEase(value: number) {
|
|
642
|
+
this._uniformsDirty = true;
|
|
658
643
|
this._flowEase = value;
|
|
659
644
|
}
|
|
660
645
|
|
|
661
646
|
set flowEnabled(value: boolean) {
|
|
647
|
+
this._uniformsDirty = true;
|
|
662
648
|
this._flowEnabled = value;
|
|
663
649
|
}
|
|
664
650
|
|
|
@@ -667,34 +653,9 @@ export class NeatGradient implements NeatController {
|
|
|
667
653
|
}
|
|
668
654
|
|
|
669
655
|
|
|
670
|
-
set mouseDistortionStrength(value: number) {
|
|
671
|
-
this._mouseDistortionStrength = Math.max(0, value);
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
set mouseDistortionRadius(value: number) {
|
|
675
|
-
// Clamp to a sane range in UV space
|
|
676
|
-
this._mouseDistortionRadius = Math.max(0.01, Math.min(value, 1.0));
|
|
677
|
-
// Update brush scale when radius changes
|
|
678
|
-
this._updateBrushScale();
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
_updateBrushScale() {
|
|
682
|
-
if (!this._mouseObjects || this._mouseObjects.length === 0) return;
|
|
683
|
-
// Radius directly controls the brush scale
|
|
684
|
-
// Base geometry is 200px, so radius 0.25 = 50px, radius 1.0 = 200px
|
|
685
|
-
this._mouseBrushBaseScale = this._mouseDistortionRadius;
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
set mouseDecayRate(value: number) {
|
|
689
|
-
// Clamp between 0.9 (slow decay, more wobble) and 0.99 (fast decay, less wobble)
|
|
690
|
-
this._mouseDecayRate = Math.max(0.9, Math.min(value, 0.99));
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
set mouseDarken(value: number) {
|
|
694
|
-
this._mouseDarken = value;
|
|
695
|
-
}
|
|
696
656
|
|
|
697
657
|
set enableProceduralTexture(value: boolean) {
|
|
658
|
+
this._uniformsDirty = true;
|
|
698
659
|
this._enableProceduralTexture = value;
|
|
699
660
|
if (value && !this._proceduralTexture) {
|
|
700
661
|
this._textureNeedsUpdate = true;
|
|
@@ -702,6 +663,7 @@ export class NeatGradient implements NeatController {
|
|
|
702
663
|
}
|
|
703
664
|
|
|
704
665
|
set textureVoidLikelihood(value: number) {
|
|
666
|
+
this._uniformsDirty = true;
|
|
705
667
|
this._textureVoidLikelihood = value;
|
|
706
668
|
if (this._enableProceduralTexture) {
|
|
707
669
|
this._textureNeedsUpdate = true;
|
|
@@ -709,6 +671,7 @@ export class NeatGradient implements NeatController {
|
|
|
709
671
|
}
|
|
710
672
|
|
|
711
673
|
set textureVoidWidthMin(value: number) {
|
|
674
|
+
this._uniformsDirty = true;
|
|
712
675
|
this._textureVoidWidthMin = value;
|
|
713
676
|
if (this._enableProceduralTexture) {
|
|
714
677
|
this._textureNeedsUpdate = true;
|
|
@@ -716,6 +679,7 @@ export class NeatGradient implements NeatController {
|
|
|
716
679
|
}
|
|
717
680
|
|
|
718
681
|
set textureVoidWidthMax(value: number) {
|
|
682
|
+
this._uniformsDirty = true;
|
|
719
683
|
this._textureVoidWidthMax = value;
|
|
720
684
|
if (this._enableProceduralTexture) {
|
|
721
685
|
this._textureNeedsUpdate = true;
|
|
@@ -723,6 +687,7 @@ export class NeatGradient implements NeatController {
|
|
|
723
687
|
}
|
|
724
688
|
|
|
725
689
|
set textureBandDensity(value: number) {
|
|
690
|
+
this._uniformsDirty = true;
|
|
726
691
|
this._textureBandDensity = value;
|
|
727
692
|
if (this._enableProceduralTexture) {
|
|
728
693
|
this._textureNeedsUpdate = true;
|
|
@@ -730,6 +695,7 @@ export class NeatGradient implements NeatController {
|
|
|
730
695
|
}
|
|
731
696
|
|
|
732
697
|
set textureColorBlending(value: number) {
|
|
698
|
+
this._uniformsDirty = true;
|
|
733
699
|
this._textureColorBlending = value;
|
|
734
700
|
if (this._enableProceduralTexture) {
|
|
735
701
|
this._textureNeedsUpdate = true;
|
|
@@ -737,6 +703,7 @@ export class NeatGradient implements NeatController {
|
|
|
737
703
|
}
|
|
738
704
|
|
|
739
705
|
set textureSeed(value: number) {
|
|
706
|
+
this._uniformsDirty = true;
|
|
740
707
|
this._textureSeed = value;
|
|
741
708
|
if (this._enableProceduralTexture) {
|
|
742
709
|
this._textureNeedsUpdate = true;
|
|
@@ -748,10 +715,12 @@ export class NeatGradient implements NeatController {
|
|
|
748
715
|
}
|
|
749
716
|
|
|
750
717
|
set textureEase(value: number) {
|
|
718
|
+
this._uniformsDirty = true;
|
|
751
719
|
this._textureEase = value;
|
|
752
720
|
}
|
|
753
721
|
|
|
754
722
|
set proceduralBackgroundColor(value: string) {
|
|
723
|
+
this._uniformsDirty = true;
|
|
755
724
|
this._proceduralBackgroundColor = value;
|
|
756
725
|
if (this._enableProceduralTexture) {
|
|
757
726
|
this._textureNeedsUpdate = true;
|
|
@@ -759,244 +728,222 @@ export class NeatGradient implements NeatController {
|
|
|
759
728
|
}
|
|
760
729
|
|
|
761
730
|
set textureShapeTriangles(value: number) {
|
|
731
|
+
this._uniformsDirty = true;
|
|
762
732
|
this._textureShapeTriangles = value;
|
|
763
733
|
if (this._enableProceduralTexture) this._textureNeedsUpdate = true;
|
|
764
734
|
}
|
|
765
735
|
set textureShapeCircles(value: number) {
|
|
736
|
+
this._uniformsDirty = true;
|
|
766
737
|
this._textureShapeCircles = value;
|
|
767
738
|
if (this._enableProceduralTexture) this._textureNeedsUpdate = true;
|
|
768
739
|
}
|
|
769
740
|
set textureShapeBars(value: number) {
|
|
741
|
+
this._uniformsDirty = true;
|
|
770
742
|
this._textureShapeBars = value;
|
|
771
743
|
if (this._enableProceduralTexture) this._textureNeedsUpdate = true;
|
|
772
744
|
}
|
|
773
745
|
set textureShapeSquiggles(value: number) {
|
|
746
|
+
this._uniformsDirty = true;
|
|
774
747
|
this._textureShapeSquiggles = value;
|
|
775
748
|
if (this._enableProceduralTexture) this._textureNeedsUpdate = true;
|
|
776
749
|
}
|
|
777
750
|
|
|
778
|
-
|
|
751
|
+
_hexToRgb(hex: string): [number, number, number] {
|
|
752
|
+
const bigint = parseInt(hex.replace('#', ''), 16);
|
|
753
|
+
return [
|
|
754
|
+
((bigint >> 16) & 255) / 255.0,
|
|
755
|
+
((bigint >> 8) & 255) / 255.0,
|
|
756
|
+
(bigint & 255) / 255.0
|
|
757
|
+
];
|
|
758
|
+
}
|
|
779
759
|
|
|
780
|
-
|
|
781
|
-
height = this._ref.height;
|
|
760
|
+
_initScene(resolution: number): WebGLState {
|
|
782
761
|
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
this.sceneState.renderer.dispose();
|
|
786
|
-
this.sceneState.meshes.forEach(m => {
|
|
787
|
-
m.geometry.dispose();
|
|
788
|
-
if (Array.isArray(m.material)) m.material.forEach(mat => mat.dispose());
|
|
789
|
-
else m.material.dispose();
|
|
790
|
-
});
|
|
791
|
-
}
|
|
762
|
+
const width = this._ref.clientWidth;
|
|
763
|
+
const height = this._ref.clientHeight;
|
|
792
764
|
|
|
793
|
-
const
|
|
794
|
-
|
|
795
|
-
alpha: true,
|
|
796
|
-
preserveDrawingBuffer: true,
|
|
797
|
-
canvas: this._ref
|
|
798
|
-
});
|
|
765
|
+
const gl = this._ref.getContext("webgl2", { alpha: true, preserveDrawingBuffer: true, antialias: true }) ||
|
|
766
|
+
this._ref.getContext("webgl", { alpha: true, preserveDrawingBuffer: true, antialias: true });
|
|
799
767
|
|
|
800
|
-
|
|
801
|
-
|
|
768
|
+
if (!gl) {
|
|
769
|
+
throw new Error("WebGL not supported");
|
|
770
|
+
}
|
|
802
771
|
|
|
803
|
-
const
|
|
772
|
+
const ext = gl.getExtension("OES_standard_derivatives");
|
|
773
|
+
gl.getExtension("OES_element_index_uint");
|
|
804
774
|
|
|
805
|
-
|
|
775
|
+
gl.viewport(0, 0, width, height);
|
|
806
776
|
|
|
807
|
-
|
|
777
|
+
// Generate plane geometry with Uint32Array for large meshes
|
|
778
|
+
const { position, normal, uv, index, wireframeIndex } = generatePlaneGeometry(PLANE_WIDTH, PLANE_HEIGHT, 240 * resolution, 240 * resolution);
|
|
808
779
|
|
|
809
|
-
const
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
plane.position.z = -1;
|
|
813
|
-
meshes.push(plane);
|
|
814
|
-
scene.add(plane);
|
|
780
|
+
const positionBuffer = gl.createBuffer()!;
|
|
781
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
|
782
|
+
gl.bufferData(gl.ARRAY_BUFFER, position, gl.STATIC_DRAW);
|
|
815
783
|
|
|
816
|
-
const
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
updateCamera(camera, width, height);
|
|
784
|
+
const normalBuffer = gl.createBuffer()!;
|
|
785
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
|
|
786
|
+
gl.bufferData(gl.ARRAY_BUFFER, normal, gl.STATIC_DRAW);
|
|
820
787
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
scene,
|
|
825
|
-
meshes,
|
|
826
|
-
resolution
|
|
827
|
-
};
|
|
828
|
-
}
|
|
788
|
+
const uvBuffer = gl.createBuffer()!;
|
|
789
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, uvBuffer);
|
|
790
|
+
gl.bufferData(gl.ARRAY_BUFFER, uv, gl.STATIC_DRAW);
|
|
829
791
|
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
// We create 6 objects and just update them in the render loop to avoid GC
|
|
834
|
-
const colors = Array.from({ length: COLORS_COUNT }).map((_, i) => ({
|
|
835
|
-
is_active: i < this._colors.length ? (this._colors[i].enabled ? 1.0 : 0.0) : 0.0,
|
|
836
|
-
color: new THREE.Color(i < this._colors.length ? this._colors[i].color : 0x000000),
|
|
837
|
-
influence: i < this._colors.length ? (this._colors[i].influence || 0) : 0
|
|
838
|
-
}));
|
|
839
|
-
|
|
840
|
-
const uniforms = {
|
|
841
|
-
u_time: { value: 0 },
|
|
842
|
-
u_color_pressure: { value: new THREE.Vector2(this._horizontalPressure, this._verticalPressure) },
|
|
843
|
-
u_wave_frequency_x: { value: this._waveFrequencyX },
|
|
844
|
-
u_wave_frequency_y: { value: this._waveFrequencyY },
|
|
845
|
-
u_wave_amplitude: { value: this._waveAmplitude },
|
|
846
|
-
u_resolution: { value: new THREE.Vector2(width, height) },
|
|
847
|
-
u_colors: { value: colors },
|
|
848
|
-
u_colors_count: { value: this._colors.length },
|
|
849
|
-
u_plane_width: { value: PLANE_WIDTH },
|
|
850
|
-
u_plane_height: { value: PLANE_HEIGHT },
|
|
851
|
-
u_shadows: { value: this._shadows },
|
|
852
|
-
u_highlights: { value: this._highlights },
|
|
853
|
-
u_grain_intensity: { value: this._grainIntensity },
|
|
854
|
-
u_grain_sparsity: { value: this._grainSparsity },
|
|
855
|
-
u_grain_scale: { value: this._grainScale },
|
|
856
|
-
u_grain_speed: { value: this._grainSpeed },
|
|
857
|
-
// Flow field
|
|
858
|
-
u_flow_distortion_a: { value: this._flowDistortionA },
|
|
859
|
-
u_flow_distortion_b: { value: this._flowDistortionB },
|
|
860
|
-
u_flow_scale: { value: this._flowScale },
|
|
861
|
-
u_flow_ease: { value: this._flowEase },
|
|
862
|
-
u_flow_enabled: { value: this._flowEnabled ? 1.0 : 0.0 },
|
|
863
|
-
// Y offset multipliers
|
|
864
|
-
u_y_offset: { value: this._yOffset },
|
|
865
|
-
u_y_offset_wave_multiplier: { value: this._yOffsetWaveMultiplier },
|
|
866
|
-
u_y_offset_color_multiplier: { value: this._yOffsetColorMultiplier },
|
|
867
|
-
u_y_offset_flow_multiplier: { value: this._yOffsetFlowMultiplier },
|
|
868
|
-
// Mouse interaction
|
|
869
|
-
u_mouse_distortion_strength: { value: this._mouseDistortionStrength },
|
|
870
|
-
u_mouse_distortion_radius: { value: this._mouseDistortionRadius },
|
|
871
|
-
u_mouse_darken: { value: this._mouseDarken },
|
|
872
|
-
u_mouse_texture: { value: this._mouseFBO ? this._mouseFBO.texture : null },
|
|
873
|
-
// Procedural texture
|
|
874
|
-
u_procedural_texture: { value: this._proceduralTexture },
|
|
875
|
-
u_enable_procedural_texture: { value: this._enableProceduralTexture ? 1.0 : 0.0 },
|
|
876
|
-
u_texture_ease: { value: this._textureEase },
|
|
877
|
-
u_saturation: { value: this._saturation },
|
|
878
|
-
u_brightness: { value: this._brightness },
|
|
879
|
-
u_color_blending: { value: this._colorBlending }
|
|
880
|
-
};
|
|
792
|
+
const indexBuffer = gl.createBuffer()!;
|
|
793
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
|
|
794
|
+
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, index, gl.STATIC_DRAW);
|
|
881
795
|
|
|
882
|
-
const
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
fragmentShader: buildUniforms() + buildColorFunctions() + buildNoise() + buildFragmentShader()
|
|
886
|
-
});
|
|
796
|
+
const wireframeIndexBuffer = gl.createBuffer()!;
|
|
797
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, wireframeIndexBuffer);
|
|
798
|
+
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, wireframeIndex, gl.STATIC_DRAW);
|
|
887
799
|
|
|
888
|
-
//
|
|
889
|
-
|
|
800
|
+
// Rebind the triangle index buffer as default
|
|
801
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
|
|
890
802
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
803
|
+
const vertShaderSourceCombined = buildVertUniforms() + "\n" + buildNoise() + "\n" + buildColorFunctions() + "\n" + vertexShaderSource;
|
|
804
|
+
const vertShader = gl.createShader(gl.VERTEX_SHADER)!;
|
|
805
|
+
gl.shaderSource(vertShader, vertShaderSourceCombined);
|
|
806
|
+
gl.compileShader(vertShader);
|
|
807
|
+
if (!gl.getShaderParameter(vertShader, gl.COMPILE_STATUS)) {
|
|
808
|
+
console.log("VERTEX_SHADER_ERROR_START");
|
|
809
|
+
console.log("Vertex shader error: ", gl.getShaderInfoLog(vertShader));
|
|
810
|
+
console.log("GL Error Code:", gl.getError());
|
|
811
|
+
console.log("Vertex Shader Source Dump:");
|
|
812
|
+
console.log(vertShaderSourceCombined.split('\n').map((line, i) => `${i + 1}: ${line}`).join('\n'));
|
|
813
|
+
console.log("VERTEX_SHADER_ERROR_END");
|
|
814
|
+
}
|
|
894
815
|
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
const fSize = height / 2;
|
|
907
|
-
const aspect = width / height;
|
|
908
|
-
|
|
909
|
-
// FIX 4: Ensure near plane allows viewing objects at Z=0
|
|
910
|
-
// Near -100 is safer for objects at 0
|
|
911
|
-
this._cameraMouse = new THREE.OrthographicCamera(
|
|
912
|
-
-fSize * aspect, fSize * aspect,
|
|
913
|
-
fSize, -fSize,
|
|
914
|
-
0, 10000
|
|
915
|
-
);
|
|
916
|
-
this._cameraMouse.position.set(0, 0, 100);
|
|
917
|
-
|
|
918
|
-
// Create brush texture - More visible and impactful
|
|
919
|
-
const brushCanvas = document.createElement('canvas');
|
|
920
|
-
brushCanvas.width = 128;
|
|
921
|
-
brushCanvas.height = 128;
|
|
922
|
-
const bCtx = brushCanvas.getContext('2d');
|
|
923
|
-
if (bCtx) {
|
|
924
|
-
const grd = bCtx.createRadialGradient(64, 64, 0, 64, 64, 64);
|
|
925
|
-
// Match reference implementation's stronger gradient
|
|
926
|
-
grd.addColorStop(0, 'rgba(255,255,255,0.8)');
|
|
927
|
-
grd.addColorStop(0.5, 'rgba(255,255,255,0.4)');
|
|
928
|
-
grd.addColorStop(1, 'rgba(255,255,255,0)');
|
|
929
|
-
bCtx.fillStyle = grd;
|
|
930
|
-
bCtx.fillRect(0, 0, 128, 128);
|
|
816
|
+
const fragShaderSourceCombined = buildFragUniforms() + "\n" + buildColorFunctions() + "\n" + buildNoise() + "\n" + fragmentShaderSource;
|
|
817
|
+
const fragShader = gl.createShader(gl.FRAGMENT_SHADER)!;
|
|
818
|
+
gl.shaderSource(fragShader, fragShaderSourceCombined);
|
|
819
|
+
gl.compileShader(fragShader);
|
|
820
|
+
if (!gl.getShaderParameter(fragShader, gl.COMPILE_STATUS)) {
|
|
821
|
+
console.log("FRAGMENT_SHADER_ERROR_START");
|
|
822
|
+
console.log("Fragment shader error: ", gl.getShaderInfoLog(fragShader));
|
|
823
|
+
console.log("GL Error Code:", gl.getError());
|
|
824
|
+
console.log("Fragment Shader Source Dump:");
|
|
825
|
+
console.log(fragShaderSourceCombined.split('\n').map((line, i) => `${i + 1}: ${line}`).join('\n'));
|
|
826
|
+
console.log("FRAGMENT_SHADER_ERROR_END");
|
|
931
827
|
}
|
|
932
|
-
|
|
933
|
-
const
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
// Create brush pool
|
|
944
|
-
const brushPoolSize = 50;
|
|
945
|
-
for (let i = 0; i < brushPoolSize; i++) {
|
|
946
|
-
const m = new THREE.Mesh(brushGeo, brushMat.clone());
|
|
947
|
-
m.visible = false;
|
|
948
|
-
this._sceneMouse!.add(m);
|
|
949
|
-
this._mouseObjects.push({ mesh: m, active: false });
|
|
828
|
+
|
|
829
|
+
const program = gl.createProgram()!;
|
|
830
|
+
gl.attachShader(program, vertShader);
|
|
831
|
+
gl.attachShader(program, fragShader);
|
|
832
|
+
gl.linkProgram(program);
|
|
833
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
834
|
+
console.log("PROGRAM_LINK_ERROR_START");
|
|
835
|
+
console.log("Program linking error: ", gl.getProgramInfoLog(program));
|
|
836
|
+
console.log("GL Error Code:", gl.getError());
|
|
837
|
+
console.log("PROGRAM_LINK_ERROR_END");
|
|
950
838
|
}
|
|
951
839
|
|
|
952
|
-
|
|
953
|
-
this._updateBrushScale();
|
|
840
|
+
gl.useProgram(program);
|
|
954
841
|
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
842
|
+
const camera = new OrthographicCamera(0, 0, 0, 0, 0, 1000);
|
|
843
|
+
camera.position = [0, 0, 5];
|
|
844
|
+
updateCamera(camera, width, height);
|
|
845
|
+
|
|
846
|
+
// Define attributes
|
|
847
|
+
const aPosition = gl.getAttribLocation(program, "position");
|
|
848
|
+
const aNormal = gl.getAttribLocation(program, "normal");
|
|
849
|
+
const aUv = gl.getAttribLocation(program, "uv");
|
|
850
|
+
|
|
851
|
+
gl.enableVertexAttribArray(aPosition);
|
|
852
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
|
853
|
+
gl.vertexAttribPointer(aPosition, 3, gl.FLOAT, false, 0, 0);
|
|
854
|
+
|
|
855
|
+
gl.enableVertexAttribArray(aNormal);
|
|
856
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
|
|
857
|
+
gl.vertexAttribPointer(aNormal, 3, gl.FLOAT, false, 0, 0);
|
|
858
|
+
|
|
859
|
+
gl.enableVertexAttribArray(aUv);
|
|
860
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, uvBuffer);
|
|
861
|
+
gl.vertexAttribPointer(aUv, 2, gl.FLOAT, false, 0, 0);
|
|
862
|
+
|
|
863
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
|
|
864
|
+
|
|
865
|
+
const modelViewMatrix = new Matrix4();
|
|
866
|
+
// The View Matrix is the inverse of the Camera's position
|
|
867
|
+
// Camera is at [0, 0, 5], so view matrix translates by [0, 0, -5]
|
|
868
|
+
modelViewMatrix.translate(-camera.position[0], -camera.position[1], -camera.position[2]);
|
|
869
|
+
|
|
870
|
+
// The Model Matrix mimicking: plane.rotation.x = -Math.PI / 3.5; plane.position.z = -1;
|
|
871
|
+
modelViewMatrix.translate(0, 0, -1);
|
|
872
|
+
modelViewMatrix.rotateX(-Math.PI / 3.5);
|
|
958
873
|
|
|
959
|
-
|
|
960
|
-
|
|
874
|
+
const mvLoc = gl.getUniformLocation(program, "modelViewMatrix");
|
|
875
|
+
gl.uniformMatrix4fv(mvLoc, false, modelViewMatrix.elements);
|
|
961
876
|
|
|
962
|
-
const
|
|
963
|
-
|
|
964
|
-
const height = this._ref.height;
|
|
877
|
+
const projLoc = gl.getUniformLocation(program, "projectionMatrix");
|
|
878
|
+
gl.uniformMatrix4fv(projLoc, false, camera.projectionMatrix.elements);
|
|
965
879
|
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
880
|
+
const planeWidthLoc = gl.getUniformLocation(program, "u_plane_width");
|
|
881
|
+
gl.uniform1f(planeWidthLoc, PLANE_WIDTH);
|
|
882
|
+
|
|
883
|
+
const planeHeightLoc = gl.getUniformLocation(program, "u_plane_height");
|
|
884
|
+
gl.uniform1f(planeHeightLoc, PLANE_HEIGHT);
|
|
885
|
+
|
|
886
|
+
const colorsCountLoc = gl.getUniformLocation(program, "u_colors_count");
|
|
887
|
+
gl.uniform1i(colorsCountLoc, COLORS_COUNT);
|
|
888
|
+
|
|
889
|
+
const uniformsList = [
|
|
890
|
+
"u_time", "u_resolution", "u_color_pressure", "u_wave_frequency_x", "u_wave_frequency_y",
|
|
891
|
+
"u_wave_amplitude", "u_colors_count", "u_plane_width", "u_plane_height", "u_shadows",
|
|
892
|
+
"u_highlights", "u_grain_intensity", "u_grain_sparsity", "u_grain_scale", "u_grain_speed",
|
|
893
|
+
"u_flow_distortion_a", "u_flow_distortion_b", "u_flow_scale", "u_flow_ease", "u_flow_enabled",
|
|
894
|
+
"u_y_offset", "u_y_offset_wave_multiplier", "u_y_offset_color_multiplier", "u_y_offset_flow_multiplier",
|
|
895
|
+
|
|
896
|
+
"u_procedural_texture", "u_enable_procedural_texture", "u_texture_ease", "u_saturation", "u_brightness", "u_color_blending"
|
|
897
|
+
];
|
|
898
|
+
|
|
899
|
+
const locations: WebGLState["locations"] = {
|
|
900
|
+
attributes: { position: aPosition, normal: aNormal, uv: aUv },
|
|
901
|
+
uniforms: {}
|
|
970
902
|
};
|
|
971
903
|
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
requestAnimationFrame(() => {
|
|
976
|
-
this._mouseUpdateScheduled = false;
|
|
977
|
-
|
|
978
|
-
if (!this._pendingMousePosition) return;
|
|
979
|
-
|
|
980
|
-
this._mouse.x = this._pendingMousePosition.x;
|
|
981
|
-
this._mouse.y = this._pendingMousePosition.y;
|
|
982
|
-
|
|
983
|
-
const brush = this._mouseObjects[this._currentBrush];
|
|
984
|
-
brush.mesh.scale.set(this._mouseBrushBaseScale, this._mouseBrushBaseScale, 1.0);
|
|
985
|
-
brush.active = true;
|
|
986
|
-
brush.mesh.visible = true;
|
|
987
|
-
brush.mesh.position.set(this._mouse.x, this._mouse.y, 0);
|
|
988
|
-
brush.mesh.rotation.z = Math.random() * Math.PI * 2;
|
|
989
|
-
if (brush.mesh.material instanceof THREE.MeshBasicMaterial) {
|
|
990
|
-
brush.mesh.material.opacity = 1.0;
|
|
991
|
-
}
|
|
992
|
-
this._currentBrush = (this._currentBrush + 1) % this._mouseObjects.length;
|
|
904
|
+
uniformsList.forEach(name => {
|
|
905
|
+
locations.uniforms[name] = gl.getUniformLocation(program, name);
|
|
906
|
+
});
|
|
993
907
|
|
|
994
|
-
|
|
995
|
-
|
|
908
|
+
// Add colors uniforms manually
|
|
909
|
+
for (let i = 0; i < COLORS_COUNT; i++) {
|
|
910
|
+
locations.uniforms[`u_colors[${i}].is_active`] = gl.getUniformLocation(program, `u_colors[${i}].is_active`);
|
|
911
|
+
locations.uniforms[`u_colors[${i}].color`] = gl.getUniformLocation(program, `u_colors[${i}].color`);
|
|
912
|
+
locations.uniforms[`u_colors[${i}].influence`] = gl.getUniformLocation(program, `u_colors[${i}].influence`);
|
|
996
913
|
}
|
|
914
|
+
|
|
915
|
+
this._initialized = true;
|
|
916
|
+
// New program needs all uniforms re-uploaded on first frame
|
|
917
|
+
this._uniformsDirty = true;
|
|
918
|
+
this._colorsChanged = true;
|
|
919
|
+
this._textureDirty = true;
|
|
920
|
+
|
|
921
|
+
// Enable alpha blending
|
|
922
|
+
gl.enable(gl.BLEND);
|
|
923
|
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
924
|
+
gl.enable(gl.DEPTH_TEST);
|
|
925
|
+
|
|
926
|
+
return {
|
|
927
|
+
gl,
|
|
928
|
+
program,
|
|
929
|
+
buffers: {
|
|
930
|
+
position: positionBuffer,
|
|
931
|
+
normal: normalBuffer,
|
|
932
|
+
uv: uvBuffer,
|
|
933
|
+
index: indexBuffer,
|
|
934
|
+
wireframeIndex: wireframeIndexBuffer
|
|
935
|
+
},
|
|
936
|
+
locations,
|
|
937
|
+
camera,
|
|
938
|
+
indexCount: index.length,
|
|
939
|
+
wireframeIndexCount: wireframeIndex.length,
|
|
940
|
+
indexType: (index instanceof Uint32Array) ? gl.UNSIGNED_INT : gl.UNSIGNED_SHORT
|
|
941
|
+
};
|
|
997
942
|
}
|
|
998
943
|
|
|
999
|
-
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
_createProceduralTexture(gl: WebGLRenderingContext | WebGL2RenderingContext): WebGLTexture | null {
|
|
1000
947
|
// Texture size - 1024 provides good balance between quality and performance
|
|
1001
948
|
// Reduced from 2048 for better performance
|
|
1002
949
|
const texSize = 1024;
|
|
@@ -1004,7 +951,7 @@ export class NeatGradient implements NeatController {
|
|
|
1004
951
|
sourceCanvas.width = texSize;
|
|
1005
952
|
sourceCanvas.height = texSize;
|
|
1006
953
|
const sCtx = sourceCanvas.getContext('2d', { willReadFrequently: true });
|
|
1007
|
-
if (!sCtx) return
|
|
954
|
+
if (!sCtx) return null;
|
|
1008
955
|
|
|
1009
956
|
let seed = this._textureSeed;
|
|
1010
957
|
const baseSeed = this._textureSeed;
|
|
@@ -1020,7 +967,7 @@ export class NeatGradient implements NeatController {
|
|
|
1020
967
|
};
|
|
1021
968
|
|
|
1022
969
|
const colors = this._colors.filter(c => c.enabled).map(c => c.color);
|
|
1023
|
-
if (colors.length === 0) return
|
|
970
|
+
if (colors.length === 0) return null;
|
|
1024
971
|
|
|
1025
972
|
// Helper functions
|
|
1026
973
|
function hexToRgb(hex: string) {
|
|
@@ -1033,7 +980,7 @@ export class NeatGradient implements NeatController {
|
|
|
1033
980
|
}
|
|
1034
981
|
|
|
1035
982
|
function rgbToHex(r: number, g: number, b: number) {
|
|
1036
|
-
return "#" + ((1 << 24) + (Math.round(r) << 16) + (Math.round(g) << 8) + Math.round(b)).toString(16).slice(1);
|
|
983
|
+
return "#" + ((1 << 24) + (Math.round(r) << 16) + (Math.round(g) << 8) + Math.round(b)).toString(16).slice(1).padStart(6, '0');
|
|
1037
984
|
}
|
|
1038
985
|
|
|
1039
986
|
const getInterColor = () => {
|
|
@@ -1124,7 +1071,7 @@ export class NeatGradient implements NeatController {
|
|
|
1124
1071
|
canvas.width = texSize;
|
|
1125
1072
|
canvas.height = texSize;
|
|
1126
1073
|
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
|
1127
|
-
if (!ctx) return
|
|
1074
|
+
if (!ctx) return null;
|
|
1128
1075
|
|
|
1129
1076
|
// Start filled with the chosen void color so gaps show that color
|
|
1130
1077
|
ctx.fillStyle = baseColor;
|
|
@@ -1168,539 +1115,29 @@ export class NeatGradient implements NeatController {
|
|
|
1168
1115
|
// void segments: leave as baseColor
|
|
1169
1116
|
}
|
|
1170
1117
|
|
|
1171
|
-
const tex =
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
function updateCamera(camera: THREE.Camera, width: number, height: number) {
|
|
1192
|
-
|
|
1193
|
-
const viewPortAreaRatio = 1000000;
|
|
1194
|
-
const areaViewPort = width * height;
|
|
1195
|
-
const targetPlaneArea =
|
|
1196
|
-
areaViewPort / viewPortAreaRatio *
|
|
1197
|
-
PLANE_WIDTH * PLANE_HEIGHT / 1.5;
|
|
1198
|
-
|
|
1199
|
-
const ratio = width / height;
|
|
1200
|
-
|
|
1201
|
-
const targetWidth = Math.sqrt(targetPlaneArea * ratio);
|
|
1202
|
-
const targetHeight = targetPlaneArea / targetWidth;
|
|
1203
|
-
|
|
1204
|
-
let left = -PLANE_WIDTH / 2;
|
|
1205
|
-
let right = Math.min((left + targetWidth) / 1.5, PLANE_WIDTH / 2);
|
|
1206
|
-
|
|
1207
|
-
let top = PLANE_HEIGHT / 4;
|
|
1208
|
-
let bottom = Math.max((top - targetHeight) / 2, -PLANE_HEIGHT / 4);
|
|
1209
|
-
|
|
1210
|
-
// Fix for mobile portrait: adjust bounds for proper aspect ratio AND zoom out slightly
|
|
1211
|
-
if (ratio < 1) {
|
|
1212
|
-
// Portrait mode - scale horizontal bounds by aspect ratio to prevent stretching
|
|
1213
|
-
const horizontalScale = ratio;
|
|
1214
|
-
left = left * horizontalScale;
|
|
1215
|
-
right = right * horizontalScale;
|
|
1216
|
-
|
|
1217
|
-
// Zoom out slightly on mobile (1.1 = 10% zoom out)
|
|
1218
|
-
const mobileZoomFactor = 1.05;
|
|
1219
|
-
left = left * mobileZoomFactor;
|
|
1220
|
-
right = right * mobileZoomFactor;
|
|
1221
|
-
top = top * mobileZoomFactor;
|
|
1222
|
-
bottom = bottom * mobileZoomFactor;
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
const near = -100;
|
|
1226
|
-
const far = 1000;
|
|
1227
|
-
if (camera instanceof THREE.OrthographicCamera) {
|
|
1228
|
-
camera.left = left;
|
|
1229
|
-
camera.right = right;
|
|
1230
|
-
camera.top = top;
|
|
1231
|
-
camera.bottom = bottom;
|
|
1232
|
-
camera.near = near;
|
|
1233
|
-
camera.far = far;
|
|
1234
|
-
camera.updateProjectionMatrix();
|
|
1235
|
-
} else if (camera instanceof THREE.PerspectiveCamera) {
|
|
1236
|
-
camera.aspect = width / height;
|
|
1237
|
-
camera.updateProjectionMatrix();
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
// Cache shader strings to avoid repeated concatenation
|
|
1244
|
-
let cachedVertexShader: string | null = null;
|
|
1245
|
-
let cachedFragmentShader: string | null = null;
|
|
1246
|
-
|
|
1247
|
-
function buildVertexShader() {
|
|
1248
|
-
if (cachedVertexShader) return cachedVertexShader;
|
|
1249
|
-
cachedVertexShader = `
|
|
1250
|
-
void main() {
|
|
1251
|
-
vUv = uv;
|
|
1252
|
-
|
|
1253
|
-
// SCROLLING LOGIC
|
|
1254
|
-
// Separate multipliers for wave, color, and flow offsets
|
|
1255
|
-
float waveOffset = -u_y_offset * u_y_offset_wave_multiplier;
|
|
1256
|
-
float colorOffset = -u_y_offset * u_y_offset_color_multiplier;
|
|
1257
|
-
float flowOffset = -u_y_offset * u_y_offset_flow_multiplier;
|
|
1258
|
-
|
|
1259
|
-
// 1. DISPLACEMENT (WAVES)
|
|
1260
|
-
// We add waveOffset to Y to scroll the wave pattern
|
|
1261
|
-
v_displacement_amount = cnoise( vec3(
|
|
1262
|
-
u_wave_frequency_x * position.x + u_time,
|
|
1263
|
-
u_wave_frequency_y * (position.y + waveOffset) + u_time,
|
|
1264
|
-
u_time
|
|
1265
|
-
));
|
|
1266
|
-
|
|
1267
|
-
// 2. FLOW FIELD
|
|
1268
|
-
// Apply flow offset to scroll the flow field mask
|
|
1269
|
-
vec2 baseUv = vUv;
|
|
1270
|
-
baseUv.y += flowOffset / u_plane_height; // Scale to match wave speed
|
|
1271
|
-
vec2 flowUv = baseUv;
|
|
1272
|
-
|
|
1273
|
-
if (u_flow_enabled > 0.5) {
|
|
1274
|
-
if (u_flow_ease > 0.0 || u_flow_distortion_a > 0.0) {
|
|
1275
|
-
vec2 ppp = -1.0 + 2.0 * baseUv;
|
|
1276
|
-
ppp += 0.1 * cos((1.5 * u_flow_scale) * ppp.yx + 1.1 * u_time + vec2(0.1, 1.1));
|
|
1277
|
-
ppp += 0.1 * cos((2.3 * u_flow_scale) * ppp.yx + 1.3 * u_time + vec2(3.2, 3.4));
|
|
1278
|
-
ppp += 0.1 * cos((2.2 * u_flow_scale) * ppp.yx + 1.7 * u_time + vec2(1.8, 5.2));
|
|
1279
|
-
ppp += u_flow_distortion_a * cos((u_flow_distortion_b * u_flow_scale) * ppp.yx + 1.4 * u_time + vec2(6.3, 3.9));
|
|
1280
|
-
|
|
1281
|
-
float r = length(ppp);
|
|
1282
|
-
flowUv = mix(baseUv, vec2(baseUv.x * (1.0 - u_flow_ease) + r * u_flow_ease, baseUv.y), u_flow_ease);
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
// Pass the standard flow UV to fragment shader (for mouse/texture)
|
|
1287
|
-
vFlowUv = flowUv;
|
|
1288
|
-
|
|
1289
|
-
// 3. COLOR MIXING
|
|
1290
|
-
// We take the computed flow UVs and apply the color offset
|
|
1291
|
-
// Scale by plane height to match wave offset speed (world space vs UV space)
|
|
1292
|
-
vec3 color = u_colors[0].color;
|
|
1293
|
-
vec2 adjustedUv = flowUv;
|
|
1294
|
-
adjustedUv.y += colorOffset / u_plane_height; // Scroll the color mixing pattern
|
|
1295
|
-
|
|
1296
|
-
vec2 noise_cord = adjustedUv * u_color_pressure;
|
|
1297
|
-
const float minNoise = .0;
|
|
1298
|
-
const float maxNoise = .9;
|
|
1299
|
-
|
|
1300
|
-
for (int i = 1; i < u_colors_count; i++) {
|
|
1301
|
-
if(u_colors[i].is_active > 0.5){
|
|
1302
|
-
float noiseFlow = (1. + float(i)) / 30.;
|
|
1303
|
-
float noiseSpeed = (1. + float(i)) * 0.11;
|
|
1304
|
-
float noiseSeed = 13. + float(i) * 7.;
|
|
1305
|
-
|
|
1306
|
-
float noise = snoise(
|
|
1307
|
-
vec3(
|
|
1308
|
-
noise_cord.x * u_color_pressure.x + u_time * noiseFlow * 2.,
|
|
1309
|
-
noise_cord.y * u_color_pressure.y,
|
|
1310
|
-
u_time * noiseSpeed
|
|
1311
|
-
) + noiseSeed
|
|
1312
|
-
) - (.1 * float(i)) + (.5 * u_color_blending);
|
|
1313
|
-
|
|
1314
|
-
noise = clamp(minNoise, maxNoise + float(i) * 0.02, noise);
|
|
1315
|
-
color = mix(color, u_colors[i].color, smoothstep(0.0, u_color_blending, noise));
|
|
1118
|
+
const tex = gl.createTexture()!;
|
|
1119
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
1120
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
|
|
1121
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
|
|
1122
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
|
|
1123
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
|
|
1124
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
1125
|
+
gl.generateMipmap(gl.TEXTURE_2D);
|
|
1126
|
+
|
|
1127
|
+
const ext = gl.getExtension('EXT_texture_filter_anisotropic') ||
|
|
1128
|
+
gl.getExtension('MOZ_EXT_texture_filter_anisotropic') ||
|
|
1129
|
+
gl.getExtension('WEBKIT_EXT_texture_filter_anisotropic');
|
|
1130
|
+
if (ext) {
|
|
1131
|
+
const max = gl.getParameter(ext.MAX_TEXTURE_MAX_ANISOTROPY_EXT);
|
|
1132
|
+
gl.texParameterf(gl.TEXTURE_2D, ext.TEXTURE_MAX_ANISOTROPY_EXT, Math.min(16, max));
|
|
1316
1133
|
}
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
v_color = color;
|
|
1320
|
-
|
|
1321
|
-
// 4. VERTEX POSITION
|
|
1322
|
-
vec3 newPosition = position + normal * v_displacement_amount * u_wave_amplitude;
|
|
1323
|
-
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
|
|
1324
|
-
v_new_position = gl_Position;
|
|
1325
|
-
}
|
|
1326
|
-
`;
|
|
1327
|
-
return cachedVertexShader;
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
function buildFragmentShader() {
|
|
1331
|
-
if (cachedFragmentShader) return cachedFragmentShader;
|
|
1332
|
-
cachedFragmentShader = `
|
|
1333
|
-
float random(vec2 p) {
|
|
1334
|
-
return fract(sin(dot(p, vec2(12.9898,78.233))) * 43758.5453);
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
float fbm(vec3 x) {
|
|
1338
|
-
float value = 0.0;
|
|
1339
|
-
float amplitude = 0.5;
|
|
1340
|
-
float frequency = 1.0;
|
|
1341
|
-
for (int i = 0; i < 4; i++) {
|
|
1342
|
-
value += amplitude * snoise(x * frequency);
|
|
1343
|
-
frequency *= 2.0;
|
|
1344
|
-
amplitude *= 0.5;
|
|
1345
|
-
}
|
|
1346
|
-
return value;
|
|
1347
|
-
}
|
|
1348
1134
|
|
|
1349
|
-
|
|
1350
|
-
// MOUSE DISTORTION
|
|
1351
|
-
vec2 finalUv = vFlowUv;
|
|
1352
|
-
|
|
1353
|
-
if (u_mouse_distortion_strength > 0.0) {
|
|
1354
|
-
vec4 mouseColor = texture2D(u_mouse_texture, vUv);
|
|
1355
|
-
float mouseValue = mouseColor.r;
|
|
1356
|
-
|
|
1357
|
-
if (mouseValue > 0.001) {
|
|
1358
|
-
float distortionAmount = mouseValue * u_mouse_distortion_strength;
|
|
1359
|
-
vec2 mouseDisp = vec2(distortionAmount, distortionAmount);
|
|
1360
|
-
finalUv -= mouseDisp;
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
vec3 baseColor;
|
|
1365
|
-
|
|
1366
|
-
if (u_enable_procedural_texture > 0.5) {
|
|
1367
|
-
// Calculate flow field distance for ease effect
|
|
1368
|
-
vec2 ppp = -1.0 + 2.0 * finalUv;
|
|
1369
|
-
ppp += 0.1 * cos((1.5 * u_flow_scale) * ppp.yx + 1.1 * u_time + vec2(0.1, 1.1));
|
|
1370
|
-
ppp += 0.1 * cos((2.3 * u_flow_scale) * ppp.yx + 1.3 * u_time + vec2(3.2, 3.4));
|
|
1371
|
-
ppp += 0.1 * cos((2.2 * u_flow_scale) * ppp.yx + 1.7 * u_time + vec2(1.8, 5.2));
|
|
1372
|
-
ppp += u_flow_distortion_a * cos((u_flow_distortion_b * u_flow_scale) * ppp.yx + 1.4 * u_time + vec2(6.3, 3.9));
|
|
1373
|
-
float r = length(ppp); // Flow distance
|
|
1374
|
-
|
|
1375
|
-
// Ease blending: 0 = topographic (flow), 1 = image (UV)
|
|
1376
|
-
float vx = (finalUv.x * u_texture_ease) + (r * (1.0 - u_texture_ease));
|
|
1377
|
-
float vy = (finalUv.y * u_texture_ease) + (0.0 * (1.0 - u_texture_ease));
|
|
1378
|
-
vec2 texUv = vec2(vx, vy);
|
|
1379
|
-
|
|
1380
|
-
// PARALLAX SCROLLING
|
|
1381
|
-
// We manually apply a smaller offset here to make the texture lag behind
|
|
1382
|
-
float parallaxFactor = 0.25; // 25% speed of the color mixing
|
|
1383
|
-
texUv.y -= (u_y_offset * u_y_offset_color_multiplier / u_plane_height) * parallaxFactor;
|
|
1384
|
-
|
|
1385
|
-
texUv *= 1.5; // Tiling scale
|
|
1386
|
-
|
|
1387
|
-
vec4 texSample = texture2D(u_procedural_texture, texUv);
|
|
1388
|
-
baseColor = texSample.rgb;
|
|
1389
|
-
} else {
|
|
1390
|
-
baseColor = v_color;
|
|
1135
|
+
return tex;
|
|
1391
1136
|
}
|
|
1392
1137
|
|
|
1393
|
-
vec3 color = baseColor;
|
|
1394
|
-
|
|
1395
|
-
// Post-processing
|
|
1396
|
-
color += pow(v_displacement_amount, 1.0) * u_highlights;
|
|
1397
|
-
color -= pow(1.0 - v_displacement_amount, 2.0) * u_shadows;
|
|
1398
|
-
color = saturation(color, 1.0 + u_saturation);
|
|
1399
|
-
color = color * u_brightness;
|
|
1400
|
-
|
|
1401
|
-
// Grain
|
|
1402
|
-
vec2 noiseCoords = gl_FragCoord.xy / u_grain_scale;
|
|
1403
|
-
float grain = (u_grain_speed != 0.0) ? fbm(vec3(noiseCoords, u_time * u_grain_speed)) : fbm(vec3(noiseCoords, 0.0));
|
|
1404
|
-
|
|
1405
|
-
grain = grain * 0.5 + 0.5;
|
|
1406
|
-
grain -= 0.5;
|
|
1407
|
-
grain = (grain > u_grain_sparsity) ? grain : 0.0;
|
|
1408
|
-
grain *= u_grain_intensity;
|
|
1409
|
-
|
|
1410
|
-
color += vec3(grain);
|
|
1411
|
-
|
|
1412
|
-
gl_FragColor = vec4(color, 1.0);
|
|
1413
|
-
}
|
|
1414
|
-
`;
|
|
1415
|
-
return cachedFragmentShader;
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
// Cache uniforms string as well
|
|
1419
|
-
let cachedUniformsShader: string | null = null;
|
|
1420
|
-
|
|
1421
|
-
const buildUniforms = () => {
|
|
1422
|
-
if (cachedUniformsShader) return cachedUniformsShader;
|
|
1423
|
-
cachedUniformsShader = `
|
|
1424
|
-
precision highp float;
|
|
1425
|
-
|
|
1426
|
-
struct Color {
|
|
1427
|
-
float is_active;
|
|
1428
|
-
vec3 color;
|
|
1429
|
-
float value;
|
|
1430
|
-
};
|
|
1431
|
-
|
|
1432
|
-
uniform float u_grain_intensity;
|
|
1433
|
-
uniform float u_grain_sparsity;
|
|
1434
|
-
uniform float u_grain_scale;
|
|
1435
|
-
uniform float u_grain_speed;
|
|
1436
|
-
uniform float u_time;
|
|
1437
|
-
|
|
1438
|
-
uniform float u_wave_amplitude;
|
|
1439
|
-
uniform float u_wave_frequency_x;
|
|
1440
|
-
uniform float u_wave_frequency_y;
|
|
1441
|
-
|
|
1442
|
-
uniform vec2 u_color_pressure;
|
|
1443
|
-
|
|
1444
|
-
uniform float u_plane_width;
|
|
1445
|
-
uniform float u_plane_height;
|
|
1446
|
-
|
|
1447
|
-
uniform float u_shadows;
|
|
1448
|
-
uniform float u_highlights;
|
|
1449
|
-
uniform float u_saturation;
|
|
1450
|
-
uniform float u_brightness;
|
|
1451
|
-
|
|
1452
|
-
uniform float u_color_blending;
|
|
1453
|
-
|
|
1454
|
-
uniform int u_colors_count;
|
|
1455
|
-
uniform Color u_colors[6];
|
|
1456
|
-
uniform vec2 u_resolution;
|
|
1457
|
-
|
|
1458
|
-
uniform float u_y_offset;
|
|
1459
|
-
uniform float u_y_offset_wave_multiplier;
|
|
1460
|
-
uniform float u_y_offset_color_multiplier;
|
|
1461
|
-
uniform float u_y_offset_flow_multiplier;
|
|
1462
|
-
|
|
1463
|
-
// Flow field uniforms
|
|
1464
|
-
uniform float u_flow_distortion_a;
|
|
1465
|
-
uniform float u_flow_distortion_b;
|
|
1466
|
-
uniform float u_flow_scale;
|
|
1467
|
-
uniform float u_flow_ease;
|
|
1468
|
-
uniform float u_flow_enabled;
|
|
1469
|
-
|
|
1470
|
-
// Mouse interaction uniforms
|
|
1471
|
-
uniform float u_mouse_distortion_strength;
|
|
1472
|
-
uniform float u_mouse_distortion_radius;
|
|
1473
|
-
uniform float u_mouse_darken;
|
|
1474
|
-
uniform sampler2D u_mouse_texture;
|
|
1475
|
-
|
|
1476
|
-
// Procedural texture uniforms
|
|
1477
|
-
uniform sampler2D u_procedural_texture;
|
|
1478
|
-
uniform float u_enable_procedural_texture;
|
|
1479
|
-
uniform float u_texture_ease;
|
|
1480
|
-
|
|
1481
|
-
varying vec2 vUv;
|
|
1482
|
-
varying vec2 vFlowUv;
|
|
1483
|
-
varying vec4 v_new_position;
|
|
1484
|
-
varying vec3 v_color;
|
|
1485
|
-
varying float v_displacement_amount;
|
|
1486
|
-
|
|
1487
|
-
`;
|
|
1488
|
-
return cachedUniformsShader;
|
|
1489
|
-
};
|
|
1490
|
-
|
|
1491
|
-
// Cache noise functions as well
|
|
1492
|
-
let cachedNoiseShader: string | null = null;
|
|
1493
|
-
|
|
1494
|
-
const buildNoise = () => {
|
|
1495
|
-
if (cachedNoiseShader) return cachedNoiseShader;
|
|
1496
|
-
cachedNoiseShader = `
|
|
1497
|
-
|
|
1498
|
-
// 1. REPLACEMENT PERMUTE:
|
|
1499
|
-
// Uses a hash function (fract/sin) instead of a modular lookup table.
|
|
1500
|
-
vec4 permute(vec4 x) {
|
|
1501
|
-
return floor(fract(sin(x) * 43758.5453123) * 289.0);
|
|
1502
|
-
}
|
|
1503
|
-
|
|
1504
|
-
// Taylor Inverse Sqrt
|
|
1505
|
-
vec4 taylorInvSqrt(vec4 r) {
|
|
1506
|
-
return 1.79284291400159 - 0.85373472095314 * r;
|
|
1507
|
-
}
|
|
1508
|
-
|
|
1509
|
-
// Fade function
|
|
1510
|
-
vec3 fade(vec3 t) {
|
|
1511
|
-
return t*t*t*(t*(t*6.0-15.0)+10.0);
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
|
-
// 3D Simplex Noise
|
|
1515
|
-
float snoise(vec3 v) {
|
|
1516
|
-
const vec2 C = vec2(1.0/6.0, 1.0/3.0) ;
|
|
1517
|
-
const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
|
|
1518
|
-
|
|
1519
|
-
// First corner
|
|
1520
|
-
vec3 i = floor(v + dot(v, C.yyy) );
|
|
1521
|
-
vec3 x0 = v - i + dot(i, C.xxx) ;
|
|
1522
|
-
|
|
1523
|
-
// Other corners
|
|
1524
|
-
vec3 g = step(x0.yzx, x0.xyz);
|
|
1525
|
-
vec3 l = 1.0 - g;
|
|
1526
|
-
vec3 i1 = min( g.xyz, l.zxy );
|
|
1527
|
-
vec3 i2 = max( g.xyz, l.zxy );
|
|
1528
|
-
|
|
1529
|
-
vec3 x1 = x0 - i1 + C.xxx;
|
|
1530
|
-
vec3 x2 = x0 - i2 + C.yyy;
|
|
1531
|
-
vec3 x3 = x0 - D.yyy;
|
|
1532
|
-
|
|
1533
|
-
// Permutations
|
|
1534
|
-
vec4 p = permute( permute( permute(
|
|
1535
|
-
i.z + vec4(0.0, i1.z, i2.z, 1.0 ))
|
|
1536
|
-
+ i.y + vec4(0.0, i1.y, i2.y, 1.0 ))
|
|
1537
|
-
+ i.x + vec4(0.0, i1.x, i2.x, 1.0 ));
|
|
1538
|
-
|
|
1539
|
-
// Gradients
|
|
1540
|
-
float n_ = 0.142857142857; // 1.0/7.0
|
|
1541
|
-
vec3 ns = n_ * D.wyz - D.xzx;
|
|
1542
|
-
|
|
1543
|
-
vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
|
|
1544
|
-
|
|
1545
|
-
vec4 x_ = floor(j * ns.z);
|
|
1546
|
-
vec4 y_ = floor(j - 7.0 * x_ );
|
|
1547
|
-
|
|
1548
|
-
vec4 x = x_ *ns.x + ns.yyyy;
|
|
1549
|
-
vec4 y = y_ *ns.x + ns.yyyy;
|
|
1550
|
-
vec4 h = 1.0 - abs(x) - abs(y);
|
|
1551
|
-
|
|
1552
|
-
vec4 b0 = vec4( x.xy, y.xy );
|
|
1553
|
-
vec4 b1 = vec4( x.zw, y.zw );
|
|
1554
|
-
|
|
1555
|
-
vec4 s0 = floor(b0)*2.0 + 1.0;
|
|
1556
|
-
vec4 s1 = floor(b1)*2.0 + 1.0;
|
|
1557
|
-
vec4 sh = -step(h, vec4(0.0));
|
|
1558
|
-
|
|
1559
|
-
vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ;
|
|
1560
|
-
vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ;
|
|
1561
|
-
|
|
1562
|
-
vec3 p0 = vec3(a0.xy,h.x);
|
|
1563
|
-
vec3 p1 = vec3(a0.zw,h.y);
|
|
1564
|
-
vec3 p2 = vec3(a1.xy,h.z);
|
|
1565
|
-
vec3 p3 = vec3(a1.zw,h.w);
|
|
1566
|
-
|
|
1567
|
-
// Normalise gradients
|
|
1568
|
-
vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
|
|
1569
|
-
p0 *= norm.x;
|
|
1570
|
-
p1 *= norm.y;
|
|
1571
|
-
p2 *= norm.z;
|
|
1572
|
-
p3 *= norm.w;
|
|
1573
|
-
|
|
1574
|
-
// Mix final noise value
|
|
1575
|
-
vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
|
|
1576
|
-
m = m * m;
|
|
1577
|
-
return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1),
|
|
1578
|
-
dot(p2,x2), dot(p3,x3) ) );
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
|
-
// Classic Perlin noise
|
|
1582
|
-
float cnoise(vec3 P)
|
|
1583
|
-
{
|
|
1584
|
-
vec3 Pi0 = floor(P);
|
|
1585
|
-
vec3 Pi1 = Pi0 + vec3(1.0);
|
|
1586
|
-
|
|
1587
|
-
vec3 Pf0 = fract(P);
|
|
1588
|
-
vec3 Pf1 = Pf0 - vec3(1.0);
|
|
1589
|
-
vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x);
|
|
1590
|
-
vec4 iy = vec4(Pi0.yy, Pi1.yy);
|
|
1591
|
-
vec4 iz0 = Pi0.zzzz;
|
|
1592
|
-
vec4 iz1 = Pi1.zzzz;
|
|
1593
|
-
|
|
1594
|
-
vec4 ixy = permute(permute(ix) + iy);
|
|
1595
|
-
vec4 ixy0 = permute(ixy + iz0);
|
|
1596
|
-
vec4 ixy1 = permute(ixy + iz1);
|
|
1597
|
-
|
|
1598
|
-
vec4 gx0 = ixy0 * (1.0 / 7.0);
|
|
1599
|
-
vec4 gy0 = fract(floor(gx0) * (1.0 / 7.0)) - 0.5;
|
|
1600
|
-
gx0 = fract(gx0);
|
|
1601
|
-
vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0);
|
|
1602
|
-
vec4 sz0 = step(gz0, vec4(0.0));
|
|
1603
|
-
gx0 -= sz0 * (step(0.0, gx0) - 0.5);
|
|
1604
|
-
gy0 -= sz0 * (step(0.0, gy0) - 0.5);
|
|
1605
|
-
|
|
1606
|
-
vec4 gx1 = ixy1 * (1.0 / 7.0);
|
|
1607
|
-
vec4 gy1 = fract(floor(gx1) * (1.0 / 7.0)) - 0.5;
|
|
1608
|
-
gx1 = fract(gx1);
|
|
1609
|
-
vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1);
|
|
1610
|
-
vec4 sz1 = step(gz1, vec4(0.0));
|
|
1611
|
-
gx1 -= sz1 * (step(0.0, gx1) - 0.5);
|
|
1612
|
-
gy1 -= sz1 * (step(0.0, gy1) - 0.5);
|
|
1613
|
-
|
|
1614
|
-
vec3 g000 = vec3(gx0.x,gy0.x,gz0.x);
|
|
1615
|
-
vec3 g100 = vec3(gx0.y,gy0.y,gz0.y);
|
|
1616
|
-
vec3 g010 = vec3(gx0.z,gy0.z,gz0.z);
|
|
1617
|
-
vec3 g110 = vec3(gx0.w,gy0.w,gz0.w);
|
|
1618
|
-
vec3 g001 = vec3(gx1.x,gy1.x,gz1.x);
|
|
1619
|
-
vec3 g101 = vec3(gx1.y,gy1.y,gz1.y);
|
|
1620
|
-
vec3 g011 = vec3(gx1.z,gy1.z,gz1.z);
|
|
1621
|
-
vec3 g111 = vec3(gx1.w,gy1.w,gz1.w);
|
|
1622
|
-
|
|
1623
|
-
vec4 norm0 = taylorInvSqrt(vec4(dot(g000, g000), dot(g010, g010), dot(g100, g100), dot(g110, g110)));
|
|
1624
|
-
g000 *= norm0.x;
|
|
1625
|
-
g010 *= norm0.y;
|
|
1626
|
-
g100 *= norm0.z;
|
|
1627
|
-
g110 *= norm0.w;
|
|
1628
|
-
vec4 norm1 = taylorInvSqrt(vec4(dot(g001, g001), dot(g011, g011), dot(g101, g101), dot(g111, g111)));
|
|
1629
|
-
g001 *= norm1.x;
|
|
1630
|
-
g011 *= norm1.y;
|
|
1631
|
-
g101 *= norm1.z;
|
|
1632
|
-
g111 *= norm1.w;
|
|
1633
|
-
|
|
1634
|
-
float n000 = dot(g000, Pf0);
|
|
1635
|
-
float n100 = dot(g100, vec3(Pf1.x, Pf0.yz));
|
|
1636
|
-
float n010 = dot(g010, vec3(Pf0.x, Pf1.y, Pf0.z));
|
|
1637
|
-
float n110 = dot(g110, vec3(Pf1.xy, Pf0.z));
|
|
1638
|
-
float n001 = dot(g001, vec3(Pf0.xy, Pf1.z));
|
|
1639
|
-
float n101 = dot(g101, vec3(Pf1.x, Pf0.y, Pf1.z));
|
|
1640
|
-
float n011 = dot(g011, vec3(Pf0.x, Pf1.yz));
|
|
1641
|
-
float n111 = dot(g111, Pf1);
|
|
1642
|
-
|
|
1643
|
-
vec3 fade_xyz = fade(Pf0);
|
|
1644
|
-
vec4 n_z = mix(vec4(n000, n100, n010, n110), vec4(n001, n101, n011, n111), fade_xyz.z);
|
|
1645
|
-
vec2 n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y);
|
|
1646
|
-
float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x);
|
|
1647
|
-
return 2.2 * n_xyz;
|
|
1648
|
-
}
|
|
1649
|
-
`;
|
|
1650
|
-
return cachedNoiseShader;
|
|
1651
|
-
};
|
|
1652
|
-
|
|
1653
|
-
// Cache color functions as well
|
|
1654
|
-
let cachedColorFunctionsShader: string | null = null;
|
|
1655
1138
|
|
|
1656
|
-
const buildColorFunctions = () => {
|
|
1657
|
-
if (cachedColorFunctionsShader) return cachedColorFunctionsShader;
|
|
1658
|
-
cachedColorFunctionsShader = `
|
|
1659
|
-
|
|
1660
|
-
vec3 saturation(vec3 rgb, float adjustment) {
|
|
1661
|
-
const vec3 W = vec3(0.2125, 0.7154, 0.0721);
|
|
1662
|
-
vec3 intensity = vec3(dot(rgb, W));
|
|
1663
|
-
return mix(intensity, rgb, adjustment);
|
|
1664
|
-
}
|
|
1665
|
-
|
|
1666
|
-
float saturation(vec3 rgb)
|
|
1667
|
-
{
|
|
1668
|
-
vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
|
|
1669
|
-
vec4 p = mix(vec4(rgb.bg, K.wz), vec4(rgb.gb, K.xy), step(rgb.b, rgb.g));
|
|
1670
|
-
vec4 q = mix(vec4(p.xyw, rgb.r), vec4(rgb.r, p.yzx), step(p.x, rgb.r));
|
|
1671
|
-
|
|
1672
|
-
float d = q.x - min(q.w, q.y);
|
|
1673
|
-
float e = 1.0e-10;
|
|
1674
|
-
return abs(6.0 * d + e);
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
|
-
// get saturation of a color in values between 0 and 1
|
|
1678
|
-
float getSaturation(vec3 color) {
|
|
1679
|
-
float max = max(color.r, max(color.g, color.b));
|
|
1680
|
-
float min = min(color.r, min(color.g, color.b));
|
|
1681
|
-
return (max - min) / max;
|
|
1682
|
-
}
|
|
1683
|
-
|
|
1684
|
-
vec3 rgb2hsv(vec3 c)
|
|
1685
|
-
{
|
|
1686
|
-
vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
|
|
1687
|
-
vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
|
|
1688
|
-
vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
|
|
1689
|
-
|
|
1690
|
-
float d = q.x - min(q.w, q.y);
|
|
1691
|
-
float e = 1.0e-10;
|
|
1692
|
-
return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
|
|
1693
1139
|
}
|
|
1694
1140
|
|
|
1695
|
-
vec3 hsv2rgb(vec3 c)
|
|
1696
|
-
{
|
|
1697
|
-
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
|
|
1698
|
-
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
|
|
1699
|
-
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
|
|
1700
|
-
}
|
|
1701
|
-
`;
|
|
1702
|
-
return cachedColorFunctionsShader;
|
|
1703
|
-
};
|
|
1704
1141
|
|
|
1705
1142
|
const setLinkStyles = (link: HTMLAnchorElement) => {
|
|
1706
1143
|
link.id = LINK_ID;
|
|
@@ -1718,22 +1155,28 @@ const setLinkStyles = (link: HTMLAnchorElement) => {
|
|
|
1718
1155
|
link.style.fontWeight = "bold";
|
|
1719
1156
|
link.style.textDecoration = "none";
|
|
1720
1157
|
link.style.zIndex = "10000";
|
|
1158
|
+
link.style.pointerEvents = "auto";
|
|
1159
|
+
link.setAttribute("data-n", "1");
|
|
1721
1160
|
link.innerHTML = "NEAT";
|
|
1722
1161
|
}
|
|
1723
1162
|
|
|
1724
1163
|
const addNeatLink = (ref: HTMLCanvasElement): HTMLAnchorElement => {
|
|
1725
|
-
const
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1164
|
+
const parent = ref.parentElement;
|
|
1165
|
+
// Ensure parent has position so absolute link is positioned relative to it
|
|
1166
|
+
if (parent && getComputedStyle(parent).position === "static") {
|
|
1167
|
+
parent.style.position = "relative";
|
|
1168
|
+
}
|
|
1169
|
+
// Search parent for existing neat link (survives HMR where LINK_ID changes)
|
|
1170
|
+
if (parent) {
|
|
1171
|
+
const existing = parent.querySelector('a[data-n]') as HTMLAnchorElement;
|
|
1172
|
+
if (existing) {
|
|
1173
|
+
setLinkStyles(existing);
|
|
1174
|
+
return existing;
|
|
1732
1175
|
}
|
|
1733
1176
|
}
|
|
1734
1177
|
const link = document.createElement("a");
|
|
1735
1178
|
setLinkStyles(link);
|
|
1736
|
-
|
|
1179
|
+
parent?.appendChild(link);
|
|
1737
1180
|
return link;
|
|
1738
1181
|
}
|
|
1739
1182
|
|