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