(function(storyContent) { // Create ink story from the content using inkjs var story = new inkjs.Story(storyContent); var savePoint = ""; let fade_in = true; // Global tags - those at the top of the ink file // We support: // # theme: dark // # author: Your Name var globalTags = story.globalTags; if( globalTags ) { for(var i=0; i func(...args), timeoutId: null }; timeoutObject.timeoutId = setTimeout(() => { timeoutObject.execute(); timeoutQueue = timeoutQueue.filter(t => t !== timeoutObject); }, delay); timeoutQueue.push(timeoutObject); return timeoutObject.timeoutId; } function fastForward() { // Sort the queue based on timeoutId (assuming that smaller ids are scheduled earlier) timeoutQueue.sort((a, b) => a.timeoutId - b.timeoutId); // Clear and execute all timeouts timeoutQueue.forEach(timeoutObject => { clearTimeout(timeoutObject.timeoutId); timeoutObject.execute(); }); timeoutQueue = []; document.getElementById("page_right").scrollTo({top: document.getElementById("page_right").scrollHeight, behavior: 'smooth'}); } // var numberOfPreviewLines = 0; function typesetParagraph(paragraph_data, indent_width, delay = 0) { console.log("Typesetting Paragraph with: ", paragraph_data, indent_width); var left = indent_width; var p = document.createElement("p"); p.style.position = 'relative'; var line_height = parseFloat(window.getComputedStyle(document.querySelector("#ruler")).lineHeight); // numberOfPreviewLines += paragraph_data.breaks.length - 1; // console.log("Calculated line height:", line_height); p.style.height = line_height * (paragraph_data.breaks.length - 1) + 'px'; p.style.marginBlockEnd = 0; for(let i = 1; i < paragraph_data.breaks.length; i++) { if(i > 1) left = 0; for(let j = paragraph_data.breaks[i-1].position; j <= paragraph_data.breaks[i].position; j++) { // console.log("i =",i,"j =",j,"from =",paragraph_data.breaks[i-1].position,"to =",paragraph_data.breaks[i].position,"node_width =", paragraph_data.nodes[j].width, "left =", left, "type =", paragraph_data.nodes[j].type, "value =", paragraph_data.nodes[j].value); if(paragraph_data.nodes[j].type === 'box' && paragraph_data.nodes[j].value !== '' && j < paragraph_data.breaks[i].position) { if(j > paragraph_data.breaks[i-1].position + 1 && paragraph_data.nodes[j-1].type === 'penalty' && p.lastChild) { p.lastChild.textContent += paragraph_data.nodes[j].value; left += paragraph_data.nodes[j].width; } else { let word = document.createElement("span"); word.style.position = 'absolute'; word.classList.add("fade-in"); word.style.top = line_height * (i - 1) + 'px'; word.style.left = left + 'px'; word.innerHTML = paragraph_data.nodes[j].value; insertAfter(delay, p, word); delay += 100.0; // p.appendChild(word); if(j > 0) left += paragraph_data.nodes[j].width; else left += paragraph_data.nodes[j].width - indent_width; } } else if(j > paragraph_data.breaks[i-1].position && paragraph_data.nodes[j].type === 'glue' && paragraph_data.nodes[j].width !== 0 && j <= paragraph_data.breaks[i].position) { // Insert space character if(paragraph_data.breaks[i].ratio > 0) { left += paragraph_data.nodes[j].width + paragraph_data.breaks[i].ratio * paragraph_data.nodes[j].stretch; } else { left += paragraph_data.nodes[j].width + paragraph_data.breaks[i].ratio * paragraph_data.nodes[j].shrink; } } else if(paragraph_data.nodes[j].type === 'penalty' && paragraph_data.nodes[j].penalty === 100 && j === paragraph_data.breaks[i].position) { let word = document.createElement("span"); word.style.position = 'absolute'; word.style.top = line_height * (i - 1) + 'px'; word.style.left = left + 'px'; word.innerHTML = "-"; insertAfter(delay, p, word); delay += 100; // p.appendChild(word); // left += paragraph_data.nodes[j].width; } } }; return [p, delay]; } function measureText(str) { if (str === ' ') { str = '\u00A0'; } ruler.textContent = str; return ruler.getClientRects()[0].width; } function updateBookDimensions() { const vw = window.innerWidth; const vh = window.innerHeight; const viewportAspectRatio = vw / vh; const imageAspectRatio = 2727 / 1691; let bookWidth, bookHeight; if (viewportAspectRatio > imageAspectRatio) { bookWidth = vh * imageAspectRatio; bookHeight = vh; } else { bookWidth = vw; bookHeight = vw / imageAspectRatio; } document.documentElement.style.setProperty('--book-width', `${bookWidth}px`); document.documentElement.style.setProperty('--book-height', `${bookHeight}px`); // Setting a CSS variable that will be either vw or vh depending on the viewport aspect ratio document.documentElement.style.setProperty( "--viewport-dimension", viewportAspectRatio > imageAspectRatio ? 'vw' : 'vh' ); document.documentElement.style.setProperty('--viewport-aspect-ratio', viewportAspectRatio); let story = document.getElementById("story"); let paddingTop = window.getComputedStyle(story).paddingTop; let paddingBottom = window.getComputedStyle(story).paddingBottom; document.documentElement.style.setProperty('--story-line-height', (story.clientHeight - paddingTop - paddingBottom) / 28) } // Update the aspect ratio when the page loads updateBookDimensions(); // Update the aspect ratio whenever the window is resized window.addEventListener('resize', updateBookDimensions); window.addEventListener('keydown', (event) => { if (event.code === 'Space') { fade_in = false; fastForward(); } }); // page features setup var hasSave = loadSavePoint(); setupButtons(hasSave); // Set initial save point savePoint = story.state.toJson(); // Kick off the start of the story! continueStory(true); // Main story processing function. Each time this is called it generates // all the next content up as far as the next set of choices. function continueStory(firstTime) { var paragraphIndex = 0; var delay = 0.0; // Don't over-scroll past new content var previousBottomEdge = firstTime ? 0 : contentBottomEdgeY(); var fade_in = true // Generate story text - loop through available content while(story.canContinue) { // Get ink to generate the next paragraph var paragraphText = story.Continue(); var tags = story.currentTags; // Any special tags included with this line var customClasses = []; for(var i=0; i { var measure = parseFloat(window.getComputedStyle(document.getElementById("story")).width); var indentWidth = parseFloat(window.getComputedStyle(document.querySelector("#indent")).textIndent); var previewWidth = measure; var preview_data = kap(hyphenator_en(text, '.hyphenatePipe'), measureText, 'align-justify', measure, true, indentWidth); return { preview_data, indentWidth, previewWidth}; }); hyphenator_promise.then(({ preview_data, indentWidth, previewWidth }) => { // updateParagraphPreview(preview_data, indentWidth, previewWidth); var p, d; [p, d] = typesetParagraph(preview_data, indentWidth, delay); delay = d; // Add any custom classes derived from ink tags for(var i=0; i { // var wordElement = document.createElement('span'); // Hyphenopoly.hyphenators["en-us"].then((hyphenator_en) => { // wordElement.innerHTML = hyphenator_en(word); // }); // // showAfter(delay, wordElement); // insertAfter(delay, paragraphElement, wordElement, fade_in); // insertAfter(delay, paragraphElement, document.createTextNode(" "), false); // delay +=100.0; // // paragraphElement.appendChild(wordElement); // // paragraphElement.appendChild(document.createTextNode(" ")); // }); // // paragraphElement.innerHTML = paragraphText; // storyContainer.appendChild(paragraphElement); // Fade in paragraph after a short delay // showAfter(delay, paragraphElement); // delay += 200.0; } // Create HTML choices from ink choices story.currentChoices.forEach(function(choice) { // Create paragraph with anchor element var choiceParagraphElement = document.createElement('p'); choiceParagraphElement.classList.add("choice"); choiceParagraphElement.innerHTML = `${choice.text}` // choiceContainer.appendChild(choiceParagraphElement); insertAfter(delay, choiceContainer, choiceParagraphElement, fade_in); // Fade choice in after a short delay // showAfter(delay, choiceParagraphElement); delay += 200.0; // Click on choice var choiceAnchorEl = choiceParagraphElement.querySelectorAll("a")[0]; choiceAnchorEl.addEventListener("click", function(event) { // Don't follow link event.preventDefault(); // Remove all existing choices removeAll(".choice", true); // Tell the story where to go next story.ChooseChoiceIndex(choice.index); // This is where the save button will save from savePoint = story.state.toJson(); // Aaand loop continueStory(); }); }); // Extend height to fit // We do this manually so that removing elements and creating new ones doesn't // cause the height (and therefore scroll) to jump backwards temporarily. // storyContainer.style.height = contentBottomEdgeY()+"px"; if( !firstTime ) scrollDown(previousBottomEdge); } function restart() { story.ResetState(); setVisible(".header", true); removeAll(".choice", true); // set save point to here savePoint = story.state.toJson(); continueStory(true); outerScrollContainer.scrollTo({ top: 0, left: 0, behavior: 'smooth'}); } // ----------------------------------- // Various Helper functions // ----------------------------------- // Fades in an element after a specified delay function showAfter(delay, el) { el.classList.add("hide"); setTimeout(function() { setTimeout(function() { el.classList.remove("hide") }, delay); }); } function insertAfter(delay, target, el, fade_in = true) { if(fade_in) { el.classList.add("fade-in"); scheduleTimeout(function() { target.appendChild(el); el.scrollIntoView({ behavior: 'smooth'}); }, delay); } else { scheduleTimeout(function() { target.appendChild(el); }, delay); } } // Scrolls the page down, but no further than the bottom edge of what you could // see previously, so it doesn't go too far. function scrollDown(previousBottomEdge) { return; // TODO: Fix or remove function // Line up top of screen with the bottom of where the previous content ended var target = previousBottomEdge; // Can't go further than the very bottom of the page var limit = outerScrollContainer.scrollHeight - outerScrollContainer.clientHeight; if( target > limit ) target = limit; var start = outerScrollContainer.scrollTop; var dist = target - start; var duration = 300 + 300*dist/100; var startTime = null; function step(time) { if( startTime == null ) startTime = time; var t = (time-startTime) / duration; var lerp = 3*t*t - 2*t*t*t; // ease in/out outerScrollContainer.scrollTo({ left: 0, top: (1.0-lerp)*start + lerp*target, behavior: 'smooth'}); if( t < 1 ) requestAnimationFrame(step); } requestAnimationFrame(step); } // The Y coordinate of the bottom end of all the story content, used // for growing the container, and deciding how far to scroll. function contentBottomEdgeY() { var bottomElement = storyContainer.lastElementChild; return bottomElement ? bottomElement.offsetTop + bottomElement.offsetHeight : 0; } // Remove all elements that match the given selector. Used for removing choices after // you've picked one, as well as for the CLEAR and RESTART tags. function removeAll(selector, choices = false) { if(choices) var allElements = choiceContainer.querySelectorAll(selector); else var allElements = storyContainer.querySelectorAll(selector); for(var i=0; i