Correct WebGL book page projection
This commit is contained in:
@@ -9,16 +9,16 @@ class BookPageFormatModule extends BaseModule {
|
|||||||
super('book-page-format', 'Book Page Format');
|
super('book-page-format', 'Book Page Format');
|
||||||
this.dependencies = [];
|
this.dependencies = [];
|
||||||
this.format = Object.freeze({
|
this.format = Object.freeze({
|
||||||
id: 'us-mass-market-hardcover',
|
id: 'us-mass-market-paperback',
|
||||||
trim: Object.freeze({
|
trim: Object.freeze({
|
||||||
widthIn: 4.25,
|
widthIn: 4.25,
|
||||||
heightIn: 6.375
|
heightIn: 6.87
|
||||||
}),
|
}),
|
||||||
margins: Object.freeze({
|
margins: Object.freeze({
|
||||||
topIn: 0.46,
|
topIn: 0.46,
|
||||||
bottomIn: 0.58,
|
bottomIn: 0.58,
|
||||||
innerIn: 0.62,
|
innerIn: 0.56,
|
||||||
outerIn: 0.86
|
outerIn: 0.44
|
||||||
}),
|
}),
|
||||||
typography: Object.freeze({
|
typography: Object.freeze({
|
||||||
fontFamily: '"EB Garamond", "EB Garamond 12", serif',
|
fontFamily: '"EB Garamond", "EB Garamond 12", serif',
|
||||||
@@ -69,6 +69,16 @@ class BookPageFormatModule extends BaseModule {
|
|||||||
width: Math.max(1, width - margins.outer - margins.inner),
|
width: Math.max(1, width - margins.outer - margins.inner),
|
||||||
height: Math.max(1, height - margins.top - margins.bottom)
|
height: Math.max(1, height - margins.top - margins.bottom)
|
||||||
};
|
};
|
||||||
|
const contentBySide = {
|
||||||
|
left: {
|
||||||
|
...content,
|
||||||
|
x: margins.outer
|
||||||
|
},
|
||||||
|
right: {
|
||||||
|
...content,
|
||||||
|
x: margins.inner
|
||||||
|
}
|
||||||
|
};
|
||||||
const linesPerPage = Math.max(1, Number(this.format.typography.linesPerPage || 25));
|
const linesPerPage = Math.max(1, Number(this.format.typography.linesPerPage || 25));
|
||||||
const typographyLineHeightPx = content.height / linesPerPage;
|
const typographyLineHeightPx = content.height / linesPerPage;
|
||||||
const bodyFontSizePx = typographyLineHeightPx / Math.max(1, Number(this.format.typography.bodyLineRatio || 1.5));
|
const bodyFontSizePx = typographyLineHeightPx / Math.max(1, Number(this.format.typography.bodyLineRatio || 1.5));
|
||||||
@@ -78,6 +88,7 @@ class BookPageFormatModule extends BaseModule {
|
|||||||
aspectRatio: this.getAspectRatio(),
|
aspectRatio: this.getAspectRatio(),
|
||||||
margins,
|
margins,
|
||||||
content,
|
content,
|
||||||
|
contentBySide,
|
||||||
linesPerPage,
|
linesPerPage,
|
||||||
bodyFontSizePx,
|
bodyFontSizePx,
|
||||||
typographyLineHeightPx,
|
typographyLineHeightPx,
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ class BookPaginationModule extends BaseModule {
|
|||||||
'layoutTextBlock',
|
'layoutTextBlock',
|
||||||
'getDropCapText',
|
'getDropCapText',
|
||||||
'extractDropCapText',
|
'extractDropCapText',
|
||||||
|
'measureDropCapReservation',
|
||||||
|
'measureNormalTextGap',
|
||||||
|
'calculateDropCapLayout',
|
||||||
|
'extractLayoutLine',
|
||||||
|
'extractRemainingLayoutText',
|
||||||
'extractLines',
|
'extractLines',
|
||||||
'countLineWords',
|
'countLineWords',
|
||||||
'getLineGeometry',
|
'getLineGeometry',
|
||||||
@@ -136,7 +141,7 @@ class BookPaginationModule extends BaseModule {
|
|||||||
const bottomSpaceLines = role === 'chapter-heading' || role === 'section-heading' ? 1 : 0;
|
const bottomSpaceLines = role === 'chapter-heading' || role === 'section-heading' ? 1 : 0;
|
||||||
const lineHeightPx = Math.max(1, Number(this.metrics.typographyLineHeightPx || 1));
|
const lineHeightPx = Math.max(1, Number(this.metrics.typographyLineHeightPx || 1));
|
||||||
const fontPx = Math.max(1, Number(this.metrics.bodyFontSizePx || lineHeightPx / 1.5));
|
const fontPx = Math.max(1, Number(this.metrics.bodyFontSizePx || lineHeightPx / 1.5));
|
||||||
const dropCapWidth = dropCap ? lineHeightPx * 1.72 : 0;
|
const dropCapWidth = dropCap ? this.measureDropCapReservation(dropCapText, fontPx, lineHeightPx) : 0;
|
||||||
const indent = (isHeading || block.isFirstParagraphInChapter || block.metadata?.isFirstParagraphInChapter || block.addTopSpace)
|
const indent = (isHeading || block.isFirstParagraphInChapter || block.metadata?.isFirstParagraphInChapter || block.addTopSpace)
|
||||||
? 0
|
? 0
|
||||||
: lineHeightPx * 1.5;
|
: lineHeightPx * 1.5;
|
||||||
@@ -147,14 +152,17 @@ class BookPaginationModule extends BaseModule {
|
|||||||
: [Math.max(120, this.metrics.content.width - indent), this.metrics.content.width, this.metrics.content.width];
|
: [Math.max(120, this.metrics.content.width - indent), this.metrics.content.width, this.metrics.content.width];
|
||||||
const lineOffsets = isHeading ? [0] : dropCap ? [dropCapWidth, dropCapWidth, 0] : [indent, 0, 0];
|
const lineOffsets = isHeading ? [0] : dropCap ? [dropCapWidth, dropCapWidth, 0] : [indent, 0, 0];
|
||||||
|
|
||||||
const layout = this.paragraphLayout.calculateLayout(text, {
|
const layoutOptions = {
|
||||||
measures,
|
measures,
|
||||||
fontSize: `${fontPx}px`,
|
fontSize: `${fontPx}px`,
|
||||||
fontFamily: typography.fontFamily,
|
fontFamily: typography.fontFamily,
|
||||||
fontFeatureSettings: '"kern" on, "liga" on, "onum" on, "pnum" on, "dlig" on, "clig" on, "calt" on',
|
fontFeatureSettings: '"kern" on, "liga" on, "onum" on, "pnum" on, "dlig" on, "clig" on, "calt" on',
|
||||||
lineHeightPx,
|
lineHeightPx,
|
||||||
lineHeight: lineHeightPx / fontPx
|
lineHeight: lineHeightPx / fontPx
|
||||||
});
|
};
|
||||||
|
const layout = dropCap
|
||||||
|
? this.calculateDropCapLayout(text, measures, lineOffsets, layoutOptions)
|
||||||
|
: this.paragraphLayout.calculateLayout(text, layoutOptions);
|
||||||
if (!layout) return null;
|
if (!layout) return null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -184,7 +192,118 @@ class BookPaginationModule extends BaseModule {
|
|||||||
return String(text || '').replace(dropCap, '').trimStart();
|
return String(text || '').replace(dropCap, '').trimStart();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
measureDropCapReservation(dropCapText, fontPx, lineHeightPx) {
|
||||||
|
if (!dropCapText) return lineHeightPx * 1.34;
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
if (!context) return lineHeightPx * 1.34;
|
||||||
|
|
||||||
|
const dropCapFontPx = Math.round(fontPx * 2.68);
|
||||||
|
context.font = `${dropCapFontPx}px "EB Garamond Initials", ${this.metrics.typography.fontFamily}`;
|
||||||
|
const metrics = context.measureText(dropCapText);
|
||||||
|
const inkRight = Number.isFinite(metrics.actualBoundingBoxRight) && metrics.actualBoundingBoxRight > 0
|
||||||
|
? metrics.actualBoundingBoxRight
|
||||||
|
: (metrics.width || 0);
|
||||||
|
return Math.max(inkRight, lineHeightPx * 1.08) + this.measureNormalTextGap(fontPx);
|
||||||
|
}
|
||||||
|
|
||||||
|
measureNormalTextGap(fontPx) {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
if (!context) return fontPx * 0.75;
|
||||||
|
context.font = `${fontPx}px ${this.metrics.typography.fontFamily}`;
|
||||||
|
const gap = context.measureText('\u2002').width;
|
||||||
|
return Number.isFinite(gap) && gap > 0 ? gap : fontPx * 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateDropCapLayout(text, measures, lineOffsets, layoutOptions) {
|
||||||
|
const firstLineOptions = {
|
||||||
|
...layoutOptions,
|
||||||
|
measures: [measures[0], Math.max(measures[0] * 20, 10000)],
|
||||||
|
fontVariantCaps: 'all-small-caps',
|
||||||
|
fontFeatureSettings: '"smcp" on, "c2sc" on, "kern" on, "liga" on, "onum" on, "pnum" on'
|
||||||
|
};
|
||||||
|
const firstLayout = this.paragraphLayout.calculateLayout(text, firstLineOptions);
|
||||||
|
if (!firstLayout?.breaks || firstLayout.breaks.length < 2) {
|
||||||
|
return this.paragraphLayout.calculateLayout(text, layoutOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstLine = this.extractLayoutLine(firstLayout, 0, {
|
||||||
|
measure: measures[0],
|
||||||
|
offset: lineOffsets[0],
|
||||||
|
smallCaps: true
|
||||||
|
});
|
||||||
|
const remainingText = this.extractRemainingLayoutText(firstLayout, firstLayout.breaks[1].position);
|
||||||
|
const remainingLayout = this.paragraphLayout.calculateLayout(remainingText, {
|
||||||
|
...layoutOptions,
|
||||||
|
measures: [measures[1], ...measures.slice(2)]
|
||||||
|
});
|
||||||
|
const remainingLines = [];
|
||||||
|
if (remainingLayout?.breaks?.length > 1) {
|
||||||
|
for (let lineIndex = 0; lineIndex < remainingLayout.breaks.length - 1; lineIndex += 1) {
|
||||||
|
remainingLines.push(this.extractLayoutLine(remainingLayout, lineIndex, {
|
||||||
|
measure: measures[Math.min(lineIndex + 1, measures.length - 1)],
|
||||||
|
offset: lineOffsets[Math.min(lineIndex + 1, lineOffsets.length - 1)] || 0,
|
||||||
|
smallCaps: false
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
lines: [firstLine, ...remainingLines].filter(Boolean),
|
||||||
|
processedText: text,
|
||||||
|
lineHeight: layoutOptions.lineHeight,
|
||||||
|
lineHeightPx: layoutOptions.lineHeightPx,
|
||||||
|
fontSize: layoutOptions.fontSize,
|
||||||
|
fontFamily: layoutOptions.fontFamily
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
extractLayoutLine(layout, lineIndex, metadata = {}) {
|
||||||
|
const startBreak = layout.breaks?.[lineIndex];
|
||||||
|
const endBreak = layout.breaks?.[lineIndex + 1];
|
||||||
|
if (!startBreak || !endBreak || !Array.isArray(layout.nodes)) return null;
|
||||||
|
const nodes = [];
|
||||||
|
for (let index = startBreak.position; index <= endBreak.position; index += 1) {
|
||||||
|
const node = layout.nodes[index];
|
||||||
|
if (!node) continue;
|
||||||
|
if (node.type === 'glue' && (index === startBreak.position || index === endBreak.position)) continue;
|
||||||
|
const forcedBreak = window.linebreak?.infinity ? -window.linebreak.infinity : -100000;
|
||||||
|
if (node.type === 'penalty' && node.penalty <= forcedBreak) continue;
|
||||||
|
nodes.push({ ...node });
|
||||||
|
}
|
||||||
|
const endNode = layout.nodes[endBreak.position];
|
||||||
|
return {
|
||||||
|
nodes,
|
||||||
|
measure: metadata.measure,
|
||||||
|
offset: metadata.offset || 0,
|
||||||
|
ratio: endBreak.ratio || 0,
|
||||||
|
isFinal: lineIndex === layout.breaks.length - 2,
|
||||||
|
smallCaps: Boolean(metadata.smallCaps),
|
||||||
|
hyphenated: Boolean(endNode?.type === 'penalty' && endNode.penalty === 100),
|
||||||
|
align: 'justify'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
extractRemainingLayoutText(layout, breakPosition) {
|
||||||
|
if (!Array.isArray(layout.nodes)) return '';
|
||||||
|
const fragments = [];
|
||||||
|
for (let index = breakPosition + 1; index < layout.nodes.length; index += 1) {
|
||||||
|
const node = layout.nodes[index];
|
||||||
|
if (!node) continue;
|
||||||
|
if (node.type === 'box' || node.type === 'tag') {
|
||||||
|
fragments.push(node.value || '');
|
||||||
|
} else if (node.type === 'glue' && node.width > 0) {
|
||||||
|
fragments.push(' ');
|
||||||
|
} else if (node.type === 'penalty' && node.penalty === 100) {
|
||||||
|
fragments.push('|');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fragments.join('').replace(/\s+/g, ' ').trimStart();
|
||||||
|
}
|
||||||
|
|
||||||
extractLines(layout, options = {}) {
|
extractLines(layout, options = {}) {
|
||||||
|
if (Array.isArray(layout?.lines)) return layout.lines;
|
||||||
const lines = [];
|
const lines = [];
|
||||||
const breaks = Array.isArray(layout.breaks) ? layout.breaks : [];
|
const breaks = Array.isArray(layout.breaks) ? layout.breaks : [];
|
||||||
const nodes = Array.isArray(layout.nodes) ? layout.nodes : [];
|
const nodes = Array.isArray(layout.nodes) ? layout.nodes : [];
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
'drawPageLines',
|
'drawPageLines',
|
||||||
'drawLine',
|
'drawLine',
|
||||||
'drawWord',
|
'drawWord',
|
||||||
|
'getPageContent',
|
||||||
'buildLineSegments',
|
'buildLineSegments',
|
||||||
'startRevealAnimation',
|
'startRevealAnimation',
|
||||||
'fastForwardAnimations',
|
'fastForwardAnimations',
|
||||||
@@ -145,30 +146,35 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
ctx.textBaseline = 'alphabetic';
|
ctx.textBaseline = 'alphabetic';
|
||||||
if ('fontKerning' in ctx) ctx.fontKerning = 'normal';
|
if ('fontKerning' in ctx) ctx.fontKerning = 'normal';
|
||||||
if ('fontVariantCaps' in ctx) ctx.fontVariantCaps = 'normal';
|
if ('fontVariantCaps' in ctx) ctx.fontVariantCaps = 'normal';
|
||||||
lines.forEach(line => this.drawLine(ctx, line));
|
lines.forEach(line => this.drawLine(ctx, line, side));
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
drawLine(ctx, lineRecord = {}) {
|
drawLine(ctx, lineRecord = {}, side = 'left') {
|
||||||
const metrics = this.metrics;
|
const metrics = this.metrics;
|
||||||
|
const content = this.getPageContent(side);
|
||||||
const fontPx = Math.max(1, Number(lineRecord.fontPx || 22));
|
const fontPx = Math.max(1, Number(lineRecord.fontPx || 22));
|
||||||
const lineHeightPx = Math.max(fontPx + 2, Number(lineRecord.lineHeightPx || metrics.typographyLineHeightPx || 30));
|
const lineHeightPx = Math.max(fontPx + 2, Number(lineRecord.lineHeightPx || metrics.typographyLineHeightPx || 30));
|
||||||
const fontStyle = lineRecord.fontStyle === 'italic' ? 'italic ' : '';
|
const fontStyle = lineRecord.fontStyle === 'italic' ? 'italic ' : '';
|
||||||
const line = lineRecord.line || {};
|
const line = lineRecord.line || {};
|
||||||
const nodes = Array.isArray(line.nodes) ? line.nodes : [];
|
const nodes = Array.isArray(line.nodes) ? line.nodes : [];
|
||||||
const baseY = metrics.content.y + (Number(lineRecord.pageLine || 0) * lineHeightPx) + fontPx;
|
const baseY = content.y + (Number(lineRecord.pageLine || 0) * lineHeightPx) + fontPx;
|
||||||
const ratio = line.isFinal || line.align === 'center' ? 0 : Number(line.ratio || 0);
|
const ratio = line.isFinal || line.align === 'center' ? 0 : Number(line.ratio || 0);
|
||||||
const naturalWidth = nodes.reduce((sum, node) => {
|
const naturalWidth = nodes.reduce((sum, node) => {
|
||||||
if (node.type === 'box' || node.type === 'glue') return sum + Number(node.width || 0);
|
if (node.type === 'box' || node.type === 'glue') return sum + Number(node.width || 0);
|
||||||
return sum;
|
return sum;
|
||||||
}, 0);
|
}, 0);
|
||||||
const centerOffset = line.align === 'center'
|
const centerOffset = line.align === 'center'
|
||||||
? Math.max(0, (metrics.content.width - naturalWidth) / 2)
|
? Math.max(0, (content.width - naturalWidth) / 2)
|
||||||
: Number(line.offset || 0);
|
: Number(line.offset || 0);
|
||||||
let x = metrics.content.x + centerOffset;
|
let x = content.x + centerOffset;
|
||||||
let wordIndex = 0;
|
const smallCaps = Boolean(lineRecord.smallCaps || line.smallCaps);
|
||||||
|
const previousVariantCaps = 'fontVariantCaps' in ctx ? ctx.fontVariantCaps : null;
|
||||||
|
const previousLetterSpacing = 'letterSpacing' in ctx ? ctx.letterSpacing : null;
|
||||||
|
|
||||||
ctx.font = `${fontStyle}${lineRecord.smallCaps ? 'small-caps ' : ''}${fontPx}px ${metrics.typography.fontFamily}`;
|
if ('fontVariantCaps' in ctx) ctx.fontVariantCaps = smallCaps ? 'all-small-caps' : 'normal';
|
||||||
|
if ('letterSpacing' in ctx) ctx.letterSpacing = smallCaps ? `${fontPx * 0.012}px` : '0px';
|
||||||
|
ctx.font = `${fontStyle}${smallCaps ? 'small-caps ' : ''}${fontPx}px ${metrics.typography.fontFamily}`;
|
||||||
if (lineRecord.dropCapText) {
|
if (lineRecord.dropCapText) {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
const alpha = this.getWordAlpha(lineRecord, 0);
|
const alpha = this.getWordAlpha(lineRecord, 0);
|
||||||
@@ -176,20 +182,33 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
ctx.restore();
|
ctx.restore();
|
||||||
} else {
|
} else {
|
||||||
ctx.globalAlpha *= alpha;
|
ctx.globalAlpha *= alpha;
|
||||||
ctx.font = `${Math.round(lineHeightPx * 2.14)}px "EB Garamond Initials", ${metrics.typography.fontFamily}`;
|
ctx.font = `${Math.round(fontPx * 2.68)}px "EB Garamond Initials", ${metrics.typography.fontFamily}`;
|
||||||
ctx.textBaseline = 'top';
|
ctx.textBaseline = 'top';
|
||||||
ctx.fillText(
|
ctx.fillText(
|
||||||
String(lineRecord.dropCapText),
|
String(lineRecord.dropCapText),
|
||||||
metrics.content.x,
|
content.x,
|
||||||
metrics.content.y + (Number(lineRecord.pageLine || 0) * lineHeightPx) - (lineHeightPx * 0.05)
|
content.y + (Number(lineRecord.pageLine || 0) * lineHeightPx) + (fontPx * 0.25)
|
||||||
);
|
);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
ctx.font = `${fontStyle}${lineRecord.smallCaps ? 'small-caps ' : ''}${fontPx}px ${metrics.typography.fontFamily}`;
|
if ('fontVariantCaps' in ctx) ctx.fontVariantCaps = smallCaps ? 'all-small-caps' : 'normal';
|
||||||
|
if ('letterSpacing' in ctx) ctx.letterSpacing = smallCaps ? `${fontPx * 0.012}px` : '0px';
|
||||||
|
ctx.font = `${fontStyle}${smallCaps ? 'small-caps ' : ''}${fontPx}px ${metrics.typography.fontFamily}`;
|
||||||
}
|
}
|
||||||
this.buildLineSegments(ctx, nodes, line, ratio).forEach((segment) => {
|
this.buildLineSegments(ctx, nodes, line, ratio).forEach((segment) => {
|
||||||
this.drawWord(ctx, segment.value, x + segment.x, baseY, lineRecord, segment.wordIndex);
|
this.drawWord(ctx, segment.value, x + segment.x, baseY, lineRecord, segment.wordIndex);
|
||||||
});
|
});
|
||||||
|
if ('fontVariantCaps' in ctx) ctx.fontVariantCaps = previousVariantCaps || 'normal';
|
||||||
|
if ('letterSpacing' in ctx) ctx.letterSpacing = previousLetterSpacing || '0px';
|
||||||
|
}
|
||||||
|
|
||||||
|
getPageContent(side = 'left') {
|
||||||
|
return this.metrics?.contentBySide?.[side] || this.metrics?.content || {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: this.metrics?.width || 1,
|
||||||
|
height: this.metrics?.height || 1
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
buildLineSegments(ctx, nodes = [], line = {}, ratio = 0) {
|
buildLineSegments(ctx, nodes = [], line = {}, ratio = 0) {
|
||||||
|
|||||||
+1
-1
@@ -24,7 +24,7 @@ const ModuleState = {
|
|||||||
ERROR: 'ERROR'
|
ERROR: 'ERROR'
|
||||||
};
|
};
|
||||||
|
|
||||||
const MODULE_CACHE_BUSTER = '20260606-webgl-texture-refresh-fix';
|
const MODULE_CACHE_BUSTER = '20260607-webgl-page-uv-endpoints';
|
||||||
window.MODULE_CACHE_BUSTER = MODULE_CACHE_BUSTER;
|
window.MODULE_CACHE_BUSTER = MODULE_CACHE_BUSTER;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ export const PROCEDURAL_BOOK = {
|
|||||||
PAGE_COUNT_STEP: 10,
|
PAGE_COUNT_STEP: 10,
|
||||||
PAGE_LINE_SEGMENTS: 48,
|
PAGE_LINE_SEGMENTS: 48,
|
||||||
PAGE_DEPTH: 2.24,
|
PAGE_DEPTH: 2.24,
|
||||||
PAGE_WIDTH: 2.24 * 2 / 3,
|
PAGE_WIDTH: 2.24 * (4.25 / 6.87),
|
||||||
COVER_DEPTH: 2.30,
|
COVER_DEPTH: 2.30,
|
||||||
OPEN_SEAM_GAP: 0.003,
|
OPEN_SEAM_GAP: 0.003,
|
||||||
PAGE_TEXTURE_FORE_EDGE_INSET_RATIO: 0.12,
|
PAGE_TEXTURE_FORE_EDGE_INSET_RATIO: 0.105,
|
||||||
PROFILE: {
|
PROFILE: {
|
||||||
tableY: 0,
|
tableY: 0,
|
||||||
coverThickness: 0.03,
|
coverThickness: 0.03,
|
||||||
@@ -566,12 +566,14 @@ function createLoftedLineBody(model, lines, depth) {
|
|||||||
v: (z + depth * 0.5) / depth
|
v: (z + depth * 0.5) / depth
|
||||||
});
|
});
|
||||||
const topCapUv = (point, z, col, row) => {
|
const topCapUv = (point, z, col, row) => {
|
||||||
const side = lines[row]?.side ?? 1;
|
const line = lines[row] || {};
|
||||||
const pageDistance = side > 0
|
const side = line.side ?? 1;
|
||||||
? point.x - model.spineHalf
|
const anchor = line.anchor || smoothLines[row][0] || { x: side * model.spineHalf };
|
||||||
: -model.spineHalf - point.x;
|
const endpoint = line.endpoint || smoothLines[row].at(-1) || { x: side * model.foreEdgeX };
|
||||||
const textureInset = model.pageWidth * PROCEDURAL_BOOK.PAGE_TEXTURE_FORE_EDGE_INSET_RATIO;
|
const pageDistance = side * (point.x - anchor.x);
|
||||||
const pageU = THREE.MathUtils.clamp(pageDistance / Math.max(0.001, model.pageWidth - textureInset), 0, 1);
|
const totalDistance = Math.max(0.001, side * (endpoint.x - anchor.x));
|
||||||
|
const textureInset = totalDistance * PROCEDURAL_BOOK.PAGE_TEXTURE_FORE_EDGE_INSET_RATIO;
|
||||||
|
const pageU = THREE.MathUtils.clamp(pageDistance / Math.max(0.001, totalDistance - textureInset), 0, 1);
|
||||||
return {
|
return {
|
||||||
u: side < 0 ? 1 - pageU : pageU,
|
u: side < 0 ? 1 - pageU : pageU,
|
||||||
v: 1 - ((z + depth * 0.5) / depth)
|
v: 1 - ((z + depth * 0.5) / depth)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { RenderPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postproces
|
|||||||
import { SSAOPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SSAOPass.js';
|
import { SSAOPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SSAOPass.js';
|
||||||
import { SMAAPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SMAAPass.js';
|
import { SMAAPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SMAAPass.js';
|
||||||
import { OutputPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/OutputPass.js';
|
import { OutputPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/OutputPass.js';
|
||||||
import { PROCEDURAL_BOOK, createProceduralBookModel, snapProceduralPageCount } from './procedural-book-model.js?v=20260606-webgl-texture-refresh-fix';
|
import { PROCEDURAL_BOOK, createProceduralBookModel, snapProceduralPageCount } from './procedural-book-model.js?v=20260607-webgl-page-uv-endpoints';
|
||||||
|
|
||||||
const canvas = document.getElementById('scene');
|
const canvas = document.getElementById('scene');
|
||||||
canvas.style.cursor = 'grab';
|
canvas.style.cursor = 'grab';
|
||||||
|
|||||||
Reference in New Issue
Block a user