← Illustrations

Walk Cycle

A simple character walks on the spot while the ground slides by underneath. Legs swing from the hip and bend at the knee, feet roll from heel to toe, the arms swing against the legs and the whole body bobs as the weight passes over each foot.

How to use it

  1. Paste the snippet just before </head>.
  2. Add an empty <div data-walk-cycle> where the scene should go. It fills the element with an SVG scene.

Options

Add any of these attributes to the same element.

data-colorColour of the top. Default #7c3aed.
data-trousersColour of the trousers. Default #1e293b.
data-skinSkin colour. Default #f1c27d.
data-hairHair colour. Default #3b2414.
data-speedSpeed multiplier. At 1 a full stride takes 1.1 seconds. Default 1.
data-ground="false"Keep the ground still, so it reads as walking on the spot.

It is 8:5 on a transparent background by default. It pauses when off screen. With reduced motion it shows one still pose mid-stride.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Walk Cycle: a simple character walks on the spot. Legs swing from the
    // hip and bend at the knee, feet roll from heel to toe, the arms swing
    // against the legs and the body bobs as weight passes over each foot,
    // while the ground slides past underneath. Drawn as SVG that fills the
    // element, and paused when off screen.
    // Usage: <div data-walk-cycle></div>
    // It is 8:5 by default on a transparent background.
    // Optional: data-color (top, default #7c3aed), data-trousers (default
    // #1e293b), data-skin (default #f1c27d), data-hair (default #3b2414),
    // data-speed (default 1: one stride every 1.1 s), data-ground="false"
    // (keep the ground still).
    var DEFAULTS = { color: '#7c3aed', trousers: '#1e293b', skin: '#f1c27d', hair: '#3b2414', speed: 1 };
    var CYCLE = 1100;          // ms for one full stride (two steps)
    var THIGH = 38, SHIN = 38, FOOT = 15;
    var HIPX = 196, GROUND = 214;
    var STRIDE = 128;          // how far the ground moves in one cycle
    var DEG = Math.PI / 180;
    var uid = 0;

    var css =
      '[data-walk-cycle]{display:block;position:relative;overflow:hidden;width:100%;aspect-ratio:8/5}' +
      '.wcy-svg{position:absolute;inset:0;width:100%;height:100%;display:block}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }

    function shade(hex, amt) {
      var h = hex.replace('#', '');
      if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
      var out = '#';
      for (var i = 0; i < 3; i++) {
        var c = parseInt(h.substr(i * 2, 2), 16);
        c = amt < 0 ? c * (1 + amt) : c + (255 - c) * amt;
        out += ('0' + Math.round(c).toString(16)).slice(-2);
      }
      return out;
    }

    // Leg pose for phase p (radians). Angles are from straight down, positive forwards.
    function leg(p) {
      var thigh = 25 * Math.sin(p);
      var c = Math.max(0, Math.cos(p + 0.5));
      var knee = 4 + 56 * c * c;                        // bends most early in the swing
      var shin = thigh - knee;
      var kx = Math.sin(thigh * DEG) * THIGH, ky = Math.cos(thigh * DEG) * THIGH;
      var ax = kx + Math.sin(shin * DEG) * SHIN, ay = ky + Math.cos(shin * DEG) * SHIN;
      // The foot stays near flat: toe down at push-off, toe up as it swings through.
      var tilt = (-shin * 0.55 - 8 * c) * DEG;
      var tx = ax + Math.cos(tilt) * FOOT, ty = ay + Math.sin(tilt) * FOOT;
      return { kx: kx, ky: ky, ax: ax, ay: ay, tx: tx, ty: ty };
    }

    // Arm pose: swings against its leg and bends more going forwards.
    function arm(p) {
      var up = -20 * Math.sin(p);
      var fore = up + 14 + 22 * Math.max(0, -Math.sin(p));
      var ex = Math.sin(up * DEG) * 28, ey = Math.cos(up * DEG) * 28;
      return { ex: ex, ey: ey, hx: ex + Math.sin(fore * DEG) * 26, hy: ey + Math.cos(fore * DEG) * 26 };
    }

    function f(n) { return n.toFixed(1); }

    function build(el) {
      var d = el.dataset;
      var top = d.color || DEFAULTS.color;
      var trousers = d.trousers || DEFAULTS.trousers;
      var skin = d.skin || DEFAULTS.skin;
      var hair = d.hair || DEFAULTS.hair;
      var speed = d.speed !== undefined ? +d.speed : DEFAULTS.speed;
      var moveGround = d.ground !== 'false';
      var n = ++uid;
      if (!el.hasAttribute('aria-label')) el.setAttribute('aria-label', 'A character walking on the spot');
      if (!el.hasAttribute('role')) el.setAttribute('role', 'img');

      var ticks = '';
      for (var i = 0; i < 14; i++) ticks += '<path d="M' + (i * 32) + ' ' + (GROUND + 9) + 'h14" />';
      var tufts = '';
      for (var j = 0; j < 5; j++) tufts += '<path d="M' + (j * 128 + 40) + ' ' + GROUND + 'l3 -7l3 6l3 -9l3 10" />';

      el.innerHTML =
        '<svg class="wcy-svg" viewBox="0 0 400 250" preserveAspectRatio="xMidYMid meet" aria-hidden="true" focusable="false">' +
        '<defs><linearGradient id="wcy-fade' + n + '" x1="0" x2="1"><stop offset="0" stop-color="#fff" stop-opacity="0"/><stop offset=".25" stop-color="#fff"/><stop offset=".75" stop-color="#fff"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient>' +
        '<mask id="wcy-mask' + n + '"><rect x="40" y="0" width="320" height="250" fill="url(#wcy-fade' + n + ')"/></mask></defs>' +
        '<g mask="url(#wcy-mask' + n + ')">' +
          '<path d="M0 ' + GROUND + 'H400" stroke="#94a3b8" stroke-width="2.5" stroke-linecap="round"/>' +
          '<g class="wcy-ticks" stroke="#94a3b8" stroke-opacity=".55" stroke-width="2.5" stroke-linecap="round">' + ticks + '</g>' +
          '<g class="wcy-tufts" stroke="#4ade80" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round">' + tufts + '</g>' +
        '</g>' +
        '<ellipse class="wcy-shadow" cx="' + HIPX + '" cy="' + (GROUND + 1) + '" rx="34" ry="4.5" fill="#1e1b2e" opacity=".12"/>' +
        '<g class="wcy-body" stroke-linecap="round" stroke-linejoin="round" fill="none">' +
          '<path class="wcy-arm-far" stroke="' + shade(skin, -0.18) + '" stroke-width="9"/>' +
          '<path class="wcy-sleeve-far" stroke="' + shade(top, -0.25) + '" stroke-width="12"/>' +
          '<path class="wcy-leg-far" stroke="' + shade(trousers, 0.12) + '" stroke-width="14"/>' +
          '<path class="wcy-shoe-far" stroke="#111827" stroke-width="9"/>' +
          '<path class="wcy-torso" stroke="' + top + '" stroke-width="30"/>' +
          '<path class="wcy-hips" stroke="' + trousers + '" stroke-width="28"/>' +
          '<path class="wcy-leg-near" stroke="' + trousers + '" stroke-width="14"/>' +
          '<path class="wcy-shoe-near" stroke="#0b0f19" stroke-width="9"/>' +
          '<g class="wcy-head">' +
            '<rect x="-4.5" y="-8" width="9" height="10" rx="3" fill="' + shade(skin, -0.12) + '"/>' +
            '<circle cx="0" cy="-20" r="15" fill="' + skin + '"/>' +
            '<path d="M-15 -21Q-16 -38 1 -37Q15 -36 15 -24Q8 -30 -2 -28Q-4 -22 -9 -20Q-11 -16 -15 -21Z" fill="' + hair + '"/>' +
            '<circle cx="8" cy="-20" r="1.9" fill="#1f2937"/>' +
            '<path d="M5 -12Q9 -10 12 -13" stroke="#7c2d12" stroke-width="1.6"/>' +
            '<circle cx="2" cy="-14" r="3" fill="#f472b6" opacity=".35"/>' +
            '<circle cx="-5" cy="-20" r="3.4" fill="' + shade(skin, -0.1) + '"/>' +
          '</g>' +
          '<path class="wcy-sleeve-near" stroke="' + top + '" stroke-width="12"/>' +
          '<path class="wcy-arm-near" stroke="' + skin + '" stroke-width="9"/>' +
        '</g>' +
        '</svg>';

      var svg = el.firstChild;
      function q(c) { return svg.querySelector('.' + c); }
      var parts = {
        armFar: q('wcy-arm-far'), sleeveFar: q('wcy-sleeve-far'), legFar: q('wcy-leg-far'), shoeFar: q('wcy-shoe-far'),
        torso: q('wcy-torso'), hips: q('wcy-hips'), legNear: q('wcy-leg-near'), shoeNear: q('wcy-shoe-near'),
        head: q('wcy-head'), sleeveNear: q('wcy-sleeve-near'), armNear: q('wcy-arm-near')
      };
      var tickG = q('wcy-ticks'), tuftG = q('wcy-tufts'), shadow = q('wcy-shadow');

      function legPath(hx, hy, L) {
        return 'M' + f(hx) + ' ' + f(hy) + 'L' + f(hx + L.kx) + ' ' + f(hy + L.ky) + 'L' + f(hx + L.ax) + ' ' + f(hy + L.ay);
      }
      function shoePath(hx, hy, L) {
        return 'M' + f(hx + L.ax - 2) + ' ' + f(hy + L.ay + 1) + 'L' + f(hx + L.tx) + ' ' + f(hy + L.ty + 1);
      }

      function frame(t) {
        var p = (t / CYCLE) * Math.PI * 2;
        var near = leg(p), far = leg(p + Math.PI);
        // Plant whichever foot is lowest on the ground: that gives the natural bob.
        var low = Math.max(near.ay, near.ty, far.ay, far.ty) + 5.5;
        var hy = GROUND - low;
        var hx = HIPX;
        var sway = Math.sin(p * 2) * 1.2;           // a slight lean as each foot pushes off
        var sx = hx + 6 + sway, sy = hy - 46;        // shoulder
        parts.legFar.setAttribute('d', legPath(hx - 2, hy, far));
        parts.shoeFar.setAttribute('d', shoePath(hx - 2, hy, far));
        parts.legNear.setAttribute('d', legPath(hx + 2, hy, near));
        parts.shoeNear.setAttribute('d', shoePath(hx + 2, hy, near));
        parts.torso.setAttribute('d', 'M' + f(hx + 1) + ' ' + f(hy - 12) + 'L' + f(sx) + ' ' + f(sy + 6));
        parts.hips.setAttribute('d', 'M' + f(hx) + ' ' + f(hy - 4) + 'L' + f(hx + 1) + ' ' + f(hy));
        parts.head.setAttribute('transform', 'translate(' + f(sx + 3) + ' ' + f(sy - 2) + ') rotate(' + (sway * 1.5).toFixed(2) + ')');
        var an = arm(p + Math.PI), af = arm(p);
        function armD(A, dx) {
          return 'M' + f(sx + dx) + ' ' + f(sy) + 'L' + f(sx + dx + A.ex) + ' ' + f(sy + A.ey) + 'L' + f(sx + dx + A.hx) + ' ' + f(sy + A.hy);
        }
        function sleeveD(A, dx) {
          return 'M' + f(sx + dx) + ' ' + f(sy) + 'L' + f(sx + dx + A.ex * 0.55) + ' ' + f(sy + A.ey * 0.55);
        }
        parts.armNear.setAttribute('d', armD(an, 0));
        parts.sleeveNear.setAttribute('d', sleeveD(an, 0));
        parts.armFar.setAttribute('d', armD(af, -3));
        parts.sleeveFar.setAttribute('d', sleeveD(af, -3));
        // The shadow tightens as the body rises.
        shadow.setAttribute('rx', f(30 + (hy - (GROUND - 82)) * 0.8));
        if (moveGround) {
          var off = (t / CYCLE) * STRIDE;
          tickG.setAttribute('transform', 'translate(' + f(-(off % 32)) + ' 0)');
          tuftG.setAttribute('transform', 'translate(' + f(-(off % 128)) + ' 0)');
        }
      }

      if (reduced()) { frame(CYCLE * 0.18); return; }

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

    function init() {
      document.querySelectorAll('[data-walk-cycle]').forEach(function (el) {
        if (el._wcy) return;   // idempotent
        el._wcy = true;
        build(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-walk-cycle></div>
<h2>On our way</h2>

<!-- a brisk walk in a green top: -->
<div data-walk-cycle data-speed="1.4" data-color="#16a34a"></div>