一个创意打赏按钮

Published on
/
/趣玩前端
<!DOCTYPE html>
<html lang="zh">
  <head>
    <meta charset="UTF-8" />
    <title>一个创意打赏按钮</title>
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, user-scalable=0, maximum-scale=1.0"
    />
    <style>
      @import url("https://unpkg.com/normalize.css") layer(normalize);
      @import url("https://fonts.googleapis.com/css2?family=Gloria+Hallelujah&family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap");
      @layer normalize, base, demo;

      @layer demo {
        :root {
          --ru: 15;
        }
        *,
        *::before,
        *::after {
          transform-style: preserve-3d;
        }

        .tp-lblv.tp-v-disabled .tp-lblv_l {
          opacity: 1 !important;
        }

        :root:has([aria-label]:active) .tp-txtv.tp-v-disabled {
          -webkit-clip-path: inset(0 0 0 0);
          clip-path: inset(0 0 0 0);
        }

        .tp-txtv.tp-v-disabled {
          height: 14.3px;
          background: repeating-linear-gradient(
            90deg,
            var(--lbl-fg) 0 3%,
            #0000 3% 5%
          );
          -webkit-clip-path: inset(0 100% 0 0);
          clip-path: inset(0 100% 0 0);
          transition: -webkit-clip-path 0.26s;
          transition: clip-path 0.26s;
          transition: clip-path 0.26s, -webkit-clip-path 0.26s;
          input {
            display: none;
          }
        }
        main {
          scale: 1.2;
          transform: translate3d(0, 0, 100vmax);
        }
        [aria-label] {
          touch-action: none;
          -webkit-user-select: none;
          -moz-user-select: none;
          -ms-user-select: none;
          user-select: none;
          -webkit-tap-highlight-color: #0000;
          --bg: #1871f4;
          background: var(--bg);
          border-radius: 6px;
          font-size: 0.875rem;
          color: #fff;
          font-family: inherit;
          border: 1px solid color-mix(in oklch, var(--bg), #000 12%);
          cursor: pointer;
          transform-origin: 75% 50%;
          transition: transform 0.26s, box-shadow 0.26s;
          padding: 0;
          --shadow-color: 0 0% 0%;
          box-shadow: 0px 0.6px 0.7px hsl(var(--shadow-color) / 0.14),
            0px 2.3px 2.6px -0.8px hsl(var(--shadow-color) / 0.14),
            0px 5.9px 6.6px -1.7px hsl(var(--shadow-color) / 0.14),
            0px 14.5px 16.3px -2.5px hsl(var(--shadow-color) / 0.14);

          .content {
            align-items: center;
            -webkit-clip-path: inset(-100vmax 0 1px 0);
            clip-path: inset(-100vmax 0 1px 0);
            display: flex;
            gap: 0.75rem;
            padding: 0.5rem 0.75rem;
            height: 100%;
          }

          &[data-tipping="false"]:active {
            transform: rotate(calc(var(--ru) * -1deg));
            box-shadow: -0.5px 0.7px 1px hsl(var(--shadow-color) / 0.14),
              -1.8px 2.3px 3.3px -0.8px hsl(var(--shadow-color) / 0.14),
              -4.6px 6px 8.5px -1.7px hsl(var(--shadow-color) / 0.14),
              -11.4px 14.6px 20.8px -2.5px hsl(var(--shadow-color) / 0.14);
          }

          &:is(:focus-visible, :hover) {
            --bg: color-mix(in oklch, #1871f4, #000 5%);
            .purse {
              rotate: y 360deg;
              transition: rotate 0.26s 0.12s ease-out;
            }
          }

          .purse {
            height: 100%;
            width: 100%;
            position: absolute;
            inset: 0;
            transform-style: preserve-3d;
          }

          .scene {
            --thickness: 4;
            display: inline-block;
            width: 1.2lh;
            aspect-ratio: 1;
            position: relative;
            transform-style: preserve-3d;
            perspective: 100vh;

            .hole {
              position: absolute;
              z-index: 10;
              inset: 0;
              scale: 0;
              transform-style: preserve-3d;
              transform: translate3d(0, 0, calc(var(--thickness) * -2px));
              transform-origin: 50% 70%;

              &::before {
                content: "";
                position: absolute;
                width: 125%;
                height: 40%;
                border-radius: 50%;
                top: 70%;
                left: 50%;
                translate: -50% -50%;
                background: black;
                box-shadow: 0 2px hsl(0 0% 20%) inset;
              }

              &::after {
                transform-style: preserve-3d;
                content: "";
                background: var(--bg);
                height: 200%;
                top: 0;
                left: 50%;
                translate: -50% 25%;
                width: 121%;
                position: absolute;
                transform: translate3d(0, 0, calc(var(--thickness) * 5px));
                -webkit-mask: radial-gradient(
                  125% 32% at 50% 3%,
                  rgba(0, 0, 0, 0) 50%,
                  #fff 50%
                );
                mask: radial-gradient(
                  125% 32% at 50% 3%,
                  rgba(0, 0, 0, 0) 50%,
                  #fff 50%
                );
              }
            }
          }
        }
        .coin {
          --depth: 2;
          --detail: hsl(43 97% 46%);
          --face: #ffdc02;
          --side: #f4ae00;
          width: 100%;
          aspect-ratio: 1;

          border-radius: 50%;
          position: absolute;
          translate: -50% -50%;
          top: 50%;
          left: 50%;
          transform-style: preserve-3d;

          .coin__core {
            height: 100%;
            width: calc(var(--depth) * 2px);
            background: var(--side);
            position: absolute;
            top: 50%;
            left: 50%;
            translate: -50% -50%;
            transform: rotateY(90deg) rotateX(calc((90 - var(--rx, 0)) * -1deg));
            transform-style: preserve-3d;

            &.coin__core--rotated {
              --base: 90;
              transform: rotateY(90deg)
                rotateX(calc((90 - var(--rx, 0)) * 1deg));
            }

            &::after,
            &::before {
              content: "";
              height: 100%;
              width: calc(var(--depth) * 2px);
              background: var(--side);
              position: absolute;
              inset: 0;
              transform-style: preserve-3d;
            }

            &::after {
              transform: rotateX(calc((var(--base, 0) - var(--rx, 0)) * 1deg));
            }
            &::before {
              transform: rotateX(calc((var(--base, 0) - var(--rx, 0)) * -1deg));
            }
          }

          .coin__face {
            height: 100%;
            width: 100%;
            position: absolute;
            inset: 0;
            border-radius: 50%;
            transform-style: preserve-3d;
            background: var(--face);
            display: grid;
            place-items: center;
            color: var(--detail);

            svg {
              width: 65%;
              scale: -1 1;
              translate: -5% 0;
            }

            &::after {
              content: "";
              position: absolute;
              inset: 0;
              border-radius: 50%;
              background: var(--side);
              -webkit-backface-visibility: hidden;
              backface-visibility: hidden;
            }

            &.coin__face--front {
              transform: translate3d(0, 0, calc((var(--depth) * 1px) + 0.5px))
                rotateY(180deg);
            }
            &.coin__face--rear {
              transform: translate3d(0, 0, calc((var(--depth) * -1px) - 0.5px));
            }
          }
        }
      }

      @layer base {
        :root {
          --font-size-min: 16;
          --font-size-max: 20;
          --font-ratio-min: 1.2;
          --font-ratio-max: 1.33;
          --font-width-min: 375;
          --font-width-max: 1500;
        }

        html {
          color-scheme: light dark;
        }

        [data-theme="light"] {
          color-scheme: light only;
        }

        [data-theme="dark"] {
          color-scheme: dark only;
        }

        :where(.fluid) {
          --fluid-min: calc(
            var(--font-size-min) *
              pow(var(--font-ratio-min), var(--font-level, 0))
          );
          --fluid-max: calc(
            var(--font-size-max) *
              pow(var(--font-ratio-max), var(--font-level, 0))
          );
          --fluid-preferred: calc(
            (var(--fluid-max) - var(--fluid-min)) /
              (var(--font-width-max) - var(--font-width-min))
          );
          --fluid-type: clamp(
            (var(--fluid-min) / 16) * 1rem,
            ((var(--fluid-min) / 16) * 1rem) -
              (((var(--fluid-preferred) * var(--font-width-min)) / 16) * 1rem) +
              (var(--fluid-preferred) * var(--variable-unit, 100vi)),
            (var(--fluid-max) / 16) * 1rem
          );
          font-size: var(--fluid-type);
        }

        *,
        *:after,
        *:before {
          box-sizing: border-box;
        }

        body {
          background: light-dark(#fff, #000);
          display: grid;
          overflow: hidden;
          place-items: center;
          min-height: 100vh;
          font-family: "SF Pro Text", "SF Pro Icons", "AOS Icons",
            "Helvetica Neue", Helvetica, Arial, sans-serif, system-ui;
        }
      }
    </style>
  </head>
  <body>
    <main>
      <button aria-label="Leave a tip" data-tipping="false">
        <span class="content">
          <span class="scene">
            <span class="hole"></span>
            <div class="purse">
              <div class="coin">
                <div class="coin__face coin__face--front">
                  <svg
                    role="img"
                    viewBox="0 0 24 24"
                    xmlns="http://www.w3.org/2000/svg"
                  >
                    <title>Webflow</title>
                    <path
                      d="m24 4.515-7.658 14.97H9.149l3.205-6.204h-.144C9.566 16.713 5.621 18.973 0 19.485v-6.118s3.596-.213 5.71-2.435H0V4.515h6.417v5.278l.144-.001 2.622-5.277h4.854v5.244h.144l2.72-5.244H24Z"
                      fill="currentColor"
                    />
                  </svg>
                </div>
                <div class="coin__core"></div>
                <div class="coin__core coin__core--rotated"></div>
                <div class="coin__face coin__face--rear">
                  <svg
                    role="img"
                    viewBox="0 0 24 24"
                    xmlns="http://www.w3.org/2000/svg"
                  >
                    <title>Webflow</title>
                    <path
                      d="m24 4.515-7.658 14.97H9.149l3.205-6.204h-.144C9.566 16.713 5.621 18.973 0 19.485v-6.118s3.596-.213 5.71-2.435H0V4.515h6.417v5.278l.144-.001 2.622-5.277h4.854v5.244h.144l2.72-5.244H24Z"
                      fill="currentColor"
                    />
                  </svg>
                </div>
              </div>
            </div>
          </span>
          <span>留下你的小费</span>
        </span>
      </button>
    </main>
    <script type="module">
      import gsap from "https://cdn.skypack.dev/gsap@3.13.0";
      import { Physics2DPlugin } from "https://cdn.skypack.dev/gsap@3.13.0/Physics2DPlugin";
      import { Pane } from "https://cdn.skypack.dev/tweakpane@4.0.4";

      gsap.registerPlugin(Physics2DPlugin);

      const button = document.querySelector('[aria-label="Leave a tip"]');
      const coin = button.querySelector(".coin");

      const config = {
        theme: "light",
        power: "",
        muted: true,
        timeScale: 1.1,
        distance: {
          lower: 100,
          upper: 350,
        },

        bounce: {
          lower: 2,
          upper: 12,
        },

        velocity: {
          lower: 300,
          upper: 700,
        },

        rotation: {
          lower: 0,
          upper: 15,
        },

        flipSpeed: {
          lower: 0.25,
          upper: 0.6,
        },

        spins: {
          lower: 1,
          upper: 6,
        },

        rotate: {
          lower: 0,
          upper: 90,
        },
      };

      const tipSound = new Audio(
        "https://myinstants.com/media/sounds/coin_1.mp3"
      );
      tipSound.volume = 0.3;
      tipSound.muted = config.muted;

      const tip = () => {
        if (button.dataset.tipping === "true") return;
        const currentRotation = gsap.getProperty(button, "rotate");
        if (currentRotation < 0)
          document.documentElement.dataset.flipped = "true";
        button.dataset.tipping = "true";
        const duration = gsap.utils.mapRange(
          config.rotation.lower,
          config.rotation.upper,
          0,
          config.flipSpeed.upper
        )(Math.abs(currentRotation));
        const distance = gsap.utils.snap(
          1,
          gsap.utils.mapRange(
            config.rotation.lower,
            config.rotation.upper,
            config.distance.lower,
            config.distance.upper
          )(Math.abs(currentRotation))
        );

        const velocity = gsap.utils.mapRange(
          config.rotation.lower,
          config.rotation.upper,
          config.velocity.lower,
          config.velocity.upper
        )(Math.abs(currentRotation));
        const bounce = gsap.utils.mapRange(
          config.velocity.lower,
          config.velocity.upper,
          config.bounce.lower,
          config.bounce.upper
        )(Math.abs(velocity));

        const distanceDuration = gsap.utils.mapRange(
          config.distance.lower,
          config.distance.upper,
          config.flipSpeed.lower,
          config.flipSpeed.upper
        )(distance);

        const spin = gsap.utils.snap(
          1,
          gsap.utils.mapRange(
            config.distance.lower,
            config.distance.upper,
            config.spins.lower,
            config.spins.upper
          )(distance)
        );

        const offRotate =
          gsap.utils.random(config.rotate.lower, config.rotate.upper, 1) * -1;
        const hangtime = Math.max(1, duration * 4);

        const tl = gsap
          .timeline({
            onComplete: () => {
              if (config.muted === false) {
                tipSound.muted = config.muted;
                tipSound.play();
              }
              gsap.set(coin, {
                yPercent: 100,
              });

              gsap
                .timeline({
                  onComplete: () => {
                    gsap.set(button, { clearProps: "all" });
                    gsap.set(coin, { clearProps: "all" });
                    gsap.set(".purse", { clearProps: "all" });
                    button.dataset.tipping = "false";
                  },
                })
                .to(button, {
                  yPercent: bounce,
                  repeat: 1,
                  duration: 0.12,
                  yoyo: true,
                })
                .fromTo(
                  ".hole",
                  {
                    scale: 1,
                  },

                  {
                    scale: 0,
                    duration: 0.2,
                    delay: 0.2,
                  }
                )
                .set(coin, {
                  clearProps: "all",
                })
                .set(coin, {
                  yPercent: -50,
                })
                .fromTo(
                  ".purse",
                  {
                    xPercent: -200,
                  },

                  {
                    delay: 0.5,
                    xPercent: 0,
                    duration: 0.5,
                    ease: "power1.out",
                  }
                )
                .fromTo(
                  coin,
                  {
                    rotate: -460,
                  },

                  {
                    rotate: 0,
                    duration: 0.5,
                    ease: "power1.out",
                  },

                  "<"
                )
                .timeScale(config.timeScale);
            },
          })
          .set(button, { transition: "none" })
          .fromTo(
            button,
            {
              rotate: currentRotation,
            },

            {
              rotate: 0,
              duration,
              ease: "elastic.out(1.75,0.75)",
            }
          )
          .to(
            coin,
            {
              onUpdate: function () {
                const y = gsap.getProperty(coin, "y");
                if (y >= coin.offsetHeight) {
                  this.progress(1);
                  tl.progress(1);
                }
              },
              duration: hangtime,
              physics2D: {
                velocity,
                angle: -90,
                gravity: 1000,
              },
            },

            `>-${duration * 0.825}`
          )
          .fromTo(
            coin,
            {
              rotateX: 0,
            },

            {
              duration: distanceDuration * 2,
              rotateX: spin * -360,
            },

            "<"
          )
          .to(
            coin,
            {
              rotateY: offRotate,
              duration: distanceDuration,
            },

            "<"
          )
          .to(
            coin,
            {
              "--rx": offRotate,
              duration: distanceDuration,
            },

            "<"
          )
          .fromTo(
            ".hole",
            {
              scale: 0,
            },

            {
              scale: 1,
              duration: 0.2,
            },

            hangtime * 0.35
          )
          .timeScale(config.timeScale);
      };
      button.addEventListener("click", tip);

      const ctrl = new Pane({
        title: "设置",
        expanded: true,
      });

      const update = () => {
        document.documentElement.dataset.theme = config.theme;
      };

      const sync = (event) => {
        if (
          !document.startViewTransition ||
          event.target.controller.view.labelElement.innerText !== "theme"
        )
          return update();
        document.startViewTransition(() => update());
      };

      ctrl.addBinding(config, "timeScale", {
        label: "速度",
        min: 0.1,
        max: 2,
        step: 0.1,
      });

      ctrl.addBinding(config, "muted", {
        label: "静音",
      });

      ctrl.addBinding(config, "power", {
        label: "力度",
        disabled: true,
      });

      ctrl.addBinding(config, "theme", {
        label: "主题",
        options: {
          System: "system",
          Light: "light",
          Dark: "dark",
        },
      });

      ctrl.on("change", sync);
      update();
    </script>
  </body>
</html>