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 level-1 Menger sponge turning slowly, nodding so each square tunnel lines up with your eye and you see straight through. Twenty small cubes are drawn as just 18 surfaces: six masked outer faces with a real hole each, and twelve tunnel walls, with the next level painted on as pits.
Your edited version
33.333%, and the hole in the middle is a real hole: a mask of two crossing gradients shows the left and right thirds plus the top and bottom thirds, so the centre square is see-through. The next level down is painted, not cut: a small SVG tile puts a shaded square pit in the middle of every small cube, since a real hole there would need tunnels of its own.translate and rotate half a small cube off the axis, four strips line each tunnel; walls that share a plane never overlap, so the 3D sorting stays clean. Every face has backface-visibility: hidden, since its back is always inside the solid.::after whose opacity follows one shared keyframe list; animation-delay from --a, the direction it faces, puts it at the right phase, so a face brightens as it turns towards the light.--u.mask cuts a real hole in one faceonly the visible surfaces are drawnphase-shifted opacity shadingnested spin and nod<div class="scene">
<div class="tilt">
<div class="sponge" role="img" aria-label="A Menger sponge turning slowly: a cube with a square tunnel through each axis">
<i class="face front" style="--a:45"></i>
<i class="face right" style="--a:135"></i>
<i class="face back" style="--a:225"></i>
<i class="face left" style="--a:315"></i>
<i class="face top"></i>
<i class="face bottom"></i>
<b class="wall x floor"></b> <!-- left–right tunnel: floor -->
<b class="wall x ceil"></b> <!-- left–right tunnel: ceiling -->
<b class="wall x near" style="--a:225"></b> <!-- left–right tunnel: near wall, facing back -->
<b class="wall x far" style="--a:45"></b> <!-- left–right tunnel: far wall, facing front -->
<b class="wall y right" style="--a:315"></b> <!-- up–down tunnel: right wall, facing left -->
<b class="wall y left" style="--a:135"></b> <!-- up–down tunnel: left wall, facing right -->
<b class="wall y near" style="--a:225"></b> <!-- up–down tunnel: near wall, facing back -->
<b class="wall y far" style="--a:45"></b> <!-- up–down tunnel: far wall, facing front -->
<b class="wall z right" style="--a:315"></b> <!-- front–back tunnel: right wall, facing left -->
<b class="wall z left" style="--a:135"></b> <!-- front–back tunnel: left wall, facing right -->
<b class="wall z floor"></b> <!-- front–back tunnel: floor -->
<b class="wall z ceil"></b> <!-- front–back tunnel: ceiling -->
</div>
</div>
</div>
.scene {
/* one base unit: every length below is a multiple of it, so the sponge is the same share of a
gallery card, the editor, a full screen and a recording canvas */
--u: 0.22vmin;
perspective: calc(700 * var(--u));
/* looking down on a corner, the near bottom corner is the closest point and the perspective
magnifies it, so the drawing hangs low of the cube's centre: lift it by that much */
transform: translateY(calc(-10 * var(--u)));
}
/* the nod: from looking down on a corner to looking almost level and back, four times a turn */
.tilt {
transform-style: preserve-3d;
transform: rotateX(-34deg);
animation: menger-nod 4.5s ease-in-out infinite;
}
.sponge {
--s: calc(180 * var(--u)); /* the cube */
--h: calc(30 * var(--u)); /* half a small cube: where each tunnel's walls stand */
/* level 2, painted rather than cut: a pit in the middle of every small cube's face */
--pit: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 9 9'><rect x='3' y='3' width='3' height='3' fill='%230d0a26' fill-opacity='.62'/><path d='M3 3h3l-.5.5h-2z' fill='%230d0a26' fill-opacity='.5'/><path d='M3 3l.5.5v2L3 6z' fill='%230d0a26' fill-opacity='.35'/><path d='M3 6l.5-.5h2l.5.5z' fill='%23fff' fill-opacity='.35'/><path d='M6 3v3l-.5-.5v-2z' fill='%23fff' fill-opacity='.18'/></svg>");
position: relative;
width: var(--s);
height: var(--s);
transform-style: preserve-3d;
transform: rotateY(45deg);
animation: menger-spin 18s linear infinite;
}
.sponge > * {
position: absolute;
backface-visibility: hidden; /* the back of every surface is inside the solid */
}
/* the dark layer that shades a surface by the way it faces; it is cut by the same mask */
.sponge > *::after {
content: '';
position: absolute;
inset: 0;
background: #0d0a26;
opacity: 0;
}
/* side surfaces: the shade follows the turn, phase-shifted by the direction each one faces */
.sponge > [style*='--a']::after {
animation: menger-shade 18s linear infinite;
animation-delay: calc(var(--a) * -0.05s); /* 18 s / 360deg */
}
/* ---- the six outer faces: one element each, with the middle square cut out ---- */
.face {
inset: 0;
background:
/* the next level down: a small square pit in the middle of every small cube */
var(--pit) 0 0 / 33.333% 33.333%,
/* each small cube: a lit top-left edge and a shaded bottom-right one. Repeating gradients
rather than tiles: tile seams sparkle when a face is seen edge-on */
repeating-linear-gradient(90deg, rgb(255 255 255 / 0.3) 0 calc(1.5 * var(--u)), transparent 0 calc(60 * var(--u) - 1.5 * var(--u)), rgb(13 10 38 / 0.3) 0 calc(60 * var(--u))),
repeating-linear-gradient(rgb(255 255 255 / 0.36) 0 calc(1.5 * var(--u)), transparent 0 calc(60 * var(--u) - 1.5 * var(--u)), rgb(13 10 38 / 0.36) 0 calc(60 * var(--u))),
linear-gradient(135deg, rgb(255 255 255 / 0.14), transparent 70%),
var(--c, #8b6cff);
/* left and right thirds, plus top and bottom thirds: everything but the middle square. The
hole is a hair smaller than a third, so its rim laps over the tunnel walls' ends */
mask:
linear-gradient(90deg, #000 calc(33.333% + 1 * var(--u)), transparent 0 calc(66.667% - 1 * var(--u)), #000 0),
linear-gradient(#000 calc(33.333% + 1 * var(--u)), transparent 0 calc(66.667% - 1 * var(--u)), #000 0);
}
.front { transform: translateZ(calc(var(--s) / 2)); }
.right { transform: rotateY(90deg) translateZ(calc(var(--s) / 2)); }
.back { transform: rotateY(180deg) translateZ(calc(var(--s) / 2)); }
.left { transform: rotateY(-90deg) translateZ(calc(var(--s) / 2)); }
.top { --c: #b4a3ff; transform: rotateX(90deg) translateZ(calc(var(--s) / 2)); }
.bottom { --c: #4a32b8; transform: rotateX(-90deg) translateZ(calc(var(--s) / 2)); }
/* ---- the twelve tunnel walls: a strip the cube's length, open in its middle third ----
Drawn lying left-right, facing you, centred on the cube; each is then turned into place. One
unit wider than a small cube each side, tucked into the solid, so no seam shows at the corners */
.wall {
left: 0;
top: calc(59 * var(--u));
width: var(--s);
height: calc(62 * var(--u));
background:
var(--pit) 0 calc(1 * var(--u)) / calc(60 * var(--u)) calc(60 * var(--u)) repeat-x,
/* the seam between the two small cubes of each arm is the solid's edge; deeper is darker */
linear-gradient(90deg, rgb(255 255 255 / 0.3) 0 calc(1.5 * var(--u)), transparent 0 calc(100% - 1.5 * var(--u)), rgb(255 255 255 / 0.3) 0),
linear-gradient(90deg, var(--c1) 0, var(--c2) 33.333% 66.667%, var(--c1) 100%);
mask: linear-gradient(90deg, #000 33.333%, transparent 0 66.667%, #000 0);
--c1: #2bc9bb;
--c2: #0c4f5a;
}
/* floors catch light, ceilings are in shadow */
.floor { --c1: #5ff0e2; --c2: #157a7a; }
.ceil { --c1: #157a7a; --c2: #08303c; }
.x.floor { transform: translateY(var(--h)) rotateX(90deg); }
.x.ceil { transform: translateY(calc(-1 * var(--h))) rotateX(-90deg); }
.x.near { transform: translateZ(var(--h)) rotateY(180deg); }
.x.far { transform: translateZ(calc(-1 * var(--h))); }
.y.right { transform: translateX(var(--h)) rotateY(-90deg) rotateZ(90deg); }
.y.left { transform: translateX(calc(-1 * var(--h))) rotateY(90deg) rotateZ(90deg); }
.y.near { transform: translateZ(var(--h)) rotateY(180deg) rotateZ(90deg); }
.y.far { transform: translateZ(calc(-1 * var(--h))) rotateZ(90deg); }
.z.right { transform: translateX(var(--h)) rotateY(-90deg); }
.z.left { transform: translateX(calc(-1 * var(--h))) rotateY(90deg); }
.z.floor { transform: translateY(var(--h)) rotateX(90deg) rotateZ(90deg); }
.z.ceil { transform: translateY(calc(-1 * var(--h))) rotateX(-90deg) rotateZ(90deg); }
@keyframes menger-spin {
from { transform: rotateY(45deg); }
to { transform: rotateY(405deg); }
}
/* a corner towards you at 0% and 100%, a face square on at 50% */
@keyframes menger-nod {
0%, 100% { transform: rotateX(-34deg); }
50% { transform: rotateX(-7deg); }
}
/* how dark a side is when it faces each way, the light up and to the front-left:
0% facing you, 25% facing right, 50% away, 75% facing left */
@keyframes menger-shade {
0% { opacity: 0.2; }
12.5% { opacity: 0.42; }
25% { opacity: 0.58; }
37.5%, 62.5% { opacity: 0.62; }
75% { opacity: 0.26; }
87.5% { opacity: 0; }
100% { opacity: 0.2; }
}