Разработка формы с подписью (Signature Field) на сайте
Поле для рукописной подписи используется в юридических документах, актах приёма-передачи, онлайн-договорах. Реализация выглядит просто, но нюансов много: разные устройства, сохранение в разных форматах, юридическая значимость.
Сценарии применения
- Подписание договоров и соглашений прямо в браузере
- Акты выполненных работ в CRM/ERP
- Согласие на обработку персональных данных с доказательной базой
- Медицинские формы, страховые заявления
- Курьерские приложения (подтверждение доставки)
Как это работает технически
Подпись рисуется на элементе <canvas>. Обработчики событий отслеживают mousedown/mousemove/mouseup на десктопе и touchstart/touchmove/touchend на мобильных. Итоговое изображение экспортируется через canvas.toDataURL('image/png') или конвертируется в SVG-путь для масштабируемости.
class SignaturePad {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.points = [];
this.isDrawing = false;
this._setupEvents();
this._setupCanvas();
}
_setupCanvas() {
// HiDPI поддержка
const ratio = window.devicePixelRatio || 1;
const rect = this.canvas.getBoundingClientRect();
this.canvas.width = rect.width * ratio;
this.canvas.height = rect.height * ratio;
this.ctx.scale(ratio, ratio);
this.ctx.strokeStyle = '#1a1a1a';
this.ctx.lineWidth = 2;
this.ctx.lineCap = 'round';
this.ctx.lineJoin = 'round';
}
_getPos(e) {
const rect = this.canvas.getBoundingClientRect();
const src = e.touches ? e.touches[0] : e;
return {
x: src.clientX - rect.left,
y: src.clientY - rect.top,
};
}
_setupEvents() {
this.canvas.addEventListener('mousedown', (e) => this._start(e));
this.canvas.addEventListener('mousemove', (e) => this._draw(e));
this.canvas.addEventListener('mouseup', () => this._end());
this.canvas.addEventListener('touchstart', (e) => { e.preventDefault(); this._start(e); }, { passive: false });
this.canvas.addEventListener('touchmove', (e) => { e.preventDefault(); this._draw(e); }, { passive: false });
this.canvas.addEventListener('touchend', () => this._end());
}
_start(e) {
this.isDrawing = true;
const pos = this._getPos(e);
this.ctx.beginPath();
this.ctx.moveTo(pos.x, pos.y);
this.points.push({ x: pos.x, y: pos.y, type: 'start' });
}
_draw(e) {
if (!this.isDrawing) return;
const pos = this._getPos(e);
this.ctx.lineTo(pos.x, pos.y);
this.ctx.stroke();
this.points.push({ x: pos.x, y: pos.y, type: 'draw' });
}
_end() {
this.isDrawing = false;
}
isEmpty() {
const data = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height).data;
return !data.some((v, i) => i % 4 === 3 && v > 0);
}
toDataURL(type = 'image/png') {
return this.canvas.toDataURL(type);
}
clear() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.points = [];
}
}
Готовые библиотеки
Если нет смысла писать с нуля — signature_pad от Szimek (MIT, ~4 KB gzip). Поддерживает сглаживание кривых Безье, экспорт в SVG, восстановление подписи из данных.
import SignaturePad from 'signature_pad';
const pad = new SignaturePad(document.getElementById('sig-canvas'), {
minWidth: 0.5,
maxWidth: 2.5,
penColor: '#000',
backgroundColor: 'rgba(0,0,0,0)', // прозрачный фон для PNG
});
// Сохранение
const dataURL = pad.toDataURL('image/svg+xml');
// Восстановление
pad.fromData(savedPointsArray);
Форматы хранения
| Формат | Плюсы | Минусы |
|---|---|---|
| PNG (base64) | Простота, совместимость | Большой размер, не масштабируется |
| SVG | Масштабируется, редактируем | Можно подделать |
| JSON (точки) | Полное восстановление, минимум данных | Требует рендеринг для отображения |
| PDF (встроенная) | Юридически значимо | Сложнее в реализации |
Для юридически значимых документов рекомендуется хранить JSON с точками + метаданные (timestamp, IP, user-agent, хэш документа).
Встраивание в PDF
Для генерации подписанных PDF используем pdf-lib (Node.js/браузер):
import { PDFDocument } from 'pdf-lib';
async function embedSignature(pdfBytes, signatureDataURL) {
const pdfDoc = await PDFDocument.load(pdfBytes);
const pages = pdfDoc.getPages();
const page = pages[pages.length - 1];
const pngBytes = await fetch(signatureDataURL).then(r => r.arrayBuffer());
const img = await pdfDoc.embedPng(pngBytes);
page.drawImage(img, {
x: 50,
y: 50,
width: 200,
height: 80,
});
return await pdfDoc.save();
}
Мобильные устройства
На тачскрине нужно блокировать scroll при рисовании (touch-action: none на canvas), иначе страница будет прокручиваться вместо рисования. Apple Pencil и стилусы работают через те же Touch Events, но touchForce доступна только в Safari.
Сроки
Базовое поле подписи с очисткой, валидацией и сохранением PNG — 2–3 рабочих дня. Интеграция с генерацией PDF, хранением метаданных и восстановлением подписи из базы — 4–6 дней.







