HYPERSPHERE - RETROSCOPIC CRT SHADER SOURCE CODE
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>Hypersphere</title>
<style>
:root { color-scheme: dark; }
html, body {
margin: 0;
height: 100%;
background: #000;
overflow: hidden;
}
#stage {
position: fixed;
inset: 0;
filter: grayscale(1) sepia(1) hue-rotate(80deg) saturate(5) brightness(1.15) contrast(1.2);
z-index: 2;
}
canvas {
display: block;
width: 100vw;
height: 100vh;
touch-action: none;
image-rendering: optimizeQuality;
}
/* Retroscopic Overlay */
#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>
<div id="scope">
<div class="glow"></div>
<div class="scan"></div>
<div class="vig"></div>
<div class="mask"></div>
</div>
<div id="stage">
<canvas id="canvas"></canvas>
</div>
<script>
(() => {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d', { alpha: false });
const setSize = () => {
const dpr = Math.max(1, Math.min(3, window.devicePixelRatio || 1));
const w = Math.floor(canvas.clientWidth || window.innerWidth);
const h = Math.floor(canvas.clientHeight || window.innerHeight);
canvas.width = Math.floor(w * dpr);
canvas.height = Math.floor(h * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
cx = w / 2;
cy = h / 2;
baseScale = Math.min(w, h) * 0.2375;
};
let cx = 0, cy = 0, baseScale = 300, dynamicScale = 1;
const MAX_POINTS_DESKTOP = 1800;
const MAX_POINTS_MOBILE = 900;
const TRAIL_FADE = 0.045;
const LINE_ALPHA = 0.65;
const REDUCED_MOTION = matchMedia('(prefers-reduced-motion: reduce)').matches;
const isMobile = /Mobi|Android/i.test(navigator.userAgent) ||
Math.min(window.innerWidth, window.innerHeight) < 768;
const COUNT = REDUCED_MOTION ? 250 : (isMobile ? MAX_POINTS_MOBILE : MAX_POINTS_DESKTOP);
const STAR_COUNT = (Math.min(window.innerWidth, window.innerHeight) < 768 ? 140 : 280);
const stars = new Array(STAR_COUNT).fill(0).map(() => ({
x: Math.random(),
y: Math.random(),
r: Math.random() * 1.2 + 0.3,
p: Math.random() * Math.PI * 2
}));
function drawStars(time){
const w = cx * 2, h = cy * 2;
ctx.save();
ctx.globalCompositeOperation = 'source-over';
for (let i=0; i<STAR_COUNT; i++){
const s = stars[i];
const tw = 0.5 + 0.5 * Math.sin(time*0.001 + s.p);
ctx.globalAlpha = 0.15 + 0.45 * tw;
ctx.beginPath();
ctx.arc(s.x * w, s.y * h, s.r, 0, Math.PI*2);
ctx.fillStyle = `hsla(${(200 + 120*Math.sin(s.p))%360}, 100%, 70%, 1)`;
ctx.fill();
}
ctx.restore();
}
const gauss = (() => {
let spare, hasSpare = false;
return () => {
if (hasSpare) { hasSpare = false; return spare; }
let u=0, v=0, s=0;
while (s===0 || s>=1) { u = Math.random()*2-1; v = Math.random()*2-1; s = u*u + v*v; }
const mul = Math.sqrt(-2*Math.log(s)/s);
spare = v * mul;
hasSpare = true;
return u * mul;
};
})();
function randS3() {
let x = gauss(), y = gauss(), z = gauss(), w = gauss();
const m = Math.hypot(x,y,z,w) || 1;
return [x/m, y/m, z/m, w/m];
}
const nodes = new Array(COUNT);
const prev = new Array(COUNT);
const hues = new Array(COUNT);
const curr = new Array(COUNT);
const buddies = new Uint32Array(COUNT);
const isComet = new Uint8Array(COUNT);
const cometTrails = new Array(COUNT);
for (let i=0; i<COUNT; i++) {
const v = randS3();
nodes[i] = v;
prev[i] = [0, 0];
const h1 = Math.atan2(v[1], v[0]);
const h2 = Math.atan2(v[3], v[2]);
let hue = ((h1 + h2) / (Math.PI*2));
hue = (hue - Math.floor(hue)) * 360;
hues[i] = hue;
buddies[i] = (i * 73 + 97) % COUNT;
}
const COMET_COUNT = Math.max(12, Math.round(COUNT * 0.06));
for (let k=0; k<COMET_COUNT; k++){
const idx = Math.floor(Math.random()*COUNT);
if (isComet[idx]) { k--; continue; }
isComet[idx] = 1;
cometTrails[idx] = [];
}
let t0 = performance.now();
let cxTilt = 0, cyTilt = 0;
window.addEventListener('pointermove', (e) => {
const w = canvas.clientWidth || window.innerWidth;
const h = canvas.clientHeight || window.innerHeight;
const nx = (e.clientX / w) * 2 - 1;
const ny = (e.clientY / h) * 2 - 1;
cxTilt = nx * 0.35;
cyTilt = ny * 0.35;
}, { passive: true });
window.addEventListener('resize', setSize, { passive: true });
setSize();
ctx.globalCompositeOperation = 'lighter';
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
function fadeFrame() {
ctx.save();
ctx.globalCompositeOperation = 'source-over';
ctx.fillStyle = `rgba(0,0,0,${TRAIL_FADE})`;
ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.restore();
}
function rot4(v, ax, ay, az, aw, au, avv) {
let x=v[0], y=v[1], z=v[2], w=v[3];
let c, s, x1,y1,z1,w1;
c = Math.cos(ax); s = Math.sin(ax); x1 = c*x - s*y; y1 = s*x + c*y; x = x1; y = y1;
c = Math.cos(ay); s = Math.sin(ay); z1 = c*z - s*w; w1 = s*z + c*w; z = z1; w = w1;
c = Math.cos(az); s = Math.sin(az); x1 = c*x - s*z; z1 = s*x + c*z; x = x1; z = z1;
c = Math.cos(aw); s = Math.sin(aw); y1 = c*y - s*w; w1 = s*y + c*w; y = y1; w = w1;
c = Math.cos(au); s = Math.sin(au); x1 = c*x - s*w; w1 = s*x + c*w; x = x1; w = w1;
c = Math.cos(avv); s = Math.sin(avv); y1 = c*y - s*z; z1 = s*y + c*z; y = y1; z = z1;
return [x,y,z,w];
}
function projectTo2D(v4) {
let x=v4[0], y=v4[1], z=v4[2], w=v4[3];
const wCam = 1.6;
const f4 = 3.8;
const denom4 = (wCam - w);
const s4 = f4 / (denom4 <= 0.05 ? 0.05 : denom4);
let x3 = x * s4, y3 = y * s4, z3 = z * s4;
const zCam = 3.2;
const f3 = baseScale * dynamicScale;
const denom3 = (zCam - z3);
const s3 = f3 / (denom3 <= 0.05 ? 0.05 : denom3);
const sx = cx + x3 * s3;
const sy = cy + y3 * s3;
const scale = s3;
return [sx, sy, scale];
}
function frame(now) {
const dt = Math.max(0.001, Math.min(0.033, (now - t0) / 1000));
t0 = now;
fadeFrame();
drawStars(now);
const T = now * 0.001;
dynamicScale = 1.0 + 0.12 * Math.sin(T*0.9) + 0.05 * Math.sin(T*0.37 + 1.4);
const ax = 0.6*T + 0.35*Math.sin(T*0.9) + cxTilt*0.6;
const ay = 0.8*T + 0.25*Math.cos(T*1.2) + cyTilt*0.5;
const az = 0.5*T + 0.30*Math.sin(T*0.7);
const aw = 0.7*T + 0.20*Math.cos(T*0.6 + 1.0);
const au = 0.9*T + 0.15*Math.sin(T*0.5 + 0.7);
const avv= 1.1*T + 0.10*Math.cos(T*0.4 + 1.4);
ctx.save();
ctx.lineWidth = Math.max(1, Math.min(3, baseScale / 220));
for (let i=0; i<COUNT; i++) {
const v = nodes[i];
const rot = rot4(v, ax, ay, az, aw, au, avv);
const [sx, sy, sc] = projectTo2D(rot);
const p = prev[i];
const px = p[0] || sx, py = p[1] || sy;
const hue = hues[i];
const depthAlpha = LINE_ALPHA * (0.6 + 0.4 * Math.min(1, sc / (baseScale*0.018)));
ctx.strokeStyle = `hsla(${hue}, 100%, 60%, ${depthAlpha})`;
ctx.lineWidth = Math.max(1.0, Math.min(7, sc * 0.016));
ctx.beginPath();
ctx.moveTo(px, py);
ctx.lineTo(sx, sy);
ctx.stroke();
p[0] = sx; p[1] = sy;
curr[i] = [sx, sy, sc];
if (isComet[i]) {
const trail = cometTrails[i];
trail.push([sx, sy]);
if (trail.length > 14) trail.shift();
ctx.save();
ctx.globalAlpha = 0.8;
ctx.lineWidth = Math.max(1.2, Math.min(6, sc * 0.012));
ctx.strokeStyle = `hsla(${hue}, 100%, 70%, ${Math.min(0.95, depthAlpha + 0.2)})`;
ctx.beginPath();
for (let k=0; k<trail.length; k++) {
const [tx, ty] = trail[k];
if (k === 0) ctx.moveTo(tx, ty); else ctx.lineTo(tx, ty);
}
ctx.stroke();
ctx.restore();
ctx.save();
ctx.globalAlpha = 0.9;
ctx.beginPath();
ctx.arc(sx, sy, Math.max(1.5, Math.min(4.5, sc*0.009)), 0, Math.PI*2);
ctx.fillStyle = `hsla(${hue}, 100%, 65%, 0.85)`;
ctx.fill();
ctx.restore();
}
}
ctx.globalAlpha = 0.18;
ctx.lineWidth = 1;
for (let i=0; i<COUNT; i+=3) {
const j = buddies[i];
const a = curr[i];
const b = curr[j];
if (!a || !b) continue;
const hueMix = ((hues[i] + hues[j]) * 0.5) % 360;
ctx.strokeStyle = `hsla(${hueMix}, 100%, 70%, 0.18)`;
ctx.beginPath();
ctx.moveTo(a[0], a[1]);
ctx.lineTo(b[0], b[1]);
ctx.stroke();
}
ctx.restore();
requestAnimationFrame(frame);
}
ctx.fillStyle = '#000';
ctx.fillRect(0,0,canvas.width,canvas.height);
requestAnimationFrame(frame);
})();
</script>
</body>
</html>