Skip to main content

如何使用 JavaScript Canvas 构建签名板

签名板是 Web 表单、合同和电子签署工作流中用于采集数字签名的常见 UI 组件。Konva 可以轻松帮你构建一个签名板,支持平滑线条绘制、可自定义的笔设置,以及一键导出为 PNG。

说明: 请在下方绘制你的签名。使用控件更改笔颜色、清除签名板,或将签名保存/导出为 PNG 图像。

import Konva from 'konva';

// --- UI 控件 ---
const controls = document.createElement('div');
controls.style.cssText = 'display:flex;gap:8px;align-items:center;margin-bottom:4px;flex-wrap:wrap;';

const colorLabel = document.createElement('label');
colorLabel.textContent = '笔颜色:';
const colorInput = document.createElement('input');
colorInput.type = 'color';
colorInput.value = '#000000';
colorLabel.appendChild(colorInput);

const widthLabel = document.createElement('label');
widthLabel.textContent = '宽度:';
const widthSelect = document.createElement('select');
[2, 3, 4, 5, 6].forEach((w) => {
const opt = document.createElement('option');
opt.value = w;
opt.textContent = w + 'px';
if (w === 3) opt.selected = true;
widthSelect.appendChild(opt);
});
widthLabel.appendChild(widthSelect);

const clearBtn = document.createElement('button');
clearBtn.textContent = '清除';
const saveBtn = document.createElement('button');
saveBtn.textContent = '另存为 PNG';

controls.appendChild(colorLabel);
controls.appendChild(widthLabel);
controls.appendChild(clearBtn);
controls.appendChild(saveBtn);
const container = document.getElementById('container');
container.parentNode.insertBefore(controls, container);

// --- 舞台设置 ---
const width = window.innerWidth;
const height = window.innerHeight - 40;

const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});

const bgLayer = new Konva.Layer();
stage.add(bgLayer);

// 底部签名线
const signLine = new Konva.Line({
points: [40, height - 60, width - 40, height - 60],
stroke: '#ccc',
strokeWidth: 1,
dash: [6, 4],
});
bgLayer.add(signLine);

const signLabel = new Konva.Text({
x: 40,
y: height - 50,
text: '在此签名',
fontSize: 14,
fill: '#aaa',
fontFamily: 'Arial',
});
bgLayer.add(signLabel);

const drawLayer = new Konva.Layer();
stage.add(drawLayer);

let isPaint = false;
let lastLine;
let lastPointerPosition;

stage.on('mousedown touchstart', function (e) {
isPaint = true;
lastPointerPosition = stage.getPointerPosition();
lastLine = new Konva.Line({
stroke: colorInput.value,
strokeWidth: parseInt(widthSelect.value),
lineCap: 'round',
lineJoin: 'round',
tension: 0.3,
points: [lastPointerPosition.x, lastPointerPosition.y],
});
drawLayer.add(lastLine);
});

stage.on('mouseup touchend', function () {
isPaint = false;
});

stage.on('mousemove touchmove', function (e) {
if (!isPaint) return;
e.evt.preventDefault();
const pos = stage.getPointerPosition();
const newPoints = lastLine.points().concat([pos.x, pos.y]);
lastLine.points(newPoints);
lastPointerPosition = pos;
});

clearBtn.addEventListener('click', function () {
drawLayer.destroyChildren();
});

saveBtn.addEventListener('click', function () {
// 为了干净导出,暂时隐藏背景元素
bgLayer.hide();
const dataURL = stage.toDataURL({ pixelRatio: 2 });
bgLayer.show();

const link = document.createElement('a');
link.download = 'signature.png';
link.href = dataURL;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});