@firecms/neat 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +365 -68
- package/dist/NeatGradient.d.ts +105 -0
- package/dist/NeatGradient.js +855 -200
- package/dist/NeatGradient.js.map +1 -1
- package/dist/index.es.js +619 -235
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +139 -115
- package/dist/index.umd.js.map +1 -1
- package/package.json +3 -3
- package/src/NeatGradient.ts +1017 -204
package/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,67 @@ 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();
|
|
74
|
+
// Performance optimizations
|
|
75
|
+
_resizeTimeoutId = null;
|
|
76
|
+
_textureNeedsUpdate = false;
|
|
77
|
+
_lastColorUpdate = 0;
|
|
78
|
+
_linkCheckCounter = 0;
|
|
79
|
+
_mouseUpdateScheduled = false;
|
|
80
|
+
_pendingMousePosition = null;
|
|
81
|
+
_colorsChanged = true; // Track if colors need update
|
|
33
82
|
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
|
|
83
|
+
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,
|
|
84
|
+
// Flow field parameters
|
|
85
|
+
flowDistortionA = 0, flowDistortionB = 0, flowScale = 1.0, flowEase = 0.0, flowEnabled = true,
|
|
86
|
+
// Mouse interaction
|
|
87
|
+
mouseDistortionStrength = 0.0, mouseDistortionRadius = 0.25, mouseDecayRate = 0.96, mouseDarken = 0.0,
|
|
88
|
+
// Texture generation
|
|
89
|
+
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
90
|
this._ref = ref;
|
|
36
91
|
this.destroy = this.destroy.bind(this);
|
|
37
92
|
this._initScene = this._initScene.bind(this);
|
|
@@ -56,75 +111,155 @@ export class NeatGradient {
|
|
|
56
111
|
this.backgroundColor = backgroundColor;
|
|
57
112
|
this.backgroundAlpha = backgroundAlpha;
|
|
58
113
|
this.yOffset = yOffset;
|
|
114
|
+
this.yOffsetWaveMultiplier = yOffsetWaveMultiplier;
|
|
115
|
+
this.yOffsetColorMultiplier = yOffsetColorMultiplier;
|
|
116
|
+
this.yOffsetFlowMultiplier = yOffsetFlowMultiplier;
|
|
117
|
+
// Flow field
|
|
118
|
+
this.flowDistortionA = flowDistortionA;
|
|
119
|
+
this.flowDistortionB = flowDistortionB;
|
|
120
|
+
this.flowScale = flowScale;
|
|
121
|
+
this.flowEase = flowEase;
|
|
122
|
+
this.flowEnabled = flowEnabled;
|
|
123
|
+
// Mouse interaction
|
|
124
|
+
this.mouseDistortionStrength = mouseDistortionStrength;
|
|
125
|
+
this.mouseDistortionRadius = mouseDistortionRadius;
|
|
126
|
+
this.mouseDecayRate = mouseDecayRate;
|
|
127
|
+
this.mouseDarken = mouseDarken;
|
|
128
|
+
// Texture generation
|
|
129
|
+
this.enableProceduralTexture = enableProceduralTexture;
|
|
130
|
+
this.textureVoidLikelihood = textureVoidLikelihood;
|
|
131
|
+
this.textureVoidWidthMin = textureVoidWidthMin;
|
|
132
|
+
this.textureVoidWidthMax = textureVoidWidthMax;
|
|
133
|
+
this.textureBandDensity = textureBandDensity;
|
|
134
|
+
this.textureColorBlending = textureColorBlending;
|
|
135
|
+
this.textureSeed = textureSeed;
|
|
136
|
+
this.textureEase = textureEase;
|
|
137
|
+
this._proceduralBackgroundColor = proceduralBackgroundColor;
|
|
138
|
+
this._textureShapeTriangles = textureShapeTriangles;
|
|
139
|
+
this._textureShapeCircles = textureShapeCircles;
|
|
140
|
+
this._textureShapeBars = textureShapeBars;
|
|
141
|
+
this._textureShapeSquiggles = textureShapeSquiggles;
|
|
142
|
+
// FIX 1: Setup mouse resources BEFORE building the material/scene
|
|
143
|
+
// This ensures u_mouse_texture isn't null during material compilation
|
|
144
|
+
this._setupMouseInteraction();
|
|
59
145
|
this.sceneState = this._initScene(resolution);
|
|
60
146
|
let tick = seed !== undefined ? seed : getElapsedSecondsInLastHour();
|
|
61
147
|
const render = () => {
|
|
62
|
-
const { renderer, camera, scene
|
|
63
|
-
if
|
|
64
|
-
|
|
148
|
+
const { renderer, camera, scene } = this.sceneState;
|
|
149
|
+
// Optimization: check if cached link is still valid in DOM less frequently
|
|
150
|
+
this._linkCheckCounter++;
|
|
151
|
+
if (this._linkCheckCounter >= 300) { // Check every ~5 seconds at 60fps
|
|
152
|
+
this._linkCheckCounter = 0;
|
|
153
|
+
if (!this._linkElement || !document.contains(this._linkElement)) {
|
|
154
|
+
this._linkElement = addNeatLink(ref);
|
|
155
|
+
}
|
|
65
156
|
}
|
|
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
|
-
];
|
|
157
|
+
// Update Uniforms efficiently without creating new objects
|
|
158
|
+
if (this._cachedUniforms) {
|
|
159
|
+
const u = this._cachedUniforms;
|
|
84
160
|
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
|
-
// @ts-ignore
|
|
126
|
-
|
|
127
|
-
|
|
161
|
+
u.u_time.value = tick;
|
|
162
|
+
u.u_resolution.value.set(this._ref.width, this._ref.height);
|
|
163
|
+
u.u_color_pressure.value.set(this._horizontalPressure, this._verticalPressure);
|
|
164
|
+
// Directly assign simple values
|
|
165
|
+
u.u_wave_frequency_x.value = this._waveFrequencyX;
|
|
166
|
+
u.u_wave_frequency_y.value = this._waveFrequencyY;
|
|
167
|
+
u.u_wave_amplitude.value = this._waveAmplitude;
|
|
168
|
+
u.u_color_blending.value = this._colorBlending;
|
|
169
|
+
u.u_shadows.value = this._shadows;
|
|
170
|
+
u.u_highlights.value = this._highlights;
|
|
171
|
+
u.u_saturation.value = this._saturation;
|
|
172
|
+
u.u_brightness.value = this._brightness;
|
|
173
|
+
u.u_grain_intensity.value = this._grainIntensity;
|
|
174
|
+
u.u_grain_sparsity.value = this._grainSparsity;
|
|
175
|
+
u.u_grain_speed.value = this._grainSpeed;
|
|
176
|
+
u.u_grain_scale.value = this._grainScale;
|
|
177
|
+
u.u_y_offset.value = this._yOffset;
|
|
178
|
+
u.u_y_offset_wave_multiplier.value = this._yOffsetWaveMultiplier;
|
|
179
|
+
u.u_y_offset_color_multiplier.value = this._yOffsetColorMultiplier;
|
|
180
|
+
u.u_y_offset_flow_multiplier.value = this._yOffsetFlowMultiplier;
|
|
181
|
+
u.u_flow_distortion_a.value = this._flowDistortionA;
|
|
182
|
+
u.u_flow_distortion_b.value = this._flowDistortionB;
|
|
183
|
+
u.u_flow_scale.value = this._flowScale;
|
|
184
|
+
u.u_flow_ease.value = this._flowEase;
|
|
185
|
+
u.u_flow_enabled.value = this._flowEnabled ? 1.0 : 0.0;
|
|
186
|
+
u.u_mouse_distortion_strength.value = this._mouseDistortionStrength;
|
|
187
|
+
u.u_mouse_distortion_radius.value = this._mouseDistortionRadius;
|
|
188
|
+
u.u_mouse_darken.value = this._mouseDarken;
|
|
189
|
+
u.u_enable_procedural_texture.value = this._enableProceduralTexture ? 1.0 : 0.0;
|
|
190
|
+
// Only regenerate procedural texture when needed
|
|
191
|
+
if (this._textureNeedsUpdate && this._enableProceduralTexture) {
|
|
192
|
+
if (this._proceduralTexture) {
|
|
193
|
+
this._proceduralTexture.dispose();
|
|
194
|
+
}
|
|
195
|
+
this._proceduralTexture = this._createProceduralTexture();
|
|
196
|
+
this._textureNeedsUpdate = false;
|
|
197
|
+
}
|
|
198
|
+
u.u_procedural_texture.value = this._proceduralTexture;
|
|
199
|
+
u.u_texture_ease.value = this._textureEase;
|
|
200
|
+
// Wireframe is a material property and must update every frame to avoid artifacts
|
|
201
|
+
// @ts-ignore - access material safely
|
|
202
|
+
this.sceneState.meshes[0].material.wireframe = this._wireframe;
|
|
203
|
+
// Optimized Color Update: Update immediately on change, or throttle to 10 times per second
|
|
204
|
+
const now = Date.now();
|
|
205
|
+
const shouldUpdate = this._colorsChanged || (now - this._lastColorUpdate > 100);
|
|
206
|
+
if (shouldUpdate) {
|
|
207
|
+
this._lastColorUpdate = now;
|
|
208
|
+
this._colorsChanged = false;
|
|
209
|
+
const shaderColors = u.u_colors.value;
|
|
210
|
+
for (let i = 0; i < COLORS_COUNT; i++) {
|
|
211
|
+
if (i < this._colors.length) {
|
|
212
|
+
const c = this._colors[i];
|
|
213
|
+
shaderColors[i].is_active = c.enabled ? 1.0 : 0.0;
|
|
214
|
+
shaderColors[i].color.setStyle(c.color, "");
|
|
215
|
+
shaderColors[i].influence = c.influence || 0;
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
shaderColors[i].is_active = 0.0;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
u.u_colors_count.value = COLORS_COUNT;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// Render mouse interaction to FBO - optimize by only rendering when needed
|
|
225
|
+
if (this._mouseFBO && this._sceneMouse && this._cameraMouse && this._mouseDistortionStrength > 0) {
|
|
226
|
+
let hasActiveBrushes = false;
|
|
227
|
+
// Update mouse objects - decay rate controls how fast trails fade
|
|
228
|
+
for (let i = 0; i < this._mouseObjects.length; i++) {
|
|
229
|
+
const obj = this._mouseObjects[i];
|
|
230
|
+
if (obj.mesh.visible) {
|
|
231
|
+
hasActiveBrushes = true;
|
|
232
|
+
obj.mesh.rotation.z += 0.01;
|
|
233
|
+
if (obj.mesh.material instanceof THREE.MeshBasicMaterial) {
|
|
234
|
+
// Decay only affects opacity
|
|
235
|
+
obj.mesh.material.opacity *= this._mouseDecayRate;
|
|
236
|
+
if (obj.mesh.material.opacity < 0.01) {
|
|
237
|
+
obj.mesh.visible = false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// Only render FBO if there are active brushes
|
|
243
|
+
if (hasActiveBrushes) {
|
|
244
|
+
// Store current clear color (likely the main background color)
|
|
245
|
+
renderer.getClearColor(this._tempClearColor);
|
|
246
|
+
const oldClearAlpha = renderer.getClearAlpha();
|
|
247
|
+
// Set clear color to Black/Transparent for the FBO.
|
|
248
|
+
renderer.setClearColor(0x000000, 0.0);
|
|
249
|
+
renderer.setRenderTarget(this._mouseFBO);
|
|
250
|
+
renderer.clear();
|
|
251
|
+
renderer.render(this._sceneMouse, this._cameraMouse);
|
|
252
|
+
renderer.setRenderTarget(null);
|
|
253
|
+
// Restore main background color for the actual scene render
|
|
254
|
+
renderer.setClearColor(this._tempClearColor, oldClearAlpha);
|
|
255
|
+
// Update mouse texture uniform
|
|
256
|
+
if (this._cachedUniforms) {
|
|
257
|
+
this._cachedUniforms.u_mouse_texture.value = this._mouseFBO.texture;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// Ensure we set the clear color for the main scene explicitly before rendering
|
|
262
|
+
renderer.setClearColor(this._backgroundColor, this._backgroundAlpha);
|
|
128
263
|
renderer.render(scene, camera);
|
|
129
264
|
this.requestRef = requestAnimationFrame(render);
|
|
130
265
|
};
|
|
@@ -135,9 +270,28 @@ export class NeatGradient {
|
|
|
135
270
|
const height = canvas.clientHeight;
|
|
136
271
|
this.sceneState.renderer.setSize(width, height, false);
|
|
137
272
|
updateCamera(this.sceneState.camera, width, height);
|
|
273
|
+
// FIX 3: Update Mouse FBO and Camera on resize
|
|
274
|
+
// If we don't do this, mouse coordinates map incorrectly after a resize
|
|
275
|
+
if (this._mouseFBO && this._cameraMouse) {
|
|
276
|
+
const fSize = height / 2;
|
|
277
|
+
const aspect = width / height;
|
|
278
|
+
this._mouseFBO.setSize(width / 2, height / 2);
|
|
279
|
+
this._cameraMouse.left = -fSize * aspect;
|
|
280
|
+
this._cameraMouse.right = fSize * aspect;
|
|
281
|
+
this._cameraMouse.top = fSize;
|
|
282
|
+
this._cameraMouse.bottom = -fSize;
|
|
283
|
+
this._cameraMouse.updateProjectionMatrix();
|
|
284
|
+
}
|
|
138
285
|
};
|
|
139
|
-
|
|
140
|
-
|
|
286
|
+
// Debounce resize to prevent excessive operations
|
|
287
|
+
this.sizeObserver = new ResizeObserver(() => {
|
|
288
|
+
if (this._resizeTimeoutId !== null) {
|
|
289
|
+
clearTimeout(this._resizeTimeoutId);
|
|
290
|
+
}
|
|
291
|
+
this._resizeTimeoutId = window.setTimeout(() => {
|
|
292
|
+
setSize();
|
|
293
|
+
this._resizeTimeoutId = null;
|
|
294
|
+
}, 100); // Wait 100ms after last resize event
|
|
141
295
|
});
|
|
142
296
|
this.sizeObserver.observe(ref);
|
|
143
297
|
render();
|
|
@@ -146,6 +300,26 @@ export class NeatGradient {
|
|
|
146
300
|
if (this) {
|
|
147
301
|
cancelAnimationFrame(this.requestRef);
|
|
148
302
|
this.sizeObserver.disconnect();
|
|
303
|
+
// Clear resize timeout
|
|
304
|
+
if (this._resizeTimeoutId !== null) {
|
|
305
|
+
clearTimeout(this._resizeTimeoutId);
|
|
306
|
+
this._resizeTimeoutId = null;
|
|
307
|
+
}
|
|
308
|
+
// Cleanup WebGL resources
|
|
309
|
+
if (this.sceneState) {
|
|
310
|
+
this.sceneState.renderer.dispose();
|
|
311
|
+
this.sceneState.meshes.forEach(m => {
|
|
312
|
+
m.geometry.dispose();
|
|
313
|
+
if (Array.isArray(m.material))
|
|
314
|
+
m.material.forEach(mat => mat.dispose());
|
|
315
|
+
else
|
|
316
|
+
m.material.dispose();
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
if (this._mouseFBO)
|
|
320
|
+
this._mouseFBO.dispose();
|
|
321
|
+
if (this._proceduralTexture)
|
|
322
|
+
this._proceduralTexture.dispose();
|
|
149
323
|
}
|
|
150
324
|
}
|
|
151
325
|
downloadAsPNG(filename = "neat.png") {
|
|
@@ -174,6 +348,7 @@ export class NeatGradient {
|
|
|
174
348
|
}
|
|
175
349
|
set colors(colors) {
|
|
176
350
|
this._colors = colors;
|
|
351
|
+
this._colorsChanged = true; // Flag for immediate update
|
|
177
352
|
}
|
|
178
353
|
set highlights(highlights) {
|
|
179
354
|
this._highlights = highlights / 100;
|
|
@@ -217,8 +392,152 @@ export class NeatGradient {
|
|
|
217
392
|
set yOffset(yOffset) {
|
|
218
393
|
this._yOffset = yOffset;
|
|
219
394
|
}
|
|
395
|
+
get yOffsetWaveMultiplier() {
|
|
396
|
+
return this._yOffsetWaveMultiplier * 1000;
|
|
397
|
+
}
|
|
398
|
+
set yOffsetWaveMultiplier(value) {
|
|
399
|
+
this._yOffsetWaveMultiplier = value / 1000;
|
|
400
|
+
}
|
|
401
|
+
get yOffsetColorMultiplier() {
|
|
402
|
+
return this._yOffsetColorMultiplier * 1000;
|
|
403
|
+
}
|
|
404
|
+
set yOffsetColorMultiplier(value) {
|
|
405
|
+
this._yOffsetColorMultiplier = value / 1000;
|
|
406
|
+
}
|
|
407
|
+
get yOffsetFlowMultiplier() {
|
|
408
|
+
return this._yOffsetFlowMultiplier * 1000;
|
|
409
|
+
}
|
|
410
|
+
set yOffsetFlowMultiplier(value) {
|
|
411
|
+
this._yOffsetFlowMultiplier = value / 1000;
|
|
412
|
+
}
|
|
413
|
+
set flowDistortionA(value) {
|
|
414
|
+
this._flowDistortionA = value;
|
|
415
|
+
}
|
|
416
|
+
set flowDistortionB(value) {
|
|
417
|
+
this._flowDistortionB = value;
|
|
418
|
+
}
|
|
419
|
+
set flowScale(value) {
|
|
420
|
+
this._flowScale = value;
|
|
421
|
+
}
|
|
422
|
+
set flowEase(value) {
|
|
423
|
+
this._flowEase = value;
|
|
424
|
+
}
|
|
425
|
+
set flowEnabled(value) {
|
|
426
|
+
this._flowEnabled = value;
|
|
427
|
+
}
|
|
428
|
+
get flowEnabled() {
|
|
429
|
+
return this._flowEnabled;
|
|
430
|
+
}
|
|
431
|
+
set mouseDistortionStrength(value) {
|
|
432
|
+
this._mouseDistortionStrength = Math.max(0, value);
|
|
433
|
+
}
|
|
434
|
+
set mouseDistortionRadius(value) {
|
|
435
|
+
// Clamp to a sane range in UV space
|
|
436
|
+
this._mouseDistortionRadius = Math.max(0.01, Math.min(value, 1.0));
|
|
437
|
+
// Update brush scale when radius changes
|
|
438
|
+
this._updateBrushScale();
|
|
439
|
+
}
|
|
440
|
+
_updateBrushScale() {
|
|
441
|
+
if (!this._mouseObjects || this._mouseObjects.length === 0)
|
|
442
|
+
return;
|
|
443
|
+
// Radius directly controls the brush scale
|
|
444
|
+
// Base geometry is 200px, so radius 0.25 = 50px, radius 1.0 = 200px
|
|
445
|
+
this._mouseBrushBaseScale = this._mouseDistortionRadius;
|
|
446
|
+
}
|
|
447
|
+
set mouseDecayRate(value) {
|
|
448
|
+
// Clamp between 0.9 (slow decay, more wobble) and 0.99 (fast decay, less wobble)
|
|
449
|
+
this._mouseDecayRate = Math.max(0.9, Math.min(value, 0.99));
|
|
450
|
+
}
|
|
451
|
+
set mouseDarken(value) {
|
|
452
|
+
this._mouseDarken = value;
|
|
453
|
+
}
|
|
454
|
+
set enableProceduralTexture(value) {
|
|
455
|
+
this._enableProceduralTexture = value;
|
|
456
|
+
if (value && !this._proceduralTexture) {
|
|
457
|
+
this._textureNeedsUpdate = true;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
set textureVoidLikelihood(value) {
|
|
461
|
+
this._textureVoidLikelihood = value;
|
|
462
|
+
if (this._enableProceduralTexture) {
|
|
463
|
+
this._textureNeedsUpdate = true;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
set textureVoidWidthMin(value) {
|
|
467
|
+
this._textureVoidWidthMin = value;
|
|
468
|
+
if (this._enableProceduralTexture) {
|
|
469
|
+
this._textureNeedsUpdate = true;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
set textureVoidWidthMax(value) {
|
|
473
|
+
this._textureVoidWidthMax = value;
|
|
474
|
+
if (this._enableProceduralTexture) {
|
|
475
|
+
this._textureNeedsUpdate = true;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
set textureBandDensity(value) {
|
|
479
|
+
this._textureBandDensity = value;
|
|
480
|
+
if (this._enableProceduralTexture) {
|
|
481
|
+
this._textureNeedsUpdate = true;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
set textureColorBlending(value) {
|
|
485
|
+
this._textureColorBlending = value;
|
|
486
|
+
if (this._enableProceduralTexture) {
|
|
487
|
+
this._textureNeedsUpdate = true;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
set textureSeed(value) {
|
|
491
|
+
this._textureSeed = value;
|
|
492
|
+
if (this._enableProceduralTexture) {
|
|
493
|
+
this._textureNeedsUpdate = true;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
get textureEase() {
|
|
497
|
+
return this._textureEase;
|
|
498
|
+
}
|
|
499
|
+
set textureEase(value) {
|
|
500
|
+
this._textureEase = value;
|
|
501
|
+
}
|
|
502
|
+
set proceduralBackgroundColor(value) {
|
|
503
|
+
this._proceduralBackgroundColor = value;
|
|
504
|
+
if (this._enableProceduralTexture) {
|
|
505
|
+
this._textureNeedsUpdate = true;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
set textureShapeTriangles(value) {
|
|
509
|
+
this._textureShapeTriangles = value;
|
|
510
|
+
if (this._enableProceduralTexture)
|
|
511
|
+
this._textureNeedsUpdate = true;
|
|
512
|
+
}
|
|
513
|
+
set textureShapeCircles(value) {
|
|
514
|
+
this._textureShapeCircles = value;
|
|
515
|
+
if (this._enableProceduralTexture)
|
|
516
|
+
this._textureNeedsUpdate = true;
|
|
517
|
+
}
|
|
518
|
+
set textureShapeBars(value) {
|
|
519
|
+
this._textureShapeBars = value;
|
|
520
|
+
if (this._enableProceduralTexture)
|
|
521
|
+
this._textureNeedsUpdate = true;
|
|
522
|
+
}
|
|
523
|
+
set textureShapeSquiggles(value) {
|
|
524
|
+
this._textureShapeSquiggles = value;
|
|
525
|
+
if (this._enableProceduralTexture)
|
|
526
|
+
this._textureNeedsUpdate = true;
|
|
527
|
+
}
|
|
220
528
|
_initScene(resolution) {
|
|
221
529
|
const width = this._ref.width, height = this._ref.height;
|
|
530
|
+
// Cleanup existing renderer if needed
|
|
531
|
+
if (this.sceneState && this.sceneState.renderer) {
|
|
532
|
+
this.sceneState.renderer.dispose();
|
|
533
|
+
this.sceneState.meshes.forEach(m => {
|
|
534
|
+
m.geometry.dispose();
|
|
535
|
+
if (Array.isArray(m.material))
|
|
536
|
+
m.material.forEach(mat => mat.dispose());
|
|
537
|
+
else
|
|
538
|
+
m.material.dispose();
|
|
539
|
+
});
|
|
540
|
+
}
|
|
222
541
|
const renderer = new THREE.WebGLRenderer({
|
|
223
542
|
// antialias: true,
|
|
224
543
|
alpha: true,
|
|
@@ -249,17 +568,13 @@ export class NeatGradient {
|
|
|
249
568
|
};
|
|
250
569
|
}
|
|
251
570
|
_buildMaterial(width, height) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
is_active: false,
|
|
260
|
-
color: new THREE.Color(0x000000)
|
|
261
|
-
}))
|
|
262
|
-
];
|
|
571
|
+
// Initialize stable array structure for colors
|
|
572
|
+
// We create 6 objects and just update them in the render loop to avoid GC
|
|
573
|
+
const colors = Array.from({ length: COLORS_COUNT }).map((_, i) => ({
|
|
574
|
+
is_active: i < this._colors.length ? (this._colors[i].enabled ? 1.0 : 0.0) : 0.0,
|
|
575
|
+
color: new THREE.Color(i < this._colors.length ? this._colors[i].color : 0x000000),
|
|
576
|
+
influence: i < this._colors.length ? (this._colors[i].influence || 0) : 0
|
|
577
|
+
}));
|
|
263
578
|
const uniforms = {
|
|
264
579
|
u_time: { value: 0 },
|
|
265
580
|
u_color_pressure: { value: new THREE.Vector2(this._horizontalPressure, this._verticalPressure) },
|
|
@@ -277,15 +592,287 @@ export class NeatGradient {
|
|
|
277
592
|
u_grain_sparsity: { value: this._grainSparsity },
|
|
278
593
|
u_grain_scale: { value: this._grainScale },
|
|
279
594
|
u_grain_speed: { value: this._grainSpeed },
|
|
595
|
+
// Flow field
|
|
596
|
+
u_flow_distortion_a: { value: this._flowDistortionA },
|
|
597
|
+
u_flow_distortion_b: { value: this._flowDistortionB },
|
|
598
|
+
u_flow_scale: { value: this._flowScale },
|
|
599
|
+
u_flow_ease: { value: this._flowEase },
|
|
600
|
+
u_flow_enabled: { value: this._flowEnabled ? 1.0 : 0.0 },
|
|
601
|
+
// Y offset multipliers
|
|
602
|
+
u_y_offset: { value: this._yOffset },
|
|
603
|
+
u_y_offset_wave_multiplier: { value: this._yOffsetWaveMultiplier },
|
|
604
|
+
u_y_offset_color_multiplier: { value: this._yOffsetColorMultiplier },
|
|
605
|
+
u_y_offset_flow_multiplier: { value: this._yOffsetFlowMultiplier },
|
|
606
|
+
// Mouse interaction
|
|
607
|
+
u_mouse_distortion_strength: { value: this._mouseDistortionStrength },
|
|
608
|
+
u_mouse_distortion_radius: { value: this._mouseDistortionRadius },
|
|
609
|
+
u_mouse_darken: { value: this._mouseDarken },
|
|
610
|
+
u_mouse_texture: { value: this._mouseFBO ? this._mouseFBO.texture : null },
|
|
611
|
+
// Procedural texture
|
|
612
|
+
u_procedural_texture: { value: this._proceduralTexture },
|
|
613
|
+
u_enable_procedural_texture: { value: this._enableProceduralTexture ? 1.0 : 0.0 },
|
|
614
|
+
u_texture_ease: { value: this._textureEase },
|
|
615
|
+
u_saturation: { value: this._saturation },
|
|
616
|
+
u_brightness: { value: this._brightness },
|
|
617
|
+
u_color_blending: { value: this._colorBlending }
|
|
280
618
|
};
|
|
281
619
|
const material = new THREE.ShaderMaterial({
|
|
282
620
|
uniforms: uniforms,
|
|
283
621
|
vertexShader: buildUniforms() + buildNoise() + buildColorFunctions() + buildVertexShader(),
|
|
284
622
|
fragmentShader: buildUniforms() + buildColorFunctions() + buildNoise() + buildFragmentShader()
|
|
285
623
|
});
|
|
624
|
+
// Cache the uniforms object for direct access in render loop
|
|
625
|
+
this._cachedUniforms = uniforms;
|
|
286
626
|
material.wireframe = WIREFRAME;
|
|
287
627
|
return material;
|
|
288
628
|
}
|
|
629
|
+
_setupMouseInteraction() {
|
|
630
|
+
if (!this._ref)
|
|
631
|
+
return;
|
|
632
|
+
const width = this._ref.width;
|
|
633
|
+
const height = this._ref.height;
|
|
634
|
+
// Create mouse FBO
|
|
635
|
+
this._mouseFBO = new THREE.WebGLRenderTarget(width / 2, height / 2);
|
|
636
|
+
// Create mouse scene and camera
|
|
637
|
+
this._sceneMouse = new THREE.Scene();
|
|
638
|
+
const fSize = height / 2;
|
|
639
|
+
const aspect = width / height;
|
|
640
|
+
// FIX 4: Ensure near plane allows viewing objects at Z=0
|
|
641
|
+
// Near -100 is safer for objects at 0
|
|
642
|
+
this._cameraMouse = new THREE.OrthographicCamera(-fSize * aspect, fSize * aspect, fSize, -fSize, 0, 10000);
|
|
643
|
+
this._cameraMouse.position.set(0, 0, 100);
|
|
644
|
+
// Create brush texture - More visible and impactful
|
|
645
|
+
const brushCanvas = document.createElement('canvas');
|
|
646
|
+
brushCanvas.width = 128;
|
|
647
|
+
brushCanvas.height = 128;
|
|
648
|
+
const bCtx = brushCanvas.getContext('2d');
|
|
649
|
+
if (bCtx) {
|
|
650
|
+
const grd = bCtx.createRadialGradient(64, 64, 0, 64, 64, 64);
|
|
651
|
+
// Match reference implementation's stronger gradient
|
|
652
|
+
grd.addColorStop(0, 'rgba(255,255,255,0.8)');
|
|
653
|
+
grd.addColorStop(0.5, 'rgba(255,255,255,0.4)');
|
|
654
|
+
grd.addColorStop(1, 'rgba(255,255,255,0)');
|
|
655
|
+
bCtx.fillStyle = grd;
|
|
656
|
+
bCtx.fillRect(0, 0, 128, 128);
|
|
657
|
+
}
|
|
658
|
+
const brushTex = new THREE.CanvasTexture(brushCanvas);
|
|
659
|
+
const brushMat = new THREE.MeshBasicMaterial({
|
|
660
|
+
map: brushTex,
|
|
661
|
+
transparent: true,
|
|
662
|
+
opacity: 1.0,
|
|
663
|
+
depthTest: false,
|
|
664
|
+
blending: THREE.AdditiveBlending // Additive blending for better accumulation
|
|
665
|
+
});
|
|
666
|
+
// Brush geometry size - will be scaled by radius parameter
|
|
667
|
+
const brushGeo = new THREE.PlaneGeometry(200, 200);
|
|
668
|
+
// Create brush pool
|
|
669
|
+
const brushPoolSize = 50;
|
|
670
|
+
for (let i = 0; i < brushPoolSize; i++) {
|
|
671
|
+
const m = new THREE.Mesh(brushGeo, brushMat.clone());
|
|
672
|
+
m.visible = false;
|
|
673
|
+
this._sceneMouse.add(m);
|
|
674
|
+
this._mouseObjects.push({ mesh: m, active: false });
|
|
675
|
+
}
|
|
676
|
+
// Initialize brush scale based on current radius
|
|
677
|
+
this._updateBrushScale();
|
|
678
|
+
// Add mouse move listener
|
|
679
|
+
this._ref.addEventListener('mousemove', this._onMouseMove.bind(this));
|
|
680
|
+
}
|
|
681
|
+
_onMouseMove(e) {
|
|
682
|
+
if (!this._ref || !this._sceneMouse)
|
|
683
|
+
return;
|
|
684
|
+
const rect = this._ref.getBoundingClientRect();
|
|
685
|
+
const width = this._ref.width;
|
|
686
|
+
const height = this._ref.height;
|
|
687
|
+
// Store pending mouse position
|
|
688
|
+
this._pendingMousePosition = {
|
|
689
|
+
x: e.clientX - rect.left - width / 2,
|
|
690
|
+
y: -(e.clientY - rect.top - height / 2)
|
|
691
|
+
};
|
|
692
|
+
// Batch mouse updates using requestAnimationFrame
|
|
693
|
+
if (!this._mouseUpdateScheduled) {
|
|
694
|
+
this._mouseUpdateScheduled = true;
|
|
695
|
+
requestAnimationFrame(() => {
|
|
696
|
+
this._mouseUpdateScheduled = false;
|
|
697
|
+
if (!this._pendingMousePosition)
|
|
698
|
+
return;
|
|
699
|
+
this._mouse.x = this._pendingMousePosition.x;
|
|
700
|
+
this._mouse.y = this._pendingMousePosition.y;
|
|
701
|
+
const brush = this._mouseObjects[this._currentBrush];
|
|
702
|
+
brush.mesh.scale.set(this._mouseBrushBaseScale, this._mouseBrushBaseScale, 1.0);
|
|
703
|
+
brush.active = true;
|
|
704
|
+
brush.mesh.visible = true;
|
|
705
|
+
brush.mesh.position.set(this._mouse.x, this._mouse.y, 0);
|
|
706
|
+
brush.mesh.rotation.z = Math.random() * Math.PI * 2;
|
|
707
|
+
if (brush.mesh.material instanceof THREE.MeshBasicMaterial) {
|
|
708
|
+
brush.mesh.material.opacity = 1.0;
|
|
709
|
+
}
|
|
710
|
+
this._currentBrush = (this._currentBrush + 1) % this._mouseObjects.length;
|
|
711
|
+
this._pendingMousePosition = null;
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
_createProceduralTexture() {
|
|
716
|
+
// Texture size - 1024 provides good balance between quality and performance
|
|
717
|
+
// Reduced from 2048 for better performance
|
|
718
|
+
const texSize = 1024;
|
|
719
|
+
const sourceCanvas = document.createElement('canvas');
|
|
720
|
+
sourceCanvas.width = texSize;
|
|
721
|
+
sourceCanvas.height = texSize;
|
|
722
|
+
const sCtx = sourceCanvas.getContext('2d', { willReadFrequently: true });
|
|
723
|
+
if (!sCtx)
|
|
724
|
+
return new THREE.Texture();
|
|
725
|
+
let seed = this._textureSeed;
|
|
726
|
+
const baseSeed = this._textureSeed;
|
|
727
|
+
function random() {
|
|
728
|
+
const x = Math.sin(seed++) * 10000;
|
|
729
|
+
return x - Math.floor(x);
|
|
730
|
+
}
|
|
731
|
+
// Helper to reset seed for isolated shape generation
|
|
732
|
+
const setSeed = (offset) => {
|
|
733
|
+
seed = baseSeed + offset;
|
|
734
|
+
};
|
|
735
|
+
const colors = this._colors.filter(c => c.enabled).map(c => c.color);
|
|
736
|
+
if (colors.length === 0)
|
|
737
|
+
return new THREE.Texture();
|
|
738
|
+
// Helper functions
|
|
739
|
+
function hexToRgb(hex) {
|
|
740
|
+
const bigint = parseInt(hex.replace('#', ''), 16);
|
|
741
|
+
return {
|
|
742
|
+
r: (bigint >> 16) & 255,
|
|
743
|
+
g: (bigint >> 8) & 255,
|
|
744
|
+
b: bigint & 255
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
function rgbToHex(r, g, b) {
|
|
748
|
+
return "#" + ((1 << 24) + (Math.round(r) << 16) + (Math.round(g) << 8) + Math.round(b)).toString(16).slice(1);
|
|
749
|
+
}
|
|
750
|
+
const getInterColor = () => {
|
|
751
|
+
const c1 = colors[Math.floor(random() * colors.length)];
|
|
752
|
+
const c2 = colors[Math.floor(random() * colors.length)];
|
|
753
|
+
const mix = random() * this._textureColorBlending;
|
|
754
|
+
const rgb1 = hexToRgb(c1);
|
|
755
|
+
const rgb2 = hexToRgb(c2);
|
|
756
|
+
const r = rgb1.r + (rgb2.r - rgb1.r) * mix;
|
|
757
|
+
const g = rgb1.g + (rgb2.g - rgb1.g) * mix;
|
|
758
|
+
const b = rgb1.b + (rgb2.b - rgb1.b) * mix;
|
|
759
|
+
return rgbToHex(r, g, b);
|
|
760
|
+
};
|
|
761
|
+
// === SOURCE CANVAS ===
|
|
762
|
+
// Base with procedural background color so even sparse areas pick it up
|
|
763
|
+
const baseColor = this._proceduralBackgroundColor || "#000000";
|
|
764
|
+
sCtx.fillStyle = baseColor;
|
|
765
|
+
sCtx.fillRect(0, 0, texSize, texSize);
|
|
766
|
+
// Then lay a vertical gradient of mixed colors on top for richness
|
|
767
|
+
const bgGrad = sCtx.createLinearGradient(0, 0, 0, texSize);
|
|
768
|
+
bgGrad.addColorStop(0, getInterColor());
|
|
769
|
+
bgGrad.addColorStop(1, getInterColor());
|
|
770
|
+
sCtx.fillStyle = bgGrad;
|
|
771
|
+
sCtx.fillRect(0, 0, texSize, texSize);
|
|
772
|
+
// Triangles: use configurable count
|
|
773
|
+
for (let i = 0; i < this._textureShapeTriangles; i++) {
|
|
774
|
+
sCtx.fillStyle = getInterColor();
|
|
775
|
+
sCtx.beginPath();
|
|
776
|
+
const x = random() * texSize;
|
|
777
|
+
const y = random() * texSize;
|
|
778
|
+
const s = 100 + random() * 300;
|
|
779
|
+
sCtx.moveTo(x, y);
|
|
780
|
+
sCtx.lineTo(x + (random() - 0.5) * s, y + (random() - 0.5) * s);
|
|
781
|
+
sCtx.lineTo(x + (random() - 0.5) * s, y + (random() - 0.5) * s);
|
|
782
|
+
sCtx.fill();
|
|
783
|
+
}
|
|
784
|
+
// Circles / rings: use configurable count
|
|
785
|
+
for (let i = 0; i < this._textureShapeCircles; i++) {
|
|
786
|
+
sCtx.strokeStyle = getInterColor();
|
|
787
|
+
sCtx.lineWidth = 10 + random() * 50;
|
|
788
|
+
sCtx.beginPath();
|
|
789
|
+
const x = random() * texSize;
|
|
790
|
+
const y = random() * texSize;
|
|
791
|
+
const r = 50 + random() * 150;
|
|
792
|
+
sCtx.arc(x, y, r, 0, Math.PI * 2);
|
|
793
|
+
sCtx.stroke();
|
|
794
|
+
}
|
|
795
|
+
// Bars: use configurable count
|
|
796
|
+
for (let i = 0; i < this._textureShapeBars; i++) {
|
|
797
|
+
sCtx.fillStyle = getInterColor();
|
|
798
|
+
sCtx.save();
|
|
799
|
+
sCtx.translate(random() * texSize, random() * texSize);
|
|
800
|
+
sCtx.rotate(random() * Math.PI);
|
|
801
|
+
sCtx.fillRect(-150, -25, 300, 50);
|
|
802
|
+
sCtx.restore();
|
|
803
|
+
}
|
|
804
|
+
// Squiggles: use configurable count
|
|
805
|
+
sCtx.lineWidth = 15;
|
|
806
|
+
sCtx.lineCap = 'round';
|
|
807
|
+
for (let i = 0; i < this._textureShapeSquiggles; i++) {
|
|
808
|
+
sCtx.strokeStyle = getInterColor();
|
|
809
|
+
sCtx.beginPath();
|
|
810
|
+
let x = random() * texSize;
|
|
811
|
+
let y = random() * texSize;
|
|
812
|
+
sCtx.moveTo(x, y);
|
|
813
|
+
for (let j = 0; j < 4; j++) {
|
|
814
|
+
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);
|
|
815
|
+
x += (random() - 0.5) * 300;
|
|
816
|
+
y += (random() - 0.5) * 300;
|
|
817
|
+
}
|
|
818
|
+
sCtx.stroke();
|
|
819
|
+
}
|
|
820
|
+
// === MASKED CANVAS ===
|
|
821
|
+
// Masking: Seed isolation
|
|
822
|
+
setSeed(50000);
|
|
823
|
+
const canvas = document.createElement('canvas');
|
|
824
|
+
canvas.width = texSize;
|
|
825
|
+
canvas.height = texSize;
|
|
826
|
+
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
|
827
|
+
if (!ctx)
|
|
828
|
+
return new THREE.Texture();
|
|
829
|
+
// Start filled with the chosen void color so gaps show that color
|
|
830
|
+
ctx.fillStyle = baseColor;
|
|
831
|
+
ctx.fillRect(0, 0, texSize, texSize);
|
|
832
|
+
// Determine layout segments (matter vs void)
|
|
833
|
+
let layoutHead = 0;
|
|
834
|
+
const segments = [];
|
|
835
|
+
while (layoutHead < texSize) {
|
|
836
|
+
const isVoid = random() < this._textureVoidLikelihood;
|
|
837
|
+
if (isVoid) {
|
|
838
|
+
const w = this._textureVoidWidthMin + random() * (this._textureVoidWidthMax - this._textureVoidWidthMin);
|
|
839
|
+
segments.push({ type: 'void', x: layoutHead, width: w });
|
|
840
|
+
layoutHead += w;
|
|
841
|
+
}
|
|
842
|
+
else {
|
|
843
|
+
const w = 50 + random() * 200;
|
|
844
|
+
segments.push({ type: 'matter', x: layoutHead, width: w });
|
|
845
|
+
layoutHead += w;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
// Render only matter bands from the source into the masked canvas
|
|
849
|
+
for (const seg of segments) {
|
|
850
|
+
if (seg.type === 'matter') {
|
|
851
|
+
const startX = seg.x;
|
|
852
|
+
const endX = Math.min(seg.x + seg.width, texSize);
|
|
853
|
+
let currentX = startX;
|
|
854
|
+
while (currentX < endX) {
|
|
855
|
+
const stripeWidth = (2 + random() * 20) / this._textureBandDensity;
|
|
856
|
+
const sourceX = Math.floor(random() * texSize);
|
|
857
|
+
ctx.drawImage(sourceCanvas, sourceX, 0, stripeWidth, texSize, currentX, 0, stripeWidth, texSize);
|
|
858
|
+
currentX += stripeWidth;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
// void segments: leave as baseColor
|
|
862
|
+
}
|
|
863
|
+
const tex = new THREE.CanvasTexture(canvas);
|
|
864
|
+
// Use mipmapping for better quality when texture is scaled
|
|
865
|
+
tex.minFilter = THREE.LinearMipmapLinearFilter;
|
|
866
|
+
tex.magFilter = THREE.LinearFilter;
|
|
867
|
+
tex.wrapS = THREE.RepeatWrapping;
|
|
868
|
+
tex.wrapT = THREE.RepeatWrapping;
|
|
869
|
+
// Enable anisotropic filtering for much better quality when texture is stretched
|
|
870
|
+
// 16 is a commonly supported value that dramatically improves quality
|
|
871
|
+
tex.anisotropy = 16;
|
|
872
|
+
// Ensure mipmaps are generated
|
|
873
|
+
tex.needsUpdate = true;
|
|
874
|
+
return tex;
|
|
875
|
+
}
|
|
289
876
|
}
|
|
290
877
|
function updateCamera(camera, width, height) {
|
|
291
878
|
const viewPortAreaRatio = 1000000;
|
|
@@ -295,10 +882,23 @@ function updateCamera(camera, width, height) {
|
|
|
295
882
|
const ratio = width / height;
|
|
296
883
|
const targetWidth = Math.sqrt(targetPlaneArea * ratio);
|
|
297
884
|
const targetHeight = targetPlaneArea / targetWidth;
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
885
|
+
let left = -PLANE_WIDTH / 2;
|
|
886
|
+
let right = Math.min((left + targetWidth) / 1.5, PLANE_WIDTH / 2);
|
|
887
|
+
let top = PLANE_HEIGHT / 4;
|
|
888
|
+
let bottom = Math.max((top - targetHeight) / 2, -PLANE_HEIGHT / 4);
|
|
889
|
+
// Fix for mobile portrait: adjust bounds for proper aspect ratio AND zoom out slightly
|
|
890
|
+
if (ratio < 1) {
|
|
891
|
+
// Portrait mode - scale horizontal bounds by aspect ratio to prevent stretching
|
|
892
|
+
const horizontalScale = ratio;
|
|
893
|
+
left = left * horizontalScale;
|
|
894
|
+
right = right * horizontalScale;
|
|
895
|
+
// Zoom out slightly on mobile (1.1 = 10% zoom out)
|
|
896
|
+
const mobileZoomFactor = 1.05;
|
|
897
|
+
left = left * mobileZoomFactor;
|
|
898
|
+
right = right * mobileZoomFactor;
|
|
899
|
+
top = top * mobileZoomFactor;
|
|
900
|
+
bottom = bottom * mobileZoomFactor;
|
|
901
|
+
}
|
|
302
902
|
const near = -100;
|
|
303
903
|
const far = 1000;
|
|
304
904
|
if (camera instanceof THREE.OrthographicCamera) {
|
|
@@ -315,43 +915,69 @@ function updateCamera(camera, width, height) {
|
|
|
315
915
|
camera.updateProjectionMatrix();
|
|
316
916
|
}
|
|
317
917
|
}
|
|
918
|
+
// Cache shader strings to avoid repeated concatenation
|
|
919
|
+
let cachedVertexShader = null;
|
|
920
|
+
let cachedFragmentShader = null;
|
|
318
921
|
function buildVertexShader() {
|
|
319
|
-
|
|
320
|
-
|
|
922
|
+
if (cachedVertexShader)
|
|
923
|
+
return cachedVertexShader;
|
|
924
|
+
cachedVertexShader = `
|
|
321
925
|
void main() {
|
|
322
|
-
|
|
323
926
|
vUv = uv;
|
|
324
927
|
|
|
928
|
+
// SCROLLING LOGIC
|
|
929
|
+
// Separate multipliers for wave, color, and flow offsets
|
|
930
|
+
float waveOffset = -u_y_offset * u_y_offset_wave_multiplier;
|
|
931
|
+
float colorOffset = -u_y_offset * u_y_offset_color_multiplier;
|
|
932
|
+
float flowOffset = -u_y_offset * u_y_offset_flow_multiplier;
|
|
933
|
+
|
|
934
|
+
// 1. DISPLACEMENT (WAVES)
|
|
935
|
+
// We add waveOffset to Y to scroll the wave pattern
|
|
325
936
|
v_displacement_amount = cnoise( vec3(
|
|
326
937
|
u_wave_frequency_x * position.x + u_time,
|
|
327
|
-
u_wave_frequency_y * position.y + u_time,
|
|
938
|
+
u_wave_frequency_y * (position.y + waveOffset) + u_time,
|
|
328
939
|
u_time
|
|
329
940
|
));
|
|
330
941
|
|
|
331
|
-
|
|
942
|
+
// 2. FLOW FIELD
|
|
943
|
+
// Apply flow offset to scroll the flow field mask
|
|
944
|
+
vec2 baseUv = vUv;
|
|
945
|
+
baseUv.y += flowOffset / u_plane_height; // Scale to match wave speed
|
|
946
|
+
vec2 flowUv = baseUv;
|
|
947
|
+
|
|
948
|
+
if (u_flow_enabled > 0.5) {
|
|
949
|
+
if (u_flow_ease > 0.0 || u_flow_distortion_a > 0.0) {
|
|
950
|
+
vec2 ppp = -1.0 + 2.0 * baseUv;
|
|
951
|
+
ppp += 0.1 * cos((1.5 * u_flow_scale) * ppp.yx + 1.1 * u_time + vec2(0.1, 1.1));
|
|
952
|
+
ppp += 0.1 * cos((2.3 * u_flow_scale) * ppp.yx + 1.3 * u_time + vec2(3.2, 3.4));
|
|
953
|
+
ppp += 0.1 * cos((2.2 * u_flow_scale) * ppp.yx + 1.7 * u_time + vec2(1.8, 5.2));
|
|
954
|
+
ppp += u_flow_distortion_a * cos((u_flow_distortion_b * u_flow_scale) * ppp.yx + 1.4 * u_time + vec2(6.3, 3.9));
|
|
955
|
+
|
|
956
|
+
float r = length(ppp);
|
|
957
|
+
flowUv = mix(baseUv, vec2(baseUv.x * (1.0 - u_flow_ease) + r * u_flow_ease, baseUv.y), u_flow_ease);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
332
960
|
|
|
333
|
-
//
|
|
334
|
-
|
|
961
|
+
// Pass the standard flow UV to fragment shader (for mouse/texture)
|
|
962
|
+
vFlowUv = flowUv;
|
|
335
963
|
|
|
336
|
-
//
|
|
337
|
-
|
|
338
|
-
//
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
964
|
+
// 3. COLOR MIXING
|
|
965
|
+
// We take the computed flow UVs and apply the color offset
|
|
966
|
+
// Scale by plane height to match wave offset speed (world space vs UV space)
|
|
967
|
+
vec3 color = u_colors[0].color;
|
|
968
|
+
vec2 adjustedUv = flowUv;
|
|
969
|
+
adjustedUv.y += colorOffset / u_plane_height; // Scroll the color mixing pattern
|
|
342
970
|
|
|
971
|
+
vec2 noise_cord = adjustedUv * u_color_pressure;
|
|
343
972
|
const float minNoise = .0;
|
|
344
973
|
const float maxNoise = .9;
|
|
345
974
|
|
|
346
975
|
for (int i = 1; i < u_colors_count; i++) {
|
|
347
|
-
|
|
348
|
-
if(u_colors[i].is_active == 1.0){
|
|
976
|
+
if(u_colors[i].is_active > 0.5){
|
|
349
977
|
float noiseFlow = (1. + float(i)) / 30.;
|
|
350
978
|
float noiseSpeed = (1. + float(i)) * 0.11;
|
|
351
979
|
float noiseSeed = 13. + float(i) * 7.;
|
|
352
980
|
|
|
353
|
-
int reverseIndex = u_colors_count - i;
|
|
354
|
-
|
|
355
981
|
float noise = snoise(
|
|
356
982
|
vec3(
|
|
357
983
|
noise_cord.x * u_color_pressure.x + u_time * noiseFlow * 2.,
|
|
@@ -361,22 +987,24 @@ void main() {
|
|
|
361
987
|
) - (.1 * float(i)) + (.5 * u_color_blending);
|
|
362
988
|
|
|
363
989
|
noise = clamp(minNoise, maxNoise + float(i) * 0.02, noise);
|
|
364
|
-
|
|
365
|
-
color = mix(color, nextColor, smoothstep(0.0, u_color_blending, noise));
|
|
990
|
+
color = mix(color, u_colors[i].color, smoothstep(0.0, u_color_blending, noise));
|
|
366
991
|
}
|
|
367
992
|
}
|
|
368
993
|
|
|
369
994
|
v_color = color;
|
|
370
995
|
|
|
996
|
+
// 4. VERTEX POSITION
|
|
371
997
|
vec3 newPosition = position + normal * v_displacement_amount * u_wave_amplitude;
|
|
372
998
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
|
|
373
|
-
|
|
374
999
|
v_new_position = gl_Position;
|
|
375
1000
|
}
|
|
376
1001
|
`;
|
|
1002
|
+
return cachedVertexShader;
|
|
377
1003
|
}
|
|
378
1004
|
function buildFragmentShader() {
|
|
379
|
-
|
|
1005
|
+
if (cachedFragmentShader)
|
|
1006
|
+
return cachedFragmentShader;
|
|
1007
|
+
cachedFragmentShader = `
|
|
380
1008
|
float random(vec2 p) {
|
|
381
1009
|
return fract(sin(dot(p, vec2(12.9898,78.233))) * 43758.5453);
|
|
382
1010
|
}
|
|
@@ -394,34 +1022,79 @@ float fbm(vec3 x) {
|
|
|
394
1022
|
}
|
|
395
1023
|
|
|
396
1024
|
void main() {
|
|
397
|
-
|
|
1025
|
+
// MOUSE DISTORTION
|
|
1026
|
+
vec2 finalUv = vFlowUv;
|
|
1027
|
+
|
|
1028
|
+
if (u_mouse_distortion_strength > 0.0) {
|
|
1029
|
+
vec4 mouseColor = texture2D(u_mouse_texture, vUv);
|
|
1030
|
+
float mouseValue = mouseColor.r;
|
|
1031
|
+
|
|
1032
|
+
if (mouseValue > 0.001) {
|
|
1033
|
+
float distortionAmount = mouseValue * u_mouse_distortion_strength;
|
|
1034
|
+
vec2 mouseDisp = vec2(distortionAmount, distortionAmount);
|
|
1035
|
+
finalUv -= mouseDisp;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
vec3 baseColor;
|
|
1040
|
+
|
|
1041
|
+
if (u_enable_procedural_texture > 0.5) {
|
|
1042
|
+
// Calculate flow field distance for ease effect
|
|
1043
|
+
vec2 ppp = -1.0 + 2.0 * finalUv;
|
|
1044
|
+
ppp += 0.1 * cos((1.5 * u_flow_scale) * ppp.yx + 1.1 * u_time + vec2(0.1, 1.1));
|
|
1045
|
+
ppp += 0.1 * cos((2.3 * u_flow_scale) * ppp.yx + 1.3 * u_time + vec2(3.2, 3.4));
|
|
1046
|
+
ppp += 0.1 * cos((2.2 * u_flow_scale) * ppp.yx + 1.7 * u_time + vec2(1.8, 5.2));
|
|
1047
|
+
ppp += u_flow_distortion_a * cos((u_flow_distortion_b * u_flow_scale) * ppp.yx + 1.4 * u_time + vec2(6.3, 3.9));
|
|
1048
|
+
float r = length(ppp); // Flow distance
|
|
1049
|
+
|
|
1050
|
+
// Ease blending: 0 = topographic (flow), 1 = image (UV)
|
|
1051
|
+
float vx = (finalUv.x * u_texture_ease) + (r * (1.0 - u_texture_ease));
|
|
1052
|
+
float vy = (finalUv.y * u_texture_ease) + (0.0 * (1.0 - u_texture_ease));
|
|
1053
|
+
vec2 texUv = vec2(vx, vy);
|
|
1054
|
+
|
|
1055
|
+
// PARALLAX SCROLLING
|
|
1056
|
+
// We manually apply a smaller offset here to make the texture lag behind
|
|
1057
|
+
float parallaxFactor = 0.25; // 25% speed of the color mixing
|
|
1058
|
+
texUv.y -= (u_y_offset * u_y_offset_color_multiplier / u_plane_height) * parallaxFactor;
|
|
1059
|
+
|
|
1060
|
+
texUv *= 1.5; // Tiling scale
|
|
1061
|
+
|
|
1062
|
+
vec4 texSample = texture2D(u_procedural_texture, texUv);
|
|
1063
|
+
baseColor = texSample.rgb;
|
|
1064
|
+
} else {
|
|
1065
|
+
baseColor = v_color;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
vec3 color = baseColor;
|
|
1069
|
+
|
|
1070
|
+
// Post-processing
|
|
398
1071
|
color += pow(v_displacement_amount, 1.0) * u_highlights;
|
|
399
1072
|
color -= pow(1.0 - v_displacement_amount, 2.0) * u_shadows;
|
|
400
1073
|
color = saturation(color, 1.0 + u_saturation);
|
|
401
1074
|
color = color * u_brightness;
|
|
402
1075
|
|
|
403
|
-
//
|
|
1076
|
+
// Grain
|
|
404
1077
|
vec2 noiseCoords = gl_FragCoord.xy / u_grain_scale;
|
|
405
1078
|
float grain = (u_grain_speed != 0.0) ? fbm(vec3(noiseCoords, u_time * u_grain_speed)) : fbm(vec3(noiseCoords, 0.0));
|
|
406
1079
|
|
|
407
|
-
// Center the grain around zero
|
|
408
1080
|
grain = grain * 0.5 + 0.5;
|
|
409
1081
|
grain -= 0.5;
|
|
410
|
-
|
|
411
|
-
// Add sparsity control
|
|
412
1082
|
grain = (grain > u_grain_sparsity) ? grain : 0.0;
|
|
413
|
-
|
|
414
|
-
// Apply grain intensity
|
|
415
1083
|
grain *= u_grain_intensity;
|
|
416
1084
|
|
|
417
|
-
// Add grain to color
|
|
418
1085
|
color += vec3(grain);
|
|
419
1086
|
|
|
420
1087
|
gl_FragColor = vec4(color, 1.0);
|
|
421
1088
|
}
|
|
422
1089
|
`;
|
|
1090
|
+
return cachedFragmentShader;
|
|
423
1091
|
}
|
|
424
|
-
|
|
1092
|
+
// Cache uniforms string as well
|
|
1093
|
+
let cachedUniformsShader = null;
|
|
1094
|
+
const buildUniforms = () => {
|
|
1095
|
+
if (cachedUniformsShader)
|
|
1096
|
+
return cachedUniformsShader;
|
|
1097
|
+
cachedUniformsShader = `
|
|
425
1098
|
precision highp float;
|
|
426
1099
|
|
|
427
1100
|
struct Color {
|
|
@@ -453,82 +1126,97 @@ uniform float u_brightness;
|
|
|
453
1126
|
uniform float u_color_blending;
|
|
454
1127
|
|
|
455
1128
|
uniform int u_colors_count;
|
|
456
|
-
uniform Color u_colors[
|
|
1129
|
+
uniform Color u_colors[6];
|
|
457
1130
|
uniform vec2 u_resolution;
|
|
458
1131
|
|
|
459
1132
|
uniform float u_y_offset;
|
|
1133
|
+
uniform float u_y_offset_wave_multiplier;
|
|
1134
|
+
uniform float u_y_offset_color_multiplier;
|
|
1135
|
+
uniform float u_y_offset_flow_multiplier;
|
|
1136
|
+
|
|
1137
|
+
// Flow field uniforms
|
|
1138
|
+
uniform float u_flow_distortion_a;
|
|
1139
|
+
uniform float u_flow_distortion_b;
|
|
1140
|
+
uniform float u_flow_scale;
|
|
1141
|
+
uniform float u_flow_ease;
|
|
1142
|
+
uniform float u_flow_enabled;
|
|
1143
|
+
|
|
1144
|
+
// Mouse interaction uniforms
|
|
1145
|
+
uniform float u_mouse_distortion_strength;
|
|
1146
|
+
uniform float u_mouse_distortion_radius;
|
|
1147
|
+
uniform float u_mouse_darken;
|
|
1148
|
+
uniform sampler2D u_mouse_texture;
|
|
1149
|
+
|
|
1150
|
+
// Procedural texture uniforms
|
|
1151
|
+
uniform sampler2D u_procedural_texture;
|
|
1152
|
+
uniform float u_enable_procedural_texture;
|
|
1153
|
+
uniform float u_texture_ease;
|
|
460
1154
|
|
|
461
1155
|
varying vec2 vUv;
|
|
1156
|
+
varying vec2 vFlowUv;
|
|
462
1157
|
varying vec4 v_new_position;
|
|
463
1158
|
varying vec3 v_color;
|
|
464
1159
|
varying float v_displacement_amount;
|
|
465
1160
|
|
|
466
1161
|
`;
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
{
|
|
481
|
-
return mod289(((x*34.0)+1.0)*x);
|
|
1162
|
+
return cachedUniformsShader;
|
|
1163
|
+
};
|
|
1164
|
+
// Cache noise functions as well
|
|
1165
|
+
let cachedNoiseShader = null;
|
|
1166
|
+
const buildNoise = () => {
|
|
1167
|
+
if (cachedNoiseShader)
|
|
1168
|
+
return cachedNoiseShader;
|
|
1169
|
+
cachedNoiseShader = `
|
|
1170
|
+
|
|
1171
|
+
// 1. REPLACEMENT PERMUTE:
|
|
1172
|
+
// Uses a hash function (fract/sin) instead of a modular lookup table.
|
|
1173
|
+
vec4 permute(vec4 x) {
|
|
1174
|
+
return floor(fract(sin(x) * 43758.5453123) * 289.0);
|
|
482
1175
|
}
|
|
483
1176
|
|
|
484
|
-
|
|
485
|
-
{
|
|
1177
|
+
// Taylor Inverse Sqrt
|
|
1178
|
+
vec4 taylorInvSqrt(vec4 r) {
|
|
486
1179
|
return 1.79284291400159 - 0.85373472095314 * r;
|
|
487
1180
|
}
|
|
488
1181
|
|
|
1182
|
+
// Fade function
|
|
489
1183
|
vec3 fade(vec3 t) {
|
|
490
1184
|
return t*t*t*(t*(t*6.0-15.0)+10.0);
|
|
491
1185
|
}
|
|
492
1186
|
|
|
493
|
-
|
|
494
|
-
{
|
|
1187
|
+
// 3D Simplex Noise
|
|
1188
|
+
float snoise(vec3 v) {
|
|
495
1189
|
const vec2 C = vec2(1.0/6.0, 1.0/3.0) ;
|
|
496
1190
|
const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
|
|
497
1191
|
|
|
498
|
-
// First corner
|
|
1192
|
+
// First corner
|
|
499
1193
|
vec3 i = floor(v + dot(v, C.yyy) );
|
|
500
1194
|
vec3 x0 = v - i + dot(i, C.xxx) ;
|
|
501
1195
|
|
|
502
|
-
// Other corners
|
|
1196
|
+
// Other corners
|
|
503
1197
|
vec3 g = step(x0.yzx, x0.xyz);
|
|
504
1198
|
vec3 l = 1.0 - g;
|
|
505
1199
|
vec3 i1 = min( g.xyz, l.zxy );
|
|
506
1200
|
vec3 i2 = max( g.xyz, l.zxy );
|
|
507
1201
|
|
|
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
1202
|
vec3 x1 = x0 - i1 + C.xxx;
|
|
513
|
-
vec3 x2 = x0 - i2 + C.yyy;
|
|
514
|
-
vec3 x3 = x0 - D.yyy;
|
|
1203
|
+
vec3 x2 = x0 - i2 + C.yyy;
|
|
1204
|
+
vec3 x3 = x0 - D.yyy;
|
|
515
1205
|
|
|
516
|
-
// Permutations
|
|
517
|
-
i = mod289(i);
|
|
1206
|
+
// Permutations
|
|
518
1207
|
vec4 p = permute( permute( permute(
|
|
519
1208
|
i.z + vec4(0.0, i1.z, i2.z, 1.0 ))
|
|
520
1209
|
+ i.y + vec4(0.0, i1.y, i2.y, 1.0 ))
|
|
521
1210
|
+ i.x + vec4(0.0, i1.x, i2.x, 1.0 ));
|
|
522
1211
|
|
|
523
|
-
// Gradients
|
|
524
|
-
// The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294)
|
|
1212
|
+
// Gradients
|
|
525
1213
|
float n_ = 0.142857142857; // 1.0/7.0
|
|
526
1214
|
vec3 ns = n_ * D.wyz - D.xzx;
|
|
527
1215
|
|
|
528
|
-
vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
|
|
1216
|
+
vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
|
|
529
1217
|
|
|
530
1218
|
vec4 x_ = floor(j * ns.z);
|
|
531
|
-
vec4 y_ = floor(j - 7.0 * x_ );
|
|
1219
|
+
vec4 y_ = floor(j - 7.0 * x_ );
|
|
532
1220
|
|
|
533
1221
|
vec4 x = x_ *ns.x + ns.yyyy;
|
|
534
1222
|
vec4 y = y_ *ns.x + ns.yyyy;
|
|
@@ -537,8 +1225,6 @@ float snoise(vec3 v)
|
|
|
537
1225
|
vec4 b0 = vec4( x.xy, y.xy );
|
|
538
1226
|
vec4 b1 = vec4( x.zw, y.zw );
|
|
539
1227
|
|
|
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
1228
|
vec4 s0 = floor(b0)*2.0 + 1.0;
|
|
543
1229
|
vec4 s1 = floor(b1)*2.0 + 1.0;
|
|
544
1230
|
vec4 sh = -step(h, vec4(0.0));
|
|
@@ -551,14 +1237,14 @@ float snoise(vec3 v)
|
|
|
551
1237
|
vec3 p2 = vec3(a1.xy,h.z);
|
|
552
1238
|
vec3 p3 = vec3(a1.zw,h.w);
|
|
553
1239
|
|
|
554
|
-
//Normalise gradients
|
|
1240
|
+
// Normalise gradients
|
|
555
1241
|
vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
|
|
556
1242
|
p0 *= norm.x;
|
|
557
1243
|
p1 *= norm.y;
|
|
558
1244
|
p2 *= norm.z;
|
|
559
1245
|
p3 *= norm.w;
|
|
560
1246
|
|
|
561
|
-
// Mix final noise value
|
|
1247
|
+
// Mix final noise value
|
|
562
1248
|
vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
|
|
563
1249
|
m = m * m;
|
|
564
1250
|
return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1),
|
|
@@ -568,12 +1254,11 @@ float snoise(vec3 v)
|
|
|
568
1254
|
// Classic Perlin noise
|
|
569
1255
|
float cnoise(vec3 P)
|
|
570
1256
|
{
|
|
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
|
|
1257
|
+
vec3 Pi0 = floor(P);
|
|
1258
|
+
vec3 Pi1 = Pi0 + vec3(1.0);
|
|
1259
|
+
|
|
1260
|
+
vec3 Pf0 = fract(P);
|
|
1261
|
+
vec3 Pf1 = Pf0 - vec3(1.0);
|
|
577
1262
|
vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x);
|
|
578
1263
|
vec4 iy = vec4(Pi0.yy, Pi1.yy);
|
|
579
1264
|
vec4 iz0 = Pi0.zzzz;
|
|
@@ -634,48 +1319,15 @@ float cnoise(vec3 P)
|
|
|
634
1319
|
float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x);
|
|
635
1320
|
return 2.2 * n_xyz;
|
|
636
1321
|
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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
|
-
`;
|
|
678
|
-
const buildColorFunctions = () => `
|
|
1322
|
+
`;
|
|
1323
|
+
return cachedNoiseShader;
|
|
1324
|
+
};
|
|
1325
|
+
// Cache color functions as well
|
|
1326
|
+
let cachedColorFunctionsShader = null;
|
|
1327
|
+
const buildColorFunctions = () => {
|
|
1328
|
+
if (cachedColorFunctionsShader)
|
|
1329
|
+
return cachedColorFunctionsShader;
|
|
1330
|
+
cachedColorFunctionsShader = `
|
|
679
1331
|
|
|
680
1332
|
vec3 saturation(vec3 rgb, float adjustment) {
|
|
681
1333
|
const vec3 W = vec3(0.2125, 0.7154, 0.0721);
|
|
@@ -719,6 +1371,8 @@ vec3 hsv2rgb(vec3 c)
|
|
|
719
1371
|
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
|
|
720
1372
|
}
|
|
721
1373
|
`;
|
|
1374
|
+
return cachedColorFunctionsShader;
|
|
1375
|
+
};
|
|
722
1376
|
const setLinkStyles = (link) => {
|
|
723
1377
|
link.id = LINK_ID;
|
|
724
1378
|
link.href = "https://neat.firecms.co";
|
|
@@ -743,13 +1397,14 @@ const addNeatLink = (ref) => {
|
|
|
743
1397
|
for (let i = 0; i < existingLinks.length; i++) {
|
|
744
1398
|
if (existingLinks[i].id === LINK_ID) {
|
|
745
1399
|
setLinkStyles(existingLinks[i]);
|
|
746
|
-
return;
|
|
1400
|
+
return existingLinks[i];
|
|
747
1401
|
}
|
|
748
1402
|
}
|
|
749
1403
|
}
|
|
750
1404
|
const link = document.createElement("a");
|
|
751
1405
|
setLinkStyles(link);
|
|
752
1406
|
ref.parentElement?.appendChild(link);
|
|
1407
|
+
return link;
|
|
753
1408
|
};
|
|
754
1409
|
function getElapsedSecondsInLastHour() {
|
|
755
1410
|
const now = new Date();
|