POLYP - RETROSCOPIC CRT SHADER SOURCE CODE
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Bogie</title>
<style>
html, body { margin:0; height:100%; background:#000; overflow:hidden; font-family: -apple-system, "Segoe UI", sans-serif; }
canvas {
display:block;
width:100%;
height:100%;
cursor: grab;
filter: grayscale(1) sepia(1) hue-rotate(80deg) saturate(5) brightness(1.15) contrast(1.2);
position: relative;
z-index: 2;
}
canvas:active { cursor: grabbing; }
#scope {
position: fixed;
inset: 0;
z-index: 4;
pointer-events: none;
display: block;
}
#scope>div {
position: absolute;
inset: 0;
}
#scope .glow {
background: radial-gradient(circle at 50% 46%, rgba(30, 255, 130, .08), rgba(0, 0, 0, 0) 62%);
animation: nvflick 3.6s steps(24) infinite;
}
#scope .scan {
background: repeating-linear-gradient(to bottom, rgba(0, 0, 0, 0) 0 2px, rgba(0, 25, 8, .45) 2px 3px);
animation: nvscan 7s linear infinite;
}
#scope .vig {
box-shadow: inset 0 0 200px 50px rgba(0, 0, 0, .8);
}
#scope .mask {
background: radial-gradient(circle at 50% 50%, rgba(0, 0, 0, 0) 54%, rgba(0, 0, 0, .5) 72%, rgba(0, 0, 0, .94) 100%);
}
@keyframes nvscan {
from { background-position: 0 0; }
to { background-position: 0 240px; }
}
@keyframes nvflick {
0%, 100% { opacity: .55; }
40% { opacity: .9; }
62% { opacity: .4; }
80% { opacity: .75; }
}
</style>
</head>
<body>
<canvas id="gl"></canvas>
<div id="scope">
<div class="glow"></div>
<div class="scan"></div>
<div class="vig"></div>
<div class="mask"></div>
</div>
<script>
const canvas = document.getElementById('gl');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (!gl) { document.body.innerHTML = '<p style="color:#fff;padding:40px;font-family:sans-serif">WebGL not available in this browser.</p>'; }
function resize() {
const scale = 0.6; // internal render scale — canvas stays full size on screen, just fewer pixels to raymarch
canvas.width = Math.round(innerWidth * scale);
canvas.height = Math.round(innerHeight * scale);
canvas.style.width = innerWidth + 'px';
canvas.style.height = innerHeight + 'px';
gl.viewport(0, 0, canvas.width, canvas.height);
}
window.addEventListener('resize', resize);
resize();
const vertSrc = `
attribute vec2 a_pos;
void main(){ gl_Position = vec4(a_pos, 0.0, 1.0); }
`;
const fragSrc = `
precision highp float;
uniform vec2 u_res;
uniform float u_time;
uniform vec2 u_lightDir; // mouse-driven azimuth/elevation offsets
uniform vec2 u_mousePoke; // eased cursor position, deforms the blob surface
uniform vec2 u_camRot; // drag-driven camera yaw/pitch
uniform vec3 u_slimeColor;
uniform vec3 u_bubbleColor;
uniform vec3 u_rimColor;
uniform float u_oozeSpeed;
uniform float u_noiseAmt;
uniform float u_fresnelPow;
uniform float u_translucency;
uniform float u_bubbleCount;
// ---------- hash / noise ----------
float hash1(float n){ return fract(sin(n)*43758.5453123); }
vec3 hash3(float n){
return fract(sin(vec3(n,n+1.0,n+2.0))*vec3(43758.5453123,22578.1459123,19642.3490423));
}
float noise(vec3 p){
vec3 i = floor(p); vec3 f = fract(p);
f = f*f*(3.0-2.0*f);
float n = i.x + i.y*157.0 + 113.0*i.z;
return mix(
mix(mix(hash1(n+0.0),hash1(n+1.0),f.x), mix(hash1(n+157.0),hash1(n+158.0),f.x), f.y),
mix(mix(hash1(n+113.0),hash1(n+114.0),f.x), mix(hash1(n+270.0),hash1(n+271.0),f.x), f.y),
f.z);
}
float fbm(vec3 p){
float s = 0.0, a = 0.5;
for(int i=0;i<2;i++){ s += a*noise(p); p *= 2.02; a *= 0.5; }
return s;
}
// ---------- sdf ----------
float sdSphere(vec3 p, float r){ return length(p) - r; }
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 bubblePos(float idx){
vec3 h = hash3(idx*17.13);
float t = u_time*u_oozeSpeed*0.6 + h.x*40.0;
float r = 0.55 + h.y*0.55;
float ang = h.x*6.2831 + t*0.5;
float y = -0.7 + 1.1*fract(h.z + t*0.07);
float yFold = abs(fract(h.z + t*0.07)*2.0 - 1.0); // bounce 0..1..0
return vec3(cos(ang)*r*(1.0-0.3*yFold), -0.9 + yFold*1.5, sin(ang)*r*(1.0-0.3*yFold));
}
float blobSDF(vec3 p){
vec3 q = p;
float n = fbm(q*1.7 + vec3(0.0, u_time*u_oozeSpeed*0.8, u_time*u_oozeSpeed*0.3));
float d = sdSphere(p, 1.25) + (n-0.5)*u_noiseAmt;
// subtle cursor interaction: a soft dimple that follows the eased
// mouse position across the front of the blob, like a fingertip
// pressing into it
vec3 pokePos = vec3(u_mousePoke.x*1.05, 0.15 + u_mousePoke.y*0.55, 0.95);
float pokeDist = length(p - pokePos);
float dimple = smoothstep(0.85, 0.0, pokeDist) * 0.10;
d += dimple;
return d;
}
float map(vec3 p){
float blob = blobSDF(p);
float floorPlane = p.y + 1.45;
return smin(blob, floorPlane, 0.55);
}
vec3 calcNormal(vec3 p){
const vec2 e = vec2(1.0,-1.0)*0.0015;
return normalize(
e.xyy*map(p+e.xyy)+
e.yyx*map(p+e.yyx)+
e.yxy*map(p+e.yxy)+
e.xxx*map(p+e.xxx));
}
float softShadow(vec3 ro, vec3 rd, float mint, float maxt, float k){
float res = 1.0; float t = mint;
for(int i=0;i<12;i++){
if(t>=maxt) break;
float h = map(ro+rd*t);
if(h<0.001) return 0.0;
res = min(res, k*h/t);
t += clamp(h, 0.05, 0.5);
}
return clamp(res,0.0,1.0);
}
float calcAO(vec3 p, vec3 n){
float occ = 0.0, sca = 1.0;
for(int i=0;i<3;i++){
float h = 0.02 + 0.16*float(i);
float d = map(p + n*h);
occ += (h-d)*sca;
sca *= 0.7;
}
return clamp(1.0 - 1.6*occ, 0.0, 1.0);
}
void main(){
vec2 uv = (gl_FragCoord.xy - 0.5*u_res) / u_res.y;
// camera, orbit-controlled
float yaw = u_camRot.x;
float pitch = clamp(u_camRot.y, -1.3, 1.1);
float dist = 4.3;
vec3 camPos = vec3(sin(yaw)*cos(pitch), sin(pitch), cos(yaw)*cos(pitch)) * dist;
camPos.y += 0.3;
vec3 camTarget = vec3(0.0, -0.3, 0.0);
vec3 fwd = normalize(camTarget - camPos);
vec3 right = normalize(cross(fwd, vec3(0.0,1.0,0.0)));
vec3 up = cross(right, fwd);
vec3 rd = normalize(fwd*1.6 + uv.x*right + uv.y*up);
vec3 ro = camPos;
// light position driven by mouse
vec3 lightPos = vec3(u_lightDir.x*4.0, 1.4 + u_lightDir.y*2.2, 2.6);
// raymarch
float t = 0.0; bool hit = false; vec3 p;
for(int i=0;i<64;i++){
p = ro + rd*t;
float d = map(p);
if(d < 0.0015){ hit = true; break; }
t += d * 0.9;
if(t > 16.0) break;
}
vec3 col;
vec3 bg1 = vec3(0.02,0.035,0.045);
vec3 bg2 = vec3(0.06,0.10,0.10);
col = mix(bg1, bg2, smoothstep(-1.0,1.0,uv.y)) * (1.0 - 0.35*length(uv));
if(hit){
vec3 n = calcNormal(p);
vec3 V = normalize(ro - p);
vec3 L = normalize(lightPos - p);
vec3 H = normalize(L + V);
float diff = max(dot(n,L), 0.0);
float spec = pow(max(dot(n,H),0.0), 70.0);
float fres = pow(1.0 - max(dot(n,V),0.0), u_fresnelPow);
float ao = calcAO(p, n);
float shadow = softShadow(p + n*0.02, L, 0.02, 6.0, 12.0);
// fake subsurface: cheap noise-based thinness estimate instead of
// extra map() samples
float thin = clamp(0.5 + 0.6*(fbm(p*2.0)-0.5), 0.0, 1.0);
float wrap = max(0.0, dot(-n, L)*0.5 + 0.5);
float sss = wrap * (0.4 + 0.6*thin) * u_translucency;
vec3 base = u_slimeColor;
vec3 ambient = base * 0.16 * ao;
vec3 diffuse = base * diff * shadow * 0.9;
vec3 sssCol = mix(base, vec3(1.0), 0.4) * sss * 0.8;
vec3 specCol = vec3(1.0) * spec * shadow;
vec3 rim = u_rimColor * fres * (0.5 + 0.5*diff) ;
// emissive internal bubbles glowing through the slime
vec3 bubbleGlow = vec3(0.0);
int maxB = int(u_bubbleCount);
for(int i=0;i<8;i++){
if(i >= maxB) break;
vec3 bc = bubblePos(float(i));
float d = length(p - bc);
float pulse = 0.6 + 0.4*sin(u_time*1.7 + float(i)*2.1);
float g = exp(-d*d*9.0) * pulse;
bubbleGlow += u_bubbleColor * g;
}
bubbleGlow *= (0.5 + 0.5*thin);
// faint glint where the cursor is pressing into the surface
vec3 pokePos = vec3(u_mousePoke.x*1.05, 0.15 + u_mousePoke.y*0.55, 0.95);
float glintD = length(p - pokePos);
float glint = exp(-glintD*glintD*9.0) * 0.35;
vec3 glintCol = vec3(1.0) * glint;
col = ambient + diffuse + sssCol + specCol + rim + bubbleGlow + glintCol;
// floor checker fade where blob meets ground
if(p.y < -1.4 + 0.02){
float chk = mod(floor(p.x*1.5)+floor(p.z*1.5), 2.0);
col = mix(col, vec3(0.05,0.07,0.08)*(0.7+0.3*chk), 0.0);
}
// distance fog
col = mix(col, bg2*0.6, smoothstep(6.0, 16.0, t));
}
// tonemap + gamma
col = col / (1.0 + col);
col = pow(col, vec3(0.4545));
gl_FragColor = vec4(col, 1.0);
}
`;
function compile(src, type){
const s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
if(!gl.getShaderParameter(s, gl.COMPILE_STATUS)){
console.error(gl.getShaderInfoLog(s));
}
return s;
}
const prog = gl.createProgram();
gl.attachShader(prog, compile(vertSrc, gl.VERTEX_SHADER));
gl.attachShader(prog, compile(fragSrc, gl.FRAGMENT_SHADER));
gl.linkProgram(prog);
if(!gl.getProgramParameter(prog, gl.LINK_STATUS)){
console.error(gl.getProgramInfoLog(prog));
}
gl.useProgram(prog);
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 3,-1, -1,3]), gl.STATIC_DRAW);
const aPos = gl.getAttribLocation(prog, 'a_pos');
gl.enableVertexAttribArray(aPos);
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
const U = {};
['u_res','u_time','u_lightDir','u_mousePoke','u_camRot','u_slimeColor','u_bubbleColor','u_rimColor',
'u_oozeSpeed','u_noiseAmt','u_fresnelPow','u_translucency','u_bubbleCount'].forEach(name=>{
U[name] = gl.getUniformLocation(prog, name);
});
function hsl2rgb(h, s, l){
h = ((h % 1) + 1) % 1;
const k = n => (n + h*12) % 12;
const a = s * Math.min(l, 1-l);
const f = n => l - a*Math.max(-1, Math.min(k(n)-3, Math.min(9-k(n), 1)));
return [f(0), f(8), f(4)];
}
// ---- state ----
let mouseLight = [0,0];
let lightSmooth = [0,0];
let pokeSmooth = [0,0];
let userMoved = false;
let camRot = [0.4, 0.15];
let dragging = false, lastX=0, lastY=0;
canvas.addEventListener('mousemove', (e)=>{
userMoved = true;
const nx = (e.clientX / innerWidth)*2 - 1;
const ny = (e.clientY / innerHeight)*2 - 1;
mouseLight = [nx, -ny];
if(dragging){
camRot[0] += (e.clientX - lastX) * 0.006;
camRot[1] += (e.clientY - lastY) * 0.006;
lastX = e.clientX; lastY = e.clientY;
}
});
canvas.addEventListener('mousedown', (e)=>{ dragging = true; lastX = e.clientX; lastY = e.clientY; });
window.addEventListener('mouseup', ()=>{ dragging = false; });
canvas.addEventListener('touchstart', (e)=>{ dragging=true; lastX=e.touches[0].clientX; lastY=e.touches[0].clientY; }, {passive:true});
canvas.addEventListener('touchmove', (e)=>{
const t = e.touches[0];
const nx = (t.clientX/innerWidth)*2-1, ny=(t.clientY/innerHeight)*2-1;
mouseLight=[nx,-ny];
camRot[0]+=(t.clientX-lastX)*0.006; camRot[1]+=(t.clientY-lastY)*0.006;
lastX=t.clientX; lastY=t.clientY;
}, {passive:true});
window.addEventListener('touchend', ()=>{ dragging=false; });
const params = {
slime: [0.1, 0.7, 0.5],
bubble: [1.0, 0.9, 0.4],
rim: [0.5, 1.0, 0.85],
ooze: 0.35, noiseAmt: 0.18, fres: 2.4, trans: 1.0, bub: 7
};
// Drives every parameter on its own slow, organic curve so the whole
// shader looks like it's being live-tweaked, without any UI at all.
function animateParams(time){
params.ooze = 0.25 + 0.30 * (0.5 + 0.5*Math.sin(time*0.17));
params.noiseAmt = 0.10 + 0.16 * (0.5 + 0.5*Math.sin(time*0.09 + 1.3));
params.fres = 1.2 + 3.2 * (0.5 + 0.5*Math.sin(time*0.06 + 2.6));
params.trans = 0.5 + 1.1 * (0.5 + 0.5*Math.sin(time*0.11 + 0.4));
params.bub = Math.round(2 + 6 * (0.5 + 0.5*Math.sin(time*0.05 + 4.1)));
// colours cruise slowly through the hue wheel, offset from each other
// for a vivid, ever-shifting palette
const hueBase = time * 0.025;
params.slime = hsl2rgb(hueBase, 0.70, 0.50 + 0.08*Math.sin(time*0.13));
params.bubble = hsl2rgb(hueBase + 0.36, 0.85, 0.55 + 0.10*Math.sin(time*0.21 + 1.0));
params.rim = hsl2rgb(hueBase + 0.68, 0.75, 0.65 + 0.08*Math.sin(time*0.17 + 2.0));
}
const start = performance.now();
function frame(){
const time = (performance.now() - start) / 1000;
if(!dragging) camRot[0] += 0.0022;
animateParams(time);
if(!userMoved){
mouseLight = [Math.sin(time*0.31)*0.8, Math.sin(time*0.21+1.4)*0.7];
}
lightSmooth[0] += (mouseLight[0]-lightSmooth[0])*0.08;
lightSmooth[1] += (mouseLight[1]-lightSmooth[1])*0.08;
pokeSmooth[0] += (mouseLight[0]-pokeSmooth[0])*0.035;
pokeSmooth[1] += (mouseLight[1]-pokeSmooth[1])*0.035;
gl.uniform2f(U.u_res, canvas.width, canvas.height);
gl.uniform1f(U.u_time, time);
gl.uniform2f(U.u_lightDir, lightSmooth[0], lightSmooth[1]);
gl.uniform2f(U.u_mousePoke, pokeSmooth[0], pokeSmooth[1]);
gl.uniform2f(U.u_camRot, camRot[0], camRot[1]);
gl.uniform3f(U.u_slimeColor, params.slime[0], params.slime[1], params.slime[2]);
gl.uniform3f(U.u_bubbleColor, params.bubble[0], params.bubble[1], params.bubble[2]);
gl.uniform3f(U.u_rimColor, params.rim[0], params.rim[1], params.rim[2]);
gl.uniform1f(U.u_oozeSpeed, params.ooze);
gl.uniform1f(U.u_noiseAmt, params.noiseAmt);
gl.uniform1f(U.u_fresnelPow, params.fres);
gl.uniform1f(U.u_translucency, params.trans);
gl.uniform1f(U.u_bubbleCount, params.bub);
gl.drawArrays(gl.TRIANGLES, 0, 3);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>
</body>
</html>