@firecms/neat 0.6.0 → 0.7.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 +5 -29
- package/dist/NeatGradient.d.ts +29 -43
- package/dist/NeatGradient.js +346 -897
- package/dist/NeatGradient.js.map +1 -1
- package/dist/index.es.js +757 -731
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +103 -120
- package/dist/index.umd.js.map +1 -1
- package/dist/math.d.ts +26 -0
- package/dist/math.js +148 -0
- package/dist/math.js.map +1 -0
- package/dist/shaders.d.ts +6 -0
- package/dist/shaders.js +407 -0
- package/dist/shaders.js.map +1 -0
- package/dist/types.d.ts +52 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +3 -7
- package/src/NeatGradient.ts +410 -967
- package/src/math.ts +162 -0
- package/src/shaders.ts +411 -0
- package/src/types.ts +54 -0
package/dist/NeatGradient.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { buildColorFunctions, buildNoise, buildVertUniforms, buildFragUniforms, fragmentShaderSource, vertexShaderSource } from "./shaders";
|
|
2
|
+
import { generatePlaneGeometry, OrthographicCamera, updateCamera, Matrix4 } from "./math";
|
|
2
3
|
console.info("%c🌈 Neat Gradients%c\n\nLicensed under MIT + The Commons Clause.\nFree for personal and commercial use.\nSelling this software or its derivatives is strictly prohibited.\nhttps://neat.firecms.co", "font-weight: bold; font-size: 14px; color: #FF5772;", "color: inherit;");
|
|
3
4
|
const PLANE_WIDTH = 50;
|
|
4
5
|
const PLANE_HEIGHT = 80;
|
|
5
|
-
const WIREFRAME = true;
|
|
6
6
|
const COLORS_COUNT = 6;
|
|
7
|
-
const clock = new THREE.Clock();
|
|
8
7
|
const LINK_ID = generateRandomString();
|
|
9
8
|
export class NeatGradient {
|
|
10
9
|
_ref;
|
|
@@ -26,6 +25,7 @@ export class NeatGradient {
|
|
|
26
25
|
_colors = [];
|
|
27
26
|
_wireframe = false;
|
|
28
27
|
_backgroundColor = "#FFFFFF";
|
|
28
|
+
_backgroundColorRgb = [1, 1, 1];
|
|
29
29
|
_backgroundAlpha = 1.0;
|
|
30
30
|
// Flow field properties
|
|
31
31
|
_flowDistortionA = 0;
|
|
@@ -33,18 +33,7 @@ export class NeatGradient {
|
|
|
33
33
|
_flowScale = 1.0;
|
|
34
34
|
_flowEase = 0.0;
|
|
35
35
|
_flowEnabled = true;
|
|
36
|
-
|
|
37
|
-
_mouseDistortionStrength = 0.0;
|
|
38
|
-
_mouseDistortionRadius = 0.25;
|
|
39
|
-
_mouseDecayRate = 0.96;
|
|
40
|
-
_mouseDarken = 0.0;
|
|
41
|
-
_mouse = new THREE.Vector2(-1000, -1000);
|
|
42
|
-
_mouseFBO = null;
|
|
43
|
-
_sceneMouse = null;
|
|
44
|
-
_cameraMouse = null;
|
|
45
|
-
_mouseObjects = [];
|
|
46
|
-
_currentBrush = 0;
|
|
47
|
-
_mouseBrushBaseScale = 1;
|
|
36
|
+
glState;
|
|
48
37
|
// Texture generation properties
|
|
49
38
|
_enableProceduralTexture = false;
|
|
50
39
|
_textureVoidLikelihood = 0.45;
|
|
@@ -62,36 +51,29 @@ export class NeatGradient {
|
|
|
62
51
|
_textureShapeSquiggles = 10;
|
|
63
52
|
requestRef = -1;
|
|
64
53
|
sizeObserver;
|
|
65
|
-
|
|
66
|
-
// Optimization: Cache uniforms to avoid lookups and object creation in render loop
|
|
67
|
-
_cachedUniforms = null;
|
|
54
|
+
_initialized = false;
|
|
68
55
|
_linkElement = null;
|
|
56
|
+
_cachedColorRgb = [];
|
|
69
57
|
_yOffset = 0;
|
|
70
58
|
_yOffsetWaveMultiplier = 0.004;
|
|
71
59
|
_yOffsetColorMultiplier = 0.004;
|
|
72
60
|
_yOffsetFlowMultiplier = 0.004;
|
|
73
|
-
// For saving/restoring clear color
|
|
74
|
-
_tempClearColor = new THREE.Color();
|
|
75
61
|
// Performance optimizations
|
|
76
62
|
_resizeTimeoutId = null;
|
|
77
63
|
_textureNeedsUpdate = false;
|
|
78
|
-
_lastColorUpdate = 0;
|
|
79
64
|
_linkCheckCounter = 0;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
65
|
+
_colorsChanged = true;
|
|
66
|
+
_uniformsDirty = true;
|
|
67
|
+
_textureDirty = true;
|
|
83
68
|
constructor(config) {
|
|
84
69
|
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,
|
|
85
70
|
// Flow field parameters
|
|
86
71
|
flowDistortionA = 0, flowDistortionB = 0, flowScale = 1.0, flowEase = 0.0, flowEnabled = true,
|
|
87
|
-
// Mouse interaction
|
|
88
|
-
mouseDistortionStrength = 0.0, mouseDistortionRadius = 0.25, mouseDecayRate = 0.96, mouseDarken = 0.0,
|
|
89
72
|
// Texture generation
|
|
90
73
|
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;
|
|
91
74
|
this._ref = ref;
|
|
92
75
|
this.destroy = this.destroy.bind(this);
|
|
93
76
|
this._initScene = this._initScene.bind(this);
|
|
94
|
-
this._buildMaterial = this._buildMaterial.bind(this);
|
|
95
77
|
this.speed = speed;
|
|
96
78
|
this.horizontalPressure = horizontalPressure;
|
|
97
79
|
this.verticalPressure = verticalPressure;
|
|
@@ -121,11 +103,6 @@ export class NeatGradient {
|
|
|
121
103
|
this.flowScale = flowScale;
|
|
122
104
|
this.flowEase = flowEase;
|
|
123
105
|
this.flowEnabled = flowEnabled;
|
|
124
|
-
// Mouse interaction
|
|
125
|
-
this.mouseDistortionStrength = mouseDistortionStrength;
|
|
126
|
-
this.mouseDistortionRadius = mouseDistortionRadius;
|
|
127
|
-
this.mouseDecayRate = mouseDecayRate;
|
|
128
|
-
this.mouseDarken = mouseDarken;
|
|
129
106
|
// Texture generation
|
|
130
107
|
this.enableProceduralTexture = enableProceduralTexture;
|
|
131
108
|
this.textureVoidLikelihood = textureVoidLikelihood;
|
|
@@ -140,14 +117,12 @@ export class NeatGradient {
|
|
|
140
117
|
this._textureShapeCircles = textureShapeCircles;
|
|
141
118
|
this._textureShapeBars = textureShapeBars;
|
|
142
119
|
this._textureShapeSquiggles = textureShapeSquiggles;
|
|
143
|
-
|
|
144
|
-
// This ensures u_mouse_texture isn't null during material compilation
|
|
145
|
-
this._setupMouseInteraction();
|
|
146
|
-
this.sceneState = this._initScene(resolution);
|
|
120
|
+
this.glState = this._initScene(resolution);
|
|
147
121
|
injectSEO();
|
|
148
122
|
let tick = seed !== undefined ? seed : getElapsedSecondsInLastHour();
|
|
123
|
+
let lastTime = performance.now();
|
|
149
124
|
const render = () => {
|
|
150
|
-
const {
|
|
125
|
+
const { gl, program, locations, indexCount, indexType } = this.glState;
|
|
151
126
|
// Optimization: check if cached link is still valid in DOM less frequently
|
|
152
127
|
this._linkCheckCounter++;
|
|
153
128
|
if (this._linkCheckCounter >= 300) { // Check every ~5 seconds at 60fps
|
|
@@ -156,134 +131,101 @@ export class NeatGradient {
|
|
|
156
131
|
this._linkElement = addNeatLink(ref);
|
|
157
132
|
}
|
|
158
133
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
134
|
+
if (this._initialized) {
|
|
135
|
+
const timeNow = performance.now();
|
|
136
|
+
tick += ((timeNow - lastTime) / 1000) * this._speed;
|
|
137
|
+
lastTime = timeNow;
|
|
138
|
+
gl.useProgram(program);
|
|
139
|
+
gl.uniform1f(locations.uniforms['u_time'], tick);
|
|
140
|
+
// Only upload static uniforms when they've been modified
|
|
141
|
+
if (this._uniformsDirty) {
|
|
142
|
+
gl.uniform2f(locations.uniforms['u_resolution'], this._ref.clientWidth, this._ref.clientHeight);
|
|
143
|
+
gl.uniform2f(locations.uniforms['u_color_pressure'], this._horizontalPressure, this._verticalPressure);
|
|
144
|
+
gl.uniform1f(locations.uniforms['u_wave_frequency_x'], this._waveFrequencyX);
|
|
145
|
+
gl.uniform1f(locations.uniforms['u_wave_frequency_y'], this._waveFrequencyY);
|
|
146
|
+
gl.uniform1f(locations.uniforms['u_wave_amplitude'], this._waveAmplitude);
|
|
147
|
+
gl.uniform1f(locations.uniforms['u_color_blending'], this._colorBlending);
|
|
148
|
+
gl.uniform1f(locations.uniforms['u_shadows'], this._shadows);
|
|
149
|
+
gl.uniform1f(locations.uniforms['u_highlights'], this._highlights);
|
|
150
|
+
gl.uniform1f(locations.uniforms['u_saturation'], this._saturation);
|
|
151
|
+
gl.uniform1f(locations.uniforms['u_brightness'], this._brightness);
|
|
152
|
+
gl.uniform1f(locations.uniforms['u_grain_intensity'], this._grainIntensity);
|
|
153
|
+
gl.uniform1f(locations.uniforms['u_grain_sparsity'], this._grainSparsity);
|
|
154
|
+
gl.uniform1f(locations.uniforms['u_grain_speed'], this._grainSpeed);
|
|
155
|
+
gl.uniform1f(locations.uniforms['u_grain_scale'], this._grainScale);
|
|
156
|
+
gl.uniform1f(locations.uniforms['u_y_offset'], this._yOffset);
|
|
157
|
+
gl.uniform1f(locations.uniforms['u_y_offset_wave_multiplier'], this._yOffsetWaveMultiplier);
|
|
158
|
+
gl.uniform1f(locations.uniforms['u_y_offset_color_multiplier'], this._yOffsetColorMultiplier);
|
|
159
|
+
gl.uniform1f(locations.uniforms['u_y_offset_flow_multiplier'], this._yOffsetFlowMultiplier);
|
|
160
|
+
gl.uniform1f(locations.uniforms['u_flow_distortion_a'], this._flowDistortionA);
|
|
161
|
+
gl.uniform1f(locations.uniforms['u_flow_distortion_b'], this._flowDistortionB);
|
|
162
|
+
gl.uniform1f(locations.uniforms['u_flow_scale'], this._flowScale);
|
|
163
|
+
gl.uniform1f(locations.uniforms['u_flow_ease'], this._flowEase);
|
|
164
|
+
gl.uniform1f(locations.uniforms['u_flow_enabled'], this._flowEnabled ? 1.0 : 0.0);
|
|
165
|
+
gl.uniform1f(locations.uniforms['u_enable_procedural_texture'], this._enableProceduralTexture ? 1.0 : 0.0);
|
|
166
|
+
gl.uniform1f(locations.uniforms['u_texture_ease'], this._textureEase);
|
|
167
|
+
this._uniformsDirty = false;
|
|
168
|
+
}
|
|
192
169
|
// Only regenerate procedural texture when needed
|
|
193
170
|
if (this._textureNeedsUpdate && this._enableProceduralTexture) {
|
|
194
171
|
if (this._proceduralTexture) {
|
|
195
|
-
this._proceduralTexture
|
|
172
|
+
gl.deleteTexture(this._proceduralTexture);
|
|
196
173
|
}
|
|
197
|
-
this._proceduralTexture = this._createProceduralTexture();
|
|
174
|
+
this._proceduralTexture = this._createProceduralTexture(gl);
|
|
198
175
|
this._textureNeedsUpdate = false;
|
|
176
|
+
this._textureDirty = true;
|
|
177
|
+
}
|
|
178
|
+
// Procedural texture binding — only when texture changes
|
|
179
|
+
if (this._textureDirty && this._proceduralTexture) {
|
|
180
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
181
|
+
gl.bindTexture(gl.TEXTURE_2D, this._proceduralTexture);
|
|
182
|
+
gl.uniform1i(locations.uniforms['u_procedural_texture'], 1);
|
|
183
|
+
this._textureDirty = false;
|
|
199
184
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
// Wireframe is a material property and must update every frame to avoid artifacts
|
|
203
|
-
// @ts-ignore - access material safely
|
|
204
|
-
this.sceneState.meshes[0].material.wireframe = this._wireframe;
|
|
205
|
-
// Optimized Color Update: Update immediately on change, or throttle to 10 times per second
|
|
206
|
-
const now = Date.now();
|
|
207
|
-
const shouldUpdate = this._colorsChanged || (now - this._lastColorUpdate > 100);
|
|
208
|
-
if (shouldUpdate) {
|
|
209
|
-
this._lastColorUpdate = now;
|
|
185
|
+
// Color update — only when colors have changed
|
|
186
|
+
if (this._colorsChanged) {
|
|
210
187
|
this._colorsChanged = false;
|
|
211
|
-
const shaderColors = u.u_colors.value;
|
|
212
188
|
for (let i = 0; i < COLORS_COUNT; i++) {
|
|
213
189
|
if (i < this._colors.length) {
|
|
214
190
|
const c = this._colors[i];
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
191
|
+
const rgb = this._cachedColorRgb[i] || [0, 0, 0];
|
|
192
|
+
gl.uniform1f(locations.uniforms[`u_colors[${i}].is_active`], c.enabled ? 1.0 : 0.0);
|
|
193
|
+
gl.uniform3fv(locations.uniforms[`u_colors[${i}].color`], rgb);
|
|
194
|
+
gl.uniform1f(locations.uniforms[`u_colors[${i}].influence`], c.influence || 0);
|
|
218
195
|
}
|
|
219
196
|
else {
|
|
220
|
-
|
|
197
|
+
gl.uniform1f(locations.uniforms[`u_colors[${i}].is_active`], 0.0);
|
|
221
198
|
}
|
|
222
199
|
}
|
|
223
|
-
|
|
200
|
+
gl.uniform1i(locations.uniforms['u_colors_count'], COLORS_COUNT);
|
|
224
201
|
}
|
|
225
202
|
}
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
// Decay only affects opacity
|
|
237
|
-
obj.mesh.material.opacity *= this._mouseDecayRate;
|
|
238
|
-
if (obj.mesh.material.opacity < 0.01) {
|
|
239
|
-
obj.mesh.visible = false;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
// Only render FBO if there are active brushes
|
|
245
|
-
if (hasActiveBrushes) {
|
|
246
|
-
// Store current clear color (likely the main background color)
|
|
247
|
-
renderer.getClearColor(this._tempClearColor);
|
|
248
|
-
const oldClearAlpha = renderer.getClearAlpha();
|
|
249
|
-
// Set clear color to Black/Transparent for the FBO.
|
|
250
|
-
renderer.setClearColor(0x000000, 0.0);
|
|
251
|
-
renderer.setRenderTarget(this._mouseFBO);
|
|
252
|
-
renderer.clear();
|
|
253
|
-
renderer.render(this._sceneMouse, this._cameraMouse);
|
|
254
|
-
renderer.setRenderTarget(null);
|
|
255
|
-
// Restore main background color for the actual scene render
|
|
256
|
-
renderer.setClearColor(this._tempClearColor, oldClearAlpha);
|
|
257
|
-
// Update mouse texture uniform
|
|
258
|
-
if (this._cachedUniforms) {
|
|
259
|
-
this._cachedUniforms.u_mouse_texture.value = this._mouseFBO.texture;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
203
|
+
// Draw scene
|
|
204
|
+
gl.clearColor(this._backgroundColorRgb[0], this._backgroundColorRgb[1], this._backgroundColorRgb[2], this._backgroundAlpha);
|
|
205
|
+
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
|
206
|
+
if (this._wireframe) {
|
|
207
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.glState.buffers.wireframeIndex);
|
|
208
|
+
gl.drawElements(gl.LINES, this.glState.wireframeIndexCount, indexType, 0);
|
|
209
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.glState.buffers.index);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
gl.drawElements(gl.TRIANGLES, indexCount, indexType, 0);
|
|
262
213
|
}
|
|
263
|
-
// Ensure we set the clear color for the main scene explicitly before rendering
|
|
264
|
-
renderer.setClearColor(this._backgroundColor, this._backgroundAlpha);
|
|
265
|
-
renderer.render(scene, camera);
|
|
266
214
|
this.requestRef = requestAnimationFrame(render);
|
|
267
215
|
};
|
|
268
216
|
const setSize = () => {
|
|
269
|
-
const {
|
|
270
|
-
const
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
this.
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
this._cameraMouse.left = -fSize * aspect;
|
|
282
|
-
this._cameraMouse.right = fSize * aspect;
|
|
283
|
-
this._cameraMouse.top = fSize;
|
|
284
|
-
this._cameraMouse.bottom = -fSize;
|
|
285
|
-
this._cameraMouse.updateProjectionMatrix();
|
|
286
|
-
}
|
|
217
|
+
const { gl, camera } = this.glState;
|
|
218
|
+
const width = this._ref.clientWidth;
|
|
219
|
+
const height = this._ref.clientHeight;
|
|
220
|
+
// Handle high DPI displays properly without scaling buffer resolution, matching client width
|
|
221
|
+
this._ref.width = width;
|
|
222
|
+
this._ref.height = height;
|
|
223
|
+
gl.viewport(0, 0, width, height);
|
|
224
|
+
updateCamera(camera, width, height);
|
|
225
|
+
// Recompute projection matrix on resize
|
|
226
|
+
const projLoc = gl.getUniformLocation(this.glState.program, "projectionMatrix");
|
|
227
|
+
gl.useProgram(this.glState.program);
|
|
228
|
+
gl.uniformMatrix4fv(projLoc, false, camera.projectionMatrix.elements);
|
|
287
229
|
};
|
|
288
230
|
// Debounce resize to prevent excessive operations
|
|
289
231
|
this.sizeObserver = new ResizeObserver(() => {
|
|
@@ -299,197 +241,220 @@ export class NeatGradient {
|
|
|
299
241
|
render();
|
|
300
242
|
}
|
|
301
243
|
destroy() {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
244
|
+
cancelAnimationFrame(this.requestRef);
|
|
245
|
+
this.sizeObserver.disconnect();
|
|
246
|
+
// Clear resize timeout
|
|
247
|
+
if (this._resizeTimeoutId !== null) {
|
|
248
|
+
clearTimeout(this._resizeTimeoutId);
|
|
249
|
+
this._resizeTimeoutId = null;
|
|
250
|
+
}
|
|
251
|
+
// Remove NEAT link
|
|
252
|
+
if (this._linkElement && this._linkElement.parentElement) {
|
|
253
|
+
this._linkElement.parentElement.removeChild(this._linkElement);
|
|
254
|
+
this._linkElement = null;
|
|
255
|
+
}
|
|
256
|
+
// Cleanup WebGL resources
|
|
257
|
+
if (this.glState) {
|
|
258
|
+
const gl = this.glState.gl;
|
|
259
|
+
gl.deleteProgram(this.glState.program);
|
|
260
|
+
gl.deleteBuffer(this.glState.buffers.position);
|
|
261
|
+
gl.deleteBuffer(this.glState.buffers.normal);
|
|
262
|
+
gl.deleteBuffer(this.glState.buffers.uv);
|
|
263
|
+
gl.deleteBuffer(this.glState.buffers.index);
|
|
264
|
+
gl.deleteBuffer(this.glState.buffers.wireframeIndex);
|
|
265
|
+
}
|
|
266
|
+
if (this._proceduralTexture && this.glState) {
|
|
267
|
+
this.glState.gl.deleteTexture(this._proceduralTexture);
|
|
325
268
|
}
|
|
326
269
|
}
|
|
327
270
|
downloadAsPNG(filename = "neat.png") {
|
|
328
|
-
console.log("Downloading as PNG", this._ref);
|
|
329
271
|
const dataURL = this._ref.toDataURL("image/png");
|
|
330
|
-
console.log("data", dataURL);
|
|
331
272
|
downloadURI(dataURL, filename);
|
|
332
273
|
}
|
|
333
274
|
set speed(speed) {
|
|
275
|
+
this._uniformsDirty = true;
|
|
334
276
|
this._speed = speed / 20;
|
|
335
277
|
}
|
|
336
278
|
set horizontalPressure(horizontalPressure) {
|
|
279
|
+
this._uniformsDirty = true;
|
|
337
280
|
this._horizontalPressure = horizontalPressure / 4;
|
|
338
281
|
}
|
|
339
282
|
set verticalPressure(verticalPressure) {
|
|
283
|
+
this._uniformsDirty = true;
|
|
340
284
|
this._verticalPressure = verticalPressure / 4;
|
|
341
285
|
}
|
|
342
286
|
set waveFrequencyX(waveFrequencyX) {
|
|
287
|
+
this._uniformsDirty = true;
|
|
343
288
|
this._waveFrequencyX = waveFrequencyX * 0.04;
|
|
344
289
|
}
|
|
345
290
|
set waveFrequencyY(waveFrequencyY) {
|
|
291
|
+
this._uniformsDirty = true;
|
|
346
292
|
this._waveFrequencyY = waveFrequencyY * 0.04;
|
|
347
293
|
}
|
|
348
294
|
set waveAmplitude(waveAmplitude) {
|
|
295
|
+
this._uniformsDirty = true;
|
|
349
296
|
this._waveAmplitude = waveAmplitude * .75;
|
|
350
297
|
}
|
|
351
298
|
set colors(colors) {
|
|
299
|
+
this._uniformsDirty = true;
|
|
352
300
|
this._colors = colors;
|
|
353
|
-
this.
|
|
301
|
+
this._cachedColorRgb = colors.map(c => this._hexToRgb(c.color));
|
|
302
|
+
this._colorsChanged = true;
|
|
354
303
|
}
|
|
355
304
|
set highlights(highlights) {
|
|
305
|
+
this._uniformsDirty = true;
|
|
356
306
|
this._highlights = highlights / 100;
|
|
357
307
|
}
|
|
358
308
|
set shadows(shadows) {
|
|
309
|
+
this._uniformsDirty = true;
|
|
359
310
|
this._shadows = shadows / 100;
|
|
360
311
|
}
|
|
361
312
|
set colorSaturation(colorSaturation) {
|
|
313
|
+
this._uniformsDirty = true;
|
|
362
314
|
this._saturation = colorSaturation / 10;
|
|
363
315
|
}
|
|
364
316
|
set colorBrightness(colorBrightness) {
|
|
317
|
+
this._uniformsDirty = true;
|
|
365
318
|
this._brightness = colorBrightness;
|
|
366
319
|
}
|
|
367
320
|
set colorBlending(colorBlending) {
|
|
321
|
+
this._uniformsDirty = true;
|
|
368
322
|
this._colorBlending = colorBlending / 10;
|
|
369
323
|
}
|
|
370
324
|
set grainScale(grainScale) {
|
|
325
|
+
this._uniformsDirty = true;
|
|
371
326
|
this._grainScale = grainScale == 0 ? 1 : grainScale;
|
|
372
327
|
}
|
|
373
328
|
set grainIntensity(grainIntensity) {
|
|
329
|
+
this._uniformsDirty = true;
|
|
374
330
|
this._grainIntensity = grainIntensity;
|
|
375
331
|
}
|
|
376
332
|
set grainSparsity(grainSparsity) {
|
|
333
|
+
this._uniformsDirty = true;
|
|
377
334
|
this._grainSparsity = grainSparsity;
|
|
378
335
|
}
|
|
379
336
|
set grainSpeed(grainSpeed) {
|
|
337
|
+
this._uniformsDirty = true;
|
|
380
338
|
this._grainSpeed = grainSpeed;
|
|
381
339
|
}
|
|
382
340
|
set wireframe(wireframe) {
|
|
341
|
+
this._uniformsDirty = true;
|
|
383
342
|
this._wireframe = wireframe;
|
|
384
343
|
}
|
|
385
344
|
set resolution(resolution) {
|
|
386
|
-
this.
|
|
345
|
+
this._uniformsDirty = true;
|
|
346
|
+
if (this.glState) {
|
|
347
|
+
const gl = this.glState.gl;
|
|
348
|
+
gl.deleteProgram(this.glState.program);
|
|
349
|
+
gl.deleteBuffer(this.glState.buffers.position);
|
|
350
|
+
gl.deleteBuffer(this.glState.buffers.normal);
|
|
351
|
+
gl.deleteBuffer(this.glState.buffers.uv);
|
|
352
|
+
gl.deleteBuffer(this.glState.buffers.index);
|
|
353
|
+
gl.deleteBuffer(this.glState.buffers.wireframeIndex);
|
|
354
|
+
}
|
|
355
|
+
this.glState = this._initScene(resolution);
|
|
387
356
|
}
|
|
388
357
|
set backgroundColor(backgroundColor) {
|
|
358
|
+
this._uniformsDirty = true;
|
|
389
359
|
this._backgroundColor = backgroundColor;
|
|
360
|
+
this._backgroundColorRgb = this._hexToRgb(backgroundColor);
|
|
390
361
|
}
|
|
391
362
|
set backgroundAlpha(backgroundAlpha) {
|
|
363
|
+
this._uniformsDirty = true;
|
|
392
364
|
this._backgroundAlpha = backgroundAlpha;
|
|
393
365
|
}
|
|
394
366
|
set yOffset(yOffset) {
|
|
367
|
+
this._uniformsDirty = true;
|
|
395
368
|
this._yOffset = yOffset;
|
|
396
369
|
}
|
|
397
370
|
get yOffsetWaveMultiplier() {
|
|
398
371
|
return this._yOffsetWaveMultiplier * 1000;
|
|
399
372
|
}
|
|
400
373
|
set yOffsetWaveMultiplier(value) {
|
|
374
|
+
this._uniformsDirty = true;
|
|
401
375
|
this._yOffsetWaveMultiplier = value / 1000;
|
|
402
376
|
}
|
|
403
377
|
get yOffsetColorMultiplier() {
|
|
404
378
|
return this._yOffsetColorMultiplier * 1000;
|
|
405
379
|
}
|
|
406
380
|
set yOffsetColorMultiplier(value) {
|
|
381
|
+
this._uniformsDirty = true;
|
|
407
382
|
this._yOffsetColorMultiplier = value / 1000;
|
|
408
383
|
}
|
|
409
384
|
get yOffsetFlowMultiplier() {
|
|
410
385
|
return this._yOffsetFlowMultiplier * 1000;
|
|
411
386
|
}
|
|
412
387
|
set yOffsetFlowMultiplier(value) {
|
|
388
|
+
this._uniformsDirty = true;
|
|
413
389
|
this._yOffsetFlowMultiplier = value / 1000;
|
|
414
390
|
}
|
|
415
391
|
set flowDistortionA(value) {
|
|
392
|
+
this._uniformsDirty = true;
|
|
416
393
|
this._flowDistortionA = value;
|
|
417
394
|
}
|
|
418
395
|
set flowDistortionB(value) {
|
|
396
|
+
this._uniformsDirty = true;
|
|
419
397
|
this._flowDistortionB = value;
|
|
420
398
|
}
|
|
421
399
|
set flowScale(value) {
|
|
400
|
+
this._uniformsDirty = true;
|
|
422
401
|
this._flowScale = value;
|
|
423
402
|
}
|
|
424
403
|
set flowEase(value) {
|
|
404
|
+
this._uniformsDirty = true;
|
|
425
405
|
this._flowEase = value;
|
|
426
406
|
}
|
|
427
407
|
set flowEnabled(value) {
|
|
408
|
+
this._uniformsDirty = true;
|
|
428
409
|
this._flowEnabled = value;
|
|
429
410
|
}
|
|
430
411
|
get flowEnabled() {
|
|
431
412
|
return this._flowEnabled;
|
|
432
413
|
}
|
|
433
|
-
set mouseDistortionStrength(value) {
|
|
434
|
-
this._mouseDistortionStrength = Math.max(0, value);
|
|
435
|
-
}
|
|
436
|
-
set mouseDistortionRadius(value) {
|
|
437
|
-
// Clamp to a sane range in UV space
|
|
438
|
-
this._mouseDistortionRadius = Math.max(0.01, Math.min(value, 1.0));
|
|
439
|
-
// Update brush scale when radius changes
|
|
440
|
-
this._updateBrushScale();
|
|
441
|
-
}
|
|
442
|
-
_updateBrushScale() {
|
|
443
|
-
if (!this._mouseObjects || this._mouseObjects.length === 0)
|
|
444
|
-
return;
|
|
445
|
-
// Radius directly controls the brush scale
|
|
446
|
-
// Base geometry is 200px, so radius 0.25 = 50px, radius 1.0 = 200px
|
|
447
|
-
this._mouseBrushBaseScale = this._mouseDistortionRadius;
|
|
448
|
-
}
|
|
449
|
-
set mouseDecayRate(value) {
|
|
450
|
-
// Clamp between 0.9 (slow decay, more wobble) and 0.99 (fast decay, less wobble)
|
|
451
|
-
this._mouseDecayRate = Math.max(0.9, Math.min(value, 0.99));
|
|
452
|
-
}
|
|
453
|
-
set mouseDarken(value) {
|
|
454
|
-
this._mouseDarken = value;
|
|
455
|
-
}
|
|
456
414
|
set enableProceduralTexture(value) {
|
|
415
|
+
this._uniformsDirty = true;
|
|
457
416
|
this._enableProceduralTexture = value;
|
|
458
417
|
if (value && !this._proceduralTexture) {
|
|
459
418
|
this._textureNeedsUpdate = true;
|
|
460
419
|
}
|
|
461
420
|
}
|
|
462
421
|
set textureVoidLikelihood(value) {
|
|
422
|
+
this._uniformsDirty = true;
|
|
463
423
|
this._textureVoidLikelihood = value;
|
|
464
424
|
if (this._enableProceduralTexture) {
|
|
465
425
|
this._textureNeedsUpdate = true;
|
|
466
426
|
}
|
|
467
427
|
}
|
|
468
428
|
set textureVoidWidthMin(value) {
|
|
429
|
+
this._uniformsDirty = true;
|
|
469
430
|
this._textureVoidWidthMin = value;
|
|
470
431
|
if (this._enableProceduralTexture) {
|
|
471
432
|
this._textureNeedsUpdate = true;
|
|
472
433
|
}
|
|
473
434
|
}
|
|
474
435
|
set textureVoidWidthMax(value) {
|
|
436
|
+
this._uniformsDirty = true;
|
|
475
437
|
this._textureVoidWidthMax = value;
|
|
476
438
|
if (this._enableProceduralTexture) {
|
|
477
439
|
this._textureNeedsUpdate = true;
|
|
478
440
|
}
|
|
479
441
|
}
|
|
480
442
|
set textureBandDensity(value) {
|
|
443
|
+
this._uniformsDirty = true;
|
|
481
444
|
this._textureBandDensity = value;
|
|
482
445
|
if (this._enableProceduralTexture) {
|
|
483
446
|
this._textureNeedsUpdate = true;
|
|
484
447
|
}
|
|
485
448
|
}
|
|
486
449
|
set textureColorBlending(value) {
|
|
450
|
+
this._uniformsDirty = true;
|
|
487
451
|
this._textureColorBlending = value;
|
|
488
452
|
if (this._enableProceduralTexture) {
|
|
489
453
|
this._textureNeedsUpdate = true;
|
|
490
454
|
}
|
|
491
455
|
}
|
|
492
456
|
set textureSeed(value) {
|
|
457
|
+
this._uniformsDirty = true;
|
|
493
458
|
this._textureSeed = value;
|
|
494
459
|
if (this._enableProceduralTexture) {
|
|
495
460
|
this._textureNeedsUpdate = true;
|
|
@@ -499,222 +464,195 @@ export class NeatGradient {
|
|
|
499
464
|
return this._textureEase;
|
|
500
465
|
}
|
|
501
466
|
set textureEase(value) {
|
|
467
|
+
this._uniformsDirty = true;
|
|
502
468
|
this._textureEase = value;
|
|
503
469
|
}
|
|
504
470
|
set proceduralBackgroundColor(value) {
|
|
471
|
+
this._uniformsDirty = true;
|
|
505
472
|
this._proceduralBackgroundColor = value;
|
|
506
473
|
if (this._enableProceduralTexture) {
|
|
507
474
|
this._textureNeedsUpdate = true;
|
|
508
475
|
}
|
|
509
476
|
}
|
|
510
477
|
set textureShapeTriangles(value) {
|
|
478
|
+
this._uniformsDirty = true;
|
|
511
479
|
this._textureShapeTriangles = value;
|
|
512
480
|
if (this._enableProceduralTexture)
|
|
513
481
|
this._textureNeedsUpdate = true;
|
|
514
482
|
}
|
|
515
483
|
set textureShapeCircles(value) {
|
|
484
|
+
this._uniformsDirty = true;
|
|
516
485
|
this._textureShapeCircles = value;
|
|
517
486
|
if (this._enableProceduralTexture)
|
|
518
487
|
this._textureNeedsUpdate = true;
|
|
519
488
|
}
|
|
520
489
|
set textureShapeBars(value) {
|
|
490
|
+
this._uniformsDirty = true;
|
|
521
491
|
this._textureShapeBars = value;
|
|
522
492
|
if (this._enableProceduralTexture)
|
|
523
493
|
this._textureNeedsUpdate = true;
|
|
524
494
|
}
|
|
525
495
|
set textureShapeSquiggles(value) {
|
|
496
|
+
this._uniformsDirty = true;
|
|
526
497
|
this._textureShapeSquiggles = value;
|
|
527
498
|
if (this._enableProceduralTexture)
|
|
528
499
|
this._textureNeedsUpdate = true;
|
|
529
500
|
}
|
|
501
|
+
_hexToRgb(hex) {
|
|
502
|
+
const bigint = parseInt(hex.replace('#', ''), 16);
|
|
503
|
+
return [
|
|
504
|
+
((bigint >> 16) & 255) / 255.0,
|
|
505
|
+
((bigint >> 8) & 255) / 255.0,
|
|
506
|
+
(bigint & 255) / 255.0
|
|
507
|
+
];
|
|
508
|
+
}
|
|
530
509
|
_initScene(resolution) {
|
|
531
|
-
const width = this._ref.
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
this.
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
if (Array.isArray(m.material))
|
|
538
|
-
m.material.forEach(mat => mat.dispose());
|
|
539
|
-
else
|
|
540
|
-
m.material.dispose();
|
|
541
|
-
});
|
|
510
|
+
const width = this._ref.clientWidth;
|
|
511
|
+
const height = this._ref.clientHeight;
|
|
512
|
+
const gl = this._ref.getContext("webgl2", { alpha: true, preserveDrawingBuffer: true, antialias: true }) ||
|
|
513
|
+
this._ref.getContext("webgl", { alpha: true, preserveDrawingBuffer: true, antialias: true });
|
|
514
|
+
if (!gl) {
|
|
515
|
+
throw new Error("WebGL not supported");
|
|
542
516
|
}
|
|
543
|
-
const
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
517
|
+
const ext = gl.getExtension("OES_standard_derivatives");
|
|
518
|
+
gl.getExtension("OES_element_index_uint");
|
|
519
|
+
gl.viewport(0, 0, width, height);
|
|
520
|
+
// Generate plane geometry with Uint32Array for large meshes
|
|
521
|
+
const { position, normal, uv, index, wireframeIndex } = generatePlaneGeometry(PLANE_WIDTH, PLANE_HEIGHT, 240 * resolution, 240 * resolution);
|
|
522
|
+
const positionBuffer = gl.createBuffer();
|
|
523
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
|
524
|
+
gl.bufferData(gl.ARRAY_BUFFER, position, gl.STATIC_DRAW);
|
|
525
|
+
const normalBuffer = gl.createBuffer();
|
|
526
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
|
|
527
|
+
gl.bufferData(gl.ARRAY_BUFFER, normal, gl.STATIC_DRAW);
|
|
528
|
+
const uvBuffer = gl.createBuffer();
|
|
529
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, uvBuffer);
|
|
530
|
+
gl.bufferData(gl.ARRAY_BUFFER, uv, gl.STATIC_DRAW);
|
|
531
|
+
const indexBuffer = gl.createBuffer();
|
|
532
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
|
|
533
|
+
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, index, gl.STATIC_DRAW);
|
|
534
|
+
const wireframeIndexBuffer = gl.createBuffer();
|
|
535
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, wireframeIndexBuffer);
|
|
536
|
+
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, wireframeIndex, gl.STATIC_DRAW);
|
|
537
|
+
// Rebind the triangle index buffer as default
|
|
538
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
|
|
539
|
+
const vertShaderSourceCombined = buildVertUniforms() + "\n" + buildNoise() + "\n" + buildColorFunctions() + "\n" + vertexShaderSource;
|
|
540
|
+
const vertShader = gl.createShader(gl.VERTEX_SHADER);
|
|
541
|
+
gl.shaderSource(vertShader, vertShaderSourceCombined);
|
|
542
|
+
gl.compileShader(vertShader);
|
|
543
|
+
if (!gl.getShaderParameter(vertShader, gl.COMPILE_STATUS)) {
|
|
544
|
+
console.log("VERTEX_SHADER_ERROR_START");
|
|
545
|
+
console.log("Vertex shader error: ", gl.getShaderInfoLog(vertShader));
|
|
546
|
+
console.log("GL Error Code:", gl.getError());
|
|
547
|
+
console.log("Vertex Shader Source Dump:");
|
|
548
|
+
console.log(vertShaderSourceCombined.split('\n').map((line, i) => `${i + 1}: ${line}`).join('\n'));
|
|
549
|
+
console.log("VERTEX_SHADER_ERROR_END");
|
|
550
|
+
}
|
|
551
|
+
const fragShaderSourceCombined = buildFragUniforms() + "\n" + buildColorFunctions() + "\n" + buildNoise() + "\n" + fragmentShaderSource;
|
|
552
|
+
const fragShader = gl.createShader(gl.FRAGMENT_SHADER);
|
|
553
|
+
gl.shaderSource(fragShader, fragShaderSourceCombined);
|
|
554
|
+
gl.compileShader(fragShader);
|
|
555
|
+
if (!gl.getShaderParameter(fragShader, gl.COMPILE_STATUS)) {
|
|
556
|
+
console.log("FRAGMENT_SHADER_ERROR_START");
|
|
557
|
+
console.log("Fragment shader error: ", gl.getShaderInfoLog(fragShader));
|
|
558
|
+
console.log("GL Error Code:", gl.getError());
|
|
559
|
+
console.log("Fragment Shader Source Dump:");
|
|
560
|
+
console.log(fragShaderSourceCombined.split('\n').map((line, i) => `${i + 1}: ${line}`).join('\n'));
|
|
561
|
+
console.log("FRAGMENT_SHADER_ERROR_END");
|
|
562
|
+
}
|
|
563
|
+
const program = gl.createProgram();
|
|
564
|
+
gl.attachShader(program, vertShader);
|
|
565
|
+
gl.attachShader(program, fragShader);
|
|
566
|
+
gl.linkProgram(program);
|
|
567
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
568
|
+
console.log("PROGRAM_LINK_ERROR_START");
|
|
569
|
+
console.log("Program linking error: ", gl.getProgramInfoLog(program));
|
|
570
|
+
console.log("GL Error Code:", gl.getError());
|
|
571
|
+
console.log("PROGRAM_LINK_ERROR_END");
|
|
572
|
+
}
|
|
573
|
+
gl.useProgram(program);
|
|
574
|
+
const camera = new OrthographicCamera(0, 0, 0, 0, 0, 1000);
|
|
575
|
+
camera.position = [0, 0, 5];
|
|
563
576
|
updateCamera(camera, width, height);
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
577
|
+
// Define attributes
|
|
578
|
+
const aPosition = gl.getAttribLocation(program, "position");
|
|
579
|
+
const aNormal = gl.getAttribLocation(program, "normal");
|
|
580
|
+
const aUv = gl.getAttribLocation(program, "uv");
|
|
581
|
+
gl.enableVertexAttribArray(aPosition);
|
|
582
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
|
583
|
+
gl.vertexAttribPointer(aPosition, 3, gl.FLOAT, false, 0, 0);
|
|
584
|
+
gl.enableVertexAttribArray(aNormal);
|
|
585
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
|
|
586
|
+
gl.vertexAttribPointer(aNormal, 3, gl.FLOAT, false, 0, 0);
|
|
587
|
+
gl.enableVertexAttribArray(aUv);
|
|
588
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, uvBuffer);
|
|
589
|
+
gl.vertexAttribPointer(aUv, 2, gl.FLOAT, false, 0, 0);
|
|
590
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
|
|
591
|
+
const modelViewMatrix = new Matrix4();
|
|
592
|
+
// The View Matrix is the inverse of the Camera's position
|
|
593
|
+
// Camera is at [0, 0, 5], so view matrix translates by [0, 0, -5]
|
|
594
|
+
modelViewMatrix.translate(-camera.position[0], -camera.position[1], -camera.position[2]);
|
|
595
|
+
// The Model Matrix mimicking: plane.rotation.x = -Math.PI / 3.5; plane.position.z = -1;
|
|
596
|
+
modelViewMatrix.translate(0, 0, -1);
|
|
597
|
+
modelViewMatrix.rotateX(-Math.PI / 3.5);
|
|
598
|
+
const mvLoc = gl.getUniformLocation(program, "modelViewMatrix");
|
|
599
|
+
gl.uniformMatrix4fv(mvLoc, false, modelViewMatrix.elements);
|
|
600
|
+
const projLoc = gl.getUniformLocation(program, "projectionMatrix");
|
|
601
|
+
gl.uniformMatrix4fv(projLoc, false, camera.projectionMatrix.elements);
|
|
602
|
+
const planeWidthLoc = gl.getUniformLocation(program, "u_plane_width");
|
|
603
|
+
gl.uniform1f(planeWidthLoc, PLANE_WIDTH);
|
|
604
|
+
const planeHeightLoc = gl.getUniformLocation(program, "u_plane_height");
|
|
605
|
+
gl.uniform1f(planeHeightLoc, PLANE_HEIGHT);
|
|
606
|
+
const colorsCountLoc = gl.getUniformLocation(program, "u_colors_count");
|
|
607
|
+
gl.uniform1i(colorsCountLoc, COLORS_COUNT);
|
|
608
|
+
const uniformsList = [
|
|
609
|
+
"u_time", "u_resolution", "u_color_pressure", "u_wave_frequency_x", "u_wave_frequency_y",
|
|
610
|
+
"u_wave_amplitude", "u_colors_count", "u_plane_width", "u_plane_height", "u_shadows",
|
|
611
|
+
"u_highlights", "u_grain_intensity", "u_grain_sparsity", "u_grain_scale", "u_grain_speed",
|
|
612
|
+
"u_flow_distortion_a", "u_flow_distortion_b", "u_flow_scale", "u_flow_ease", "u_flow_enabled",
|
|
613
|
+
"u_y_offset", "u_y_offset_wave_multiplier", "u_y_offset_color_multiplier", "u_y_offset_flow_multiplier",
|
|
614
|
+
"u_procedural_texture", "u_enable_procedural_texture", "u_texture_ease", "u_saturation", "u_brightness", "u_color_blending"
|
|
615
|
+
];
|
|
616
|
+
const locations = {
|
|
617
|
+
attributes: { position: aPosition, normal: aNormal, uv: aUv },
|
|
618
|
+
uniforms: {}
|
|
570
619
|
};
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
// Initialize stable array structure for colors
|
|
574
|
-
// We create 6 objects and just update them in the render loop to avoid GC
|
|
575
|
-
const colors = Array.from({ length: COLORS_COUNT }).map((_, i) => ({
|
|
576
|
-
is_active: i < this._colors.length ? (this._colors[i].enabled ? 1.0 : 0.0) : 0.0,
|
|
577
|
-
color: new THREE.Color(i < this._colors.length ? this._colors[i].color : 0x000000),
|
|
578
|
-
influence: i < this._colors.length ? (this._colors[i].influence || 0) : 0
|
|
579
|
-
}));
|
|
580
|
-
const uniforms = {
|
|
581
|
-
u_time: { value: 0 },
|
|
582
|
-
u_color_pressure: { value: new THREE.Vector2(this._horizontalPressure, this._verticalPressure) },
|
|
583
|
-
u_wave_frequency_x: { value: this._waveFrequencyX },
|
|
584
|
-
u_wave_frequency_y: { value: this._waveFrequencyY },
|
|
585
|
-
u_wave_amplitude: { value: this._waveAmplitude },
|
|
586
|
-
u_resolution: { value: new THREE.Vector2(width, height) },
|
|
587
|
-
u_colors: { value: colors },
|
|
588
|
-
u_colors_count: { value: this._colors.length },
|
|
589
|
-
u_plane_width: { value: PLANE_WIDTH },
|
|
590
|
-
u_plane_height: { value: PLANE_HEIGHT },
|
|
591
|
-
u_shadows: { value: this._shadows },
|
|
592
|
-
u_highlights: { value: this._highlights },
|
|
593
|
-
u_grain_intensity: { value: this._grainIntensity },
|
|
594
|
-
u_grain_sparsity: { value: this._grainSparsity },
|
|
595
|
-
u_grain_scale: { value: this._grainScale },
|
|
596
|
-
u_grain_speed: { value: this._grainSpeed },
|
|
597
|
-
// Flow field
|
|
598
|
-
u_flow_distortion_a: { value: this._flowDistortionA },
|
|
599
|
-
u_flow_distortion_b: { value: this._flowDistortionB },
|
|
600
|
-
u_flow_scale: { value: this._flowScale },
|
|
601
|
-
u_flow_ease: { value: this._flowEase },
|
|
602
|
-
u_flow_enabled: { value: this._flowEnabled ? 1.0 : 0.0 },
|
|
603
|
-
// Y offset multipliers
|
|
604
|
-
u_y_offset: { value: this._yOffset },
|
|
605
|
-
u_y_offset_wave_multiplier: { value: this._yOffsetWaveMultiplier },
|
|
606
|
-
u_y_offset_color_multiplier: { value: this._yOffsetColorMultiplier },
|
|
607
|
-
u_y_offset_flow_multiplier: { value: this._yOffsetFlowMultiplier },
|
|
608
|
-
// Mouse interaction
|
|
609
|
-
u_mouse_distortion_strength: { value: this._mouseDistortionStrength },
|
|
610
|
-
u_mouse_distortion_radius: { value: this._mouseDistortionRadius },
|
|
611
|
-
u_mouse_darken: { value: this._mouseDarken },
|
|
612
|
-
u_mouse_texture: { value: this._mouseFBO ? this._mouseFBO.texture : null },
|
|
613
|
-
// Procedural texture
|
|
614
|
-
u_procedural_texture: { value: this._proceduralTexture },
|
|
615
|
-
u_enable_procedural_texture: { value: this._enableProceduralTexture ? 1.0 : 0.0 },
|
|
616
|
-
u_texture_ease: { value: this._textureEase },
|
|
617
|
-
u_saturation: { value: this._saturation },
|
|
618
|
-
u_brightness: { value: this._brightness },
|
|
619
|
-
u_color_blending: { value: this._colorBlending }
|
|
620
|
-
};
|
|
621
|
-
const material = new THREE.ShaderMaterial({
|
|
622
|
-
uniforms: uniforms,
|
|
623
|
-
vertexShader: buildUniforms() + buildNoise() + buildColorFunctions() + buildVertexShader(),
|
|
624
|
-
fragmentShader: buildUniforms() + buildColorFunctions() + buildNoise() + buildFragmentShader()
|
|
620
|
+
uniformsList.forEach(name => {
|
|
621
|
+
locations.uniforms[name] = gl.getUniformLocation(program, name);
|
|
625
622
|
});
|
|
626
|
-
//
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
_setupMouseInteraction() {
|
|
632
|
-
if (!this._ref)
|
|
633
|
-
return;
|
|
634
|
-
const width = this._ref.width;
|
|
635
|
-
const height = this._ref.height;
|
|
636
|
-
// Create mouse FBO
|
|
637
|
-
this._mouseFBO = new THREE.WebGLRenderTarget(width / 2, height / 2);
|
|
638
|
-
// Create mouse scene and camera
|
|
639
|
-
this._sceneMouse = new THREE.Scene();
|
|
640
|
-
const fSize = height / 2;
|
|
641
|
-
const aspect = width / height;
|
|
642
|
-
// FIX 4: Ensure near plane allows viewing objects at Z=0
|
|
643
|
-
// Near -100 is safer for objects at 0
|
|
644
|
-
this._cameraMouse = new THREE.OrthographicCamera(-fSize * aspect, fSize * aspect, fSize, -fSize, 0, 10000);
|
|
645
|
-
this._cameraMouse.position.set(0, 0, 100);
|
|
646
|
-
// Create brush texture - More visible and impactful
|
|
647
|
-
const brushCanvas = document.createElement('canvas');
|
|
648
|
-
brushCanvas.width = 128;
|
|
649
|
-
brushCanvas.height = 128;
|
|
650
|
-
const bCtx = brushCanvas.getContext('2d');
|
|
651
|
-
if (bCtx) {
|
|
652
|
-
const grd = bCtx.createRadialGradient(64, 64, 0, 64, 64, 64);
|
|
653
|
-
// Match reference implementation's stronger gradient
|
|
654
|
-
grd.addColorStop(0, 'rgba(255,255,255,0.8)');
|
|
655
|
-
grd.addColorStop(0.5, 'rgba(255,255,255,0.4)');
|
|
656
|
-
grd.addColorStop(1, 'rgba(255,255,255,0)');
|
|
657
|
-
bCtx.fillStyle = grd;
|
|
658
|
-
bCtx.fillRect(0, 0, 128, 128);
|
|
623
|
+
// Add colors uniforms manually
|
|
624
|
+
for (let i = 0; i < COLORS_COUNT; i++) {
|
|
625
|
+
locations.uniforms[`u_colors[${i}].is_active`] = gl.getUniformLocation(program, `u_colors[${i}].is_active`);
|
|
626
|
+
locations.uniforms[`u_colors[${i}].color`] = gl.getUniformLocation(program, `u_colors[${i}].color`);
|
|
627
|
+
locations.uniforms[`u_colors[${i}].influence`] = gl.getUniformLocation(program, `u_colors[${i}].influence`);
|
|
659
628
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
if (!this._ref || !this._sceneMouse)
|
|
685
|
-
return;
|
|
686
|
-
const rect = this._ref.getBoundingClientRect();
|
|
687
|
-
const width = this._ref.width;
|
|
688
|
-
const height = this._ref.height;
|
|
689
|
-
// Store pending mouse position
|
|
690
|
-
this._pendingMousePosition = {
|
|
691
|
-
x: e.clientX - rect.left - width / 2,
|
|
692
|
-
y: -(e.clientY - rect.top - height / 2)
|
|
629
|
+
this._initialized = true;
|
|
630
|
+
// New program needs all uniforms re-uploaded on first frame
|
|
631
|
+
this._uniformsDirty = true;
|
|
632
|
+
this._colorsChanged = true;
|
|
633
|
+
this._textureDirty = true;
|
|
634
|
+
// Enable alpha blending
|
|
635
|
+
gl.enable(gl.BLEND);
|
|
636
|
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
637
|
+
gl.enable(gl.DEPTH_TEST);
|
|
638
|
+
return {
|
|
639
|
+
gl,
|
|
640
|
+
program,
|
|
641
|
+
buffers: {
|
|
642
|
+
position: positionBuffer,
|
|
643
|
+
normal: normalBuffer,
|
|
644
|
+
uv: uvBuffer,
|
|
645
|
+
index: indexBuffer,
|
|
646
|
+
wireframeIndex: wireframeIndexBuffer
|
|
647
|
+
},
|
|
648
|
+
locations,
|
|
649
|
+
camera,
|
|
650
|
+
indexCount: index.length,
|
|
651
|
+
wireframeIndexCount: wireframeIndex.length,
|
|
652
|
+
indexType: (index instanceof Uint32Array) ? gl.UNSIGNED_INT : gl.UNSIGNED_SHORT
|
|
693
653
|
};
|
|
694
|
-
// Batch mouse updates using requestAnimationFrame
|
|
695
|
-
if (!this._mouseUpdateScheduled) {
|
|
696
|
-
this._mouseUpdateScheduled = true;
|
|
697
|
-
requestAnimationFrame(() => {
|
|
698
|
-
this._mouseUpdateScheduled = false;
|
|
699
|
-
if (!this._pendingMousePosition)
|
|
700
|
-
return;
|
|
701
|
-
this._mouse.x = this._pendingMousePosition.x;
|
|
702
|
-
this._mouse.y = this._pendingMousePosition.y;
|
|
703
|
-
const brush = this._mouseObjects[this._currentBrush];
|
|
704
|
-
brush.mesh.scale.set(this._mouseBrushBaseScale, this._mouseBrushBaseScale, 1.0);
|
|
705
|
-
brush.active = true;
|
|
706
|
-
brush.mesh.visible = true;
|
|
707
|
-
brush.mesh.position.set(this._mouse.x, this._mouse.y, 0);
|
|
708
|
-
brush.mesh.rotation.z = Math.random() * Math.PI * 2;
|
|
709
|
-
if (brush.mesh.material instanceof THREE.MeshBasicMaterial) {
|
|
710
|
-
brush.mesh.material.opacity = 1.0;
|
|
711
|
-
}
|
|
712
|
-
this._currentBrush = (this._currentBrush + 1) % this._mouseObjects.length;
|
|
713
|
-
this._pendingMousePosition = null;
|
|
714
|
-
});
|
|
715
|
-
}
|
|
716
654
|
}
|
|
717
|
-
_createProceduralTexture() {
|
|
655
|
+
_createProceduralTexture(gl) {
|
|
718
656
|
// Texture size - 1024 provides good balance between quality and performance
|
|
719
657
|
// Reduced from 2048 for better performance
|
|
720
658
|
const texSize = 1024;
|
|
@@ -723,7 +661,7 @@ export class NeatGradient {
|
|
|
723
661
|
sourceCanvas.height = texSize;
|
|
724
662
|
const sCtx = sourceCanvas.getContext('2d', { willReadFrequently: true });
|
|
725
663
|
if (!sCtx)
|
|
726
|
-
return
|
|
664
|
+
return null;
|
|
727
665
|
let seed = this._textureSeed;
|
|
728
666
|
const baseSeed = this._textureSeed;
|
|
729
667
|
function random() {
|
|
@@ -736,7 +674,7 @@ export class NeatGradient {
|
|
|
736
674
|
};
|
|
737
675
|
const colors = this._colors.filter(c => c.enabled).map(c => c.color);
|
|
738
676
|
if (colors.length === 0)
|
|
739
|
-
return
|
|
677
|
+
return null;
|
|
740
678
|
// Helper functions
|
|
741
679
|
function hexToRgb(hex) {
|
|
742
680
|
const bigint = parseInt(hex.replace('#', ''), 16);
|
|
@@ -747,7 +685,7 @@ export class NeatGradient {
|
|
|
747
685
|
};
|
|
748
686
|
}
|
|
749
687
|
function rgbToHex(r, g, b) {
|
|
750
|
-
return "#" + ((1 << 24) + (Math.round(r) << 16) + (Math.round(g) << 8) + Math.round(b)).toString(16).slice(1);
|
|
688
|
+
return "#" + ((1 << 24) + (Math.round(r) << 16) + (Math.round(g) << 8) + Math.round(b)).toString(16).slice(1).padStart(6, '0');
|
|
751
689
|
}
|
|
752
690
|
const getInterColor = () => {
|
|
753
691
|
const c1 = colors[Math.floor(random() * colors.length)];
|
|
@@ -827,7 +765,7 @@ export class NeatGradient {
|
|
|
827
765
|
canvas.height = texSize;
|
|
828
766
|
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
|
829
767
|
if (!ctx)
|
|
830
|
-
return
|
|
768
|
+
return null;
|
|
831
769
|
// Start filled with the chosen void color so gaps show that color
|
|
832
770
|
ctx.fillStyle = baseColor;
|
|
833
771
|
ctx.fillRect(0, 0, texSize, texSize);
|
|
@@ -862,519 +800,24 @@ export class NeatGradient {
|
|
|
862
800
|
}
|
|
863
801
|
// void segments: leave as baseColor
|
|
864
802
|
}
|
|
865
|
-
const tex =
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
function updateCamera(camera, width, height) {
|
|
880
|
-
const viewPortAreaRatio = 1000000;
|
|
881
|
-
const areaViewPort = width * height;
|
|
882
|
-
const targetPlaneArea = areaViewPort / viewPortAreaRatio *
|
|
883
|
-
PLANE_WIDTH * PLANE_HEIGHT / 1.5;
|
|
884
|
-
const ratio = width / height;
|
|
885
|
-
const targetWidth = Math.sqrt(targetPlaneArea * ratio);
|
|
886
|
-
const targetHeight = targetPlaneArea / targetWidth;
|
|
887
|
-
let left = -PLANE_WIDTH / 2;
|
|
888
|
-
let right = Math.min((left + targetWidth) / 1.5, PLANE_WIDTH / 2);
|
|
889
|
-
let top = PLANE_HEIGHT / 4;
|
|
890
|
-
let bottom = Math.max((top - targetHeight) / 2, -PLANE_HEIGHT / 4);
|
|
891
|
-
// Fix for mobile portrait: adjust bounds for proper aspect ratio AND zoom out slightly
|
|
892
|
-
if (ratio < 1) {
|
|
893
|
-
// Portrait mode - scale horizontal bounds by aspect ratio to prevent stretching
|
|
894
|
-
const horizontalScale = ratio;
|
|
895
|
-
left = left * horizontalScale;
|
|
896
|
-
right = right * horizontalScale;
|
|
897
|
-
// Zoom out slightly on mobile (1.1 = 10% zoom out)
|
|
898
|
-
const mobileZoomFactor = 1.05;
|
|
899
|
-
left = left * mobileZoomFactor;
|
|
900
|
-
right = right * mobileZoomFactor;
|
|
901
|
-
top = top * mobileZoomFactor;
|
|
902
|
-
bottom = bottom * mobileZoomFactor;
|
|
903
|
-
}
|
|
904
|
-
const near = -100;
|
|
905
|
-
const far = 1000;
|
|
906
|
-
if (camera instanceof THREE.OrthographicCamera) {
|
|
907
|
-
camera.left = left;
|
|
908
|
-
camera.right = right;
|
|
909
|
-
camera.top = top;
|
|
910
|
-
camera.bottom = bottom;
|
|
911
|
-
camera.near = near;
|
|
912
|
-
camera.far = far;
|
|
913
|
-
camera.updateProjectionMatrix();
|
|
914
|
-
}
|
|
915
|
-
else if (camera instanceof THREE.PerspectiveCamera) {
|
|
916
|
-
camera.aspect = width / height;
|
|
917
|
-
camera.updateProjectionMatrix();
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
// Cache shader strings to avoid repeated concatenation
|
|
921
|
-
let cachedVertexShader = null;
|
|
922
|
-
let cachedFragmentShader = null;
|
|
923
|
-
function buildVertexShader() {
|
|
924
|
-
if (cachedVertexShader)
|
|
925
|
-
return cachedVertexShader;
|
|
926
|
-
cachedVertexShader = `
|
|
927
|
-
void main() {
|
|
928
|
-
vUv = uv;
|
|
929
|
-
|
|
930
|
-
// SCROLLING LOGIC
|
|
931
|
-
// Separate multipliers for wave, color, and flow offsets
|
|
932
|
-
float waveOffset = -u_y_offset * u_y_offset_wave_multiplier;
|
|
933
|
-
float colorOffset = -u_y_offset * u_y_offset_color_multiplier;
|
|
934
|
-
float flowOffset = -u_y_offset * u_y_offset_flow_multiplier;
|
|
935
|
-
|
|
936
|
-
// 1. DISPLACEMENT (WAVES)
|
|
937
|
-
// We add waveOffset to Y to scroll the wave pattern
|
|
938
|
-
v_displacement_amount = cnoise( vec3(
|
|
939
|
-
u_wave_frequency_x * position.x + u_time,
|
|
940
|
-
u_wave_frequency_y * (position.y + waveOffset) + u_time,
|
|
941
|
-
u_time
|
|
942
|
-
));
|
|
943
|
-
|
|
944
|
-
// 2. FLOW FIELD
|
|
945
|
-
// Apply flow offset to scroll the flow field mask
|
|
946
|
-
vec2 baseUv = vUv;
|
|
947
|
-
baseUv.y += flowOffset / u_plane_height; // Scale to match wave speed
|
|
948
|
-
vec2 flowUv = baseUv;
|
|
949
|
-
|
|
950
|
-
if (u_flow_enabled > 0.5) {
|
|
951
|
-
if (u_flow_ease > 0.0 || u_flow_distortion_a > 0.0) {
|
|
952
|
-
vec2 ppp = -1.0 + 2.0 * baseUv;
|
|
953
|
-
ppp += 0.1 * cos((1.5 * u_flow_scale) * ppp.yx + 1.1 * u_time + vec2(0.1, 1.1));
|
|
954
|
-
ppp += 0.1 * cos((2.3 * u_flow_scale) * ppp.yx + 1.3 * u_time + vec2(3.2, 3.4));
|
|
955
|
-
ppp += 0.1 * cos((2.2 * u_flow_scale) * ppp.yx + 1.7 * u_time + vec2(1.8, 5.2));
|
|
956
|
-
ppp += u_flow_distortion_a * cos((u_flow_distortion_b * u_flow_scale) * ppp.yx + 1.4 * u_time + vec2(6.3, 3.9));
|
|
957
|
-
|
|
958
|
-
float r = length(ppp);
|
|
959
|
-
flowUv = mix(baseUv, vec2(baseUv.x * (1.0 - u_flow_ease) + r * u_flow_ease, baseUv.y), u_flow_ease);
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
// Pass the standard flow UV to fragment shader (for mouse/texture)
|
|
964
|
-
vFlowUv = flowUv;
|
|
965
|
-
|
|
966
|
-
// 3. COLOR MIXING
|
|
967
|
-
// We take the computed flow UVs and apply the color offset
|
|
968
|
-
// Scale by plane height to match wave offset speed (world space vs UV space)
|
|
969
|
-
vec3 color = u_colors[0].color;
|
|
970
|
-
vec2 adjustedUv = flowUv;
|
|
971
|
-
adjustedUv.y += colorOffset / u_plane_height; // Scroll the color mixing pattern
|
|
972
|
-
|
|
973
|
-
vec2 noise_cord = adjustedUv * u_color_pressure;
|
|
974
|
-
const float minNoise = .0;
|
|
975
|
-
const float maxNoise = .9;
|
|
976
|
-
|
|
977
|
-
for (int i = 1; i < u_colors_count; i++) {
|
|
978
|
-
if(u_colors[i].is_active > 0.5){
|
|
979
|
-
float noiseFlow = (1. + float(i)) / 30.;
|
|
980
|
-
float noiseSpeed = (1. + float(i)) * 0.11;
|
|
981
|
-
float noiseSeed = 13. + float(i) * 7.;
|
|
982
|
-
|
|
983
|
-
float noise = snoise(
|
|
984
|
-
vec3(
|
|
985
|
-
noise_cord.x * u_color_pressure.x + u_time * noiseFlow * 2.,
|
|
986
|
-
noise_cord.y * u_color_pressure.y,
|
|
987
|
-
u_time * noiseSpeed
|
|
988
|
-
) + noiseSeed
|
|
989
|
-
) - (.1 * float(i)) + (.5 * u_color_blending);
|
|
990
|
-
|
|
991
|
-
noise = clamp(minNoise, maxNoise + float(i) * 0.02, noise);
|
|
992
|
-
color = mix(color, u_colors[i].color, smoothstep(0.0, u_color_blending, noise));
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
v_color = color;
|
|
997
|
-
|
|
998
|
-
// 4. VERTEX POSITION
|
|
999
|
-
vec3 newPosition = position + normal * v_displacement_amount * u_wave_amplitude;
|
|
1000
|
-
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
|
|
1001
|
-
v_new_position = gl_Position;
|
|
1002
|
-
}
|
|
1003
|
-
`;
|
|
1004
|
-
return cachedVertexShader;
|
|
1005
|
-
}
|
|
1006
|
-
function buildFragmentShader() {
|
|
1007
|
-
if (cachedFragmentShader)
|
|
1008
|
-
return cachedFragmentShader;
|
|
1009
|
-
cachedFragmentShader = `
|
|
1010
|
-
float random(vec2 p) {
|
|
1011
|
-
return fract(sin(dot(p, vec2(12.9898,78.233))) * 43758.5453);
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
float fbm(vec3 x) {
|
|
1015
|
-
float value = 0.0;
|
|
1016
|
-
float amplitude = 0.5;
|
|
1017
|
-
float frequency = 1.0;
|
|
1018
|
-
for (int i = 0; i < 4; i++) {
|
|
1019
|
-
value += amplitude * snoise(x * frequency);
|
|
1020
|
-
frequency *= 2.0;
|
|
1021
|
-
amplitude *= 0.5;
|
|
1022
|
-
}
|
|
1023
|
-
return value;
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
void main() {
|
|
1027
|
-
// MOUSE DISTORTION
|
|
1028
|
-
vec2 finalUv = vFlowUv;
|
|
1029
|
-
|
|
1030
|
-
if (u_mouse_distortion_strength > 0.0) {
|
|
1031
|
-
vec4 mouseColor = texture2D(u_mouse_texture, vUv);
|
|
1032
|
-
float mouseValue = mouseColor.r;
|
|
1033
|
-
|
|
1034
|
-
if (mouseValue > 0.001) {
|
|
1035
|
-
float distortionAmount = mouseValue * u_mouse_distortion_strength;
|
|
1036
|
-
vec2 mouseDisp = vec2(distortionAmount, distortionAmount);
|
|
1037
|
-
finalUv -= mouseDisp;
|
|
803
|
+
const tex = gl.createTexture();
|
|
804
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
805
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
|
|
806
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
|
|
807
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
|
|
808
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
|
|
809
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
810
|
+
gl.generateMipmap(gl.TEXTURE_2D);
|
|
811
|
+
const ext = gl.getExtension('EXT_texture_filter_anisotropic') ||
|
|
812
|
+
gl.getExtension('MOZ_EXT_texture_filter_anisotropic') ||
|
|
813
|
+
gl.getExtension('WEBKIT_EXT_texture_filter_anisotropic');
|
|
814
|
+
if (ext) {
|
|
815
|
+
const max = gl.getParameter(ext.MAX_TEXTURE_MAX_ANISOTROPY_EXT);
|
|
816
|
+
gl.texParameterf(gl.TEXTURE_2D, ext.TEXTURE_MAX_ANISOTROPY_EXT, Math.min(16, max));
|
|
1038
817
|
}
|
|
818
|
+
return tex;
|
|
1039
819
|
}
|
|
1040
|
-
|
|
1041
|
-
vec3 baseColor;
|
|
1042
|
-
|
|
1043
|
-
if (u_enable_procedural_texture > 0.5) {
|
|
1044
|
-
// Calculate flow field distance for ease effect
|
|
1045
|
-
vec2 ppp = -1.0 + 2.0 * finalUv;
|
|
1046
|
-
ppp += 0.1 * cos((1.5 * u_flow_scale) * ppp.yx + 1.1 * u_time + vec2(0.1, 1.1));
|
|
1047
|
-
ppp += 0.1 * cos((2.3 * u_flow_scale) * ppp.yx + 1.3 * u_time + vec2(3.2, 3.4));
|
|
1048
|
-
ppp += 0.1 * cos((2.2 * u_flow_scale) * ppp.yx + 1.7 * u_time + vec2(1.8, 5.2));
|
|
1049
|
-
ppp += u_flow_distortion_a * cos((u_flow_distortion_b * u_flow_scale) * ppp.yx + 1.4 * u_time + vec2(6.3, 3.9));
|
|
1050
|
-
float r = length(ppp); // Flow distance
|
|
1051
|
-
|
|
1052
|
-
// Ease blending: 0 = topographic (flow), 1 = image (UV)
|
|
1053
|
-
float vx = (finalUv.x * u_texture_ease) + (r * (1.0 - u_texture_ease));
|
|
1054
|
-
float vy = (finalUv.y * u_texture_ease) + (0.0 * (1.0 - u_texture_ease));
|
|
1055
|
-
vec2 texUv = vec2(vx, vy);
|
|
1056
|
-
|
|
1057
|
-
// PARALLAX SCROLLING
|
|
1058
|
-
// We manually apply a smaller offset here to make the texture lag behind
|
|
1059
|
-
float parallaxFactor = 0.25; // 25% speed of the color mixing
|
|
1060
|
-
texUv.y -= (u_y_offset * u_y_offset_color_multiplier / u_plane_height) * parallaxFactor;
|
|
1061
|
-
|
|
1062
|
-
texUv *= 1.5; // Tiling scale
|
|
1063
|
-
|
|
1064
|
-
vec4 texSample = texture2D(u_procedural_texture, texUv);
|
|
1065
|
-
baseColor = texSample.rgb;
|
|
1066
|
-
} else {
|
|
1067
|
-
baseColor = v_color;
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
vec3 color = baseColor;
|
|
1071
|
-
|
|
1072
|
-
// Post-processing
|
|
1073
|
-
color += pow(v_displacement_amount, 1.0) * u_highlights;
|
|
1074
|
-
color -= pow(1.0 - v_displacement_amount, 2.0) * u_shadows;
|
|
1075
|
-
color = saturation(color, 1.0 + u_saturation);
|
|
1076
|
-
color = color * u_brightness;
|
|
1077
|
-
|
|
1078
|
-
// Grain
|
|
1079
|
-
vec2 noiseCoords = gl_FragCoord.xy / u_grain_scale;
|
|
1080
|
-
float grain = (u_grain_speed != 0.0) ? fbm(vec3(noiseCoords, u_time * u_grain_speed)) : fbm(vec3(noiseCoords, 0.0));
|
|
1081
|
-
|
|
1082
|
-
grain = grain * 0.5 + 0.5;
|
|
1083
|
-
grain -= 0.5;
|
|
1084
|
-
grain = (grain > u_grain_sparsity) ? grain : 0.0;
|
|
1085
|
-
grain *= u_grain_intensity;
|
|
1086
|
-
|
|
1087
|
-
color += vec3(grain);
|
|
1088
|
-
|
|
1089
|
-
gl_FragColor = vec4(color, 1.0);
|
|
1090
|
-
}
|
|
1091
|
-
`;
|
|
1092
|
-
return cachedFragmentShader;
|
|
1093
|
-
}
|
|
1094
|
-
// Cache uniforms string as well
|
|
1095
|
-
let cachedUniformsShader = null;
|
|
1096
|
-
const buildUniforms = () => {
|
|
1097
|
-
if (cachedUniformsShader)
|
|
1098
|
-
return cachedUniformsShader;
|
|
1099
|
-
cachedUniformsShader = `
|
|
1100
|
-
precision highp float;
|
|
1101
|
-
|
|
1102
|
-
struct Color {
|
|
1103
|
-
float is_active;
|
|
1104
|
-
vec3 color;
|
|
1105
|
-
float value;
|
|
1106
|
-
};
|
|
1107
|
-
|
|
1108
|
-
uniform float u_grain_intensity;
|
|
1109
|
-
uniform float u_grain_sparsity;
|
|
1110
|
-
uniform float u_grain_scale;
|
|
1111
|
-
uniform float u_grain_speed;
|
|
1112
|
-
uniform float u_time;
|
|
1113
|
-
|
|
1114
|
-
uniform float u_wave_amplitude;
|
|
1115
|
-
uniform float u_wave_frequency_x;
|
|
1116
|
-
uniform float u_wave_frequency_y;
|
|
1117
|
-
|
|
1118
|
-
uniform vec2 u_color_pressure;
|
|
1119
|
-
|
|
1120
|
-
uniform float u_plane_width;
|
|
1121
|
-
uniform float u_plane_height;
|
|
1122
|
-
|
|
1123
|
-
uniform float u_shadows;
|
|
1124
|
-
uniform float u_highlights;
|
|
1125
|
-
uniform float u_saturation;
|
|
1126
|
-
uniform float u_brightness;
|
|
1127
|
-
|
|
1128
|
-
uniform float u_color_blending;
|
|
1129
|
-
|
|
1130
|
-
uniform int u_colors_count;
|
|
1131
|
-
uniform Color u_colors[6];
|
|
1132
|
-
uniform vec2 u_resolution;
|
|
1133
|
-
|
|
1134
|
-
uniform float u_y_offset;
|
|
1135
|
-
uniform float u_y_offset_wave_multiplier;
|
|
1136
|
-
uniform float u_y_offset_color_multiplier;
|
|
1137
|
-
uniform float u_y_offset_flow_multiplier;
|
|
1138
|
-
|
|
1139
|
-
// Flow field uniforms
|
|
1140
|
-
uniform float u_flow_distortion_a;
|
|
1141
|
-
uniform float u_flow_distortion_b;
|
|
1142
|
-
uniform float u_flow_scale;
|
|
1143
|
-
uniform float u_flow_ease;
|
|
1144
|
-
uniform float u_flow_enabled;
|
|
1145
|
-
|
|
1146
|
-
// Mouse interaction uniforms
|
|
1147
|
-
uniform float u_mouse_distortion_strength;
|
|
1148
|
-
uniform float u_mouse_distortion_radius;
|
|
1149
|
-
uniform float u_mouse_darken;
|
|
1150
|
-
uniform sampler2D u_mouse_texture;
|
|
1151
|
-
|
|
1152
|
-
// Procedural texture uniforms
|
|
1153
|
-
uniform sampler2D u_procedural_texture;
|
|
1154
|
-
uniform float u_enable_procedural_texture;
|
|
1155
|
-
uniform float u_texture_ease;
|
|
1156
|
-
|
|
1157
|
-
varying vec2 vUv;
|
|
1158
|
-
varying vec2 vFlowUv;
|
|
1159
|
-
varying vec4 v_new_position;
|
|
1160
|
-
varying vec3 v_color;
|
|
1161
|
-
varying float v_displacement_amount;
|
|
1162
|
-
|
|
1163
|
-
`;
|
|
1164
|
-
return cachedUniformsShader;
|
|
1165
|
-
};
|
|
1166
|
-
// Cache noise functions as well
|
|
1167
|
-
let cachedNoiseShader = null;
|
|
1168
|
-
const buildNoise = () => {
|
|
1169
|
-
if (cachedNoiseShader)
|
|
1170
|
-
return cachedNoiseShader;
|
|
1171
|
-
cachedNoiseShader = `
|
|
1172
|
-
|
|
1173
|
-
// 1. REPLACEMENT PERMUTE:
|
|
1174
|
-
// Uses a hash function (fract/sin) instead of a modular lookup table.
|
|
1175
|
-
vec4 permute(vec4 x) {
|
|
1176
|
-
return floor(fract(sin(x) * 43758.5453123) * 289.0);
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
// Taylor Inverse Sqrt
|
|
1180
|
-
vec4 taylorInvSqrt(vec4 r) {
|
|
1181
|
-
return 1.79284291400159 - 0.85373472095314 * r;
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
// Fade function
|
|
1185
|
-
vec3 fade(vec3 t) {
|
|
1186
|
-
return t*t*t*(t*(t*6.0-15.0)+10.0);
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
// 3D Simplex Noise
|
|
1190
|
-
float snoise(vec3 v) {
|
|
1191
|
-
const vec2 C = vec2(1.0/6.0, 1.0/3.0) ;
|
|
1192
|
-
const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
|
|
1193
|
-
|
|
1194
|
-
// First corner
|
|
1195
|
-
vec3 i = floor(v + dot(v, C.yyy) );
|
|
1196
|
-
vec3 x0 = v - i + dot(i, C.xxx) ;
|
|
1197
|
-
|
|
1198
|
-
// Other corners
|
|
1199
|
-
vec3 g = step(x0.yzx, x0.xyz);
|
|
1200
|
-
vec3 l = 1.0 - g;
|
|
1201
|
-
vec3 i1 = min( g.xyz, l.zxy );
|
|
1202
|
-
vec3 i2 = max( g.xyz, l.zxy );
|
|
1203
|
-
|
|
1204
|
-
vec3 x1 = x0 - i1 + C.xxx;
|
|
1205
|
-
vec3 x2 = x0 - i2 + C.yyy;
|
|
1206
|
-
vec3 x3 = x0 - D.yyy;
|
|
1207
|
-
|
|
1208
|
-
// Permutations
|
|
1209
|
-
vec4 p = permute( permute( permute(
|
|
1210
|
-
i.z + vec4(0.0, i1.z, i2.z, 1.0 ))
|
|
1211
|
-
+ i.y + vec4(0.0, i1.y, i2.y, 1.0 ))
|
|
1212
|
-
+ i.x + vec4(0.0, i1.x, i2.x, 1.0 ));
|
|
1213
|
-
|
|
1214
|
-
// Gradients
|
|
1215
|
-
float n_ = 0.142857142857; // 1.0/7.0
|
|
1216
|
-
vec3 ns = n_ * D.wyz - D.xzx;
|
|
1217
|
-
|
|
1218
|
-
vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
|
|
1219
|
-
|
|
1220
|
-
vec4 x_ = floor(j * ns.z);
|
|
1221
|
-
vec4 y_ = floor(j - 7.0 * x_ );
|
|
1222
|
-
|
|
1223
|
-
vec4 x = x_ *ns.x + ns.yyyy;
|
|
1224
|
-
vec4 y = y_ *ns.x + ns.yyyy;
|
|
1225
|
-
vec4 h = 1.0 - abs(x) - abs(y);
|
|
1226
|
-
|
|
1227
|
-
vec4 b0 = vec4( x.xy, y.xy );
|
|
1228
|
-
vec4 b1 = vec4( x.zw, y.zw );
|
|
1229
|
-
|
|
1230
|
-
vec4 s0 = floor(b0)*2.0 + 1.0;
|
|
1231
|
-
vec4 s1 = floor(b1)*2.0 + 1.0;
|
|
1232
|
-
vec4 sh = -step(h, vec4(0.0));
|
|
1233
|
-
|
|
1234
|
-
vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ;
|
|
1235
|
-
vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ;
|
|
1236
|
-
|
|
1237
|
-
vec3 p0 = vec3(a0.xy,h.x);
|
|
1238
|
-
vec3 p1 = vec3(a0.zw,h.y);
|
|
1239
|
-
vec3 p2 = vec3(a1.xy,h.z);
|
|
1240
|
-
vec3 p3 = vec3(a1.zw,h.w);
|
|
1241
|
-
|
|
1242
|
-
// Normalise gradients
|
|
1243
|
-
vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
|
|
1244
|
-
p0 *= norm.x;
|
|
1245
|
-
p1 *= norm.y;
|
|
1246
|
-
p2 *= norm.z;
|
|
1247
|
-
p3 *= norm.w;
|
|
1248
|
-
|
|
1249
|
-
// Mix final noise value
|
|
1250
|
-
vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
|
|
1251
|
-
m = m * m;
|
|
1252
|
-
return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1),
|
|
1253
|
-
dot(p2,x2), dot(p3,x3) ) );
|
|
1254
820
|
}
|
|
1255
|
-
|
|
1256
|
-
// Classic Perlin noise
|
|
1257
|
-
float cnoise(vec3 P)
|
|
1258
|
-
{
|
|
1259
|
-
vec3 Pi0 = floor(P);
|
|
1260
|
-
vec3 Pi1 = Pi0 + vec3(1.0);
|
|
1261
|
-
|
|
1262
|
-
vec3 Pf0 = fract(P);
|
|
1263
|
-
vec3 Pf1 = Pf0 - vec3(1.0);
|
|
1264
|
-
vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x);
|
|
1265
|
-
vec4 iy = vec4(Pi0.yy, Pi1.yy);
|
|
1266
|
-
vec4 iz0 = Pi0.zzzz;
|
|
1267
|
-
vec4 iz1 = Pi1.zzzz;
|
|
1268
|
-
|
|
1269
|
-
vec4 ixy = permute(permute(ix) + iy);
|
|
1270
|
-
vec4 ixy0 = permute(ixy + iz0);
|
|
1271
|
-
vec4 ixy1 = permute(ixy + iz1);
|
|
1272
|
-
|
|
1273
|
-
vec4 gx0 = ixy0 * (1.0 / 7.0);
|
|
1274
|
-
vec4 gy0 = fract(floor(gx0) * (1.0 / 7.0)) - 0.5;
|
|
1275
|
-
gx0 = fract(gx0);
|
|
1276
|
-
vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0);
|
|
1277
|
-
vec4 sz0 = step(gz0, vec4(0.0));
|
|
1278
|
-
gx0 -= sz0 * (step(0.0, gx0) - 0.5);
|
|
1279
|
-
gy0 -= sz0 * (step(0.0, gy0) - 0.5);
|
|
1280
|
-
|
|
1281
|
-
vec4 gx1 = ixy1 * (1.0 / 7.0);
|
|
1282
|
-
vec4 gy1 = fract(floor(gx1) * (1.0 / 7.0)) - 0.5;
|
|
1283
|
-
gx1 = fract(gx1);
|
|
1284
|
-
vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1);
|
|
1285
|
-
vec4 sz1 = step(gz1, vec4(0.0));
|
|
1286
|
-
gx1 -= sz1 * (step(0.0, gx1) - 0.5);
|
|
1287
|
-
gy1 -= sz1 * (step(0.0, gy1) - 0.5);
|
|
1288
|
-
|
|
1289
|
-
vec3 g000 = vec3(gx0.x,gy0.x,gz0.x);
|
|
1290
|
-
vec3 g100 = vec3(gx0.y,gy0.y,gz0.y);
|
|
1291
|
-
vec3 g010 = vec3(gx0.z,gy0.z,gz0.z);
|
|
1292
|
-
vec3 g110 = vec3(gx0.w,gy0.w,gz0.w);
|
|
1293
|
-
vec3 g001 = vec3(gx1.x,gy1.x,gz1.x);
|
|
1294
|
-
vec3 g101 = vec3(gx1.y,gy1.y,gz1.y);
|
|
1295
|
-
vec3 g011 = vec3(gx1.z,gy1.z,gz1.z);
|
|
1296
|
-
vec3 g111 = vec3(gx1.w,gy1.w,gz1.w);
|
|
1297
|
-
|
|
1298
|
-
vec4 norm0 = taylorInvSqrt(vec4(dot(g000, g000), dot(g010, g010), dot(g100, g100), dot(g110, g110)));
|
|
1299
|
-
g000 *= norm0.x;
|
|
1300
|
-
g010 *= norm0.y;
|
|
1301
|
-
g100 *= norm0.z;
|
|
1302
|
-
g110 *= norm0.w;
|
|
1303
|
-
vec4 norm1 = taylorInvSqrt(vec4(dot(g001, g001), dot(g011, g011), dot(g101, g101), dot(g111, g111)));
|
|
1304
|
-
g001 *= norm1.x;
|
|
1305
|
-
g011 *= norm1.y;
|
|
1306
|
-
g101 *= norm1.z;
|
|
1307
|
-
g111 *= norm1.w;
|
|
1308
|
-
|
|
1309
|
-
float n000 = dot(g000, Pf0);
|
|
1310
|
-
float n100 = dot(g100, vec3(Pf1.x, Pf0.yz));
|
|
1311
|
-
float n010 = dot(g010, vec3(Pf0.x, Pf1.y, Pf0.z));
|
|
1312
|
-
float n110 = dot(g110, vec3(Pf1.xy, Pf0.z));
|
|
1313
|
-
float n001 = dot(g001, vec3(Pf0.xy, Pf1.z));
|
|
1314
|
-
float n101 = dot(g101, vec3(Pf1.x, Pf0.y, Pf1.z));
|
|
1315
|
-
float n011 = dot(g011, vec3(Pf0.x, Pf1.yz));
|
|
1316
|
-
float n111 = dot(g111, Pf1);
|
|
1317
|
-
|
|
1318
|
-
vec3 fade_xyz = fade(Pf0);
|
|
1319
|
-
vec4 n_z = mix(vec4(n000, n100, n010, n110), vec4(n001, n101, n011, n111), fade_xyz.z);
|
|
1320
|
-
vec2 n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y);
|
|
1321
|
-
float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x);
|
|
1322
|
-
return 2.2 * n_xyz;
|
|
1323
|
-
}
|
|
1324
|
-
`;
|
|
1325
|
-
return cachedNoiseShader;
|
|
1326
|
-
};
|
|
1327
|
-
// Cache color functions as well
|
|
1328
|
-
let cachedColorFunctionsShader = null;
|
|
1329
|
-
const buildColorFunctions = () => {
|
|
1330
|
-
if (cachedColorFunctionsShader)
|
|
1331
|
-
return cachedColorFunctionsShader;
|
|
1332
|
-
cachedColorFunctionsShader = `
|
|
1333
|
-
|
|
1334
|
-
vec3 saturation(vec3 rgb, float adjustment) {
|
|
1335
|
-
const vec3 W = vec3(0.2125, 0.7154, 0.0721);
|
|
1336
|
-
vec3 intensity = vec3(dot(rgb, W));
|
|
1337
|
-
return mix(intensity, rgb, adjustment);
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
float saturation(vec3 rgb)
|
|
1341
|
-
{
|
|
1342
|
-
vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
|
|
1343
|
-
vec4 p = mix(vec4(rgb.bg, K.wz), vec4(rgb.gb, K.xy), step(rgb.b, rgb.g));
|
|
1344
|
-
vec4 q = mix(vec4(p.xyw, rgb.r), vec4(rgb.r, p.yzx), step(p.x, rgb.r));
|
|
1345
|
-
|
|
1346
|
-
float d = q.x - min(q.w, q.y);
|
|
1347
|
-
float e = 1.0e-10;
|
|
1348
|
-
return abs(6.0 * d + e);
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
// get saturation of a color in values between 0 and 1
|
|
1352
|
-
float getSaturation(vec3 color) {
|
|
1353
|
-
float max = max(color.r, max(color.g, color.b));
|
|
1354
|
-
float min = min(color.r, min(color.g, color.b));
|
|
1355
|
-
return (max - min) / max;
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
vec3 rgb2hsv(vec3 c)
|
|
1359
|
-
{
|
|
1360
|
-
vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
|
|
1361
|
-
vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
|
|
1362
|
-
vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
|
|
1363
|
-
|
|
1364
|
-
float d = q.x - min(q.w, q.y);
|
|
1365
|
-
float e = 1.0e-10;
|
|
1366
|
-
return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
vec3 hsv2rgb(vec3 c)
|
|
1370
|
-
{
|
|
1371
|
-
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
|
|
1372
|
-
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
|
|
1373
|
-
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
|
|
1374
|
-
}
|
|
1375
|
-
`;
|
|
1376
|
-
return cachedColorFunctionsShader;
|
|
1377
|
-
};
|
|
1378
821
|
const setLinkStyles = (link) => {
|
|
1379
822
|
link.id = LINK_ID;
|
|
1380
823
|
link.href = "https://neat.firecms.co";
|
|
@@ -1391,21 +834,27 @@ const setLinkStyles = (link) => {
|
|
|
1391
834
|
link.style.fontWeight = "bold";
|
|
1392
835
|
link.style.textDecoration = "none";
|
|
1393
836
|
link.style.zIndex = "10000";
|
|
837
|
+
link.style.pointerEvents = "auto";
|
|
838
|
+
link.setAttribute("data-n", "1");
|
|
1394
839
|
link.innerHTML = "NEAT";
|
|
1395
840
|
};
|
|
1396
841
|
const addNeatLink = (ref) => {
|
|
1397
|
-
const
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
842
|
+
const parent = ref.parentElement;
|
|
843
|
+
// Ensure parent has position so absolute link is positioned relative to it
|
|
844
|
+
if (parent && getComputedStyle(parent).position === "static") {
|
|
845
|
+
parent.style.position = "relative";
|
|
846
|
+
}
|
|
847
|
+
// Search parent for existing neat link (survives HMR where LINK_ID changes)
|
|
848
|
+
if (parent) {
|
|
849
|
+
const existing = parent.querySelector('a[data-n]');
|
|
850
|
+
if (existing) {
|
|
851
|
+
setLinkStyles(existing);
|
|
852
|
+
return existing;
|
|
1404
853
|
}
|
|
1405
854
|
}
|
|
1406
855
|
const link = document.createElement("a");
|
|
1407
856
|
setLinkStyles(link);
|
|
1408
|
-
|
|
857
|
+
parent?.appendChild(link);
|
|
1409
858
|
return link;
|
|
1410
859
|
};
|
|
1411
860
|
function getElapsedSecondsInLastHour() {
|