sign-free.openapps.xyz

Source

This app’s code and revision history are public. Download a copy or explore the files below.

Public
The app’s code and revision history are public.
Owned app · rev 528326727848
Run it yourself
Take the whole tree and run it on your own machine. The download needs no account. Cloning needs a sign-in.
Download .zip
bounded clone 1da91c033c738ab74538d13d
A copy you download is not bounded. The guarantees this app runs under - declared egress, proven invariants, writes that fail closed - live in the runtime serving this page, not in the files. What you get is auditable source you can read and run, with no runtime guarantees attached.
← All files
src/SignaturePad.tsx
// Draw or type a signature; returns a trimmed transparent PNG data URL.
import { useEffect, useRef, useState } from "react";
import { Modal } from "./ui";

const INK = "#14244f";

function trimToPng(src: HTMLCanvasElement): string | null {
  const ctx = src.getContext("2d")!;
  const { width, height } = src;
  const data = ctx.getImageData(0, 0, width, height).data;
  let minX = width, minY = height, maxX = -1, maxY = -1;
  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
      if (data[(y * width + x) * 4 + 3] > 8) {
        if (x < minX) minX = x;
        if (x > maxX) maxX = x;
        if (y < minY) minY = y;
        if (y > maxY) maxY = y;
      }
    }
  }
  if (maxX < 0) return null;
  const pad = 6;
  const w = maxX - minX + 1 + pad * 2;
  const h = maxY - minY + 1 + pad * 2;
  const scale = Math.min(1, 900 / w);
  const out = document.createElement("canvas");
  out.width = Math.ceil(w * scale);
  out.height = Math.ceil(h * scale);
  const octx = out.getContext("2d")!;
  octx.scale(scale, scale);
  octx.drawImage(src, minX - pad, minY - pad, w, h, 0, 0, w, h);
  return out.toDataURL("image/png");
}

export function SignaturePad({ open, kind, name, onDone, onClose }: {
  open: boolean; kind: "signature" | "initials"; name: string; onDone: (png: string) => void; onClose: () => void;
}) {
  const initials = name.split(/\s+/).filter(Boolean).map((w) => w[0]?.toUpperCase()).join("").slice(0, 3);
  const [mode, setMode] = useState<"draw" | "type">("draw");
  const [typed, setTyped] = useState(kind === "initials" ? initials : name);
  const [empty, setEmpty] = useState(true);
  const canvas = useRef<HTMLCanvasElement>(null);
  const last = useRef<{ x: number; y: number } | null>(null);
  const drawing = useRef(false);

  useEffect(() => { if (open) setTyped(kind === "initials" ? initials : name); }, [open, kind]); // eslint-disable-line react-hooks/exhaustive-deps

  useEffect(() => {
    const c = canvas.current;
    if (!open || mode !== "draw" || !c) return;
    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    // Layout size, not the on-screen box: the sheet may still be sliding or scaling in.
    c.width = Math.round(c.offsetWidth * dpr);
    c.height = Math.round(c.offsetHeight * dpr);
    const ctx = c.getContext("2d")!;
    ctx.scale(dpr, dpr);
    ctx.lineCap = "round";
    ctx.lineJoin = "round";
    ctx.strokeStyle = INK;
    ctx.lineWidth = 2.4;
    setEmpty(true);
  }, [open, mode, kind]);

  const point = (e: React.PointerEvent) => {
    const c = canvas.current!;
    const r = c.getBoundingClientRect();
    return { x: (e.clientX - r.left) * (c.offsetWidth / r.width), y: (e.clientY - r.top) * (c.offsetHeight / r.height) };
  };
  const down = (e: React.PointerEvent) => {
    e.currentTarget.setPointerCapture(e.pointerId);
    drawing.current = true;
    last.current = point(e);
  };
  const move = (e: React.PointerEvent) => {
    if (!drawing.current || !last.current) return;
    const p = point(e);
    const ctx = canvas.current!.getContext("2d")!;
    const mid = { x: (last.current.x + p.x) / 2, y: (last.current.y + p.y) / 2 };
    ctx.beginPath();
    ctx.moveTo(last.current.x, last.current.y);
    ctx.quadraticCurveTo(last.current.x, last.current.y, mid.x, mid.y);
    ctx.lineTo(p.x, p.y);
    ctx.stroke();
    last.current = p;
    setEmpty(false);
  };
  const up = () => { drawing.current = false; last.current = null; };
  const clear = () => {
    const c = canvas.current!;
    c.getContext("2d")!.clearRect(0, 0, c.width, c.height);
    setEmpty(true);
  };

  const typedPng = () => {
    const c = document.createElement("canvas");
    const size = kind === "initials" ? 110 : 96;
    c.width = 1400;
    c.height = 220;
    const ctx = c.getContext("2d")!;
    ctx.fillStyle = INK;
    ctx.font = `italic 380 ${size}px "Newsreader Variable", Georgia, serif`;
    ctx.textBaseline = "middle";
    ctx.fillText(typed.trim(), 20, 115);
    return trimToPng(c);
  };

  const done = () => {
    const png = mode === "draw" ? trimToPng(canvas.current!) : typedPng();
    if (png) onDone(png);
  };

  const ready = mode === "draw" ? !empty : typed.trim().length > 0;
  return (
    <Modal open={open} title={kind === "initials" ? "Your initials" : "Your signature"} onClose={onClose} footer={<>
      <button className="btn btn-line" type="button" onClick={mode === "draw" ? clear : onClose} disabled={mode === "draw" && empty}>{mode === "draw" ? "Clear" : "Cancel"}</button>
      <button className="btn" type="button" onClick={done} disabled={!ready}>{kind === "initials" ? "Use these initials" : "Use this signature"}</button>
    </>}>
      <div className="stack">
        <div className="segmented segmented-2" role="radiogroup" aria-label="How to sign">
          <button type="button" role="radio" aria-checked={mode === "draw"} onClick={() => setMode("draw")}>Draw</button>
          <button type="button" role="radio" aria-checked={mode === "type"} onClick={() => setMode("type")}>Type</button>
        </div>
        {mode === "draw" ? (
          <div className="sigpad">
            <canvas ref={canvas} onPointerDown={down} onPointerMove={move} onPointerUp={up} onPointerCancel={up} onPointerLeave={up} aria-label="Draw your signature here" />
            <span className="sigpad-line" aria-hidden="true" />
            {empty && <span className="sigpad-hint"><span className="on-fine">Sign with your mouse or trackpad</span><span className="on-coarse">Sign with your finger</span></span>}
          </div>
        ) : (
          <>
            <label className="field"><span>{kind === "initials" ? "Initials" : "Full name"}</span>
              <input className="input" data-autofocus value={typed} onChange={(e) => setTyped(e.target.value)} maxLength={kind === "initials" ? 4 : 60} autoFocus />
            </label>
            <div className="sigpad sigpad-typed" aria-hidden="true"><span>{typed || " "}</span></div>
          </>
        )}
      </div>
    </Modal>
  );
}

Change history

Quality pass: phone signing, edit signers, copy to send again
5283267278 · 5 hours ago
Bounded source sync
15f0fa2e27 · 1 day ago
Bounded source sync
d3aa1ca65b · 1 day ago
Bounded source sync
8e2ca0b773 · 1 day ago
Bounded source sync
5cfa84715c · 1 day ago
Bounded source sync
c849e7b286 · 1 day ago
Bounded source sync
68c1cabe03 · 1 day ago
Bounded source sync
62ae8944f0 · 1 day ago
Bounded source sync
7ed54e7381 · 1 day ago