3D equalizer
Pure CSSSeven real cuboids bouncing like an audio meter. Walls are squashed with scaleY and each lid rides down by the same amount, so nothing but transforms ever changes.
A loader that solves the Tower of Hanoi: three disks lift, cross and drop in the 7-move legal order from one peg to another. Then the round board turns a third of a turn, so the finished peg stands where the first one did and the loop starts again with no jump.
Your edited version
@keyframes of translate3d stops, four per move: on its peg, lifted clear of the pegs, over the new peg, dropped onto the stack. Between its moves a disk has no stops, so it simply waits. The stops were worked out once from the move list and written in as plain CSS.rotateY(-120deg) and C lands exactly where A stood, so the last frame is the first. The base slab and its rim are round and never move; only the turntable on top turns, with its three peg sockets.rotateY(k × 30deg) translateZ(r), with a darkness from its direction baked in. The cap is one square cut to a 12-gon with clip-path, its dark centre the hole the peg comes up through.--u; the paused first moment is the full stack on A, at rest.keyframes worked out from the move lista third of a turn makes the loop seamlessfront-half prisms for disksbillboard pegs that turn back<div class="scene">
<div class="view" role="img" aria-label="Loading: the Tower of Hanoi solving itself">
<div class="base">
<i style="--k:-3; --d:0.22"></i>
<i style="--k:-2; --d:0.04"></i>
<i style="--k:-1; --d:0"></i>
<i style="--k:0; --d:0.12"></i>
<i style="--k:1; --d:0.34"></i>
<i style="--k:2; --d:0.52"></i>
<i style="--k:3; --d:0.6"></i>
<b></b>
</div>
<div class="board">
<div class="turntable"></div>
<div class="peg" style="--x:-52; --z:30"></div>
<div class="peg" style="--x:0; --z:-60"></div>
<div class="peg" style="--x:52; --z:30"></div>
<div class="disk d3">
<i style="--k:-3; --d:0.22"></i>
<i style="--k:-2; --d:0.04"></i>
<i style="--k:-1; --d:0"></i>
<i style="--k:0; --d:0.12"></i>
<i style="--k:1; --d:0.34"></i>
<i style="--k:2; --d:0.52"></i>
<i style="--k:3; --d:0.6"></i>
<b></b>
</div>
<div class="disk d2">
<i style="--k:-3; --d:0.22"></i>
<i style="--k:-2; --d:0.04"></i>
<i style="--k:-1; --d:0"></i>
<i style="--k:0; --d:0.12"></i>
<i style="--k:1; --d:0.34"></i>
<i style="--k:2; --d:0.52"></i>
<i style="--k:3; --d:0.6"></i>
<b></b>
</div>
<div class="disk d1">
<i style="--k:-3; --d:0.22"></i>
<i style="--k:-2; --d:0.04"></i>
<i style="--k:-1; --d:0"></i>
<i style="--k:0; --d:0.12"></i>
<i style="--k:1; --d:0.34"></i>
<i style="--k:2; --d:0.52"></i>
<i style="--k:3; --d:0.6"></i>
<b></b>
</div>
</div>
</div>
</div>
.scene {
/* one base unit: every length below is a multiple of it, so the puzzle is the same share of a
gallery card, the editor, a full screen and a recording canvas */
--u: 0.32vmin;
perspective: calc(800 * var(--u));
}
/* the camera looks down on the board; its origin is the middle of the board's top. It sits a
little low, since the pegs and the lifted disks rise above the board and nothing goes below */
.view {
position: relative;
transform-style: preserve-3d;
transform: translateY(calc(8 * var(--u))) rotateX(-30deg);
}
.view * {
position: absolute;
transform-style: preserve-3d;
}
/* ---- the base: a 12-sided slab that never moves ---- */
.base {
--r: 108;
--c: #3b4078;
}
/* a side strip: k × 30deg round, pushed out to the rim, shaded by the way it faces */
.base > i,
.disk > i {
left: calc((var(--r) * -0.26795 - 0.5) * var(--u));
width: calc((var(--r) * 0.5359 + 1) * var(--u)); /* 2r × tan 15deg, and a unit over the seam */
background:
linear-gradient(rgb(255 255 255 / 0.35) 0 calc(1 * var(--u)), transparent 0),
linear-gradient(rgb(13 10 38 / var(--d)), rgb(13 10 38 / var(--d))),
var(--c);
backface-visibility: hidden;
transform: rotateY(calc(var(--k) * 30deg)) translateZ(calc(var(--r) * var(--u)));
}
.base > i {
top: 0;
height: calc(16 * var(--u));
background:
linear-gradient(rgb(255 255 255 / 0.25) 0 calc(1 * var(--u)), transparent 0),
linear-gradient(rgb(13 10 38 / var(--d)), rgb(13 10 38 / var(--d))),
linear-gradient(var(--c), #262a52);
}
/* a cap: a square cut to a 12-gon (2 / cos 15deg = 2.0706 times the strips' distance across) */
.base > b,
.disk > b {
left: calc(var(--r) * -1.0353 * var(--u));
top: calc(var(--r) * -1.0353 * var(--u));
width: calc(var(--r) * 2.0706 * var(--u));
height: calc(var(--r) * 2.0706 * var(--u));
clip-path: polygon(98.3% 62.94%, 85.36% 85.36%, 62.94% 98.3%, 37.06% 98.3%, 14.64% 85.36%, 1.7% 62.94%, 1.7% 37.06%, 14.64% 14.64%, 37.06% 1.7%, 62.94% 1.7%, 85.36% 14.64%, 98.3% 37.06%);
}
.base > b {
background: #4a5094;
transform: rotateX(90deg);
}
/* ---- the board: everything on it turns a third of a turn at the end of each solve ---- */
.board {
animation: hanoi-board 7s linear infinite;
}
/* the turntable, a hair above the base's top, with a socket under each peg. It is the same after
a third of a turn, so the loop shows no jump */
.turntable {
left: calc(-100 * var(--u));
top: calc(-100 * var(--u));
width: calc(200 * var(--u));
height: calc(200 * var(--u));
border-radius: 50%;
background:
radial-gradient(circle at 24% 65%, #191b3b 0 calc(5 * var(--u)), #8c92d6 0 calc(7 * var(--u)), transparent 0 calc(12 * var(--u))),
radial-gradient(circle at 50% 20%, #191b3b 0 calc(5 * var(--u)), #8c92d6 0 calc(7 * var(--u)), transparent 0 calc(12 * var(--u))),
radial-gradient(circle at 76% 65%, #191b3b 0 calc(5 * var(--u)), #8c92d6 0 calc(7 * var(--u)), transparent 0 calc(12 * var(--u))),
radial-gradient(circle, #6c72c0 0 calc(10 * var(--u)), #585ea8 calc(30 * var(--u)), #4c5298 calc(100 * var(--u)));
box-shadow: inset 0 0 0 calc(1.5 * var(--u)) rgb(255 255 255 / 0.18);
transform: translateY(calc(-0.5 * var(--u))) rotateX(90deg);
}
/* a peg: one flat bar that always faces you, which is what a thin rod looks like from any side.
It rides the board, so it turns back as the board turns */
.peg {
left: calc(-4 * var(--u));
top: calc(-63 * var(--u));
width: calc(8 * var(--u));
height: calc(63 * var(--u));
border-radius: calc(4 * var(--u)) calc(4 * var(--u)) 0 0;
background: linear-gradient(90deg, #9aa0d8, #f4f3ff 35%, #d2d0ee 60%, #6d7099);
transform: translate3d(calc(var(--x) * var(--u)), 0, calc(var(--z) * var(--u))) rotateY(0deg);
animation: hanoi-peg 7s linear infinite;
}
/* ---- the disks: seven front strips and a cap with the hole ---- */
.disk {
--t: calc(16 * var(--u)); /* thickness */
}
.disk > i {
top: calc(-1 * var(--t));
height: var(--t);
}
.disk > b {
background: radial-gradient(circle, #191b3b 0 calc(5.5 * var(--u)), color-mix(in srgb, var(--c) 70%, #fff) calc(6.5 * var(--u)), color-mix(in srgb, var(--c) 85%, #fff));
transform: translateY(calc(-1 * var(--t))) rotateX(90deg);
}
.d3 { --r: 42; --c: #8b6cff; animation: hanoi-disk3 7s linear infinite; }
.d2 { --r: 32; --c: #2ee6d6; animation: hanoi-disk2 7s linear infinite; }
.d1 { --r: 22; --c: #ff4d9d; animation: hanoi-disk1 7s linear infinite; }
/* a rest, seven moves, then a third of a turn: C comes round to where A was */
@keyframes hanoi-board {
0%, 80% { transform: rotateY(0deg); animation-timing-function: ease-in-out; }
100% { transform: rotateY(-120deg); }
}
@keyframes hanoi-peg {
0%, 80% { transform: translate3d(calc(var(--x) * var(--u)), 0, calc(var(--z) * var(--u))) rotateY(0deg); animation-timing-function: ease-in-out; }
100% { transform: translate3d(calc(var(--x) * var(--u)), 0, calc(var(--z) * var(--u))) rotateY(120deg); }
}
/* each disk: on its peg, lifted clear, across, dropped; worked out from the 7 moves */
@keyframes hanoi-disk1 {
0% { transform: translate3d(calc(-52 * var(--u)), calc(-32 * var(--u)), calc(30 * var(--u))) rotateY(0deg); }
10% { transform: translate3d(calc(-52 * var(--u)), calc(-32 * var(--u)), calc(30 * var(--u))) rotateY(0deg); animation-timing-function: cubic-bezier(0.3, 0, 0.3, 1); } /* lift */
13% { transform: translate3d(calc(-52 * var(--u)), calc(-70 * var(--u)), calc(30 * var(--u))) rotateY(0deg); animation-timing-function: ease-in-out; } /* across */
17% { transform: translate3d(calc(52 * var(--u)), calc(-70 * var(--u)), calc(30 * var(--u))) rotateY(0deg); animation-timing-function: cubic-bezier(0.5, 0, 0.75, 0.4); } /* drop */
20% { transform: translate3d(calc(52 * var(--u)), calc(0 * var(--u)), calc(30 * var(--u))) rotateY(0deg); }
30% { transform: translate3d(calc(52 * var(--u)), calc(0 * var(--u)), calc(30 * var(--u))) rotateY(0deg); animation-timing-function: cubic-bezier(0.3, 0, 0.3, 1); } /* lift */
33% { transform: translate3d(calc(52 * var(--u)), calc(-70 * var(--u)), calc(30 * var(--u))) rotateY(0deg); animation-timing-function: ease-in-out; } /* across */
37% { transform: translate3d(calc(0 * var(--u)), calc(-70 * var(--u)), calc(-60 * var(--u))) rotateY(0deg); animation-timing-function: cubic-bezier(0.5, 0, 0.75, 0.4); } /* drop */
40% { transform: translate3d(calc(0 * var(--u)), calc(-16 * var(--u)), calc(-60 * var(--u))) rotateY(0deg); }
50% { transform: translate3d(calc(0 * var(--u)), calc(-16 * var(--u)), calc(-60 * var(--u))) rotateY(0deg); animation-timing-function: cubic-bezier(0.3, 0, 0.3, 1); } /* lift */
53% { transform: translate3d(calc(0 * var(--u)), calc(-70 * var(--u)), calc(-60 * var(--u))) rotateY(0deg); animation-timing-function: ease-in-out; } /* across */
57% { transform: translate3d(calc(-52 * var(--u)), calc(-70 * var(--u)), calc(30 * var(--u))) rotateY(0deg); animation-timing-function: cubic-bezier(0.5, 0, 0.75, 0.4); } /* drop */
60% { transform: translate3d(calc(-52 * var(--u)), calc(0 * var(--u)), calc(30 * var(--u))) rotateY(0deg); }
70% { transform: translate3d(calc(-52 * var(--u)), calc(0 * var(--u)), calc(30 * var(--u))) rotateY(0deg); animation-timing-function: cubic-bezier(0.3, 0, 0.3, 1); } /* lift */
73% { transform: translate3d(calc(-52 * var(--u)), calc(-70 * var(--u)), calc(30 * var(--u))) rotateY(0deg); animation-timing-function: ease-in-out; } /* across */
77% { transform: translate3d(calc(52 * var(--u)), calc(-70 * var(--u)), calc(30 * var(--u))) rotateY(0deg); animation-timing-function: cubic-bezier(0.5, 0, 0.75, 0.4); } /* drop */
80% { transform: translate3d(calc(52 * var(--u)), calc(-32 * var(--u)), calc(30 * var(--u))) rotateY(0deg); }
80% { transform: translate3d(calc(52 * var(--u)), calc(-32 * var(--u)), calc(30 * var(--u))) rotateY(0deg); animation-timing-function: ease-in-out; }
100% { transform: translate3d(calc(52 * var(--u)), calc(-32 * var(--u)), calc(30 * var(--u))) rotateY(120deg); }
}
@keyframes hanoi-disk2 {
0% { transform: translate3d(calc(-52 * var(--u)), calc(-16 * var(--u)), calc(30 * var(--u))) rotateY(0deg); }
20% { transform: translate3d(calc(-52 * var(--u)), calc(-16 * var(--u)), calc(30 * var(--u))) rotateY(0deg); animation-timing-function: cubic-bezier(0.3, 0, 0.3, 1); } /* lift */
23% { transform: translate3d(calc(-52 * var(--u)), calc(-70 * var(--u)), calc(30 * var(--u))) rotateY(0deg); animation-timing-function: ease-in-out; } /* across */
27% { transform: translate3d(calc(0 * var(--u)), calc(-70 * var(--u)), calc(-60 * var(--u))) rotateY(0deg); animation-timing-function: cubic-bezier(0.5, 0, 0.75, 0.4); } /* drop */
30% { transform: translate3d(calc(0 * var(--u)), calc(0 * var(--u)), calc(-60 * var(--u))) rotateY(0deg); }
60% { transform: translate3d(calc(0 * var(--u)), calc(0 * var(--u)), calc(-60 * var(--u))) rotateY(0deg); animation-timing-function: cubic-bezier(0.3, 0, 0.3, 1); } /* lift */
63% { transform: translate3d(calc(0 * var(--u)), calc(-70 * var(--u)), calc(-60 * var(--u))) rotateY(0deg); animation-timing-function: ease-in-out; } /* across */
67% { transform: translate3d(calc(52 * var(--u)), calc(-70 * var(--u)), calc(30 * var(--u))) rotateY(0deg); animation-timing-function: cubic-bezier(0.5, 0, 0.75, 0.4); } /* drop */
70% { transform: translate3d(calc(52 * var(--u)), calc(-16 * var(--u)), calc(30 * var(--u))) rotateY(0deg); }
80% { transform: translate3d(calc(52 * var(--u)), calc(-16 * var(--u)), calc(30 * var(--u))) rotateY(0deg); animation-timing-function: ease-in-out; }
100% { transform: translate3d(calc(52 * var(--u)), calc(-16 * var(--u)), calc(30 * var(--u))) rotateY(120deg); }
}
@keyframes hanoi-disk3 {
0% { transform: translate3d(calc(-52 * var(--u)), calc(0 * var(--u)), calc(30 * var(--u))) rotateY(0deg); }
40% { transform: translate3d(calc(-52 * var(--u)), calc(0 * var(--u)), calc(30 * var(--u))) rotateY(0deg); animation-timing-function: cubic-bezier(0.3, 0, 0.3, 1); } /* lift */
43% { transform: translate3d(calc(-52 * var(--u)), calc(-70 * var(--u)), calc(30 * var(--u))) rotateY(0deg); animation-timing-function: ease-in-out; } /* across */
47% { transform: translate3d(calc(52 * var(--u)), calc(-70 * var(--u)), calc(30 * var(--u))) rotateY(0deg); animation-timing-function: cubic-bezier(0.5, 0, 0.75, 0.4); } /* drop */
50% { transform: translate3d(calc(52 * var(--u)), calc(0 * var(--u)), calc(30 * var(--u))) rotateY(0deg); }
80% { transform: translate3d(calc(52 * var(--u)), calc(0 * var(--u)), calc(30 * var(--u))) rotateY(0deg); animation-timing-function: ease-in-out; }
100% { transform: translate3d(calc(52 * var(--u)), calc(0 * var(--u)), calc(30 * var(--u))) rotateY(120deg); }
}