Add five visualizers to the scene library
This commit is contained in:
parent
7d59ef8e5f
commit
045ba6a322
@ -19,6 +19,11 @@ import { scanTear } from './shader/scan-tear.js';
|
|||||||
import { blockMosh } from './shader/block-mosh.js';
|
import { blockMosh } from './shader/block-mosh.js';
|
||||||
import { neonCity } from './shader/neon-city.js';
|
import { neonCity } from './shader/neon-city.js';
|
||||||
import { flora } from './shader/flora.js';
|
import { flora } from './shader/flora.js';
|
||||||
|
import { fireflyDrift } from './shader/firefly-drift.js';
|
||||||
|
import { silkRibbon } from './shader/silk-ribbon.js';
|
||||||
|
import { pylonGrid } from './shader/pylon-grid.js';
|
||||||
|
import { prismBloom } from './shader/prism-bloom.js';
|
||||||
|
import { pitchShatter } from './shader/pitch-shatter.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The scene library. Families exist so the arc driver can choose by section
|
* The scene library. Families exist so the arc driver can choose by section
|
||||||
@ -57,6 +62,11 @@ const MODULES = [
|
|||||||
blockMosh,
|
blockMosh,
|
||||||
neonCity,
|
neonCity,
|
||||||
flora,
|
flora,
|
||||||
|
fireflyDrift,
|
||||||
|
silkRibbon,
|
||||||
|
pylonGrid,
|
||||||
|
prismBloom,
|
||||||
|
pitchShatter,
|
||||||
];
|
];
|
||||||
|
|
||||||
const errors = [];
|
const errors = [];
|
||||||
|
|||||||
62
flow-state/src/scenes/shader/firefly-drift.js
Normal file
62
flow-state/src/scenes/shader/firefly-drift.js
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
// Flow family: a drifting swarm of glowing motes advected along a curl-noise
|
||||||
|
// field. Each mote rides a divergence-free curl flow and wanders on top of it,
|
||||||
|
// so the swarm pools and diverges like embers on a breeze rather than orbiting.
|
||||||
|
//
|
||||||
|
// A base field is always present so the scene stands alone before any feedback
|
||||||
|
// has converged; the motes just make the movement legible.
|
||||||
|
|
||||||
|
export const fireflyDrift = {
|
||||||
|
name: 'Firefly Drift',
|
||||||
|
family: 'flow',
|
||||||
|
kind: 'fragment',
|
||||||
|
// Personality: see look/Personality.js.
|
||||||
|
traits: ['camera', 'style'],
|
||||||
|
|
||||||
|
params: {
|
||||||
|
count: { type: 'float', range: [6, 48], default: 22, uniform: 'u_count', bias: 'density' },
|
||||||
|
speed: { type: 'float', range: [0.05, 0.8], default: 0.3, uniform: 'u_speed', bias: 'motion', rate: true },
|
||||||
|
glow: { type: 'float', range: [0, 1.2], default: 0.5, uniform: 'u_glow', bias: 'energy' },
|
||||||
|
jitter: { type: 'float', range: [0, 0.4], default: 0.12, uniform: 'u_jitter' },
|
||||||
|
spread: { type: 'float', range: [0.4, 1.3], default: 0.95, uniform: 'u_spread' },
|
||||||
|
palette: { type: 'palette', count: 4 },
|
||||||
|
},
|
||||||
|
|
||||||
|
reactive: {
|
||||||
|
glow: { feature: 'beat', amount: 0.3, response: 'spike' },
|
||||||
|
jitter: { feature: 'bandHigh', amount: 0.2, response: 'smooth' },
|
||||||
|
},
|
||||||
|
|
||||||
|
shader: `
|
||||||
|
vec4 scene(vec2 uv, vec2 p) {
|
||||||
|
float t = u_time * u_speed + u_seed;
|
||||||
|
p = sigCamera(p);
|
||||||
|
|
||||||
|
// Soft ambient wash so the frame is never black, even standing alone.
|
||||||
|
vec3 col = pal(0) * (0.06 + 0.05 * fbm(p * 1.5 + vec2(0.0, t * 0.3), 3));
|
||||||
|
|
||||||
|
for (int i = 0; i < 48; i++) {
|
||||||
|
if (float(i) >= u_count) break;
|
||||||
|
float fi = float(i);
|
||||||
|
|
||||||
|
// Seed the mote to a stable place in the frame (deterministic per i).
|
||||||
|
vec2 grid = vec2(hash12(vec2(fi, 1.7)), hash12(vec2(fi, 9.1)));
|
||||||
|
vec2 base = (grid - 0.5) * 2.0 * u_spread * vec2(1.0, 0.7);
|
||||||
|
|
||||||
|
// Advect along the curl field plus a bounded wander term on top.
|
||||||
|
vec2 flow = curl(grid * 3.0 + vec2(0.0, t * 0.15), t * 0.5);
|
||||||
|
vec2 pos = base + flow * 1.3
|
||||||
|
+ vec2(sin(t * (0.4 + fract(fi * 0.13))),
|
||||||
|
cos(t * (0.5 + fract(fi * 0.29)))) * u_jitter;
|
||||||
|
|
||||||
|
float d = length(p - pos);
|
||||||
|
|
||||||
|
vec3 hc = pal(int(mod(fi, 4.0)));
|
||||||
|
col += hc * (exp(-d * d / (0.015 + u_glow * 0.04)) * (1.0 + u_glow * 0.6));
|
||||||
|
col += hc * exp(-d * d * 60.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
col += sigGrain(uv);
|
||||||
|
return vec4(col, 1.0);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
71
flow-state/src/scenes/shader/pitch-shatter.js
Normal file
71
flow-state/src/scenes/shader/pitch-shatter.js
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
// Glitch family: vertical transposition. The frame is cut into vertical bands,
|
||||||
|
// each of which shifts up or down by a small amount. That alone is a tear; what
|
||||||
|
// makes it churn is that the offset is re-rolled on the quantised grid and the
|
||||||
|
// previous frame's transposed image is smeared underneath — so slices translate
|
||||||
|
// to a new fixed pose each step instead of crawling, and the pitch reads as an
|
||||||
|
// edit rather than a broken renderer.
|
||||||
|
|
||||||
|
export const pitchShatter = {
|
||||||
|
name: 'Pitch Shatter',
|
||||||
|
family: 'glitch',
|
||||||
|
kind: 'fragment',
|
||||||
|
// Personality: see look/Personality.js.
|
||||||
|
traits: ['camera', 'style'],
|
||||||
|
|
||||||
|
params: {
|
||||||
|
slices: { type: 'float', range: [4, 40], default: 18, uniform: 'u_slices', bias: 'density' },
|
||||||
|
amp: { type: 'float', range: [0, 0.35], default: 0.1, uniform: 'u_amp', bias: 'energy' },
|
||||||
|
quantize: { type: 'float', range: [1, 8], default: 4, uniform: 'u_quantize' },
|
||||||
|
bleed: { type: 'float', range: [0, 0.9], default: 0.4, uniform: 'u_bleed' },
|
||||||
|
glow: { type: 'float', range: [0, 1], default: 0.3, uniform: 'u_glow', bias: 'energy' },
|
||||||
|
speed: { type: 'float', range: [0.05, 0.8], default: 0.25, uniform: 'u_speed', bias: 'motion', rate: true },
|
||||||
|
palette: { type: 'palette', count: 5 },
|
||||||
|
},
|
||||||
|
|
||||||
|
reactive: {
|
||||||
|
amp: { feature: 'beat', amount: 0.4, response: 'spike' },
|
||||||
|
bleed: { feature: 'flux', amount: 0.25, response: 'smooth' },
|
||||||
|
glow: { feature: 'bandHigh', amount: 0.2 },
|
||||||
|
},
|
||||||
|
|
||||||
|
shader: `
|
||||||
|
vec4 scene(vec2 uv, vec2 p) {
|
||||||
|
float t = u_time * u_speed + u_seed;
|
||||||
|
p = sigCamera(p);
|
||||||
|
|
||||||
|
// One shared grid for every discontinuity below: slices re-roll on the
|
||||||
|
// quantised step rather than moving continuously frame-to-frame.
|
||||||
|
float q = max(u_quantize, 1.0);
|
||||||
|
float step_ = floor(u_barPhase * q) + floor(t * 6.0) * q;
|
||||||
|
|
||||||
|
// A vertical slice is a column of pixels; each gets one fixed offset per
|
||||||
|
// step, decided by a hash of (slice, step) so it jumps cleanly on the grid.
|
||||||
|
float slice = floor(uv.x * u_slices);
|
||||||
|
float sliceRand = hash12(vec2(slice, step_ * 31.0));
|
||||||
|
float pitch = (sliceRand - 0.5) * 2.0 * u_amp;
|
||||||
|
|
||||||
|
// The base image is always sampled from the un-pitched field so the scene
|
||||||
|
// has real content behind the displacement, even on its first frame.
|
||||||
|
vec2 baseUv = vec2(uv.x, fract(uv.y - pitch));
|
||||||
|
float field = fbm(vec2(baseUv.x * 2.5, baseUv.y * 4.0) + t * 0.4, 4);
|
||||||
|
float ramp = fract(field * 2.0 + baseUv.y * 2.0 - t * 0.4);
|
||||||
|
vec3 col = palRamp(ramp * 0.7 + slice * 0.02);
|
||||||
|
col *= 0.4 + 0.6 * smoothstep(0.1, 0.9, field);
|
||||||
|
|
||||||
|
// The smear: pull the previous frame across the pitch so slices leave a
|
||||||
|
// transient ghost as they land, and the corruption feels sludgy.
|
||||||
|
vec3 ghost = prev(vec2(uv.x, fract(uv.y + pitch * 0.55)));
|
||||||
|
col = mix(col, ghost, u_bleed * (0.4 + sliceRand));
|
||||||
|
|
||||||
|
// A thin scan beam rooting the hard edge of each step, kept local and beat
|
||||||
|
// -pinned so the frame never swings in whole-frame luminance.
|
||||||
|
float beamY = fract(u_beatPhase) * 2.0 - 1.0;
|
||||||
|
float beam = exp(-abs(p.y - beamY * 0.6) * 6.0);
|
||||||
|
col += pal(3) * beam * u_slices * 0.001 * (0.5 + u_beat);
|
||||||
|
|
||||||
|
col += pal(int(mod(slice, 5.0))) * (1.0 - sliceRand) * u_glow * 0.3 * sliceRand;
|
||||||
|
col += sigGrain(uv);
|
||||||
|
return vec4(col, 1.0);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
75
flow-state/src/scenes/shader/prism-bloom.js
Normal file
75
flow-state/src/scenes/shader/prism-bloom.js
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
// Geometric family: a burst of rotating folded prisms. A core sits at the
|
||||||
|
// centre in the track's signature form, and around it a constellation of folded
|
||||||
|
// rings counter-rotate and breathe — prism facets read as hard geometry meeting
|
||||||
|
// a soft bloom. Everything is closed-form, so the whole lattice is seek-exact.
|
||||||
|
|
||||||
|
export const prismBloom = {
|
||||||
|
name: 'Prism Bloom',
|
||||||
|
family: 'geometric',
|
||||||
|
kind: 'fragment',
|
||||||
|
// Personality: see look/Personality.js.
|
||||||
|
traits: ['shape', 'camera', 'style'],
|
||||||
|
|
||||||
|
params: {
|
||||||
|
sites: { type: 'float', range: [2, 12], default: 6, uniform: 'u_sites', bias: 'density' },
|
||||||
|
layers: { type: 'float', range: [1, 8], default: 5, uniform: 'u_layers', bias: 'density' },
|
||||||
|
thickness: { type: 'float', range: [0.008, 0.05], default: 0.02, uniform: 'u_thickness' },
|
||||||
|
radius: { type: 'float', range: [0.15, 0.7], default: 0.32, uniform: 'u_radius', bias: 'energy' },
|
||||||
|
spin: { type: 'float', range: [0, 1.5], default: 0.4, uniform: 'u_spin', bias: 'motion' },
|
||||||
|
glow: { type: 'float', range: [0, 1.4], default: 0.5, uniform: 'u_glow', bias: 'energy' },
|
||||||
|
speed: { type: 'float', range: [0.05, 0.8], default: 0.3, uniform: 'u_speed', bias: 'motion', rate: true },
|
||||||
|
palette: { type: 'palette', count: 6 },
|
||||||
|
},
|
||||||
|
|
||||||
|
reactive: {
|
||||||
|
glow: { feature: 'beat', amount: 0.35, response: 'smooth' },
|
||||||
|
radius: { feature: 'bandLow', amount: 0.2 },
|
||||||
|
spin: { feature: 'flux', amount: 0.15, response: 'smooth' },
|
||||||
|
},
|
||||||
|
|
||||||
|
shader: `
|
||||||
|
// A ring whose radius is folded into N star points, keeping the geometry angular
|
||||||
|
// rather than circular.
|
||||||
|
float ring(vec2 q, float sites, float radius, float width) {
|
||||||
|
float a = atan(q.y, q.x);
|
||||||
|
float seg = 6.28318530718 / sites;
|
||||||
|
float wa = abs(mod(a + seg * 0.5, seg) - seg * 0.5);
|
||||||
|
float starRadius = radius * max(cos(wa * sites), 1e-3);
|
||||||
|
float d = abs(length(q) - starRadius);
|
||||||
|
return smoothstep(width + 0.004, width - 0.004, d);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 scene(vec2 uv, vec2 p) {
|
||||||
|
float t = u_time * u_speed + u_seed;
|
||||||
|
p = sigFolded(sigCamera(p));
|
||||||
|
|
||||||
|
// Facet count yields to the track's signature form when it names one.
|
||||||
|
float sites = u_sigSides > 2.5 ? u_sigSides : float(u_sites);
|
||||||
|
|
||||||
|
vec3 col = pal(0) * 0.04;
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
if (float(i) >= u_layers) break;
|
||||||
|
float fi = float(i);
|
||||||
|
|
||||||
|
// Layers counter-rotate and breathe independently; all closed-form.
|
||||||
|
float layerRadius = u_radius * (0.5 + fi * 0.24);
|
||||||
|
float breathe = 0.78 + 0.22 * sin(u_time * 0.9 + fi * 1.7);
|
||||||
|
vec2 lp = rot(fi * 0.7 + t * (fi * 0.5 + 0.3) * u_spin) * p;
|
||||||
|
|
||||||
|
float r = ring(lp, sites, layerRadius * breathe, u_thickness);
|
||||||
|
col += pal(int(mod(fi, 6.0))) * r * (0.4 + u_glow * 0.8);
|
||||||
|
|
||||||
|
float halo = exp(-(length(lp) / max(layerRadius * breathe, 1e-3)) * 3.0) * u_glow * 0.25;
|
||||||
|
col += pal(0) * halo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The heart of the bloom is the track's own form.
|
||||||
|
float core = sigForm(p, vec2(0.0), 0.09 + u_beat * 0.06);
|
||||||
|
col = mix(col, pal(3), core);
|
||||||
|
col += pal(3) * exp(-dot(p, p) * 8.0) * (0.5 + u_glow);
|
||||||
|
|
||||||
|
col += sigGrain(uv);
|
||||||
|
return vec4(col, 1.0);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
84
flow-state/src/scenes/shader/pylon-grid.js
Normal file
84
flow-state/src/scenes/shader/pylon-grid.js
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
// Structural family: a field of standing pylons receding to the track's
|
||||||
|
// horizon. The columns converge on a vanishing point, giving real perspective
|
||||||
|
// depth, and each column carries a crown in the signature form so a hexagonal
|
||||||
|
// track grows hexagonal pylons. The beat walks a pulse of light down the rows
|
||||||
|
// rather than flashing the whole frame.
|
||||||
|
|
||||||
|
export const pylonGrid = {
|
||||||
|
name: 'Pylon Grid',
|
||||||
|
family: 'structural',
|
||||||
|
kind: 'fragment',
|
||||||
|
// Personality: see look/Personality.js.
|
||||||
|
traits: ['shape', 'space'],
|
||||||
|
|
||||||
|
params: {
|
||||||
|
columns: { type: 'float', range: [3, 16], default: 8, uniform: 'u_columns', bias: 'density' },
|
||||||
|
rows: { type: 'float', range: [2, 10], default: 6, uniform: 'u_rows', bias: 'density' },
|
||||||
|
spread: { type: 'float', range: [0.6, 1.6], default: 1.1, uniform: 'u_spread' },
|
||||||
|
height: { type: 'float', range: [0.3, 1.4], default: 0.85, uniform: 'u_height', bias: 'energy' },
|
||||||
|
pulse: { type: 'float', range: [0, 1.2], default: 0.5, uniform: 'u_pulse', bias: 'energy' },
|
||||||
|
speed: { type: 'float', range: [0.05, 0.8], default: 0.3, uniform: 'u_speed', bias: 'motion', rate: true },
|
||||||
|
palette: { type: 'palette', count: 4 },
|
||||||
|
},
|
||||||
|
|
||||||
|
reactive: {
|
||||||
|
pulse: { feature: 'beat', amount: 0.4, response: 'smooth' },
|
||||||
|
rows: { feature: 'bandLow', amount: 0.25 },
|
||||||
|
},
|
||||||
|
|
||||||
|
shader: `
|
||||||
|
// Distance to a thin vertical strut from (cx, y0) up to (cx, y1).
|
||||||
|
float strut(vec2 p, float cx, float y0, float y1, float w) {
|
||||||
|
float xd = abs(p.x - cx);
|
||||||
|
float yd = max(max(y0 - p.y, p.y - y1), 0.0);
|
||||||
|
float d = sqrt(xd * xd + yd * yd);
|
||||||
|
return smoothstep(w + 0.004, w - 0.004, d);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 scene(vec2 uv, vec2 p) {
|
||||||
|
float t = u_time * u_speed + u_seed;
|
||||||
|
float hy = sigHorizonY();
|
||||||
|
vec3 col = pal(0) * 0.05;
|
||||||
|
|
||||||
|
for (int r = 0; r < 10; r++) {
|
||||||
|
if (float(r) >= u_rows) break;
|
||||||
|
float fr = float(r);
|
||||||
|
|
||||||
|
// Perspective: the last row sits on the horizon, the first at the bottom
|
||||||
|
// of the frame, everything between scaling by its depth fraction.
|
||||||
|
float f = fr / max(u_rows - 1.0, 1.0); // 0 near .. 1 far
|
||||||
|
float y0 = mix(-1.08, hy + 0.06, f); // row baseline
|
||||||
|
float scale = mix(1.0, 0.12, f); // perspective shrink
|
||||||
|
float depthShade = mix(1.0, 0.35, f); // far rows dim
|
||||||
|
|
||||||
|
for (int c = 0; c < 16; c++) {
|
||||||
|
if (float(c) >= u_columns) break;
|
||||||
|
float fc = float(c);
|
||||||
|
float cx = (fc / max(u_columns - 1.0, 1.0) - 0.5) * 2.0 * u_spread * scale;
|
||||||
|
|
||||||
|
float h = u_height * scale;
|
||||||
|
float yTop = y0 + h;
|
||||||
|
|
||||||
|
// The beat pulse walks a glow down the depth axis, row by row, so the
|
||||||
|
// light travels rather than strobing the whole frame.
|
||||||
|
float walk = sat(1.0 - abs(fr - mod(u_beatPhase * 6.0 + t * 0.4, u_rows)));
|
||||||
|
float inten = (0.35 + 0.65 * walk) * depthShade * (0.7 + 0.3 * u_beat);
|
||||||
|
|
||||||
|
float w = 0.02 * scale + 0.006;
|
||||||
|
col += pal(int(mod(fc, 4.0))) * strut(p, cx, y0, yTop, w) * inten;
|
||||||
|
|
||||||
|
// Crown stamped in the track's signature form.
|
||||||
|
vec2 crown = vec2(cx, yTop);
|
||||||
|
float size = (0.06 + 0.2 * scale) * (1.0 + u_beat * 0.4);
|
||||||
|
col += pal(3) * sigForm(p, crown, size) * inten * (0.6 + u_pulse);
|
||||||
|
col += pal(int(mod(fr, 4.0))) * exp(-dot(p - crown, p - crown) * 60.0) * u_pulse * depthShade;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ground haze and the shared air between here and the horizon.
|
||||||
|
col = sigAir(col, p, smoothstep(0.0, 1.6, abs(p.y - hy)));
|
||||||
|
col += sigGrain(uv);
|
||||||
|
return vec4(col, 1.0);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
68
flow-state/src/scenes/shader/silk-ribbon.js
Normal file
68
flow-state/src/scenes/shader/silk-ribbon.js
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
// Minimal family: a single ribbon of light draping across an almost empty
|
||||||
|
// frame. The curve is a closed-form travelling wave, so it is seek-exact; it is
|
||||||
|
// drawn by sampling the polyline and taking the minimum distance, which reads as
|
||||||
|
// one fluid strand rather than a repeated motif. Most of the frame is negative
|
||||||
|
// space — what an intro or a breakdown needs.
|
||||||
|
|
||||||
|
export const silkRibbon = {
|
||||||
|
name: 'Silk Ribbon',
|
||||||
|
family: 'minimal',
|
||||||
|
kind: 'fragment',
|
||||||
|
// Personality: see look/Personality.js.
|
||||||
|
traits: ['camera', 'style'],
|
||||||
|
|
||||||
|
params: {
|
||||||
|
thickness: { type: 'float', range: [0.008, 0.08], default: 0.03, uniform: 'u_thickness' },
|
||||||
|
wave: { type: 'float', range: [0.1, 2.5], default: 1.1, uniform: 'u_wave' },
|
||||||
|
sway: { type: 'float', range: [0.05, 0.6], default: 0.25, uniform: 'u_sway', bias: 'motion' },
|
||||||
|
glow: { type: 'float', range: [0, 1.5], default: 0.5, uniform: 'u_glow', bias: 'energy' },
|
||||||
|
speed: { type: 'float', range: [0.02, 0.4], default: 0.12, uniform: 'u_speed', bias: 'motion', rate: true },
|
||||||
|
spread: { type: 'float', range: [0.5, 1.2], default: 0.9, uniform: 'u_spread' },
|
||||||
|
palette: { type: 'palette', count: 4 },
|
||||||
|
},
|
||||||
|
|
||||||
|
reactive: {
|
||||||
|
glow: { feature: 'beat', amount: 0.3, response: 'smooth' },
|
||||||
|
wave: { feature: 'bandLow', amount: 0.25, response: 'smooth' },
|
||||||
|
},
|
||||||
|
|
||||||
|
shader: `
|
||||||
|
// Nearest distance from p to a fixed travelling strand.
|
||||||
|
vec4 scene(vec2 uv, vec2 p) {
|
||||||
|
float t = u_time * u_speed + u_seed;
|
||||||
|
p = sigCamera(p);
|
||||||
|
|
||||||
|
// Bare wash so the frame is never black.
|
||||||
|
vec3 col = pal(0) * 0.04;
|
||||||
|
|
||||||
|
// Two nested strands, offset in phase so the ribbon reads as a drape with a
|
||||||
|
// folded edge rather than a single hairline.
|
||||||
|
for (int k = 0; k < 2; k++) {
|
||||||
|
float fk = float(k);
|
||||||
|
float ph = fk * 2.2 + t * 0.3;
|
||||||
|
float best = 1e9;
|
||||||
|
|
||||||
|
for (int n = 0; n < 48; n++) {
|
||||||
|
float uu = (float(n) + 0.5) / 48.0;
|
||||||
|
float x = (uu - 0.5) * 2.0 * u_spread;
|
||||||
|
float y = sin(uu * 6.28318530718 * u_wave + ph) * 0.28
|
||||||
|
+ sin(uu * 13.1 - t * 0.8 + fk) * 0.06
|
||||||
|
+ u_sway * 0.4 * sin(t * 0.6 + fk);
|
||||||
|
float d = length(p - vec2(x, y));
|
||||||
|
best = min(best, d);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 hc = pal(int(fk) % 3);
|
||||||
|
col += hc * exp(-best * best / (u_thickness * u_thickness));
|
||||||
|
col += pal(3) * exp(-best * best * 60.0) * u_glow * 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Knot the ribbon just below frame centre so it has somewhere to pull from.
|
||||||
|
float knot = length(p - vec2(0.0, 0.1));
|
||||||
|
col = mix(col, pal(2), exp(-knot * knot * 30.0) * u_glow * 0.5);
|
||||||
|
|
||||||
|
col += sigGrain(uv);
|
||||||
|
return vec4(col, 1.0);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user