initial commit
commit
5ce49f9e69
@ -0,0 +1,59 @@
|
||||
Blood zigzags out in all eight directions. Acoustic aromas flood the air. Night begins to turn and rain finds its way down from the threadbare sky. In the middle of the room stands a thought, "what had caused this tragedy?" The body of a man was blundered to a puddy. The body of an aristocrat. The body of a lecher. He sure would taste like leather. Out the window, children run wild in sporadic grey motion.
|
||||
|
||||
* [Continue]
|
||||
-> paragraph_2
|
||||
|
||||
=== paragraph_2 ===
|
||||
Out of all people, you had to be the one to discover the body. You want to scream, but you can't, and you know that if you do, there'll be repercussions. The terror feels like daylight beneath your skin in the neon of night.
|
||||
|
||||
* [Continue]
|
||||
-> paragraph_3
|
||||
|
||||
=== paragraph_3 ===
|
||||
|
||||
"Who could have done this?" the thought repeats its presence in the white velvet-coated study as the blood crusts slowly to a mediocre brown stain across the parquet floor.
|
||||
|
||||
* [Continue]
|
||||
-> paragraph_4
|
||||
|
||||
=== paragraph_4 ===
|
||||
|
||||
[SEPARATE FRAGMENT]
|
||||
|
||||
Dmitry ran into Katya's mother, Dolores, on the way to the prison. She was silk, saying, "I thought I meant birth like the apparation of Summer approaching the South, but how wrong I was to give the gift of life to such a cruel woman." There she stands with her moon-shaven eyes against the delicately placed watermelons on the table of the street vendor selling fruit.
|
||||
"Are you going to buy anything? I'm about ready to pack up." the street vendor said interrupting the intense conversation we were having.
|
||||
Dolores's son was originally sent to die in a some unimportant war while her daughter amounted the social ladder to become a laywer with cunning ambition. She was responsible for more executions than holidays in the Hebrew year.
|
||||
|
||||
* [Continue]
|
||||
-> paragraph_5
|
||||
|
||||
=== paragraph_5 ===
|
||||
|
||||
[SEPARATE FRAGMENT]
|
||||
|
||||
You spot a black notebook left on the bench by the traintracks. This is nice, to have something to discover. People should leave their belongings in public more often.
|
||||
// in this, you see notes about Katya's mother that make you change your mind
|
||||
|
||||
[SEPARATE FRAGMENT]
|
||||
|
||||
Face-down you fall onto the bed into a straight-jacket sleep. Your eyes gently-pressing to the sheets reveal all sorts of hypnagogic decompressions, decompressing into memories that you're not sure you can trust.
|
||||
// The memory of X
|
||||
|
||||
// It became evident to me that I was socially invisible to these people, but I didn't care. I'd rather not be involved in it, it was all the more entertaining as a voyeur.
|
||||
|
||||
/*
|
||||
two different guy visiting the same bartender and lying about each other or having an affair
|
||||
being forced to serve in X but wanting to walk away from that life
|
||||
being an ethical lawyer in an unethical law firm in order to pay off student debt
|
||||
|
||||
|
||||
His face turns cartoon. The reversion is scarier than the wars I've seen.
|
||||
The food smells infantile.
|
||||
He was dementionless while mother fucking to microtonal music.
|
||||
crimes that I'd meet.
|
||||
*/
|
||||
|
||||
// character that learns a language that feels more natural to them than their foreign tongue, which leads them to unearthing some family history
|
||||
|
||||
// The only music that made sense him during sex was Golden Age Cantorial music, not having anything to do with identity, he was not Jewish.
|
||||
-> END
|
||||
@ -0,0 +1 @@
|
||||
var storyContent = {"inkVersion":21,"root":[["^Blood zigzags out in all eight directions. Acoustic aromas flood the air. Night begins to turn and rain finds its way down from the threadbare sky. In the middle of the room stands a thought, \"what had caused this tragedy?\" The body of a man was blundered to a puddy. The body of an aristocrat. The body of a lecher. He sure would taste like leather. Out the window, children run wild in sporadic grey motion.","\n","ev","str","^Continue","/str","/ev",{"*":"0.c-0","flg":20},{"c-0":["\n",{"->":"paragraph_2"},{"->":"0.g-0"},{"#f":5}],"g-0":["done",{"#f":5}]}],"done",{"paragraph_2":[["^Out of all people, you had to be the one to discover the body. You want to scream, but you can't, and you know that if you do, there'll be repercussions. The terror feels like daylight beneath your skin in the neon of night.","\n","ev","str","^Continue","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["\n",{"->":"paragraph_3"},{"#f":5}]}],{"#f":1}],"paragraph_3":[["^\"Who could have done this?\" the thought repeats its presence in the white velvet-coated study as the blood crusts slowly to a mediocre brown stain across the parquet floor.","\n","ev","str","^Continue","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["\n",{"->":"paragraph_4"},{"#f":5}]}],{"#f":1}],"paragraph_4":[["^[SEPARATE FRAGMENT]","\n","^Dmitry ran into Katya's mother, Dolores, on the way to the prison. She was silk, saying, \"I thought I meant birth like the apparation of Summer approaching the South, but how wrong I was to give the gift of life to such a cruel woman.\" There she stands with her moon-shaven eyes against the delicately placed watermelons on the table of the street vendor selling fruit.","\n","^\"Are you going to buy anything? I'm about ready to pack up.\" the street vendor said interrupting the intense conversation we were having.","\n","^Dolores's son was originally sent to die in a some unimportant war while her daughter amounted the social ladder to become a laywer with cunning ambition. She was responsible for more executions than holidays in the Hebrew year.","\n","ev","str","^Continue","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["\n",{"->":"paragraph_5"},{"#f":5}]}],{"#f":1}],"paragraph_5":["^[SEPARATE FRAGMENT]","\n","^You spot a black notebook left on the bench by the traintracks. This is nice, to have something to discover. People should leave their belongings in public more often.","\n","^[SEPARATE FRAGMENT]","\n","^Face-down you fall onto the bed into a straight-jacket sleep. Your eyes gently-pressing to the sheets reveal all sorts of hypnagogic decompressions, decompressing into memories that you're not sure you can trust.","\n","end",{"#f":1}],"#f":1}],"listDefs":{}};
|
||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,439 @@
|
||||
(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);
|
||||
splitTag.property = splitTag.property.toUpperCase();
|
||||
|
||||
// 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);
|
||||
|
||||
imageElement.onload = () => {
|
||||
console.log(`scrollingto ${previousBottomEdge}`)
|
||||
scrollDown(previousBottomEdge)
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if paragraphText is empty
|
||||
if (paragraphText.trim().length == 0) {
|
||||
continue; // Skip empty paragraphs
|
||||
}
|
||||
|
||||
// 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 choiceTags = choice.tags;
|
||||
var customClasses = [];
|
||||
var isClickable = true;
|
||||
for(var i=0; i<choiceTags.length; i++) {
|
||||
var choiceTag = choiceTags[i];
|
||||
var splitTag = splitPropertyTag(choiceTag);
|
||||
splitTag.property = splitTag.property.toUpperCase();
|
||||
|
||||
if(choiceTag.toUpperCase() == "UNCLICKABLE"){
|
||||
isClickable = false
|
||||
}
|
||||
|
||||
if( splitTag && splitTag.property == "CLASS" ) {
|
||||
customClasses.push(splitTag.val);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
var choiceParagraphElement = document.createElement('p');
|
||||
choiceParagraphElement.classList.add("choice");
|
||||
|
||||
for(var i=0; i<customClasses.length; i++)
|
||||
choiceParagraphElement.classList.add(customClasses[i]);
|
||||
|
||||
if(isClickable){
|
||||
choiceParagraphElement.innerHTML = `<a href='#'>${choice.text}</a>`
|
||||
}else{
|
||||
choiceParagraphElement.innerHTML = `<span class='unclickable'>${choice.text}</span>`
|
||||
}
|
||||
storyContainer.appendChild(choiceParagraphElement);
|
||||
|
||||
// Fade choice in after a short delay
|
||||
showAfter(delay, choiceParagraphElement);
|
||||
delay += 200.0;
|
||||
|
||||
// Click on choice
|
||||
if(isClickable){
|
||||
var choiceAnchorEl = choiceParagraphElement.querySelectorAll("a")[0];
|
||||
choiceAnchorEl.addEventListener("click", function(event) {
|
||||
|
||||
// Don't follow <a> link
|
||||
event.preventDefault();
|
||||
|
||||
// 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";
|
||||
|
||||
// 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();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Unset storyContainer's height, allowing it to resize itself
|
||||
storyContainer.style.height = "";
|
||||
|
||||
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
|
||||
// -----------------------------------
|
||||
|
||||
// Detects whether the user accepts animations
|
||||
function isAnimationEnabled() {
|
||||
return window.matchMedia('(prefers-reduced-motion: no-preference)').matches;
|
||||
}
|
||||
|
||||
// Fades in an element after a specified delay
|
||||
function showAfter(delay, el) {
|
||||
if( isAnimationEnabled() ) {
|
||||
el.classList.add("hide");
|
||||
setTimeout(function() { el.classList.remove("hide") }, delay);
|
||||
} else {
|
||||
// If the user doesn't want animations, show immediately
|
||||
el.classList.remove("hide");
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// If the user doesn't want animations, let them scroll manually
|
||||
if ( !isAnimationEnabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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);
|
||||
@ -0,0 +1,306 @@
|
||||
@import url('https://fonts.googleapis.com/css?family=Open+Sans:300,700|Quattrocento:700');
|
||||
|
||||
body {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-weight: lighter;
|
||||
background: white;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media screen and (prefers-reduced-motion: no-preference) {
|
||||
body.switched {
|
||||
transition: color 0.6s, background-color 0.6s;
|
||||
}
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
text-align: center;
|
||||
font-family: "Quattrocento", Georgia, 'Times New Roman', Times, serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 30pt;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 14pt;
|
||||
font-style: italic;
|
||||
font-family: sans-serif;
|
||||
font-weight: lighter;
|
||||
color: #BBB;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding-top: 3em;
|
||||
padding-bottom: 3em;
|
||||
}
|
||||
|
||||
/*
|
||||
Built-in class:
|
||||
# author: Name
|
||||
*/
|
||||
.byline {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.written-in-ink {
|
||||
z-index: 3;
|
||||
font-size: 9pt;
|
||||
font-family: sans-serif;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
position: fixed;
|
||||
display: block;
|
||||
width: 100%;
|
||||
background: white;
|
||||
margin: 0;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
height: 14px;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
@media screen and (prefers-reduced-motion: no-preference) {
|
||||
.written-in-ink {
|
||||
transition: color 0.6s, background 0.6s;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Enables <iframe> support work on itch.io when using mobile iOS
|
||||
*/
|
||||
.outerContainer {
|
||||
position: absolute;
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overflow: scroll;
|
||||
overflow-x: hidden;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
margin-top: 24px;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 980px) {
|
||||
.outerContainer {
|
||||
margin-top: 44px;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
display: block;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
padding-top: 4em;
|
||||
background: white;
|
||||
}
|
||||
|
||||
@media screen and (prefers-reduced-motion: no-preference) {
|
||||
.switched .container {
|
||||
transition: background-color 0.6s;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 13pt;
|
||||
color: #888;
|
||||
line-height: 1.7em;
|
||||
font-weight: lighter;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 700;
|
||||
color: #b97c2c;
|
||||
font-family: sans-serif;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.unclickable {
|
||||
font-weight: 700;
|
||||
color: #4f3411;
|
||||
font-family: sans-serif;
|
||||
text-decoration: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: black;
|
||||
}
|
||||
|
||||
@media screen and (prefers-reduced-motion: no-preference) {
|
||||
a {
|
||||
transition: color 0.6s;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
transition: color 0.1s;
|
||||
}
|
||||
}
|
||||
|
||||
strong {
|
||||
color: black;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.container .hide {
|
||||
opacity: 0.0;
|
||||
}
|
||||
|
||||
.container .invisible {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.container>* {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
@media screen and (prefers-reduced-motion: no-preference) {
|
||||
.container>* {
|
||||
transition: opacity 1.0s;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Class applied to all choices
|
||||
(Will always appear inside <p> element by default.)
|
||||
*/
|
||||
.choice {
|
||||
text-align: center;
|
||||
line-height: 1.7em;
|
||||
}
|
||||
|
||||
/*
|
||||
Class applied to first choice
|
||||
*/
|
||||
:not(.choice)+.choice {
|
||||
padding-top: 1em;
|
||||
}
|
||||
|
||||
/*
|
||||
Class applied to choice links
|
||||
*/
|
||||
.choice a, .choice span {
|
||||
font-size: 15pt;
|
||||
}
|
||||
|
||||
/*
|
||||
Built-in class:
|
||||
The End # CLASS: end
|
||||
*/
|
||||
.end {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
color: black;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
#controls {
|
||||
z-index: 4;
|
||||
font-size: 9pt;
|
||||
text-align: center;
|
||||
padding-bottom: 6px;
|
||||
position: fixed;
|
||||
right: 14px;
|
||||
top: 4px;
|
||||
user-select: none;
|
||||
background: white;
|
||||
}
|
||||
|
||||
@media screen and (prefers-reduced-motion: no-preference) {
|
||||
#controls {
|
||||
transition: color 0.6s, background 0.6s;
|
||||
}
|
||||
}
|
||||
|
||||
#controls [disabled] {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
#controls>*:not(:last-child):after {
|
||||
content: " | ";
|
||||
}
|
||||
|
||||
@media screen and (max-width: 980px) {
|
||||
#controls {
|
||||
z-index: 2;
|
||||
padding-top: 24px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Dark Theme (Added in Inky 0.10.0)
|
||||
# theme: dark
|
||||
*/
|
||||
|
||||
body.dark {
|
||||
background: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dark h2 {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.dark .container {
|
||||
background: black;
|
||||
}
|
||||
|
||||
.dark .written-in-ink {
|
||||
background: black;
|
||||
}
|
||||
|
||||
.dark a {
|
||||
color: #cc8f1a;
|
||||
}
|
||||
.dark .unclickable{
|
||||
color: #c4af87;
|
||||
cursor:not-allowed;
|
||||
}
|
||||
|
||||
.dark a:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media screen and (prefers-reduced-motion: no-preference) {
|
||||
.dark a {
|
||||
transition: color 0.6s;
|
||||
}
|
||||
}
|
||||
|
||||
.dark strong {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dark #controls [disabled] {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.dark .end {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dark #controls {
|
||||
background: black;
|
||||
}
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
@ -0,0 +1,439 @@
|
||||
(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);
|
||||
splitTag.property = splitTag.property.toUpperCase();
|
||||
|
||||
// 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);
|
||||
|
||||
imageElement.onload = () => {
|
||||
console.log(`scrollingto ${previousBottomEdge}`)
|
||||
scrollDown(previousBottomEdge)
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if paragraphText is empty
|
||||
if (paragraphText.trim().length == 0) {
|
||||
continue; // Skip empty paragraphs
|
||||
}
|
||||
|
||||
// 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 choiceTags = choice.tags;
|
||||
var customClasses = [];
|
||||
var isClickable = true;
|
||||
for(var i=0; i<choiceTags.length; i++) {
|
||||
var choiceTag = choiceTags[i];
|
||||
var splitTag = splitPropertyTag(choiceTag);
|
||||
splitTag.property = splitTag.property.toUpperCase();
|
||||
|
||||
if(choiceTag.toUpperCase() == "UNCLICKABLE"){
|
||||
isClickable = false
|
||||
}
|
||||
|
||||
if( splitTag && splitTag.property == "CLASS" ) {
|
||||
customClasses.push(splitTag.val);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
var choiceParagraphElement = document.createElement('p');
|
||||
choiceParagraphElement.classList.add("choice");
|
||||
|
||||
for(var i=0; i<customClasses.length; i++)
|
||||
choiceParagraphElement.classList.add(customClasses[i]);
|
||||
|
||||
if(isClickable){
|
||||
choiceParagraphElement.innerHTML = `<a href='#'>${choice.text}</a>`
|
||||
}else{
|
||||
choiceParagraphElement.innerHTML = `<span class='unclickable'>${choice.text}</span>`
|
||||
}
|
||||
storyContainer.appendChild(choiceParagraphElement);
|
||||
|
||||
// Fade choice in after a short delay
|
||||
showAfter(delay, choiceParagraphElement);
|
||||
delay += 200.0;
|
||||
|
||||
// Click on choice
|
||||
if(isClickable){
|
||||
var choiceAnchorEl = choiceParagraphElement.querySelectorAll("a")[0];
|
||||
choiceAnchorEl.addEventListener("click", function(event) {
|
||||
|
||||
// Don't follow <a> link
|
||||
event.preventDefault();
|
||||
|
||||
// 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";
|
||||
|
||||
// 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();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Unset storyContainer's height, allowing it to resize itself
|
||||
storyContainer.style.height = "";
|
||||
|
||||
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
|
||||
// -----------------------------------
|
||||
|
||||
// Detects whether the user accepts animations
|
||||
function isAnimationEnabled() {
|
||||
return window.matchMedia('(prefers-reduced-motion: no-preference)').matches;
|
||||
}
|
||||
|
||||
// Fades in an element after a specified delay
|
||||
function showAfter(delay, el) {
|
||||
if( isAnimationEnabled() ) {
|
||||
el.classList.add("hide");
|
||||
setTimeout(function() { el.classList.remove("hide") }, delay);
|
||||
} else {
|
||||
// If the user doesn't want animations, show immediately
|
||||
el.classList.remove("hide");
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// If the user doesn't want animations, let them scroll manually
|
||||
if ( !isAnimationEnabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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);
|
||||
@ -0,0 +1 @@
|
||||
var storyContent = {"inkVersion":21,"root":[["^Blood zigzags out in all eight directions. Acoustic aromas flood the air. Night begins to turn and rain finds its way down from the threadbare sky. In the middle of the room stands a thought, \"what had caused this tragedy?\" The body of a man was blundered to a puddy. The body of an aristocrat. The body of a lecher. He sure would taste like leather. Out the window, children run wild in sporadic grey motion.","\n","ev","str","^Continue","/str","/ev",{"*":"0.c-0","flg":20},{"c-0":["\n",{"->":"paragraph_2"},{"->":"0.g-0"},{"#f":5}],"g-0":["done",{"#f":5}]}],"done",{"paragraph_2":[["^Out of all people, you had to be the one to discover the body. You want to scream, but you can't, and you know that if you do, there'll be repercussions. The terror feels like daylight beneath your skin in the neon of night.","\n","ev","str","^Continue","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["\n",{"->":"paragraph_3"},{"#f":5}]}],{"#f":1}],"paragraph_3":[["^\"Who could have done this?\" the thought repeats its presence in the white velvet-coated study as the blood crusts slowly to a mediocre brown stain across the parquet floor.","\n","ev","str","^Continue","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["\n",{"->":"paragraph_4"},{"#f":5}]}],{"#f":1}],"paragraph_4":[["^[SEPARATE FRAGMENT]","\n","^Dmitry ran into Katya's mother, Dolores, on the way to the prison. She was silk, saying, \"I thought I meant birth like the apparation of Summer approaching the South, but how wrong I was to give the gift of life to such a cruel woman.\" There she stands with her moon-shaven eyes against the delicately placed watermelons on the table of the street vendor selling fruit.","\n","^\"Are you going to buy anything? I'm about ready to pack up.\" the street vendor said interrupting the intense conversation we were having.","\n","^Dolores's son was originally sent to die in a some unimportant war while her daughter amounted the social ladder to become a laywer with cunning ambition. She was responsible for more executions than holidays in the Hebrew year.","\n","ev","str","^Continue","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["\n",{"->":"paragraph_5"},{"#f":5}]}],{"#f":1}],"paragraph_5":["^[SEPARATE FRAGMENT]","\n","^You spot a black notebook left on the bench by the traintracks. This is nice, to have something to discover. People should leave their belongings in public more often.","\n","^[SEPARATE FRAGMENT]","\n","^Face-down you fall onto the bed into a straight-jacket sleep. Your eyes gently-pressing to the sheets reveal all sorts of hypnagogic decompressions, decompressing into memories that you're not sure you can trust.","\n","end",{"#f":1}],"#f":1}],"listDefs":{}};
|
||||
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
@ -0,0 +1,439 @@
|
||||
(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);
|
||||
splitTag.property = splitTag.property.toUpperCase();
|
||||
|
||||
// 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);
|
||||
|
||||
imageElement.onload = () => {
|
||||
console.log(`scrollingto ${previousBottomEdge}`)
|
||||
scrollDown(previousBottomEdge)
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if paragraphText is empty
|
||||
if (paragraphText.trim().length == 0) {
|
||||
continue; // Skip empty paragraphs
|
||||
}
|
||||
|
||||
// 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 choiceTags = choice.tags;
|
||||
var customClasses = [];
|
||||
var isClickable = true;
|
||||
for(var i=0; i<choiceTags.length; i++) {
|
||||
var choiceTag = choiceTags[i];
|
||||
var splitTag = splitPropertyTag(choiceTag);
|
||||
splitTag.property = splitTag.property.toUpperCase();
|
||||
|
||||
if(choiceTag.toUpperCase() == "UNCLICKABLE"){
|
||||
isClickable = false
|
||||
}
|
||||
|
||||
if( splitTag && splitTag.property == "CLASS" ) {
|
||||
customClasses.push(splitTag.val);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
var choiceParagraphElement = document.createElement('p');
|
||||
choiceParagraphElement.classList.add("choice");
|
||||
|
||||
for(var i=0; i<customClasses.length; i++)
|
||||
choiceParagraphElement.classList.add(customClasses[i]);
|
||||
|
||||
if(isClickable){
|
||||
choiceParagraphElement.innerHTML = `<a href='#'>${choice.text}</a>`
|
||||
}else{
|
||||
choiceParagraphElement.innerHTML = `<span class='unclickable'>${choice.text}</span>`
|
||||
}
|
||||
storyContainer.appendChild(choiceParagraphElement);
|
||||
|
||||
// Fade choice in after a short delay
|
||||
showAfter(delay, choiceParagraphElement);
|
||||
delay += 200.0;
|
||||
|
||||
// Click on choice
|
||||
if(isClickable){
|
||||
var choiceAnchorEl = choiceParagraphElement.querySelectorAll("a")[0];
|
||||
choiceAnchorEl.addEventListener("click", function(event) {
|
||||
|
||||
// Don't follow <a> link
|
||||
event.preventDefault();
|
||||
|
||||
// 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";
|
||||
|
||||
// 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();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Unset storyContainer's height, allowing it to resize itself
|
||||
storyContainer.style.height = "";
|
||||
|
||||
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
|
||||
// -----------------------------------
|
||||
|
||||
// Detects whether the user accepts animations
|
||||
function isAnimationEnabled() {
|
||||
return window.matchMedia('(prefers-reduced-motion: no-preference)').matches;
|
||||
}
|
||||
|
||||
// Fades in an element after a specified delay
|
||||
function showAfter(delay, el) {
|
||||
if( isAnimationEnabled() ) {
|
||||
el.classList.add("hide");
|
||||
setTimeout(function() { el.classList.remove("hide") }, delay);
|
||||
} else {
|
||||
// If the user doesn't want animations, show immediately
|
||||
el.classList.remove("hide");
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// If the user doesn't want animations, let them scroll manually
|
||||
if ( !isAnimationEnabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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);
|
||||
@ -0,0 +1 @@
|
||||
var storyContent = {"inkVersion":21,"root":[["^Blood zigzags out in all eight directions. Acoustic aromas flood the air. Night begins to turn and rain finds its way down from the threadbare sky. In the middle of the room stands a thought, \"what had caused this tragedy?\" The body of a man was blundered to a puddy. The body of an aristocrat. The body of a lecher. He sure would taste like leather. Out the window, children run wild in sporadic grey motion.","\n","ev","str","^Continue","/str","/ev",{"*":"0.c-0","flg":20},{"c-0":["\n",{"->":"paragraph_2"},{"->":"0.g-0"},{"#f":5}],"g-0":["done",{"#f":5}]}],"done",{"paragraph_2":[["^Out of all people, you had to be the one to discover the body. You want to scream, but you can't, and you know that if you do, there'll be repercussions. The terror feels like daylight beneath your skin in the neon of night.","\n","ev","str","^Continue","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["\n",{"->":"paragraph_3"},{"#f":5}]}],{"#f":1}],"paragraph_3":[["^\"Who could have done this?\" the thought repeats its presence in the white velvet-coated study as the blood crusts slowly to a mediocre brown stain across the parquet floor.","\n","ev","str","^Continue","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["\n",{"->":"paragraph_4"},{"#f":5}]}],{"#f":1}],"paragraph_4":[["^[SEPARATE FRAGMENT]","\n","^Dmitry ran into Katya's mother, Dolores, on the way to the prison. She was silk, saying, \"I thought I meant birth like the apparation of Summer approaching the South, but how wrong I was to give the gift of life to such a cruel woman.\" There she stands with her moon-shaven eyes against the delicately placed watermelons on the table of the street vendor selling fruit.","\n","^\"Are you going to buy anything? I'm about ready to pack up.\" the street vendor said interrupting the intense conversation we were having.","\n","^Dolores's son was originally sent to die in a some unimportant war while her daughter amounted the social ladder to become a laywer with cunning ambition. She was responsible for more executions than holidays in the Hebrew year.","\n","ev","str","^Continue","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["\n",{"->":"paragraph_5"},{"#f":5}]}],{"#f":1}],"paragraph_5":["^[SEPARATE FRAGMENT]","\n","^You spot a black notebook left on the bench by the traintracks. This is nice, to have something to discover. People should leave their belongings in public more often.","\n","^[SEPARATE FRAGMENT]","\n","^Face-down you fall onto the bed into a straight-jacket sleep. Your eyes gently-pressing to the sheets reveal all sorts of hypnagogic decompressions, decompressing into memories that you're not sure you can trust.","\n","end",{"#f":1}],"#f":1}],"listDefs":{}};
|
||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,439 @@
|
||||
(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);
|
||||
splitTag.property = splitTag.property.toUpperCase();
|
||||
|
||||
// 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);
|
||||
|
||||
imageElement.onload = () => {
|
||||
console.log(`scrollingto ${previousBottomEdge}`)
|
||||
scrollDown(previousBottomEdge)
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if paragraphText is empty
|
||||
if (paragraphText.trim().length == 0) {
|
||||
continue; // Skip empty paragraphs
|
||||
}
|
||||
|
||||
// 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 choiceTags = choice.tags;
|
||||
var customClasses = [];
|
||||
var isClickable = true;
|
||||
for(var i=0; i<choiceTags.length; i++) {
|
||||
var choiceTag = choiceTags[i];
|
||||
var splitTag = splitPropertyTag(choiceTag);
|
||||
splitTag.property = splitTag.property.toUpperCase();
|
||||
|
||||
if(choiceTag.toUpperCase() == "UNCLICKABLE"){
|
||||
isClickable = false
|
||||
}
|
||||
|
||||
if( splitTag && splitTag.property == "CLASS" ) {
|
||||
customClasses.push(splitTag.val);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
var choiceParagraphElement = document.createElement('p');
|
||||
choiceParagraphElement.classList.add("choice");
|
||||
|
||||
for(var i=0; i<customClasses.length; i++)
|
||||
choiceParagraphElement.classList.add(customClasses[i]);
|
||||
|
||||
if(isClickable){
|
||||
choiceParagraphElement.innerHTML = `<a href='#'>${choice.text}</a>`
|
||||
}else{
|
||||
choiceParagraphElement.innerHTML = `<span class='unclickable'>${choice.text}</span>`
|
||||
}
|
||||
storyContainer.appendChild(choiceParagraphElement);
|
||||
|
||||
// Fade choice in after a short delay
|
||||
showAfter(delay, choiceParagraphElement);
|
||||
delay += 200.0;
|
||||
|
||||
// Click on choice
|
||||
if(isClickable){
|
||||
var choiceAnchorEl = choiceParagraphElement.querySelectorAll("a")[0];
|
||||
choiceAnchorEl.addEventListener("click", function(event) {
|
||||
|
||||
// Don't follow <a> link
|
||||
event.preventDefault();
|
||||
|
||||
// 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";
|
||||
|
||||
// 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();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Unset storyContainer's height, allowing it to resize itself
|
||||
storyContainer.style.height = "";
|
||||
|
||||
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
|
||||
// -----------------------------------
|
||||
|
||||
// Detects whether the user accepts animations
|
||||
function isAnimationEnabled() {
|
||||
return window.matchMedia('(prefers-reduced-motion: no-preference)').matches;
|
||||
}
|
||||
|
||||
// Fades in an element after a specified delay
|
||||
function showAfter(delay, el) {
|
||||
if( isAnimationEnabled() ) {
|
||||
el.classList.add("hide");
|
||||
setTimeout(function() { el.classList.remove("hide") }, delay);
|
||||
} else {
|
||||
// If the user doesn't want animations, show immediately
|
||||
el.classList.remove("hide");
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// If the user doesn't want animations, let them scroll manually
|
||||
if ( !isAnimationEnabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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);
|
||||
@ -0,0 +1 @@
|
||||
var storyContent = {"inkVersion":21,"root":[["^Blood zigzags out in all eight directions. Acoustic aromas flood the air. Night begins to turn and rain finds its way down from the threadbare sky. In the middle of the room stands a thought, \"what had caused this tragedy?\" The body of a man was blundered to a puddy. The body of an aristocrat. The body of a lecher. He sure would taste like leather. Out the window, children run wild in sporadic grey motion.","\n","ev","str","^Continue","/str","/ev",{"*":"0.c-0","flg":20},{"c-0":["\n",{"->":"paragraph_2"},{"->":"0.g-0"},{"#f":5}],"g-0":["done",{"#f":5}]}],"done",{"paragraph_2":[["^Out of all people, you had to be the one to discover the body. You want to scream, but you can't, and you know that if you do, there'll be repercussions. The terror feels like daylight beneath your skin in the neon of night.","\n","ev","str","^Continue","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["\n",{"->":"paragraph_3"},{"#f":5}]}],{"#f":1}],"paragraph_3":[["^\"Who could have done this?\" the thought repeats its presence in the white velvet-coated study as the blood crusts slowly to a mediocre brown stain across the parquet floor.","\n","ev","str","^Continue","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["\n",{"->":"paragraph_4"},{"#f":5}]}],{"#f":1}],"paragraph_4":[["^Dmitry ran into Katya's mother, Dolores, on the way to the prison. She was silk, saying, \"I thought I meant birth like the apparation of Summer approaching the South, but how wrong I was to give the gift of life to such a cruel woman.\" There she stands with her moon-shaven eyes against the delicately placed watermelons on the table of the street vendor selling fruit.","\n","^\"Are you going to buy anything? I'm about ready to pack up.\" the street vendor said interrupting the intense conversation we were having.","\n","^Dolores's son was originally sent to die in a some unimportant war while her daughter amounted the social ladder to become a laywer with cunning ambition. She was responsible for more executions than holidays in the Hebrew year.","\n","ev","str","^Continue","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["\n",{"->":"paragraph_5"},{"#f":5}]}],{"#f":1}],"paragraph_5":["^You spot a black notebook left on the bench by the traintracks. This is nice, to have something to discover. People should leave their belongings in public more often.","\n","^Face-down you fall onto the bed into a straight-jacket sleep. Your eyes gently-pressing to the sheets reveal all sorts of hypnagogic decompressions, decompressing into memories that you're not sure you can trust.","\n","end",{"#f":1}],"#f":1}],"listDefs":{}};
|
||||
@ -0,0 +1,306 @@
|
||||
@import url('https://fonts.googleapis.com/css?family=Open+Sans:300,700|Quattrocento:700');
|
||||
|
||||
body {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-weight: lighter;
|
||||
background: white;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media screen and (prefers-reduced-motion: no-preference) {
|
||||
body.switched {
|
||||
transition: color 0.6s, background-color 0.6s;
|
||||
}
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
text-align: center;
|
||||
font-family: "Quattrocento", Georgia, 'Times New Roman', Times, serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 30pt;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 14pt;
|
||||
font-style: italic;
|
||||
font-family: sans-serif;
|
||||
font-weight: lighter;
|
||||
color: #BBB;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding-top: 3em;
|
||||
padding-bottom: 3em;
|
||||
}
|
||||
|
||||
/*
|
||||
Built-in class:
|
||||
# author: Name
|
||||
*/
|
||||
.byline {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.written-in-ink {
|
||||
z-index: 3;
|
||||
font-size: 9pt;
|
||||
font-family: sans-serif;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
position: fixed;
|
||||
display: block;
|
||||
width: 100%;
|
||||
background: white;
|
||||
margin: 0;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
height: 14px;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
@media screen and (prefers-reduced-motion: no-preference) {
|
||||
.written-in-ink {
|
||||
transition: color 0.6s, background 0.6s;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Enables <iframe> support work on itch.io when using mobile iOS
|
||||
*/
|
||||
.outerContainer {
|
||||
position: absolute;
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overflow: scroll;
|
||||
overflow-x: hidden;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
margin-top: 24px;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 980px) {
|
||||
.outerContainer {
|
||||
margin-top: 44px;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
display: block;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
padding-top: 4em;
|
||||
background: white;
|
||||
}
|
||||
|
||||
@media screen and (prefers-reduced-motion: no-preference) {
|
||||
.switched .container {
|
||||
transition: background-color 0.6s;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 13pt;
|
||||
color: #888;
|
||||
line-height: 1.7em;
|
||||
font-weight: lighter;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 700;
|
||||
color: #b97c2c;
|
||||
font-family: sans-serif;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.unclickable {
|
||||
font-weight: 700;
|
||||
color: #4f3411;
|
||||
font-family: sans-serif;
|
||||
text-decoration: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: black;
|
||||
}
|
||||
|
||||
@media screen and (prefers-reduced-motion: no-preference) {
|
||||
a {
|
||||
transition: color 0.6s;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
transition: color 0.1s;
|
||||
}
|
||||
}
|
||||
|
||||
strong {
|
||||
color: black;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.container .hide {
|
||||
opacity: 0.0;
|
||||
}
|
||||
|
||||
.container .invisible {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.container>* {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
@media screen and (prefers-reduced-motion: no-preference) {
|
||||
.container>* {
|
||||
transition: opacity 1.0s;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Class applied to all choices
|
||||
(Will always appear inside <p> element by default.)
|
||||
*/
|
||||
.choice {
|
||||
text-align: center;
|
||||
line-height: 1.7em;
|
||||
}
|
||||
|
||||
/*
|
||||
Class applied to first choice
|
||||
*/
|
||||
:not(.choice)+.choice {
|
||||
padding-top: 1em;
|
||||
}
|
||||
|
||||
/*
|
||||
Class applied to choice links
|
||||
*/
|
||||
.choice a, .choice span {
|
||||
font-size: 15pt;
|
||||
}
|
||||
|
||||
/*
|
||||
Built-in class:
|
||||
The End # CLASS: end
|
||||
*/
|
||||
.end {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
color: black;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
#controls {
|
||||
z-index: 4;
|
||||
font-size: 9pt;
|
||||
text-align: center;
|
||||
padding-bottom: 6px;
|
||||
position: fixed;
|
||||
right: 14px;
|
||||
top: 4px;
|
||||
user-select: none;
|
||||
background: white;
|
||||
}
|
||||
|
||||
@media screen and (prefers-reduced-motion: no-preference) {
|
||||
#controls {
|
||||
transition: color 0.6s, background 0.6s;
|
||||
}
|
||||
}
|
||||
|
||||
#controls [disabled] {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
#controls>*:not(:last-child):after {
|
||||
content: " | ";
|
||||
}
|
||||
|
||||
@media screen and (max-width: 980px) {
|
||||
#controls {
|
||||
z-index: 2;
|
||||
padding-top: 24px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Dark Theme (Added in Inky 0.10.0)
|
||||
# theme: dark
|
||||
*/
|
||||
|
||||
body.dark {
|
||||
background: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dark h2 {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.dark .container {
|
||||
background: black;
|
||||
}
|
||||
|
||||
.dark .written-in-ink {
|
||||
background: black;
|
||||
}
|
||||
|
||||
.dark a {
|
||||
color: #cc8f1a;
|
||||
}
|
||||
.dark .unclickable{
|
||||
color: #c4af87;
|
||||
cursor:not-allowed;
|
||||
}
|
||||
|
||||
.dark a:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media screen and (prefers-reduced-motion: no-preference) {
|
||||
.dark a {
|
||||
transition: color 0.6s;
|
||||
}
|
||||
}
|
||||
|
||||
.dark strong {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dark #controls [disabled] {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.dark .end {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dark #controls {
|
||||
background: black;
|
||||
}
|
||||
@ -0,0 +1,306 @@
|
||||
@import url('https://fonts.googleapis.com/css?family=Open+Sans:300,700|Quattrocento:700');
|
||||
|
||||
body {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-weight: lighter;
|
||||
background: white;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media screen and (prefers-reduced-motion: no-preference) {
|
||||
body.switched {
|
||||
transition: color 0.6s, background-color 0.6s;
|
||||
}
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
text-align: center;
|
||||
font-family: "Quattrocento", Georgia, 'Times New Roman', Times, serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 30pt;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 14pt;
|
||||
font-style: italic;
|
||||
font-family: sans-serif;
|
||||
font-weight: lighter;
|
||||
color: #BBB;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding-top: 3em;
|
||||
padding-bottom: 3em;
|
||||
}
|
||||
|
||||
/*
|
||||
Built-in class:
|
||||
# author: Name
|
||||
*/
|
||||
.byline {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.written-in-ink {
|
||||
z-index: 3;
|
||||
font-size: 9pt;
|
||||
font-family: sans-serif;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
position: fixed;
|
||||
display: block;
|
||||
width: 100%;
|
||||
background: white;
|
||||
margin: 0;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
height: 14px;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
@media screen and (prefers-reduced-motion: no-preference) {
|
||||
.written-in-ink {
|
||||
transition: color 0.6s, background 0.6s;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Enables <iframe> support work on itch.io when using mobile iOS
|
||||
*/
|
||||
.outerContainer {
|
||||
position: absolute;
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overflow: scroll;
|
||||
overflow-x: hidden;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
margin-top: 24px;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 980px) {
|
||||
.outerContainer {
|
||||
margin-top: 44px;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
display: block;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
padding-top: 4em;
|
||||
background: white;
|
||||
}
|
||||
|
||||
@media screen and (prefers-reduced-motion: no-preference) {
|
||||
.switched .container {
|
||||
transition: background-color 0.6s;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 13pt;
|
||||
color: #888;
|
||||
line-height: 1.7em;
|
||||
font-weight: lighter;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 700;
|
||||
color: #b97c2c;
|
||||
font-family: sans-serif;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.unclickable {
|
||||
font-weight: 700;
|
||||
color: #4f3411;
|
||||
font-family: sans-serif;
|
||||
text-decoration: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: black;
|
||||
}
|
||||
|
||||
@media screen and (prefers-reduced-motion: no-preference) {
|
||||
a {
|
||||
transition: color 0.6s;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
transition: color 0.1s;
|
||||
}
|
||||
}
|
||||
|
||||
strong {
|
||||
color: black;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.container .hide {
|
||||
opacity: 0.0;
|
||||
}
|
||||
|
||||
.container .invisible {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.container>* {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
@media screen and (prefers-reduced-motion: no-preference) {
|
||||
.container>* {
|
||||
transition: opacity 1.0s;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Class applied to all choices
|
||||
(Will always appear inside <p> element by default.)
|
||||
*/
|
||||
.choice {
|
||||
text-align: center;
|
||||
line-height: 1.7em;
|
||||
}
|
||||
|
||||
/*
|
||||
Class applied to first choice
|
||||
*/
|
||||
:not(.choice)+.choice {
|
||||
padding-top: 1em;
|
||||
}
|
||||
|
||||
/*
|
||||
Class applied to choice links
|
||||
*/
|
||||
.choice a, .choice span {
|
||||
font-size: 15pt;
|
||||
}
|
||||
|
||||
/*
|
||||
Built-in class:
|
||||
The End # CLASS: end
|
||||
*/
|
||||
.end {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
color: black;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
#controls {
|
||||
z-index: 4;
|
||||
font-size: 9pt;
|
||||
text-align: center;
|
||||
padding-bottom: 6px;
|
||||
position: fixed;
|
||||
right: 14px;
|
||||
top: 4px;
|
||||
user-select: none;
|
||||
background: white;
|
||||
}
|
||||
|
||||
@media screen and (prefers-reduced-motion: no-preference) {
|
||||
#controls {
|
||||
transition: color 0.6s, background 0.6s;
|
||||
}
|
||||
}
|
||||
|
||||
#controls [disabled] {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
#controls>*:not(:last-child):after {
|
||||
content: " | ";
|
||||
}
|
||||
|
||||
@media screen and (max-width: 980px) {
|
||||
#controls {
|
||||
z-index: 2;
|
||||
padding-top: 24px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Dark Theme (Added in Inky 0.10.0)
|
||||
# theme: dark
|
||||
*/
|
||||
|
||||
body.dark {
|
||||
background: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dark h2 {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.dark .container {
|
||||
background: black;
|
||||
}
|
||||
|
||||
.dark .written-in-ink {
|
||||
background: black;
|
||||
}
|
||||
|
||||
.dark a {
|
||||
color: #cc8f1a;
|
||||
}
|
||||
.dark .unclickable{
|
||||
color: #c4af87;
|
||||
cursor:not-allowed;
|
||||
}
|
||||
|
||||
.dark a:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media screen and (prefers-reduced-motion: no-preference) {
|
||||
.dark a {
|
||||
transition: color 0.6s;
|
||||
}
|
||||
}
|
||||
|
||||
.dark strong {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dark #controls [disabled] {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.dark .end {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dark #controls {
|
||||
background: black;
|
||||
}
|
||||
@ -0,0 +1,306 @@
|
||||
@import url('https://fonts.googleapis.com/css?family=Open+Sans:300,700|Quattrocento:700');
|
||||
|
||||
body {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-weight: lighter;
|
||||
background: white;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media screen and (prefers-reduced-motion: no-preference) {
|
||||
body.switched {
|
||||
transition: color 0.6s, background-color 0.6s;
|
||||
}
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
text-align: center;
|
||||
font-family: "Quattrocento", Georgia, 'Times New Roman', Times, serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 30pt;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 14pt;
|
||||
font-style: italic;
|
||||
font-family: sans-serif;
|
||||
font-weight: lighter;
|
||||
color: #BBB;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding-top: 3em;
|
||||
padding-bottom: 3em;
|
||||
}
|
||||
|
||||
/*
|
||||
Built-in class:
|
||||
# author: Name
|
||||
*/
|
||||
.byline {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.written-in-ink {
|
||||
z-index: 3;
|
||||
font-size: 9pt;
|
||||
font-family: sans-serif;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
position: fixed;
|
||||
display: block;
|
||||
width: 100%;
|
||||
background: white;
|
||||
margin: 0;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
height: 14px;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
@media screen and (prefers-reduced-motion: no-preference) {
|
||||
.written-in-ink {
|
||||
transition: color 0.6s, background 0.6s;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Enables <iframe> support work on itch.io when using mobile iOS
|
||||
*/
|
||||
.outerContainer {
|
||||
position: absolute;
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overflow: scroll;
|
||||
overflow-x: hidden;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
margin-top: 24px;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 980px) {
|
||||
.outerContainer {
|
||||
margin-top: 44px;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
display: block;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
padding-top: 4em;
|
||||
background: white;
|
||||
}
|
||||
|
||||
@media screen and (prefers-reduced-motion: no-preference) {
|
||||
.switched .container {
|
||||
transition: background-color 0.6s;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 13pt;
|
||||
color: #888;
|
||||
line-height: 1.7em;
|
||||
font-weight: lighter;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 700;
|
||||
color: #b97c2c;
|
||||
font-family: sans-serif;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.unclickable {
|
||||
font-weight: 700;
|
||||
color: #4f3411;
|
||||
font-family: sans-serif;
|
||||
text-decoration: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: black;
|
||||
}
|
||||
|
||||
@media screen and (prefers-reduced-motion: no-preference) {
|
||||
a {
|
||||
transition: color 0.6s;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
transition: color 0.1s;
|
||||
}
|
||||
}
|
||||
|
||||
strong {
|
||||
color: black;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.container .hide {
|
||||
opacity: 0.0;
|
||||
}
|
||||
|
||||
.container .invisible {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.container>* {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
@media screen and (prefers-reduced-motion: no-preference) {
|
||||
.container>* {
|
||||
transition: opacity 1.0s;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Class applied to all choices
|
||||
(Will always appear inside <p> element by default.)
|
||||
*/
|
||||
.choice {
|
||||
text-align: center;
|
||||
line-height: 1.7em;
|
||||
}
|
||||
|
||||
/*
|
||||
Class applied to first choice
|
||||
*/
|
||||
:not(.choice)+.choice {
|
||||
padding-top: 1em;
|
||||
}
|
||||
|
||||
/*
|
||||
Class applied to choice links
|
||||
*/
|
||||
.choice a, .choice span {
|
||||
font-size: 15pt;
|
||||
}
|
||||
|
||||
/*
|
||||
Built-in class:
|
||||
The End # CLASS: end
|
||||
*/
|
||||
.end {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
color: black;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
#controls {
|
||||
z-index: 4;
|
||||
font-size: 9pt;
|
||||
text-align: center;
|
||||
padding-bottom: 6px;
|
||||
position: fixed;
|
||||
right: 14px;
|
||||
top: 4px;
|
||||
user-select: none;
|
||||
background: white;
|
||||
}
|
||||
|
||||
@media screen and (prefers-reduced-motion: no-preference) {
|
||||
#controls {
|
||||
transition: color 0.6s, background 0.6s;
|
||||
}
|
||||
}
|
||||
|
||||
#controls [disabled] {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
#controls>*:not(:last-child):after {
|
||||
content: " | ";
|
||||
}
|
||||
|
||||
@media screen and (max-width: 980px) {
|
||||
#controls {
|
||||
z-index: 2;
|
||||
padding-top: 24px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Dark Theme (Added in Inky 0.10.0)
|
||||
# theme: dark
|
||||
*/
|
||||
|
||||
body.dark {
|
||||
background: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dark h2 {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.dark .container {
|
||||
background: black;
|
||||
}
|
||||
|
||||
.dark .written-in-ink {
|
||||
background: black;
|
||||
}
|
||||
|
||||
.dark a {
|
||||
color: #cc8f1a;
|
||||
}
|
||||
.dark .unclickable{
|
||||
color: #c4af87;
|
||||
cursor:not-allowed;
|
||||
}
|
||||
|
||||
.dark a:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media screen and (prefers-reduced-motion: no-preference) {
|
||||
.dark a {
|
||||
transition: color 0.6s;
|
||||
}
|
||||
}
|
||||
|
||||
.dark strong {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dark #controls [disabled] {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.dark .end {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dark #controls {
|
||||
background: black;
|
||||
}
|
||||
Loading…
Reference in New Issue