Files
gw-svelte/src/lib/components/OnboardingSignaturePad.svelte
T

200 lines
4.4 KiB
Svelte
Raw Normal View History

<script lang="ts">
import { onMount } from 'svelte';
export let value = '';
export let disabled = false;
let canvas: HTMLCanvasElement;
let isDrawing = false;
let hasSigned = false;
let activePointerId: number | null = null;
let lines: { x: number; y: number }[][] = [];
function resizeCanvas() {
if (!canvas) return;
const ratio = Math.max(window.devicePixelRatio || 1, 1);
const rect = canvas.getBoundingClientRect();
canvas.width = Math.max(1, Math.round(rect.width * ratio));
canvas.height = Math.max(1, Math.round(rect.height * ratio));
const ctx = canvas.getContext('2d');
if (!ctx) return;
ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
drawAllLines();
syncValue();
}
function getContext() {
return canvas?.getContext('2d') ?? null;
}
function drawAllLines() {
const ctx = getContext();
if (!ctx || !canvas) return;
const width = canvas.width / Math.max(window.devicePixelRatio || 1, 1);
const height = canvas.height / Math.max(window.devicePixelRatio || 1, 1);
ctx.clearRect(0, 0, width, height);
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.strokeStyle = '#213021';
ctx.lineWidth = 3;
for (const line of lines) {
if (!line.length) continue;
ctx.beginPath();
ctx.moveTo(line[0].x, line[0].y);
if (line.length === 1) {
ctx.lineTo(line[0].x + 0.01, line[0].y + 0.01);
} else {
for (const point of line.slice(1)) {
ctx.lineTo(point.x, point.y);
}
}
ctx.stroke();
}
}
function pointFromEvent(event: PointerEvent) {
const rect = canvas.getBoundingClientRect();
return {
x: event.clientX - rect.left,
y: event.clientY - rect.top
};
}
function syncValue() {
value = hasSigned && canvas ? canvas.toDataURL('image/png') : '';
}
function startDrawing(event: PointerEvent) {
if (disabled) return;
activePointerId = event.pointerId;
isDrawing = true;
canvas.setPointerCapture(event.pointerId);
const point = pointFromEvent(event);
lines = [...lines, [point]];
hasSigned = true;
drawAllLines();
syncValue();
}
function continueDrawing(event: PointerEvent) {
if (!isDrawing || disabled || activePointerId !== event.pointerId) return;
const point = pointFromEvent(event);
const nextLines = [...lines];
const currentLine = nextLines[nextLines.length - 1];
if (!currentLine) return;
currentLine.push(point);
lines = nextLines;
drawAllLines();
syncValue();
}
function stopDrawing(event?: PointerEvent) {
if (event && activePointerId === event.pointerId && canvas.hasPointerCapture(event.pointerId)) {
canvas.releasePointerCapture(event.pointerId);
}
activePointerId = null;
isDrawing = false;
syncValue();
}
export function clear() {
lines = [];
hasSigned = false;
drawAllLines();
syncValue();
}
onMount(() => {
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
return () => {
window.removeEventListener('resize', resizeCanvas);
};
});
</script>
<div class:signature-disabled={disabled} class="signature-shell">
<canvas
bind:this={canvas}
class="signature-canvas"
aria-label="Draw your signature"
on:pointerdown={startDrawing}
on:pointermove={continueDrawing}
on:pointerup={stopDrawing}
on:pointerleave={stopDrawing}
on:pointercancel={stopDrawing}
></canvas>
{#if !value}
<div class="signature-hint" aria-hidden="true">Sign here</div>
{/if}
</div>
<style>
.signature-shell {
position: relative;
width: 100%;
min-height: 180px;
border-radius: 18px;
background: #fff;
overflow: hidden;
}
.signature-shell.signature-disabled {
opacity: 0.7;
}
.signature-canvas {
display: block;
width: 100%;
height: 180px;
touch-action: none;
cursor: crosshair;
}
.signature-disabled .signature-canvas {
cursor: not-allowed;
}
.signature-hint {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-head);
font-size: 24px;
color: rgba(33, 48, 33, 0.22);
pointer-events: none;
}
@media (max-width: 768px) {
.signature-shell {
min-height: 160px;
}
.signature-canvas {
height: 160px;
}
.signature-hint {
font-size: 21px;
}
}
</style>