bl791's picture
download
raw
13.4 kB
(function(storyContent) {
// Create ink story from the content using inkjs
var story = new inkjs.Story(storyContent);
var savePoint = "";
let savedTheme;
let globalTagTheme;
// 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<story.globalTags.length; i++) {
var globalTag = story.globalTags[i];
var splitTag = splitPropertyTag(globalTag);
// THEME: dark
if( splitTag && splitTag.property == "theme" ) {
globalTagTheme = splitTag.val;
}
// author: Your Name
else if( splitTag && splitTag.property == "author" ) {
var byline = document.querySelector('.byline');
byline.innerHTML = "by "+splitTag.val;
}
}
}
var storyContainer = document.querySelector('#story');
var outerScrollContainer = document.querySelector('.outerContainer');
// page features setup
setupTheme(globalTagTheme);
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();
// 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<tags.length; i++) {
var tag = tags[i];
// Detect tags of the form "X: Y". Currently used for IMAGE and CLASS but could be
// customised to be used for other things too.
var splitTag = splitPropertyTag(tag);
// AUDIO: src
if( splitTag && splitTag.property == "AUDIO" ) {
if('audio' in this) {
this.audio.pause();
this.audio.removeAttribute('src');
this.audio.load();
}
this.audio = new Audio(splitTag.val);
this.audio.play();
}
// AUDIOLOOP: src
else if( splitTag && splitTag.property == "AUDIOLOOP" ) {
if('audioLoop' in this) {
this.audioLoop.pause();
this.audioLoop.removeAttribute('src');
this.audioLoop.load();
}
this.audioLoop = new Audio(splitTag.val);
this.audioLoop.play();
this.audioLoop.loop = true;
}
// IMAGE: src
if( splitTag && splitTag.property == "IMAGE" ) {
var imageElement = document.createElement('img');
imageElement.src = splitTag.val;
storyContainer.appendChild(imageElement);
showAfter(delay, imageElement);
delay += 200.0;
}
// LINK: url
else if( splitTag && splitTag.property == "LINK" ) {
window.location.href = splitTag.val;
}
// LINKOPEN: url
else if( splitTag && splitTag.property == "LINKOPEN" ) {
window.open(splitTag.val);
}
// BACKGROUND: src
else if( splitTag && splitTag.property == "BACKGROUND" ) {
outerScrollContainer.style.backgroundImage = 'url('+splitTag.val+')';
}
// CLASS: className
else if( splitTag && splitTag.property == "CLASS" ) {
customClasses.push(splitTag.val);
}
// CLEAR - removes all existing content.
// RESTART - clears everything and restarts the story from the beginning
else if( tag == "CLEAR" || tag == "RESTART" ) {
removeAll("p");
removeAll("img");
// Comment out this line if you want to leave the header visible when clearing
setVisible(".header", false);
if( tag == "RESTART" ) {
restart();
return;
}
}
}
// Create paragraph element (initially hidden)
var paragraphElement = document.createElement('p');
paragraphElement.innerHTML = paragraphText;
storyContainer.appendChild(paragraphElement);
// Add any custom classes derived from ink tags
for(var i=0; i<customClasses.length; i++)
paragraphElement.classList.add(customClasses[i]);
// 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 = `<a href='#'>${choice.text}</a>`
storyContainer.appendChild(choiceParagraphElement);
// 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 <a> link
event.preventDefault();
// Remove all existing choices
removeAll(".choice");
// 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);
// set save point to here
savePoint = story.state.toJson();
continueStory(true);
outerScrollContainer.scrollTo(0, 0);
}
// -----------------------------------
// Various Helper functions
// -----------------------------------
// Fades in an element after a specified delay
function showAfter(delay, el) {
el.classList.add("hide");
setTimeout(function() { el.classList.remove("hide") }, 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) {
// 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(0, (1.0-lerp)*start + lerp*target);
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)
{
var allElements = storyContainer.querySelectorAll(selector);
for(var i=0; i<allElements.length; i++) {
var el = allElements[i];
el.parentNode.removeChild(el);
}
}
// Used for hiding and showing the header when you CLEAR or RESTART the story respectively.
function setVisible(selector, visible)
{
var allElements = storyContainer.querySelectorAll(selector);
for(var i=0; i<allElements.length; i++) {
var el = allElements[i];
if( !visible )
el.classList.add("invisible");
else
el.classList.remove("invisible");
}
}
// Helper for parsing out tags of the form:
// # PROPERTY: value
// e.g. IMAGE: source path
function splitPropertyTag(tag) {
var propertySplitIdx = tag.indexOf(":");
if( propertySplitIdx != null ) {
var property = tag.substr(0, propertySplitIdx).trim();
var val = tag.substr(propertySplitIdx+1).trim();
return {
property: property,
val: val
};
}
return null;
}
// Loads save state if exists in the browser memory
function loadSavePoint() {
try {
let savedState = window.localStorage.getItem('save-state');
if (savedState) {
story.state.LoadJson(savedState);
return true;
}
} catch (e) {
console.debug("Couldn't load save state");
}
return false;
}
// Detects which theme (light or dark) to use
function setupTheme(globalTagTheme) {
// load theme from browser memory
var savedTheme;
try {
savedTheme = window.localStorage.getItem('theme');
} catch (e) {
console.debug("Couldn't load saved theme");
}
// Check whether the OS/browser is configured for dark mode
var browserDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
if (savedTheme === "dark"
|| (savedTheme == undefined && globalTagTheme === "dark")
|| (savedTheme == undefined && globalTagTheme == undefined && browserDark))
document.body.classList.add("dark");
}
// Used to hook up the functionality for global functionality buttons
function setupButtons(hasSave) {
let rewindEl = document.getElementById("rewind");
if (rewindEl) rewindEl.addEventListener("click", function(event) {
removeAll("p");
removeAll("img");
setVisible(".header", false);
restart();
});
let saveEl = document.getElementById("save");
if (saveEl) saveEl.addEventListener("click", function(event) {
try {
window.localStorage.setItem('save-state', savePoint);
document.getElementById("reload").removeAttribute("disabled");
window.localStorage.setItem('theme', document.body.classList.contains("dark") ? "dark" : "");
} catch (e) {
console.warn("Couldn't save state");
}
});
let reloadEl = document.getElementById("reload");
if (!hasSave) {
reloadEl.setAttribute("disabled", "disabled");
}
reloadEl.addEventListener("click", function(event) {
if (reloadEl.getAttribute("disabled"))
return;
removeAll("p");
removeAll("img");
try {
let savedState = window.localStorage.getItem('save-state');
if (savedState) story.state.LoadJson(savedState);
} catch (e) {
console.debug("Couldn't load save state");
}
continueStory(true);
});
let themeSwitchEl = document.getElementById("theme-switch");
if (themeSwitchEl) themeSwitchEl.addEventListener("click", function(event) {
document.body.classList.add("switched");
document.body.classList.toggle("dark");
});
}
})(storyContent);

Xet Storage Details

Size:
13.4 kB
·
Xet hash:
46191e5d5fe2b4f497693cbf796cb7aaf101fed601eebb4193c65ab184b4a5eb

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.