猴子下山

Published on
/
/趣玩前端
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>猴子下山</title>
    <style>
      * {
        box-sizing: border-box;
      }

      body {
        padding: 0;
        margin: 0;
        font-family: sans-serif;
        background-color: #024f32;
        color: white;
      }

      .monkey {
        position: absolute;
        transition: 0.7s;
        width: 10px;
        height: 10px;
        transform: translateX(44px);
        --m: 1;
        --left-shoulder-angle: 40deg;
        --right-shoulder-angle: -170deg;
        --left-shoulder-pos: 0px;
        --right-shoulder-pos: calc(var(--m) * 30px);
        --left-elbow-angle: -50deg;
        --right-elbow-angle: -20deg;
        --waist-joint-angle: -45deg;
        --tail-angle: 12deg;
        --tail-base-angle: 40deg;
        --jump-height: 50px;
      }

      .monkey.reverse:not(.alternate) {
        --left-shoulder-angle: 10deg;
        --right-shoulder-angle: 140deg;
        --left-elbow-angle: 50deg;
        --right-elbow-angle: 30deg;
        --waist-joint-angle: 45deg;
        --tail-angle: -12deg;
        --tail-base-angle: -40deg;
      }

      .monkey.alternate {
        --left-shoulder-angle: -140deg;
        --right-shoulder-angle: -20deg;
        --left-elbow-angle: -30deg;
        --right-elbow-angle: -20deg;
      }

      .monkey.alternate.reverse {
        --left-shoulder-angle: 170deg;
        --right-shoulder-angle: -40deg;
        --left-elbow-angle: 20deg;
        --right-elbow-angle: 50deg;
        --waist-joint-angle: 45deg;
        --tail-angle: -12deg;
        --tail-base-angle: -40deg;
      }

      .swing-wrapper {
        width: 10px;
        height: 10px;
        animation: swing ease-in-out infinite 3s;
      }

      @keyframes swing {
        0%,
        100% {
          transform: rotate(-3deg);
        }
        50% {
          transform: rotate(3deg);
        }
      }

      .monkey * {
        position: absolute;
        transition: 0.7s;
        background-size: calc(var(--w) * var(--m)) calc(var(--h) * var(--m)) !important;
        width: calc(var(--w) * var(--m));
        height: calc(var(--h) * var(--m));
        background-repeat: no-repeat !important;
        image-rendering: pixelated;
      }

      .left {
        rotate: var(--left-shoulder-angle);
        translate: var(--left-shoulder-pos) 0;
        z-index: 5;
      }

      .reverse .left {
        z-index: -5;
      }

      .reverse .right {
        z-index: 5;
      }

      .right {
        rotate: var(--right-shoulder-angle);
        translate: var(--right-shoulder-pos) 0;
        z-index: -5;
      }

      .elbow {
        bottom: 0;
      }

      .left .elbow {
        rotate: var(--left-elbow-angle);
      }

      .right .elbow {
        rotate: var(--right-elbow-angle);
      }

      .waist > .joint {
        rotate: var(--waist-joint-angle);
      }

      .knee {
        bottom: 0;
        rotate: calc(var(--waist-joint-angle) * -2);
      }

      .body {
        --w: 30px;
        --h: 30px;
        background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAAAXNSR0IArs4c6QAAAIZJREFUSEtjZEADG1rN/qOLUYMfUH2KEdkcOIdWFqI7GuYAsMX0shTmCJDlA2cxvX0L8zXjqMXUyDrEmDEa1MSEElXUjAY1VYKRGENGg5qYUKKKmtGgpkowEmPICAzqkdfmGlAf09NylHb1gFpMD8uRuzEo/RlaWo6z74Rc2lC7rY1uKcguAOOsW5NYqXbPAAAAAElFTkSuQmCC);
      }

      .head {
        top: calc(var(--m) * -10px);
        left: calc(var(--m) * 4px);
        --w: 22px;
        --h: 22px;
        background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAAAXNSR0IArs4c6QAAAGtJREFUSEtjZMACNrSa/ccmjkssoPoUI7ocigCpBqIbhmwB3GBKDYVZAjMcbDC1DEU2nJHahsIMHzUYnlBGg2I0KDCLpyGYKmhWCNHUYGoajlIew+KU0iIUaw2CnGBItYBgnUeO4dgMBZkDAArwRQ8BLYjYAAAAAElFTkSuQmCC);
        z-index: 1;
      }

      .face {
        top: calc(var(--m) * 5px);
        left: calc(var(--m) * 4px);
        --w: 18px;
        --h: 14px;
        background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAAOCAYAAAAi2ky3AAAAAXNSR0IArs4c6QAAAHVJREFUOE+1k9sOwCAIQ9f//2gWiBCCuO7ifMKIB2gKjnRERPKdxQDgORY8BdQCCsRXSHT0C0hHvitTzY3Rkm4U1uXuB6loW0ZjnmHvSx913a06Nh95pc4GWdRh3KkxZ1yC2DhDV2ME6M2qTLtWKzO3Z4D/PQHES1jzyue2wwAAAABJRU5ErkJggg==);
      }

      .reverse .face {
        left: 0;
      }

      .shoulder {
        width: calc(100% + (calc(var(--m) * 10px)));
        top: 0;
        left: calc(var(--m) * -5px);
        height: calc(var(--m) * 10px);
      }

      .waist {
        height: calc(var(--m) * 10px);
        bottom: 0;
        width: 100%;
      }

      .right-leg {
        right: 0;
      }

      .thigh,
      .leg {
        background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAUCAYAAAC07qxWAAAAAXNSR0IArs4c6QAAADhJREFUOE9jZICCDa1m/2FsZDqg+hQjiA8mcCmCaQApZiSkCKZ4VCG28IaLjQYPlYKH6IRLbFYAAGcMNrX4mA6GAAAAAElFTkSuQmCC);
        --w: 10px;
        --h: 20px;
      }

      .joint {
        width: calc(var(--m) * 10px);
        height: calc(var(--m) * 10px);
      }

      .arm {
        background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAZCAYAAAAIcL+IAAAAAXNSR0IArs4c6QAAADpJREFUOE9jZICCDa1m/2FsZDqg+hQjiA8mcCmCaQApZiSkCKZ4VCG28IaLjQbPaPBghADxuZDYfA0AXddCzD8VHloAAAAASUVORK5CYII=);
        --w: 10px;
        --h: 25px;
      }

      .monkey.jump .body {
        animation: jump-up forwards 0.35s;
      }

      @keyframes jump-up {
        0%,
        100% {
          translate: 0;
        }
        70% {
          translate: 0 calc(var(--m) * var(--jump-height));
        }
      }

      .monkey .body {
        top: calc(var(--m) * 30px);
        left: calc(var(--m) * -28px);
      }

      .monkey.alternate .body {
        top: calc(var(--m) * 28px);
        left: calc(var(--m) * -10px);
      }

      .monkey.reverse .body {
        top: calc(var(--m) * 28px);
        left: calc(var(--m) * -15px);
      }

      .monkey.reverse.alternate .body {
        top: calc(var(--m) * 30px);
        left: calc(var(--m) * 0px);
      }

      .outer-handle-wrapper {
        position: absolute;
        width: 100%;
        display: flex;
        justify-content: space-evenly;
        max-width: 1000px;
        left: 50%;
        transform: translateX(-50%);
      }

      .outer-handle-wrapper:last-of-type {
        padding-bottom: 440px;
      }

      .tail-joint {
        width: calc(4px * var(--m));
        height: calc(4px * var(--m));
        bottom: 0;
        left: 2px;
        transform: rotate(var(--tail-base-angle));
      }

      .tail-joint .joint {
        width: calc(4px * var(--m));
        height: calc(4px * var(--m));
        bottom: 0;
        transform: rotate(var(--tail-angle));
      }

      .reverse .tail-joint {
        left: calc(28px * var(--m));
      }

      .tail {
        width: calc(4px * var(--m));
        height: calc(12px * var(--m));
        background-color: #b08536;
      }

      .outer-handle-wrapper:nth-child(odd) {
        flex-direction: row-reverse;
      }

      .handle-wrapper {
        width: 20px;
        height: 100%;
      }

      .handle-wrapper:nth-child(2) {
        padding-top: 20px;
      }

      .handle-wrapper:nth-child(3) {
        padding-top: 40px;
      }

      .handle-wrapper:nth-child(4) {
        padding-top: 60px;
      }

      .handle-wrapper:nth-child(5) {
        padding-top: 80px;
      }

      .handle {
        background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAAXNSR0IArs4c6QAAADpJREFUKFNjZEACnPqK/5H53y/eZ4Tx4Qx0RTAFMMVghbgUIStmJKQIbvUAKiTaMzDHEhU8uBQjBzgAXjoda3RhMkUAAAAASUVORK5CYII=);
        width: 20px;
        height: 20px;
        background-size: 20px;
        background-repeat: no-repeat !important;
        image-rendering: pixelated;
      }

      .text-wrapper {
        position: absolute;
        top: 0;
        width: 100%;
        margin: 80px auto 0;
      }

      p {
        text-align: center;
        color: #b08536;
        font-family: 'Courier New', Courier, monospace;
        font-size: 1.3rem;
      }

      small {
        font-size: 0.95rem;
      }

      .sign {
        position: fixed;
        font-family: Arial, Helvetica, sans-serif;
        color: #b08536;
        bottom: 10px;
        right: 10px;
        font-size: 0.6rem;
        text-transform: none;
      }

      a {
        color: #b08536;
        text-decoration: none;
        text-transform: none;
      }

      a:hover {
        text-decoration: underline;
      }
    </style>
  </head>
  <body>
    <div class="text-wrapper">
      <p>滚动界面</p>
      <p><small>不要滚太快,猴子下山得要点时间</small></p>
    </div>
    <div class="wrapper">
      <div class="monkey alternate">
        <div class="swing-wrapper">
          <div class="body">
            <div class="head">
              <div class="face"></div>
            </div>
            <div class="shoulder">
              <div class="joint left">
                <div class="arm">
                  <div class="joint"></div>
                  <div class="joint elbow">
                    <div class="arm"></div>
                  </div>
                </div>
              </div>
              <div class="joint right">
                <div class="arm">
                  <div class="joint"></div>
                  <div class="joint elbow">
                    <div class="arm"></div>
                  </div>
                </div>
              </div>
            </div>
            <div class="waist">
              <div class="joint">
                <div class="thigh">
                  <div class="joint knee">
                    <div class="leg"></div>
                  </div>
                </div>
              </div>
              <div class="tail-joint">
                <div class="tail">
                  <div class="joint">
                    <div class="tail">
                      <div class="joint">
                        <div class="tail">
                          <div class="joint">
                            <div class="tail"></div>
                          </div>
                        </div>
                      </div>
                    </div>
                  </div>
                </div>
              </div>
              <div class="joint right-leg">
                <div class="thigh">
                  <div class="joint knee">
                    <div class="leg"></div>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>

    <script>
      function init() {
        const settings = {
          handlePos: null,
          index: 0,
          nearestIndex: 0,
        };

        const monkey = {
          el: document.querySelector('.monkey'),
          x: 44,
          y: 0,
          alternate: false,
        };

        const wrapper = document.querySelector('.wrapper');

        const setStyles = ({ el, x, y, deg }) => {
          el.style.transform = `translate(${x ? `${x}px` : 0}, ${
            y ? `${y}px` : 0
          }) rotate(${deg || 0}deg)`;
        };

        const createHandles = (y, n) => {
          const handles = {
            el: Object.assign(document.createElement('div'), {
              className: 'outer-handle-wrapper',
              innerHTML: new Array(n)
                .fill('')
                .map(() => {
                  return `<div class="handle-wrapper">
                      <div class="handle"></div>
                    </div>`;
                })
                .join(''),
            }),
          };
          handles.el.style.top = `${y}px`;
          wrapper.appendChild(handles.el);
        };

        [
          [300, 5],
          [150, 3],
          [150, 4],
          [150, 2],
          [150, 4],
          [100, 2],
          [100, 5],
          [150, 2],
          [150, 4],
          [150, 3],
          [100, 2],
          [100, 5],
        ]
          .map((itemA, indexA, arr) => {
            return [
              arr
                .map((itemB, indexB) => {
                  const prevHandlesHeight =
                    indexB === 0 ? 0 : arr[indexB - 1][1] * 20;
                  return indexB <= indexA ? itemB[0] + prevHandlesHeight : 0;
                })
                .reduce((n, acc) => {
                  return n + acc;
                }, 0),
              itemA[1],
            ];
          })
          .forEach((itemC) => createHandles(itemC[0], itemC[1]));

        const handles = document.querySelectorAll('.handle');

        const monkeyJump = ({ x, y }) => {
          monkey.el.classList[monkey.x > x ? 'add' : 'remove']('reverse');
          monkey.el.classList[monkey.alternate ? 'add' : 'remove']('alternate');
          monkey.alternate = !monkey.alternate;
          monkey.x = x;
          monkey.el.style.setProperty(
            '--jump-height',
            monkey.y - y > 80 ? `${y - monkey.y - 10}px` : '-50px'
          );
          monkey.y = y;
          setStyles(monkey);

          monkey.el.classList.add('jump');
          setTimeout(() => {
            monkey.el.classList.remove('jump');
          }, 500);
        };

        const getHandlePos = () => {
          return [...handles].map((handle) => {
            const { x, y } = handle.getBoundingClientRect();
            return {
              x: x + window.scrollX,
              y: y + window.scrollY,
            };
          });
        };
        settings.handlePos = getHandlePos();

        window.addEventListener('resize', () => {
          settings.handlePos = getHandlePos();
          const { x, y } = settings.handlePos[settings.index];
          monkeyJump({ x, y });
        });

        const getNearestHandleIndex = () => {
          const y = Math.round(window.scrollY + window.innerHeight / 2);
          const nearestIndex = settings.handlePos
            .map((handle, i) => {
              return {
                i,
                diff: Math.abs(handle.y - y),
              };
            })
            .sort((a, b) => a.diff - b.diff)[0].i;
          settings.nearestIndex = nearestIndex;
        };

        setInterval(() => {
          getNearestHandleIndex();
          if (settings.index === settings.nearestIndex) return;
          const { x, y } = settings.handlePos[settings.index];
          monkeyJump({
            x: x + 5,
            y: y + 5,
          });
          if (settings.index > settings.nearestIndex && settings.index >= 0) {
            settings.index--;
          } else if (
            settings.index < settings.nearestIndex &&
            settings.index < handles.length - 1
          ) {
            settings.index++;
          }
        }, 700);
      }
      window.addEventListener('DOMContentLoaded', init);
    </script>
  </body>
</html>