Feature: Synthwave visualizer

This commit is contained in:
Dejvino 2026-05-24 14:17:53 +02:00
parent c6324d3ccd
commit ec2465222c
3 changed files with 253 additions and 0 deletions

View File

@ -0,0 +1,111 @@
import { VisualizerInterface } from './VisualizerInterface.js';
const fragmentShader = `
uniform float u_time;
uniform float u_beat;
uniform float u_opacity;
uniform vec2 u_resolution;
uniform vec3 u_colors[16];
uniform int u_colorCount;
uniform float u_seed;
varying vec2 vUv;
float sdCircle(vec2 p, float r) {
return length(p) - r;
}
float sdBox(vec2 p, vec2 b) {
vec2 d = abs(p) - b;
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
}
vec2 rotate(vec2 p, float a) {
float s = sin(a);
float c = cos(a);
return mat2(c, -s, s, c) * p;
}
void main() {
// LED Grid Setup
float ledCountX = u_resolution.x;
float ledCountY = u_resolution.y;
vec2 gridUV = vec2(vUv.x * ledCountX, vUv.y * ledCountY);
vec2 gridDeriv = fwidth(gridUV);
float gridDensity = max(gridDeriv.x, gridDeriv.y);
float blurFactor = smoothstep(0.3, 0.8, gridDensity);
vec2 cell = fract(gridUV);
vec2 uv = mix((floor(gridUV) + 0.5) / vec2(ledCountX, ledCountY), vUv, blurFactor);
float distMask = distance(cell, vec2(0.5));
float edgeSoftness = clamp(gridDensity * 1.5, 0.0, 0.5);
float mask = 1.0 - smoothstep(0.35 - edgeSoftness, 0.45 + edgeSoftness, distMask);
mask = mix(mask, 1.0, blurFactor);
// Aspect correction for shapes
vec2 p = (uv - 0.5);
p.x *= u_resolution.x / u_resolution.y;
float t = u_time * 0.4 + u_seed;
float beatImpact = u_beat * 0.1;
// Background color shift
vec3 colorA = vec3(0.05, 0.05, 0.1);
vec3 colorB = vec3(0.1, 0.0, 0.2);
if (u_colorCount >= 2) {
colorA = u_colors[0] * 0.2;
colorB = u_colors[int(mod(1.0 + floor(t * 0.1), float(u_colorCount)))] * 0.3;
}
vec3 finalColor = mix(colorA, colorB, sin(t) * 0.5 + 0.5);
// Render shapes
float shapes = 0.0;
for (int i = 0; i < 6; i++) {
float fi = float(i);
float shapeSeed = u_seed + fi * 123.456;
// Motion
vec2 pos = vec2(
sin(t * 0.5 + shapeSeed) * 0.8,
cos(t * 0.3 + shapeSeed * 1.1) * 0.4
);
vec2 sp = p - pos;
sp = rotate(sp, t * (0.5 + fract(shapeSeed)));
float d;
float size = 0.1 + fract(shapeSeed * 0.7) * 0.2 + beatImpact;
if (mod(fi, 2.0) == 0.0) {
d = sdCircle(sp, size);
} else {
d = sdBox(sp, vec2(size * 0.8));
}
float intensity = smoothstep(0.01, 0.0, d);
// Pick a color for the shape
vec3 shapeColor = vec3(1.0);
if (u_colorCount > 0) {
int colorIdx = int(mod(fi + floor(t), float(u_colorCount)));
shapeColor = u_colors[colorIdx];
}
finalColor = mix(finalColor, shapeColor, intensity * 0.8);
// Add a slight glow/aura
finalColor += shapeColor * (1.0 - smoothstep(0.0, size * 2.0, abs(d))) * 0.2 * (0.5 + u_beat);
}
gl_FragColor = vec4(finalColor, mask * u_opacity);
}
`;
export class FloatingShapesVisualizer extends VisualizerInterface {
getName() {
return "Floating Geometry";
}
getFragmentShader() {
return fragmentShader;
}
}

View File

@ -0,0 +1,138 @@
import { VisualizerInterface } from './VisualizerInterface.js';
const fragmentShader = `
uniform float u_time;
uniform float u_beat;
uniform float u_opacity;
uniform vec2 u_resolution;
uniform vec3 u_colors[16];
uniform int u_colorCount;
uniform float u_seed;
varying vec2 vUv;
float hash(float n) { return fract(sin(n) * 43758.5453123); }
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float n = i.x + i.y * 57.0;
return mix(mix(hash(n + 0.0), hash(n + 1.0), f.x),
mix(hash(n + 57.0), hash(n + 58.0), f.x), f.y);
}
void main() {
// LED Grid Setup
float ledCountX = u_resolution.x;
float ledCountY = u_resolution.y;
vec2 gridUV = vec2(vUv.x * ledCountX, vUv.y * ledCountY);
vec2 gridDeriv = fwidth(gridUV);
float gridDensity = max(gridDeriv.x, gridDeriv.y);
float blurFactor = smoothstep(0.3, 0.8, gridDensity);
vec2 cell = fract(gridUV);
vec2 uv = mix((floor(gridUV) + 0.5) / vec2(ledCountX, ledCountY), vUv, blurFactor);
float distMask = distance(cell, vec2(0.5));
float edgeSoftness = clamp(gridDensity * 1.5, 0.0, 0.5);
float mask = 1.0 - smoothstep(0.35 - edgeSoftness, 0.45 + edgeSoftness, distMask);
mask = mix(mask, 1.0, blurFactor);
// --- Synthwave Logic ---
vec2 p = uv * 2.0 - 1.0;
p.x *= u_resolution.x / u_resolution.y;
float t = u_time * 1.5 + u_seed;
float beat = u_beat;
// Colors
vec3 colorMain = vec3(1.0, 0.0, 0.8); // Pink/Magenta
vec3 colorSec = vec3(0.0, 0.8, 1.0); // Cyan
vec3 colorBg = vec3(0.02, 0.0, 0.05);
if (u_colorCount >= 2) {
colorMain = u_colors[0];
colorSec = u_colors[int(mod(1.0, float(u_colorCount)))];
}
vec3 finalColor = colorBg;
// Horizon
float horizon = 0.0;
// 1. Perspective Grid (Ground)
if (p.y < horizon) {
float perspective = 1.0 / (horizon - p.y + 0.05);
vec2 gridP = vec2(p.x * perspective, perspective + t * 2.0);
float gridLines = smoothstep(0.05, 0.0, abs(fract(gridP.x + 0.5) - 0.5)) +
smoothstep(0.05, 0.0, abs(fract(gridP.y + 0.5) - 0.5));
finalColor = mix(finalColor, colorSec, gridLines * 0.5 * (0.5 + beat));
// Road highlight
if (abs(p.x * perspective) < 0.8) {
finalColor += colorMain * 0.2 * perspective;
}
}
// 2. Wireframe Mountains
float mountainHeight = 0.0;
if (p.y > horizon - 0.1) {
float side = sign(p.x);
float mountainX = abs(p.x) - 0.5;
if (mountainX > 0.0) {
float mNoise = noise(vec2(mountainX * 2.0 + t * 0.1, u_seed));
mountainHeight = mNoise * 0.6 * smoothstep(0.0, 0.5, mountainX);
if (p.y - horizon < mountainHeight) {
float distToEdge = abs(p.y - horizon - mountainHeight);
float wireframe = smoothstep(0.02, 0.0, distToEdge);
// Internal wireframe lines
wireframe += smoothstep(0.01, 0.0, abs(fract((p.y - horizon) * 10.0) - 0.5));
wireframe += smoothstep(0.01, 0.0, abs(fract(mountainX * 10.0) - 0.5));
finalColor = mix(finalColor, colorMain, wireframe * 0.4);
}
}
}
// 3. Retro Sun
vec2 sunPos = vec2(0.0, 0.1);
float sunRad = 0.4;
float distToSun = length(p - sunPos);
if (distToSun < sunRad && p.y > horizon) {
// Scanlines / Cutouts
float scanline = sin((p.y - t * 0.1) * 50.0);
if (scanline < mix(0.8, -1.0, (p.y - sunPos.y + sunRad) / (sunRad * 2.0))) {
float grad = 1.0 - (distToSun / sunRad);
vec3 sunCol = mix(colorMain, vec3(1.0, 0.8, 0.0), p.y * 2.0);
finalColor = mix(finalColor, sunCol, 0.8 + beat * 0.2);
}
}
// 4. Car Silhouette (Simplified)
vec2 carP = p - vec2(0.0, -0.75);
carP.x = abs(carP.x);
if (carP.y > -0.1 && carP.y < 0.1 && carP.x < 0.25) {
float body = step(carP.x, 0.25) * step(abs(carP.y), 0.05);
float cabin = step(carP.x, 0.15) * step(carP.y, 0.1) * step(0.0, carP.y);
if (body + cabin > 0.0) {
finalColor = mix(finalColor, vec3(0.0), 0.9);
finalColor += colorSec * 0.5 * body * (smoothstep(0.0, 0.05, abs(carP.x - 0.2))); // Tail lights / glow
}
}
gl_FragColor = vec4(finalColor, mask * u_opacity);
}
`;
export class SynthwaveRunVisualizer extends VisualizerInterface {
getName() {
return "Synthwave Run";
}
getFragmentShader() {
return fragmentShader;
}
}

View File

@ -1,9 +1,13 @@
import { ClassicVisualizer } from './ClassicVisualizer.js'; import { ClassicVisualizer } from './ClassicVisualizer.js';
import { NebulaVisualizer } from './NebulaVisualizer.js'; import { NebulaVisualizer } from './NebulaVisualizer.js';
import { FloatingShapesVisualizer } from './FloatingShapesVisualizer.js';
import { SynthwaveRunVisualizer } from './SynthwaveRunVisualizer.js';
export const visualizerLibrary = [ export const visualizerLibrary = [
new ClassicVisualizer(), new ClassicVisualizer(),
new NebulaVisualizer(), new NebulaVisualizer(),
new FloatingShapesVisualizer(),
new SynthwaveRunVisualizer(),
]; ];
export default visualizerLibrary; export default visualizerLibrary;