-
Notifications
You must be signed in to change notification settings - Fork 0
/
shell.html
283 lines (246 loc) · 18.9 KB
/
shell.html
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
<!doctype html>
<html lang='en-us'>
<meta charset='utf-8'>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- solves mobile issues. see http://www.javierusobiaga.com/blog/stop-using-the-viewport-tag-until-you-know-ho/ for what initial-scale does -->
<title>Interactive Text</title>
<style>
html {height: 100%;}
body {
font-family: 'Open Sans', sans-serif;
--color-background: #fff;
--color-text: #222; /* off-black color to converge text and link luminance. */
--color-link: blue;
--color-history: #060;
--color-history-link: #009; /* links in history aren't clickable. the user has Pavlovian training to click on blue links, and is constantly surprised by nonclickable blue text. darkening fixes this problem */
background-color: var(--color-background);
color: var(--color-text);
}
/* pure black background, off-white foreground for same reason: black for highest luminance difference between background and links, and off-white to decrease the luminance difference between text and links. */
.nightmode {
--color-background: #000;
--color-text: #ddd;
--color-link: #77f;
--color-history: #8e8;
--color-history-link: #00f;
}
/* bold-only, non-blue links are hard to distinguish and unconventional */
/* a:not([href]) system actions (non-links) only */
/* we prefix the icon rather than postfix, because postfix implies a non-essential adjective, whereas prefix implies that the icon is crucial to meaning. since external links are completely different from in-game buttons, prefixes make more sense */
a {color: var(--color-link);}
a[onmousedown]:not([href]) {cursor: pointer;}
a[href] {
text-decoration: none;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12"><path fill="white" stroke="%2336c" d="M1.5 4.5h6v6h-6z"/><path d="M5.5 8.5l-2-2 2.7-2.7-1.5-1.5.8-.8h5v5l-.8.79L8.2 5.8z" fill="white" stroke="%2336c"/></svg>') left center no-repeat;
padding-left: 13px;
}
/* off-color external links are functional, but they look strange to a user who doesn't expect them, as if unintentionally inconsistent. because there's not enough space in the blue color region to separate them very much */
/* but now that [] only wraps links when keyboard shortcuts are enabled, it's necessary for clarity. */
a[href]:link {color: #36c;}
a[href]:hover, a[onmousedown]:hover {text-decoration: underline;}
.disabled_link {text-decoration: line-through;}
.disabled_link:hover {text-decoration: none;}
/* surround with brackets. used to separate options */
.ca::after {content: ']';}
.ca::before {content: '[';}
/* bold links in history have an intuitive meaning. links surrounded with brackets have unclear meaning, overlapping with the keyboard shortcuts */
.chosen_link_in_history {font-weight: bold;}
a:not([href]).history {color: var(--color-history-link);}
/* hide text so that a screenreader can find it, but not visual users */
.screenreader {
height: 1px;
width: 1px;
position: absolute;
overflow: hidden;
top: -10px;
}
/* space at beginning to work around emscripten bug, preprocessor causes error during compilation for # at beginning of line */
#history_separator {display:none;}
p {margin-bottom: 0px}
li {margin-bottom: 0px}
</style>
<!-- inverse flexbox moves the options tab order to the end of the page, so that the first tab chooses a game link, instead of an options link -->
<body id='flexboxflip' style='display:flex; min-height: 100%; margin-top:0px; margin-bottom:0px'>
<!-- role='status' is for ARIA accessibility. role='main' by itself is not re-read properly. w3c validator says one role only. note that IE doesn't support the main tag. -->
<!-- bottom margin must be 0 so that scrollTo doesn't have extra space left afterwards, which makes the scrollbar not reach the bottom -->
<div id=main_body role=status style='margin-bottom:0em; white-space:pre-wrap;' aria-live=polite>Waiting for game to load</div>
<hr id=history_separator style='margin:0.8em'>
<!-- visual testing shows that when history is displayed above the main message, both visible, then this horizontal bar is critical to helping the user find the beginning of the main message. that means when the main body fills the page, the bar should still be visible near the beginning of the page, instead of being off of it. if the user doesn't see the bar, the user worries and scrolls up to make sure he didn't miss anything. -->
<!-- since our main_body now has min_height=100vh, and the bar is not visible anymore, we don't need it to be visible under any circumstances. otherwise, to make the bar always visible, we add a scrolltarget which is slightly higher than the bar, because scrollIntoView(bar) makes the bar indistinguishable from the top of the page. -->
<!-- low opacity and "font-size: 0.9em;" let the user distinguish history from the main message. but low opacity is hard to read, and high opacity is indistinguishable from black.
the history needs to be clearly distinguishable so the user's eyes instantly see where the end is. a color change is better. blue and purple overlap with links, red sucks, yellow is too bright. orange/brown, green. -->
<ul id=history_container style='white-space:pre-wrap; display:flex; list-style:circle; padding-left:1em; margin:0; color: var(--color-history);'></ul>
<!-- options jumping around are really distracting, so they're kept fixed at the bottom or top. they can't be below the page bottom; that would cause a scrollbar. and hiding the scrollbar is bad. -->
<div style='text-align:center; margin:0.3em auto;'>Options:
<a class=ca tabindex=0 onmousedown=change_history_retention() id=history_number_option>History retention</a>
<a class=ca tabindex=0 onmousedown=flip_page_order()>Flip order</a><span class=screenreader>Reorder the main body, history, and options back-to-front.</span>
<a class=ca tabindex=0 onmousedown=invert_color()>Invert color</a><span class=screenreader>Change black/white to white/black.</span>
<a class=ca tabindex=0 onmousedown=flip_keyboard_enabled() id=keyboard_shortcuts_option>Keyboard shortcuts</a><span class=screenreader>Screenreader users should disable this setting.</span>
<span id=keyboard_rebinding_options>
rebind keys:
<a id=c0 tabindex=0 onmousedown=changekeybind(0)></a>
<a id=c1 tabindex=0 onmousedown=changekeybind(1)></a>
<a id=c2 tabindex=0 onmousedown=changekeybind(2)></a>
<a id=c3 tabindex=0 onmousedown=changekeybind(3)></a>
<a id=c4 tabindex=0 onmousedown=changekeybind(4)></a>
<a id=c5 tabindex=0 onmousedown=changekeybind(5)></a>
<a id=c6 tabindex=0 onmousedown=changekeybind(6)></a>
<a id=c7 tabindex=0 onmousedown=changekeybind(7)></a>
<a id=c8 tabindex=0 onmousedown=changekeybind(8)></a>
</span>
</div>
</body>
<script type='text/javascript'>
var stylesheet = function() {
var new_stylesheet = document.createElement('style');
document.head.appendChild(new_stylesheet);
return new_stylesheet.sheet;
}();
//helper script for css manipulation
function css(selector, property, value) {
/* selector: '#myId', '.myClass', 'thead td'
* property: 'font-size'
* value: '16px'
*/
for (var i = 0; i < document.styleSheets.length; ++i) {
var rules = document.styleSheets[i].cssRules;
for (var n in rules) {
if (rules[n].selectorText === selector) {
rules[n].style[property] = value;
return;
}
}
}
//wasn't found, so insert a new rule
stylesheet.insertRule(selector + '{' + property + ':' + value + '}', stylesheet.cssRules.length);
}
//inverts the page order
var history_above = (localStorage.getItem('history_above') === 'true');
(window.set_page_order = function() {
if (navigator.userAgent.indexOf('MSIE') !== -1 || navigator.appVersion.indexOf('Trident/') > -1)
history_above = 'true'; //IE11 workaround for poor flexbox support
//otherwise, would require annoying workarounds seen in the css linked here: https://philipwalton.github.io/solved-by-flexbox/demos/sticky-footer/
css('#history_container', 'flex-direction', 'column' + (history_above ? '-reverse' : ''));
css('#main_body', 'min-height', history_above ? '100vh' : '50vh'); //75vh preserves some of the previous scrollback to maintain context while keeping the new message at a constant position. when the history is below, it's not as useful. to have no height, use 'auto'. we choose history=100vh instead of 75vh to prevent the main message from shifting when it grows too long
css('#main_body', 'flex', history_above ? '1' : '0');
css('#history_container', 'flex', history_above ? '0' : '1');
//note: if you use auto, then the options shouldn't flip when the history flips, and should instead stay on top permanently.
css('#flexboxflip', 'flex-direction', 'column' + (history_above ? '-reverse' : ''));
})();
if (localStorage.getItem('invert_color') === 'true')
document.body.classList.toggle('nightmode');
//keyboard shortcut handling and rebinding. also, makes Enter key trigger onclick()
var keyboard_shortcuts_enabled = (localStorage.getItem('keyboard_shortcuts_enabled') === 'true');
var keycoderaw = [113, 119, 101, 114, 97, 115, 100, 102, 103]; //qwerasdf g
for (var n in keycoderaw) {
if (!(localStorage.getItem('keycode ' + n) === null))
keycoderaw[n] = localStorage.getItem('keycode ' + n).charCodeAt();
}
//enable/disable keyboard shortcuts
(window.write_keyboard_shortcut_text = function() {
keyboard_shortcuts_option.textContent = 'Keyboard shortcuts: ' + (keyboard_shortcuts_enabled ? 'on' : 'off');
for (var n in keycoderaw) {
var key_text = String.fromCharCode(keycoderaw[n]);
css('#l' + n + '::after', 'content', keyboard_shortcuts_enabled ? '\'' + key_text + '\'' : '\'\'');
css('#l' + n + '::after', 'vertical-align', keyboard_shortcuts_enabled ? 'sub' : 'initial'); //subscripts cause line space distortion even with zero text, so we reset it to initial when keybinds are disabled
css('#l' + n + '::after', 'font-size', keyboard_shortcuts_enabled ? 'smaller' : 'initial');
document.getElementById('c' + n).textContent = keyboard_shortcuts_enabled ? '[' + key_text + ']' : ''; //change-keybind link in options
}
css('.w::after', 'content', keyboard_shortcuts_enabled ? "']'" : "");
css('.w::before', 'content', keyboard_shortcuts_enabled ? "'['" : "");
css('#keyboard_rebinding_options', 'display', keyboard_shortcuts_enabled ? '' : 'none'); //visibility=hidden doesn't keep it in place. the rest of the options still shift around
localStorage.setItem('keyboard_shortcuts_enabled', keyboard_shortcuts_enabled);
})();
//history handler
var history_retention = 0; //better as 0 by default. for example, in our intro document, images persist between screens, and duplicated images are confusing.
if (!(localStorage.getItem('history_retention') === null))
history_retention = parseInt(localStorage.getItem('history_retention'));
history_number_option.textContent = 'History kept: ' + history_retention;
//set margins according to window width, and set line spacing in response to line length
//ratios change as font size changes. Open Sans looks ridiculously cramped at 16px and somewhat open at every other font size.
//measure the font.
var x_height_ratio = 0.52; //ratio of x-height to font size. 0.52 is an estimate. if the estimate is used as a default, it's better to have the default be a little too open than a little too cramped
var character_width; //how wide the average character is
var reported_font_size = parseFloat(window.getComputedStyle(main_body).fontSize); //nominal point size
var effective_font_size; //how tall and wide the font really is. 16px standard font gives 16.
var effective_font_height; //how tall the font is.
(function() {
//height measurement
var temporary_div = document.createElement('div');
document.body.appendChild(temporary_div);
temporary_div.style.height = '100ex';
var ex_pixels = temporary_div.offsetHeight;
temporary_div.style.height = '100em';
var em_pixels = temporary_div.offsetHeight;
x_height_ratio = ex_pixels / em_pixels;
document.body.removeChild(temporary_div);
//width measurement
var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');
context.font = '100px ' + window.getComputedStyle(document.body).fontFamily; //get metrics for huge-size fonts instead of actual-size fonts, because the browser ignores zoom here.
var average_width_ratio = 0.4; //ratio of letter width to font size, for the letters in the string 'Default i'. 0.4 is an estimate
average_width_ratio = context.measureText('Default i').width / 900; //divide by 9 because 'Default i' has 9 characters. use px here, em causes trouble.
character_width = average_width_ratio * reported_font_size;
//finds the true effective size of the font.
//the visual size of a font is very different from its font size. most of its size is determined by its x-height. thus, here we blend the x-height and the font size to produce a more accurate font measure.
//font size is used here instead of the more accurate (ascent + descent), in order to respect the font designer's intentions.
var x_height_proportion = 0.7; //how much the x-height should matter in determining the effective font size. the remaining proportion is determined by the font size. try testing with Open Sans (x-height 0.535), Verdana (0.545), Garamond (0.384), and TNR (0.448) since they have the highest and lowest x-heights.
var font_proportion = x_height_ratio * x_height_proportion + 1 - x_height_proportion; //blends x-height and font size to find a "visual font height" as a ratio of the stated font size.
var font_proportion_reference = 0.5 * x_height_proportion + 1 - x_height_proportion; //using the same formula, calculate the effective size of a "standard" calibration font with x-height = 0.5.
//the ratio used in conversion is (height + width / 2). we divide width by 2, because it doesn't seem to matter as much when determining the font size.
var current_font_to_standard_font_ratio = (font_proportion + average_width_ratio / 2) / (font_proportion_reference + 0.4 / 2);
effective_font_size = reported_font_size * current_font_to_standard_font_ratio;
effective_font_height = font_proportion / font_proportion_reference * reported_font_size;
})();
//we only calculate metrics once, on page load, since zooming doesn't change any of the measurements or point sizes, and presumably the fonts should not change either. (even though zooming really should change things)
//there's no cheap way to detect zoom events/font size changes.
//window.addEventListener('resize', set_ratios(document.body), false); //this leads to infinite recursion, and probably doesn't work in every browser. http://stackoverflow.com/a/2658045/
function remargin() {
/* margins */
var min_desired_line_length = 100; //in characters
var max_desired_line_length = 180; //in characters
//use innerWidth so it doesn't change when the scrollbar appears
var available_width = window.innerWidth; //in px
var available_height = window.innerHeight;
var total_area = available_width * available_height / effective_font_size / effective_font_size;
var top_margin = Math.atan(total_area / 4000) * effective_font_size * 0.3; //margin off the top if there's enough space in the page to see things without a vertical scrollbar too often
var minimum_margin = 0.7 * Math.atan(available_width / character_width / 40); //in visual arc units
var line_length_desire = available_width - minimum_margin * effective_font_size * 2; //in px
line_length_desire /= character_width; //in characters now
if (line_length_desire > min_desired_line_length) {
var additional_width = max_desired_line_length - min_desired_line_length;
line_length_desire = min_desired_line_length + additional_width * Math.atan((line_length_desire - min_desired_line_length)/additional_width/3); //approaches the max desired length at infinity. not sure what the derivative should be.
}
line_length_desire *= character_width; //in px now
var desired_margin = (available_width - line_length_desire) / 2;
//console.log(desired_margin);
css('body', 'margin-left', desired_margin + 'px');
css('body', 'margin-right', desired_margin + 'px');
css('#main_body', 'margin-top', top_margin + 'px'); //don't change the vertical margins of the <body>, those need to be 0 for flexbox to work
/* line spacing */
//the purpose of the gap between lines is to give a guide for your eyes to move back to the left.
//I think the gap should be a visual arc depending on the visual arc of the line length. it should be independent of the font height.
//however, there's an extra factor, the density of the text. high density text requires more line spacing. and narrow text is more dense. our current calculations don't consider this.
//unfortunately, what is cramped for Open Sans 16pt is open for Times New Roman. I think this is the usual zoom problem.
var element = document.body;
//all these default values are chosen by eyeballing, and without any peer-reviewed research.
var minimum_line_spacing = 0.47; //in em, for the standard font. this specifies the minimum line spacing for narrow passages.
var maximum_line_spacing = 0.66; //line spacing for very wide passages
var line_length_for_line_spacing_scaling = 30; //in em, for the standard font. when line length equals this value, the line spacing will be the average of the minimum and maximum.
var paragraph_extra_gap = 0.45; //additional paragraph gap as a multiple of (gap between lines + 0.4 of font tallness). we use part of font tallness because the paragraph gap should still be there even when the gap is zero.
var section_extra_gap = 0.9;
var line_length = element.clientWidth / effective_font_size; //find the line's length in arcminutes in the user's vision
//clientWidth is better than scrollWidth, because a horizontal scrollbar can be created by a single word whose length is larger than the containing box.
//if your box has a horizontal scrollbar by default, then scrollWidth might be more reasonable, but that is a horrible situation to design for. you don't want horizontal scrollbars.
var scaled_line_length = Math.pow(line_length / line_length_for_line_spacing_scaling, 2);
var desired_ratio = (maximum_line_spacing * scaled_line_length + minimum_line_spacing)/(scaled_line_length + 1);
var pixel_line_height = effective_font_height * 0.8 + effective_font_size * desired_ratio; //* 0.8 is because the spacing is lower than the x-height
element.style.lineHeight = (pixel_line_height / reported_font_size).toString();
css('body p', 'margin-top', (pixel_line_height - 0.4 * effective_font_height) * paragraph_extra_gap + 'px'); //margin is in addition to line-height. we add part of the font height because the x-height is smaller than the font size.
css('#history_container > li', 'margin-bottom', (pixel_line_height - 0.4 * effective_font_height) * section_extra_gap + 'px');
//when calculating visual arc, we could use overall viewport width or screen width, not clientWidth. this also helps with side-by-side windows, or websites embedded in other websites. but multi-monitors have large sections that the user doesn't bother to look at. so viewport width/client width is fine, rather than screen width. note that viewport width includes the scrollbar, we don't want that. % doesn't include the scrollbar.
}
remargin();
window.addEventListener('resize', remargin);
</script>
{{{ SCRIPT }}}
</html>