@firecms/neat 0.3.0 → 0.5.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 +365 -68
- package/dist/NeatGradient.d.ts +105 -0
- package/dist/NeatGradient.js +793 -202
- package/dist/NeatGradient.js.map +1 -1
- package/dist/index.es.js +604 -236
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +156 -133
- package/dist/index.umd.js.map +1 -1
- package/package.json +3 -3
- package/src/NeatGradient.ts +955 -209
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;
|
|
@@ -34,10 +44,43 @@ export type NeatConfig = {
|
|
|
34
44
|
colorBlending?: number;
|
|
35
45
|
grainScale?: number;
|
|
36
46
|
grainIntensity?: number;
|
|
47
|
+
grainSparsity?: number;
|
|
37
48
|
grainSpeed?: number;
|
|
38
49
|
wireframe?: boolean;
|
|
39
50
|
backgroundColor?: string;
|
|
40
51
|
backgroundAlpha?: number;
|
|
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;
|
|
41
84
|
};
|
|
42
85
|
|
|
43
86
|
export type NeatColor = {
|
|
@@ -73,6 +116,7 @@ export class NeatGradient implements NeatController {
|
|
|
73
116
|
|
|
74
117
|
private _grainScale: number = -1;
|
|
75
118
|
private _grainIntensity: number = -1;
|
|
119
|
+
private _grainSparsity: number = -1;
|
|
76
120
|
private _grainSpeed: number = -1;
|
|
77
121
|
|
|
78
122
|
private _colorBlending: number = -1;
|
|
@@ -83,10 +127,59 @@ export class NeatGradient implements NeatController {
|
|
|
83
127
|
private _backgroundColor: string = "#FFFFFF";
|
|
84
128
|
private _backgroundAlpha: number = 1.0;
|
|
85
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
|
+
|
|
86
167
|
private requestRef: number = -1;
|
|
87
168
|
private sizeObserver: ResizeObserver;
|
|
88
169
|
private sceneState: SceneState;
|
|
89
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
|
+
|
|
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
|
+
|
|
90
183
|
constructor(config: NeatConfig & { ref: HTMLCanvasElement, resolution?: number, seed?: number }) {
|
|
91
184
|
|
|
92
185
|
const {
|
|
@@ -105,12 +198,42 @@ export class NeatGradient implements NeatController {
|
|
|
105
198
|
colorBlending = 5,
|
|
106
199
|
grainScale = 2,
|
|
107
200
|
grainIntensity = 0.55,
|
|
201
|
+
grainSparsity = 0.0,
|
|
108
202
|
grainSpeed = 0.1,
|
|
109
203
|
wireframe = false,
|
|
110
204
|
backgroundColor = "#FFFFFF",
|
|
111
205
|
backgroundAlpha = 1.0,
|
|
112
206
|
resolution = 1,
|
|
113
|
-
seed
|
|
207
|
+
seed,
|
|
208
|
+
yOffset = 0,
|
|
209
|
+
yOffsetWaveMultiplier = 4,
|
|
210
|
+
yOffsetColorMultiplier = 4,
|
|
211
|
+
yOffsetFlowMultiplier = 4,
|
|
212
|
+
// Flow field parameters
|
|
213
|
+
flowDistortionA = 0,
|
|
214
|
+
flowDistortionB = 0,
|
|
215
|
+
flowScale = 1.0,
|
|
216
|
+
flowEase = 0.0,
|
|
217
|
+
flowEnabled = true,
|
|
218
|
+
// Mouse interaction
|
|
219
|
+
mouseDistortionStrength = 0.0,
|
|
220
|
+
mouseDistortionRadius = 0.25,
|
|
221
|
+
mouseDecayRate = 0.96,
|
|
222
|
+
mouseDarken = 0.0,
|
|
223
|
+
// Texture generation
|
|
224
|
+
enableProceduralTexture = false,
|
|
225
|
+
textureVoidLikelihood = 0.45,
|
|
226
|
+
textureVoidWidthMin = 200,
|
|
227
|
+
textureVoidWidthMax = 486,
|
|
228
|
+
textureBandDensity = 2.15,
|
|
229
|
+
textureColorBlending = 0.01,
|
|
230
|
+
textureSeed = 333,
|
|
231
|
+
textureEase = 0.5,
|
|
232
|
+
proceduralBackgroundColor = "#000000",
|
|
233
|
+
textureShapeTriangles = 20,
|
|
234
|
+
textureShapeCircles = 15,
|
|
235
|
+
textureShapeBars = 15,
|
|
236
|
+
textureShapeSquiggles = 10,
|
|
114
237
|
} = config;
|
|
115
238
|
|
|
116
239
|
|
|
@@ -129,6 +252,7 @@ export class NeatGradient implements NeatController {
|
|
|
129
252
|
this.colorBlending = colorBlending;
|
|
130
253
|
this.grainScale = grainScale;
|
|
131
254
|
this.grainIntensity = grainIntensity;
|
|
255
|
+
this.grainSparsity = grainSparsity;
|
|
132
256
|
this.grainSpeed = grainSpeed;
|
|
133
257
|
this.colors = colors;
|
|
134
258
|
this.shadows = shadows;
|
|
@@ -138,80 +262,166 @@ export class NeatGradient implements NeatController {
|
|
|
138
262
|
this.wireframe = wireframe;
|
|
139
263
|
this.backgroundColor = backgroundColor;
|
|
140
264
|
this.backgroundAlpha = backgroundAlpha;
|
|
141
|
-
|
|
265
|
+
this.yOffset = yOffset;
|
|
266
|
+
this.yOffsetWaveMultiplier = yOffsetWaveMultiplier;
|
|
267
|
+
this.yOffsetColorMultiplier = yOffsetColorMultiplier;
|
|
268
|
+
this.yOffsetFlowMultiplier = yOffsetFlowMultiplier;
|
|
269
|
+
|
|
270
|
+
// Flow field
|
|
271
|
+
this.flowDistortionA = flowDistortionA;
|
|
272
|
+
this.flowDistortionB = flowDistortionB;
|
|
273
|
+
this.flowScale = flowScale;
|
|
274
|
+
this.flowEase = flowEase;
|
|
275
|
+
this.flowEnabled = flowEnabled;
|
|
276
|
+
|
|
277
|
+
// Mouse interaction
|
|
278
|
+
this.mouseDistortionStrength = mouseDistortionStrength;
|
|
279
|
+
this.mouseDistortionRadius = mouseDistortionRadius;
|
|
280
|
+
this.mouseDecayRate = mouseDecayRate;
|
|
281
|
+
this.mouseDarken = mouseDarken;
|
|
282
|
+
|
|
283
|
+
// Texture generation
|
|
284
|
+
this.enableProceduralTexture = enableProceduralTexture;
|
|
285
|
+
this.textureVoidLikelihood = textureVoidLikelihood;
|
|
286
|
+
this.textureVoidWidthMin = textureVoidWidthMin;
|
|
287
|
+
this.textureVoidWidthMax = textureVoidWidthMax;
|
|
288
|
+
this.textureBandDensity = textureBandDensity;
|
|
289
|
+
this.textureColorBlending = textureColorBlending;
|
|
290
|
+
this.textureSeed = textureSeed;
|
|
291
|
+
this.textureEase = textureEase;
|
|
292
|
+
this._proceduralBackgroundColor = proceduralBackgroundColor;
|
|
293
|
+
|
|
294
|
+
this._textureShapeTriangles = textureShapeTriangles;
|
|
295
|
+
this._textureShapeCircles = textureShapeCircles;
|
|
296
|
+
this._textureShapeBars = textureShapeBars;
|
|
297
|
+
this._textureShapeSquiggles = textureShapeSquiggles;
|
|
298
|
+
|
|
299
|
+
// FIX 1: Setup mouse resources BEFORE building the material/scene
|
|
300
|
+
// This ensures u_mouse_texture isn't null during material compilation
|
|
301
|
+
this._setupMouseInteraction();
|
|
142
302
|
this.sceneState = this._initScene(resolution);
|
|
143
303
|
|
|
144
304
|
let tick = seed !== undefined ? seed : getElapsedSecondsInLastHour();
|
|
305
|
+
|
|
145
306
|
const render = () => {
|
|
146
307
|
|
|
147
|
-
const { renderer, camera, scene
|
|
308
|
+
const { renderer, camera, scene } = this.sceneState;
|
|
309
|
+
|
|
310
|
+
// Optimization: check if cached link is still valid in DOM, otherwise search
|
|
148
311
|
if (Math.floor(tick * 10) % 5 === 0) {
|
|
149
|
-
|
|
312
|
+
if (!this._linkElement || !document.contains(this._linkElement)) {
|
|
313
|
+
this._linkElement = addNeatLink(ref);
|
|
314
|
+
}
|
|
150
315
|
}
|
|
151
316
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const width = this._ref.width,
|
|
156
|
-
height = this._ref.height;
|
|
157
|
-
|
|
158
|
-
const colors = [
|
|
159
|
-
...this._colors.map(color => {
|
|
160
|
-
let threeColor = new THREE.Color();
|
|
161
|
-
threeColor.setStyle(color.color, "");
|
|
162
|
-
return ({
|
|
163
|
-
is_active: color.enabled,
|
|
164
|
-
color: threeColor,
|
|
165
|
-
influence: color.influence
|
|
166
|
-
});
|
|
167
|
-
}),
|
|
168
|
-
...Array.from({ length: COLORS_COUNT - this._colors.length }).map(() => ({
|
|
169
|
-
is_active: false,
|
|
170
|
-
color: new THREE.Color(0x000000)
|
|
171
|
-
}))
|
|
172
|
-
];
|
|
317
|
+
// Update Uniforms efficiently without creating new objects
|
|
318
|
+
if (this._cachedUniforms) {
|
|
319
|
+
const u = this._cachedUniforms;
|
|
173
320
|
|
|
174
321
|
tick += clock.getDelta() * this._speed;
|
|
175
|
-
// @ts-ignore
|
|
176
|
-
mesh.material.uniforms.u_time.value = tick;
|
|
177
|
-
// @ts-ignore
|
|
178
|
-
mesh.material.uniforms.u_resolution = { value: new THREE.Vector2(width, height) };
|
|
179
|
-
// @ts-ignore
|
|
180
|
-
mesh.material.uniforms.u_color_pressure = { value: new THREE.Vector2(this._horizontalPressure, this._verticalPressure) };
|
|
181
|
-
// @ts-ignore
|
|
182
|
-
mesh.material.uniforms.u_wave_frequency_x = { value: this._waveFrequencyX };
|
|
183
|
-
// @ts-ignore
|
|
184
|
-
mesh.material.uniforms.u_wave_frequency_y = { value: this._waveFrequencyY };
|
|
185
|
-
// @ts-ignore
|
|
186
|
-
mesh.material.uniforms.u_wave_amplitude = { value: this._waveAmplitude };
|
|
187
|
-
// @ts-ignore
|
|
188
|
-
mesh.material.uniforms.u_plane_width = { value: PLANE_WIDTH };
|
|
189
|
-
// @ts-ignore
|
|
190
|
-
mesh.material.uniforms.u_plane_height = { value: PLANE_HEIGHT };
|
|
191
|
-
// @ts-ignore
|
|
192
|
-
mesh.material.uniforms.u_color_blending = { value: this._colorBlending };
|
|
193
|
-
// @ts-ignore
|
|
194
|
-
mesh.material.uniforms.u_colors = { value: colors };
|
|
195
|
-
// @ts-ignore
|
|
196
|
-
mesh.material.uniforms.u_colors_count = { value: COLORS_COUNT };
|
|
197
|
-
// @ts-ignore
|
|
198
|
-
mesh.material.uniforms.u_shadows = { value: this._shadows };
|
|
199
|
-
// @ts-ignore
|
|
200
|
-
mesh.material.uniforms.u_highlights = { value: this._highlights };
|
|
201
|
-
// @ts-ignore
|
|
202
|
-
mesh.material.uniforms.u_saturation = { value: this._saturation };
|
|
203
|
-
// @ts-ignore
|
|
204
|
-
mesh.material.uniforms.u_brightness = { value: this._brightness };
|
|
205
|
-
// @ts-ignore
|
|
206
|
-
mesh.material.uniforms.u_grain_intensity = { value: this._grainIntensity };
|
|
207
|
-
// @ts-ignore
|
|
208
|
-
mesh.material.uniforms.u_grain_speed = { value: this._grainSpeed };
|
|
209
|
-
// @ts-ignore
|
|
210
|
-
mesh.material.uniforms.u_grain_scale = { value: this._grainScale };
|
|
211
|
-
// @ts-ignore
|
|
212
|
-
mesh.material.wireframe = this._wireframe;
|
|
213
|
-
});
|
|
214
322
|
|
|
323
|
+
u.u_time.value = tick;
|
|
324
|
+
u.u_resolution.value.set(this._ref.width, this._ref.height);
|
|
325
|
+
u.u_color_pressure.value.set(this._horizontalPressure, this._verticalPressure);
|
|
326
|
+
|
|
327
|
+
// Directly assign simple values
|
|
328
|
+
u.u_wave_frequency_x.value = this._waveFrequencyX;
|
|
329
|
+
u.u_wave_frequency_y.value = this._waveFrequencyY;
|
|
330
|
+
u.u_wave_amplitude.value = this._waveAmplitude;
|
|
331
|
+
u.u_color_blending.value = this._colorBlending;
|
|
332
|
+
u.u_shadows.value = this._shadows;
|
|
333
|
+
u.u_highlights.value = this._highlights;
|
|
334
|
+
u.u_saturation.value = this._saturation;
|
|
335
|
+
u.u_brightness.value = this._brightness;
|
|
336
|
+
u.u_grain_intensity.value = this._grainIntensity;
|
|
337
|
+
u.u_grain_sparsity.value = this._grainSparsity;
|
|
338
|
+
u.u_grain_speed.value = this._grainSpeed;
|
|
339
|
+
u.u_grain_scale.value = this._grainScale;
|
|
340
|
+
u.u_y_offset.value = this._yOffset;
|
|
341
|
+
u.u_y_offset_wave_multiplier.value = this._yOffsetWaveMultiplier;
|
|
342
|
+
u.u_y_offset_color_multiplier.value = this._yOffsetColorMultiplier;
|
|
343
|
+
u.u_y_offset_flow_multiplier.value = this._yOffsetFlowMultiplier;
|
|
344
|
+
u.u_flow_distortion_a.value = this._flowDistortionA;
|
|
345
|
+
u.u_flow_distortion_b.value = this._flowDistortionB;
|
|
346
|
+
u.u_flow_scale.value = this._flowScale;
|
|
347
|
+
u.u_flow_ease.value = this._flowEase;
|
|
348
|
+
u.u_flow_enabled.value = this._flowEnabled ? 1.0 : 0.0;
|
|
349
|
+
u.u_mouse_distortion_strength.value = this._mouseDistortionStrength;
|
|
350
|
+
u.u_mouse_distortion_radius.value = this._mouseDistortionRadius;
|
|
351
|
+
u.u_mouse_darken.value = this._mouseDarken;
|
|
352
|
+
u.u_enable_procedural_texture.value = this._enableProceduralTexture ? 1.0 : 0.0;
|
|
353
|
+
u.u_procedural_texture.value = this._proceduralTexture;
|
|
354
|
+
u.u_texture_ease.value = this._textureEase;
|
|
355
|
+
|
|
356
|
+
// Optimized Color Update: Update the existing array objects instead of recreating array
|
|
357
|
+
const shaderColors = u.u_colors.value;
|
|
358
|
+
for(let i = 0; i < COLORS_COUNT; i++) {
|
|
359
|
+
if (i < this._colors.length) {
|
|
360
|
+
const c = this._colors[i];
|
|
361
|
+
shaderColors[i].is_active = c.enabled ? 1.0 : 0.0;
|
|
362
|
+
shaderColors[i].color.setStyle(c.color, "");
|
|
363
|
+
shaderColors[i].influence = c.influence || 0;
|
|
364
|
+
} else {
|
|
365
|
+
shaderColors[i].is_active = 0.0;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
u.u_colors_count.value = COLORS_COUNT;
|
|
370
|
+
// Wireframe is a material property, not a uniform
|
|
371
|
+
// @ts-ignore - access material safely
|
|
372
|
+
this.sceneState.meshes[0].material.wireframe = this._wireframe;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Render mouse interaction to FBO
|
|
376
|
+
if (this._mouseFBO && this._sceneMouse && this._cameraMouse) {
|
|
377
|
+
let hasActiveBrushes = false;
|
|
378
|
+
|
|
379
|
+
// Update mouse objects - decay rate controls how fast trails fade
|
|
380
|
+
for(let i = 0; i < this._mouseObjects.length; i++) {
|
|
381
|
+
const obj = this._mouseObjects[i];
|
|
382
|
+
if (obj.mesh.visible) {
|
|
383
|
+
hasActiveBrushes = true;
|
|
384
|
+
obj.mesh.rotation.z += 0.01;
|
|
385
|
+
if (obj.mesh.material instanceof THREE.MeshBasicMaterial) {
|
|
386
|
+
// Decay only affects opacity
|
|
387
|
+
obj.mesh.material.opacity *= this._mouseDecayRate;
|
|
388
|
+
|
|
389
|
+
if (obj.mesh.material.opacity < 0.01) {
|
|
390
|
+
obj.mesh.visible = false;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// FIX 2: Handle FBO Clearing correctly
|
|
397
|
+
// Store current clear color (likely the main background color)
|
|
398
|
+
renderer.getClearColor(this._tempClearColor);
|
|
399
|
+
const oldClearAlpha = renderer.getClearAlpha();
|
|
400
|
+
|
|
401
|
+
// Set clear color to Black/Transparent for the FBO.
|
|
402
|
+
// Important: If we use the main background color (e.g. White), the FBO
|
|
403
|
+
// will be white, causing 100% distortion everywhere.
|
|
404
|
+
renderer.setClearColor(0x000000, 0.0);
|
|
405
|
+
|
|
406
|
+
renderer.setRenderTarget(this._mouseFBO);
|
|
407
|
+
renderer.clear();
|
|
408
|
+
|
|
409
|
+
if (hasActiveBrushes) {
|
|
410
|
+
renderer.render(this._sceneMouse, this._cameraMouse);
|
|
411
|
+
}
|
|
412
|
+
renderer.setRenderTarget(null);
|
|
413
|
+
|
|
414
|
+
// Restore main background color for the actual scene render
|
|
415
|
+
renderer.setClearColor(this._tempClearColor, oldClearAlpha);
|
|
416
|
+
|
|
417
|
+
// Update mouse texture uniform
|
|
418
|
+
if (this._cachedUniforms) {
|
|
419
|
+
this._cachedUniforms.u_mouse_texture.value = this._mouseFBO.texture;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Ensure we set the clear color for the main scene explicitly before rendering
|
|
424
|
+
renderer.setClearColor(this._backgroundColor, this._backgroundAlpha);
|
|
215
425
|
renderer.render(scene, camera);
|
|
216
426
|
this.requestRef = requestAnimationFrame(render);
|
|
217
427
|
};
|
|
@@ -225,6 +435,19 @@ export class NeatGradient implements NeatController {
|
|
|
225
435
|
|
|
226
436
|
this.sceneState.renderer.setSize(width, height, false);
|
|
227
437
|
updateCamera(this.sceneState.camera, width, height);
|
|
438
|
+
|
|
439
|
+
// FIX 3: Update Mouse FBO and Camera on resize
|
|
440
|
+
// If we don't do this, mouse coordinates map incorrectly after a resize
|
|
441
|
+
if (this._mouseFBO && this._cameraMouse) {
|
|
442
|
+
const fSize = height / 2;
|
|
443
|
+
const aspect = width / height;
|
|
444
|
+
this._mouseFBO.setSize(width / 2, height / 2);
|
|
445
|
+
this._cameraMouse.left = -fSize * aspect;
|
|
446
|
+
this._cameraMouse.right = fSize * aspect;
|
|
447
|
+
this._cameraMouse.top = fSize;
|
|
448
|
+
this._cameraMouse.bottom = -fSize;
|
|
449
|
+
this._cameraMouse.updateProjectionMatrix();
|
|
450
|
+
}
|
|
228
451
|
};
|
|
229
452
|
|
|
230
453
|
this.sizeObserver = new ResizeObserver(entries => {
|
|
@@ -241,9 +464,28 @@ export class NeatGradient implements NeatController {
|
|
|
241
464
|
if (this) {
|
|
242
465
|
cancelAnimationFrame(this.requestRef);
|
|
243
466
|
this.sizeObserver.disconnect();
|
|
467
|
+
|
|
468
|
+
// Cleanup WebGL resources
|
|
469
|
+
if (this.sceneState) {
|
|
470
|
+
this.sceneState.renderer.dispose();
|
|
471
|
+
this.sceneState.meshes.forEach(m => {
|
|
472
|
+
m.geometry.dispose();
|
|
473
|
+
if(Array.isArray(m.material)) m.material.forEach(mat => mat.dispose());
|
|
474
|
+
else m.material.dispose();
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
if (this._mouseFBO) this._mouseFBO.dispose();
|
|
478
|
+
if (this._proceduralTexture) this._proceduralTexture.dispose();
|
|
244
479
|
}
|
|
245
480
|
}
|
|
246
481
|
|
|
482
|
+
downloadAsPNG(filename = "neat.png") {
|
|
483
|
+
console.log("Downloading as PNG", this._ref);
|
|
484
|
+
const dataURL = this._ref.toDataURL("image/png");
|
|
485
|
+
console.log("data", dataURL);
|
|
486
|
+
downloadURI(dataURL, filename);
|
|
487
|
+
}
|
|
488
|
+
|
|
247
489
|
set speed(speed: number) {
|
|
248
490
|
this._speed = speed / 20;
|
|
249
491
|
}
|
|
@@ -300,6 +542,10 @@ export class NeatGradient implements NeatController {
|
|
|
300
542
|
this._grainIntensity = grainIntensity;
|
|
301
543
|
}
|
|
302
544
|
|
|
545
|
+
set grainSparsity(grainSparsity: number) {
|
|
546
|
+
this._grainSparsity = grainSparsity;
|
|
547
|
+
}
|
|
548
|
+
|
|
303
549
|
set grainSpeed(grainSpeed: number) {
|
|
304
550
|
this._grainSpeed = grainSpeed;
|
|
305
551
|
}
|
|
@@ -320,14 +566,186 @@ export class NeatGradient implements NeatController {
|
|
|
320
566
|
this._backgroundAlpha = backgroundAlpha;
|
|
321
567
|
}
|
|
322
568
|
|
|
569
|
+
set yOffset(yOffset: number) {
|
|
570
|
+
this._yOffset = yOffset;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
get yOffsetWaveMultiplier(): number {
|
|
574
|
+
return this._yOffsetWaveMultiplier * 1000;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
set yOffsetWaveMultiplier(value: number) {
|
|
578
|
+
this._yOffsetWaveMultiplier = value / 1000;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
get yOffsetColorMultiplier(): number {
|
|
582
|
+
return this._yOffsetColorMultiplier * 1000;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
set yOffsetColorMultiplier(value: number) {
|
|
586
|
+
this._yOffsetColorMultiplier = value / 1000;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
get yOffsetFlowMultiplier(): number {
|
|
590
|
+
return this._yOffsetFlowMultiplier * 1000;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
set yOffsetFlowMultiplier(value: number) {
|
|
594
|
+
this._yOffsetFlowMultiplier = value / 1000;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
set flowDistortionA(value: number) {
|
|
598
|
+
this._flowDistortionA = value;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
set flowDistortionB(value: number) {
|
|
602
|
+
this._flowDistortionB = value;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
set flowScale(value: number) {
|
|
606
|
+
this._flowScale = value;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
set flowEase(value: number) {
|
|
610
|
+
this._flowEase = value;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
set flowEnabled(value: boolean) {
|
|
614
|
+
this._flowEnabled = value;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
get flowEnabled(): boolean {
|
|
618
|
+
return this._flowEnabled;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
set mouseDistortionStrength(value: number) {
|
|
623
|
+
this._mouseDistortionStrength = Math.max(0, value);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
set mouseDistortionRadius(value: number) {
|
|
627
|
+
// Clamp to a sane range in UV space
|
|
628
|
+
this._mouseDistortionRadius = Math.max(0.01, Math.min(value, 1.0));
|
|
629
|
+
// Update brush scale when radius changes
|
|
630
|
+
this._updateBrushScale();
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
_updateBrushScale() {
|
|
634
|
+
if (!this._mouseObjects || this._mouseObjects.length === 0) return;
|
|
635
|
+
// Radius directly controls the brush scale
|
|
636
|
+
// Base geometry is 200px, so radius 0.25 = 50px, radius 1.0 = 200px
|
|
637
|
+
this._mouseBrushBaseScale = this._mouseDistortionRadius;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
set mouseDecayRate(value: number) {
|
|
641
|
+
// Clamp between 0.9 (slow decay, more wobble) and 0.99 (fast decay, less wobble)
|
|
642
|
+
this._mouseDecayRate = Math.max(0.9, Math.min(value, 0.99));
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
set mouseDarken(value: number) {
|
|
646
|
+
this._mouseDarken = value;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
set enableProceduralTexture(value: boolean) {
|
|
650
|
+
this._enableProceduralTexture = value;
|
|
651
|
+
if (value && !this._proceduralTexture) {
|
|
652
|
+
this._proceduralTexture = this._createProceduralTexture();
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
set textureVoidLikelihood(value: number) {
|
|
657
|
+
this._textureVoidLikelihood = value;
|
|
658
|
+
if (this._enableProceduralTexture) {
|
|
659
|
+
this._proceduralTexture = this._createProceduralTexture();
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
set textureVoidWidthMin(value: number) {
|
|
664
|
+
this._textureVoidWidthMin = value;
|
|
665
|
+
if (this._enableProceduralTexture) {
|
|
666
|
+
this._proceduralTexture = this._createProceduralTexture();
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
set textureVoidWidthMax(value: number) {
|
|
671
|
+
this._textureVoidWidthMax = value;
|
|
672
|
+
if (this._enableProceduralTexture) {
|
|
673
|
+
this._proceduralTexture = this._createProceduralTexture();
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
set textureBandDensity(value: number) {
|
|
678
|
+
this._textureBandDensity = value;
|
|
679
|
+
if (this._enableProceduralTexture) {
|
|
680
|
+
this._proceduralTexture = this._createProceduralTexture();
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
set textureColorBlending(value: number) {
|
|
685
|
+
this._textureColorBlending = value;
|
|
686
|
+
if (this._enableProceduralTexture) {
|
|
687
|
+
this._proceduralTexture = this._createProceduralTexture();
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
set textureSeed(value: number) {
|
|
692
|
+
this._textureSeed = value;
|
|
693
|
+
if (this._enableProceduralTexture) {
|
|
694
|
+
this._proceduralTexture = this._createProceduralTexture();
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
get textureEase(): number {
|
|
699
|
+
return this._textureEase;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
set textureEase(value: number) {
|
|
703
|
+
this._textureEase = value;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
set proceduralBackgroundColor(value: string) {
|
|
707
|
+
this._proceduralBackgroundColor = value;
|
|
708
|
+
if (this._enableProceduralTexture) {
|
|
709
|
+
this._proceduralTexture = this._createProceduralTexture();
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
set textureShapeTriangles(value: number) {
|
|
714
|
+
this._textureShapeTriangles = value;
|
|
715
|
+
if (this._enableProceduralTexture) this._proceduralTexture = this._createProceduralTexture();
|
|
716
|
+
}
|
|
717
|
+
set textureShapeCircles(value: number) {
|
|
718
|
+
this._textureShapeCircles = value;
|
|
719
|
+
if (this._enableProceduralTexture) this._proceduralTexture = this._createProceduralTexture();
|
|
720
|
+
}
|
|
721
|
+
set textureShapeBars(value: number) {
|
|
722
|
+
this._textureShapeBars = value;
|
|
723
|
+
if (this._enableProceduralTexture) this._proceduralTexture = this._createProceduralTexture();
|
|
724
|
+
}
|
|
725
|
+
set textureShapeSquiggles(value: number) {
|
|
726
|
+
this._textureShapeSquiggles = value;
|
|
727
|
+
if (this._enableProceduralTexture) this._proceduralTexture = this._createProceduralTexture();
|
|
728
|
+
}
|
|
729
|
+
|
|
323
730
|
_initScene(resolution: number): SceneState {
|
|
324
731
|
|
|
325
732
|
const width = this._ref.width,
|
|
326
733
|
height = this._ref.height;
|
|
327
734
|
|
|
735
|
+
// Cleanup existing renderer if needed
|
|
736
|
+
if (this.sceneState && this.sceneState.renderer) {
|
|
737
|
+
this.sceneState.renderer.dispose();
|
|
738
|
+
this.sceneState.meshes.forEach(m => {
|
|
739
|
+
m.geometry.dispose();
|
|
740
|
+
if(Array.isArray(m.material)) m.material.forEach(mat => mat.dispose());
|
|
741
|
+
else m.material.dispose();
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
|
|
328
745
|
const renderer = new THREE.WebGLRenderer({
|
|
329
746
|
// antialias: true,
|
|
330
747
|
alpha: true,
|
|
748
|
+
preserveDrawingBuffer: true,
|
|
331
749
|
canvas: this._ref
|
|
332
750
|
});
|
|
333
751
|
|
|
@@ -363,17 +781,13 @@ export class NeatGradient implements NeatController {
|
|
|
363
781
|
|
|
364
782
|
_buildMaterial(width: number, height: number) {
|
|
365
783
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
is_active: false,
|
|
374
|
-
color: new THREE.Color(0x000000)
|
|
375
|
-
}))
|
|
376
|
-
];
|
|
784
|
+
// Initialize stable array structure for colors
|
|
785
|
+
// We create 6 objects and just update them in the render loop to avoid GC
|
|
786
|
+
const colors = Array.from({ length: COLORS_COUNT }).map((_, i) => ({
|
|
787
|
+
is_active: i < this._colors.length ? (this._colors[i].enabled ? 1.0 : 0.0) : 0.0,
|
|
788
|
+
color: new THREE.Color(i < this._colors.length ? this._colors[i].color : 0x000000),
|
|
789
|
+
influence: i < this._colors.length ? (this._colors[i].influence || 0) : 0
|
|
790
|
+
}));
|
|
377
791
|
|
|
378
792
|
const uniforms = {
|
|
379
793
|
u_time: { value: 0 },
|
|
@@ -389,8 +803,32 @@ export class NeatGradient implements NeatController {
|
|
|
389
803
|
u_shadows: { value: this._shadows },
|
|
390
804
|
u_highlights: { value: this._highlights },
|
|
391
805
|
u_grain_intensity: { value: this._grainIntensity },
|
|
806
|
+
u_grain_sparsity: { value: this._grainSparsity },
|
|
392
807
|
u_grain_scale: { value: this._grainScale },
|
|
393
808
|
u_grain_speed: { value: this._grainSpeed },
|
|
809
|
+
// Flow field
|
|
810
|
+
u_flow_distortion_a: { value: this._flowDistortionA },
|
|
811
|
+
u_flow_distortion_b: { value: this._flowDistortionB },
|
|
812
|
+
u_flow_scale: { value: this._flowScale },
|
|
813
|
+
u_flow_ease: { value: this._flowEase },
|
|
814
|
+
u_flow_enabled: { value: this._flowEnabled ? 1.0 : 0.0 },
|
|
815
|
+
// Y offset multipliers
|
|
816
|
+
u_y_offset: { value: this._yOffset },
|
|
817
|
+
u_y_offset_wave_multiplier: { value: this._yOffsetWaveMultiplier },
|
|
818
|
+
u_y_offset_color_multiplier: { value: this._yOffsetColorMultiplier },
|
|
819
|
+
u_y_offset_flow_multiplier: { value: this._yOffsetFlowMultiplier },
|
|
820
|
+
// Mouse interaction
|
|
821
|
+
u_mouse_distortion_strength: { value: this._mouseDistortionStrength },
|
|
822
|
+
u_mouse_distortion_radius: { value: this._mouseDistortionRadius },
|
|
823
|
+
u_mouse_darken: { value: this._mouseDarken },
|
|
824
|
+
u_mouse_texture: { value: this._mouseFBO ? this._mouseFBO.texture : null },
|
|
825
|
+
// Procedural texture
|
|
826
|
+
u_procedural_texture: { value: this._proceduralTexture },
|
|
827
|
+
u_enable_procedural_texture: { value: this._enableProceduralTexture ? 1.0 : 0.0 },
|
|
828
|
+
u_texture_ease: { value: this._textureEase },
|
|
829
|
+
u_saturation: { value: this._saturation },
|
|
830
|
+
u_brightness: { value: this._brightness },
|
|
831
|
+
u_color_blending: { value: this._colorBlending }
|
|
394
832
|
};
|
|
395
833
|
|
|
396
834
|
const material = new THREE.ShaderMaterial({
|
|
@@ -399,10 +837,287 @@ export class NeatGradient implements NeatController {
|
|
|
399
837
|
fragmentShader: buildUniforms() + buildColorFunctions() + buildNoise() + buildFragmentShader()
|
|
400
838
|
});
|
|
401
839
|
|
|
840
|
+
// Cache the uniforms object for direct access in render loop
|
|
841
|
+
this._cachedUniforms = uniforms as unknown as NeatUniforms;
|
|
842
|
+
|
|
402
843
|
material.wireframe = WIREFRAME;
|
|
403
844
|
return material;
|
|
404
845
|
}
|
|
405
846
|
|
|
847
|
+
_setupMouseInteraction() {
|
|
848
|
+
if (!this._ref) return;
|
|
849
|
+
|
|
850
|
+
const width = this._ref.width;
|
|
851
|
+
const height = this._ref.height;
|
|
852
|
+
|
|
853
|
+
// Create mouse FBO
|
|
854
|
+
this._mouseFBO = new THREE.WebGLRenderTarget(width / 2, height / 2);
|
|
855
|
+
|
|
856
|
+
// Create mouse scene and camera
|
|
857
|
+
this._sceneMouse = new THREE.Scene();
|
|
858
|
+
const fSize = height / 2;
|
|
859
|
+
const aspect = width / height;
|
|
860
|
+
|
|
861
|
+
// FIX 4: Ensure near plane allows viewing objects at Z=0
|
|
862
|
+
// Near -100 is safer for objects at 0
|
|
863
|
+
this._cameraMouse = new THREE.OrthographicCamera(
|
|
864
|
+
-fSize * aspect, fSize * aspect,
|
|
865
|
+
fSize, -fSize,
|
|
866
|
+
0, 10000
|
|
867
|
+
);
|
|
868
|
+
this._cameraMouse.position.set(0, 0, 100);
|
|
869
|
+
|
|
870
|
+
// Create brush texture - More visible and impactful
|
|
871
|
+
const brushCanvas = document.createElement('canvas');
|
|
872
|
+
brushCanvas.width = 128;
|
|
873
|
+
brushCanvas.height = 128;
|
|
874
|
+
const bCtx = brushCanvas.getContext('2d');
|
|
875
|
+
if (bCtx) {
|
|
876
|
+
const grd = bCtx.createRadialGradient(64, 64, 0, 64, 64, 64);
|
|
877
|
+
// Match reference implementation's stronger gradient
|
|
878
|
+
grd.addColorStop(0, 'rgba(255,255,255,0.8)');
|
|
879
|
+
grd.addColorStop(0.5, 'rgba(255,255,255,0.4)');
|
|
880
|
+
grd.addColorStop(1, 'rgba(255,255,255,0)');
|
|
881
|
+
bCtx.fillStyle = grd;
|
|
882
|
+
bCtx.fillRect(0, 0, 128, 128);
|
|
883
|
+
}
|
|
884
|
+
const brushTex = new THREE.CanvasTexture(brushCanvas);
|
|
885
|
+
const brushMat = new THREE.MeshBasicMaterial({
|
|
886
|
+
map: brushTex,
|
|
887
|
+
transparent: true,
|
|
888
|
+
opacity: 1.0,
|
|
889
|
+
depthTest: false,
|
|
890
|
+
blending: THREE.AdditiveBlending // Additive blending for better accumulation
|
|
891
|
+
});
|
|
892
|
+
// Brush geometry size - will be scaled by radius parameter
|
|
893
|
+
const brushGeo = new THREE.PlaneGeometry(200, 200);
|
|
894
|
+
|
|
895
|
+
// Create brush pool
|
|
896
|
+
const brushPoolSize = 50;
|
|
897
|
+
for (let i = 0; i < brushPoolSize; i++) {
|
|
898
|
+
const m = new THREE.Mesh(brushGeo, brushMat.clone());
|
|
899
|
+
m.visible = false;
|
|
900
|
+
this._sceneMouse!.add(m);
|
|
901
|
+
this._mouseObjects.push({ mesh: m, active: false });
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Initialize brush scale based on current radius
|
|
905
|
+
this._updateBrushScale();
|
|
906
|
+
|
|
907
|
+
// Add mouse move listener
|
|
908
|
+
this._ref.addEventListener('mousemove', this._onMouseMove.bind(this));
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
_onMouseMove(e: MouseEvent) {
|
|
912
|
+
if (!this._ref || !this._sceneMouse) return;
|
|
913
|
+
const rect = this._ref.getBoundingClientRect();
|
|
914
|
+
const width = this._ref.width;
|
|
915
|
+
const height = this._ref.height;
|
|
916
|
+
|
|
917
|
+
this._mouse.x = e.clientX - rect.left - width / 2;
|
|
918
|
+
this._mouse.y = -(e.clientY - rect.top - height / 2);
|
|
919
|
+
|
|
920
|
+
const brush = this._mouseObjects[this._currentBrush];
|
|
921
|
+
brush.mesh.scale.set(this._mouseBrushBaseScale, this._mouseBrushBaseScale, 1.0);
|
|
922
|
+
brush.active = true;
|
|
923
|
+
brush.mesh.visible = true;
|
|
924
|
+
brush.mesh.position.set(this._mouse.x, this._mouse.y, 0);
|
|
925
|
+
brush.mesh.rotation.z = Math.random() * Math.PI * 2;
|
|
926
|
+
if (brush.mesh.material instanceof THREE.MeshBasicMaterial) {
|
|
927
|
+
brush.mesh.material.opacity = 1.0;
|
|
928
|
+
}
|
|
929
|
+
this._currentBrush = (this._currentBrush + 1) % this._mouseObjects.length;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
_createProceduralTexture(): THREE.Texture {
|
|
933
|
+
// Texture size - 1024 provides good balance between quality and performance
|
|
934
|
+
// Can be increased to 2048 for even better quality if needed
|
|
935
|
+
const texSize = 1024;
|
|
936
|
+
const sourceCanvas = document.createElement('canvas');
|
|
937
|
+
sourceCanvas.width = texSize;
|
|
938
|
+
sourceCanvas.height = texSize;
|
|
939
|
+
const sCtx = sourceCanvas.getContext('2d');
|
|
940
|
+
if (!sCtx) return new THREE.Texture();
|
|
941
|
+
|
|
942
|
+
let seed = this._textureSeed;
|
|
943
|
+
const baseSeed = this._textureSeed;
|
|
944
|
+
|
|
945
|
+
function random() {
|
|
946
|
+
const x = Math.sin(seed++) * 10000;
|
|
947
|
+
return x - Math.floor(x);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// Helper to reset seed for isolated shape generation
|
|
951
|
+
const setSeed = (offset: number) => {
|
|
952
|
+
seed = baseSeed + offset;
|
|
953
|
+
};
|
|
954
|
+
|
|
955
|
+
const colors = this._colors.filter(c => c.enabled).map(c => c.color);
|
|
956
|
+
if (colors.length === 0) return new THREE.Texture();
|
|
957
|
+
|
|
958
|
+
// Helper functions
|
|
959
|
+
function hexToRgb(hex: string) {
|
|
960
|
+
const bigint = parseInt(hex.replace('#', ''), 16);
|
|
961
|
+
return {
|
|
962
|
+
r: (bigint >> 16) & 255,
|
|
963
|
+
g: (bigint >> 8) & 255,
|
|
964
|
+
b: bigint & 255
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function rgbToHex(r: number, g: number, b: number) {
|
|
969
|
+
return "#" + ((1 << 24) + (Math.round(r) << 16) + (Math.round(g) << 8) + Math.round(b)).toString(16).slice(1);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
const getInterColor = () => {
|
|
973
|
+
const c1 = colors[Math.floor(random() * colors.length)];
|
|
974
|
+
const c2 = colors[Math.floor(random() * colors.length)];
|
|
975
|
+
const mix = random() * this._textureColorBlending;
|
|
976
|
+
const rgb1 = hexToRgb(c1);
|
|
977
|
+
const rgb2 = hexToRgb(c2);
|
|
978
|
+
const r = rgb1.r + (rgb2.r - rgb1.r) * mix;
|
|
979
|
+
const g = rgb1.g + (rgb2.g - rgb1.g) * mix;
|
|
980
|
+
const b = rgb1.b + (rgb2.b - rgb1.b) * mix;
|
|
981
|
+
return rgbToHex(r, g, b);
|
|
982
|
+
};
|
|
983
|
+
|
|
984
|
+
// === SOURCE CANVAS ===
|
|
985
|
+
// Base with procedural background color so even sparse areas pick it up
|
|
986
|
+
const baseColor = this._proceduralBackgroundColor || "#000000";
|
|
987
|
+
sCtx.fillStyle = baseColor;
|
|
988
|
+
sCtx.fillRect(0, 0, texSize, texSize);
|
|
989
|
+
|
|
990
|
+
// Then lay a vertical gradient of mixed colors on top for richness
|
|
991
|
+
const bgGrad = sCtx.createLinearGradient(0, 0, 0, texSize);
|
|
992
|
+
bgGrad.addColorStop(0, getInterColor());
|
|
993
|
+
bgGrad.addColorStop(1, getInterColor());
|
|
994
|
+
sCtx.fillStyle = bgGrad;
|
|
995
|
+
sCtx.fillRect(0, 0, texSize, texSize);
|
|
996
|
+
|
|
997
|
+
// Triangles: use configurable count
|
|
998
|
+
for (let i = 0; i < this._textureShapeTriangles; i++) {
|
|
999
|
+
sCtx.fillStyle = getInterColor();
|
|
1000
|
+
sCtx.beginPath();
|
|
1001
|
+
const x = random() * texSize;
|
|
1002
|
+
const y = random() * texSize;
|
|
1003
|
+
const s = 100 + random() * 300;
|
|
1004
|
+
sCtx.moveTo(x, y);
|
|
1005
|
+
sCtx.lineTo(x + (random() - 0.5) * s, y + (random() - 0.5) * s);
|
|
1006
|
+
sCtx.lineTo(x + (random() - 0.5) * s, y + (random() - 0.5) * s);
|
|
1007
|
+
sCtx.fill();
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// Circles / rings: use configurable count
|
|
1011
|
+
for (let i = 0; i < this._textureShapeCircles; i++) {
|
|
1012
|
+
sCtx.strokeStyle = getInterColor();
|
|
1013
|
+
sCtx.lineWidth = 10 + random() * 50;
|
|
1014
|
+
sCtx.beginPath();
|
|
1015
|
+
const x = random() * texSize;
|
|
1016
|
+
const y = random() * texSize;
|
|
1017
|
+
const r = 50 + random() * 150;
|
|
1018
|
+
sCtx.arc(x, y, r, 0, Math.PI * 2);
|
|
1019
|
+
sCtx.stroke();
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// Bars: use configurable count
|
|
1023
|
+
for (let i = 0; i < this._textureShapeBars; i++) {
|
|
1024
|
+
sCtx.fillStyle = getInterColor();
|
|
1025
|
+
sCtx.save();
|
|
1026
|
+
sCtx.translate(random() * texSize, random() * texSize);
|
|
1027
|
+
sCtx.rotate(random() * Math.PI);
|
|
1028
|
+
sCtx.fillRect(-150, -25, 300, 50);
|
|
1029
|
+
sCtx.restore();
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// Squiggles: use configurable count
|
|
1033
|
+
sCtx.lineWidth = 15;
|
|
1034
|
+
sCtx.lineCap = 'round';
|
|
1035
|
+
for (let i = 0; i < this._textureShapeSquiggles; i++) {
|
|
1036
|
+
sCtx.strokeStyle = getInterColor();
|
|
1037
|
+
sCtx.beginPath();
|
|
1038
|
+
let x = random() * texSize;
|
|
1039
|
+
let y = random() * texSize;
|
|
1040
|
+
sCtx.moveTo(x, y);
|
|
1041
|
+
for (let j = 0; j < 4; j++) {
|
|
1042
|
+
sCtx.bezierCurveTo(
|
|
1043
|
+
x + (random() - 0.5) * 300, y + (random() - 0.5) * 300,
|
|
1044
|
+
x + (random() - 0.5) * 300, y + (random() - 0.5) * 300,
|
|
1045
|
+
x + (random() - 0.5) * 300, y + (random() - 0.5) * 300
|
|
1046
|
+
);
|
|
1047
|
+
x += (random() - 0.5) * 300;
|
|
1048
|
+
y += (random() - 0.5) * 300;
|
|
1049
|
+
}
|
|
1050
|
+
sCtx.stroke();
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// === MASKED CANVAS ===
|
|
1054
|
+
// Masking: Seed isolation
|
|
1055
|
+
setSeed(50000);
|
|
1056
|
+
const canvas = document.createElement('canvas');
|
|
1057
|
+
canvas.width = texSize;
|
|
1058
|
+
canvas.height = texSize;
|
|
1059
|
+
const ctx = canvas.getContext('2d');
|
|
1060
|
+
if (!ctx) return new THREE.Texture();
|
|
1061
|
+
|
|
1062
|
+
// Start filled with the chosen void color so gaps show that color
|
|
1063
|
+
ctx.fillStyle = baseColor;
|
|
1064
|
+
ctx.fillRect(0, 0, texSize, texSize);
|
|
1065
|
+
|
|
1066
|
+
// Determine layout segments (matter vs void)
|
|
1067
|
+
let layoutHead = 0;
|
|
1068
|
+
const segments: Array<{ type: 'void' | 'matter', x: number, width: number }> = [];
|
|
1069
|
+
|
|
1070
|
+
while (layoutHead < texSize) {
|
|
1071
|
+
const isVoid = random() < this._textureVoidLikelihood;
|
|
1072
|
+
if (isVoid) {
|
|
1073
|
+
const w = this._textureVoidWidthMin + random() * (this._textureVoidWidthMax - this._textureVoidWidthMin);
|
|
1074
|
+
segments.push({ type: 'void', x: layoutHead, width: w });
|
|
1075
|
+
layoutHead += w;
|
|
1076
|
+
} else {
|
|
1077
|
+
const w = 50 + random() * 200;
|
|
1078
|
+
segments.push({ type: 'matter', x: layoutHead, width: w });
|
|
1079
|
+
layoutHead += w;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// Render only matter bands from the source into the masked canvas
|
|
1084
|
+
for (const seg of segments) {
|
|
1085
|
+
if (seg.type === 'matter') {
|
|
1086
|
+
const startX = seg.x;
|
|
1087
|
+
const endX = Math.min(seg.x + seg.width, texSize);
|
|
1088
|
+
let currentX = startX;
|
|
1089
|
+
|
|
1090
|
+
while (currentX < endX) {
|
|
1091
|
+
const stripeWidth = (2 + random() * 20) / this._textureBandDensity;
|
|
1092
|
+
const sourceX = Math.floor(random() * texSize);
|
|
1093
|
+
ctx.drawImage(
|
|
1094
|
+
sourceCanvas,
|
|
1095
|
+
sourceX, 0, stripeWidth, texSize,
|
|
1096
|
+
currentX, 0, stripeWidth, texSize
|
|
1097
|
+
);
|
|
1098
|
+
currentX += stripeWidth;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
// void segments: leave as baseColor
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
const tex = new THREE.CanvasTexture(canvas);
|
|
1105
|
+
// Use mipmapping for better quality when texture is scaled
|
|
1106
|
+
tex.minFilter = THREE.LinearMipmapLinearFilter;
|
|
1107
|
+
tex.magFilter = THREE.LinearFilter;
|
|
1108
|
+
tex.wrapS = THREE.RepeatWrapping;
|
|
1109
|
+
tex.wrapT = THREE.RepeatWrapping;
|
|
1110
|
+
|
|
1111
|
+
// Enable anisotropic filtering for much better quality when texture is stretched
|
|
1112
|
+
// 16 is a commonly supported value that dramatically improves quality
|
|
1113
|
+
tex.anisotropy = 16;
|
|
1114
|
+
|
|
1115
|
+
// Ensure mipmaps are generated
|
|
1116
|
+
tex.needsUpdate = true;
|
|
1117
|
+
|
|
1118
|
+
return tex;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
406
1121
|
|
|
407
1122
|
}
|
|
408
1123
|
|
|
@@ -445,36 +1160,62 @@ function updateCamera(camera: THREE.Camera, width: number, height: number) {
|
|
|
445
1160
|
|
|
446
1161
|
function buildVertexShader() {
|
|
447
1162
|
return `
|
|
448
|
-
|
|
449
1163
|
void main() {
|
|
450
|
-
|
|
451
1164
|
vUv = uv;
|
|
452
1165
|
|
|
1166
|
+
// SCROLLING LOGIC
|
|
1167
|
+
// Separate multipliers for wave, color, and flow offsets
|
|
1168
|
+
float waveOffset = -u_y_offset * u_y_offset_wave_multiplier;
|
|
1169
|
+
float colorOffset = -u_y_offset * u_y_offset_color_multiplier;
|
|
1170
|
+
float flowOffset = -u_y_offset * u_y_offset_flow_multiplier;
|
|
1171
|
+
|
|
1172
|
+
// 1. DISPLACEMENT (WAVES)
|
|
1173
|
+
// We add waveOffset to Y to scroll the wave pattern
|
|
453
1174
|
v_displacement_amount = cnoise( vec3(
|
|
454
1175
|
u_wave_frequency_x * position.x + u_time,
|
|
455
|
-
u_wave_frequency_y * position.y + u_time,
|
|
1176
|
+
u_wave_frequency_y * (position.y + waveOffset) + u_time,
|
|
456
1177
|
u_time
|
|
457
1178
|
));
|
|
458
|
-
|
|
459
|
-
vec3 color;
|
|
460
1179
|
|
|
461
|
-
//
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
1180
|
+
// 2. FLOW FIELD
|
|
1181
|
+
// Apply flow offset to scroll the flow field mask
|
|
1182
|
+
vec2 baseUv = vUv;
|
|
1183
|
+
baseUv.y += flowOffset / u_plane_height; // Scale to match wave speed
|
|
1184
|
+
vec2 flowUv = baseUv;
|
|
1185
|
+
|
|
1186
|
+
if (u_flow_enabled > 0.5) {
|
|
1187
|
+
if (u_flow_ease > 0.0 || u_flow_distortion_a > 0.0) {
|
|
1188
|
+
vec2 ppp = -1.0 + 2.0 * baseUv;
|
|
1189
|
+
ppp += 0.1 * cos((1.5 * u_flow_scale) * ppp.yx + 1.1 * u_time + vec2(0.1, 1.1));
|
|
1190
|
+
ppp += 0.1 * cos((2.3 * u_flow_scale) * ppp.yx + 1.3 * u_time + vec2(3.2, 3.4));
|
|
1191
|
+
ppp += 0.1 * cos((2.2 * u_flow_scale) * ppp.yx + 1.7 * u_time + vec2(1.8, 5.2));
|
|
1192
|
+
ppp += u_flow_distortion_a * cos((u_flow_distortion_b * u_flow_scale) * ppp.yx + 1.4 * u_time + vec2(6.3, 3.9));
|
|
1193
|
+
|
|
1194
|
+
float r = length(ppp);
|
|
1195
|
+
flowUv = mix(baseUv, vec2(baseUv.x * (1.0 - u_flow_ease) + r * u_flow_ease, baseUv.y), u_flow_ease);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// Pass the standard flow UV to fragment shader (for mouse/texture)
|
|
1200
|
+
vFlowUv = flowUv;
|
|
1201
|
+
|
|
1202
|
+
// 3. COLOR MIXING
|
|
1203
|
+
// We take the computed flow UVs and apply the color offset
|
|
1204
|
+
// Scale by plane height to match wave offset speed (world space vs UV space)
|
|
1205
|
+
vec3 color = u_colors[0].color;
|
|
1206
|
+
vec2 adjustedUv = flowUv;
|
|
1207
|
+
adjustedUv.y += colorOffset / u_plane_height; // Scroll the color mixing pattern
|
|
1208
|
+
|
|
1209
|
+
vec2 noise_cord = adjustedUv * u_color_pressure;
|
|
466
1210
|
const float minNoise = .0;
|
|
467
1211
|
const float maxNoise = .9;
|
|
468
|
-
|
|
1212
|
+
|
|
469
1213
|
for (int i = 1; i < u_colors_count; i++) {
|
|
470
|
-
|
|
471
|
-
if(u_colors[i].is_active == 1.0){
|
|
1214
|
+
if(u_colors[i].is_active > 0.5){
|
|
472
1215
|
float noiseFlow = (1. + float(i)) / 30.;
|
|
473
1216
|
float noiseSpeed = (1. + float(i)) * 0.11;
|
|
474
1217
|
float noiseSeed = 13. + float(i) * 7.;
|
|
475
|
-
|
|
476
|
-
int reverseIndex = u_colors_count - i;
|
|
477
|
-
|
|
1218
|
+
|
|
478
1219
|
float noise = snoise(
|
|
479
1220
|
vec3(
|
|
480
1221
|
noise_cord.x * u_color_pressure.x + u_time * noiseFlow * 2.,
|
|
@@ -482,25 +1223,17 @@ void main() {
|
|
|
482
1223
|
u_time * noiseSpeed
|
|
483
1224
|
) + noiseSeed
|
|
484
1225
|
) - (.1 * float(i)) + (.5 * u_color_blending);
|
|
485
|
-
|
|
1226
|
+
|
|
486
1227
|
noise = clamp(minNoise, maxNoise + float(i) * 0.02, noise);
|
|
487
|
-
|
|
488
|
-
color = mix(color, nextColor, smoothstep(0.0, u_color_blending, noise));
|
|
489
|
-
|
|
490
|
-
// vec3 colorOklab = oklab2rgb(color);
|
|
491
|
-
// vec3 nextColorOklab = oklab2rgb(nextColor);
|
|
492
|
-
// vec3 mixColor = mix(colorOklab, nextColorOklab, smoothstep(0.0, u_color_blending, noise));
|
|
493
|
-
// color = rgb2oklab(mixColor);
|
|
494
|
-
|
|
1228
|
+
color = mix(color, u_colors[i].color, smoothstep(0.0, u_color_blending, noise));
|
|
495
1229
|
}
|
|
496
|
-
|
|
497
1230
|
}
|
|
498
|
-
|
|
1231
|
+
|
|
499
1232
|
v_color = color;
|
|
500
|
-
|
|
1233
|
+
|
|
1234
|
+
// 4. VERTEX POSITION
|
|
501
1235
|
vec3 newPosition = position + normal * v_displacement_amount * u_wave_amplitude;
|
|
502
|
-
gl_Position = projectionMatrix * modelViewMatrix * vec4(
|
|
503
|
-
|
|
1236
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
|
|
504
1237
|
v_new_position = gl_Position;
|
|
505
1238
|
}
|
|
506
1239
|
`;
|
|
@@ -516,7 +1249,6 @@ float fbm(vec3 x) {
|
|
|
516
1249
|
float value = 0.0;
|
|
517
1250
|
float amplitude = 0.5;
|
|
518
1251
|
float frequency = 1.0;
|
|
519
|
-
|
|
520
1252
|
for (int i = 0; i < 4; i++) {
|
|
521
1253
|
value += amplitude * snoise(x * frequency);
|
|
522
1254
|
frequency *= 2.0;
|
|
@@ -524,37 +1256,74 @@ float fbm(vec3 x) {
|
|
|
524
1256
|
}
|
|
525
1257
|
return value;
|
|
526
1258
|
}
|
|
1259
|
+
|
|
1260
|
+
void main() {
|
|
1261
|
+
// MOUSE DISTORTION
|
|
1262
|
+
vec2 finalUv = vFlowUv;
|
|
527
1263
|
|
|
528
|
-
|
|
529
|
-
|
|
1264
|
+
if (u_mouse_distortion_strength > 0.0) {
|
|
1265
|
+
vec4 mouseColor = texture2D(u_mouse_texture, vUv);
|
|
1266
|
+
float mouseValue = mouseColor.r;
|
|
1267
|
+
|
|
1268
|
+
if (mouseValue > 0.001) {
|
|
1269
|
+
float distortionAmount = mouseValue * u_mouse_distortion_strength;
|
|
1270
|
+
vec2 mouseDisp = vec2(distortionAmount, distortionAmount);
|
|
1271
|
+
finalUv -= mouseDisp;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
530
1274
|
|
|
1275
|
+
vec3 baseColor;
|
|
1276
|
+
|
|
1277
|
+
if (u_enable_procedural_texture > 0.5) {
|
|
1278
|
+
// Calculate flow field distance for ease effect
|
|
1279
|
+
vec2 ppp = -1.0 + 2.0 * finalUv;
|
|
1280
|
+
ppp += 0.1 * cos((1.5 * u_flow_scale) * ppp.yx + 1.1 * u_time + vec2(0.1, 1.1));
|
|
1281
|
+
ppp += 0.1 * cos((2.3 * u_flow_scale) * ppp.yx + 1.3 * u_time + vec2(3.2, 3.4));
|
|
1282
|
+
ppp += 0.1 * cos((2.2 * u_flow_scale) * ppp.yx + 1.7 * u_time + vec2(1.8, 5.2));
|
|
1283
|
+
ppp += u_flow_distortion_a * cos((u_flow_distortion_b * u_flow_scale) * ppp.yx + 1.4 * u_time + vec2(6.3, 3.9));
|
|
1284
|
+
float r = length(ppp); // Flow distance
|
|
1285
|
+
|
|
1286
|
+
// Ease blending: 0 = topographic (flow), 1 = image (UV)
|
|
1287
|
+
float vx = (finalUv.x * u_texture_ease) + (r * (1.0 - u_texture_ease));
|
|
1288
|
+
float vy = (finalUv.y * u_texture_ease) + (0.0 * (1.0 - u_texture_ease));
|
|
1289
|
+
vec2 texUv = vec2(vx, vy);
|
|
1290
|
+
|
|
1291
|
+
// PARALLAX SCROLLING
|
|
1292
|
+
// We manually apply a smaller offset here to make the texture lag behind
|
|
1293
|
+
float parallaxFactor = 0.25; // 25% speed of the color mixing
|
|
1294
|
+
texUv.y -= (u_y_offset * u_y_offset_color_multiplier / u_plane_height) * parallaxFactor;
|
|
1295
|
+
|
|
1296
|
+
texUv *= 1.5; // Tiling scale
|
|
1297
|
+
|
|
1298
|
+
vec4 texSample = texture2D(u_procedural_texture, texUv);
|
|
1299
|
+
baseColor = texSample.rgb;
|
|
1300
|
+
} else {
|
|
1301
|
+
baseColor = v_color;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
vec3 color = baseColor;
|
|
1305
|
+
|
|
1306
|
+
// Post-processing
|
|
531
1307
|
color += pow(v_displacement_amount, 1.0) * u_highlights;
|
|
532
1308
|
color -= pow(1.0 - v_displacement_amount, 2.0) * u_shadows;
|
|
533
1309
|
color = saturation(color, 1.0 + u_saturation);
|
|
534
1310
|
color = color * u_brightness;
|
|
535
|
-
|
|
536
|
-
// Generate grain using fbm
|
|
537
|
-
vec2 noiseCoords = gl_FragCoord.xy / u_grain_scale;
|
|
538
1311
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
1312
|
+
// Grain
|
|
1313
|
+
vec2 noiseCoords = gl_FragCoord.xy / u_grain_scale;
|
|
1314
|
+
float grain = (u_grain_speed != 0.0) ? fbm(vec3(noiseCoords, u_time * u_grain_speed)) : fbm(vec3(noiseCoords, 0.0));
|
|
542
1315
|
|
|
543
|
-
// Center the grain around zero
|
|
544
1316
|
grain = grain * 0.5 + 0.5;
|
|
545
1317
|
grain -= 0.5;
|
|
546
|
-
|
|
547
|
-
// Apply grain intensity
|
|
1318
|
+
grain = (grain > u_grain_sparsity) ? grain : 0.0;
|
|
548
1319
|
grain *= u_grain_intensity;
|
|
549
1320
|
|
|
550
|
-
// Add grain to color
|
|
551
1321
|
color += vec3(grain);
|
|
552
|
-
|
|
553
|
-
gl_FragColor = vec4(color,1.0);
|
|
1322
|
+
|
|
1323
|
+
gl_FragColor = vec4(color, 1.0);
|
|
554
1324
|
}
|
|
555
|
-
`;
|
|
1325
|
+
`;
|
|
556
1326
|
}
|
|
557
|
-
|
|
558
1327
|
const buildUniforms = () => `
|
|
559
1328
|
precision highp float;
|
|
560
1329
|
|
|
@@ -565,6 +1334,7 @@ struct Color {
|
|
|
565
1334
|
};
|
|
566
1335
|
|
|
567
1336
|
uniform float u_grain_intensity;
|
|
1337
|
+
uniform float u_grain_sparsity;
|
|
568
1338
|
uniform float u_grain_scale;
|
|
569
1339
|
uniform float u_grain_speed;
|
|
570
1340
|
uniform float u_time;
|
|
@@ -586,10 +1356,34 @@ uniform float u_brightness;
|
|
|
586
1356
|
uniform float u_color_blending;
|
|
587
1357
|
|
|
588
1358
|
uniform int u_colors_count;
|
|
589
|
-
uniform Color u_colors[
|
|
1359
|
+
uniform Color u_colors[6];
|
|
590
1360
|
uniform vec2 u_resolution;
|
|
591
1361
|
|
|
1362
|
+
uniform float u_y_offset;
|
|
1363
|
+
uniform float u_y_offset_wave_multiplier;
|
|
1364
|
+
uniform float u_y_offset_color_multiplier;
|
|
1365
|
+
uniform float u_y_offset_flow_multiplier;
|
|
1366
|
+
|
|
1367
|
+
// Flow field uniforms
|
|
1368
|
+
uniform float u_flow_distortion_a;
|
|
1369
|
+
uniform float u_flow_distortion_b;
|
|
1370
|
+
uniform float u_flow_scale;
|
|
1371
|
+
uniform float u_flow_ease;
|
|
1372
|
+
uniform float u_flow_enabled;
|
|
1373
|
+
|
|
1374
|
+
// Mouse interaction uniforms
|
|
1375
|
+
uniform float u_mouse_distortion_strength;
|
|
1376
|
+
uniform float u_mouse_distortion_radius;
|
|
1377
|
+
uniform float u_mouse_darken;
|
|
1378
|
+
uniform sampler2D u_mouse_texture;
|
|
1379
|
+
|
|
1380
|
+
// Procedural texture uniforms
|
|
1381
|
+
uniform sampler2D u_procedural_texture;
|
|
1382
|
+
uniform float u_enable_procedural_texture;
|
|
1383
|
+
uniform float u_texture_ease;
|
|
1384
|
+
|
|
592
1385
|
varying vec2 vUv;
|
|
1386
|
+
varying vec2 vFlowUv;
|
|
593
1387
|
varying vec4 v_new_position;
|
|
594
1388
|
varying vec3 v_color;
|
|
595
1389
|
varying float v_displacement_amount;
|
|
@@ -598,69 +1392,55 @@ varying float v_displacement_amount;
|
|
|
598
1392
|
|
|
599
1393
|
const buildNoise = () => `
|
|
600
1394
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
1395
|
+
// 1. REPLACEMENT PERMUTE:
|
|
1396
|
+
// Uses a hash function (fract/sin) instead of a modular lookup table.
|
|
1397
|
+
vec4 permute(vec4 x) {
|
|
1398
|
+
return floor(fract(sin(x) * 43758.5453123) * 289.0);
|
|
604
1399
|
}
|
|
605
1400
|
|
|
606
|
-
|
|
607
|
-
{
|
|
608
|
-
return x - floor(x * (1.0 / 289.0)) * 289.0;
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
vec4 permute(vec4 x)
|
|
612
|
-
{
|
|
613
|
-
return mod289(((x*34.0)+1.0)*x);
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
vec4 taylorInvSqrt(vec4 r)
|
|
617
|
-
{
|
|
1401
|
+
// Taylor Inverse Sqrt
|
|
1402
|
+
vec4 taylorInvSqrt(vec4 r) {
|
|
618
1403
|
return 1.79284291400159 - 0.85373472095314 * r;
|
|
619
1404
|
}
|
|
620
1405
|
|
|
1406
|
+
// Fade function
|
|
621
1407
|
vec3 fade(vec3 t) {
|
|
622
1408
|
return t*t*t*(t*(t*6.0-15.0)+10.0);
|
|
623
1409
|
}
|
|
624
1410
|
|
|
625
|
-
|
|
626
|
-
{
|
|
1411
|
+
// 3D Simplex Noise
|
|
1412
|
+
float snoise(vec3 v) {
|
|
627
1413
|
const vec2 C = vec2(1.0/6.0, 1.0/3.0) ;
|
|
628
1414
|
const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
|
|
629
1415
|
|
|
630
|
-
// First corner
|
|
1416
|
+
// First corner
|
|
631
1417
|
vec3 i = floor(v + dot(v, C.yyy) );
|
|
632
1418
|
vec3 x0 = v - i + dot(i, C.xxx) ;
|
|
633
1419
|
|
|
634
|
-
// Other corners
|
|
1420
|
+
// Other corners
|
|
635
1421
|
vec3 g = step(x0.yzx, x0.xyz);
|
|
636
1422
|
vec3 l = 1.0 - g;
|
|
637
1423
|
vec3 i1 = min( g.xyz, l.zxy );
|
|
638
1424
|
vec3 i2 = max( g.xyz, l.zxy );
|
|
639
1425
|
|
|
640
|
-
// x0 = x0 - 0.0 + 0.0 * C.xxx;
|
|
641
|
-
// x1 = x0 - i1 + 1.0 * C.xxx;
|
|
642
|
-
// x2 = x0 - i2 + 2.0 * C.xxx;
|
|
643
|
-
// x3 = x0 - 1.0 + 3.0 * C.xxx;
|
|
644
1426
|
vec3 x1 = x0 - i1 + C.xxx;
|
|
645
|
-
vec3 x2 = x0 - i2 + C.yyy;
|
|
646
|
-
vec3 x3 = x0 - D.yyy;
|
|
1427
|
+
vec3 x2 = x0 - i2 + C.yyy;
|
|
1428
|
+
vec3 x3 = x0 - D.yyy;
|
|
647
1429
|
|
|
648
|
-
// Permutations
|
|
649
|
-
i = mod289(i);
|
|
1430
|
+
// Permutations
|
|
650
1431
|
vec4 p = permute( permute( permute(
|
|
651
1432
|
i.z + vec4(0.0, i1.z, i2.z, 1.0 ))
|
|
652
1433
|
+ i.y + vec4(0.0, i1.y, i2.y, 1.0 ))
|
|
653
1434
|
+ i.x + vec4(0.0, i1.x, i2.x, 1.0 ));
|
|
654
1435
|
|
|
655
|
-
// Gradients
|
|
656
|
-
// The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294)
|
|
1436
|
+
// Gradients
|
|
657
1437
|
float n_ = 0.142857142857; // 1.0/7.0
|
|
658
1438
|
vec3 ns = n_ * D.wyz - D.xzx;
|
|
659
1439
|
|
|
660
|
-
vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
|
|
1440
|
+
vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
|
|
661
1441
|
|
|
662
1442
|
vec4 x_ = floor(j * ns.z);
|
|
663
|
-
vec4 y_ = floor(j - 7.0 * x_ );
|
|
1443
|
+
vec4 y_ = floor(j - 7.0 * x_ );
|
|
664
1444
|
|
|
665
1445
|
vec4 x = x_ *ns.x + ns.yyyy;
|
|
666
1446
|
vec4 y = y_ *ns.x + ns.yyyy;
|
|
@@ -669,8 +1449,6 @@ float snoise(vec3 v)
|
|
|
669
1449
|
vec4 b0 = vec4( x.xy, y.xy );
|
|
670
1450
|
vec4 b1 = vec4( x.zw, y.zw );
|
|
671
1451
|
|
|
672
|
-
//vec4 s0 = vec4(lessThan(b0,0.0))*2.0 - 1.0;
|
|
673
|
-
//vec4 s1 = vec4(lessThan(b1,0.0))*2.0 - 1.0;
|
|
674
1452
|
vec4 s0 = floor(b0)*2.0 + 1.0;
|
|
675
1453
|
vec4 s1 = floor(b1)*2.0 + 1.0;
|
|
676
1454
|
vec4 sh = -step(h, vec4(0.0));
|
|
@@ -683,14 +1461,14 @@ float snoise(vec3 v)
|
|
|
683
1461
|
vec3 p2 = vec3(a1.xy,h.z);
|
|
684
1462
|
vec3 p3 = vec3(a1.zw,h.w);
|
|
685
1463
|
|
|
686
|
-
//Normalise gradients
|
|
1464
|
+
// Normalise gradients
|
|
687
1465
|
vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
|
|
688
1466
|
p0 *= norm.x;
|
|
689
1467
|
p1 *= norm.y;
|
|
690
1468
|
p2 *= norm.z;
|
|
691
1469
|
p3 *= norm.w;
|
|
692
1470
|
|
|
693
|
-
// Mix final noise value
|
|
1471
|
+
// Mix final noise value
|
|
694
1472
|
vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
|
|
695
1473
|
m = m * m;
|
|
696
1474
|
return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1),
|
|
@@ -700,12 +1478,11 @@ float snoise(vec3 v)
|
|
|
700
1478
|
// Classic Perlin noise
|
|
701
1479
|
float cnoise(vec3 P)
|
|
702
1480
|
{
|
|
703
|
-
vec3 Pi0 = floor(P);
|
|
704
|
-
vec3 Pi1 = Pi0 + vec3(1.0);
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
vec3
|
|
708
|
-
vec3 Pf1 = Pf0 - vec3(1.0); // Fractional part - 1.0
|
|
1481
|
+
vec3 Pi0 = floor(P);
|
|
1482
|
+
vec3 Pi1 = Pi0 + vec3(1.0);
|
|
1483
|
+
|
|
1484
|
+
vec3 Pf0 = fract(P);
|
|
1485
|
+
vec3 Pf1 = Pf0 - vec3(1.0);
|
|
709
1486
|
vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x);
|
|
710
1487
|
vec4 iy = vec4(Pi0.yy, Pi1.yy);
|
|
711
1488
|
vec4 iz0 = Pi0.zzzz;
|
|
@@ -766,47 +1543,7 @@ float cnoise(vec3 P)
|
|
|
766
1543
|
float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x);
|
|
767
1544
|
return 2.2 * n_xyz;
|
|
768
1545
|
}
|
|
769
|
-
|
|
770
|
-
// YUV to RGB matrix
|
|
771
|
-
mat3 yuv2rgb = mat3(1.0, 0.0, 1.13983,
|
|
772
|
-
1.0, -0.39465, -0.58060,
|
|
773
|
-
1.0, 2.03211, 0.0);
|
|
774
|
-
|
|
775
|
-
// RGB to YUV matrix
|
|
776
|
-
mat3 rgb2yuv = mat3(0.2126, 0.7152, 0.0722,
|
|
777
|
-
-0.09991, -0.33609, 0.43600,
|
|
778
|
-
0.615, -0.5586, -0.05639);
|
|
779
|
-
|
|
780
|
-
vec3 oklab2rgb(vec3 linear)
|
|
781
|
-
{
|
|
782
|
-
const mat3 im1 = mat3(0.4121656120, 0.2118591070, 0.0883097947,
|
|
783
|
-
0.5362752080, 0.6807189584, 0.2818474174,
|
|
784
|
-
0.0514575653, 0.1074065790, 0.6302613616);
|
|
785
|
-
|
|
786
|
-
const mat3 im2 = mat3(+0.2104542553, +1.9779984951, +0.0259040371,
|
|
787
|
-
+0.7936177850, -2.4285922050, +0.7827717662,
|
|
788
|
-
-0.0040720468, +0.4505937099, -0.8086757660);
|
|
789
|
-
|
|
790
|
-
vec3 lms = im1 * linear;
|
|
791
|
-
|
|
792
|
-
return im2 * (sign(lms) * pow(abs(lms), vec3(1.0/3.0)));
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
vec3 rgb2oklab(vec3 oklab)
|
|
796
|
-
{
|
|
797
|
-
const mat3 m1 = mat3(+1.000000000, +1.000000000, +1.000000000,
|
|
798
|
-
+0.396337777, -0.105561346, -0.089484178,
|
|
799
|
-
+0.215803757, -0.063854173, -1.291485548);
|
|
800
|
-
|
|
801
|
-
const mat3 m2 = mat3(+4.076724529, -1.268143773, -0.004111989,
|
|
802
|
-
-3.307216883, +2.609332323, -0.703476310,
|
|
803
|
-
+0.230759054, -0.341134429, +1.706862569);
|
|
804
|
-
vec3 lms = m1 * oklab;
|
|
805
|
-
|
|
806
|
-
return m2 * (lms * lms * lms);
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
`;
|
|
1546
|
+
`;
|
|
810
1547
|
|
|
811
1548
|
const buildColorFunctions = () => `
|
|
812
1549
|
|
|
@@ -853,7 +1590,6 @@ vec3 hsv2rgb(vec3 c)
|
|
|
853
1590
|
}
|
|
854
1591
|
`;
|
|
855
1592
|
|
|
856
|
-
|
|
857
1593
|
const setLinkStyles = (link: HTMLAnchorElement) => {
|
|
858
1594
|
link.id = LINK_ID;
|
|
859
1595
|
link.href = "https://neat.firecms.co";
|
|
@@ -873,19 +1609,20 @@ const setLinkStyles = (link: HTMLAnchorElement) => {
|
|
|
873
1609
|
link.innerHTML = "NEAT";
|
|
874
1610
|
}
|
|
875
1611
|
|
|
876
|
-
const addNeatLink = (ref: HTMLCanvasElement) => {
|
|
1612
|
+
const addNeatLink = (ref: HTMLCanvasElement): HTMLAnchorElement => {
|
|
877
1613
|
const existingLinks = ref.parentElement?.getElementsByTagName("a");
|
|
878
1614
|
if (existingLinks) {
|
|
879
1615
|
for (let i = 0; i < existingLinks.length; i++) {
|
|
880
1616
|
if (existingLinks[i].id === LINK_ID) {
|
|
881
1617
|
setLinkStyles(existingLinks[i]);
|
|
882
|
-
return;
|
|
1618
|
+
return existingLinks[i];
|
|
883
1619
|
}
|
|
884
1620
|
}
|
|
885
1621
|
}
|
|
886
1622
|
const link = document.createElement("a");
|
|
887
1623
|
setLinkStyles(link);
|
|
888
1624
|
ref.parentElement?.appendChild(link);
|
|
1625
|
+
return link;
|
|
889
1626
|
}
|
|
890
1627
|
|
|
891
1628
|
function getElapsedSecondsInLastHour() {
|
|
@@ -904,3 +1641,12 @@ function generateRandomString(length: number = 6): string {
|
|
|
904
1641
|
}
|
|
905
1642
|
return result;
|
|
906
1643
|
}
|
|
1644
|
+
|
|
1645
|
+
function downloadURI(uri: string, name: string) {
|
|
1646
|
+
const link = document.createElement("a");
|
|
1647
|
+
link.download = name;
|
|
1648
|
+
link.href = uri;
|
|
1649
|
+
document.body.appendChild(link);
|
|
1650
|
+
link.click();
|
|
1651
|
+
document.body.removeChild(link);
|
|
1652
|
+
}
|