Pure CSS Scenes & objects #loop #scene #building

Windmill in pure CSS

A tapered tower of four leaning trapezoids with a pyramid cap, on a grass block. The sails turn with one rotateZ loop while the whole model turns slowly, so the depth reads.

  • 174 lines of CSS
  • 30 lines of HTML
  • No JavaScript
  • No dependencies
  • Snippet under MIT
GitHub

How it works

  1. Everything hangs from one point, the middle of the grass block’s top. The outer box tilts the camera down with rotateX(-20deg); a child spins the whole model with rotateY, so you see it from every side.
  2. The tower is a frustum: four trapezoids (clip-path), each standing on its bottom edge. rotateY(side) and a translateZ of 24 units put it on its side of the base; rotateX(5.59deg) then leans it in by atan((24 − 15) / 92), exactly enough to bring its top edge in to the narrower top.
  3. A leaning side is longer than the tower is high: it is the hypotenuse, √(92² + 9²) = 92.44 units. The cap is the same trick with triangles, leaning so far in that the tips meet.
  4. The sails are four identical blades drawn pointing up from the hub (transform-origin = the hub) and turned by 90° steps. Their parent loops rotate(0 → -360deg). It needs preserve-3d too: a flat, zero-size group gets sorted as one plane and hides behind the tower.
  5. Light is baked in: every side has a dark overlay of its own strength (--d), so the model looks lit from the front-left as it turns. backface-visibility: hidden skips the sides facing away.
  6. Every length is a multiple of one base unit, --u, tied to the canvas: the grass block is 150 units square and the tower 92 high, so the windmill is the same share of a gallery card, the editor and a recording canvas.

Key techniques

  • frustum: rotateY → translateZ → rotateX(lean)
  • clip-path trapezoids and triangles
  • rotateZ sails on a hub in front of the cap
  • one model spun with rotateY

Copy-paste code

HTML 30 lines

<div class="scene">
  <div class="windmill">
    <div class="spin">
      <div class="hill">
      <i style="--n:0;--d:0"></i>
      <i style="--n:1;--d:0.2"></i>
      <i style="--n:2;--d:0.36"></i>
      <i style="--n:3;--d:0.12"></i>
        <i class="top"></i>
      </div>
      <div class="tree" style="--x:-50;--z:-44;--k:1"><i></i><i></i></div>
      <div class="tree" style="--x:52;--z:34;--k:0.8"><i></i><i></i></div>
      <div class="tower">
      <i class="front" style="--n:0;--d:0"></i>
      <i style="--n:1;--d:0.2"></i>
      <i style="--n:2;--d:0.36"></i>
      <i style="--n:3;--d:0.12"></i>
      </div>
      <div class="cap">
      <i style="--n:0;--d:0"></i>
      <i style="--n:1;--d:0.2"></i>
      <i style="--n:2;--d:0.36"></i>
      <i style="--n:3;--d:0.12"></i>
      </div>
      <div class="rotor">
        <div class="sails"><b style="--n:0"></b><b style="--n:1"></b><b style="--n:2"></b><b style="--n:3"></b><u></u></div>
      </div>
    </div>
  </div>
</div>

CSS 174 lines

.scene {
  /* one base unit: every length below is a multiple of it, so the windmill is the same share of
     a gallery card, the editor, a full screen and a recording canvas */
  --u: 0.25vmin;
  display: grid;
  place-items: center;
  perspective: calc(800 * var(--u));
}

.windmill {
  --grass: color-mix(in srgb, #2ee6d6 35%, #4f9a3a);
  --earth: color-mix(in srgb, #ffb547 30%, #4a3226);
  --plaster: color-mix(in srgb, #ffb547 8%, #f2ede5);
  --roof: color-mix(in srgb, #ff4d9d 70%, #4a1024);
  --wood: #5b3b25;
  position: relative;
  width: calc(210 * var(--u));
  height: calc(200 * var(--u));
  transform-style: preserve-3d;
  transform: rotateX(-20deg); /* the camera looks down a little */
}

/* the model's origin: the middle of the grass top */
.spin {
  position: absolute;
  left: 50%;
  top: calc(164 * var(--u));
  transform-style: preserve-3d;
  animation: turn 28s linear infinite;
}

.hill,
.tower,
.cap,
.rotor,
.tree {
  position: absolute;
  transform-style: preserve-3d;
}

/* grass block: four earth sides hanging from the top edge, and the top laid flat */
.hill i {
  position: absolute;
  left: calc(-75 * var(--u));
  top: 0;
  width: calc(150 * var(--u));
  height: calc(24 * var(--u));
  background:
    linear-gradient(rgb(0 0 0 / var(--d)), rgb(0 0 0 / var(--d))),   /* baked-in shade */
    linear-gradient(var(--grass) 0 calc(5 * var(--u)), transparent calc(7 * var(--u))),
    linear-gradient(var(--earth), color-mix(in srgb, var(--earth) 65%, #000));
  backface-visibility: hidden;
  transform: rotateY(calc(var(--n) * 90deg)) translateZ(calc(75 * var(--u)));
}

.hill .top {
  top: calc(-75 * var(--u));
  height: calc(150 * var(--u));
  background:
    radial-gradient(circle at 60% 30%, #ffe0a8 0 calc(1.5 * var(--u)), transparent calc(2 * var(--u))) 0 0 / calc(34 * var(--u)) calc(30 * var(--u)),
    linear-gradient(#c9a066, #c9a066) 50% 100% / calc(12 * var(--u)) 50% no-repeat,   /* the path to the door */
    radial-gradient(closest-side, color-mix(in srgb, var(--grass) 70%, #fff), var(--grass) 70%, color-mix(in srgb, var(--grass) 80%, #000));
  transform: rotateX(90deg);
}

/* a tree: two crossed planes, each cut to a tree outline; --x and --z are plain numbers, in units */
.tree {
  transform: translate3d(calc(var(--x) * var(--u)), 0, calc(var(--z) * var(--u))) scale(var(--k));
}

.tree i {
  position: absolute;
  left: calc(-13 * var(--u));
  top: calc(-44 * var(--u));
  width: calc(26 * var(--u));
  height: calc(44 * var(--u));
  background: linear-gradient(color-mix(in srgb, var(--grass) 80%, #fff), color-mix(in srgb, var(--grass) 70%, #000) 80%, var(--wood) 80%);
  clip-path: polygon(50% 0, 70% 32%, 60% 32%, 88% 64%, 74% 64%, 100% 80%, 58% 80%, 58% 100%, 42% 100%, 42% 80%, 0 80%, 26% 64%, 12% 64%, 40% 32%, 30% 32%);
}

.tree i + i {
  transform: rotateY(90deg);
}

/* Tower sides: trapezoids 48 units wide at the base, 30 at the top. Each stands on its bottom
   edge (the origin), steps out 24 units and leans in by atan(9 / 92) = 5.59deg. */
.tower i {
  position: absolute;
  left: calc(-24 * var(--u));
  top: calc(-92.44 * var(--u));
  width: calc(48 * var(--u));
  height: calc(92.44 * var(--u)); /* the slanted length: √(92² + 9²) */
  clip-path: polygon(18.75% 0, 81.25% 0, 100% 100%, 0 100%);
  transform-origin: 50% 100%;
  transform: rotateY(calc(var(--n) * 90deg)) translateZ(calc(24 * var(--u))) rotateX(5.59deg);
  backface-visibility: hidden;
  background:
    linear-gradient(rgb(0 0 0 / var(--d)), rgb(0 0 0 / var(--d))),
    linear-gradient(var(--wood) 0 calc(6 * var(--u)), transparent calc(6 * var(--u))),
    radial-gradient(circle at 50% 38%, #ffb547 0 calc(3.5 * var(--u)), var(--wood) calc(4 * var(--u)) calc(5.5 * var(--u)), transparent calc(6 * var(--u))),
    var(--plaster);
}

.tower .front {
  background:
    linear-gradient(var(--wood) 0 calc(6 * var(--u)), transparent calc(6 * var(--u))),
    radial-gradient(circle at 50% 38%, #ffb547 0 calc(3.5 * var(--u)), var(--wood) calc(4 * var(--u)) calc(5.5 * var(--u)), transparent calc(6 * var(--u))),
    radial-gradient(circle at 50% calc(100% - calc(16 * var(--u))), var(--wood) 0 calc(7.5 * var(--u)), transparent calc(8 * var(--u))),
    linear-gradient(var(--wood), var(--wood)) 50% 100% / calc(15 * var(--u)) calc(16 * var(--u)) no-repeat,
    var(--plaster);
}

/* the cap: the same trick with triangles, standing on the tower's top (y = -92 units) */
.cap i {
  position: absolute;
  left: calc(-21 * var(--u));
  top: calc(-128.62 * var(--u));
  width: calc(42 * var(--u));
  height: calc(36.62 * var(--u));
  clip-path: polygon(50% 0, 100% 100%, 0 100%);
  transform-origin: 50% 100%;
  transform: rotateY(calc(var(--n) * 90deg)) translateZ(calc(21 * var(--u))) rotateX(34.99deg);
  backface-visibility: hidden;
  background:
    linear-gradient(rgb(0 0 0 / var(--d)), rgb(0 0 0 / var(--d))),
    repeating-linear-gradient(transparent 0 calc(5 * var(--u)), rgb(0 0 0 / 0.22) calc(5 * var(--u)) calc(6 * var(--u))),
    linear-gradient(color-mix(in srgb, var(--roof) 75%, #fff), var(--roof));
}

/* the hub: above the tower, in front of the cap */
.rotor {
  transform: translate3d(0, calc(-100 * var(--u)), calc(27 * var(--u)));
}

.sails {
  position: absolute;
  transform-style: preserve-3d; /* sort each sail on its own */
  animation: sails 6s linear infinite;
}

/* one blade pointing up from the hub: stock on the left edge, lattice and cloth beside it */
.sails b {
  position: absolute;
  left: calc(-2 * var(--u));
  bottom: calc(5 * var(--u));
  width: calc(24 * var(--u));
  height: calc(70 * var(--u));
  transform-origin: calc(2 * var(--u)) calc(100% + calc(5 * var(--u))); /* the hub */
  transform: rotate(calc(var(--n) * 90deg));
  background:
    linear-gradient(90deg, var(--wood) 0 calc(4 * var(--u)), transparent calc(4 * var(--u))),
    linear-gradient(90deg, transparent calc(100% - calc(2 * var(--u))), var(--wood) 0) 0 0 / 100% 80% no-repeat,
    repeating-linear-gradient(transparent 0 calc(7 * var(--u)), var(--wood) calc(7 * var(--u)) calc(8.5 * var(--u))) 0 0 / 100% 80% no-repeat,
    linear-gradient(#fff6e9, #efd5ab) calc(4 * var(--u)) 0 / calc(100% - calc(4 * var(--u))) 80% no-repeat;
}

.sails u {
  position: absolute;
  left: calc(-6 * var(--u));
  top: calc(-6 * var(--u));
  width: calc(12 * var(--u));
  height: calc(12 * var(--u));
  border-radius: 50%;
  background: radial-gradient(circle at 35% 35%, #c8b5a8, var(--wood) 70%);
  transform: translateZ(calc(1 * var(--u)));
}

@keyframes turn {
  to { transform: rotateY(360deg); }
}

@keyframes sails {
  to { transform: rotate(-360deg); }
}
Standalone snippet · plain CSS, no build step, no dependencies.