前端嘛 Logo
前端嘛
两个自制交互按钮

两个自制交互按钮

2026-06-02
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Starmaker Heads</title>
    <style>
      :root {
        --border: 1px solid black;
      }

      body {
        width: 100vw;
        height: 100vh;
        background: #f4e9ea;
        display: flex;
        justify-content: center;
        align-items: center;
      }

      .container {
        display: flex;
        gap: 40px;
        align-items: flex-end;
      }

      .head {
        border-radius: 10px;
        border: var(--border);
        height: 100px;
        position: relative;
      }

      .ear {
        height: 50px;
        width: 40px;
        border-radius: 50%;
        border: var(--border);
        position: absolute;
        bottom: 15px;
        left: -12px;
        z-index: -1;
        background: #3c4758;
        overflow: hidden;

        &:after {
          content: "";
          height: 35px;
          width: 35px;
          border-radius: 50%;
          border: var(--border);
          background: #4390a6;
          position: absolute;
        }

        &.right {
          transform: scaleX(-1);
          left: unset;
          right: -12px;
        }
      }

      .first {
        position: relative;
        cursor: pointer;
        user-select: none;
        -webkit-user-select: none;
        -webkit-touch-callout: none;
        touch-action: manipulation;

        .head {
          width: 120px;
          background: linear-gradient(#d3cecf 30%, #8b8084);
          overflow: hidden;
          display: flex;
          flex-direction: column;
          justify-content: flex-end;
        }

        .mouth {
          height: 30px;
          width: 100%;
          background: linear-gradient(#d3cecf 40%, #a79ca0);
          border-top: var(--border);
        }

        .face {
          display: flex;
          justify-content: space-between;
          margin-left: -5px;
          margin-right: -5px;
          position: relative;
        }

        .eye {
          height: 45px;
          width: 45px;
          border-radius: 50%;
          border: var(--border);
          background: linear-gradient(#b0a7aa, white);
          position: relative;
          overflow: hidden;

          &:before {
            content: "";
            height: 33px;
            width: 33px;
            border-radius: 50%;
            position: absolute;
            top: 6px;
            left: 6px;
            background: #503641;
          }

          &:after {
            content: "";
            height: 22px;
            width: 45px;
            border-radius: 22px 22px 0 0;
            background: #ebac6d;
            position: absolute;
            top: -2px;
            left: 0;
            border: var(--border);
          }
        }

        .nose {
          position: absolute;
          left: 50%;
          transform: translateX(-50%);
          top: -2px;
          height: 50px;
          width: 50px;
          border-radius: 50%;
          border: var(--border);
          background: #395267;
          overflow: hidden;

          &:after {
            content: "";
            width: 55px;
            height: 100%;
            border: var(--border);
            border-radius: 50%;
            position: absolute;
            background: linear-gradient(#6fb0aa, #2f81a1);
            top: -30%;
            left: 50%;
            transform: translateX(-50%);
          }
        }

        .head-gear {
          height: 40px;
          width: 10px;
          border: var(--border);
          border-bottom: none;
          margin: 0 auto;
          position: relative;
          border-top-left-radius: 10px;
          border-top-right-radius: 10px;
          background: linear-gradient(#d5d0d2 20%, #6a5e60 40%, #d5d0d2);

          .button {
            position: absolute;
            height: 5px;
            width: 15px;
            top: 10px;
            left: 50%;
            transform: translateX(-50%);
            background: #6a5e60;
            border: var(--border);
            border-radius: 5px;
          }

          .paddle {
            position: absolute;
            filter: url("#goo");
            right: 54px;
            top: 10px;

            &:before,
            &:after {
              content: "";
              width: 50px;
              height: 20px;
              clip-path: polygon(15% 0, 100% 0, 0 100%);
              position: absolute;
            }

            &:before {
              background: #7e4640;
              top: 5px;
            }

            &:after {
              background: #ea9564;
            }

            &.reverse {
              right: -44px;
              top: 15px;
              transform: scale(-1, -1);
            }
          }
        }
      }

      .second {
        cursor: pointer;
        user-select: none;
        -webkit-user-select: none;
        -webkit-touch-callout: none;
        touch-action: manipulation;

        .head {
          width: 130px;
          background: linear-gradient(#eb87b4 40%, #e44c8d);
          display: flex;
          justify-content: center;
          align-items: center;

          &:before,
          &:after {
            content: "";
            height: 30px;
            width: 45px;
            border-radius: 15px;
            border: var(--border);
            position: absolute;
            top: -15px;
            z-index: -1;
          }

          &:before {
            transform: rotate(-15deg);
            left: 0;
            background: linear-gradient(195deg, #eb87b4, #e44c8d, #903d5e 70%);
          }

          &:after {
            transform: rotate(15deg);
            right: 0;
            background: linear-gradient(155deg, #eb87b4, #e44c8d, #903d5e 70%);
          }
        }

        .face {
          height: 75px;
          width: 100px;
          border: var(--border);
          border-radius: 10px;
          overflow: hidden;
          position: relative;
          display: flex;
          flex-direction: column;
          justify-content: space-between;
          align-items: center;
          background: linear-gradient(#3e2c32, #593744, #794b5d, #8a5f6d);
        }

        .forehead {
          position: absolute;
          height: 7px;
          width: 100%;
          border-bottom: var(--border);
          background: #73394f;
          z-index: 2;
        }

        .eyes {
          display: flex;
          justify-content: space-between;
          margin-bottom: -20px;
          margin-top: -22px;
          gap: 25px;

          .eye {
            height: 40px;
            width: 40px;
            border-radius: 50%;
            background: linear-gradient(#8b7d7d, white);
            border: var(--border);
            position: relative;
            overflow: hidden;

            &:before,
            &:after {
              content: "";
              height: 28px;
              width: 25px;
              border-radius: 50%;
              position: absolute;
              top: 0;
              background: linear-gradient(#433243, #594c70);
            }

            &:before {
              left: 10px;
            }

            &:after {
              right: 10px;
            }

            /* Lid sits hidden (height 0) above the static eye; the blink
               animation grows it down to cover the eye, then back. */
            .eyelid {
              position: absolute;
              top: 0;
              left: 0;
              right: 0;
              height: 0;
              background: linear-gradient(#6e4154, #8a5a6d);
              border-bottom: var(--border);
              z-index: 2;
            }
          }
        }

        .brows {
          display: flex;
          justify-content: space-between;
          gap: 20px;
          height: 40px;
          margin-top: -10px;
          z-index: 1;

          &:before,
          &:after {
            content: "";
            height: 40px;
            width: 50px;
            border-radius: 15px;
            border: var(--border);
            background: linear-gradient(#4b313e, #6e4154, #8a5a6d, #956e7b);
          }

          &:before {
            transform: rotate(-5deg);
          }

          &:after {
            transform: rotate(5deg);
          }
        }

        .mouth {
          height: 40px;
          width: 60px;
          border: var(--border);
          border-bottom: none;
          border-top-left-radius: 30px;
          border-top-right-radius: 30px;
          background: linear-gradient(#c0afb4, #977080);
          display: flex;
          align-items: center;
          justify-content: center;
          z-index: 1;

          .lips {
            height: 7px;
            width: 40px;
            border-radius: 50%;
            border-bottom: var(--border);
            position: relative;
            margin-top: 10px;

            &:before,
            &:after {
              content: "";
              height: 5px;
              width: 0;
              border-left: var(--border);
              position: absolute;
              transform: rotate(20deg);
              top: 2px;
            }

            &:after {
              transform: rotate(-20deg);
              right: 0;
            }
          }
        }

        .nose {
          position: absolute;
          height: 20px;
          width: 30px;
          border-radius: 50%;
          border: var(--border);
          background: linear-gradient(#665fb4, #635398);
          top: 55%;
          transform: translateY(-50%);
          overflow: hidden;
          z-index: 1;

          &:before {
            content: "";
            height: 20px;
            width: 30px;
            border: var(--border);
            border-radius: inherit;
            position: absolute;
            top: -8px;
            left: 50%;
            transform: translateX(-50%);
            background: linear-gradient(#4f97d3, #597fbb);
          }
        }
      }

      .third,
      .fourth {
        position: relative;

        .head {
          width: 100px;
          position: relative;

          &:before {
            content: "";
            height: 70px;
            width: 70px;
            border-radius: 10px;
            border: var(--border);
            position: absolute;
            left: 15px;
            bottom: 15px;
          }

          .face {
            position: absolute;
            top: 20px;
            left: 15px;
            height: 65px;
            width: 70px;
            background: linear-gradient(#d0684b, #efbd77);
            border-radius: 10px;
            border: var(--border);
            overflow: hidden;
          }

          .eyes {
            display: flex;
            margin: 5px -10px 0;
            justify-content: space-between;
            .eye {
              height: 35px;
              width: 35px;
              border: var(--border);
              border-top-left-radius: 5px;
              border-top-right-radius: 5px;
              border-bottom-right-radius: 50%;
              border-bottom-left-radius: 50%;
              background: linear-gradient(#b0a7aa 60%, white);
              display: flex;
              justify-content: center;
              align-items: center;
              overflow: hidden;
              position: relative;

              &:before {
                content: "";
                height: 22px;
                width: 25px;
                border-radius: 50%;
                background: black;
              }

              &:after {
                content: "";
                position: absolute;
                height: 18px;
                width: 35px;
                top: 0;
                background: linear-gradient(#eeb974, #e58553);
                border-bottom: var(--border);
              }
            }
          }

          .nose {
            height: 35px;
            width: 35px;
            border-radius: 50%;
            border: var(--border);
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: linear-gradient(#af4970, #5c3041);
            overflow: hidden;
            z-index: 10;

            &:after {
              content: "";
              height: 100%;
              width: 100%;
              border-radius: 50%;
              border: var(--border);
              position: absolute;
              top: -8px;
              background: linear-gradient(#ed7aa8, #c6497d);
            }
          }
        }

        .ear {
          left: -15px;
          background: #7f3854;

          &:after {
            background: linear-gradient(#e884af, #c3447a);
          }

          &.right {
            left: unset;
            right: -15px;
          }
        }
      }

      .third {
        .head {
          background: linear-gradient(#7bb8ad, #337493);
          &:before {
            background: #356178;
          }

          .mouth {
            margin-left: 15px;
            margin-top: 5px;
            transform: rotate(180deg);
            height: 20px;
            width: 40px;
            background:
              linear-gradient(135deg, #eebb77 35%, transparent 1%) -10px 0,
              linear-gradient(225deg, #eebb77 35%, transparent 1%) -10px 0,
              linear-gradient(315deg, #e6a56b 35%, transparent 1%),
              linear-gradient(45deg, #e6a56b 35%, transparent 1%);
            background-size: 20px 20px;
            background-color: black;
          }
        }
      }

      .fourth {
        .head {
          background: linear-gradient(#5498ce, #62569c);
          &:before {
            background: #5e4985;
          }

          .mouth {
            height: 10px;
            width: 50px;
            border: var(--border);
            border-radius: 10px;
            margin-left: 10px;
            background: linear-gradient(#513840, #7b4958);

            &:after {
              content: "";
              width: 30px;
              margin-left: 10px;
              margin-top: 15px;
              border-top: var(--border);
              display: flex;
            }
          }
        }
      }

      /* First head = press-and-hold button.
         These animations run only while the head is pressed. On release, JS
         lets each one finish its current cycle (see <script> at end of body),
         so the propeller stops at a full turn and the eyes stop open — never
         frozen mid-action. */
      .first.spinning .paddles {
        animation: rotate-fan 1s linear infinite;
      }

      .first.blinking .eye:after {
        animation: close-eyes 0.9s ease-in-out infinite;
      }

      /* Second head = click button. One click blinks the eyes and pops the
         eyebrows once; JS then clears the class so the next click replays it.
         Static otherwise. */
      .second.reacting .eyes .eye .eyelid {
        animation: blink-second 0.6s ease-in-out;
      }

      .second.reacting .brows:before,
      .second.reacting .brows:after {
        animation: raise-eyebrows 0.6s ease-in-out;
      }

      @keyframes rotate-fan {
        to {
          transform: rotateY(360deg);
        }
      }

      /* Blink lands at the start of each cycle, so a press blinks immediately
         (the old version waited until 85% of a 3s cycle). The cycle ends back
         at the open height (22px), which is also where it stops on release. */
      @keyframes close-eyes {
        0% {
          height: 22px;
        }
        15% {
          height: 50px;
        }
        30% {
          height: 22px;
        }
        100% {
          height: 22px;
        }
      }

      /* Two quick pops starting immediately (was a slow 3s loop with a long
         dead lead-in) — now a snappy one-shot for the click reaction. */
      @keyframes raise-eyebrows {
        0% {
          height: 40px;
        }
        25% {
          height: 30px;
        }
        50% {
          height: 40px;
        }
        75% {
          height: 30px;
        }
        100% {
          height: 40px;
        }
      }

      /* An eyelid drops over the (static) eye and lifts again = one blink.
         The eye and pupils never move — only the lid. */
      @keyframes blink-second {
        0% {
          height: 0;
        }
        35% {
          height: 110%;
        }
        70% {
          height: 0;
        }
        100% {
          height: 0;
        }
      }
    </style>
  </head>

  <body>
    <div class="container">
      <div class="first">
        <div class="head-gear">
          <div class="paddles">
            <div class="paddle"></div>
            <div class="paddle reverse"></div>
          </div>
          <div class="button"></div>
        </div>
        <div class="head">
          <div class="face">
            <div class="eye"></div>
            <div class="eye"></div>
            <div class="nose"></div>
          </div>
          <div class="mouth"></div>
        </div>
        <div class="ear"></div>
        <div class="ear right"></div>
      </div>
      <div class="second">
        <div class="head">
          <div class="face">
            <div class="forehead"></div>
            <div class="brows"></div>
            <div class="eyes">
              <div class="eye"><div class="eyelid"></div></div>
              <div class="eye"><div class="eyelid"></div></div>
            </div>
            <div class="mouth">
              <div class="lips"></div>
            </div>
            <div class="nose"></div>
          </div>
        </div>
      </div>
    </div>

    <svg
      style="visibility: hidden; position: absolute"
      width="0"
      height="0"
      xmlns="http://www.w3.org/2000/svg"
      version="1.1"
    >
      <defs>
        <filter id="goo">
          <feGaussianBlur in="SourceGraphic" stdDeviation="4" />
          <feColorMatrix
            in="blur"
            mode="matrix"
            values="1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 19 -9"
            result="goo"
          />
          <feComposite in="SourceGraphic" in2="goo" operator="atop" />
        </filter>
      </defs>
    </svg>

    <script>
      /* Make the first head a press-and-hold button.
           Press   -> propeller spins + eyes blink immediately (start at 0%).
           Release -> each animation finishes its CURRENT cycle, then stops. */
      (function () {
        const first = document.querySelector(".first");
        if (!first) return;

        let pressing = false;
        let stopSpin = false;
        let stopBlink = false;

        function press() {
          if (pressing) return;
          pressing = true;
          stopSpin = false;
          stopBlink = false;
          first.classList.add("spinning", "blinking");
          first.setAttribute("aria-pressed", "true");
        }

        function release() {
          if (!pressing) return;
          pressing = false;
          /* Don't stop now — let each running animation reach its next cycle
             boundary first (handled in the animationiteration listener below). */
          if (first.classList.contains("spinning")) stopSpin = true;
          if (first.classList.contains("blinking")) stopBlink = true;
          first.setAttribute("aria-pressed", "false");
        }

        /* Fires at the boundary between cycles. If a stop was requested and the
           user has let go, remove the animation here so it ends at rest:
           propeller at a full turn, eyelids open — never cut off mid-action. */
        first.addEventListener("animationiteration", function (e) {
          if (pressing) return;
          if (e.animationName === "rotate-fan" && stopSpin) {
            first.classList.remove("spinning");
            stopSpin = false;
          } else if (e.animationName === "close-eyes" && stopBlink) {
            first.classList.remove("blinking");
            stopBlink = false;
          }
        });

        /* Pointer (mouse + touch). Release is detected anywhere on the page, so
           letting go after dragging off the head still counts. */
        first.addEventListener("pointerdown", press);
        window.addEventListener("pointerup", release);
        window.addEventListener("pointercancel", release);

        /* Keyboard: hold Space/Enter, like a real button. */
        first.setAttribute("role", "button");
        first.setAttribute("tabindex", "0");
        first.setAttribute("aria-pressed", "false");
        first.addEventListener("keydown", function (e) {
          if (e.key === " " || e.key === "Enter") {
            e.preventDefault(); /* stop Space from scrolling the page */
            press();
          }
        });
        first.addEventListener("keyup", function (e) {
          if (e.key === " " || e.key === "Enter") {
            e.preventDefault();
            release();
          }
        });
      })();

      /* Second head: click (mouse/touch) or Space/Enter to blink the eyes and
         pop the eyebrows once. The class is reset on each trigger so rapid
         clicks replay the reaction. */
      (function () {
        const second = document.querySelector(".second");
        if (!second) return;

        function react() {
          second.classList.remove("reacting"); /* reset... */
          void second.offsetWidth; /* ...force reflow to restart... */
          second.classList.add("reacting"); /* ...then replay. */
        }

        /* Clear the class once the reaction finishes so it can run again. */
        second.addEventListener("animationend", function () {
          second.classList.remove("reacting");
        });

        second.addEventListener("click", react);

        second.setAttribute("role", "button");
        second.setAttribute("tabindex", "0");
        second.addEventListener("keydown", function (e) {
          if (e.repeat) return;
          if (e.key === " " || e.key === "Enter") {
            e.preventDefault();
            react();
          }
        });
      })();
    </script>
  </body>
</html>