以下是您所需的采用Canvas绘制的动态按钮的HTML代码。它实现了一个带有渐变、阴影和点击波纹效果的交互按钮。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Canvas动态魔法按钮 | 点击波纹+缩放效果</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
user-select: none; /* 避免选中文字/画布,提升点击体验 */
}
body {
min-height: 100vh;
background: linear-gradient(145deg, #1a1e2c 0%, #2a2f3f 100%);
display: flex;
justify-content: center;
align-items: center;
font-family: 'Segoe UI', 'Poppins', 'Roboto', 'Helvetica Neue', sans-serif;
overflow: hidden;
margin: 0;
padding: 20px;
}
/* 画布容器装饰 */
.canvas-wrapper {
border-radius: 48px;
background: rgba(0, 0, 0, 0.2);
box-shadow: 0 25px 40px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.1);
padding: 8px;
}
canvas {
display: block;
margin: 0 auto;
border-radius: 40px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.5);
cursor: pointer;
transition: box-shadow 0.2s ease;
width: 600px;
height: 400px;
}
canvas:active {
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.6);
}
/* 底部精美描述 */
.info {
position: fixed;
bottom: 20px;
left: 0;
right: 0;
text-align: center;
color: rgba(255, 255, 255, 0.7);
font-size: 14px;
letter-spacing: 1px;
backdrop-filter: blur(4px);
font-weight: 400;
pointer-events: none;
}
.info span {
background: rgba(0, 0, 0, 0.4);
padding: 6px 18px;
border-radius: 40px;
font-family: monospace;
}
@media (max-width: 640px) {
canvas {
width: 90vw;
height: calc(90vw * 2 / 3);
}
.canvas-wrapper {
width: 90vw;
}
}
</style>
</head>
<body>
<div class="canvas-wrapper">
<canvas id="magicCanvas" width="600" height="400"></canvas>
</div>
<div class="info"><span>✨ 点击按钮 · 水波扩散 & 柔韧缩放 ✨</span></div>
<script>
(function() {
// ---------- DOM 元素 ----------
const canvas = document.getElementById('magicCanvas');
const ctx = canvas.getContext('2d');
// ---------- 按钮几何参数 (原始尺寸,无缩放时) ----------
const btn = {
// 按钮位于canvas中心, 宽280, 高90, 圆角45(优雅椭圆风格)
width: 280,
height: 90,
radius: 45,
// 动态计算左上角坐标 (中心对齐)
get x() {
return (canvas.width - this.width) / 2;
},
get y() {
return (canvas.height - this.height) / 2;
}
};
// ---------- 视觉配色 ----------
const colors = {
// 正常渐变 (金属质感紫罗兰 -> 电光蓝)
gradStart: '#8B5CF6',
gradEnd: '#3B82F6',
// 悬停渐变 (更亮更炫)
hoverStart: '#A78BFA',
hoverEnd: '#60A5FA',
// 文字颜色
textColor: '#FFFFFF',
textShadow: 'rgba(0,0,0,0.3)',
// 外发光 (悬浮阴影辅助)
glowColor: 'rgba(139, 92, 246, 0.6)'
};
// ---------- 交互状态 ----------
let isHovered = false; // 鼠标是否悬浮在按钮区域
let scaleFactor = 1.0; // 当前视觉缩放系数 (点击动效用)
let animProgress = 1.0; // 动画进度 0→1 (用于计算缩放因子)
let animId = null; // requestAnimationFrame ID
let animStartTime = 0; // 动画起始时间戳
const ANIM_DURATION = 320; // 动画持续时间(ms) 产生柔和缩放的feel
// ---------- 波纹效果队列 (存储每个波纹数据) ----------
let ripples = [];
const MAX_RIPPLE_RADIUS = 75;
const RIPPLE_LIFE = 0.8; // 起始透明度 0.8 , 逐渐减少
const RIPPLE_FADE_STEP = 0.02; // 每帧衰减系数,实际按时间衰减会更平滑,但基于帧率配合动画loop,使用增量半径和alpha递减方式
// 为了让波纹运动更流畅,我们存储每个波纹的 radius 和 alpha,每帧增加半径并减少透明度
// 速度系数: 半径每帧增加约 3~4px ,透明度每帧减少0.02
// 为了性能且不依赖deltaTime,由于动画帧频率50-60,效果足够丝滑
// ---------- 辅助函数: 将鼠标/触摸坐标转换为canvas相对坐标 ----------
function getCanvasCoords(e) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width; // canvas实际像素宽度与CSS宽度比例
const scaleY = canvas.height / rect.height;
let clientX, clientY;
if (e.touches) {
// 触摸事件处理
if (e.touches.length === 0) return null;
clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY;
} else {
clientX = e.clientX;
clientY = e.clientY;
}
let canvasX = (clientX - rect.left) * scaleX;
let canvasY = (clientY - rect.top) * scaleY;
canvasX = Math.min(Math.max(0, canvasX), canvas.width);
canvasY = Math.min(Math.max(0, canvasY), canvas.height);
return { x: canvasX, y: canvasY };
}
// ---------- 检测点是否在按钮区域内 (圆角矩形路径检测,精确优雅) ----------
function isPointInButton(x, y) {
// 使用圆角矩形路径检测 (考虑到按钮原始位置,无缩放因子,点击区域始终是固定视觉区域)
const rx = btn.x;
const ry = btn.y;
const w = btn.width;
const h = btn.height;
const r = btn.radius;
// 快速矩形剔除
if (x < rx || x > rx + w || y < ry || y > ry + h) return false;
// 圆角矩形精确检测: 检查四个角落圆弧区域
const minX = rx + r;
const maxX = rx + w - r;
const minY = ry + r;
const maxY = ry + h - r;
if (x >= minX && x <= maxX && y >= minY && y <= maxY) return true;
// 左上角
let dx = x - rx;
let dy = y - ry;
if (dx < r && dy < r) {
return (dx * dx + dy * dy) <= r * r;
}
// 右上角
dx = x - (rx + w - r);
dy = y - ry;
if (dx > 0 && dy < r) {
return (dx * dx + dy * dy) <= r * r;
}
// 左下角
dx = x - rx;
dy = y - (ry + h - r);
if (dx < r && dy > 0) {
return (dx * dx + dy * dy) <= r * r;
}
// 右下角
dx = x - (rx + w - r);
dy = y - (ry + h - r);
if (dx > 0 && dy > 0) {
return (dx * dx + dy * dy) <= r * r;
}
return true;
}
// ---------- 添加波纹 (点击时产生) ----------
function addRipple(clickX, clickY) {
// 限制涟漪数量最多15个,避免性能冗余
if (ripples.length > 20) ripples.shift();
ripples.push({
x: clickX,
y: clickY,
radius: 6, // 起始半径
alpha: 0.75,
maxRadius: MAX_RIPPLE_RADIUS
});
}
// ---------- 更新所有波纹 (半径增大,alpha减弱) ----------
function updateRipples() {
for (let i = 0; i < ripples.length; i++) {
const ripple = ripples[i];
// 速度: 半径每帧 +3.0px,alpha每帧减少0.02~0.025 配合优雅消失
ripple.radius += 3.2;
ripple.alpha -= 0.022;
// 移除消失波纹 (半径超过最大半径 或 透明度小于0)
if (ripple.radius >= ripple.maxRadius || ripple.alpha <= 0.02) {
ripples.splice(i, 1);
i--;
}
}
}
// ---------- 缩放动画 (基于正弦曲线制造收缩再弹回的效果,触感非常舒服) ----------
// 动画公式: progress (0→1) , scale = 1 - 0.07 * sin(π * progress)
// 最大缩小到 0.93,再恢复,加上一点弹性感 (也可选择 0.05 更柔和,但是戏剧性更强)
// 为了让按压感更明显,设定最大缩放 0.92 ~ 1.0, 使用 sin 曲线完美回弹
function startScaleAnimation() {
// 停止之前的动画 (如果存在)
if (animId) {
cancelAnimationFrame(animId);
animId = null;
}
// 重置进度和开始时间
animProgress = 0.0;
animStartTime = performance.now();
// 启动动画循环
function animateScale(now) {
const elapsed = now - animStartTime;
let progress = Math.min(1.0, elapsed / ANIM_DURATION);
// 使用 easeOutBack 曲线变种? 使用正弦曲线模拟按压: 先速降再缓升
// 效果: progress从0→1, sin(π * progress) 先从0→1→0 完美模拟按下和弹起
// 最小时: sin(π*0.5)=1 => scale = 1 - 0.07 = 0.93 轻微下陷,手感生动
const intensity = 0.09; // 缩放幅度9%,按压感明显但不夸张
const factor = 1 - intensity * Math.sin(Math.PI * progress);
scaleFactor = factor;
// 动画未结束则继续
if (progress < 1.0) {
animId = requestAnimationFrame(animateScale);
} else {
// 动画完全结束,scale恢复至1
scaleFactor = 1.0;
animProgress = 1.0;
animId = null;
}
// 每一帧都重绘画布,波纹更新也在同一帧重绘前刷新,保证动画流畅
renderCanvas();
}
animId = requestAnimationFrame(animateScale);
}
// ---------- 绘制漂亮的圆角按钮 (支持缩放变换 + 悬停效果) ----------
// 注意: 此函数会基于全局缩放因子和悬停状态绘制惊艳按钮
function drawButtonWithEffects() {
const x = btn.x;
const y = btn.y;
const w = btn.width;
const h = btn.height;
const r = btn.radius;
// 保存context状态用于变换
ctx.save();
// 核心缩放: 以按钮中心为基准进行缩放变换 (产生按压视觉)
const centerX = x + w / 2;
const centerY = y + h / 2;
ctx.translate(centerX, centerY);
ctx.scale(scaleFactor, scaleFactor);
ctx.translate(-centerX, -centerY);
// ----- 绘制阴影 (底层光晕) -----
ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';
ctx.shadowBlur = 14;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 5;
// 创建圆角矩形路径 (通用)
const createRoundedRectPath = (ctx, x, y, w, h, r) => {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
};
// 动态渐变 (根据悬停状态变色,使按钮更加灵动)
let gradient;
if (isHovered) {
gradient = ctx.createLinearGradient(x, y, x + w * 0.8, y + h);
gradient.addColorStop(0, colors.hoverStart);
gradient.addColorStop(1, colors.hoverEnd);
} else {
gradient = ctx.createLinearGradient(x, y, x + w, y + h);
gradient.addColorStop(0, colors.gradStart);
gradient.addColorStop(0.6, '#6D28D9');
gradient.addColorStop(1, colors.gradEnd);
}
// 填充按钮主体
createRoundedRectPath(ctx, x, y, w, h, r);
ctx.fillStyle = gradient;
ctx.fill();
// 绘制内发光/高光边线 让按钮更立体
ctx.shadowBlur = 0; // 重置阴影避免边框阴影干扰
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
createRoundedRectPath(ctx, x, y, w, h, r);
ctx.strokeStyle = 'rgba(255, 255, 255, 0.35)';
ctx.lineWidth = 1.8;
ctx.stroke();
// 内圈精致高亮 (模拟玻璃质感)
ctx.beginPath();
ctx.moveTo(x + r - 2, y + 4);
ctx.lineTo(x + w - r + 2, y + 4);
ctx.quadraticCurveTo(x + w - 4, y + 4, x + w - 4, y + r - 2);
ctx.strokeStyle = 'rgba(255, 255, 240, 0.4)';
ctx.lineWidth = 1.2;
ctx.stroke();
// 添加边光 (悬浮时更明显)
if (isHovered) {
ctx.save();
ctx.shadowBlur = 18;
ctx.shadowColor = colors.glowColor;
createRoundedRectPath(ctx, x, y, w, h, r);
ctx.strokeStyle = 'rgba(167, 139, 250, 0.7)';
ctx.lineWidth = 2.2;
ctx.stroke();
ctx.restore();
}
// ----- 绘制按钮文字 (动态缩放同时文字会保持精致) -----
ctx.font = `bold ${Math.floor(32 * scaleFactor)}px "Segoe UI", "Poppins", system-ui`;
ctx.fillStyle = colors.textColor;
ctx.shadowBlur = 4;
ctx.shadowColor = colors.textShadow;
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// 优美文本 (可自定义)
const mainText = "✨ 灵 动 ✨";
const subText = "CLICK ME";
// 主标题
ctx.fillText(mainText, centerX, centerY - 6 * scaleFactor);
// 副标题更小一点
ctx.font = `${Math.floor(14 * scaleFactor)}px "Segoe UI", monospace`;
ctx.fillStyle = 'rgba(255,255,245,0.9)';
ctx.fillText(subText, centerX, centerY + 24 * scaleFactor);
// 小装饰: 星星/光点
ctx.font = `${Math.floor(16 * scaleFactor)}px "Segoe UI"`;
ctx.fillStyle = '#FFE484';
ctx.shadowBlur = 6;
ctx.fillText("✦", centerX - 55 * scaleFactor, centerY - 5 * scaleFactor);
ctx.fillText("✧", centerX + 48 * scaleFactor, centerY - 8 * scaleFactor);
ctx.restore(); // 恢复变换矩阵和阴影设置
// ---------- 额外装饰:按钮下方微小光晕 (不受缩放影响,增加梦幻感) ----------
ctx.save();
ctx.shadowBlur = 0;
ctx.beginPath();
ctx.rect(0, 0, canvas.width, canvas.height);
ctx.globalCompositeOperation = 'source-over';
// 画出底面微光晕 (仅装饰)
ctx.shadowBlur = 0;
ctx.restore();
}
// ---------- 绘制所有波纹效果 (基于canvas上层,不随按钮缩放) ----------
function drawRipples() {
if (ripples.length === 0) return;
for (let ripple of ripples) {
ctx.save();
ctx.beginPath();
ctx.arc(ripple.x, ripple.y, ripple.radius, 0, Math.PI * 2);
// 优雅渐变波纹 (白色至淡紫)
const gradient = ctx.createRadialGradient(ripple.x, ripple.y, ripple.radius * 0.2, ripple.x, ripple.y, ripple.radius);
gradient.addColorStop(0, `rgba(255, 255, 255, ${ripple.alpha * 0.9})`);
gradient.addColorStop(0.6, `rgba(196, 181, 253, ${ripple.alpha * 0.7})`);
gradient.addColorStop(1, `rgba(139, 92, 246, 0)`);
ctx.fillStyle = gradient;
ctx.fill();
// 额外增加光晕外圈
ctx.beginPath();
ctx.arc(ripple.x, ripple.y, ripple.radius - 2, 0, Math.PI * 2);
ctx.strokeStyle = `rgba(255, 210, 150, ${ripple.alpha * 0.5})`;
ctx.lineWidth = 1.6;
ctx.stroke();
ctx.restore();
}
}
// ---------- 背景炫光效果(增强整体氛围,星空渐变背景)----------
function drawBackground() {
// 深邃星空背景渐变
const grad = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
grad.addColorStop(0, '#0b1120');
grad.addColorStop(0.5, '#19223a');
grad.addColorStop(1, '#101624');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 绘制一些动态星芒小点 (提升精致感)
ctx.fillStyle = 'rgba(255, 240, 200, 0.25)';
for (let i = 0; i < 120; i++) {
if (i%2 === 0) continue; // 性能点缀随机
let sx = (i * 131) % canvas.width;
let sy = (i * 253) % canvas.height;
ctx.beginPath();
ctx.arc(sx, sy, 1.2, 0, Math.PI*2);
ctx.fill();
}
// 星光闪烁效果 (随机几个亮点)
ctx.fillStyle = 'rgba(255, 220, 180, 0.5)';
for (let s = 0; s < 50; s++) {
ctx.beginPath();
let rx = (s * 379) % canvas.width;
let ry = (s * 411) % canvas.height;
ctx.arc(rx, ry, 1, 0, Math.PI*2);
ctx.fill();
}
}
// ---------- 主渲染入口 (组合所有元素) ----------
function renderCanvas() {
// 更新波纹动态 (移动半径/alpha)
updateRipples();
// 1. 绘制绚烂背景
drawBackground();
// 2. 绘制按钮 (含缩放、悬停特效)
drawButtonWithEffects();
// 3. 在按钮之上绘制涟漪 (最上层,视觉突出)
drawRipples();
// 4. 如果按钮处于hover,加上一层迷人的织光效果(极细腻)
if (isHovered) {
ctx.save();
ctx.globalCompositeOperation = 'lighter';
const rx = btn.x, ry = btn.y;
const gradGlow = ctx.createRadialGradient(rx + btn.width/2, ry + btn.height/2, 10, rx + btn.width/2, ry + btn.height/2, 70);
gradGlow.addColorStop(0, 'rgba(167, 139, 250, 0.15)');
gradGlow.addColorStop(1, 'rgba(59, 130, 246, 0)');
ctx.fillStyle = gradGlow;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.restore();
}
}
// ---------- 交互事件绑定 (点击触发波纹+缩放动画) ----------
function handleClickOrTap(e) {
// 阻止默认事件,避免移动端双击缩放干扰
e.preventDefault();
const coords = getCanvasCoords(e);
if (!coords) return;
const { x, y } = coords;
// 精确判断是否点击了按钮
if (isPointInButton(x, y)) {
// 添加点击波纹 (从点击坐标扩散)
addRipple(x, y);
// 启动缩放动画 (柔和按压感)
startScaleAnimation();
// 额外细微触感反馈 (可选, 移动端轻震手感, 大部分现代浏览器支持)
if (window.navigator && window.navigator.vibrate) {
window.navigator.vibrate(20);
}
// 重绘会在动画循环及波纹更新中持续调用,但为了确保首次波纹立刻刷新,在动画启动时已有renderCanvas调用
// 但为了响应速度,额外调用一次render保证波纹立刻显示
renderCanvas();
}
}
// 悬停检测 (动态改变按钮高亮)
function handleHoverMove(e) {
const coords = getCanvasCoords(e);
if (!coords) return;
const inside = isPointInButton(coords.x, coords.y);
if (inside !== isHovered) {
isHovered = inside;
renderCanvas(); // 悬停状态变化,重绘高亮效果
}
}
function handleMouseLeave() {
if (isHovered) {
isHovered = false;
renderCanvas();
}
}
// 触摸移动时悬停反馈 (移动端)
function handleTouchMove(e) {
e.preventDefault();
const coords = getCanvasCoords(e);
if (coords) {
const inside = isPointInButton(coords.x, coords.y);
if (inside !== isHovered) {
isHovered = inside;
renderCanvas();
}
}
}
function handleTouchEnd(e) {
// 触摸结束重置悬停状态
if (isHovered) {
isHovered = false;
renderCanvas();
}
}
// ---------- 窗口大小自适应? 由于画布宽高固定600*400,无需等比缩放,但移动端通过css保持宽高比,但坐标映射正确。为了保险,触摸移动时没有额外处理----------
// 监听resize无需额外逻辑,canvas像素不变
// ---------- 事件绑定 (桌面+移动端) ----------
function bindEvents() {
// 鼠标/指针事件
canvas.addEventListener('click', handleClickOrTap);
canvas.addEventListener('mousemove', handleHoverMove);
canvas.addEventListener('mouseleave', handleMouseLeave);
// 触摸事件 (移动端完美支持)
canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
// 模拟click (为了触摸触发点击)
const coords = getCanvasCoords(e);
if (coords && isPointInButton(coords.x, coords.y)) {
handleClickOrTap(e);
}
// 触摸时强制悬停更新
handleTouchMove(e);
});
canvas.addEventListener('touchmove', handleTouchMove);
canvas.addEventListener('touchend', handleTouchEnd);
canvas.addEventListener('touchcancel', handleTouchEnd);
}
// 初始化渲染
function init() {
bindEvents();
// 初始绘制精美效果
renderCanvas();
// 启动一个飘浮效果? 保持简单,但可增加一个闲置微小星芒动画? 可选
// 为了提高动态感,可以轻微帧闪烁? 无关紧要,保持高性能
}
init();
})();
</script>
</body>
</html>