Solar system
Pure CSSOrbits are rings on a tilted plane, each spinning at its own speed. Every planet undoes both rotations, so it always faces the camera.
A striped tower of leaning strips on a rock in a night sea. Two long translucent planes, crossed, make each light beam; they sweep round with rotateY while a lit wedge on the water turns with them.
Your edited version
rotateY(side) translateZ(bottom radius) rotateX(lean), with the lean atan((bottom − top) / height) and a trapezoid clip-path. Rock, striped tower, glass room and roof are the same recipe with other numbers.rotateY, and the far beam passes behind the tower.rotate(-a) turns the same way as rotateY(a) above it.frustums of leaning stripsbeam: two crossed clip-path planes, rotateYsea: flat disc, wave lines slid by one periodrotateZ(-a) on the floor = rotateY(a) above it<div class="scene">
<div class="lighthouse">
<div class="layer"><div class="model">
<div class="sea"><i></i><i></i><u></u></div>
</div></div>
<div class="layer"><div class="model">
<div class="rock">
<i style="--a:0deg;--d:0.06"></i>
<i style="--a:45deg;--d:0.22"></i>
<i style="--a:90deg;--d:0.40"></i>
<i style="--a:135deg;--d:0.50"></i>
<i style="--a:180deg;--d:0.46"></i>
<i style="--a:225deg;--d:0.30"></i>
<i style="--a:270deg;--d:0.12"></i>
<i style="--a:315deg;--d:0.02"></i>
<b></b>
</div>
</div></div>
<div class="layer"><div class="model">
<div class="tower">
<i style="--a:0deg;--d:0.06"></i>
<i style="--a:36deg;--d:0.18"></i>
<i style="--a:72deg;--d:0.33"></i>
<i style="--a:108deg;--d:0.45"></i>
<i style="--a:144deg;--d:0.50"></i>
<i style="--a:180deg;--d:0.46"></i>
<i style="--a:216deg;--d:0.34"></i>
<i style="--a:252deg;--d:0.19"></i>
<i style="--a:288deg;--d:0.07"></i>
<i style="--a:324deg;--d:0.02"></i>
</div>
<div class="gallery"></div>
<div class="rail"></div>
<div class="room">
<i style="--a:0deg"></i>
<i style="--a:60deg"></i>
<i style="--a:120deg"></i>
<i style="--a:180deg"></i>
<i style="--a:240deg"></i>
<i style="--a:300deg"></i>
<b></b>
</div>
<div class="roof">
<i style="--a:0deg;--d:0.06"></i>
<i style="--a:60deg;--d:0.28"></i>
<i style="--a:120deg;--d:0.48"></i>
<i style="--a:180deg;--d:0.46"></i>
<i style="--a:240deg;--d:0.24"></i>
<i style="--a:300deg;--d:0.04"></i>
<b></b>
</div>
<div class="beam"><i></i><i></i><i></i><i></i></div>
</div></div>
</div>
</div>
/* the root is flat (three layers painted in order), so it holds the perspective itself */
.lighthouse {
position: relative;
width: 220px;
height: 200px;
perspective: 800px;
}
.lighthouse * {
box-sizing: border-box;
}
/* each layer carries the same camera */
.layer {
position: absolute;
inset: 0;
transform-style: preserve-3d;
transform: rotateX(-22deg) rotateY(-24deg);
}
/* the model's origin: the middle of the sea */
.model,
.rock,
.tower,
.room,
.roof {
position: absolute;
transform-style: preserve-3d;
}
.model {
left: 50%;
top: 150px;
}
/* the sea: a flat disc; it may clip, nothing in it is 3D */
.sea {
position: absolute;
left: -110px;
top: -110px;
width: 220px;
height: 220px;
overflow: hidden;
border: 1.5px solid rgb(46 230 214 / 0.4);
border-radius: 50%;
background:
radial-gradient(circle, transparent 0 44px, rgb(255 255 255 / 0.5) 45px 47px, transparent 50px 54px, rgb(255 255 255 / 0.16) 55px 56px, transparent 58px),
radial-gradient(circle, #3f3b94, #1e1c4a 72%);
transform: rotateX(90deg);
}
/* wave lines broken into dashes by a mask; they slide by exactly one period */
.sea i {
position: absolute;
inset: -20px 0 0;
background: repeating-linear-gradient(transparent 0 16px, rgb(46 230 214 / 0.55) 16px 17.5px, transparent 17.5px 20px);
mask: repeating-linear-gradient(90deg, #000 0 22px, transparent 22px 36px);
animation: waves 2.6s linear infinite;
}
.sea i + i {
inset: -28px 0 0;
background: repeating-linear-gradient(transparent 0 9px, rgb(160 140 255 / 0.6) 9px 10px, transparent 10px 28px);
mask: repeating-linear-gradient(90deg, transparent 0 14px, #000 14px 30px, transparent 30px 46px);
animation: waves-2 4.2s linear infinite;
}
/* light on the water under the beams, turning with them */
.sea u {
position: absolute;
inset: 0;
background: conic-gradient(from 70deg, transparent, rgb(255 181 71 / 0.55) 20deg, transparent 40deg 180deg, rgb(255 181 71 / 0.55) 200deg, transparent 220deg);
mask: radial-gradient(closest-side, transparent 14%, #000 34%, transparent 98%);
animation: pool 8s linear infinite;
}
/* rock: 8 sides, radius 40 → 31 over 18px */
.rock i {
position: absolute;
left: -16.87px;
top: -20.12px;
width: 33.74px;
height: 20.12px;
clip-path: polygon(11.94% 0, 88.06% 0, 100% 100%, 0 100%);
transform-origin: 50% 100%;
/* turn to its side, step out to the bottom radius, lean in by atan((bottom - top) / height) */
transform: rotateY(var(--a)) translateZ(40px) rotateX(26.57deg);
backface-visibility: hidden;
background:
linear-gradient(rgb(6 8 26 / var(--d)), rgb(6 8 26 / var(--d))),
radial-gradient(circle at 30% 30%, rgb(255 255 255 / 0.12) 0 3px, transparent 3.5px) 0 0 / 13px 11px,
linear-gradient(#636885, #393c4f 80%, #114949);
}
/* the rock's flat top, an octagon */
.rock b {
position: absolute;
left: -31px;
top: -49px;
width: 62px;
height: 62px;
clip-path: polygon(29.3% 0, 70.7% 0, 100% 29.3%, 100% 70.7%, 70.7% 100%, 29.3% 100%, 0 70.7%, 0 29.3%);
background: radial-gradient(circle at 40% 40%, #82869d, #636885 70%);
transform: rotateX(90deg);
}
/* tower: 10 sides, radius 16 → 11 over 84px, standing on the rock (y = -18px) */
.tower i {
position: absolute;
left: -5.5px;
top: -102.15px;
width: 11px;
height: 84.15px;
clip-path: polygon(17.5% 0, 82.5% 0, 100% 100%, 0 100%);
transform-origin: 50% 100%;
/* turn to its side, step out to the bottom radius, lean in by atan((bottom - top) / height) */
transform: rotateY(var(--a)) translateZ(16px) rotateX(3.41deg);
backface-visibility: hidden;
background:
linear-gradient(rgb(6 8 26 / var(--d)), rgb(6 8 26 / var(--d))),
repeating-linear-gradient(#e04285 0 14px, #f3efe7 14px 28px);
}
/* the front side: a door and a lit window */
.tower i:first-child {
background:
linear-gradient(#2a1c28, #2a1c28) 50% 100% / 6px 11px no-repeat,
linear-gradient(#ffb547, #ffb547) 50% 45% / 4px 6px no-repeat,
repeating-linear-gradient(#e04285 0 14px, #f3efe7 14px 28px);
}
.gallery,
.rail {
position: absolute;
left: -18px;
top: -120px;
width: 36px;
height: 36px;
border-radius: 50%;
background: radial-gradient(closest-side, #3a3e58 70%, #23263a);
transform: rotateX(90deg);
}
.rail {
top: -126px;
border: 1.5px solid #a3a8c8;
background: none;
}
/* lamp room: 6 glass panes (open: the far panes show through) */
.room i {
position: absolute;
left: -4.92px;
top: -116px;
width: 9.84px;
height: 14px;
clip-path: polygon(3.05% 0, 96.95% 0, 100% 100%, 0 100%);
transform-origin: 50% 100%;
/* turn to its side, step out to the bottom radius, lean in by atan((bottom - top) / height) */
transform: rotateY(var(--a)) translateZ(8px) rotateX(0deg);
background:
linear-gradient(90deg, #2b2e44 0 1px, transparent 1px calc(100% - 1px), #2b2e44 0),
rgb(255 181 71 / 0.4);
}
/* a flat halo at the lamp, level with the beams */
.room b {
position: absolute;
left: -16px;
top: -16px;
width: 32px;
height: 32px;
border-radius: 50%;
background: radial-gradient(closest-side, #fff 0 18%, #ffd08a 30%, rgb(255 181 71 / 0.45) 55%, transparent);
transform: translateY(-109px) rotateX(90deg);
animation: flash 4s ease-in-out infinite;
}
/* roof: a cone of 6 triangles */
.roof i {
position: absolute;
left: -6.65px;
top: -132.28px;
width: 13.3px;
height: 16.28px;
clip-path: polygon(50% 0, 100% 100%, 0 100%);
transform-origin: 50% 100%;
/* turn to its side, step out to the bottom radius, lean in by atan((bottom - top) / height) */
transform: rotateY(var(--a)) translateZ(11px) rotateX(42.51deg);
backface-visibility: hidden;
background:
linear-gradient(rgb(6 8 26 / var(--d)), rgb(6 8 26 / var(--d))),
linear-gradient(#5f53a3, #1d2033);
}
.roof b {
position: absolute;
left: -3px;
top: -132px;
width: 6px;
height: 6px;
border-radius: 50%;
background: radial-gradient(circle at 35% 35%, #fff, #8a8fb0);
}
/* two beams, each two tapering planes crossed at ±14deg around the beam's axis */
.beam {
position: absolute;
top: -109px;
transform-style: preserve-3d;
animation: sweep 8s linear infinite;
}
.beam i {
position: absolute;
left: 4px;
top: -17px;
width: 104px;
height: 34px;
clip-path: polygon(0 42%, 100% 0, 100% 100%, 0 58%);
background: linear-gradient(90deg, #ffe2b0, rgb(255 181 71 / 0.45) 30%, transparent 97%);
transform-origin: -4px 50%; /* the lamp */
transform: rotateX(76deg);
}
.beam i:nth-child(2) { transform: rotateX(104deg); }
.beam i:nth-child(3) { transform: rotateY(180deg) rotateX(76deg); }
.beam i:nth-child(4) { transform: rotateY(180deg) rotateX(104deg); }
@keyframes sweep {
to { transform: rotateY(360deg); }
}
/* on the floor, rotate(-a) turns the same way as rotateY(a) above it */
@keyframes pool {
to { transform: rotate(-360deg); }
}
@keyframes waves {
to { transform: translateY(20px); }
}
@keyframes waves-2 {
to { transform: translateY(28px); }
}
/* half a sweep: a beam faces you at about 114deg and 294deg of the turn */
@keyframes flash {
0%, 45%, 82%, 100% { transform: translateY(-109px) rotateX(90deg) scale(1); opacity: 0.7; }
63.3% { transform: translateY(-109px) rotateX(90deg) scale(2.2); opacity: 1; }
}
@use 'sass:math';
// Every round part is a frustum of $n leaning sides around the model's axis. A side stands on its
// bottom edge: rotateY(--a) → translateZ(bottom apothem) → rotateX(lean), with the lean
// atan((bottom - top) / height) bringing its top edge in to the top apothem. A top of 0 gives
// triangles, i.e. a cone. Sizes are unitless px; $y is the height the frustum stands on.
@mixin frustum-side($n, $bottom, $top, $h, $y, $closed: true) {
$tan: math.tan(math.div(180deg, $n));
$w: 2 * $bottom * $tan + 0.6; // a hair wider: no cracks between sides
$w-top: 2 * $top * $tan;
$side: math.sqrt($h * $h + ($bottom - $top) * ($bottom - $top));
position: absolute;
left: $w * -0.5px;
top: ($y - $side) * 1px;
width: $w * 1px;
height: $side * 1px;
transform-origin: 50% 100%;
transform: rotateY(var(--a)) translateZ($bottom * 1px) rotateX(math.atan(math.div($bottom - $top, $h)));
// a closed solid: its far sides never show, so they are not drawn (nothing to mis-sort)
@if $closed {
backface-visibility: hidden;
}
@if $top == 0 {
clip-path: polygon(50% 0, 100% 100%, 0 100%);
} @else {
$in: math.div($w - $w-top, 2 * $w) * 100%;
clip-path: polygon($in 0, 100% - $in 0, 100% 100%, 0 100%);
}
}
// heights, from the sea (y = 0) up
$rock-h: 18;
$tower-top: $rock-h + 84; // 102: gallery
$room-top: $tower-top + 14; // 116: roof
$lamp: ($tower-top + 7) * -1px; // the light's centre
$sweep: 8s; // one turn of the beams
$shade: linear-gradient(rgb(6 8 26 / var(--d)), rgb(6 8 26 / var(--d)));
$stone: color-mix(in srgb, var(--muted) 45%, #3b3e55);
// Three layers with the same camera, painted in order: the sea, the rock, the tower with its
// light. The camera is above, so a lower part can never cover a higher one, and painting in
// order is always right. It also keeps the big sea plane and the rock's leaning sides out of the
// tower's depth sorting: the browser sorts 3D planes by cutting them along each other's
// (infinite) planes, and those cuts showed as shards on the tower. The root is flat, so it
// carries its own perspective.
.d-lighthouse {
position: relative;
width: 220px;
height: 200px;
perspective: 800px;
&__layer {
position: absolute;
inset: 0;
transform-style: preserve-3d;
transform: rotateX(-22deg) rotateY(-24deg);
}
&__model {
position: absolute;
left: 50%;
top: 150px;
transform-style: preserve-3d;
}
// The sea: a flat disc. It clips its wave layers (a flat parent may clip: nothing in it is 3D).
&__sea {
position: absolute;
left: -110px;
top: -110px;
width: 220px;
height: 220px;
overflow: hidden;
border: 1.5px solid color-mix(in srgb, var(--accent-2) 40%, transparent);
border-radius: 50%;
background:
radial-gradient(circle, transparent 0 44px, rgb(255 255 255 / 0.5) 45px 47px, transparent 50px 54px, rgb(255 255 255 / 0.16) 55px 56px, transparent 58px),
radial-gradient(circle, color-mix(in srgb, var(--accent) 35%, #16205a), color-mix(in srgb, var(--accent) 18%, #060a22) 72%);
transform: rotateX(90deg);
// wave lines, broken into dashes by a mask, slid toward you by exactly one period
i {
position: absolute;
inset: -20px 0 0;
background: repeating-linear-gradient(transparent 0 16px, color-mix(in srgb, var(--accent-2) 55%, transparent) 16px 17.5px, transparent 17.5px 20px);
mask: repeating-linear-gradient(90deg, #000 0 22px, transparent 22px 36px);
animation: d-lighthouse-waves 2.6s linear infinite;
}
i + i {
inset: -28px 0 0;
background: repeating-linear-gradient(transparent 0 9px, color-mix(in srgb, var(--accent) 60%, #fff 10%) 9px 10px, transparent 10px 28px);
mask: repeating-linear-gradient(90deg, transparent 0 14px, #000 14px 30px, transparent 30px 46px);
animation-duration: 4.2s;
animation-name: d-lighthouse-waves-2;
}
// light on the water under the two beams. On the floor, rotateZ(-a) turns the same way as
// rotateY(a) above it (the floor's y axis points toward you).
u {
position: absolute;
inset: 0;
background: conic-gradient(
from 70deg,
transparent,
color-mix(in srgb, var(--warm) 55%, transparent) 20deg,
transparent 40deg 180deg,
color-mix(in srgb, var(--warm) 55%, transparent) 200deg,
transparent 220deg
);
mask: radial-gradient(closest-side, transparent 14%, #000 34%, transparent 98%);
animation: d-lighthouse-pool $sweep linear infinite;
}
}
&__rock,
&__tower,
&__room,
&__roof {
position: absolute;
transform-style: preserve-3d;
}
&__rock {
i {
@include frustum-side(8, 40, 31, $rock-h, 0);
background:
$shade,
radial-gradient(circle at 30% 30%, rgb(255 255 255 / 0.12) 0 3px, transparent 3.5px) 0 0 / 13px 11px,
linear-gradient($stone, color-mix(in srgb, $stone 55%, #05060c) 80%, color-mix(in srgb, var(--accent-2) 30%, #05060c));
}
// the flat top: an octagon, laid flat
b {
position: absolute;
left: -31px;
top: -31px - $rock-h * 1px;
width: 62px;
height: 62px;
clip-path: polygon(29.3% 0, 70.7% 0, 100% 29.3%, 100% 70.7%, 70.7% 100%, 29.3% 100%, 0 70.7%, 0 29.3%);
background: radial-gradient(circle at 40% 40%, color-mix(in srgb, $stone 80%, #fff), $stone 70%);
transform: rotateX(90deg);
}
}
// red and white bands along each strip; the front strip also has the door and a lit window
&__tower i {
@include frustum-side(10, 16, 11, 84, -$rock-h);
background:
$shade,
repeating-linear-gradient(color-mix(in srgb, var(--hot) 85%, #300) 0 14px, #f3efe7 14px 28px);
&:first-child {
background:
linear-gradient(#2a1c28, #2a1c28) 50% 100% / 6px 11px no-repeat,
linear-gradient(var(--warm), var(--warm)) 50% 45% / 4px 6px no-repeat,
repeating-linear-gradient(color-mix(in srgb, var(--hot) 85%, #300) 0 14px, #f3efe7 14px 28px);
}
}
&__gallery,
&__rail {
position: absolute;
left: -18px;
top: -18px - $tower-top * 1px;
width: 36px;
height: 36px;
border-radius: 50%;
background: radial-gradient(closest-side, #3a3e58 70%, #23263a);
transform: rotateX(90deg);
}
// the railing's top rail: a thin ring a little higher
&__rail {
top: -18px - ($tower-top + 6) * 1px;
border: 1.5px solid color-mix(in srgb, var(--text) 45%, #5a5f80);
background: none;
}
// lamp room: six glass panes with mullions, and a flat halo of light at the lamp
&__room {
i {
@include frustum-side(6, 8, 8, 14, -$tower-top, $closed: false); // glass: the far panes show through
background:
linear-gradient(90deg, #2b2e44 0 1px, transparent 1px calc(100% - 1px), #2b2e44 0),
color-mix(in srgb, var(--warm) 40%, transparent);
}
b {
position: absolute;
left: -16px;
top: -16px;
width: 32px;
height: 32px;
border-radius: 50%;
background: radial-gradient(closest-side, #fff 0 18%, color-mix(in srgb, var(--warm) 80%, #fff) 30%, color-mix(in srgb, var(--warm) 45%, transparent) 55%, transparent);
// Flat, level with the beams. An upright glow facing the camera would be a plane cutting
// through the tower, and the browser's 3D sorting shows such cuts as seams.
transform: translateY($lamp) rotateX(90deg);
animation: d-lighthouse-flash $sweep * 0.5 ease-in-out infinite;
}
}
&__roof {
i {
@include frustum-side(6, 11, 0, 12, -$room-top);
background: $shade, linear-gradient(color-mix(in srgb, var(--accent) 45%, #3a3e58), #1d2033);
}
b {
position: absolute;
left: -3px;
top: -3px - ($room-top + 13) * 1px;
width: 6px;
height: 6px;
border-radius: 50%;
background: radial-gradient(circle at 35% 35%, #fff, #8a8fb0);
}
}
// Two beams, each two planes tapering out from the lamp and crossed like a flat X around the
// beam's axis (±14deg from level). The pair sweeps round with rotateY; the far beam passes
// behind the tower. Why not one upright plane: the browser sorts 3D planes by cutting them
// along each other's (infinite) planes, and an upright plane through the axis would slice
// every tower strip, which shows as seams. These two stay above the tower's top.
&__beam {
position: absolute;
top: $lamp;
transform-style: preserve-3d;
animation: d-lighthouse-sweep $sweep linear infinite;
i {
position: absolute;
left: 4px;
top: -17px;
width: 104px;
height: 34px;
clip-path: polygon(0 42%, 100% 0, 100% 100%, 0 58%);
background: linear-gradient(90deg, color-mix(in srgb, var(--warm) 60%, #fff) 0, color-mix(in srgb, var(--warm) 45%, transparent) 30%, transparent 97%);
transform-origin: -4px 50%; // the lamp's axis
transform: rotateX(76deg);
}
i:nth-child(2) {
transform: rotateX(104deg);
}
i:nth-child(3) {
transform: rotateY(180deg) rotateX(76deg);
}
i:nth-child(4) {
transform: rotateY(180deg) rotateX(104deg);
}
}
}
@keyframes d-lighthouse-sweep {
to {
transform: rotateY(360deg);
}
}
@keyframes d-lighthouse-pool {
to {
transform: rotate(-360deg);
}
}
@keyframes d-lighthouse-waves {
to {
transform: translateY(20px);
}
}
@keyframes d-lighthouse-waves-2 {
to {
transform: translateY(28px);
}
}
// A beam points at the camera twice a turn: at about 114deg and 294deg of the sweep (the
// camera's direction in the model's frame). The lamp flares then, so this loop is half a turn.
@keyframes d-lighthouse-flash {
0%,
45%,
82%,
100% {
transform: translateY($lamp) rotateX(90deg) scale(1);
opacity: 0.7;
}
63.3% {
transform: translateY($lamp) rotateX(90deg) scale(2.2);
opacity: 1;
}
}