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.
JS builds the grid and gives each cell its distance from the centre; CSS turns that into a ripple.
Your edited version
--d.translateZ.animation-delay: calc(var(--d) * -0.22s) offsets each cell by its distance, so the motion reads as a ripple spreading outward.--u, across, and the cells rise 40 of them. Tied to the canvas, that keeps the wave the same share of a gallery card, the editor and a recording canvas.generated DOManimation-delay from distanceone shared @keyframes<div class="scene">
<div class="grid"></div>
</div>
.scene {
/* one base unit: every length below is a multiple of it, so the wave is the same share of a
gallery card, the editor, a full screen and a recording canvas */
--u: 0.24vmin;
perspective: calc(800 * var(--u));
}
.grid {
display: grid;
grid-template-columns: repeat(var(--n), 1fr);
gap: calc(4 * var(--u));
width: calc(260 * var(--u));
height: calc(260 * var(--u));
transform-style: preserve-3d;
/* the solid part of the wave is the cells high up and near the camera, so what reads as the
wave rides up and down the grid as the ripple spreads: the lift puts the middle of that ride,
not the flat grid, in the middle of the canvas */
transform: translateY(calc(-2 * var(--u))) rotateX(58deg) rotateZ(-45deg);
}
.grid i {
border-radius: calc(3 * var(--u));
background: #ffb547;
animation: bob 2.4s ease-in-out infinite;
/* the ripple, plus 0.6s into the loop: the first frame, which a paused card shows, has the
middle up and the wave spread evenly over the grid */
animation-delay: calc(var(--d) * -0.22s - 0.6s);
}
/* opacity + transform only: both run on the compositor, even with 100+ cells */
@keyframes bob {
0%, 100% { transform: translateZ(calc(-14 * var(--u))); opacity: 0.4; }
50% { transform: translateZ(calc(40 * var(--u))); opacity: 1; }
}
const grid = document.querySelector('.grid');
const N = 11;
const mid = (N - 1) / 2;
grid.style.setProperty('--n', N);
for (let row = 0; row < N; row++) {
for (let col = 0; col < N; col++) {
const cell = document.createElement('i');
cell.style.setProperty('--d', Math.hypot(row - mid, col - mid).toFixed(2));
grid.append(cell);
}
}