← 3D & depth

Wireframe Terrain

A glowing wireframe landscape rolls endlessly towards you: a flat valley between neon hills, under a striped retro sun. Hills hide the lines behind them, and the far distance fades into a haze.

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-wireframe-terrain to a box with a height and a dark background.

Options

Add any of these attributes to the same element.

data-colorLine colour. Default #e879f9.
data-speedHow fast the land rolls by. Default 1.
data-heightHow tall the hills are. Default 1.
data-sun="false"No sun.
data-bgColour the hills are filled with. Default the box’s background colour.

Drawn on a canvas, paused when off screen. With reduced motion it shows a still frame.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Wireframe Terrain: a glowing wireframe landscape rolls towards you, a
    // flat valley between hills, under a striped retro sun. Drawn on a canvas
    // with hidden lines removed, and paused when off screen.
    // Usage: <div data-wireframe-terrain style="height: 360px; background: #12052a"></div>
    // Optional: data-color (line colour, default #e879f9), data-speed (default
    // 1), data-height (hill height, default 1), data-sun="false" (no sun),
    // data-bg (colour the hills are filled with, default the box's background).
    var DEFAULTS = { color: '#e879f9', speed: 1, height: 1 };

    var css =
      '[data-wireframe-terrain]{position:relative;overflow:hidden}' +
      '.wft-canvas{position:absolute;inset:0;width:100%;height:100%;display:block;pointer-events:none}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    // Smooth value noise: random heights on a grid, blended between.
    function hash(x, y) {
      var h = Math.sin(x * 127.1 + y * 311.7) * 43758.5453;
      return h - Math.floor(h);
    }
    function noise(x, y) {
      var xi = Math.floor(x), yi = Math.floor(y), xf = x - xi, yf = y - yi;
      var u = xf * xf * (3 - 2 * xf), v = yf * yf * (3 - 2 * yf);
      var a = hash(xi, yi), b = hash(xi + 1, yi), c = hash(xi, yi + 1), d = hash(xi + 1, yi + 1);
      return a + (b - a) * u + (c - a) * v + (a - b - c + d) * u * v;
    }

    function build(el) {
      var d = el.dataset;
      var canvas = document.createElement('canvas');
      canvas.className = 'wft-canvas';
      canvas.setAttribute('aria-hidden', 'true');
      el.insertBefore(canvas, el.firstChild);
      var ctx = canvas.getContext('2d');
      var color = d.color || DEFAULTS.color;
      var speed = d.speed !== undefined ? +d.speed : DEFAULTS.speed;
      var tall = d.height !== undefined ? +d.height : DEFAULTS.height;
      var sun = d.sun !== 'false';
      var bg = d.bg || getComputedStyle(el).backgroundColor;
      if (!bg || bg === 'transparent' || /rgba\(.*,\s*0\)$/.test(bg)) bg = '#12052a';
      var w = 0, h = 0, dpr = 1, travel = 0, t = 0;
      // The world: columns from -HALF to HALF, rows from NEAR to FAR.
      var HALF = 16, NEAR = 0.6, FAR = 26, CAM = 1.4;
      var cols = HALF * 2 + 1;

      function resize() {
        dpr = Math.min(2, window.devicePixelRatio || 1);
        w = el.clientWidth;
        h = el.clientHeight;
        canvas.width = Math.round(w * dpr);
        canvas.height = Math.round(h * dpr);
      }

      function height(x, z) {
        var ax = Math.abs(x);
        // Flat down the middle, rising into hills either side.
        var side = Math.max(0, Math.min(1, (ax - 1.5) / 6));
        side = side * side * (3 - 2 * side);
        var n = noise(x * 0.32, z * 0.32) * 0.75 + noise(x * 0.9 + 9, z * 0.9) * 0.25;
        return (side * (1.2 + n * 5.5) + n * 0.18) * tall;
      }

      function draw() {
        ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
        ctx.clearRect(0, 0, w, h);
        var horizon = h * 0.46, cx = w / 2, f = Math.max(w, h * 1.4) * 0.55;
        var sway = Math.sin(t * 0.25) * 0.8;

        if (sun) {
          var r = Math.min(w * 0.22, h * 0.26), sy = horizon - r * 0.32;
          var g = ctx.createLinearGradient(0, sy - r, 0, sy + r);
          g.addColorStop(0, '#fde68a');
          g.addColorStop(0.55, '#fb7185');
          g.addColorStop(1, '#c026d3');
          ctx.fillStyle = g;
          ctx.beginPath();
          ctx.arc(cx, sy, r, 0, Math.PI * 2);
          ctx.fill();
          // The stripes across its lower half, thicker towards the bottom.
          ctx.fillStyle = bg;
          for (var k = 0; k < 6; k++) {
            var yy = sy + r * (0.08 + k * 0.16), th = r * (0.025 + k * 0.022);
            ctx.fillRect(cx - r, yy, r * 2, th);
          }
        }

        var shift = travel % 1;
        var rows = [];
        for (var zi = Math.ceil(FAR); zi >= 0; zi--) {
          var z = NEAR + zi - shift;
          if (z < 0.25) continue;
          var row = [];
          for (var c = 0; c < cols; c++) {
            var x = c - HALF;
            var y = height(x, zi + Math.floor(travel));
            row.push([cx + (x - sway) * f / z, horizon + (CAM - y) * f / z]);
          }
          rows.push({ z: z, pts: row });
        }

        ctx.lineJoin = 'round';
        for (var i = 0; i < rows.length; i++) {
          var far = rows[i], near = rows[i + 1];
          var fade = Math.max(0, Math.min(1, 1 - (far.z - 2) / (FAR - 2)));
          ctx.strokeStyle = color;
          ctx.globalAlpha = 0.12 + 0.88 * fade * fade;
          ctx.lineWidth = 0.6 + fade * 0.9;
          if (near) {
            // Fill the strip in front of this row so it hides what's behind.
            ctx.fillStyle = bg;
            ctx.globalAlpha = 1;
            ctx.beginPath();
            far.pts.forEach(function (p, j) { if (j) ctx.lineTo(p[0], p[1]); else ctx.moveTo(p[0], p[1]); });
            for (var j = cols - 1; j >= 0; j--) ctx.lineTo(near.pts[j][0], near.pts[j][1]);
            ctx.closePath();
            ctx.fill();
            ctx.globalAlpha = 0.12 + 0.88 * fade * fade;
          }
          ctx.beginPath();
          far.pts.forEach(function (p, j) { if (j) ctx.lineTo(p[0], p[1]); else ctx.moveTo(p[0], p[1]); });
          if (near) {
            for (var q = 0; q < cols; q++) {
              ctx.moveTo(far.pts[q][0], far.pts[q][1]);
              ctx.lineTo(near.pts[q][0], near.pts[q][1]);
            }
          }
          ctx.stroke();
        }
        ctx.globalAlpha = 1;
        // A soft haze where the land meets the sky.
        var haze = ctx.createLinearGradient(0, horizon - h * 0.12, 0, horizon + h * 0.06);
        haze.addColorStop(0, 'rgba(0,0,0,0)');
        haze.addColorStop(0.7, hexA(color, 0.14));
        haze.addColorStop(1, 'rgba(0,0,0,0)');
        ctx.fillStyle = haze;
        ctx.fillRect(0, horizon - h * 0.12, w, h * 0.18);
      }

      function hexA(hex, a) {
        var m = /^#?([0-9a-f]{6})$/i.exec(hex);
        if (!m) return 'rgba(255,255,255,' + a + ')';
        var n = parseInt(m[1], 16);
        return 'rgba(' + (n >> 16) + ',' + (n >> 8 & 255) + ',' + (n & 255) + ',' + a + ')';
      }

      resize();
      if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
        draw();
        if (window.ResizeObserver) new ResizeObserver(function () { resize(); draw(); }).observe(el);
        return;
      }
      if (window.ResizeObserver) new ResizeObserver(resize).observe(el);

      var last = 0;
      var timer = anime.createTimer({
        loop: true,
        duration: 1000,
        autoplay: false,
        onUpdate: function () {
          var now = performance.now();
          var dt = last ? Math.min(50, now - last) / 1000 : 0.016;
          last = now;
          t += dt;
          travel += dt * 2.2 * speed;
          draw();
        }
      });
      new IntersectionObserver(function (entries) {
        if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
      }).observe(el);
    }

    function init() {
      document.querySelectorAll('[data-wireframe-terrain]').forEach(function (el) {
        if (el._wft) return;
        el._wft = true;
        build(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-wireframe-terrain style="height: 360px; background: #12052a"></div>