@firecms/neat 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -29
- package/dist/NeatGradient.d.ts +29 -43
- package/dist/NeatGradient.js +394 -897
- package/dist/NeatGradient.js.map +1 -1
- package/dist/index.es.js +778 -715
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +104 -116
- 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 +467 -968
- package/src/math.ts +162 -0
- package/src/shaders.ts +411 -0
- package/src/types.ts +54 -0
package/dist/NeatGradient.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { buildColorFunctions, buildNoise, buildVertUniforms, buildFragUniforms, fragmentShaderSource, vertexShaderSource } from "./shaders";
|
|
2
|
+
import { generatePlaneGeometry, OrthographicCamera, updateCamera, Matrix4 } from "./math";
|
|
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;");
|
|
2
4
|
const PLANE_WIDTH = 50;
|
|
3
5
|
const PLANE_HEIGHT = 80;
|
|
4
|
-
const WIREFRAME = true;
|
|
5
6
|
const COLORS_COUNT = 6;
|
|
6
|
-
const clock = new THREE.Clock();
|
|
7
7
|
const LINK_ID = generateRandomString();
|
|
8
8
|
export class NeatGradient {
|
|
9
9
|
_ref;
|
|
@@ -25,6 +25,7 @@ export class NeatGradient {
|
|
|
25
25
|
_colors = [];
|
|
26
26
|
_wireframe = false;
|
|
27
27
|
_backgroundColor = "#FFFFFF";
|
|
28
|
+
_backgroundColorRgb = [1, 1, 1];
|
|
28
29
|
_backgroundAlpha = 1.0;
|
|
29
30
|
// Flow field properties
|
|
30
31
|
_flowDistortionA = 0;
|
|
@@ -32,18 +33,7 @@ export class NeatGradient {
|
|
|
32
33
|
_flowScale = 1.0;
|
|
33
34
|
_flowEase = 0.0;
|
|
34
35
|
_flowEnabled = true;
|
|
35
|
-
|
|
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;
|
|
36
|
+
glState;
|
|
47
37
|
// Texture generation properties
|
|
48
38
|
_enableProceduralTexture = false;
|
|
49
39
|
_textureVoidLikelihood = 0.45;
|
|
@@ -61,36 +51,29 @@ export class NeatGradient {
|
|
|
61
51
|
_textureShapeSquiggles = 10;
|
|
62
52
|
requestRef = -1;
|
|
63
53
|
sizeObserver;
|
|
64
|
-
|
|
65
|
-
// Optimization: Cache uniforms to avoid lookups and object creation in render loop
|
|
66
|
-
_cachedUniforms = null;
|
|
54
|
+
_initialized = false;
|
|
67
55
|
_linkElement = null;
|
|
56
|
+
_cachedColorRgb = [];
|
|
68
57
|
_yOffset = 0;
|
|
69
58
|
_yOffsetWaveMultiplier = 0.004;
|
|
70
59
|
_yOffsetColorMultiplier = 0.004;
|
|
71
60
|
_yOffsetFlowMultiplier = 0.004;
|
|
72
|
-
// For saving/restoring clear color
|
|
73
|
-
_tempClearColor = new THREE.Color();
|
|
74
61
|
// Performance optimizations
|
|
75
62
|
_resizeTimeoutId = null;
|
|
76
63
|
_textureNeedsUpdate = false;
|
|
77
|
-
_lastColorUpdate = 0;
|
|
78
64
|
_linkCheckCounter = 0;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
65
|
+
_colorsChanged = true;
|
|
66
|
+
_uniformsDirty = true;
|
|
67
|
+
_textureDirty = true;
|
|
82
68
|
constructor(config) {
|
|
83
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,
|
|
84
70
|
// Flow field parameters
|
|
85
71
|
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
72
|
// Texture generation
|
|
89
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;
|
|
90
74
|
this._ref = ref;
|
|
91
75
|
this.destroy = this.destroy.bind(this);
|
|
92
76
|
this._initScene = this._initScene.bind(this);
|
|
93
|
-
this._buildMaterial = this._buildMaterial.bind(this);
|
|
94
77
|
this.speed = speed;
|
|
95
78
|
this.horizontalPressure = horizontalPressure;
|
|
96
79
|
this.verticalPressure = verticalPressure;
|
|
@@ -120,11 +103,6 @@ export class NeatGradient {
|
|
|
120
103
|
this.flowScale = flowScale;
|
|
121
104
|
this.flowEase = flowEase;
|
|
122
105
|
this.flowEnabled = flowEnabled;
|
|
123
|
-
// Mouse interaction
|
|
124
|
-
this.mouseDistortionStrength = mouseDistortionStrength;
|
|
125
|
-
this.mouseDistortionRadius = mouseDistortionRadius;
|
|
126
|
-
this.mouseDecayRate = mouseDecayRate;
|
|
127
|
-
this.mouseDarken = mouseDarken;
|
|
128
106
|
// Texture generation
|
|
129
107
|
this.enableProceduralTexture = enableProceduralTexture;
|
|
130
108
|
this.textureVoidLikelihood = textureVoidLikelihood;
|
|
@@ -139,13 +117,12 @@ export class NeatGradient {
|
|
|
139
117
|
this._textureShapeCircles = textureShapeCircles;
|
|
140
118
|
this._textureShapeBars = textureShapeBars;
|
|
141
119
|
this._textureShapeSquiggles = textureShapeSquiggles;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
this._setupMouseInteraction();
|
|
145
|
-
this.sceneState = this._initScene(resolution);
|
|
120
|
+
this.glState = this._initScene(resolution);
|
|
121
|
+
injectSEO();
|
|
146
122
|
let tick = seed !== undefined ? seed : getElapsedSecondsInLastHour();
|
|
123
|
+
let lastTime = performance.now();
|
|
147
124
|
const render = () => {
|
|
148
|
-
const {
|
|
125
|
+
const { gl, program, locations, indexCount, indexType } = this.glState;
|
|
149
126
|
// Optimization: check if cached link is still valid in DOM less frequently
|
|
150
127
|
this._linkCheckCounter++;
|
|
151
128
|
if (this._linkCheckCounter >= 300) { // Check every ~5 seconds at 60fps
|
|
@@ -154,134 +131,101 @@ export class NeatGradient {
|
|
|
154
131
|
this._linkElement = addNeatLink(ref);
|
|
155
132
|
}
|
|
156
133
|
}
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
+
}
|
|
190
169
|
// Only regenerate procedural texture when needed
|
|
191
170
|
if (this._textureNeedsUpdate && this._enableProceduralTexture) {
|
|
192
171
|
if (this._proceduralTexture) {
|
|
193
|
-
this._proceduralTexture
|
|
172
|
+
gl.deleteTexture(this._proceduralTexture);
|
|
194
173
|
}
|
|
195
|
-
this._proceduralTexture = this._createProceduralTexture();
|
|
174
|
+
this._proceduralTexture = this._createProceduralTexture(gl);
|
|
196
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;
|
|
197
184
|
}
|
|
198
|
-
|
|
199
|
-
|
|
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;
|
|
185
|
+
// Color update — only when colors have changed
|
|
186
|
+
if (this._colorsChanged) {
|
|
208
187
|
this._colorsChanged = false;
|
|
209
|
-
const shaderColors = u.u_colors.value;
|
|
210
188
|
for (let i = 0; i < COLORS_COUNT; i++) {
|
|
211
189
|
if (i < this._colors.length) {
|
|
212
190
|
const c = this._colors[i];
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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);
|
|
216
195
|
}
|
|
217
196
|
else {
|
|
218
|
-
|
|
197
|
+
gl.uniform1f(locations.uniforms[`u_colors[${i}].is_active`], 0.0);
|
|
219
198
|
}
|
|
220
199
|
}
|
|
221
|
-
|
|
200
|
+
gl.uniform1i(locations.uniforms['u_colors_count'], COLORS_COUNT);
|
|
222
201
|
}
|
|
223
202
|
}
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
}
|
|
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);
|
|
260
213
|
}
|
|
261
|
-
// Ensure we set the clear color for the main scene explicitly before rendering
|
|
262
|
-
renderer.setClearColor(this._backgroundColor, this._backgroundAlpha);
|
|
263
|
-
renderer.render(scene, camera);
|
|
264
214
|
this.requestRef = requestAnimationFrame(render);
|
|
265
215
|
};
|
|
266
216
|
const setSize = () => {
|
|
267
|
-
const {
|
|
268
|
-
const
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
this.
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
}
|
|
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);
|
|
285
229
|
};
|
|
286
230
|
// Debounce resize to prevent excessive operations
|
|
287
231
|
this.sizeObserver = new ResizeObserver(() => {
|
|
@@ -297,197 +241,220 @@ export class NeatGradient {
|
|
|
297
241
|
render();
|
|
298
242
|
}
|
|
299
243
|
destroy() {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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);
|
|
323
268
|
}
|
|
324
269
|
}
|
|
325
270
|
downloadAsPNG(filename = "neat.png") {
|
|
326
|
-
console.log("Downloading as PNG", this._ref);
|
|
327
271
|
const dataURL = this._ref.toDataURL("image/png");
|
|
328
|
-
console.log("data", dataURL);
|
|
329
272
|
downloadURI(dataURL, filename);
|
|
330
273
|
}
|
|
331
274
|
set speed(speed) {
|
|
275
|
+
this._uniformsDirty = true;
|
|
332
276
|
this._speed = speed / 20;
|
|
333
277
|
}
|
|
334
278
|
set horizontalPressure(horizontalPressure) {
|
|
279
|
+
this._uniformsDirty = true;
|
|
335
280
|
this._horizontalPressure = horizontalPressure / 4;
|
|
336
281
|
}
|
|
337
282
|
set verticalPressure(verticalPressure) {
|
|
283
|
+
this._uniformsDirty = true;
|
|
338
284
|
this._verticalPressure = verticalPressure / 4;
|
|
339
285
|
}
|
|
340
286
|
set waveFrequencyX(waveFrequencyX) {
|
|
287
|
+
this._uniformsDirty = true;
|
|
341
288
|
this._waveFrequencyX = waveFrequencyX * 0.04;
|
|
342
289
|
}
|
|
343
290
|
set waveFrequencyY(waveFrequencyY) {
|
|
291
|
+
this._uniformsDirty = true;
|
|
344
292
|
this._waveFrequencyY = waveFrequencyY * 0.04;
|
|
345
293
|
}
|
|
346
294
|
set waveAmplitude(waveAmplitude) {
|
|
295
|
+
this._uniformsDirty = true;
|
|
347
296
|
this._waveAmplitude = waveAmplitude * .75;
|
|
348
297
|
}
|
|
349
298
|
set colors(colors) {
|
|
299
|
+
this._uniformsDirty = true;
|
|
350
300
|
this._colors = colors;
|
|
351
|
-
this.
|
|
301
|
+
this._cachedColorRgb = colors.map(c => this._hexToRgb(c.color));
|
|
302
|
+
this._colorsChanged = true;
|
|
352
303
|
}
|
|
353
304
|
set highlights(highlights) {
|
|
305
|
+
this._uniformsDirty = true;
|
|
354
306
|
this._highlights = highlights / 100;
|
|
355
307
|
}
|
|
356
308
|
set shadows(shadows) {
|
|
309
|
+
this._uniformsDirty = true;
|
|
357
310
|
this._shadows = shadows / 100;
|
|
358
311
|
}
|
|
359
312
|
set colorSaturation(colorSaturation) {
|
|
313
|
+
this._uniformsDirty = true;
|
|
360
314
|
this._saturation = colorSaturation / 10;
|
|
361
315
|
}
|
|
362
316
|
set colorBrightness(colorBrightness) {
|
|
317
|
+
this._uniformsDirty = true;
|
|
363
318
|
this._brightness = colorBrightness;
|
|
364
319
|
}
|
|
365
320
|
set colorBlending(colorBlending) {
|
|
321
|
+
this._uniformsDirty = true;
|
|
366
322
|
this._colorBlending = colorBlending / 10;
|
|
367
323
|
}
|
|
368
324
|
set grainScale(grainScale) {
|
|
325
|
+
this._uniformsDirty = true;
|
|
369
326
|
this._grainScale = grainScale == 0 ? 1 : grainScale;
|
|
370
327
|
}
|
|
371
328
|
set grainIntensity(grainIntensity) {
|
|
329
|
+
this._uniformsDirty = true;
|
|
372
330
|
this._grainIntensity = grainIntensity;
|
|
373
331
|
}
|
|
374
332
|
set grainSparsity(grainSparsity) {
|
|
333
|
+
this._uniformsDirty = true;
|
|
375
334
|
this._grainSparsity = grainSparsity;
|
|
376
335
|
}
|
|
377
336
|
set grainSpeed(grainSpeed) {
|
|
337
|
+
this._uniformsDirty = true;
|
|
378
338
|
this._grainSpeed = grainSpeed;
|
|
379
339
|
}
|
|
380
340
|
set wireframe(wireframe) {
|
|
341
|
+
this._uniformsDirty = true;
|
|
381
342
|
this._wireframe = wireframe;
|
|
382
343
|
}
|
|
383
344
|
set resolution(resolution) {
|
|
384
|
-
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);
|
|
385
356
|
}
|
|
386
357
|
set backgroundColor(backgroundColor) {
|
|
358
|
+
this._uniformsDirty = true;
|
|
387
359
|
this._backgroundColor = backgroundColor;
|
|
360
|
+
this._backgroundColorRgb = this._hexToRgb(backgroundColor);
|
|
388
361
|
}
|
|
389
362
|
set backgroundAlpha(backgroundAlpha) {
|
|
363
|
+
this._uniformsDirty = true;
|
|
390
364
|
this._backgroundAlpha = backgroundAlpha;
|
|
391
365
|
}
|
|
392
366
|
set yOffset(yOffset) {
|
|
367
|
+
this._uniformsDirty = true;
|
|
393
368
|
this._yOffset = yOffset;
|
|
394
369
|
}
|
|
395
370
|
get yOffsetWaveMultiplier() {
|
|
396
371
|
return this._yOffsetWaveMultiplier * 1000;
|
|
397
372
|
}
|
|
398
373
|
set yOffsetWaveMultiplier(value) {
|
|
374
|
+
this._uniformsDirty = true;
|
|
399
375
|
this._yOffsetWaveMultiplier = value / 1000;
|
|
400
376
|
}
|
|
401
377
|
get yOffsetColorMultiplier() {
|
|
402
378
|
return this._yOffsetColorMultiplier * 1000;
|
|
403
379
|
}
|
|
404
380
|
set yOffsetColorMultiplier(value) {
|
|
381
|
+
this._uniformsDirty = true;
|
|
405
382
|
this._yOffsetColorMultiplier = value / 1000;
|
|
406
383
|
}
|
|
407
384
|
get yOffsetFlowMultiplier() {
|
|
408
385
|
return this._yOffsetFlowMultiplier * 1000;
|
|
409
386
|
}
|
|
410
387
|
set yOffsetFlowMultiplier(value) {
|
|
388
|
+
this._uniformsDirty = true;
|
|
411
389
|
this._yOffsetFlowMultiplier = value / 1000;
|
|
412
390
|
}
|
|
413
391
|
set flowDistortionA(value) {
|
|
392
|
+
this._uniformsDirty = true;
|
|
414
393
|
this._flowDistortionA = value;
|
|
415
394
|
}
|
|
416
395
|
set flowDistortionB(value) {
|
|
396
|
+
this._uniformsDirty = true;
|
|
417
397
|
this._flowDistortionB = value;
|
|
418
398
|
}
|
|
419
399
|
set flowScale(value) {
|
|
400
|
+
this._uniformsDirty = true;
|
|
420
401
|
this._flowScale = value;
|
|
421
402
|
}
|
|
422
403
|
set flowEase(value) {
|
|
404
|
+
this._uniformsDirty = true;
|
|
423
405
|
this._flowEase = value;
|
|
424
406
|
}
|
|
425
407
|
set flowEnabled(value) {
|
|
408
|
+
this._uniformsDirty = true;
|
|
426
409
|
this._flowEnabled = value;
|
|
427
410
|
}
|
|
428
411
|
get flowEnabled() {
|
|
429
412
|
return this._flowEnabled;
|
|
430
413
|
}
|
|
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
414
|
set enableProceduralTexture(value) {
|
|
415
|
+
this._uniformsDirty = true;
|
|
455
416
|
this._enableProceduralTexture = value;
|
|
456
417
|
if (value && !this._proceduralTexture) {
|
|
457
418
|
this._textureNeedsUpdate = true;
|
|
458
419
|
}
|
|
459
420
|
}
|
|
460
421
|
set textureVoidLikelihood(value) {
|
|
422
|
+
this._uniformsDirty = true;
|
|
461
423
|
this._textureVoidLikelihood = value;
|
|
462
424
|
if (this._enableProceduralTexture) {
|
|
463
425
|
this._textureNeedsUpdate = true;
|
|
464
426
|
}
|
|
465
427
|
}
|
|
466
428
|
set textureVoidWidthMin(value) {
|
|
429
|
+
this._uniformsDirty = true;
|
|
467
430
|
this._textureVoidWidthMin = value;
|
|
468
431
|
if (this._enableProceduralTexture) {
|
|
469
432
|
this._textureNeedsUpdate = true;
|
|
470
433
|
}
|
|
471
434
|
}
|
|
472
435
|
set textureVoidWidthMax(value) {
|
|
436
|
+
this._uniformsDirty = true;
|
|
473
437
|
this._textureVoidWidthMax = value;
|
|
474
438
|
if (this._enableProceduralTexture) {
|
|
475
439
|
this._textureNeedsUpdate = true;
|
|
476
440
|
}
|
|
477
441
|
}
|
|
478
442
|
set textureBandDensity(value) {
|
|
443
|
+
this._uniformsDirty = true;
|
|
479
444
|
this._textureBandDensity = value;
|
|
480
445
|
if (this._enableProceduralTexture) {
|
|
481
446
|
this._textureNeedsUpdate = true;
|
|
482
447
|
}
|
|
483
448
|
}
|
|
484
449
|
set textureColorBlending(value) {
|
|
450
|
+
this._uniformsDirty = true;
|
|
485
451
|
this._textureColorBlending = value;
|
|
486
452
|
if (this._enableProceduralTexture) {
|
|
487
453
|
this._textureNeedsUpdate = true;
|
|
488
454
|
}
|
|
489
455
|
}
|
|
490
456
|
set textureSeed(value) {
|
|
457
|
+
this._uniformsDirty = true;
|
|
491
458
|
this._textureSeed = value;
|
|
492
459
|
if (this._enableProceduralTexture) {
|
|
493
460
|
this._textureNeedsUpdate = true;
|
|
@@ -497,222 +464,195 @@ export class NeatGradient {
|
|
|
497
464
|
return this._textureEase;
|
|
498
465
|
}
|
|
499
466
|
set textureEase(value) {
|
|
467
|
+
this._uniformsDirty = true;
|
|
500
468
|
this._textureEase = value;
|
|
501
469
|
}
|
|
502
470
|
set proceduralBackgroundColor(value) {
|
|
471
|
+
this._uniformsDirty = true;
|
|
503
472
|
this._proceduralBackgroundColor = value;
|
|
504
473
|
if (this._enableProceduralTexture) {
|
|
505
474
|
this._textureNeedsUpdate = true;
|
|
506
475
|
}
|
|
507
476
|
}
|
|
508
477
|
set textureShapeTriangles(value) {
|
|
478
|
+
this._uniformsDirty = true;
|
|
509
479
|
this._textureShapeTriangles = value;
|
|
510
480
|
if (this._enableProceduralTexture)
|
|
511
481
|
this._textureNeedsUpdate = true;
|
|
512
482
|
}
|
|
513
483
|
set textureShapeCircles(value) {
|
|
484
|
+
this._uniformsDirty = true;
|
|
514
485
|
this._textureShapeCircles = value;
|
|
515
486
|
if (this._enableProceduralTexture)
|
|
516
487
|
this._textureNeedsUpdate = true;
|
|
517
488
|
}
|
|
518
489
|
set textureShapeBars(value) {
|
|
490
|
+
this._uniformsDirty = true;
|
|
519
491
|
this._textureShapeBars = value;
|
|
520
492
|
if (this._enableProceduralTexture)
|
|
521
493
|
this._textureNeedsUpdate = true;
|
|
522
494
|
}
|
|
523
495
|
set textureShapeSquiggles(value) {
|
|
496
|
+
this._uniformsDirty = true;
|
|
524
497
|
this._textureShapeSquiggles = value;
|
|
525
498
|
if (this._enableProceduralTexture)
|
|
526
499
|
this._textureNeedsUpdate = true;
|
|
527
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
|
+
}
|
|
528
509
|
_initScene(resolution) {
|
|
529
|
-
const width = this._ref.
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
this.
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
if (Array.isArray(m.material))
|
|
536
|
-
m.material.forEach(mat => mat.dispose());
|
|
537
|
-
else
|
|
538
|
-
m.material.dispose();
|
|
539
|
-
});
|
|
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");
|
|
540
516
|
}
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
|
|
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];
|
|
561
576
|
updateCamera(camera, width, height);
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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: {}
|
|
568
619
|
};
|
|
569
|
-
|
|
570
|
-
|
|
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
|
-
}));
|
|
578
|
-
const uniforms = {
|
|
579
|
-
u_time: { value: 0 },
|
|
580
|
-
u_color_pressure: { value: new THREE.Vector2(this._horizontalPressure, this._verticalPressure) },
|
|
581
|
-
u_wave_frequency_x: { value: this._waveFrequencyX },
|
|
582
|
-
u_wave_frequency_y: { value: this._waveFrequencyY },
|
|
583
|
-
u_wave_amplitude: { value: this._waveAmplitude },
|
|
584
|
-
u_resolution: { value: new THREE.Vector2(width, height) },
|
|
585
|
-
u_colors: { value: colors },
|
|
586
|
-
u_colors_count: { value: this._colors.length },
|
|
587
|
-
u_plane_width: { value: PLANE_WIDTH },
|
|
588
|
-
u_plane_height: { value: PLANE_HEIGHT },
|
|
589
|
-
u_shadows: { value: this._shadows },
|
|
590
|
-
u_highlights: { value: this._highlights },
|
|
591
|
-
u_grain_intensity: { value: this._grainIntensity },
|
|
592
|
-
u_grain_sparsity: { value: this._grainSparsity },
|
|
593
|
-
u_grain_scale: { value: this._grainScale },
|
|
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 }
|
|
618
|
-
};
|
|
619
|
-
const material = new THREE.ShaderMaterial({
|
|
620
|
-
uniforms: uniforms,
|
|
621
|
-
vertexShader: buildUniforms() + buildNoise() + buildColorFunctions() + buildVertexShader(),
|
|
622
|
-
fragmentShader: buildUniforms() + buildColorFunctions() + buildNoise() + buildFragmentShader()
|
|
620
|
+
uniformsList.forEach(name => {
|
|
621
|
+
locations.uniforms[name] = gl.getUniformLocation(program, name);
|
|
623
622
|
});
|
|
624
|
-
//
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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);
|
|
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`);
|
|
657
628
|
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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)
|
|
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
|
|
691
653
|
};
|
|
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
654
|
}
|
|
715
|
-
_createProceduralTexture() {
|
|
655
|
+
_createProceduralTexture(gl) {
|
|
716
656
|
// Texture size - 1024 provides good balance between quality and performance
|
|
717
657
|
// Reduced from 2048 for better performance
|
|
718
658
|
const texSize = 1024;
|
|
@@ -721,7 +661,7 @@ export class NeatGradient {
|
|
|
721
661
|
sourceCanvas.height = texSize;
|
|
722
662
|
const sCtx = sourceCanvas.getContext('2d', { willReadFrequently: true });
|
|
723
663
|
if (!sCtx)
|
|
724
|
-
return
|
|
664
|
+
return null;
|
|
725
665
|
let seed = this._textureSeed;
|
|
726
666
|
const baseSeed = this._textureSeed;
|
|
727
667
|
function random() {
|
|
@@ -734,7 +674,7 @@ export class NeatGradient {
|
|
|
734
674
|
};
|
|
735
675
|
const colors = this._colors.filter(c => c.enabled).map(c => c.color);
|
|
736
676
|
if (colors.length === 0)
|
|
737
|
-
return
|
|
677
|
+
return null;
|
|
738
678
|
// Helper functions
|
|
739
679
|
function hexToRgb(hex) {
|
|
740
680
|
const bigint = parseInt(hex.replace('#', ''), 16);
|
|
@@ -745,7 +685,7 @@ export class NeatGradient {
|
|
|
745
685
|
};
|
|
746
686
|
}
|
|
747
687
|
function rgbToHex(r, g, b) {
|
|
748
|
-
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');
|
|
749
689
|
}
|
|
750
690
|
const getInterColor = () => {
|
|
751
691
|
const c1 = colors[Math.floor(random() * colors.length)];
|
|
@@ -825,7 +765,7 @@ export class NeatGradient {
|
|
|
825
765
|
canvas.height = texSize;
|
|
826
766
|
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
|
827
767
|
if (!ctx)
|
|
828
|
-
return
|
|
768
|
+
return null;
|
|
829
769
|
// Start filled with the chosen void color so gaps show that color
|
|
830
770
|
ctx.fillStyle = baseColor;
|
|
831
771
|
ctx.fillRect(0, 0, texSize, texSize);
|
|
@@ -860,519 +800,24 @@ export class NeatGradient {
|
|
|
860
800
|
}
|
|
861
801
|
// void segments: leave as baseColor
|
|
862
802
|
}
|
|
863
|
-
const tex =
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
function updateCamera(camera, width, height) {
|
|
878
|
-
const viewPortAreaRatio = 1000000;
|
|
879
|
-
const areaViewPort = width * height;
|
|
880
|
-
const targetPlaneArea = areaViewPort / viewPortAreaRatio *
|
|
881
|
-
PLANE_WIDTH * PLANE_HEIGHT / 1.5;
|
|
882
|
-
const ratio = width / height;
|
|
883
|
-
const targetWidth = Math.sqrt(targetPlaneArea * ratio);
|
|
884
|
-
const targetHeight = targetPlaneArea / targetWidth;
|
|
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
|
-
}
|
|
902
|
-
const near = -100;
|
|
903
|
-
const far = 1000;
|
|
904
|
-
if (camera instanceof THREE.OrthographicCamera) {
|
|
905
|
-
camera.left = left;
|
|
906
|
-
camera.right = right;
|
|
907
|
-
camera.top = top;
|
|
908
|
-
camera.bottom = bottom;
|
|
909
|
-
camera.near = near;
|
|
910
|
-
camera.far = far;
|
|
911
|
-
camera.updateProjectionMatrix();
|
|
912
|
-
}
|
|
913
|
-
else if (camera instanceof THREE.PerspectiveCamera) {
|
|
914
|
-
camera.aspect = width / height;
|
|
915
|
-
camera.updateProjectionMatrix();
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
// Cache shader strings to avoid repeated concatenation
|
|
919
|
-
let cachedVertexShader = null;
|
|
920
|
-
let cachedFragmentShader = null;
|
|
921
|
-
function buildVertexShader() {
|
|
922
|
-
if (cachedVertexShader)
|
|
923
|
-
return cachedVertexShader;
|
|
924
|
-
cachedVertexShader = `
|
|
925
|
-
void main() {
|
|
926
|
-
vUv = uv;
|
|
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
|
|
936
|
-
v_displacement_amount = cnoise( vec3(
|
|
937
|
-
u_wave_frequency_x * position.x + u_time,
|
|
938
|
-
u_wave_frequency_y * (position.y + waveOffset) + u_time,
|
|
939
|
-
u_time
|
|
940
|
-
));
|
|
941
|
-
|
|
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
|
-
}
|
|
960
|
-
|
|
961
|
-
// Pass the standard flow UV to fragment shader (for mouse/texture)
|
|
962
|
-
vFlowUv = flowUv;
|
|
963
|
-
|
|
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
|
|
970
|
-
|
|
971
|
-
vec2 noise_cord = adjustedUv * u_color_pressure;
|
|
972
|
-
const float minNoise = .0;
|
|
973
|
-
const float maxNoise = .9;
|
|
974
|
-
|
|
975
|
-
for (int i = 1; i < u_colors_count; i++) {
|
|
976
|
-
if(u_colors[i].is_active > 0.5){
|
|
977
|
-
float noiseFlow = (1. + float(i)) / 30.;
|
|
978
|
-
float noiseSpeed = (1. + float(i)) * 0.11;
|
|
979
|
-
float noiseSeed = 13. + float(i) * 7.;
|
|
980
|
-
|
|
981
|
-
float noise = snoise(
|
|
982
|
-
vec3(
|
|
983
|
-
noise_cord.x * u_color_pressure.x + u_time * noiseFlow * 2.,
|
|
984
|
-
noise_cord.y * u_color_pressure.y,
|
|
985
|
-
u_time * noiseSpeed
|
|
986
|
-
) + noiseSeed
|
|
987
|
-
) - (.1 * float(i)) + (.5 * u_color_blending);
|
|
988
|
-
|
|
989
|
-
noise = clamp(minNoise, maxNoise + float(i) * 0.02, noise);
|
|
990
|
-
color = mix(color, u_colors[i].color, smoothstep(0.0, u_color_blending, noise));
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
v_color = color;
|
|
995
|
-
|
|
996
|
-
// 4. VERTEX POSITION
|
|
997
|
-
vec3 newPosition = position + normal * v_displacement_amount * u_wave_amplitude;
|
|
998
|
-
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
|
|
999
|
-
v_new_position = gl_Position;
|
|
1000
|
-
}
|
|
1001
|
-
`;
|
|
1002
|
-
return cachedVertexShader;
|
|
1003
|
-
}
|
|
1004
|
-
function buildFragmentShader() {
|
|
1005
|
-
if (cachedFragmentShader)
|
|
1006
|
-
return cachedFragmentShader;
|
|
1007
|
-
cachedFragmentShader = `
|
|
1008
|
-
float random(vec2 p) {
|
|
1009
|
-
return fract(sin(dot(p, vec2(12.9898,78.233))) * 43758.5453);
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
float fbm(vec3 x) {
|
|
1013
|
-
float value = 0.0;
|
|
1014
|
-
float amplitude = 0.5;
|
|
1015
|
-
float frequency = 1.0;
|
|
1016
|
-
for (int i = 0; i < 4; i++) {
|
|
1017
|
-
value += amplitude * snoise(x * frequency);
|
|
1018
|
-
frequency *= 2.0;
|
|
1019
|
-
amplitude *= 0.5;
|
|
1020
|
-
}
|
|
1021
|
-
return value;
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
void main() {
|
|
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;
|
|
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));
|
|
1036
817
|
}
|
|
818
|
+
return tex;
|
|
1037
819
|
}
|
|
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
|
|
1071
|
-
color += pow(v_displacement_amount, 1.0) * u_highlights;
|
|
1072
|
-
color -= pow(1.0 - v_displacement_amount, 2.0) * u_shadows;
|
|
1073
|
-
color = saturation(color, 1.0 + u_saturation);
|
|
1074
|
-
color = color * u_brightness;
|
|
1075
|
-
|
|
1076
|
-
// Grain
|
|
1077
|
-
vec2 noiseCoords = gl_FragCoord.xy / u_grain_scale;
|
|
1078
|
-
float grain = (u_grain_speed != 0.0) ? fbm(vec3(noiseCoords, u_time * u_grain_speed)) : fbm(vec3(noiseCoords, 0.0));
|
|
1079
|
-
|
|
1080
|
-
grain = grain * 0.5 + 0.5;
|
|
1081
|
-
grain -= 0.5;
|
|
1082
|
-
grain = (grain > u_grain_sparsity) ? grain : 0.0;
|
|
1083
|
-
grain *= u_grain_intensity;
|
|
1084
|
-
|
|
1085
|
-
color += vec3(grain);
|
|
1086
|
-
|
|
1087
|
-
gl_FragColor = vec4(color, 1.0);
|
|
1088
|
-
}
|
|
1089
|
-
`;
|
|
1090
|
-
return cachedFragmentShader;
|
|
1091
|
-
}
|
|
1092
|
-
// Cache uniforms string as well
|
|
1093
|
-
let cachedUniformsShader = null;
|
|
1094
|
-
const buildUniforms = () => {
|
|
1095
|
-
if (cachedUniformsShader)
|
|
1096
|
-
return cachedUniformsShader;
|
|
1097
|
-
cachedUniformsShader = `
|
|
1098
|
-
precision highp float;
|
|
1099
|
-
|
|
1100
|
-
struct Color {
|
|
1101
|
-
float is_active;
|
|
1102
|
-
vec3 color;
|
|
1103
|
-
float value;
|
|
1104
|
-
};
|
|
1105
|
-
|
|
1106
|
-
uniform float u_grain_intensity;
|
|
1107
|
-
uniform float u_grain_sparsity;
|
|
1108
|
-
uniform float u_grain_scale;
|
|
1109
|
-
uniform float u_grain_speed;
|
|
1110
|
-
uniform float u_time;
|
|
1111
|
-
|
|
1112
|
-
uniform float u_wave_amplitude;
|
|
1113
|
-
uniform float u_wave_frequency_x;
|
|
1114
|
-
uniform float u_wave_frequency_y;
|
|
1115
|
-
|
|
1116
|
-
uniform vec2 u_color_pressure;
|
|
1117
|
-
|
|
1118
|
-
uniform float u_plane_width;
|
|
1119
|
-
uniform float u_plane_height;
|
|
1120
|
-
|
|
1121
|
-
uniform float u_shadows;
|
|
1122
|
-
uniform float u_highlights;
|
|
1123
|
-
uniform float u_saturation;
|
|
1124
|
-
uniform float u_brightness;
|
|
1125
|
-
|
|
1126
|
-
uniform float u_color_blending;
|
|
1127
|
-
|
|
1128
|
-
uniform int u_colors_count;
|
|
1129
|
-
uniform Color u_colors[6];
|
|
1130
|
-
uniform vec2 u_resolution;
|
|
1131
|
-
|
|
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;
|
|
1154
|
-
|
|
1155
|
-
varying vec2 vUv;
|
|
1156
|
-
varying vec2 vFlowUv;
|
|
1157
|
-
varying vec4 v_new_position;
|
|
1158
|
-
varying vec3 v_color;
|
|
1159
|
-
varying float v_displacement_amount;
|
|
1160
|
-
|
|
1161
|
-
`;
|
|
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);
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
// Taylor Inverse Sqrt
|
|
1178
|
-
vec4 taylorInvSqrt(vec4 r) {
|
|
1179
|
-
return 1.79284291400159 - 0.85373472095314 * r;
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
// Fade function
|
|
1183
|
-
vec3 fade(vec3 t) {
|
|
1184
|
-
return t*t*t*(t*(t*6.0-15.0)+10.0);
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
// 3D Simplex Noise
|
|
1188
|
-
float snoise(vec3 v) {
|
|
1189
|
-
const vec2 C = vec2(1.0/6.0, 1.0/3.0) ;
|
|
1190
|
-
const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
|
|
1191
|
-
|
|
1192
|
-
// First corner
|
|
1193
|
-
vec3 i = floor(v + dot(v, C.yyy) );
|
|
1194
|
-
vec3 x0 = v - i + dot(i, C.xxx) ;
|
|
1195
|
-
|
|
1196
|
-
// Other corners
|
|
1197
|
-
vec3 g = step(x0.yzx, x0.xyz);
|
|
1198
|
-
vec3 l = 1.0 - g;
|
|
1199
|
-
vec3 i1 = min( g.xyz, l.zxy );
|
|
1200
|
-
vec3 i2 = max( g.xyz, l.zxy );
|
|
1201
|
-
|
|
1202
|
-
vec3 x1 = x0 - i1 + C.xxx;
|
|
1203
|
-
vec3 x2 = x0 - i2 + C.yyy;
|
|
1204
|
-
vec3 x3 = x0 - D.yyy;
|
|
1205
|
-
|
|
1206
|
-
// Permutations
|
|
1207
|
-
vec4 p = permute( permute( permute(
|
|
1208
|
-
i.z + vec4(0.0, i1.z, i2.z, 1.0 ))
|
|
1209
|
-
+ i.y + vec4(0.0, i1.y, i2.y, 1.0 ))
|
|
1210
|
-
+ i.x + vec4(0.0, i1.x, i2.x, 1.0 ));
|
|
1211
|
-
|
|
1212
|
-
// Gradients
|
|
1213
|
-
float n_ = 0.142857142857; // 1.0/7.0
|
|
1214
|
-
vec3 ns = n_ * D.wyz - D.xzx;
|
|
1215
|
-
|
|
1216
|
-
vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
|
|
1217
|
-
|
|
1218
|
-
vec4 x_ = floor(j * ns.z);
|
|
1219
|
-
vec4 y_ = floor(j - 7.0 * x_ );
|
|
1220
|
-
|
|
1221
|
-
vec4 x = x_ *ns.x + ns.yyyy;
|
|
1222
|
-
vec4 y = y_ *ns.x + ns.yyyy;
|
|
1223
|
-
vec4 h = 1.0 - abs(x) - abs(y);
|
|
1224
|
-
|
|
1225
|
-
vec4 b0 = vec4( x.xy, y.xy );
|
|
1226
|
-
vec4 b1 = vec4( x.zw, y.zw );
|
|
1227
|
-
|
|
1228
|
-
vec4 s0 = floor(b0)*2.0 + 1.0;
|
|
1229
|
-
vec4 s1 = floor(b1)*2.0 + 1.0;
|
|
1230
|
-
vec4 sh = -step(h, vec4(0.0));
|
|
1231
|
-
|
|
1232
|
-
vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ;
|
|
1233
|
-
vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ;
|
|
1234
|
-
|
|
1235
|
-
vec3 p0 = vec3(a0.xy,h.x);
|
|
1236
|
-
vec3 p1 = vec3(a0.zw,h.y);
|
|
1237
|
-
vec3 p2 = vec3(a1.xy,h.z);
|
|
1238
|
-
vec3 p3 = vec3(a1.zw,h.w);
|
|
1239
|
-
|
|
1240
|
-
// Normalise gradients
|
|
1241
|
-
vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
|
|
1242
|
-
p0 *= norm.x;
|
|
1243
|
-
p1 *= norm.y;
|
|
1244
|
-
p2 *= norm.z;
|
|
1245
|
-
p3 *= norm.w;
|
|
1246
|
-
|
|
1247
|
-
// Mix final noise value
|
|
1248
|
-
vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
|
|
1249
|
-
m = m * m;
|
|
1250
|
-
return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1),
|
|
1251
|
-
dot(p2,x2), dot(p3,x3) ) );
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
// Classic Perlin noise
|
|
1255
|
-
float cnoise(vec3 P)
|
|
1256
|
-
{
|
|
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);
|
|
1262
|
-
vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x);
|
|
1263
|
-
vec4 iy = vec4(Pi0.yy, Pi1.yy);
|
|
1264
|
-
vec4 iz0 = Pi0.zzzz;
|
|
1265
|
-
vec4 iz1 = Pi1.zzzz;
|
|
1266
|
-
|
|
1267
|
-
vec4 ixy = permute(permute(ix) + iy);
|
|
1268
|
-
vec4 ixy0 = permute(ixy + iz0);
|
|
1269
|
-
vec4 ixy1 = permute(ixy + iz1);
|
|
1270
|
-
|
|
1271
|
-
vec4 gx0 = ixy0 * (1.0 / 7.0);
|
|
1272
|
-
vec4 gy0 = fract(floor(gx0) * (1.0 / 7.0)) - 0.5;
|
|
1273
|
-
gx0 = fract(gx0);
|
|
1274
|
-
vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0);
|
|
1275
|
-
vec4 sz0 = step(gz0, vec4(0.0));
|
|
1276
|
-
gx0 -= sz0 * (step(0.0, gx0) - 0.5);
|
|
1277
|
-
gy0 -= sz0 * (step(0.0, gy0) - 0.5);
|
|
1278
|
-
|
|
1279
|
-
vec4 gx1 = ixy1 * (1.0 / 7.0);
|
|
1280
|
-
vec4 gy1 = fract(floor(gx1) * (1.0 / 7.0)) - 0.5;
|
|
1281
|
-
gx1 = fract(gx1);
|
|
1282
|
-
vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1);
|
|
1283
|
-
vec4 sz1 = step(gz1, vec4(0.0));
|
|
1284
|
-
gx1 -= sz1 * (step(0.0, gx1) - 0.5);
|
|
1285
|
-
gy1 -= sz1 * (step(0.0, gy1) - 0.5);
|
|
1286
|
-
|
|
1287
|
-
vec3 g000 = vec3(gx0.x,gy0.x,gz0.x);
|
|
1288
|
-
vec3 g100 = vec3(gx0.y,gy0.y,gz0.y);
|
|
1289
|
-
vec3 g010 = vec3(gx0.z,gy0.z,gz0.z);
|
|
1290
|
-
vec3 g110 = vec3(gx0.w,gy0.w,gz0.w);
|
|
1291
|
-
vec3 g001 = vec3(gx1.x,gy1.x,gz1.x);
|
|
1292
|
-
vec3 g101 = vec3(gx1.y,gy1.y,gz1.y);
|
|
1293
|
-
vec3 g011 = vec3(gx1.z,gy1.z,gz1.z);
|
|
1294
|
-
vec3 g111 = vec3(gx1.w,gy1.w,gz1.w);
|
|
1295
|
-
|
|
1296
|
-
vec4 norm0 = taylorInvSqrt(vec4(dot(g000, g000), dot(g010, g010), dot(g100, g100), dot(g110, g110)));
|
|
1297
|
-
g000 *= norm0.x;
|
|
1298
|
-
g010 *= norm0.y;
|
|
1299
|
-
g100 *= norm0.z;
|
|
1300
|
-
g110 *= norm0.w;
|
|
1301
|
-
vec4 norm1 = taylorInvSqrt(vec4(dot(g001, g001), dot(g011, g011), dot(g101, g101), dot(g111, g111)));
|
|
1302
|
-
g001 *= norm1.x;
|
|
1303
|
-
g011 *= norm1.y;
|
|
1304
|
-
g101 *= norm1.z;
|
|
1305
|
-
g111 *= norm1.w;
|
|
1306
|
-
|
|
1307
|
-
float n000 = dot(g000, Pf0);
|
|
1308
|
-
float n100 = dot(g100, vec3(Pf1.x, Pf0.yz));
|
|
1309
|
-
float n010 = dot(g010, vec3(Pf0.x, Pf1.y, Pf0.z));
|
|
1310
|
-
float n110 = dot(g110, vec3(Pf1.xy, Pf0.z));
|
|
1311
|
-
float n001 = dot(g001, vec3(Pf0.xy, Pf1.z));
|
|
1312
|
-
float n101 = dot(g101, vec3(Pf1.x, Pf0.y, Pf1.z));
|
|
1313
|
-
float n011 = dot(g011, vec3(Pf0.x, Pf1.yz));
|
|
1314
|
-
float n111 = dot(g111, Pf1);
|
|
1315
|
-
|
|
1316
|
-
vec3 fade_xyz = fade(Pf0);
|
|
1317
|
-
vec4 n_z = mix(vec4(n000, n100, n010, n110), vec4(n001, n101, n011, n111), fade_xyz.z);
|
|
1318
|
-
vec2 n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y);
|
|
1319
|
-
float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x);
|
|
1320
|
-
return 2.2 * n_xyz;
|
|
1321
820
|
}
|
|
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 = `
|
|
1331
|
-
|
|
1332
|
-
vec3 saturation(vec3 rgb, float adjustment) {
|
|
1333
|
-
const vec3 W = vec3(0.2125, 0.7154, 0.0721);
|
|
1334
|
-
vec3 intensity = vec3(dot(rgb, W));
|
|
1335
|
-
return mix(intensity, rgb, adjustment);
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
float saturation(vec3 rgb)
|
|
1339
|
-
{
|
|
1340
|
-
vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
|
|
1341
|
-
vec4 p = mix(vec4(rgb.bg, K.wz), vec4(rgb.gb, K.xy), step(rgb.b, rgb.g));
|
|
1342
|
-
vec4 q = mix(vec4(p.xyw, rgb.r), vec4(rgb.r, p.yzx), step(p.x, rgb.r));
|
|
1343
|
-
|
|
1344
|
-
float d = q.x - min(q.w, q.y);
|
|
1345
|
-
float e = 1.0e-10;
|
|
1346
|
-
return abs(6.0 * d + e);
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
// get saturation of a color in values between 0 and 1
|
|
1350
|
-
float getSaturation(vec3 color) {
|
|
1351
|
-
float max = max(color.r, max(color.g, color.b));
|
|
1352
|
-
float min = min(color.r, min(color.g, color.b));
|
|
1353
|
-
return (max - min) / max;
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
vec3 rgb2hsv(vec3 c)
|
|
1357
|
-
{
|
|
1358
|
-
vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
|
|
1359
|
-
vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
|
|
1360
|
-
vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
|
|
1361
|
-
|
|
1362
|
-
float d = q.x - min(q.w, q.y);
|
|
1363
|
-
float e = 1.0e-10;
|
|
1364
|
-
return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
vec3 hsv2rgb(vec3 c)
|
|
1368
|
-
{
|
|
1369
|
-
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
|
|
1370
|
-
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
|
|
1371
|
-
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
|
|
1372
|
-
}
|
|
1373
|
-
`;
|
|
1374
|
-
return cachedColorFunctionsShader;
|
|
1375
|
-
};
|
|
1376
821
|
const setLinkStyles = (link) => {
|
|
1377
822
|
link.id = LINK_ID;
|
|
1378
823
|
link.href = "https://neat.firecms.co";
|
|
@@ -1389,21 +834,27 @@ const setLinkStyles = (link) => {
|
|
|
1389
834
|
link.style.fontWeight = "bold";
|
|
1390
835
|
link.style.textDecoration = "none";
|
|
1391
836
|
link.style.zIndex = "10000";
|
|
837
|
+
link.style.pointerEvents = "auto";
|
|
838
|
+
link.setAttribute("data-n", "1");
|
|
1392
839
|
link.innerHTML = "NEAT";
|
|
1393
840
|
};
|
|
1394
841
|
const addNeatLink = (ref) => {
|
|
1395
|
-
const
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
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;
|
|
1402
853
|
}
|
|
1403
854
|
}
|
|
1404
855
|
const link = document.createElement("a");
|
|
1405
856
|
setLinkStyles(link);
|
|
1406
|
-
|
|
857
|
+
parent?.appendChild(link);
|
|
1407
858
|
return link;
|
|
1408
859
|
};
|
|
1409
860
|
function getElapsedSecondsInLastHour() {
|
|
@@ -1429,4 +880,50 @@ function downloadURI(uri, name) {
|
|
|
1429
880
|
link.click();
|
|
1430
881
|
document.body.removeChild(link);
|
|
1431
882
|
}
|
|
883
|
+
function injectSEO() {
|
|
884
|
+
if (document.getElementById("neat-seo-schema"))
|
|
885
|
+
return;
|
|
886
|
+
// 1. JSON-LD Schema
|
|
887
|
+
const script = document.createElement('script');
|
|
888
|
+
script.id = "neat-seo-schema";
|
|
889
|
+
script.type = 'application/ld+json';
|
|
890
|
+
script.text = JSON.stringify({
|
|
891
|
+
"@context": "https://schema.org",
|
|
892
|
+
"@type": "WebSite",
|
|
893
|
+
"name": "NEAT Gradient",
|
|
894
|
+
"url": "https://neat.firecms.co",
|
|
895
|
+
"author": {
|
|
896
|
+
"@type": "Organization",
|
|
897
|
+
"name": "FireCMS",
|
|
898
|
+
"url": "https://firecms.co"
|
|
899
|
+
},
|
|
900
|
+
"description": "Beautiful, fast, heavily customizable, WebGL based gradients."
|
|
901
|
+
});
|
|
902
|
+
document.head.appendChild(script);
|
|
903
|
+
// 2. Hidden Backlink via Shadow DOM
|
|
904
|
+
const hiddenContainer = document.createElement('div');
|
|
905
|
+
hiddenContainer.style.position = 'absolute';
|
|
906
|
+
hiddenContainer.style.width = '1px';
|
|
907
|
+
hiddenContainer.style.height = '1px';
|
|
908
|
+
hiddenContainer.style.padding = '0';
|
|
909
|
+
hiddenContainer.style.margin = '-1px';
|
|
910
|
+
hiddenContainer.style.overflow = 'hidden';
|
|
911
|
+
hiddenContainer.style.clip = 'rect(0, 0, 0, 0)';
|
|
912
|
+
hiddenContainer.style.whiteSpace = 'nowrap';
|
|
913
|
+
hiddenContainer.style.borderWidth = '0';
|
|
914
|
+
try {
|
|
915
|
+
const shadow = hiddenContainer.attachShadow({ mode: 'closed' });
|
|
916
|
+
const link = document.createElement('a');
|
|
917
|
+
link.href = "https://firecms.co";
|
|
918
|
+
link.textContent = "FireCMS";
|
|
919
|
+
shadow.appendChild(link);
|
|
920
|
+
}
|
|
921
|
+
catch (e) {
|
|
922
|
+
const link = document.createElement('a');
|
|
923
|
+
link.href = "https://firecms.co";
|
|
924
|
+
link.textContent = "FireCMS";
|
|
925
|
+
hiddenContainer.appendChild(link);
|
|
926
|
+
}
|
|
927
|
+
document.body.appendChild(hiddenContainer);
|
|
928
|
+
}
|
|
1432
929
|
//# sourceMappingURL=NeatGradient.js.map
|