Gradients & NoiseATM
Surfaces made of color: the quiet backgrounds of a landing page and the textures of an editorial one. Each is a .shader with its data-effect; colors and parameters are data-* attributes.
On this page (20)
§Mesh gradient
Up to eight color points drift on slow orbits; every pixel takes their colors weighted by distance and mixed in Oklab, so the blends stay vivid instead of turning gray. A noise field bends the space between them, a gentle swirl turns its middle.
§Five colors, drifting
The controls beside it are generated from the effect: its lists, its parameters and its colors.
data-distortionHow far the noise bends the space (0.8)data-swirlThe turn at the middle (0.35)data-grainFilm grain over it (0)§Grain gradient
A soft gradient from the first color to the last, its bands shaped by slow noise and broken up by grain at every transition - the texture of risograph prints and premium landing pages. Without data-colors it starts at the page background and runs through the chart colors.
§Dusk in grain
The controls beside it are generated from the effect: its lists, its parameters and its colors.
data-softnessHow soft the band edges are (0.8)data-grainHow much the grain breaks them up (0.5)data-zoomThe size of the shapes (1)§Warp
Noise fed into itself twice - domain warping - folds into marble, smoke and lava. The colors run from the first (the deep folds) to the last (the bright veins).
§Marbled lava
The controls beside it are generated from the effect: its lists, its parameters and its colors.
data-zoomThe scale of the folds (1)data-warpHow strongly the noise folds itself (1)data-contrastThe spread of the colors (1.15)§Neuro noise
Layers of rotated waves add up to glowing filaments - a network that slowly rewires itself. The first color is the glow, the second its bright core, the third the hottest strands; the background shows between them.
§A rose network
The controls beside it are generated from the effect: its lists, its parameters and its colors.
data-zoomThe size of the cells (1)data-brightnessThe glow (1)§Dithering
An ordered 8 × 8 Bayer dither rasterizes a moving scene - or a picture inside the shader - into its colors, sorted from dark to light, at data-pixel CSS pixels a dot. With data-interactive the pointer opens a window onto the original.
§A scene and a photo, dithered
Move the pointer over the photo. The controls beside it are generated from the effect: its lists, its parameters and its colors.
data-pixelThe size of one dot in CSS pixels (3)data-revealThe radius of the pointer's window (0.18)data-contrastContrast before the dither (1.2)§Plasma
Four sine waves - along x, along y, along the diagonal and around a wandering center - summed and run through the palette as a cycle. The oldest effect of the demo scene, here mixed in Oklab, so the bands stay bright between two colors. data-warp bends the plane before the waves are read.
§Sine waves through a palette
The controls beside it are generated from the effect: its lists, its parameters and its colors.
data-zoomThe scale of the waves (1)data-warpHow much the plane bends (1)§Silk
A sheet of cloth in slow folds: a sine sheet carried by a few drifting waves, lit with a wrapped diffuse light so the shadows stay colored and a narrow sheen along each crest. The ramp runs from the palette's first color in the folds to its last on the highlights.
§Folds of silk
The controls beside it are generated from the effect: its lists, its parameters and its colors.
data-foldsHow many folds cross the box (4)data-sheenThe highlight on the crests (0.8)data-zoomThe scale of the cloth (1)§Marble
Polished stone with veins: noise warped twice over itself gives the cloudy ground, a folded sine through it the veins - at least a pixel wide, so they never break up. The veins flow slowly; data-flow="0" keeps the stone still.
§Veined stone
The controls beside it are generated from the effect: its lists, its parameters and its colors.
data-veinsHow wide the veins are, 0-1 (0.4)data-flowHow fast the stone flows (1)data-zoomThe scale of the pattern (1)§Oil
A thin film of oil on dark water: the film's thickness shifts the phase of each color channel differently - the interference colors of a soap bubble or a puddle - and a slow swirl moves it.
§An oil slick
The controls beside it are generated from the effect: its lists, its parameters and its colors.
data-filmHow many interference bands (1)data-swirlHow much the film swirls (1)data-zoomThe scale of the slick (1)§CSS view file
/* -- Shader ------------------------------------------------------------------ A WebGL2 effect inside or behind an element. The runtime adds a 2D canvas (.shader-canvas) the shared WebGL2 context copies each frame into; until the first frame - and wherever WebGL2 is missing - the element shows its poster: a static gradient of the same palette (--shader-poster). Variants: block (default, 16 / 9 or its picture's size), background (fills its parent behind the content) and border (a glow around its parent). */@layer components { .shader { --shader-aspect: 16 / 9; --shader-glow: 1.25rem; --shader-poster-default: radial-gradient(120% 90% at 15% 20%, color-mix(in oklch, var(--chart-1) 75%, transparent), transparent 60%), radial-gradient(100% 80% at 85% 25%, color-mix(in oklch, var(--chart-3) 70%, transparent), transparent 60%), radial-gradient(110% 90% at 50% 100%, color-mix(in oklch, var(--chart-5) 65%, transparent), transparent 65%), var(--muted); position: relative; display: block; overflow: hidden; aspect-ratio: var(--shader-aspect); background: var(--shader-poster, var(--shader-poster-default)); background-size: cover; &[hidden] { display: none; } & > .shader-canvas { position: absolute; inset: 0; display: block; width: 100%; height: 100%; opacity: 0; pointer-events: none; transition: opacity 0.6s ease; } &[data-ready]:not([data-fallback]) > .shader-canvas { opacity: 1; } &[data-fallback] > .shader-canvas { display: none; } /* the picture an effect reads: it sizes the box, stays for assistive technology, and gives way to the canvas */ &:has(> img, > svg, > video) { aspect-ratio: auto; background: none; } & > :is(img, svg, video) { display: block; max-width: 100%; height: auto; transition: opacity 0.6s ease; } /* an inline SVG has no width of its own: it takes the box's, and the box takes the room it is given */ &:has(> svg) { inline-size: 100%; & > svg { width: 100%; } } &[data-ready]:not([data-fallback], [data-image="blocked"]) > :is(img, svg, video) { opacity: 0; } & > script { display: none; } /* the posters of the effects with their own palettes - close to their first frame */ &[data-effect="warp"] { --shader-poster-default: radial-gradient(90% 80% at 30% 40%, #e0548c, transparent 70%), radial-gradient(80% 70% at 75% 70%, #ffb36b, transparent 70%), #2a1a5e; } &:is( [data-effect="neuro-noise"], [data-effect="god-rays"], [data-effect="aurora"], [data-effect="galaxy"], [data-effect="orb"], [data-effect="fluid"], [data-effect="trails"] ) { --shader-poster-default: radial-gradient(70% 60% at 50% 45%, color-mix(in oklch, #7a6cff 35%, transparent), transparent 70%), #06070f; } &[data-effect="hologram"] { --shader-poster-default: linear-gradient(115deg, #ff8fd8, #8fe3ff 35%, #b7ffcf 55%, #fff3a0 75%, #c9a8ff); } &:is([data-effect="liquid-metal"], [data-effect="pulsing-border"]) { --shader-poster-default: none; } &[data-effect="water"] { --shader-poster-default: linear-gradient(#18a0b4, #0a4d68); } &[data-effect="fluted-glass"] { --shader-poster-default: repeating-linear-gradient(90deg, rgb(255 255 255 / 0.18) 0 2px, transparent 2px 7.14%), linear-gradient(120deg, #ff5f6d, #ffc371 40%, #47cacc 70%, #5b5bd6); } &[data-effect="waves"] { --shader-poster-default: linear-gradient(#35548a, #f6c49c 48%, #0f7d8c 52%, #03253a); } &:is([data-effect="dot-orbit"], [data-effect="topography"], [data-effect="metaballs"], [data-effect="dithering"]) { --shader-poster-default: var(--background); } &[data-effect="plasma"] { --shader-poster-default: radial-gradient(70% 80% at 40% 30%, #ff006e, transparent 70%), radial-gradient(80% 80% at 80% 70%, #3a86ff, transparent 70%), #fb5607; } &:is([data-effect="bokeh"], [data-effect="starfield"]) { --shader-poster-default: radial-gradient(60% 60% at 50% 50%, color-mix(in oklch, #9b8cff 18%, transparent), transparent 70%), #07070f; } &[data-effect="ripple"] { --shader-poster-default: linear-gradient(#3fb8c7, #0d5c75); } &[data-effect="voronoi"] { --shader-poster-default: conic-gradient(from 30deg, #d1495b, #edae49, #00798c, #30638e, #7f5a83, #d1495b); } &[data-effect="clouds"] { --shader-poster-default: linear-gradient(#2f6fc4, #a9d1f5); } &[data-effect="fire"] { --shader-poster-default: linear-gradient(#0a0505 35%, #b3160b 70%, #ffb21f); } &[data-effect="silk"] { --shader-poster-default: linear-gradient(135deg, #2b0f3a, #7a2e8e 45%, #e07bb5 75%, #ffe3f1); } &[data-effect="nebula"] { --shader-poster-default: radial-gradient(circle at 40% 45%, #f72585 0, #3a0ca3 35%, #05030d 75%); } &[data-effect="prism"] { --shader-poster-default: linear-gradient( 170deg, #0b0b12 40%, #ff5f5f 46%, #ffd25f 48%, #5fff8a 50%, #5fc8ff 52%, #a05fff 54%, #0b0b12 60% ); } &[data-effect="matrix"] { --shader-poster-default: linear-gradient(#020a04, #0b3d1a); } &[data-effect="marble"] { --shader-poster-default: linear-gradient(120deg, #f4f1ec, #d9d4cc 60%, #f4f1ec); } &[data-effect="oil"] { --shader-poster-default: linear-gradient(120deg, #0a0a0f, #3b2a5c 40%, #1f5c5a 60%, #1d1b2a); } &[data-effect="op-art"] { --shader-poster-default: repeating-radial-gradient(circle, var(--foreground) 0 6px, var(--background) 6px 12px); } &:is([data-effect="lightning"], [data-effect="crt"]) { --shader-poster-default: linear-gradient(#070914, #141a33); } &[data-effect="snow"] { --shader-poster-default: linear-gradient(#6c8ead, #1b2a41); } &[data-effect="fireflies"] { --shader-poster-default: linear-gradient(#0b2e22, #03140d); } &[data-effect="glitch"] { --shader-poster-default: linear-gradient(90deg, #ff2e63, #08d9d6, #252a34, #eaeaea); } &[data-effect="caustics"] { --shader-poster-default: linear-gradient(#20a4c4, #0b4f6c); } &[data-effect="smoke"] { --shader-poster-default: linear-gradient(#26262e, #101014); } /* behind the content of its parent: the parent becomes the positioning and stacking context */ &[data-variant="background"] { position: absolute; inset: 0; z-index: -1; aspect-ratio: auto; border-radius: inherit; pointer-events: none; } /* a glow around its parent: the box reaches --shader-glow past the parent's edges, the inside stays clear */ &[data-variant="border"] { --shader-poster-default: none; position: absolute; inset: calc(-1 * var(--shader-glow)); z-index: -1; overflow: visible; aspect-ratio: auto; pointer-events: none; } } /* outside controls, generated from the effect: a list between a previous and a next button, sliders, colors */ .shader-controls { display: grid; gap: 0.75rem; align-content: start; min-inline-size: 0; & .shader-controls-field { display: grid; gap: 0.375rem; min-inline-size: 0; } & .shader-controls-head { display: flex; align-items: baseline; justify-content: space-between; gap: 0.5rem; } & .slider-value { font-family: var(--font-mono); font-size: 0.75rem; color: var(--muted-foreground); font-variant-numeric: tabular-nums; } & .shader-controls-cycle { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 0.25rem; align-items: center; & .select { inline-size: 100%; min-inline-size: 0; } } & .shader-controls-colors { display: flex; flex-wrap: wrap; gap: 0.375rem; & input[type="color"] { inline-size: 2rem; block-size: 2rem; padding: 0.125rem; border: 1px solid var(--input); border-radius: var(--radius-sm); background: var(--background); cursor: pointer; &:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; } } } & .shader-controls-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; } } :where(:has(> .shader:is([data-variant="background"], [data-variant="border"]))) { position: relative; isolation: isolate; } @media (prefers-reduced-motion: reduce) { .shader > .shader-canvas, .shader > :is(img, svg, video) { transition: none; } } /* more contrast: no moving picture behind text */ @media (prefers-contrast: more) { .shader[data-variant="background"] { display: none; } } @media (forced-colors: active) { .shader { background: Canvas; & > .shader-canvas { display: none; } & > :is(img, svg, video) { opacity: 1; } &:is([data-variant="background"], [data-variant="border"]) { display: none; } } }}§JS view file
// -- Shader -----------------------------------------------------// Animated WebGL2 effects - gradients, light, liquid, patterns and small 3D// scenes - inside or behind any element. ONE WebGL2 context per page draws// every instance and copies its frame into the instance's own 2D canvas: a// page of twenty effects never meets the browser's context limit, a program// compiles once per page, and each instance stays an ordinary box in the// layout (clipped, masked, stacked, rounded like any element). Instances draw// only near the viewport and while the tab shows, hold one still frame under// reduced motion, and keep their CSS poster where WebGL2 is missing. Every// shader here is written for this system (no third-party shader code). Plus// the named-state API (AGENTS.md "State API").// Shared preamble (AGENTS.md "State API"); the implementation lives in core.js -// build.ts rewrites this import into a df$.shadcn.shared binding in dist/.import { defussGlobals, defussQuery, componentState, bindComponent } from '../../../shared/state-api.js';const df$ = defussGlobals();const dfDollar = defussQuery();// VERIFIED: (verify's component types ratchet - tsc -p tsconfig.components.json) every type// this file's API docs state - arguments, return values, event details - holds against its code./** What shader-ready carries. */interface ShaderReadyDetail { /** the effect on the canvas */ effect: string; /** the canvas width in device pixels */ width: number; /** the canvas height in device pixels */ height: number; /** device pixels per CSS pixel the effect renders at (quality and the adaptive scale) */ pixelRatio: number;}/** What shader-error carries. */interface ShaderErrorDetail { /** the effect that cannot run */ effect: string; /** why: no WebGL2, an unknown effect, a GLSL compile or link error (the driver's log) */ message: string;}/** An effect: a fragment shader and its defaults - df$.shadcn.shader.register() adds one. */export interface ShaderEffect { /** GLSL ES 3.00 - the body after the prelude (its uniforms and helpers come first), or a whole shader starting with #version */ fragment: string; /** numeric parameters, name to default: the shader declares `uniform float u_<name>`, the element sets it with data-<name> (underscores written as dashes) */ params?: Record<string, number>; /** the default colors, any CSS color - var(--chart-1) follows the theme */ colors?: string[]; /** the default background color (transparent when omitted) */ background?: string; /** seconds on the effect's clock for the still frame - the first frame, and the only one under reduced motion */ still?: number; /** 'mask': a source image is the shape, contained in the box; 'image': the effect samples the picture, covering the box */ image?: 'mask' | 'image'; /** true: follows the pointer without data-interactive */ interactive?: boolean; /** resolution factor for the costly ones (0.5 - 1) */ cost?: number; /** true: the colors are levels, ordered from dark to light */ sortColors?: boolean; /** named choices, name to the words it accepts: the shader declares `uniform float u_<name>`, data-<name> picks a word and the uniform holds its index (the first word is the default) */ options?: Record<string, string[]>; /** the slider range of a parameter in generated controls, name to [min, max, step] (default: a range around the default value) */ ranges?: Record<string, [number, number, number]>; /** 'fxaa': an anti-aliasing pass over every frame (data-aa overrides it) */ aa?: 'fxaa' | 'none'; /** 0 - 0.95: how much of the last frame each frame keeps where the shader marks detail finer than a pixel (its alpha) - temporal smoothing for opaque effects (data-smooth overrides it) */ temporal?: number;}/** What df$.shadcn.shader.stats() reports - the engine as it is now. */interface ShaderStats { /** false once WebGL2 turned out to be missing */ supported: boolean; /** WebGL contexts the page holds: 0 before the first visible effect (and after a long time with none in view), else 1 */ contexts: number; /** programs compiled in that context */ programs: number; /** bound .shader elements */ instances: number; /** of those, in the viewport */ visible: number; /** of those, running their clock now */ animating: number; /** frames drawn since the page loaded, every instance counted */ frames: number; /** the adaptive resolution factor: 1 (full quality), 0.75 or 0.5 while frames run late */ scale: number;}const shaderStates = ['default', 'paused'];/** setState() configs per state - an omitted key keeps the element's value (the configs merge). */export interface ShaderStateConfigs { /** Running - the effect's clock moves (under reduced motion it holds its still frame). */ default: { /** the effect to show, a name from df$.shadcn.shader.effects() - written to data-effect */ effect?: string; /** the clock rate, 1 = the effect's own pace - written to data-speed */ speed?: number; /** the colors, CSS colors separated by spaces - written to data-colors */ colors?: string; }; /** Frozen on one frame - the clock stops; the pointer and a resize still redraw it. */ paused: { /** the effect to show - written to data-effect */ effect?: string; /** the clock rate - written to data-speed */ speed?: number; /** the colors - written to data-colors */ colors?: string; /** seconds on the effect's clock to freeze at (default: where it is) */ time?: number; };}// ---------------------------------------------------------------------------// GLSL - the prelude every effect starts with: the uniform contract, noise,// color in Oklab, small helpers. Uniforms an effect leaves unused cost nothing.// ---------------------------------------------------------------------------const VERTEX = `#version 300 esin vec2 a_position;out vec2 v_uv;void main() { v_uv = a_position * .5 + .5; gl_Position = vec4(a_position, 0., 1.); }`;const PRELUDE = `#version 300 esprecision highp float;precision highp int;uniform vec2 u_resolution;uniform float u_time;uniform float u_delta;uniform vec2 u_pointer;uniform float u_hover;uniform vec3 u_colors[8];uniform int u_color_count;uniform vec4 u_background;uniform sampler2D u_image;uniform float u_has_image;uniform float u_seed;uniform float u_pixel_ratio;uniform vec2 u_frame;uniform vec3 u_taps[8];uniform int u_tap_count;uniform float u_temporal;in vec2 v_uv;out vec4 fragColor;#define PI 3.14159265#define TAU 6.28318531uint ihash(uint x) { x ^= x >> 16; x *= 0x7feb352du; x ^= x >> 15; x *= 0x846ca68bu; x ^= x >> 16; return x; }uint cellHash(vec2 p) { uvec2 q = uvec2(ivec2(floor(p))); return ihash(q.x + ihash(q.y + ihash(uint(u_seed) + 7u))); }float hash12(vec2 p) { return float(cellHash(p)) / 4294967295.; }vec2 hash22(vec2 p) { uint h = cellHash(p); return vec2(float(h), float(ihash(h))) / 4294967295.; }vec3 hash33(vec3 p) { uvec3 q = uvec3(ivec3(floor(p))); uint h = ihash(q.x + ihash(q.y + ihash(q.z + ihash(uint(u_seed) + 7u)))); uint h2 = ihash(h); return vec3(float(h), float(h2), float(ihash(h2))) / 4294967295.;}float gnoise(vec2 p) { vec2 i = floor(p), f = fract(p); vec2 u = f * f * f * (f * (f * 6. - 15.) + 10.); float a = dot(hash22(i) * 2. - 1., f); float b = dot(hash22(i + vec2(1., 0.)) * 2. - 1., f - vec2(1., 0.)); float c = dot(hash22(i + vec2(0., 1.)) * 2. - 1., f - vec2(0., 1.)); float d = dot(hash22(i + 1.) * 2. - 1., f - 1.); return mix(mix(a, b, u.x), mix(c, d, u.x), u.y) * 1.5;}float gnoise3(vec3 p) { vec3 i = floor(p), f = fract(p); vec3 u = f * f * f * (f * (f * 6. - 15.) + 10.); float n000 = dot(hash33(i) * 2. - 1., f); float n100 = dot(hash33(i + vec3(1., 0., 0.)) * 2. - 1., f - vec3(1., 0., 0.)); float n010 = dot(hash33(i + vec3(0., 1., 0.)) * 2. - 1., f - vec3(0., 1., 0.)); float n110 = dot(hash33(i + vec3(1., 1., 0.)) * 2. - 1., f - vec3(1., 1., 0.)); float n001 = dot(hash33(i + vec3(0., 0., 1.)) * 2. - 1., f - vec3(0., 0., 1.)); float n101 = dot(hash33(i + vec3(1., 0., 1.)) * 2. - 1., f - vec3(1., 0., 1.)); float n011 = dot(hash33(i + vec3(0., 1., 1.)) * 2. - 1., f - vec3(0., 1., 1.)); float n111 = dot(hash33(i + 1.) * 2. - 1., f - 1.); return mix(mix(mix(n000, n100, u.x), mix(n010, n110, u.x), u.y), mix(mix(n001, n101, u.x), mix(n011, n111, u.x), u.y), u.z) * 1.4;}float fbm(vec2 p, int octaves) { float s = 0., a = .5; for (int i = 0; i < 8; i++) { if (i >= octaves) break; s += a * gnoise(p); p = mat2(1.6, 1.2, -1.2, 1.6) * p + 7.13; a *= .5; } return s;}mat2 rot(float a) { float c = cos(a), s = sin(a); return mat2(c, -s, s, c); }float aspect() { return u_resolution.x / u_resolution.y; }vec2 centered() { return (v_uv - .5) * vec2(aspect(), 1.); }vec3 srgbToLinear(vec3 c) { return mix(c / 12.92, pow((c + .055) / 1.055, vec3(2.4)), step(.04045, c)); }vec3 linearToSrgb(vec3 c) { c = max(c, 0.); return mix(c * 12.92, 1.055 * pow(c, vec3(1. / 2.4)) - .055, step(.0031308, c)); }vec3 toOklab(vec3 srgb) { vec3 c = srgbToLinear(srgb); vec3 lms = vec3(dot(c, vec3(.4122214708, .5363325363, .0514459929)), dot(c, vec3(.2119034982, .6806995451, .1073969566)), dot(c, vec3(.0883024619, .2817188376, .6299787005))); lms = sign(lms) * pow(abs(lms), vec3(1. / 3.)); return vec3(dot(lms, vec3(.2104542553, .7936177850, -.0040720468)), dot(lms, vec3(1.9779984951, -2.4285922050, .4505937099)), dot(lms, vec3(.0259040371, .7827717662, -.8086757660)));}vec3 fromOklab(vec3 lab) { vec3 lms = vec3(dot(lab, vec3(1., .3963377774, .2158037573)), dot(lab, vec3(1., -.1055613458, -.0638541728)), dot(lab, vec3(1., -.0894841775, -1.2914855480))); lms = lms * lms * lms; return linearToSrgb(vec3(dot(lms, vec3(4.0767416621, -3.3077115913, .2309699292)), dot(lms, vec3(-1.2684380046, 2.6097574011, -.3413193965)), dot(lms, vec3(-.0041960863, -.7034186147, 1.7076147010))));}vec3 mixOk(vec3 a, vec3 b, float t) { return fromOklab(mix(toOklab(a), toOklab(b), t)); }int colorCount() { return max(u_color_count, 1); }vec3 color(int i) { return u_colors[clamp(i, 0, colorCount() - 1)]; }vec3 ramp(float t) { int n = colorCount(); float x = clamp(t, 0., 1.) * float(n - 1); int i = int(min(floor(x), float(n - 1))); return mixOk(color(i), color(i + 1), x - float(i));}vec3 rampCycle(float t) { int n = colorCount(); float x = fract(t) * float(n); int i = int(floor(x)); return mixOk(color(i), color((i + 1) % n), x - float(i));}vec3 dither(vec3 c) { return c + (hash12(gl_FragCoord.xy) - .5) / 255.; }vec4 image(vec2 uv) { return texture(u_image, uv); }vec4 backdrop() { return vec4(u_background.rgb * u_background.a, u_background.a); }vec4 over(vec3 c, float a) { return vec4(c * a, a) + backdrop() * (1. - a); }float sdRoundBox(vec2 p, vec2 b, float r) { vec2 q = abs(p) - b + r; return length(max(q, 0.)) + min(max(q.x, q.y), 0.) - r; }`;const THEME = ['var(--chart-1)', 'var(--chart-2)', 'var(--chart-3)', 'var(--chart-4)', 'var(--chart-5)'];/** The built-in effects. Their GLSL follows the prelude; every `u_<param>` is a data-<param> attribute. */const EFFECTS: Record<string, ShaderEffect> = { // -- gradients and noise -------------------------------------------------- 'mesh-gradient': { params: { distortion: 0.8, swirl: 0.35, grain: 0 }, colors: THEME, still: 3.5, fragment: `uniform float u_distortion, u_swirl, u_grain;void main() { float a = aspect(), t = u_time * .18; vec2 p = v_uv * vec2(a, 1.); vec2 q = p + u_distortion * .22 * vec2(gnoise(p * 1.3 + vec2(t, -t * .8)), gnoise(p * 1.3 + vec2(-t * .7, t) + 5.2)); vec2 c = vec2(a * .5, .5), d = q - c; q = c + rot(u_swirl * 2.2 * exp(-dot(d, d) * 3.) * sin(t * 1.3)) * d; vec3 lab = vec3(0.); float wsum = 0.; int n = colorCount(); for (int i = 0; i < 8; i++) { if (i >= n) break; float f = float(i); vec2 pt = vec2(a * (.5 + .38 * sin(t * (.9 + .17 * f) + f * 2.4)), .5 + .38 * cos(t * (.7 + .13 * f) + f * 1.9)); float w = 1. / pow(length(q - pt) + .04, 3.); lab += toOklab(u_colors[i]) * w; wsum += w; } vec3 col = fromOklab(lab / wsum); col += (hash12(gl_FragCoord.xy + floor(u_time * 24.) * 37.) - .5) * u_grain * .25; fragColor = vec4(dither(col), 1.);}`, }, 'grain-gradient': { params: { softness: 0.8, grain: 0.5, zoom: 1 }, colors: ['var(--background)', 'var(--chart-1)', 'var(--chart-3)', 'var(--chart-5)'], still: 2, fragment: `uniform float u_softness, u_grain, u_zoom;void main() { float t = u_time * .12; vec2 p = centered() * 2.2 / max(u_zoom, .05); float f = .5 + .45 * fbm(p * .6 + vec2(t * .6, -t * .4), 3); f = mix(f, 1.05 - v_uv.y + .14 * sin(v_uv.x * 3. + t * 2.) + .08 * sin(v_uv.x * 7. - t * 3.), .62); f += (hash12(gl_FragCoord.xy + floor(u_time * 18.) * 41.) - .5) * u_grain * .55; int n = colorCount(); float x = clamp(f, 0., 1.) * float(n - 1); int i = int(min(floor(x), float(n) - 1.5)); float s = max(u_softness, .001) * .5; vec3 col = mixOk(color(i), color(i + 1), smoothstep(.5 - s, .5 + s, x - float(i))); col *= 1. + (hash12(gl_FragCoord.xy * 1.7 + 11.) - .5) * u_grain * .3; fragColor = vec4(dither(col), 1.);}`, }, warp: { params: { zoom: 1, warp: 1, contrast: 1.15 }, colors: ['#0f0c29', '#4b2a8c', '#e0548c', '#ffb36b', '#fff4d6'], still: 6, fragment: `uniform float u_zoom, u_warp, u_contrast;void main() { float t = u_time * .06; vec2 p = centered() * 3. / max(u_zoom, .05); vec2 q = vec2(fbm(p + vec2(0., t), 5), fbm(p + vec2(5.2, 1.3) - t, 5)); vec2 r = vec2(fbm(p + u_warp * 3. * q + vec2(1.7, 9.2) + t * 1.5, 5), fbm(p + u_warp * 3. * q + vec2(8.3, 2.8) - t, 5)); float f = fbm(p + u_warp * 3. * r, 5); float x = clamp((.5 + .9 * f - .5) * max(u_contrast, 0.) + .5, 0., 1.); vec3 col = ramp(x); col = mixOk(col, color(colorCount() - 1), clamp(dot(r, r) * .5, 0., .45)); col *= .82 + .3 * clamp(length(q), 0., 1.); fragColor = vec4(dither(col), 1.);}`, }, 'neuro-noise': { params: { zoom: 1, brightness: 1 }, colors: ['#3d5afe', '#7cf8ff', '#ffffff'], background: '#04050d', still: 4, fragment: `uniform float u_zoom, u_brightness;void main() { vec2 p = centered() * 1.6 / max(u_zoom, .05); float t = u_time * .35; vec2 acc = vec2(0.), res = vec2(0.); float s = 6.; for (int j = 0; j < 14; j++) { p = rot(1.) * p; acc = rot(1.) * acc; vec2 layer = p * s + float(j) + acc - t; acc += sin(layer); res += (.5 + .5 * cos(layer)) / s; s *= 1.2; } float n = res.x + res.y; float glow = pow(max(n - .5, 0.) * 1.9, 2.6) * u_brightness * 1.6; glow *= smoothstep(1.15, .15, length(centered() * vec2(.9, 1.2))); vec3 col = mix(u_background.rgb, color(0), clamp(glow * 1.6, 0., 1.)); col = mix(col, color(1), clamp(glow - .45, 0., 1.)); col = mix(col, color(2), clamp(glow - 1.2, 0., 1.)); fragColor = over(dither(col), max(clamp(glow * 1.6, 0., 1.), u_background.a));}`, }, dithering: { params: { pixel: 3, reveal: 0.18, contrast: 1.2 }, colors: ['var(--foreground)', 'var(--background)'], sortColors: true, image: 'image', still: 2.5, fragment: `uniform float u_pixel, u_reveal, u_contrast;float bayer8(vec2 cell) { ivec2 c = ivec2(mod(cell, 8.)); int x = c.x ^ c.y, y = c.y; int v = ((x & 1) << 5) | ((y & 1) << 4) | ((x & 2) << 2) | ((y & 2) << 1) | ((x & 4) >> 1) | ((y & 4) >> 2); return (float(v) + .5) / 64.;}float scene(vec2 uv) { if (u_has_image > .5) return dot(image(uv).rgb, vec3(.2126, .7152, .0722)); float t = u_time * .15; vec2 p = (uv - .5) * vec2(aspect(), 1.); float f = .5 + .55 * fbm(p * 2.2 + vec2(t, t * .7), 4); vec2 c = .28 * vec2(sin(t * 1.3), cos(t)); return clamp(mix(f, 1.1 - length(p - c) * 1.5, .55), 0., 1.);}void main() { float px = max(u_pixel, 1.) * u_pixel_ratio; vec2 cell = floor(gl_FragCoord.xy / px); vec2 uv = (cell + .5) * px / u_resolution; float lum = clamp((scene(uv) - .5) * max(u_contrast, 0.) + .5, 0., 1.); int n = max(colorCount(), 2); float x = lum * float(n - 1); int i = int(min(floor(x), float(n) - 1.5)); vec3 dithered = color(x - float(i) > bayer8(cell) ? i + 1 : i); float d = length((v_uv - u_pointer) * vec2(aspect(), 1.)); float show = u_hover * (1. - smoothstep(u_reveal * .75, u_reveal, d)); vec3 original = u_has_image > .5 ? image(v_uv).rgb : ramp(clamp((scene(v_uv) - .5) * u_contrast + .5, 0., 1.)); fragColor = vec4(mix(dithered, original, show), 1.);}`, }, // -- light and glow ------------------------------------------------------- 'god-rays': { params: { origin_x: 0.5, origin_y: 1.08, density: 1, intensity: 1, spread: 1 }, colors: ['#fff3d1', '#ffd08a', '#ffe7ba', '#c9dcff'], background: '#0a0b12', still: 2, fragment: `uniform float u_origin_x, u_origin_y, u_density, u_intensity, u_spread;float rayNoise(float ang, float freq, float t) { return .5 + .5 * gnoise(vec2(cos(ang), sin(ang)) * freq + vec2(t, -t * .7)); }void main() { float a = aspect(), t = u_time * .2; vec2 o = mix(vec2(u_origin_x * a, u_origin_y), vec2(u_pointer.x * a, u_pointer.y), u_hover * .85); vec2 d = v_uv * vec2(a, 1.) - o; float r = length(d), ang = atan(d.y, d.x); float dens = max(u_density, .1); float rays = pow(rayNoise(ang, 3. * dens, t), 3.) * .9 + pow(rayNoise(ang, 7. * dens, -t * 1.3 + 3.), 4.) * .7 + pow(rayNoise(ang, 15. * dens, t * 1.7 + 7.), 5.) * .5; float fall = exp(-r * 1.1 / max(u_spread, .05)); float core = exp(-r * r * 9.); float light = (rays * fall * 1.5 + core * 1.1) * u_intensity; vec3 tint = rampCycle(ang / TAU * 2. + t * .05); vec3 glow = tint * light; vec4 bg = backdrop(); vec3 col = bg.rgb + glow * (1. - .35 * bg.rgb); float alpha = max(bg.a, clamp(max(glow.r, max(glow.g, glow.b)), 0., 1.)); fragColor = vec4(min(dither(col), vec3(alpha)), alpha);}`, }, aurora: { params: { intensity: 1, stars: 1, height: 1 }, colors: ['#3cffb0', '#25c9ff', '#8b5cff', '#ff6fb5'], background: '#040714', still: 6, fragment: `uniform float u_intensity, u_stars, u_height;void main() { float a = aspect(), t = u_time * .08; vec2 uv = v_uv, p = uv * vec2(a, 1.); vec3 bg = u_background.rgb; vec3 col = mix(bg * 1.8 + .015, bg * .7, uv.y); vec2 sc = gl_FragCoord.xy / u_pixel_ratio / 3.; float h = hash12(sc); float star = step(.996, h) * (.55 + .45 * sin(u_time * 2. + hash12(sc + 17.) * 40.)); col += star * smoothstep(.25, .95, uv.y) * u_stars * .8; vec3 light = vec3(0.); for (int i = 0; i < 3; i++) { float fi = float(i); float base = (.34 + fi * .11) / max(u_height, .3) + .1 * fbm(vec2(p.x * .5 + fi * 3.7, t * .7 + fi), 3); float x = p.x * (1.1 + fi * .35) + t * (1. + fi * .4) + fi * 10.; float y0 = base + .07 * sin(x * 1.3 + t * 3.) + .05 * gnoise(vec2(x * 1.6, t * 2.)); float above = uv.y - y0; float band = smoothstep(-.012, .025, above) * exp(-max(above, 0.) * (4.2 - fi * .8)); float rays = .35 + .65 * pow(.5 + .5 * gnoise(vec2(x * 11., t * 4. + fi * 5.)), 2.); vec3 c = mixOk(color(i), color(i + 1), clamp(above * 2.5, 0., 1.)); light += c * band * rays * (1. - fi * .22); } col += light * u_intensity * .9; col += light * light * .25 * u_intensity; fragColor = vec4(dither(col), 1.);}`, }, 'pulsing-border': { params: { thickness: 2, softness: 1, spots: 3, pulse: 0.6 }, colors: ['var(--chart-1)', 'var(--chart-2)', 'var(--chart-4)', 'var(--chart-3)'], still: 1.2, fragment: `uniform float u_thickness, u_softness, u_spots, u_pulse;void main() { vec2 fc = v_uv * u_resolution, c = u_resolution * .5; vec2 half_ = max(c - u_frame.x, vec2(1.)); float d = sdRoundBox(fc - c, half_, min(u_frame.y, min(half_.x, half_.y))); float ang = atan(fc.y - c.y, (fc.x - c.x) * u_resolution.y / u_resolution.x); float t = u_time; float travel = pow(.5 + .5 * cos(ang * max(floor(u_spots), 1.) - t * 1.6), 4.); float pulse = 1. - u_pulse * .45 * (.5 - .5 * sin(t * 2.4)); float th = max(u_thickness, .5) * u_pixel_ratio; float line = 1. - smoothstep(th * .5, th * .5 + u_pixel_ratio, abs(d)); float glowR = max(u_frame.x * .7 * max(u_softness, .05), u_pixel_ratio); // outside, the glow reaches zero before the canvas ends (u_frame.x past the frame) - exp alone left about a quarter of // it at the edge, a hard rectangle around every pulsing border float glow = d > 0. ? exp(-d / glowR) * (1. - smoothstep(u_frame.x * .5, u_frame.x, d)) : exp(d / (glowR * .35)); float energy = (.35 + .9 * travel) * pulse; vec3 col = rampCycle(ang / TAU + t * .04); float alpha = clamp(line * (.8 + .2 * travel) + glow * energy * .9, 0., 1.); vec3 lit = mix(col, vec3(1.), line * travel * .35); fragColor = over(lit, alpha);}`, }, hologram: { params: { intensity: 0.85, scanlines: 0.6, sparkle: 1 }, colors: ['#ff8fd8', '#8fe3ff', '#b7ffcf', '#fff3a0', '#c9a8ff'], background: '#1a1726', image: 'image', still: 1.5, fragment: `uniform float u_intensity, u_scanlines, u_sparkle;void main() { float t = u_time * .3; vec2 p = centered(); vec2 tilt = mix(vec2(sin(t * 1.1), cos(t * .8)) * .4, (u_pointer - .5) * 1.4, u_hover); vec3 base = u_has_image > .5 ? image(v_uv).rgb : mix(u_background.rgb * .7, u_background.rgb * 1.6 + .03, v_uv.y); float bands = dot(p, vec2(.7, .45)) * 2.4 + tilt.x * 1.2 - tilt.y * .8 + .2 * gnoise(p * 3. + t); vec3 foil = rampCycle(bands); float sheen = exp(-pow((dot(p, normalize(vec2(1., -.6))) - tilt.x * .7 + tilt.y * .35) * 2.6, 2.)); vec2 sp = gl_FragCoord.xy / u_pixel_ratio / 14.; vec2 sf = fract(sp) - .5; float sh = hash12(sp); float glint = max(1. - length(sf) * 2.2, 0.); glint = pow(glint, 4.) + .7 * (exp(-abs(sf.x) * 40.) + exp(-abs(sf.y) * 40.)) * glint; float sparkle = step(.93, sh) * glint * pow(.5 + .5 * sin(u_time * 2.5 + sh * 60. + tilt.x * 14.), 6.); float lum = dot(base, vec3(.299, .587, .114)); vec3 col = base * (1. - .15 * u_intensity); vec3 film = foil * (.22 + .6 * sheen) * u_intensity * (.45 + .4 * lum); col = 1. - (1. - col) * (1. - film); col += foil * sheen * .25 * u_intensity; col += sparkle * u_sparkle * 1.4; col *= 1. - u_scanlines * .1 * (.5 + .5 * sin(gl_FragCoord.y / u_pixel_ratio * 2.2 + u_time * 4.)); fragColor = vec4(dither(clamp(col, 0., 1.)), 1.);}`, }, // -- liquid and glass ----------------------------------------------------- metaballs: { params: { count: 7, size: 1, gloss: 1 }, colors: THEME, background: 'var(--background)', still: 3, fragment: `uniform float u_count, u_size, u_gloss;void main() { float a = aspect(), t = u_time * .35; vec2 p = v_uv * vec2(a, 1.); float field = 0.; vec3 lab = vec3(0.); int n = int(clamp(u_count, 1., 16.)); for (int i = 0; i < 16; i++) { if (i >= n) break; float f = float(i); vec2 c = vec2(a * (.5 + .28 * sin(t * (.6 + .11 * f) + f * 1.7)), .5 + .27 * sin(t * (.5 + .13 * f) + f * 2.9 + 1.)); float r = (.1 + .055 * (.5 + .5 * sin(f * 3.7))) * u_size * (1. + .12 * sin(t * 2. + f)); vec2 d = p - c; float k = r * r / (dot(d, d) + r * r * .08); field += k; lab += toOklab(color(i % colorCount())) * k; } if (u_hover > .001) { vec2 d = p - u_pointer * vec2(a, 1.); float r = .13 * u_size, k = r * r / (dot(d, d) + r * r * .08) * u_hover; field += k; lab += toOklab(color(0)) * k; } vec3 col = fromOklab(lab / max(field, 1e-5)); float aa = fwidth(field) * 1.2; float inside = smoothstep(1. - aa, 1. + aa, field); float h = clamp(1. - 1. / max(field, 1e-3), 0., 1.); vec3 nrm = normalize(vec3(-vec2(dFdx(h), dFdy(h)) * u_resolution.y * .05, 1.)); vec3 L = normalize(vec3(-.45, .55, .7)); float spec = pow(max(dot(reflect(-L, nrm), vec3(0., 0., 1.)), 0.), 28.) * u_gloss; col = col * (.74 + .26 * dot(nrm, L)) + spec * .5; fragColor = over(dither(col), inside);}`, }, 'liquid-metal': { params: { repetition: 3, distortion: 0.6, bevel: 1, shift: 0 }, colors: ['#fbfcfd', '#a7adb6', '#23262c', '#d9dee5', '#646a74', '#f3f5f8'], image: 'mask', still: 2, fragment: `uniform float u_repetition, u_distortion, u_bevel, u_shift;// a picture's relief: its alpha blurred over two rings - high inside, falling smoothly to the edgefloat relief(vec2 uv) { float acc = 0., rad = .02 + .025 * u_bevel; for (int i = 0; i < 8; i++) { float an = float(i) * TAU / 8. + .2; vec2 o = vec2(cos(an) / aspect(), sin(an)) * rad; acc += image(uv + o).a + image(uv + o * .45).a; } return smoothstep(0., 1., acc / 16.);}float height(vec2 uv, out float mask) { if (u_has_image > .5) { mask = image(uv).a; return relief(uv); } vec2 p = centered(); float t = u_time * .2; float d = pow(pow(abs(p.x), 4.) + pow(abs(p.y), 4.), .25) - .32 + .012 * gnoise(p * 3. + t); float px = 1.5 / u_resolution.y; mask = smoothstep(px, -px, d); return smoothstep(0., .16 * u_bevel + .02, -d);}vec3 chrome(float s) { int n = colorCount(); float x = fract(s) * float(n); int i = int(floor(x)); return mixOk(color(i), color((i + 1) % n), smoothstep(.12, .88, x - float(i)));}void main() { float mask; float h = height(v_uv, mask); vec2 g = vec2(dFdx(h), dFdy(h)); if (u_has_image > .5) { // central differences: smooth where dFdx would step per pixel quad vec2 e = 1.5 / u_resolution; g = vec2(relief(v_uv + vec2(e.x, 0.)) - relief(v_uv - vec2(e.x, 0.)), relief(v_uv + vec2(0., e.y)) - relief(v_uv - vec2(0., e.y))) / 3.; } g *= u_resolution.y * .03 * u_bevel; vec3 n = normalize(vec3(-g, 1.)); float t = u_time * .25 + u_shift; vec2 p = centered(); float flow = u_distortion * (.25 * gnoise(p * 2.2 + vec2(t * .8, -t * .5)) + .12 * gnoise(p * 5. - t)); float s = ((p.y * .9 + p.x * .35) + flow) * u_repetition + n.x * 1.4 + n.y * 2.2 - t; float ca = .05 * (1. - n.z) * 6.; vec3 col = vec3(chrome(s + ca).r, chrome(s).g, chrome(s - ca).b); col = mix(col, vec3(1.), pow(1. - n.z, 2.) * .25); fragColor = over(dither(col), mask);}`, }, water: { params: { caustics: 0.8, refraction: 0.5, zoom: 1 }, colors: ['#0a4d68', '#18a0b4', '#e6fbff'], image: 'image', still: 3, fragment: `uniform float u_caustics, u_refraction, u_zoom;float waveH(vec2 x, float t) { return gnoise(x + vec2(t * .35, t * .2)) * .6 + gnoise(x * 2.1 + vec2(-t * .3, t * .45)) * .3 + gnoise(x * 4.3 + t * .6) * .1; }float cellEdge(vec2 x, float t) { vec2 i = floor(x), f = fract(x); float f1 = 8., f2 = 8.; for (int y = -1; y <= 1; y++) for (int xx = -1; xx <= 1; xx++) { vec2 o = vec2(float(xx), float(y)); vec2 h = hash22(i + o); vec2 r = o + .5 + .4 * sin(t * .6 + TAU * h) - f; float d = dot(r, r); if (d < f1) { f2 = f1; f1 = d; } else if (d < f2) f2 = d; } return sqrt(f2) - sqrt(f1);}void main() { float t = u_time; vec2 p = centered() * 3. / max(u_zoom, .05); float e = .02; vec2 g = vec2(waveH(p + vec2(e, 0.), t) - waveH(p - vec2(e, 0.), t), waveH(p + vec2(0., e), t) - waveH(p - vec2(0., e), t)) / (2. * e); vec2 uv = v_uv + g * u_refraction * .02; vec3 base = u_has_image > .5 ? image(uv).rgb : mixOk(color(0), color(1), smoothstep(-.3, 1.2, uv.y + .12 * g.x)); float c1 = pow(1. - smoothstep(0., .32, cellEdge(p * 1.6 + g * .35, t)), 3.); float c2 = pow(1. - smoothstep(0., .28, cellEdge(p * 2.7 - g * .25 + 3., t * 1.2)), 3.); float caus = (c1 + c2 * .6) * u_caustics; vec3 col = base * (.82 + .3 * caus) + color(2) * caus * .32; col += .05 * dot(g, vec2(.6, .8)); fragColor = vec4(dither(col), 1.);}`, }, 'fluted-glass': { params: { flutes: 14, distortion: 0.7, angle: 0, shade: 0.6 }, colors: ['#ff5f6d', '#ffc371', '#47cacc', '#5b5bd6'], background: '#f2ede6', image: 'image', still: 2, fragment: `uniform float u_flutes, u_distortion, u_angle, u_shade;vec3 behind(vec2 uv) { if (u_has_image > .5) return image(uv).rgb; float a = aspect(), t = u_time * .2; vec2 p = uv * vec2(a, 1.); vec3 lab = vec3(0.); float ws = 0.; int n = colorCount(); for (int i = 0; i < 8; i++) { if (i >= n) break; float f = float(i); vec2 c = vec2(a * (.5 + .4 * sin(t * (.7 + .2 * f) + f * 2.)), .5 + .4 * cos(t * (.6 + .15 * f) + f * 1.3)); float w = 1. / pow(length(p - c) + .05, 2.5); lab += toOklab(u_colors[i]) * w; ws += w; } return fromOklab(lab / max(ws, 1e-4));}void main() { float a = aspect(), ang = u_angle * PI / 180.; vec2 dir = vec2(cos(ang), sin(ang)); vec2 p = v_uv * vec2(a, 1.); float count = max(u_flutes, 1.); float cell = fract(dot(p, dir) * count / a) - .5; float w = a / count; vec2 off = dir * (-cell * w * u_distortion * 1.6); vec2 uv = v_uv + vec2(off.x / a, off.y); vec2 ca = dir * .004 * u_distortion; vec3 col = vec3(behind(uv + vec2(ca.x / a, ca.y)).r, behind(uv).g, behind(uv - vec2(ca.x / a, ca.y)).b); col *= 1. - u_shade * pow(abs(cell) * 2., 3.) * .55; col += u_shade * .16 * smoothstep(.16, 0., abs(cell + .22)); fragColor = vec4(dither(col), 1.);}`, }, fluid: { params: { curl: 24, dissipation: 0.45, radius: 0.4, shading: 0.6 }, colors: ['#ff4d6d', '#ffb703', '#3a86ff', '#8338ec', '#06d6a0'], background: '#07070c', interactive: true, still: 0, fragment: '', }, // -- patterns and trails -------------------------------------------------- 'dot-orbit': { params: { spacing: 26, size: 0.5, orbit: 0.4 }, colors: THEME, background: 'var(--background)', still: 2, fragment: `uniform float u_spacing, u_size, u_orbit;void main() { float sp = max(u_spacing, 4.) * u_pixel_ratio; vec2 fc = gl_FragCoord.xy / sp; vec2 cell = floor(fc), f = fract(fc) - .5; vec2 mid = u_resolution / sp * .5, ptr = u_pointer * u_resolution / sp; float t = u_time, aa = 1.2 / sp; vec4 col = backdrop(); for (int y = -1; y <= 1; y++) for (int x = -1; x <= 1; x++) { vec2 o = vec2(float(x), float(y)), id = cell + o; float h = hash12(id), h2 = hash12(id + 31.7); float ang = t * (.4 + h) * (h2 > .5 ? 1. : -1.) + h * TAU; vec2 c = o + u_orbit * .5 * vec2(cos(ang), sin(ang)); float wave = .5 + .5 * sin(t * 1.2 - length(id - mid) * .32 + h * 1.5); float r = u_size * .5 * (.3 + .7 * wave); r *= 1. + u_hover * .9 * exp(-dot(id - ptr, id - ptr) * .03); float m = 1. - smoothstep(-aa, aa, length(f - c) - r); col = mix(col, vec4(ramp(fract(h + length(id - mid) * .02 - t * .02)), 1.), m); } fragColor = vec4(dither(col.rgb), col.a);}`, }, topography: { params: { lines: 12, thickness: 1, zoom: 1, relief: 0.8 }, colors: ['var(--chart-1)', 'var(--chart-3)', 'var(--chart-5)'], background: 'var(--background)', still: 4, fragment: `uniform float u_lines, u_thickness, u_zoom, u_relief;void main() { float t = u_time * .05, z = 2.5 / max(u_zoom, .05); vec2 p = centered() * z; float h = fbm(p * .7 + vec2(t, t * .6), 4) + .4 * fbm(p * .35 - t * .5, 2); vec2 pp = (u_pointer - .5) * vec2(aspect(), 1.) * z; h += u_hover * .55 * exp(-dot(p - pp, p - pp) * 2.5); float hn = clamp(h * .6 + .5, 0., 1.); vec3 nrm = normalize(vec3(-vec2(dFdx(h), dFdy(h)) * u_resolution.y / z * .12 * u_relief, 1.)); float lit = clamp(dot(nrm, normalize(vec3(-.5, .6, .62))), 0., 1.); vec3 col = mixOk(u_background.rgb, ramp(hn), .22 + .26 * hn) * (.8 + .3 * lit); float c = h * u_lines; float dist = abs(fract(c - .5) - .5) / max(fwidth(c), 1e-4); float major = step(mod(floor(c + .5), 5.), .5); float lw = max(u_thickness, .3) * u_pixel_ratio * (1. + major); float line = 1. - smoothstep(lw * .5, lw * .5 + 1., dist); col = mix(col, mixOk(ramp(hn), color(colorCount() - 1), .6), line * (.6 + .3 * major)); fragColor = vec4(dither(col), 1.);}`, }, trails: { params: { decay: 0.978, brush: 1, flow: 1 }, colors: ['#ff6b6b', '#ffd93d', '#6bcbff', '#c77dff'], background: '#08080d', interactive: true, still: 0, fragment: '', }, // -- 3D scenes ------------------------------------------------------------ orb: { params: { size: 1, iridescence: 1, wobble: 1 }, colors: ['#cfd9ff', '#1b1530', '#ff7ad9', '#7ee8ff'], background: '#0c0a15', cost: 0.75, still: 2, fragment: `uniform float u_size, u_iridescence, u_wobble;float t3;float surface(vec3 q) { return length(q) - .78 * u_size - u_wobble * .07 * gnoise3(q * 2.2 + t3); }vec3 env(vec3 d) { vec3 c = mixOk(color(1), color(0), smoothstep(-.6, 1., d.y)); c += pow(max(dot(d, normalize(vec3(-.5, .7, .5))), 0.), 20.) * 1.3; c += color(2) * pow(max(dot(d, normalize(vec3(.85, -.15, .3))), 0.), 5.) * .7; c += color(3) * pow(max(dot(d, normalize(vec3(-.9, -.3, -.1))), 0.), 4.) * .45; return c;}vec3 thinFilm(float cosT, float thick) { float opd = 2. * 1.33 * thick * sqrt(max(1. - (1. - cosT * cosT) / 1.769, 0.)); return .5 + .5 * cos(TAU * opd / vec3(650., 532., 450.));}void main() { vec2 p = centered() * 2.; float t = u_time * .3; t3 = t; vec3 ro = vec3(0., 0., 3.), rd = normalize(vec3(p, -2.2)); vec2 look = mix(vec2(sin(t * .7) * .25, cos(t * .5) * .12), (u_pointer - .5) * vec2(1.4, .9), u_hover); mat2 ry = rot(look.x), rx = rot(look.y); ro.xz = ry * ro.xz; rd.xz = ry * rd.xz; ro.yz = rx * ro.yz; rd.yz = rx * rd.yz; float R = .78 * u_size; vec3 bg = u_background.rgb + color(2) * .16 * exp(-dot(p, p) * 1.1) + color(3) * .06 * exp(-dot(p - vec2(.6, .5), p - vec2(.6, .5)) * 2.); bg *= 1. - .4 * exp(-pow((p.y + R * 1.25) / .07, 2.) - p.x * p.x * 2.5); vec3 col = bg; float b = dot(ro, rd), c = dot(ro, ro) - pow(R * 1.2, 2.), disc = b * b - c; if (disc > 0.) { float tt = max(-b - sqrt(disc), 0.), mind = 1e3; bool hit = false; vec3 pos; for (int i = 0; i < 40; i++) { pos = ro + rd * tt; float d = surface(pos); mind = min(mind, d); if (d < .0015) { hit = true; break; } tt += d * .85; if (tt > -b + sqrt(disc)) break; } float px = 2.5 * tt / (u_resolution.y * 1.1); float cover = hit ? 1. : 1. - smoothstep(0., px, mind); if (cover > 0.) { vec2 e = vec2(.002, 0.); vec3 n = normalize(vec3(surface(pos + e.xyy) - surface(pos - e.xyy), surface(pos + e.yxy) - surface(pos - e.yxy), surface(pos + e.yyx) - surface(pos - e.yyx))); float cosT = clamp(dot(n, -rd), 0., 1.); float F = .04 + .96 * pow(1. - cosT, 5.); vec3 refl = env(reflect(rd, n)), refr = env(refract(rd, n, 1. / 1.45)) * .85; vec3 s = mix(refr, refl, F); vec3 film = thinFilm(cosT, 380. + 260. * gnoise3(pos * 1.6 + t * .7)); s = mix(s, s * film * 1.5 + film * .12, u_iridescence * (.35 + .65 * (1. - cosT))); s += pow(max(dot(reflect(rd, n), normalize(vec3(-.5, .7, .5))), 0.), 90.) * 1.6; col = mix(bg, s, cover); } } fragColor = vec4(dither(col), 1.);}`, }, galaxy: { params: { arms: 3, twist: 2.6, stars: 1, tilt: 0.55 }, colors: ['#ffd9a0', '#7fb2ff', '#ff6fcf', '#ffffff'], background: '#02030a', still: 5, fragment: `uniform float u_arms, u_twist, u_stars, u_tilt;float stars(vec2 x, float density, float seed) { vec2 id = floor(x), f = fract(x) - .5; vec2 o = (hash22(id + seed) - .5) * .7; float on = step(1. - density, hash12(id + seed + 7.)); float tw = .55 + .45 * sin(u_time * (1. + hash12(id + seed + 3.) * 3.) + hash12(id - seed) * 30.); return on * tw * smoothstep(.12, 0., length(f - o));}void main() { vec2 p = centered() * 1.7; float t = u_time * .04; vec2 par = (u_pointer - .5) * u_hover; vec2 q = rot(t) * (p - par * .2); q.y /= max(u_tilt, .2); q = rot(-.5) * q; float r = length(q), ang = atan(q.y, q.x); float phase = max(floor(u_arms), 1.) * (ang + u_twist * log(r + .03)) + t * 2.; float armD = pow(.5 + .5 * cos(phase), 2.5); float disk = exp(-r * 2.1); float dust = fbm(q * 3. + t * 4., 4); float arm = armD * disk * (.65 + .45 * dust) * smoothstep(0., .25, r); float neb = pow(clamp(fbm(q * 1.6 - t * 3., 4) * .5 + .5, 0., 1.), 2.) * disk * 1.4; float core = exp(-r * r * 26.) + .45 * exp(-r * 6.); vec3 col = u_background.rgb + color(1) * arm * 2.1 + color(2) * neb * armD * 1.2 + color(0) * core * 1.25; col *= 1. - .55 * smoothstep(.2, .7, dust) * pow(.5 + .5 * cos(phase + .9), 4.) * disk; float st = 0.; for (int i = 0; i < 3; i++) { float fi = float(i); st += stars(gl_FragCoord.xy / u_pixel_ratio / (12. + fi * 8.) + par * (fi + 1.) * 4., .14 * u_stars, fi * 13.) * (1. - fi * .25); } col += st * color(3) * .85; col += color(1) * step(.997, hash12(floor(gl_FragCoord.xy / u_pixel_ratio))) * arm * 3.; fragColor = vec4(dither(1. - exp(-col * 1.25)), 1.);}`, }, waves: { params: { height: 1, choppy: 1, camera: 1 }, colors: ['#03253a', '#0f7d8c', '#f6c49c', '#35548a', '#fff0d4'], cost: 0.75, still: 4, fragment: `uniform float u_height, u_choppy, u_camera;vec3 ocean(vec2 x, float t) { float h = 0., amp = .3 * u_height, freq = .5; vec2 g = vec2(0.), dir = normalize(vec2(1., .35)); for (int i = 0; i < 7; i++) { float ph = dot(dir, x) * freq + t * sqrt(9.8 * freq) * .7 + float(i) * 1.7; float k = max(u_choppy, .05) * 1.4; float w = exp((sin(ph) - 1.) * k); h += amp * w; g += amp * w * cos(ph) * k * freq * dir; dir = rot(1.1) * dir; amp *= .6; freq *= 1.75; } return vec3(h, g);}vec3 sky(vec3 d) { vec3 sun = normalize(vec3(-.25, .12, 1.)); vec3 c = mixOk(color(2), color(3), smoothstep(0., .55, d.y)); c += color(4) * (pow(max(dot(d, sun), 0.), 400.) * 3. + pow(max(dot(d, sun), 0.), 12.) * .35); return c;}void main() { vec2 p = centered() * 2.; float t = u_time; vec2 look = (u_pointer - .5) * vec2(.5, .25) * u_hover; vec3 ro = vec3(t * .2, 1.4 * max(u_camera, .2), 0.); vec3 rd = normalize(vec3(p.x, p.y - .3, 1.8)); rd.xz = rot(look.x) * rd.xz; rd.yz = rot(-look.y) * rd.yz; vec3 sun = normalize(vec3(-.25, .12, 1.)); vec3 col; if (rd.y > -.002) col = sky(rd); else { float top = .6 * u_height, tt = (ro.y - top) / -rd.y, prevT = tt, prevD = 1.; for (int i = 0; i < 56; i++) { vec3 pos = ro + rd * tt; float d = pos.y - ocean(pos.xz, t).x; if (d < .002 * tt) { tt = mix(prevT, tt, prevD / max(prevD - d, 1e-4)); break; } prevT = tt; prevD = d; tt += max(d * .7, .015 * tt); if (tt > 140.) break; } vec3 pos = ro + rd * tt; vec3 o = ocean(pos.xz, t); vec3 n = normalize(mix(vec3(-o.y, 1., -o.z), vec3(0., 1., 0.), smoothstep(8., 90., tt))); float F = .02 + .98 * pow(1. - max(dot(n, -rd), 0.), 5.); vec3 water = mixOk(color(0), color(1), clamp(o.x / max(.6 * u_height, .01) * 1.3 + .1, 0., 1.)) * (.45 + .55 * max(dot(n, sun), 0.)); water += color(1) * pow(max(dot(rd, sun), 0.), 3.) * clamp(o.x, 0., 1.) * .6; col = mix(water, sky(reflect(rd, n)), F); col += color(4) * pow(max(dot(reflect(rd, n), sun), 0.), 420.) * 3.; col += smoothstep(.5, .95, o.x / max(.6 * u_height, .01)) * .18; col = mix(col, sky(vec3(rd.x, .01, rd.z)), 1. - exp(-tt * .018)); } fragColor = vec4(dither(col), 1.);}`, }, // -- more gradients, light, liquid and patterns --------------------------- plasma: { params: { zoom: 1, warp: 1 }, colors: ['#ff006e', '#fb5607', '#ffbe0b', '#3a86ff', '#8338ec'], still: 2, fragment: `uniform float u_zoom, u_warp;void main() { vec2 p = centered() * 3. / max(u_zoom, .05); float t = u_time * .4; p += u_warp * .35 * vec2(sin(p.y * 1.3 + t), cos(p.x * 1.1 - t)); float v = sin(p.x * 1.7 + t) + sin(p.y * 1.9 - t * .8) + sin((p.x + p.y) * 1.2 + t * .6) + sin(length(p - vec2(sin(t * .7), cos(t * .5)) * 1.5) * 2.6); fragColor = vec4(dither(rampCycle(v * .125 + .5 + t * .05)), 1.);}`, }, bokeh: { params: { size: 1, density: 0.7, blur: 0.5 }, colors: ['#ffb347', '#ff6f91', '#9b8cff', '#5ad1ff', '#ffe08a'], background: '#0b0a14', still: 4, fragment: `uniform float u_size, u_density, u_blur;void main() { vec2 uv = centered(); vec3 col = u_background.rgb * u_background.a; for (int l = 0; l < 4; l++) { float fl = float(l); float sc = (2.6 + fl * 1.6) / max(u_size, .1); vec2 p = uv * sc + vec2(fl * 7.3, -u_time * (.05 + fl * .025)); vec2 c = floor(p), f = fract(p) - .5; vec2 h = hash22(c + vec2(fl * 31., fl * 17.)); if (h.x > u_density * .9) continue; vec2 o = (hash22(c + 51.) - .5) * .5; float r = .16 + .2 * h.y; float d = length(f - o) - r; float soft = (.015 + u_blur * .1) * (1. + fl * .4); float disc = 1. - smoothstep(-soft, soft, d); float rim = exp(-abs(d) * 26.) * .45 * disc; float tw = .65 + .35 * sin(u_time * (.4 + h.x) + h.y * 20.); col += color(int(h.y * 8.) % colorCount()) * (disc * .32 + rim) * tw / (1. + fl * .55); } fragColor = vec4(dither(col), 1.);}`, }, ripple: { params: { strength: 0.6, decay: 0.6, drops: 0.6 }, colors: ['#0d5c75', '#3fb8c7', '#d8f6f2'], image: 'image', interactive: true, still: 1.2, fragment: `uniform float u_strength, u_decay, u_drops;// one ring: its height and slope (d/duv) at p, started at c at time t0vec3 ring(vec2 p, vec2 c, float t0) { float age = u_time - t0; if (age < 0. || age > 7.) return vec3(0.); vec2 k = vec2(aspect(), 1.); vec2 dp = (p - c) * k; float r = length(dp) + 1e-4; float x = r - age * .32; float env = exp(-x * x * 90.) * exp(-age * max(u_decay, .05)) / (1. + r * 5.); float h = sin(x * 55.) * env; float dh = 55. * cos(x * 55.) * env; return vec3(h, dp / r * k * dh);}void main() { vec2 p = v_uv; vec3 w = vec3(0.); float rate = max(u_drops, 0.); if (rate > 0.) { float now = floor(u_time * rate); for (int i = 0; i < 5; i++) { float slot = now - float(i); w += ring(p, hash22(vec2(slot, 7.)) * .8 + .1, slot / rate); } } for (int i = 0; i < 8; i++) { if (i >= u_tap_count) break; w += ring(p, u_taps[i].xy, u_taps[i].z); } vec2 off = w.yz * u_strength * .006; vec3 base = u_has_image > .5 ? image(p + off).rgb : mixOk(color(0), color(1), smoothstep(-.2, 1.2, p.y + off.y * 3. + gnoise((p + off) * 3. + u_time * .05) * .15)); vec3 n = normalize(vec3(-w.yz * u_strength * .05, 1.)); float spec = pow(max(dot(n, normalize(vec3(-.35, .45, .82))), 0.), 40.); fragColor = vec4(dither(base * (1. + w.x * u_strength * .5) + color(2) * spec * .7), 1.);}`, }, voronoi: { params: { scale: 5, lead: 0.06, glow: 0.6 }, colors: ['#d1495b', '#edae49', '#00798c', '#30638e', '#7f5a83', '#8ab17d'], still: 1, fragment: `uniform float u_scale, u_lead, u_glow;void main() { vec2 p = centered() * max(u_scale, .5); float t = u_time * .25; vec2 n = floor(p), f = fract(p); float f1 = 8., f2 = 8.; vec2 id = vec2(0.), r1 = vec2(0.); for (int j = -1; j <= 1; j++) for (int i = -1; i <= 1; i++) { vec2 g = vec2(float(i), float(j)); vec2 o = .5 + .36 * sin(t + TAU * hash22(n + g)); vec2 r = g + o - f; float d = dot(r, r); if (d < f1) { f2 = f1; f1 = d; id = n + g; r1 = r; } else if (d < f2) f2 = d; } // the distance to the cell's border: the second pass measures it along the bisectors float edge = 8.; for (int j = -2; j <= 2; j++) for (int i = -2; i <= 2; i++) { vec2 g = vec2(float(i), float(j)); vec2 o = .5 + .36 * sin(t + TAU * hash22(n + g)); vec2 r = g + o - f; if (dot(r1 - r, r1 - r) > 1e-5) edge = min(edge, dot(.5 * (r1 + r), normalize(r - r1))); } float light = .7 + .3 * gnoise(p * .3 + t * .4) + u_glow * .45 * (1. - smoothstep(0., .5, sqrt(f1))); vec3 glass = rampCycle(hash12(id) + t * .03) * light; float aa = fwidth(edge) * 1.2; vec3 col = mix(vec3(.05, .045, .04), glass, smoothstep(u_lead - aa, u_lead + aa, edge)); fragColor = vec4(dither(col), 1.);}`, }, halftone: { params: { cell: 8, angle: 15, contrast: 1.2 }, colors: ['var(--foreground)', 'var(--background)'], image: 'image', still: 1, fragment: `uniform float u_cell, u_angle, u_contrast;void main() { vec2 px = gl_FragCoord.xy / u_pixel_ratio; float cs = max(u_cell, 2.); mat2 R = rot(radians(u_angle)); vec2 g = R * px / cs; vec2 c = floor(g) + .5; vec2 center = (transpose(R) * (c * cs)) * u_pixel_ratio / u_resolution; float l; if (u_has_image > .5) l = dot(image(clamp(center, 0., 1.)).rgb, vec3(.2126, .7152, .0722)); else { vec2 q = (center - .5) * vec2(aspect(), 1.); l = .5 + .5 * sin(length(q) * 9. - u_time * 1.4) * cos(q.x * 3. + u_time * .5); } l = clamp((l - .5) * u_contrast + .5, 0., 1.); float d = length(g - c) - sqrt(1. - l) * .74; float aa = fwidth(length(g - c)) * .8; fragColor = vec4(dither(mix(color(1), color(0), 1. - smoothstep(-aa, aa, d))), 1.);}`, }, kaleidoscope: { params: { segments: 8, zoom: 1, twist: 1 }, colors: THEME, image: 'image', still: 3, fragment: `uniform float u_segments, u_zoom, u_twist;void main() { vec2 p = centered() * 1.4 / max(u_zoom, .05); float t = u_time * .15; float r = length(p), a = atan(p.y, p.x) + t; float seg = TAU / max(floor(u_segments), 2.); a = mod(a, seg); a = abs(a - seg * .5); vec2 q = rot(t * u_twist) * (vec2(cos(a), sin(a)) * r) + vec2(t * .7, 0.); vec3 col; if (u_has_image > .5) col = image(abs(fract(q * .5 + .5) * 2. - 1.)).rgb; else { float n = fbm(q * 2.2, 4); col = rampCycle(n * .8 + r * .3 - t * .4) * (.75 + .35 * smoothstep(0., .6, abs(sin(n * 8.)))); } col *= 1. - smoothstep(.7, 1.4, r) * .35; fragColor = vec4(dither(col), 1.);}`, }, // -- nature and space ----------------------------------------------------- clouds: { params: { cover: 0.5, softness: 0.6, zoom: 1 }, colors: ['#2f6fc4', '#a9d1f5', '#ffffff', '#8f9db3'], still: 6, fragment: `uniform float u_cover, u_softness, u_zoom;void main() { vec2 p = centered() * 2.4 / max(u_zoom, .05) + vec2(u_time * .05, 0.); vec3 sky = mixOk(color(1), color(0), pow(v_uv.y, .8)); vec2 w = vec2(fbm(p * .5 + u_time * .02, 3), fbm(p * .5 - u_time * .015 + 4.2, 3)); float n = fbm(p + w * .7, 6) * .5 + .5; float c = smoothstep(1. - u_cover, 1. - u_cover + .35 * max(u_softness, .05), n); // light from above: a sample nearer the sun is thinner - the cloud's top is lit, its base shaded float lit = fbm(p + w * .7 + vec2(.08, .16), 6) * .5 + .5; vec3 cloud = mixOk(color(3), color(2), clamp(.55 + (n - lit) * 5., 0., 1.)); fragColor = vec4(dither(mix(sky, cloud, c)), 1.);}`, }, fire: { params: { intensity: 1, height: 0.75, zoom: 1 }, colors: ['#1a0402', '#b3160b', '#f2590d', '#ffb21f', '#fff3c4'], background: '#0a0505', still: 2, fragment: `uniform float u_intensity, u_height, u_zoom;void main() { vec2 p = centered() * 3. / max(u_zoom, .05); float t = u_time; vec2 q = vec2(p.x, p.y - t * 1.5); float n = fbm(q * vec2(1.4, 1.) + vec2(0., -t * .3), 5) * .5 + .5; float n2 = fbm(q * 2.3 + 3.7, 4) * .5 + .5; float shape = 1. - v_uv.y / max(u_height, .1); float f = clamp(shape * 1.05 + n * .9 + n2 * .35 - .75, 0., 1.); f = clamp(pow(f, 1.3) * u_intensity * .92, 0., 1.); fragColor = over(dither(ramp(f)), clamp(f * 2.4, 0., 1.));}`, }, starfield: { params: { density: 0.6, warp: 0.4, twinkle: 0.5 }, colors: ['#ffffff', '#cfe0ff', '#ffe2c4', '#b9c7ff'], background: '#03040a', still: 3, fragment: `uniform float u_density, u_warp, u_twinkle;void main() { vec2 uv = centered(); vec3 col = u_background.rgb * u_background.a; vec2 dir = normalize(uv + 1e-4); for (int l = 0; l < 6; l++) { float fl = float(l); float z = fract(fl / 6. + u_time * .045 * (.4 + u_warp)); float fade = smoothstep(0., .35, z) * smoothstep(1., .85, z); vec2 p = uv * mix(14., 1.6, z * z) + vec2(fl * 13.7, fl * 7.1); vec2 c = floor(p), f = fract(p) - .5; vec2 h = hash22(c + vec2(fl * 17., 3.)); if (h.x > u_density * .9) continue; vec2 d = f - (hash22(c + 41.) - .5) * .7; float along = dot(d, dir), across = dot(d, vec2(-dir.y, dir.x)); float len = .01 + u_warp * z * z * .35; float s = exp(-across * across * 2600.) * exp(-pow(max(abs(along) - len, 0.), 2.) * 2600.); float tw = 1. - u_twinkle * .6 * (.5 + .5 * sin(u_time * (2. + h.y * 5.) + h.x * 30.)); col += color(int(h.y * 8.) % colorCount()) * s * fade * tw * (.5 + z * 1.4); } fragColor = vec4(dither(col), 1.);}`, }, // -- more looks ---------------------------------------------------------- silk: { params: { folds: 4, sheen: 0.8, zoom: 1 }, colors: ['#2b0f3a', '#7a2e8e', '#e07bb5', '#ffe3f1'], ranges: { folds: [1, 8, 0.1], sheen: [0, 2, 0.01] }, aa: 'fxaa', still: 3, fragment: `uniform float u_folds, u_sheen, u_zoom;// the cloth's height: a sine sheet carried by a few slow foldsfloat silk(vec2 p) { float t = u_time * .18; vec2 q = p; for (int i = 1; i < 4; i++) { float fi = float(i); q += .45 / fi * vec2(sin(fi * q.y * .9 + t * fi * .7 + 1.7 * fi), cos(fi * q.x * .8 - t * fi * .5)); } float k = max(u_folds, .2); return sin(q.x * k + q.y * .6) * .6 + sin(q.x * k * .5 - q.y * .4 + t) * .4;}void main() { vec2 p = centered() * 2. / max(u_zoom, .05); float e = .003; float h = silk(p); vec3 n = normalize(vec3(-(silk(p + vec2(e, 0.)) - h) / e, -(silk(p + vec2(0., e)) - h) / e, 1.6)); vec3 l = normalize(vec3(-.4, .6, .7)); // wrapped diffuse: the folds' shadows stay colored, as on cloth float diff = dot(n, l) * .5 + .5; float spec = pow(max(dot(reflect(-l, n), vec3(0., 0., 1.)), 0.), 50.) * u_sheen; vec3 col = ramp(clamp(diff * diff * .95 + h * .08, 0., 1.)) + color(colorCount() - 1) * spec * .7; fragColor = vec4(dither(col), 1.);}`, }, nebula: { params: { density: 0.9, stars: 0.6, zoom: 1 }, colors: ['#3a0ca3', '#f72585', '#4cc9f0', '#ffffff'], background: '#05030d', ranges: { density: [0, 1.5, 0.01] }, still: 8, fragment: `uniform float u_density, u_stars, u_zoom;void main() { vec2 p = centered() * 2.2 / max(u_zoom, .05); float t = u_time * .03; vec2 w = vec2(fbm(p + t, 4), fbm(p * 1.3 - t + 5.2, 4)); float n = fbm(p * 1.1 + w * 1.6 + vec2(t, -t * .5), 6) * .5 + .5; float n2 = fbm(p * 2.3 - w + 9.1, 5) * .5 + .5; float gas = smoothstep(.35, .95, n) * u_density; vec3 tint = mixOk(color(0), color(1), smoothstep(.35, .8, n)); tint = mixOk(tint, color(2), smoothstep(.55, .95, n2) * .8); vec3 col = u_background.rgb * u_background.a + tint * gas * 1.25; // dark lanes of dust across the glow col *= 1. - smoothstep(.55, .85, fbm(p * 1.8 + w * 2. + 3.3, 5) * .5 + .5) * .55 * min(u_density, 1.); // stars on a pixel grid: the same size at every resolution vec2 px = gl_FragCoord.xy / u_pixel_ratio; for (int l = 0; l < 2; l++) { float cell = 22. + float(l) * 17.; vec2 sp = px / cell + float(l) * 7.7; vec2 c = floor(sp), f = fract(sp) - .5; vec2 h = hash22(c + float(l) * 13.); if (h.x > u_stars * .5) continue; vec2 o = (hash22(c + 7.) - .5) * .7; float d = length(f - o) * cell; float tw = .55 + .45 * sin(u_time * (1. + h.y * 3.) + h.x * 40.); col += color(3) * exp(-d * d * (.9 + float(l) * .6)) * tw * (1. - float(l) * .35); } fragColor = vec4(dither(col), 1.);}`, }, prism: { params: { spread: 0.6, sparkle: 0.5, angle: -12 }, colors: ['#ffffff', '#a0c4ff', '#ffc6ff', '#caffbf'], background: '#0b0b12', still: 2, fragment: `uniform float u_spread, u_sparkle, u_angle;// a wavelength 0..1 as a color: red, yellow, green, cyan, blue, violetvec3 spectrum(float x) { return clamp(abs(mod(x * 5. + vec3(0., 4., 2.), 6.) - 3.) - 1., 0., 1.); }void main() { vec2 p = rot(radians(u_angle)) * centered(); float t = u_time * .2; vec3 col = u_background.rgb * u_background.a; for (int i = 0; i < 4; i++) { float fi = float(i); // a beam bending slowly; across it the light splits into its colors float y = p.y + .22 * sin(p.x * (1.2 + fi * .3) + t * (1. + fi * .2) + fi * 2.) - (fi - 1.5) * .26; float w = (.045 + .025 * sin(t + fi * 1.7)) * (1. + u_spread); float x = y / w; float inside = 1. - smoothstep(.7, 1.25, abs(x)); float glow = exp(-y * y * 30.) * .22; float pulse = .65 + .35 * sin(p.x * 2.6 + t * 2. + fi); col += (spectrum(clamp(x * .5 + .5, 0., 1.)) * inside * .75 + color(i) * glow) * pulse; } col += color(0) * pow(max(gnoise(p * 7. + t), 0.), 4.) * u_sparkle * 1.5; fragColor = vec4(dither(col), 1.);}`, }, matrix: { params: { size: 16, rain: 1, trail: 0.6 }, colors: ['#0b3d1a', '#26ff6a', '#d9ffe4'], background: '#020a04', ranges: { size: [8, 40, 1], rain: [0.1, 3, 0.05], trail: [0.1, 1, 0.01] }, still: 6, fragment: `uniform float u_size, u_rain, u_trail;void main() { // the glyph grid in CSS px, counted from the top float cs = max(u_size, 6.); vec2 cell = vec2(cs * .64, cs); float rows = u_resolution.y / u_pixel_ratio / cell.y; vec2 g = vec2(gl_FragCoord.x / u_pixel_ratio, u_resolution.y / u_pixel_ratio - gl_FragCoord.y / u_pixel_ratio) / cell; vec2 id = floor(g), f = fract(g); // each column: its own speed, trail length and phase; the head falls, the trail fades behind it float sp = (.5 + hash12(vec2(id.x, 3.)) * .9) * max(u_rain, .02); float len = (6. + hash12(vec2(id.x, 9.)) * 20.) * max(u_trail, .05) * 1.6; float head = fract(u_time * sp * .09 + hash12(vec2(id.x, 1.))) * (rows + len + 4.) - 2.; float d = head - id.y; float trail = d >= 0. && d < len ? pow(1. - d / len, 1.6) : 0.; float lit = d >= 0. && d < 1. ? 1. : 0.; // a 5 x 7 glyph, mirrored so it reads like a character; it changes now and then (faster near the head) float tick = floor(u_time * (1. + 5. * lit + hash12(id) * 2.) + hash12(id + 5.) * 9.); vec2 q = (f - vec2(.14, .1)) / vec2(.72, .8); vec2 gp = floor(q * vec2(5., 7.)); float on = 0.; if (q.x >= 0. && q.x < 1. && q.y >= 0. && q.y < 1.) { float gx = min(gp.x, 4. - gp.x); on = step(.48, hash12(vec2(gx + gp.y * 5., hash12(id + tick) * 97.))); vec2 pf = fract(q * vec2(5., 7.)) - .5; on *= 1. - smoothstep(.32, .5, max(abs(pf.x), abs(pf.y))); } vec3 col = u_background.rgb * u_background.a; float ghost = .07 * step(.3, hash12(vec2(id.x, 21.))); col += on * (mix(color(0), color(1), trail) * max(trail, ghost) + color(2) * lit * 1.2); // a soft glow around the lit trail col += color(1) * trail * .06; fragColor = vec4(dither(col), 1.);}`, }, marble: { params: { veins: 0.4, flow: 1, zoom: 1 }, colors: ['#f4f1ec', '#d9d4cc', '#4f535b', '#c9a86a'], ranges: { veins: [0, 1, 0.01], flow: [0, 3, 0.05] }, aa: 'fxaa', still: 4, fragment: `uniform float u_veins, u_flow, u_zoom;void main() { vec2 p = centered() * 1.4 / max(u_zoom, .05); float t = u_time * .05 * u_flow; vec2 q = vec2(fbm(p + t, 4), fbm(p + vec2(5.2, 1.3) - t, 4)); vec2 r = vec2(fbm(p + 2. * q + vec2(1.7, 9.2) + t * .5, 4), fbm(p + 2. * q + vec2(8.3, 2.8), 4)); float v = fbm(p + 1.8 * r, 4); // veins: where a folded sine crosses zero - kept as wide as a pixel at least float s = abs(sin((p.x * 1.1 + p.y * .45) * 1.6 + v * 3.2)); float w = mix(.015, .2, u_veins); float aa = fwidth(s); float vein = 1. - smoothstep(w * .4, w + aa, s); float halo = (1. - smoothstep(0., w * 4. + aa, s)) * .25; vec3 col = mixOk(color(0), color(1), clamp(v * .9 + .5, 0., 1.)); col = mixOk(col, color(2), clamp(vein + halo, 0., 1.)); col = mixOk(col, color(3), smoothstep(.75, 1.1, length(r)) * .35); fragColor = vec4(dither(col), 1.);}`, }, oil: { params: { film: 1, swirl: 1, zoom: 1 }, colors: ['#0a0a0f', '#1d1b2a', '#ffffff'], ranges: { film: [0, 3, 0.01], swirl: [0, 3, 0.01] }, still: 3, fragment: `uniform float u_film, u_swirl, u_zoom;void main() { vec2 p = centered() * 1.8 / max(u_zoom, .05); float t = u_time * .1; vec2 w = vec2(fbm(p * 1.2 + t, 4), fbm(p * 1.2 - t + 3.1, 4)) * u_swirl; float h = fbm(p + w * 1.5 + t * .3, 5) * .5 + .5; // thin-film interference: the thickness shifts each channel's phase differently float d = h * u_film * 6. + t; vec3 film = .5 + .5 * cos(TAU * (d + vec3(0., .33, .67))); vec3 base = mixOk(color(0), color(1), smoothstep(.2, .9, h)); vec3 col = mix(base, film * mix(vec3(1.), color(2), .3), smoothstep(.3, .75, h) * .85); fragColor = vec4(dither(col), 1.);}`, }, 'op-art': { params: { rings: 18, offset: 0.5, warp: 0.3 }, colors: ['var(--foreground)', 'var(--background)'], ranges: { rings: [4, 48, 1], offset: [0, 1, 0.01], warp: [0, 1, 0.01] }, still: 1, fragment: `uniform float u_rings, u_offset, u_warp;// one set of rings around c: 1 on a dark band, 0 on a light one, smoothed to the pixel; a gray where they get finer than a pixelfloat rings(vec2 p, vec2 c) { float r = length(p - c) + u_warp * .05 * gnoise(p * 3. + u_time * .2); float s = sin(r * u_rings * TAU); float aa = fwidth(s); float fine = smoothstep(.25, .5, fwidth(r) * u_rings); return mix(smoothstep(-aa, aa, s), .5, fine);}void main() { vec2 p = centered(); float t = u_time * .3; vec2 c = vec2(sin(t), cos(t * .7) * .7) * .2 * u_offset; // two sets of rings, combined as exclusive or: the moire is where they disagree float m = abs(rings(p, c) - rings(p, -c)); fragColor = vec4(dither(mix(color(0), color(1), m)), 1.);}`, }, lightning: { params: { bolts: 2, branch: 0.6, glow: 1 }, colors: ['#ffffff', '#8aa8ff'], background: '#070914', ranges: { bolts: [1, 3, 1], branch: [0, 1, 0.01], glow: [0, 2, 0.01] }, still: 0.05, fragment: `uniform float u_bolts, u_branch, u_glow;// the bolt's x at height y (centered units): a jagged path, new on every strikefloat boltX(float y, float seed) { return (hash12(vec2(seed, 1.)) - .5) * aspect() * .9 + fbm(vec2(y * 2.6, seed), 5) * .3 + fbm(vec2(y * 11., seed * 3.), 3) * .05;}void main() { vec2 p = centered(); float ppx = u_resolution.y / u_pixel_ratio; vec3 col = u_background.rgb * u_background.a + color(1) * (fbm(p * 2. + u_time * .02, 4) * .5 + .5) * .05; float t = u_time * .6; for (int i = 0; i < 3; i++) { float fi = float(i); if (fi >= u_bolts) break; float tt = t + fi * .37; float strike = floor(tt); float ph = fract(tt); // not every period strikes; a strike flickers and fades float flash = step(.2, hash12(vec2(fi, strike))) * exp(-ph * 4.) * (.7 + .3 * step(.5, fract(ph * 11.))); if (flash < .002) continue; float seed = fi * 11.3 + strike * 7.1; float end = -.5 + hash12(vec2(seed, 4.)) * .4; float reach = smoothstep(end - .02, end + .02, p.y); float d = abs(p.x - boltX(p.y, seed)) * ppx; // a branch leaves the bolt at y0 and runs aside and down float y0 = .25 - hash12(vec2(seed, 6.)) * .45; float side = hash12(vec2(seed, 8.)) > .5 ? 1. : -1.; float drop = y0 - p.y; float bx = boltX(y0, seed) + side * drop * .7 + fbm(vec2(p.y * 9., seed + 2.), 3) * .05; float bmask = step(0., drop) * (1. - smoothstep(.0, .35 * u_branch, drop)); float db = abs(p.x - bx) * ppx; float core = exp(-d * d * .18) * reach + exp(-db * db * .35) * bmask * .8; float halo = (exp(-d * .05) * reach + exp(-db * .08) * bmask * .5) * u_glow; col += (color(0) * core * 1.4 + color(1) * halo * .55) * flash + color(1) * flash * .05 * u_glow; } fragColor = vec4(dither(col), 1.);}`, }, snow: { params: { density: 0.55, wind: 0.3, size: 1 }, colors: ['#1b2a41', '#6c8ead', '#ffffff'], ranges: { density: [0, 1, 0.01], wind: [-1, 1, 0.01], size: [0.3, 3, 0.05] }, still: 4, fragment: `uniform float u_density, u_wind, u_size;void main() { vec2 px = gl_FragCoord.xy / u_pixel_ratio; vec3 col = mixOk(color(0), color(1), v_uv.y); for (int l = 0; l < 5; l++) { float fl = float(l); // near layers: big, fast, blurred flakes; far ones small and slow float cell = (110. / (1. + fl * .75)) * max(u_size, .2); float fall = (.9 - fl * .13) * cell * .6; vec2 q = px + vec2(-u_wind * fall * u_time * 1.2, fall * u_time); vec2 p = q / cell + fl * 3.7; vec2 c = floor(p), f = fract(p) - .5; vec2 h = hash22(c + fl * 9.); if (h.x > u_density) continue; vec2 o = (hash22(c + 3.) - .5) * .6; o.x += sin(u_time * (.8 + h.y) + h.x * 6.) * .12; float r = cell * (.03 + .025 * h.y); float blur = 1. + (l == 0 ? r * .6 : 0.); float s = 1. - smoothstep(r - blur, r + blur, length(f - o) * cell); col = mix(col, color(2), s * (1. - fl * .14)); } fragColor = vec4(dither(col), 1.);}`, }, fireflies: { params: { count: 24, glow: 1, drift: 1 }, colors: ['#03140d', '#0b2e22', '#d6ff7a'], ranges: { count: [1, 40, 1], glow: [0, 2, 0.01], drift: [0, 3, 0.05] }, still: 5, fragment: `uniform float u_count, u_glow, u_drift;void main() { vec2 uv = v_uv * vec2(aspect(), 1.); float ppx = u_resolution.y / u_pixel_ratio; vec3 col = mixOk(color(0), color(1), smoothstep(0., 1., v_uv.y)); for (int i = 0; i < 40; i++) { float fi = float(i); if (fi >= u_count) break; vec2 h = hash22(vec2(fi, 3.)), h2 = hash22(vec2(fi, 11.)); float t = u_time * (.1 + h2.x * .15) * u_drift; vec2 pos = vec2(h.x * aspect(), h.y * .8 + .1) + .12 * vec2(sin(t * 2.1 + h2.y * 6.), cos(t * 1.7 + h.x * 6.)); pos += .05 * vec2(gnoise(vec2(t, fi)), gnoise(vec2(fi, t))); // each one blinks on its own slow rhythm float blink = smoothstep(.15, 1., sin(u_time * (.7 + h2.y) + fi * 2.3) * .5 + .5); float d = length(uv - pos) * ppx; col += color(2) * (exp(-d * d * .12) * 1.1 + exp(-d * .07) * .3 * u_glow) * blink; } fragColor = vec4(dither(col), 1.);}`, }, glitch: { params: { amount: 0.7, blocks: 0.5, shift: 1 }, colors: ['#ff2e63', '#08d9d6', '#252a34', '#eaeaea'], image: 'image', ranges: { amount: [0, 1, 0.01], blocks: [0, 1, 0.01], shift: [0, 3, 0.05] }, still: 2.3, fragment: `uniform float u_amount, u_blocks, u_shift;// the picture, or a test card of the palette without onevec3 src(vec2 uv) { uv = clamp(uv, 0., 1.); if (u_has_image > .5) return image(uv).rgb; vec3 c = ramp(uv.x); return c * (.78 + .22 * step(.5, fract(uv.y * 9.))) * (.85 + .15 * step(.5, fract(uv.x * 23. + uv.y * 3.)));}void main() { vec2 uv = v_uv; float t = floor(u_time * 8.); // glitches come in bursts: most moments are clean float burst = step(.55, hash12(vec2(t, 1.))) * u_amount; vec2 bs = vec2(floor(mix(3., 18., u_blocks)), floor(mix(6., 40., u_blocks))); vec2 b = floor(uv * bs); float r = hash12(b + t * 3.); uv.x += (r > .82 ? (hash12(b + t * 7.) - .5) * .25 : 0.) * burst; uv.x += (hash12(vec2(floor(uv.y * 140.), t)) - .5) * .015 * burst; float sh = u_shift * .01 * (.25 + burst); vec3 col = vec3(src(uv + vec2(sh, 0.)).r, src(uv).g, src(uv - vec2(sh, 0.)).b); col += (hash12(gl_FragCoord.xy + t) - .5) * .14 * burst; if (r > .975 && burst > 0.) col = 1. - col; fragColor = vec4(dither(col), 1.);}`, }, crt: { params: { curve: 0.6, scanlines: 240, glow: 0.6 }, colors: ['#c0c0c0', '#c0c000', '#00c0c0', '#00c000', '#c000c0', '#c00000', '#0000c0'], background: '#050505', image: 'image', ranges: { curve: [0, 1.5, 0.01], scanlines: [60, 600, 10], glow: [0, 2, 0.01] }, still: 1, fragment: `uniform float u_curve, u_scanlines, u_glow;// the picture, or color bars of the palette without onevec3 src(vec2 uv) { if (u_has_image > .5) return image(clamp(uv, 0., 1.)).rgb; if (uv.y > .3) return color(int(uv.x * float(colorCount()))); return vec3(uv.x) * .8 * (.6 + .4 * step(.5, fract(uv.x * 8. - u_time * .25)));}void main() { vec2 c = v_uv * 2. - 1.; c *= 1. + u_curve * .22 * pow(abs(c.yx), vec2(2.)); vec2 uv = c * .5 + .5; float edge = min(min(uv.x, 1. - uv.x), min(uv.y, 1. - uv.y)); float inside = smoothstep(0., fwidth(uv.y) * 1.5, edge); vec3 col = vec3(src(uv + vec2(.0015, 0.)).r, src(uv).g, src(uv - vec2(.0015, 0.)).b); // scanlines: never finer than two device pixels, so they cannot alias float lines = min(u_scanlines, u_resolution.y / 2.5); col *= .7 + .3 * sin(uv.y * lines * TAU) * .5 + .15; // the aperture: red, green and blue stripes, one device pixel each float m = mod(gl_FragCoord.x, 3.); col *= mix(vec3(1.), vec3(step(m, 1.), step(1., m) * step(m, 2.), step(2., m)) * .5 + .6, .45); vec3 bloom = (src(uv + vec2(.004, 0.)) + src(uv - vec2(.004, 0.)) + src(uv + vec2(0., .006)) + src(uv - vec2(0., .006))) * .25; col += bloom * u_glow * .25; col *= 1. + .07 * exp(-pow(fract(uv.y - u_time * .07) - .5, 2.) * 90.); col *= 1. - .35 * dot(c * .7, c * .7); fragColor = vec4(dither(mix(u_background.rgb, col * 1.15, inside)), 1.);}`, }, caustics: { params: { scale: 4, light: 1, warp: 0.4 }, colors: ['#0b4f6c', '#20a4c4', '#e8fbff'], ranges: { scale: [1, 12, 0.1], light: [0, 2, 0.01], warp: [0, 1.5, 0.01] }, aa: 'fxaa', still: 2, fragment: `uniform float u_scale, u_light, u_warp;// the gap between the nearest and the second nearest of moving points: small along the cells' bordersfloat gap(vec2 p, float t) { vec2 n = floor(p), f = fract(p); float f1 = 8., f2 = 8.; for (int j = -1; j <= 1; j++) for (int i = -1; i <= 1; i++) { vec2 g = vec2(float(i), float(j)); vec2 r = g + .5 + .4 * sin(t + TAU * hash22(n + g)) - f; float d = dot(r, r); if (d < f1) { f2 = f1; f1 = d; } else if (d < f2) f2 = d; } return sqrt(f2) - sqrt(f1);}void main() { vec2 p = centered() * max(u_scale, .5); float t = u_time * .6; vec2 w = vec2(gnoise(p * .6 + t * .2), gnoise(p * .6 - t * .2 + 4.)) * u_warp; // two nets of light at different scales: where they meet, the light gathers float a = gap(p + w, t), b = gap(p * 1.6 - w + 3.1, t * 1.3); float light = pow(exp(-a * 8.) + exp(-b * 6.) * .6, 1.6) * u_light; vec3 water = mixOk(color(0), color(1), clamp(v_uv.y * .7 + gnoise(p * .4 + t * .1) * .15 + .15, 0., 1.)); fragColor = vec4(dither(water + color(2) * light * .75), 1.);}`, }, smoke: { params: { density: 0.8, rise: 1, zoom: 1 }, colors: ['#101014', '#26262e', '#d9d9e3'], ranges: { density: [0, 1.5, 0.01], rise: [0, 3, 0.05] }, still: 4, fragment: `uniform float u_density, u_rise, u_zoom;void main() { vec2 p = centered() * 2.4 / max(u_zoom, .05); float t = u_time * .25 * u_rise; vec2 q = vec2(p.x, p.y - t); vec2 w = vec2(fbm(q * .8 + vec2(0., t * .3), 4), fbm(q * .8 + 5.2, 4)); float n = fbm(q + w * 1.8, 6) * .5 + .5; // lit from above: a sample nearer the light is thinner float lit = fbm(q + w * 1.8 + vec2(-.04, .09), 6) * .5 + .5; float dens = smoothstep(.38, .9, n) * u_density * (1.15 - v_uv.y * .55); vec3 bg = mixOk(color(0), color(1), v_uv.y); vec3 puff = color(2) * clamp(.65 + (n - lit) * 4., .25, 1.2); fragColor = vec4(dither(mix(bg, puff, clamp(dens, 0., 1.))), 1.);}`, }, // -- infinite surfaces: one pattern x material x projection x motion ----- // The four choices are uniforms of ONE program, not #define variants: every pixel takes the same // branch, so a uniform branch costs next to nothing, while variants would compile a program per // combination (26 x 28 x 13 x 24) the first time a page shows it. VERIFIED: one program compiles // and draws every word in headless Chromium on SwiftShader (shader.e2e "surface: every pattern..."). surface: { params: { scale: 8, line: 0.06, bevel: 0.5, depth: 1, tilt: 0.5, fog: 0.5, glow: 1, variation: 0.15, light: 135, elevation: 45, direction: 30, rotation: 0, }, options: { pattern: [ 'hex', 'checker', 'grid', 'dots', 'triangles', 'diamonds', 'bricks', 'herringbone', 'weave', 'scales', 'waves', 'truchet', 'maze', 'cubes', 'circuit', 'topography', 'stripes', 'chevron', 'circles', 'octagons', 'stars', 'stones', 'cells', 'glyphs', 'crosses', 'lattice', ], material: [ 'bevel', 'flat', 'emboss', 'neon', 'glass', 'chrome', 'ceramic', 'paper', 'holo', 'quilted', 'metal', 'gold', 'copper', 'wood', 'marble', 'stone', 'rubber', 'plastic', 'velvet', 'ice', 'lava', 'gem', 'pearl', 'clay', 'toon', 'glitter', 'wireframe', 'xray', ], projection: [ 'flat', 'tilted', 'floor', 'wall', 'corridor', 'tunnel', 'sphere', 'isometric', 'ceiling', 'hallway', 'column', 'dome', 'cube', ], motion: [ 'drift', 'none', 'pulse', 'shimmer', 'ripple', 'rotate', 'warp', 'mutate', 'orbit', 'zoom', 'spiral', 'wave', 'breathe', 'swirl', 'rain', 'cascade', 'flip', 'spin', 'sparkle', 'cycle', 'glitch', 'scan', 'domino', 'shake', ], }, ranges: { scale: [1, 30, 0.5], bevel: [0, 1.5, 0.01], depth: [0, 3, 0.01], glow: [0, 3, 0.01] }, colors: ['var(--background)', 'var(--chart-1)', 'var(--chart-2)', 'var(--chart-3)'], image: 'image', aa: 'fxaa', temporal: 0.6, cost: 0.8, still: 1, fragment: `uniform float u_pattern, u_material, u_projection, u_motion;uniform float u_scale, u_line, u_bevel, u_depth, u_tilt, u_fog, u_glow, u_variation, u_light, u_elevation, u_direction, u_rotation;// cell ids wrap around a tunnel, a ball or a column: every pattern repeats after gPer tiles along xfloat gPer = 0.;int gMotion = 0;vec2 wcell(vec2 c) { return gPer > 0. ? vec2(mod(c.x, gPer), c.y) : c; }// mutate and rain: every cell re-rolls on its own beatfloat epoch(vec2 c) { if (gMotion == 7) return floor(u_time * .35 + hash12(c * 2. + 91.) * 9.); if (gMotion == 14) return floor(u_time * 1.6 + hash12(c * 2. + 91.) * 9.); return 0.;}float crand(vec2 c, float k) { vec2 w = wcell(c); return hash12(w * 2. + vec2(k * 131., epoch(w) * 977.)); }vec2 pgrad(vec2 i, float per) { if (per > 0.) i.x = mod(i.x, per); return hash22(i) * 2. - 1.; }float pnoise(vec2 p, float per) { vec2 i = floor(p), f = fract(p); vec2 u = f * f * (3. - 2. * f); return mix(mix(dot(pgrad(i, per), f), dot(pgrad(i + vec2(1., 0.), per), f - vec2(1., 0.)), u.x), mix(dot(pgrad(i + vec2(0., 1.), per), f - vec2(0., 1.)), dot(pgrad(i + 1., per), f - 1.), u.x), u.y) * 1.4;}float sdSeg(vec2 p, vec2 a, vec2 b) { vec2 pa = p - a, ba = b - a; return length(pa - ba * clamp(dot(pa, ba) / dot(ba, ba), 0., 1.)); }float rayDist(vec2 h, float a) { vec2 d = vec2(cos(a), sin(a)); float t = dot(h, d); return t > 0. ? abs(h.x * d.y - h.y * d.x) : length(h); }// pointy-top hexagons with an inradius of .5: local position and cell idvec4 hexTile(vec2 p) { const vec2 s = vec2(1., 1.7320508); vec4 hc = floor(vec4(p, p - vec2(.5, 1.)) / s.xyxy) + .5; vec4 h = vec4(p - hc.xy * s, p - (hc.zw + .5) * s); return dot(h.xy, h.xy) < dot(h.zw, h.zw) ? vec4(h.xy, hc.xy) : vec4(h.zw, hc.zw + .5);}float hexDist(vec2 h) { h = abs(h); return max(h.x, dot(h, vec2(.5, .8660254))); }// moving points, one per cell: x = the distance to the nearest border (about), yz = the nearest point's cellvec3 voro(vec2 p, float jitter) { vec2 n = floor(p), f = fract(p); float f1 = 8., f2 = 8.; vec2 id = n; for (int j = -1; j <= 1; j++) for (int i = -1; i <= 1; i++) { vec2 g = vec2(float(i), float(j)); vec2 r = g + .5 + (vec2(crand(n + g, 31.), crand(n + g, 32.)) - .5) * jitter - f; float d = dot(r, r); if (d < f1) { f2 = f1; f1 = d; id = n + g; } else if (d < f2) f2 = d; } return vec3((sqrt(f2) - sqrt(f1)) * .5, id);}// a pattern at p (tile units): x = signed distance to the raised shape (negative inside), y = its class, zw = its cellvec4 pattern(vec2 p, int id) { float L = u_line; float d = 1.; float cls = 0.; vec2 cell = vec2(0.); if (id == 0) { // hex vec4 h = hexTile(p); d = hexDist(h.xy) - (.5 - L); cell = h.zw; float r = crand(cell, 1.); cls = r < .55 ? 0. : r < .88 ? 1. : 2.; } else if (id == 1) { // checker cell = floor(p); d = sdRoundBox(fract(p) - .5, vec2(.5 - L), .1 * u_bevel); cls = mod(cell.x + cell.y, 2.); } else if (id == 2) { // grid: the lines are raised, every fourth one stronger vec2 f = abs(fract(p) - .5); d = .5 - max(f.x, f.y) - L * .5; vec2 g = abs(fract(p * .25) - .5); float dm = (.5 - max(g.x, g.y)) * 4. - L * 1.1; if (dm < d) { d = dm; cls = 1.; } cell = floor(p); } else if (id == 3) { // dots cell = floor(p); d = length(fract(p) - .5) - .3 * (1. + (crand(cell, 3.) - .5) * u_variation * 1.5); cls = crand(cell, 4.) < .06 + u_variation * .6 ? 1. : 0.; } else if (id == 4) { // triangles vec2 r = vec2(p.x - p.y * .57735027, p.y * 1.1547005); vec2 c = floor(r), f = fract(r); float up = step(f.x + f.y, 1.); vec3 b = up > .5 ? vec3(f, 1. - f.x - f.y) : vec3(1. - f, f.x + f.y - 1.); d = L * .7 - min(b.x, min(b.y, b.z)) * .8660254; cell = c * 2. + vec2(up, 0.); cls = up; } else if (id == 5) { // diamonds vec2 r = rot(PI * .25) * p * 1.41421356; cell = floor(r); d = sdRoundBox(fract(r) - .5, vec2(.5 - L * 1.414), .08 * u_bevel) / 1.41421356; cls = mod(cell.x + cell.y, 2.); } else if (id == 6) { // bricks: running bond float row = floor(p.y); vec2 b = vec2(p.x * .5 + .5 * mod(row, 2.), p.y); cell = floor(b); vec2 f = fract(b) - .5; d = sdRoundBox(vec2(f.x * 2., f.y), vec2(1. - L, .5 - L), .06 * u_bevel); cls = crand(cell, 5.) < .2 + u_variation ? 1. : 0.; } else if (id == 7) { // herringbone: 2 x 1 bricks on a staircase, turned 45 degrees vec2 r = rot(PI * .25) * p * 1.41421356; float n0 = floor((r.x - r.y + 1.) * .25); d = 1e5; for (int i = 0; i < 2; i++) { float n = n0 - float(i); vec2 s = r - n * vec2(2., -2.); float k = floor(s.y); if (s.x >= k && s.x < k + 2.) { float dd = sdRoundBox(s - vec2(k + 1., k + .5), vec2(1. - L * 1.414, .5 - L * 1.414), .05); if (dd < d) { d = dd; cell = vec2(n * 7. + k, k); cls = 0.; } } float k2 = floor(s.x) - 2.; if (s.y >= k2 - 1. && s.y < k2 + 1.) { float dd = sdRoundBox(s - vec2(k2 + 2.5, k2), vec2(.5 - L * 1.414, 1. - L * 1.414), .05); if (dd < d) { d = dd; cell = vec2(n * 7. + k2, k2 + 50.); cls = 1.; } } } d /= 1.41421356; } else if (id == 8) { // weave: three strands per cell, crossing in turn vec2 c = floor(p), f = fract(p); float horiz = mod(c.x + c.y, 2.); vec2 g = horiz > .5 ? f : f.yx; float strand = floor(g.y * 3.); d = max((abs(fract(g.y * 3.) - .5) - .5) / 3. + L * .6, abs(g.x - .5) - .5 + L * .4); cell = c * 3. + vec2(strand, 0.); cls = horiz; } else if (id == 9) { // scales: rows of discs, each row over the one before it d = 1e5; float j0 = floor(p.y * 2.); for (int k = -1; k <= 1; k++) { float j = j0 + float(k); float x0 = .5 * mod(j, 2.); float i = floor(p.x - x0 + .5); float dd = length(p - vec2(i + x0, j * .5)) - .62; if (dd < 0.) { d = dd + L * 1.2; cell = vec2(i * 2. + mod(j, 2.), j); } else if (d > 0.) d = min(d, dd); } cls = crand(cell, 9.) < .06 + u_variation * .6 ? 1. : 0.; } else if (id == 10) { // waves: ribbons on a sine float y = p.y + .22 * sin(p.x * TAU); float g = length(vec2(.22 * TAU * cos(p.x * TAU), 1.)); d = (abs(fract(y) - .5) - .38 + L) / g; cell = vec2(0., floor(y)); cls = mod(cell.y, 2.); } else if (id == 11) { // truchet: quarter arcs, every cell turned at random cell = floor(p); vec2 f = fract(p) - .5; if (crand(cell, 6.) > .5) f.x = -f.x; float a = abs(length(f - .5) - .5), b = abs(length(f + .5) - .5); d = min(a, b) - (.14 - L * .3); cls = crand(cell, 7.) < .1 + u_variation ? 1. : 0.; } else if (id == 12) { // maze: one diagonal per cell cell = floor(p); vec2 f = fract(p) - .5; if (crand(cell, 8.) > .5) f.x = -f.x; d = abs(f.x + f.y) * .70710678 - (.08 + L * .5); } else if (id == 13) { // cubes: every hexagon three rhombi - top, left, right vec4 h = hexTile(p); float a = atan(h.y, h.x); float deg = mod(degrees(a) + 360., 360.); cls = deg > 30. && deg <= 150. ? 0. : deg > 150. && deg <= 270. ? 1. : 2.; float e = min(.5 - hexDist(h.xy), min(rayDist(h.xy, PI / 6.), min(rayDist(h.xy, PI * 5. / 6.), rayDist(h.xy, PI * 1.5)))); d = L * .6 - e; cell = h.zw * 3. + vec2(cls, 0.); } else if (id == 14) { // circuit: an edge is on or off for both cells it joins, so traces always meet cell = floor(p); vec2 f = fract(p) - .5; float eR = step(.55, crand(cell, 11.)), eL = step(.55, crand(cell - vec2(1., 0.), 11.)); float eT = step(.55, crand(cell, 12.)), eB = step(.55, crand(cell - vec2(0., 1.), 12.)); float tr = 1e5; if (eR > .5) tr = min(tr, sdSeg(f, vec2(0.), vec2(.5, 0.))); if (eL > .5) tr = min(tr, sdSeg(f, vec2(0.), vec2(-.5, 0.))); if (eT > .5) tr = min(tr, sdSeg(f, vec2(0.), vec2(0., .5))); if (eB > .5) tr = min(tr, sdSeg(f, vec2(0.), vec2(0., -.5))); float count = eR + eL + eT + eB; float pad = count == 1. ? length(f) - .17 : count == 0. ? length(f) - .07 : 1e5; tr -= L * .9; d = min(tr, pad); cls = pad < tr ? 1. : 0.; } else if (id == 15) { // topography: contour lines of a noise (periodic around a tunnel) float per = gPer > 0. ? gPer / 3. : 0.; float n = pnoise(p / 3., per) * .75 + pnoise(p / 1.5 + 9.1, per * 2.) * .3; float v = n * 5.; float rv = floor(v + .5); float major = step(abs(mod(rv, 4.)), .5); d = abs(v - rv) / 4.5 - L * (.45 + major * .55); cls = major; cell = vec2(0., rv); } else if (id == 16) { // stripes: bands in two colors, a groove between them cell = vec2(0., floor(p.y)); d = abs(fract(p.y) - .5) - (.5 - L); cls = mod(cell.y, 2.); } else if (id == 17) { // chevron: the stripes folded into zigzags float y = p.y - abs(fract(p.x * .5) - .5) * 2.; cell = vec2(0., floor(y)); d = (abs(fract(y) - .5) - (.5 - L)) * .70710678; cls = mod(cell.y, 2.); } else if (id == 18) { // circles: rings in every cell cell = floor(p); vec2 f = fract(p) - .5; float r = length(f) * 3.; d = max((abs(fract(r) - .5) - .5 + L * 3.) / 3., length(f) - .48); cls = mod(floor(r), 2.); } else if (id == 19) { // octagons with a small square between every four cell = floor(p); vec2 a = abs(fract(p) - .5); float r = .5 - L; float dOct = max(max(a.x, a.y), (a.x + a.y) * .70710678) - r; vec2 g = .5 - a; float dSq = (g.x + g.y) * .70710678 - ((1. - r * 1.41421356) * .70710678 - L * 1.5); if (dSq < dOct) { d = dSq; cls = 1.; cell = floor(p + .5) + vec2(0., 500.); } else { d = dOct; cls = 0.; } } else if (id == 20) { // stars: five points, every other row moved half a cell float row = floor(p.y); vec2 q = vec2(p.x + .5 * mod(row, 2.), p.y); cell = floor(q); vec2 f = fract(q) - .5; float seg = TAU / 5.; float m = abs(mod(atan(f.x, f.y) + 4. * TAU, seg) - seg * .5) / (seg * .5); d = (length(f) - mix(.17, .43 - L, m)) * .7; cls = crand(cell, 13.) < .1 + u_variation ? 1. : 0.; } else if (id == 21) { // stones: rounded cobbles vec3 v = voro(p, .8); d = L * 1.6 + .02 - v.x; cell = v.yz; cls = crand(cell, 14.) < .15 + u_variation ? 1. : 0.; } else if (id == 22) { // cells: the raised borders of an organic net vec3 v = voro(p, .9); d = v.x - L * .7; cell = v.yz; } else if (id == 23) { // glyphs: a 5 x 7 character in every cell, mirrored so it reads like one cell = floor(vec2(p.x, p.y / 1.4)); vec2 q = (vec2(fract(p.x), fract(p.y / 1.4)) - vec2(.12, .08)) / vec2(.76, .84); vec2 gp = floor(q * vec2(5., 7.)); vec2 pf = fract(q * vec2(5., 7.)) - .5; float px = max(abs(pf.x), abs(pf.y)) - .36; d = .1; if (q.x >= 0. && q.x < 1. && q.y >= 0. && q.y < 1. && crand(cell, 15. + min(gp.x, 4. - gp.x) + gp.y * 3.) > .5) d = px * .15; cls = crand(cell, 40.) < .08 + u_variation * .5 ? 1. : 0.; } else if (id == 24) { // crosses cell = floor(p); vec2 f = abs(fract(p) - .5); float arm = .13 - L * .3, len = .4 - L; d = min(max(f.x - len, f.y - arm), max(f.x - arm, f.y - len)); cls = mod(cell.x + cell.y, 2.); } else { // lattice: a diagonal trellis, a stud at every crossing vec2 r = rot(PI * .25) * p * 1.41421356; vec2 g = abs(fract(r) - .5); float dl = (.5 - max(g.x, g.y)) / 1.41421356 - L * .7; float stud = length(fract(r + .5) - .5) / 1.41421356 - (L * 1.6 + .04); cell = floor(r + .5); if (stud < dl) { d = stud; cls = 1.; } else { d = dl; cls = 0.; } } return vec4(d, cls, cell);}float relief(float d, float bev, int mat) { float x = clamp(-d / bev, 0., 1.); return mat == 9 ? sqrt(clamp(-d / (bev * 2.6), 0., 1.)) : x * x * (3. - 2. * x);}// how much of a tile the raised shape covers, per pattern: the far, averaged colorconst float COVER[26] = float[26](.8, .8, .25, .3, .8, .75, .85, .8, .75, .95, .65, .45, .35, .9, .3, .2, .85, .85, .6, .85, .35, .8, .3, .3, .35, .35);bool isLine(int pid) { return pid == 2 || pid == 11 || pid == 12 || pid == 14 || pid == 15 || pid == 22 || pid == 25; }// the light, the view and the camera's frame, shared by the materialsvec3 gV, gLd, gUp;vec3 env(vec3 R, vec3 sc) { float ry = dot(R, gUp); vec3 e = ry > 0. ? mix(mixOk(sc, vec3(1.), .55), mixOk(sc, vec3(.04), .25), pow(ry, .6)) : mix(vec3(.88), color(0) * .22, pow(-ry, .4)); e += smoothstep(.05, 0., abs(ry - .02)) * .55; return e + pow(max(dot(R, gLd), 0.), 24.) * 1.2;}// a material at one point: m = how much of the raised shape covers it, d = the distance to its edge, nl = the// relief's normal in the plane, Nw = the same in the world, detail = 0 for the far average (no fine texture)vec3 shade(int mat, float m, float d, float aa, vec3 nl, vec3 Nw, vec3 sc, float face, vec2 p, vec2 cell, float shine, bool line, float detail) { vec3 V = gV, Ld = gLd; vec3 H = normalize(Ld + V); float diff = max(dot(Nw, Ld), 0.); float nh = max(dot(Nw, H), 0.); float nv = max(dot(Nw, V), 0.); float spec = pow(nh, 48.); float fres = pow(1. - nv, 4.); vec3 R = reflect(-V, Nw); vec3 c0 = color(0); float t = u_time; vec3 col; switch (mat) { case 1: { // flat col = mix(c0, sc * face, m) + shine * .35 * m; break; } case 2: { // emboss: one color, the relief alone draws the pattern col = mixOk(c0, color(1), .12) * (.62 + .48 * diff) + spec * .12 + shine * .25; break; } case 3: { // neon: tubes along the lines and around the shapes float dl = line ? max(d, 0.) : abs(d); float core = (1. - smoothstep(0., .015 + aa * 2., dl)) * detail; float glow = exp(-dl * 14. / max(u_glow, .05)); col = c0 * .07 + sc * glow * .85 * u_glow + mix(sc, vec3(1.), .65) * core + sc * shine * .5; break; } case 4: { // glass: tiles that bend what lies behind them vec2 off = nl.xy * .06; vec3 back = u_has_image > .5 ? image(v_uv + off).rgb : rampCycle(dot(p + off * 8., vec2(.05, .03)) + t * .02); vec3 inside = mix(back, sc, .3) * (.85 + .3 * diff) + spec * .9 + fres * .35; col = mix(mix(back * .5, c0, .5), inside, m) + shine * .3; break; } case 5: { // chrome: a mirror of a studio vec3 e = env(R, sc); col = mix(e * .42, e, m) * face + spec * .8 + shine * .6; break; } case 6: { // ceramic: glazed tiles in matte grout vec3 glaze = sc * face * (.45 + .6 * diff) + pow(nh, 160.) * 1.1 + fres * .2; vec3 grout = mixOk(c0, vec3(.9), .5) * (.8 + .2 * diff); col = mix(grout, glaze, m) + shine * .4 * m; break; } case 7: { // paper: a matte sheet, the pattern pressed and printed in vec3 paper = mixOk(c0, vec3(.95, .92, .85), .35) * (.84 + .22 * diff) * (1. + gnoise(p * 11.) * .04 * detail); col = mix(paper, paper * mix(vec3(1.), sc, .55), m * .85); break; } case 8: { // holo: thin-film colors that turn with the view float hue = dot(Nw, V) * 2.2 + dot(p, vec2(.05, .08)) + t * .08; vec3 rainbow = .55 + .45 * cos(TAU * (hue + vec3(0., .33, .67))); float sparkle = step(.985, hash12(floor(p * 14.) + floor(t * 6.))) * diff * detail; col = mix(mix(c0, rainbow, .25) * .8, mix(rainbow, sc, .25) * (.6 + .5 * diff) + spec * .7 + sparkle, m) + shine * .5; break; } case 9: { // quilted: padded cells, a stitched seam around each float stitch = (1. - smoothstep(0., aa * 2. + .01, abs(d + .056 * max(u_bevel, .02)))) * step(.5, fract((p.x + p.y) * 7.)) * detail; col = mix(c0 * .5, sc * face * (.4 + .7 * diff), m) + stitch * .35 * m; break; } case 10: { // metal: brushed steel, the grain along the tile vec3 base = mixOk(vec3(.74), sc, .22); float brush = gnoise(vec2(p.x * 1.5, p.y * 70.)) * detail; col = mix(c0 * .3 + base * .12, base * (env(R, base) * .55 + .22) * (1. + brush * .14) + spec * .5, m) + shine * .4 * m; break; } case 11: { // gold: a warm mirror vec3 gold = mixOk(vec3(1., .77, .32), sc, .12); col = mix(gold * .16, gold * (env(R, gold) * .7 + .12) + pow(nh, 90.) * gold * 1.2, m) + shine * .5 * m; break; } case 12: { // copper: a reddish mirror, a green patina in the grooves vec3 cu = mixOk(vec3(.93, .5, .34), sc, .1); vec3 patina = mixOk(vec3(.27, .62, .52), c0, .25) * (.55 + .45 * diff); col = mix(patina, cu * (env(R, cu) * .6 + .18) + pow(nh, 70.) * cu, m) + shine * .4 * m; break; } case 13: { // wood: rings across each tile, every tile cut its own way vec2 q = crand(cell, 41.) > .5 ? p.yx : p; float g = q.y * 6. + gnoise(q * vec2(.7, 4.) + cell * 3.1) * 1.4 + crand(cell, 42.) * 20.; float ring = mix(.5, smoothstep(.05, .45, abs(fract(g) - .5)), detail); vec3 light = mixOk(vec3(.8, .57, .34), sc, .14), dark = mixOk(vec3(.44, .25, .12), sc, .1); col = mix(dark * .4, mix(dark, light, ring) * (.55 + .55 * diff) + pow(nh, 70.) * .3, m); break; } case 14: { // marble: polished stone with veins float v = fbm(p * .45 + cell * .37, 4); float vein = (1. - smoothstep(0., .06 + aa * 4., abs(sin((p.x + p.y * .4) * 1.4 + v * 5.)))) * detail; vec3 stone = mixOk(mixOk(vec3(.95, .94, .92), sc, .1), mixOk(c0 * .3, sc, .25), vein * .75 + .1 * (1. - detail)); col = mix(mixOk(vec3(.6), c0, .3), stone * (.72 + .33 * diff) + pow(nh, 120.) * .9 + fres * .15, m); break; } case 15: { // stone: rough and matte float n = (gnoise(p * 6.) * .5 + gnoise(p * 17.) * .25) * detail; vec3 base = mixOk(vec3(.56, .54, .5), sc, .25) * (.86 + n * .3); col = mix(c0 * .25 + base * .1, base * (.32 + .78 * diff), m); break; } case 16: { // rubber: dark and soft vec3 base = mixOk(vec3(.07), sc, .35); col = mix(c0 * .12, base * (.45 + .6 * diff) + pow(nh, 12.) * .1, m); break; } case 17: { // plastic: a clear color, a sharp white highlight col = mix(c0 * .6, sc * face * (.35 + .7 * diff) + pow(nh, 90.) * .9 + fres * .08, m) + shine * .4 * m; break; } case 18: { // velvet: dark facing you, bright at the grazing edges float rim = pow(1. - nv, 2.5); col = mix(c0 * .25, sc * (.22 + .32 * diff) + mixOk(sc, vec3(1.), .4) * rim * .85, m); break; } case 19: { // ice: cold, translucent, cracked vec3 ice = mixOk(vec3(.82, .93, 1.), sc, .2); float crack = (1. - smoothstep(0., .02 + aa * 2., abs(gnoise(p * 2.5 + cell * 1.7)))) * detail; col = mix(ice * .5, ice * (.62 + .3 * diff) - crack * .18 + spec * 1.1 + fres * .5, m); break; } case 20: { // lava: dark rock, the grooves glowing vec3 rock = mixOk(vec3(.12, .1, .1), c0, .1) * (.5 + .7 * diff) * (.85 + gnoise(p * 5.) * .2 * detail); vec3 hot = mix(vec3(1., .22, .02), vec3(1., .82, .3), .5 + .5 * sin(t * 1.5 + gnoise(p * .8) * 6.)); col = mix(hot * u_glow, rock, m) + hot * exp(-abs(d) * 18.) * .45 * u_glow * m; break; } case 21: { // gem: faceted, a deep color, flashes of the spectrum vec3 disp = .5 + .5 * cos(TAU * (nv * 3. + vec3(0., .33, .67))); col = mix(c0 * .22, sc * .38 * (.5 + diff) + disp * fres * .7 + pow(nh, 200.) * 2.2, m); break; } case 22: { // pearl: soft white, a play of colors vec3 base = mixOk(vec3(.96, .94, .92), sc, .15); vec3 irid = .5 + .5 * cos(TAU * (nv * 1.5 + vec3(0., .33, .67))); col = mix(c0 * .5, base * (.55 + .45 * diff) + irid * .14 * (1. - nv) + pow(nh, 30.) * .35, m); break; } case 23: { // clay: matte, the light wrapping round vec3 base = mixOk(sc, vec3(.85, .6, .45), .25); float w = max((dot(Nw, Ld) + .5) / 1.5, 0.); col = mix(base * .45, base * (.28 + .75 * w), m); break; } case 24: { // toon: three tones and an ink outline float lv = diff > .6 ? 1. : diff > .25 ? .72 : .48; col = mix(c0, sc * face * lv + step(.97, nh) * .55, m); col = mix(col, vec3(.05), (1. - smoothstep(aa * 1.5, aa * 3. + .006, abs(d))) * detail); break; } case 25: { // glitter: flecks that twinkle vec2 g = floor(p * 22.); float fleck = step(.955, hash12(g + floor(t * 4. + hash12(g) * 4.))) * (.5 + .5 * diff) * detail; col = mix(c0 * .4, sc * (.4 + .6 * diff) + spec * .4 + fleck * 1.6, m) + shine * .4 * m; break; } case 26: { // wireframe: only the outlines and a fine mesh float edge = (1. - smoothstep(0., aa * 1.5 + .008, abs(d))) * detail; vec2 mg = abs(fract(p * 4. + .5) - .5) / 4.; float mesh = (1. - smoothstep(0., aa * 1.2 + .002, min(mg.x, mg.y))) * detail; col = c0 * .06 + sc * edge + sc * mesh * .2 * m + sc * .08 * m * (1. - detail); break; } case 27: { // x-ray: the slopes bright, the flat dim, on black vec3 bone = mixOk(vec3(.7, .9, 1.), sc, .2); float slope = length(nl.xy); col = vec3(.02, .04, .08) + bone * (m * .16 + slope * .9 + (1. - smoothstep(0., aa * 2. + .01, abs(d))) * .45 * detail); break; } default: { // bevel: raised tiles, lit vec3 tile = sc * face * (.3 + .8 * diff) + spec * .45 + shine * .4; col = mix(c0 * (.55 + .3 * smoothstep(0., .12, d)), tile, m); } } return col;}// the materials that glow on blackbool isDark(int mat) { return mat == 3 || mat == 20 || mat == 26 || mat == 27; }void main() { gMotion = int(u_motion + .5); int proj = int(u_projection + .5), mat = int(u_material + .5), pid = int(u_pattern + .5); float t = u_time; float S = max(u_scale, .5); vec2 uv = centered(); vec2 ptr = (u_pointer - .5) * vec2(aspect(), 1.); vec3 N = vec3(0., 0., 1.), T = vec3(1., 0., 0.), B = vec3(0., 1., 0.); vec3 rt = vec3(1., 0., 0.), up = vec3(0., 1., 0.), fw = vec3(0., 0., -1.), rd = fw; vec2 p; float dist = 0., sky = 0.; bool drift = gMotion == 0; // the drift's direction in the plane: on a flat surface 0 degrees is to the right; on a receding plane the // default (30) runs toward the viewer vec2 D = vec2(cos(radians(u_direction)), sin(radians(u_direction))); vec2 toward = rot(radians(u_direction - 30.)) * vec2(0., -1.); if (proj == 0 || proj == 7) { vec2 q = proj == 7 ? vec2(uv.x * .57735027 + uv.y, -uv.x * .57735027 + uv.y) : uv; p = rot(radians(u_rotation)) * q * S; rd = normalize(vec3(uv * .7, -1.)); if (drift) p += D * t * .41; if (gMotion == 8) p += 2.5 * vec2(cos(t * .2), sin(t * .2)); } else { // a camera over the plane, in the corridor, the tunnel, the dome, or before the ball, the column, the cube float k = S * .25; float yaw = gMotion == 8 ? sin(t * .25) * .45 : 0.; float pitch = 0., fl = 1.1; vec3 ro = vec3(0.); bool spin = drift || gMotion == 5; if (proj == 1) { pitch = .7 + .5 * u_tilt; ro.y = 1.6; } else if (proj == 2) { pitch = .06 + .3 * u_tilt; ro.y = 1.; } else if (proj == 8) { pitch = -.06 - .3 * u_tilt; ro.y = -1.; } else if (proj == 3) { yaw += .3 + .5 * u_tilt; } else if (proj == 9) { pitch = .05 + .12 * u_tilt; ro.y = -.2; } else if (proj == 6 || proj == 10) { ro.z = 3.2; fl = 1.7; } else if (proj == 11) { pitch = -1.15 + .5 * u_tilt; fl = .8; } else if (proj == 12) { ro.z = 4.6; fl = 1.9; } if (drift && (proj == 4 || proj == 5 || proj == 9)) ro.z -= t * .9 / k; fw = vec3(sin(yaw) * cos(pitch), -sin(pitch), -cos(yaw) * cos(pitch)); rt = normalize(vec3(cos(yaw), 0., sin(yaw))); up = cross(rt, fw); rd = normalize(uv.x * rt + uv.y * up + fl * fw); float tt = 400.; vec3 h; if (proj == 1 || proj == 2 || proj == 8) { // a floor (a ceiling): the plane at y = 0 (y = 0 from below); beyond the horizon the sky float dir = proj == 8 ? 1. : -1.; sky = rd.y * dir > 1e-4 ? 0. : 1.; tt = min(rd.y * dir > 1e-4 ? -ro.y / rd.y : 400., 400.); h = ro + rd * tt; p = rot(radians(u_rotation)) * h.xz * k; if (drift) p += toward * t * .9; N = vec3(0., -dir, 0.); T = vec3(1., 0., 0.); B = vec3(0., 0., 1.); } else if (proj == 3) { sky = rd.z < -1e-4 ? 0. : 1.; tt = min(rd.z < -1e-4 ? (-2.2 - ro.z) / rd.z : 400., 400.); h = ro + rd * tt; p = rot(radians(u_rotation)) * h.xy * k * 1.6; if (drift) p += rot(radians(u_direction - 30.)) * vec2(1., 0.) * t * .9; N = vec3(0., 0., 1.); T = vec3(1., 0., 0.); B = vec3(0., 1., 0.); } else if (proj == 4) { // a square corridor: walls, floor and ceiling one unit away float tx = abs(rd.x) > 1e-4 ? (sign(rd.x) - ro.x) / rd.x : 400.; float ty = abs(rd.y) > 1e-4 ? (sign(rd.y) - ro.y) / rd.y : 400.; tt = min(min(tx, ty), 400.); h = ro + rd * tt; if (tx < ty) { p = vec2(h.z, h.y) * k * 2.; N = vec3(-sign(rd.x), 0., 0.); T = vec3(0., 0., 1.); B = vec3(0., 1., 0.); } else { p = vec2(h.x, h.z) * k * 2.; N = vec3(0., -sign(rd.y), 0.); T = vec3(1., 0., 0.); B = vec3(0., 0., 1.); } } else if (proj == 9) { // a hallway: a floor and two walls, open to the sky above them float tf = rd.y < -1e-4 ? (-1. - ro.y) / rd.y : 400.; float tw = abs(rd.x) > 1e-4 ? (sign(rd.x) * 1.6 - ro.x) / rd.x : 400.; bool wall = tw < tf && ro.y + rd.y * tw < 1.4; tt = min(wall ? tw : tf, 400.); sky = !wall && tf >= 400. ? 1. : 0.; h = ro + rd * tt; if (wall) { p = vec2(h.z, h.y) * k * 2.; N = vec3(-sign(rd.x), 0., 0.); T = vec3(0., 0., 1.); B = vec3(0., 1., 0.); } else { p = vec2(h.x, h.z) * k * 2.; N = vec3(0., 1., 0.); T = vec3(1., 0., 0.); B = vec3(0., 0., 1.); } } else if (proj == 5) { gPer = 6. * max(1., floor(S * .5 + .5)); tt = min(1. / max(length(rd.xy), 1e-4), 400.); h = ro + rd * tt; float ang = atan(h.y, h.x); p = vec2(ang / TAU * gPer, h.z * gPer / TAU); N = -normalize(vec3(h.xy, 0.)); T = vec3(-sin(ang), cos(ang), 0.); B = vec3(0., 0., 1.); } else if (proj == 10) { // a column, turning: the pattern wraps around it gPer = 6. * max(1., floor(S * .5 + .5)); float a = dot(rd.xz, rd.xz), b = dot(ro.xz, rd.xz), c = dot(ro.xz, ro.xz) - 1., disc = b * b - a * c; sky = disc > 0. ? 0. : 1.; tt = disc > 0. ? (-b - sqrt(max(disc, 0.))) / max(a, 1e-5) : 3.; h = ro + rd * tt; float ang = atan(h.z, h.x) + (spin ? t * .2 : 0.); p = vec2(ang / TAU * gPer, h.y * gPer / TAU); N = normalize(vec3(h.x, 0., h.z)); T = vec3(-N.z, 0., N.x); B = vec3(0., 1., 0.); } else if (proj == 11) { // a dome overhead: the inside of a ball, seen from its center gPer = 6. * max(1., floor(S * .5 + .5)); h = rd; tt = 1.; float az = atan(h.z, h.x) + (spin ? t * .05 : 0.); p = vec2(az / TAU * gPer, acos(clamp(h.y, -1., 1.)) / PI * gPer * .5); N = -h; T = normalize(vec3(-h.z, 0., h.x) + vec3(1e-5, 0., 0.)); B = cross(N, T); } else if (proj == 12) { // a cube, turning: the pattern on each face float a = spin ? t * .25 : .6; mat2 ry = rot(a), rx = rot(.42); vec3 o = ro, d = rd; o.yz = rx * o.yz; d.yz = rx * d.yz; o.xz = ry * o.xz; d.xz = ry * d.xz; vec3 inv = 1. / (d + vec3(1e-6)); vec3 t0 = (-1. - o) * inv, t1 = (1. - o) * inv; vec3 tn = min(t0, t1), tf = max(t0, t1); float tin = max(max(tn.x, tn.y), tn.z), tout = min(min(tf.x, tf.y), tf.z); sky = tin < tout && tin > 0. ? 0. : 1.; tt = sky > .5 ? 3. : tin; vec3 q = o + d * tt; vec3 aq = abs(q); float kf = max(1., floor(S * .25 + .5)); vec3 nl = aq.x > aq.y && aq.x > aq.z ? vec3(sign(q.x), 0., 0.) : aq.y > aq.z ? vec3(0., sign(q.y), 0.) : vec3(0., 0., sign(q.z)); p = (nl.x != 0. ? q.zy : nl.y != 0. ? q.xz : q.xy) * kf; vec3 tl = nl.x != 0. ? vec3(0., 0., 1.) : vec3(1., 0., 0.); vec3 bl = nl.y != 0. ? vec3(0., 0., 1.) : vec3(0., 1., 0.); // back to the camera's frame: undo the turns mat2 iy = rot(-a), ix = rot(-.42); nl.xz = iy * nl.xz; nl.yz = ix * nl.yz; tl.xz = iy * tl.xz; tl.yz = ix * tl.yz; bl.xz = iy * bl.xz; bl.yz = ix * bl.yz; N = nl; T = tl; B = bl; h = ro + rd * tt; } else { gPer = 6. * max(1., floor(S * .5 + .5)); float b = dot(ro, rd), c = dot(ro, ro) - 1., disc = b * b - c; sky = disc > 0. ? 0. : 1.; tt = disc > 0. ? -b - sqrt(max(disc, 0.)) : 3.; h = ro + rd * tt; vec3 s = normalize(h); if (spin) s.xz = rot(t * .2) * s.xz; p = vec2(atan(s.z, s.x) / TAU * gPer, acos(clamp(s.y, -1., 1.)) / PI * gPer * .5); N = normalize(h); T = normalize(vec3(-h.z, 0., h.x) + vec3(1e-5, 0., 0.)); B = cross(N, T); } if (proj == 4 || proj == 9) p = rot(radians(u_rotation)) * p; dist = tt; } // the motions that move the pattern in its plane vec2 rip = vec2(0.); if (gMotion == 5 && proj != 6 && proj != 10 && proj != 12) p = proj == 5 || proj == 11 ? p + vec2(t * .4, 0.) : rot(t * .08) * p; if (gMotion == 6) p += .3 * vec2(sin(p.y * .45 + t * .9), cos(p.x * .45 + t * .7)); if (gMotion == 9) p *= exp(-.55 * sin(t * .3)); if (gMotion == 10) p = rot(t * .25 + length(p) * .04) * p; if (gMotion == 11) { float w = p.x * .45 + t * 2.; p.y += .45 * sin(w); rip = vec2(-.8 * cos(w), 0.); } if (gMotion == 12) p *= 1. + .06 * sin(t * 1.3); if (gMotion == 13) p = rot(2.2 * exp(-length(p) * .12) * sin(t * .45)) * p; if (gMotion == 17) { vec2 c = floor(p) + .5; p = c + rot(t * .9 + hash12(c) * TAU) * (p - c); } if (gMotion == 20) { float g = floor(t * 7.), band = floor(v_uv.y * 18.); p.x += step(.7, hash12(vec2(band, g))) * step(.5, hash12(vec2(g, 3.))) * (hash12(vec2(band, g + 1.)) - .5) * 3.; } if (gMotion == 23) p += (hash22(vec2(floor(t * 14.), 1.)) - .5) * .35 * step(.75, hash12(vec2(floor(t * 1.2), 9.))); if (gMotion == 4) { vec2 c = u_hover > .01 && proj == 0 ? ptr * S : vec2(0.); vec2 dp = p - c; float r = length(dp) + 1e-4; float w = cos(r * 1.6 - t * 3.) * exp(-r * .12); rip = dp / r * w; p += rip * .1; } // how many tiles one pixel covers: past a tile or so the pattern is only its average - computed before any // branch, so the derivatives stay defined vec2 fwv = fwidth(p); if (gPer > 0.) fwv.x = min(fwv.x, fwidth(mod(p.x, gPer))); float fwp = length(fwv); float lod = smoothstep(.18, .75, fwp); // the light: from u_light degrees around the view (135 = upper left), u_elevation above the surface; the pointer moves it float el = radians(clamp(u_elevation, 1., 89.)); vec2 lp = vec2(cos(radians(u_light)), sin(radians(u_light))) * cos(el); if (u_hover > .01) lp = mix(lp, ptr * 1.8, u_hover); gLd = normalize(rt * lp.x + up * lp.y - fw * sin(el) * 1.2); gV = -rd; gUp = up; vec3 c0 = color(0); bool line = isLine(pid); // the far color: the material over a flat tile, outside and inside the shape, mixed by the pattern's cover vec3 sAvg = mixOk(color(1), color(2), .3); vec3 avg = mix(shade(mat, 0., .06, .05, vec3(0., 0., 1.), N, sAvg, 1., p, vec2(0.), 0., line, 0.), shade(mat, 1., -.06, .05, vec3(0., 0., 1.), N, sAvg, 1., p, vec2(0.), 0., line, 0.), COVER[pid]); vec3 col = avg; float rain = 1., head = 0., bright = 1.; if (lod < 1.) { vec4 P = pattern(p, pid); vec2 cell = P.zw; float cls = P.y; float pulse = gMotion == 2 ? .055 * sin(t * 2.4 - length(p) * .45) : 0.; if (gMotion == 15) { // cascade: the cells rise and sink in a diagonal wave float lift = .5 + .5 * sin(t * 2. - (cell.x + cell.y) * .35); pulse += (1. - lift) * .07; bright = .75 + .35 * lift; } if (gMotion == 16) { // flip: every cell turns over now and then, showing its other color float ph = t * .2 + crand(cell, 51.); float a = smoothstep(.75, 1., fract(ph)) * PI; cls = mod(cls + floor(ph) + step(PI * .5, a), 2.); bright = .35 + .65 * abs(cos(a)); } if (gMotion == 22) { // domino: the cells change color in a wave that runs across the plane float w = t * .25 - (cell.x * .08 + cell.y * .05); cls = mod(cls + floor(w), 2.); bright = 1. + .35 * (1. - smoothstep(0., .12, fract(w))); } float d = P.x + pulse; float aa = max(fwp * .7, 1e-4); float m = 1. - smoothstep(-aa, aa, d); float bev = max(u_bevel, .02) * .14; vec3 nl = vec3(0., 0., 1.); if (mat != 1 && mat != 3 && mat != 26 && fwp < .9) { float e = max(.012, fwp * .5); float h0 = relief(d, bev, mat); float hx = relief(pattern(p + vec2(e, 0.), pid).x + pulse, bev, mat); float hy = relief(pattern(p + vec2(0., e), pid).x + pulse, bev, mat); float slope = bev * u_depth * 1.1; vec2 g = vec2(hx - h0, hy - h0) / e * slope; // a gem's facets: the slope in steps if (mat == 21) g = floor(g * 2.5 + .5) / 2.5; nl = normalize(vec3(-g.x - rip.x * .5, -g.y - rip.y * .5, 1.)); } vec3 Nw = normalize(T * nl.x + B * nl.y + N * nl.z); int ci = int(cls + .5); vec3 sc = color(1 + ci); if (gMotion == 19) sc = rampCycle(cls * .33 + t * .06 + crand(cell, 61.) * u_variation); vec3 lab = toOklab(sc); lab.x += (crand(cell, 21.) - .5) * u_variation * .3; sc = fromOklab(lab); float face = pid == 13 ? (ci == 0 ? 1. : ci == 1 ? .74 : .52) : 1.; float shine = gMotion == 3 ? exp(-pow((dot(p, vec2(.8, .6)) - mod(t * 4., 60.) + 30.) * .35, 2.)) : 0.; if (gMotion == 14) { // rain: each column a falling head, a fading trail behind it - with the glyphs, digital rain float column = floor(p.x); float sp = .6 + hash12(vec2(column, 3.)) * 1.2; float ph = fract(hash12(vec2(column, 1.)) - (p.y + t * sp * 2.2) / 26.); rain = .12 + pow(ph, 5.) * 1.4; head = smoothstep(.965, 1., ph); } vec3 detail = shade(mat, m, d, aa, nl, Nw, sc, face, p, cell, shine, line, 1.) * bright; if (gMotion == 14) detail = detail * rain + mix(sc, vec3(1.), .7) * head * m * 1.4; if (gMotion == 18) { vec2 g = floor(p * 2.), f = fract(p * 2.) - .5; float on = step(.93, hash12(g + floor(t * 3. + hash12(g) * 3.))); float star = exp(-abs(f.x) * 40.) * exp(-abs(f.y) * 7.) + exp(-abs(f.y) * 40.) * exp(-abs(f.x) * 7.); detail += on * star * (.5 + .5 * sin(t * 9. + hash12(g) * 20.)) * 1.2; } col = mix(detail, avg * (gMotion == 14 ? .35 : 1.), lod); } else if (gMotion == 14) col *= .35; if (gMotion == 21) { // scan: a bright line runs down the picture, a glow fading behind it float y = 1. - fract(t * .2); col *= .82 + .3 * exp(-max(v_uv.y - y, 0.) * 7.) * step(y, v_uv.y + .001); col += mixOk(color(1), vec3(1.), .5) * exp(-pow((v_uv.y - y) * 60., 2.)) * .6; } // distance: a fog toward the horizon color; the sky starts from the same color, so the horizon is seamless bool dark = isDark(mat); vec3 fogCol = dark ? mix(c0 * .07, color(1), .16) : mixOk(c0, vec3(1.), .18); bool plane = proj == 1 || proj == 2 || proj == 3 || proj == 8 || proj == 9; if (proj != 0 && proj != 7 && proj != 6 && proj != 10 && proj != 12) { float fog = 1. - exp(-dist * u_fog * .03); // a haze along the horizon line itself, whatever the fog float haze = proj == 1 || proj == 2 || proj == 8 ? exp(-abs(rd.y) * 22.) * .85 : proj == 3 ? exp(-abs(rd.z) * 22.) * .85 : 0.; // a dark material's far field is mostly its dark ground: where the pattern is gone it sinks into the fog if (dark) haze = max(haze, lod * .75); col = mix(col, fogCol, max(fog, haze)); } vec3 skyTop = dark ? c0 * .03 : mixOk(c0, color(1), .12); vec3 skyCol = proj == 6 || proj == 10 || proj == 12 ? c0 * (.92 - .25 * length(uv)) : mix(fogCol, skyTop, smoothstep(0., .5, proj == 8 ? -rd.y : proj == 3 ? rd.z : rd.y)); col = mix(col, skyCol, sky); // a dark scene's horizon glows - on both sides of the line, so the floor brightens into it with no edge if (dark && plane) col += color(1) * exp(-abs(proj == 8 ? -rd.y : proj == 3 ? rd.z : rd.y) * 38.) * .7; // where the detail is finer than a pixel, the engine may blend in the last frame (data-smooth): the alpha says how much float hint = sky > .5 ? 0. : smoothstep(.15, .7, fwp); fragColor = vec4(dither(col), u_temporal > .5 ? hint : 1.);}`, },};/** The defaults of a custom shader - an element's own <script type="x-shader/x-fragment">. */const CUSTOM: Omit<ShaderEffect, 'fragment'> = { colors: THEME, background: 'var(--background)', still: 0 };// -- the simulations: fluid and trails run passes into float targets --------const SIM = { advect: `uniform sampler2D u_velocity, u_source; uniform vec2 u_texel; uniform float u_dt, u_dissipation;void main() { vec2 coord = v_uv - u_dt * texture(u_velocity, v_uv).xy * u_texel; fragColor = texture(u_source, coord) / (1. + u_dissipation * u_dt); }`, divergence: `uniform sampler2D u_velocity; uniform vec2 u_texel;void main() { vec2 C = texture(u_velocity, v_uv).xy; float L = texture(u_velocity, v_uv - vec2(u_texel.x, 0.)).x, R = texture(u_velocity, v_uv + vec2(u_texel.x, 0.)).x; float B = texture(u_velocity, v_uv - vec2(0., u_texel.y)).y, T = texture(u_velocity, v_uv + vec2(0., u_texel.y)).y; if (v_uv.x - u_texel.x < 0.) L = -C.x; if (v_uv.x + u_texel.x > 1.) R = -C.x; if (v_uv.y - u_texel.y < 0.) B = -C.y; if (v_uv.y + u_texel.y > 1.) T = -C.y; fragColor = vec4(.5 * (R - L + T - B), 0., 0., 1.);}`, curl: `uniform sampler2D u_velocity; uniform vec2 u_texel;void main() { float L = texture(u_velocity, v_uv - vec2(u_texel.x, 0.)).y, R = texture(u_velocity, v_uv + vec2(u_texel.x, 0.)).y; float B = texture(u_velocity, v_uv - vec2(0., u_texel.y)).x, T = texture(u_velocity, v_uv + vec2(0., u_texel.y)).x; fragColor = vec4(.5 * (R - L - T + B), 0., 0., 1.);}`, vorticity: `uniform sampler2D u_velocity, u_curl; uniform vec2 u_texel; uniform float u_strength, u_dt;void main() { float L = texture(u_curl, v_uv - vec2(u_texel.x, 0.)).x, R = texture(u_curl, v_uv + vec2(u_texel.x, 0.)).x; float B = texture(u_curl, v_uv - vec2(0., u_texel.y)).x, T = texture(u_curl, v_uv + vec2(0., u_texel.y)).x; float C = texture(u_curl, v_uv).x; vec2 force = .5 * vec2(abs(T) - abs(B), abs(R) - abs(L)); force = force / (length(force) + .0001) * u_strength * C * vec2(1., -1.); fragColor = vec4(clamp(texture(u_velocity, v_uv).xy + force * u_dt, -1000., 1000.), 0., 1.);}`, pressure: `uniform sampler2D u_pressure, u_divergence; uniform vec2 u_texel;void main() { float L = texture(u_pressure, v_uv - vec2(u_texel.x, 0.)).x, R = texture(u_pressure, v_uv + vec2(u_texel.x, 0.)).x; float B = texture(u_pressure, v_uv - vec2(0., u_texel.y)).x, T = texture(u_pressure, v_uv + vec2(0., u_texel.y)).x; fragColor = vec4((L + R + B + T - texture(u_divergence, v_uv).x) * .25, 0., 0., 1.);}`, gradient: `uniform sampler2D u_pressure, u_velocity; uniform vec2 u_texel;void main() { float L = texture(u_pressure, v_uv - vec2(u_texel.x, 0.)).x, R = texture(u_pressure, v_uv + vec2(u_texel.x, 0.)).x; float B = texture(u_pressure, v_uv - vec2(0., u_texel.y)).x, T = texture(u_pressure, v_uv + vec2(0., u_texel.y)).x; fragColor = vec4(texture(u_velocity, v_uv).xy - .5 * vec2(R - L, T - B), 0., 1.);}`, splat: `uniform sampler2D u_target; uniform vec2 u_point; uniform vec3 u_value; uniform float u_radius;void main() { vec2 d = (v_uv - u_point) * vec2(aspect(), 1.); fragColor = vec4(texture(u_target, v_uv).xyz + exp(-dot(d, d) / u_radius) * u_value, 1.);}`, scale: `uniform sampler2D u_target; uniform float u_value;void main() { fragColor = u_value * texture(u_target, v_uv); }`, fluid: `uniform sampler2D u_dye; uniform vec2 u_texel; uniform float u_shading;void main() { vec3 c = texture(u_dye, v_uv).rgb; float l = length(texture(u_dye, v_uv - vec2(u_texel.x, 0.)).rgb), r = length(texture(u_dye, v_uv + vec2(u_texel.x, 0.)).rgb); float b = length(texture(u_dye, v_uv - vec2(0., u_texel.y)).rgb), t = length(texture(u_dye, v_uv + vec2(0., u_texel.y)).rgb); vec3 n = normalize(vec3(l - r, b - t, .25)); c *= mix(1., clamp(dot(n, normalize(vec3(-.3, .4, 1.))) + .25, .6, 1.15), u_shading); float a = clamp(max(c.r, max(c.g, c.b)), 0., 1.); fragColor = vec4(dither(c / max(a, .001)) * a, a) + backdrop() * (1. - a);}`, trail: `uniform sampler2D u_prev; uniform vec2 u_a, u_b; uniform float u_brush, u_fade, u_flow, u_strength;void main() { vec2 flow = (vec2(gnoise(v_uv * 4. + u_time * .3), gnoise(v_uv * 4. - u_time * .3 + 9.)) * .0016 + vec2(0., .0005)) * u_flow; vec3 prev = texture(u_prev, v_uv - flow).rgb * u_fade; vec2 k = vec2(aspect(), 1.), a = u_a * k, ab = u_b * k - a, x = v_uv * k; float h = clamp(dot(x - a, ab) / max(dot(ab, ab), 1e-6), 0., 1.); float r = u_brush * .065, d = length(x - a - ab * h); fragColor = vec4(prev + rampCycle(u_time * .22) * exp(-d * d / (r * r)) * u_strength, 1.);}`, trails: `uniform sampler2D u_trail; uniform vec2 u_texel;void main() { vec3 c = texture(u_trail, v_uv).rgb; vec3 halo = (texture(u_trail, v_uv + vec2(u_texel.x, 0.) * 3.).rgb + texture(u_trail, v_uv - vec2(u_texel.x, 0.) * 3.).rgb + texture(u_trail, v_uv + vec2(0., u_texel.y) * 3.).rgb + texture(u_trail, v_uv - vec2(0., u_texel.y) * 3.).rgb) * .25; c = c + halo * .35; c = 1. - exp(-c * 2.4); float a = clamp(max(c.r, max(c.g, c.b)), 0., 1.); fragColor = vec4(dither(c / max(a, .001)) * a, a) + backdrop() * (1. - a);}`,};/** * The post passes. FXAA (after Lottes' console FXAA): one pass that blurs only along the edges it finds in the * luma, cheaper than drawing the effect at a higher resolution - the effect's own fwidth() smoothing cannot see * the edges between cells, a projection's silhouettes or a material's highlights. temporal: an exponential * moving average of the frames, weighted per pixel by the effect's alpha, which an effect that opts in sets to how * much finer than a pixel its detail is - the moire near a horizon settles, the near tiles stay sharp. */const POST = { fxaa: `uniform sampler2D u_scene, u_history; uniform vec2 u_texel; uniform float u_fxaa, u_smooth, u_has_history;vec4 fxaa(vec2 uv) { vec4 m = texture(u_scene, uv); vec3 lw = vec3(.299, .587, .114); float lNW = dot(texture(u_scene, uv + vec2(-1., 1.) * u_texel).rgb, lw); float lNE = dot(texture(u_scene, uv + vec2(1., 1.) * u_texel).rgb, lw); float lSW = dot(texture(u_scene, uv + vec2(-1., -1.) * u_texel).rgb, lw); float lSE = dot(texture(u_scene, uv + vec2(1., -1.) * u_texel).rgb, lw); float lM = dot(m.rgb, lw); float lMin = min(lM, min(min(lNW, lNE), min(lSW, lSE))); float lMax = max(lM, max(max(lNW, lNE), max(lSW, lSE))); if (lMax - lMin < max(.0312, lMax * .125)) return m; vec2 dir = vec2(-((lNW + lNE) - (lSW + lSE)), (lNW + lSW) - (lNE + lSE)); float reduce = max((lNW + lNE + lSW + lSE) * .03125, 1. / 128.); dir = clamp(dir / (min(abs(dir.x), abs(dir.y)) + reduce), -8., 8.) * u_texel; vec4 a = .5 * (texture(u_scene, uv + dir * (1. / 3. - .5)) + texture(u_scene, uv + dir * (2. / 3. - .5))); vec4 b = a * .5 + .25 * (texture(u_scene, uv - dir * .5) + texture(u_scene, uv + dir * .5)); float lB = dot(b.rgb, lw); return lB < lMin || lB > lMax ? a : b;}void main() { vec4 scene = texture(u_scene, v_uv); vec4 c = u_fxaa > .5 ? fxaa(v_uv) : scene; if (u_smooth > 0.) { float w = u_has_history * u_smooth * scene.a; c = vec4(mix(c.rgb, texture(u_history, v_uv).rgb, w), 1.); } fragColor = c;}`, blit: `uniform sampler2D u_scene;void main() { fragColor = texture(u_scene, v_uv); }`,};// ---------------------------------------------------------------------------// The engine: one context, the programs, the frame loop.// ---------------------------------------------------------------------------const GL = globalThis.WebGL2RenderingContext;const BUILTIN = new Set([ 'u_resolution', 'u_time', 'u_delta', 'u_pointer', 'u_hover', 'u_colors', 'u_color_count', 'u_background', 'u_image', 'u_has_image', 'u_seed', 'u_pixel_ratio', 'u_frame', 'u_taps', 'u_tap_count', 'u_temporal',]);const engine = { gl: null, canvas: null, unsupported: false, floatTargets: false, vao: null, blank: null, programs: new Map<string, any>(), /** KHR_parallel_shader_compile: programs link off the main thread, polled each frame */ parallel: null as { COMPLETION_STATUS_KHR: number } | null, /** when this frame's synchronous compiles must stop (no parallel compile): later programs wait a frame */ compileUntil: 0, instances: new Set<any>(), raf: 0, frames: 0, scale: 1, /** the step of QUALITY_LEVELS the scale is on */ level: 0, /** when the level last went up; a step down soon after holds the level for good (held) */ raisedAt: -Infinity, held: false, /** good windows in a row since the last change */ good: 0, lastTick: 0, /** the frames of the current measuring window, when it started and the late ones among them */ window: 0, windowStart: 0, slow: 0, idleSince: 0, /** in the viewport: the instance draws */ io: null, /** within 20% of the viewport: the instance is prepared (its program compiled, its controls built), not drawn */ near: null, ro: null, controls: new Set<HTMLElement>(),};const reducedMotion = () => !!globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches;const num = (v, fallback) => { const n = v === undefined || v === null || v === '' ? NaN : Number(v); return Number.isFinite(n) ? n : fallback;};/** The shared context, created with the first visible effect. null without WebGL2. */function context() { if (engine.gl) return engine.gl.isContextLost() ? null : engine.gl; if (engine.unsupported) return null; const canvas = document.createElement('canvas'); const gl = canvas.getContext('webgl2', { alpha: true, premultipliedAlpha: true, antialias: false, depth: false, stencil: false, // the frame is copied out with drawImage after every draw - the buffer is never presented preserveDrawingBuffer: true, }); if (!gl) { engine.unsupported = true; return null; } canvas.addEventListener('webglcontextlost', (e) => { e.preventDefault(); forget(); }); canvas.addEventListener('webglcontextrestored', () => { setup(gl); for (const inst of engine.instances) inst.needsFrame = true; schedule(); }); engine.gl = gl; engine.canvas = canvas; setup(gl); return gl;}/** The context's shared resources: the one triangle that covers the viewport, a blank texture. */function setup(gl) { engine.floatTargets = !!(gl.getExtension('EXT_color_buffer_float') || gl.getExtension('EXT_color_buffer_half_float')); engine.parallel = gl.getExtension('KHR_parallel_shader_compile'); const buffer = gl.createBuffer(); gl.bindBuffer(GL.ARRAY_BUFFER, buffer); gl.bufferData(GL.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), GL.STATIC_DRAW); engine.vao = gl.createVertexArray(); gl.bindVertexArray(engine.vao); gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, GL.FLOAT, false, 0, 0); engine.blank = gl.createTexture(); gl.bindTexture(GL.TEXTURE_2D, engine.blank); gl.texImage2D(GL.TEXTURE_2D, 0, GL.RGBA, 1, 1, 0, GL.RGBA, GL.UNSIGNED_BYTE, new Uint8Array(4));}/** Everything the context held is gone (lost or released): instances rebuild on their next frame. */function forget() { engine.programs.clear(); engine.vao = null; for (const inst of engine.instances) { inst.texture = null; inst.sim = null; inst.post = null; inst.imageDirty = true; inst.prog = null; inst.dirty = true; }}/** Release the context after a while with nothing in view - offscreen effects keep their last frame. */function release() { if (!engine.gl) return; engine.gl.getExtension('WEBGL_lose_context')?.loseContext(); engine.gl = null; engine.canvas = null; forget();}function compile(gl, type, source, check = true) { const shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); if (check && !gl.getShaderParameter(shader, GL.COMPILE_STATUS)) { const log = gl.getShaderInfoLog(shader) || 'compile error'; gl.deleteShader(shader); throw new Error(log.trim()); } return shader;}/** * A program for a fragment shader (prelude first unless it brings its own #version), compiled once per context. * With `lazy` it never blocks the page: with KHR_parallel_shader_compile the driver links it off the main thread * and the entry stays `pending` until ready() sees it done; without it, a frame compiles only until its budget * is spent and a later program returns null (asked again next frame). Forty effects on one page once froze it for * 6.7 s on a GPU, 20 s on a software one, compiling all at once. */function program(fragment, lazy = false) { const gl = engine.gl; const source = /^\s*#version/.test(fragment) ? fragment : PRELUDE + fragment; const known = engine.programs.get(source); if (known) return known; const parallel = lazy && !!engine.parallel; if (lazy && !parallel && engine.compileUntil && performance.now() > engine.compileUntil) return null; let entry; try { const prog = gl.createProgram(); const shaders = [ compile(gl, GL.VERTEX_SHADER, VERTEX, !parallel), compile(gl, GL.FRAGMENT_SHADER, source, !parallel), ]; for (const shader of shaders) gl.attachShader(prog, shader); gl.bindAttribLocation(prog, 0, 'a_position'); gl.linkProgram(prog); entry = { prog, shaders, uniforms: new Map(), params: [], error: '', pending: true }; if (!parallel) link(entry); } catch (err) { entry = { prog: null, uniforms: new Map(), params: [], error: err instanceof Error ? err.message : String(err) }; } engine.programs.set(source, entry); return entry;}/** A linked program's uniforms and data-* parameters - or its error, from the compile and link logs. */function link(entry) { const gl = engine.gl; const prog = entry.prog; entry.pending = false; if (!gl.getProgramParameter(prog, GL.LINK_STATUS)) { const logs = entry.shaders .filter((sh) => !gl.getShaderParameter(sh, GL.COMPILE_STATUS)) .map((sh) => gl.getShaderInfoLog(sh) || ''); entry.error = (logs.join('\n').trim() || gl.getProgramInfoLog(prog) || 'link error').trim(); entry.prog = null; return; } const count = gl.getProgramParameter(prog, GL.ACTIVE_UNIFORMS); for (let i = 0; i < count; i++) { const info = gl.getActiveUniform(prog, i); const name = info.name.replace(/\[0\]$/, ''); entry.uniforms.set(name, { loc: gl.getUniformLocation(prog, name), type: info.type, size: info.size }); if (!BUILTIN.has(name) && info.type === GL.FLOAT && info.size === 1 && name.startsWith('u_')) entry.params.push(name); }}/** Whether a program is usable now: a parallel compile is polled (never waited for); a finished one is linked. */function ready(entry) { if (!entry) return false; if (!entry.pending) return true; if (!engine.gl.getProgramParameter(entry.prog, engine.parallel!.COMPLETION_STATUS_KHR)) return false; link(entry); return true;}/** Set a program's uniforms from a name map: numbers, vec arrays, textures (bound to units in order). */function setUniforms(entry, values) { const gl = engine.gl; let unit = 0; for (const name in values) { const u = entry.uniforms.get(name); if (!u) continue; const v = values[name]; if (v instanceof WebGLTexture) { gl.activeTexture(GL.TEXTURE0 + unit); gl.bindTexture(GL.TEXTURE_2D, v); gl.uniform1i(u.loc, unit++); } else if (typeof v === 'number') { if (u.type === GL.INT || u.type === GL.BOOL) gl.uniform1i(u.loc, v); else gl.uniform1f(u.loc, v); } else if (u.type === GL.FLOAT_VEC2) gl.uniform2fv(u.loc, v); else if (u.type === GL.FLOAT_VEC3) gl.uniform3fv(u.loc, v); else if (u.type === GL.FLOAT_VEC4) gl.uniform4fv(u.loc, v); }}/** The uniforms every effect may read. */function common(inst, w, h) { return { u_resolution: [w, h], u_time: inst.time, u_delta: inst.delta, u_pointer: inst.pointer, u_hover: inst.hover, u_colors: inst.liveColors, u_color_count: inst.colorCount, u_background: inst.background, u_has_image: inst.hasImage, u_seed: inst.seed, u_pixel_ratio: inst.scale, u_frame: [inst.frame[0] * inst.scale, inst.frame[1] * inst.scale], u_taps: inst.tapData, u_tap_count: inst.taps.length, u_temporal: inst.temporalOn ? 1 : 0, };}/** One full-viewport pass of a program into a target (null: the shared canvas at the instance's size). */function pass(inst, entry, target, values) { const gl = engine.gl; const w = target ? target.w : inst.w; const h = target ? target.h : inst.h; gl.bindFramebuffer(GL.FRAMEBUFFER, target ? target.fbo : null); gl.viewport(0, 0, w, h); gl.useProgram(entry.prog); gl.bindVertexArray(engine.vao); setUniforms(entry, { ...common(inst, w, h), u_image: inst.texture || engine.blank, ...values }); gl.drawArrays(GL.TRIANGLES, 0, 3);}// -- simulation targets ---------------------------------------------------------function target(w, h, float) { const gl = engine.gl; const tex = gl.createTexture(); gl.bindTexture(GL.TEXTURE_2D, tex); gl.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_MIN_FILTER, GL.LINEAR); gl.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_MAG_FILTER, GL.LINEAR); gl.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_WRAP_S, GL.CLAMP_TO_EDGE); gl.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_WRAP_T, GL.CLAMP_TO_EDGE); if (float) gl.texImage2D(GL.TEXTURE_2D, 0, GL.RGBA16F, w, h, 0, GL.RGBA, GL.HALF_FLOAT, null); else gl.texImage2D(GL.TEXTURE_2D, 0, GL.RGBA, w, h, 0, GL.RGBA, GL.UNSIGNED_BYTE, null); const fbo = gl.createFramebuffer(); gl.bindFramebuffer(GL.FRAMEBUFFER, fbo); gl.framebufferTexture2D(GL.FRAMEBUFFER, GL.COLOR_ATTACHMENT0, GL.TEXTURE_2D, tex, 0); gl.viewport(0, 0, w, h); gl.clearColor(0, 0, 0, 0); gl.clear(GL.COLOR_BUFFER_BIT); return { tex, fbo, w, h };}function swapTarget(w, h, float) { const t = { read: target(w, h, float), write: target(w, h, float), swap() {} }; t.swap = () => { const r = t.read; t.read = t.write; t.write = r; }; return t;}function dropSim(inst) { const gl = engine.gl; const sim = inst.sim; const post = inst.post; inst.sim = null; inst.post = null; if (!gl) return; for (const t of [...(sim?.targets || []), ...(post?.targets || [])]) { gl.deleteTexture(t.tex); gl.deleteFramebuffer(t.fbo); }}/** The targets of the post passes at the render size: the scene, and the history when frames are blended. */function postTargets(inst, temporal) { const { w, h } = inst; let post = inst.post; if (!post || post.w !== w || post.h !== h || !!post.history !== temporal) { dropSim(inst); const scene = target(w, h, false); const history = temporal ? swapTarget(w, h, false) : null; post = inst.post = { w, h, scene, history, fresh: true, targets: [scene, ...(history ? [history.read, history.write] : [])], }; } return post;}/** Draw an instance through the post passes: the effect into the scene, FXAA (and the blend) onto the canvas. */function drawPost(inst, values) { const programs = { fxaa: program(POST.fxaa), blit: program(POST.blit) }; const bad = Object.values(programs).find((p) => p.error); if (bad) return fail(inst, bad.error); const temporal = inst.smooth > 0 && !reducedMotion(); inst.temporalOn = temporal; const post = postTargets(inst, temporal); pass(inst, inst.prog, post.scene, values); const blend = { u_scene: post.scene.tex, u_history: post.history ? post.history.read.tex : engine.blank, u_texel: [1 / inst.w, 1 / inst.h], u_fxaa: inst.aa === 'fxaa' ? 1 : 0, u_smooth: temporal ? inst.smooth : 0, u_has_history: post.fresh ? 0 : 1, }; post.fresh = false; if (!post.history) return pass(inst, programs.fxaa, null, blend); // the blended frame is the next frame's history: draw it into the history, then show it pass(inst, programs.fxaa, post.history.write, blend); pass(inst, programs.blit, null, { u_scene: post.history.write.tex }); post.history.swap();}const SIM_PROGRAMS = () => { const out: Record<string, any> = {}; for (const key in SIM) out[key] = program(SIM[key]); return out;};/** A deterministic random for the simulations (the seed and a counter - the same splats every run). */function simRandom(sim) { sim.n = (sim.n * 1664525 + 1013904223) >>> 0; return sim.n / 4294967296;}/** The ink color for a splat: one of the colors, in turn. */function splatColor(inst, sim, gain) { const i = (sim.colorIndex++ % Math.max(inst.colorCount, 1)) * 3; return [inst.colors[i] * gain, inst.colors[i + 1] * gain, inst.colors[i + 2] * gain];}function fluidStep(inst, dt) { if (!engine.floatTargets) return fail(inst, 'fluid needs float render targets (EXT_color_buffer_float)'); const P = SIM_PROGRAMS(); const bad = Object.values(P).find((p: { error: string }) => p.error) as { error: string } | undefined; if (bad) return fail(inst, bad.error); const quality = inst.quality === 'low' ? 0.6 : inst.quality === 'high' ? 1.4 : 1; const ratio = inst.w / inst.h; const simShort = Math.round(128 * quality); const dyeShort = Math.round(Math.min(512 * quality, Math.min(inst.w, inst.h))); const dims = (short) => (ratio >= 1 ? [Math.round(short * ratio), short] : [short, Math.round(short / ratio)]); const [sw, sh] = dims(simShort); const [dw, dh] = dims(dyeShort); let sim = inst.sim; if (!sim || sim.kind !== 'fluid' || sim.sw !== sw || sim.dw !== dw) { dropSim(inst); const vel = swapTarget(sw, sh, true); const dye = swapTarget(dw, dh, true); const pressure = swapTarget(sw, sh, true); const div = target(sw, sh, true); const curl = target(sw, sh, true); sim = inst.sim = { kind: 'fluid', sw, sh, dw, vel, dye, pressure, div, curl, targets: [vel.read, vel.write, dye.read, dye.write, pressure.read, pressure.write, div, curl], n: (inst.seed * 7919 + 17) >>> 0, colorIndex: 0, nextAuto: 0, warm: false, }; } const texel = [1 / sw, 1 / sh]; const prm = inst.params; const splat = (x, y, dx, dy, color) => { const radius = Math.max(prm.u_radius ?? 0.4, 0.01) / 100; pass(inst, P.splat, sim.vel.write, { u_target: sim.vel.read.tex, u_point: [x, y], u_value: [dx, dy, 0], u_radius: radius, }); sim.vel.swap(); pass(inst, P.splat, sim.dye.write, { u_target: sim.dye.read.tex, u_point: [x, y], u_value: color, u_radius: radius, }); sim.dye.swap(); }; const randomSplats = (count) => { for (let i = 0; i < count; i++) { const x = 0.15 + simRandom(sim) * 0.7; const y = 0.15 + simRandom(sim) * 0.7; splat(x, y, (simRandom(sim) - 0.5) * 2600, (simRandom(sim) - 0.5) * 2600, splatColor(inst, sim, 2.2)); } }; const step = (h) => { pass(inst, P.curl, sim.curl, { u_velocity: sim.vel.read.tex, u_texel: texel }); pass(inst, P.vorticity, sim.vel.write, { u_velocity: sim.vel.read.tex, u_curl: sim.curl.tex, u_texel: texel, u_strength: prm.u_curl ?? 22, u_dt: h, }); sim.vel.swap(); pass(inst, P.divergence, sim.div, { u_velocity: sim.vel.read.tex, u_texel: texel }); pass(inst, P.scale, sim.pressure.write, { u_target: sim.pressure.read.tex, u_value: 0.8 }); sim.pressure.swap(); for (let i = 0; i < 20; i++) { pass(inst, P.pressure, sim.pressure.write, { u_pressure: sim.pressure.read.tex, u_divergence: sim.div.tex, u_texel: texel, }); sim.pressure.swap(); } pass(inst, P.gradient, sim.vel.write, { u_pressure: sim.pressure.read.tex, u_velocity: sim.vel.read.tex, u_texel: texel, }); sim.vel.swap(); pass(inst, P.advect, sim.vel.write, { u_velocity: sim.vel.read.tex, u_source: sim.vel.read.tex, u_texel: texel, u_dt: h, u_dissipation: 0.2, }); sim.vel.swap(); pass(inst, P.advect, sim.dye.write, { u_velocity: sim.vel.read.tex, u_source: sim.dye.read.tex, u_texel: texel, u_dt: h, u_dissipation: prm.u_dissipation ?? 0.6, }); sim.dye.swap(); }; if (!sim.warm) { // the still frame: a few splats, played forward a second sim.warm = true; randomSplats(8); for (let i = 0; i < 60; i++) step(1 / 60); } for (const s of inst.splats.splice(0)) splat(s.x, s.y, s.dx * 5000, s.dy * 5000, splatColor(inst, sim, 0.4)); if (dt > 0) { sim.nextAuto -= dt; if (sim.nextAuto <= 0 && inst.hover < 0.5) { randomSplats(1 + Math.floor(simRandom(sim) * 2)); sim.nextAuto = 1.6 + simRandom(sim) * 1.4; } step(Math.min(dt, 1 / 30)); } pass(inst, P.fluid, null, { u_dye: sim.dye.read.tex, u_texel: [1 / dw, 1 / dh], u_shading: prm.u_shading ?? 0.6 });}function trailsStep(inst, dt) { const P = SIM_PROGRAMS(); const bad = Object.values(P).find((p: { error: string }) => p.error) as { error: string } | undefined; if (bad) return fail(inst, bad.error); const tw = Math.max(1, Math.round(inst.w / 2)); const th = Math.max(1, Math.round(inst.h / 2)); let sim = inst.sim; if (!sim || sim.kind !== 'trails' || sim.w !== tw || sim.h !== th) { dropSim(inst); const trail = swapTarget(tw, th, engine.floatTargets); sim = inst.sim = { kind: 'trails', w: tw, h: th, trail, targets: [trail.read, trail.write], last: null, warm: false, clock: 0, }; } const prm = inst.params; const auto = (c) => [0.5 + 0.34 * Math.sin(c * 1.3), 0.5 + 0.3 * Math.sin(c * 1.9 + 1)]; const draw = (a, b, h) => { const len = Math.hypot(b[0] - a[0], b[1] - a[1]); pass(inst, P.trail, sim.trail.write, { u_prev: sim.trail.read.tex, u_a: a, u_b: b, u_brush: prm.u_brush ?? 1, u_fade: Math.pow(Math.min(prm.u_decay ?? 0.965, 0.999), h * 60), u_flow: prm.u_flow ?? 1, u_strength: Math.min(Math.max(len * 40, 0.12), 1.2), }); sim.trail.swap(); }; if (!sim.warm) { // the still frame: the automatic path, drawn for two seconds sim.warm = true; let prev = auto(0); for (let i = 1; i <= 120; i++) { const next = auto(i / 60); draw(prev, next, 1 / 60); prev = next; } sim.clock = 2; sim.last = prev; } if (dt > 0 || inst.hover > 0.01) { const h = Math.max(dt, 1 / 120); sim.clock += dt; const next = inst.hover > 0.5 ? [...inst.pointer] : auto(sim.clock); draw(sim.last ?? next, next, h); sim.last = next; } pass(inst, P.trails, null, { u_trail: sim.trail.read.tex, u_texel: [1 / tw, 1 / th] });}const SIMS = { fluid: fluidStep, trails: trailsStep };// -- instances ------------------------------------------------------------------/** The element's own fragment shader: a <script type="x-shader/x-fragment"> child. */function customSource(el) { const script = Array.from(el.children).find( (c) => c instanceof HTMLScriptElement && /x-shader\/x-fragment/i.test(c.type), ); return script ? (script as HTMLScriptElement).textContent || '' : '';}/** The picture an effect may read: the first img, svg, video or canvas child. */function sourceOf(el) { return ( Array.from(el.children).find( (c) => (c instanceof HTMLImageElement || c instanceof HTMLVideoElement || c instanceof SVGSVGElement || c instanceof HTMLCanvasElement) && !c.classList.contains('shader-canvas'), ) || null );}/** CSS colors separated by spaces or commas outside parentheses. */function splitColors(text) { const out = []; let depth = 0; let cur = ''; for (const ch of text) { if (ch === '(') depth++; if (ch === ')') depth--; if (depth === 0 && (ch === ',' || /\s/.test(ch))) { if (cur) out.push(cur); cur = ''; } else cur += ch; } if (cur) out.push(cur); return out;}let probeContext = null;/** Any computed CSS color (oklch, color(), rgb) as sRGB floats [r, g, b, a]. */function rgba(css) { if (!probeContext) { const c = document.createElement('canvas'); c.width = c.height = 1; probeContext = c.getContext('2d', { willReadFrequently: true }); } const ctx = probeContext; ctx.clearRect(0, 0, 1, 1); ctx.fillStyle = '#000'; ctx.fillStyle = css; ctx.fillRect(0, 0, 1, 1); const d = ctx.getImageData(0, 0, 1, 1).data; return [d[0] / 255, d[1] / 255, d[2] / 255, d[3] / 255];}/** Resolve data-colors / data-background (or the effect's defaults) inside the element - its tokens apply. */function resolveColors(inst) { inst.colorsDirty = false; const el = inst.el; const def = inst.def; const list = el.dataset.colors ? splitColors(el.dataset.colors) : def.colors || THEME; const probe = document.createElement('span'); probe.className = 'shader-probe'; probe.style.display = 'none'; el.append(probe); const read = (value) => { probe.style.color = ''; probe.style.color = value; return probe.style.color ? rgba(getComputedStyle(probe).color) : null; }; const colors = list.map(read).filter(Boolean).slice(0, 8); if (def.sortColors) colors.sort( (a, b) => a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722 - (b[0] * 0.2126 + b[1] * 0.7152 + b[2] * 0.0722), ); inst.colors = new Float32Array(24); colors.forEach((c, i) => inst.colors.set(c.slice(0, 3), i * 3)); inst.colorCount = colors.length; inst.background = read(el.dataset.background ?? def.background ?? 'transparent') || [0, 0, 0, 0]; probe.remove();}/** The ring a border-variant effect draws around: its inset (the glow) and the host's corner radius, in CSS px. */function measureFrame(inst) { const el = inst.el; if (inst.variant === 'border') { const host = el.parentElement; const inset = -parseFloat(getComputedStyle(el).top) || 0; inst.frame = [Math.max(inset, 0), host ? parseFloat(getComputedStyle(host).borderTopLeftRadius) || 0 : 0]; } else inst.frame = [Math.min(inst.cssW, inst.cssH) * 0.1, 14];}/** Read the element's attributes: effect, program, params, timing, quality. */function refresh(inst) { const el = inst.el; const d = el.dataset; const custom = customSource(el); const name = d.effect || (custom ? 'custom' : 'mesh-gradient'); const def = name === 'custom' ? (custom ? { ...CUSTOM, fragment: custom } : null) : EFFECTS[name]; // the program first: until it is linked the instance keeps what it drew (an effect change: the old one) const prog = def && !SIMS[name] ? program(def.fragment, true) : null; if (def && !SIMS[name] && !ready(prog)) return; inst.dirty = false; if (!def) return fail( inst, name === 'custom' ? 'a custom shader needs a <script type="x-shader/x-fragment"> child' : `unknown effect "${name}"`, ); const changed = inst.effect !== name || inst.def?.fragment !== def.fragment; if (changed) { dropSim(inst); inst.colorsDirty = true; inst.imageDirty = true; inst.snap = true; } inst.effect = name; inst.def = def; if (!inst.timeSet) { inst.time = num(d.time, def.still ?? 0); inst.timeSet = true; } inst.speed = Math.max(num(d.speed, 1), 0); inst.fps = Math.max(num(d.fps, 0), 0); inst.quality = d.quality || 'auto'; inst.variant = d.variant || ''; inst.interactive = d.interactive !== undefined ? d.interactive !== 'false' : !!def.interactive; inst.seed = Math.max(Math.round(num(d.seed, 0)), 0); inst.aa = d.aa || def.aa || 'none'; inst.smooth = Math.min(Math.max(num(d.smooth, def.temporal ?? 0), 0), 0.95); inst.source = sourceOf(el); measureFrame(inst); if (inst.colorsDirty) resolveColors(inst); inst.prog = prog; if (inst.prog?.error) return fail(inst, inst.prog.error); // every float uniform the program declares beyond the contract is a data-* parameter const names = inst.prog ? inst.prog.params : Object.keys(def.params || {}).map((k) => `u_${k}`); inst.params = {}; for (const u of names) { const key = u.slice(2); inst.params[u] = num(el.getAttribute(`data-${key.replace(/_/g, '-')}`), def.params?.[key] ?? 0); } // a named choice holds the index of its word (an unknown word: the first one) for (const key in def.options || {}) { const index = def.options[key].indexOf(el.getAttribute(`data-${key.replace(/_/g, '-')}`) || ''); inst.params[`u_${key}`] = Math.max(index, 0); } if (inst.error) { inst.error = ''; delete el.dataset.fallback; } syncControls(inst, changed);}/** The effect cannot run: the CSS poster (and any source image) stays; shader-error says why. */function fail(inst, message) { if (inst.error === message) return; inst.error = message; inst.el.dataset.fallback = ''; // Fires when the effect cannot run - no WebGL2, an unknown effect, a GLSL error; the poster stays. inst.el.dispatchEvent( new CustomEvent<ShaderErrorDetail>('shader-error', { bubbles: true, detail: { effect: inst.effect || inst.el.dataset.effect || '', message }, }), );}/** The render size: CSS size × device pixels (capped by quality) × the adaptive scale × the effect's cost. */function renderSize(inst) { const dpr = globalThis.devicePixelRatio || 1; let s; if (inst.quality === 'low') s = Math.min(dpr, 1) * 0.5; else if (inst.quality === 'high') s = Math.min(dpr, 2); else { s = Math.min(dpr, 1.5) * engine.scale; const budget = 2.2e6; const px = inst.cssW * inst.cssH * s * s; if (px > budget) s *= Math.sqrt(budget / px); } s *= inst.def?.cost ?? 1; inst.scale = s; inst.w = Math.max(1, Math.round(inst.cssW * s)); inst.h = Math.max(1, Math.round(inst.cssH * s));}/** The source picture fitted into the render size, uploaded as u_image. SVG is rasterized first. */function uploadImage(inst) { const gl = engine.gl; inst.imageDirty = false; inst.hasImage = 0; const src = inst.source; if (!src || !inst.def?.image) return; let picture = src; let sw = 0; let sh = 0; if (src instanceof HTMLImageElement) { if (!src.complete || !src.naturalWidth) { src.addEventListener('load', () => wake(inst, true), { once: true }); return; } sw = src.naturalWidth; sh = src.naturalHeight; } else if (src instanceof HTMLVideoElement) { if (src.readyState < 2) { src.addEventListener('loadeddata', () => wake(inst, true), { once: true }); return; } sw = src.videoWidth; sh = src.videoHeight; } else if (src instanceof HTMLCanvasElement) { sw = src.width; sh = src.height; } else { // inline SVG: serialize it at the render size, with its text color, into an image (same origin, never tainted) const key = `${inst.w}x${inst.h}`; const markup = new XMLSerializer().serializeToString(src); if (inst.svgImage?.key !== key || inst.svgImage.markup !== markup) { const clone = src.cloneNode(true) as SVGSVGElement; clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); clone.setAttribute('width', String(inst.w)); clone.setAttribute('height', String(inst.h)); clone.style.color = getComputedStyle(src).color; const img = new Image(); inst.svgImage = { key, markup, img, ready: false }; img.onload = () => { if (inst.svgImage?.img === img) { inst.svgImage.ready = true; wake(inst, true); } }; img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(new XMLSerializer().serializeToString(clone))}`; return; } if (!inst.svgImage.ready) return; picture = inst.svgImage.img; sw = inst.w; sh = inst.h; } if (!sw || !sh) return; const fit = inst.el.dataset.fit || (inst.def.image === 'mask' ? 'contain' : 'cover'); const canvas = (inst.imageCanvas ??= document.createElement('canvas')); canvas.width = inst.w; canvas.height = inst.h; const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, inst.w, inst.h); const r = fit === 'fill' ? 0 : fit === 'cover' ? Math.max(inst.w / sw, inst.h / sh) : Math.min(inst.w / sw, inst.h / sh); const dw = r ? sw * r : inst.w; const dh = r ? sh * r : inst.h; try { ctx.drawImage(picture, (inst.w - dw) / 2, (inst.h - dh) / 2, dw, dh); if (!inst.texture) { inst.texture = gl.createTexture(); gl.bindTexture(GL.TEXTURE_2D, inst.texture); gl.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_MIN_FILTER, GL.LINEAR); gl.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_WRAP_S, GL.CLAMP_TO_EDGE); gl.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_WRAP_T, GL.CLAMP_TO_EDGE); } gl.bindTexture(GL.TEXTURE_2D, inst.texture); gl.pixelStorei(GL.UNPACK_FLIP_Y_WEBGL, true); gl.pixelStorei(GL.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false); gl.texImage2D(GL.TEXTURE_2D, 0, GL.RGBA, GL.RGBA, GL.UNSIGNED_BYTE, canvas); gl.pixelStorei(GL.UNPACK_FLIP_Y_WEBGL, false); inst.hasImage = 1; delete inst.el.dataset.image; } catch { // a cross-origin picture without CORS taints the canvas: the effect runs without it, the picture stays visible inst.el.dataset.image = 'blocked'; } if (src instanceof HTMLVideoElement && !src.paused) inst.imageDirty = true;}/** Draw one frame of an instance and copy it into its canvas. */function draw(inst, dt) { if (inst.dirty) refresh(inst); if (inst.error || !inst.def) return; // its program is still compiling (a released context dropped the last one): the canvas keeps its frame if (!inst.prog && !SIMS[inst.effect]) return; if (!inst.cssW || !inst.cssH) return; if (inst.colorsDirty) resolveColors(inst); const before = `${inst.w}x${inst.h}`; renderSize(inst); if (`${inst.w}x${inst.h}` !== before) inst.imageDirty = true; const { w, h } = inst; if (inst.canvas.width !== w || inst.canvas.height !== h) { inst.canvas.width = w; inst.canvas.height = h; } const shared = engine.canvas; if (shared.width < w || shared.height < h) { shared.width = Math.max(shared.width, w); shared.height = Math.max(shared.height, h); } if (inst.imageDirty) uploadImage(inst); inst.delta = dt; const sim = SIMS[inst.effect]; inst.temporalOn = false; if (sim) sim(inst, dt); else if (inst.aa === 'fxaa' || inst.smooth > 0) drawPost(inst, inst.live); else pass(inst, inst.prog, null, inst.live); if (inst.error) return; inst.ctx.clearRect(0, 0, w, h); inst.ctx.drawImage(shared, 0, shared.height - h, w, h, 0, 0, w, h); inst.frames++; engine.frames++; for (const done of inst.waiters.splice(0)) done(); if (!inst.ready) { inst.ready = true; inst.el.dataset.ready = ''; // Fires once the first frame is on the canvas (the poster fades out). inst.el.dispatchEvent( new CustomEvent<ShaderReadyDetail>('shader-ready', { bubbles: true, detail: { effect: inst.effect, width: w, height: h, pixelRatio: inst.scale }, }), ); }}/** * Ease the parameters and the palette toward what the attributes say (a quarter second): a slider, a theme or a * setState glides instead of jumping. Named choices (option indices) and the first frame snap; so does reduced * motion. True while still moving. */function easeParams(inst, dt) { const snap = inst.snap || reducedMotion() || !inst.live; inst.snap = false; const k = snap ? 1 : 1 - Math.exp(-dt * 12); const live = (inst.live ??= {}); const options = inst.def?.options || {}; let moving = false; for (const u in inst.params) { const target = inst.params[u]; const discrete = u.slice(2) in options; // another pattern, material ...: the last frame is no history for it if (discrete && live[u] !== target && inst.post) inst.post.fresh = true; if (!(u in live) || discrete) live[u] = target; else live[u] += (target - live[u]) * k; if (Math.abs(target - live[u]) > Math.abs(target) * 1e-4 + 1e-5) moving = true; else live[u] = target; } for (const u in live) if (!(u in inst.params)) delete live[u]; const colors = (inst.liveColors ??= new Float32Array(24)); if (snap || inst.colorsSnap) colors.set(inst.colors); else for (let i = 0; i < 24; i++) { colors[i] += (inst.colors[i] - colors[i]) * k; if (Math.abs(inst.colors[i] - colors[i]) > 1e-3) moving = true; else colors[i] = inst.colors[i]; } inst.colorsSnap = false; return moving;}/** Ease the pointer and the hover toward their targets; true while still moving. */function easePointer(inst, dt) { const k = 1 - Math.exp(-dt * 10); const p = inst.pointer; const t = inst.pointerTarget; p[0] += (t[0] - p[0]) * k; p[1] += (t[1] - p[1]) * k; inst.hover += (inst.hoverTarget - inst.hover) * k; return Math.abs(t[0] - p[0]) + Math.abs(t[1] - p[1]) + Math.abs(inst.hoverTarget - inst.hover) > 0.0005;}function moving(inst) { return ( inst.easing || Math.abs(inst.pointerTarget[0] - inst.pointer[0]) + Math.abs(inst.pointerTarget[1] - inst.pointer[1]) + Math.abs(inst.hoverTarget - inst.hover) > 0.0005 );}/** * Adapt the resolution to the frame rate, in windows of ADAPT_WINDOW frames or ADAPT_WINDOW_MS, whichever ends first * (at least eight frames): a window with a quarter of its frames late steps the resolution down a level; eight good * windows in a row step it up again. A change re-samples every * picture, so it must be rare: the level steps, never drifts, and a raise that a step down follows within * RAISE_HOLD_MS holds the level for the page's life. VERIFIED (tmp/probe-shader-wobble.ts): the earlier factor (x0.85 * after 24 late frames, x1.1 after 300 fast ones) changed the canvases every few seconds on a busy page - the * shaders wobbled. */const QUALITY_LEVELS = [1, 0.75, 0.5];const ADAPT_WINDOW = 120;const ADAPT_WINDOW_MS = 2000;const RAISE_HOLD_MS = 20000;function adapt(now) { const interval = engine.lastTick ? now - engine.lastTick : 16; engine.lastTick = now; // a second and more is a pause (a hidden tab, a debugger), not a late frame - a frame of a crowded page on a // software renderer takes a few hundred ms and must count (VERIFIED: shader.e2e under SwiftShader) if (interval >= 1000) return; if (!engine.window) engine.windowStart = now; engine.window++; if (interval > 30) engine.slow++; if (engine.window < 8 || (engine.window < ADAPT_WINDOW && now - engine.windowStart < ADAPT_WINDOW_MS)) return; const late = engine.slow / engine.window; engine.window = engine.slow = 0; if (late >= 0.25 && engine.level < QUALITY_LEVELS.length - 1) { engine.level++; engine.good = 0; if (now - engine.raisedAt < RAISE_HOLD_MS) engine.held = true; } else if (late <= 0.02 && !engine.held && engine.level > 0 && ++engine.good >= 8) { engine.level--; engine.good = 0; engine.raisedAt = now; } else if (late > 0.02) engine.good = 0; engine.scale = QUALITY_LEVELS[engine.level];}function schedule() { if (!engine.raf) engine.raf = requestAnimationFrame(tick);}function tick(now) { engine.raf = 0; let visible = 0; let preparing = 0; for (const inst of engine.instances) { if (!inst.el.isConnected) destroy(inst); else if (inst.visible) visible++; else if (inst.near && inst.dirty) preparing++; } if (!visible && !preparing) { // nothing in view: let go of the context after a while (the canvases keep their last frame) if (engine.gl && !engine.idleSince) engine.idleSince = now; if (engine.gl && now - engine.idleSince > 8000) release(); else if (engine.gl) setTimeout(schedule, 2000); engine.lastTick = 0; return; } engine.idleSince = 0; const gl = context(); if (!gl) { for (const inst of engine.instances) fail(inst, 'WebGL2 is not available'); return; } const still = reducedMotion(); let more = false; let animating = false; // programs first, within the frame's budget; without parallel compiles a frame that still waits for one redraws // nothing that only moves on - the compiles come first and the page answers between them engine.compileUntil = performance.now() + 24; let waiting = false; for (const inst of engine.instances) { if (!(inst.visible || inst.near) || !inst.dirty) continue; refresh(inst); if (inst.dirty) waiting = true; } // an instance still compiling (in view or near): the next frame polls it again if (waiting) more = true; const hold = waiting && !engine.parallel; for (const inst of engine.instances) { if (!inst.visible || (inst.error && !inst.dirty)) continue; const motion = inst.running && !still && inst.speed > 0; const busy = motion || inst.needsFrame || moving(inst) || (inst.def?.image && inst.source instanceof HTMLVideoElement && !inst.source.paused); if (!busy) { inst.last = 0; continue; } if (hold && inst.frames && !inst.needsFrame && !inst.dirty) { more = true; continue; } if (motion) animating = true; if (motion && inst.fps && inst.last && now - inst.last < 1000 / inst.fps - 1) { more = true; continue; } const dt = inst.last ? Math.min((now - inst.last) / 1000, 0.1) : 1 / 60; inst.last = now; if (inst.dirty) refresh(inst); if (inst.colorsDirty && inst.def && !inst.error) resolveColors(inst); inst.easing = !!inst.def && !inst.error && easeParams(inst, dt); const easing = easePointer(inst, dt) || inst.easing; if (motion) inst.time += dt * inst.speed; inst.needsFrame = false; draw(inst, motion ? dt * inst.speed : 0); if (motion || easing || inst.imageDirty) more = true; // its program is still compiling: draw it when it is linked if (inst.dirty) inst.needsFrame = more = true; } engine.compileUntil = 0; if (animating) adapt(now); else engine.lastTick = 0; if (more) schedule();}/** Something changed for an instance: draw it again (and re-read it when `reread`). */function wake(inst, reread = false) { if (reread) inst.imageDirty = true; inst.needsFrame = true; schedule();}function destroy(inst) { dropSim(inst); if (engine.gl && inst.texture) engine.gl.deleteTexture(inst.texture); inst.texture = null; engine.io?.unobserve(inst.el); engine.near?.unobserve(inst.el); engine.ro?.unobserve(inst.el); inst.attrObserver?.disconnect(); engine.instances.delete(inst); for (const done of inst.waiters.splice(0)) done();}// -- page-wide listeners: pointer, theme, motion preference -------------------function onPointer(e) { for (const inst of engine.instances) { if (!inst.interactive || !inst.visible) continue; const region = inst.variant === 'background' || inst.variant === 'border' ? inst.el.parentElement || inst.el : inst.el; const r = region.getBoundingClientRect(); const inside = e.type !== 'pointerleave' && e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom; const c = inst.el.getBoundingClientRect(); if (inside && c.width && c.height) { const x = (e.clientX - c.left) / c.width; const y = 1 - (e.clientY - c.top) / c.height; if (inst.lastEvent) inst.splats.push({ x, y, dx: x - inst.lastEvent[0], dy: y - inst.lastEvent[1] }); if (inst.splats.length > 12) inst.splats.splice(0, inst.splats.length - 12); // a press is a tap: u_taps keeps the last eight (where and when on the effect's clock) if (e.type === 'pointerdown') { inst.taps.push([x, y, inst.time]); if (inst.taps.length > 8) inst.taps.shift(); inst.taps.forEach((tap, i) => inst.tapData.set(tap, i * 3)); } inst.lastEvent = [x, y]; inst.pointerTarget = [x, y]; } else inst.lastEvent = null; const hover = inside ? 1 : 0; if (hover !== inst.hoverTarget || inside) { inst.hoverTarget = hover; wake(inst); } }}/** The theme changed (a class, a token, a stylesheet): every effect resolves its colors again. */function themeChanged() { for (const inst of engine.instances) { inst.colorsDirty = true; // the page switched at once: the palette follows without a glide inst.colorsSnap = true; wake(inst); }}let listening = false;function listen() { if (listening) return; listening = true; if (globalThis.IntersectionObserver) { engine.io = new IntersectionObserver( (entries) => { for (const entry of entries) { const inst = (entry.target as HTMLElement)._shader; if (!inst) continue; inst.visible = entry.isIntersecting; if (inst.visible) wake(inst); } }, // only what is in the viewport draws; one scrolled in shows its last frame (or its poster) until its next one { rootMargin: '0px' }, ); // what is about to scroll in gets ready first: a program compiled, generated controls built (they take room - // built only once in view, they pushed the page under the reader) engine.near = new IntersectionObserver( (entries) => { for (const entry of entries) { const inst = (entry.target as HTMLElement)._shader; if (!inst) continue; inst.near = entry.isIntersecting; if (inst.near && inst.dirty) schedule(); } }, { rootMargin: '20% 0px' }, ); } if (globalThis.ResizeObserver) { engine.ro = new ResizeObserver((entries) => { for (const entry of entries) { const inst = (entry.target as HTMLElement)._shader; if (!inst) continue; inst.cssW = inst.el.clientWidth; inst.cssH = inst.el.clientHeight; measureFrame(inst); wake(inst); } }); } dfDollar(document).on('pointermove', onPointer); dfDollar(document).on('pointerdown', onPointer); dfDollar(document.documentElement).on('pointerleave', onPointer); dfDollar(document).on('defuss-theme-change', () => setTimeout(themeChanged, 60)); // a stylesheet (a theme) finished loading document.addEventListener('load', (e) => e.target instanceof HTMLLinkElement && themeChanged(), true); const debounced = (() => { let t = 0; return () => { clearTimeout(t); t = setTimeout(themeChanged, 30) as unknown as number; }; })(); new MutationObserver(debounced).observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'], }); if (document.head) new MutationObserver(debounced).observe(document.head, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ['href'], }); globalThis.matchMedia?.('(prefers-color-scheme: dark)').addEventListener?.('change', themeChanged); globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').addEventListener?.('change', () => { for (const inst of engine.instances) wake(inst); });}// -- the State API ----------------------------------------------------------------const MARKUP_KEYS = { effect: 'data-effect', speed: 'data-speed', colors: 'data-colors' };/** * The markup of a state - the one place a state becomes attributes. setState * runs it on the live element, render() on a detached copy of the authored * markup: the config's effect, speed and colors as data-* attributes. Both * states share it - running or paused is the clock, not markup. */function applyMarkup(el, _stateName, config) { for (const key in MARKUP_KEYS) { const v = config?.[key]; if (v !== undefined && v !== null && v !== '') dfDollar(el).attr(MARKUP_KEYS[key], String(v)); }}/** UI side of setState: the markup, then the clock - 'default' runs it, 'paused' stops it (at `time` when given). */function triggerStateChange(el, stateName, config, incoming) { applyMarkup(el, stateName, config); const inst = el._shader; if (!inst) return; inst.running = stateName === 'default'; if (incoming && incoming.time !== undefined && incoming.time !== null && incoming.time !== '') { inst.time = num(incoming.time, inst.time); inst.timeSet = true; } inst.dirty = true; wake(inst);}/** Registry-level API; pass the .shader element explicitly. Unknown names throw. */export const shaderApi = componentState({ component: 'shader', states: shaderStates, mergeConfig: true, apply: (el, state, _previous, incoming) => { el.dataset.stateName = state.name; triggerStateChange(el, state.name, state.config, incoming); }, // the config reports the live attributes it names (an edited data-effect is the effect) read: (el, state) => { const config = { ...state.config }; for (const key in MARKUP_KEYS) if (key in config && el.hasAttribute(MARKUP_KEYS[key])) config[key] = key === 'speed' ? num(el.getAttribute(MARKUP_KEYS[key]), config[key]) : el.getAttribute(MARKUP_KEYS[key]); return { name: el.dataset.stateName === 'paused' ? 'paused' : 'default', config }; }, markup: (el, state) => applyMarkup(el, state.name, state.config),});df$.shaderApi = shaderApi;df$.shaderStates = shaderStates;const resolveTarget = (target: string | HTMLElement): HTMLElement | undefined => typeof target === 'string' ? dfDollar(target).get(0) : target;df$.shader = { /** * The names of the effects a .shader can show - the built-in ones and those added with register(). * @returns the effect names, for data-effect */ effects: (): string[] => Object.keys(EFFECTS).concat('custom'), /** * The named choices of an effect - data-<name> takes one of the words (the surface effect: pattern, material, projection, motion). * @param effect - the effect name * @returns option name to its words, the first word the default ({} for an effect without options) */ options: (effect: string): Record<string, string[]> => { const options = EFFECTS[effect]?.options || {}; return Object.fromEntries(Object.entries(options).map(([k, v]) => [k, [...v]])); }, /** * Add an effect under a name; data-effect="<name>" shows it. Its params become data-* attributes, its colors the default palette. * @param name - the effect name (a lowercase word, dashes allowed) * @param effect - the fragment shader and its defaults */ register: (name: string, effect: ShaderEffect): void => { if (!/^[a-z][a-z0-9-]*$/.test(name) || name === 'custom') throw new Error(`shader: "${name}" is no effect name`); if (!effect || typeof effect.fragment !== 'string' || !effect.fragment.trim()) throw new Error(`shader: effect "${name}" needs a fragment shader`); for (const key in effect.options || {}) if (!Array.isArray(effect.options[key]) || !effect.options[key].length) throw new Error(`shader: option "${key}" of "${name}" needs a list of words`); EFFECTS[name] = { ...effect }; for (const inst of engine.instances) if (inst.effect === name || inst.el.dataset.effect === name) { inst.dirty = true; wake(inst); } }, /** * The engine as it is now - contexts, programs, instances, frames, the adaptive scale. * @returns the counts */ stats: (): ShaderStats => { let visible = 0; let animating = 0; const still = reducedMotion(); for (const inst of engine.instances) { if (!inst.visible) continue; visible++; if (inst.running && !still && inst.speed > 0 && !inst.error) animating++; } return { supported: !engine.unsupported, contexts: engine.gl ? 1 : 0, programs: engine.programs.size, instances: engine.instances.size, visible, animating, frames: engine.frames, scale: engine.scale, }; }, /** * Draw the element's next frame - after a change from outside (a token, a picture drawn into a source canvas). * @param target - the .shader element or its selector * @returns resolves once the frame is on the canvas (at once for an element without an effect or out of view) */ frame: (target: string | HTMLElement): Promise<void> => { const inst = resolveTarget(target)?._shader; if (!inst || !inst.visible || inst.error) return Promise.resolve(); return new Promise<void>((done) => { inst.waiters.push(done); inst.colorsDirty = true; inst.imageDirty = true; wake(inst); }); },};// -- outside controls: .shader-controls[data-shader-for] -----------------------------/** * Slider ranges by parameter name, for the effects that do not bring their own. A name means the same thing in * every effect (an angle in degrees, a share 0 - 1), so one table serves all of them; the rest get a range * around their default. */const NAMED_RANGES: Record<string, [number, number, number]> = { angle: [-90, 90, 1], light: [0, 360, 1], direction: [0, 360, 1], rotation: [0, 360, 1], elevation: [5, 85, 1], decay: [0.9, 0.999, 0.001], origin_x: [-0.5, 1.5, 0.01], origin_y: [-0.5, 1.5, 0.01], tilt: [0, 1, 0.01], variation: [0, 1, 0.01], cover: [0, 1, 0.01], density: [0, 1, 0.01], fog: [0, 2, 0.01], line: [0.01, 0.3, 0.005], scale: [1, 30, 0.5], spacing: [8, 80, 1], segments: [2, 24, 1], pixel: [1, 12, 1], cell: [3, 32, 1],};/** A parameter's [min, max, step]: the effect's own range, the named one, or one around the default. */function paramRange(def: ShaderEffect, key: string): [number, number, number] { const own = def.ranges?.[key] || NAMED_RANGES[key]; if (own) return own; const v = def.params?.[key] ?? 1; if (Number.isInteger(v) && v >= 2) return [1, Math.max(v * 3, 10), 1]; const max = Math.max(Math.abs(v) * 2.5, 1); // a round step (0.01, 0.1 ...), so the default value lies on it return [v < 0 ? -max : 0, max, 10 ** Math.floor(Math.log10(max / 100))];}const label = (key: string) => key.replace(/_/g, ' ').replace(/^./, (c) => c.toUpperCase());const esc = (text: string) => text.replace(/&/g, '&').replace(/</g, '<').replace(/"/g, '"');const hex = (c: ArrayLike<number>, i: number) => `#${[0, 1, 2] .map((k) => Math.round(Math.min(Math.max(c[i * 3 + k], 0), 1) * 255) .toString(16) .padStart(2, '0'), ) .join('')}`;const CHEVRON = (d: string) => `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="${d}"/></svg>`;let controlsCount = 0;/** A select between a previous and a next button - both wrap around at the ends. */function cycleField(id: string, name: string, words: string[], value: string, kind: string) { const options = words .map((w) => `<option value="${esc(w)}"${w === value ? ' selected' : ''}>${esc(w)}</option>`) .join(''); return `<div class="shader-controls-field"><label class="label" for="${id}">${esc(label(name))}</label><div class="shader-controls-cycle" role="group" aria-label="${esc(label(name))}"><button type="button" class="btn" data-variant="outline" data-size="icon-sm" id="${id}-prev" data-shader-step="-1" aria-label="Previous ${esc(name)}">${CHEVRON('m15 18-6-6 6-6')}</button><select class="select" id="${id}" data-shader-${kind}="${esc(name)}">${options}</select><button type="button" class="btn" data-variant="outline" data-size="icon-sm" id="${id}-next" data-shader-step="1" aria-label="Next ${esc(name)}">${CHEVRON('m9 18 6-6-6-6')}</button></div></div>`;}/** A slider with its value. */function rangeField(id: string, name: string, [min, max, step]: number[], value: number, kind: string) { return `<div class="shader-controls-field"><div class="shader-controls-head"><label class="label" for="${id}">${esc(label(name))}</label><output class="slider-value" for="${id}">${value}</output></div><input class="slider" type="range" id="${id}" min="${min}" max="${max}" step="${step}" value="${value}" data-shader-${kind}="${esc(name)}" /></div>`;}/** The markup of a controls box for its target's effect: what data-controls names, in that order. */function controlsMarkup(box: HTMLElement, target: HTMLElement, inst) { const def: ShaderEffect = inst.def; const id = box.id || (box.id = `shader-controls-${++controlsCount}`); const parts = (box.dataset.controls || 'options params colors speed actions').split(/\s+/).filter(Boolean); const read = (key: string) => target.getAttribute(`data-${key.replace(/_/g, '-')}`); let out = ''; for (const part of parts) { if (part === 'effect') out += cycleField( `${id}-effect`, 'effect', df$.shader.effects().filter((e) => e !== 'custom'), inst.effect, 'control', ); else if (part === 'options') for (const [key, words] of Object.entries(def.options || {})) out += cycleField(`${id}-${key}`, key, words, words.includes(read(key) || '') ? read(key) : words[0], 'option'); else if (part === 'params') for (const key of Object.keys(def.params || {})) out += rangeField(`${id}-${key}`, key, paramRange(def, key), num(read(key), def.params[key]), 'param'); else if (part === 'speed') out += rangeField(`${id}-speed`, 'speed', [0, 3, 0.05], inst.speed, 'control'); else if (part === 'colors' && inst.colorCount) out += `<div class="shader-controls-field"><span class="label" id="${id}-colors-label">Colors</span><div class="shader-controls-colors" role="group" aria-labelledby="${id}-colors-label">${Array.from( { length: inst.colorCount }, (_, i) => `<input type="color" value="${hex(inst.colors, i)}" data-shader-color="${i}" aria-label="Color ${i + 1}" />`, ).join('')}</div></div>`; else if (part === 'actions') out += `<div class="shader-controls-actions"><button type="button" class="btn" data-variant="outline" data-size="sm" data-shader-action="pause" aria-pressed="${inst.running ? 'false' : 'true'}">${inst.running ? 'Pause' : 'Play'}</button><button type="button" class="btn" data-variant="ghost" data-size="sm" data-shader-action="reset">Reset</button></div>`; } return out;}/** Write an attribute on the target, remembering what it was the first time (a reset or another effect restores it). */function controlWrite(target: HTMLElement, name: string, value: string) { const before = (target._controlled ??= new Map<string, string | null>()); if (!before.has(name)) before.set(name, target.getAttribute(name)); dfDollar(target).attr(name, value);}/** Undo what the controls wrote: every attribute back to its authored value (or gone). */function controlClear(target: HTMLElement) { for (const [name, value] of target._controlled || []) { if (value === null) target.removeAttribute(name); else dfDollar(target).attr(name, value); } target._controlled?.clear();}/** Render the controls of every box bound to an instance (its effect changed), or bring their values up to date. */function syncControls(inst, rebuild: boolean) { for (const box of engine.controls) { if (!box.isConnected) { engine.controls.delete(box); continue; } if (controlsTarget(box) !== inst.el) continue; if (rebuild || !box._rendered) { box._rendered = true; // keep the focus on the control that had it (a previous / next button stepping through the effects) const active = box.ownerDocument.activeElement; const focused = active && box.contains(active) ? active.id : ''; dfDollar(box).html(controlsMarkup(box, inst.el, inst)); if (focused) dfDollar(`#${CSS.escape(focused)}`) .get(0) ?.focus(); continue; } dfDollar(box) .find('[data-shader-option]') .toArray() .forEach((select: HTMLSelectElement) => { const key = select.dataset.shaderOption; const words = inst.def.options?.[key] || []; const word = inst.el.getAttribute(`data-${key.replace(/_/g, '-')}`) || ''; const v = words.includes(word) ? word : words[0]; if (v !== undefined && select.value !== v) select.value = v; }); // values only: what the attributes say now (a script or setState may have changed them) const speed = dfDollar(box).find('[data-shader-control="speed"]').get(0) as HTMLInputElement | undefined; if (speed && box.ownerDocument.activeElement !== speed && speed.value !== String(inst.speed)) speed.value = String(inst.speed); dfDollar(box) .find('[data-shader-param]') .toArray() .forEach((input: HTMLInputElement) => { const key = input.dataset.shaderParam; const v = String(num(inst.el.getAttribute(`data-${key.replace(/_/g, '-')}`), inst.def.params?.[key] ?? 0)); if (input.value !== v && box.ownerDocument.activeElement !== input) input.value = v; }); dfDollar(box) .find('[data-shader-action="pause"]') .toArray() .forEach((button: HTMLButtonElement) => { dfDollar(button).attr('aria-pressed', inst.running ? 'false' : 'true'); dfDollar(button).text(inst.running ? 'Pause' : 'Play'); }); }}const controlsTarget = (box: HTMLElement): HTMLElement | undefined => { const id = box.dataset.shaderFor || ''; return id ? dfDollar(`#${CSS.escape(id)}`).get(0) : undefined;};/** Wire a controls box once: every change writes the target's attributes (or its state); the runtime redraws. */function bindControls(box: HTMLElement) { engine.controls.add(box); const apply = (el: HTMLElement) => { const target = controlsTarget(box); const inst = target?._shader; if (!target || !inst) return; if (el.dataset.shaderOption) controlWrite(target, `data-${el.dataset.shaderOption.replace(/_/g, '-')}`, (el as HTMLSelectElement).value); else if (el.dataset.shaderParam) controlWrite(target, `data-${el.dataset.shaderParam.replace(/_/g, '-')}`, (el as HTMLInputElement).value); else if (el.dataset.shaderColor) { const colors = dfDollar(box) .find('[data-shader-color]') .toArray() .map((c: HTMLInputElement) => c.value); controlWrite(target, 'data-colors', colors.join(' ')); } else if (el.dataset.shaderControl === 'speed') controlWrite(target, 'data-speed', (el as HTMLInputElement).value); else if (el.dataset.shaderControl === 'effect') { // the other attributes belonged to the previous effect: back to the authored ones, then the new effect const authored = target._controlled?.has('data-effect') ? target._controlled.get('data-effect') : target.getAttribute('data-effect'); controlClear(target); (target._controlled ??= new Map<string, string | null>()).set('data-effect', authored); dfDollar(target).attr('data-effect', (el as HTMLSelectElement).value); } }; dfDollar(box).on('input', (e: Event) => apply(e.target as HTMLElement)); dfDollar(box).on('change', (e: Event) => { if (e.target instanceof HTMLSelectElement) apply(e.target); }); dfDollar(box).on('click', (e: Event) => { const button = (e.target as Element).closest('button'); if (!button || !box.contains(button)) return; const target = controlsTarget(box); if (!target?._shader) return; if (button.dataset.shaderStep) { // previous / next: the select beside the button, wrapping around at both ends const select = dfDollar(button).parent().find('select').get(0) as HTMLSelectElement; const n = select.options.length; select.selectedIndex = (select.selectedIndex + Number(button.dataset.shaderStep) + n) % n; apply(select); } else if (button.dataset.shaderAction === 'pause') { target.api.setState(target._shader.running ? 'paused' : 'default'); syncControls(target._shader, false); } else if (button.dataset.shaderAction === 'reset') { controlClear(target); syncControls(target._shader, true); } }); const inst = controlsTarget(box)?._shader; if (inst?.def) syncControls(inst, true);}function init() { dfDollar('.shader:not([data-init])') .toArray() .forEach((el) => { el.dataset.init = ''; // a canvas a serialized preview carried over is the runtime's - never authored for (const c of Array.from(el.children)) if (c.classList.contains('shader-canvas')) c.remove(); const canvas = document.createElement('canvas'); canvas.className = 'shader-canvas'; canvas.setAttribute('aria-hidden', 'true'); el.append(canvas); listen(); const inst = { el, canvas, ctx: canvas.getContext('2d'), visible: !engine.io, near: !engine.near, running: true, needsFrame: true, dirty: true, colorsDirty: true, imageDirty: true, time: 0, timeSet: false, delta: 0, last: 0, frames: 0, ready: false, error: '', effect: '', def: null, prog: null, params: {}, speed: 1, fps: 0, quality: 'auto', variant: '', interactive: false, seed: 0, colors: new Float32Array(24), colorCount: 0, background: [0, 0, 0, 0], cssW: el.clientWidth, cssH: el.clientHeight, scale: 1, w: 0, h: 0, frame: [0, 0], pointer: [0.5, 0.5], pointerTarget: [0.5, 0.5], hover: 0, hoverTarget: 0, splats: [], taps: [], tapData: new Float32Array(24), lastEvent: null, source: null, texture: null, hasImage: 0, imageCanvas: null, svgImage: null, sim: null, waiters: [], attrObserver: null, }; el._shader = inst; engine.instances.add(inst); // an edited attribute (a data-effect, a parameter, a palette) or a changed source picture redraws it const own = (n) => n === canvas || (n instanceof Element && n.classList.contains('shader-probe')); inst.attrObserver = new MutationObserver((records) => { const outside = records.filter((r) => { // the runtime's own nodes: the canvas (its size) and the color probe (its style) if (own(r.target)) return false; if (r.type === 'attributes') return !( r.target === el && /^data-(init|ready|state-name|fallback|image|api)$/.test(r.attributeName || '') ); if (r.type === 'childList') return ![...r.addedNodes, ...r.removedNodes].every(own); return true; }); if (!outside.length) return; // a palette, a token in a style or a class may have changed: resolve the colors again (one style read) inst.colorsDirty = true; inst.dirty = true; inst.imageDirty = true; wake(inst); }); inst.attrObserver.observe(el, { attributes: true, childList: true, subtree: true, characterData: true }); engine.io?.observe(el); engine.near?.observe(el); engine.ro?.observe(el); // el.store + el.api (AGENTS.md "State through stores") bindComponent(el, shaderApi, { name: 'default', config: {} }); el.dataset.stateName = 'default'; wake(inst); }); dfDollar('.shader-controls[data-shader-for]:not([data-init])') .toArray() .forEach((box: HTMLElement) => { box.dataset.init = ''; bindControls(box); });}init();new MutationObserver(init).observe(document, { childList: true, subtree: true });Comments, ideas or improvements? Edit this page's source on GitHub