Fix portrait image flow and drop-cap spacing
This commit is contained in:
@@ -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++) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user