@firecms/neat 0.4.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 +98 -0
- package/dist/NeatGradient.js +749 -187
- package/dist/NeatGradient.js.map +1 -1
- package/dist/index.es.js +570 -222
- 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 +902 -194
package/dist/NeatGradient.js
CHANGED
|
@@ -2,7 +2,7 @@ import * as THREE from "three";
|
|
|
2
2
|
const PLANE_WIDTH = 50;
|
|
3
3
|
const PLANE_HEIGHT = 80;
|
|
4
4
|
const WIREFRAME = true;
|
|
5
|
-
const COLORS_COUNT =
|
|
5
|
+
const COLORS_COUNT = 6;
|
|
6
6
|
const clock = new THREE.Clock();
|
|
7
7
|
const LINK_ID = generateRandomString();
|
|
8
8
|
export class NeatGradient {
|
|
@@ -26,12 +26,59 @@ export class NeatGradient {
|
|
|
26
26
|
_wireframe = false;
|
|
27
27
|
_backgroundColor = "#FFFFFF";
|
|
28
28
|
_backgroundAlpha = 1.0;
|
|
29
|
+
// Flow field properties
|
|
30
|
+
_flowDistortionA = 0;
|
|
31
|
+
_flowDistortionB = 0;
|
|
32
|
+
_flowScale = 1.0;
|
|
33
|
+
_flowEase = 0.0;
|
|
34
|
+
_flowEnabled = true;
|
|
35
|
+
// Mouse interaction properties
|
|
36
|
+
_mouseDistortionStrength = 0.0;
|
|
37
|
+
_mouseDistortionRadius = 0.25;
|
|
38
|
+
_mouseDecayRate = 0.96;
|
|
39
|
+
_mouseDarken = 0.0;
|
|
40
|
+
_mouse = new THREE.Vector2(-1000, -1000);
|
|
41
|
+
_mouseFBO = null;
|
|
42
|
+
_sceneMouse = null;
|
|
43
|
+
_cameraMouse = null;
|
|
44
|
+
_mouseObjects = [];
|
|
45
|
+
_currentBrush = 0;
|
|
46
|
+
_mouseBrushBaseScale = 1;
|
|
47
|
+
// Texture generation properties
|
|
48
|
+
_enableProceduralTexture = false;
|
|
49
|
+
_textureVoidLikelihood = 0.45;
|
|
50
|
+
_textureVoidWidthMin = 200;
|
|
51
|
+
_textureVoidWidthMax = 486;
|
|
52
|
+
_textureBandDensity = 2.15;
|
|
53
|
+
_textureColorBlending = 0.01;
|
|
54
|
+
_textureSeed = 333;
|
|
55
|
+
_textureEase = 0.5;
|
|
56
|
+
_proceduralTexture = null;
|
|
57
|
+
_proceduralBackgroundColor = "#000000";
|
|
58
|
+
_textureShapeTriangles = 20;
|
|
59
|
+
_textureShapeCircles = 15;
|
|
60
|
+
_textureShapeBars = 15;
|
|
61
|
+
_textureShapeSquiggles = 10;
|
|
29
62
|
requestRef = -1;
|
|
30
63
|
sizeObserver;
|
|
31
64
|
sceneState;
|
|
65
|
+
// Optimization: Cache uniforms to avoid lookups and object creation in render loop
|
|
66
|
+
_cachedUniforms = null;
|
|
67
|
+
_linkElement = null;
|
|
32
68
|
_yOffset = 0;
|
|
69
|
+
_yOffsetWaveMultiplier = 0.004;
|
|
70
|
+
_yOffsetColorMultiplier = 0.004;
|
|
71
|
+
_yOffsetFlowMultiplier = 0.004;
|
|
72
|
+
// For saving/restoring clear color
|
|
73
|
+
_tempClearColor = new THREE.Color();
|
|
33
74
|
constructor(config) {
|
|
34
|
-
const { ref, speed = 4, horizontalPressure = 3, verticalPressure = 3, waveFrequencyX = 5, waveFrequencyY = 5, waveAmplitude = 3, colors, highlights = 4, shadows = 4, colorSaturation = 0, colorBrightness = 1, colorBlending = 5, grainScale = 2, grainIntensity = 0.55, grainSparsity = 0.0, grainSpeed = 0.1, wireframe = false, backgroundColor = "#FFFFFF", backgroundAlpha = 1.0, resolution = 1, seed, yOffset = 0
|
|
75
|
+
const { ref, speed = 4, horizontalPressure = 3, verticalPressure = 3, waveFrequencyX = 5, waveFrequencyY = 5, waveAmplitude = 3, colors, highlights = 4, shadows = 4, colorSaturation = 0, colorBrightness = 1, colorBlending = 5, grainScale = 2, grainIntensity = 0.55, grainSparsity = 0.0, grainSpeed = 0.1, wireframe = false, backgroundColor = "#FFFFFF", backgroundAlpha = 1.0, resolution = 1, seed, yOffset = 0, yOffsetWaveMultiplier = 4, yOffsetColorMultiplier = 4, yOffsetFlowMultiplier = 4,
|
|
76
|
+
// Flow field parameters
|
|
77
|
+
flowDistortionA = 0, flowDistortionB = 0, flowScale = 1.0, flowEase = 0.0, flowEnabled = true,
|
|
78
|
+
// Mouse interaction
|
|
79
|
+
mouseDistortionStrength = 0.0, mouseDistortionRadius = 0.25, mouseDecayRate = 0.96, mouseDarken = 0.0,
|
|
80
|
+
// Texture generation
|
|
81
|
+
enableProceduralTexture = false, textureVoidLikelihood = 0.45, textureVoidWidthMin = 200, textureVoidWidthMax = 486, textureBandDensity = 2.15, textureColorBlending = 0.01, textureSeed = 333, textureEase = 0.5, proceduralBackgroundColor = "#000000", textureShapeTriangles = 20, textureShapeCircles = 15, textureShapeBars = 15, textureShapeSquiggles = 10, } = config;
|
|
35
82
|
this._ref = ref;
|
|
36
83
|
this.destroy = this.destroy.bind(this);
|
|
37
84
|
this._initScene = this._initScene.bind(this);
|
|
@@ -56,75 +103,141 @@ export class NeatGradient {
|
|
|
56
103
|
this.backgroundColor = backgroundColor;
|
|
57
104
|
this.backgroundAlpha = backgroundAlpha;
|
|
58
105
|
this.yOffset = yOffset;
|
|
106
|
+
this.yOffsetWaveMultiplier = yOffsetWaveMultiplier;
|
|
107
|
+
this.yOffsetColorMultiplier = yOffsetColorMultiplier;
|
|
108
|
+
this.yOffsetFlowMultiplier = yOffsetFlowMultiplier;
|
|
109
|
+
// Flow field
|
|
110
|
+
this.flowDistortionA = flowDistortionA;
|
|
111
|
+
this.flowDistortionB = flowDistortionB;
|
|
112
|
+
this.flowScale = flowScale;
|
|
113
|
+
this.flowEase = flowEase;
|
|
114
|
+
this.flowEnabled = flowEnabled;
|
|
115
|
+
// Mouse interaction
|
|
116
|
+
this.mouseDistortionStrength = mouseDistortionStrength;
|
|
117
|
+
this.mouseDistortionRadius = mouseDistortionRadius;
|
|
118
|
+
this.mouseDecayRate = mouseDecayRate;
|
|
119
|
+
this.mouseDarken = mouseDarken;
|
|
120
|
+
// Texture generation
|
|
121
|
+
this.enableProceduralTexture = enableProceduralTexture;
|
|
122
|
+
this.textureVoidLikelihood = textureVoidLikelihood;
|
|
123
|
+
this.textureVoidWidthMin = textureVoidWidthMin;
|
|
124
|
+
this.textureVoidWidthMax = textureVoidWidthMax;
|
|
125
|
+
this.textureBandDensity = textureBandDensity;
|
|
126
|
+
this.textureColorBlending = textureColorBlending;
|
|
127
|
+
this.textureSeed = textureSeed;
|
|
128
|
+
this.textureEase = textureEase;
|
|
129
|
+
this._proceduralBackgroundColor = proceduralBackgroundColor;
|
|
130
|
+
this._textureShapeTriangles = textureShapeTriangles;
|
|
131
|
+
this._textureShapeCircles = textureShapeCircles;
|
|
132
|
+
this._textureShapeBars = textureShapeBars;
|
|
133
|
+
this._textureShapeSquiggles = textureShapeSquiggles;
|
|
134
|
+
// FIX 1: Setup mouse resources BEFORE building the material/scene
|
|
135
|
+
// This ensures u_mouse_texture isn't null during material compilation
|
|
136
|
+
this._setupMouseInteraction();
|
|
59
137
|
this.sceneState = this._initScene(resolution);
|
|
60
138
|
let tick = seed !== undefined ? seed : getElapsedSecondsInLastHour();
|
|
61
139
|
const render = () => {
|
|
62
|
-
const { renderer, camera, scene
|
|
140
|
+
const { renderer, camera, scene } = this.sceneState;
|
|
141
|
+
// Optimization: check if cached link is still valid in DOM, otherwise search
|
|
63
142
|
if (Math.floor(tick * 10) % 5 === 0) {
|
|
64
|
-
|
|
143
|
+
if (!this._linkElement || !document.contains(this._linkElement)) {
|
|
144
|
+
this._linkElement = addNeatLink(ref);
|
|
145
|
+
}
|
|
65
146
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
const colors = [
|
|
70
|
-
...this._colors.map(color => {
|
|
71
|
-
let threeColor = new THREE.Color();
|
|
72
|
-
threeColor.setStyle(color.color, "");
|
|
73
|
-
return ({
|
|
74
|
-
is_active: color.enabled,
|
|
75
|
-
color: threeColor,
|
|
76
|
-
influence: color.influence
|
|
77
|
-
});
|
|
78
|
-
}),
|
|
79
|
-
...Array.from({ length: COLORS_COUNT - this._colors.length }).map(() => ({
|
|
80
|
-
is_active: false,
|
|
81
|
-
color: new THREE.Color(0x000000)
|
|
82
|
-
}))
|
|
83
|
-
];
|
|
147
|
+
// Update Uniforms efficiently without creating new objects
|
|
148
|
+
if (this._cachedUniforms) {
|
|
149
|
+
const u = this._cachedUniforms;
|
|
84
150
|
tick += clock.getDelta() * this._speed;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
151
|
+
u.u_time.value = tick;
|
|
152
|
+
u.u_resolution.value.set(this._ref.width, this._ref.height);
|
|
153
|
+
u.u_color_pressure.value.set(this._horizontalPressure, this._verticalPressure);
|
|
154
|
+
// Directly assign simple values
|
|
155
|
+
u.u_wave_frequency_x.value = this._waveFrequencyX;
|
|
156
|
+
u.u_wave_frequency_y.value = this._waveFrequencyY;
|
|
157
|
+
u.u_wave_amplitude.value = this._waveAmplitude;
|
|
158
|
+
u.u_color_blending.value = this._colorBlending;
|
|
159
|
+
u.u_shadows.value = this._shadows;
|
|
160
|
+
u.u_highlights.value = this._highlights;
|
|
161
|
+
u.u_saturation.value = this._saturation;
|
|
162
|
+
u.u_brightness.value = this._brightness;
|
|
163
|
+
u.u_grain_intensity.value = this._grainIntensity;
|
|
164
|
+
u.u_grain_sparsity.value = this._grainSparsity;
|
|
165
|
+
u.u_grain_speed.value = this._grainSpeed;
|
|
166
|
+
u.u_grain_scale.value = this._grainScale;
|
|
167
|
+
u.u_y_offset.value = this._yOffset;
|
|
168
|
+
u.u_y_offset_wave_multiplier.value = this._yOffsetWaveMultiplier;
|
|
169
|
+
u.u_y_offset_color_multiplier.value = this._yOffsetColorMultiplier;
|
|
170
|
+
u.u_y_offset_flow_multiplier.value = this._yOffsetFlowMultiplier;
|
|
171
|
+
u.u_flow_distortion_a.value = this._flowDistortionA;
|
|
172
|
+
u.u_flow_distortion_b.value = this._flowDistortionB;
|
|
173
|
+
u.u_flow_scale.value = this._flowScale;
|
|
174
|
+
u.u_flow_ease.value = this._flowEase;
|
|
175
|
+
u.u_flow_enabled.value = this._flowEnabled ? 1.0 : 0.0;
|
|
176
|
+
u.u_mouse_distortion_strength.value = this._mouseDistortionStrength;
|
|
177
|
+
u.u_mouse_distortion_radius.value = this._mouseDistortionRadius;
|
|
178
|
+
u.u_mouse_darken.value = this._mouseDarken;
|
|
179
|
+
u.u_enable_procedural_texture.value = this._enableProceduralTexture ? 1.0 : 0.0;
|
|
180
|
+
u.u_procedural_texture.value = this._proceduralTexture;
|
|
181
|
+
u.u_texture_ease.value = this._textureEase;
|
|
182
|
+
// Optimized Color Update: Update the existing array objects instead of recreating array
|
|
183
|
+
const shaderColors = u.u_colors.value;
|
|
184
|
+
for (let i = 0; i < COLORS_COUNT; i++) {
|
|
185
|
+
if (i < this._colors.length) {
|
|
186
|
+
const c = this._colors[i];
|
|
187
|
+
shaderColors[i].is_active = c.enabled ? 1.0 : 0.0;
|
|
188
|
+
shaderColors[i].color.setStyle(c.color, "");
|
|
189
|
+
shaderColors[i].influence = c.influence || 0;
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
shaderColors[i].is_active = 0.0;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
u.u_colors_count.value = COLORS_COUNT;
|
|
196
|
+
// Wireframe is a material property, not a uniform
|
|
197
|
+
// @ts-ignore - access material safely
|
|
198
|
+
this.sceneState.meshes[0].material.wireframe = this._wireframe;
|
|
199
|
+
}
|
|
200
|
+
// Render mouse interaction to FBO
|
|
201
|
+
if (this._mouseFBO && this._sceneMouse && this._cameraMouse) {
|
|
202
|
+
let hasActiveBrushes = false;
|
|
203
|
+
// Update mouse objects - decay rate controls how fast trails fade
|
|
204
|
+
for (let i = 0; i < this._mouseObjects.length; i++) {
|
|
205
|
+
const obj = this._mouseObjects[i];
|
|
206
|
+
if (obj.mesh.visible) {
|
|
207
|
+
hasActiveBrushes = true;
|
|
208
|
+
obj.mesh.rotation.z += 0.01;
|
|
209
|
+
if (obj.mesh.material instanceof THREE.MeshBasicMaterial) {
|
|
210
|
+
// Decay only affects opacity
|
|
211
|
+
obj.mesh.material.opacity *= this._mouseDecayRate;
|
|
212
|
+
if (obj.mesh.material.opacity < 0.01) {
|
|
213
|
+
obj.mesh.visible = false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// FIX 2: Handle FBO Clearing correctly
|
|
219
|
+
// Store current clear color (likely the main background color)
|
|
220
|
+
renderer.getClearColor(this._tempClearColor);
|
|
221
|
+
const oldClearAlpha = renderer.getClearAlpha();
|
|
222
|
+
// Set clear color to Black/Transparent for the FBO.
|
|
223
|
+
// Important: If we use the main background color (e.g. White), the FBO
|
|
224
|
+
// will be white, causing 100% distortion everywhere.
|
|
225
|
+
renderer.setClearColor(0x000000, 0.0);
|
|
226
|
+
renderer.setRenderTarget(this._mouseFBO);
|
|
227
|
+
renderer.clear();
|
|
228
|
+
if (hasActiveBrushes) {
|
|
229
|
+
renderer.render(this._sceneMouse, this._cameraMouse);
|
|
230
|
+
}
|
|
231
|
+
renderer.setRenderTarget(null);
|
|
232
|
+
// Restore main background color for the actual scene render
|
|
233
|
+
renderer.setClearColor(this._tempClearColor, oldClearAlpha);
|
|
234
|
+
// Update mouse texture uniform
|
|
235
|
+
if (this._cachedUniforms) {
|
|
236
|
+
this._cachedUniforms.u_mouse_texture.value = this._mouseFBO.texture;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// Ensure we set the clear color for the main scene explicitly before rendering
|
|
240
|
+
renderer.setClearColor(this._backgroundColor, this._backgroundAlpha);
|
|
128
241
|
renderer.render(scene, camera);
|
|
129
242
|
this.requestRef = requestAnimationFrame(render);
|
|
130
243
|
};
|
|
@@ -135,6 +248,18 @@ export class NeatGradient {
|
|
|
135
248
|
const height = canvas.clientHeight;
|
|
136
249
|
this.sceneState.renderer.setSize(width, height, false);
|
|
137
250
|
updateCamera(this.sceneState.camera, width, height);
|
|
251
|
+
// FIX 3: Update Mouse FBO and Camera on resize
|
|
252
|
+
// If we don't do this, mouse coordinates map incorrectly after a resize
|
|
253
|
+
if (this._mouseFBO && this._cameraMouse) {
|
|
254
|
+
const fSize = height / 2;
|
|
255
|
+
const aspect = width / height;
|
|
256
|
+
this._mouseFBO.setSize(width / 2, height / 2);
|
|
257
|
+
this._cameraMouse.left = -fSize * aspect;
|
|
258
|
+
this._cameraMouse.right = fSize * aspect;
|
|
259
|
+
this._cameraMouse.top = fSize;
|
|
260
|
+
this._cameraMouse.bottom = -fSize;
|
|
261
|
+
this._cameraMouse.updateProjectionMatrix();
|
|
262
|
+
}
|
|
138
263
|
};
|
|
139
264
|
this.sizeObserver = new ResizeObserver(entries => {
|
|
140
265
|
setSize();
|
|
@@ -146,6 +271,21 @@ export class NeatGradient {
|
|
|
146
271
|
if (this) {
|
|
147
272
|
cancelAnimationFrame(this.requestRef);
|
|
148
273
|
this.sizeObserver.disconnect();
|
|
274
|
+
// Cleanup WebGL resources
|
|
275
|
+
if (this.sceneState) {
|
|
276
|
+
this.sceneState.renderer.dispose();
|
|
277
|
+
this.sceneState.meshes.forEach(m => {
|
|
278
|
+
m.geometry.dispose();
|
|
279
|
+
if (Array.isArray(m.material))
|
|
280
|
+
m.material.forEach(mat => mat.dispose());
|
|
281
|
+
else
|
|
282
|
+
m.material.dispose();
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
if (this._mouseFBO)
|
|
286
|
+
this._mouseFBO.dispose();
|
|
287
|
+
if (this._proceduralTexture)
|
|
288
|
+
this._proceduralTexture.dispose();
|
|
149
289
|
}
|
|
150
290
|
}
|
|
151
291
|
downloadAsPNG(filename = "neat.png") {
|
|
@@ -217,8 +357,152 @@ export class NeatGradient {
|
|
|
217
357
|
set yOffset(yOffset) {
|
|
218
358
|
this._yOffset = yOffset;
|
|
219
359
|
}
|
|
360
|
+
get yOffsetWaveMultiplier() {
|
|
361
|
+
return this._yOffsetWaveMultiplier * 1000;
|
|
362
|
+
}
|
|
363
|
+
set yOffsetWaveMultiplier(value) {
|
|
364
|
+
this._yOffsetWaveMultiplier = value / 1000;
|
|
365
|
+
}
|
|
366
|
+
get yOffsetColorMultiplier() {
|
|
367
|
+
return this._yOffsetColorMultiplier * 1000;
|
|
368
|
+
}
|
|
369
|
+
set yOffsetColorMultiplier(value) {
|
|
370
|
+
this._yOffsetColorMultiplier = value / 1000;
|
|
371
|
+
}
|
|
372
|
+
get yOffsetFlowMultiplier() {
|
|
373
|
+
return this._yOffsetFlowMultiplier * 1000;
|
|
374
|
+
}
|
|
375
|
+
set yOffsetFlowMultiplier(value) {
|
|
376
|
+
this._yOffsetFlowMultiplier = value / 1000;
|
|
377
|
+
}
|
|
378
|
+
set flowDistortionA(value) {
|
|
379
|
+
this._flowDistortionA = value;
|
|
380
|
+
}
|
|
381
|
+
set flowDistortionB(value) {
|
|
382
|
+
this._flowDistortionB = value;
|
|
383
|
+
}
|
|
384
|
+
set flowScale(value) {
|
|
385
|
+
this._flowScale = value;
|
|
386
|
+
}
|
|
387
|
+
set flowEase(value) {
|
|
388
|
+
this._flowEase = value;
|
|
389
|
+
}
|
|
390
|
+
set flowEnabled(value) {
|
|
391
|
+
this._flowEnabled = value;
|
|
392
|
+
}
|
|
393
|
+
get flowEnabled() {
|
|
394
|
+
return this._flowEnabled;
|
|
395
|
+
}
|
|
396
|
+
set mouseDistortionStrength(value) {
|
|
397
|
+
this._mouseDistortionStrength = Math.max(0, value);
|
|
398
|
+
}
|
|
399
|
+
set mouseDistortionRadius(value) {
|
|
400
|
+
// Clamp to a sane range in UV space
|
|
401
|
+
this._mouseDistortionRadius = Math.max(0.01, Math.min(value, 1.0));
|
|
402
|
+
// Update brush scale when radius changes
|
|
403
|
+
this._updateBrushScale();
|
|
404
|
+
}
|
|
405
|
+
_updateBrushScale() {
|
|
406
|
+
if (!this._mouseObjects || this._mouseObjects.length === 0)
|
|
407
|
+
return;
|
|
408
|
+
// Radius directly controls the brush scale
|
|
409
|
+
// Base geometry is 200px, so radius 0.25 = 50px, radius 1.0 = 200px
|
|
410
|
+
this._mouseBrushBaseScale = this._mouseDistortionRadius;
|
|
411
|
+
}
|
|
412
|
+
set mouseDecayRate(value) {
|
|
413
|
+
// Clamp between 0.9 (slow decay, more wobble) and 0.99 (fast decay, less wobble)
|
|
414
|
+
this._mouseDecayRate = Math.max(0.9, Math.min(value, 0.99));
|
|
415
|
+
}
|
|
416
|
+
set mouseDarken(value) {
|
|
417
|
+
this._mouseDarken = value;
|
|
418
|
+
}
|
|
419
|
+
set enableProceduralTexture(value) {
|
|
420
|
+
this._enableProceduralTexture = value;
|
|
421
|
+
if (value && !this._proceduralTexture) {
|
|
422
|
+
this._proceduralTexture = this._createProceduralTexture();
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
set textureVoidLikelihood(value) {
|
|
426
|
+
this._textureVoidLikelihood = value;
|
|
427
|
+
if (this._enableProceduralTexture) {
|
|
428
|
+
this._proceduralTexture = this._createProceduralTexture();
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
set textureVoidWidthMin(value) {
|
|
432
|
+
this._textureVoidWidthMin = value;
|
|
433
|
+
if (this._enableProceduralTexture) {
|
|
434
|
+
this._proceduralTexture = this._createProceduralTexture();
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
set textureVoidWidthMax(value) {
|
|
438
|
+
this._textureVoidWidthMax = value;
|
|
439
|
+
if (this._enableProceduralTexture) {
|
|
440
|
+
this._proceduralTexture = this._createProceduralTexture();
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
set textureBandDensity(value) {
|
|
444
|
+
this._textureBandDensity = value;
|
|
445
|
+
if (this._enableProceduralTexture) {
|
|
446
|
+
this._proceduralTexture = this._createProceduralTexture();
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
set textureColorBlending(value) {
|
|
450
|
+
this._textureColorBlending = value;
|
|
451
|
+
if (this._enableProceduralTexture) {
|
|
452
|
+
this._proceduralTexture = this._createProceduralTexture();
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
set textureSeed(value) {
|
|
456
|
+
this._textureSeed = value;
|
|
457
|
+
if (this._enableProceduralTexture) {
|
|
458
|
+
this._proceduralTexture = this._createProceduralTexture();
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
get textureEase() {
|
|
462
|
+
return this._textureEase;
|
|
463
|
+
}
|
|
464
|
+
set textureEase(value) {
|
|
465
|
+
this._textureEase = value;
|
|
466
|
+
}
|
|
467
|
+
set proceduralBackgroundColor(value) {
|
|
468
|
+
this._proceduralBackgroundColor = value;
|
|
469
|
+
if (this._enableProceduralTexture) {
|
|
470
|
+
this._proceduralTexture = this._createProceduralTexture();
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
set textureShapeTriangles(value) {
|
|
474
|
+
this._textureShapeTriangles = value;
|
|
475
|
+
if (this._enableProceduralTexture)
|
|
476
|
+
this._proceduralTexture = this._createProceduralTexture();
|
|
477
|
+
}
|
|
478
|
+
set textureShapeCircles(value) {
|
|
479
|
+
this._textureShapeCircles = value;
|
|
480
|
+
if (this._enableProceduralTexture)
|
|
481
|
+
this._proceduralTexture = this._createProceduralTexture();
|
|
482
|
+
}
|
|
483
|
+
set textureShapeBars(value) {
|
|
484
|
+
this._textureShapeBars = value;
|
|
485
|
+
if (this._enableProceduralTexture)
|
|
486
|
+
this._proceduralTexture = this._createProceduralTexture();
|
|
487
|
+
}
|
|
488
|
+
set textureShapeSquiggles(value) {
|
|
489
|
+
this._textureShapeSquiggles = value;
|
|
490
|
+
if (this._enableProceduralTexture)
|
|
491
|
+
this._proceduralTexture = this._createProceduralTexture();
|
|
492
|
+
}
|
|
220
493
|
_initScene(resolution) {
|
|
221
494
|
const width = this._ref.width, height = this._ref.height;
|
|
495
|
+
// Cleanup existing renderer if needed
|
|
496
|
+
if (this.sceneState && this.sceneState.renderer) {
|
|
497
|
+
this.sceneState.renderer.dispose();
|
|
498
|
+
this.sceneState.meshes.forEach(m => {
|
|
499
|
+
m.geometry.dispose();
|
|
500
|
+
if (Array.isArray(m.material))
|
|
501
|
+
m.material.forEach(mat => mat.dispose());
|
|
502
|
+
else
|
|
503
|
+
m.material.dispose();
|
|
504
|
+
});
|
|
505
|
+
}
|
|
222
506
|
const renderer = new THREE.WebGLRenderer({
|
|
223
507
|
// antialias: true,
|
|
224
508
|
alpha: true,
|
|
@@ -249,17 +533,13 @@ export class NeatGradient {
|
|
|
249
533
|
};
|
|
250
534
|
}
|
|
251
535
|
_buildMaterial(width, height) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
is_active: false,
|
|
260
|
-
color: new THREE.Color(0x000000)
|
|
261
|
-
}))
|
|
262
|
-
];
|
|
536
|
+
// Initialize stable array structure for colors
|
|
537
|
+
// We create 6 objects and just update them in the render loop to avoid GC
|
|
538
|
+
const colors = Array.from({ length: COLORS_COUNT }).map((_, i) => ({
|
|
539
|
+
is_active: i < this._colors.length ? (this._colors[i].enabled ? 1.0 : 0.0) : 0.0,
|
|
540
|
+
color: new THREE.Color(i < this._colors.length ? this._colors[i].color : 0x000000),
|
|
541
|
+
influence: i < this._colors.length ? (this._colors[i].influence || 0) : 0
|
|
542
|
+
}));
|
|
263
543
|
const uniforms = {
|
|
264
544
|
u_time: { value: 0 },
|
|
265
545
|
u_color_pressure: { value: new THREE.Vector2(this._horizontalPressure, this._verticalPressure) },
|
|
@@ -277,15 +557,272 @@ export class NeatGradient {
|
|
|
277
557
|
u_grain_sparsity: { value: this._grainSparsity },
|
|
278
558
|
u_grain_scale: { value: this._grainScale },
|
|
279
559
|
u_grain_speed: { value: this._grainSpeed },
|
|
560
|
+
// Flow field
|
|
561
|
+
u_flow_distortion_a: { value: this._flowDistortionA },
|
|
562
|
+
u_flow_distortion_b: { value: this._flowDistortionB },
|
|
563
|
+
u_flow_scale: { value: this._flowScale },
|
|
564
|
+
u_flow_ease: { value: this._flowEase },
|
|
565
|
+
u_flow_enabled: { value: this._flowEnabled ? 1.0 : 0.0 },
|
|
566
|
+
// Y offset multipliers
|
|
567
|
+
u_y_offset: { value: this._yOffset },
|
|
568
|
+
u_y_offset_wave_multiplier: { value: this._yOffsetWaveMultiplier },
|
|
569
|
+
u_y_offset_color_multiplier: { value: this._yOffsetColorMultiplier },
|
|
570
|
+
u_y_offset_flow_multiplier: { value: this._yOffsetFlowMultiplier },
|
|
571
|
+
// Mouse interaction
|
|
572
|
+
u_mouse_distortion_strength: { value: this._mouseDistortionStrength },
|
|
573
|
+
u_mouse_distortion_radius: { value: this._mouseDistortionRadius },
|
|
574
|
+
u_mouse_darken: { value: this._mouseDarken },
|
|
575
|
+
u_mouse_texture: { value: this._mouseFBO ? this._mouseFBO.texture : null },
|
|
576
|
+
// Procedural texture
|
|
577
|
+
u_procedural_texture: { value: this._proceduralTexture },
|
|
578
|
+
u_enable_procedural_texture: { value: this._enableProceduralTexture ? 1.0 : 0.0 },
|
|
579
|
+
u_texture_ease: { value: this._textureEase },
|
|
580
|
+
u_saturation: { value: this._saturation },
|
|
581
|
+
u_brightness: { value: this._brightness },
|
|
582
|
+
u_color_blending: { value: this._colorBlending }
|
|
280
583
|
};
|
|
281
584
|
const material = new THREE.ShaderMaterial({
|
|
282
585
|
uniforms: uniforms,
|
|
283
586
|
vertexShader: buildUniforms() + buildNoise() + buildColorFunctions() + buildVertexShader(),
|
|
284
587
|
fragmentShader: buildUniforms() + buildColorFunctions() + buildNoise() + buildFragmentShader()
|
|
285
588
|
});
|
|
589
|
+
// Cache the uniforms object for direct access in render loop
|
|
590
|
+
this._cachedUniforms = uniforms;
|
|
286
591
|
material.wireframe = WIREFRAME;
|
|
287
592
|
return material;
|
|
288
593
|
}
|
|
594
|
+
_setupMouseInteraction() {
|
|
595
|
+
if (!this._ref)
|
|
596
|
+
return;
|
|
597
|
+
const width = this._ref.width;
|
|
598
|
+
const height = this._ref.height;
|
|
599
|
+
// Create mouse FBO
|
|
600
|
+
this._mouseFBO = new THREE.WebGLRenderTarget(width / 2, height / 2);
|
|
601
|
+
// Create mouse scene and camera
|
|
602
|
+
this._sceneMouse = new THREE.Scene();
|
|
603
|
+
const fSize = height / 2;
|
|
604
|
+
const aspect = width / height;
|
|
605
|
+
// FIX 4: Ensure near plane allows viewing objects at Z=0
|
|
606
|
+
// Near -100 is safer for objects at 0
|
|
607
|
+
this._cameraMouse = new THREE.OrthographicCamera(-fSize * aspect, fSize * aspect, fSize, -fSize, 0, 10000);
|
|
608
|
+
this._cameraMouse.position.set(0, 0, 100);
|
|
609
|
+
// Create brush texture - More visible and impactful
|
|
610
|
+
const brushCanvas = document.createElement('canvas');
|
|
611
|
+
brushCanvas.width = 128;
|
|
612
|
+
brushCanvas.height = 128;
|
|
613
|
+
const bCtx = brushCanvas.getContext('2d');
|
|
614
|
+
if (bCtx) {
|
|
615
|
+
const grd = bCtx.createRadialGradient(64, 64, 0, 64, 64, 64);
|
|
616
|
+
// Match reference implementation's stronger gradient
|
|
617
|
+
grd.addColorStop(0, 'rgba(255,255,255,0.8)');
|
|
618
|
+
grd.addColorStop(0.5, 'rgba(255,255,255,0.4)');
|
|
619
|
+
grd.addColorStop(1, 'rgba(255,255,255,0)');
|
|
620
|
+
bCtx.fillStyle = grd;
|
|
621
|
+
bCtx.fillRect(0, 0, 128, 128);
|
|
622
|
+
}
|
|
623
|
+
const brushTex = new THREE.CanvasTexture(brushCanvas);
|
|
624
|
+
const brushMat = new THREE.MeshBasicMaterial({
|
|
625
|
+
map: brushTex,
|
|
626
|
+
transparent: true,
|
|
627
|
+
opacity: 1.0,
|
|
628
|
+
depthTest: false,
|
|
629
|
+
blending: THREE.AdditiveBlending // Additive blending for better accumulation
|
|
630
|
+
});
|
|
631
|
+
// Brush geometry size - will be scaled by radius parameter
|
|
632
|
+
const brushGeo = new THREE.PlaneGeometry(200, 200);
|
|
633
|
+
// Create brush pool
|
|
634
|
+
const brushPoolSize = 50;
|
|
635
|
+
for (let i = 0; i < brushPoolSize; i++) {
|
|
636
|
+
const m = new THREE.Mesh(brushGeo, brushMat.clone());
|
|
637
|
+
m.visible = false;
|
|
638
|
+
this._sceneMouse.add(m);
|
|
639
|
+
this._mouseObjects.push({ mesh: m, active: false });
|
|
640
|
+
}
|
|
641
|
+
// Initialize brush scale based on current radius
|
|
642
|
+
this._updateBrushScale();
|
|
643
|
+
// Add mouse move listener
|
|
644
|
+
this._ref.addEventListener('mousemove', this._onMouseMove.bind(this));
|
|
645
|
+
}
|
|
646
|
+
_onMouseMove(e) {
|
|
647
|
+
if (!this._ref || !this._sceneMouse)
|
|
648
|
+
return;
|
|
649
|
+
const rect = this._ref.getBoundingClientRect();
|
|
650
|
+
const width = this._ref.width;
|
|
651
|
+
const height = this._ref.height;
|
|
652
|
+
this._mouse.x = e.clientX - rect.left - width / 2;
|
|
653
|
+
this._mouse.y = -(e.clientY - rect.top - height / 2);
|
|
654
|
+
const brush = this._mouseObjects[this._currentBrush];
|
|
655
|
+
brush.mesh.scale.set(this._mouseBrushBaseScale, this._mouseBrushBaseScale, 1.0);
|
|
656
|
+
brush.active = true;
|
|
657
|
+
brush.mesh.visible = true;
|
|
658
|
+
brush.mesh.position.set(this._mouse.x, this._mouse.y, 0);
|
|
659
|
+
brush.mesh.rotation.z = Math.random() * Math.PI * 2;
|
|
660
|
+
if (brush.mesh.material instanceof THREE.MeshBasicMaterial) {
|
|
661
|
+
brush.mesh.material.opacity = 1.0;
|
|
662
|
+
}
|
|
663
|
+
this._currentBrush = (this._currentBrush + 1) % this._mouseObjects.length;
|
|
664
|
+
}
|
|
665
|
+
_createProceduralTexture() {
|
|
666
|
+
// Texture size - 1024 provides good balance between quality and performance
|
|
667
|
+
// Can be increased to 2048 for even better quality if needed
|
|
668
|
+
const texSize = 1024;
|
|
669
|
+
const sourceCanvas = document.createElement('canvas');
|
|
670
|
+
sourceCanvas.width = texSize;
|
|
671
|
+
sourceCanvas.height = texSize;
|
|
672
|
+
const sCtx = sourceCanvas.getContext('2d');
|
|
673
|
+
if (!sCtx)
|
|
674
|
+
return new THREE.Texture();
|
|
675
|
+
let seed = this._textureSeed;
|
|
676
|
+
const baseSeed = this._textureSeed;
|
|
677
|
+
function random() {
|
|
678
|
+
const x = Math.sin(seed++) * 10000;
|
|
679
|
+
return x - Math.floor(x);
|
|
680
|
+
}
|
|
681
|
+
// Helper to reset seed for isolated shape generation
|
|
682
|
+
const setSeed = (offset) => {
|
|
683
|
+
seed = baseSeed + offset;
|
|
684
|
+
};
|
|
685
|
+
const colors = this._colors.filter(c => c.enabled).map(c => c.color);
|
|
686
|
+
if (colors.length === 0)
|
|
687
|
+
return new THREE.Texture();
|
|
688
|
+
// Helper functions
|
|
689
|
+
function hexToRgb(hex) {
|
|
690
|
+
const bigint = parseInt(hex.replace('#', ''), 16);
|
|
691
|
+
return {
|
|
692
|
+
r: (bigint >> 16) & 255,
|
|
693
|
+
g: (bigint >> 8) & 255,
|
|
694
|
+
b: bigint & 255
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
function rgbToHex(r, g, b) {
|
|
698
|
+
return "#" + ((1 << 24) + (Math.round(r) << 16) + (Math.round(g) << 8) + Math.round(b)).toString(16).slice(1);
|
|
699
|
+
}
|
|
700
|
+
const getInterColor = () => {
|
|
701
|
+
const c1 = colors[Math.floor(random() * colors.length)];
|
|
702
|
+
const c2 = colors[Math.floor(random() * colors.length)];
|
|
703
|
+
const mix = random() * this._textureColorBlending;
|
|
704
|
+
const rgb1 = hexToRgb(c1);
|
|
705
|
+
const rgb2 = hexToRgb(c2);
|
|
706
|
+
const r = rgb1.r + (rgb2.r - rgb1.r) * mix;
|
|
707
|
+
const g = rgb1.g + (rgb2.g - rgb1.g) * mix;
|
|
708
|
+
const b = rgb1.b + (rgb2.b - rgb1.b) * mix;
|
|
709
|
+
return rgbToHex(r, g, b);
|
|
710
|
+
};
|
|
711
|
+
// === SOURCE CANVAS ===
|
|
712
|
+
// Base with procedural background color so even sparse areas pick it up
|
|
713
|
+
const baseColor = this._proceduralBackgroundColor || "#000000";
|
|
714
|
+
sCtx.fillStyle = baseColor;
|
|
715
|
+
sCtx.fillRect(0, 0, texSize, texSize);
|
|
716
|
+
// Then lay a vertical gradient of mixed colors on top for richness
|
|
717
|
+
const bgGrad = sCtx.createLinearGradient(0, 0, 0, texSize);
|
|
718
|
+
bgGrad.addColorStop(0, getInterColor());
|
|
719
|
+
bgGrad.addColorStop(1, getInterColor());
|
|
720
|
+
sCtx.fillStyle = bgGrad;
|
|
721
|
+
sCtx.fillRect(0, 0, texSize, texSize);
|
|
722
|
+
// Triangles: use configurable count
|
|
723
|
+
for (let i = 0; i < this._textureShapeTriangles; i++) {
|
|
724
|
+
sCtx.fillStyle = getInterColor();
|
|
725
|
+
sCtx.beginPath();
|
|
726
|
+
const x = random() * texSize;
|
|
727
|
+
const y = random() * texSize;
|
|
728
|
+
const s = 100 + random() * 300;
|
|
729
|
+
sCtx.moveTo(x, y);
|
|
730
|
+
sCtx.lineTo(x + (random() - 0.5) * s, y + (random() - 0.5) * s);
|
|
731
|
+
sCtx.lineTo(x + (random() - 0.5) * s, y + (random() - 0.5) * s);
|
|
732
|
+
sCtx.fill();
|
|
733
|
+
}
|
|
734
|
+
// Circles / rings: use configurable count
|
|
735
|
+
for (let i = 0; i < this._textureShapeCircles; i++) {
|
|
736
|
+
sCtx.strokeStyle = getInterColor();
|
|
737
|
+
sCtx.lineWidth = 10 + random() * 50;
|
|
738
|
+
sCtx.beginPath();
|
|
739
|
+
const x = random() * texSize;
|
|
740
|
+
const y = random() * texSize;
|
|
741
|
+
const r = 50 + random() * 150;
|
|
742
|
+
sCtx.arc(x, y, r, 0, Math.PI * 2);
|
|
743
|
+
sCtx.stroke();
|
|
744
|
+
}
|
|
745
|
+
// Bars: use configurable count
|
|
746
|
+
for (let i = 0; i < this._textureShapeBars; i++) {
|
|
747
|
+
sCtx.fillStyle = getInterColor();
|
|
748
|
+
sCtx.save();
|
|
749
|
+
sCtx.translate(random() * texSize, random() * texSize);
|
|
750
|
+
sCtx.rotate(random() * Math.PI);
|
|
751
|
+
sCtx.fillRect(-150, -25, 300, 50);
|
|
752
|
+
sCtx.restore();
|
|
753
|
+
}
|
|
754
|
+
// Squiggles: use configurable count
|
|
755
|
+
sCtx.lineWidth = 15;
|
|
756
|
+
sCtx.lineCap = 'round';
|
|
757
|
+
for (let i = 0; i < this._textureShapeSquiggles; i++) {
|
|
758
|
+
sCtx.strokeStyle = getInterColor();
|
|
759
|
+
sCtx.beginPath();
|
|
760
|
+
let x = random() * texSize;
|
|
761
|
+
let y = random() * texSize;
|
|
762
|
+
sCtx.moveTo(x, y);
|
|
763
|
+
for (let j = 0; j < 4; j++) {
|
|
764
|
+
sCtx.bezierCurveTo(x + (random() - 0.5) * 300, y + (random() - 0.5) * 300, x + (random() - 0.5) * 300, y + (random() - 0.5) * 300, x + (random() - 0.5) * 300, y + (random() - 0.5) * 300);
|
|
765
|
+
x += (random() - 0.5) * 300;
|
|
766
|
+
y += (random() - 0.5) * 300;
|
|
767
|
+
}
|
|
768
|
+
sCtx.stroke();
|
|
769
|
+
}
|
|
770
|
+
// === MASKED CANVAS ===
|
|
771
|
+
// Masking: Seed isolation
|
|
772
|
+
setSeed(50000);
|
|
773
|
+
const canvas = document.createElement('canvas');
|
|
774
|
+
canvas.width = texSize;
|
|
775
|
+
canvas.height = texSize;
|
|
776
|
+
const ctx = canvas.getContext('2d');
|
|
777
|
+
if (!ctx)
|
|
778
|
+
return new THREE.Texture();
|
|
779
|
+
// Start filled with the chosen void color so gaps show that color
|
|
780
|
+
ctx.fillStyle = baseColor;
|
|
781
|
+
ctx.fillRect(0, 0, texSize, texSize);
|
|
782
|
+
// Determine layout segments (matter vs void)
|
|
783
|
+
let layoutHead = 0;
|
|
784
|
+
const segments = [];
|
|
785
|
+
while (layoutHead < texSize) {
|
|
786
|
+
const isVoid = random() < this._textureVoidLikelihood;
|
|
787
|
+
if (isVoid) {
|
|
788
|
+
const w = this._textureVoidWidthMin + random() * (this._textureVoidWidthMax - this._textureVoidWidthMin);
|
|
789
|
+
segments.push({ type: 'void', x: layoutHead, width: w });
|
|
790
|
+
layoutHead += w;
|
|
791
|
+
}
|
|
792
|
+
else {
|
|
793
|
+
const w = 50 + random() * 200;
|
|
794
|
+
segments.push({ type: 'matter', x: layoutHead, width: w });
|
|
795
|
+
layoutHead += w;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
// Render only matter bands from the source into the masked canvas
|
|
799
|
+
for (const seg of segments) {
|
|
800
|
+
if (seg.type === 'matter') {
|
|
801
|
+
const startX = seg.x;
|
|
802
|
+
const endX = Math.min(seg.x + seg.width, texSize);
|
|
803
|
+
let currentX = startX;
|
|
804
|
+
while (currentX < endX) {
|
|
805
|
+
const stripeWidth = (2 + random() * 20) / this._textureBandDensity;
|
|
806
|
+
const sourceX = Math.floor(random() * texSize);
|
|
807
|
+
ctx.drawImage(sourceCanvas, sourceX, 0, stripeWidth, texSize, currentX, 0, stripeWidth, texSize);
|
|
808
|
+
currentX += stripeWidth;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
// void segments: leave as baseColor
|
|
812
|
+
}
|
|
813
|
+
const tex = new THREE.CanvasTexture(canvas);
|
|
814
|
+
// Use mipmapping for better quality when texture is scaled
|
|
815
|
+
tex.minFilter = THREE.LinearMipmapLinearFilter;
|
|
816
|
+
tex.magFilter = THREE.LinearFilter;
|
|
817
|
+
tex.wrapS = THREE.RepeatWrapping;
|
|
818
|
+
tex.wrapT = THREE.RepeatWrapping;
|
|
819
|
+
// Enable anisotropic filtering for much better quality when texture is stretched
|
|
820
|
+
// 16 is a commonly supported value that dramatically improves quality
|
|
821
|
+
tex.anisotropy = 16;
|
|
822
|
+
// Ensure mipmaps are generated
|
|
823
|
+
tex.needsUpdate = true;
|
|
824
|
+
return tex;
|
|
825
|
+
}
|
|
289
826
|
}
|
|
290
827
|
function updateCamera(camera, width, height) {
|
|
291
828
|
const viewPortAreaRatio = 1000000;
|
|
@@ -317,41 +854,62 @@ function updateCamera(camera, width, height) {
|
|
|
317
854
|
}
|
|
318
855
|
function buildVertexShader() {
|
|
319
856
|
return `
|
|
320
|
-
|
|
321
857
|
void main() {
|
|
322
|
-
|
|
323
858
|
vUv = uv;
|
|
324
859
|
|
|
860
|
+
// SCROLLING LOGIC
|
|
861
|
+
// Separate multipliers for wave, color, and flow offsets
|
|
862
|
+
float waveOffset = -u_y_offset * u_y_offset_wave_multiplier;
|
|
863
|
+
float colorOffset = -u_y_offset * u_y_offset_color_multiplier;
|
|
864
|
+
float flowOffset = -u_y_offset * u_y_offset_flow_multiplier;
|
|
865
|
+
|
|
866
|
+
// 1. DISPLACEMENT (WAVES)
|
|
867
|
+
// We add waveOffset to Y to scroll the wave pattern
|
|
325
868
|
v_displacement_amount = cnoise( vec3(
|
|
326
869
|
u_wave_frequency_x * position.x + u_time,
|
|
327
|
-
u_wave_frequency_y * position.y + u_time,
|
|
870
|
+
u_wave_frequency_y * (position.y + waveOffset) + u_time,
|
|
328
871
|
u_time
|
|
329
872
|
));
|
|
330
873
|
|
|
331
|
-
|
|
874
|
+
// 2. FLOW FIELD
|
|
875
|
+
// Apply flow offset to scroll the flow field mask
|
|
876
|
+
vec2 baseUv = vUv;
|
|
877
|
+
baseUv.y += flowOffset / u_plane_height; // Scale to match wave speed
|
|
878
|
+
vec2 flowUv = baseUv;
|
|
879
|
+
|
|
880
|
+
if (u_flow_enabled > 0.5) {
|
|
881
|
+
if (u_flow_ease > 0.0 || u_flow_distortion_a > 0.0) {
|
|
882
|
+
vec2 ppp = -1.0 + 2.0 * baseUv;
|
|
883
|
+
ppp += 0.1 * cos((1.5 * u_flow_scale) * ppp.yx + 1.1 * u_time + vec2(0.1, 1.1));
|
|
884
|
+
ppp += 0.1 * cos((2.3 * u_flow_scale) * ppp.yx + 1.3 * u_time + vec2(3.2, 3.4));
|
|
885
|
+
ppp += 0.1 * cos((2.2 * u_flow_scale) * ppp.yx + 1.7 * u_time + vec2(1.8, 5.2));
|
|
886
|
+
ppp += u_flow_distortion_a * cos((u_flow_distortion_b * u_flow_scale) * ppp.yx + 1.4 * u_time + vec2(6.3, 3.9));
|
|
887
|
+
|
|
888
|
+
float r = length(ppp);
|
|
889
|
+
flowUv = mix(baseUv, vec2(baseUv.x * (1.0 - u_flow_ease) + r * u_flow_ease, baseUv.y), u_flow_ease);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
332
892
|
|
|
333
|
-
//
|
|
334
|
-
|
|
893
|
+
// Pass the standard flow UV to fragment shader (for mouse/texture)
|
|
894
|
+
vFlowUv = flowUv;
|
|
335
895
|
|
|
336
|
-
//
|
|
337
|
-
|
|
338
|
-
//
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
896
|
+
// 3. COLOR MIXING
|
|
897
|
+
// We take the computed flow UVs and apply the color offset
|
|
898
|
+
// Scale by plane height to match wave offset speed (world space vs UV space)
|
|
899
|
+
vec3 color = u_colors[0].color;
|
|
900
|
+
vec2 adjustedUv = flowUv;
|
|
901
|
+
adjustedUv.y += colorOffset / u_plane_height; // Scroll the color mixing pattern
|
|
342
902
|
|
|
903
|
+
vec2 noise_cord = adjustedUv * u_color_pressure;
|
|
343
904
|
const float minNoise = .0;
|
|
344
905
|
const float maxNoise = .9;
|
|
345
906
|
|
|
346
907
|
for (int i = 1; i < u_colors_count; i++) {
|
|
347
|
-
|
|
348
|
-
if(u_colors[i].is_active == 1.0){
|
|
908
|
+
if(u_colors[i].is_active > 0.5){
|
|
349
909
|
float noiseFlow = (1. + float(i)) / 30.;
|
|
350
910
|
float noiseSpeed = (1. + float(i)) * 0.11;
|
|
351
911
|
float noiseSeed = 13. + float(i) * 7.;
|
|
352
912
|
|
|
353
|
-
int reverseIndex = u_colors_count - i;
|
|
354
|
-
|
|
355
913
|
float noise = snoise(
|
|
356
914
|
vec3(
|
|
357
915
|
noise_cord.x * u_color_pressure.x + u_time * noiseFlow * 2.,
|
|
@@ -361,16 +919,15 @@ void main() {
|
|
|
361
919
|
) - (.1 * float(i)) + (.5 * u_color_blending);
|
|
362
920
|
|
|
363
921
|
noise = clamp(minNoise, maxNoise + float(i) * 0.02, noise);
|
|
364
|
-
|
|
365
|
-
color = mix(color, nextColor, smoothstep(0.0, u_color_blending, noise));
|
|
922
|
+
color = mix(color, u_colors[i].color, smoothstep(0.0, u_color_blending, noise));
|
|
366
923
|
}
|
|
367
924
|
}
|
|
368
925
|
|
|
369
926
|
v_color = color;
|
|
370
927
|
|
|
928
|
+
// 4. VERTEX POSITION
|
|
371
929
|
vec3 newPosition = position + normal * v_displacement_amount * u_wave_amplitude;
|
|
372
930
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
|
|
373
|
-
|
|
374
931
|
v_new_position = gl_Position;
|
|
375
932
|
}
|
|
376
933
|
`;
|
|
@@ -394,27 +951,66 @@ float fbm(vec3 x) {
|
|
|
394
951
|
}
|
|
395
952
|
|
|
396
953
|
void main() {
|
|
397
|
-
|
|
954
|
+
// MOUSE DISTORTION
|
|
955
|
+
vec2 finalUv = vFlowUv;
|
|
956
|
+
|
|
957
|
+
if (u_mouse_distortion_strength > 0.0) {
|
|
958
|
+
vec4 mouseColor = texture2D(u_mouse_texture, vUv);
|
|
959
|
+
float mouseValue = mouseColor.r;
|
|
960
|
+
|
|
961
|
+
if (mouseValue > 0.001) {
|
|
962
|
+
float distortionAmount = mouseValue * u_mouse_distortion_strength;
|
|
963
|
+
vec2 mouseDisp = vec2(distortionAmount, distortionAmount);
|
|
964
|
+
finalUv -= mouseDisp;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
vec3 baseColor;
|
|
969
|
+
|
|
970
|
+
if (u_enable_procedural_texture > 0.5) {
|
|
971
|
+
// Calculate flow field distance for ease effect
|
|
972
|
+
vec2 ppp = -1.0 + 2.0 * finalUv;
|
|
973
|
+
ppp += 0.1 * cos((1.5 * u_flow_scale) * ppp.yx + 1.1 * u_time + vec2(0.1, 1.1));
|
|
974
|
+
ppp += 0.1 * cos((2.3 * u_flow_scale) * ppp.yx + 1.3 * u_time + vec2(3.2, 3.4));
|
|
975
|
+
ppp += 0.1 * cos((2.2 * u_flow_scale) * ppp.yx + 1.7 * u_time + vec2(1.8, 5.2));
|
|
976
|
+
ppp += u_flow_distortion_a * cos((u_flow_distortion_b * u_flow_scale) * ppp.yx + 1.4 * u_time + vec2(6.3, 3.9));
|
|
977
|
+
float r = length(ppp); // Flow distance
|
|
978
|
+
|
|
979
|
+
// Ease blending: 0 = topographic (flow), 1 = image (UV)
|
|
980
|
+
float vx = (finalUv.x * u_texture_ease) + (r * (1.0 - u_texture_ease));
|
|
981
|
+
float vy = (finalUv.y * u_texture_ease) + (0.0 * (1.0 - u_texture_ease));
|
|
982
|
+
vec2 texUv = vec2(vx, vy);
|
|
983
|
+
|
|
984
|
+
// PARALLAX SCROLLING
|
|
985
|
+
// We manually apply a smaller offset here to make the texture lag behind
|
|
986
|
+
float parallaxFactor = 0.25; // 25% speed of the color mixing
|
|
987
|
+
texUv.y -= (u_y_offset * u_y_offset_color_multiplier / u_plane_height) * parallaxFactor;
|
|
988
|
+
|
|
989
|
+
texUv *= 1.5; // Tiling scale
|
|
990
|
+
|
|
991
|
+
vec4 texSample = texture2D(u_procedural_texture, texUv);
|
|
992
|
+
baseColor = texSample.rgb;
|
|
993
|
+
} else {
|
|
994
|
+
baseColor = v_color;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
vec3 color = baseColor;
|
|
998
|
+
|
|
999
|
+
// Post-processing
|
|
398
1000
|
color += pow(v_displacement_amount, 1.0) * u_highlights;
|
|
399
1001
|
color -= pow(1.0 - v_displacement_amount, 2.0) * u_shadows;
|
|
400
1002
|
color = saturation(color, 1.0 + u_saturation);
|
|
401
1003
|
color = color * u_brightness;
|
|
402
1004
|
|
|
403
|
-
//
|
|
1005
|
+
// Grain
|
|
404
1006
|
vec2 noiseCoords = gl_FragCoord.xy / u_grain_scale;
|
|
405
1007
|
float grain = (u_grain_speed != 0.0) ? fbm(vec3(noiseCoords, u_time * u_grain_speed)) : fbm(vec3(noiseCoords, 0.0));
|
|
406
1008
|
|
|
407
|
-
// Center the grain around zero
|
|
408
1009
|
grain = grain * 0.5 + 0.5;
|
|
409
1010
|
grain -= 0.5;
|
|
410
|
-
|
|
411
|
-
// Add sparsity control
|
|
412
1011
|
grain = (grain > u_grain_sparsity) ? grain : 0.0;
|
|
413
|
-
|
|
414
|
-
// Apply grain intensity
|
|
415
1012
|
grain *= u_grain_intensity;
|
|
416
1013
|
|
|
417
|
-
// Add grain to color
|
|
418
1014
|
color += vec3(grain);
|
|
419
1015
|
|
|
420
1016
|
gl_FragColor = vec4(color, 1.0);
|
|
@@ -453,12 +1049,34 @@ uniform float u_brightness;
|
|
|
453
1049
|
uniform float u_color_blending;
|
|
454
1050
|
|
|
455
1051
|
uniform int u_colors_count;
|
|
456
|
-
uniform Color u_colors[
|
|
1052
|
+
uniform Color u_colors[6];
|
|
457
1053
|
uniform vec2 u_resolution;
|
|
458
1054
|
|
|
459
1055
|
uniform float u_y_offset;
|
|
1056
|
+
uniform float u_y_offset_wave_multiplier;
|
|
1057
|
+
uniform float u_y_offset_color_multiplier;
|
|
1058
|
+
uniform float u_y_offset_flow_multiplier;
|
|
1059
|
+
|
|
1060
|
+
// Flow field uniforms
|
|
1061
|
+
uniform float u_flow_distortion_a;
|
|
1062
|
+
uniform float u_flow_distortion_b;
|
|
1063
|
+
uniform float u_flow_scale;
|
|
1064
|
+
uniform float u_flow_ease;
|
|
1065
|
+
uniform float u_flow_enabled;
|
|
1066
|
+
|
|
1067
|
+
// Mouse interaction uniforms
|
|
1068
|
+
uniform float u_mouse_distortion_strength;
|
|
1069
|
+
uniform float u_mouse_distortion_radius;
|
|
1070
|
+
uniform float u_mouse_darken;
|
|
1071
|
+
uniform sampler2D u_mouse_texture;
|
|
1072
|
+
|
|
1073
|
+
// Procedural texture uniforms
|
|
1074
|
+
uniform sampler2D u_procedural_texture;
|
|
1075
|
+
uniform float u_enable_procedural_texture;
|
|
1076
|
+
uniform float u_texture_ease;
|
|
460
1077
|
|
|
461
1078
|
varying vec2 vUv;
|
|
1079
|
+
varying vec2 vFlowUv;
|
|
462
1080
|
varying vec4 v_new_position;
|
|
463
1081
|
varying vec3 v_color;
|
|
464
1082
|
varying float v_displacement_amount;
|
|
@@ -466,69 +1084,55 @@ varying float v_displacement_amount;
|
|
|
466
1084
|
`;
|
|
467
1085
|
const buildNoise = () => `
|
|
468
1086
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
vec4 mod289(vec4 x)
|
|
475
|
-
{
|
|
476
|
-
return x - floor(x * (1.0 / 289.0)) * 289.0;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
vec4 permute(vec4 x)
|
|
480
|
-
{
|
|
481
|
-
return mod289(((x*34.0)+1.0)*x);
|
|
1087
|
+
// 1. REPLACEMENT PERMUTE:
|
|
1088
|
+
// Uses a hash function (fract/sin) instead of a modular lookup table.
|
|
1089
|
+
vec4 permute(vec4 x) {
|
|
1090
|
+
return floor(fract(sin(x) * 43758.5453123) * 289.0);
|
|
482
1091
|
}
|
|
483
1092
|
|
|
484
|
-
|
|
485
|
-
{
|
|
1093
|
+
// Taylor Inverse Sqrt
|
|
1094
|
+
vec4 taylorInvSqrt(vec4 r) {
|
|
486
1095
|
return 1.79284291400159 - 0.85373472095314 * r;
|
|
487
1096
|
}
|
|
488
1097
|
|
|
1098
|
+
// Fade function
|
|
489
1099
|
vec3 fade(vec3 t) {
|
|
490
1100
|
return t*t*t*(t*(t*6.0-15.0)+10.0);
|
|
491
1101
|
}
|
|
492
1102
|
|
|
493
|
-
|
|
494
|
-
{
|
|
1103
|
+
// 3D Simplex Noise
|
|
1104
|
+
float snoise(vec3 v) {
|
|
495
1105
|
const vec2 C = vec2(1.0/6.0, 1.0/3.0) ;
|
|
496
1106
|
const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
|
|
497
1107
|
|
|
498
|
-
// First corner
|
|
1108
|
+
// First corner
|
|
499
1109
|
vec3 i = floor(v + dot(v, C.yyy) );
|
|
500
1110
|
vec3 x0 = v - i + dot(i, C.xxx) ;
|
|
501
1111
|
|
|
502
|
-
// Other corners
|
|
1112
|
+
// Other corners
|
|
503
1113
|
vec3 g = step(x0.yzx, x0.xyz);
|
|
504
1114
|
vec3 l = 1.0 - g;
|
|
505
1115
|
vec3 i1 = min( g.xyz, l.zxy );
|
|
506
1116
|
vec3 i2 = max( g.xyz, l.zxy );
|
|
507
1117
|
|
|
508
|
-
// x0 = x0 - 0.0 + 0.0 * C.xxx;
|
|
509
|
-
// x1 = x0 - i1 + 1.0 * C.xxx;
|
|
510
|
-
// x2 = x0 - i2 + 2.0 * C.xxx;
|
|
511
|
-
// x3 = x0 - 1.0 + 3.0 * C.xxx;
|
|
512
1118
|
vec3 x1 = x0 - i1 + C.xxx;
|
|
513
|
-
vec3 x2 = x0 - i2 + C.yyy;
|
|
514
|
-
vec3 x3 = x0 - D.yyy;
|
|
1119
|
+
vec3 x2 = x0 - i2 + C.yyy;
|
|
1120
|
+
vec3 x3 = x0 - D.yyy;
|
|
515
1121
|
|
|
516
|
-
// Permutations
|
|
517
|
-
i = mod289(i);
|
|
1122
|
+
// Permutations
|
|
518
1123
|
vec4 p = permute( permute( permute(
|
|
519
1124
|
i.z + vec4(0.0, i1.z, i2.z, 1.0 ))
|
|
520
1125
|
+ i.y + vec4(0.0, i1.y, i2.y, 1.0 ))
|
|
521
1126
|
+ i.x + vec4(0.0, i1.x, i2.x, 1.0 ));
|
|
522
1127
|
|
|
523
|
-
// Gradients
|
|
524
|
-
// The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294)
|
|
1128
|
+
// Gradients
|
|
525
1129
|
float n_ = 0.142857142857; // 1.0/7.0
|
|
526
1130
|
vec3 ns = n_ * D.wyz - D.xzx;
|
|
527
1131
|
|
|
528
|
-
vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
|
|
1132
|
+
vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
|
|
529
1133
|
|
|
530
1134
|
vec4 x_ = floor(j * ns.z);
|
|
531
|
-
vec4 y_ = floor(j - 7.0 * x_ );
|
|
1135
|
+
vec4 y_ = floor(j - 7.0 * x_ );
|
|
532
1136
|
|
|
533
1137
|
vec4 x = x_ *ns.x + ns.yyyy;
|
|
534
1138
|
vec4 y = y_ *ns.x + ns.yyyy;
|
|
@@ -537,8 +1141,6 @@ float snoise(vec3 v)
|
|
|
537
1141
|
vec4 b0 = vec4( x.xy, y.xy );
|
|
538
1142
|
vec4 b1 = vec4( x.zw, y.zw );
|
|
539
1143
|
|
|
540
|
-
//vec4 s0 = vec4(lessThan(b0,0.0))*2.0 - 1.0;
|
|
541
|
-
//vec4 s1 = vec4(lessThan(b1,0.0))*2.0 - 1.0;
|
|
542
1144
|
vec4 s0 = floor(b0)*2.0 + 1.0;
|
|
543
1145
|
vec4 s1 = floor(b1)*2.0 + 1.0;
|
|
544
1146
|
vec4 sh = -step(h, vec4(0.0));
|
|
@@ -551,14 +1153,14 @@ float snoise(vec3 v)
|
|
|
551
1153
|
vec3 p2 = vec3(a1.xy,h.z);
|
|
552
1154
|
vec3 p3 = vec3(a1.zw,h.w);
|
|
553
1155
|
|
|
554
|
-
//Normalise gradients
|
|
1156
|
+
// Normalise gradients
|
|
555
1157
|
vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
|
|
556
1158
|
p0 *= norm.x;
|
|
557
1159
|
p1 *= norm.y;
|
|
558
1160
|
p2 *= norm.z;
|
|
559
1161
|
p3 *= norm.w;
|
|
560
1162
|
|
|
561
|
-
// Mix final noise value
|
|
1163
|
+
// Mix final noise value
|
|
562
1164
|
vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
|
|
563
1165
|
m = m * m;
|
|
564
1166
|
return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1),
|
|
@@ -568,12 +1170,11 @@ float snoise(vec3 v)
|
|
|
568
1170
|
// Classic Perlin noise
|
|
569
1171
|
float cnoise(vec3 P)
|
|
570
1172
|
{
|
|
571
|
-
vec3 Pi0 = floor(P);
|
|
572
|
-
vec3 Pi1 = Pi0 + vec3(1.0);
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
vec3
|
|
576
|
-
vec3 Pf1 = Pf0 - vec3(1.0); // Fractional part - 1.0
|
|
1173
|
+
vec3 Pi0 = floor(P);
|
|
1174
|
+
vec3 Pi1 = Pi0 + vec3(1.0);
|
|
1175
|
+
|
|
1176
|
+
vec3 Pf0 = fract(P);
|
|
1177
|
+
vec3 Pf1 = Pf0 - vec3(1.0);
|
|
577
1178
|
vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x);
|
|
578
1179
|
vec4 iy = vec4(Pi0.yy, Pi1.yy);
|
|
579
1180
|
vec4 iz0 = Pi0.zzzz;
|
|
@@ -634,47 +1235,7 @@ float cnoise(vec3 P)
|
|
|
634
1235
|
float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x);
|
|
635
1236
|
return 2.2 * n_xyz;
|
|
636
1237
|
}
|
|
637
|
-
|
|
638
|
-
// YUV to RGB matrix
|
|
639
|
-
mat3 yuv2rgb = mat3(1.0, 0.0, 1.13983,
|
|
640
|
-
1.0, -0.39465, -0.58060,
|
|
641
|
-
1.0, 2.03211, 0.0);
|
|
642
|
-
|
|
643
|
-
// RGB to YUV matrix
|
|
644
|
-
mat3 rgb2yuv = mat3(0.2126, 0.7152, 0.0722,
|
|
645
|
-
-0.09991, -0.33609, 0.43600,
|
|
646
|
-
0.615, -0.5586, -0.05639);
|
|
647
|
-
|
|
648
|
-
vec3 oklab2rgb(vec3 linear)
|
|
649
|
-
{
|
|
650
|
-
const mat3 im1 = mat3(0.4121656120, 0.2118591070, 0.0883097947,
|
|
651
|
-
0.5362752080, 0.6807189584, 0.2818474174,
|
|
652
|
-
0.0514575653, 0.1074065790, 0.6302613616);
|
|
653
|
-
|
|
654
|
-
const mat3 im2 = mat3(+0.2104542553, +1.9779984951, +0.0259040371,
|
|
655
|
-
+0.7936177850, -2.4285922050, +0.7827717662,
|
|
656
|
-
-0.0040720468, +0.4505937099, -0.8086757660);
|
|
657
|
-
|
|
658
|
-
vec3 lms = im1 * linear;
|
|
659
|
-
|
|
660
|
-
return im2 * (sign(lms) * pow(abs(lms), vec3(1.0/3.0)));
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
vec3 rgb2oklab(vec3 oklab)
|
|
664
|
-
{
|
|
665
|
-
const mat3 m1 = mat3(+1.000000000, +1.000000000, +1.000000000,
|
|
666
|
-
+0.396337777, -0.105561346, -0.089484178,
|
|
667
|
-
+0.215803757, -0.063854173, -1.291485548);
|
|
668
|
-
|
|
669
|
-
const mat3 m2 = mat3(+4.076724529, -1.268143773, -0.004111989,
|
|
670
|
-
-3.307216883, +2.609332323, -0.703476310,
|
|
671
|
-
+0.230759054, -0.341134429, +1.706862569);
|
|
672
|
-
vec3 lms = m1 * oklab;
|
|
673
|
-
|
|
674
|
-
return m2 * (lms * lms * lms);
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
`;
|
|
1238
|
+
`;
|
|
678
1239
|
const buildColorFunctions = () => `
|
|
679
1240
|
|
|
680
1241
|
vec3 saturation(vec3 rgb, float adjustment) {
|
|
@@ -743,13 +1304,14 @@ const addNeatLink = (ref) => {
|
|
|
743
1304
|
for (let i = 0; i < existingLinks.length; i++) {
|
|
744
1305
|
if (existingLinks[i].id === LINK_ID) {
|
|
745
1306
|
setLinkStyles(existingLinks[i]);
|
|
746
|
-
return;
|
|
1307
|
+
return existingLinks[i];
|
|
747
1308
|
}
|
|
748
1309
|
}
|
|
749
1310
|
}
|
|
750
1311
|
const link = document.createElement("a");
|
|
751
1312
|
setLinkStyles(link);
|
|
752
1313
|
ref.parentElement?.appendChild(link);
|
|
1314
|
+
return link;
|
|
753
1315
|
};
|
|
754
1316
|
function getElapsedSecondsInLastHour() {
|
|
755
1317
|
const now = new Date();
|