-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.ts
283 lines (260 loc) · 12.8 KB
/
main.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
// TYPE DEFINITIONS
type utterance = {
readonly speaker: string,
readonly text: string
readonly showUtterance?: () => boolean,
readonly dynamicText?: () => string,
readonly noTypewriter?: boolean,
readonly additionalDelay?: () => number,
}
type link = {
readonly text: string,
readonly passageTitle: string
readonly showLink?: () => boolean,
readonly dynamicText?: () => string,
readonly onLinkClick?: () => void,
readonly dynamicReference?: () => string,
readonly ignoreDebug?: boolean,
}
type passage = {
readonly utterances: utterance[],
readonly links: link[]
readonly onEnter?: () => void,
readonly onLinkRender?: () => void,
readonly onExit?: () => void,
readonly autoLink?: () => string,
readonly ignoreDebug?: boolean,
}
type passages = {
[passageTitle: string]: passage
}
let delay = baseDelay;
// given a string passageName, get the appropriate passage
let getPassage = (passageName: string) => {
if (passageName in passages) return passages[passageName];
else {
// error out if the passage doesn't exist
alert("This passage doesn't lead anywhere");
return passages[startingPassageTitle];
}
}
// reverse search, given a passage, get its title
let getPassageTitle = (passage: passage) => Object.keys(passages).find(key => passages[key] === passage);
let scrollToBottom = () => window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
// whether you are using typewriter passage rendering or not, the links are rendered in the same way through this function.
let renderLinksGeneric = (main: Element, passage: passage) => {
// Check for an autolink
let autoLinkTarget = passage.autoLink?.();
if (autoLinkTarget) {
passage.onExit?.();
onAnyExit(passage);
// if it exists, go there
renderPassageGeneric(getPassage(autoLinkTarget));
}
else {
// Autolink didn't exist or returned an empty string, let's render the links
passage.links.forEach(link => {
// render the link only if the link has no "showLink" hook or if the showLink hook passes
if (!('showLink' in link) || link.showLink!()) {
let linkElem = document.createElement("a");
linkElem.innerHTML = link.dynamicText?.() || link.text;
// Set the onclick property to render the next passage
linkElem.onclick = () => {
// don't do anything if the link has been clicked in the past (or if the link was unclicked, but part of a group of links where another one was clicked)
if (linkElem.getAttribute("class") === "clicked") return false;
if (linkElem.getAttribute("class") === "old-link") return false;
else {
link.onLinkClick?.();
// set this link as clicked
linkElem.setAttribute("class", "clicked");
// either remove all the other unclicked links, or mark them as an old links
Array.from(document.getElementsByClassName("unclicked"))
.forEach(elem => {
if (clearOldLinks) elem.remove();
else elem.setAttribute("class", "old-link")
});
// run the onExit hooks
passage.onExit?.();
onAnyExit(passage);
// render the passage this points to. Use the dynamicReference if it exists, or just the normal reference
renderPassageGeneric(getPassage(link.dynamicReference?.() || link.passageTitle));
scrollToBottom();
return false;
}
};
// set as unclicked and append to the main flow
linkElem.setAttribute("class", "unclicked");
main.appendChild(linkElem);
main.appendChild(document.createElement("br"));
main.appendChild(document.createElement("br"));
scrollToBottom();
}
});
}
// run links rendering hook
passage.onLinkRender?.();
onAnyLinkRender(passage);
}
let sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
// rendering the passage without a typewriter effect
let renderPassageSimple = async (passage: passage) => {
let main = document.getElementById("main")!;
// simple append of all the passages
for (let idx in passage.utterances) {
let utterance = passage.utterances[idx];
// render the utterance only if the utterance has no "showUtterance" hook or if the showUtterance hook passes
if (!('showUtterance' in utterance) || utterance.showUtterance!()) {
let utteranceElem = document.createElement("p");
utteranceElem.setAttribute("class", `${utterance.speaker} fade-in`);
// Use the dynamic text if it exists, else use the normal text
utteranceElem.innerHTML = utterance.dynamicText?.() || utterance.text;
main.appendChild(utteranceElem);
// if the passage has additionalDelay, delay that much as well
if ('additionalDelay' in utterance) {
console.log("ADDITIONAL DELAY NO TYPEWRITER");
await sleep(utterance.additionalDelay!());
}
}
scrollToBottom();
}
renderLinksGeneric(main, passage);
}
// render passage with a typewriter effect.
let renderPassageTypewriter = async (passage: passage) => {
let main = document.getElementById("main")!;
// unfortunately we have to use for loops because the lambdas in map don't work easily with async
// for every utterance...
for (let idx in passage.utterances) {
let utterance = passage.utterances[idx];
// render the utterance only if the utterance has no "showUtterance" hook or if the showUtterance hook passes
if (!('showUtterance' in utterance) || utterance.showUtterance!()) {
let utteranceElem = document.createElement("p");
utteranceElem.setAttribute("class", utterance.speaker);
main.appendChild(utteranceElem);
// Use the dynamic text if it exists, else use the normal text
let characters = utterance.dynamicText?.() || utterance.text;
// if noTypewriter, just set it and move on.
if (utterance.noTypewriter) {
utteranceElem.innerHTML = characters;
utteranceElem.setAttribute("class", `${utterance.speaker} fade-in`);
}
else
// for every character index...
for (let charidx = 0; charidx < characters.length; charidx++) {
// convert the innerHTML into the substring upto that index
utteranceElem.innerHTML = characters.slice(0, charidx + 1);
let character = characters[charidx];
// if the character was a comma wait a bit
if (character === ",")
await sleep(delayComma * delay);
// if the character was other punctuation, wait a bit longer
if (".:;!?-".split('').includes(character))
await sleep(delayPunctuation * delay);
// wait between characters
await sleep(delay);
scrollToBottom();
}
// wait between speakers
await sleep(delay * delayBetweenSpeakers);
// if the passage has additionalDelay, delay that much as well
if ('additionalDelay' in utterance)
await sleep(utterance.additionalDelay!());
scrollToBottom();
}
}
// render the links
renderLinksGeneric(main, passage);
}
// the entrypoint function for rendering any passage
let renderPassageGeneric = (passage: passage) => {
// run the hooks
passage.onEnter?.();
onAnyEnter(passage);
// render based on whether configuration asks for a typewriter effect or not
if (doTypewriterEffect) renderPassageTypewriter(passage);
else renderPassageSimple(passage);
}
// render the starting passage
renderPassageGeneric(passages[startingPassageTitle])
// Ensure there are no typos, hanging passages etc.
let validatePassages = () => {
let doAlertIf = (doAlert: boolean, alertMsg: string) => {
if (!doAlert) return;
else {
if (debug)
alert(`${alertMsg}
To silence this message, set "debug = false" in configuration.js or add ignoreDebug to the link / passage referenced`);
else
console.warn(alertMsg);
}
}
// all the titles except for 'empty'. Maybe this could be a configuration var, but it's fine for now
let titlesNonEmpty = Object.keys(passages)
.filter(title => title !== "empty");
titlesNonEmpty
.forEach(title => {
// for each passage...
let passage = passages[title];
// and the linkReferences in the passage
let linkReferences = passage.links
.filter(link => !link.ignoreDebug)
.map(link => link.passageTitle);
// is each linkReference included in the list of titles?
linkReferences.forEach(linkReference =>
doAlertIf(
!titlesNonEmpty.includes(linkReference),
`Passage with title "${title}" contains link that leads to "${linkReference}" which does not exist`));
});
// make a mega list of all link references
let allLinkReferences = Object
.values(passages)
.map(passage => {
let simpleLinks = passage.links.map(link => link.passageTitle);
if (simpleLinks.length === 0 && 'autoLink' in passage)
return passage.autoLink!();
else
return simpleLinks;
})
// This is a list of lists, so let's flatten it
.flat();
let allLinksAndIntro = allLinkReferences.concat([startingPassageTitle]);
// is every title accounted for in the list of all the references?
titlesNonEmpty.forEach(title =>
doAlertIf(
!(allLinksAndIntro.includes(title) || passages[title].ignoreDebug),
`No way to get to passage with title "${title}"`));
}
validatePassages();
// TEXT SPEED SLIDER
let textSpeedSlider = (document.getElementById("textSpeedSlider")! as HTMLInputElement);
let setDelayToSliderVal = () => {
// Rather than a linear speed up or down, it'll feel better if it's a curve
// https://www.desmos.com/calculator/ntlhlbmqiz
let sliderVal = parseInt(textSpeedSlider.value);
let piece1 = (x: number) => ((2 * baseDelay * (x ** 2)) / 250000) - (2 * baseDelay * x / 250) + 3 * baseDelay;
let piece2 = (x: number) => ((-1 * baseDelay * (x ** 2)) / 250000) + (baseDelay * x / 250);
// the curve is piecewise, and switches at 500 (halfway down the slider)
let pieceOfPiecewiseFunctionToUse = sliderVal < 500 ? piece1 : piece2;
delay = Math.round(pieceOfPiecewiseFunctionToUse(sliderVal));
}
textSpeedSlider.oninput = setDelayToSliderVal;
// COLOR SCHEME CHANGER
let colorSchemeChanger = document.getElementById("colorSchemeChanger")!;
let swapColorScheme = () => {
let cssElement = document.getElementById("colorSchemeCSS")!;
// what is the current theme, aka what css file is the main css pointing at?
let curTheme = cssElement.getAttribute("href")!;
// depending, decide on the new color changer icon and the new main css file
let newTheme = curTheme === "solarized-dark.css" ? "solarized-light.css" : "solarized-dark.css";
let newImg = curTheme === "solarized-dark.css" ? "imgs/moon-black.png" : "imgs/sun-warm.png";
// Actually set the values
cssElement.setAttribute("href", newTheme);
colorSchemeChanger.setAttribute("src", newImg);
}
colorSchemeChanger.onclick = swapColorScheme;
if (defaultColorScheme === "light") swapColorScheme();
document.title = title;
// Hold space to temporarily speed up
// When space is pressed, it sets delay to 0, when space is lifted, it resets delay
document.addEventListener('keydown', event => { if (event.key === " ") delay = 0; });
document.addEventListener('keyup', event => { if (event.key === " ") setDelayToSliderVal(); });