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) {
|
if (layoutData.dropCap && layoutData.dropCapText) {
|
||||||
const dropCap = document.createElement('span');
|
const dropCap = document.createElement('span');
|
||||||
dropCap.className = 'drop-cap story-drop-cap';
|
dropCap.className = 'drop-cap story-drop-cap';
|
||||||
@@ -137,6 +131,26 @@ class LayoutRendererModule extends BaseModule {
|
|||||||
paragraph.appendChild(dropCap);
|
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++) {
|
for (let i = 1; i < breaks.length; i++) {
|
||||||
const lineIndex = i - 1;
|
const lineIndex = i - 1;
|
||||||
const lineWidth = measures[Math.min(lineIndex, measures.length - 1)];
|
const lineWidth = measures[Math.min(lineIndex, measures.length - 1)];
|
||||||
@@ -277,6 +291,75 @@ class LayoutRendererModule extends BaseModule {
|
|||||||
return paragraph;
|
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) {
|
measureNaturalLineWidth(nodes, startPosition, endPosition) {
|
||||||
let width = 0;
|
let width = 0;
|
||||||
for (let j = startPosition; j <= endPosition; j++) {
|
for (let j = startPosition; j <= endPosition; j++) {
|
||||||
|
|||||||
@@ -873,23 +873,21 @@ class SentenceQueueModule extends BaseModule {
|
|||||||
computed.fontFamily
|
computed.fontFamily
|
||||||
].filter(Boolean).join(' ');
|
].filter(Boolean).join(' ');
|
||||||
const metrics = context.measureText(dropCapText);
|
const metrics = context.measureText(dropCapText);
|
||||||
inkRight = Math.max(
|
inkRight = Number.isFinite(metrics.actualBoundingBoxRight) && metrics.actualBoundingBoxRight > 0
|
||||||
metrics.width || 0,
|
? metrics.actualBoundingBoxRight
|
||||||
metrics.actualBoundingBoxRight || 0
|
: (metrics.width || 0);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('SentenceQueue: Could not measure drop-cap canvas ink bounds', error);
|
console.warn('SentenceQueue: Could not measure drop-cap canvas ink bounds', error);
|
||||||
}
|
}
|
||||||
probeParagraph.remove();
|
probeParagraph.remove();
|
||||||
|
|
||||||
const measuredAdvance = Math.max(
|
const fallbackAdvance = Math.max(
|
||||||
Number.isFinite(rect.width) && rect.width > 0 ? rect.width : 0,
|
Number.isFinite(rect.width) && rect.width > 0 ? rect.width : 0,
|
||||||
Number.isFinite(probe.offsetWidth) && probe.offsetWidth > 0 ? probe.offsetWidth : 0,
|
Number.isFinite(probe.offsetWidth) && probe.offsetWidth > 0 ? probe.offsetWidth : 0,
|
||||||
Number.isFinite(probe.scrollWidth) && probe.scrollWidth > 0 ? probe.scrollWidth : 0,
|
Number.isFinite(probe.scrollWidth) && probe.scrollWidth > 0 ? probe.scrollWidth : 0
|
||||||
inkRight
|
|
||||||
);
|
);
|
||||||
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);
|
return glyphAdvance + this.measureNormalTextGap(container, lineHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ class UIDisplayHandlerModule extends BaseModule {
|
|||||||
'dedupeRenderedWindow',
|
'dedupeRenderedWindow',
|
||||||
'reflowTextBlocksForActiveExclusions',
|
'reflowTextBlocksForActiveExclusions',
|
||||||
'blockIntersectsExclusions',
|
'blockIntersectsExclusions',
|
||||||
|
'getFlowLineFromItems',
|
||||||
'insertStoredElement',
|
'insertStoredElement',
|
||||||
'handleHistoryWheel',
|
'handleHistoryWheel',
|
||||||
'handleManualScrollStart',
|
'handleManualScrollStart',
|
||||||
@@ -957,8 +958,8 @@ class UIDisplayHandlerModule extends BaseModule {
|
|||||||
try {
|
try {
|
||||||
await this.ensureLiveTailWindow();
|
await this.ensureLiveTailWindow();
|
||||||
await this.scrollTo(this.getLiveEndLine(), { mode: 'enter-live-tail', smooth: false });
|
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.rebuildLayoutExclusions(this.renderedItems);
|
||||||
|
this.layoutFlowLine = this.getFlowLineFromItems(this.renderedItems);
|
||||||
const element = await this.renderStoryBlock(sentence, { animate: true, playback: true, placement: 'append' });
|
const element = await this.renderStoryBlock(sentence, { animate: true, playback: true, placement: 'append' });
|
||||||
if (!element) return null;
|
if (!element) return null;
|
||||||
sentence.element = element;
|
sentence.element = element;
|
||||||
@@ -1246,6 +1247,20 @@ class UIDisplayHandlerModule extends BaseModule {
|
|||||||
return this.layoutExclusions.some(exclusion => start < exclusion.endLine && end > exclusion.startLine);
|
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) {
|
async reflowTextBlocksForActiveExclusions(token = this.renderWindowToken) {
|
||||||
if (!this.layoutExclusions.length || !this.paragraphContainer) return;
|
if (!this.layoutExclusions.length || !this.paragraphContainer) return;
|
||||||
const candidates = this.renderedItems.filter(item => this.blockIntersectsExclusions(item));
|
const candidates = this.renderedItems.filter(item => this.blockIntersectsExclusions(item));
|
||||||
@@ -2037,7 +2052,7 @@ class UIDisplayHandlerModule extends BaseModule {
|
|||||||
this.historyWindowEndId = 0;
|
this.historyWindowEndId = 0;
|
||||||
this.windowOriginLine = 0;
|
this.windowOriginLine = 0;
|
||||||
}
|
}
|
||||||
this.layoutFlowLine = Math.max(0, Number(this.storyHistory.renderedLineCount || 0));
|
this.layoutFlowLine = this.getFlowLineFromItems(this.renderedItems);
|
||||||
this.activeCenterBlockId = latestRendered || null;
|
this.activeCenterBlockId = latestRendered || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user