Tesseract
A four-dimensional hypercube turns through the fourth dimension, so its inner cube swells out to become the outer one while the outer one shrinks inside: it seems to turn itself inside out, forever. Edges glow brighter the nearer they are in that fourth direction. Drag to turn it in 3D too.
How to use it
- Paste the snippet just before
</head>. - Add
data-tesseractto a box with a height and a dark background.
Options
Add any of these attributes to the same element.
data-color | Colour of the edges nearest in the fourth dimension. Default #f0abfc. |
data-far | Colour of the farthest edges. Default #7c3aed. |
data-speed | How fast it turns. Default 1. |
data-size | Size, as a share of the box’s shorter side. Default 0.8. |
data-drag="false" | Don’t let people turn it by dragging. |
Its 16 corners and 32 edges are turned in four dimensions, projected into 3D, then into 2D. It’s paused when off screen; with reduced motion it shows a still frame.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Tesseract: a four-dimensional hypercube turning through the fourth
// dimension, so its inner cube swells out to become the outer one while
// the outer shrinks inside: it seems to turn itself inside out. Drawn as
// glowing edges on a canvas; drag to turn the whole thing in 3D.
// Usage: <div data-tesseract style="height: 360px; background: #0f0720"></div>
// Optional: data-color (edges near you in the fourth dimension, default
// #f0abfc), data-far (edges far away in it, default #7c3aed), data-speed
// (default 1), data-size (share of the box's shorter side, default 0.8),
// data-drag="false".
var DEFAULTS = { color: '#f0abfc', far: '#7c3aed', speed: 1, size: 0.8 };
var css =
'[data-tesseract]{position:relative;overflow:hidden;touch-action:pan-y}' +
'.tsr-canvas{position:absolute;inset:0;width:100%;height:100%;display:block}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function rgb(hex) {
hex = hex.replace('#', '');
if (hex.length === 3) hex = hex.replace(/./g, '$&$&');
var n = parseInt(hex, 16);
return [n >> 16 & 255, n >> 8 & 255, n & 255];
}
// The 16 corners (every mix of ±1 in x, y, z, w) and the 32 edges
// between corners that differ in one coordinate.
var VERTS = [], EDGES = [];
for (var i = 0; i < 16; i++) VERTS.push([i & 1 ? 1 : -1, i & 2 ? 1 : -1, i & 4 ? 1 : -1, i & 8 ? 1 : -1]);
for (var a = 0; a < 16; a++) for (var b = 0; b < 4; b++) if (!(a & (1 << b))) EDGES.push([a, a | (1 << b)]);
function build(el) {
var d = el.dataset;
var canvas = document.createElement('canvas');
canvas.className = 'tsr-canvas';
canvas.setAttribute('aria-hidden', 'true');
el.insertBefore(canvas, el.firstChild);
var ctx = canvas.getContext('2d');
var near = rgb(d.color || DEFAULTS.color), far = rgb(d.far || DEFAULTS.far);
var speed = d.speed !== undefined ? +d.speed : DEFAULTS.speed;
var share = +d.size || DEFAULTS.size;
var w = 0, h = 0, dpr = 1;
var s = { a: 0.35, yaw: 0.5, pitch: -0.35, vyaw: 0, dragging: false };
function resize() {
dpr = Math.min(2, window.devicePixelRatio || 1);
w = el.clientWidth;
h = el.clientHeight;
canvas.width = Math.round(w * dpr);
canvas.height = Math.round(h * dpr);
}
function project() {
var a = s.a, ca = Math.cos(a), sa = Math.sin(a);
var cb = Math.cos(a * 0.62), sb = Math.sin(a * 0.62);
var cy = Math.cos(s.yaw), sy = Math.sin(s.yaw), cp = Math.cos(s.pitch), sp = Math.sin(s.pitch);
var unit = Math.min(w, h) * share / 5.6;
return VERTS.map(function (v) {
// Turn in the x–w and z–w planes: the fourth-dimensional part.
var x = v[0] * ca - v[3] * sa, w4 = v[0] * sa + v[3] * ca;
var z = v[2] * cb - w4 * sb;
w4 = v[2] * sb + w4 * cb;
var y = v[1];
// Into 3D: nearer in w looks bigger.
var k = 2.6 / (2.6 - w4 * 0.9);
x *= k; y *= k; z *= k;
// An ordinary 3D turn for the view.
var x2 = x * cy + z * sy, z2 = -x * sy + z * cy;
var y2 = y * cp - z2 * sp, z3 = y * sp + z2 * cp;
// Into 2D with perspective.
var p = 7 / (7 - z3);
return { x: w / 2 + x2 * p * unit, y: h / 2 + y2 * p * unit, z: z3, w: w4, p: p };
});
}
function mix(t, alpha) {
var c = [0, 1, 2].map(function (i) { return Math.round(far[i] + (near[i] - far[i]) * t); });
return 'rgba(' + c.join(',') + ',' + alpha.toFixed(3) + ')';
}
function draw() {
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, w, h);
var P = project();
var list = EDGES.map(function (e) {
var p = P[e[0]], q = P[e[1]];
return { p: p, q: q, z: (p.z + q.z) / 2, w: (p.w + q.w) / 2 };
}).sort(function (m, n) { return m.z - n.z; });
ctx.lineCap = 'round';
ctx.globalCompositeOperation = 'lighter';
var base = Math.max(1.2, Math.min(w, h) / 160);
list.forEach(function (e) {
var t = Math.max(0, Math.min(1, (e.w + 1.4) / 2.8));
var depth = Math.max(0.25, Math.min(1, 0.6 + e.z * 0.18));
// A wide faint stroke for the glow, then the line itself.
ctx.strokeStyle = mix(t, 0.16 * depth);
ctx.lineWidth = base * 4.5;
ctx.beginPath(); ctx.moveTo(e.p.x, e.p.y); ctx.lineTo(e.q.x, e.q.y); ctx.stroke();
ctx.strokeStyle = mix(t, 0.85 * depth);
ctx.lineWidth = base * (0.7 + t * 0.6);
ctx.stroke();
});
P.forEach(function (v) {
var t = Math.max(0, Math.min(1, (v.w + 1.4) / 2.8));
ctx.fillStyle = mix(t, 0.95);
ctx.beginPath();
ctx.arc(v.x, v.y, base * (1 + t * 1.3) * v.p, 0, Math.PI * 2);
ctx.fill();
});
ctx.globalCompositeOperation = 'source-over';
}
resize();
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
draw();
if (window.ResizeObserver) new ResizeObserver(function () { resize(); draw(); }).observe(el);
return;
}
if (window.ResizeObserver) new ResizeObserver(resize).observe(el);
if (d.drag !== 'false') {
el.style.cursor = 'grab';
var lx = 0, ly = 0, lt = 0;
el.addEventListener('pointerdown', function (e) {
s.dragging = true;
lx = e.clientX; ly = e.clientY; lt = performance.now();
el.setPointerCapture(e.pointerId);
el.style.cursor = 'grabbing';
});
el.addEventListener('pointermove', function (e) {
if (!s.dragging) return;
var now = performance.now(), dx = (e.clientX - lx) * 0.01, dy = (e.clientY - ly) * 0.01;
s.yaw += dx;
s.pitch = Math.max(-1.3, Math.min(1.3, s.pitch - dy));
s.vyaw = dx / Math.max(8, now - lt) * 1000;
lx = e.clientX; ly = e.clientY; lt = now;
draw();
});
var end = function () { s.dragging = false; el.style.cursor = 'grab'; };
el.addEventListener('pointerup', end);
el.addEventListener('pointercancel', end);
}
var last = 0;
var timer = anime.createTimer({
loop: true,
duration: 1000,
autoplay: false,
onUpdate: function () {
var now = performance.now();
var dt = last ? Math.min(50, now - last) / 1000 : 0.016;
last = now;
s.a += dt * 0.55 * speed;
if (!s.dragging) {
s.vyaw += (0.12 * speed - s.vyaw) * Math.min(1, dt * 1.2);
s.yaw += s.vyaw * dt;
}
draw();
}
});
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
}).observe(el);
}
function init() {
document.querySelectorAll('[data-tesseract]').forEach(function (el) {
if (el._tsr) return;
el._tsr = true;
build(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-tesseract style="height: 360px; background: #0f0720"></div>