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.generated DOManimation-delay from distanceone shared @keyframes<div class="scene">
<div class="grid"></div>
</div>
.scene {
perspective: 800px;
}
.grid {
display: grid;
grid-template-columns: repeat(var(--n), 1fr);
gap: 4px;
width: 260px;
height: 260px;
transform-style: preserve-3d;
transform: rotateX(58deg) rotateZ(-45deg);
}
.grid i {
border-radius: 3px;
background: #ffb547;
animation: bob 2.4s ease-in-out infinite;
animation-delay: calc(var(--d) * -0.22s);
}
/* opacity + transform only: both run on the compositor, even with 100+ cells */
@keyframes bob {
0%, 100% { transform: translateZ(-14px); opacity: 0.4; }
50% { transform: translateZ(40px); 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);
}
}
.d-wavegrid {
display: grid;
grid-template-rows: 1fr auto;
width: 100%;
height: 100%;
padding: 10px 14px 14px; // the control dock: the same place in every demo with controls
&__view {
display: grid;
place-items: center;
perspective: 700px;
}
&__grid {
// --n (columns) is set by JS together with the cells
display: grid;
grid-template-columns: repeat(var(--n), 1fr);
gap: 3px;
width: 170px;
height: 170px;
transform-style: preserve-3d;
transform: rotateX(58deg) rotateZ(-45deg);
i {
border-radius: 3px;
background: var(--warm);
animation: d-wavegrid-bob 2.4s ease-in-out infinite;
// --d = distance from the centre, so the ripple travels outward
animation-delay: calc(var(--d) * -0.22s);
}
}
// the site's control pattern: the text centred on top, the slider under it
label {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 4px 6px;
width: min(100%, 260px);
margin-inline: auto;
color: var(--muted);
font-family: var(--mono);
font-size: 11px;
}
output {
min-width: 5ch;
color: var(--text);
}
input {
flex: 1 1 100%;
accent-color: var(--warm);
}
}
@keyframes d-wavegrid-bob {
0%,
100% {
opacity: 0.4;
transform: translateZ(-14px);
}
50% {
opacity: 1;
transform: translateZ(34px);
}
}