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.
Sixteen thin triangles on a circle, each leaning in by atan(radius / height) so all the tips meet on the axis, over a flat disc base.
Your edited version
rotateY(i × 22.5deg) translateZ(r). Each is 2r × tan(180° / 16) wide, so their bottom edges close the ring.atan(r / h). That is exactly the angle at which every tip lands on the axis, h above the base.√(h² + r²), not h, because leaning shortens it.triangle width = 2r × tan(180° / n)lean = atan(r / h)rocking wrapper + spinning childSass @for<div class="scene">
<div class="cone">
<div class="cone-body">
<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>
<i style="--i:8"></i>
<i style="--i:9"></i>
<i style="--i:10"></i>
<i style="--i:11"></i>
<i style="--i:12"></i>
<i style="--i:13"></i>
<i style="--i:14"></i>
<i style="--i:15"></i>
<b></b>
</div>
</div>
</div>
.scene {
perspective: 800px;
}
/* wrapper: rocks back and forth */
.cone {
transform-style: preserve-3d;
animation: rock 7s ease-in-out infinite alternate;
}
/* child: spins */
.cone-body {
position: relative;
width: 19.89px; /* triangle base = 2 × 50px × tan(180deg / 16) */
height: 110px; /* cone height */
transform-style: preserve-3d;
animation: spin 10s linear infinite;
}
.cone-body i {
--hue: calc(293 + 40 * cos(var(--i) * 22.5deg)); /* pink ... violet ... pink */
--edge: hsl(var(--hue) 90% 68% / 0.8);
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 120.83px; /* slant = √(110² + 50²) */
transform-origin: 50% 100%;
/* stand on the base circle, then lean in by atan(50 / 110) */
transform: rotateY(calc(var(--i) * 22.5deg)) translateZ(50px) rotateX(24.44deg);
clip-path: polygon(50% 0, 0 100%, 100% 100%);
background:
linear-gradient(to top left, transparent calc(50% - 1px), var(--edge) calc(50% - 1px) 50%, transparent 50%) left / 50% 100% no-repeat,
linear-gradient(to top right, transparent calc(50% - 1px), var(--edge) calc(50% - 1px) 50%, transparent 50%) right / 50% 100% no-repeat,
linear-gradient(to top, hsl(var(--hue) 90% 68% / 0.45), hsl(var(--hue) 90% 68% / 0.14));
}
/* base disc through the triangles' corners: 2 × 50px / cos(180deg / 16) */
.cone-body b {
position: absolute;
left: calc(50% - 50.98px);
top: calc(100% - 50.98px);
width: 101.96px;
height: 101.96px;
border-radius: 50%;
background: rgb(46 230 214 / 0.26);
border: 1px solid rgb(46 230 214 / 0.75);
box-shadow: inset 0 0 24px rgb(46 230 214 / 0.3);
transform: rotateX(90deg);
}
@keyframes rock {
from { transform: rotateX(-30deg) rotateZ(-6deg); }
to { transform: rotateX(24deg) rotateZ(6deg); }
}
@keyframes spin {
to { transform: rotateY(360deg); }
}
@use 'sass:math';
@use '../mixins' as *;
$n: 16;
$R: 50px; // base radius (to the middle of a triangle's bottom edge)
$H: 110px;
$w: 2 * $R * math.tan(math.div(180deg, $n)); // bottom edge of one triangle
$L: math.hypot($H, $R); // slant length = height of the triangle element
$lean: math.atan(math.div($R, $H)); // how far each triangle leans in from upright
$disc: math.div($R, math.cos(math.div(180deg, $n))) * 2; // circle through the triangles' corners
$edge: color-mix(in srgb, var(--c) 80%, transparent);
// Two animations cannot share one transform, so the rocking and the spinning live on two elements.
.d-cone {
transform-style: preserve-3d;
animation: d-cone-rock 7s ease-in-out infinite alternate;
&__body {
position: relative;
width: $w;
height: $H;
transform-style: preserve-3d;
animation: d-cone-spin 10s linear infinite;
}
// Triangles stand upright on the base circle (like carousel panels), hinged on their bottom
// edge, then lean back by atan(R / H): exactly enough for all the tips to meet on the axis.
i {
--c: color-mix(in srgb, var(--hot) var(--mix), var(--accent));
position: absolute;
left: 0;
bottom: 0;
width: $w;
height: $L;
transform-origin: 50% 100%;
transform: rotateY(calc(var(--i) * #{math.div(360deg, $n)})) translateZ($R) rotateX($lean);
clip-path: polygon(50% 0, 0 100%, 100% 100%);
// slanted edges are drawn with "to corner" gradients: their 50% line is the box diagonal
background:
linear-gradient(to top left, transparent calc(50% - 1px), $edge calc(50% - 1px), $edge 50%, transparent 50%) left / 50%
100% no-repeat,
linear-gradient(to top right, transparent calc(50% - 1px), $edge calc(50% - 1px), $edge 50%, transparent 50%) right /
50% 100% no-repeat,
linear-gradient(to top, color-mix(in srgb, var(--c) 45%, transparent), color-mix(in srgb, var(--c) 14%, transparent));
}
@for $i from 0 to $n {
i:nth-child(#{$i + 1}) {
--i: #{$i};
--mix: #{math.round(math.div(math.abs($i - $n * 0.5), $n * 0.5) * 100) * 1%};
}
}
// base disc, laid flat at the bottom
b {
position: absolute;
left: calc(50% - #{$disc * 0.5});
top: calc(100% - #{$disc * 0.5});
width: $disc;
height: $disc;
border-radius: 50%;
transform: rotateX(90deg);
@include face(var(--accent-2), 26%);
}
}
@keyframes d-cone-rock {
from {
transform: rotateX(-30deg) rotateZ(-6deg);
}
to {
transform: rotateX(24deg) rotateZ(6deg);
}
}
@keyframes d-cone-spin {
to {
transform: rotateY(360deg);
}
}