Add signature pad and animated visor/cord canvas

Added signature-pad.js: a minimal, dependency-free signature pad with attach, clear, toDataUrl, and detach methods, supporting mouse and touch events and high-DPI displays. Updated error-space.js to draw a visor shape and animate a cord using bezier curves for a dynamic visual effect.
This commit is contained in:
2026-05-13 22:47:06 +02:00
parent 8ee3ae55d9
commit 4da21133a6
2 changed files with 191 additions and 0 deletions

View File

@@ -0,0 +1,77 @@
function drawVisor() {
const canvas = document.getElementById('visor');
const ctx = canvas.getContext('2d');
ctx.beginPath();
ctx.moveTo(5, 45);
ctx.bezierCurveTo(15, 64, 45, 64, 55, 45);
ctx.lineTo(55, 20);
ctx.bezierCurveTo(55, 15, 50, 10, 45, 10);
ctx.lineTo(15, 10);
ctx.bezierCurveTo(15, 10, 5, 10, 5, 20);
ctx.lineTo(5, 45);
ctx.fillStyle = '#2f3640';
ctx.strokeStyle = '#f5f6fa';
ctx.fill();
ctx.stroke();
}
const cordCanvas = document.getElementById('cord');
const ctx = cordCanvas.getContext('2d');
let y1 = 160;
let y2 = 100;
let y3 = 100;
let y1Forward = true;
let y2Forward = false;
let y3Forward = true;
function animate() {
requestAnimationFrame(animate);
ctx.clearRect(0, 0, innerWidth, innerHeight);
ctx.beginPath();
ctx.moveTo(130, 170);
ctx.bezierCurveTo(250, y1, 345, y2, 400, y3);
ctx.strokeStyle = 'white';
ctx.lineWidth = 8;
ctx.stroke();
if (y1 === 100) {
y1Forward = true;
}
if (y1 === 300) {
y1Forward = false;
}
if (y2 === 100) {
y2Forward = true;
}
if (y2 === 310) {
y2Forward = false;
}
if (y3 === 100) {
y3Forward = true;
}
if (y3 === 317) {
y3Forward = false;
}
y1Forward ? y1 += 1 : y1 -= 1;
y2Forward ? y2 += 1 : y2 -= 1;
y3Forward ? y3 += 1 : y3 -= 1;
}
drawVisor();
animate();

View File

@@ -0,0 +1,114 @@
// Minimal signature-pad implementation used by SignaturePadDialog.razor.
// Exposes window.signaturePad with attach() / clear() / toDataUrl() / detach().
//
// Why a hand-rolled implementation instead of a library?
// • Zero external dependencies (no npm / no bundler).
// • Works identically under InteractiveServer and InteractiveWebAssembly
// because the Blazor side only calls four trivial JS functions.
// • Supports both mouse and pointer/touch events.
window.signaturePad = (function () {
const instances = new Map();
function resize(canvas) {
// Backing store size must follow the DPR so the line stays crisp.
const ratio = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
canvas.width = Math.max(1, rect.width * ratio);
canvas.height = Math.max(1, rect.height * ratio);
const ctx = canvas.getContext('2d');
ctx.scale(ratio, ratio);
ctx.lineWidth = 2;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.strokeStyle = '#0d2540';
}
function attach(canvasId) {
const canvas = document.getElementById(canvasId);
if (!canvas) return false;
if (instances.has(canvasId)) detach(canvasId);
resize(canvas);
const ctx = canvas.getContext('2d');
let drawing = false;
let hasInk = false;
let last = { x: 0, y: 0 };
function pos(ev) {
const rect = canvas.getBoundingClientRect();
const clientX = ev.touches ? ev.touches[0].clientX : ev.clientX;
const clientY = ev.touches ? ev.touches[0].clientY : ev.clientY;
return { x: clientX - rect.left, y: clientY - rect.top };
}
function start(ev) {
ev.preventDefault();
drawing = true;
last = pos(ev);
}
function move(ev) {
if (!drawing) return;
ev.preventDefault();
const p = pos(ev);
ctx.beginPath();
ctx.moveTo(last.x, last.y);
ctx.lineTo(p.x, p.y);
ctx.stroke();
last = p;
hasInk = true;
}
function end() { drawing = false; }
const onResize = () => {
const data = canvas.toDataURL();
resize(canvas);
const img = new Image();
img.onload = () => ctx.drawImage(img, 0, 0, canvas.getBoundingClientRect().width, canvas.getBoundingClientRect().height);
img.src = data;
};
canvas.addEventListener('mousedown', start);
canvas.addEventListener('mousemove', move);
canvas.addEventListener('mouseup', end);
canvas.addEventListener('mouseleave', end);
canvas.addEventListener('touchstart', start, { passive: false });
canvas.addEventListener('touchmove', move, { passive: false });
canvas.addEventListener('touchend', end);
window.addEventListener('resize', onResize);
instances.set(canvasId, {
canvas, ctx,
hasInk: () => hasInk,
clear: () => { ctx.clearRect(0, 0, canvas.width, canvas.height); hasInk = false; },
toDataUrl: () => hasInk ? canvas.toDataURL('image/png') : null,
cleanup: () => {
canvas.removeEventListener('mousedown', start);
canvas.removeEventListener('mousemove', move);
canvas.removeEventListener('mouseup', end);
canvas.removeEventListener('mouseleave', end);
canvas.removeEventListener('touchstart', start);
canvas.removeEventListener('touchmove', move);
canvas.removeEventListener('touchend', end);
window.removeEventListener('resize', onResize);
}
});
return true;
}
function clear(canvasId) {
instances.get(canvasId)?.clear();
}
function toDataUrl(canvasId) {
return instances.get(canvasId)?.toDataUrl() ?? null;
}
function detach(canvasId) {
const inst = instances.get(canvasId);
if (inst) {
inst.cleanup();
instances.delete(canvasId);
}
}
return { attach, clear, toDataUrl, detach };
})();