diff --git a/src/index.html b/src/index.html
index 6bf8b74..e360766 100644
--- a/src/index.html
+++ b/src/index.html
@@ -10651,7 +10651,13 @@
// exist).
thumb.style.background = p.background || book.spec?.background || '#ffffff';
const ctxToken = bookTokenContext(book, p.id, 0);
- renderBookDesignedPageToCanvas(p, 72, ctxToken).then(canvas => {
+ // 144 dpi instead of 72 — at typical grid widths (~150 px
+ // tile) the 72 dpi raster's text scaled below ~3 px, which
+ // was illegible. 144 dpi quadruples the pixel count
+ // (~16 ms → ~60 ms per tile) but text becomes readable in
+ // the thumb. Cache by content hash is the next perf step
+ // if grids get big.
+ renderBookDesignedPageToCanvas(p, 144, ctxToken).then(canvas => {
if (!thumb.isConnected) return;
const img = document.createElement('img');
img.src = canvas.toDataURL('image/png');
@@ -23332,12 +23338,17 @@
// breaks within a frame add `paragraphSpacing` inches of gap.
//
// Returns { perFrame: [{ lines: [...] }, ...], hasMoreText, ... }
- function _paginateParagraphsIntoFrames(paragraphs, chain, typography) {
+ function _paginateParagraphsIntoFrames(paragraphs, chain, fallbackTypography) {
+ // fallbackTypography is the head's typography — used when a
+ // mid-thread frame doesn't have its own. J4.d.2: each frame can
+ // override its own font, size, line-height, alignment, color,
+ // and paragraph gap. The paginator re-reads typography on every
+ // frame advance so size differences flow correctly.
const perFrame = chain.map(() => ({ lines: [], paragraphBreaksAfter: new Set() }));
const ctx = document.createElement('canvas').getContext('2d');
- ctx.font = `${typography.fontSize * 96 / 72}px ${typography.fontFamily}`;
- const lineHIn = (typography.fontSize * 96 / 72 / 96) * typography.lineHeight;
- const paragraphGapIn = Math.max(0, typography.paragraphSpacing);
+ const typoFor = (i) => _getFrameTypography(chain[i] || null) || fallbackTypography;
+ const lineHFor = (t) => (t.fontSize * 96 / 72 / 96) * t.lineHeight;
+ const setFontFor = (t) => { ctx.font = `${t.fontSize * 96 / 72}px ${t.fontFamily}`; };
let frameIdx = 0;
let yCursor = 0; // inches consumed in current frame
const advanceFrame = () => {
@@ -23346,11 +23357,16 @@
};
for (let pi = 0; pi < paragraphs.length; pi++) {
if (frameIdx >= chain.length) break;
- // Wrap paragraph into lines for the CURRENT frame's width.
+ // Wrap paragraph into lines for the CURRENT frame's width
+ // AND typography. If the paragraph spills into a frame with
+ // different typography mid-flow, the spilled lines were
+ // computed at the old width/font — minor visual artifact but
+ // they still place. (J4.d.3 would re-wrap on advance.)
const frame = chain[frameIdx];
const padding = Number(frame.shape?.padding) || 0;
const innerW = Math.max(0.1, (Number(frame.w) || 0) - 2 * padding);
- const innerH = Math.max(0.1, (Number(frame.h) || 0) - 2 * padding);
+ const startTypo = typoFor(frameIdx);
+ setFontFor(startTypo);
const words = paragraphs[pi].split(/\s+/).filter(Boolean);
let line = '';
const linesForPara = [];
@@ -23366,37 +23382,38 @@
if (line) linesForPara.push(line);
// Place lines into frames, advancing as each frame fills.
+ // Line height tracks the CURRENT frame's typography — when
+ // we cross a boundary into a frame with a different size,
+ // the per-line advance picks up the new height.
+ let curLineH = lineHFor(typoFor(frameIdx));
for (let li = 0; li < linesForPara.length; li++) {
if (frameIdx >= chain.length) break;
- let curFrame = chain[frameIdx];
- let curInnerH = Math.max(0.1, (Number(curFrame.h) || 0) - 2 * (Number(curFrame.shape?.padding) || 0));
- // Recompute line height for the current frame in case we
- // moved across a boundary where typography changes (J4.d.2;
- // today typography is uniform across the chain).
- if (yCursor + lineHIn > curInnerH && perFrame[frameIdx].lines.length > 0) {
+ const curFrame = chain[frameIdx];
+ const curInnerH = Math.max(0.1, (Number(curFrame.h) || 0) - 2 * (Number(curFrame.shape?.padding) || 0));
+ if (yCursor + curLineH > curInnerH && perFrame[frameIdx].lines.length > 0) {
advanceFrame();
if (frameIdx >= chain.length) break;
- // Re-wrap remaining lines if the new frame has a different
- // width — for now we keep the same wrap (same typography),
- // so the line just slots into the next frame.
+ curLineH = lineHFor(typoFor(frameIdx));
}
if (frameIdx >= chain.length) break;
perFrame[frameIdx].lines.push(linesForPara[li]);
- yCursor += lineHIn;
+ yCursor += curLineH;
}
- // Paragraph break after this paragraph — adds vertical
- // spacing in the current frame.
+ // Paragraph break after this paragraph — gap from the
+ // typography of the frame where the break lands.
if (pi < paragraphs.length - 1 && frameIdx < chain.length) {
perFrame[frameIdx].paragraphBreaksAfter.add(perFrame[frameIdx].lines.length - 1);
- yCursor += paragraphGapIn;
+ yCursor += Math.max(0, typoFor(frameIdx).paragraphSpacing);
}
}
- const consumed = paragraphs.length;
- // Compute whether any text didn't make it.
+ // Overset stats: probe against the head frame's typography
+ // + width. Approximation when typography varies per frame, but
+ // close enough for the "X of Y lines fit" warning.
let totalLinesPlaced = 0;
for (const pf of perFrame) totalLinesPlaced += pf.lines.length;
- // Re-wrap full text against head width for a hasMoreText probe.
const headFrame = chain[0];
+ const headTypo = typoFor(0);
+ setFontFor(headTypo);
const headInnerW = Math.max(0.1, (Number(headFrame?.w) || 0) - 2 * (Number(headFrame?.shape?.padding) || 0));
let totalLinesNeeded = 0;
for (const p of paragraphs) {
@@ -23838,12 +23855,12 @@
};
}
- // J4.d — typography controls. They edit the HEAD frame's
- // typography (the whole thread inherits via _getFrameTypography
- // in the paginator). On a mid-thread frame we still surface
- // the head's values but disable editing — keeps state singular
- // per thread.
- const typo = _getFrameTypography(head);
+ // J4.d.2 — typography is now per-frame. Each frame in the
+ // chain can override its own font / size / line-height /
+ // alignment / color / paragraph gap. The chain still reads
+ // the head's typography as a fallback for any field a frame
+ // doesn't set explicitly.
+ const typo = _getFrameTypography(layer);
const fontSel = document.getElementById('bookTextFrameFont');
if (fontSel && !fontSel.dataset.populated) {
// Reuse the existing font list the text tool uses.
@@ -23867,20 +23884,20 @@
const paraIn = document.getElementById('bookTextFrameParaSpace');
const alignSel = document.getElementById('bookTextFrameAlign');
const typoHint = document.getElementById('bookTextFrameTypoHint');
- const editable = isHead;
- if (fontSel) { fontSel.value = typo.fontFamily; fontSel.disabled = !editable; }
- if (sizeIn) { sizeIn.value = typo.fontSize; sizeIn.disabled = !editable; }
- if (colorIn) { colorIn.value = typo.color; colorIn.disabled = !editable; }
- if (lhIn) { lhIn.value = typo.lineHeight; lhIn.disabled = !editable; }
- if (paraIn) { paraIn.value = typo.paragraphSpacing; paraIn.disabled = !editable; }
- if (alignSel) { alignSel.value = typo.alignment; alignSel.disabled = !editable; }
- if (typoHint) typoHint.textContent = editable
- ? 'Applies to every frame in this thread.'
- : 'Edit typography on the head frame; mid-thread frames inherit.';
+ if (fontSel) { fontSel.value = typo.fontFamily; fontSel.disabled = false; }
+ if (sizeIn) { sizeIn.value = typo.fontSize; sizeIn.disabled = false; }
+ if (colorIn) { colorIn.value = typo.color; colorIn.disabled = false; }
+ if (lhIn) { lhIn.value = typo.lineHeight; lhIn.disabled = false; }
+ if (paraIn) { paraIn.value = typo.paragraphSpacing; paraIn.disabled = false; }
+ if (alignSel) { alignSel.value = typo.alignment; alignSel.disabled = false; }
+ if (typoHint) typoHint.textContent = isHead
+ ? 'Applies to this frame. The head\'s typography is also the chain\'s fallback.'
+ : 'Applies to this frame only — overrides the head\'s default.';
const writeTypo = (patch) => {
pushHistory('book: edit text-frame typography');
- head.typography = { ..._getFrameTypography(head), ...patch };
+ // Writes to THIS layer, not the head — per-frame override.
+ layer.typography = { ..._getFrameTypography(layer), ...patch };
render();
};
if (fontSel) fontSel.onchange = () => writeTypo({ fontFamily: fontSel.value });
@@ -24131,12 +24148,15 @@
// formatting via the inspector).
const entry = (state.book?.pages || []).find(p => p.id === head.sourceChapter);
const paragraphs = _flattenChapterToParagraphs(entry?.chapterMarkdown || '');
- const typography = _getFrameTypography(head); // head defines style for the thread
- const paginated = _paginateParagraphsIntoFrames(paragraphs, chain, typography);
+ // J4.d.2: head's typography is the chain's default; each
+ // frame paints with its OWN typography (overrides allowed).
+ const fallbackTypography = _getFrameTypography(head);
+ const localTypography = _getFrameTypography(layer);
+ const paginated = _paginateParagraphsIntoFrames(paragraphs, chain, fallbackTypography);
const sliceForThisFrame = paginated.perFrame[myIndex];
const padding = Number(layer.shape?.padding) || 0;
if (sliceForThisFrame && sliceForThisFrame.lines.length) {
- _paintFrameLines(el, sliceForThisFrame, Number(layer.w) || 0, Number(layer.h) || 0, padding, typography);
+ _paintFrameLines(el, sliceForThisFrame, Number(layer.w) || 0, Number(layer.h) || 0, padding, localTypography);
if (myIndex === chain.length - 1 && paginated.hasMoreText) {
const overset = document.createElement('div');
overset.style.cssText = 'position:absolute;bottom:6px;right:6px;background:#fee2e2;color:#991b1b;border:1px solid #fca5a5;border-radius:3px;padding:1px 6px;font-size:.62rem;font-weight:700;pointer-events:none;z-index:9;letter-spacing:.05em';
@@ -28964,13 +28984,16 @@
const entry = (book.pages || []).find(p => p.id === head.sourceChapter);
if (!entry) continue;
const paragraphs = _flattenChapterToParagraphs(entry.chapterMarkdown || '');
- const typography = _getFrameTypography(head);
+ // J4.d.2 — head typography is the fallback; each frame's own
+ // typography wins when present.
+ const fallbackTypography = _getFrameTypography(head);
+ const localTypography = _getFrameTypography(layer);
let paginated;
- try { paginated = _paginateParagraphsIntoFrames(paragraphs, chain, typography); }
+ try { paginated = _paginateParagraphsIntoFrames(paragraphs, chain, fallbackTypography); }
catch (err) { console.warn('PDF thread paginate failed', err); continue; }
const slice = paginated.perFrame[myIndex];
if (!slice || !slice.lines.length) continue;
- _drawFrameLinesToCanvas(ctx, slice, layer, dpi, bleed, typography);
+ _drawFrameLinesToCanvas(ctx, slice, layer, dpi, bleed, localTypography);
}
return canvas;
}