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 rocket built from strips and leaning triangles fills the frame on its pad, lifts off in a cloud of smoke and climbs out of the top of the frame, the camera following it up as the ground drops away. Then the camera pans back down to the pad, where the next rocket stands ready, so the loop has no seam. Flame and smoke are gradient layers that only scale and fade.
Your edited version
rotateY(n × 36deg), then a translateZ of 13 units. The nose uses the same angles with triangles that stand on the body’s top edge and lean in by atan(13 / 30), so their tips meet on the axis.@keyframes: ignition at 6%, lift-off at 13%, out of the top of the frame by 54%, the next rocket back on its pad at 70%. Start and end are the same frame, so the loop has no seam.translate property, so it adds to the swaying transform instead of replacing it. Once the rocket has gone the camera pans back down, and the pad comes up into view with the next rocket already standing on it: the flight layer runs the same keyframes and easing as the ground for that stretch, so the rocket stays on the pad all the way.mask-image gradient fades its top and bottom edges over a few units, so the rocket and the ground slip out of view instead of being cut by a hard line, and nothing leaves the band. The mask is on a flat wrapper, .sky, not on the 3D .launch: on an element with preserve-3d it would flatten the rocket into one plane. For the same reason the rocket is hidden with visibility, not opacity, for the 0.1s it is put back on the pad, and the ground while it is wholly below the frame.alternate loops. Only transform, translate and opacity move.--u, tied to the canvas: the frame is 215 × 150 units, the pad 124 units down it and the rocket 111 tall from fins to nose, so the launch is the same share of a gallery card, the editor and a recording canvas.cylinder of 10 strips + cone of 10 trianglesthe ground drops with translate = a camera following the climbflame: crossed planes, scale-only flickera mask-faded window, on a flat wrapper round the 3D scene<div class="sky">
<div class="launch">
<div class="base">
<div class="pad"></div>
<div class="glow"></div>
<div class="tower"><i></i><i></i></div>
<div class="arm"></div>
<em style="--x:-1;--z:8;--s:1"></em>
<em style="--x:-0.55;--z:-10;--s:0.8"></em>
<em style="--x:-0.2;--z:14;--s:1.1"></em>
<em style="--x:0.25;--z:-6;--s:0.9"></em>
<em style="--x:0.6;--z:12;--s:1.2"></em>
<em style="--x:1;--z:-12;--s:0.85"></em>
</div>
<div class="flight">
<div class="craft">
<div class="ship">
<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>
<b style="--a:0deg;--d:0.06"></b>
<b style="--a:36deg;--d:0.18"></b>
<b style="--a:72deg;--d:0.33"></b>
<b style="--a:108deg;--d:0.45"></b>
<b style="--a:144deg;--d:0.50"></b>
<b style="--a:180deg;--d:0.46"></b>
<b style="--a:216deg;--d:0.34"></b>
<b style="--a:252deg;--d:0.19"></b>
<b style="--a:288deg;--d:0.07"></b>
<b style="--a:324deg;--d:0.02"></b>
<s style="--a:90deg"></s><s style="--a:210deg"></s><s style="--a:330deg"></s>
<u></u><u></u>
<span class="window"></span>
<div class="flame"><i></i><i></i></div>
</div>
</div>
</div>
</div>
</div>
.sky {
/* one base unit: every length below is a multiple of it, so the launch is the same share of a
gallery card, the editor, a full screen and a recording canvas */
--u: 0.46vmin;
/* the frame is a window onto the sky: the rocket flies out through its top edge and the ground
drops out through its bottom one, each fading out over a few units, so nothing is cut by a
hard line and nothing leaves the band. The mask is on this flat wrapper, not on .launch: on an
element with preserve-3d it would flatten the 3D rocket into one plane */
-webkit-mask-image: linear-gradient(transparent, #000 calc(10 * var(--u)), #000 calc(100% - 8 * var(--u)), transparent);
mask-image: linear-gradient(transparent, #000 calc(10 * var(--u)), #000 calc(100% - 8 * var(--u)), transparent);
}
.launch {
position: relative;
/* 215 units is 99vmin, inside a square canvas: a box wider than the canvas does not centre, it
hangs off the right. Everything in it is placed from its middle, so the width moves nothing */
width: calc(215 * var(--u));
height: calc(150 * var(--u));
perspective: calc(800 * var(--u));
perspective-origin: 50% 45%;
transform-style: preserve-3d;
}
/* the pad point, on the ground and in the flight layer; both carry the same swaying camera */
.base,
.craft {
position: absolute;
left: 50%;
top: calc(124 * var(--u));
transform-style: preserve-3d;
animation: sway 16s ease-in-out infinite alternate;
}
/* the camera follows the rocket up: the ground drops away out of the bottom of the frame, and
after the rocket has gone it pans back down to the pad. translate, not transform, so it adds to
the sway instead of replacing it */
.base {
animation: sway 16s ease-in-out infinite alternate, camera 10s infinite;
}
.base::before {
content: '';
position: absolute;
left: calc(-100 * var(--u));
top: calc(-100 * var(--u));
width: calc(200 * var(--u));
height: calc(200 * var(--u));
border-radius: 50%;
background: radial-gradient(closest-side, rgb(139 108 255 / 0.24), transparent);
transform: translateY(calc(1 * var(--u))) rotateX(90deg);
}
.pad,
.glow {
position: absolute;
left: calc(-46 * var(--u));
top: calc(-46 * var(--u));
width: calc(92 * var(--u));
height: calc(92 * var(--u));
border-radius: 50%;
background: radial-gradient(closest-side, #555a7c 0 50%, #ffb547 51% 55%, #33374f 56% 92%, #5c6186 93% 97%, transparent 98%);
transform: rotateX(90deg);
}
.glow {
left: calc(-70 * var(--u));
top: calc(-70 * var(--u));
width: calc(150 * var(--u));
height: calc(150 * var(--u));
background: radial-gradient(closest-side, #ffe2b0, rgb(255 77 157 / 0.45) 45%, transparent);
opacity: 0;
transform: translateY(calc(-1 * var(--u))) rotateX(90deg);
animation: glow 10s linear infinite;
}
.tower {
position: absolute;
transform-style: preserve-3d;
transform: translate3d(calc(-40 * var(--u)), 0, calc(-8 * var(--u)));
}
.tower i {
position: absolute;
left: calc(-6 * var(--u));
top: calc(-104 * var(--u));
width: calc(12 * var(--u));
height: calc(104 * var(--u));
background:
linear-gradient(90deg, #aab0d0 0 calc(2 * var(--u)), transparent calc(2 * var(--u)) calc(100% - calc(2 * var(--u))), #aab0d0 0),
repeating-linear-gradient(40deg, #aab0d0 0 calc(1.2 * var(--u)), transparent calc(1.2 * var(--u)) calc(10 * var(--u))),
repeating-linear-gradient(-40deg, #aab0d0 0 calc(1.2 * var(--u)), transparent calc(1.2 * var(--u)) calc(10 * var(--u)));
opacity: 0.85;
}
.tower i + i {
transform: rotateY(90deg);
}
.arm {
position: absolute;
left: calc(-34 * var(--u));
top: calc(-74 * var(--u));
width: calc(21 * var(--u));
height: calc(4 * var(--u));
background: #aab0d0;
transform-origin: 0 50%;
animation: arm 10s ease-in-out infinite;
}
/* smoke: flat discs pushed out sideways (--x) at lift-off; --x and --z are
plain numbers, and --z counts units */
.base em {
position: absolute;
left: calc(-15 * var(--u));
top: calc(-26 * var(--u));
width: calc(30 * var(--u));
height: calc(30 * var(--u));
border-radius: 50%;
background: radial-gradient(circle at 40% 35%, #d8dbeb, rgb(148 155 192 / 0.75) 55%, transparent 71%);
opacity: 0;
animation: smoke 10s linear infinite;
}
/* the flight layer is the whole frame: the rocket lifts off and climbs out of the top, and the
next one rides back into view on the pad */
.flight {
position: absolute;
inset: 0;
transform-style: preserve-3d;
animation: fly 10s infinite;
}
.ship {
position: absolute;
transform-style: preserve-3d;
transform: translateY(calc(-9 * var(--u))); /* the fins are the legs */
}
/* body: 10 strips round the axis; --d is each side's baked-in shade */
.ship i {
position: absolute;
left: calc(-4.5 * var(--u));
top: calc(-72 * var(--u));
width: calc(9 * var(--u)); /* a hair more than 2 · 13 · tan(18°) = 8.45 units: no cracks */
height: calc(72 * var(--u));
background:
linear-gradient(rgb(12 14 34 / var(--d)), rgb(12 14 34 / var(--d))),
linear-gradient(#eef0fa 0 12%, #8b6cff 12% 20%, #eef0fa 20% 84%, #3b3f5e 84%);
backface-visibility: hidden;
transform: rotateY(var(--a)) translateZ(calc(13 * var(--u)));
}
/* nose: triangles on the same sides, leaning in by atan(13 / 30) */
.ship b {
position: absolute;
left: calc(-4.23 * var(--u));
top: calc(-104.7 * var(--u));
width: calc(8.45 * var(--u));
height: calc(32.7 * var(--u)); /* √(13² + 30²) */
clip-path: polygon(50% 0, 100% 100%, 0 100%);
background:
linear-gradient(rgb(12 14 34 / var(--d)), rgb(12 14 34 / var(--d))),
linear-gradient(#ffb8d8, #ff4d9d);
backface-visibility: hidden;
transform-origin: 50% 100%;
transform: rotateY(var(--a)) translateZ(calc(13 * var(--u))) rotateX(23.43deg);
}
/* fins: planes standing out from the axis, turned round it */
.ship s {
position: absolute;
left: calc(11 * var(--u));
top: calc(-30 * var(--u));
width: calc(20 * var(--u));
height: calc(39 * var(--u));
clip-path: polygon(0 0, 100% 60%, 100% 100%, 0 78%);
background: linear-gradient(90deg, #b3366e, #ff4d9d 70%);
transform-origin: calc(-11 * var(--u)) 0; /* = the axis */
transform: rotateY(var(--a));
}
.ship u {
position: absolute;
left: calc(-7 * var(--u));
top: 0;
width: calc(14 * var(--u));
height: calc(8 * var(--u));
clip-path: polygon(22% 0, 78% 0, 100% 100%, 0 100%);
background: linear-gradient(#6a6f90, #2a2d42);
}
.ship u + u {
transform: rotateY(90deg);
}
.window {
position: absolute;
left: calc(-5 * var(--u));
top: calc(-54 * var(--u));
width: calc(10 * var(--u));
height: calc(10 * var(--u));
border-radius: 50%;
background: radial-gradient(circle at 35% 35%, #fff 0 12%, #2ee6d6 42%, #125e58);
box-shadow: 0 0 0 calc(1.5 * var(--u)) #b8bcd6;
transform: translateZ(calc(13.6 * var(--u)));
}
.flame {
position: absolute;
top: calc(7 * var(--u));
transform-style: preserve-3d;
transform-origin: 0 0;
animation: burn 10s linear infinite;
}
.flame i {
position: absolute;
left: calc(-10 * var(--u));
top: 0;
width: calc(20 * var(--u));
height: calc(56 * var(--u));
}
.flame i + i {
transform: rotateY(90deg);
}
.flame i::before,
.flame i::after {
content: '';
position: absolute;
inset: 0;
border-radius: 40% 40% 50% 50% / 16% 16% 84% 84%;
transform-origin: 50% 0;
background: radial-gradient(ellipse 50% 100% at 50% 0, #fff 0 10%, #ffb547 32%, rgb(255 77 157 / 0.7) 62%, transparent 98%);
animation: flicker 0.11s ease-in-out infinite alternate;
}
.flame i::after {
inset: 0 calc(5 * var(--u)) 38%;
background: radial-gradient(ellipse 50% 100% at 50% 0, #fff 0 35%, #ffdaa3 60%, transparent 98%);
animation-duration: 0.15s;
animation-direction: alternate-reverse;
}
@keyframes sway {
from { transform: rotateX(-12deg) rotateY(-28deg); }
to { transform: rotateX(-12deg) rotateY(28deg); }
}
/* where the rocket is in the frame. rest · lift-off: the camera follows,
so the rocket rises only a few units in the frame while the ground drops away under it · out through the top, speeding up ·
hidden for 0.2s while it is put back on the pad, still below the frame · rides back up with the
ground (the same numbers and easing as camera, so it stands on the pad all the way) · rest.
visibility, not opacity: opacity below 1 would flatten the 3D rocket. */
@keyframes fly {
0%, 13% { transform: translate3d(0, 0, 0); animation-timing-function: ease-in; }
20% { transform: translate3d(0, calc(-3 * var(--u)), 0); }
50% { transform: translate3d(0, calc(-14 * var(--u)), calc(-220 * var(--u))); animation-timing-function: cubic-bezier(0.5, 0, 1, 0.5); }
53.9% { visibility: visible; }
54% { visibility: hidden; }
55% { transform: translate3d(0, calc(-330 * var(--u)), calc(-500 * var(--u))); }
55.1% { transform: translate3d(0, calc(150 * var(--u)), 0); }
55.15% { visibility: hidden; }
55.2% { visibility: visible; transform: translate3d(0, calc(150 * var(--u)), 0); animation-timing-function: cubic-bezier(0.2, 0.6, 0.35, 1); }
70%, 100% { transform: translate3d(0, 0, 0); }
}
/* the ground: still until lift-off, then it drops out of the frame, faster and faster, waits
there while the rocket leaves, and comes back up with the next rocket on it */
@keyframes camera {
0%, 14% { translate: 0 0; animation-timing-function: cubic-bezier(0.55, 0, 0.85, 0.55); }
50%, 55.2% { translate: 0 calc(150 * var(--u)); animation-timing-function: cubic-bezier(0.2, 0.6, 0.35, 1); }
70%, 100% { translate: 0 0; }
/* while it is wholly below the frame it is hidden, so it is not there at all */
47.9% { visibility: visible; }
48%, 55.5% { visibility: hidden; }
55.6% { visibility: visible; }
}
/* the flame only grows as the rocket clears the pad, and is out on the next rocket */
@keyframes burn {
0%, 6% { transform: scale(0.4, 0); }
10% { transform: scale(1, 0.2); }
20% { transform: scale(1, 0.35); }
34%, 55% { transform: scale(1.1, 1.7); }
55.1%, 100% { transform: scale(0.4, 0); }
}
@keyframes flicker {
from { transform: scale(0.94, 0.86); }
to { transform: scale(1.04, 1.08); }
}
@keyframes glow {
0%, 6%, 28%, 100% { opacity: 0; }
12%, 17% { opacity: 1; }
}
@keyframes arm {
0%, 3%, 80%, 100% { transform: translateZ(calc(-4 * var(--u))) rotateY(0deg); }
9%, 72% { transform: translateZ(calc(-4 * var(--u))) rotateY(75deg); }
}
@keyframes smoke {
0%, 8% { opacity: 0; transform: translate3d(0, 0, calc(var(--z) * var(--u))) scale(0.3); }
13% { opacity: 0.85; transform: translate3d(calc(var(--x) * 18 * var(--u)), calc(-3 * var(--u)), calc(var(--z) * var(--u))) scale(0.9); }
32% { opacity: 0; transform: translate3d(calc(var(--x) * 62 * var(--u)), calc(-18 * var(--u)), calc(var(--z) * var(--u))) scale(calc(var(--s) * 1.7)); }
33%, 100% { opacity: 0; transform: translate3d(0, 0, calc(var(--z) * var(--u))) scale(0.3); }
}