@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/demo.js ADDED
@@ -0,0 +1,1044 @@
1
+ import { GradientPlugin } from './src/lib/plugins/GradientPlugin';
2
+ import { GrainyFogPlugin } from './src/lib/plugins/GrainyFogPlugin';
3
+ import { RetroGridPlugin } from './src/lib/plugins/RetroGridPlugin';
4
+ import { LiquidOrbPlugin } from './src/lib/plugins/LiquidOrbPlugin';
5
+ import { CausticsPlugin } from './src/lib/plugins/CausticsPlugin';
6
+ import { AuroraWavesPlugin } from './src/lib/plugins/AuroraWavesPlugin';
7
+ import { SoftStarfieldPlugin } from './src/lib/plugins/SoftStarfieldPlugin';
8
+ import { ContourLinesPlugin } from './src/lib/plugins/ContourLinesPlugin';
9
+ import { DreamyBokehPlugin } from './src/lib/plugins/DreamyBokehPlugin';
10
+ import { InkWashPlugin } from './src/lib/plugins/InkWashPlugin';
11
+ import { StainedGlassPlugin } from './src/lib/plugins/StainedGlassPlugin';
12
+
13
+ const el = document.getElementById('bg');
14
+ const pluginSelect = document.getElementById('pluginSelect');
15
+ const filterPreset = document.getElementById('filterPreset');
16
+ const filterInput = document.getElementById('filterInput');
17
+ const pluginParamsEl = document.getElementById('pluginParams');
18
+ const overlayToggleBtn = document.getElementById('overlayToggle');
19
+ const overlayEl = overlayToggleBtn?.closest?.('.overlay');
20
+
21
+ const gradientConfig1 = [
22
+ { x: -0.5, y: -0.5, colors: ['#ff0000', '#ffaa00'], speed: 0.5 },
23
+ { x: 0.5, y: 0.5, colors: ['#0000ff', '#00aaff'], speed: 1.0 },
24
+ { x: 0.0, y: 0.0, colors: ['#00ff00', '#ccffcc'], speed: 2.0 },
25
+ ];
26
+
27
+ const randomHex = () => {
28
+ const n = Math.floor(Math.random() * 0xffffff);
29
+ return `#${n.toString(16).padStart(6, '0')}`;
30
+ };
31
+
32
+ // --- Canvas CSS (from outside the web component) ---
33
+ // The internal <canvas> is exposed as a shadow part: `::part(canvas)`.
34
+ // You can style it via regular CSS like:
35
+ // #bg::part(canvas) { filter: blur(8px) saturate(1.2); }
36
+ const canvasStyleEl = document.createElement('style');
37
+ canvasStyleEl.id = 'canvasPartStyle';
38
+ document.head.appendChild(canvasStyleEl);
39
+
40
+ function setCanvasCss({ filter = 'none', transition = '180ms ease' } = {}) {
41
+ const safeFilter = (filter ?? '').trim() === '' ? 'none' : filter;
42
+ const safeTransition = (transition ?? '').trim();
43
+ const transitionCss =
44
+ safeTransition === ''
45
+ ? ''
46
+ : `transition: filter ${safeTransition}, -webkit-filter ${safeTransition};`;
47
+
48
+ canvasStyleEl.textContent = `
49
+ #bg::part(canvas) {
50
+ filter: ${safeFilter};
51
+ -webkit-filter: ${safeFilter};
52
+ ${transitionCss}
53
+ }
54
+ `;
55
+ }
56
+
57
+ // --- Dynamic plugin parameters form ---
58
+ const clamp = (n, min, max) => Math.max(min, Math.min(max, n));
59
+
60
+ function buildDefaults(schema) {
61
+ const cfg = {};
62
+ for (const f of schema.fields) {
63
+ cfg[f.key] = typeof f.default === 'function' ? f.default() : f.default;
64
+ }
65
+ return cfg;
66
+ }
67
+
68
+ function renderParamsForm(schema, cfg, onChange) {
69
+ if (!pluginParamsEl) return;
70
+ pluginParamsEl.innerHTML = '';
71
+
72
+ const row = document.createElement('div');
73
+ row.className = 'row';
74
+ pluginParamsEl.appendChild(row);
75
+
76
+ const setVal = (k, v) => {
77
+ cfg[k] = v;
78
+ onChange();
79
+ };
80
+
81
+ for (const field of schema.fields) {
82
+ const wrap = document.createElement('label');
83
+ wrap.className = 'field';
84
+
85
+ const label = document.createElement('span');
86
+ label.textContent = field.label;
87
+ wrap.appendChild(label);
88
+
89
+ let input;
90
+ if (field.type === 'select') {
91
+ input = document.createElement('select');
92
+ for (const opt of field.options) {
93
+ const o = document.createElement('option');
94
+ o.value = opt.value;
95
+ o.textContent = opt.label;
96
+ input.appendChild(o);
97
+ }
98
+ input.value = String(cfg[field.key] ?? field.default ?? '');
99
+ input.addEventListener('change', () => setVal(field.key, input.value));
100
+ } else if (field.type === 'boolean') {
101
+ input = document.createElement('input');
102
+ input.type = 'checkbox';
103
+ input.checked = Boolean(cfg[field.key]);
104
+ input.addEventListener('change', () => setVal(field.key, input.checked));
105
+ } else {
106
+ input = document.createElement('input');
107
+ input.type = field.type === 'color' ? 'color' : field.type === 'text' ? 'text' : 'number';
108
+
109
+ if (field.type === 'number') {
110
+ if (field.min != null) input.min = String(field.min);
111
+ if (field.max != null) input.max = String(field.max);
112
+ if (field.step != null) input.step = String(field.step);
113
+ input.value = String(cfg[field.key] ?? field.default ?? '');
114
+ } else {
115
+ input.value = String(cfg[field.key] ?? field.default ?? '');
116
+ }
117
+
118
+ const commit = () => {
119
+ if (field.type === 'number') {
120
+ const v = Number(input.value);
121
+ if (!Number.isNaN(v)) {
122
+ setVal(field.key, clamp(v, field.min ?? -Infinity, field.max ?? Infinity));
123
+ }
124
+ } else {
125
+ setVal(field.key, input.value);
126
+ }
127
+ };
128
+
129
+ input.addEventListener('input', commit);
130
+ input.addEventListener('change', commit);
131
+ }
132
+
133
+ wrap.appendChild(input);
134
+ row.appendChild(wrap);
135
+ }
136
+ }
137
+
138
+ const PLUGIN_SCHEMAS = {
139
+ gradient: {
140
+ // Special-cased in the demo (points editor UI)
141
+ fields: [],
142
+ },
143
+ 'grainy-fog': {
144
+ fields: [
145
+ { key: 'firstColor', label: 'Color 1', type: 'color', default: () => randomHex() },
146
+ { key: 'secondColor', label: 'Color 2', type: 'color', default: () => randomHex() },
147
+ { key: 'backgroundColor', label: 'Background', type: 'color', default: '#05070b' },
148
+ { key: 'speed', label: 'Speed', type: 'number', min: 0, max: 3, step: 0.05, default: 0.8 },
149
+ { key: 'scale', label: 'Scale', type: 'number', min: 0.5, max: 5, step: 0.05, default: 2.25 },
150
+ { key: 'octaves', label: 'Octaves', type: 'number', min: 1, max: 6, step: 1, default: 4 },
151
+ { key: 'contrast', label: 'Contrast', type: 'number', min: 0.5, max: 2.5, step: 0.05, default: 1.25 },
152
+ { key: 'grainAmount', label: 'Grain', type: 'number', min: 0, max: 0.25, step: 0.01, default: 0.15 },
153
+ ],
154
+ },
155
+ 'retro-grid': {
156
+ fields: [
157
+ { key: 'gridColor', label: 'Grid', type: 'color', default: '#ff4fd8' },
158
+ { key: 'backgroundColor', label: 'Background', type: 'color', default: '#050212' },
159
+ { key: 'speed', label: 'Speed', type: 'number', min: 0, max: 4, step: 0.05, default: 1.2 },
160
+ ],
161
+ },
162
+ 'liquid-orb': {
163
+ fields: [
164
+ { key: 'color', label: 'Blob', type: 'color', default: () => randomHex() },
165
+ { key: 'backgroundColor', label: 'Background', type: 'color', default: '#0b0f18' },
166
+ { key: 'count', label: 'Count', type: 'number', min: 1, max: 20, step: 1, default: 6 },
167
+ { key: 'speed', label: 'Speed', type: 'number', min: 0, max: 2, step: 0.05, default: 0.7 },
168
+ { key: 'gooeyness', label: 'Gooeyness', type: 'number', min: 0.05, max: 0.8, step: 0.01, default: 0.3 },
169
+ { key: 'edgeSoftness', label: 'Edge Softness', type: 'number', min: 0.001, max: 0.08, step: 0.001, default: 0.02 },
170
+ ],
171
+ },
172
+ caustics: {
173
+ fields: [
174
+ { key: 'color', label: 'Light', type: 'color', default: '#b9fff7' },
175
+ { key: 'backgroundColor', label: 'Background', type: 'color', default: '#031028' },
176
+ { key: 'intensity', label: 'Intensity', type: 'number', min: 0, max: 2, step: 0.05, default: 1.0 },
177
+ { key: 'speed', label: 'Speed', type: 'number', min: 0, max: 2, step: 0.05, default: 0.6 },
178
+ { key: 'scale', label: 'Scale', type: 'number', min: 0.5, max: 5, step: 0.05, default: 2.2 },
179
+ { key: 'distortion', label: 'Distortion', type: 'number', min: 0, max: 2, step: 0.05, default: 0.9 },
180
+ { key: 'sharpness', label: 'Sharpness', type: 'number', min: 0.5, max: 6, step: 0.05, default: 3.2 },
181
+ { key: 'antiAlias', label: 'Anti-alias', type: 'number', min: 0, max: 2, step: 0.05, default: 1.0 },
182
+ ],
183
+ },
184
+ 'aurora-waves': {
185
+ fields: [
186
+ { key: 'backgroundColor', label: 'Background', type: 'color', default: '#050611' },
187
+ { key: 'color1', label: 'Color 1', type: 'color', default: '#21f6c7' },
188
+ { key: 'color2', label: 'Color 2', type: 'color', default: '#5a66ff' },
189
+ { key: 'intensity', label: 'Intensity', type: 'number', min: 0, max: 2, step: 0.05, default: 0.95 },
190
+ { key: 'speed', label: 'Speed', type: 'number', min: 0, max: 2, step: 0.05, default: 0.55 },
191
+ { key: 'scale', label: 'Scale', type: 'number', min: 0.5, max: 4, step: 0.05, default: 1.5 },
192
+ { key: 'grainAmount', label: 'Grain', type: 'number', min: 0, max: 0.2, step: 0.01, default: 0.05 },
193
+ ],
194
+ },
195
+ 'soft-starfield': {
196
+ fields: [
197
+ { key: 'backgroundBottom', label: 'Bottom', type: 'color', default: '#040512' },
198
+ { key: 'backgroundTop', label: 'Top', type: 'color', default: '#0b1630' },
199
+ { key: 'starColor', label: 'Stars', type: 'color', default: '#ff4fd8' },
200
+ { key: 'nebulaColor', label: 'Nebula', type: 'color', default: '#6a5cff' },
201
+ { key: 'nebula', label: 'Nebula Amt', type: 'number', min: 0, max: 1.5, step: 0.05, default: 0.85 },
202
+ { key: 'density', label: 'Density', type: 'number', min: 0, max: 2.5, step: 0.05, default: 0.5 },
203
+ { key: 'size', label: 'Size', type: 'number', min: 0.5, max: 2, step: 0.05, default: 1.0 },
204
+ { key: 'twinkle', label: 'Twinkle', type: 'number', min: 0, max: 1, step: 0.05, default: 0.35 },
205
+ { key: 'speed', label: 'Speed', type: 'number', min: 0, max: 1, step: 0.02, default: 0.2 },
206
+ { key: 'grainAmount', label: 'Grain', type: 'number', min: 0, max: 0.2, step: 0.01, default: 0.0 },
207
+ ],
208
+ },
209
+ 'contour-lines': {
210
+ fields: [
211
+ { key: 'backgroundColor', label: 'Background', type: 'color', default: '#060a10' },
212
+ { key: 'lineColor', label: 'Lines', type: 'color', default: '#a9b9ff' },
213
+ { key: 'accentColor', label: 'Accent', type: 'color', default: '#35ffd1' },
214
+ { key: 'density', label: 'Density', type: 'number', min: 4, max: 30, step: 1, default: 12 },
215
+ { key: 'thickness', label: 'Thickness', type: 'number', min: 0.01, max: 0.2, step: 0.005, default: 0.075 },
216
+ { key: 'warp', label: 'Warp', type: 'number', min: 0, max: 2, step: 0.05, default: 0.9 },
217
+ { key: 'glow', label: 'Glow', type: 'number', min: 0, max: 1.5, step: 0.05, default: 0.35 },
218
+ { key: 'speed', label: 'Speed', type: 'number', min: 0, max: 2, step: 0.05, default: 0.35 },
219
+ { key: 'grainAmount', label: 'Grain', type: 'number', min: 0, max: 0.2, step: 0.01, default: 0.04 },
220
+ ],
221
+ },
222
+ 'dreamy-bokeh': {
223
+ fields: [
224
+ { key: 'backgroundBottom', label: 'Bottom', type: 'color', default: '#070818' },
225
+ { key: 'backgroundTop', label: 'Top', type: 'color', default: '#0e1734' },
226
+ { key: 'colorA', label: 'Color A', type: 'color', default: '#ffd1f3' },
227
+ { key: 'colorB', label: 'Color B', type: 'color', default: '#8be9ff' },
228
+ { key: 'colorC', label: 'Color C', type: 'color', default: '#b7ff9b' },
229
+ { key: 'density', label: 'Density', type: 'number', min: 0, max: 3, step: 0.05, default: 1.0 },
230
+ { key: 'size', label: 'Size', type: 'number', min: 0.5, max: 2, step: 0.05, default: 1.0 },
231
+ { key: 'blur', label: 'Blur', type: 'number', min: 0.5, max: 2.5, step: 0.05, default: 1.0 },
232
+ { key: 'speed', label: 'Speed', type: 'number', min: 0, max: 1, step: 0.02, default: 0.25 },
233
+ { key: 'vignette', label: 'Vignette', type: 'number', min: 0, max: 1, step: 0.05, default: 0.35 },
234
+ { key: 'grainAmount', label: 'Grain', type: 'number', min: 0, max: 0.2, step: 0.01, default: 0.03 },
235
+ ],
236
+ },
237
+ 'ink-wash': {
238
+ fields: [
239
+ { key: 'paperColor', label: 'Paper', type: 'color', default: '#f4f1ea' },
240
+ { key: 'inkColor', label: 'Ink', type: 'color', default: '#121521' },
241
+ { key: 'scale', label: 'Scale', type: 'number', min: 0.5, max: 4, step: 0.05, default: 1.4 },
242
+ { key: 'flow', label: 'Flow', type: 'number', min: 0, max: 1.5, step: 0.05, default: 0.85 },
243
+ { key: 'contrast', label: 'Contrast', type: 'number', min: 0.5, max: 2.5, step: 0.05, default: 1.15 },
244
+ { key: 'granulation', label: 'Granulation', type: 'number', min: 0, max: 1.2, step: 0.05, default: 0.35 },
245
+ { key: 'speed', label: 'Speed', type: 'number', min: 0, max: 1, step: 0.02, default: 0.18 },
246
+ { key: 'vignette', label: 'Vignette', type: 'number', min: 0, max: 1, step: 0.05, default: 0.35 },
247
+ { key: 'grainAmount', label: 'Grain', type: 'number', min: 0, max: 0.2, step: 0.01, default: 0.03 },
248
+ ],
249
+ },
250
+ 'stained-glass': {
251
+ fields: [
252
+ { key: 'backgroundColor', label: 'Background', type: 'color', default: '#05070b' },
253
+ { key: 'leadColor', label: 'Lead', type: 'color', default: '#0b0b10' },
254
+ { key: 'colorA', label: 'Color A', type: 'color', default: '#38bdf8' },
255
+ { key: 'colorB', label: 'Color B', type: 'color', default: '#a78bfa' },
256
+ { key: 'colorC', label: 'Color C', type: 'color', default: '#fb7185' },
257
+ { key: 'colorD', label: 'Color D', type: 'color', default: '#fbbf24' },
258
+ { key: 'scale', label: 'Scale', type: 'number', min: 1, max: 8, step: 0.1, default: 3.2 },
259
+ { key: 'variant', label: 'Variant (0..2)', type: 'number', min: 0, max: 2, step: 1, default: 0 },
260
+ { key: 'seed', label: 'Seed', type: 'number', min: 0, max: 9999, step: 1, default: 0 },
261
+ { key: 'jitter', label: 'Jitter', type: 'number', min: 0, max: 1, step: 0.02, default: 1.0 },
262
+ { key: 'rotation', label: 'Rotation', type: 'number', min: 0, max: 6.283, step: 0.05, default: 0.0 },
263
+ { key: 'edgeWidth', label: 'Edge Width', type: 'number', min: 0.01, max: 0.25, step: 0.01, default: 0.08 },
264
+ { key: 'edgeSharpness', label: 'Edge Sharpness', type: 'number', min: 0.25, max: 3.0, step: 0.05, default: 1.25 },
265
+ { key: 'edgeGlow', label: 'Edge Glow', type: 'number', min: 0, max: 1.5, step: 0.05, default: 0.45 },
266
+ { key: 'distortion', label: 'Distortion', type: 'number', min: 0, max: 1.5, step: 0.05, default: 0.55 },
267
+ { key: 'speed', label: 'Speed', type: 'number', min: 0, max: 1, step: 0.02, default: 0.12 },
268
+ { key: 'grainAmount', label: 'Grain', type: 'number', min: 0, max: 0.2, step: 0.01, default: 0.02 },
269
+ ],
270
+ },
271
+ };
272
+
273
+ let currentConfig = {};
274
+ let updateQueued = false;
275
+
276
+ function queuePluginUpdate() {
277
+ if (updateQueued) return;
278
+ updateQueued = true;
279
+ requestAnimationFrame(() => {
280
+ updateQueued = false;
281
+ setPlugin(currentKind());
282
+ });
283
+ }
284
+
285
+ function schemaFor(kind) {
286
+ return PLUGIN_SCHEMAS[kind] || PLUGIN_SCHEMAS.gradient;
287
+ }
288
+
289
+ function buildDefaultsForKind(kind) {
290
+ if (kind === 'gradient') {
291
+ return {
292
+ points: gradientConfig1.map((p) => ({
293
+ x: p.x,
294
+ y: p.y,
295
+ colors: [...p.colors],
296
+ speed: p.speed ?? 1.0,
297
+ motion: {
298
+ mode: 'none',
299
+ easing: 'smoothstep',
300
+ duration: 3.0,
301
+ path: [],
302
+ bounds: { minX: -1, maxX: 1, minY: -1, maxY: 1 },
303
+ randomRadius: 0,
304
+ },
305
+ })),
306
+ selectedPoint: 0,
307
+ clickToPlace: true,
308
+ };
309
+ }
310
+ return buildDefaults(schemaFor(kind));
311
+ }
312
+
313
+ function renderGradientEditor(cfg) {
314
+ if (!pluginParamsEl) return;
315
+ pluginParamsEl.innerHTML = '';
316
+
317
+ const header = document.createElement('div');
318
+ header.className = 'row';
319
+
320
+ const controls = document.createElement('div');
321
+ controls.className = 'field';
322
+ const title = document.createElement('span');
323
+ title.textContent = 'Points';
324
+ controls.appendChild(title);
325
+
326
+ const pointSelect = document.createElement('select');
327
+ const points = cfg.points || [];
328
+ points.forEach((p, idx) => {
329
+ const o = document.createElement('option');
330
+ o.value = String(idx);
331
+ o.textContent = `Point ${idx + 1}`;
332
+ pointSelect.appendChild(o);
333
+ });
334
+ pointSelect.value = String(cfg.selectedPoint ?? 0);
335
+ pointSelect.addEventListener('change', () => {
336
+ cfg.selectedPoint = Number(pointSelect.value) || 0;
337
+ renderGradientEditor(cfg);
338
+ });
339
+ controls.appendChild(pointSelect);
340
+
341
+ const actions = document.createElement('div');
342
+ actions.className = 'field';
343
+ const actionLabel = document.createElement('span');
344
+ actionLabel.textContent = 'Actions';
345
+ actions.appendChild(actionLabel);
346
+
347
+ const addBtn = document.createElement('button');
348
+ addBtn.textContent = 'Add Point';
349
+ addBtn.onclick = () => {
350
+ if (cfg.points.length >= 16) return;
351
+ cfg.points.push({
352
+ x: 0,
353
+ y: 0,
354
+ colors: [randomHex(), randomHex()],
355
+ speed: 1.0,
356
+ motion: {
357
+ mode: 'none',
358
+ easing: 'smoothstep',
359
+ duration: 3.0,
360
+ path: [],
361
+ bounds: { minX: -1, maxX: 1, minY: -1, maxY: 1 },
362
+ randomRadius: 0,
363
+ },
364
+ });
365
+ cfg.selectedPoint = cfg.points.length - 1;
366
+ queuePluginUpdate();
367
+ renderGradientEditor(cfg);
368
+ };
369
+
370
+ const removeBtn = document.createElement('button');
371
+ removeBtn.textContent = 'Remove Point';
372
+ removeBtn.onclick = () => {
373
+ if (cfg.points.length <= 1) return;
374
+ const idx = clamp(cfg.selectedPoint ?? 0, 0, cfg.points.length - 1);
375
+ cfg.points.splice(idx, 1);
376
+ cfg.selectedPoint = clamp(idx, 0, cfg.points.length - 1);
377
+ queuePluginUpdate();
378
+ renderGradientEditor(cfg);
379
+ };
380
+
381
+ const clickWrap = document.createElement('label');
382
+ clickWrap.className = 'field';
383
+ const clickText = document.createElement('span');
384
+ clickText.textContent = 'Click-to-place';
385
+ const clickToggle = document.createElement('input');
386
+ clickToggle.type = 'checkbox';
387
+ clickToggle.checked = Boolean(cfg.clickToPlace);
388
+ clickToggle.onchange = () => {
389
+ cfg.clickToPlace = clickToggle.checked;
390
+ };
391
+ clickWrap.appendChild(clickText);
392
+ clickWrap.appendChild(clickToggle);
393
+
394
+ actions.appendChild(addBtn);
395
+ actions.appendChild(removeBtn);
396
+ actions.appendChild(clickWrap);
397
+
398
+ header.appendChild(controls);
399
+ header.appendChild(actions);
400
+ pluginParamsEl.appendChild(header);
401
+
402
+ const idx = clamp(cfg.selectedPoint ?? 0, 0, cfg.points.length - 1);
403
+ const p = cfg.points[idx];
404
+ if (!p) return;
405
+
406
+ const fieldsRow = document.createElement('div');
407
+ fieldsRow.className = 'row';
408
+
409
+ const makeNumberField = ({ label, value, min, max, step, onInput }) => {
410
+ const wrap = document.createElement('label');
411
+ wrap.className = 'field';
412
+ const l = document.createElement('span');
413
+ l.textContent = label;
414
+ wrap.appendChild(l);
415
+
416
+ const input = document.createElement('input');
417
+ input.type = 'number';
418
+ input.min = String(min);
419
+ input.max = String(max);
420
+ input.step = String(step);
421
+ input.value = String(value);
422
+ input.addEventListener('input', () => {
423
+ const v = Number(input.value);
424
+ if (Number.isNaN(v)) return;
425
+ onInput(clamp(v, min, max));
426
+ queuePluginUpdate();
427
+ });
428
+ wrap.appendChild(input);
429
+ return wrap;
430
+ };
431
+
432
+ fieldsRow.appendChild(
433
+ makeNumberField({
434
+ label: 'X (-1..1)',
435
+ value: p.x,
436
+ min: -1,
437
+ max: 1,
438
+ step: 0.01,
439
+ onInput: (v) => (p.x = v),
440
+ })
441
+ );
442
+ fieldsRow.appendChild(
443
+ makeNumberField({
444
+ label: 'Y (-1..1)',
445
+ value: p.y,
446
+ min: -1,
447
+ max: 1,
448
+ step: 0.01,
449
+ onInput: (v) => (p.y = v),
450
+ })
451
+ );
452
+ fieldsRow.appendChild(
453
+ makeNumberField({
454
+ label: 'Speed',
455
+ value: p.speed ?? 1.0,
456
+ min: 0,
457
+ max: 5,
458
+ step: 0.05,
459
+ onInput: (v) => (p.speed = v),
460
+ })
461
+ );
462
+
463
+ // Motion controls
464
+ const motion = (p.motion = p.motion || {
465
+ mode: 'none',
466
+ easing: 'smoothstep',
467
+ duration: 3.0,
468
+ path: [],
469
+ bounds: { minX: -1, maxX: 1, minY: -1, maxY: 1 },
470
+ randomRadius: 0,
471
+ });
472
+
473
+ const motionRow = document.createElement('div');
474
+ motionRow.className = 'row';
475
+
476
+ const modeWrap = document.createElement('label');
477
+ modeWrap.className = 'field';
478
+ const modeLabel = document.createElement('span');
479
+ modeLabel.textContent = 'Motion Mode';
480
+ modeWrap.appendChild(modeLabel);
481
+ const modeSelect = document.createElement('select');
482
+ for (const v of ['none', 'path', 'random']) {
483
+ const o = document.createElement('option');
484
+ o.value = v;
485
+ o.textContent = v;
486
+ modeSelect.appendChild(o);
487
+ }
488
+ modeSelect.value = motion.mode || 'none';
489
+ modeSelect.onchange = () => {
490
+ motion.mode = modeSelect.value;
491
+ queuePluginUpdate();
492
+ renderGradientEditor(cfg);
493
+ };
494
+ modeWrap.appendChild(modeSelect);
495
+
496
+ const easeWrap = document.createElement('label');
497
+ easeWrap.className = 'field';
498
+ const easeLabel = document.createElement('span');
499
+ easeLabel.textContent = 'Easing';
500
+ easeWrap.appendChild(easeLabel);
501
+ const easeSelect = document.createElement('select');
502
+ for (const v of ['smoothstep', 'linear', 'easeInOutQuad', 'easeInOutCubic']) {
503
+ const o = document.createElement('option');
504
+ o.value = v;
505
+ o.textContent = v;
506
+ easeSelect.appendChild(o);
507
+ }
508
+ easeSelect.value = motion.easing || 'smoothstep';
509
+ easeSelect.onchange = () => {
510
+ motion.easing = easeSelect.value;
511
+ queuePluginUpdate();
512
+ };
513
+ easeWrap.appendChild(easeSelect);
514
+
515
+ const durWrap = document.createElement('label');
516
+ durWrap.className = 'field';
517
+ const durLabel = document.createElement('span');
518
+ durLabel.textContent = 'Duration (s)';
519
+ durWrap.appendChild(durLabel);
520
+ const durInput = document.createElement('input');
521
+ durInput.type = 'number';
522
+ durInput.min = '0.1';
523
+ durInput.max = '30';
524
+ durInput.step = '0.1';
525
+ durInput.value = String(motion.duration ?? 3.0);
526
+ durInput.addEventListener('input', () => {
527
+ const v = Number(durInput.value);
528
+ if (Number.isNaN(v)) return;
529
+ motion.duration = clamp(v, 0.1, 30);
530
+ queuePluginUpdate();
531
+ });
532
+ durWrap.appendChild(durInput);
533
+
534
+ motionRow.appendChild(modeWrap);
535
+ motionRow.appendChild(easeWrap);
536
+ motionRow.appendChild(durWrap);
537
+
538
+ // placeholder cell (keeps the grid feel consistent)
539
+ const motionHint = document.createElement('div');
540
+ motionHint.className = 'field';
541
+ const motionHintLabel = document.createElement('span');
542
+ motionHintLabel.textContent = 'Motion';
543
+ const motionHintText = document.createElement('div');
544
+ motionHintText.style.fontSize = '12px';
545
+ motionHintText.style.opacity = '0.8';
546
+ motionHintText.textContent =
547
+ motion.mode === 'path'
548
+ ? 'Use waypoints below to move between positions.'
549
+ : motion.mode === 'random'
550
+ ? 'Moves to random targets within bounds.'
551
+ : 'Static (no motion).';
552
+ motionHint.appendChild(motionHintLabel);
553
+ motionHint.appendChild(motionHintText);
554
+ motionRow.appendChild(motionHint);
555
+
556
+ pluginParamsEl.appendChild(motionRow);
557
+
558
+ if (motion.mode === 'path') {
559
+ motion.path = motion.path || [];
560
+
561
+ const pathWrap = document.createElement('div');
562
+ pathWrap.className = 'field';
563
+ const pathLabel = document.createElement('span');
564
+ pathLabel.textContent = 'Waypoints';
565
+ pathWrap.appendChild(pathLabel);
566
+
567
+ const list = document.createElement('div');
568
+ list.style.display = 'flex';
569
+ list.style.flexDirection = 'column';
570
+ list.style.gap = '8px';
571
+
572
+ motion.path.forEach((wp, wIdx) => {
573
+ const line = document.createElement('div');
574
+ line.style.display = 'grid';
575
+ line.style.gridTemplateColumns = '1fr 1fr auto';
576
+ line.style.gap = '8px';
577
+ line.style.alignItems = 'center';
578
+
579
+ const xIn = document.createElement('input');
580
+ xIn.type = 'number';
581
+ xIn.min = '-1';
582
+ xIn.max = '1';
583
+ xIn.step = '0.01';
584
+ xIn.value = String(wp.x);
585
+ xIn.addEventListener('input', () => {
586
+ const v = Number(xIn.value);
587
+ if (Number.isNaN(v)) return;
588
+ wp.x = clamp(v, -1, 1);
589
+ queuePluginUpdate();
590
+ });
591
+
592
+ const yIn = document.createElement('input');
593
+ yIn.type = 'number';
594
+ yIn.min = '-1';
595
+ yIn.max = '1';
596
+ yIn.step = '0.01';
597
+ yIn.value = String(wp.y);
598
+ yIn.addEventListener('input', () => {
599
+ const v = Number(yIn.value);
600
+ if (Number.isNaN(v)) return;
601
+ wp.y = clamp(v, -1, 1);
602
+ queuePluginUpdate();
603
+ });
604
+
605
+ const rm = document.createElement('button');
606
+ rm.textContent = 'Remove';
607
+ rm.onclick = () => {
608
+ motion.path.splice(wIdx, 1);
609
+ queuePluginUpdate();
610
+ renderGradientEditor(cfg);
611
+ };
612
+
613
+ line.appendChild(xIn);
614
+ line.appendChild(yIn);
615
+ line.appendChild(rm);
616
+ list.appendChild(line);
617
+ });
618
+
619
+ const btnRow = document.createElement('div');
620
+ btnRow.style.display = 'flex';
621
+ btnRow.style.gap = '8px';
622
+
623
+ const addWp = document.createElement('button');
624
+ addWp.textContent = 'Add Waypoint';
625
+ addWp.onclick = () => {
626
+ motion.path.push({ x: 0, y: 0 });
627
+ queuePluginUpdate();
628
+ renderGradientEditor(cfg);
629
+ };
630
+
631
+ const addCurrent = document.createElement('button');
632
+ addCurrent.textContent = 'Add Current';
633
+ addCurrent.onclick = () => {
634
+ motion.path.push({ x: p.x, y: p.y });
635
+ queuePluginUpdate();
636
+ renderGradientEditor(cfg);
637
+ };
638
+
639
+ btnRow.appendChild(addWp);
640
+ btnRow.appendChild(addCurrent);
641
+
642
+ pathWrap.appendChild(list);
643
+ pathWrap.appendChild(btnRow);
644
+ pluginParamsEl.appendChild(pathWrap);
645
+ }
646
+
647
+ if (motion.mode === 'random') {
648
+ motion.bounds = motion.bounds || { minX: -1, maxX: 1, minY: -1, maxY: 1 };
649
+
650
+ const randWrap = document.createElement('div');
651
+ randWrap.className = 'field';
652
+ const randLabel = document.createElement('span');
653
+ randLabel.textContent = 'Random Settings';
654
+ randWrap.appendChild(randLabel);
655
+
656
+ const randRow = document.createElement('div');
657
+ randRow.className = 'row';
658
+
659
+ const mk = (label, value, min, max, step, onSet) =>
660
+ makeNumberField({
661
+ label,
662
+ value,
663
+ min,
664
+ max,
665
+ step,
666
+ onInput: (v) => {
667
+ onSet(v);
668
+ queuePluginUpdate();
669
+ },
670
+ });
671
+
672
+ randRow.appendChild(
673
+ mk('minX', motion.bounds.minX ?? -1, -1, 1, 0.01, (v) => (motion.bounds.minX = v))
674
+ );
675
+ randRow.appendChild(
676
+ mk('maxX', motion.bounds.maxX ?? 1, -1, 1, 0.01, (v) => (motion.bounds.maxX = v))
677
+ );
678
+ randRow.appendChild(
679
+ mk('minY', motion.bounds.minY ?? -1, -1, 1, 0.01, (v) => (motion.bounds.minY = v))
680
+ );
681
+ randRow.appendChild(
682
+ mk('maxY', motion.bounds.maxY ?? 1, -1, 1, 0.01, (v) => (motion.bounds.maxY = v))
683
+ );
684
+
685
+ const radiusRow = document.createElement('div');
686
+ radiusRow.className = 'row';
687
+ radiusRow.appendChild(
688
+ mk(
689
+ 'Radius (0=full)',
690
+ motion.randomRadius ?? 0,
691
+ 0,
692
+ 2,
693
+ 0.01,
694
+ (v) => (motion.randomRadius = v)
695
+ )
696
+ );
697
+
698
+ randWrap.appendChild(randRow);
699
+ randWrap.appendChild(radiusRow);
700
+ pluginParamsEl.appendChild(randWrap);
701
+ }
702
+
703
+ // Spacer to keep grid aligned
704
+ const hint = document.createElement('div');
705
+ hint.className = 'field';
706
+ const hintLabel = document.createElement('span');
707
+ hintLabel.textContent = 'Hint';
708
+ const hintText = document.createElement('div');
709
+ hintText.style.fontSize = '12px';
710
+ hintText.style.opacity = '0.8';
711
+ hintText.textContent = cfg.clickToPlace
712
+ ? 'Click on the background to move the selected point.'
713
+ : 'Enable click-to-place to position points by clicking the background.';
714
+ hint.appendChild(hintLabel);
715
+ hint.appendChild(hintText);
716
+ fieldsRow.appendChild(hint);
717
+
718
+ pluginParamsEl.appendChild(fieldsRow);
719
+
720
+ // Colors list
721
+ const colorsWrap = document.createElement('div');
722
+ colorsWrap.className = 'field';
723
+ const colorsLabel = document.createElement('span');
724
+ colorsLabel.textContent = 'Colors';
725
+ colorsWrap.appendChild(colorsLabel);
726
+
727
+ const list = document.createElement('div');
728
+ list.style.display = 'flex';
729
+ list.style.flexDirection = 'column';
730
+ list.style.gap = '8px';
731
+
732
+ p.colors = p.colors || [randomHex()];
733
+ p.colors.forEach((c, cIdx) => {
734
+ const line = document.createElement('div');
735
+ line.style.display = 'flex';
736
+ line.style.gap = '8px';
737
+ line.style.alignItems = 'center';
738
+
739
+ const colorInput = document.createElement('input');
740
+ colorInput.type = 'color';
741
+ colorInput.value = c;
742
+ colorInput.addEventListener('input', () => {
743
+ p.colors[cIdx] = colorInput.value;
744
+ queuePluginUpdate();
745
+ });
746
+
747
+ const removeColor = document.createElement('button');
748
+ removeColor.textContent = 'Remove';
749
+ removeColor.onclick = () => {
750
+ if (p.colors.length <= 1) return;
751
+ p.colors.splice(cIdx, 1);
752
+ queuePluginUpdate();
753
+ renderGradientEditor(cfg);
754
+ };
755
+
756
+ line.appendChild(colorInput);
757
+ line.appendChild(removeColor);
758
+ list.appendChild(line);
759
+ });
760
+
761
+ const addColor = document.createElement('button');
762
+ addColor.textContent = 'Add Color';
763
+ addColor.onclick = () => {
764
+ p.colors.push(randomHex());
765
+ queuePluginUpdate();
766
+ renderGradientEditor(cfg);
767
+ };
768
+
769
+ colorsWrap.appendChild(list);
770
+ colorsWrap.appendChild(addColor);
771
+ pluginParamsEl.appendChild(colorsWrap);
772
+ }
773
+
774
+ function renderParamsForKind(kind, cfg) {
775
+ if (kind === 'gradient') {
776
+ renderGradientEditor(cfg);
777
+ return;
778
+ }
779
+ renderParamsForm(schemaFor(kind), cfg, queuePluginUpdate);
780
+ }
781
+
782
+ function makePlugin(kind) {
783
+ switch (kind) {
784
+ case 'gradient':
785
+ return new GradientPlugin(currentConfig.points);
786
+
787
+ case 'grainy-fog':
788
+ return new GrainyFogPlugin({
789
+ firstColor: currentConfig.firstColor,
790
+ secondColor: currentConfig.secondColor,
791
+ backgroundColor: currentConfig.backgroundColor,
792
+ grainAmount: currentConfig.grainAmount,
793
+ speed: currentConfig.speed,
794
+ scale: currentConfig.scale,
795
+ octaves: currentConfig.octaves,
796
+ contrast: currentConfig.contrast,
797
+ });
798
+
799
+ case 'retro-grid':
800
+ return new RetroGridPlugin({
801
+ gridColor: currentConfig.gridColor,
802
+ backgroundColor: currentConfig.backgroundColor,
803
+ speed: currentConfig.speed,
804
+ });
805
+
806
+ case 'liquid-orb':
807
+ return new LiquidOrbPlugin({
808
+ color: currentConfig.color,
809
+ backgroundColor: currentConfig.backgroundColor,
810
+ count: currentConfig.count,
811
+ speed: currentConfig.speed,
812
+ gooeyness: currentConfig.gooeyness,
813
+ edgeSoftness: currentConfig.edgeSoftness,
814
+ });
815
+
816
+ case 'caustics':
817
+ return new CausticsPlugin({
818
+ color: currentConfig.color,
819
+ backgroundColor: currentConfig.backgroundColor,
820
+ intensity: currentConfig.intensity,
821
+ speed: currentConfig.speed,
822
+ scale: currentConfig.scale,
823
+ distortion: currentConfig.distortion,
824
+ sharpness: currentConfig.sharpness,
825
+ antiAlias: currentConfig.antiAlias,
826
+ });
827
+
828
+ case 'aurora-waves':
829
+ return new AuroraWavesPlugin({
830
+ backgroundColor: currentConfig.backgroundColor,
831
+ color1: currentConfig.color1,
832
+ color2: currentConfig.color2,
833
+ intensity: currentConfig.intensity,
834
+ speed: currentConfig.speed,
835
+ scale: currentConfig.scale,
836
+ grainAmount: currentConfig.grainAmount,
837
+ });
838
+
839
+ case 'soft-starfield':
840
+ return new SoftStarfieldPlugin({
841
+ backgroundBottom: currentConfig.backgroundBottom,
842
+ backgroundTop: currentConfig.backgroundTop,
843
+ starColor: currentConfig.starColor,
844
+ nebulaColor: currentConfig.nebulaColor,
845
+ nebula: currentConfig.nebula,
846
+ density: currentConfig.density,
847
+ size: currentConfig.size,
848
+ twinkle: currentConfig.twinkle,
849
+ speed: currentConfig.speed,
850
+ grainAmount: currentConfig.grainAmount,
851
+ });
852
+
853
+ case 'contour-lines':
854
+ return new ContourLinesPlugin({
855
+ backgroundColor: currentConfig.backgroundColor,
856
+ lineColor: currentConfig.lineColor,
857
+ accentColor: currentConfig.accentColor,
858
+ density: currentConfig.density,
859
+ thickness: currentConfig.thickness,
860
+ warp: currentConfig.warp,
861
+ glow: currentConfig.glow,
862
+ speed: currentConfig.speed,
863
+ grainAmount: currentConfig.grainAmount,
864
+ });
865
+
866
+ case 'dreamy-bokeh':
867
+ return new DreamyBokehPlugin({
868
+ backgroundBottom: currentConfig.backgroundBottom,
869
+ backgroundTop: currentConfig.backgroundTop,
870
+ colorA: currentConfig.colorA,
871
+ colorB: currentConfig.colorB,
872
+ colorC: currentConfig.colorC,
873
+ density: currentConfig.density,
874
+ size: currentConfig.size,
875
+ blur: currentConfig.blur,
876
+ speed: currentConfig.speed,
877
+ vignette: currentConfig.vignette,
878
+ grainAmount: currentConfig.grainAmount,
879
+ });
880
+
881
+ case 'ink-wash':
882
+ return new InkWashPlugin({
883
+ paperColor: currentConfig.paperColor,
884
+ inkColor: currentConfig.inkColor,
885
+ scale: currentConfig.scale,
886
+ speed: currentConfig.speed,
887
+ flow: currentConfig.flow,
888
+ contrast: currentConfig.contrast,
889
+ granulation: currentConfig.granulation,
890
+ vignette: currentConfig.vignette,
891
+ grainAmount: currentConfig.grainAmount,
892
+ });
893
+
894
+ case 'stained-glass':
895
+ return new StainedGlassPlugin({
896
+ backgroundColor: currentConfig.backgroundColor,
897
+ leadColor: currentConfig.leadColor,
898
+ colorA: currentConfig.colorA,
899
+ colorB: currentConfig.colorB,
900
+ colorC: currentConfig.colorC,
901
+ colorD: currentConfig.colorD,
902
+ scale: currentConfig.scale,
903
+ variant: currentConfig.variant,
904
+ seed: currentConfig.seed,
905
+ jitter: currentConfig.jitter,
906
+ rotation: currentConfig.rotation,
907
+ edgeWidth: currentConfig.edgeWidth,
908
+ edgeSharpness: currentConfig.edgeSharpness,
909
+ edgeGlow: currentConfig.edgeGlow,
910
+ distortion: currentConfig.distortion,
911
+ speed: currentConfig.speed,
912
+ grainAmount: currentConfig.grainAmount,
913
+ });
914
+
915
+ default:
916
+ return new GradientPlugin(gradientConfig1);
917
+ }
918
+ }
919
+
920
+ function currentKind() {
921
+ return pluginSelect?.value || 'gradient';
922
+ }
923
+
924
+ function setPlugin(kind) {
925
+ el.plugin = makePlugin(kind);
926
+ }
927
+
928
+ // Wait for the custom element to be defined before setting config
929
+ customElements.whenDefined('shader-background').then(() => {
930
+ console.log('[index.html] shader-background element defined, setting initial plugin');
931
+ const kind = currentKind();
932
+ currentConfig = buildDefaultsForKind(kind);
933
+ renderParamsForKind(kind, currentConfig);
934
+ setPlugin(kind);
935
+
936
+ // Initialize filter UI
937
+ const initialFilter = filterPreset?.value || 'none';
938
+ setCanvasCss({ filter: initialFilter, transition: '180ms ease' });
939
+ if (filterInput) filterInput.value = initialFilter;
940
+
941
+ // Overlay collapse state
942
+ if (overlayEl && overlayToggleBtn) {
943
+ const saved = localStorage.getItem('demo.overlayCollapsed');
944
+ const collapsed = saved === '1';
945
+ overlayEl.classList.toggle('collapsed', collapsed);
946
+ overlayToggleBtn.textContent = collapsed ? 'Show' : 'Hide';
947
+
948
+ overlayToggleBtn.addEventListener('click', () => {
949
+ const next = !overlayEl.classList.contains('collapsed');
950
+ overlayEl.classList.toggle('collapsed', next);
951
+ overlayToggleBtn.textContent = next ? 'Show' : 'Hide';
952
+ localStorage.setItem('demo.overlayCollapsed', next ? '1' : '0');
953
+ });
954
+ }
955
+ });
956
+
957
+ pluginSelect?.addEventListener('change', () => {
958
+ const kind = currentKind();
959
+ currentConfig = buildDefaultsForKind(kind);
960
+ renderParamsForKind(kind, currentConfig);
961
+ setPlugin(kind);
962
+ });
963
+
964
+ filterPreset?.addEventListener('change', () => {
965
+ const f = filterPreset.value || 'none';
966
+ setCanvasCss({ filter: f, transition: '180ms ease' });
967
+ if (filterInput) filterInput.value = f;
968
+ });
969
+
970
+ window.applyFilter = () => {
971
+ const val = (filterInput?.value ?? '').trim();
972
+ setCanvasCss({ filter: val === '' ? 'none' : val, transition: '180ms ease' });
973
+ if (filterPreset) filterPreset.value = 'none';
974
+ };
975
+
976
+ // Click-to-place for Gradient points (when enabled in the params UI).
977
+ el.addEventListener('pointerdown', (e) => {
978
+ if (currentKind() !== 'gradient') return;
979
+ if (!currentConfig?.clickToPlace) return;
980
+
981
+ // Ignore clicks on the overlay UI (so buttons/inputs don't move points).
982
+ if (e.target?.closest?.('.overlay')) return;
983
+
984
+ const points = currentConfig.points;
985
+ if (!Array.isArray(points) || points.length === 0) return;
986
+
987
+ const idx = clamp(currentConfig.selectedPoint ?? 0, 0, points.length - 1);
988
+ const rect = el.getBoundingClientRect();
989
+ if (!rect.width || !rect.height) return;
990
+
991
+ const px = (e.clientX - rect.left) / rect.width;
992
+ const py = (e.clientY - rect.top) / rect.height;
993
+
994
+ const x = clamp(px * 2 - 1, -1, 1);
995
+ const y = clamp((1 - py) * 2 - 1, -1, 1);
996
+
997
+ points[idx].x = x;
998
+ points[idx].y = y;
999
+ renderGradientEditor(currentConfig);
1000
+ queuePluginUpdate();
1001
+ });
1002
+
1003
+ window.changeColors = () => {
1004
+ const kind = currentKind();
1005
+ if (kind === 'gradient') {
1006
+ // Randomize all gradient point colors.
1007
+ for (const p of currentConfig.points || []) {
1008
+ p.colors = (p.colors || []).map(() => randomHex());
1009
+ if (!p.colors.length) p.colors = [randomHex()];
1010
+ }
1011
+ renderGradientEditor(currentConfig);
1012
+ queuePluginUpdate();
1013
+ return;
1014
+ }
1015
+
1016
+ // For other plugins, randomize obvious color fields (if present) and re-render.
1017
+ for (const k of Object.keys(currentConfig)) {
1018
+ if (k.toLowerCase().includes('color')) {
1019
+ currentConfig[k] = randomHex();
1020
+ }
1021
+ }
1022
+ renderParamsForKind(kind, currentConfig);
1023
+ setPlugin(kind);
1024
+ };
1025
+
1026
+ window.toggleRenderScale = () => {
1027
+ const currentScale = el.renderScale;
1028
+ el.renderScale = currentScale === 1.0 ? 0.5 : 1.0;
1029
+ console.log(`Render scale set to: ${el.renderScale}`);
1030
+ };
1031
+
1032
+ window.toggleSingleRender = () => {
1033
+ el.singleRender = !el.singleRender;
1034
+ const manualBtn = document.getElementById('manualRenderBtn');
1035
+ manualBtn.style.display = el.singleRender ? 'block' : 'none';
1036
+ console.log(`Single render mode: ${el.singleRender}`);
1037
+ };
1038
+
1039
+ window.manualRender = () => {
1040
+ if (el.singleRender) {
1041
+ el.render();
1042
+ console.log('Manual render triggered');
1043
+ }
1044
+ };