Twisting puzzle cube
Pure CSSA 3×3×3 cube built as three layers that take turns twisting a quarter turn. Each layer is one flat box with the stickers painted on by gradients.
A cube’s cross-shaped net lies open on a floor, folds up into a cube that turns a full circle, and opens flat again. The lid hangs off a wall, so its hinge rides along with the wall’s: nested transforms compound. The camera moves in as it folds, so the cube is as big as the net.
Your edited version
transform-origin.rotateX(±90deg) or rotateY(±90deg) per face. The angle lives in --fx / --fy, and one shared keyframe rule reads it, so every wall folds its own way.0%, 8% and 92%, 100% hold the same angle, 360° apart). It eases in and out, so most of the turn happens while the cube is shut: every side gets seen, and the net always opens the same way round.scale3d(1.45, …)) and down as the walls stand up, so the shut cube fills the frame as the open net does, and the middle of the drawing stays in the middle.--u, so the net is the same share of a gallery card, the editor and a recording canvas. The faces are coloured glass at 55% with a solid edge, so they read on a light stage as well as a dark one.nested hinged facestransform-origin on the shared edgevar() fold angle inside @keyframescamera zoom on the fold’s own timeline<div class="scene">
<div class="net">
<div class="base">
<i class="n"><i class="lid"></i></i>
<i class="s"></i>
<i class="e"></i>
<i class="w"></i>
</div>
</div>
</div>
.scene {
/* one base unit: every length below is a multiple of it, so the net is the same share of a
card, the editor, a full screen and a recording canvas */
--u: 0.33vmin;
perspective: calc(560 * var(--u));
}
/* the camera looks down at a floor the net lies on. It moves in as the walls fold up, so the
closed cube is as big in the frame as the open net: the zoom runs on the fold's own timeline */
.net {
transform-style: preserve-3d;
animation: zoom 8s ease-in-out infinite;
}
/* the bottom face turns a full circle while the cube is up, so every side gets seen, and is
back where it started before the net lies flat again. So the open net always lies the same
way: turned 160deg, the lid's arm reaching towards you, larger than the far squares, so even
a paused card shows a net lying on a floor and never a flat plus sign. The north wall then
stands up at the front, where its height does not tower over the rest */
.base {
--c: 139 108 255;
position: relative;
width: calc(60 * var(--u));
height: calc(60 * var(--u));
transform-style: preserve-3d;
animation: turn 8s ease-in-out infinite;
}
.base,
.base i {
/* coloured glass, dense enough to read on a light stage as well as a dark one */
background: rgb(var(--c) / 0.55);
/* inset shadows instead of a border: no layout offset, so hinges sit exactly on the edges */
box-shadow: inset 0 0 0 calc(1.5 * var(--u)) rgb(var(--c)), inset 0 0 calc(18 * var(--u)) rgb(var(--c) / 0.6);
}
.base i {
position: absolute;
width: calc(60 * var(--u));
height: calc(60 * var(--u));
transform-style: preserve-3d; /* the north wall carries the lid */
animation: fold 8s ease-in-out infinite;
}
/* each face lies next to its shared edge and hinges on it */
.n { --c: 255 77 157; --fx: -90deg; left: 0; bottom: 100%; transform-origin: bottom; }
.s { --c: 46 230 214; --fx: 90deg; left: 0; top: 100%; transform-origin: top; }
.e { --c: 255 181 71; --fy: -90deg; top: 0; left: 100%; transform-origin: left; }
.w { --c: 255 181 71; --fy: 90deg; top: 0; right: 100%; transform-origin: right; }
.lid { --c: 139 108 255; --fx: -90deg; left: 0; bottom: 100%; transform-origin: bottom; }
.base .lid {
animation-name: lid;
}
@keyframes turn {
0%, 8% { transform: rotateZ(160deg); }
92%, 100% { transform: rotateZ(520deg); }
}
/* the camera: tilted down at the floor all the time. It follows the middle of what is drawn
(the near arm of the open net hangs low, the cube stands up above the floor) and moves in
once the walls are up, so the shut cube fills the frame as the open net does */
@keyframes zoom {
0%, 6%, 94%, 100% { transform: translateY(calc(-31 * var(--u))) rotateX(56deg) scale3d(1, 1, 1); }
16%, 84% { transform: translateY(calc(12 * var(--u))) rotateX(56deg) scale3d(1.12, 1.12, 1.12); }
28%, 72% { transform: translateY(calc(33 * var(--u))) rotateX(56deg) scale3d(1.45, 1.45, 1.45); }
}
/* The loop starts lying open flat, the net itself: that is the first frame, so it is what a
paused card shows. walls: flat, fold up, stay closed while the cube turns, unfold */
@keyframes fold {
0%, 6%, 94%, 100% { transform: rotateX(0deg) rotateY(0deg); }
28%, 72% { transform: rotateX(var(--fx, 0deg)) rotateY(var(--fy, 0deg)); }
}
/* lid: folds at the same time as the wall that carries it, so the wall and the lid never stand
up together as one tall plank. It can never pass through a wall: it is exactly as wide as the
base, and the side walls lean outside that width until they are upright */
@keyframes lid {
0%, 6%, 94%, 100% { transform: rotateX(0deg) rotateY(0deg); }
26%, 74% { transform: rotateX(var(--fx, 0deg)) rotateY(var(--fy, 0deg)); }
}