跳到主要内容

如何使用 JavaScript Canvas 构建签名板

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

操作说明: 在下方绘制签名。使用控件更改画笔颜色、清空签名板,或将签名保存或导出为 PNG 图像。

import Konva from 'konva';

// --- UI Controls ---
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 = 'Pen Color: ';
const colorInput = document.createElement('input');
colorInput.type = 'color';
colorInput.value = '#000000';
colorLabel.appendChild(colorInput);

const widthLabel = document.createElement('label');
widthLabel.textContent = 'Width: ';
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 = 'Clear';
const saveBtn = document.createElement('button');
saveBtn.textContent = 'Save as PNG';

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

// --- Stage Setup ---
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);

// signature line at bottom
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: 'Sign here',
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 () {
// temporarily hide background elements for clean export
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);
});