HTML5 Canvas 在线签字功能实现
我将创建一个完整的在线签字应用,包含画布签字、颜色选择、线条粗细调整、清除、撤销、保存和下载功能。
思路分析
使用HTML5 Canvas作为绘制区域
监听鼠标/触摸事件捕获用户手势
实现平滑的线条绘制
添加工具控制:颜色、粗细、清除、撤销
提供签名保存功能
下面是完整实现代码:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HTML5 Canvas 在线签字板</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
}
body {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
max-width: 1000px;
width: 100%;
background-color: white;
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
header {
background: linear-gradient(90deg, #2c3e50, #4a6491);
color: white;
padding: 20px 30px;
text-align: center;
}
h1 {
font-size: 28px;
margin-bottom: 8px;
letter-spacing: 1px;
}
.subtitle {
font-size: 16px;
opacity: 0.85;
font-weight: 300;
}
.main-content {
display: flex;
flex-wrap: wrap;
padding: 25px;
gap: 25px;
}
.canvas-container {
flex: 1;
min-width: 300px;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
background-color: #f8f9fa;
border: 1px solid #eaeaea;
}
#signatureCanvas {
width: 100%;
height: 400px;
cursor: crosshair;
display: block;
background-color: white;
}
.tools-panel {
width: 280px;
display: flex;
flex-direction: column;
gap: 20px;
}
.panel {
background-color: #f8f9fa;
border-radius: 10px;
padding: 20px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
border: 1px solid #eaeaea;
}
.panel h3 {
color: #2c3e50;
margin-bottom: 16px;
padding-bottom: 10px;
border-bottom: 1px solid #e0e0e0;
font-size: 18px;
}
.color-options {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 10px;
}
.color-option {
width: 32px;
height: 32px;
border-radius: 50%;
cursor: pointer;
border: 3px solid transparent;
transition: transform 0.2s, border 0.2s;
}
.color-option:hover {
transform: scale(1.1);
}
.color-option.active {
border-color: #2c3e50;
transform: scale(1.1);
}
.brush-size {
margin-top: 15px;
}
.size-options {
display: flex;
gap: 12px;
align-items: center;
}
.size-option {
width: 24px;
height: 24px;
border-radius: 50%;
background-color: #2c3e50;
cursor: pointer;
transition: transform 0.2s;
}
.size-option:hover {
transform: scale(1.1);
}
.size-option.active {
outline: 2px solid #3498db;
outline-offset: 2px;
}
.size-option.small {
width: 16px;
height: 16px;
}
.size-option.medium {
width: 24px;
height: 24px;
}
.size-option.large {
width: 32px;
height: 32px;
}
.action-buttons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
button {
padding: 12px 16px;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
font-size: 15px;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.primary-btn {
background-color: #3498db;
color: white;
}
.primary-btn:hover {
background-color: #2980b9;
}
.secondary-btn {
background-color: #e0e0e0;
color: #333;
}
.secondary-btn:hover {
background-color: #d0d0d0;
}
.danger-btn {
background-color: #e74c3c;
color: white;
}
.danger-btn:hover {
background-color: #c0392b;
}
.success-btn {
background-color: #2ecc71;
color: white;
}
.success-btn:hover {
background-color: #27ae60;
}
.info-section {
margin-top: 10px;
font-size: 14px;
color: #666;
line-height: 1.5;
background-color: #f0f7ff;
padding: 15px;
border-radius: 8px;
border-left: 4px solid #3498db;
}
.signature-preview {
text-align: center;
margin-top: 20px;
padding: 15px;
border-top: 1px dashed #ddd;
}
#signatureImage {
max-width: 100%;
max-height: 120px;
margin-top: 10px;
border: 1px solid #eee;
border-radius: 4px;
display: none;
}
.mobile-notice {
display: none;
background-color: #fff9e6;
padding: 10px 15px;
border-radius: 8px;
margin-bottom: 15px;
font-size: 14px;
color: #8a6d3b;
border-left: 4px solid #f0ad4e;
}
@media (max-width: 768px) {
.main-content {
flex-direction: column;
}
.tools-panel {
width: 100%;
}
.mobile-notice {
display: block;
}
}
footer {
text-align: center;
padding: 20px;
color: #666;
font-size: 14px;
border-top: 1px solid #eee;
background-color: #f8f9fa;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>HTML5 Canvas 在线签字板</h1>
<div class="subtitle">支持鼠标/触摸绘制 • 可调整颜色和线条粗细 • 支持保存签名</div>
</header>
<div class="main-content">
<div class="canvas-container">
<div class="mobile-notice">
📱 在移动设备上,请用手指在下方区域直接绘制
</div>
<canvas id="signatureCanvas">
您的浏览器不支持HTML5 Canvas,请使用现代浏览器如Chrome、Firefox等。
</canvas>
</div>
<div class="tools-panel">
<div class="panel">
<h3>画笔颜色</h3>
<div class="color-options">
<div class="color-option active" style="background-color: #000000;" data-color="#000000"></div>
<div class="color-option" style="background-color: #e74c3c;" data-color="#e74c3c"></div>
<div class="color-option" style="background-color: #3498db;" data-color="#3498db"></div>
<div class="color-option" style="background-color: #2ecc71;" data-color="#2ecc71"></div>
<div class="color-option" style="background-color: #f39c12;" data-color="#f39c12"></div>
<div class="color-option" style="background-color: #9b59b6;" data-color="#9b59b6"></div>
<div class="color-option" style="background-color: #34495e;" data-color="#34495e"></div>
<div class="color-option" style="background-color: #e84393;" data-color="#e84393"></div>
</div>
<div class="brush-size">
<h3>线条粗细</h3>
<div class="size-options">
<div class="size-option small active" data-size="2"></div>
<div class="size-option medium" data-size="4"></div>
<div class="size-option large" data-size="6"></div>
</div>
</div>
</div>
<div class="panel">
<h3>操作工具</h3>
<div class="action-buttons">
<button id="clearBtn" class="danger-btn">
<span>🗑️</span> 清除
</button>
<button id="undoBtn" class="secondary-btn">
<span>↶</span> 撤销
</button>
<button id="saveBtn" class="success-btn">
<span>💾</span> 保存
</button>
<button id="downloadBtn" class="primary-btn">
<span>⬇️</span> 下载
</button>
</div>
<div class="info-section">
<p><strong>使用提示:</strong></p>
<p>1. 在画布区域拖动鼠标/手指进行签名</p>
<p>2. 点击"保存"可预览签名图像</p>
<p>3. 点击"下载"可将签名保存为PNG图片</p>
</div>
</div>
<div class="signature-preview">
<h3>签名预览</h3>
<img id="signatureImage" alt="签名预览">
<p id="previewText">签名保存后将在此显示预览</p>
</div>
</div>
</div>
<footer>
<p>© 2023 HTML5 Canvas 签字板 | 使用HTML5 Canvas API实现</p>
</footer>
</div>
<script>
// 获取Canvas元素和上下文
const canvas = document.getElementById('signatureCanvas');
const ctx = canvas.getContext('2d');
// 设置Canvas尺寸为容器尺寸
function resizeCanvas() {
const container = canvas.parentElement;
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
// 设置绘制样式
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.strokeStyle = '#000000';
ctx.lineWidth = 2;
// 绘制初始背景
drawBackground();
}
// 绘制网格背景
function drawBackground() {
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 绘制浅色网格
ctx.strokeStyle = '#f0f0f0';
ctx.lineWidth = 1;
const gridSize = 20;
// 垂直线
for (let x = 0; x < canvas.width; x += gridSize) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, canvas.height);
ctx.stroke();
}
// 水平线
for (let y = 0; y < canvas.height; y += gridSize) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(canvas.width, y);
ctx.stroke();
}
// 重置线条样式
ctx.strokeStyle = document.querySelector('.color-option.active').getAttribute('data-color');
ctx.lineWidth = parseInt(document.querySelector('.size-option.active').getAttribute('data-size'));
}
// 初始化变量
let isDrawing = false;
let lastX = 0;
let lastY = 0;
let drawingHistory = [];
let currentHistory = null;
// 开始绘制
function startDrawing(e) {
isDrawing = true;
[lastX, lastY] = getCoordinates(e);
// 开始新的绘制路径
currentHistory = {
points: [],
color: ctx.strokeStyle,
width: ctx.lineWidth
};
}
// 绘制中
function draw(e) {
if (!isDrawing) return;
e.preventDefault();
const [x, y] = getCoordinates(e);
// 添加点到当前绘制历史
currentHistory.points.push({x, y});
// 绘制线条
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(x, y);
ctx.stroke();
[lastX, lastY] = [x, y];
}
// 结束绘制
function stopDrawing() {
if (!isDrawing) return;
isDrawing = false;
// 保存当前绘制历史
if (currentHistory && currentHistory.points.length > 0) {
drawingHistory.push(currentHistory);
currentHistory = null;
}
}
// 获取坐标(支持触摸事件)
function getCoordinates(e) {
let x, y;
if (e.type.includes('touch')) {
const touch = e.touches[0] || e.changedTouches[0];
const rect = canvas.getBoundingClientRect();
x = touch.clientX - rect.left;
y = touch.clientY - rect.top;
} else {
x = e.offsetX;
y = e.offsetY;
}
return [x, y];
}
// 清除画布
function clearCanvas() {
if (confirm('确定要清除所有绘制内容吗?')) {
drawingHistory = [];
currentHistory = null;
drawBackground();
// 隐藏预览
document.getElementById('signatureImage').style.display = 'none';
document.getElementById('previewText').textContent = '签名保存后将在此显示预览';
}
}
// 撤销上一步
function undoLast() {
if (drawingHistory.length > 0) {
drawingHistory.pop();
// 重新绘制所有历史
drawBackground();
// 重新绘制历史记录
drawingHistory.forEach(history => {
ctx.strokeStyle = history.color;
ctx.lineWidth = history.width;
if (history.points.length > 0) {
ctx.beginPath();
ctx.moveTo(history.points[0].x, history.points[0].y);
for (let i = 1; i < history.points.length; i++) {
ctx.lineTo(history.points[i].x, history.points[i].y);
}
ctx.stroke();
}
});
// 恢复当前画笔设置
ctx.strokeStyle = document.querySelector('.color-option.active').getAttribute('data-color');
ctx.lineWidth = parseInt(document.querySelector('.size-option.active').getAttribute('data-size'));
// 更新预览
updatePreview();
} else {
alert('没有可撤销的操作');
}
}
// 保存签名(预览)
function saveSignature() {
if (drawingHistory.length === 0) {
alert('请先绘制签名');
return;
}
// 创建临时Canvas用于保存
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
// 设置临时Canvas尺寸
tempCanvas.width = canvas.width;
tempCanvas.height = canvas.height;
// 绘制白色背景
tempCtx.fillStyle = '#ffffff';
tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height);
// 复制原始Canvas内容
tempCtx.drawImage(canvas, 0, 0);
// 转换为数据URL
const dataURL = tempCanvas.toDataURL('image/png');
// 显示预览
const previewImg = document.getElementById('signatureImage');
previewImg.src = dataURL;
previewImg.style.display = 'block';
document.getElementById('previewText').textContent = '签名预览:';
// 保存到本地存储
try {
localStorage.setItem('signatureData', dataURL);
showMessage('签名已保存到本地存储', 'success');
} catch (e) {
console.warn('本地存储失败:', e);
showMessage('签名预览已生成,但本地存储失败', 'warning');
}
}
// 下载签名
function downloadSignature() {
if (drawingHistory.length === 0) {
alert('请先绘制签名');
return;
}
// 创建临时Canvas用于下载
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
// 设置临时Canvas尺寸
tempCanvas.width = canvas.width;
tempCanvas.height = canvas.height;
// 绘制白色背景
tempCtx.fillStyle = '#ffffff';
tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height);
// 复制原始Canvas内容
tempCtx.drawImage(canvas, 0, 0);
// 创建下载链接
const link = document.createElement('a');
link.download = `signature_${new Date().getTime()}.png`;
link.href = tempCanvas.toDataURL('image/png');
link.click();
showMessage('签名已下载', 'success');
}
// 显示临时消息
function showMessage(message, type) {
// 创建消息元素
const messageEl = document.createElement('div');
messageEl.textContent = message;
messageEl.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 12px 20px;
border-radius: 8px;
color: white;
font-weight: 600;
z-index: 1000;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
animation: slideIn 0.3s ease-out;
`;
// 根据类型设置颜色
if (type === 'success') {
messageEl.style.backgroundColor = '#2ecc71';
} else if (type === 'warning') {
messageEl.style.backgroundColor = '#f39c12';
} else {
messageEl.style.backgroundColor = '#3498db';
}
document.body.appendChild(messageEl);
// 3秒后移除消息
setTimeout(() => {
messageEl.style.animation = 'slideOut 0.3s ease-out';
setTimeout(() => {
document.body.removeChild(messageEl);
}, 300);
}, 3000);
}
// 更新预览
function updatePreview() {
if (drawingHistory.length === 0) {
document.getElementById('signatureImage').style.display = 'none';
document.getElementById('previewText').textContent = '签名保存后将在此显示预览';
}
}
// 初始化事件监听
function initEventListeners() {
// 鼠标事件
canvas.addEventListener('mousedown', startDrawing);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mouseup', stopDrawing);
canvas.addEventListener('mouseout', stopDrawing);
// 触摸事件(移动设备支持)
canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
startDrawing(e);
});
canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
draw(e);
});
canvas.addEventListener('touchend', (e) => {
e.preventDefault();
stopDrawing();
});
// 颜色选择
document.querySelectorAll('.color-option').forEach(option => {
option.addEventListener('click', function() {
document.querySelectorAll('.color-option').forEach(opt => {
opt.classList.remove('active');
});
this.classList.add('active');
ctx.strokeStyle = this.getAttribute('data-color');
});
});
// 线条粗细选择
document.querySelectorAll('.size-option').forEach(option => {
option.addEventListener('click', function() {
document.querySelectorAll('.size-option').forEach(opt => {
opt.classList.remove('active');
});
this.classList.add('active');
ctx.lineWidth = parseInt(this.getAttribute('data-size'));
});
});
// 按钮事件
document.getElementById('clearBtn').addEventListener('click', clearCanvas);
document.getElementById('undoBtn').addEventListener('click', undoLast);
document.getElementById('saveBtn').addEventListener('click', saveSignature);
document.getElementById('downloadBtn').addEventListener('click', downloadSignature);
// 窗口大小改变时调整Canvas尺寸
window.addEventListener('resize', resizeCanvas);
// 尝试从本地存储加载签名
try {
const savedSignature = localStorage.getItem('signatureData');
if (savedSignature) {
const img = new Image();
img.onload = function() {
ctx.drawImage(img, 0, 0);
// 由于无法解析历史,我们创建一个占位历史
drawingHistory.push({
points: [],
color: '#000000',
width: 2
});
// 显示预览
const previewImg = document.getElementById('signatureImage');
previewImg.src = savedSignature;
previewImg.style.display = 'block';
document.getElementById('previewText').textContent = '从本地存储加载的签名:';
};
img.src = savedSignature;
}
} catch (e) {
console.warn('无法从本地存储加载签名:', e);
}
}
// 初始化应用
function init() {
resizeCanvas();
initEventListeners();
// 添加CSS动画
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
`;
document.head.appendChild(style);
}
// 页面加载完成后初始化
window.addEventListener('load', init);
</script>
</body>
</html>
功能说明
绘制功能:
- 使用鼠标或手指在画布上绘制签名
- 支持平滑的线条绘制
自定义选项:
操作工具:
- 清除:清空整个画布
- 撤销:撤销上一步绘制操作
- 保存:保存签名并预览,同时存储到本地
- 下载:将签名下载为PNG图片
其他特性:
- 响应式设计,支持移动设备
- 自动保存签名到本地存储
- 从本地存储加载签名
- 美观的用户界面
- 操作提示和反馈
使用说明
在画布区域使用鼠标或手指绘制签名
选择画笔颜色和线条粗细
使用操作工具进行清除、撤销、保存或下载
签名会自动保存到浏览器本地存储,下次打开页面时会自动加载
这个实现使用了纯HTML5 Canvas API,没有依赖任何外部库,可以直接复制代码到HTML文件中运行。