Upload files to "/"
This commit is contained in:
905
Shader Wallpapers.html
Normal file
905
Shader Wallpapers.html
Normal file
@@ -0,0 +1,905 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Aurora · Shader Wallpapers</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--ink: #ffffff;
|
||||
--dim: rgba(255,255,255,0.55);
|
||||
--faint: rgba(255,255,255,0.25);
|
||||
--mono: 'JetBrains Mono', ui-monospace, monospace;
|
||||
--sans: 'Inter', system-ui, sans-serif;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; color: var(--ink); font-family: var(--sans); }
|
||||
canvas#gl { position: fixed; inset: 0; width: 100%; height: 100%; display: block; cursor: crosshair; }
|
||||
|
||||
/* ── TOP-LEFT INDEX ── */
|
||||
.top-left {
|
||||
position: fixed; top: 28px; left: 32px; z-index: 10;
|
||||
display: flex; flex-direction: column; gap: 6px;
|
||||
pointer-events: none;
|
||||
mix-blend-mode: difference;
|
||||
}
|
||||
.top-left .kicker {
|
||||
font-family: var(--mono); font-size: 10px;
|
||||
letter-spacing: 3px; text-transform: uppercase;
|
||||
color: var(--dim);
|
||||
}
|
||||
.top-left .title {
|
||||
font-family: var(--sans); font-weight: 300;
|
||||
font-size: clamp(28px, 3.4vw, 44px);
|
||||
letter-spacing: -1.2px; line-height: 1;
|
||||
color: var(--ink);
|
||||
}
|
||||
.top-left .title .accent { font-style: italic; font-weight: 400; }
|
||||
.top-left .meta {
|
||||
font-family: var(--mono); font-size: 10px;
|
||||
letter-spacing: 1.5px; color: var(--faint);
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
/* ── TOP-RIGHT INFO ── */
|
||||
.top-right {
|
||||
position: fixed; top: 28px; right: 32px; z-index: 10;
|
||||
display: flex; flex-direction: column; gap: 8px; align-items: flex-end;
|
||||
mix-blend-mode: difference; pointer-events: none;
|
||||
}
|
||||
.badge {
|
||||
display: inline-flex; align-items: center; gap: 8px;
|
||||
font-family: var(--mono); font-size: 10px;
|
||||
letter-spacing: 2px; text-transform: uppercase;
|
||||
color: var(--ink);
|
||||
padding: 6px 12px;
|
||||
border: 0.5px solid rgba(255,255,255,0.3);
|
||||
background: rgba(0,0,0,0.001);
|
||||
}
|
||||
.badge .dot {
|
||||
width: 6px; height: 6px; border-radius: 50%;
|
||||
background: #66ffaa;
|
||||
box-shadow: 0 0 8px #66ffaa;
|
||||
animation: pulse 1.6s ease-in-out infinite;
|
||||
}
|
||||
@keyframes pulse { 50% { opacity: 0.3; } }
|
||||
.hint {
|
||||
font-family: var(--mono); font-size: 10px;
|
||||
color: var(--dim); letter-spacing: 1px;
|
||||
}
|
||||
.hint kbd {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
border: 0.5px solid rgba(255,255,255,0.4);
|
||||
font-family: var(--mono); font-size: 9px;
|
||||
margin: 0 2px; color: var(--ink);
|
||||
}
|
||||
|
||||
/* ── BOTTOM DOCK / SWITCHER ── */
|
||||
.dock {
|
||||
position: fixed; bottom: 28px; left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 10;
|
||||
display: flex; gap: 1px;
|
||||
background: rgba(255,255,255,0.08);
|
||||
padding: 1px;
|
||||
backdrop-filter: blur(20px) saturate(140%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(140%);
|
||||
border: 0.5px solid rgba(255,255,255,0.18);
|
||||
border-radius: 2px;
|
||||
}
|
||||
.dock-btn {
|
||||
background: rgba(0,0,0,0.55);
|
||||
border: none;
|
||||
color: var(--dim);
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 2.5px;
|
||||
text-transform: uppercase;
|
||||
padding: 14px 22px;
|
||||
cursor: pointer;
|
||||
transition: color 0.25s, background 0.25s;
|
||||
position: relative;
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
}
|
||||
.dock-btn::before {
|
||||
content: ''; display: block; width: 6px; height: 6px;
|
||||
background: currentColor; opacity: 0.5;
|
||||
transition: opacity 0.25s, transform 0.25s;
|
||||
}
|
||||
.dock-btn[data-shape="circle"]::before { border-radius: 50%; }
|
||||
.dock-btn[data-shape="diamond"]::before { transform: rotate(45deg); }
|
||||
.dock-btn[data-shape="bar"]::before { width: 10px; height: 2px; }
|
||||
.dock-btn[data-shape="ring"]::before { background: transparent; border: 1px solid currentColor; border-radius: 50%; }
|
||||
.dock-btn[data-shape="cross"]::before {
|
||||
width: 8px; height: 8px;
|
||||
background: linear-gradient(currentColor, currentColor) center/100% 1px no-repeat,
|
||||
linear-gradient(currentColor, currentColor) center/1px 100% no-repeat;
|
||||
}
|
||||
.dock-btn:hover { color: var(--ink); background: rgba(0,0,0,0.7); }
|
||||
.dock-btn:hover::before { opacity: 1; }
|
||||
.dock-btn.active { color: var(--ink); background: rgba(0,0,0,0.85); }
|
||||
.dock-btn.active::before { opacity: 1; transform: scale(1.3); }
|
||||
.dock-btn[data-shape="diamond"].active::before { transform: rotate(45deg) scale(1.3); }
|
||||
|
||||
/* ── BOTTOM-LEFT STATS ── */
|
||||
.bottom-left {
|
||||
position: fixed; bottom: 28px; left: 32px; z-index: 10;
|
||||
display: flex; flex-direction: column; gap: 6px;
|
||||
font-family: var(--mono); font-size: 10px;
|
||||
letter-spacing: 1.5px;
|
||||
mix-blend-mode: difference;
|
||||
color: var(--dim);
|
||||
pointer-events: none;
|
||||
}
|
||||
.bottom-left .row { display: flex; gap: 14px; }
|
||||
.bottom-left .row .k { color: var(--faint); }
|
||||
.bottom-left .row .v { color: var(--ink); min-width: 50px; }
|
||||
|
||||
/* ── BOTTOM-RIGHT FPS / DESC ── */
|
||||
.bottom-right {
|
||||
position: fixed; bottom: 28px; right: 32px; z-index: 10;
|
||||
text-align: right; max-width: 320px;
|
||||
mix-blend-mode: difference;
|
||||
pointer-events: none;
|
||||
}
|
||||
.desc-name {
|
||||
font-family: var(--sans); font-weight: 500;
|
||||
font-size: 13px; color: var(--ink);
|
||||
letter-spacing: 0.5px; margin-bottom: 4px;
|
||||
}
|
||||
.desc-body {
|
||||
font-family: var(--mono); font-size: 10px;
|
||||
color: var(--dim); line-height: 1.6;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* ── INTRO PULSE ON SWITCH ── */
|
||||
.name-flash {
|
||||
position: fixed; top: 50%; left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
pointer-events: none; z-index: 20;
|
||||
font-family: var(--sans); font-weight: 200;
|
||||
font-size: clamp(60px, 12vw, 180px);
|
||||
letter-spacing: -4px; color: rgba(255,255,255,0.95);
|
||||
mix-blend-mode: difference;
|
||||
opacity: 0; transition: opacity 0.4s, transform 0.6s;
|
||||
}
|
||||
.name-flash.show { opacity: 1; }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.dock { flex-wrap: wrap; max-width: calc(100vw - 40px); }
|
||||
.dock-btn { padding: 12px 14px; font-size: 9px; }
|
||||
.top-right .hint, .bottom-right { display: none; }
|
||||
.bottom-left { display: none; }
|
||||
.top-left .title { font-size: 24px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<canvas id="gl"></canvas>
|
||||
|
||||
<!-- TOP LEFT -->
|
||||
<div class="top-left">
|
||||
<div class="kicker">Frontier Algorithmics · Lab</div>
|
||||
<div class="title">Aurora<span class="accent"> · Field</span></div>
|
||||
<div class="meta" id="meta-coord">42.0235 N · 11.1812 E</div>
|
||||
</div>
|
||||
|
||||
<!-- TOP RIGHT -->
|
||||
<div class="top-right">
|
||||
<div class="badge"><span class="dot"></span><span>Live · GLSL</span></div>
|
||||
<div class="hint">Move <kbd>cursor</kbd> · Click for ripple</div>
|
||||
</div>
|
||||
|
||||
<!-- BOTTOM LEFT -->
|
||||
<div class="bottom-left">
|
||||
<div class="row"><span class="k">FPS</span><span class="v" id="fps">--</span></div>
|
||||
<div class="row"><span class="k">CURSOR</span><span class="v" id="curs">0,0</span></div>
|
||||
<div class="row"><span class="k">SHADER</span><span class="v" id="shname">aurora</span></div>
|
||||
</div>
|
||||
|
||||
<!-- BOTTOM DOCK -->
|
||||
<div class="dock" id="dock">
|
||||
<button class="dock-btn active" data-shape="diamond" data-shader="aurora">Aurora</button>
|
||||
<button class="dock-btn" data-shape="circle" data-shader="plasma">Plasma</button>
|
||||
<button class="dock-btn" data-shape="ring" data-shader="mercury">Mercury</button>
|
||||
<button class="dock-btn" data-shape="cross" data-shader="crystal">Crystal</button>
|
||||
<button class="dock-btn" data-shape="bar" data-shader="waves">Waves</button>
|
||||
</div>
|
||||
|
||||
<!-- BOTTOM RIGHT -->
|
||||
<div class="bottom-right">
|
||||
<div class="desc-name" id="desc-name">Aurora Field</div>
|
||||
<div class="desc-body" id="desc-body">FBM-warped ribbon bands drifting over a deep-night gradient. Cursor displaces field, click discharges a luminous burst.</div>
|
||||
</div>
|
||||
|
||||
<!-- FULLSCREEN FLASH -->
|
||||
<div class="name-flash" id="flash">aurora</div>
|
||||
|
||||
<!-- ─────────── VERTEX SHADER (shared) ─────────── -->
|
||||
<script id="vs" type="x-shader/x-vertex">
|
||||
attribute vec2 a_pos;
|
||||
void main() { gl_Position = vec4(a_pos, 0.0, 1.0); }
|
||||
</script>
|
||||
|
||||
<!-- ─────────── COMMON FRAGMENT HEADER ─────────── -->
|
||||
<script id="frag-header" type="x-shader/x-fragment">
|
||||
precision highp float;
|
||||
uniform vec2 uRes;
|
||||
uniform float uTime;
|
||||
uniform vec2 uMouse; // pixels, top-left origin
|
||||
uniform vec2 uMouseSmooth; // eased mouse
|
||||
uniform vec4 uClicks[6]; // xy = pos px, z = startTime, w = active flag
|
||||
uniform float uShaderTime; // time since this shader was activated
|
||||
|
||||
// ── Hash & noise utilities ──
|
||||
float hash21(vec2 p) {
|
||||
p = fract(p * vec2(123.34, 456.21));
|
||||
p += dot(p, p + 45.32);
|
||||
return fract(p.x * p.y);
|
||||
}
|
||||
vec2 hash22(vec2 p) {
|
||||
vec3 p3 = fract(vec3(p.xyx) * vec3(0.1031, 0.1030, 0.0973));
|
||||
p3 += dot(p3, p3.yzx + 33.33);
|
||||
return fract((p3.xx + p3.yz) * p3.zy);
|
||||
}
|
||||
float vnoise(vec2 p) {
|
||||
vec2 i = floor(p), f = fract(p);
|
||||
vec2 u = f*f*(3.0-2.0*f);
|
||||
float a = hash21(i);
|
||||
float b = hash21(i + vec2(1.0, 0.0));
|
||||
float c = hash21(i + vec2(0.0, 1.0));
|
||||
float d = hash21(i + vec2(1.0, 1.0));
|
||||
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
|
||||
}
|
||||
float fbm(vec2 p) {
|
||||
float v = 0.0, a = 0.5;
|
||||
mat2 rot = mat2(0.8, -0.6, 0.6, 0.8);
|
||||
for (int i = 0; i < 5; i++) {
|
||||
v += a * vnoise(p);
|
||||
p = rot * p * 2.02;
|
||||
a *= 0.5;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
// Click ripple decay (returns 0..1, peaks then decays)
|
||||
float clickRipple(vec2 frag, vec4 click) {
|
||||
if (click.w < 0.5) return 0.0;
|
||||
float age = uTime - click.z;
|
||||
if (age < 0.0 || age > 2.4) return 0.0;
|
||||
float d = distance(frag, click.xy);
|
||||
float radius = age * 600.0;
|
||||
float thickness = 60.0 + age * 80.0;
|
||||
float ring = exp(-pow((d - radius) / thickness, 2.0));
|
||||
return ring * exp(-age * 1.3);
|
||||
}
|
||||
float clickEnergy(vec2 frag) {
|
||||
float e = 0.0;
|
||||
e += clickRipple(frag, uClicks[0]);
|
||||
e += clickRipple(frag, uClicks[1]);
|
||||
e += clickRipple(frag, uClicks[2]);
|
||||
e += clickRipple(frag, uClicks[3]);
|
||||
e += clickRipple(frag, uClicks[4]);
|
||||
e += clickRipple(frag, uClicks[5]);
|
||||
return e;
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- ─────────── 1. AURORA ─────────── -->
|
||||
<script id="fs-aurora" type="x-shader/x-fragment">
|
||||
void main() {
|
||||
vec2 frag = gl_FragCoord.xy;
|
||||
vec2 uv = frag / uRes.xy;
|
||||
vec2 m = uMouseSmooth / uRes.xy;
|
||||
|
||||
// Deep-night background gradient (top: cold violet, bottom: black-teal)
|
||||
vec3 sky = mix(
|
||||
vec3(0.005, 0.01, 0.06),
|
||||
vec3(0.04, 0.02, 0.10),
|
||||
pow(1.0 - uv.y, 1.5)
|
||||
);
|
||||
|
||||
// Stars
|
||||
float sH = hash21(floor(frag));
|
||||
float starMask = step(0.9985, sH);
|
||||
float twinkle = 0.5 + 0.5 * sin(uTime * 2.0 + sH * 200.0);
|
||||
sky += vec3(1.0) * starMask * twinkle * (1.0 - smoothstep(0.0, 0.55, uv.y));
|
||||
|
||||
// Aurora bands — multiple stacked, animated, mouse-displaced
|
||||
vec3 aurora = vec3(0.0);
|
||||
float mouseInf = exp(-distance(uv, m) * 2.2);
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
float fi = float(i);
|
||||
float seed = fi * 17.3;
|
||||
|
||||
// Domain warp using FBM
|
||||
vec2 q = vec2(uv.x * 1.6 + uTime * 0.08 + seed, uv.y * 0.9);
|
||||
float n1 = fbm(q + vec2(0.0, uTime * 0.07));
|
||||
float n2 = fbm(q * 1.5 + n1 + vec2(uTime * 0.05, 0.0));
|
||||
|
||||
// Base band centerline (varies with x)
|
||||
float center = 0.18 + fi * 0.16 + sin(uTime * 0.25 + fi * 1.3) * 0.04;
|
||||
center += (n1 - 0.5) * 0.28;
|
||||
// Mouse pushes nearby bands upward
|
||||
center += mouseInf * (m.y - center) * 0.25;
|
||||
|
||||
// Vertical profile: thin band that softens at top
|
||||
float band = exp(-pow((uv.y - center) * (9.0 - fi * 0.8), 2.0));
|
||||
|
||||
// Vertical streak detail (the iconic vertical strands)
|
||||
float strand = fbm(vec2(q.x * 18.0 + uTime * 0.2, q.y * 4.0 + n2 * 2.0));
|
||||
strand = smoothstep(0.35, 0.85, strand);
|
||||
band *= 0.55 + 0.7 * strand;
|
||||
|
||||
// Color shifts across bands and across x
|
||||
vec3 cA = vec3(0.10, 0.95, 0.55); // emerald
|
||||
vec3 cB = vec3(0.20, 0.45, 1.00); // cyan-blue
|
||||
vec3 cC = vec3(0.90, 0.30, 0.85); // magenta
|
||||
float mixA = sin(uTime * 0.3 + fi * 1.5 + uv.x * 1.2) * 0.5 + 0.5;
|
||||
float mixB = cos(uTime * 0.22 + fi * 0.9) * 0.5 + 0.5;
|
||||
vec3 bandCol = mix(mix(cA, cB, mixA), cC, mixB * 0.35);
|
||||
|
||||
aurora += band * bandCol * (1.1 - fi * 0.08);
|
||||
}
|
||||
|
||||
// Reflective glow on lower edge
|
||||
float ground = smoothstep(0.0, 0.25, uv.y);
|
||||
aurora *= ground;
|
||||
|
||||
// Click ripple: brightens region, slight chromatic separation
|
||||
float ce = clickEnergy(frag);
|
||||
vec3 burst = vec3(0.5, 1.2, 1.5) * ce;
|
||||
|
||||
vec3 col = sky + aurora * 1.15 + burst;
|
||||
|
||||
// Subtle vignette
|
||||
vec2 vuv = uv - 0.5;
|
||||
col *= 1.0 - dot(vuv, vuv) * 0.6;
|
||||
|
||||
gl_FragColor = vec4(col, 1.0);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- ─────────── 2. PLASMA SUN ─────────── -->
|
||||
<script id="fs-plasma" type="x-shader/x-fragment">
|
||||
void main() {
|
||||
vec2 frag = gl_FragCoord.xy;
|
||||
vec2 uv = (frag - 0.5 * uRes) / min(uRes.x, uRes.y);
|
||||
vec2 m = (uMouseSmooth - 0.5 * uRes) / min(uRes.x, uRes.y);
|
||||
|
||||
// Orb center follows mouse, with slow return-to-center
|
||||
vec2 center = m * 0.7;
|
||||
vec2 p = uv - center;
|
||||
float r = length(p);
|
||||
float a = atan(p.y, p.x);
|
||||
|
||||
// Domain-warped corona using polar coords
|
||||
float corona = 0.0;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
float fi = float(i);
|
||||
float swirl = fbm(vec2(a * (3.0 + fi) + uTime * (0.2 + fi * 0.1), r * 4.0 - uTime * 0.4));
|
||||
corona += swirl / (fi + 1.0);
|
||||
}
|
||||
corona /= 1.8;
|
||||
|
||||
// Core
|
||||
float core = exp(-pow(r * 3.5, 2.0));
|
||||
|
||||
// Outer corona pulsing
|
||||
float outer = exp(-r * 2.8) * (0.6 + 0.4 * corona);
|
||||
|
||||
// Solar arc filaments
|
||||
float fil = smoothstep(0.55, 0.85, fbm(vec2(a * 6.0, r * 8.0 + uTime * 0.5)));
|
||||
fil *= smoothstep(0.05, 0.25, r) * smoothstep(0.7, 0.2, r);
|
||||
|
||||
// Background space
|
||||
vec3 bg = mix(vec3(0.02, 0.005, 0.04), vec3(0.0, 0.0, 0.0), smoothstep(0.0, 1.2, r));
|
||||
// Distant nebula
|
||||
float neb = fbm(uv * 1.5 + uTime * 0.02);
|
||||
bg += vec3(0.15, 0.05, 0.30) * neb * 0.18;
|
||||
|
||||
// Sun palette: deep orange → yellow → white core
|
||||
vec3 sunCol = mix(
|
||||
vec3(1.0, 0.25, 0.05), // outer red
|
||||
vec3(1.0, 0.85, 0.4), // mid amber
|
||||
smoothstep(0.0, 0.35, core + outer * 0.5)
|
||||
);
|
||||
sunCol = mix(sunCol, vec3(1.0, 1.0, 0.95), pow(core, 2.5));
|
||||
|
||||
// Color shift hue toward magenta when cursor far
|
||||
float hue = clamp(length(m), 0.0, 1.0);
|
||||
sunCol = mix(sunCol, sunCol.bgr * vec3(1.2, 0.6, 1.0), hue * 0.25);
|
||||
|
||||
vec3 col = bg
|
||||
+ sunCol * core * 1.5
|
||||
+ sunCol * outer * 1.0
|
||||
+ vec3(1.0, 0.6, 0.2) * fil * 0.9;
|
||||
|
||||
// Click ripple → shockwave
|
||||
float ce = clickEnergy(frag);
|
||||
col += vec3(1.4, 1.0, 0.6) * ce * 1.5;
|
||||
|
||||
// Lens dust speckle
|
||||
float dust = step(0.9992, hash21(floor(frag * 0.8)));
|
||||
col += vec3(dust) * 0.6;
|
||||
|
||||
// Tonemap
|
||||
col = col / (1.0 + col);
|
||||
col = pow(col, vec3(0.92));
|
||||
|
||||
gl_FragColor = vec4(col, 1.0);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- ─────────── 3. MERCURY (chrome metaballs) ─────────── -->
|
||||
<script id="fs-mercury" type="x-shader/x-fragment">
|
||||
// 2D metaball SDF approximation
|
||||
float metaball(vec2 p, vec2 c, float r) {
|
||||
return r / max(length(p - c), 0.0001);
|
||||
}
|
||||
void main() {
|
||||
vec2 frag = gl_FragCoord.xy;
|
||||
vec2 uv = (frag - 0.5 * uRes) / min(uRes.x, uRes.y);
|
||||
vec2 m = (uMouseSmooth - 0.5 * uRes) / min(uRes.x, uRes.y);
|
||||
|
||||
// Five drifting metaballs + one tied to the cursor
|
||||
vec2 b0 = vec2(sin(uTime * 0.31) * 0.45, cos(uTime * 0.27) * 0.32);
|
||||
vec2 b1 = vec2(cos(uTime * 0.41) * 0.5, sin(uTime * 0.23) * 0.28);
|
||||
vec2 b2 = vec2(sin(uTime * 0.19 + 2.0) * 0.55, cos(uTime * 0.35 + 1.0) * 0.4);
|
||||
vec2 b3 = vec2(cos(uTime * 0.29 + 4.0) * 0.4, sin(uTime * 0.17 + 3.0) * 0.5);
|
||||
vec2 b4 = m;
|
||||
vec2 b5 = -m * 0.6;
|
||||
|
||||
float f = 0.0;
|
||||
f += metaball(uv, b0, 0.18);
|
||||
f += metaball(uv, b1, 0.20);
|
||||
f += metaball(uv, b2, 0.14);
|
||||
f += metaball(uv, b3, 0.16);
|
||||
f += metaball(uv, b4, 0.26); // cursor-attached blob — biggest
|
||||
f += metaball(uv, b5, 0.12);
|
||||
|
||||
// Smoothstep into a "surface"
|
||||
float surface = smoothstep(2.4, 3.0, f);
|
||||
float edge = smoothstep(2.0, 2.4, f) - smoothstep(2.4, 3.0, f);
|
||||
|
||||
// Approximate a normal by finite-differencing the field
|
||||
vec2 e = vec2(0.0035, 0.0);
|
||||
float fx = (
|
||||
metaball(uv + e.xy, b0, 0.18) + metaball(uv + e.xy, b1, 0.20)
|
||||
+ metaball(uv + e.xy, b2, 0.14) + metaball(uv + e.xy, b3, 0.16)
|
||||
+ metaball(uv + e.xy, b4, 0.26) + metaball(uv + e.xy, b5, 0.12)
|
||||
) - f;
|
||||
float fy = (
|
||||
metaball(uv + e.yx, b0, 0.18) + metaball(uv + e.yx, b1, 0.20)
|
||||
+ metaball(uv + e.yx, b2, 0.14) + metaball(uv + e.yx, b3, 0.16)
|
||||
+ metaball(uv + e.yx, b4, 0.26) + metaball(uv + e.yx, b5, 0.12)
|
||||
) - f;
|
||||
vec2 n = normalize(vec2(fx, fy) + vec2(1e-5));
|
||||
|
||||
// Iridescent fresnel based on "view angle" (here: how grazing the normal is)
|
||||
float fres = 1.0 - abs(n.y);
|
||||
fres = pow(fres, 1.5);
|
||||
|
||||
// Reflected environment: spectral bands as a function of normal angle
|
||||
float ang = atan(n.y, n.x);
|
||||
vec3 envA = vec3(0.20, 0.55, 1.0); // cyan
|
||||
vec3 envB = vec3(0.95, 0.25, 0.85); // magenta
|
||||
vec3 envC = vec3(0.30, 1.0, 0.60); // mint
|
||||
vec3 envD = vec3(1.0, 0.8, 0.25); // gold
|
||||
vec3 chrome = mix(envA, envB, sin(ang * 2.0 + uTime * 0.4) * 0.5 + 0.5);
|
||||
chrome = mix(chrome, envC, sin(ang * 3.0 + uTime * 0.7) * 0.5 + 0.5 * 0.4);
|
||||
chrome = mix(chrome, envD, fres);
|
||||
|
||||
// Specular highlight where normal points toward "light"
|
||||
float spec = pow(max(dot(n, normalize(vec2(0.6, 0.8))), 0.0), 16.0);
|
||||
chrome += vec3(spec) * 0.9;
|
||||
|
||||
// Background: subtle gradient with vignette
|
||||
vec3 bg = mix(vec3(0.02, 0.03, 0.05), vec3(0.05, 0.02, 0.07), uv.y * 0.5 + 0.5);
|
||||
// Soft glow under metaballs
|
||||
bg += smoothstep(2.0, 3.0, f) * chrome * 0.15;
|
||||
|
||||
vec3 col = bg;
|
||||
col = mix(col, chrome, surface);
|
||||
col += edge * mix(envA, envB, sin(uTime * 0.6) * 0.5 + 0.5) * 0.6;
|
||||
|
||||
// Click → push surface
|
||||
float ce = clickEnergy(frag);
|
||||
col += vec3(0.9, 0.95, 1.0) * ce * 1.2;
|
||||
|
||||
// Tonemap
|
||||
col = col / (1.0 + col * 0.6);
|
||||
gl_FragColor = vec4(col, 1.0);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- ─────────── 4. CRYSTAL (voronoi lattice) ─────────── -->
|
||||
<script id="fs-crystal" type="x-shader/x-fragment">
|
||||
// Voronoi distance to 1st and 2nd nearest
|
||||
vec2 voronoi(vec2 p, out vec2 closestId) {
|
||||
vec2 i = floor(p), f = fract(p);
|
||||
float d1 = 1e9, d2 = 1e9;
|
||||
vec2 ic = vec2(0.0);
|
||||
for (int y = -1; y <= 1; y++) {
|
||||
for (int x = -1; x <= 1; x++) {
|
||||
vec2 g = vec2(float(x), float(y));
|
||||
vec2 o = hash22(i + g);
|
||||
o = 0.5 + 0.5 * sin(uTime * 0.6 + 6.28 * o);
|
||||
vec2 r = g + o - f;
|
||||
float d = dot(r, r);
|
||||
if (d < d1) { d2 = d1; d1 = d; ic = i + g; }
|
||||
else if (d < d2) { d2 = d; }
|
||||
}
|
||||
}
|
||||
closestId = ic;
|
||||
return vec2(sqrt(d1), sqrt(d2));
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 frag = gl_FragCoord.xy;
|
||||
vec2 uv = frag / min(uRes.x, uRes.y);
|
||||
vec2 m = uMouseSmooth / min(uRes.x, uRes.y);
|
||||
|
||||
// Mouse warps the lattice (radial pull)
|
||||
vec2 d = uv - m;
|
||||
float warpStrength = 0.6 * exp(-length(d) * 2.0);
|
||||
vec2 uvw = uv + d * warpStrength;
|
||||
|
||||
// Click pulses: temporary scale push
|
||||
float ce = clickEnergy(frag);
|
||||
uvw *= 1.0 - ce * 0.05;
|
||||
|
||||
vec2 ic;
|
||||
vec2 v = voronoi(uvw * 4.0, ic);
|
||||
float edge = v.y - v.x;
|
||||
float cellId = hash21(ic);
|
||||
|
||||
// Cell base color — prismatic palette per cell
|
||||
vec3 cA = vec3(0.05, 0.15, 0.40);
|
||||
vec3 cB = vec3(0.30, 0.10, 0.50);
|
||||
vec3 cC = vec3(0.10, 0.50, 0.55);
|
||||
vec3 cD = vec3(0.55, 0.20, 0.35);
|
||||
vec3 base = mix(mix(cA, cB, cellId), mix(cC, cD, fract(cellId * 7.1)), fract(cellId * 3.3));
|
||||
|
||||
// Inside-cell gradient — darken away from center
|
||||
float inner = smoothstep(0.0, 0.6, v.x);
|
||||
vec3 fill = base * (1.0 - inner * 0.75);
|
||||
|
||||
// Glowing prismatic edges
|
||||
float edgeMask = 1.0 - smoothstep(0.0, 0.04, edge);
|
||||
vec3 edgeCol = mix(vec3(0.4, 0.9, 1.0), vec3(1.0, 0.3, 0.8),
|
||||
sin(uTime * 0.4 + cellId * 10.0) * 0.5 + 0.5);
|
||||
// Hover highlight on cell under cursor
|
||||
vec2 mIc;
|
||||
voronoi(m * min(uRes.x, uRes.y) / min(uRes.x, uRes.y) * 4.0, mIc);
|
||||
float hovered = step(distance(ic, mIc), 0.1);
|
||||
edgeCol = mix(edgeCol, vec3(1.0, 1.0, 0.85), hovered * 0.8);
|
||||
|
||||
// Refractive caustic — fine secondary lines inside cells
|
||||
float caustic = sin((v.x * 30.0) + uTime * 1.2 + cellId * 20.0);
|
||||
caustic = smoothstep(0.4, 0.95, caustic) * (1.0 - edgeMask);
|
||||
vec3 causticCol = mix(vec3(0.6, 0.8, 1.0), vec3(1.0, 0.7, 0.9), cellId);
|
||||
|
||||
vec3 col = fill + edgeCol * edgeMask * 1.4 + causticCol * caustic * 0.15;
|
||||
|
||||
// Hovered cell brightens overall
|
||||
col += hovered * vec3(0.15, 0.18, 0.22) * (1.0 - edgeMask);
|
||||
|
||||
// Click → ripple of brightness
|
||||
col += vec3(0.6, 0.9, 1.0) * ce * 0.9;
|
||||
|
||||
// Vignette
|
||||
vec2 vuv = frag / uRes - 0.5;
|
||||
col *= 1.0 - dot(vuv, vuv) * 0.7;
|
||||
|
||||
gl_FragColor = vec4(col, 1.0);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- ─────────── 5. WAVES (interference) ─────────── -->
|
||||
<script id="fs-waves" type="x-shader/x-fragment">
|
||||
void main() {
|
||||
vec2 frag = gl_FragCoord.xy;
|
||||
vec2 uv = frag / uRes;
|
||||
vec2 m = uMouseSmooth / uRes;
|
||||
|
||||
// Drifting wave sources (4 of them) + cursor source + click sources
|
||||
vec2 sources[5];
|
||||
sources[0] = vec2(0.2 + 0.05 * sin(uTime * 0.3), 0.3 + 0.04 * cos(uTime * 0.4));
|
||||
sources[1] = vec2(0.75 + 0.06 * cos(uTime * 0.25), 0.25 + 0.05 * sin(uTime * 0.35));
|
||||
sources[2] = vec2(0.3 + 0.07 * sin(uTime * 0.2 + 1.0), 0.78 + 0.04 * cos(uTime * 0.22));
|
||||
sources[3] = vec2(0.78 + 0.05 * cos(uTime * 0.28 + 2.0), 0.72 + 0.06 * sin(uTime * 0.31));
|
||||
sources[4] = m;
|
||||
|
||||
float field = 0.0;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
float dist = distance(uv, sources[i]);
|
||||
// Wave with falloff so sources don't dominate at distance
|
||||
float falloff = 1.0 / (1.0 + dist * 4.0);
|
||||
float speed = (i == 4) ? 5.0 : 3.5 + float(i) * 0.4;
|
||||
float freq = (i == 4) ? 55.0 : 36.0 + float(i) * 4.0;
|
||||
field += sin(dist * freq - uTime * speed) * falloff;
|
||||
}
|
||||
|
||||
// Click sources add expanding wavefronts
|
||||
for (int i = 0; i < 6; i++) {
|
||||
if (uClicks[i].w < 0.5) continue;
|
||||
float age = uTime - uClicks[i].z;
|
||||
if (age < 0.0 || age > 4.0) continue;
|
||||
vec2 cp = uClicks[i].xy / uRes;
|
||||
float d = distance(uv, cp);
|
||||
float radius = age * 0.35;
|
||||
float thickness = 0.04 + age * 0.05;
|
||||
float front = exp(-pow((d - radius) / thickness, 2.0));
|
||||
field += front * exp(-age * 0.7) * 3.0;
|
||||
}
|
||||
|
||||
// Color from field amplitude
|
||||
float v = field * 0.18;
|
||||
vec3 cDeep = vec3(0.02, 0.04, 0.12);
|
||||
vec3 cMid = vec3(0.20, 0.55, 0.95);
|
||||
vec3 cHigh = vec3(0.85, 0.95, 1.00);
|
||||
vec3 cCrest = vec3(1.0, 0.8, 0.5);
|
||||
|
||||
vec3 col = cDeep;
|
||||
col = mix(col, cMid, smoothstep(-0.2, 0.4, v));
|
||||
col = mix(col, cHigh, smoothstep(0.4, 0.8, v));
|
||||
col = mix(col, cCrest, smoothstep(0.8, 1.2, v));
|
||||
|
||||
// Iso-lines for that schlieren / oscilloscope look
|
||||
float iso = abs(fract(field * 1.0) - 0.5);
|
||||
float lines = 1.0 - smoothstep(0.0, 0.03, iso);
|
||||
col += lines * vec3(0.05, 0.15, 0.25);
|
||||
|
||||
// Subtle radial highlight at cursor source
|
||||
float curDist = distance(uv, m);
|
||||
col += exp(-curDist * 10.0) * vec3(0.4, 0.6, 1.0) * 0.3;
|
||||
|
||||
// Vignette
|
||||
vec2 vuv = uv - 0.5;
|
||||
col *= 1.0 - dot(vuv, vuv) * 0.5;
|
||||
|
||||
gl_FragColor = vec4(col, 1.0);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- ─────────── RENDERER ─────────── -->
|
||||
<script>
|
||||
(function() {
|
||||
const canvas = document.getElementById('gl');
|
||||
const gl = canvas.getContext('webgl', { antialias: false, alpha: false, preserveDrawingBuffer: false });
|
||||
if (!gl) {
|
||||
document.body.innerHTML = '<div style="color:#fff;font-family:monospace;padding:40px;">WebGL not available.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const SHADERS = ['aurora','plasma','mercury','crystal','waves'];
|
||||
const DESC = {
|
||||
aurora: { name: 'Aurora Field', body: 'FBM-warped ribbon bands drifting over a deep-night gradient. Cursor displaces field, click discharges a luminous burst.' },
|
||||
plasma: { name: 'Plasma Sun', body: 'A breathing star anchored to the cursor. Corona swirls via polar-domain noise; click triggers a solar shockwave.' },
|
||||
mercury: { name: 'Liquid Mercury', body: 'Iridescent metaball field with one blob attached to the cursor. Chrome reflections shift by surface angle.' },
|
||||
crystal: { name: 'Crystal Lattice', body: 'Animated Voronoi cells with prismatic edges. Cursor warps the lattice radially; the cell beneath it highlights.' },
|
||||
waves: { name: 'Wave Interference',body: 'Superposed sine emitters with the cursor as a roaming source. Click drops a real wavefront that propagates outward.' },
|
||||
};
|
||||
|
||||
// Build vertex shader once
|
||||
const VS = document.getElementById('vs').textContent;
|
||||
const HEADER = document.getElementById('frag-header').textContent;
|
||||
|
||||
function compile(type, src) {
|
||||
const s = gl.createShader(type);
|
||||
gl.shaderSource(s, src);
|
||||
gl.compileShader(s);
|
||||
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
|
||||
console.error('Shader compile error:', gl.getShaderInfoLog(s), src);
|
||||
return null;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
function makeProgram(fsSrc) {
|
||||
const vs = compile(gl.VERTEX_SHADER, VS);
|
||||
const fs = compile(gl.FRAGMENT_SHADER, HEADER + '\n' + fsSrc);
|
||||
if (!vs || !fs) return null;
|
||||
const p = gl.createProgram();
|
||||
gl.attachShader(p, vs); gl.attachShader(p, fs);
|
||||
gl.linkProgram(p);
|
||||
if (!gl.getProgramParameter(p, gl.LINK_STATUS)) {
|
||||
console.error('Link error:', gl.getProgramInfoLog(p));
|
||||
return null;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
// Fullscreen quad
|
||||
const buf = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
|
||||
-1,-1, 1,-1, -1, 1,
|
||||
-1, 1, 1,-1, 1, 1,
|
||||
]), gl.STATIC_DRAW);
|
||||
|
||||
// Compile all programs lazily
|
||||
const programs = {};
|
||||
const uniformCache = {};
|
||||
function getProgram(name) {
|
||||
if (programs[name]) return programs[name];
|
||||
const fsEl = document.getElementById('fs-' + name);
|
||||
if (!fsEl) return null;
|
||||
const p = makeProgram(fsEl.textContent);
|
||||
programs[name] = p;
|
||||
uniformCache[name] = {
|
||||
uRes: gl.getUniformLocation(p, 'uRes'),
|
||||
uTime: gl.getUniformLocation(p, 'uTime'),
|
||||
uMouse: gl.getUniformLocation(p, 'uMouse'),
|
||||
uMouseSmooth: gl.getUniformLocation(p, 'uMouseSmooth'),
|
||||
uClicks: gl.getUniformLocation(p, 'uClicks[0]'),
|
||||
uShaderTime: gl.getUniformLocation(p, 'uShaderTime'),
|
||||
a_pos: gl.getAttribLocation(p, 'a_pos'),
|
||||
};
|
||||
return p;
|
||||
}
|
||||
|
||||
// Resize
|
||||
function resize() {
|
||||
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||
const w = Math.floor(window.innerWidth * dpr);
|
||||
const h = Math.floor(window.innerHeight * dpr);
|
||||
if (canvas.width !== w || canvas.height !== h) {
|
||||
canvas.width = w; canvas.height = h;
|
||||
gl.viewport(0, 0, w, h);
|
||||
}
|
||||
}
|
||||
window.addEventListener('resize', resize);
|
||||
resize();
|
||||
|
||||
// Mouse state
|
||||
const dprFor = () => Math.min(window.devicePixelRatio || 1, 2);
|
||||
const mouse = { x: window.innerWidth * 0.5, y: window.innerHeight * 0.5 };
|
||||
const mouseSmooth = { x: mouse.x, y: mouse.y };
|
||||
window.addEventListener('mousemove', e => {
|
||||
mouse.x = e.clientX;
|
||||
mouse.y = e.clientY;
|
||||
});
|
||||
window.addEventListener('touchmove', e => {
|
||||
if (e.touches.length) {
|
||||
mouse.x = e.touches[0].clientX;
|
||||
mouse.y = e.touches[0].clientY;
|
||||
}
|
||||
}, { passive: true });
|
||||
|
||||
// Clicks — keep last 6, each vec4(x, y, startTime, active)
|
||||
const clicks = new Float32Array(6 * 4);
|
||||
let clickIdx = 0;
|
||||
function recordClick(x, y) {
|
||||
const base = clickIdx * 4;
|
||||
clicks[base + 0] = x;
|
||||
clicks[base + 1] = y;
|
||||
clicks[base + 2] = (performance.now() - startMs) / 1000;
|
||||
clicks[base + 3] = 1.0;
|
||||
clickIdx = (clickIdx + 1) % 6;
|
||||
}
|
||||
canvas.addEventListener('mousedown', e => {
|
||||
const dpr = dprFor();
|
||||
recordClick(e.clientX * dpr, (window.innerHeight - e.clientY) * dpr);
|
||||
});
|
||||
canvas.addEventListener('touchstart', e => {
|
||||
if (e.touches.length) {
|
||||
const dpr = dprFor();
|
||||
recordClick(e.touches[0].clientX * dpr, (window.innerHeight - e.touches[0].clientY) * dpr);
|
||||
}
|
||||
}, { passive: true });
|
||||
|
||||
// UI / switching
|
||||
const dock = document.getElementById('dock');
|
||||
const fpsEl = document.getElementById('fps');
|
||||
const cursEl = document.getElementById('curs');
|
||||
const shNameEl = document.getElementById('shname');
|
||||
const descName = document.getElementById('desc-name');
|
||||
const descBody = document.getElementById('desc-body');
|
||||
const titleEl = document.querySelector('.top-left .title');
|
||||
const flashEl = document.getElementById('flash');
|
||||
|
||||
let current = 'aurora';
|
||||
let shaderStartMs = performance.now();
|
||||
|
||||
function setShader(name) {
|
||||
if (!SHADERS.includes(name) || name === current) {
|
||||
// still allow refresh of UI
|
||||
if (name !== current) return;
|
||||
}
|
||||
current = name;
|
||||
shaderStartMs = performance.now();
|
||||
getProgram(name); // compile if needed
|
||||
|
||||
// UI
|
||||
dock.querySelectorAll('.dock-btn').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.shader === name);
|
||||
});
|
||||
shNameEl.textContent = name;
|
||||
descName.textContent = DESC[name].name;
|
||||
descBody.textContent = DESC[name].body;
|
||||
titleEl.innerHTML = DESC[name].name.split(' ')[0] +
|
||||
'<span class="accent"> · ' + (DESC[name].name.split(' ')[1] || 'Field') + '</span>';
|
||||
|
||||
// Flash overlay
|
||||
flashEl.textContent = name;
|
||||
flashEl.classList.remove('show');
|
||||
// force reflow
|
||||
void flashEl.offsetWidth;
|
||||
flashEl.classList.add('show');
|
||||
setTimeout(() => flashEl.classList.remove('show'), 650);
|
||||
}
|
||||
dock.querySelectorAll('.dock-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => setShader(btn.dataset.shader));
|
||||
});
|
||||
|
||||
// Keyboard shortcuts 1–5
|
||||
window.addEventListener('keydown', e => {
|
||||
const i = parseInt(e.key, 10);
|
||||
if (i >= 1 && i <= 5) setShader(SHADERS[i - 1]);
|
||||
});
|
||||
|
||||
// Prime aurora
|
||||
getProgram('aurora');
|
||||
|
||||
// Render loop
|
||||
const startMs = performance.now();
|
||||
let lastFrame = startMs;
|
||||
let fpsAccum = 0, fpsFrames = 0, fpsLast = startMs;
|
||||
|
||||
function frame() {
|
||||
const now = performance.now();
|
||||
const dt = (now - lastFrame) / 1000;
|
||||
lastFrame = now;
|
||||
const t = (now - startMs) / 1000;
|
||||
const tShader = (now - shaderStartMs) / 1000;
|
||||
|
||||
// FPS
|
||||
fpsAccum += dt; fpsFrames++;
|
||||
if (now - fpsLast > 500) {
|
||||
fpsEl.textContent = (fpsFrames / fpsAccum).toFixed(0);
|
||||
fpsAccum = 0; fpsFrames = 0; fpsLast = now;
|
||||
}
|
||||
|
||||
// Smooth mouse
|
||||
mouseSmooth.x += (mouse.x - mouseSmooth.x) * Math.min(1, dt * 6);
|
||||
mouseSmooth.y += (mouse.y - mouseSmooth.y) * Math.min(1, dt * 6);
|
||||
cursEl.textContent = Math.round(mouse.x) + ',' + Math.round(mouse.y);
|
||||
|
||||
resize();
|
||||
const dpr = dprFor();
|
||||
const W = canvas.width, H = canvas.height;
|
||||
|
||||
const p = getProgram(current);
|
||||
if (!p) { requestAnimationFrame(frame); return; }
|
||||
const u = uniformCache[current];
|
||||
|
||||
gl.useProgram(p);
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
|
||||
gl.enableVertexAttribArray(u.a_pos);
|
||||
gl.vertexAttribPointer(u.a_pos, 2, gl.FLOAT, false, 0, 0);
|
||||
|
||||
gl.uniform2f(u.uRes, W, H);
|
||||
gl.uniform1f(u.uTime, t);
|
||||
gl.uniform1f(u.uShaderTime, tShader);
|
||||
// Mouse in GL coords (Y inverted relative to CSS)
|
||||
gl.uniform2f(u.uMouse, mouse.x * dpr, (window.innerHeight - mouse.y) * dpr);
|
||||
gl.uniform2f(u.uMouseSmooth, mouseSmooth.x * dpr, (window.innerHeight - mouseSmooth.y) * dpr);
|
||||
gl.uniform4fv(u.uClicks, clicks);
|
||||
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||
|
||||
requestAnimationFrame(frame);
|
||||
}
|
||||
requestAnimationFrame(frame);
|
||||
})();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user