252 lines
11 KiB
JavaScript
252 lines
11 KiB
JavaScript
/**
|
|
* LayoutRenderer Module
|
|
* Translates the abstract layout data into concrete visual elements (DOM nodes).
|
|
*/
|
|
export class LayoutRenderer {
|
|
/**
|
|
* Create a new LayoutRenderer
|
|
* @param {Object} animationQueue - The AnimationQueue instance
|
|
*/
|
|
constructor(animationQueue) {
|
|
this.animationQueue = animationQueue;
|
|
this.fastForwardingAll = false;
|
|
}
|
|
|
|
/**
|
|
* Render a paragraph based on layout data
|
|
* @param {Object} paragraphData - The layout data from ParagraphLayout
|
|
* @param {number} delay - Initial delay for animations
|
|
* @param {Array<number>} measure - Array of line width measurements
|
|
* @returns {Array} Array containing the paragraph element and the final delay
|
|
*/
|
|
renderParagraph(paragraphData, delay = 0, measure = []) {
|
|
const stack = [];
|
|
let left = 0;
|
|
const p = document.createElement("p");
|
|
p.style.position = 'relative';
|
|
p.classList.add("latest-paragraph");
|
|
p.dataset.numberOfLines = paragraphData.breaks.length - 1;
|
|
|
|
const lineHeight = parseFloat(window.getComputedStyle(document.querySelector('#ruler')).lineHeight);
|
|
const lineWidth = parseFloat(window.getComputedStyle(document.getElementById('story')).width);
|
|
const pageHeight = parseFloat(window.getComputedStyle(document.getElementById('page_right')).height);
|
|
|
|
p.style.height = lineHeight * (paragraphData.breaks.length - 1) + 'px';
|
|
const paragraphHeight = parseFloat(p.style.height);
|
|
p.dataset.vpc = paragraphHeight * 100 / pageHeight;
|
|
p.style.marginBlockEnd = 0;
|
|
stack.push(p);
|
|
|
|
for (let i = 1; i < paragraphData.breaks.length; i++) {
|
|
left = measure[measure.length - 1] - measure[Math.min(i - 1, measure.length - 1)];
|
|
let lastChild = null;
|
|
let syllable = "";
|
|
|
|
for (let j = paragraphData.breaks[i-1].position; j <= paragraphData.breaks[i].position; j++) {
|
|
if (paragraphData.nodes[j].type === 'box' && paragraphData.nodes[j].value !== '' && j < paragraphData.breaks[i].position) {
|
|
if (j > paragraphData.breaks[i-1].position + 1 && paragraphData.nodes[j-1].type === 'penalty' && lastChild) {
|
|
syllable += '\u200c' + paragraphData.nodes[j].value;
|
|
lastChild.innerHTML = syllable;
|
|
left += paragraphData.nodes[j].width;
|
|
} else {
|
|
let word = document.createElement("span");
|
|
word.style.position = 'absolute';
|
|
word.classList.add("fade-in");
|
|
word.style.animationDuration = this.animationQueue.getSpeed() * 10 + 'ms';
|
|
word.style.top = lineHeight * (i - 1) * 100 / paragraphHeight + '%';
|
|
word.style.left = left * 100 / lineWidth + '%';
|
|
syllable = paragraphData.nodes[j].value;
|
|
word.innerHTML = syllable;
|
|
lastChild = word;
|
|
|
|
if (!this.fastForwardingAll) {
|
|
this.insertAfter(delay, stack[stack.length-1], word);
|
|
}
|
|
|
|
delay += this.animationQueue.getSpeed();
|
|
left += paragraphData.nodes[j].width;
|
|
}
|
|
} else if (paragraphData.nodes[j].type === 'tag') {
|
|
if (paragraphData.nodes[j].value.substr(0, 2) == '</') {
|
|
stack.pop();
|
|
} else {
|
|
let tmp = document.createElement('div');
|
|
tmp.innerHTML = paragraphData.nodes[j].value;
|
|
const word = tmp.firstChild;
|
|
word.style.left = left * 100 / lineWidth + '%';
|
|
stack[stack.length-1].appendChild(word);
|
|
stack.push(word);
|
|
}
|
|
} else if (j > paragraphData.breaks[i-1].position && paragraphData.nodes[j].type === 'glue' && paragraphData.nodes[j].width !== 0 && j <= paragraphData.breaks[i].position) {
|
|
// Insert space character
|
|
if (paragraphData.breaks[i].ratio > 0) {
|
|
left += paragraphData.nodes[j].width + paragraphData.breaks[i].ratio * paragraphData.nodes[j].stretch;
|
|
} else {
|
|
left += paragraphData.nodes[j].width + paragraphData.breaks[i].ratio * paragraphData.nodes[j].shrink;
|
|
}
|
|
|
|
let word = document.createElement("span");
|
|
word.style.position = 'absolute';
|
|
word.classList.add("fade-in");
|
|
word.style.top = lineHeight * (i - 1) * 100 / paragraphHeight + '%';
|
|
word.style.left = left * 100 / lineWidth + '%';
|
|
word.innerHTML = " ";
|
|
|
|
if (!this.fastForwardingAll) {
|
|
this.insertAfter(delay, stack[stack.length-1], word);
|
|
}
|
|
} else if (paragraphData.nodes[j].type === 'penalty' && paragraphData.nodes[j].penalty === 100 && j === paragraphData.breaks[i].position) {
|
|
// Create a hyphen at the end of the line if breaking at a hyphenation point
|
|
let hyphen = document.createElement("span");
|
|
hyphen.style.position = 'absolute';
|
|
hyphen.classList.add("fade-in");
|
|
hyphen.classList.add("hyphen-marker"); // Add a class for easier styling if needed
|
|
hyphen.style.top = lineHeight * (i - 1) * 100 / paragraphHeight + '%';
|
|
hyphen.style.left = left * 100 / lineWidth + '%';
|
|
hyphen.innerHTML = "-";
|
|
|
|
// Ensure hyphen is visible with stronger styling
|
|
hyphen.style.fontWeight = "normal";
|
|
hyphen.style.opacity = "1";
|
|
|
|
if (!this.fastForwardingAll) {
|
|
this.insertAfter(delay, stack[stack.length-1], hyphen);
|
|
// Log for debugging
|
|
console.log("Inserted hyphen at line break:", i, "position:", left);
|
|
}
|
|
|
|
delay += this.animationQueue.getSpeed();
|
|
}
|
|
}
|
|
}
|
|
|
|
return [p, delay];
|
|
}
|
|
|
|
/**
|
|
* Insert an element after a delay
|
|
* @param {number} delay - The delay in milliseconds
|
|
* @param {HTMLElement} target - The target element to append to
|
|
* @param {HTMLElement} el - The element to insert
|
|
* @param {boolean} fadeIn - Whether to fade in the element
|
|
*/
|
|
insertAfter(delay, target, el, fadeIn = true) {
|
|
if (fadeIn) {
|
|
el.classList.add("fade-in");
|
|
this.animationQueue.schedule(function() {
|
|
target.appendChild(el);
|
|
}, delay);
|
|
} else {
|
|
this.animationQueue.schedule(function() {
|
|
target.appendChild(el);
|
|
}, delay);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show an element after a delay
|
|
* @param {number} delay - The delay in milliseconds
|
|
* @param {HTMLElement} el - The element to show
|
|
*/
|
|
showAfter(delay, el) {
|
|
el.classList.add("hide");
|
|
setTimeout(function() {
|
|
setTimeout(function() { el.classList.remove("hide") }, delay);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Render a visual tag
|
|
* @param {string} tagType - The type of tag (IMAGE, BACKGROUND, etc.)
|
|
* @param {string} tagValue - The value of the tag
|
|
* @param {HTMLElement} container - The container to append to
|
|
* @param {number} delay - The delay in milliseconds
|
|
* @returns {HTMLElement|null} The created element or null
|
|
*/
|
|
renderVisualTag(tagType, tagValue, container, delay = 0) {
|
|
switch (tagType) {
|
|
case "IMAGE":
|
|
const imageElement = document.createElement('img');
|
|
imageElement.src = tagValue;
|
|
container.appendChild(imageElement);
|
|
this.showAfter(delay, imageElement);
|
|
return imageElement;
|
|
|
|
case "BACKGROUND":
|
|
const outerScrollContainer = document.querySelector('#book');
|
|
outerScrollContainer.style.backgroundImage = 'url(' + tagValue + ')';
|
|
return null;
|
|
|
|
case "CHAPTER":
|
|
const h = document.createElement('H2');
|
|
h.appendChild(document.createTextNode(tagValue));
|
|
h.classList.add("chapter-heading");
|
|
h.classList.add("fade-in");
|
|
container.appendChild(h);
|
|
return h;
|
|
|
|
case "SEPARATOR":
|
|
const d = document.createElement('double');
|
|
d.appendChild(document.createTextNode('\u2766'));
|
|
d.classList.add("fade-in");
|
|
d.classList.add("separator");
|
|
container.appendChild(d);
|
|
return d;
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the fast forwarding state
|
|
* @param {boolean} state - The fast forwarding state
|
|
*/
|
|
setFastForwardingAll(state) {
|
|
this.fastForwardingAll = state;
|
|
}
|
|
|
|
/**
|
|
* Get the fast forwarding state
|
|
* @returns {boolean} The fast forwarding state
|
|
*/
|
|
getFastForwardingAll() {
|
|
return this.fastForwardingAll;
|
|
}
|
|
|
|
/**
|
|
* Smooth scroll to an element
|
|
* @param {HTMLElement} target - The target element to scroll to
|
|
* @param {number} duration - The duration of the scroll animation
|
|
*/
|
|
smoothScroll(target, duration) {
|
|
const display = document.getElementById('page_right');
|
|
const targetPosition = target.getBoundingClientRect().top;
|
|
const startPosition = display.scrollTop;
|
|
const distance = targetPosition;
|
|
let startTime = null;
|
|
|
|
if (duration < 5) {
|
|
display.scrollTo(0, targetPosition);
|
|
return;
|
|
}
|
|
|
|
function animation(currentTime) {
|
|
if (startTime === null) startTime = currentTime;
|
|
const timeElapsed = currentTime - startTime;
|
|
const run = ease(timeElapsed, startPosition, distance, duration);
|
|
display.scrollTo(0, run);
|
|
if (timeElapsed < duration) requestAnimationFrame(animation);
|
|
}
|
|
|
|
function ease(t, b, c, d) {
|
|
t /= d / 2;
|
|
if (t < 1) return c / 2 * t * t + b;
|
|
t--;
|
|
return -c / 2 * (t * (t - 2) - 1) + b;
|
|
}
|
|
|
|
requestAnimationFrame(animation);
|
|
}
|
|
}
|