Custom Code Experiment •
Sphere 2
Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SVG: Animated Glyph - Glitch Icosphere Edition</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background-color: #000;
overflow: hidden;
height: 100vh;
width: 100vw;
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
.container {
width: 100%; height: 100%;
display: flex;
justify-content: center;
align-items: center;
position: relative;
z-index: 1;
}
canvas {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
z-index: 1;
}
.instructions {
position: absolute;
bottom: 20px;
color: #0ff;
font-family: 'Courier New', monospace;
font-size: 14px;
text-align: center;
width: 100%;
text-shadow: 0 0 10px #0ff, 0 0 20px #0ff;
z-index: 2;
animation: textGlitch 5s infinite;
}
</style>
</head>
<body>
<div class="container">
<canvas id="c"></canvas>
</div>
<script>
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resize();
window.addEventListener('resize', resize);
// --- Icosphere geometry (subdivision level 2) ---
const t = (1 + Math.sqrt(5)) / 2;
function normalize(v) {
const len = Math.sqrt(v[0]*v[0]+v[1]*v[1]+v[2]*v[2]);
return [v[0]/len, v[1]/len, v[2]/len];
}
// Base icosahedron vertices
let verts = [
[-1,t,0],[1,t,0],[-1,-t,0],[1,-t,0],
[0,-1,t],[0,1,t],[0,-1,-t],[0,1,-t],
[t,0,-1],[t,0,1],[-t,0,-1],[-t,0,1]
].map(normalize);
let faces = [
[0,11,5],[0,5,1],[0,1,7],[0,7,10],[0,10,11],
[1,5,9],[5,11,4],[11,10,2],[10,7,6],[7,1,8],
[3,9,4],[3,4,2],[3,2,6],[3,6,8],[3,8,9],
[4,9,5],[2,4,11],[6,2,10],[8,6,7],[9,8,1]
];
// Subdivide once
function midpoint(a, b, cache, verts) {
const key = [Math.min(a,b), Math.max(a,b)].join('_');
if (cache[key] !== undefined) return cache[key];
const v = normalize([
(verts[a][0]+verts[b][0])/2,
(verts[a][1]+verts[b][1])/2,
(verts[a][2]+verts[b][2])/2
]);
cache[key] = verts.length;
verts.push(v);
return cache[key];
}
function subdivide(verts, faces) {
const cache = {};
const newFaces = [];
for (const [a,b,c] of faces) {
const ab = midpoint(a,b,cache,verts);
const bc = midpoint(b,c,cache,verts);
const ca = midpoint(c,a,cache,verts);
newFaces.push([a,ab,ca],[b,bc,ab],[c,ca,bc],[ab,bc,ca]);
}
return newFaces;
}
faces = subdivide(verts, faces);
faces = subdivide(verts, faces);
// Unique edges
const edgeSet = new Set();
const edges = [];
for (const [a,b,c] of faces) {
for (const [i,j] of [[a,b],[b,c],[c,a]]) {
const key = [Math.min(i,j),Math.max(i,j)].join('_');
if (!edgeSet.has(key)) { edgeSet.add(key); edges.push([i,j]); }
}
}
// --- Projection ---
let rotX = 0, rotY = 0, rotZ = 0;
function rotateXYZ(v, rx, ry, rz) {
let [x,y,z] = v;
// Rotate X
let y2 = y*Math.cos(rx) - z*Math.sin(rx);
let z2 = y*Math.sin(rx) + z*Math.cos(rx);
y=y2; z=z2;
// Rotate Y
let x2 = x*Math.cos(ry) + z*Math.sin(ry);
z2 = -x*Math.sin(ry) + z*Math.cos(ry);
x=x2; z=z2;
// Rotate Z
x2 = x*Math.cos(rz) - y*Math.sin(rz);
y2 = x*Math.sin(rz) + y*Math.cos(rz);
return [x2, y2, z];
}
function project(v, cx, cy, scale) {
const fov = 4;
const z = v[2] + fov;
return [cx + (v[0] / z) * scale, cy + (v[1] / z) * scale];
}
function draw(ts) {
const W = canvas.width, H = canvas.height;
const cx = W/2, cy = H/2;
const scale = Math.min(W,H) * 1.4;
ctx.clearRect(0,0,W,H);
rotX += 0.003;
rotY += 0.007;
rotZ += 0.001;
// Project all vertices
const projected = verts.map(v => {
const rv = rotateXYZ(v, rotX, rotY, rotZ);
return project(rv, cx, cy, scale);
});
// Compute depth for faces (for back-face sorting + shading)
const faceDepths = faces.map((f, i) => {
const rv0 = rotateXYZ(verts[f[0]], rotX, rotY, rotZ);
const rv1 = rotateXYZ(verts[f[1]], rotX, rotY, rotZ);
const rv2 = rotateXYZ(verts[f[2]], rotX, rotY, rotZ);
const z = (rv0[2]+rv1[2]+rv2[2])/3;
// Normal (for backface culling)
const ax = rv1[0]-rv0[0], ay = rv1[1]-rv0[1];
const bx = rv2[0]-rv0[0], by = rv2[1]-rv0[1];
const nz = ax*by - ay*bx; // z component of cross product
return { i, z, visible: nz < 0 };
});
faceDepths.sort((a,b) => a.z - b.z);
// Draw faces — depth + diffuse + camera-facing colour baked per triangle
const hueShift = ts * 0.0004;
const cycleR = Math.floor(128 + 127 * Math.sin(hueShift));
const cycleG = Math.floor(128 + 127 * Math.sin(hueShift + 2.09));
const cycleB = Math.floor(128 + 127 * Math.sin(hueShift + 4.19));
for (const {i, z, visible} of faceDepths) {
if (!visible) continue;
const [a,b,c] = faces[i];
const pa = projected[a], pb = projected[b], pc = projected[c];
// Rotated vertices for normal calculation
const rv0 = rotateXYZ(verts[a], rotX, rotY, rotZ);
const rv1 = rotateXYZ(verts[b], rotX, rotY, rotZ);
const rv2 = rotateXYZ(verts[c], rotX, rotY, rotZ);
// Face normal via cross product
const ax = rv1[0]-rv0[0], ay = rv1[1]-rv0[1], az = rv1[2]-rv0[2];
const bx = rv2[0]-rv0[0], by = rv2[1]-rv0[1], bz = rv2[2]-rv0[2];
const nx = ay*bz - az*by;
const ny = az*bx - ax*bz;
const nz2 = ax*by - ay*bx;
const nlen = Math.sqrt(nx*nx + ny*ny + nz2*nz2) || 1;
// Light from top-right-front: normalised (0.5, -0.5, 1)
const lx = 0.5/1.22, ly = -0.5/1.22, lz = 1/1.22;
const diffuse = Math.max(0, (nx/nlen)*lx + (ny/nlen)*ly + (nz2/nlen)*lz);
// Camera-facing factor: dot of face normal with view direction (0,0,1)
// nz2/nlen = 1 means face points straight at camera, 0 = edge-on
const camDot = Math.max(0, -(nz2 / nlen)); // negated because backface-culled normals point away
const camFacing = Math.pow(camDot, 2.5); // sharpen so only the most front-facing triangles glow
// Depth factor
const d = Math.max(0, Math.min(1, (z + 1) / 2));
// Base diffuse colour
const light = 0.08 + diffuse * 0.92;
let r = light * (20 + d * 160);
let g = light * (80 + d * 150);
let bl = light * (120 + d * 135);
// Blend in cycling colour on camera-facing triangles
r = r + camFacing * (cycleR - r);
g = g + camFacing * (cycleG - g);
bl = bl + camFacing * (cycleB - bl);
const alpha = 0.15 + diffuse * 0.6 + d * 0.15 + camFacing * 0.25;
ctx.beginPath();
ctx.moveTo(pa[0],pa[1]);
ctx.lineTo(pb[0],pb[1]);
ctx.lineTo(pc[0],pc[1]);
ctx.closePath();
ctx.fillStyle = `rgba(${Math.floor(r)},${Math.floor(g)},${Math.floor(bl)},${Math.min(1, alpha)})`;
ctx.fill();
}
// Draw edges
const edgeDepths = edges.map(([a,b]) => {
const ra = rotateXYZ(verts[a], rotX, rotY, rotZ);
const rb = rotateXYZ(verts[b], rotX, rotY, rotZ);
return (ra[2]+rb[2])/2;
});
for (let ei = 0; ei < edges.length; ei++) {
const [a,b] = edges[ei];
const pa = projected[a], pb = projected[b];
const depth = edgeDepths[ei];
const alpha = 0.08 + Math.max(0, depth+1) * 0.18;
ctx.beginPath();
ctx.moveTo(pa[0],pa[1]);
ctx.lineTo(pb[0],pb[1]);
ctx.strokeStyle = `rgba(0,255,255,${alpha})`;
ctx.lineWidth = 0.5;
ctx.stroke();
}
// Glow outline pass
for (let ei = 0; ei < edges.length; ei++) {
const [a,b] = edges[ei];
const ra = rotateXYZ(verts[a], rotX, rotY, rotZ);
if (ra[2] < 0.3) continue;
const pa = projected[a], pb = projected[b];
ctx.beginPath();
ctx.moveTo(pa[0],pa[1]);
ctx.lineTo(pb[0],pb[1]);
ctx.strokeStyle = `rgba(0,255,255,0.12)`;
ctx.lineWidth = 2.5;
ctx.shadowColor = '#0ff';
ctx.shadowBlur = 6;
ctx.stroke();
ctx.shadowBlur = 0;
}
requestAnimationFrame(draw);
}
requestAnimationFrame(draw);
</script>
</body>
</html>