@firecms/neat 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +365 -68
- package/dist/NeatGradient.d.ts +105 -0
- package/dist/NeatGradient.js +855 -200
- package/dist/NeatGradient.js.map +1 -1
- package/dist/index.es.js +619 -235
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +139 -115
- package/dist/index.umd.js.map +1 -1
- package/package.json +3 -3
- package/src/NeatGradient.ts +1017 -204
package/src/NeatGradient.ts
CHANGED
|
@@ -4,7 +4,7 @@ const PLANE_WIDTH = 50;
|
|
|
4
4
|
const PLANE_HEIGHT = 80;
|
|
5
5
|
|
|
6
6
|
const WIREFRAME = true;
|
|
7
|
-
const COLORS_COUNT =
|
|
7
|
+
const COLORS_COUNT = 6;
|
|
8
8
|
|
|
9
9
|
const clock = new THREE.Clock();
|
|
10
10
|
|
|
@@ -18,6 +18,16 @@ type SceneState = {
|
|
|
18
18
|
resolution: number
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// Interface for the Uniforms to avoid @ts-ignore and improve access speed
|
|
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
|
+
}
|
|
30
|
+
|
|
21
31
|
export type NeatConfig = {
|
|
22
32
|
resolution?: number;
|
|
23
33
|
speed?: number;
|
|
@@ -40,6 +50,37 @@ export type NeatConfig = {
|
|
|
40
50
|
backgroundColor?: string;
|
|
41
51
|
backgroundAlpha?: number;
|
|
42
52
|
yOffset?: number;
|
|
53
|
+
yOffsetWaveMultiplier?: number;
|
|
54
|
+
yOffsetColorMultiplier?: number;
|
|
55
|
+
yOffsetFlowMultiplier?: number;
|
|
56
|
+
// Flow field parameters
|
|
57
|
+
flowDistortionA?: number;
|
|
58
|
+
flowDistortionB?: number;
|
|
59
|
+
flowScale?: number;
|
|
60
|
+
flowEase?: number;
|
|
61
|
+
flowEnabled?: boolean;
|
|
62
|
+
// Mouse interaction
|
|
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
|
+
// Texture generation
|
|
71
|
+
enableProceduralTexture?: boolean;
|
|
72
|
+
textureVoidLikelihood?: number;
|
|
73
|
+
textureVoidWidthMin?: number;
|
|
74
|
+
textureVoidWidthMax?: number;
|
|
75
|
+
textureBandDensity?: number;
|
|
76
|
+
textureColorBlending?: number;
|
|
77
|
+
textureSeed?: number;
|
|
78
|
+
textureEase?: number;
|
|
79
|
+
proceduralBackgroundColor?: string;
|
|
80
|
+
textureShapeTriangles?: number;
|
|
81
|
+
textureShapeCircles?: number;
|
|
82
|
+
textureShapeBars?: number;
|
|
83
|
+
textureShapeSquiggles?: number;
|
|
43
84
|
};
|
|
44
85
|
|
|
45
86
|
export type NeatColor = {
|
|
@@ -86,11 +127,67 @@ export class NeatGradient implements NeatController {
|
|
|
86
127
|
private _backgroundColor: string = "#FFFFFF";
|
|
87
128
|
private _backgroundAlpha: number = 1.0;
|
|
88
129
|
|
|
130
|
+
// Flow field properties
|
|
131
|
+
private _flowDistortionA: number = 0;
|
|
132
|
+
private _flowDistortionB: number = 0;
|
|
133
|
+
private _flowScale: number = 1.0;
|
|
134
|
+
private _flowEase: number = 0.0;
|
|
135
|
+
private _flowEnabled: boolean = true;
|
|
136
|
+
|
|
137
|
+
// Mouse interaction properties
|
|
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;
|
|
149
|
+
|
|
150
|
+
// Texture generation properties
|
|
151
|
+
private _enableProceduralTexture: boolean = false;
|
|
152
|
+
private _textureVoidLikelihood: number = 0.45;
|
|
153
|
+
private _textureVoidWidthMin: number = 200;
|
|
154
|
+
private _textureVoidWidthMax: number = 486;
|
|
155
|
+
private _textureBandDensity: number = 2.15;
|
|
156
|
+
private _textureColorBlending: number = 0.01;
|
|
157
|
+
private _textureSeed: number = 333;
|
|
158
|
+
private _textureEase: number = 0.5;
|
|
159
|
+
private _proceduralTexture: THREE.Texture | null = null;
|
|
160
|
+
private _proceduralBackgroundColor: string = "#000000";
|
|
161
|
+
|
|
162
|
+
private _textureShapeTriangles: number = 20;
|
|
163
|
+
private _textureShapeCircles: number = 15;
|
|
164
|
+
private _textureShapeBars: number = 15;
|
|
165
|
+
private _textureShapeSquiggles: number = 10;
|
|
166
|
+
|
|
89
167
|
private requestRef: number = -1;
|
|
90
168
|
private sizeObserver: ResizeObserver;
|
|
91
169
|
private sceneState: SceneState;
|
|
92
170
|
|
|
171
|
+
// Optimization: Cache uniforms to avoid lookups and object creation in render loop
|
|
172
|
+
private _cachedUniforms: NeatUniforms | null = null;
|
|
173
|
+
private _linkElement: HTMLAnchorElement | null = null;
|
|
174
|
+
|
|
93
175
|
private _yOffset: number = 0;
|
|
176
|
+
private _yOffsetWaveMultiplier: number = 0.004;
|
|
177
|
+
private _yOffsetColorMultiplier: number = 0.004;
|
|
178
|
+
private _yOffsetFlowMultiplier: number = 0.004;
|
|
179
|
+
|
|
180
|
+
// For saving/restoring clear color
|
|
181
|
+
private _tempClearColor = new THREE.Color();
|
|
182
|
+
|
|
183
|
+
// Performance optimizations
|
|
184
|
+
private _resizeTimeoutId: number | null = null;
|
|
185
|
+
private _textureNeedsUpdate: boolean = false;
|
|
186
|
+
private _lastColorUpdate: number = 0;
|
|
187
|
+
private _linkCheckCounter: number = 0;
|
|
188
|
+
private _mouseUpdateScheduled: boolean = false;
|
|
189
|
+
private _pendingMousePosition: { x: number; y: number } | null = null;
|
|
190
|
+
private _colorsChanged: boolean = true; // Track if colors need update
|
|
94
191
|
|
|
95
192
|
constructor(config: NeatConfig & { ref: HTMLCanvasElement, resolution?: number, seed?: number }) {
|
|
96
193
|
|
|
@@ -117,7 +214,35 @@ export class NeatGradient implements NeatController {
|
|
|
117
214
|
backgroundAlpha = 1.0,
|
|
118
215
|
resolution = 1,
|
|
119
216
|
seed,
|
|
120
|
-
yOffset = 0
|
|
217
|
+
yOffset = 0,
|
|
218
|
+
yOffsetWaveMultiplier = 4,
|
|
219
|
+
yOffsetColorMultiplier = 4,
|
|
220
|
+
yOffsetFlowMultiplier = 4,
|
|
221
|
+
// Flow field parameters
|
|
222
|
+
flowDistortionA = 0,
|
|
223
|
+
flowDistortionB = 0,
|
|
224
|
+
flowScale = 1.0,
|
|
225
|
+
flowEase = 0.0,
|
|
226
|
+
flowEnabled = true,
|
|
227
|
+
// Mouse interaction
|
|
228
|
+
mouseDistortionStrength = 0.0,
|
|
229
|
+
mouseDistortionRadius = 0.25,
|
|
230
|
+
mouseDecayRate = 0.96,
|
|
231
|
+
mouseDarken = 0.0,
|
|
232
|
+
// Texture generation
|
|
233
|
+
enableProceduralTexture = false,
|
|
234
|
+
textureVoidLikelihood = 0.45,
|
|
235
|
+
textureVoidWidthMin = 200,
|
|
236
|
+
textureVoidWidthMax = 486,
|
|
237
|
+
textureBandDensity = 2.15,
|
|
238
|
+
textureColorBlending = 0.01,
|
|
239
|
+
textureSeed = 333,
|
|
240
|
+
textureEase = 0.5,
|
|
241
|
+
proceduralBackgroundColor = "#000000",
|
|
242
|
+
textureShapeTriangles = 20,
|
|
243
|
+
textureShapeCircles = 15,
|
|
244
|
+
textureShapeBars = 15,
|
|
245
|
+
textureShapeSquiggles = 10,
|
|
121
246
|
} = config;
|
|
122
247
|
|
|
123
248
|
|
|
@@ -147,84 +272,183 @@ export class NeatGradient implements NeatController {
|
|
|
147
272
|
this.backgroundColor = backgroundColor;
|
|
148
273
|
this.backgroundAlpha = backgroundAlpha;
|
|
149
274
|
this.yOffset = yOffset;
|
|
150
|
-
|
|
275
|
+
this.yOffsetWaveMultiplier = yOffsetWaveMultiplier;
|
|
276
|
+
this.yOffsetColorMultiplier = yOffsetColorMultiplier;
|
|
277
|
+
this.yOffsetFlowMultiplier = yOffsetFlowMultiplier;
|
|
278
|
+
|
|
279
|
+
// Flow field
|
|
280
|
+
this.flowDistortionA = flowDistortionA;
|
|
281
|
+
this.flowDistortionB = flowDistortionB;
|
|
282
|
+
this.flowScale = flowScale;
|
|
283
|
+
this.flowEase = flowEase;
|
|
284
|
+
this.flowEnabled = flowEnabled;
|
|
285
|
+
|
|
286
|
+
// Mouse interaction
|
|
287
|
+
this.mouseDistortionStrength = mouseDistortionStrength;
|
|
288
|
+
this.mouseDistortionRadius = mouseDistortionRadius;
|
|
289
|
+
this.mouseDecayRate = mouseDecayRate;
|
|
290
|
+
this.mouseDarken = mouseDarken;
|
|
291
|
+
|
|
292
|
+
// Texture generation
|
|
293
|
+
this.enableProceduralTexture = enableProceduralTexture;
|
|
294
|
+
this.textureVoidLikelihood = textureVoidLikelihood;
|
|
295
|
+
this.textureVoidWidthMin = textureVoidWidthMin;
|
|
296
|
+
this.textureVoidWidthMax = textureVoidWidthMax;
|
|
297
|
+
this.textureBandDensity = textureBandDensity;
|
|
298
|
+
this.textureColorBlending = textureColorBlending;
|
|
299
|
+
this.textureSeed = textureSeed;
|
|
300
|
+
this.textureEase = textureEase;
|
|
301
|
+
this._proceduralBackgroundColor = proceduralBackgroundColor;
|
|
302
|
+
|
|
303
|
+
this._textureShapeTriangles = textureShapeTriangles;
|
|
304
|
+
this._textureShapeCircles = textureShapeCircles;
|
|
305
|
+
this._textureShapeBars = textureShapeBars;
|
|
306
|
+
this._textureShapeSquiggles = textureShapeSquiggles;
|
|
307
|
+
|
|
308
|
+
// FIX 1: Setup mouse resources BEFORE building the material/scene
|
|
309
|
+
// This ensures u_mouse_texture isn't null during material compilation
|
|
310
|
+
this._setupMouseInteraction();
|
|
151
311
|
this.sceneState = this._initScene(resolution);
|
|
152
312
|
|
|
153
313
|
let tick = seed !== undefined ? seed : getElapsedSecondsInLastHour();
|
|
314
|
+
|
|
154
315
|
const render = () => {
|
|
155
316
|
|
|
156
|
-
const { renderer, camera, scene
|
|
157
|
-
|
|
158
|
-
|
|
317
|
+
const { renderer, camera, scene } = this.sceneState;
|
|
318
|
+
|
|
319
|
+
// Optimization: check if cached link is still valid in DOM less frequently
|
|
320
|
+
this._linkCheckCounter++;
|
|
321
|
+
if (this._linkCheckCounter >= 300) { // Check every ~5 seconds at 60fps
|
|
322
|
+
this._linkCheckCounter = 0;
|
|
323
|
+
if (!this._linkElement || !document.contains(this._linkElement)) {
|
|
324
|
+
this._linkElement = addNeatLink(ref);
|
|
325
|
+
}
|
|
159
326
|
}
|
|
160
327
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const width = this._ref.width,
|
|
165
|
-
height = this._ref.height;
|
|
166
|
-
|
|
167
|
-
const colors = [
|
|
168
|
-
...this._colors.map(color => {
|
|
169
|
-
let threeColor = new THREE.Color();
|
|
170
|
-
threeColor.setStyle(color.color, "");
|
|
171
|
-
return ({
|
|
172
|
-
is_active: color.enabled,
|
|
173
|
-
color: threeColor,
|
|
174
|
-
influence: color.influence
|
|
175
|
-
});
|
|
176
|
-
}),
|
|
177
|
-
...Array.from({ length: COLORS_COUNT - this._colors.length }).map(() => ({
|
|
178
|
-
is_active: false,
|
|
179
|
-
color: new THREE.Color(0x000000)
|
|
180
|
-
}))
|
|
181
|
-
];
|
|
328
|
+
// Update Uniforms efficiently without creating new objects
|
|
329
|
+
if (this._cachedUniforms) {
|
|
330
|
+
const u = this._cachedUniforms;
|
|
182
331
|
|
|
183
332
|
tick += clock.getDelta() * this._speed;
|
|
184
|
-
// @ts-ignore
|
|
185
|
-
mesh.material.uniforms.u_time.value = tick;
|
|
186
|
-
// @ts-ignore
|
|
187
|
-
mesh.material.uniforms.u_resolution = { value: new THREE.Vector2(width, height) };
|
|
188
|
-
// @ts-ignore
|
|
189
|
-
mesh.material.uniforms.u_color_pressure = { value: new THREE.Vector2(this._horizontalPressure, this._verticalPressure) };
|
|
190
|
-
// @ts-ignore
|
|
191
|
-
mesh.material.uniforms.u_wave_frequency_x = { value: this._waveFrequencyX };
|
|
192
|
-
// @ts-ignore
|
|
193
|
-
mesh.material.uniforms.u_wave_frequency_y = { value: this._waveFrequencyY };
|
|
194
|
-
// @ts-ignore
|
|
195
|
-
mesh.material.uniforms.u_wave_amplitude = { value: this._waveAmplitude };
|
|
196
|
-
// @ts-ignore
|
|
197
|
-
mesh.material.uniforms.u_plane_width = { value: PLANE_WIDTH };
|
|
198
|
-
// @ts-ignore
|
|
199
|
-
mesh.material.uniforms.u_plane_height = { value: PLANE_HEIGHT };
|
|
200
|
-
// @ts-ignore
|
|
201
|
-
mesh.material.uniforms.u_color_blending = { value: this._colorBlending };
|
|
202
|
-
// @ts-ignore
|
|
203
|
-
mesh.material.uniforms.u_colors = { value: colors };
|
|
204
|
-
// @ts-ignore
|
|
205
|
-
mesh.material.uniforms.u_colors_count = { value: COLORS_COUNT };
|
|
206
|
-
// @ts-ignore
|
|
207
|
-
mesh.material.uniforms.u_shadows = { value: this._shadows };
|
|
208
|
-
// @ts-ignore
|
|
209
|
-
mesh.material.uniforms.u_highlights = { value: this._highlights };
|
|
210
|
-
// @ts-ignore
|
|
211
|
-
mesh.material.uniforms.u_saturation = { value: this._saturation };
|
|
212
|
-
// @ts-ignore
|
|
213
|
-
mesh.material.uniforms.u_brightness = { value: this._brightness };
|
|
214
|
-
// @ts-ignore
|
|
215
|
-
mesh.material.uniforms.u_grain_intensity = { value: this._grainIntensity };
|
|
216
|
-
// @ts-ignore
|
|
217
|
-
mesh.material.uniforms.u_grain_sparsity = { value: this._grainSparsity };
|
|
218
|
-
// @ts-ignore
|
|
219
|
-
mesh.material.uniforms.u_grain_speed = { value: this._grainSpeed };
|
|
220
|
-
// @ts-ignore
|
|
221
|
-
mesh.material.uniforms.u_grain_scale = { value: this._grainScale };
|
|
222
|
-
// @ts-ignore
|
|
223
|
-
mesh.material.uniforms.u_y_offset = { value: this._yOffset };
|
|
224
|
-
// @ts-ignore
|
|
225
|
-
mesh.material.wireframe = this._wireframe;
|
|
226
|
-
});
|
|
227
333
|
|
|
334
|
+
u.u_time.value = tick;
|
|
335
|
+
u.u_resolution.value.set(this._ref.width, this._ref.height);
|
|
336
|
+
u.u_color_pressure.value.set(this._horizontalPressure, this._verticalPressure);
|
|
337
|
+
|
|
338
|
+
// Directly assign simple values
|
|
339
|
+
u.u_wave_frequency_x.value = this._waveFrequencyX;
|
|
340
|
+
u.u_wave_frequency_y.value = this._waveFrequencyY;
|
|
341
|
+
u.u_wave_amplitude.value = this._waveAmplitude;
|
|
342
|
+
u.u_color_blending.value = this._colorBlending;
|
|
343
|
+
u.u_shadows.value = this._shadows;
|
|
344
|
+
u.u_highlights.value = this._highlights;
|
|
345
|
+
u.u_saturation.value = this._saturation;
|
|
346
|
+
u.u_brightness.value = this._brightness;
|
|
347
|
+
u.u_grain_intensity.value = this._grainIntensity;
|
|
348
|
+
u.u_grain_sparsity.value = this._grainSparsity;
|
|
349
|
+
u.u_grain_speed.value = this._grainSpeed;
|
|
350
|
+
u.u_grain_scale.value = this._grainScale;
|
|
351
|
+
u.u_y_offset.value = this._yOffset;
|
|
352
|
+
u.u_y_offset_wave_multiplier.value = this._yOffsetWaveMultiplier;
|
|
353
|
+
u.u_y_offset_color_multiplier.value = this._yOffsetColorMultiplier;
|
|
354
|
+
u.u_y_offset_flow_multiplier.value = this._yOffsetFlowMultiplier;
|
|
355
|
+
u.u_flow_distortion_a.value = this._flowDistortionA;
|
|
356
|
+
u.u_flow_distortion_b.value = this._flowDistortionB;
|
|
357
|
+
u.u_flow_scale.value = this._flowScale;
|
|
358
|
+
u.u_flow_ease.value = this._flowEase;
|
|
359
|
+
u.u_flow_enabled.value = this._flowEnabled ? 1.0 : 0.0;
|
|
360
|
+
u.u_mouse_distortion_strength.value = this._mouseDistortionStrength;
|
|
361
|
+
u.u_mouse_distortion_radius.value = this._mouseDistortionRadius;
|
|
362
|
+
u.u_mouse_darken.value = this._mouseDarken;
|
|
363
|
+
u.u_enable_procedural_texture.value = this._enableProceduralTexture ? 1.0 : 0.0;
|
|
364
|
+
|
|
365
|
+
// Only regenerate procedural texture when needed
|
|
366
|
+
if (this._textureNeedsUpdate && this._enableProceduralTexture) {
|
|
367
|
+
if (this._proceduralTexture) {
|
|
368
|
+
this._proceduralTexture.dispose();
|
|
369
|
+
}
|
|
370
|
+
this._proceduralTexture = this._createProceduralTexture();
|
|
371
|
+
this._textureNeedsUpdate = false;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
u.u_procedural_texture.value = this._proceduralTexture;
|
|
375
|
+
u.u_texture_ease.value = this._textureEase;
|
|
376
|
+
|
|
377
|
+
// Wireframe is a material property and must update every frame to avoid artifacts
|
|
378
|
+
// @ts-ignore - access material safely
|
|
379
|
+
this.sceneState.meshes[0].material.wireframe = this._wireframe;
|
|
380
|
+
|
|
381
|
+
// Optimized Color Update: Update immediately on change, or throttle to 10 times per second
|
|
382
|
+
const now = Date.now();
|
|
383
|
+
const shouldUpdate = this._colorsChanged || (now - this._lastColorUpdate > 100);
|
|
384
|
+
|
|
385
|
+
if (shouldUpdate) {
|
|
386
|
+
this._lastColorUpdate = now;
|
|
387
|
+
this._colorsChanged = false;
|
|
388
|
+
|
|
389
|
+
const shaderColors = u.u_colors.value;
|
|
390
|
+
for(let i = 0; i < COLORS_COUNT; i++) {
|
|
391
|
+
if (i < this._colors.length) {
|
|
392
|
+
const c = this._colors[i];
|
|
393
|
+
shaderColors[i].is_active = c.enabled ? 1.0 : 0.0;
|
|
394
|
+
shaderColors[i].color.setStyle(c.color, "");
|
|
395
|
+
shaderColors[i].influence = c.influence || 0;
|
|
396
|
+
} else {
|
|
397
|
+
shaderColors[i].is_active = 0.0;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
u.u_colors_count.value = COLORS_COUNT;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
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
|
+
|
|
440
|
+
// Restore main background color for the actual scene render
|
|
441
|
+
renderer.setClearColor(this._tempClearColor, oldClearAlpha);
|
|
442
|
+
|
|
443
|
+
// Update mouse texture uniform
|
|
444
|
+
if (this._cachedUniforms) {
|
|
445
|
+
this._cachedUniforms.u_mouse_texture.value = this._mouseFBO.texture;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Ensure we set the clear color for the main scene explicitly before rendering
|
|
451
|
+
renderer.setClearColor(this._backgroundColor, this._backgroundAlpha);
|
|
228
452
|
renderer.render(scene, camera);
|
|
229
453
|
this.requestRef = requestAnimationFrame(render);
|
|
230
454
|
};
|
|
@@ -238,10 +462,30 @@ export class NeatGradient implements NeatController {
|
|
|
238
462
|
|
|
239
463
|
this.sceneState.renderer.setSize(width, height, false);
|
|
240
464
|
updateCamera(this.sceneState.camera, width, height);
|
|
465
|
+
|
|
466
|
+
// FIX 3: Update Mouse FBO and Camera on resize
|
|
467
|
+
// If we don't do this, mouse coordinates map incorrectly after a resize
|
|
468
|
+
if (this._mouseFBO && this._cameraMouse) {
|
|
469
|
+
const fSize = height / 2;
|
|
470
|
+
const aspect = width / height;
|
|
471
|
+
this._mouseFBO.setSize(width / 2, height / 2);
|
|
472
|
+
this._cameraMouse.left = -fSize * aspect;
|
|
473
|
+
this._cameraMouse.right = fSize * aspect;
|
|
474
|
+
this._cameraMouse.top = fSize;
|
|
475
|
+
this._cameraMouse.bottom = -fSize;
|
|
476
|
+
this._cameraMouse.updateProjectionMatrix();
|
|
477
|
+
}
|
|
241
478
|
};
|
|
242
479
|
|
|
243
|
-
|
|
244
|
-
|
|
480
|
+
// Debounce resize to prevent excessive operations
|
|
481
|
+
this.sizeObserver = new ResizeObserver(() => {
|
|
482
|
+
if (this._resizeTimeoutId !== null) {
|
|
483
|
+
clearTimeout(this._resizeTimeoutId);
|
|
484
|
+
}
|
|
485
|
+
this._resizeTimeoutId = window.setTimeout(() => {
|
|
486
|
+
setSize();
|
|
487
|
+
this._resizeTimeoutId = null;
|
|
488
|
+
}, 100); // Wait 100ms after last resize event
|
|
245
489
|
});
|
|
246
490
|
|
|
247
491
|
this.sizeObserver.observe(ref);
|
|
@@ -254,6 +498,24 @@ export class NeatGradient implements NeatController {
|
|
|
254
498
|
if (this) {
|
|
255
499
|
cancelAnimationFrame(this.requestRef);
|
|
256
500
|
this.sizeObserver.disconnect();
|
|
501
|
+
|
|
502
|
+
// Clear resize timeout
|
|
503
|
+
if (this._resizeTimeoutId !== null) {
|
|
504
|
+
clearTimeout(this._resizeTimeoutId);
|
|
505
|
+
this._resizeTimeoutId = null;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Cleanup WebGL resources
|
|
509
|
+
if (this.sceneState) {
|
|
510
|
+
this.sceneState.renderer.dispose();
|
|
511
|
+
this.sceneState.meshes.forEach(m => {
|
|
512
|
+
m.geometry.dispose();
|
|
513
|
+
if(Array.isArray(m.material)) m.material.forEach(mat => mat.dispose());
|
|
514
|
+
else m.material.dispose();
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
if (this._mouseFBO) this._mouseFBO.dispose();
|
|
518
|
+
if (this._proceduralTexture) this._proceduralTexture.dispose();
|
|
257
519
|
}
|
|
258
520
|
}
|
|
259
521
|
|
|
@@ -290,6 +552,7 @@ export class NeatGradient implements NeatController {
|
|
|
290
552
|
|
|
291
553
|
set colors(colors: NeatColor[]) {
|
|
292
554
|
this._colors = colors;
|
|
555
|
+
this._colorsChanged = true; // Flag for immediate update
|
|
293
556
|
}
|
|
294
557
|
|
|
295
558
|
set highlights(highlights: number) {
|
|
@@ -348,11 +611,178 @@ export class NeatGradient implements NeatController {
|
|
|
348
611
|
this._yOffset = yOffset;
|
|
349
612
|
}
|
|
350
613
|
|
|
614
|
+
get yOffsetWaveMultiplier(): number {
|
|
615
|
+
return this._yOffsetWaveMultiplier * 1000;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
set yOffsetWaveMultiplier(value: number) {
|
|
619
|
+
this._yOffsetWaveMultiplier = value / 1000;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
get yOffsetColorMultiplier(): number {
|
|
623
|
+
return this._yOffsetColorMultiplier * 1000;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
set yOffsetColorMultiplier(value: number) {
|
|
627
|
+
this._yOffsetColorMultiplier = value / 1000;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
get yOffsetFlowMultiplier(): number {
|
|
631
|
+
return this._yOffsetFlowMultiplier * 1000;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
set yOffsetFlowMultiplier(value: number) {
|
|
635
|
+
this._yOffsetFlowMultiplier = value / 1000;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
set flowDistortionA(value: number) {
|
|
639
|
+
this._flowDistortionA = value;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
set flowDistortionB(value: number) {
|
|
643
|
+
this._flowDistortionB = value;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
set flowScale(value: number) {
|
|
647
|
+
this._flowScale = value;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
set flowEase(value: number) {
|
|
651
|
+
this._flowEase = value;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
set flowEnabled(value: boolean) {
|
|
655
|
+
this._flowEnabled = value;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
get flowEnabled(): boolean {
|
|
659
|
+
return this._flowEnabled;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
|
|
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
|
+
|
|
690
|
+
set enableProceduralTexture(value: boolean) {
|
|
691
|
+
this._enableProceduralTexture = value;
|
|
692
|
+
if (value && !this._proceduralTexture) {
|
|
693
|
+
this._textureNeedsUpdate = true;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
set textureVoidLikelihood(value: number) {
|
|
698
|
+
this._textureVoidLikelihood = value;
|
|
699
|
+
if (this._enableProceduralTexture) {
|
|
700
|
+
this._textureNeedsUpdate = true;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
set textureVoidWidthMin(value: number) {
|
|
705
|
+
this._textureVoidWidthMin = value;
|
|
706
|
+
if (this._enableProceduralTexture) {
|
|
707
|
+
this._textureNeedsUpdate = true;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
set textureVoidWidthMax(value: number) {
|
|
712
|
+
this._textureVoidWidthMax = value;
|
|
713
|
+
if (this._enableProceduralTexture) {
|
|
714
|
+
this._textureNeedsUpdate = true;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
set textureBandDensity(value: number) {
|
|
719
|
+
this._textureBandDensity = value;
|
|
720
|
+
if (this._enableProceduralTexture) {
|
|
721
|
+
this._textureNeedsUpdate = true;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
set textureColorBlending(value: number) {
|
|
726
|
+
this._textureColorBlending = value;
|
|
727
|
+
if (this._enableProceduralTexture) {
|
|
728
|
+
this._textureNeedsUpdate = true;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
set textureSeed(value: number) {
|
|
733
|
+
this._textureSeed = value;
|
|
734
|
+
if (this._enableProceduralTexture) {
|
|
735
|
+
this._textureNeedsUpdate = true;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
get textureEase(): number {
|
|
740
|
+
return this._textureEase;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
set textureEase(value: number) {
|
|
744
|
+
this._textureEase = value;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
set proceduralBackgroundColor(value: string) {
|
|
748
|
+
this._proceduralBackgroundColor = value;
|
|
749
|
+
if (this._enableProceduralTexture) {
|
|
750
|
+
this._textureNeedsUpdate = true;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
set textureShapeTriangles(value: number) {
|
|
755
|
+
this._textureShapeTriangles = value;
|
|
756
|
+
if (this._enableProceduralTexture) this._textureNeedsUpdate = true;
|
|
757
|
+
}
|
|
758
|
+
set textureShapeCircles(value: number) {
|
|
759
|
+
this._textureShapeCircles = value;
|
|
760
|
+
if (this._enableProceduralTexture) this._textureNeedsUpdate = true;
|
|
761
|
+
}
|
|
762
|
+
set textureShapeBars(value: number) {
|
|
763
|
+
this._textureShapeBars = value;
|
|
764
|
+
if (this._enableProceduralTexture) this._textureNeedsUpdate = true;
|
|
765
|
+
}
|
|
766
|
+
set textureShapeSquiggles(value: number) {
|
|
767
|
+
this._textureShapeSquiggles = value;
|
|
768
|
+
if (this._enableProceduralTexture) this._textureNeedsUpdate = true;
|
|
769
|
+
}
|
|
770
|
+
|
|
351
771
|
_initScene(resolution: number): SceneState {
|
|
352
772
|
|
|
353
773
|
const width = this._ref.width,
|
|
354
774
|
height = this._ref.height;
|
|
355
775
|
|
|
776
|
+
// Cleanup existing renderer if needed
|
|
777
|
+
if (this.sceneState && this.sceneState.renderer) {
|
|
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
|
+
}
|
|
785
|
+
|
|
356
786
|
const renderer = new THREE.WebGLRenderer({
|
|
357
787
|
// antialias: true,
|
|
358
788
|
alpha: true,
|
|
@@ -392,17 +822,13 @@ export class NeatGradient implements NeatController {
|
|
|
392
822
|
|
|
393
823
|
_buildMaterial(width: number, height: number) {
|
|
394
824
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
is_active: false,
|
|
403
|
-
color: new THREE.Color(0x000000)
|
|
404
|
-
}))
|
|
405
|
-
];
|
|
825
|
+
// Initialize stable array structure for colors
|
|
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
|
+
}));
|
|
406
832
|
|
|
407
833
|
const uniforms = {
|
|
408
834
|
u_time: { value: 0 },
|
|
@@ -421,6 +847,29 @@ export class NeatGradient implements NeatController {
|
|
|
421
847
|
u_grain_sparsity: { value: this._grainSparsity },
|
|
422
848
|
u_grain_scale: { value: this._grainScale },
|
|
423
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 }
|
|
424
873
|
};
|
|
425
874
|
|
|
426
875
|
const material = new THREE.ShaderMaterial({
|
|
@@ -429,10 +878,306 @@ export class NeatGradient implements NeatController {
|
|
|
429
878
|
fragmentShader: buildUniforms() + buildColorFunctions() + buildNoise() + buildFragmentShader()
|
|
430
879
|
});
|
|
431
880
|
|
|
881
|
+
// Cache the uniforms object for direct access in render loop
|
|
882
|
+
this._cachedUniforms = uniforms as unknown as NeatUniforms;
|
|
883
|
+
|
|
432
884
|
material.wireframe = WIREFRAME;
|
|
433
885
|
return material;
|
|
434
886
|
}
|
|
435
887
|
|
|
888
|
+
_setupMouseInteraction() {
|
|
889
|
+
if (!this._ref) return;
|
|
890
|
+
|
|
891
|
+
const width = this._ref.width;
|
|
892
|
+
const height = this._ref.height;
|
|
893
|
+
|
|
894
|
+
// Create mouse FBO
|
|
895
|
+
this._mouseFBO = new THREE.WebGLRenderTarget(width / 2, height / 2);
|
|
896
|
+
|
|
897
|
+
// Create mouse scene and camera
|
|
898
|
+
this._sceneMouse = new THREE.Scene();
|
|
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);
|
|
924
|
+
}
|
|
925
|
+
const brushTex = new THREE.CanvasTexture(brushCanvas);
|
|
926
|
+
const brushMat = new THREE.MeshBasicMaterial({
|
|
927
|
+
map: brushTex,
|
|
928
|
+
transparent: true,
|
|
929
|
+
opacity: 1.0,
|
|
930
|
+
depthTest: false,
|
|
931
|
+
blending: THREE.AdditiveBlending // Additive blending for better accumulation
|
|
932
|
+
});
|
|
933
|
+
// Brush geometry size - will be scaled by radius parameter
|
|
934
|
+
const brushGeo = new THREE.PlaneGeometry(200, 200);
|
|
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 });
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// Initialize brush scale based on current radius
|
|
946
|
+
this._updateBrushScale();
|
|
947
|
+
|
|
948
|
+
// Add mouse move listener
|
|
949
|
+
this._ref.addEventListener('mousemove', this._onMouseMove.bind(this));
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
_onMouseMove(e: MouseEvent) {
|
|
953
|
+
if (!this._ref || !this._sceneMouse) return;
|
|
954
|
+
|
|
955
|
+
const rect = this._ref.getBoundingClientRect();
|
|
956
|
+
const width = this._ref.width;
|
|
957
|
+
const height = this._ref.height;
|
|
958
|
+
|
|
959
|
+
// Store pending mouse position
|
|
960
|
+
this._pendingMousePosition = {
|
|
961
|
+
x: e.clientX - rect.left - width / 2,
|
|
962
|
+
y: -(e.clientY - rect.top - height / 2)
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
// Batch mouse updates using requestAnimationFrame
|
|
966
|
+
if (!this._mouseUpdateScheduled) {
|
|
967
|
+
this._mouseUpdateScheduled = true;
|
|
968
|
+
requestAnimationFrame(() => {
|
|
969
|
+
this._mouseUpdateScheduled = false;
|
|
970
|
+
|
|
971
|
+
if (!this._pendingMousePosition) return;
|
|
972
|
+
|
|
973
|
+
this._mouse.x = this._pendingMousePosition.x;
|
|
974
|
+
this._mouse.y = this._pendingMousePosition.y;
|
|
975
|
+
|
|
976
|
+
const brush = this._mouseObjects[this._currentBrush];
|
|
977
|
+
brush.mesh.scale.set(this._mouseBrushBaseScale, this._mouseBrushBaseScale, 1.0);
|
|
978
|
+
brush.active = true;
|
|
979
|
+
brush.mesh.visible = true;
|
|
980
|
+
brush.mesh.position.set(this._mouse.x, this._mouse.y, 0);
|
|
981
|
+
brush.mesh.rotation.z = Math.random() * Math.PI * 2;
|
|
982
|
+
if (brush.mesh.material instanceof THREE.MeshBasicMaterial) {
|
|
983
|
+
brush.mesh.material.opacity = 1.0;
|
|
984
|
+
}
|
|
985
|
+
this._currentBrush = (this._currentBrush + 1) % this._mouseObjects.length;
|
|
986
|
+
|
|
987
|
+
this._pendingMousePosition = null;
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
_createProceduralTexture(): THREE.Texture {
|
|
993
|
+
// Texture size - 1024 provides good balance between quality and performance
|
|
994
|
+
// Reduced from 2048 for better performance
|
|
995
|
+
const texSize = 1024;
|
|
996
|
+
const sourceCanvas = document.createElement('canvas');
|
|
997
|
+
sourceCanvas.width = texSize;
|
|
998
|
+
sourceCanvas.height = texSize;
|
|
999
|
+
const sCtx = sourceCanvas.getContext('2d', { willReadFrequently: true });
|
|
1000
|
+
if (!sCtx) return new THREE.Texture();
|
|
1001
|
+
|
|
1002
|
+
let seed = this._textureSeed;
|
|
1003
|
+
const baseSeed = this._textureSeed;
|
|
1004
|
+
|
|
1005
|
+
function random() {
|
|
1006
|
+
const x = Math.sin(seed++) * 10000;
|
|
1007
|
+
return x - Math.floor(x);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// Helper to reset seed for isolated shape generation
|
|
1011
|
+
const setSeed = (offset: number) => {
|
|
1012
|
+
seed = baseSeed + offset;
|
|
1013
|
+
};
|
|
1014
|
+
|
|
1015
|
+
const colors = this._colors.filter(c => c.enabled).map(c => c.color);
|
|
1016
|
+
if (colors.length === 0) return new THREE.Texture();
|
|
1017
|
+
|
|
1018
|
+
// Helper functions
|
|
1019
|
+
function hexToRgb(hex: string) {
|
|
1020
|
+
const bigint = parseInt(hex.replace('#', ''), 16);
|
|
1021
|
+
return {
|
|
1022
|
+
r: (bigint >> 16) & 255,
|
|
1023
|
+
g: (bigint >> 8) & 255,
|
|
1024
|
+
b: bigint & 255
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
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);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
const getInterColor = () => {
|
|
1033
|
+
const c1 = colors[Math.floor(random() * colors.length)];
|
|
1034
|
+
const c2 = colors[Math.floor(random() * colors.length)];
|
|
1035
|
+
const mix = random() * this._textureColorBlending;
|
|
1036
|
+
const rgb1 = hexToRgb(c1);
|
|
1037
|
+
const rgb2 = hexToRgb(c2);
|
|
1038
|
+
const r = rgb1.r + (rgb2.r - rgb1.r) * mix;
|
|
1039
|
+
const g = rgb1.g + (rgb2.g - rgb1.g) * mix;
|
|
1040
|
+
const b = rgb1.b + (rgb2.b - rgb1.b) * mix;
|
|
1041
|
+
return rgbToHex(r, g, b);
|
|
1042
|
+
};
|
|
1043
|
+
|
|
1044
|
+
// === SOURCE CANVAS ===
|
|
1045
|
+
// Base with procedural background color so even sparse areas pick it up
|
|
1046
|
+
const baseColor = this._proceduralBackgroundColor || "#000000";
|
|
1047
|
+
sCtx.fillStyle = baseColor;
|
|
1048
|
+
sCtx.fillRect(0, 0, texSize, texSize);
|
|
1049
|
+
|
|
1050
|
+
// Then lay a vertical gradient of mixed colors on top for richness
|
|
1051
|
+
const bgGrad = sCtx.createLinearGradient(0, 0, 0, texSize);
|
|
1052
|
+
bgGrad.addColorStop(0, getInterColor());
|
|
1053
|
+
bgGrad.addColorStop(1, getInterColor());
|
|
1054
|
+
sCtx.fillStyle = bgGrad;
|
|
1055
|
+
sCtx.fillRect(0, 0, texSize, texSize);
|
|
1056
|
+
|
|
1057
|
+
// Triangles: use configurable count
|
|
1058
|
+
for (let i = 0; i < this._textureShapeTriangles; i++) {
|
|
1059
|
+
sCtx.fillStyle = getInterColor();
|
|
1060
|
+
sCtx.beginPath();
|
|
1061
|
+
const x = random() * texSize;
|
|
1062
|
+
const y = random() * texSize;
|
|
1063
|
+
const s = 100 + random() * 300;
|
|
1064
|
+
sCtx.moveTo(x, y);
|
|
1065
|
+
sCtx.lineTo(x + (random() - 0.5) * s, y + (random() - 0.5) * s);
|
|
1066
|
+
sCtx.lineTo(x + (random() - 0.5) * s, y + (random() - 0.5) * s);
|
|
1067
|
+
sCtx.fill();
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// Circles / rings: use configurable count
|
|
1071
|
+
for (let i = 0; i < this._textureShapeCircles; i++) {
|
|
1072
|
+
sCtx.strokeStyle = getInterColor();
|
|
1073
|
+
sCtx.lineWidth = 10 + random() * 50;
|
|
1074
|
+
sCtx.beginPath();
|
|
1075
|
+
const x = random() * texSize;
|
|
1076
|
+
const y = random() * texSize;
|
|
1077
|
+
const r = 50 + random() * 150;
|
|
1078
|
+
sCtx.arc(x, y, r, 0, Math.PI * 2);
|
|
1079
|
+
sCtx.stroke();
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// Bars: use configurable count
|
|
1083
|
+
for (let i = 0; i < this._textureShapeBars; i++) {
|
|
1084
|
+
sCtx.fillStyle = getInterColor();
|
|
1085
|
+
sCtx.save();
|
|
1086
|
+
sCtx.translate(random() * texSize, random() * texSize);
|
|
1087
|
+
sCtx.rotate(random() * Math.PI);
|
|
1088
|
+
sCtx.fillRect(-150, -25, 300, 50);
|
|
1089
|
+
sCtx.restore();
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// Squiggles: use configurable count
|
|
1093
|
+
sCtx.lineWidth = 15;
|
|
1094
|
+
sCtx.lineCap = 'round';
|
|
1095
|
+
for (let i = 0; i < this._textureShapeSquiggles; i++) {
|
|
1096
|
+
sCtx.strokeStyle = getInterColor();
|
|
1097
|
+
sCtx.beginPath();
|
|
1098
|
+
let x = random() * texSize;
|
|
1099
|
+
let y = random() * texSize;
|
|
1100
|
+
sCtx.moveTo(x, y);
|
|
1101
|
+
for (let j = 0; j < 4; j++) {
|
|
1102
|
+
sCtx.bezierCurveTo(
|
|
1103
|
+
x + (random() - 0.5) * 300, y + (random() - 0.5) * 300,
|
|
1104
|
+
x + (random() - 0.5) * 300, y + (random() - 0.5) * 300,
|
|
1105
|
+
x + (random() - 0.5) * 300, y + (random() - 0.5) * 300
|
|
1106
|
+
);
|
|
1107
|
+
x += (random() - 0.5) * 300;
|
|
1108
|
+
y += (random() - 0.5) * 300;
|
|
1109
|
+
}
|
|
1110
|
+
sCtx.stroke();
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// === MASKED CANVAS ===
|
|
1114
|
+
// Masking: Seed isolation
|
|
1115
|
+
setSeed(50000);
|
|
1116
|
+
const canvas = document.createElement('canvas');
|
|
1117
|
+
canvas.width = texSize;
|
|
1118
|
+
canvas.height = texSize;
|
|
1119
|
+
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
|
1120
|
+
if (!ctx) return new THREE.Texture();
|
|
1121
|
+
|
|
1122
|
+
// Start filled with the chosen void color so gaps show that color
|
|
1123
|
+
ctx.fillStyle = baseColor;
|
|
1124
|
+
ctx.fillRect(0, 0, texSize, texSize);
|
|
1125
|
+
|
|
1126
|
+
// Determine layout segments (matter vs void)
|
|
1127
|
+
let layoutHead = 0;
|
|
1128
|
+
const segments: Array<{ type: 'void' | 'matter', x: number, width: number }> = [];
|
|
1129
|
+
|
|
1130
|
+
while (layoutHead < texSize) {
|
|
1131
|
+
const isVoid = random() < this._textureVoidLikelihood;
|
|
1132
|
+
if (isVoid) {
|
|
1133
|
+
const w = this._textureVoidWidthMin + random() * (this._textureVoidWidthMax - this._textureVoidWidthMin);
|
|
1134
|
+
segments.push({ type: 'void', x: layoutHead, width: w });
|
|
1135
|
+
layoutHead += w;
|
|
1136
|
+
} else {
|
|
1137
|
+
const w = 50 + random() * 200;
|
|
1138
|
+
segments.push({ type: 'matter', x: layoutHead, width: w });
|
|
1139
|
+
layoutHead += w;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// Render only matter bands from the source into the masked canvas
|
|
1144
|
+
for (const seg of segments) {
|
|
1145
|
+
if (seg.type === 'matter') {
|
|
1146
|
+
const startX = seg.x;
|
|
1147
|
+
const endX = Math.min(seg.x + seg.width, texSize);
|
|
1148
|
+
let currentX = startX;
|
|
1149
|
+
|
|
1150
|
+
while (currentX < endX) {
|
|
1151
|
+
const stripeWidth = (2 + random() * 20) / this._textureBandDensity;
|
|
1152
|
+
const sourceX = Math.floor(random() * texSize);
|
|
1153
|
+
ctx.drawImage(
|
|
1154
|
+
sourceCanvas,
|
|
1155
|
+
sourceX, 0, stripeWidth, texSize,
|
|
1156
|
+
currentX, 0, stripeWidth, texSize
|
|
1157
|
+
);
|
|
1158
|
+
currentX += stripeWidth;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
// void segments: leave as baseColor
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
const tex = new THREE.CanvasTexture(canvas);
|
|
1165
|
+
// Use mipmapping for better quality when texture is scaled
|
|
1166
|
+
tex.minFilter = THREE.LinearMipmapLinearFilter;
|
|
1167
|
+
tex.magFilter = THREE.LinearFilter;
|
|
1168
|
+
tex.wrapS = THREE.RepeatWrapping;
|
|
1169
|
+
tex.wrapT = THREE.RepeatWrapping;
|
|
1170
|
+
|
|
1171
|
+
// Enable anisotropic filtering for much better quality when texture is stretched
|
|
1172
|
+
// 16 is a commonly supported value that dramatically improves quality
|
|
1173
|
+
tex.anisotropy = 16;
|
|
1174
|
+
|
|
1175
|
+
// Ensure mipmaps are generated
|
|
1176
|
+
tex.needsUpdate = true;
|
|
1177
|
+
|
|
1178
|
+
return tex;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
436
1181
|
|
|
437
1182
|
}
|
|
438
1183
|
|
|
@@ -449,11 +1194,26 @@ function updateCamera(camera: THREE.Camera, width: number, height: number) {
|
|
|
449
1194
|
const targetWidth = Math.sqrt(targetPlaneArea * ratio);
|
|
450
1195
|
const targetHeight = targetPlaneArea / targetWidth;
|
|
451
1196
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
+
}
|
|
457
1217
|
|
|
458
1218
|
const near = -100;
|
|
459
1219
|
const far = 1000;
|
|
@@ -473,43 +1233,69 @@ function updateCamera(camera: THREE.Camera, width: number, height: number) {
|
|
|
473
1233
|
}
|
|
474
1234
|
|
|
475
1235
|
|
|
476
|
-
|
|
477
|
-
|
|
1236
|
+
// Cache shader strings to avoid repeated concatenation
|
|
1237
|
+
let cachedVertexShader: string | null = null;
|
|
1238
|
+
let cachedFragmentShader: string | null = null;
|
|
478
1239
|
|
|
1240
|
+
function buildVertexShader() {
|
|
1241
|
+
if (cachedVertexShader) return cachedVertexShader;
|
|
1242
|
+
cachedVertexShader = `
|
|
479
1243
|
void main() {
|
|
480
|
-
|
|
481
1244
|
vUv = uv;
|
|
482
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
|
|
483
1254
|
v_displacement_amount = cnoise( vec3(
|
|
484
1255
|
u_wave_frequency_x * position.x + u_time,
|
|
485
|
-
u_wave_frequency_y * position.y + u_time,
|
|
1256
|
+
u_wave_frequency_y * (position.y + waveOffset) + u_time,
|
|
486
1257
|
u_time
|
|
487
1258
|
));
|
|
488
1259
|
|
|
489
|
-
|
|
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
|
+
}
|
|
490
1278
|
|
|
491
|
-
//
|
|
492
|
-
|
|
1279
|
+
// Pass the standard flow UV to fragment shader (for mouse/texture)
|
|
1280
|
+
vFlowUv = flowUv;
|
|
493
1281
|
|
|
494
|
-
//
|
|
495
|
-
|
|
496
|
-
//
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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
|
|
500
1288
|
|
|
1289
|
+
vec2 noise_cord = adjustedUv * u_color_pressure;
|
|
501
1290
|
const float minNoise = .0;
|
|
502
1291
|
const float maxNoise = .9;
|
|
503
1292
|
|
|
504
1293
|
for (int i = 1; i < u_colors_count; i++) {
|
|
505
|
-
|
|
506
|
-
if(u_colors[i].is_active == 1.0){
|
|
1294
|
+
if(u_colors[i].is_active > 0.5){
|
|
507
1295
|
float noiseFlow = (1. + float(i)) / 30.;
|
|
508
1296
|
float noiseSpeed = (1. + float(i)) * 0.11;
|
|
509
1297
|
float noiseSeed = 13. + float(i) * 7.;
|
|
510
1298
|
|
|
511
|
-
int reverseIndex = u_colors_count - i;
|
|
512
|
-
|
|
513
1299
|
float noise = snoise(
|
|
514
1300
|
vec3(
|
|
515
1301
|
noise_cord.x * u_color_pressure.x + u_time * noiseFlow * 2.,
|
|
@@ -519,23 +1305,24 @@ void main() {
|
|
|
519
1305
|
) - (.1 * float(i)) + (.5 * u_color_blending);
|
|
520
1306
|
|
|
521
1307
|
noise = clamp(minNoise, maxNoise + float(i) * 0.02, noise);
|
|
522
|
-
|
|
523
|
-
color = mix(color, nextColor, smoothstep(0.0, u_color_blending, noise));
|
|
1308
|
+
color = mix(color, u_colors[i].color, smoothstep(0.0, u_color_blending, noise));
|
|
524
1309
|
}
|
|
525
1310
|
}
|
|
526
1311
|
|
|
527
1312
|
v_color = color;
|
|
528
1313
|
|
|
1314
|
+
// 4. VERTEX POSITION
|
|
529
1315
|
vec3 newPosition = position + normal * v_displacement_amount * u_wave_amplitude;
|
|
530
1316
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
|
|
531
|
-
|
|
532
1317
|
v_new_position = gl_Position;
|
|
533
1318
|
}
|
|
534
1319
|
`;
|
|
1320
|
+
return cachedVertexShader;
|
|
535
1321
|
}
|
|
536
1322
|
|
|
537
1323
|
function buildFragmentShader() {
|
|
538
|
-
return
|
|
1324
|
+
if (cachedFragmentShader) return cachedFragmentShader;
|
|
1325
|
+
cachedFragmentShader = `
|
|
539
1326
|
float random(vec2 p) {
|
|
540
1327
|
return fract(sin(dot(p, vec2(12.9898,78.233))) * 43758.5453);
|
|
541
1328
|
}
|
|
@@ -553,35 +1340,80 @@ float fbm(vec3 x) {
|
|
|
553
1340
|
}
|
|
554
1341
|
|
|
555
1342
|
void main() {
|
|
556
|
-
|
|
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;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
vec3 color = baseColor;
|
|
1387
|
+
|
|
1388
|
+
// Post-processing
|
|
557
1389
|
color += pow(v_displacement_amount, 1.0) * u_highlights;
|
|
558
1390
|
color -= pow(1.0 - v_displacement_amount, 2.0) * u_shadows;
|
|
559
1391
|
color = saturation(color, 1.0 + u_saturation);
|
|
560
1392
|
color = color * u_brightness;
|
|
561
1393
|
|
|
562
|
-
//
|
|
1394
|
+
// Grain
|
|
563
1395
|
vec2 noiseCoords = gl_FragCoord.xy / u_grain_scale;
|
|
564
1396
|
float grain = (u_grain_speed != 0.0) ? fbm(vec3(noiseCoords, u_time * u_grain_speed)) : fbm(vec3(noiseCoords, 0.0));
|
|
565
1397
|
|
|
566
|
-
// Center the grain around zero
|
|
567
1398
|
grain = grain * 0.5 + 0.5;
|
|
568
1399
|
grain -= 0.5;
|
|
569
|
-
|
|
570
|
-
// Add sparsity control
|
|
571
1400
|
grain = (grain > u_grain_sparsity) ? grain : 0.0;
|
|
572
|
-
|
|
573
|
-
// Apply grain intensity
|
|
574
1401
|
grain *= u_grain_intensity;
|
|
575
1402
|
|
|
576
|
-
// Add grain to color
|
|
577
1403
|
color += vec3(grain);
|
|
578
1404
|
|
|
579
1405
|
gl_FragColor = vec4(color, 1.0);
|
|
580
1406
|
}
|
|
581
1407
|
`;
|
|
1408
|
+
return cachedFragmentShader;
|
|
582
1409
|
}
|
|
583
1410
|
|
|
584
|
-
|
|
1411
|
+
// Cache uniforms string as well
|
|
1412
|
+
let cachedUniformsShader: string | null = null;
|
|
1413
|
+
|
|
1414
|
+
const buildUniforms = () => {
|
|
1415
|
+
if (cachedUniformsShader) return cachedUniformsShader;
|
|
1416
|
+
cachedUniformsShader = `
|
|
585
1417
|
precision highp float;
|
|
586
1418
|
|
|
587
1419
|
struct Color {
|
|
@@ -613,83 +1445,98 @@ uniform float u_brightness;
|
|
|
613
1445
|
uniform float u_color_blending;
|
|
614
1446
|
|
|
615
1447
|
uniform int u_colors_count;
|
|
616
|
-
uniform Color u_colors[
|
|
1448
|
+
uniform Color u_colors[6];
|
|
617
1449
|
uniform vec2 u_resolution;
|
|
618
1450
|
|
|
619
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;
|
|
620
1473
|
|
|
621
1474
|
varying vec2 vUv;
|
|
1475
|
+
varying vec2 vFlowUv;
|
|
622
1476
|
varying vec4 v_new_position;
|
|
623
1477
|
varying vec3 v_color;
|
|
624
1478
|
varying float v_displacement_amount;
|
|
625
1479
|
|
|
626
1480
|
`;
|
|
1481
|
+
return cachedUniformsShader;
|
|
1482
|
+
};
|
|
627
1483
|
|
|
628
|
-
|
|
1484
|
+
// Cache noise functions as well
|
|
1485
|
+
let cachedNoiseShader: string | null = null;
|
|
629
1486
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
}
|
|
1487
|
+
const buildNoise = () => {
|
|
1488
|
+
if (cachedNoiseShader) return cachedNoiseShader;
|
|
1489
|
+
cachedNoiseShader = `
|
|
634
1490
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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);
|
|
638
1495
|
}
|
|
639
1496
|
|
|
640
|
-
|
|
641
|
-
{
|
|
642
|
-
return mod289(((x*34.0)+1.0)*x);
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
vec4 taylorInvSqrt(vec4 r)
|
|
646
|
-
{
|
|
1497
|
+
// Taylor Inverse Sqrt
|
|
1498
|
+
vec4 taylorInvSqrt(vec4 r) {
|
|
647
1499
|
return 1.79284291400159 - 0.85373472095314 * r;
|
|
648
1500
|
}
|
|
649
1501
|
|
|
1502
|
+
// Fade function
|
|
650
1503
|
vec3 fade(vec3 t) {
|
|
651
1504
|
return t*t*t*(t*(t*6.0-15.0)+10.0);
|
|
652
1505
|
}
|
|
653
1506
|
|
|
654
|
-
|
|
655
|
-
{
|
|
1507
|
+
// 3D Simplex Noise
|
|
1508
|
+
float snoise(vec3 v) {
|
|
656
1509
|
const vec2 C = vec2(1.0/6.0, 1.0/3.0) ;
|
|
657
1510
|
const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
|
|
658
1511
|
|
|
659
|
-
// First corner
|
|
1512
|
+
// First corner
|
|
660
1513
|
vec3 i = floor(v + dot(v, C.yyy) );
|
|
661
1514
|
vec3 x0 = v - i + dot(i, C.xxx) ;
|
|
662
1515
|
|
|
663
|
-
// Other corners
|
|
1516
|
+
// Other corners
|
|
664
1517
|
vec3 g = step(x0.yzx, x0.xyz);
|
|
665
1518
|
vec3 l = 1.0 - g;
|
|
666
1519
|
vec3 i1 = min( g.xyz, l.zxy );
|
|
667
1520
|
vec3 i2 = max( g.xyz, l.zxy );
|
|
668
1521
|
|
|
669
|
-
// x0 = x0 - 0.0 + 0.0 * C.xxx;
|
|
670
|
-
// x1 = x0 - i1 + 1.0 * C.xxx;
|
|
671
|
-
// x2 = x0 - i2 + 2.0 * C.xxx;
|
|
672
|
-
// x3 = x0 - 1.0 + 3.0 * C.xxx;
|
|
673
1522
|
vec3 x1 = x0 - i1 + C.xxx;
|
|
674
|
-
vec3 x2 = x0 - i2 + C.yyy;
|
|
675
|
-
vec3 x3 = x0 - D.yyy;
|
|
1523
|
+
vec3 x2 = x0 - i2 + C.yyy;
|
|
1524
|
+
vec3 x3 = x0 - D.yyy;
|
|
676
1525
|
|
|
677
|
-
// Permutations
|
|
678
|
-
i = mod289(i);
|
|
1526
|
+
// Permutations
|
|
679
1527
|
vec4 p = permute( permute( permute(
|
|
680
1528
|
i.z + vec4(0.0, i1.z, i2.z, 1.0 ))
|
|
681
1529
|
+ i.y + vec4(0.0, i1.y, i2.y, 1.0 ))
|
|
682
1530
|
+ i.x + vec4(0.0, i1.x, i2.x, 1.0 ));
|
|
683
1531
|
|
|
684
|
-
// Gradients
|
|
685
|
-
// The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294)
|
|
1532
|
+
// Gradients
|
|
686
1533
|
float n_ = 0.142857142857; // 1.0/7.0
|
|
687
1534
|
vec3 ns = n_ * D.wyz - D.xzx;
|
|
688
1535
|
|
|
689
|
-
vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
|
|
1536
|
+
vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
|
|
690
1537
|
|
|
691
1538
|
vec4 x_ = floor(j * ns.z);
|
|
692
|
-
vec4 y_ = floor(j - 7.0 * x_ );
|
|
1539
|
+
vec4 y_ = floor(j - 7.0 * x_ );
|
|
693
1540
|
|
|
694
1541
|
vec4 x = x_ *ns.x + ns.yyyy;
|
|
695
1542
|
vec4 y = y_ *ns.x + ns.yyyy;
|
|
@@ -698,8 +1545,6 @@ float snoise(vec3 v)
|
|
|
698
1545
|
vec4 b0 = vec4( x.xy, y.xy );
|
|
699
1546
|
vec4 b1 = vec4( x.zw, y.zw );
|
|
700
1547
|
|
|
701
|
-
//vec4 s0 = vec4(lessThan(b0,0.0))*2.0 - 1.0;
|
|
702
|
-
//vec4 s1 = vec4(lessThan(b1,0.0))*2.0 - 1.0;
|
|
703
1548
|
vec4 s0 = floor(b0)*2.0 + 1.0;
|
|
704
1549
|
vec4 s1 = floor(b1)*2.0 + 1.0;
|
|
705
1550
|
vec4 sh = -step(h, vec4(0.0));
|
|
@@ -712,14 +1557,14 @@ float snoise(vec3 v)
|
|
|
712
1557
|
vec3 p2 = vec3(a1.xy,h.z);
|
|
713
1558
|
vec3 p3 = vec3(a1.zw,h.w);
|
|
714
1559
|
|
|
715
|
-
//Normalise gradients
|
|
1560
|
+
// Normalise gradients
|
|
716
1561
|
vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
|
|
717
1562
|
p0 *= norm.x;
|
|
718
1563
|
p1 *= norm.y;
|
|
719
1564
|
p2 *= norm.z;
|
|
720
1565
|
p3 *= norm.w;
|
|
721
1566
|
|
|
722
|
-
// Mix final noise value
|
|
1567
|
+
// Mix final noise value
|
|
723
1568
|
vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
|
|
724
1569
|
m = m * m;
|
|
725
1570
|
return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1),
|
|
@@ -729,12 +1574,11 @@ float snoise(vec3 v)
|
|
|
729
1574
|
// Classic Perlin noise
|
|
730
1575
|
float cnoise(vec3 P)
|
|
731
1576
|
{
|
|
732
|
-
vec3 Pi0 = floor(P);
|
|
733
|
-
vec3 Pi1 = Pi0 + vec3(1.0);
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
vec3
|
|
737
|
-
vec3 Pf1 = Pf0 - vec3(1.0); // Fractional part - 1.0
|
|
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);
|
|
738
1582
|
vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x);
|
|
739
1583
|
vec4 iy = vec4(Pi0.yy, Pi1.yy);
|
|
740
1584
|
vec4 iz0 = Pi0.zzzz;
|
|
@@ -795,49 +1639,16 @@ float cnoise(vec3 P)
|
|
|
795
1639
|
float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x);
|
|
796
1640
|
return 2.2 * n_xyz;
|
|
797
1641
|
}
|
|
1642
|
+
`;
|
|
1643
|
+
return cachedNoiseShader;
|
|
1644
|
+
};
|
|
798
1645
|
|
|
799
|
-
//
|
|
800
|
-
|
|
801
|
-
1.0, -0.39465, -0.58060,
|
|
802
|
-
1.0, 2.03211, 0.0);
|
|
803
|
-
|
|
804
|
-
// RGB to YUV matrix
|
|
805
|
-
mat3 rgb2yuv = mat3(0.2126, 0.7152, 0.0722,
|
|
806
|
-
-0.09991, -0.33609, 0.43600,
|
|
807
|
-
0.615, -0.5586, -0.05639);
|
|
808
|
-
|
|
809
|
-
vec3 oklab2rgb(vec3 linear)
|
|
810
|
-
{
|
|
811
|
-
const mat3 im1 = mat3(0.4121656120, 0.2118591070, 0.0883097947,
|
|
812
|
-
0.5362752080, 0.6807189584, 0.2818474174,
|
|
813
|
-
0.0514575653, 0.1074065790, 0.6302613616);
|
|
814
|
-
|
|
815
|
-
const mat3 im2 = mat3(+0.2104542553, +1.9779984951, +0.0259040371,
|
|
816
|
-
+0.7936177850, -2.4285922050, +0.7827717662,
|
|
817
|
-
-0.0040720468, +0.4505937099, -0.8086757660);
|
|
818
|
-
|
|
819
|
-
vec3 lms = im1 * linear;
|
|
820
|
-
|
|
821
|
-
return im2 * (sign(lms) * pow(abs(lms), vec3(1.0/3.0)));
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
vec3 rgb2oklab(vec3 oklab)
|
|
825
|
-
{
|
|
826
|
-
const mat3 m1 = mat3(+1.000000000, +1.000000000, +1.000000000,
|
|
827
|
-
+0.396337777, -0.105561346, -0.089484178,
|
|
828
|
-
+0.215803757, -0.063854173, -1.291485548);
|
|
829
|
-
|
|
830
|
-
const mat3 m2 = mat3(+4.076724529, -1.268143773, -0.004111989,
|
|
831
|
-
-3.307216883, +2.609332323, -0.703476310,
|
|
832
|
-
+0.230759054, -0.341134429, +1.706862569);
|
|
833
|
-
vec3 lms = m1 * oklab;
|
|
834
|
-
|
|
835
|
-
return m2 * (lms * lms * lms);
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
`;
|
|
1646
|
+
// Cache color functions as well
|
|
1647
|
+
let cachedColorFunctionsShader: string | null = null;
|
|
839
1648
|
|
|
840
|
-
const buildColorFunctions = () =>
|
|
1649
|
+
const buildColorFunctions = () => {
|
|
1650
|
+
if (cachedColorFunctionsShader) return cachedColorFunctionsShader;
|
|
1651
|
+
cachedColorFunctionsShader = `
|
|
841
1652
|
|
|
842
1653
|
vec3 saturation(vec3 rgb, float adjustment) {
|
|
843
1654
|
const vec3 W = vec3(0.2125, 0.7154, 0.0721);
|
|
@@ -881,7 +1692,8 @@ vec3 hsv2rgb(vec3 c)
|
|
|
881
1692
|
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
|
|
882
1693
|
}
|
|
883
1694
|
`;
|
|
884
|
-
|
|
1695
|
+
return cachedColorFunctionsShader;
|
|
1696
|
+
};
|
|
885
1697
|
|
|
886
1698
|
const setLinkStyles = (link: HTMLAnchorElement) => {
|
|
887
1699
|
link.id = LINK_ID;
|
|
@@ -902,19 +1714,20 @@ const setLinkStyles = (link: HTMLAnchorElement) => {
|
|
|
902
1714
|
link.innerHTML = "NEAT";
|
|
903
1715
|
}
|
|
904
1716
|
|
|
905
|
-
const addNeatLink = (ref: HTMLCanvasElement) => {
|
|
1717
|
+
const addNeatLink = (ref: HTMLCanvasElement): HTMLAnchorElement => {
|
|
906
1718
|
const existingLinks = ref.parentElement?.getElementsByTagName("a");
|
|
907
1719
|
if (existingLinks) {
|
|
908
1720
|
for (let i = 0; i < existingLinks.length; i++) {
|
|
909
1721
|
if (existingLinks[i].id === LINK_ID) {
|
|
910
1722
|
setLinkStyles(existingLinks[i]);
|
|
911
|
-
return;
|
|
1723
|
+
return existingLinks[i];
|
|
912
1724
|
}
|
|
913
1725
|
}
|
|
914
1726
|
}
|
|
915
1727
|
const link = document.createElement("a");
|
|
916
1728
|
setLinkStyles(link);
|
|
917
1729
|
ref.parentElement?.appendChild(link);
|
|
1730
|
+
return link;
|
|
918
1731
|
}
|
|
919
1732
|
|
|
920
1733
|
function getElapsedSecondsInLastHour() {
|