Fix portrait image flow and drop-cap spacing

This commit is contained in:
2026-05-18 03:08:23 +02:00
parent d7bb175167
commit 4f6300042c
3 changed files with 112 additions and 16 deletions
+89 -6
View File
@@ -123,12 +123,6 @@ class LayoutRendererModule extends BaseModule {
}
});
// Position words according to layout with proper justification
let wordCount = 0;
let lastChild = null;
let syllable = "";
const stack = [paragraph];
if (layoutData.dropCap && layoutData.dropCapText) {
const dropCap = document.createElement('span');
dropCap.className = 'drop-cap story-drop-cap';
@@ -137,6 +131,26 @@ class LayoutRendererModule extends BaseModule {
paragraph.appendChild(dropCap);
}
if (Array.isArray(layoutData.lines) && layoutData.lines.length > 0) {
layoutData.lines.forEach((line, lineIndex) => {
this.renderLine({
paragraph,
line,
lineIndex,
contentTopLines,
lineHeight,
maxLineWidth
});
});
return paragraph;
}
// Position words according to layout with proper justification
let wordCount = 0;
let lastChild = null;
let syllable = "";
const stack = [paragraph];
for (let i = 1; i < breaks.length; i++) {
const lineIndex = i - 1;
const lineWidth = measures[Math.min(lineIndex, measures.length - 1)];
@@ -277,6 +291,75 @@ class LayoutRendererModule extends BaseModule {
return paragraph;
}
renderLine({ paragraph, line, lineIndex, contentTopLines, lineHeight, maxLineWidth }) {
const lineWidth = Number(line.measure || maxLineWidth);
const lineOffset = Number(line.offset || 0);
const ratio = line.isFinal ? 0 : Number(line.ratio || 0);
const stack = [paragraph];
let currentLeft = 0;
let lastChild = null;
let syllable = '';
for (let j = 0; j < line.nodes.length; j += 1) {
const node = line.nodes[j];
if (!node) continue;
if (node.type === 'box' && node.value !== '') {
const followsGlue = j > 0 && line.nodes[j - 1].type === 'glue';
const isTrailingPunctuation = /^[,.;:!?…)]$/.test(node.value) && !followsGlue;
if (lastChild && isTrailingPunctuation) {
syllable += node.value;
lastChild.innerHTML = syllable;
currentLeft += node.width || 0;
continue;
}
const word = document.createElement('span');
word.className = ['word', line.styleClass || ''].filter(Boolean).join(' ');
word.style.position = 'absolute';
word.style.display = 'inline-block';
word.style.whiteSpace = 'nowrap';
word.dataset.line = String(lineIndex);
word.dataset.lineStart = String(lineOffset);
word.dataset.lineWidth = String(lineWidth);
word.style.top = `${((contentTopLines + lineIndex) * lineHeight * 100) / parseFloat(paragraph.style.height)}%`;
word.style.left = `${((lineOffset + currentLeft) * 100) / maxLineWidth}%`;
word.style.opacity = '0';
word.style.visibility = 'hidden';
word.style.clipPath = 'inset(0 100% 0 0)';
syllable = node.value;
word.innerHTML = syllable;
stack[stack.length - 1].appendChild(word);
lastChild = word;
currentLeft += node.width || 0;
} else if (node.type === 'tag') {
if (String(node.value || '').startsWith('</')) {
if (stack.length > 1) stack.pop();
} else {
const template = document.createElement('div');
template.innerHTML = node.value;
const tag = template.firstChild;
if (tag) {
tag.style.display = 'contents';
stack[stack.length - 1].appendChild(tag);
stack.push(tag);
}
}
} else if (node.type === 'glue' && node.width !== 0) {
let adjustedWidth = node.width || 0;
if (ratio > 0) {
adjustedWidth += (node.stretch || 0) * ratio;
} else if (ratio < 0) {
adjustedWidth += (node.shrink || 0) * ratio;
}
currentLeft += adjustedWidth;
} else if (node.type === 'penalty' && node.penalty === 100 && line.hyphenated && j === line.nodes.length - 1 && lastChild) {
lastChild.innerHTML = `${lastChild.innerHTML}-`;
}
}
}
measureNaturalLineWidth(nodes, startPosition, endPosition) {
let width = 0;
for (let j = startPosition; j <= endPosition; j++) {
+6 -8
View File
@@ -873,23 +873,21 @@ class SentenceQueueModule extends BaseModule {
computed.fontFamily
].filter(Boolean).join(' ');
const metrics = context.measureText(dropCapText);
inkRight = Math.max(
metrics.width || 0,
metrics.actualBoundingBoxRight || 0
);
inkRight = Number.isFinite(metrics.actualBoundingBoxRight) && metrics.actualBoundingBoxRight > 0
? metrics.actualBoundingBoxRight
: (metrics.width || 0);
}
} catch (error) {
console.warn('SentenceQueue: Could not measure drop-cap canvas ink bounds', error);
}
probeParagraph.remove();
const measuredAdvance = Math.max(
const fallbackAdvance = Math.max(
Number.isFinite(rect.width) && rect.width > 0 ? rect.width : 0,
Number.isFinite(probe.offsetWidth) && probe.offsetWidth > 0 ? probe.offsetWidth : 0,
Number.isFinite(probe.scrollWidth) && probe.scrollWidth > 0 ? probe.scrollWidth : 0,
inkRight
Number.isFinite(probe.scrollWidth) && probe.scrollWidth > 0 ? probe.scrollWidth : 0
);
const glyphAdvance = measuredAdvance > 0 ? measuredAdvance : lineHeight * 1.34;
const glyphAdvance = inkRight > 0 ? inkRight : (fallbackAdvance > 0 ? fallbackAdvance : lineHeight * 1.34);
return glyphAdvance + this.measureNormalTextGap(container, lineHeight);
}
+17 -2
View File
@@ -87,6 +87,7 @@ class UIDisplayHandlerModule extends BaseModule {
'dedupeRenderedWindow',
'reflowTextBlocksForActiveExclusions',
'blockIntersectsExclusions',
'getFlowLineFromItems',
'insertStoredElement',
'handleHistoryWheel',
'handleManualScrollStart',
@@ -957,8 +958,8 @@ class UIDisplayHandlerModule extends BaseModule {
try {
await this.ensureLiveTailWindow();
await this.scrollTo(this.getLiveEndLine(), { mode: 'enter-live-tail', smooth: false });
this.layoutFlowLine = Math.max(0, Number(this.storyHistory?.renderedLineCount || 0));
this.rebuildLayoutExclusions(this.renderedItems);
this.layoutFlowLine = this.getFlowLineFromItems(this.renderedItems);
const element = await this.renderStoryBlock(sentence, { animate: true, playback: true, placement: 'append' });
if (!element) return null;
sentence.element = element;
@@ -1246,6 +1247,20 @@ class UIDisplayHandlerModule extends BaseModule {
return this.layoutExclusions.some(exclusion => start < exclusion.endLine && end > exclusion.startLine);
}
getFlowLineFromItems(items = this.renderedItems) {
const source = Array.isArray(items) ? items : [];
return source.reduce((max, item) => {
const type = String(item?.kind || item?.type || '').toLowerCase();
const size = String(item?.metadata?.imageLayout?.size || item?.metadata?.size || item?.size || '').toLowerCase();
if (type === 'image' && size === 'portrait') {
return max;
}
const start = Number(item?.lineStart ?? item?.metadata?.lineStart);
const count = Math.max(0, Number(item?.lineCount ?? item?.metadata?.lineCount ?? 0));
return Number.isFinite(start) && count > 0 ? Math.max(max, start + count) : max;
}, 0);
}
async reflowTextBlocksForActiveExclusions(token = this.renderWindowToken) {
if (!this.layoutExclusions.length || !this.paragraphContainer) return;
const candidates = this.renderedItems.filter(item => this.blockIntersectsExclusions(item));
@@ -2037,7 +2052,7 @@ class UIDisplayHandlerModule extends BaseModule {
this.historyWindowEndId = 0;
this.windowOriginLine = 0;
}
this.layoutFlowLine = Math.max(0, Number(this.storyHistory.renderedLineCount || 0));
this.layoutFlowLine = this.getFlowLineFromItems(this.renderedItems);
this.activeCenterBlockId = latestRendered || null;
}