LUCENT - WEBGL CATHEDRAL PROCEDURAL SHADER SOURCE CODE
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Lucent</title>
<style>
html,body{margin:0;padding:0;overflow:hidden;background:#000;width:100%;height:100%;}
canvas{display:block;width:100vw;height:100vh;}
#hint{
position:fixed;left:50%;bottom:18px;transform:translateX(-50%);
color:rgba(255,255,255,0.35);font-family:Georgia,serif;font-size:12px;
letter-spacing:0.12em;text-transform:uppercase;pointer-events:none;
transition:opacity 2s ease;
}
</style>
</head>
<body>
<canvas id="gl"></canvas>
<script>
const canvas = document.getElementById('gl');
const hint = document.getElementById('hint');
let gl = canvas.getContext('webgl', {antialias:true, alpha:false, powerPreference:'high-performance'})
|| canvas.getContext('experimental-webgl');
if (!gl) {
document.body.innerHTML = '<p style="color:#fff;font-family:sans-serif;padding:2em;">WebGL is not available in this browser.</p>';
throw new Error('no webgl');
}
const VERT = `
attribute vec2 aPos;
void main(){
gl_Position = vec4(aPos, 0.0, 1.0);
}
`;
const FRAG = `
precision highp float;
uniform vec2 uRes;
uniform float uTime;
uniform vec2 uMouse;
uniform float uMouseEnergy;
#define PI 3.14159265359
#define TAU 6.28318530718
mat2 rot(float a){ float c = cos(a), s = sin(a); return mat2(c,-s,s,c); }
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){
float n = hash21(p);
float n2 = hash21(p+1.7);
return vec2(n, n2);
}
float vnoise(vec2 p){
vec2 i = floor(p);
vec2 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;
float a = 0.5;
for(int i=0;i<5;i++){
v += a*vnoise(p);
p = rot(0.6)*p*2.02;
a *= 0.55;
}
return v;
}
vec4 voronoi(vec2 p, float jitterAmp){
vec2 ip = floor(p);
vec2 fp = fract(p);
float minD1 = 8.0, minD2 = 8.0;
vec2 bestId = vec2(0.0);
for(int y=-1;y<=1;y++){
for(int x=-1;x<=1;x++){
vec2 cell = vec2(float(x), float(y));
vec2 id = ip + cell;
vec2 jitter = hash22(id);
jitter += 0.18*jitterAmp*vec2(
sin(uTime*0.07 + id.x*1.7 + id.y*0.6),
cos(uTime*0.061 + id.y*1.3 - id.x*0.4)
);
vec2 r = cell + jitter - fp;
float d = dot(r,r);
if(d < minD1){
minD2 = minD1;
minD1 = d;
bestId = id;
} else if(d < minD2){
minD2 = d;
}
}
}
return vec4(sqrt(minD1), sqrt(minD2), bestId);
}
float quasicrystal(vec2 p, float t){
float sum = 0.0;
const int N = 6;
for(int i=0;i<N;i++){
float a = float(i) * PI / float(N) + t*0.015;
vec2 dir = vec2(cos(a), sin(a));
sum += cos(dot(dir, p) * 6.0 + t*0.25*sin(float(i)*1.3));
}
return sum / float(N);
}
vec2 kaleidoFold(vec2 p, float foldCount, float twist){
float a = atan(p.y, p.x);
float r = length(p);
float seg = TAU / foldCount;
a = mod(a, seg);
a = abs(a - seg*0.5);
a += twist * r;
return vec2(cos(a), sin(a)) * r;
}
vec2 hyperBreathe(vec2 p, float t){
float r = length(p) + 1e-5;
float theta = atan(p.y, p.x);
float lr = log(r);
lr += 0.12*sin(t*0.035) + 0.05*sin(t*0.091 + 1.7);
float newR = exp(lr);
return vec2(cos(theta), sin(theta)) * newR;
}
float smin(float a, float b, float k){
float h = clamp(0.5 + 0.5*(b-a)/k, 0.0, 1.0);
return mix(b,a,h) - k*h*(1.0-h);
}
vec3 hsv2rgb(vec3 c){
vec3 rgb = clamp(abs(mod(c.x*6.0+vec3(0.0,4.0,2.0),6.0)-3.0)-1.0, 0.0, 1.0);
rgb = rgb*rgb*(3.0-2.0*rgb);
return c.z * mix(vec3(1.0), rgb, c.y);
}
void main(){
vec2 uv = (gl_FragCoord.xy - 0.5*uRes) / min(uRes.x, uRes.y);
float T = uTime;
float t1 = T * 0.063;
float t2 = T * 0.0247 + 11.0;
float t3 = T * 0.0119 + 3.0;
float t4 = T * 0.00731 + 7.0;
vec2 m = uMouse * 0.35;
vec2 p = uv - m*0.15;
p = hyperBreathe(p, T*6.0);
p = rot(t4*0.6 + sin(t2)*0.15) * p;
float foldCount = 5.0 + 3.0*sin(t3*0.7) + 2.0*sin(t1*0.31);
foldCount = max(3.0, foldCount);
float twist = 0.6*sin(t2*0.5) + 0.2*sin(t1*1.7);
vec2 kp = kaleidoFold(p*1.6, floor(foldCount)+2.0, twist*0.4);
float order = 0.5 + 0.5*sin(t1*0.4) ;
vec2 domain = mix(p*1.6, kp, order);
vec2 d1 = domain * (1.4 + 0.25*sin(t2*0.8));
vec2 d2 = domain * (3.4 + 0.6*cos(t3*0.6)) * rot(0.9);
vec2 d3 = domain * (8.0 + 1.5*sin(t1*1.1)) * rot(-1.7);
vec4 v1 = voronoi(d1 + 0.3*sin(t2), 1.0);
vec4 v2 = voronoi(d2 + 0.4*cos(t3*1.3), 0.7);
vec4 v3 = voronoi(d3 + 0.2*sin(t1*2.0), 0.5);
float e1 = smoothstep(0.0, 0.07, v1.y - v1.x);
float e2 = smoothstep(0.0, 0.05, v2.y - v2.x);
float e3 = smoothstep(0.0, 0.035, v3.y - v3.x);
float qc = quasicrystal(domain*2.2 + vec2(t1, -t2), T);
qc = qc*0.5 + 0.5;
float chemA = fbm(d1*0.8 + vec2(t3*0.3, -t4*0.4) + v1.zw*0.13);
float chemB = fbm(d2*1.3 - vec2(t4*0.2, t1*0.5) + v2.zw*0.21);
float activator = smoothstep(0.2, 0.8, chemA - chemB*0.6);
float idHash1 = hash21(v1.zw);
float idHash2 = hash21(v2.zw*1.37 + 5.1);
float idHash3 = hash21(v3.zw*2.11 + 9.3);
float baseHue = fract(0.62 + 0.08*sin(t1*0.5) + 0.05*sin(t2*0.27));
float hue = fract(baseHue
+ 0.10*idHash1
+ 0.05*idHash2
- 0.06*idHash3
+ 0.07*qc
+ 0.05*activator
+ 0.02*uMouseEnergy);
float sat = clamp(0.55 + 0.25*activator + 0.15*qc - 0.1*idHash2, 0.15, 0.95);
float depthShade = mix(1.0, 0.55, e1) * mix(1.0, 0.7, e2*0.8) ;
float val = 0.35 + 0.55*qc*depthShade + 0.25*activator;
val *= mix(0.7, 1.25, idHash1);
vec3 col = hsv2rgb(vec3(hue, sat, clamp(val,0.0,1.4)));
vec3 edgeGlow = vec3(0.0);
float edgeMask1 = 1.0 - e1;
float edgeMask2 = 1.0 - e2;
float edgeMask3 = 1.0 - e3;
vec3 edgeCol1 = hsv2rgb(vec3(fract(hue+0.5), 0.6, 1.4));
vec3 edgeCol2 = hsv2rgb(vec3(fract(hue+0.15), 0.5, 1.1));
vec3 edgeCol3 = vec3(1.0);
edgeGlow += edgeCol1 * pow(edgeMask1, 3.0) * 1.1;
edgeGlow += edgeCol2 * pow(edgeMask2, 4.0) * 0.6;
edgeGlow += edgeCol3 * pow(edgeMask3, 6.0) * 0.4;
col += edgeGlow;
float bloom = 0.0;
bloom += smoothstep(0.25, 0.0, e1) * 0.35;
bloom += smoothstep(0.15, 0.0, e2) * 0.25;
bloom += smoothstep(0.10, 0.0, e3) * 0.18;
vec3 bloomCol = hsv2rgb(vec3(fract(hue+0.55), 0.4, 1.0));
col += bloomCol * bloom * (0.6 + 0.4*qc);
float micro = quasicrystal(domain*9.0 + vec2(-t2*1.3, t3*0.9), T*1.4);
col += 0.035 * micro * vec3(0.8,0.9,1.0) * (1.0-e1);
float vig = smoothstep(1.3, 0.1, length(uv));
col *= mix(0.55, 1.05, vig);
float mouseGlow = uMouseEnergy * exp(-length(uv-uMouse)*2.2) * 0.5;
col += mouseGlow * hsv2rgb(vec3(fract(hue+0.3), 0.5, 1.0));
col = col / (1.0 + 0.6*col);
col = pow(col, vec3(0.92));
float dither = (hash21(gl_FragCoord.xy + fract(T)*97.0) - 0.5) / 255.0;
col += dither;
gl_FragColor = vec4(col, 1.0);
}
`;
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(gl.getShaderInfoLog(s));
throw new Error('shader compile error');
}
return s;
}
const prog = gl.createProgram();
gl.attachShader(prog, compile(gl.VERTEX_SHADER, VERT));
gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, FRAG));
gl.linkProgram(prog);
if(!gl.getProgramParameter(prog, gl.LINK_STATUS)){
console.error(gl.getProgramInfoLog(prog));
throw new Error('program link error');
}
gl.useProgram(prog);
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);
const aPos = gl.getAttribLocation(prog, 'aPos');
gl.enableVertexAttribArray(aPos);
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
const uRes = gl.getUniformLocation(prog, 'uRes');
const uTime = gl.getUniformLocation(prog, 'uTime');
const uMouse = gl.getUniformLocation(prog, 'uMouse');
const uMouseEnergy = gl.getUniformLocation(prog, 'uMouseEnergy');
function resize(){
const dpr = Math.min(window.devicePixelRatio || 1, 2.0);
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,canvas.width, canvas.height);
}
window.addEventListener('resize', resize);
resize();
let mouseTarget = [0,0];
let mouseSmooth = [0,0];
let mouseEnergy = 0.0;
let lastMouseMoveTime = -10;
function setMouseFromEvent(clientX, clientY){
const nx = (clientX / window.innerWidth) * 2.0 - 1.0;
const ny = -((clientY / window.innerHeight) * 2.0 - 1.0);
mouseTarget[0] = nx;
mouseTarget[1] = ny;
lastMouseMoveTime = performance.now()/1000;
}
window.addEventListener('mousemove', e => setMouseFromEvent(e.clientX, e.clientY));
window.addEventListener('touchmove', e => {
if(e.touches.length>0) setMouseFromEvent(e.touches[0].clientX, e.touches[0].clientY);
}, {passive:true});
let hintFaded = false;
function fadeHint(){
if(!hintFaded){
hintFaded = true;
if(hint) hint.style.opacity = '0';
}
}
window.addEventListener('mousemove', fadeHint, {once:true});
setTimeout(fadeHint, 6000);
const start = performance.now();
function frame(){
const now = performance.now();
const t = (now - start) / 1000.0;
mouseSmooth[0] += (mouseTarget[0]-mouseSmooth[0]) * 0.06;
mouseSmooth[1] += (mouseTarget[1]-mouseSmooth[1]) * 0.06;
const sinceMove = (now/1000) - lastMouseMoveTime;
const targetEnergy = sinceMove < 1.5 ? 1.0 : 0.0;
mouseEnergy += (targetEnergy - mouseEnergy) * 0.04;
resize();
gl.useProgram(prog);
gl.uniform2f(uRes, canvas.width, canvas.height);
gl.uniform1f(uTime, t);
gl.uniform2f(uMouse, mouseSmooth[0]*(canvas.width/Math.min(canvas.width,canvas.height)), mouseSmooth[1]*(canvas.height/Math.min(canvas.width,canvas.height)));
gl.uniform1f(uMouseEnergy, mouseEnergy);
gl.drawArrays(gl.TRIANGLES, 0, 6);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>
</body>
</html>