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.
Two glass cones tip to tip; the sand is one cone shrinking toward the neck while an identical one grows on the floor. Turned half over, it looks exactly as it started, so the loop has no seam.
Your edited version
atan((R − r) / H), like a cone with its tip cut off. --s: -1 flips a strip upward with scaleY(-1) and reverses its lean, so one rule builds both bulbs.scale3d with transform-origin on the tip), so its surface sinks and it keeps touching the glass. Below, the same cone grows from its footprint on the plate.--u, tied to the canvas, so the hourglass is the same share of a gallery card, the editor and a recording canvas. The numbers above are those units.trapezoid strips hinged on the neckscale3d() about the cone tip180° flip = start posecrossed planes for thin posts<div class="scene">
<div class="hourglass">
<div class="flip">
<i style="--i:0; --s:1; --mix:100%"></i>
<i style="--i:1; --s:1; --mix:83%"></i>
<i style="--i:2; --s:1; --mix:67%"></i>
<i style="--i:3; --s:1; --mix:50%"></i>
<i style="--i:4; --s:1; --mix:33%"></i>
<i style="--i:5; --s:1; --mix:17%"></i>
<i style="--i:6; --s:1; --mix:0%"></i>
<i style="--i:7; --s:1; --mix:17%"></i>
<i style="--i:8; --s:1; --mix:33%"></i>
<i style="--i:9; --s:1; --mix:50%"></i>
<i style="--i:10; --s:1; --mix:67%"></i>
<i style="--i:11; --s:1; --mix:83%"></i>
<i style="--i:0; --s:-1; --mix:100%"></i>
<i style="--i:1; --s:-1; --mix:83%"></i>
<i style="--i:2; --s:-1; --mix:67%"></i>
<i style="--i:3; --s:-1; --mix:50%"></i>
<i style="--i:4; --s:-1; --mix:33%"></i>
<i style="--i:5; --s:-1; --mix:17%"></i>
<i style="--i:6; --s:-1; --mix:0%"></i>
<i style="--i:7; --s:-1; --mix:17%"></i>
<i style="--i:8; --s:-1; --mix:33%"></i>
<i style="--i:9; --s:-1; --mix:50%"></i>
<i style="--i:10; --s:-1; --mix:67%"></i>
<i style="--i:11; --s:-1; --mix:83%"></i>
<b style="--y:calc(-76 * var(--u))"></b>
<b style="--y:calc(-69 * var(--u))"></b>
<b style="--y:calc(69 * var(--u))"></b>
<b style="--y:calc(76 * var(--u))"></b>
<span class="post" style="--i:0"></span>
<span class="post" style="--i:1"></span>
<span class="post" style="--i:2"></span>
<div class="sand top">
<i style="--i:0"></i>
<i style="--i:1"></i>
<i style="--i:2"></i>
<i style="--i:3"></i>
<i style="--i:4"></i>
<i style="--i:5"></i>
<i style="--i:6"></i>
<i style="--i:7"></i>
<b></b>
</div>
<div class="sand bottom">
<i style="--i:0"></i>
<i style="--i:1"></i>
<i style="--i:2"></i>
<i style="--i:3"></i>
<i style="--i:4"></i>
<i style="--i:5"></i>
<i style="--i:6"></i>
<i style="--i:7"></i>
<b></b>
</div>
<s class="stream"></s>
</div>
</div>
</div>
.scene {
/* one base unit: every length below is a multiple of it, so the hourglass is the same share of a
card, the editor, a full screen and a recording canvas */
--u: 0.34vmin;
perspective: calc(800 * var(--u));
}
.hourglass {
position: relative;
width: 0;
height: 0;
transform-style: preserve-3d;
animation: spin 20s linear infinite;
}
/* everything turns over together; the neck is at (0, 0) */
.flip {
position: absolute;
transform-style: preserve-3d;
animation: flip 9s infinite;
}
/* glass: 12 strips per bulb. Bulb height 66 units, apothem 44 units at the plate, 3 units at the neck:
strip width 2 × 44 × tan(15deg) = 23.58 units, slant √(66² + 41²) = 77.7 units, lean atan(41 / 66) = 31.85deg */
.flip > i {
--c: color-mix(in srgb, #8b6cff var(--mix), #2ee6d6);
--edge: color-mix(in srgb, var(--c) 55%, transparent);
position: absolute;
top: 0;
left: calc(-11.79 * var(--u));
width: calc(23.58 * var(--u));
height: calc(77.7 * var(--u));
transform-origin: 50% 0; /* hinged on the neck */
transform: rotateY(calc(var(--i) * 30deg)) translateZ(calc(3 * var(--u))) rotateX(calc(var(--s) * 31.85deg)) scaleY(var(--s));
clip-path: polygon(46.59% 0, 53.41% 0, 100% 100%, 0 100%); /* 1.61 units wide at the neck */
background:
linear-gradient(to top left, transparent calc(50% - 1 * var(--u)), var(--edge) calc(50% - 1 * var(--u)) 50%, transparent 50%) left / 46.59% 100% no-repeat,
linear-gradient(to top right, transparent calc(50% - 1 * var(--u)), var(--edge) calc(50% - 1 * var(--u)) 50%, transparent 50%) right / 46.59% 100% no-repeat,
linear-gradient(color-mix(in srgb, var(--c) 44%, transparent), color-mix(in srgb, var(--c) 14%, transparent) 50%, color-mix(in srgb, var(--c) 30%, transparent));
}
/* plates: two discs at each end give them thickness */
.flip > b {
position: absolute;
top: calc(-55 * var(--u));
left: calc(-55 * var(--u));
width: calc(110 * var(--u));
height: calc(110 * var(--u));
/* the rim is an inner line plus a soft shade, not a border: in the flip the discs pass
edge-on, where a 1.5 units line breaks up; the shade survives */
box-shadow: inset 0 0 0 calc(1.5 * var(--u)) #a289ff, inset 0 0 calc(8 * var(--u)) rgb(162 137 255 / 0.35);
border-radius: 50%;
background: radial-gradient(circle, #41387f, #6753c1);
transform: translateY(var(--y)) rotateX(90deg);
}
/* posts: two crossed planes each */
.post {
position: absolute;
top: calc(-76 * var(--u));
left: calc(-2.5 * var(--u));
width: calc(5 * var(--u));
height: calc(152 * var(--u));
transform-style: preserve-3d;
transform: rotateY(calc(var(--i) * 120deg + 60deg)) translateZ(calc(49 * var(--u)));
}
.post::before,
.post::after {
content: '';
position: absolute;
inset: 0;
border-radius: calc(3 * var(--u));
background: linear-gradient(90deg, #6f56cc, #b9a7ff, #6f56cc);
}
.post::after {
transform: rotateY(90deg);
}
/* sand: an 8-sided cone 55.44 units tall, base apothem 30.73 units, tip at the top of its box.
Strip width 2 × 30.73 × tan(22.5deg) = 25.46 units, slant 63.39 units, lean atan(30.73 / 55.44) = 29deg */
.sand {
position: absolute;
left: calc(-30.73 * var(--u));
width: calc(61.46 * var(--u));
height: calc(55.44 * var(--u));
transform-style: preserve-3d;
}
.sand i {
position: absolute;
bottom: 0;
left: calc(50% - 12.73 * var(--u));
width: calc(25.46 * var(--u));
height: calc(63.39 * var(--u));
transform-origin: 50% 100%;
transform: rotateY(calc(var(--i) * 45deg)) translateZ(calc(30.73 * var(--u))) rotateX(29deg);
/* the triangle is painted, not clipped: each half of the box is a gradient cut on its diagonal.
A clip-path in the sand, which is scaled every frame, makes the browser redraw the clipped
faces at every new scale, and that stalls the whole animation for seconds without a GPU */
--lo: #ffa851;
--hi: #ffcb7e;
background:
linear-gradient(to bottom right, transparent 50%, var(--hi) 50%, var(--lo)) left / 50% 100% no-repeat,
linear-gradient(to bottom left, transparent 50%, var(--hi) 50%, var(--lo)) right / 50% 100% no-repeat;
}
.sand i:nth-child(odd) {
--lo: #d18a41;
--hi: #ffb547;
}
/* the flat face, 1.5 units inside the cone so it never touches the strips. A disc, not an octagon:
it fits inside the eight sides, and it needs no clip-path (see above) */
.sand b {
position: absolute;
top: calc(100% - 32.23 * var(--u));
left: 0;
width: calc(61.46 * var(--u));
height: calc(61.46 * var(--u));
border-radius: 50%;
background: radial-gradient(circle, #ffd391, #ffa851);
transform: rotateX(90deg) scale(0.95);
}
/* top bulb: tip-down on the neck, shrinking toward that tip */
.sand.top {
top: 0;
transform-origin: 50% 0;
animation: drain 9s infinite;
}
/* bottom bulb: a pile 3 units above the plate, growing from its footprint */
.sand.bottom {
top: calc(7.56 * var(--u));
transform-origin: 50% 100%;
animation: fill 9s infinite;
}
.stream {
position: absolute;
top: 0;
left: calc(-1.5 * var(--u));
width: calc(3 * var(--u));
height: calc(66 * var(--u));
overflow: hidden;
opacity: 0;
animation: stream 9s linear infinite, face-you 20s linear infinite;
}
.stream::before {
content: '';
position: absolute;
inset: calc(-14 * var(--u)) 0 0;
background: repeating-linear-gradient(#ffcb7e 0 calc(4 * var(--u)), transparent calc(4 * var(--u)) calc(7 * var(--u)));
animation: fall 0.35s linear infinite;
}
@keyframes spin {
from { transform: rotateX(-16deg) rotateY(0deg); }
to { transform: rotateX(-16deg) rotateY(360deg); }
}
@keyframes face-you {
to { transform: rotateY(-360deg); }
}
/* half a turn over, it looks exactly like the start: the jump back to 0deg is invisible */
@keyframes flip {
0%, 84% { transform: rotateZ(0deg); animation-timing-function: ease-in-out; }
100% { transform: rotateZ(180deg); }
}
/* scale = cube root of what is left, so the flow looks steady */
@keyframes drain {
0% { transform: translateY(calc(-7.56 * var(--u))) rotateZ(180deg) scale3d(1, 1, 1); animation-timing-function: ease-in; }
8% { transform: translateY(0) rotateZ(180deg) scale3d(1, 1, 1); animation-timing-function: linear; }
15% { transform: translateY(0) rotateZ(180deg) scale3d(0.965, 0.965, 0.965); animation-timing-function: linear; }
25.5% { transform: translateY(0) rotateZ(180deg) scale3d(0.909, 0.909, 0.909); animation-timing-function: linear; }
43% { transform: translateY(0) rotateZ(180deg) scale3d(0.794, 0.794, 0.794); animation-timing-function: linear; }
60.5% { transform: translateY(0) rotateZ(180deg) scale3d(0.63, 0.63, 0.63); animation-timing-function: linear; }
71% { transform: translateY(0) rotateZ(180deg) scale3d(0.464, 0.464, 0.464); animation-timing-function: linear; }
/* emptied: hidden as well as shrunk. A cone of 18 faces squeezed to a point is so degenerate
that the browser spends seconds sorting its faces in depth for every frame */
78%, 100% { transform: translateY(0) rotateZ(180deg) scale3d(0.01, 0.01, 0.01); visibility: hidden; }
}
@keyframes fill {
0%, 8% { transform: scale3d(0.01, 0.01, 0.01); visibility: hidden; animation-timing-function: linear; }
15% { transform: scale3d(0.464, 0.464, 0.464); animation-timing-function: linear; }
25.5% { transform: scale3d(0.63, 0.63, 0.63); animation-timing-function: linear; }
43% { transform: scale3d(0.794, 0.794, 0.794); animation-timing-function: linear; }
60.5% { transform: scale3d(0.909, 0.909, 0.909); animation-timing-function: linear; }
71% { transform: scale3d(0.965, 0.965, 0.965); animation-timing-function: linear; }
78%, 100% { transform: scale3d(1, 1, 1); }
}
@keyframes stream {
0%, 8%, 80%, 100% { opacity: 0; }
10%, 77% { opacity: 1; }
}
@keyframes fall {
to { transform: translateY(calc(14 * var(--u))); }
}