Cube Net Unfold
Six squares lie flat in the cross-shaped net of a cube. One by one they fold up along their edges until the last one closes the lid, and the finished cube turns round once to show itself off before opening out flat again. Left empty, the faces are a die’s, one to six, with opposite sides adding up to seven.
How to use it
- Paste the snippet just before
</head>. - Add
data-cube-net-unfoldto a box with a width and a height, about four squares by three. - Leave it empty for a die, or put six children in it for the faces: the middle square, then those above, right of, below and left of it, then the one at the end of the long arm.
Options
Add any of these attributes to the same element.
data-loop | Fold and unfold again and again. |
data-color | Colour of the faces. Default #7c3aed. |
data-duration | Roughly how long it takes to fold up. Default 1800 ms. |
data-hold | How long it stays folded. Default 2600 ms. |
data-spin="false" | Don’t turn round once folded. |
Made with CSS 3D: each face is hinged to its neighbour, so the faces can hold any HTML. Without data-loop it folds up once when scrolled into view, and a click or tap does it again. With reduced motion it lies flat.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Cube Net Unfold: six squares lie flat in the cross-shaped net of a cube.
// They fold up along their edges, one after another, into a closed cube,
// which turns round once to show itself off, then unfolds flat again.
// Without content of your own the faces are a die's, one to six.
// Usage: <div data-cube-net-unfold data-loop style="height: 300px"></div>
// or: put six children in it for the faces: the middle square, then the
// ones above, right of, below and left of it, then the one at the end of
// the long arm (which becomes the far side).
// Optional: data-loop (fold and unfold over and over), data-color (colour of
// the faces, default "#7c3aed"), data-duration (ms to fold up, default 1800),
// data-hold (ms it stays folded and flat, default 2600 and 1200),
// data-spin="false" (don't turn round once folded).
var DEFAULTS = { color: '#7c3aed', duration: 1800, hold: 2600 };
// Which pips make each number, as cells 0–8 of a 3×3 grid.
var PIPS = { 1: [4], 2: [0, 8], 3: [0, 4, 8], 4: [0, 2, 6, 8], 5: [0, 2, 4, 6, 8], 6: [0, 2, 3, 5, 6, 8] };
var css =
'[data-cube-net-unfold]{position:relative}' +
'.cnu-rig{position:absolute;left:50%;top:50%;width:0;height:0;transform-style:preserve-3d}' +
'.cnu-face{position:absolute;width:var(--cnu-s);height:var(--cnu-s);transform-style:preserve-3d}' +
'.cnu-rig>.cnu-face{left:calc(var(--cnu-s) / -2);top:calc(var(--cnu-s) / -2)}' +
'.cnu-out,.cnu-in{position:absolute;inset:0;backface-visibility:hidden;-webkit-backface-visibility:hidden;border-radius:5%;overflow:hidden}' +
'.cnu-out{display:grid;place-items:center;color:#fff;background:linear-gradient(145deg,var(--cnu-light),var(--cnu-color));' +
'box-shadow:inset 0 0 0 1px rgba(255,255,255,.35)}' +
'.cnu-in{transform:rotateY(180deg);background:var(--cnu-dark)}' +
'.cnu-dim{position:absolute;inset:0;background:#0b0418;opacity:0;pointer-events:none}' +
'.cnu-out>*:not(.cnu-dim){max-width:100%;max-height:100%}' +
'.cnu-pips{display:grid;grid-template:repeat(3,1fr)/repeat(3,1fr);width:100%;height:100%;padding:16%;box-sizing:border-box}' +
'.cnu-pip{width:64%;aspect-ratio:1;border-radius:50%;background:currentColor;place-self:center;box-shadow:inset 0 -.1em .15em rgba(0,0,0,.2)}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function ease(t) { t = t < 0 ? 0 : t > 1 ? 1 : t; return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; }
function mix(hex, to, k) {
hex = hex.replace('#', '');
if (hex.length === 3) hex = hex.replace(/./g, '$&$&');
var n = parseInt(hex, 16), c = [n >> 16 & 255, n >> 8 & 255, n & 255];
return 'rgb(' + c.map(function (v) { return Math.round(v + (to - v) * k); }).join(',') + ')';
}
// The net, laid out with its long arm across: the middle square with one
// above, right, below and left of it, and the far side beyond the right one.
// Each folds back along its hinge, away from us, so the faces end up outside.
var NET = [
{ at: null }, // 0 middle
{ at: 0, css: 'left:0;bottom:100%', origin: '50% 100%', axis: 'X', sign: 1 }, // 1 above
{ at: 0, css: 'left:100%;top:0', origin: '0 50%', axis: 'Y', sign: 1 }, // 2 right
{ at: 0, css: 'left:0;top:100%', origin: '50% 0', axis: 'X', sign: -1 }, // 3 below
{ at: 0, css: 'right:100%;top:0', origin: '100% 50%', axis: 'Y', sign: -1 }, // 4 left
{ at: 2, css: 'left:100%;top:0', origin: '0 50%', axis: 'Y', sign: 1 } // 5 far side
];
// When each face starts to fold (share of the fold time) and how shaded it ends up.
var START = [0, 0.18, 0, 0.06, 0.12, 0.45], SHADE = [0, 0, 0.16, 0.36, 0.26, 0.3];
function setup(el) {
var d = el.dataset;
var color = d.color || DEFAULTS.color;
var dur = +d.duration || DEFAULTS.duration;
var hold = d.hold !== undefined ? +d.hold : DEFAULTS.hold;
var flatHold = d.hold !== undefined ? +d.hold * 0.5 : 1200;
var loop = d.loop !== undefined && d.loop !== 'false';
var spin = d.spin !== 'false';
el.style.setProperty('--cnu-color', color);
el.style.setProperty('--cnu-light', mix(color, 255, 0.28));
el.style.setProperty('--cnu-dark', mix(color, 0, 0.45));
var kids = Array.prototype.slice.call(el.children);
var own = kids.length >= 6;
var rig = document.createElement('div');
rig.className = 'cnu-rig';
if (!own) rig.setAttribute('aria-hidden', 'true');
var faces = NET.map(function (f, i) {
var face = document.createElement('div');
face.className = 'cnu-face';
if (f.css) face.style.cssText = f.css;
if (f.origin) face.style.transformOrigin = f.origin;
var out = document.createElement('div');
out.className = 'cnu-out';
if (own) out.appendChild(kids[i]);
else {
var n = [1, 2, 3, 5, 4, 6][i];
var pips = document.createElement('span');
pips.className = 'cnu-pips';
PIPS[n].forEach(function (c) {
var p = document.createElement('span');
p.className = 'cnu-pip';
p.style.gridArea = (Math.floor(c / 3) + 1) + ' / ' + (c % 3 + 1);
pips.appendChild(p);
});
out.appendChild(pips);
}
var dim = document.createElement('span');
dim.className = 'cnu-dim';
out.appendChild(dim);
var inner = document.createElement('div');
inner.className = 'cnu-in';
face.appendChild(out);
face.appendChild(inner);
return { el: face, dim: dim, def: f, i: i };
});
faces.forEach(function (f) { (f.def.at === null ? rig : faces[f.def.at].el).appendChild(f.el); });
el.appendChild(rig);
var size = 0;
function resize() {
var W = el.clientWidth, H = el.clientHeight;
// Big enough that the flat net (4 × 3 squares) fits.
size = Math.max(16, Math.min(W / 4.4, H / 3.4));
el.style.setProperty('--cnu-s', size + 'px');
el.style.perspective = Math.round(size * 9) + 'px';
}
// fold: 0 flat to 1 closed; turn: degrees of the show-off spin.
function render(fold, turn) {
faces.forEach(function (f, i) {
if (!i) return;
var k = ease(fold * 1.6 - START[i]);
var a = 90 * k * f.def.sign;
f.el.style.transform = 'rotate' + f.def.axis + '(' + a.toFixed(2) + 'deg)';
f.dim.style.opacity = (SHADE[i] * k).toFixed(3);
});
var p = ease(fold);
var rx = 16 - 40 * p, ry = -6 - 30 * p - turn;
rig.style.transform = 'translateX(' + (-0.5 * size * (1 - p)).toFixed(2) + 'px) translateY(' + (size * 0.12 * p).toFixed(2) + 'px) rotateX(' + rx.toFixed(2) + 'deg) rotateY(' + ry.toFixed(2) + 'deg) translateZ(' + (size / 2 * p).toFixed(2) + 'px)';
}
resize();
if (window.ResizeObserver) new ResizeObserver(function () { resize(); }).observe(el);
if (reduced()) { render(0, 0); return; }
render(0, 0);
// One cycle: flat, fold, (spin), unfold.
var T1 = flatHold, T2 = T1 + dur * 1.25, T3 = T2 + hold, T4 = T3 + dur * 1.25;
var t = 0, last = 0, done = false;
var timer = anime.createTimer({
loop: true,
duration: 1000,
autoplay: false,
onUpdate: function () {
var now = performance.now();
var dt = last ? Math.min(50, now - last) : 16;
last = now;
if (done) return;
t += dt;
if (!loop && t > T3) { t = T3; done = true; }
if (t > T4) t -= T4;
var fold = t < T1 ? 0 : t < T2 ? (t - T1) / (T2 - T1) : t < T3 ? 1 : 1 - (t - T3) / (T4 - T3);
var turn = spin && t >= T2 && t < T3 ? ease((t - T2 - hold * 0.12) / (hold * 0.76)) * 360 : 0;
render(fold, turn);
}
});
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
}, { threshold: 0.2 }).observe(el);
// Without data-loop, a click or tap folds it again.
if (!loop) el.addEventListener('click', function () { if (done) { done = false; t = 0; } });
}
function init() {
document.querySelectorAll('[data-cube-net-unfold]').forEach(function (el) {
if (el._cnu) return;
el._cnu = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-cube-net-unfold data-loop style="height: 300px"></div>
<!-- or your own faces: -->
<div data-cube-net-unfold data-loop style="height: 300px">
<div>Front</div> <div>Top</div> <div>Right</div>
<div>Bottom</div> <div>Left</div> <div>Back</div>
</div>