variable titles exports fixed
This commit is contained in:
@@ -12,6 +12,15 @@
|
||||
|
||||
const SLUG_MAX_LENGTH = 60;
|
||||
const PNG_SCALE = 4;
|
||||
// Target cap for the PNG's largest side, in pixels — plenty for real
|
||||
// print use (e.g. ~25cm at 300 DPI) regardless of how long/tall the
|
||||
// source title is on screen. Scaling PNG_SCALE off the on-screen CSS
|
||||
// size alone made sense for a short title like the logo, but for a
|
||||
// long multi-line title in a huge font it produced needlessly massive
|
||||
// files (and could exceed the browser's own <canvas> size limit). The
|
||||
// scale is reduced — never increased — only when ×PNG_SCALE would
|
||||
// exceed this cap.
|
||||
const MAX_CANVAS_DIMENSION = 3000;
|
||||
|
||||
// U+0300-U+036F: combining diacritical marks left over after NFD
|
||||
// normalization (e.g. splitting "é" into "e" + a combining acute accent).
|
||||
@@ -67,26 +76,63 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the DOM exactly as currently rendered (positions, weights,
|
||||
* italics, line wraps, text-transform) and turns every `.char` into a
|
||||
* positioned glyph path. Nothing here recomputes layout — it only reads
|
||||
* what the browser already laid out at the moment of the click.
|
||||
* Reads the current CSS `transform` matrix on `el` and returns its scale
|
||||
* factor. `font-size` never reflects an ancestor `transform: scale()`
|
||||
* (this theme animates title scale on scroll, in initPageFull()), while
|
||||
* `getBoundingClientRect()` always does — without correcting for it,
|
||||
* glyphs render at their nominal font-size while being positioned from
|
||||
* transform-scaled coordinates, overflowing past the canvas once the
|
||||
* animation has shrunk the title.
|
||||
*
|
||||
* Deliberately NOT derived from comparing a character's measured
|
||||
* `getBoundingClientRect().width` to its HarfBuzz advance: this theme
|
||||
* also sets `letter-spacing` on every `.char`, which is included in the
|
||||
* measured width and would throw that comparison off per-character
|
||||
* (not just under an active transform) — reading the transform directly
|
||||
* sidesteps that entirely.
|
||||
*/
|
||||
// Fraction of the largest font size on the title, added as padding around
|
||||
// the whole canvas. Italic glyphs slant past their own nominal advance
|
||||
// box (most visibly at the top of ascenders), and the title's own
|
||||
// getBoundingClientRect() doesn't account for that overflow — a tightly
|
||||
// fitted viewBox clips them. This margin is a generous, font-agnostic
|
||||
// safety net rather than measuring each glyph's exact ink bounds.
|
||||
function getScaleFactor(el) {
|
||||
const transform = getComputedStyle(el).transform;
|
||||
if (!transform || transform === 'none') return 1;
|
||||
try {
|
||||
const matrix = new DOMMatrix(transform);
|
||||
// GSAP's `scale` produces a uniform matrix (a === d, b === c === 0);
|
||||
// averaging the two axes also covers non-uniform cases reasonably.
|
||||
return (Math.hypot(matrix.a, matrix.b) + Math.hypot(matrix.c, matrix.d)) / 2;
|
||||
}
|
||||
catch (error) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Fraction of the largest effective font size on the title, added as
|
||||
// padding around the whole canvas. Italic glyphs slant past their own
|
||||
// nominal advance box (most visibly at the top of ascenders), and the
|
||||
// title's own getBoundingClientRect() doesn't account for that overflow
|
||||
// — a tightly fitted viewBox clips them. This margin is a generous,
|
||||
// font-agnostic safety net rather than measuring each glyph's exact ink
|
||||
// bounds.
|
||||
const ITALIC_OVERFLOW_MARGIN_RATIO = 0.3;
|
||||
|
||||
async function buildSvg(titleEl, glyphFor, hb) {
|
||||
const containerRect = titleEl.getBoundingClientRect();
|
||||
const chars = Array.from(titleEl.querySelectorAll('.char'));
|
||||
const titleScale = getScaleFactor(titleEl);
|
||||
|
||||
// Canvas bounds are measured from the actual `.char` rects, not from
|
||||
// titleEl.getBoundingClientRect(): some titles in this theme are given
|
||||
// a `max-height` smaller than their wrapped content plus
|
||||
// `overflow: visible` (so nothing is visually clipped on the page),
|
||||
// and in that case the container's own rect reports the *constrained*
|
||||
// box, not the full extent of the overflowing lines — using it here
|
||||
// silently cropped multi-line titles.
|
||||
const seenWords = new Set();
|
||||
const glyphNodes = [];
|
||||
let maxFontSizePx = 0;
|
||||
const entries = [];
|
||||
let minLeft = Infinity;
|
||||
let minTop = Infinity;
|
||||
let maxRight = -Infinity;
|
||||
let maxBottom = -Infinity;
|
||||
let maxEffectiveFontSizePx = 0;
|
||||
|
||||
for (const charEl of chars) {
|
||||
const rawText = charEl.textContent;
|
||||
if (!rawText || !rawText.trim()) continue;
|
||||
@@ -95,28 +141,45 @@
|
||||
const computed = getComputedStyle(charEl);
|
||||
const fontSizePx = parseFloat(computed.fontSize);
|
||||
if (!fontSizePx) continue;
|
||||
maxFontSizePx = Math.max(maxFontSizePx, fontSizePx);
|
||||
|
||||
maxEffectiveFontSizePx = Math.max(maxEffectiveFontSizePx, fontSizePx * titleScale);
|
||||
minLeft = Math.min(minLeft, rect.left);
|
||||
minTop = Math.min(minTop, rect.top);
|
||||
maxRight = Math.max(maxRight, rect.right);
|
||||
maxBottom = Math.max(maxBottom, rect.bottom);
|
||||
|
||||
const text = applyTextTransform(rawText, computed.textTransform, charEl, seenWords);
|
||||
entries.push({ text, computed, rect, fontSizePx });
|
||||
}
|
||||
|
||||
if (!entries.length) {
|
||||
return { svg: '<svg xmlns="http://www.w3.org/2000/svg"></svg>', width: 0, height: 0 };
|
||||
}
|
||||
|
||||
const margin = maxEffectiveFontSizePx * ITALIC_OVERFLOW_MARGIN_RATIO;
|
||||
const glyphNodes = [];
|
||||
for (const { text, computed, rect, fontSizePx } of entries) {
|
||||
const { path, upem, ascender } = await glyphFor(hb, text, computed);
|
||||
if (!path) continue;
|
||||
|
||||
const scale = fontSizePx / upem;
|
||||
const x = rect.left - containerRect.left;
|
||||
const baselineY = (rect.top - containerRect.top) + (ascender / upem) * fontSizePx;
|
||||
const effectiveScale = (fontSizePx / upem) * titleScale;
|
||||
const x = (rect.left - minLeft) + margin;
|
||||
const baselineY = (rect.top - minTop) + ascender * effectiveScale + margin;
|
||||
|
||||
glyphNodes.push(
|
||||
`<g transform="translate(${x.toFixed(2)}, ${baselineY.toFixed(2)}) scale(${scale.toFixed(5)}, ${(-scale).toFixed(5)})"><path d="${path}"/></g>`,
|
||||
`<g transform="translate(${x.toFixed(2)}, ${baselineY.toFixed(2)}) scale(${effectiveScale.toFixed(5)}, ${(-effectiveScale).toFixed(5)})"><path d="${path}"/></g>`,
|
||||
);
|
||||
}
|
||||
|
||||
const margin = maxFontSizePx * ITALIC_OVERFLOW_MARGIN_RATIO;
|
||||
const width = containerRect.width + margin * 2;
|
||||
const height = containerRect.height + margin * 2;
|
||||
const width = (maxRight - minLeft) + margin * 2;
|
||||
const height = (maxBottom - minTop) + margin * 2;
|
||||
const svg = [
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width.toFixed(2)} ${height.toFixed(2)}" width="${width.toFixed(2)}" height="${height.toFixed(2)}">`,
|
||||
`<g fill="#000000" transform="translate(${margin.toFixed(2)}, ${margin.toFixed(2)})">`,
|
||||
// No extra translate here: `margin` is already baked into each
|
||||
// glyph's own x/baselineY above — adding it again here shifted
|
||||
// everything by 2×margin, eating into the right/bottom margin
|
||||
// entirely while doubling it on the left/top.
|
||||
'<g fill="#000000">',
|
||||
...glyphNodes,
|
||||
'</g>',
|
||||
'</svg>',
|
||||
@@ -125,7 +188,13 @@
|
||||
return { svg, width, height };
|
||||
}
|
||||
|
||||
async function rasterizeToPng(svgString, width, height, scale) {
|
||||
async function rasterizeToPng(svgString, width, height, targetScale) {
|
||||
const scale = Math.min(
|
||||
targetScale,
|
||||
MAX_CANVAS_DIMENSION / width,
|
||||
MAX_CANVAS_DIMENSION / height,
|
||||
);
|
||||
|
||||
const svgBlob = new Blob([svgString], { type: 'image/svg+xml' });
|
||||
const url = URL.createObjectURL(svgBlob);
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user