Files
Voiced/src/pdf/decor/Asterisk.tsx
T
xavierk 4cea3fdd8e Add the Northline template family: Purple Pop and Citrus Split
Purple Pop (cream page, purple pills, asterisk and dome decor, opt-in
logo) and Citrus Split (grey split panel, halftone chevrons, icon
contacts, logo in the measured slot) are rebuilt from the reference PDFs on
the engine with A4/Letter support, GST slots and zero audit errors. Adds
static DM Sans, Jost and Poppins 800 as look-alikes for the commercial
fonts. The picker now lists ten templates.
2026-10-04 09:56:56 +05:30

79 lines
3.2 KiB
TypeScript

import { Path, Svg } from "@react-pdf/renderer";
import type { DecorItem } from "../blocks/FirstPageHeader";
import type { PageFrame } from "../engine/geometry";
/*
* Purple Pop's art, measured on the reference A4 page (docs/templates/12-purple-pop.md): an 8-arm asterisk normalised
* to a 0-100 box (every arm is 16.5% of the size), a yellow dome, and the huge pale watermark asterisks.
*/
export const ASTERISK_PATH =
"M100 41.7H69.9L91.1 20.6 79.7 8.9 58.3 30.1V0H41.7V30.1L20.6 8.9 8.9 20.6 30.1 41.7H0V58.3H30.1L8.9 79.4 20.6 91.1 41.7 69.9V100H58.3V69.9L79.4 91.1 91.1 79.4 69.9 58.3H100Z";
/** The yellow dome in A4 page coordinates; its box is DOME_BOX. */
export const DOME_PATH =
"M89.7,565.3 C107.7,565.3 122.3,579.9 122.3,597.9 L103.3,597.9 C114.5,603.1 122.3,614.4 122.3,627.6 L57.0,627.6 C57.0,614.4 64.8,603.1 76.1,597.9 L57.0,597.9 C57.0,579.9 71.7,565.3 89.7,565.3 Z";
export const DOME_BOX = { x: 57.0, y: 565.3, w: 65.3, h: 62.3 } as const;
/** A solid asterisk of side `size`, in the flow. */
export function Asterisk({ size, color }: { size: number; color: string }) {
return (
<Svg width={size} height={size} viewBox="0 0 100 100">
<Path d={ASTERISK_PATH} fill={color} />
</Svg>
);
}
/** The dome at its reference size scaled so it is `width` wide. */
export function Dome({ width, color }: { width: number; color: string }) {
const k = width / DOME_BOX.w;
return (
<Svg width={width} height={DOME_BOX.h * k} viewBox={`${DOME_BOX.x} ${DOME_BOX.y} ${DOME_BOX.w} ${DOME_BOX.h}`}>
<Path d={DOME_PATH} fill={color} fillRule="evenodd" />
</Svg>
);
}
/** The two pale watermarks: 352.4 pt asterisks, one hanging off the top right and one off the bottom left. */
export const WATERMARK = { size: 352.4, topRight: { overhangRight: 12.4, overhangTop: 51.4 }, bottomLeft: { overhangLeft: 58, overhangBottom: 21.55 } } as const;
export type WatermarkCorner = "top-right" | "bottom-left";
/**
* A watermark as a decor item anchored to its page corner. The part that falls off the page is cut away
* with the Svg's viewBox rather than drawn outside the page, so the box always lies on the page (on Letter
* too, where the corner moves with the page).
*/
export function asteriskWatermark(frame: PageFrame, corner: WatermarkCorner, color: string): DecorItem {
const { width: pw, height: ph } = frame.page;
const s = WATERMARK.size;
const pct = (v: number) => (v / s) * 100;
if (corner === "top-right") {
const w = s - WATERMARK.topRight.overhangRight;
const h = s - WATERMARK.topRight.overhangTop;
return {
x: pw - w,
y: 0,
width: w,
height: h,
children: (
<Svg width={w} height={h} viewBox={`${pct(WATERMARK.topRight.overhangRight)} ${pct(WATERMARK.topRight.overhangTop)} ${pct(w)} ${pct(h)}`}>
<Path d={ASTERISK_PATH} fill={color} />
</Svg>
),
};
}
const w = s - WATERMARK.bottomLeft.overhangLeft;
const h = s - WATERMARK.bottomLeft.overhangBottom;
return {
x: 0,
y: ph - h,
width: w,
height: h,
children: (
<Svg width={w} height={h} viewBox={`${pct(WATERMARK.bottomLeft.overhangLeft)} 0 ${pct(w)} ${pct(h)}`}>
<Path d={ASTERISK_PATH} fill={color} />
</Svg>
),
};
}