html
复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>手写签名</title>
<meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: #f5f5f5;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: #4299e1;
color: white;
padding: 20px;
text-align: center;
}
.header h1 {
font-size: 28px;
font-weight: 500;
}
#paper {
position: relative;
margin: 20px auto;
border: 1px solid #ccc;
background: #fff;
}
canvas {
position: absolute;
left: 0;
top: 0;
}
#guide {
z-index: 1;
}
#user {
z-index: 2;
touch-action: none;
}
.ctrl {
text-align: center;
margin: 18px 0;
}
button {
padding: 7px 16px;
margin: 0 6px;
font-size: 15px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>手写签名</h1>
</div>
<div id="paper">
<canvas id="guide"></canvas>
<canvas id="user"></canvas>
</div>
<div class="ctrl">
<button id="clearBtn">清空重写</button>
<button id="downBtn">下载保存</button>
</div>
</div>
<script>
// 配置
var CONFIG = {
chars: '海纳百川', // 范字字符串
cols: 4, // 每行格数
cellPx: 180, // 每格像素
lineW: 2, // 米字/边框线宽
guideColor: 'rgba(180,180,180,.5)', // 底稿颜色
userColor: '#000000', // 用户笔迹颜色
penWidth: 5, // 笔尖宽度
};
// 计算布局
var rows = Math.ceil(CONFIG.chars.length / CONFIG.cols);
var W = CONFIG.cellPx * CONFIG.cols;
var H = CONFIG.cellPx * rows;
document.getElementById('paper').style.width = W + 'px';
document.getElementById('paper').style.height = H + 'px';
// 底稿 canvas
var gCanvas = document.getElementById('guide');
var gCtx = gCanvas.getContext('2d');
gCanvas.width = W;
gCanvas.height = H;
// 用户 canvas
var uCanvas = document.getElementById('user');
var uCtx = uCanvas.getContext('2d');
uCanvas.width = W;
uCanvas.height = H;
// 画底稿(米字格+范字)
gCtx.lineWidth = CONFIG.lineW;
gCtx.strokeStyle = CONFIG.guideColor;
gCtx.font = `${CONFIG.cellPx * 0.75}px 楷体`;
gCtx.textAlign = 'center';
gCtx.textBaseline = 'middle';
for (var i = 0; i < CONFIG.chars.length; i++) {
var r = Math.floor(i / CONFIG.cols);
var c = i % CONFIG.cols;
var x = c * CONFIG.cellPx;
var y = r * CONFIG.cellPx;
var cx = x + CONFIG.cellPx / 2;
var cy = y + CONFIG.cellPx / 2;
// 米字格
gCtx.beginPath();
gCtx.rect(x, y, CONFIG.cellPx, CONFIG.cellPx); // 外框
gCtx.moveTo(x, y);
gCtx.lineTo(x + CONFIG.cellPx, y + CONFIG.cellPx); // 左斜
gCtx.moveTo(x + CONFIG.cellPx, y);
gCtx.lineTo(x, y + CONFIG.cellPx); // 右斜
gCtx.moveTo(cx, y);
gCtx.lineTo(cx, y + CONFIG.cellPx);// 竖
gCtx.moveTo(x, cy);
gCtx.lineTo(x + CONFIG.cellPx, cy);// 横
gCtx.stroke();
// 范字
gCtx.strokeText(CONFIG.chars[i], cx, cy);
}
// 手写层
uCtx.strokeStyle = CONFIG.userColor;
uCtx.lineWidth = CONFIG.penWidth;
uCtx.lineCap = uCtx.lineJoin = 'round';
var drawing = false, lastX, lastY;
function pos(e) {
var rect = uCanvas.getBoundingClientRect();
var x = (e.touches ? e.touches[0].clientX : e.clientX) - rect.left;
var y = (e.touches ? e.touches[0].clientY : e.clientY) - rect.top;
return [x, y];
}
function start(e) {
drawing = true;
[lastX, lastY] = pos(e);
uCtx.beginPath();
uCtx.moveTo(lastX, lastY);
}
function move(e) {
if (!drawing) return;
var [x, y] = pos(e);
uCtx.quadraticCurveTo(lastX, lastY, (lastX + x) / 2, (lastY + y) / 2);
uCtx.stroke();
[lastX, lastY] = [x, y];
}
function stop() {
drawing = false;
}
uCanvas.addEventListener('mousedown', start);
uCanvas.addEventListener('mousemove', move);
window.addEventListener('mouseup', stop);
uCanvas.addEventListener('touchstart', e => {
e.preventDefault();
start(e);
});
uCanvas.addEventListener('touchmove', e => {
e.preventDefault();
move(e);
});
uCanvas.addEventListener('touchend', e => {
e.preventDefault();
stop();
});
// 控制按钮
document.getElementById('clearBtn').onclick = () => uCtx.clearRect(0, 0, W, H);
document.getElementById('downBtn').onclick = () => {
var a = document.createElement('a');
a.download = `签名_${Date.now()}.png`;
a.href = uCanvas.toDataURL('image/png');
a.click();
};
</script>
</body>
</html>