Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | 4x 4x 4x 4x 4x 4x 8x 8x 6x 6x 6x 6x 6x 6x 6x 1x 1x 6x 6x 6x 6x 6x 6x 6x 6x | import { dataURLtoFile } from "@utils/assets";
export const SIGNATURE_FONTS = [
{ id: 1, name: "Allison" },
{ id: 2, name: "MsMadi" },
{ id: 3, name: "Fuggles" },
{ id: 4, name: "ReenieBeanie" },
] as const;
export type SignatureFontName = (typeof SIGNATURE_FONTS)[number]["name"];
export const SIGNATURE_WIDTH = 400;
export const SIGNATURE_HEIGHT = 200;
export const SIGNATURE_FONT_SIZE = 40;
/**
* A signature plate is an artefact that gets stored, re-rendered on light
* surfaces, and embedded in the agreement PDF, so its colours are FIXED rather
* than taken from the active theme.
*
* This deliberately does not read `palette.background.paper` / `text.primary`
* as the original inline implementation did. `background.paper` is not defined
* in the v2 palette, so it fell through to the MUI default — meaning anyone
* signing in dark mode produced near-white text on a #121212 plate, which then
* rendered on a light agreement panel. Values below are the light-theme tokens.
*/
const INK = "#292928"; // tokens.light.colour.text.primary
const PLATE = "#ffffff";
/**
* Renders `name` as a signature PNG and returns it as an uploadable File.
*
* Fonts are declared in index.css via @font-face, but a declared face is not
* necessarily a *loaded* one, and measuring against an unloaded face silently
* falls back to a default font — which throws off the horizontal centring.
*
* `ensureFontLoaded` controls whether we wait for the face:
* - the interactive picker does NOT need it, because its suggestion tiles have
* already rendered the name in all four faces on screen, so the browser has
* loaded them before the user can click;
* - the auto-generate path DOES need it, because nothing has rendered that face
* beforehand.
* It defaults to false so callers never pay for a FontFaceSet round-trip (or a
* non-resolving one, as in jsdom) unless they actually need it.
*/
export async function generateSignatureFile(
name: string,
fontName: SignatureFontName,
fileName = "signature.png",
ensureFontLoaded = false,
): Promise<File | null> {
const text = name.trim();
if (!text) return null;
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
Iif (!context) return null;
canvas.width = SIGNATURE_WIDTH;
canvas.height = SIGNATURE_HEIGHT;
const font = `${SIGNATURE_FONT_SIZE}px ${fontName}`;
if (ensureFontLoaded) {
const fonts = (document as Document & { fonts?: FontFaceSet }).fonts;
Iif (fonts && typeof fonts.load === "function") {
// Bounded: a browser that never settles the FontFaceSet promise must not
// be able to stall signing. Rendering with a fallback face is a worse
// signature, but a stuck modal is a worse bug.
await Promise.race([
fonts.load(font, text).catch(() => undefined),
new Promise((resolve) => setTimeout(resolve, 500)),
]);
}
}
context.fillStyle = PLATE;
context.fillRect(0, 0, canvas.width, canvas.height);
context.fillStyle = INK;
context.font = font;
// The original set no textBaseline, so the default "alphabetic" put the
// baseline at the vertical midpoint and the glyphs sat in the upper half of
// the plate. "middle" centres the text box instead.
context.textBaseline = "middle";
const textWidth = context.measureText(text).width;
context.fillText(text, (canvas.width - textWidth) / 2, canvas.height / 2);
return dataURLtoFile(canvas.toDataURL("image/png"), fileName);
}
|