Skip to content

Commit 586fe8d

Browse files
fuddlesworthruvnet
andcommitted
feat: add Mosaic Pulse audio-reactive stained glass shader
Ported from ShaderToy — colorful tiled mosaic grid with random shapes (circles, diamonds, squares), HSL color variation, sparkles, grid lines, and dithered posterization. Bass drives shape pulse, mids shift hue, treble triggers sparkles. 12 configurable parameters across 6 groups. Co-Authored-By: claude-flow <ruv@ruv.net>
1 parent 9c76b22 commit 586fe8d

File tree

3 files changed

+431
-0
lines changed

3 files changed

+431
-0
lines changed
Lines changed: 390 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,390 @@
1+
// SPDX-FileCopyrightText: 2026 fuddlesworth
2+
// SPDX-License-Identifier: GPL-3.0-or-later
3+
4+
#version 450
5+
6+
layout(location = 0) in vec2 vTexCoord;
7+
layout(location = 1) in vec2 vFragCoord;
8+
9+
layout(location = 0) out vec4 fragColor;
10+
11+
#include <common.glsl>
12+
#include <audio.glsl>
13+
14+
/*
15+
* MOSAIC PULSE — Audio-Reactive Stained Glass Mosaic
16+
*
17+
* Colorful tiled mosaic grid with random shapes (circles, diamonds, squares),
18+
* HSL color variation, sparkles, grid lines, and dithered posterization.
19+
* Bass drives shape pulse, mids shift hue, treble triggers sparkles.
20+
*
21+
* Parameters (customParams):
22+
* [0].x = gridDensity — number of vertical tiles
23+
* [0].y = edgeSoftness — shape edge anti-alias width
24+
* [0].z = gridLineWeight — grid line darkness (0=none, 1=full)
25+
* [0].w = posterize — color quantization levels
26+
* [1].x = shapeChance — probability a cell gets a shape (0-1)
27+
* [1].y = shapeSize — base shape radius
28+
* [1].z = sparkleChance — probability a cell gets a sparkle (0-1)
29+
* [1].w = speed — animation speed multiplier
30+
* [2].x = reactivity — audio sensitivity multiplier
31+
* [2].y = fillOpacity — zone fill alpha
32+
*
33+
* Colors:
34+
* customColors[0] = hue center (default: steel blue #4488cc)
35+
* customColors[1] = shape tint (default: warm white #fffff2)
36+
*/
37+
38+
// ─── Mosaic noise (prefixed to avoid common.glsl collision) ──────
39+
40+
float mosaicHash(vec2 p) {
41+
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
42+
}
43+
44+
float mosaicNoise(vec2 p) {
45+
vec2 i = floor(p), f = fract(p);
46+
vec2 u = f * f * (3.0 - 2.0 * f);
47+
return mix(
48+
mix(mosaicHash(i), mosaicHash(i + vec2(1.0, 0.0)), u.x),
49+
mix(mosaicHash(i + vec2(0.0, 1.0)), mosaicHash(i + vec2(1.0, 1.0)), u.x),
50+
u.y
51+
);
52+
}
53+
54+
float mosaicFbm(vec2 p) {
55+
float v = 0.0, a = 0.5;
56+
for (int i = 0; i < 4; i++) {
57+
v += a * mosaicNoise(p);
58+
p *= 2.0;
59+
a *= 0.5;
60+
}
61+
return v;
62+
}
63+
64+
// ─── HSL → RGB ───────────────────────────────────────────────────
65+
66+
vec3 hsl2rgb(vec3 c) {
67+
vec3 rgb = clamp(
68+
abs(mod(c.x * 6.0 + vec3(0.0, 4.0, 2.0), 6.0) - 3.0) - 1.0,
69+
0.0, 1.0
70+
);
71+
return c.z + c.y * (rgb - 0.5) * (1.0 - abs(2.0 * c.z - 1.0));
72+
}
73+
74+
// ─── Shape SDF: 0=circle, 1=diamond, 2=square, 3=cross ──────────
75+
76+
float shapeSDF(vec2 p, int t) {
77+
if (t == 0) return length(p);
78+
if (t == 1) return abs(p.x) + abs(p.y);
79+
if (t == 2) return max(abs(p.x), abs(p.y));
80+
return min(abs(p.x), abs(p.y));
81+
}
82+
83+
// ─── Per-zone rendering ─────────────────────────────────────────
84+
85+
vec4 renderZone(vec2 fragCoord, vec4 rect, vec4 fillColor, vec4 borderColor,
86+
vec4 params, bool isHighlighted,
87+
float bass, float mids, float treble, float overall, bool hasAudio)
88+
{
89+
float borderRadius = max(params.x, 6.0);
90+
float borderWidth = max(params.y, 2.5);
91+
92+
// Parameters with defaults
93+
float gridDensity = customParams[0].x >= 0.0 ? customParams[0].x : 64.0;
94+
float edgeSoftness = customParams[0].y >= 0.0 ? customParams[0].y : 0.18;
95+
float gridLineW = customParams[0].z >= 0.0 ? customParams[0].z : 0.75;
96+
float posterLevels = customParams[0].w >= 0.0 ? customParams[0].w : 8.0;
97+
float shapeChance = customParams[1].x >= 0.0 ? customParams[1].x : 0.62;
98+
float shapeSize = customParams[1].y >= 0.0 ? customParams[1].y : 0.26;
99+
float sparkleChance = customParams[1].z >= 0.0 ? customParams[1].z : 0.04;
100+
float speed = customParams[1].w >= 0.0 ? customParams[1].w : 1.25;
101+
float reactivity = customParams[2].x >= 0.0 ? customParams[2].x : 1.5;
102+
float fillOpacity = customParams[2].y >= 0.0 ? customParams[2].y : 0.9;
103+
104+
// Colors — fallbacks match the original ShaderToy palette
105+
// hueCenter: #4099BF ≈ HSL hue 0.55 (cyan-blue, matching the original 0.55 center)
106+
vec3 hueCenter = colorWithFallback(customColors[0].rgb, vec3(0.251, 0.6, 0.749));
107+
vec3 shapeTint = colorWithFallback(customColors[1].rgb, vec3(1.0, 1.0, 0.949));
108+
109+
// ── Highlighted vs dormant ──────────────────────────────
110+
float vitality = isHighlighted ? 1.0 : 0.3;
111+
112+
if (isHighlighted) {
113+
reactivity *= 1.4;
114+
speed *= 1.2;
115+
} else {
116+
hueCenter = mix(hueCenter, vec3(luminance(hueCenter)), 0.5);
117+
shapeTint = mix(shapeTint, vec3(luminance(shapeTint)), 0.4);
118+
reactivity *= 0.5;
119+
speed *= 0.6;
120+
}
121+
122+
// Zone geometry
123+
vec2 rectPos = zoneRectPos(rect);
124+
vec2 rectSize = zoneRectSize(rect);
125+
vec2 center = rectPos + rectSize * 0.5;
126+
vec2 p = fragCoord - center;
127+
vec2 localUV = zoneLocalUV(fragCoord, rectPos, rectSize);
128+
float d = sdRoundedBox(p, rectSize * 0.5, borderRadius);
129+
130+
float energy = hasAudio ? overall * reactivity : 0.0;
131+
float idlePulse = hasAudio ? 0.0 : (0.5 + 0.5 * sin(iTime * 0.8 * PI)) * 0.5;
132+
133+
vec4 result = vec4(0.0);
134+
135+
// ── Zone interior ───────────────────────────────────────
136+
137+
if (d < 0.0) {
138+
float t = iTime * speed;
139+
140+
// Derive base hue from hueCenter color (RGB → HSL hue via atan2)
141+
float hueCenterVal = fract(atan(
142+
1.732 * (hueCenter.g - hueCenter.b),
143+
2.0 * hueCenter.r - hueCenter.g - hueCenter.b
144+
) / TAU);
145+
146+
// Audio modulation
147+
float audioHueShift = hasAudio ? mids * reactivity * 0.15 : 0.0;
148+
float audioShapePulse = hasAudio ? bass * reactivity * 0.08 : 0.0;
149+
float audioSparkle = hasAudio ? treble * reactivity : 0.0;
150+
float audioSatBoost = hasAudio ? overall * reactivity * 0.1 : 0.0;
151+
float audioLitBoost = hasAudio ? overall * reactivity * 0.08 : 0.0;
152+
153+
// Virtual grid coordinates — screen-space for continuity across zones
154+
float vh = gridDensity;
155+
float vw = vh * iResolution.x / max(iResolution.y, 1.0);
156+
vec2 vUV = fragCoord / max(iResolution.xy, vec2(1.0)) * vec2(vw, vh);
157+
158+
vec2 cellId = floor(vUV);
159+
vec2 cellP = fract(vUV) - 0.5;
160+
vec2 cellNorm = cellId / vec2(vw, vh);
161+
162+
// Per-cell audio mapping: map cell x-position to spectrum
163+
float cellSpecU = cellNorm.x;
164+
float cellAudio = hasAudio ? audioBarSmooth(cellSpecU) * reactivity : 0.0;
165+
166+
// ── Tile background color (HSL) ──────────────────────
167+
float n = mosaicFbm(cellId * 0.35 + t * 0.15);
168+
169+
float hue = hueCenterVal
170+
+ 0.25 * n
171+
+ 0.05 * sin(cellNorm.x * 4.0 + t)
172+
+ 0.05 * cos(cellNorm.y * 3.0 - t)
173+
+ audioHueShift;
174+
hue += (mosaicHash(cellId + 123.4) - 0.5) * 0.3;
175+
hue = fract(hue);
176+
177+
float sat = 0.4 + 0.15 * mosaicFbm(cellId * 0.6 - t * 0.2) + audioSatBoost;
178+
sat += (mosaicHash(cellId + 234.5) - 0.5) * 0.25;
179+
sat = clamp(sat, 0.2, 0.8);
180+
181+
float lit = 0.55 + 0.25 * mosaicFbm(cellId * 0.2 + t) + audioLitBoost;
182+
lit += (mosaicHash(cellId + 345.6) - 0.5) * 0.25;
183+
// Per-cell audio brightness boost
184+
lit += cellAudio * 0.15;
185+
lit = clamp(lit, 0.35, 0.9);
186+
187+
vec3 bg = hsl2rgb(vec3(hue, sat, lit));
188+
vec3 col = bg;
189+
190+
// ── Shape overlay ────────────────────────────────────
191+
float rnd = mosaicHash(cellId);
192+
float shapeThreshold = 1.0 - shapeChance;
193+
194+
if (rnd > shapeThreshold) {
195+
int stype = int(floor(rnd * 4.0));
196+
197+
// Base pulse + audio-driven bass pulse
198+
float pulse = 0.04 * sin(t * 3.0 + rnd * 10.0)
199+
+ 0.03 * mosaicNoise(cellId + t)
200+
+ audioShapePulse
201+
+ cellAudio * 0.04;
202+
203+
float r = shapeSize + pulse;
204+
205+
float sd = shapeSDF(cellP, stype);
206+
float mask = 1.0 - smoothstep(r, r + edgeSoftness, sd);
207+
208+
// Shape palette — original pastels, subtly tinted by shapeTint
209+
vec3 pal[4];
210+
pal[0] = vec3(1.0, 0.95, 0.85) * shapeTint; // warm white
211+
pal[1] = vec3(0.85, 0.92, 1.0) * shapeTint; // cool white
212+
pal[2] = vec3(1.0, 0.82, 0.9) * shapeTint; // pink white
213+
pal[3] = vec3(0.9, 1.0, 0.85) * shapeTint; // green white
214+
215+
int cid = int(floor(mosaicHash(cellId + 9.7) * 4.0));
216+
vec3 shapeCol = pal[cid];
217+
218+
// Audio-reactive shape color: bass tints warm, treble tints bright
219+
if (hasAudio) {
220+
shapeCol = mix(shapeCol, vec3(1.0, 0.7, 0.3), bass * reactivity * 0.15);
221+
shapeCol += vec3(0.1) * cellAudio;
222+
}
223+
224+
col = mix(col, shapeCol, mask);
225+
}
226+
227+
// ── Sparkles ─────────────────────────────────────────
228+
float spark = mosaicHash(cellId + 17.3);
229+
float sparkThreshold = 1.0 - sparkleChance;
230+
// Treble boosts sparkle chance
231+
float effectiveSparkThresh = sparkThreshold - audioSparkle * 0.08;
232+
233+
if (spark > effectiveSparkThresh) {
234+
float sd = length(cellP - vec2(0.3, -0.3));
235+
float sparkBright = 1.0 - smoothstep(0.02, 0.2, sd);
236+
// Audio: treble drives sparkle intensity
237+
float sparkIntensity = 0.7 + audioSparkle * 0.5;
238+
col += vec3(1.0, 0.95, 0.8) * sparkBright * sparkIntensity;
239+
}
240+
241+
// ── Grid lines ───────────────────────────────────────
242+
if (gridLineW > 0.01) {
243+
vec2 g = fract(vUV);
244+
float line = min(min(g.x, 1.0 - g.x), min(g.y, 1.0 - g.y));
245+
float lineDarken = mix(1.0 - gridLineW * 0.25, 1.0, smoothstep(0.0, 0.06, line));
246+
col *= lineDarken;
247+
}
248+
249+
// ── Dithered posterization ───────────────────────────
250+
float ditherAmt = 0.04 + audioSparkle * 0.02;
251+
float dither = mosaicHash(fragCoord + iTime) * ditherAmt;
252+
col += dither;
253+
col = floor(col * posterLevels) / posterLevels;
254+
255+
// Dormant desaturation
256+
if (!isHighlighted) {
257+
float lum = luminance(col);
258+
col = mix(col, vec3(lum), 0.4);
259+
col *= 0.7;
260+
}
261+
262+
result.rgb = col;
263+
result.a = mix(fillOpacity * 0.7, fillOpacity, vitality);
264+
265+
// Inner edge glow
266+
float innerGlow = exp(d / mix(25.0, 12.0, vitality)) * mix(0.04, 0.15, vitality) * (1.0 + energy);
267+
result.rgb += hueCenter * innerGlow;
268+
269+
// ── Zone label integration ───────────────────────────
270+
{
271+
vec2 labelUv = fragCoord / max(iResolution, vec2(0.001));
272+
vec2 texel = 1.0 / max(iResolution, vec2(1.0));
273+
274+
float halo = 0.0;
275+
float labelAlpha = texture(uZoneLabels, labelUv).a;
276+
for (int dy = -3; dy <= 3; dy++) {
277+
for (int dx = -3; dx <= 3; dx++) {
278+
vec2 off = vec2(float(dx), float(dy)) * texel * 2.5;
279+
halo += texture(uZoneLabels, labelUv + off).a;
280+
}
281+
}
282+
halo /= 49.0;
283+
284+
if (halo > 0.005) {
285+
float haloEdge = halo * (1.0 - labelAlpha);
286+
vec3 haloColor = mix(hueCenter, shapeTint, 0.5 + 0.5 * sin(iTime * 0.7));
287+
float haloBright = haloEdge * (0.4 + energy * 0.8 + bass * 0.6);
288+
result.rgb += haloColor * haloBright;
289+
}
290+
291+
if (labelAlpha > 0.01) {
292+
vec3 boosted = result.rgb * (2.5 + energy * 2.0 + bass * 1.5);
293+
vec3 labelTint = mix(hueCenter, shapeTint, 0.5 + 0.5 * sin(iTime * 0.5));
294+
vec3 hotCore = labelTint * (0.6 + energy * 0.4);
295+
boosted += hotCore;
296+
result.rgb = mix(result.rgb, boosted, labelAlpha);
297+
result.a = max(result.a, labelAlpha);
298+
}
299+
}
300+
}
301+
302+
// ── Border ──────────────────────────────────────────────
303+
304+
float coreWidth = borderWidth * mix(0.5, 0.9, vitality);
305+
float core = softBorder(d, coreWidth);
306+
if (core > 0.0) {
307+
float angle = atan(p.x, -p.y) / TAU + 0.5;
308+
309+
float borderEnergy = 1.0 + energy * mix(0.2, 1.0, vitality) + idlePulse * 0.3;
310+
vec3 coreColor = hueCenter * mix(1.0, 2.0, vitality) * borderEnergy;
311+
312+
float flowSpeed = mix(0.3, 2.0, vitality);
313+
float flowRange = mix(0.1, 0.4, vitality);
314+
float flow = angularNoise(angle, 10.0, -iTime * flowSpeed) * flowRange + (1.0 - flowRange * 0.5);
315+
coreColor *= flow;
316+
317+
if (isHighlighted) {
318+
float breathe = 0.8 + 0.2 * sin(iTime * 3.0 + energy * 4.0);
319+
coreColor *= breathe;
320+
float accentTrace = angularNoise(angle, 6.0, iTime * 2.5);
321+
coreColor = mix(coreColor, shapeTint * borderEnergy, accentTrace * 0.3);
322+
}
323+
324+
coreColor = mix(coreColor, vec3(1.0), core * mix(0.25, 0.6, vitality));
325+
326+
if (hasAudio && bass > 0.5) {
327+
float flash = (bass - 0.5) * 2.0 * vitality;
328+
coreColor = mix(coreColor, vec3(1.0, 0.6, 0.2) * 2.0, flash * core * 0.3);
329+
}
330+
331+
result.rgb = max(result.rgb, coreColor * core);
332+
result.a = max(result.a, core);
333+
}
334+
335+
// ── Outer glow ──────────────────────────────────────────
336+
337+
float baseGlowR = mix(8.0, 20.0, vitality);
338+
float bassGlowR = mix(10.0, 35.0, vitality);
339+
float glowRadius = baseGlowR + bassGlowR * (hasAudio ? bass * reactivity : idlePulse) + 5.0 * energy;
340+
if (d > 0.0 && d < glowRadius) {
341+
float glowStr = mix(0.12, 0.35, vitality);
342+
float glow1 = expGlow(d, glowRadius * 0.2, glowStr);
343+
float glow2 = expGlow(d, glowRadius * 0.5, glowStr * 0.35);
344+
345+
vec3 glowColor = hueCenter;
346+
if (isHighlighted) {
347+
float glowAngle = atan(p.x, -p.y) / TAU + 0.5;
348+
glowColor = mix(hueCenter, shapeTint, angularNoise(glowAngle, 4.0, iTime * 0.8) * 0.5);
349+
}
350+
if (hasAudio && bass > 0.3) {
351+
glowColor = mix(glowColor, vec3(1.0, 0.6, 0.2), (bass - 0.3) * 1.5 * vitality);
352+
}
353+
354+
result.rgb += glowColor * (glow1 + glow2);
355+
result.a = max(result.a, (glow1 + glow2) * 0.5);
356+
}
357+
358+
return result;
359+
}
360+
361+
// ─── Main ───────────────────────────────────────────────────────
362+
363+
void main() {
364+
vec2 fragCoord = vFragCoord;
365+
vec4 color = vec4(0.0);
366+
367+
if (zoneCount == 0) {
368+
fragColor = vec4(0.0);
369+
return;
370+
}
371+
372+
bool hasAudio = iAudioSpectrumSize > 0;
373+
float bass = getBass();
374+
float mids = getMids();
375+
float treble = getTreble();
376+
float overall = getOverall();
377+
378+
for (int i = 0; i < zoneCount && i < 64; i++) {
379+
vec4 rect = zoneRects[i];
380+
if (rect.z <= 0.0 || rect.w <= 0.0) continue;
381+
382+
vec4 zoneColor = renderZone(fragCoord, rect, zoneFillColors[i],
383+
zoneBorderColors[i], zoneParams[i], zoneParams[i].z > 0.5,
384+
bass, mids, treble, overall, hasAudio);
385+
386+
color = blendOver(color, zoneColor);
387+
}
388+
389+
fragColor = clampFragColor(color);
390+
}

0 commit comments

Comments
 (0)