Javascript提高:使用canvas绘制一个绚丽的按钮-由Deepseek产生

以下是您所需的采用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>
相关推荐
小四的小六1 小时前
WebView安全防护实战:从XSS到中间人攻击,我的踩坑与防御总结
javascript·webview
ZC跨境爬虫2 小时前
跟着 MDN 学 HTML day_41:(DOMParser 接口详解)
前端·javascript·ui·html·音视频
threelab2 小时前
Three.js 概率统计可视化 | 三维可视化 / AI 提示词
开发语言·javascript·人工智能
光影少年2 小时前
useLayoutEffect 和 useEffect 区别、使用场景
开发语言·前端·javascript
下雨打伞干嘛2 小时前
redux的使用
开发语言·javascript·ecmascript
small_white_robot2 小时前
idek-2022 web 全wp——持续更新
开发语言·前端·javascript·网络·安全·web安全·网络安全
sp422 小时前
NativeScript 5.1:直接集成 Objective-C 代码
前端·javascript
px不是xp3 小时前
【灶台导航】 RAG系统的容错设计:从向量搜索到关键词降级,一个都不能少
javascript·微信小程序·notepad++·rag
Sanri.3 小时前
JavaScript基础语法6
开发语言·javascript·ecmascript