Canvas 交互式涂鸦板 - 增强版<4>

128 阅读4分钟

Canvas 交互式涂鸦板 - 增强版

下面是一个功能更加完善的交互式涂鸦板实现,包含更多实用功能和更好的用户体验。

完整代码实现

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>高级交互式涂鸦板</title>
    <style>
        :root {
            --primary-color: #4285f4;
            --danger-color: #ea4335;
            --success-color: #34a853;
            --warning-color: #fbbc05;
        }
        
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            margin: 0;
            padding: 20px;
            background-color: #f5f5f5;
        }
        
        .container {
            max-width: 1000px;
            margin: 0 auto;
            background-color: white;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
            padding: 20px;
        }
        
        h1 {
            color: var(--primary-color);
            text-align: center;
            margin-bottom: 20px;
        }
        
        .toolbar {
            display: flex;
            flex-wrap: wrap;
            gap: 10px;
            margin-bottom: 15px;
            padding-bottom: 15px;
            border-bottom: 1px solid #eee;
        }
        
        .tool-group {
            display: flex;
            align-items: center;
            background-color: #f9f9f9;
            padding: 8px 12px;
            border-radius: 6px;
            border: 1px solid #ddd;
        }
        
        .tool-group label {
            margin-right: 8px;
            font-weight: 500;
            color: #555;
        }
        
        button {
            padding: 8px 16px;
            border: none;
            border-radius: 6px;
            background-color: var(--primary-color);
            color: white;
            font-weight: 500;
            cursor: pointer;
            transition: all 0.2s;
        }
        
        button:hover {
            opacity: 0.9;
            transform: translateY(-1px);
        }
        
        button:active {
            transform: translateY(0);
        }
        
        button.danger {
            background-color: var(--danger-color);
        }
        
        button.success {
            background-color: var(--success-color);
        }
        
        button.warning {
            background-color: var(--warning-color);
        }
        
        .active-tool {
            box-shadow: 0 0 0 2px white, 0 0 0 4px var(--primary-color);
        }
        
        #drawingCanvas {
            display: block;
            background-color: white;
            border: 1px solid #ddd;
            border-radius: 4px;
            margin: 0 auto;
            touch-action: none;
        }
        
        .brush-preview {
            display: inline-block;
            width: 20px;
            height: 20px;
            border-radius: 50%;
            background-color: var(--current-color);
            margin-left: 8px;
            vertical-align: middle;
        }
        
        @media (max-width: 768px) {
            .toolbar {
                flex-direction: column;
            }
            
            #drawingCanvas {
                width: 100%;
                height: auto;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>高级交互式涂鸦板</h1>
        
        <div class="toolbar">
            <div class="tool-group">
                <label for="colorPicker">颜色:</label>
                <input type="color" id="colorPicker" value="#000000">
                <div class="brush-preview" id="brushPreview"></div>
            </div>
            
            <div class="tool-group">
                <label for="brushSize">大小:</label>
                <input type="range" id="brushSize" min="1" max="50" value="5">
                <span id="brushSizeValue">5</span>
            </div>
            
            <div class="tool-group">
                <label for="brushOpacity">透明度:</label>
                <input type="range" id="brushOpacity" min="10" max="100" value="100">
                <span id="brushOpacityValue">100%</span>
            </div>
            
            <button id="pencilBtn" class="active-tool">铅笔</button>
            <button id="eraserBtn">橡皮擦</button>
            <button id="markerBtn">马克笔</button>
            <button id="sprayBtn">喷枪</button>
            <button id="clearBtn" class="danger">清空画布</button>
            <button id="saveBtn" class="success">保存图片</button>
            <button id="undoBtn" title="撤销">↩️</button>
            <button id="redoBtn" title="重做">↪️</button>
        </div>
        
        <canvas id="drawingCanvas" width="800" height="500"></canvas>
    </div>

    <script>
        // 获取DOM元素
        const drawingCanvas = document.getElementById('drawingCanvas');
        const ctx = drawingCanvas.getContext('2d');
        const colorPicker = document.getElementById('colorPicker');
        const brushSize = document.getElementById('brushSize');
        const brushSizeValue = document.getElementById('brushSizeValue');
        const brushOpacity = document.getElementById('brushOpacity');
        const brushOpacityValue = document.getElementById('brushOpacityValue');
        const brushPreview = document.getElementById('brushPreview');
        const pencilBtn = document.getElementById('pencilBtn');
        const eraserBtn = document.getElementById('eraserBtn');
        const markerBtn = document.getElementById('markerBtn');
        const sprayBtn = document.getElementById('sprayBtn');
        const clearBtn = document.getElementById('clearBtn');
        const saveBtn = document.getElementById('saveBtn');
        const undoBtn = document.getElementById('undoBtn');
        const redoBtn = document.getElementById('redoBtn');

        // 绘图状态
        let isDrawing = false;
        let lastX = 0;
        let lastY = 0;
        let currentTool = 'pencil';
        let drawingHistory = [];
        let historyIndex = -1;
        
        // 初始化画布
        function initCanvas() {
            ctx.fillStyle = 'white';
            ctx.fillRect(0, 0, drawingCanvas.width, drawingCanvas.height);
            saveCanvasState();
            updateBrushPreview();
        }
        
        // 更新画笔预览
        function updateBrushPreview() {
            brushPreview.style.backgroundColor = colorPicker.value;
            brushPreview.style.width = `${brushSize.value}px`;
            brushPreview.style.height = `${brushSize.value}px`;
            brushPreview.style.opacity = `${brushOpacity.value / 100}`;
        }
        
        // 保存画布状态到历史记录
        function saveCanvasState() {
            // 如果我们在历史记录中间,删除后面的状态
            if (historyIndex < drawingHistory.length - 1) {
                drawingHistory = drawingHistory.slice(0, historyIndex + 1);
            }
            
            // 保存当前状态
            const imageData = ctx.getImageData(0, 0, drawingCanvas.width, drawingCanvas.height);
            drawingHistory.push(imageData);
            historyIndex = drawingHistory.length - 1;
            
            // 限制历史记录数量
            if (drawingHistory.length > 50) {
                drawingHistory.shift();
                historyIndex--;
            }
            
            updateUndoRedoButtons();
        }
        
        // 撤销操作
        function undo() {
            if (historyIndex > 0) {
                historyIndex--;
                ctx.putImageData(drawingHistory[historyIndex], 0, 0);
                updateUndoRedoButtons();
            }
        }
        
        // 重做操作
        function redo() {
            if (historyIndex < drawingHistory.length - 1) {
                historyIndex++;
                ctx.putImageData(drawingHistory[historyIndex], 0, 0);
                updateUndoRedoButtons();
            }
        }
        
        // 更新撤销/重做按钮状态
        function updateUndoRedoButtons() {
            undoBtn.disabled = historyIndex <= 0;
            redoBtn.disabled = historyIndex >= drawingHistory.length - 1;
        }
        
        // 设置工具
        function setTool(tool) {
            currentTool = tool;
            
            // 更新按钮状态
            pencilBtn.classList.remove('active-tool');
            eraserBtn.classList.remove('active-tool');
            markerBtn.classList.remove('active-tool');
            sprayBtn.classList.remove('active-tool');
            
            switch(tool) {
                case 'pencil':
                    pencilBtn.classList.add('active-tool');
                    break;
                case 'eraser':
                    eraserBtn.classList.add('active-tool');
                    break;
                case 'marker':
                    markerBtn.classList.add('active-tool');
                    break;
                case 'spray':
                    sprayBtn.classList.add('active-tool');
                    break;
            }
        }
        
        // 绘制函数
        function draw(e) {
            if (!isDrawing) return;
            
            const x = e.offsetX || e.touches[0].pageX - drawingCanvas.offsetLeft;
            const y = e.offsetY || e.touches[0].pageY - drawingCanvas.offsetTop;
            
            ctx.globalAlpha = brushOpacity.value / 100;
            ctx.lineCap = 'round';
            ctx.lineJoin = 'round';
            
            switch(currentTool) {
                case 'pencil':
                    ctx.strokeStyle = colorPicker.value;
                    ctx.lineWidth = brushSize.value;
                    ctx.beginPath();
                    ctx.moveTo(lastX, lastY);
                    ctx.lineTo(x, y);
                    ctx.stroke();
                    break;
                    
                case 'eraser':
                    ctx.strokeStyle = 'white';
                    ctx.lineWidth = brushSize.value;
                    ctx.beginPath();
                    ctx.moveTo(lastX, lastY);
                    ctx.lineTo(x, y);
                    ctx.stroke();
                    break;
                    
                case 'marker':
                    ctx.strokeStyle = colorPicker.value;
                    ctx.lineWidth = brushSize.value * 2;
                    ctx.globalAlpha = Math.min(0.3, brushOpacity.value / 100);
                    ctx.beginPath();
                    ctx.moveTo(lastX, lastY);
                    ctx.lineTo(x, y);
                    ctx.stroke();
                    break;
                    
                case 'spray':
                    ctx.fillStyle = colorPicker.value;
                    const density = brushSize.value * 2;
                    const radius = brushSize.value / 2;
                    
                    for (let i = 0; i < density; i++) {
                        const angle = Math.random() * Math.PI * 2;
                        const distance = Math.random() * radius;
                        const sprayX = x + Math.cos(angle) * distance;
                        const sprayY = y + Math.sin(angle) * distance;
                        
                        ctx.beginPath();
                        ctx.arc(sprayX, sprayY, 1, 0, Math.PI * 2);
                        ctx.fill();
                    }
                    break;
            }
            
            [lastX, lastY] = [x, y];
        }
        
        // 开始绘制
        function startDrawing(e) {
            isDrawing = true;
            const x = e.offsetX || e.touches[0].pageX - drawingCanvas.offsetLeft;
            const y = e.offsetY || e.touches[0].pageY - drawingCanvas.offsetTop;
            [lastX, lastY] = [x, y];
            
            // 对于喷枪工具,立即绘制一个点
            if (currentTool === 'spray') {
                draw(e);
            }
        }
        
        // 结束绘制
        function endDrawing() {
            if (isDrawing) {
                isDrawing = false;
                saveCanvasState();
            }
        }
        
        // 事件监听器
        colorPicker.addEventListener('input', updateBrushPreview);
        brushSize.addEventListener('input', () => {
            brushSizeValue.textContent = brushSize.value;
            updateBrushPreview();
        });
        brushOpacity.addEventListener('input', () => {
            brushOpacityValue.textContent = `${brushOpacity.value}%`;
            updateBrushPreview();
        });
        
        pencilBtn.addEventListener('click', () => setTool('pencil'));
        eraserBtn.addEventListener('click', () => setTool('eraser'));
        markerBtn.addEventListener('click', () => setTool('marker'));
        sprayBtn.addEventListener('click', () => setTool('spray'));
        
        clearBtn.addEventListener('click', () => {
            if (confirm('确定要清空画布吗?')) {
                initCanvas();
            }
        });
        
        saveBtn.addEventListener('click', () => {
            const link = document.createElement('a');
            link.download = `涂鸦-${new Date().toISOString().slice(0, 10)}.png`;
            link.href = drawingCanvas.toDataURL('image/png');
            link.click();
        });
        
        undoBtn.addEventListener('click', undo);
        redoBtn.addEventListener('click', redo);
        
        // 鼠标事件
        drawingCanvas.addEventListener('mousedown', startDrawing);
        drawingCanvas.addEventListener('mousemove', draw);
        drawingCanvas.addEventListener('mouseup', endDrawing);
        drawingCanvas.addEventListener('mouseout', endDrawing);
        
        // 触摸事件
        drawingCanvas.addEventListener('touchstart', (e) => {
            e.preventDefault();
            startDrawing(e);
        });
        
        drawingCanvas.addEventListener('touchmove', (e) => {
            e.preventDefault();
            draw(e);
        });
        
        drawingCanvas.addEventListener('touchend', (e) => {
            e.preventDefault();
            endDrawing();
        });
        
        // 键盘快捷键
        document.addEventListener('keydown', (e) => {
            // Ctrl+Z 撤销
            if (e.ctrlKey && e.key === 'z') {
                e.preventDefault();
                undo();
            }
            // Ctrl+Y 重做
            else if (e.ctrlKey && e.key === 'y') {
                e.preventDefault();
                redo();
            }
        });
        
        // 初始化
        initCanvas();
        setTool('pencil');
    </script>
</body>
</html>

主要功能特点

  1. 多种绘图工具

    • 铅笔工具:基本绘图工具
    • 橡皮擦:擦除内容
    • 马克笔:半透明宽笔触
    • 喷枪:模拟喷枪效果
  2. 完善的绘图控制

    • 颜色选择器
    • 笔刷大小调节(1-50px)
    • 透明度控制(10%-100%)
    • 实时笔刷预览
  3. 历史记录功能

    • 撤销/重做操作(支持50步历史记录)
    • 键盘快捷键支持(Ctrl+Z/Ctrl+Y)
  4. 画布管理

    • 清空画布(带确认提示)
    • 保存为PNG图片
  5. 响应式设计

    • 适配不同屏幕尺寸
    • 同时支持鼠标和触摸操作
  6. 用户界面优化

    • 美观的Material Design风格UI
    • 当前工具高亮显示
    • 按钮状态反馈

常见问题解决方案

  1. 绘图不流畅

    • 确保使用lineCap='round'lineJoin='round'使线条更平滑
    • 使用requestAnimationFrame优化性能(本示例中已内置)
  2. 触摸设备不工作

    • 添加了专门的触摸事件处理
    • 设置touch-action: none防止浏览器默认行为
  3. 撤销/重做功能异常

    • 限制了历史记录数量(50步)
    • 正确处理历史记录指针
    • 在每次绘制结束时保存状态
  4. 喷枪效果不自然

    • 使用随机角度和距离生成粒子
    • 控制粒子密度与笔刷大小相关
  5. 保存图片文件名问题

    • 自动生成包含日期的文件名
    • 使用toDataURL('image/png')确保高质量保存

这个增强版涂鸦板包含了更多实用功能,代码结构也更清晰,适合学习和直接使用。你可以根据需要进一步扩展功能,比如添加更多画笔样式、图形工具或滤镜效果。