大数跨境
0
0

这个会 “跳硬币” 的打赏按钮

这个会 “跳硬币” 的打赏按钮 码途钥匙
2025-08-07
0
· 点击蓝字,关注我们

































效果图

01
一、HTML
HTML 在这里扮演的角色,就像戏剧演出前搭建舞台的布景师 —— 它不负责华丽的效果,却决定了所有元素的位置和层级关系,为后续的视觉设计和交互逻辑打下基础。

1. 模块化的结构设计

这个打赏按钮的 HTML 结构采用了 “组件化” 思路,每个视觉元素都是一个独立的模块:

最外层是按钮容器,定义了交互的整体范围;

内部嵌套 “内容区”,包含文字标签和一个 “场景容器”;

“场景容器” 里又细分出 “投币孔”“钱包”“硬币” 等子元素,每个元素都有明确的功能定位。

这种结构就像俄罗斯套娃,外层包含内层,内层又可以独立存在。好处是后续修改某个部分(比如换一个硬币图案)时,不会影响其他元素的布局,极大提升了代码的可维护性。

2. 语义化的标签逻辑

虽然是创意交互组件,HTML 依然保持了良好的语义化:

用<button>标签作为容器,明确这是一个可点击的交互元素,既符合网页标准,也能被屏幕阅读器识别;

通过aria-label属性标注按钮功能(“Leave a tip”),提升 accessibility 友好度;

用<span>和<div>区分不同层级的容器,让结构层次清晰可见。

这些细节看似微小,却能确保这个创意按钮在各种设备和辅助工具上都能正常工作,避免了 “好看但不好用” 的设计陷阱。
02
二、CSS
如果说 HTML 是骨架,那么 CSS 就是赋予这个按钮血肉和灵魂的关键。它不仅负责美化外观,更通过精妙的动画设计,让静态的元素 “动” 起来。

1. 3D 视觉效果的构建

这个按钮最吸引人的地方是强烈的立体感,这背后是 CSS 对 3D 空间的精准控制:

全局开启transform-style: preserve-3d,让所有元素都处于同一个 3D 空间,为后续的旋转、位移效果提供基础;

通过perspective属性设置透视距离,模拟人眼观察物体的视角,让硬币的旋转和按钮的倾斜更具真实感;

利用box-shadow和border-radius塑造按钮的立体轮廓,配合color-mix函数生成边框颜色,让按钮边缘既有层次感又不突兀。

尤其是硬币的设计,通过前后两个 “币面” 和中间的 “币芯” 组合,再加上不同角度的旋转变换,完美模拟出金属硬币的三维质感。

2. 动画序列的精密控制

按钮的交互动画看似一气呵成,其实是由多个独立动画序列组成的 “协奏曲”:

悬停动画:当鼠标移到按钮上时,钱包图标会沿 Y 轴 360 度旋转,这个动画通过transition属性控制,延迟 0.12 秒触发,既避免了误触导致的频繁动画,又增强了交互反馈;

点击反馈:点击瞬间按钮会向左倾斜一定角度,配合阴影变化模拟 “按压感”,这个动画的时长被精确控制在 0.26 秒,既明显可感知,又不会让用户觉得拖沓;

硬币轨迹:硬币弹出时的旋转、抛物线运动、落入投币孔的缩放效果,通过@keyframes定义关键帧,再配合transform属性的位移、旋转组合,让整个过程流畅自然。

这些动画不是孤立存在的,而是通过时间差和先后顺序形成连贯的叙事 —— 就像电影中的蒙太奇手法,通过不同镜头的剪辑组合,讲述一个完整的 “投币” 故事。
03
三、JavaScript
如果说 CSS 负责 “怎么动”,那么 JavaScript 就决定了 “何时动”“动多久”“动多快”。它就像乐队指挥,根据用户的操作实时调整动画节奏,让整个交互体验既有趣又可控。

1. 交互事件的精准响应

JavaScript 在这里最核心的作用,是建立用户操作与动画效果之间的连接:

监听按钮的click事件,当用户点击时立即触发硬币弹出的动画序列;

通过变量记录按钮当前的状态(比如是否正在执行动画),避免用户连续点击导致的动画混乱;

结合addEventListener和removeEventListener,灵活控制事件的监听与移除,确保资源合理利用。

这种 “事件驱动” 的逻辑,让按钮的交互始终跟随用户的操作节奏,避免了机械的自动播放,增强了用户的参与感。

2. 动画参数的动态调控

这个按钮的动画之所以看起来生动自然,还得益于 JavaScript 对动画参数的实时计算:

根据点击力度(通过旋转角度模拟)动态调整硬币弹出的距离、速度和旋转次数 —— 点击力度越大,硬币飞得越远、旋转得越快;

利用物理引擎插件模拟重力效果,让硬币的抛物线轨迹符合现实世界的运动规律,下落时的加速度和弹跳幅度都经过精确计算;

提供 “速度”“力度” 等配置项,用户可以通过设置面板调整动画参数,让交互体验更个性化。

这些动态计算让动画不再是固定的 “播放片段”,而是能根据用户行为实时变化的 “互动表演”,极大提升了交互的趣味性。

3. 多媒介的协同控制

JavaScript 还负责协调视觉之外的其他媒介元素,丰富交互的感官体验:

控制音效的播放时机,当硬币落入投币孔时触发 “叮当” 声,强化操作的反馈感;

提供 “静音” 选项,允许用户关闭音效,兼顾不同场景的使用需求;

支持深色 / 浅色主题切换,通过修改data-theme属性触发 CSS 变量的更新,让按钮在不同主题下都保持良好的视觉效果。

这种多媒介的协同,让交互从单纯的视觉体验升级为多感官的沉浸体验。
04
完整代码
<!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 {          --ru15;        }        *,        *::before,        *::after {          transform-style: preserve-3d;        }        .tp-lblv.tp-v-disabled .tp-lblv_l {          opacity1 !important;        }        :root:has([aria-label]:active.tp-txtv.tp-v-disabled {          -webkit-clip-pathinset(0 0 0 0);          clip-pathinset(0 0 0 0);        }        .tp-txtv.tp-v-disabled {          height14.3px;          backgroundrepeating-linear-gradient(            90deg,            var(--lbl-fg) 0 3%,            #0000 3% 5%          );          -webkit-clip-pathinset(0 100% 0 0);          clip-pathinset(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;          transformtranslate3d(00100vmax);        }        [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;          backgroundvar(--bg);          border-radius6px;          font-size0.875rem;          color#fff;          font-family: inherit;          border1px solid color-mix(in oklch, var(--bg), #000 12%);          cursor: pointer;          transform-origin75% 50%;          transition: transform 0.26s, box-shadow 0.26s;          padding0;          --shadow-color0 0% 0%;          box-shadow0px 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-pathinset(-100vmax 0 1px 0);            clip-pathinset(-100vmax 0 1px 0);            display: flex;            gap0.75rem;            padding0.5rem 0.75rem;            height100%;          }          &[data-tipping="false"]:active {            transformrotate(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) {            --bgcolor-mix(in oklch, #1871f4#000 5%);            .purse {              rotate: y 360deg;              transition: rotate 0.26s 0.12s ease-out;            }          }          .purse {            height100%;            width100%;            position: absolute;            inset: 0;            transform-style: preserve-3d;          }          .scene {            --thickness4;            display: inline-block;            width1.2lh;            aspect-ratio: 1;            position: relative;            transform-style: preserve-3d;            perspective100vh;            .hole {              position: absolute;              z-index10;              inset: 0;              scale: 0;              transform-style: preserve-3d;              transformtranslate3d(00calc(var(--thickness) * -2px));              transform-origin50% 70%;              &::before {                content"";                position: absolute;                width125%;                height40%;                border-radius50%;                top70%;                left50%;                translate: -50% -50%;                background: black;                box-shadow0 2px hsl(0 0% 20%) inset;              }              &::after {                transform-style: preserve-3d;                content"";                backgroundvar(--bg);                height200%;                top0;                left50%;                translate: -50% 25%;                width121%;                position: absolute;                transformtranslate3d(00calc(var(--thickness) * 5px));                -webkit-maskradial-gradient(                  125% 32% at 50% 3%,                  rgba(000050%,                  #fff 50%                );                maskradial-gradient(                  125% 32% at 50% 3%,                  rgba(000050%,                  #fff 50%                );              }            }          }        }        .coin {          --depth2;          --detailhsl(43 97% 46%);          --face#ffdc02;          --side#f4ae00;          width100%;          aspect-ratio: 1;          border-radius50%;          position: absolute;          translate: -50% -50%;          top50%;          left50%;          transform-style: preserve-3d;          .coin__core {            height100%;            widthcalc(var(--depth) * 2px);            backgroundvar(--side);            position: absolute;            top50%;            left50%;            translate: -50% -50%;            transformrotateY(90degrotateX(calc((90 - var(--rx, 0)) * -1deg));            transform-style: preserve-3d;            &.coin__core--rotated {              --base90;              transformrotateY(90deg)                rotateX(calc((90 - var(--rx, 0)) * 1deg));            }            &::after,            &::before {              content"";              height100%;              widthcalc(var(--depth) * 2px);              backgroundvar(--side);              position: absolute;              inset: 0;              transform-style: preserve-3d;            }            &::after {              transformrotateX(calc((var(--base, 0) - var(--rx, 0)) * 1deg));            }            &::before {              transformrotateX(calc((var(--base, 0) - var(--rx, 0)) * -1deg));            }          }          .coin__face {            height100%;            width100%;            position: absolute;            inset: 0;            border-radius50%;            transform-style: preserve-3d;            backgroundvar(--face);            display: grid;            place-items: center;            colorvar(--detail);            svg {              width65%;              scale: -1 1;              translate: -5% 0;            }            &::after {              content"";              position: absolute;              inset: 0;              border-radius50%;              backgroundvar(--side);              -webkit-backface-visibility: hidden;              backface-visibility: hidden;            }            &.coin__face--front {              transformtranslate3d(00calc((var(--depth) * 1px) + 0.5px))                rotateY(180deg);            }            &.coin__face--rear {              transformtranslate3d(00calc((var(--depth) * -1px) - 0.5px));            }          }        }      }      @layer base {        :root {          --font-size-min16;          --font-size-max20;          --font-ratio-min1.2;          --font-ratio-max1.33;          --font-width-min375;          --font-width-max1500;        }        html {          color-scheme: light dark;        }        [data-theme="light"] {          color-scheme: light only;        }        [data-theme="dark"] {          color-scheme: dark only;        }        :where(.fluid) {          --fluid-mincalc(            var(--font-size-min) *              pow(var(--font-ratio-min), var(--font-level, 0))          );          --fluid-maxcalc(            var(--font-size-max) *              pow(var(--font-ratio-max), var(--font-level, 0))          );          --fluid-preferredcalc(            (var(--fluid-max) - var(--fluid-min)) /              (var(--font-width-max) - var(--font-width-min))          );          --fluid-typeclamp(            (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-sizevar(--fluid-type);        }        *,        *:after,        *:before {          box-sizing: border-box;        }        body {          backgroundlight-dark(#fff#000);          display: grid;          overflow: hidden;          place-items: center;          min-height100vh;          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"",        mutedtrue,        timeScale1.1,        distance: {          lower100,          upper350,        },        bounce: {          lower2,          upper12,        },        velocity: {          lower300,          upper700,        },        rotation: {          lower0,          upper15,        },        flipSpeed: {          lower0.25,          upper0.6,        },        spins: {          lower1,          upper6,        },        rotate: {          lower0,          upper90,        },      };      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.upper1) * -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, {                yPercent100,              });              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,                  repeat1,                  duration0.12,                  yoyotrue,                })                .fromTo(                  ".hole",                  {                    scale1,                  },                  {                    scale0,                    duration0.2,                    delay0.2,                  }                )                .set(coin, {                  clearProps"all",                })                .set(coin, {                  yPercent: -50,                })                .fromTo(                  ".purse",                  {                    xPercent: -200,                  },                  {                    delay0.5,                    xPercent0,                    duration0.5,                    ease"power1.out",                  }                )                .fromTo(                  coin,                  {                    rotate: -460,                  },                  {                    rotate0,                    duration0.5,                    ease"power1.out",                  },                  "<"                )                .timeScale(config.timeScale);            },          })          .set(button, { transition"none" })          .fromTo(            button,            {              rotate: currentRotation,            },            {              rotate0,              duration,              ease"elastic.out(1.75,0.75)",            }          )          .to(            coin,            {              onUpdatefunction () {                const y = gsap.getProperty(coin, "y");                if (y >= coin.offsetHeight) {                  this.progress(1);                  tl.progress(1);                }              },              duration: hangtime,              physics2D: {                velocity,                angle: -90,                gravity1000,              },            },            `>-${duration * 0.825}`          )          .fromTo(            coin,            {              rotateX0,            },            {              duration: distanceDuration * 2,              rotateX: spin * -360,            },            "<"          )          .to(            coin,            {              rotateY: offRotate,              duration: distanceDuration,            },            "<"          )          .to(            coin,            {              "--rx": offRotate,              duration: distanceDuration,            },            "<"          )          .fromTo(            ".hole",            {              scale0,            },            {              scale1,              duration0.2,            },            hangtime * 0.35          )          .timeScale(config.timeScale);      };      button.addEventListener("click", tip);      const ctrl = new Pane({        title"设置",        expandedtrue,      });      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"速度",        min0.1,        max2,        step0.1,      });      ctrl.addBinding(config, "muted", {        label"静音",      });      ctrl.addBinding(config, "power", {        label"力度",        disabledtrue,      });      ctrl.addBinding(config, "theme", {        label"主题",        options: {          System"system",          Light"light",          Dark"dark",        },      });      ctrl.on("change", sync);      update();    </script>  </body></html>






点分享
点收藏
点在看
点点赞

【声明】内容源于网络
0
0
码途钥匙
欢迎来到 Python 学习乐园!这里充满活力,分享前沿实用知识技术。新手或开发者,都能找到价值。一起在这个平台,以 Python 为引,开启成长之旅,探索代码世界,共同进步。携手 Python,共赴精彩未来,快来加入我们吧!
内容 992
粉丝 0
码途钥匙 欢迎来到 Python 学习乐园!这里充满活力,分享前沿实用知识技术。新手或开发者,都能找到价值。一起在这个平台,以 Python 为引,开启成长之旅,探索代码世界,共同进步。携手 Python,共赴精彩未来,快来加入我们吧!
总阅读374
粉丝0
内容992