就决定是你了,百变怪!

Published on
/
/趣玩前端
<!DOCTYPE html>
<html lang="zh">
  <head>
    <meta charset="UTF-8" />
    <title>就决定是你了,百变怪!</title>
    <link
      rel="stylesheet"
      href="https://cdnjs.cloudflare.com/ajax/libs/normalize/5.0.0/normalize.min.css"
    />
    <style>
      * {
        box-sizing: border-box;
      }
      :root {
        --size: 60;
        --margin: 20;
        --pokeball-size: 50;
        --pokeball-white: #e6e6e6;
        --pokeball-red: #f20d0d;
        --beam-color: #f20d59;
      }
      @media (max-width: 600px) {
        :root {
          --size: 85;
        }
      }
      body {
        min-height: 100vh;
        display: flex;
        align-items: center;
        justify-content: center;
        flex-direction: column;
        background: #d1ecfa;
      }
      .ditto {
        --hue: 320;
        --lightness: 75;
        --stroke: 5;
        height: calc(var(--size) * 1vmin);
        width: calc(var(--size) * 1vmin);
        z-index: 2;
      }
      .ditto > g {
        transform: translate(200px, 200px);
      }
      .ditto g {
        fill: hsla(
          var(--hue),
          100%,
          calc(var(--lightness) * 1%),
          var(--alpha, 1)
        );
        stroke: #000;
        stroke-width: calc(var(--stroke) * 1px);
        transition: d 0.15s ease;
      }
      .ditto path {
        transition: d 0.15s ease;
      }
      .ditto__real {
        opacity: 0;
      }
      .ditto__body {
        --lightness: 45;
        --stroke: 0;
      }
      .ditto__clone {
        --lightness: 80;
        --stroke: 0;
      }
      .ditto__stroke {
        --hue: 0;
        --alpha: 0;
        --stroke: 5;
        --lightness: 0;
        fill: none;
      }
      .ditto__outline {
        --hue: 340;
        --lightness: 50;
        --stroke: 0;
      }
      .pokeball {
        --level: 50;
        height: 100%;
        width: 100%;
        border-radius: 50%;
        border: 2px solid #000;
        position: relative;
        overflow: hidden;
        transition: all 0.15s ease;
        cursor: pointer;
        background: var(--pokeball-white);
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        outline: transparent;
      }
      .pokeball:after {
        content: '';
        position: absolute;
        top: 25%;
        left: 75%;
        background: rgba(255, 255, 255, 0.5);
        border-radius: 50%;
        height: 10px;
        width: 10px;
        transform: translate(-25%, -25%) rotate(-20deg);
        z-index: 2;
      }
      .pokeball:before {
        content: '';
        position: absolute;
        top: 50%;
        left: 50%;
        height: 80%;
        width: 80%;
        border: 2px solid #595959;
        z-index: 2;
        border-radius: 50%;
        transform: translate(-50%, -50%);
        -webkit-clip-path: polygon(50% 50%, 100% 65%, 100% 100%, 65% 100%);
        clip-path: polygon(50% 50%, 100% 65%, 100% 100%, 65% 100%);
      }
      .pokeball__beam {
        position: absolute;
        bottom: 50%;
        left: 50%;
        transform: translate(-50%, 0);
        width: calc(var(--pokeball-size) * 0.25px);
        background: var(--beam-color);
        filter: blur(2px);
        height: calc(
          (var(--size) * 0.22vmin) + (var(--margin) * 1vmin) +
            (var(--pokeball-size) * 0.5px)
        );
        z-index: -1;
      }
      .pokeball__wrapper {
        transform-style: preserve-3d;
        height: calc(var(--pokeball-size) * 1px);
        width: calc(var(--pokeball-size) * 1px);
        position: relative;
        margin-top: calc(var(--margin) * 1vmin);
      }
      .pokeball:hover {
        --level: 0;
      }
      .pokeball__face {
        height: 100%;
        background: linear-gradient(
          var(--pokeball-red) calc(50% - 1px),
          #000 calc(50% - 1px) calc(50% + 1px),
          var(--pokeball-white) calc(50% + 1px)
        );
        width: 100%;
        position: absolute;
        top: calc(var(--level) * 1%);
        left: 0;
        transition: all 0.15s ease;
        transform: translate(0, -50%);
      }
      .pokeball__face:after {
        content: '';
        height: 5px;
        width: 5px;
        border: 2px solid #000;
        background: #fff;
        border-radius: 50%;
        position: absolute;
        top: calc(var(--level) * 1%);
        left: 50%;
        transform: translate(-50%, -50%);
      }
      .pokeball__face:before {
        content: '';
        height: 12px;
        width: 12px;
        border: 2px solid #000;
        border-radius: 50%;
        background: #bfbfbf;
        position: absolute;
        top: calc(var(--level) * 1%);
        left: 50%;
        transition: all 0.15s ease;
        transform: translate(-50%, -50%);
      }
    </style>
  </head>
  <body>
    <svg class="ditto" viewBox="0 0 400 400">
      <defs>
        <path class="ditto__path" id="ditto__path"></path>
        <clipPath id="ditto__clip">
          <use xlink:href="#ditto__path"></use>
        </clipPath>
      </defs>
      <g class="ditto__real">
        <g class="ditto__body">
          <use xlink:href="#ditto__path"></use>
        </g>
        <g class="ditto__clone" clip-path="url(#ditto__clip)">
          <use xlink:href="#ditto__path" transform="translate(0, -20)"></use>
        </g>
        <g class="ditto__stroke">
          <use xlink:href="#ditto__path"></use>
        </g>
        <g class="ditto__face">
          <g class="ditto__eyes">
            <circle
              class="ditto__eye"
              cx="-35"
              cy="-65"
              r="5"
              fill="#000"
              stroke="none"
            ></circle>
            <circle
              class="ditto__eye"
              cx="35"
              cy="-60"
              r="5"
              fill="#000"
              stroke="none"
            ></circle>
          </g>
          <path
            class="ditto__mouth"
            d="M -40 -35 q -20 -0 80 0"
            stroke-width="4"
            stroke="#000"
            fill="none"
            stroke-linecap="round"
          ></path>
        </g>
      </g>
      <g class="ditto__outline">
        <use xlink:href="#ditto__path"></use>
      </g>
    </svg>
    <div class="pokeball__wrapper">
      <span class="pokeball__beam"></span>
      <button class="pokeball" title="Change Ditto">
        <span class="pokeball__face"></span>
      </button>
    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.2.6/gsap.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.16.0/d3.min.js"></script>
    <script>
      const {
        gsap: { to, timeline, set },
        d3: { curveBasisClosed, curveStep, lineRadial },
      } = window;

      // Elements
      const DITTO = document.querySelector('.ditto');
      const DITTO_PATH = document.querySelector('.ditto__path');
      const DITTO_MOUTH = document.querySelector('.ditto__mouth');
      const DITTO_BEAM = document.querySelector('.ditto__outline');
      const POKEBALL_BEAM = document.querySelector('.pokeball__beam');
      const DITTO_REAL = document.querySelector('.ditto__real');
      const BUTTON = document.querySelector('button');
      // Configurations
      const CURVES = [curveBasisClosed, curveStep];
      const DITTO_MOUTHS = [
        'M -40 -42.5 q 40 15 80 0', // Smile
        'M -40 -35 q -20 -0 80 0', // Straight
      ];
      // Gonna say that Ditto has around 14 points
      // Linear Radial goes 0 -> 360 CW from 12
      const DEFAULT_POINTS = [
        [0, 100],
        [15, 120],
        [30, 130],
        [50, 95],
        [70, 140],
        [80, 150],
        [90, 130],
        [100, 90],
        [120, 160],
        [130, 170],
        [140, 160],
        [145, 130],
        [180, 150],
        [215, 130],
        [220, 160],
        [230, 180],
        [270, 80],
        [280, 160],
        [300, 190],
        [315, 80],
        [325, 140],
        [345, 160],
      ];

      const SQUARE_POINTS = [
        [45, 150],
        [135, 150],
        [225, 150],
        [315, 150],
      ];

      const SWAY = {
        X: 10,
        Y: 40,
      };

      // Utility function
      const randomInRange = (min, max) =>
        Math.floor(Math.random() * (max - min + 1)) + min;
      // Return new points for Ditto body
      // Maps the default points given some bounds
      const getPoints = () => {
        if (Math.random() > 0.75) return SQUARE_POINTS;
        const POINTS = DEFAULT_POINTS.map((point) => [
          randomInRange(point[0] - SWAY.X, point[0] + SWAY.X),
          randomInRange(point[1] - SWAY.Y, point[1] + SWAY.Y),
        ]);

        return POINTS;
      };
      // Draws an outline for Ditto and determines whether
      // he's smiling or what he color he is
      const drawDitto = (
        points = DEFAULT_POINTS,
        curveBasis = curveBasisClosed,
        hue = 320
      ) => {
        const PATH = lineRadial().curve(curveBasis)(
          points.map((point) => [point[0] * (Math.PI / 180), point[1]])
        );

        DITTO.style.setProperty('--hue', hue);
        DITTO_PATH.setAttribute(
          'd',
          PATH.charAt(PATH.length).toLowerCase() !== 'z' ? `${PATH}z` : PATH
        );
      };

      // Draw ditto initially, doesn't necessarily mean he's showing
      drawDitto();

      const STATE = {
        ACTIVE: false,
        DITTO_OUT: false,
        RAN: false,
      };

      const CONFIG = {
        POKEBALL_SPEED: 0.15,
      };

      set(DITTO_BEAM, { transformOrigin: '50% 50%', scale: 1.05, opacity: 0 });
      set(POKEBALL_BEAM, { transformOrigin: '50% 100%', scaleY: 0 });
      // Event binding
      const onComplete = () => {
        BUTTON.removeAttribute('style');
        STATE.DITTO_OUT = !STATE.DITTO_OUT;
        STATE.ACTIVE = false;
      };
      const onStart = () => {
        STATE.ACTIVE = true;
      };
      BUTTON.addEventListener('click', () => {
        if (STATE.ACTIVE) return;
        if (STATE.RAN && !STATE.DITTO_OUT) {
          drawDitto(
            Math.random() > 0.5 ? DEFAULT_POINTS : getPoints(),
            Math.random() > 0.75 ? CURVES[1] : CURVES[0],
            Math.random() > 0.75 ? 180 : 320
          );
        }
        if (!STATE.RAN) STATE.RAN = true;
        set(BUTTON, {
          '--level': 0,
          transformOrigin: '50% 100%',
          rotateX: -20,
        });
        if (STATE.DITTO_OUT) {
          new timeline({
            onStart,
            onComplete,
          })
            .set(POKEBALL_BEAM, { transformOrigin: '50% 100%', opacity: 1 })
            .to(DITTO_BEAM, { duration: CONFIG.POKEBALL_SPEED, opacity: 1 }, 0)
            .to(
              POKEBALL_BEAM,
              { duration: CONFIG.POKEBALL_SPEED, scaleY: 1 },
              0
            )
            .to(DITTO_REAL, { duration: CONFIG.POKEBALL_SPEED, opacity: 0 }, 0)
            .to(
              DITTO_BEAM,
              { duration: CONFIG.POKEBALL_SPEED, opacity: 0 },
              CONFIG.POKEBALL_SPEED
            )
            .to(
              POKEBALL_BEAM,
              { duration: CONFIG.POKEBALL_SPEED, scaleY: 0 },
              CONFIG.POKEBALL_SPEED
            );
        } else {
          new timeline({
            onStart,
            onComplete,
          })
            .to(
              POKEBALL_BEAM,
              { duration: CONFIG.POKEBALL_SPEED, scaleY: 1 },
              0
            )
            .to(DITTO_BEAM, { duration: CONFIG.POKEBALL_SPEED, opacity: 1 }, 0)
            .to(
              DITTO_BEAM,
              { duration: CONFIG.POKEBALL_SPEED, opacity: 0 },
              CONFIG.POKEBALL_SPEED
            )
            .to(
              DITTO_REAL,
              { duration: CONFIG.POKEBALL_SPEED, opacity: 1 },
              CONFIG.POKEBALL_SPEED
            )
            .to(
              POKEBALL_BEAM,
              {
                duration: CONFIG.POKEBALL_SPEED,
                opacity: 0,
                transformOrigin: '50% 0',
                scaleY: 0,
              },

              CONFIG.POKEBALL_SPEED
            );
        }
      });

      // Update his mouth
      const smileOrNoSmile = () => {
        const MOUTH_INDEX = randomInRange(0, DITTO_MOUTHS.length - 1);
        DITTO_MOUTH.setAttribute('d', DITTO_MOUTHS[MOUTH_INDEX]);
        setTimeout(smileOrNoSmile, randomInRange(4000, 10000));
      };
      smileOrNoSmile();

      const EYES = document.querySelector('.ditto__eyes');
      const blink = () => {
        set(EYES, { scaleY: 1 });
        if (EYES.BLINK_TL) EYES.BLINK_TL.kill();
        EYES.BLINK_TL = new timeline({
          delay: Math.floor(Math.random() * 4) + 1,
          onComplete: () => blink(EYES),
        }).to(EYES, {
          duration: 0.05,
          transformOrigin: '50% 50%',
          scaleY: 0,
          yoyo: true,
          repeat: 1,
        });
      };
      blink();
    </script>
  </body>
</html>