@brochington/shader-backgrounds 0.1.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/.github/workflows/deploy-demo-to-pages.yml +63 -0
- package/PLUGINS.md +269 -0
- package/README.md +159 -0
- package/demo.js +1044 -0
- package/index.html +194 -0
- package/package.json +23 -0
- package/src/index.ts +5 -0
- package/src/lib/components/web-component.ts +198 -0
- package/src/lib/core/ShaderCanvas.ts +235 -0
- package/src/lib/core/types.ts +26 -0
- package/src/lib/plugins/AuroraWavesPlugin.ts +128 -0
- package/src/lib/plugins/CausticsPlugin.ts +128 -0
- package/src/lib/plugins/ContourLinesPlugin.ts +148 -0
- package/src/lib/plugins/DreamyBokehPlugin.ts +191 -0
- package/src/lib/plugins/GradientPlugin.ts +445 -0
- package/src/lib/plugins/GrainyFogPlugin.ts +139 -0
- package/src/lib/plugins/InkWashPlugin.ts +182 -0
- package/src/lib/plugins/LiquidOrbPlugin.ts +140 -0
- package/src/lib/plugins/RetroGridPlugin.ts +77 -0
- package/src/lib/plugins/SoftStarfieldPlugin.ts +156 -0
- package/src/lib/plugins/StainedGlassPlugin.ts +261 -0
- package/src/lib/plugins/index.ts +11 -0
- package/tsconfig.json +26 -0
- package/vite.config.ts +19 -0
- package/vite.demo.config.ts +13 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import { Color } from 'ogl';
|
|
2
|
+
import { ShaderPlugin } from '../core/types';
|
|
3
|
+
|
|
4
|
+
export type GradientEasing =
|
|
5
|
+
| 'linear'
|
|
6
|
+
| 'smoothstep'
|
|
7
|
+
| 'easeInOutQuad'
|
|
8
|
+
| 'easeInOutCubic';
|
|
9
|
+
|
|
10
|
+
export type GradientMotionMode = 'none' | 'path' | 'random';
|
|
11
|
+
|
|
12
|
+
export type GradientMotionBounds = {
|
|
13
|
+
minX: number;
|
|
14
|
+
maxX: number;
|
|
15
|
+
minY: number;
|
|
16
|
+
maxY: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type GradientMotion = {
|
|
20
|
+
/**
|
|
21
|
+
* - "none": static point (default)
|
|
22
|
+
* - "path": moves between points in `path`
|
|
23
|
+
* - "random": picks random targets and eases between them
|
|
24
|
+
*/
|
|
25
|
+
mode?: GradientMotionMode;
|
|
26
|
+
/** Waypoints for "path" mode. */
|
|
27
|
+
path?: Array<{ x: number; y: number }>;
|
|
28
|
+
/** Seconds to move from start -> target. Default 3.0 */
|
|
29
|
+
duration?: number;
|
|
30
|
+
/** Easing curve for interpolation. Default "smoothstep" */
|
|
31
|
+
easing?: GradientEasing;
|
|
32
|
+
/**
|
|
33
|
+
* Bounds for clamping/random target generation. Defaults to [-1..1] in both axes.
|
|
34
|
+
* (These are in the same -1..1 coordinate space as `x`/`y`.)
|
|
35
|
+
*/
|
|
36
|
+
bounds?: Partial<GradientMotionBounds>;
|
|
37
|
+
/**
|
|
38
|
+
* Random mode only: if > 0, choose random targets within this radius around the point's
|
|
39
|
+
* base `x`/`y` (then clamp to bounds). If omitted/0, choose targets anywhere in bounds.
|
|
40
|
+
*/
|
|
41
|
+
randomRadius?: number;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type GradientPoint = {
|
|
45
|
+
x: number; // Range -1 to 1
|
|
46
|
+
y: number; // Range -1 to 1
|
|
47
|
+
colors: string[]; // Hex colors e.g., ["#ff0000", "#0000ff"]
|
|
48
|
+
speed?: number; // Speed of color cycle, default 1.0
|
|
49
|
+
motion?: GradientMotion;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type GradientPluginOptions = {
|
|
53
|
+
/** Defaults applied to any point that doesn't specify a `motion` field. */
|
|
54
|
+
defaultMotion?: GradientMotion;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export class GradientPlugin implements ShaderPlugin {
|
|
58
|
+
name = 'gradient-points';
|
|
59
|
+
|
|
60
|
+
// Constants
|
|
61
|
+
private static MAX_POINTS = 16; // Hard limit for WebGL uniform array size
|
|
62
|
+
|
|
63
|
+
// Shader
|
|
64
|
+
// We use Inverse Distance Weighting for soft interpolation
|
|
65
|
+
fragmentShader = /* glsl */ `
|
|
66
|
+
precision highp float;
|
|
67
|
+
|
|
68
|
+
uniform float uTime;
|
|
69
|
+
uniform vec2 uResolution;
|
|
70
|
+
|
|
71
|
+
uniform int uPointCount;
|
|
72
|
+
uniform vec2 uPoints[${GradientPlugin.MAX_POINTS}];
|
|
73
|
+
uniform vec3 uColors[${GradientPlugin.MAX_POINTS}];
|
|
74
|
+
|
|
75
|
+
varying vec2 vUv;
|
|
76
|
+
|
|
77
|
+
void main() {
|
|
78
|
+
// 1. Normalize UVs to preserve aspect ratio
|
|
79
|
+
// This ensures points placed at (0.5, 0.5) look correct on rectangles
|
|
80
|
+
float aspect = uResolution.x / uResolution.y;
|
|
81
|
+
vec2 uv = vUv * 2.0 - 1.0; // Transform UV 0..1 to -1..1
|
|
82
|
+
uv.x *= aspect;
|
|
83
|
+
|
|
84
|
+
vec3 finalColor = vec3(0.0);
|
|
85
|
+
float totalWeight = 0.0;
|
|
86
|
+
|
|
87
|
+
for (int i = 0; i < ${GradientPlugin.MAX_POINTS}; i++) {
|
|
88
|
+
if (i >= uPointCount) break;
|
|
89
|
+
|
|
90
|
+
vec2 p = uPoints[i];
|
|
91
|
+
p.x *= aspect; // Apply same aspect correction to point
|
|
92
|
+
|
|
93
|
+
// Calculate Distance
|
|
94
|
+
float dist = distance(uv, p);
|
|
95
|
+
|
|
96
|
+
// Weight Function: 1 / (dist^power)
|
|
97
|
+
// Power controls how "fat" the points are.
|
|
98
|
+
// 2.0 is standard (Gravity), lower (e.g. 1.5) is softer/fuzzier.
|
|
99
|
+
float w = 1.0 / pow(dist, 2.0);
|
|
100
|
+
|
|
101
|
+
// Clamp weight to avoid infinity at exact point location
|
|
102
|
+
w = min(w, 1000.0);
|
|
103
|
+
|
|
104
|
+
finalColor += uColors[i] * w;
|
|
105
|
+
totalWeight += w;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Avoid division by zero
|
|
109
|
+
if (totalWeight > 0.0) {
|
|
110
|
+
finalColor /= totalWeight;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
gl_FragColor = vec4(finalColor, 1.0);
|
|
114
|
+
}
|
|
115
|
+
`;
|
|
116
|
+
|
|
117
|
+
uniforms: any;
|
|
118
|
+
|
|
119
|
+
// State for Color Animation
|
|
120
|
+
private pointsConfig: GradientPoint[];
|
|
121
|
+
// Pre-parsed RGB colors per point to avoid per-frame allocations
|
|
122
|
+
#parsedColors: Array<Array<[number, number, number]>> = [];
|
|
123
|
+
private colorStates: Array<{
|
|
124
|
+
currentIdx: number;
|
|
125
|
+
nextIdx: number;
|
|
126
|
+
t: number;
|
|
127
|
+
}>;
|
|
128
|
+
|
|
129
|
+
// State for Motion Animation
|
|
130
|
+
private motionStates: Array<{
|
|
131
|
+
mode: GradientMotionMode;
|
|
132
|
+
easing: GradientEasing;
|
|
133
|
+
duration: number;
|
|
134
|
+
bounds: GradientMotionBounds;
|
|
135
|
+
randomRadius: number;
|
|
136
|
+
// current lerp segment
|
|
137
|
+
startX: number;
|
|
138
|
+
startY: number;
|
|
139
|
+
targetX: number;
|
|
140
|
+
targetY: number;
|
|
141
|
+
t: number; // 0..1
|
|
142
|
+
// path
|
|
143
|
+
pathIndex: number;
|
|
144
|
+
// current value
|
|
145
|
+
x: number;
|
|
146
|
+
y: number;
|
|
147
|
+
}>;
|
|
148
|
+
|
|
149
|
+
private defaultMotion: GradientMotion;
|
|
150
|
+
|
|
151
|
+
constructor(points: GradientPoint[], options: GradientPluginOptions = {}) {
|
|
152
|
+
// Validate count
|
|
153
|
+
if (points.length > GradientPlugin.MAX_POINTS) {
|
|
154
|
+
console.warn(
|
|
155
|
+
`GradientPlugin: Max points is ${GradientPlugin.MAX_POINTS}. Truncating.`
|
|
156
|
+
);
|
|
157
|
+
points = points.slice(0, GradientPlugin.MAX_POINTS);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
this.pointsConfig = points;
|
|
161
|
+
this.defaultMotion = options.defaultMotion ?? {};
|
|
162
|
+
|
|
163
|
+
// Pre-parse colors once to avoid per-frame allocations in onRender.
|
|
164
|
+
this.#parsedColors = points.map((p) =>
|
|
165
|
+
(p.colors ?? []).map((hex) => {
|
|
166
|
+
const c = new Color(hex);
|
|
167
|
+
return [c.r, c.g, c.b];
|
|
168
|
+
})
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
// Initialize color animation state for each point
|
|
172
|
+
this.colorStates = points.map(() => ({
|
|
173
|
+
currentIdx: 0,
|
|
174
|
+
nextIdx: 1 % 2, // Safe default will be fixed in loop
|
|
175
|
+
t: 0,
|
|
176
|
+
}));
|
|
177
|
+
|
|
178
|
+
this.motionStates = points.map((p) => this.#createMotionState(p));
|
|
179
|
+
|
|
180
|
+
// Prepare Uniform Arrays
|
|
181
|
+
const pointArray = new Array(GradientPlugin.MAX_POINTS);
|
|
182
|
+
const colorArray = new Array(GradientPlugin.MAX_POINTS);
|
|
183
|
+
|
|
184
|
+
// Initial Population
|
|
185
|
+
points.forEach((p, i) => {
|
|
186
|
+
// Coords as vec2 array [x, y]
|
|
187
|
+
pointArray[i] = [p.x, p.y];
|
|
188
|
+
|
|
189
|
+
// Initial Color as vec3 array [r, g, b]
|
|
190
|
+
const rgb0 = this.#parsedColors[i]?.[0] ?? [0, 0, 0];
|
|
191
|
+
colorArray[i] = [rgb0[0], rgb0[1], rgb0[2]];
|
|
192
|
+
|
|
193
|
+
// Fix state logic if array length is small
|
|
194
|
+
if (p.colors.length < 2) {
|
|
195
|
+
// If only one color, it just loops to itself
|
|
196
|
+
this.colorStates[i].nextIdx = 0;
|
|
197
|
+
} else {
|
|
198
|
+
this.colorStates[i].nextIdx = 1;
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Fill remaining slots with default values
|
|
203
|
+
for (let i = points.length; i < GradientPlugin.MAX_POINTS; i++) {
|
|
204
|
+
pointArray[i] = [0, 0]; // Default center
|
|
205
|
+
colorArray[i] = [0, 0, 0]; // Default black
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
this.uniforms = {
|
|
209
|
+
uPointCount: { value: points.length },
|
|
210
|
+
uPoints: { value: pointArray },
|
|
211
|
+
uColors: { value: colorArray },
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
#clamp(n: number, min: number, max: number) {
|
|
216
|
+
return Math.max(min, Math.min(max, n));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
#lerp(a: number, b: number, t: number) {
|
|
220
|
+
return a + (b - a) * t;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
#ease(t: number, easing: GradientEasing) {
|
|
224
|
+
const x = this.#clamp(t, 0, 1);
|
|
225
|
+
switch (easing) {
|
|
226
|
+
case 'linear':
|
|
227
|
+
return x;
|
|
228
|
+
case 'easeInOutQuad':
|
|
229
|
+
return x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2;
|
|
230
|
+
case 'easeInOutCubic':
|
|
231
|
+
return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;
|
|
232
|
+
case 'smoothstep':
|
|
233
|
+
default:
|
|
234
|
+
return x * x * (3 - 2 * x);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
#resolveMotion(point: GradientPoint) {
|
|
239
|
+
const m: GradientMotion = { ...this.defaultMotion, ...(point.motion ?? {}) };
|
|
240
|
+
const bounds: GradientMotionBounds = {
|
|
241
|
+
minX: -1,
|
|
242
|
+
maxX: 1,
|
|
243
|
+
minY: -1,
|
|
244
|
+
maxY: 1,
|
|
245
|
+
...(m.bounds ?? {}),
|
|
246
|
+
};
|
|
247
|
+
// ensure sane ordering
|
|
248
|
+
if (bounds.minX > bounds.maxX) [bounds.minX, bounds.maxX] = [bounds.maxX, bounds.minX];
|
|
249
|
+
if (bounds.minY > bounds.maxY) [bounds.minY, bounds.maxY] = [bounds.maxY, bounds.minY];
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
mode: (m.mode ?? 'none') as GradientMotionMode,
|
|
253
|
+
path: m.path ?? [],
|
|
254
|
+
duration: Math.max(0.001, m.duration ?? 3.0),
|
|
255
|
+
easing: (m.easing ?? 'smoothstep') as GradientEasing,
|
|
256
|
+
bounds,
|
|
257
|
+
randomRadius: Math.max(0, m.randomRadius ?? 0),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
#randomTarget(
|
|
262
|
+
baseX: number,
|
|
263
|
+
baseY: number,
|
|
264
|
+
bounds: GradientMotionBounds,
|
|
265
|
+
radius: number
|
|
266
|
+
) {
|
|
267
|
+
if (radius > 0) {
|
|
268
|
+
// uniform disk
|
|
269
|
+
const a = Math.random() * Math.PI * 2;
|
|
270
|
+
const r = Math.sqrt(Math.random()) * radius;
|
|
271
|
+
const x = baseX + Math.cos(a) * r;
|
|
272
|
+
const y = baseY + Math.sin(a) * r;
|
|
273
|
+
return {
|
|
274
|
+
x: this.#clamp(x, bounds.minX, bounds.maxX),
|
|
275
|
+
y: this.#clamp(y, bounds.minY, bounds.maxY),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
x: this.#lerp(bounds.minX, bounds.maxX, Math.random()),
|
|
280
|
+
y: this.#lerp(bounds.minY, bounds.maxY, Math.random()),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
#createMotionState(point: GradientPoint) {
|
|
285
|
+
const r = this.#resolveMotion(point);
|
|
286
|
+
const state = {
|
|
287
|
+
mode: r.mode,
|
|
288
|
+
easing: r.easing,
|
|
289
|
+
duration: r.duration,
|
|
290
|
+
bounds: r.bounds,
|
|
291
|
+
randomRadius: r.randomRadius,
|
|
292
|
+
startX: point.x,
|
|
293
|
+
startY: point.y,
|
|
294
|
+
targetX: point.x,
|
|
295
|
+
targetY: point.y,
|
|
296
|
+
t: 1,
|
|
297
|
+
pathIndex: 0,
|
|
298
|
+
x: point.x,
|
|
299
|
+
y: point.y,
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
if (r.mode === 'path' && r.path.length > 0) {
|
|
303
|
+
state.targetX = this.#clamp(r.path[0].x, r.bounds.minX, r.bounds.maxX);
|
|
304
|
+
state.targetY = this.#clamp(r.path[0].y, r.bounds.minY, r.bounds.maxY);
|
|
305
|
+
state.t = 0;
|
|
306
|
+
} else if (r.mode === 'random') {
|
|
307
|
+
const tgt = this.#randomTarget(point.x, point.y, r.bounds, r.randomRadius);
|
|
308
|
+
state.targetX = tgt.x;
|
|
309
|
+
state.targetY = tgt.y;
|
|
310
|
+
state.t = 0;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return state;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
#stepMotion(point: GradientPoint, state: GradientPlugin['motionStates'][number], dt: number) {
|
|
317
|
+
const r = this.#resolveMotion(point);
|
|
318
|
+
|
|
319
|
+
// If motion config changed, re-seed state but preserve current position.
|
|
320
|
+
const modeChanged = state.mode !== r.mode;
|
|
321
|
+
const easingChanged = state.easing !== r.easing;
|
|
322
|
+
const durationChanged = state.duration !== r.duration;
|
|
323
|
+
const boundsChanged =
|
|
324
|
+
state.bounds.minX !== r.bounds.minX ||
|
|
325
|
+
state.bounds.maxX !== r.bounds.maxX ||
|
|
326
|
+
state.bounds.minY !== r.bounds.minY ||
|
|
327
|
+
state.bounds.maxY !== r.bounds.maxY;
|
|
328
|
+
const radiusChanged = state.randomRadius !== r.randomRadius;
|
|
329
|
+
|
|
330
|
+
if (modeChanged || easingChanged || durationChanged || boundsChanged || radiusChanged) {
|
|
331
|
+
state.mode = r.mode;
|
|
332
|
+
state.easing = r.easing;
|
|
333
|
+
state.duration = r.duration;
|
|
334
|
+
state.bounds = r.bounds;
|
|
335
|
+
state.randomRadius = r.randomRadius;
|
|
336
|
+
|
|
337
|
+
state.startX = state.x;
|
|
338
|
+
state.startY = state.y;
|
|
339
|
+
state.t = 0;
|
|
340
|
+
state.pathIndex = 0;
|
|
341
|
+
|
|
342
|
+
if (r.mode === 'path' && r.path.length > 0) {
|
|
343
|
+
state.targetX = this.#clamp(r.path[0].x, r.bounds.minX, r.bounds.maxX);
|
|
344
|
+
state.targetY = this.#clamp(r.path[0].y, r.bounds.minY, r.bounds.maxY);
|
|
345
|
+
} else if (r.mode === 'random') {
|
|
346
|
+
const tgt = this.#randomTarget(point.x, point.y, r.bounds, r.randomRadius);
|
|
347
|
+
state.targetX = tgt.x;
|
|
348
|
+
state.targetY = tgt.y;
|
|
349
|
+
} else {
|
|
350
|
+
state.targetX = point.x;
|
|
351
|
+
state.targetY = point.y;
|
|
352
|
+
state.t = 1;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (r.mode === 'none') {
|
|
357
|
+
// always track the base point position (useful if user mutates x/y without recreating plugin)
|
|
358
|
+
state.x = this.#clamp(point.x, r.bounds.minX, r.bounds.maxX);
|
|
359
|
+
state.y = this.#clamp(point.y, r.bounds.minY, r.bounds.maxY);
|
|
360
|
+
state.startX = state.x;
|
|
361
|
+
state.startY = state.y;
|
|
362
|
+
state.targetX = state.x;
|
|
363
|
+
state.targetY = state.y;
|
|
364
|
+
state.t = 1;
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Advance time
|
|
369
|
+
state.t += dt / (r.duration * 1000);
|
|
370
|
+
if (state.t >= 1) {
|
|
371
|
+
// snap to target and pick next
|
|
372
|
+
state.x = state.targetX;
|
|
373
|
+
state.y = state.targetY;
|
|
374
|
+
state.startX = state.x;
|
|
375
|
+
state.startY = state.y;
|
|
376
|
+
state.t = 0;
|
|
377
|
+
|
|
378
|
+
if (r.mode === 'path') {
|
|
379
|
+
if (r.path.length === 0) {
|
|
380
|
+
state.targetX = state.x;
|
|
381
|
+
state.targetY = state.y;
|
|
382
|
+
state.t = 1;
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
state.pathIndex = (state.pathIndex + 1) % r.path.length;
|
|
386
|
+
const wp = r.path[state.pathIndex];
|
|
387
|
+
state.targetX = this.#clamp(wp.x, r.bounds.minX, r.bounds.maxX);
|
|
388
|
+
state.targetY = this.#clamp(wp.y, r.bounds.minY, r.bounds.maxY);
|
|
389
|
+
} else {
|
|
390
|
+
// random
|
|
391
|
+
const tgt = this.#randomTarget(point.x, point.y, r.bounds, r.randomRadius);
|
|
392
|
+
state.targetX = tgt.x;
|
|
393
|
+
state.targetY = tgt.y;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const e = this.#ease(state.t, r.easing);
|
|
398
|
+
state.x = this.#lerp(state.startX, state.targetX, e);
|
|
399
|
+
state.y = this.#lerp(state.startY, state.targetY, e);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
onRender(dt: number) {
|
|
403
|
+
const pointArray = this.uniforms.uPoints.value;
|
|
404
|
+
const colorArray = this.uniforms.uColors.value;
|
|
405
|
+
|
|
406
|
+
this.pointsConfig.forEach((p, i) => {
|
|
407
|
+
// Update motion (position) first
|
|
408
|
+
const motionState = this.motionStates[i];
|
|
409
|
+
this.#stepMotion(p, motionState, dt);
|
|
410
|
+
pointArray[i][0] = motionState.x;
|
|
411
|
+
pointArray[i][1] = motionState.y;
|
|
412
|
+
|
|
413
|
+
if (p.colors.length <= 1) return;
|
|
414
|
+
|
|
415
|
+
const state = this.colorStates[i];
|
|
416
|
+
const speed = p.speed || 1.0;
|
|
417
|
+
|
|
418
|
+
// Update Interpolation T
|
|
419
|
+
// dt is in ms, we want speed relative to seconds roughly
|
|
420
|
+
state.t += dt * 0.001 * speed;
|
|
421
|
+
|
|
422
|
+
if (state.t >= 1.0) {
|
|
423
|
+
state.t = 0;
|
|
424
|
+
state.currentIdx = state.nextIdx;
|
|
425
|
+
state.nextIdx = (state.nextIdx + 1) % p.colors.length;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Perform Lerp
|
|
429
|
+
const c1 = this.#parsedColors[i][state.currentIdx];
|
|
430
|
+
const c2 = this.#parsedColors[i][state.nextIdx];
|
|
431
|
+
|
|
432
|
+
// Simple RGB Linear Interpolation
|
|
433
|
+
const r = c1[0] + (c2[0] - c1[0]) * state.t;
|
|
434
|
+
const g = c1[1] + (c2[1] - c1[1]) * state.t;
|
|
435
|
+
const b = c1[2] + (c2[2] - c1[2]) * state.t;
|
|
436
|
+
|
|
437
|
+
// Update array element
|
|
438
|
+
colorArray[i][0] = r;
|
|
439
|
+
colorArray[i][1] = g;
|
|
440
|
+
colorArray[i][2] = b;
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// No need to reassign the array - OGL will detect the changes
|
|
444
|
+
}
|
|
445
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { Color } from 'ogl';
|
|
2
|
+
import { ShaderPlugin } from '../core/types';
|
|
3
|
+
|
|
4
|
+
export type GrainyFogConfig = {
|
|
5
|
+
firstColor: string;
|
|
6
|
+
secondColor: string;
|
|
7
|
+
backgroundColor: string;
|
|
8
|
+
grainAmount?: number; // 0.0 to 1.0, default 0.12
|
|
9
|
+
speed?: number; // default 1.0
|
|
10
|
+
scale?: number; // Noise scale, default 2.25
|
|
11
|
+
octaves?: number; // 1..6, default 4
|
|
12
|
+
lacunarity?: number; // default 2.0
|
|
13
|
+
gain?: number; // default 0.5
|
|
14
|
+
contrast?: number; // 0.5..2.5, default 1.25
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export class GrainyFogPlugin implements ShaderPlugin {
|
|
18
|
+
name = 'grainy-fog';
|
|
19
|
+
|
|
20
|
+
fragmentShader = /* glsl */ `
|
|
21
|
+
precision highp float;
|
|
22
|
+
uniform float uTimeInternal;
|
|
23
|
+
uniform vec2 uResolution;
|
|
24
|
+
uniform vec3 uColor1;
|
|
25
|
+
uniform vec3 uColor2;
|
|
26
|
+
uniform vec3 uBgColor;
|
|
27
|
+
uniform float uGrain;
|
|
28
|
+
uniform float uScale;
|
|
29
|
+
uniform float uContrast;
|
|
30
|
+
uniform int uOctaves;
|
|
31
|
+
uniform float uLacunarity;
|
|
32
|
+
uniform float uGain;
|
|
33
|
+
|
|
34
|
+
varying vec2 vUv;
|
|
35
|
+
|
|
36
|
+
// --- Value Noise / FBM ---
|
|
37
|
+
float hash12(vec2 p) {
|
|
38
|
+
// Dave Hoskins-ish: cheap, stable
|
|
39
|
+
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
|
|
40
|
+
p3 += dot(p3, p3.yzx + 33.33);
|
|
41
|
+
return fract((p3.x + p3.y) * p3.z);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
float noise(in vec2 st) {
|
|
45
|
+
vec2 i = floor(st);
|
|
46
|
+
vec2 f = fract(st);
|
|
47
|
+
|
|
48
|
+
float a = hash12(i);
|
|
49
|
+
float b = hash12(i + vec2(1.0, 0.0));
|
|
50
|
+
float c = hash12(i + vec2(0.0, 1.0));
|
|
51
|
+
float d = hash12(i + vec2(1.0, 1.0));
|
|
52
|
+
|
|
53
|
+
vec2 u = f * f * (3.0 - 2.0 * f);
|
|
54
|
+
return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
float fbm(in vec2 st) {
|
|
58
|
+
float value = 0.0;
|
|
59
|
+
float amplitude = 0.5;
|
|
60
|
+
float freq = 1.0;
|
|
61
|
+
|
|
62
|
+
// rotate to reduce axial bias
|
|
63
|
+
mat2 rot = mat2(0.80, -0.60, 0.60, 0.80);
|
|
64
|
+
|
|
65
|
+
for (int i = 0; i < 6; i++) {
|
|
66
|
+
if (i >= uOctaves) break;
|
|
67
|
+
value += amplitude * noise(st * freq);
|
|
68
|
+
st = rot * st + 19.19;
|
|
69
|
+
freq *= uLacunarity;
|
|
70
|
+
amplitude *= uGain;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
void main() {
|
|
77
|
+
vec2 uv = vUv;
|
|
78
|
+
|
|
79
|
+
// aspect-correct domain
|
|
80
|
+
float aspect = uResolution.x / uResolution.y;
|
|
81
|
+
vec2 p = (uv - 0.5) * vec2(aspect, 1.0);
|
|
82
|
+
|
|
83
|
+
float t = uTimeInternal;
|
|
84
|
+
|
|
85
|
+
// Two-layer flow field -> richer motion
|
|
86
|
+
vec2 q;
|
|
87
|
+
q.x = fbm(p * uScale + vec2(0.0, 0.12 * t));
|
|
88
|
+
q.y = fbm(p * (uScale * 0.9) + vec2(3.1, -0.08 * t));
|
|
89
|
+
|
|
90
|
+
vec2 r;
|
|
91
|
+
r.x = fbm(p * (uScale * 1.2) + 1.7 * q + vec2(1.7, 9.2) + 0.15 * t);
|
|
92
|
+
r.y = fbm(p * (uScale * 1.1) + 1.3 * q + vec2(8.3, 2.8) + 0.11 * t);
|
|
93
|
+
|
|
94
|
+
float f = fbm(p * uScale + r);
|
|
95
|
+
|
|
96
|
+
// Contrast curve (keeps highlights punchy)
|
|
97
|
+
f = pow(clamp(f, 0.0, 1.0), 1.0 / max(0.001, uContrast));
|
|
98
|
+
|
|
99
|
+
// Color mix
|
|
100
|
+
vec3 col = uBgColor;
|
|
101
|
+
col = mix(col, uColor1, smoothstep(0.10, 0.85, f));
|
|
102
|
+
col = mix(col, uColor2, smoothstep(0.15, 0.95, length(q)));
|
|
103
|
+
|
|
104
|
+
// Film grain in pixel space (stable-ish)
|
|
105
|
+
vec2 px = uv * uResolution;
|
|
106
|
+
float g = (hash12(px + t * 60.0) - 0.5) * 2.0;
|
|
107
|
+
col += g * uGrain;
|
|
108
|
+
|
|
109
|
+
gl_FragColor = vec4(clamp(col, 0.0, 1.0), 1.0);
|
|
110
|
+
}
|
|
111
|
+
`;
|
|
112
|
+
|
|
113
|
+
uniforms: any;
|
|
114
|
+
private speed: number;
|
|
115
|
+
|
|
116
|
+
constructor(config: GrainyFogConfig) {
|
|
117
|
+
const c1 = new Color(config.firstColor);
|
|
118
|
+
const c2 = new Color(config.secondColor);
|
|
119
|
+
const bg = new Color(config.backgroundColor);
|
|
120
|
+
this.speed = config.speed ?? 1.0;
|
|
121
|
+
|
|
122
|
+
this.uniforms = {
|
|
123
|
+
uColor1: { value: [c1.r, c1.g, c1.b] },
|
|
124
|
+
uColor2: { value: [c2.r, c2.g, c2.b] },
|
|
125
|
+
uBgColor: { value: [bg.r, bg.g, bg.b] },
|
|
126
|
+
uGrain: { value: config.grainAmount ?? 0.12 },
|
|
127
|
+
uScale: { value: config.scale ?? 2.25 },
|
|
128
|
+
uContrast: { value: config.contrast ?? 1.25 },
|
|
129
|
+
uOctaves: { value: Math.max(1, Math.min(6, config.octaves ?? 4)) },
|
|
130
|
+
uLacunarity: { value: config.lacunarity ?? 2.0 },
|
|
131
|
+
uGain: { value: config.gain ?? 0.5 },
|
|
132
|
+
uTimeInternal: { value: 0 },
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
onRender(dt: number) {
|
|
137
|
+
this.uniforms.uTimeInternal.value += dt * 0.001 * this.speed;
|
|
138
|
+
}
|
|
139
|
+
}
|