Initial skeleton
This commit is contained in:
commit
cb5edfe635
|
@ -0,0 +1,6 @@
|
||||||
|
*~
|
||||||
|
*.orig
|
||||||
|
*.rej
|
||||||
|
|
||||||
|
/node_modules
|
||||||
|
/dist
|
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"name": "work-it-out",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "",
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "CC-BY-SA-4.0",
|
||||||
|
"browser": "index.js",
|
||||||
|
"devDependencies": {
|
||||||
|
"@tsconfig/node20": "^20.1.4",
|
||||||
|
"css-loader": "^7.1.2",
|
||||||
|
"html-webpack-plugin": "^5.6.0",
|
||||||
|
"inkjs": "^2.3",
|
||||||
|
"sass": "^1.77.8",
|
||||||
|
"sass-loader": "^16.0.1",
|
||||||
|
"style-loader": "^4.0.0",
|
||||||
|
"ts-loader": "^9.5.1",
|
||||||
|
"typescript": "^5.5.4",
|
||||||
|
"webpack": "^5.94.0",
|
||||||
|
"webpack-cli": "^5.1.4",
|
||||||
|
"webpack-dev-server": "^5.0.4"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"preinstall": "npx only-allow pnpm",
|
||||||
|
"build": "webpack --mode production",
|
||||||
|
"dev": "webpack serve --mode development",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20.0.0",
|
||||||
|
"pnpm": ">= 9.0"
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,4 @@
|
||||||
|
declare module '*.ink' {
|
||||||
|
const value: string;
|
||||||
|
export default value;
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="icon" href="data:;base64,iVBORw0KGgo="><!-- Empty favicon -->
|
||||||
|
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="outerContainer">
|
||||||
|
<h3 class="written-in-ink"><a href="https://www.inklestudios.com/ink">WRITTEN IN INK</a></h3>
|
||||||
|
|
||||||
|
<div id="controls" class="buttons">
|
||||||
|
<a id="rewind" title="Restart story from beginning">restart</a>
|
||||||
|
<a id="save" title="Save progress">save</a>
|
||||||
|
<a id="reload" title="Reload from save point">load</a>
|
||||||
|
<a id="theme-switch" title="Switch theme">theme</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="story" class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>Work It Out</h1>
|
||||||
|
<h2 class="byline"></h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,434 @@
|
||||||
|
// This file was adapted to Typescript from the original
|
||||||
|
// example from https://github.com/inkle/inky/blob/858f7e545f864d114ed2bc662e0948a53461695c/app/export-for-web-template/main.js
|
||||||
|
// Copyright (c) 2016 inkle Ltd.
|
||||||
|
// Under the MIT license.
|
||||||
|
|
||||||
|
import { Compiler } from 'inkjs/compiler/Compiler';
|
||||||
|
|
||||||
|
import "./style.scss";
|
||||||
|
import data from '../story/main.ink';
|
||||||
|
|
||||||
|
const story = new Compiler(data).Compile();
|
||||||
|
|
||||||
|
// Global tags - those at the top of the ink file
|
||||||
|
// We support:
|
||||||
|
// # theme: dark
|
||||||
|
// # author: Your Name
|
||||||
|
var globalTagTheme: string | null = null;
|
||||||
|
story.globalTags?.forEach((globalTag) => {
|
||||||
|
let splitTag = splitPropertyTag(globalTag);
|
||||||
|
switch (splitTag?.property) {
|
||||||
|
case "theme":
|
||||||
|
globalTagTheme = splitTag.val;
|
||||||
|
break;
|
||||||
|
case "author":
|
||||||
|
var byline = document.querySelector('.byline')!;
|
||||||
|
byline.innerHTML = "by " + splitTag.val;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setupTheme(globalTagTheme);
|
||||||
|
|
||||||
|
var storyContainer = document.getElementById('story')!;
|
||||||
|
var outerScrollContainer = document.querySelector<HTMLElement>('.outerContainer')!;
|
||||||
|
|
||||||
|
// page features setup
|
||||||
|
var hasSave = loadSavePoint();
|
||||||
|
setupButtons(hasSave);
|
||||||
|
|
||||||
|
// Set initial save point
|
||||||
|
var 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(this: any, firstTime: boolean) {
|
||||||
|
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()!;
|
||||||
|
|
||||||
|
// Any special tags included with this line
|
||||||
|
var customClasses: string[] = [];
|
||||||
|
|
||||||
|
story.currentTags?.forEach((tag) => {
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
switch (splitTag?.property.toUpperCase()) {
|
||||||
|
// AUDIO: src
|
||||||
|
case "AUDIO":
|
||||||
|
if ('audio' in this) {
|
||||||
|
this.audio.pause();
|
||||||
|
this.audio.removeAttribute('src');
|
||||||
|
this.audio.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.audio = new Audio(splitTag.val);
|
||||||
|
this.audio.play();
|
||||||
|
break;
|
||||||
|
|
||||||
|
|
||||||
|
// AUDIOLOOP: src
|
||||||
|
case "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;
|
||||||
|
break;
|
||||||
|
|
||||||
|
// IMAGE: src
|
||||||
|
case "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;
|
||||||
|
break;
|
||||||
|
|
||||||
|
// LINK: url
|
||||||
|
case "LINK":
|
||||||
|
window.location.href = splitTag.val;
|
||||||
|
break;
|
||||||
|
|
||||||
|
|
||||||
|
// LINKOPEN: url
|
||||||
|
case "LINKOPEN":
|
||||||
|
window.open(splitTag.val);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// BACKGROUND: src
|
||||||
|
case "BACKGROUND":
|
||||||
|
outerScrollContainer.style.backgroundImage = 'url(' + splitTag.val + ')';
|
||||||
|
break;
|
||||||
|
|
||||||
|
// CLASS: className
|
||||||
|
case "CLASS":
|
||||||
|
customClasses.push(splitTag.val);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// CLEAR - removes all existing content.
|
||||||
|
case "CLEAR":
|
||||||
|
clearStory();
|
||||||
|
break;
|
||||||
|
|
||||||
|
// RESTART - clears everything and restarts the story from the beginning
|
||||||
|
case "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((choice) => {
|
||||||
|
|
||||||
|
// Create paragraph with anchor element
|
||||||
|
var customClasses: string[] = [];
|
||||||
|
var isClickable = true;
|
||||||
|
choice.tags?.forEach((choiceTag) => {
|
||||||
|
var splitTag = splitPropertyTag(choiceTag);
|
||||||
|
switch(splitTag?.property.toUpperCase()) {
|
||||||
|
|
||||||
|
case "UNCLICKABLE":
|
||||||
|
isClickable = false;
|
||||||
|
break;
|
||||||
|
|
||||||
|
|
||||||
|
case "CLASS":
|
||||||
|
customClasses.push(splitTag.val);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var choiceParagraphElement = document.createElement('p');
|
||||||
|
choiceParagraphElement.classList.add("choice");
|
||||||
|
|
||||||
|
customClasses.forEach((klass) => {
|
||||||
|
choiceParagraphElement.classList.add(klass);
|
||||||
|
});
|
||||||
|
|
||||||
|
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(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Unset storyContainer's height, allowing it to resize itself
|
||||||
|
storyContainer.style.height = "";
|
||||||
|
|
||||||
|
if (!firstTime)
|
||||||
|
scrollDown(previousBottomEdge);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearStory() {
|
||||||
|
removeAll("p");
|
||||||
|
removeAll("img");
|
||||||
|
|
||||||
|
// Comment out this line if you want to leave the header visible when clearing
|
||||||
|
setVisible(".header", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function restart() {
|
||||||
|
clearStory();
|
||||||
|
|
||||||
|
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: number, el: Element) {
|
||||||
|
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: number) {
|
||||||
|
// 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: EpochTimeStamp | null = null;
|
||||||
|
function step(time: EpochTimeStamp) {
|
||||||
|
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 as HTMLElement;
|
||||||
|
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: string) {
|
||||||
|
storyContainer.querySelectorAll<HTMLElement>(selector).forEach((el) => {
|
||||||
|
el.parentNode!.removeChild(el);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used for hiding and showing the header when you CLEAR or RESTART the story respectively.
|
||||||
|
function setVisible(selector: string, visible: boolean) {
|
||||||
|
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: string) {
|
||||||
|
var propertySplitIdx = tag.indexOf(":");
|
||||||
|
if (propertySplitIdx != null) {
|
||||||
|
var property = tag.slice(0, propertySplitIdx).trim();
|
||||||
|
var val = tag.slice(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: string | null) {
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
let preferredTheme = savedTheme ?? globalTagTheme ?? (browserDark ? "dark" : "light");
|
||||||
|
document.body.classList.add(preferredTheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used to hook up the functionality for global functionality buttons
|
||||||
|
function setupButtons(hasSave: boolean) {
|
||||||
|
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
}
|
|
@ -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,18 @@
|
||||||
|
# author: Matteo Settenvini
|
||||||
|
|
||||||
|
-> intro
|
||||||
|
|
||||||
|
=== intro ===
|
||||||
|
|
||||||
|
We were thinking that. And again, and again.
|
||||||
|
|
||||||
|
+ Maybe yes -> ending
|
||||||
|
+ Maybe not -> intro
|
||||||
|
|
||||||
|
-> ending
|
||||||
|
|
||||||
|
=== ending ===
|
||||||
|
|
||||||
|
Goodbye then.
|
||||||
|
|
||||||
|
-> END
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"extends": "@tsconfig/node20/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["es2023", "DOM"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"]
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: './src/index.ts', // Entry file
|
||||||
|
output: {
|
||||||
|
filename: 'bundle.js',
|
||||||
|
path: path.resolve(__dirname, 'dist'),
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.ts$/,
|
||||||
|
use: 'ts-loader',
|
||||||
|
exclude: /node_modules/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.ink$/,
|
||||||
|
type: 'asset/source',
|
||||||
|
exclude: /node_modules/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.s[ac]ss$/,
|
||||||
|
use: [
|
||||||
|
"style-loader", // Creates `style` nodes from JS strings
|
||||||
|
"css-loader", // Translates CSS into CommonJS
|
||||||
|
"sass-loader", // Compiles Sass to CSS
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.ts', '.js'],
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
title: "Work It Out",
|
||||||
|
template: "src/index.html",
|
||||||
|
})
|
||||||
|
],
|
||||||
|
devServer: {
|
||||||
|
static: {
|
||||||
|
directory: path.join(__dirname, 'dist'),
|
||||||
|
},
|
||||||
|
liveReload: true,
|
||||||
|
compress: true,
|
||||||
|
port: 9000,
|
||||||
|
},
|
||||||
|
};
|
Loading…
Reference in New Issue