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-50px)
- 透明度控制(10%-100%)
- 实时笔刷预览
-
历史记录功能:
- 撤销/重做操作(支持50步历史记录)
- 键盘快捷键支持(Ctrl+Z/Ctrl+Y)
-
画布管理:
- 清空画布(带确认提示)
- 保存为PNG图片
-
响应式设计:
- 适配不同屏幕尺寸
- 同时支持鼠标和触摸操作
-
用户界面优化:
- 美观的Material Design风格UI
- 当前工具高亮显示
- 按钮状态反馈
常见问题解决方案
-
绘图不流畅:
- 确保使用
lineCap='round'和lineJoin='round'使线条更平滑 - 使用
requestAnimationFrame优化性能(本示例中已内置)
- 确保使用
-
触摸设备不工作:
- 添加了专门的触摸事件处理
- 设置
touch-action: none防止浏览器默认行为
-
撤销/重做功能异常:
- 限制了历史记录数量(50步)
- 正确处理历史记录指针
- 在每次绘制结束时保存状态
-
喷枪效果不自然:
- 使用随机角度和距离生成粒子
- 控制粒子密度与笔刷大小相关
-
保存图片文件名问题:
- 自动生成包含日期的文件名
- 使用
toDataURL('image/png')确保高质量保存
这个增强版涂鸦板包含了更多实用功能,代码结构也更清晰,适合学习和直接使用。你可以根据需要进一步扩展功能,比如添加更多画笔样式、图形工具或滤镜效果。