效果图

1. 模块化的结构设计
最外层是按钮容器,定义了交互的整体范围;
内部嵌套 “内容区”,包含文字标签和一个 “场景容器”;
“场景容器” 里又细分出 “投币孔”“钱包”“硬币” 等子元素,每个元素都有明确的功能定位。
2. 语义化的标签逻辑
用<button>标签作为容器,明确这是一个可点击的交互元素,既符合网页标准,也能被屏幕阅读器识别;
通过aria-label属性标注按钮功能(“Leave a tip”),提升 accessibility 友好度;
用<span>和<div>区分不同层级的容器,让结构层次清晰可见。
1. 3D 视觉效果的构建
全局开启transform-style: preserve-3d,让所有元素都处于同一个 3D 空间,为后续的旋转、位移效果提供基础;
通过perspective属性设置透视距离,模拟人眼观察物体的视角,让硬币的旋转和按钮的倾斜更具真实感;
利用box-shadow和border-radius塑造按钮的立体轮廓,配合color-mix函数生成边框颜色,让按钮边缘既有层次感又不突兀。
2. 动画序列的精密控制
悬停动画:当鼠标移到按钮上时,钱包图标会沿 Y 轴 360 度旋转,这个动画通过transition属性控制,延迟 0.12 秒触发,既避免了误触导致的频繁动画,又增强了交互反馈;
点击反馈:点击瞬间按钮会向左倾斜一定角度,配合阴影变化模拟 “按压感”,这个动画的时长被精确控制在 0.26 秒,既明显可感知,又不会让用户觉得拖沓;
硬币轨迹:硬币弹出时的旋转、抛物线运动、落入投币孔的缩放效果,通过@keyframes定义关键帧,再配合transform属性的位移、旋转组合,让整个过程流畅自然。
1. 交互事件的精准响应
监听按钮的click事件,当用户点击时立即触发硬币弹出的动画序列;
通过变量记录按钮当前的状态(比如是否正在执行动画),避免用户连续点击导致的动画混乱;
结合addEventListener和removeEventListener,灵活控制事件的监听与移除,确保资源合理利用。
2. 动画参数的动态调控
根据点击力度(通过旋转角度模拟)动态调整硬币弹出的距离、速度和旋转次数 —— 点击力度越大,硬币飞得越远、旋转得越快;
利用物理引擎插件模拟重力效果,让硬币的抛物线轨迹符合现实世界的运动规律,下落时的加速度和弹跳幅度都经过精确计算;
提供 “速度”“力度” 等配置项,用户可以通过设置面板调整动画参数,让交互体验更个性化。
3. 多媒介的协同控制
控制音效的播放时机,当硬币落入投币孔时触发 “叮当” 声,强化操作的反馈感;
提供 “静音” 选项,允许用户关闭音效,兼顾不同场景的使用需求;
支持深色 / 浅色主题切换,通过修改data-theme属性触发 CSS 变量的更新,让按钮在不同主题下都保持良好的视觉效果。
<html lang="zh"><head><meta charset="UTF-8" /><title>一个创意打赏按钮</title><metaname="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 ;}: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"><svgrole="img"viewBox="0 0 24 24"xmlns="http://www.w3.org/2000/svg"><title>Webflow</title><pathd="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"><svgrole="img"viewBox="0 0 24 24"xmlns="http://www.w3.org/2000/svg"><title>Webflow</title><pathd="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>

