---
format:
html:
toc: true
toc-depth: 2
theme: {dark: darkly, light: cosmo}
number-sections: false
number-depth: 0
code-fold: false
code-tools: true
highlight-style: nord
---
<style></style><style>.printedClojure .sourceCode {
background-color: transparent;
border-style: none;
}
</style><style>.clay-limit-image-width .clay-image {max-width: 100%}
.clay-side-by-side .sourceCode {margin: 0}
.clay-side-by-side {margin: 1em 0}
</style>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" type="text/javascript"></script><script src="https://code.jquery.com/ui/1.13.1/jquery-ui.min.js" type="text/javascript"></script>
```{=html}
<div style="display:none;"><style>/* ---------------------------------------------------------------
Fedoras — one palette, two schemes.
Dark is the default. Quarto puts .quarto-light on the body when the
reader flips the toggle, and everything below is written against
these variables, so a scheme change is nine lines rather than three
hundred.
--------------------------------------------------------------- */
:root {
--paper: #1b1d21; /* background */
--ink: #e6e4df; /* text */
--ink-soft: #a9a49b; /* secondary text */
--rule: rgba(230, 228, 223, 0.22);
--panel: rgba(230, 228, 223, 0.05);
--violet: #9d8dff; /* apparatus, epigraphs */
--gold: #d0a44c; /* beats, commits */
--coral: #e2707c; /* moderation, rewrites */
--green: #6fcf9b; /* the Helps */
--blurple: #8b95f0; /* Discord */
--violet-rgb: 157, 141, 255;
--gold-rgb: 208, 164, 76;
--coral-rgb: 226, 112, 124;
--green-rgb: 111, 207, 155;
--blurple-rgb: 139, 149, 240;
--ink-rgb: 168, 164, 156;
--panel-hard: #101215;
}
body.quarto-light {
--paper: #faf8f4;
--ink: #1c1f24;
--ink-soft: #6b665e;
--rule: rgba(28, 31, 36, 0.22);
--panel: rgba(28, 31, 36, 0.05);
--violet: #6c5ce7;
--gold: #b0863a;
--coral: #e04f5f;
--green: #2ea078;
--blurple: #5865f2;
--violet-rgb: 108, 92, 231;
--gold-rgb: 176, 134, 58;
--coral-rgb: 224, 79, 95;
--green-rgb: 46, 160, 120;
--blurple-rgb: 88, 101, 242;
--ink-rgb: 128, 128, 128;
--panel-hard: #14161a;
}
/* ---------------------------------------------------------------
Fedoras — screenplay typography for the HTML edition.
Loaded by fedoras.screenplay/assets. Edit here, rebuild, done.
PDF styling lives in notebooks/tex/fedoras-preamble.tex instead.
--------------------------------------------------------------- */
.screenplay {
font-family: "Courier Prime", "Courier New", ui-monospace, monospace;
font-size: 1rem;
line-height: 1.35;
max-width: 62ch;
margin: 1.6rem 0 2.2rem 0;
}
.screenplay .slug {
font-weight: 700;
letter-spacing: 0.04em;
margin: 1.6rem 0 0.8rem 0;
}
.screenplay .action {
margin: 0 0 0.85rem 0;
}
.action{
white-space: pre-line;
}
.screenplay .action.beat {
margin-bottom: 0.35rem;
}
.screenplay .dialogue {
margin: 0 0 0.95rem 0;
}
.screenplay .character {
margin: 0 0 0 12ch;
font-weight: 700;
letter-spacing: 0.06em;
}
.screenplay .paren {
margin: 0 0 0 9ch;
font-style: italic;
opacity: 0.85;
}
.screenplay .lines {
margin-left: 6ch;
max-width: 50ch;
}
.screenplay .line {
margin: 0;
}
.screenplay .transition {
margin: 1.1rem 0 1.1rem 0;
text-align: right;
font-weight: 700;
letter-spacing: 0.08em;
}
.screenplay .direction {
font-style: italic;
opacity: 0.85;
margin: 0 0 0.9rem 0;
}
/* In-world text: banners, signs, whiteboards. */
.screenplay .sign {
border: 1px solid currentColor;
padding: 0.7rem 1rem;
margin: 0.9rem 0 1.1rem 4ch;
max-width: 46ch;
text-align: center;
letter-spacing: 0.05em;
}
.screenplay .sign p {
margin: 0.15rem 0;
}
.screenplay .whiteboard {
text-align: left;
}
.screenplay .whiteboard-title {
font-weight: 700;
text-transform: uppercase;
margin-bottom: 0.4rem !important;
}
.screenplay .whiteboard ol {
margin: 0;
padding-left: 2.2ch;
}
/* --- The Fedora Handbook epigraphs ------------------------------- */
.handbook {
max-width: 54ch;
margin: 1.4rem 0 2rem 2ch;
padding-left: 1.1rem;
border-left: 3px solid var(--violet);
font-style: italic;
}
.handbook .rule-number {
font-style: normal;
font-variant: small-caps;
letter-spacing: 0.08em;
opacity: 0.75;
margin-bottom: 0.25rem;
}
.handbook .rule-text {
margin: 0;
}
/* --- Drafting apparatus (hidden in the reading copy) -------------- */
.beats {
border: 1px dashed var(--gold);
background: rgba(var(--gold-rgb), 0.07);
padding: 0.6rem 0.9rem;
margin: 1.1rem 0;
max-width: 62ch;
font-size: 0.88rem;
}
.beats-title {
font-variant: small-caps;
letter-spacing: 0.1em;
font-weight: 700;
margin: 0 0 0.35rem 0;
opacity: 0.8;
}
.beats ul {
margin: 0;
padding-left: 1.4rem;
}
.beats.note {
border-color: var(--blurple);
background: rgba(var(--blurple-rgb), 0.07);
}
.status {
display: inline-block;
font-family: ui-monospace, monospace;
font-size: 0.7rem;
letter-spacing: 0.14em;
padding: 0.15rem 0.5rem;
border-radius: 3px;
margin: 0 0 0.8rem 0;
}
.status-outline { background: var(--ink-soft); color: #fff; }
.status-draft { background: var(--gold); color: #fff; }
.status-locked { background: var(--green); color: #fff; }
/* --- Live (editable) code cells ---------------------------------- */
.live-cell {
border: 1px solid rgba(var(--ink-rgb), 0.4);
border-radius: 5px;
padding: 0.7rem 0.8rem;
margin: 1.2rem 0;
max-width: 68ch;
}
.live-label {
font-size: 0.78rem;
letter-spacing: 0.09em;
text-transform: uppercase;
opacity: 0.7;
margin-bottom: 0.45rem;
}
.live-src {
width: 100%;
font-family: ui-monospace, "Courier New", monospace;
font-size: 0.9rem;
line-height: 1.4;
padding: 0.5rem;
border: 1px solid rgba(var(--ink-rgb), 0.35);
border-radius: 4px;
background: rgba(var(--ink-rgb), 0.06);
resize: vertical;
}
.live-bar {
margin-top: 0.5rem;
display: flex;
gap: 0.5rem;
align-items: center;
}
.live-run {
font: inherit;
font-size: 0.85rem;
padding: 0.25rem 0.9rem;
border-radius: 4px;
border: 1px solid rgba(var(--ink-rgb), 0.5);
background: rgba(var(--violet-rgb), 0.15);
cursor: pointer;
}
.live-run:hover { background: rgba(var(--violet-rgb), 0.3); }
.live-out {
margin: 0.5rem 0 0 0;
padding: 0.45rem 0.6rem;
min-height: 1.6rem;
font-size: 0.88rem;
white-space: pre-wrap;
border-left: 3px solid rgba(var(--violet-rgb), 0.6);
background: rgba(var(--ink-rgb), 0.06);
}
/* --- Front matter ------------------------------------------------- */
.title-page {
text-align: center;
margin: 3rem 0 2rem 0;
}
.title-page .title {
font-family: "Courier Prime", "Courier New", monospace;
font-size: 3.2rem;
letter-spacing: 0.22em;
margin: 0;
}
.title-page .subtitle {
font-family: "Courier Prime", "Courier New", monospace;
letter-spacing: 0.1em;
margin-top: 0.8rem;
opacity: 0.85;
}
h1.sidebar-title,
h2.sidebar-title {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
.personae {
max-width: 66ch;
}
.personae h3 {
font-variant: small-caps;
letter-spacing: 0.1em;
margin-top: 2rem;
border-bottom: 1px solid rgba(var(--ink-rgb), 0.35);
padding-bottom: 0.2rem;
}
.personae .who {
font-family: "Courier Prime", "Courier New", monospace;
font-weight: 700;
letter-spacing: 0.06em;
margin: 1rem 0 0.1rem 0;
}
.personae .what {
margin: 0 0 0.2rem 0;
font-style: italic;
opacity: 0.8;
}
.personae .desc {
margin: 0.15rem 0 0 0;
max-width: 60ch;
}
/* --- The reader's protagonist ------------------------------------ */
.pc-name {
font-weight: inherit;
border-bottom: 1px dotted rgba(var(--violet-rgb), 0.55);
}
.screenplay .slug,
.screenplay .transition,
.screenplay .character { text-transform: uppercase; }
.name-field {
border: 1px solid rgba(var(--violet-rgb), 0.55);
border-radius: 5px;
padding: 0.9rem 1rem;
margin: 1.6rem auto;
max-width: 60ch;
}
.name-label { margin: 0 0 0.6rem 0; }
.name-inputs { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.name-inputs input {
font: inherit;
padding: 0.3rem 0.6rem;
border: 1px solid rgba(var(--ink-rgb), 0.45);
border-radius: 4px;
background: rgba(var(--ink-rgb), 0.06);
max-width: 14ch;
}
.name-note {
font-size: 0.82rem;
opacity: 0.7;
margin: 0.6rem 0 0 0;
}
.name-label,
.name-confirm {
font-size: 1.2rem;
}
.name-inputs input,
.name-inputs button {
font: inherit;
font-size: 1.2rem;
}
/* --- Artifacts ---------------------------------------------------- */
.artifact {
font-family: ui-monospace, "Courier New", monospace;
font-size: 0.88rem;
line-height: 1.45;
max-width: 66ch;
margin: 1.4rem 0;
border-radius: 5px;
overflow: hidden;
}
.artifact p { margin: 0.1rem 0; }
/* Discord */
.discord { border: 1px solid rgba(var(--blurple-rgb), 0.5); }
.discord-head {
background: rgba(var(--blurple-rgb), 0.14);
padding: 0.4rem 0.8rem;
display: flex; gap: 0.8rem;
}
.discord-server { font-weight: 700; letter-spacing: 0.06em; }
.discord-channel { opacity: 0.75; }
.discord-body { padding: 0.7rem 0.8rem; }
.discord-msg { margin-bottom: 0.7rem; }
.discord-who { font-weight: 700; }
.discord-role {
font-size: 0.7rem; letter-spacing: 0.08em;
background: rgba(var(--violet-rgb), 0.25);
padding: 0.05rem 0.4rem; border-radius: 3px; margin-left: 0.4rem;
}
.discord-at { opacity: 0.55; margin-left: 0.5rem; font-size: 0.78rem; }
.discord-reacts { margin-top: 0.2rem !important; }
.react {
display: inline-block; font-size: 0.75rem;
border: 1px solid rgba(var(--ink-rgb),0.35); border-radius: 10px;
padding: 0 0.45rem; margin-right: 0.3rem;
}
/* Moderation log */
.modlog {
border-left: 3px solid var(--coral);
background: rgba(var(--coral-rgb), 0.08);
padding: 0.6rem 0.9rem;
font-size: 0.82rem;
letter-spacing: 0.03em;
}
/* Git */
.commit {
border: 1px solid rgba(var(--ink-rgb), 0.4);
padding: 0.7rem 0.9rem;
background: rgba(var(--ink-rgb), 0.05);
}
.commit-hash { color: var(--gold); font-weight: 700; }
.commit-meta { opacity: 0.7; }
.commit-msg { margin-top: 0.5rem; padding-left: 1.5rem; }
/* Terminal */
.terminal {
background: var(--panel-hard);
color: var(--ink);
padding: 0.7rem 0.9rem;
border: 1px solid rgba(var(--ink-rgb),0.35);
}
.term-in { color: var(--green); }
.term-in::before { content: ""; }
.term-out { opacity: 0.9; }
/* AI */
.aichat { border: 1px solid rgba(var(--green-rgb), 0.5); }
.ai-model {
background: rgba(var(--green-rgb), 0.16);
padding: 0.35rem 0.8rem !important;
font-weight: 700; letter-spacing: 0.1em; font-size: 0.75rem;
}
.ai-system { padding: 0.4rem 0.8rem; font-size: 0.8rem; opacity: 0.85; }
.ai-system pre { white-space: pre-wrap; margin: 0.4rem 0 0 0; }
.ai-body { padding: 0.6rem 0.8rem; }
.ai-turn { margin-bottom: 0.6rem; }
.ai-role { font-size: 0.68rem; letter-spacing: 0.12em; opacity: 0.6; }
.ai-assistant .ai-text { border-left: 2px solid rgba(46,160,120,0.6); padding-left: 0.6rem; }
/* Email */
.email { border: 1px solid rgba(var(--ink-rgb),0.45); padding: 0.7rem 0.9rem; }
.email-head td { padding: 0 0.6rem 0.1rem 0; vertical-align: top; font-size: 0.82rem; }
.email-head td:first-child { opacity: 0.6; }
.email-body { margin-top: 0.6rem; }
/* Handouts */
.handout {
border: 1px solid currentColor;
padding: 1rem 1.2rem;
font-family: "Courier Prime", "Courier New", monospace;
}
.handout-title {
font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase;
border-bottom: 1px solid currentColor; padding-bottom: 0.4rem;
margin-bottom: 0.7rem !important;
}
.handout-table { width: 100%; font-size: 0.85rem; margin: 0.5rem 0; }
.handout-table td.k { opacity: 0.65; width: 34%; padding-right: 1rem; vertical-align: top; }
/* --- Prose between scenes ---------------------------------------- */
.prose {
max-width: 64ch;
margin: 1.5rem 0 2rem 0;
font-family: "Iowan Old Style", "Palatino Linotype", Palatino,
"Book Antiqua", Georgia, serif;
font-size: 1.06rem;
line-height: 1.65;
color: var(--ink);
}
.prose p {
margin: 0 0 1.05rem 0;
}
.prose p:first-child { margin-top: 0; }
.prose p:last-child { margin-bottom: 0; }
blockquote.turn {
max-width: 64ch;
border-left: 3px solid rgba(var(--violet-rgb), 0.6);
padding-left: 1rem;
margin: 1.4rem 0 0.6rem 0;
}
.name-confirm {
margin: 0.7rem 0 0 0;
font-weight: 600;
}
/* --- The d20 ------------------------------------------------------ */
.roll {
border: 1px solid rgba(var(--violet-rgb), 0.5);
border-radius: 5px;
padding: 0.7rem 0.9rem;
margin: 1.2rem 0;
max-width: 60ch;
font-family: ui-monospace, "Courier New", monospace;
}
.roll-head {
display: flex;
justify-content: space-between;
font-size: 0.75rem;
letter-spacing: 0.12em;
opacity: 0.75;
margin-bottom: 0.5rem;
}
.roll-bar { display: flex; gap: 0.7rem; align-items: center; margin-top: 0.3rem; }
.roll-go, .roll-rewrite {
font: inherit;
font-size: 0.82rem;
padding: 0.25rem 0.9rem;
border-radius: 4px;
border: 1px solid rgba(var(--ink-rgb), 0.5);
background: rgba(var(--violet-rgb), 0.15);
cursor: pointer;
}
.roll-rewrite { background: rgba(var(--coral-rgb), 0.12); }
.roll-go:hover { background: rgba(var(--violet-rgb), 0.3); }
.roll-rewrite:hover { background: rgba(var(--coral-rgb), 0.25); }
.roll-value { font-size: 0.9rem; letter-spacing: 0.05em; }
.roll-out {
margin: 0.6rem 0 0 0 !important;
font-family: "Courier Prime", "Courier New", monospace;
min-height: 1.2rem;
}
.roll-echo { font-size: 0.8rem; opacity: 0.8; }
.roll-note { font-size: 0.8rem; opacity: 0.6; margin: 0.4rem 0 0 0 !important; }
.roll-foot {
font-size: 0.72rem;
letter-spacing: 0.1em;
opacity: 0.55;
margin: 0.5rem 0 0 0 !important;
border-top: 1px solid rgba(var(--ink-rgb),0.25);
padding-top: 0.35rem;
}
.roll-cue {
margin: 0.6rem 0 0 4ch !important;
font-family: "Courier Prime", "Courier New", monospace;
font-weight: 700;
letter-spacing: 0.06em;
}
.roll-line {
margin: 0 0 0 6ch !important;
max-width: 40ch;
font-family: "Courier Prime", "Courier New", monospace;
}
.roll-out .roll-line:first-child { margin-left: 0 !important; max-width: none; }
.companion-name {
font-family: "Courier Prime", "Courier New", monospace;
font-weight: 700;
letter-spacing: 0.14em;
font-size: 1.05rem;
margin: 0.7rem 0 0.3rem 0 !important;
}
.companion-intro { max-width: 58ch; margin: 0 0 0.3rem 0 !important; }
.session { max-width: 60ch; margin: 1.1rem 0; }
.session-who {
font-family: "Courier Prime", "Courier New", monospace;
font-weight: 700; letter-spacing: 0.12em; margin: 0 0 0.3rem 0 !important;
}
.session-text { margin: 0 !important; }
.ac-name { border-bottom: 1px dotted rgba(var(--violet-rgb), 0.55); }
.companion-naming { margin-top: 0.6rem; }
/* --- Plots -------------------------------------------------------- */
.plot {
border: 1px solid rgba(var(--violet-rgb), 0.5);
border-radius: 5px;
padding: 0.7rem 0.9rem;
margin: 1.2rem 0;
max-width: 62ch;
font-family: ui-monospace, "Courier New", monospace;
}
.plot-controls {
display: flex; gap: 1rem; flex-wrap: wrap;
font-size: 0.8rem; opacity: 0.85; margin: 0.4rem 0;
}
.plot-controls input {
font: inherit; font-size: 0.8rem; width: 7ch;
padding: 0.15rem 0.35rem;
border: 1px solid rgba(var(--ink-rgb),0.45);
border-radius: 3px;
background: rgba(var(--ink-rgb),0.06);
}
.plot-status { font-size: 0.75rem; letter-spacing: 0.1em; opacity: 0.7; }
.plot canvas {
display: block;
margin-top: 0.6rem;
background: var(--paper);
border: 1px solid rgba(var(--ink-rgb),0.3);
border-radius: 3px;
}
.roll-go:disabled, .roll-rewrite:disabled {
opacity: 0.4;
cursor: default;
}
.sketch {
border: 1px solid rgba(var(--violet-rgb), 0.5);
border-radius: 5px;
padding: 0.7rem 0.9rem;
margin: 1.2rem 0;
max-width: 62ch;
font-family: ui-monospace, "Courier New", monospace;
}
.sketch canvas {
display: block;
margin-top: 0.7rem;
background: var(--paper);
border: 1px solid rgba(var(--ink-rgb),0.3);
border-radius: 3px;
max-width: 100%;
}
.sketch-page {
margin-top: 0.7rem;
padding: 0.5rem 0.6rem;
border: 1px solid rgba(var(--ink-rgb),0.3);
border-radius: 3px;
background: rgba(var(--ink-rgb),0.04);
font-family: ui-monospace, "Courier New", monospace;
font-size: 0.9rem;
}
.sketch-page button {
font: inherit; font-size: 0.85rem;
border: 1px solid rgba(var(--ink-rgb),0.45);
border-radius: 3px;
background: rgba(var(--ink-rgb),0.08);
cursor: pointer;
min-width: 2ch;
}
/* --- The page itself, when Quarto is not doing it for us ---------- */
.screenplay, .artifact, .prose, .handout, .beats, .roll, .plot, .sketch,
.todo, .puzzle-box, .personae, .name-field, .live-cell, .session {
color: var(--ink);
}
.handout, .puzzle-cell, .sign { border-color: var(--rule); }
.terminal { background: var(--panel-hard); color: var(--ink); }
.title-page .title, .title-page .subtitle { color: var(--ink); }
/* --- Form controls -------------------------------------------------
Inputs, textareas and buttons do not inherit colour from the page,
so every one of them needs telling, or the reader types black on
black and cannot see what they are writing.
------------------------------------------------------------------- */
.name-inputs input,
.companion-input,
.todo-input,
.plot-controls input,
.live-src,
.sketch textarea,
.sketch-page input {
color: var(--ink);
background: var(--panel);
border-color: var(--rule);
caret-color: var(--ink);
}
.name-inputs input::placeholder,
.companion-input::placeholder,
.todo-input::placeholder,
.sketch-page input::placeholder,
.live-src::placeholder {
color: var(--ink-soft);
opacity: 1;
}
.roll-go, .roll-rewrite, .live-run, .todo-del,
.sketch-page button, .puzzle-cell {
color: var(--ink);
}
.sketch-page button {
background: var(--panel);
border-color: var(--rule);
}
/* Quarto's own search box does not follow the book's palette on every
theme, so it is told here as well. */
#quarto-search input,
#quarto-search .aa-Input,
.aa-Form,
.aa-InputWrapper input,
.quarto-search input {
background: var(--panel) !important;
color: var(--ink) !important;
border-color: var(--rule) !important;
}
.aa-Form { border: 1px solid var(--rule) !important; }
#quarto-search input::placeholder,
.aa-Input::placeholder {
color: var(--ink-soft) !important;
opacity: 1;
}
.aa-DetachedSearchButton, .quarto-search-button {
background: var(--panel) !important;
color: var(--ink) !important;
border-color: var(--rule) !important;
}
/* Canvases inherit nothing, so the sketch cells read their ink off the
element's own resolved colour. This is the line that gives them one. */
.sketch canvas, .plot canvas {
color: var(--ink);
background: transparent;
}
.tribute-heading {
margin: 2rem 0 0.5rem;
font-variant: small-caps;
font-weight: 600;
letter-spacing: 0.04em;
}
.tribute p {
margin: 0.6rem 0;
}
.tribute-meta {
margin-top: 2rem;
padding-top: 1rem;
border-top: 1px solid var(--bs-border-color);
text-align: right;
font-size: 0.85rem;
}
.tribute-meta p {
margin: 0.25rem 0;
}
.tribute-date {
font-style: italic;
}
.tribute-attribution {
font-size: 1rem;
}
.tribute-source {
margin-top: 0.75rem !important;
}
.footnote {
margin-top: 1.5rem;
font-size: 0.9rem;
color: var(--bs-secondary);
}
/* ---------------------------------------------------------------
The three endings.
All three are in the HTML. Which one is visible is a class on
<body>, written once by fedoras.reader/paint-standing! from the
reader's standing, so a chapter that forks does it here rather than
in nine lines of DOM work per passage.
A page with no class on it shows the middle one. That is what the
print edition gets, and it is what the table actually got, and the
PDF reads straight through with no gap and no note.
--------------------------------------------------------------- */
.ending { display: none; }
.ending-reconciled { display: block; }
body.standing-closed .ending-reconciled { display: none; }
body.standing-outstanding .ending-reconciled { display: none; }
body.standing-closed .ending-closed { display: block; }
body.standing-outstanding .ending-outstanding { display: block; }
@media print {
.ending { display: none !important; }
.ending-reconciled { display: block !important; }
}
/* ---------------------------------------------------------------
The slips.
--------------------------------------------------------------- */
.slip {
margin: 1.2rem 0 1.2rem 4ch !important;
padding-left: 1rem;
max-width: 46ch;
border-left: 2px solid rgba(var(--coral-rgb), 0.55);
font-family: "Courier Prime", "Courier New", monospace;
font-style: italic;
opacity: 0.8;
white-space: pre-wrap;
}
#quarto-margin-sidebar { display: none; }
.scene-code {
border: 1px solid var(--rule);
border-radius: 3px;
padding: 0.6rem 0.8rem;
margin: 0.9rem 0 1.1rem 0;
max-width: 62ch;
font-family: ui-monospace, "Courier New", monospace;
font-size: 0.88rem;
line-height: 1.45;
background: var(--panel);
color: var(--ink);
overflow-x: auto;
}
.scene-code pre { margin: 0; }
.scene-code code { background: none; padding: 0; }
/* Pressing Enroll flashes the name it confirms, the same name or a new
one, so the press can be seen to have done something. */
.name-confirm .pc-name.pc-flash {
animation: pc-flash 1.4s ease-out;
}
@keyframes pc-flash {
0%, 30% { color: var(--green); }
}</style><script src="scittle.js" type="application/javascript"></script><div style="display:none;"><script type="application/x-scittle">(ns fedoras.carry
"What the reader carries between chapters."
(:require [clojure.string :as str]))
(def ^:private book-key
"The registration on the front of the file. It is not a secret and it
is not pretending to be one — it is the number this is keyed to, and
it is the same number a reader has been looking at since August."
3812)
;; ---------------------------------------------------------------------
;; Bytes
;; ---------------------------------------------------------------------
(defn- to-bytes [s] (.encode (js/TextEncoder.) s))
(defn- from-bytes [b] (.decode (js/TextDecoder. "utf-8") b))
(defn- bytes->str [b]
(loop [i 0 acc ""]
(if (>= i (.-length b))
acc
(recur (inc i) (str acc (js/String.fromCharCode (aget b i)))))))
(defn- str->bytes [s]
(let [out (js/Uint8Array. (count s))]
(dotimes [i (count s)] (aset out i (.charCodeAt s i)))
out))
;; ---------------------------------------------------------------------
;; The keystream
;;
;; The same small xorshift the dice and the floors use, so there is one
;; generator in this project and not four.
;; ---------------------------------------------------------------------
(defn- keystream [n seed]
(let [out (js/Uint8Array. n)]
(loop [i 0 x (bit-or 1 (bit-and seed 0x7fffffff))]
(if (>= i n)
out
(let [x (bit-and (bit-xor x (bit-shift-left x 13)) 0x7fffffff)
x (bit-and (bit-xor x (unsigned-bit-shift-right x 17)) 0x7fffffff)
x (bit-and (bit-xor x (bit-shift-left x 5)) 0x7fffffff)]
(aset out i (bit-and x 0xff))
(recur (inc i) x))))))
(defn- mask [b seed]
(let [k (keystream (.-length b) seed)
out (js/Uint8Array. (.-length b))]
(dotimes [i (.-length b)] (aset out i (bit-xor (aget b i) (aget k i))))
out))
;; ---------------------------------------------------------------------
;; Base64, url-safe
;; ---------------------------------------------------------------------
(defn- b64 [b]
(-> (js/btoa (bytes->str b))
(str/replace "+" "-")
(str/replace "/" "_")
(str/replace "=" "")))
(defn- un-b64 [s]
(let [t (-> s (str/replace "-" "+") (str/replace "_" "/"))
pad (case (mod (count t) 4) 2 "==" 3 "=" "")]
(str->bytes (js/atob (str t pad)))))
;; ---------------------------------------------------------------------
;; Rolls, short
;; ---------------------------------------------------------------------
(defn- pack-rolls
"[roll mod rewrites], shortest form. A roll with no modifier and no
rewrites is one number."
[rolls]
(into {}
(for [[id {:keys [roll mod rewrites]}] rolls
:when roll]
[id (cond
(pos? (or rewrites 0)) [roll (or mod 0) rewrites]
(not (zero? (or mod 0))) [roll mod]
:else [roll])])))
(defn- unpack-rolls
"Every shape this has ever emitted: a bare number, the short vector,
and the verbose map that went out while `pack-rolls` was sitting here
unused. Tokens from before this change still read."
[m]
(into {}
(for [[id v] m]
[(clojure.core/name id)
(cond
(number? v) {:roll v :mod 0 :rewrites 0}
(map? v) {:roll (:roll v)
:mod (or (:mod v) 0)
:rewrites (or (:rewrites v) 0)}
:else {:roll (nth v 0)
:mod (nth v 1 0)
:rewrites (nth v 2 0)})])))
;; ---------------------------------------------------------------------
;; The token
;; ---------------------------------------------------------------------
(defn- checksum [b]
(loop [i 0 s 7]
(if (>= i (.-length b))
(mod s 251)
(recur (inc i) (mod (+ (* s 31) (aget b i)) 251)))))
(defn pack
"A map to a token. The keys are one letter each because this goes in
a query string thirty times a page."
[{:keys [name companion echo rolls deeds epoch todo]}]
(try
(let [payload (js/JSON.stringify
(clj->js
(cond-> {}
name (assoc :n name)
companion (assoc :a [(:n companion) (:given companion)])
(pos? (or echo 0)) (assoc :e echo)
(seq rolls) (assoc :r (pack-rolls rolls))
(seq deeds) (assoc :d (mapv clojure.core/name deeds))
;; when the reader last withdrew the whole record.
;; it travels because the alternative is that only
;; the state travels, and then a link from before
;; the withdrawal quietly reinstates it.
(pos? (or epoch 0)) (assoc :g epoch)
(seq todo) (assoc :t [(:seq todo 0) (:items todo)]))))
body (mask (to-bytes payload) book-key)
out (js/Uint8Array. (inc (.-length body)))]
(aset out 0 (checksum body))
(dotimes [i (.-length body)] (aset out (inc i) (aget body i)))
(b64 out))
(catch :default _ nil)))
(defn unpack
"A token back to a map, or nil if anybody has been at it.
An absent field comes back empty rather than missing, which is fine
and is not a licence: the caller merges this forward and knows that an
empty Echo means the token is old, not that the Echo is nought."
[s]
(try
(when-not (str/blank? s)
(let [raw (un-b64 s)
want (aget raw 0)
body (.slice raw 1)]
(when (= want (checksum body))
(let [m (js->clj (js/JSON.parse (from-bytes (mask body book-key)))
:keywordize-keys true)]
{:name (:n m)
:companion (when-let [a (:a m)] {:n (first a) :given (or (second a) "")})
:echo (or (:e m) 0)
:rolls (unpack-rolls (or (:r m) {}))
:deeds (mapv keyword (or (:d m) []))
:epoch (or (:g m) 0)
:todo (when-let [t (:t m)] {:seq (first t) :items (or (second t) [])})}))))
(catch :default _ nil)))
;; ---------------------------------------------------------------------
;; When it does not reconcile
;; ---------------------------------------------------------------------
(defn complain! []
(js/console.warn
"016.09 — entry does not reconcile and has not been carried forward. Retained."))</script><script type="application/x-scittle">(ns fedoras.reader
"Everything the reader owns.
One atom: containing Name, animal, echo, fortune, score, deeds. A widget
reads it and never touches storage directly, and every change goes through
one of the setters, which writes, re-reads, and lets the watches fire."
(:require [clojure.string :as str]
[fedoras.carry :as carry]))
;; ---------------------------------------------------------------------
;; Storage
;; ---------------------------------------------------------------------
(def prefix "fedoras.")
(def carry-param :f)
(def forget-param
"A page that loads with this on it withdraws its own record and stops.
It is a separate field from the token because it must work on a page
that has never seen a token."
:forget)
(def withdrawn-param
"Survives exactly one reload, to say the thing was done. The state it
would otherwise have been recorded in has just been deleted, which is
the one place a confirmation cannot live."
:withdrawn)
(def legacy-params
"The fields the query string used to carry, one per fact. Stripped off
any link that still has them. There was a second copy of this list
called `carried-params` and the two had drifted."
[:pc :pcl :ac :acn :echo :fortune :score])
(defn- read-json [s]
(js->clj (js/JSON.parse s) :keywordize-keys true))
(defn- write-json [v]
(js/JSON.stringify (clj->js v)))
(defn stored [key]
(try
(when-let [s (.getItem js/localStorage (str prefix (name key)))]
(read-json s))
(catch :default _ nil)))
(defn store! [key val]
(try (.setItem js/localStorage (str prefix (name key)) (write-json val))
(catch :default _ nil))
val)
(defn remove!
"Take something out rather than writing an empty one over it, because
a stored nil and a missing key read back the same and only one of them
is what was meant."
[key]
(try (.removeItem js/localStorage (str prefix (clojure.core/name key)))
(catch :default _ nil)))
(defn storage-works?
"Whether anything at all is going to be persisted.
On a file:// path some browsers give every page its own opaque origin,
which means localStorage throws and the whole book forgets the reader
the moment they click anything. Worth knowing rather than guessing."
[]
(try
(.setItem js/localStorage (str prefix "probe") "1")
(let [ok (= "1" (.getItem js/localStorage (str prefix "probe")))]
(.removeItem js/localStorage (str prefix "probe"))
ok)
(catch :default _ false)))
;; ---------------------------------------------------------------------
;; The query string, which is a transport and nothing else
;; ---------------------------------------------------------------------
(defn- current-url [] (js/URL. (.-href js/location)))
(defn param [key]
(try
(let [v (.get (.-searchParams (current-url)) (name key))]
(when-not (str/blank? v) v))
(catch :default _ nil)))
(defn set-params! [m]
(try
(let [u (current-url) sp (.-searchParams u)]
(doseq [[k v] m]
(if (or (nil? v) (= "" v))
(.delete sp (name k))
(.set sp (name k) (str v))))
(.replaceState js/history nil "" (.toString u)))
(catch :default _ nil)))
(defn- int-or [v d]
(let [n (js/parseInt (str v) 10)] (if (js/isNaN n) d n)))
;; ---------------------------------------------------------------------
;; The atom
;; ---------------------------------------------------------------------
(def default-name ["Owen" "Mercer"])
(def state (atom nil))
(def animal-names
"Number to the animal's grammar: what it is called, what more than one
of them is called, what a ledger would file it under. Filled in by
`fedoras.state/assets` from a later script tag, so it is empty for the
moment between this file being read and the roster arriving, and
nothing here may treat an empty roster as a missing roll."
(atom {}))
(defn- as-grammar
"One roster entry, whatever shape it turned up in.
The page used to be handed {1 \"Pigeon\"} and is now handed
{1 {:animal \"Pigeon\" :plural \"pigeons\" ...}}, and a book whose
`names-form` has not caught up with its roster hands over one of each.
A bare string is an animal with no grammar, which is precisely what it
was before, so it becomes one here.
This is normalised at the boundary rather than at each use because the
alternative was a `merge` three functions away receiving a string,
throwing `conj on a map takes map entries`, and taking down every
render watch on the page — a build that is one function out of step
should read a little worse, not stop."
[v]
(cond
(map? v) v
(string? v) {:animal v}
:else {}))
(defn- read-companion []
(let [s (stored :companion)
n (int-or (first s) nil)]
(when (and n (<= 1 n 20))
(let [given (or (nth s 2 nil) "")
g (as-grammar (get @animal-names n))
;; the animal's name is a convenience and the roster may not
;; have landed yet. the number is the fact.
animal (or (:animal g) (nth s 1 nil) "")
named? (not (str/blank? given))]
(merge {:plural "" :taxonomy "" :collective "" :other-names []}
g
{:n n
:animal animal
:given given
:named? named?
:name (cond named? (str/trim given)
(not (str/blank? animal)) animal
:else (str "d20 " n))})))))
(defn- read-name []
(let [s (stored :pc)]
[(or (first s) (first default-name))
(or (second s) (second default-name))]))
(defn refresh!
"Total on purpose. Every render watch in the book hangs off this atom,
so anything that throws in here blanks the whole page, and what it
reads is JSON that a previous version of this file wrote, which is not
a thing to be optimistic about. A key that is the wrong shape reads as
absent."
[]
(reset! state
{:name (read-name)
:companion (try (read-companion) (catch :default _ nil))
:echo (int-or (stored :echo) 0)
:rolls (let [r (stored :rolls)] (if (map? r) r {}))
:deeds (let [d (stored :deeds)]
(set (map keyword (if (sequential? d) d []))))
:epoch (int-or (stored :epoch) 0)
:todo (let [t (stored :todo)] (if (sequential? t) t []))
:todo-seq (int-or (stored :todo-seq) 0)}))
;; ---------------------------------------------------------------------
;; What a chapter may ask for
;; ---------------------------------------------------------------------
(defn reader-name [] (:name @state))
(defn companion [] (:companion @state))
(defn echo [] (:echo @state))
(defn deeds [] (:deeds @state))
(defn fortune [] (count (:deeds @state)))
(defn epoch
"When this reader last withdrew the whole record, in seconds, or
nought if they never have.
It is not a clock and nothing displays it. It exists so that a
withdrawal can be compared against a link: a token stamped earlier
than this was written by somebody who has since been forgotten, and it
is not allowed to put them back."
[]
(:epoch @state))
(defn standing
"Fortune less echo. Added to every roll for the rest of the book, and
the number the last chapter reads to decide how it ends."
[]
(- (fortune) (echo)))
(defn registration [] "3812")
;; ---------------------------------------------------------------------
;; And what it may change
;; ---------------------------------------------------------------------
(defn set-name! [f l]
(store! :pc [(if (str/blank? f) (first default-name) (str/trim f))
(if (str/blank? l) (second default-name) (str/trim l))])
(refresh!))
(defn set-companion!
"Three cells: the number, the animal's own name as a courtesy, and
whatever the reader called it. The number is the only one that has to
be right, because the other two are recovered from the roster."
[n given]
(store! :companion [n (get-in @animal-names [n :animal] "") (or given "")])
(refresh!))
(defn set-echo! [n] (store! :echo (max 0 n)) (refresh!))
(defn add-echo!
"One more thing taken back."
[]
(set-echo! (inc (echo))))
(defn did!
"Record a deed. A set, so doing the same thing twice is doing it once,
and nothing anywhere says that it happened."
[k]
(store! :deeds (mapv name (conj (deeds) (keyword k))))
(refresh!))
(defn animals!
"The roster's grammar, from the page. Normalised on the way in, so the
shape `fedoras.state/names-form` happens to be emitting this week is
something only this function has an opinion about.
Refreshes, because a companion read before this arrived is holding an
empty animal name."
[m]
(reset! animal-names
(if (map? m)
(into {} (map (fn [[k v]] [k (as-grammar v)])) m)
{}))
(refresh!))
(defn rolls
"Every check the reader has resolved, by id.
Normalised on the way out. Storage keywordizes keys on read, and a
check id is an arbitrary string rather than a name, so the keys come
back as keywords and nothing matches them. And anything that is not a
map at all becomes an empty one, because assoc on a vector with a
string key takes the page down."
[]
(let [r (:rolls @state)]
(if (map? r)
(into {} (map (fn [[k v]] [(name k) v])) r)
{})))
(defn roll-entry [id]
(when-let [e (get (rolls) id)]
{:roll (or (:roll e) (get e "roll"))
:mod (or (:mod e) (get e "mod") 0)
:rewrites (or (:rewrites e) (get e "rewrites") 0)}))
(defn remember-roll! [id e]
(store! :rolls (assoc (rolls) id e))
(refresh!))
(defn score []
(+ (reduce + 0 (keep :roll (vals (rolls))))
(or (:n (companion)) 0)))
;; ---------------------------------------------------------------------
;; Import, once, on arrival
;; ---------------------------------------------------------------------
(def carries-epoch?
"Whether the codec on this page round-trips a withdrawal stamp."
(try
(= 12345 (:epoch (carry/unpack
(carry/pack {:name ["Owen" "Mercer"] :epoch 12345}))))
(catch :default _ false)))
(defn- wipe!
"Everything under the prefix, and then one thing: when that happened."
[at]
(try
(let [ks (vec (for [i (range (.-length js/localStorage))
:let [k (.key js/localStorage i)]
:when (and k (str/starts-with? k prefix))]
k))]
(doseq [k ks] (.removeItem js/localStorage k)))
(catch :default _ nil))
(store! :epoch at))
(defn- merge-token!
[{:keys [name companion echo rolls deeds epoch todo]}]
(let [ours (int-or (stored :epoch) 0)
theirs (or epoch 0)]
(cond
;; A link from before the reader withdrew the record. It is a true
;; account of somebody who does not exist any more, which is not
;; the same thing as an instruction to rebuild them. This is the
;; case that made `forget!` a lie: it emptied one page's storage
;; and every other page, and every entry in the back history, was
;; still holding a complete copy and handed it straight back.
;; A link from before the reader withdrew the record. It is a true
;; account of somebody who does not exist any more, which is not
;; the same thing as an instruction to rebuild them.
;;
;; Only when the codec can say so. A page that cannot read a stamp
;; off a token must not conclude from its absence that every link
;; in the book is stale.
(and carries-epoch? (< theirs ours))
(js/console.info
(str "016.09 — the link this page was opened from was written before the record "
"was withdrawn. Nothing carried forward. Reach this chapter from a page you "
"have opened since, and whatever is current will arrive with you."))
:else
(do
;; They withdrew it somewhere this page has not heard about yet.
;; The erasure travels the way the state travels, which is the
;; only way anything travels in a book read off a disk.
(when (and carries-epoch? (> theirs ours))
(wipe! theirs)
(js/console.info
"016.09 — prior record withdrawn at the reader's request. Nothing carried forward."))
;; the name is the one thing a reader can retype in five seconds,
;; so the token is allowed to have an opinion about it
(when (seq name)
(store! :pc name))
;; The animal:
;;
;; This was write-once — take the token's only if there is
;; nothing here — which is right on one origin and wrong on a
;; folder of files, where every page has its own storage and the
;; token is the only thing that crosses. A reader who opened Act
;; II after rolling and before naming taught that page the horse,
;; permanently, and Act II could never afterwards learn that the
;; horse was called Horace.
;;
;; The Echo is the recency stamp. It only ever goes up, and a
;; reroll raises it, so a token carrying an Echo at least as
;; large as this page's is at least as new as this page and is
;; allowed to speak. One older, and it is a link from before the
;; reroll and is not.
(let [have (read-companion)
here-echo (int-or (stored :echo) 0)
there-echo (or echo 0)]
(when (and (:n companion)
(or (nil? have) (>= there-echo here-echo)))
(store! :companion
[(:n companion) ""
;; a blank name in a token is a reader who had not
;; named it yet, not a reader who unnamed it
(if (str/blank? (:given companion))
(or (:given have) "")
(:given companion))])))
;; upward only, always. after the companion, which reads it.
(store! :echo (max (int-or (stored :echo) 0) (or echo 0)))
;; a check is resolved once, and what is already in this browser
;; was rolled by this reader, so it wins every collision
(when (seq rolls)
(let [named (fn [m] (if (map? m)
(into {} (map (fn [[k v]] [(clojure.core/name k) v])) m)
{}))]
(store! :rolls (merge (named rolls) (named (stored :rolls))))))
;; deeds are a set, and a set does not get smaller by being described
(when (seq deeds)
(let [held (let [d (stored :deeds)] (if (sequential? d) d []))]
(store! :deeds (vec (into (set (map clojure.core/name held))
(map clojure.core/name deeds))))))
(when-let [t todo]
(when (>= (:seq t 0) (int-or (stored :todo-seq) 0))
(store! :todo (:items t))
(store! :todo-seq (:seq t))))))))
(defn- import-token! []
(if-let [t (param carry-param)]
(if-let [m (try (fedoras.carry/unpack t) (catch :default _ nil))]
(merge-token! m)
(try (fedoras.carry/complain!) (catch :default _ nil)))
;; nothing to do, unless somebody has a link from before the token
(do
(when-let [pc (param :pc)]
(store! :pc [pc (or (param :pcl) (second default-name))]))
(when-let [ac (param :ac)]
(let [n (int-or ac nil)]
(when (and n (<= 1 n 20) (nil? (read-companion)))
(store! :companion [n "" (or (param :acn) "")]))))
(when-let [e (param :echo)]
(store! :echo (max (int-or (stored :echo) 0) (int-or e 0)))))))
(defn- withdraw-if-asked!
"A page loaded with a withdrawal stamp on it clears itself and does
nothing else.
This is how one button reaches twelve documents that cannot see each
other. The page the reader clicked opens each of the others in a
hidden frame with the stamp in the query string, and each one runs
this on the way up. Nothing is scripted across the boundary — every
file is its own origin and none of them can touch another's storage —
but a document can always clear its own, and a query string is the one
thing that crosses.
Returns true if this load was a withdrawal, so boot knows to skip the
rest of its work."
[]
(when-let [at (some-> (param forget-param) (int-or nil))]
(when (> at (int-or (stored :epoch) 0))
(wipe! at))
true))
;; ---------------------------------------------------------------------
;; Rendering
;; ---------------------------------------------------------------------
(defn on-render! [k f] (add-watch state k (fn [_ _ _ _] (f))) (f))
(defn el [id] (.getElementById js/document id))
(defn q [node selector] (when node (.querySelector node selector)))
(defn q-all
"Every match, as a vector. One argument searches the document; two
searches inside a node.
Written the long way because a NodeList is not a Clojure collection
and array-seq is not in Scittle's bindings."
([selector] (q-all js/document selector))
([node selector]
(if-not node
[]
(let [nodes (.querySelectorAll node selector)]
(mapv (fn [i] (.item nodes i)) (range (.-length nodes)))))))
(defn text! [node selector s]
(when-let [t (q node selector)] (set! (.-textContent t) s)))
(defn on-click! [node selector f]
(when-let [b (q node selector)] (set! (.-onclick b) f)))
(defn enable! [node selector on? label]
(when-let [b (q node selector)]
(set! (.-disabled b) (not on?))
(when label (set! (.-textContent b) label))))
(defn show! [node selector on?]
(when-let [t (q node selector)]
(set! (.-display (.-style t)) (if on? "flex" "none"))))
(defn widget [k id f]
(on-render! k (fn [] (when-let [node (el id)] (f node)))))
;; ---------------------------------------------------------------------
;; Withdrawing the record, everywhere
;;
;; Twelve documents, each its own origin, none able to read another's
;; storage. Every shared channel is shut: localStorage and IndexedDB are
;; origin-scoped, window.name is cleared on the way across, cookies do
;; not apply to a file, and an iframe gets its own origin so the parent
;; cannot script it.
;;
;; It can still LOAD it. A hidden frame pointed at a sibling chapter,
;; with a stamp in the query string, runs that chapter's own copy of
;; this file, which clears that chapter's own storage. Nothing crosses
;; the boundary except the URL, which is the one thing that was ever
;; going to.
;; ---------------------------------------------------------------------
(defn- sibling-pages
"Every other chapter this page links to.
Read off the sidebar rather than configured, because Quarto already
puts the whole book in it and a hand-kept list of filenames is a list
that goes stale the first time a chapter is renamed."
[]
(let [here (.-pathname js/location)
dir (subs here 0 (inc (.lastIndexOf here "/")))]
(->> (q-all "a[href]")
(keep (fn [a]
(let [h (.getAttribute a "href")]
(when (and h
(not (str/starts-with? h "#"))
(not (re-find #"(?i)^(https?:|mailto:|javascript:)" h)))
(try
(let [u (js/URL. (.-href a))
p (.-pathname u)]
(when (and (str/starts-with? p dir)
(str/ends-with? (str/lower-case p) ".html")
(not= p here))
(str (.-protocol u) "//" (.-host u) p)))
(catch :default _ nil))))))
distinct
vec)))
(defn- sweep!
"Tell every other chapter to withdraw, then call `done` once.
Once, whatever happens: a frame that never fires must not leave the
reader looking at a button that has stopped meaning anything, so there
is a deadline as well as a count."
[at done]
(let [pages (sibling-pages)
fired (atom false)
finish (fn [] (when-not @fired (reset! fired true) (done)))]
(if (empty? pages)
(finish)
(let [box (.createElement js/document "div")
left (atom (count pages))
one (fn [] (when (zero? (swap! left dec)) (finish)))]
(js/console.info "016.09 — withdrawing from" (count pages) "other chapters")
(set! (.-style box)
"position:absolute;width:0;height:0;overflow:hidden;border:0;visibility:hidden")
(.appendChild (.-body js/document) box)
(doseq [p pages]
(let [f (.createElement js/document "iframe")]
(set! (.-onload f) (fn [_] (one)))
(set! (.-onerror f) (fn [_] (one)))
(set! (.-src f) (str p "?" (clojure.core/name forget-param) "=" at))
(.appendChild box f)))
(js/setTimeout finish 5000)))))
(defn forget!
"Forget everything, not the six things somebody remembered to list,
and not only on this page.
Emptying this document's storage was never the whole job: the reader
was also sitting, complete, in the query string of every link on every
other chapter and in every entry of the back history, and the next
navigation put them back. So this clears here, stamps when, and then
visits every other chapter to say so. The stamp is what makes a link
written before the withdrawal refuse to be believed afterwards.
It also says so while it is happening. It used to do all of this in
silence and then reload, which is indistinguishable from a button that
does nothing except reload."
[]
(let [at (quot (.now js/Date) 1000)
btn (q js/document ".pc-forget")]
(when btn
(set! (.-disabled btn) true)
(set! (.-textContent btn) "Withdrawing the record…"))
(wipe! at)
(sweep! at
(fn []
(let [u (current-url)]
(doseq [k (concat [carry-param forget-param] legacy-params)]
(.delete (.-searchParams u) (clojure.core/name k)))
(.set (.-searchParams u) (clojure.core/name withdrawn-param) "1")
(set! (.-href js/location) (.toString u)))))))
(defn- note-withdrawal!
"One line, on the way back up, and then the button goes back to
offering. The confirmation cannot be stored, because storing things is
what has just been undone — so it is read off the query string once,
before anything else in boot is allowed to tidy the query string away."
[withdrew?]
(when withdrew?
(js/console.info
"016.09 — record withdrawn from this chapter and from every chapter linked from it.")
(when-let [b (q js/document ".pc-forget")]
(set! (.-textContent b) "Record withdrawn")
(js/setTimeout (fn [] (set! (.-textContent b) "Forget everything")) 5000))))
;; ---------------------------------------------------------------------
;; Export
;; ---------------------------------------------------------------------
(defn token
"What this reader looks like from outside. Recomputed rather than
stored, so it cannot disagree with the state it is describing.
Guarded, because this runs on every render and inside `decorate-links!`,
and a codec that throws on one unexpected key should cost the reader a
shareable link and nothing else."
[]
(try
(let [{:keys [name companion echo rolls deeds todo todo-seq]} @state]
(fedoras.carry/pack
{:name name
:companion (when companion (select-keys companion [:n :given]))
:echo echo
:rolls rolls
:deeds deeds
:epoch (epoch)
:todo (when (seq todo) {:seq (or todo-seq 0) :items todo})}))
(catch :default e
(js/console.warn "fedoras: could not pack the carry token —" e)
nil)))
(defn decorate-links!
"Every internal link carries one field.
Two things were wrong before this and both of them mattered. It ran
inline, at parse time, so any link below the script tag did not exist
yet — which is every prev-and-next arrow Quarto puts in the footer.
And it copied the query string rather than the state, so a page opened
from storage handed on nothing."
[]
(when-let [t (token)]
(doseq [a (q-all "a[href]")]
(let [h (.getAttribute a "href")]
(when (and h
(not (str/starts-with? h "#"))
(not (re-find #"(?i)^(https?:|mailto:|javascript:)" h)))
(try
(let [lu (js/URL. (.-href a))
sp (.-searchParams lu)]
(doseq [k legacy-params] (.delete sp (clojure.core/name k)))
(.set sp (clojure.core/name carry-param) t)
(set! (.-href a) (.toString lu)))
(catch :default _ nil)))))))
(defn- rewrite-address!
"Put the token in this page's own address bar and take the old fields
off it, so a reader who arrived on a legacy link stops looking at
their own Echo."
[]
(when-let [t (token)]
(set-params! (into {(clojure.core/name carry-param) t}
(map (fn [k] [k nil])
(concat [forget-param withdrawn-param] legacy-params))))))
;; ---------------------------------------------------------------------
;; Tokens
;;
;; It used to be a chain of str/replace calls, which is why {pc-last}
;; worked in a scene and rendered as the literal characters {pc-last}
;; inside a dice passage.
;;
;; Case follows the token: {ac} is `ted`, {Ac} is `Ted`, {AC} is `TED`,
;; so a passage can start a sentence or write a cue line without the
;; author reaching for a second token. Note that this makes {PC} in an
;; already-written scene render as OWEN rather than Owen; the old regex
;; was case-insensitive and threw the information away.
;;
;; An unkown token is passed over: A brace in a passage that is not
;; something this understands comes out exactly as it went in, so this
;; is safe to run over arbitrary prose.
;; ---------------------------------------------------------------------
(def ^:private token-re #"\{([A-Za-z]+)(-[A-Za-z-]+)?\}")
(defn- apply-case [head s]
(cond
(str/blank? s) s
(= head (str/upper-case head)) (str/upper-case s)
(= (subs head 0 1) (str/upper-case (subs head 0 1)))
(str (str/upper-case (subs s 0 1)) (subs s 1))
:else s))
(defn render
"Replace every {token} the resolver knows. `resolve` is called with the
lower-cased head and the lower-cased tail — \"ac\" and \"-plural\" for
{ac-plural} — and returns a string or nil."
[resolve s]
(str/replace (or s "") token-re
(fn [m]
(let [[whole head tail] (if (string? m) [m m nil] m)]
(or (some->> (resolve (str/lower-case head)
(some-> tail str/lower-case))
(apply-case head))
whole)))))
(defn- string-hash
"Stable across loads, which is the point: the same sentence picks the
same alternative name every time the reader opens the page, so nothing
reads as though it were rolled twice."
[s]
(reduce (fn [a i] (mod (+ (* 31 a) (.charCodeAt s i)) 1000003))
7 (range (count s))))
(defn- an [s]
(str (if (re-find #"(?i)^[aeiou]" (str s)) "an " "a ") s))
(defn- animal-tokens
"What a passage may say about the animal. The reader's own name for it
is a name; everything else is the species, because a reader who called
a marmot Ted did not thereby decide that two of them are Teds."
[source tail]
(when-let [{:keys [animal plural taxonomy collective other-names name named?]}
(companion)]
(let [kind (str/lower-case (or animal ""))]
(case (or tail "-name")
"-name" (if named? name (str "the " kind))
"-kind" kind
"-a" (an kind)
"-plural" (if (str/blank? plural) (str kind "s") (str/lower-case plural))
"-many" (if (str/blank? collective)
(str "several " (if (str/blank? plural) (str kind "s") plural))
(str (an collective) " of "
(if (str/blank? plural) (str kind "s") plural)))
"-taxonomy" (str/lower-case (or taxonomy kind))
"-given" (if named? name "")
"-other" (if (seq other-names)
(nth other-names (mod (string-hash source) (count other-names)))
(str "the " kind))
nil))))
(defn- reader-tokens [source]
(let [[f l] (reader-name)]
(fn [head tail]
(case head
"pc" (case (or tail "-first")
"-first" f
"-full" (str f " " l)
"-last" l
nil)
"ac" (or (animal-tokens source tail)
;; no roll yet, and a passage still has to read
(case (or tail "-name")
("-name" "-kind" "-other") "the animal"
"-a" "an animal"
"-plural" "animals"
"-many" "several animals"
"-taxonomy" "animal"
"-given" ""
nil))
nil))))
(defn fill-with
"Put the reader into a passage, plus whatever else the caller knows.
The dice box passes the die in this way."
[extra s]
(let [own (reader-tokens s)]
(render (fn [h t] (or (extra h t) (own h t))) s)))
(defn fill
"Put the reader into a passage. The only place any of these are
resolved."
[s]
(fill-with (fn [_ _] nil) s))
(defn animal-label
"Kept for anything still asking. The display name, as stored, not
shouted — the old one upper-cased and every caller lower-cased it
again somewhere else."
[]
(when-let [c (companion)] (:name c)))
;; ---------------------------------------------------------------------
;; The carry-over
;;
;; What a reader takes to the next book.
;; ---------------------------------------------------------------------
(def on-the-record
"What goes in the token. Everything else is furniture.
A key is on this list because it is small and because losing it would
lose the reader's year. Adding one is a decision about how long every
link on every page is going to be, so it is a list and not a
convention."
#{:pc :companion :echo :rolls :deeds :todo})
(defn kept
"Read something the reader left in a room. No widget that calls this
needs to know anything about tokens."
[k]
(stored k))
(defn keep!
"Leave something in a room. It stays where it was made."
[k v]
(store! k v)
(when (contains? on-the-record k) (refresh!))
v)
(def ^:private alphabet "23456789ABCDEFGHJKLMNPQRSTUVWXYZ")
(defn- b32 [n len]
(loop [n n i 0 out ""]
(if (= i len)
out
(recur (quot n 32) (inc i)
(str (nth alphabet (mod n 32)) out)))))
;; ---------------------------------------------------------------------
;; The name and the animal, on every page
;; ---------------------------------------------------------------------
(defn- paint-name []
(let [[f l] (reader-name)]
(doseq [node (q-all ".pc-name")]
(set! (.-textContent node)
(apply-case (or (.getAttribute node "data-case") "pc")
(case (.getAttribute node "data-pc")
"-full" (str f " " l) "-last" l f))))
(when-let [i (el "pc-first")] (when (str/blank? (.-value i)) (set! (.-value i) f)))
(when-let [i (el "pc-last")] (when (str/blank? (.-value i)) (set! (.-value i) l)))))
(defn- paint-companion
"Each span carries the token that made it, so {ac-plural} written in a
scene at build time still says `marmots` after the reader rolls a
marmot. It used to carry nothing and every span got the same word."
[]
(doseq [node (q-all ".ac-name")]
(let [tail (or (.getAttribute node "data-ac") "-name")
head (or (.getAttribute node "data-case") "ac")]
(set! (.-textContent node)
(or (some->> ((reader-tokens (or (.-textContent node) "")) "ac" tail)
(apply-case head))
"the animal")))))
(defn- paint-tokens []
(doseq [node (q-all ".tok")]
(set! (.-textContent node) (fill (.getAttribute node "data-tok")))))
;; ---------------------------------------------------------------------
;; Ending / Standing
;; ---------------------------------------------------------------------
(def deed-roll
[:nerve ;; a natural twenty with the Echo at nought
:named-it ;; the animal was given a name and kept it
:kept-the-programmes ;; both sheets went in the bag and stayed there
:no-name-for-the-box ;; nothing was spoken into the undertaker's box
:read-it-back ;; the ledger was read against the room out loud
:opened-the-cage
:held-the-door
:went-back-for-it
:signed-nothing
:walked-out
:told-a-new-one ;; Ariathne was paid in something that was theirs
:said-why]) ;; the chancellor asked, and got a true answer
(def ^:private deed-bits 15)
(defn- deed-mask []
(let [held (deeds)]
(reduce (fn [acc [index deed]]
(if (contains? held deed)
(+ acc (bit-shift-left 1 index))
acc))
0
(map-indexed vector deed-roll))))
(defn- deeds-from-mask [mask]
(set (keep-indexed (fn [index deed]
(when (pos? (bit-and mask (bit-shift-left 1 index)))
deed))
deed-roll)))
(def band-code {:closed 0 :reconciled 1 :outstanding 2})
(def code-band {0 :closed 1 :reconciled 2 :outstanding})
(defn ending-band []
(let [reader-standing (standing)]
(cond
(>= reader-standing 4) :outstanding
(>= reader-standing 0) :reconciled
:else :closed)))
(defn- paint-standing!
"One class on the body, so a chapter that forks three ways does it in
the stylesheet instead of in nine lines of DOM work per passage. A page
with no class on it shows :reconciled, which is what the print edition
gets and what the table actually got.
This existed and was never wired to anything, which meant every
`sp/by-standing` on every page was rendering all three endings at
once, or none."
[]
(when-let [body (.-body js/document)]
(let [classes (.-classList body)]
(doseq [old ["standing-closed" "standing-reconciled" "standing-outstanding"]]
(.remove classes old))
(.add classes (str "standing-" (clojure.core/name (ending-band)))))))
(def code-version 3)
(defn- check-char [body]
(nth alphabet
(mod (reduce + (map (fn [c] (.indexOf alphabet c)) body)) 32)))
(defn carry-code
"What this reader looks like to the next book, as an accession line."
[]
(let [n (+ (* code-version 4294967296)
(* (get band-code (ending-band) 1) 1073741824)
(* (min 31 (or (:n (companion)) 0)) 33554432)
(* (min 31 (echo)) 1048576)
(* (deed-mask) 32)
(min 31 (quot (score) 10)))
body (b32 n 7)]
(str "016.14 · " (registration) " · "
(subs body 0 4) "-" (subs body 4) (check-char body))))
(defn read-carry
"The other side of it, for the book after this one."
[s]
(let [cleaned (-> (str s) str/upper-case (str/replace #"[^2-9A-Z]" ""))
tail (subs cleaned (max 0 (- (count cleaned) 8)))]
(when (= 8 (count tail))
(let [body (subs tail 0 7)
n (reduce (fn [acc c] (+ (* 32 acc) (.indexOf alphabet c))) 0 body)]
(when (= (nth tail 7) (check-char body))
{:version (quot n 4294967296)
:band (get code-band (mod (quot n 1073741824) 4))
:companion (mod (quot n 33554432) 32)
:echo (mod (quot n 1048576) 32)
:deeds (deeds-from-mask (mod (quot n 32) (bit-shift-left 1 deed-bits)))
:score-floor (* 10 (mod n 32))})))))
;; ---------------------------------------------------------------------
;; Is any of this working
;;
;; Type fedoras.reader.diagnose_BANG_() in the console. Faster than
;; opening the storage inspector and reading JSON, and it says whether
;; the token agrees with the storage, which is the failure that cost an
;; animal.
;; ---------------------------------------------------------------------
(defn diagnose! []
(let [t (param carry-param)]
(js/console.log "storage works: " (storage-works?))
(js/console.log "origin: " (.-origin js/location)
(if (= "file:" (.-protocol js/location))
"— every page may have its own storage"
"— one bucket for the whole book"))
(js/console.log "name: " (pr-str (reader-name)))
(js/console.log "companion: " (pr-str (companion)))
(js/console.log " raw storage: " (pr-str (stored :companion)))
(js/console.log "roster loaded: " (count @animal-names) "animals")
(js/console.log "echo / fortune: " (echo) "/" (fortune) " → standing" (standing))
(js/console.log "withdrawn at: " (if (pos? (epoch)) (epoch) "never")
(if carries-epoch?
""
"— CODEC DROPS THE STAMP, carry.cljs is out of date"))
(js/console.log "rolls: " (pr-str (rolls)))
(js/console.log "token on url: " (or t "none"))
(when t
(js/console.log " unpacks to: "
(pr-str (try (fedoras.carry/unpack t)
(catch :default e (str "THREW: " e))))))
(js/console.log "token we'd emit:" (or (token) "none — pack failed"))
:ok))
;; A handle on `window`, because Scittle keeps its namespaces inside the
;; SCI context and the browser console cannot see into it. There is no
;; `fedoras.reader.diagnose_BANG_()` to call — that is the shape a
;; compiled build would have, and this is an interpreted one.
;;
;; fedoras.diagnose()
;; fedoras.forget()
;;
;; and for anything else, scittle.core.eval_string("(fedoras.reader/...)")
(set! (.-fedoras js/window)
(js-obj "diagnose" (fn [] (diagnose!))
"state" (fn [] (clj->js @state))
"token" (fn [] (token))
"forget" (fn [] (forget!))))
;; ---------------------------------------------------------------------
;; Boot
;; ---------------------------------------------------------------------
(if (withdraw-if-asked!)
;; a hidden frame, doing one job. no token, no links, no painting.
(refresh!)
;; read before anything registers a watch: `on-render!` calls its
;; function once on the spot, and the links watch tidies the query
;; string, so the flag is gone by the time the DOM is ready
(let [withdrew? (boolean (param withdrawn-param))]
(when-not carries-epoch?
(js/console.warn
(str "fedoras: carry.cljs does not round-trip the withdrawal stamp. "
"`forget!` will clear this chapter and every chapter linked from it, "
"but a stale link will not be refused afterwards. "
"Update resources/fedoras/carry.cljs — `pack` needs :epoch in its "
"destructuring and :g in its payload, and `unpack` needs :epoch back out.")))
(import-token!)
(refresh!)
(on-render! ::name paint-name)
(on-render! ::companion paint-companion)
(on-render! ::tokens paint-tokens)
(on-render! ::standing paint-standing!)
(on-render! ::links (fn [] (decorate-links!) (rewrite-address!)))
(if (= "loading" (.-readyState js/document))
(.addEventListener js/document "DOMContentLoaded"
(fn [_]
(note-withdrawal! withdrew?)
(decorate-links!)
(paint-standing!)))
(do (note-withdrawal! withdrew?)
(decorate-links!)))))</script><script type="application/x-scittle">(fedoras.reader/animals! {1 {:animal "Pigeon", :plural "pigeons", :taxonomy "bird", :collective "kit", :other-names ["dusty wings" "coooo"]}, 2 {:animal "Giant Centipede", :plural "centipedes", :taxonomy "arthropod", :collective "colony", :other-names ["clicker" "legs gooo"]}, 3 {:animal "Marmot", :plural "marmots", :taxonomy "rodent", :collective "madness", :other-names ["the alarm" "the fat rat"]}, 4 {:animal "Cat", :plural "cats", :taxonomy "felid", :collective "clowder", :other-names ["the cat" "cat"]}, 5 {:animal "Turkey", :plural "turkeys", :taxonomy "fowl", :collective "rafter", :other-names ["thanksgiving dinner for non vegans" "gobbler"]}, 6 {:animal "Giant Badger", :plural "badgers", :taxonomy "mustelid", :collective "cete", :other-names ["angry grandpa" "stripes"]}, 7 {:animal "Giant Weasel", :plural "weasels", :taxonomy "mustelid", :collective "sneak", :other-names ["the scout" "slenderman" "the ermine"]}, 8 {:animal "Fox", :plural "foxes", :taxonomy "canid", :collective "skulk", :other-names ["foxxxy" "spry foot"]}, 9 {:animal "Wolverine", :plural "wolverines", :taxonomy "mustelid", :collective "gang", :other-names ["the small trouble"]}, 10 {:animal "Dog", :plural "dogs", :taxonomy "canid", :collective "pack", :other-names ["the dog" "the ordinary dog"]}, 11 {:animal "Python", :plural "pythons", :taxonomy "serpent", :collective "knot", :other-names ["ssSSssSsss" "coilssss"]}, 12 {:animal "Mule", :plural "mules", :taxonomy "equid", :collective "barren", :other-names ["dapple" "HeeHaw :("]}, 13 {:animal "Lynx", :plural "lynxes", :taxonomy "felid", :collective "clowder", :other-names ["tufts" "yelper"]}, 14 {:animal "Ape", :plural "apes", :taxonomy "primate", :collective "shrewdness", :other-names ["treecrawler"]}, 15 {:animal "Horse", :plural "horses", :taxonomy "equid", :collective "harras", :other-names ["rocinante" "the one nobody rides"]}, 16 {:animal "Moose", :plural "moose", :taxonomy "cervid", :collective "herd", :other-names ["its a giant moose"]}, 17 {:animal "Wyrmling", :plural "wyrmlings", :taxonomy "draconic", :collective "clutch", :other-names ["the wyrmling" "scales"]}, 18 {:animal "Falcon", :plural "falcons", :taxonomy "raptor", :collective "cast", :other-names ["the raptor" "true sight"]}, 19 {:animal "Wolf", :plural "wolves", :taxonomy "canid", :collective "pack", :other-names ["the wolf" "the packleader"]}, 20 {:animal "Bear", :plural "bears", :taxonomy "ursid", :collective "sloth", :other-names ["the bear" "the citadel"]}})</script></div></div>
```
# Scene 5 — Held
```{=html}
<span style="display:none;"></span>
```
```{=html}
<blockquote class="handbook"><p class="rule-number">The Fedora Handbook, Rule #22</p><p class="rule-text">“Do not tell a man what you know. Tell him what he already said, and let him hear it in his own voice.”</p></blockquote>
```
---
```{=html}
<div class="scene"><div class="screenplay"><p class="action">Review is the third week of May. Nobody works. Everybody works. The reading room is full at four in the morning for six nights and the Commons never closes, and there is a fortnight of the year in which this institution genuinely resembles the thing on the banner, and it is the same fortnight it was in December.</p></div><div class="artifact email"><table class="email-head"><tbody><tr><td>From:</td><td><span>Office of the Registrar <no-reply@newcarthage.edu></span></td></tr><tr><td>To:</td><td><span><span class="pc-name" data-case="pc" data-pc="-full">Owen Mercer</span> <<span class="pc-name" data-case="pc" data-pc="-first">Owen</span>@newcarthage.edu></span></td></tr><tr><td>Date:</td><td>22 May, 4:00 AM</td></tr><tr><td>Subject:</td><td><strong><span>Year — closed</span></strong></td></tr></tbody></table><div class="email-body"><p><span>Review is complete. Your standing is: no action.</span></p><p><span>Provisional standing is discharged. Your standing from the first day of next term is: enrolled.</span></p><p><span>Set three is marked. Set two assignment six is marked separately and is recorded as accepted.</span></p><p><span>Enrolment for next year is confirmed. Enrolment stands at 1,024.</span></p><p><span>Summer arrangements are attached separately.</span></p><p><span>Congratulations on the completion of your first year.</span></p></div></div><div class="screenplay"><p class="action">Provisional is discharged, which is the thing he came here to do, and which he has not thought about since about the second week of February.</p><p class="action">He reads it twice looking for the sentence about Meridian and there isn't one.</p></div><div class="screenplay"><p class="slug">INT. ASHCROFT HOUSE — HIS ROOM — A TUESDAY, LATE</p></div><div class="screenplay"><p class="action">The map on the wall by the door is the one every room has. Buildings as rectangles, the loop in gold, and fourteen little circles that are not explained anywhere.</p><p class="action beat">He has walked past it about four hundred times.</p><p class="action">What he notices, at some point after midnight and for no reason he could give you, is that it is a list. Eleven boxes, each with a corner and a width and a height, and somebody typed them in.</p><p class="action">And if it is a list of boxes, then standing anywhere on it and asking what is in front of you is arithmetic.</p></div><div class="screenplay"><p class="transition">Blackout.</p></div><div class="screenplay"><p class="action">Meridian Hall, and the Annexe under it, and the gap between them that is four feet wide and has a plant door at the back of it.</p><p class="action">The same spot, turned to face Whitlock.</p><p class="action">It is the map. There is nothing in the picture that is not in the map, and he has not drawn a single building, and that is the part he sits with.</p><p class="action">Yours. Arrows to turn and to walk. It will not let you into a building, because a wall is a wall, and it says what you are looking at and how far off it is.</p></div><div><style>/* ---------------------------------------------------------------
The widget shell. Every cs_class widget sits in one of these, so
anything here is a decision about all of them at once.
--------------------------------------------------------------- */
.widget {
border: 1px solid rgba(var(--violet-rgb), 0.5);
border-radius: 5px;
padding: 0.7rem 0.9rem;
margin: 1.2rem 0;
max-width: 62ch;
font-family: ui-monospace, "Courier New", monospace;
color: var(--ink);
}
.widget:focus { outline: 1px solid rgba(var(--violet-rgb), 0.7); }
.widget:focus-visible { outline: 1px solid rgba(var(--violet-rgb), 0.9); }
/* the plate on the right of the header. every widget writes to it and
nothing else does, so it is the one line a reader learns to check. */
.widget-state {
font-variant-numeric: tabular-nums;
text-align: right;
}
.widget-source {
margin: 0.9rem 0 0.3rem 0;
border: 1px solid rgba(var(--ink-rgb), 0.35);
border-radius: 4px;
overflow: hidden;
}
.widget-source-head {
display: flex;
justify-content: space-between;
padding: 0.3rem 0.6rem;
background: rgba(var(--ink-rgb), 0.10);
font-size: 0.72rem;
letter-spacing: 0.1em;
text-transform: uppercase;
opacity: 0.75;
}
.widget-settings {
display: block;
width: 100%;
box-sizing: border-box;
font-family: ui-monospace, "Courier New", monospace;
font-size: 0.82rem;
line-height: 1.5;
padding: 0.6rem 0.7rem;
border: none;
border-top: 1px solid rgba(var(--ink-rgb), 0.25);
background: rgba(var(--ink-rgb), 0.05);
color: var(--ink);
caret-color: var(--ink);
resize: vertical;
}
.widget-source .live-bar { padding: 0 0.7rem 0.6rem 0.7rem; }
.widget-settings-said {
margin: 0 0.7rem 0.5rem 0.7rem !important;
font-size: 0.76rem;
min-height: 1rem;
opacity: 0.7;
}
.widget-settings-broke {
opacity: 1;
color: var(--coral);
}
.widget-body { display: none; }
.widget-body-shown { display: block; }
.widget-source { margin: 0.7rem 0 0.6rem 0; }</style><style>/* fedoras/legend/legend.css
*
* One key, one palette, every board.
*
* THE ALIGNMENT. Three columns per item — glyph, label, note — with the
* first two a fixed width, so every note in the key begins at the same
* x whether it is in the first column of the key or the third. The old
* one was flex, which sizes each item to its own text and lets them
* pair up wherever they fit, and that is why the right-hand column
* wandered about.
*
* --legend-cols is set per board. --legend-label widens the name column
* for a program with long names in it. */
.legend {
display: grid;
grid-template-columns: repeat(var(--legend-cols, 2), minmax(0, 1fr));
gap: 0.12rem 1.4rem;
align-items: start;
font-size: 0.72rem;
line-height: 1.7;
margin: 0.5rem 0 0.2rem;
}
.legend-item {
display: grid;
grid-template-columns: 1.15rem var(--legend-label, 5.6rem) minmax(0, 1fr);
align-items: center;
gap: 0.4rem;
min-width: 0;
}
.legend-glyph {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.15rem;
height: 1.15rem;
border: 1px solid currentColor;
background: rgba(128, 128, 128, 0.14);
flex: none;
}
.legend-glyph svg {
display: block;
width: 100%;
height: 100%;
overflow: visible;
fill: none;
stroke: currentColor;
stroke-width: 1.6;
stroke-linecap: round;
stroke-linejoin: round;
}
/* the two that are filled rather than drawn */
.legend-glyph.tone-you svg circle,
.legend-glyph.tone-entity svg path,
.legend-glyph.tone-entity svg circle { fill: currentColor; }
.legend-glyph.tone-you svg path { fill: none; }
.legend-letter {
font-size: 0.62rem;
font-weight: 600;
line-height: 1;
color: currentColor;
}
.legend-label {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.legend-note {
opacity: 0.5;
font-style: italic;
min-width: 0;
}
/* a bad one is red and its box is dashed, because somebody is reading
this on paper and somebody else cannot see the red at all */
.legend-item:has(.tone-bad) .legend-note { opacity: 0.72; }
.legend-glyph.tone-bad { border-style: dashed; }
@media (max-width: 34rem) {
.legend { grid-template-columns: 1fr; }
}
/* ---------------------------------------------------------------------
* THE PALETTE
*
* Set on any ancestor. A board that wants its own look overrides one
* property rather than touching a renderer.
* ------------------------------------------------------------------ */
.legend,
.floor,
.brk,
.widget-rogue,
.walk {
--tone-ink: var(--ink);
--tone-bad: #c0453a;
--tone-good: #6f9f5e;
--tone-aim: #d8a657;
--tone-box: #a5794a;
--tone-shield: #5a86b8;
--tone-you: #f2e8c4;
--tone-entity: #b8503f;
}
.tone-ink { color: var(--tone-ink); }
.tone-bad { color: var(--tone-bad); }
.tone-good { color: var(--tone-good); }
.tone-aim { color: var(--tone-aim); }
.tone-box { color: var(--tone-box); }
.tone-shield { color: var(--tone-shield); }
.tone-you { color: var(--tone-you); }
.tone-entity { color: var(--tone-entity); }</style><script type="application/x-scittle">(ns fedoras.ui.widget-ui
"What every widget's page side does the same way.
Called fully qualified from the widgets, the way `fedoras.reader` is,
because the frame emits this in an earlier Scittle tag and Scittle
evaluates tags in document order. Nothing here depends on a require
crossing that boundary."
(:require [clojure.string :as str]))
(defonce mounted
(atom #{}))
(defonce running
(atom #{}))
(defonce shipped
(atom {}))
(defn running? [id] (contains? @running id))
(defn reveal!
"Show the widget's body. Called on the first successful run, and never
undone: a reader who breaks the setting afterwards keeps the thing that
was already working."
[node id]
(when-let [body (fedoras.reader/q node ".widget-body")]
(.add (.-classList body) "widget-body-shown"))
(swap! running conj id))
(defn state!
"Write the plate in the header."
[node text]
(fedoras.reader/text! node ".widget-state" text))
(defn kept
"What this widget left here last time, or nil."
[k]
(fedoras.reader/kept k))
(defn keep!
"Leave it here. Whether it also travels is the reader's business and
not this widget's."
[k v]
(fedoras.reader/keep! k v))
(defn did!
"Award the reader a point of fortune, once, silently.
A widget calls this when somebody has actually done the thing —
finished the conversation, raked the garden, written on the card. It
is a set, so it cannot be given twice, and nothing anywhere tells the
reader it happened.
Keep the keys in fedoras.endings.standing/deeds so there is one place
to see how much fortune a reader can actually get. Six to ten is the
ceiling: past that the modifier swamps the die."
[k]
(fedoras.reader/did! k))
(defn q!
"An element inside a widget, or a complaint in the header.
`fedoras.reader/q` returns nil for a selector that matches nothing,
which then fails as a null dereference three functions away from the
typo that caused it. This says which selector was wrong, in the one
place a reader is already looking."
[node selector]
(or (fedoras.reader/q node selector)
(do (state! node (str "missing: " selector)) nil)))
(defn wire!
"One control, wired in isolation.
Widgets used to set up every handler in one function, so the first
thing that threw took every handler after it with it, silently. Each
call here stands alone and says so if it fails."
[node what f]
(try
(f node)
(catch :default e
(js/console.error (str "widget: " what " did not wire") e)
(state! node (str what " did not wire")))))
(defn evaluate
"Evaluate ClojureScript source, in the page, and hand back what it
returned. This is Scittle evaluating the same way it evaluates the
book's own script tags, so what a reader types is not a lesser kind of
code than what shipped."
[source]
(if-let [core (some-> (.-scittle js/window) (.-core))]
(.eval_string core source)
(throw (js/Error. "the ClojureScript runtime is not on this page"))))
(defn- say!
[node message ok?]
(when-let [line (fedoras.reader/q node ".widget-settings-said")]
(set! (.-textContent line) message)
(.toggle (.-classList line) "widget-settings-broke" (not ok?))))
(defn reveal!
"Show the widget's body. Called on the first successful run, and never
undone: a reader who breaks the settings afterwards keeps the thing
that was already working."
[node id]
(when-let [body (fedoras.reader/q node ".widget-body")]
(.add (.-classList body) "widget-body-shown"))
(swap! running conj id))
(defn run-settings!
"Evaluate the panel and hand the result to `f`. A reader who breaks it
gets the message the runtime gave, under the box, and the widget goes
on running on the last settings that worked."
[node id f]
(when-let [box (fedoras.reader/q node ".widget-settings")]
(try
(let [value (evaluate (.-value box))]
(if (map? value)
(do (reveal! node id)
(f value)
(say! node (str "Running on " (count value) " settings.") true)
(fedoras.reader/enable! node ".widget-settings-run" true "Run again"))
(say! node "That did not come out as a map, so nothing was changed." false)))
(catch :default e
(say! node (str "It says: " (.-message e)) false)))))
(defn settings!
"Wire the source panel. Nothing runs until the reader presses Run,
which is the whole point of putting the file above the program."
[node id f]
(when-let [box (fedoras.reader/q node ".widget-settings")]
(swap! shipped assoc id (.-value box))
(fedoras.reader/on-click! node ".widget-settings-run"
(fn [_] (run-settings! node id f)))
(fedoras.reader/on-click! node ".widget-settings-reset"
(fn [_]
(set! (.-value box) (get @shipped id))
(run-settings! node id f)
(say! node "Put back as it shipped, and run again." true)))))
(defn mount!
"Register a widget. `setup!` runs once for a given element, `paint!`
runs on every render once the widget is running.
The node is checked because `getElementById` returns the first match in
the document, so a stale cell earlier in a chapter carrying the same id
silently hands a widget somebody else's element. Every selector inside
it then misses, nothing throws, and the widget reads as a Run button
that does nothing."
[k id setup! paint!]
(fedoras.reader/widget
k id
(fn [node]
(if-not (.contains (.-classList node) "widget")
(js/console.error "widget:" id "is not a widget element. Another"
"element on this page claims that id." node)
(do
(when-not (contains? @mounted [k id])
(swap! mounted conj [k id])
(when-not (fedoras.reader/q node ".widget-settings")
(reveal! node id))
(setup! node))
(when (running? id) (paint! node)))))))
(defn download!
"Hand the reader a file. Theirs, on their disc, out of the book."
[text filename]
(let [blob (js/Blob. #js [text] #js {:type "text/plain;charset=utf-8"})
url (js/URL.createObjectURL blob)
anchor (.createElement js/document "a")]
(set! (.-href anchor) url)
(set! (.-download anchor) filename)
(.click anchor)
(.revokeObjectURL js/URL url)))
(defn html!
"Replace the contents of one element inside a widget. Silent if it is
not there, because a renderer runs on every chapter and most chapters
do not have most widgets."
[node selector markup]
(when-let [target (fedoras.reader/q node selector)]
(set! (.-innerHTML target) markup)))
(defn grid!
"Repaint a grid: the column count as a custom property, the cells as
markup. Two lines that every grid widget in the book was writing out."
[node selector cols markup]
(when-let [target (fedoras.reader/q node selector)]
(.setProperty (.-style target) "--cols" (str cols))
(set! (.-innerHTML target) markup)))
(defn data
"Integer data attributes off the nearest element matching `selector`,
or nil if the event did not land on one.
(data event \".floor-room\" \"r\" \"c\") => [3 7]
This is how a grid gets its clicks: one handler on the container that
reads the coordinates back off the element, rather than a handler per
cell that has to be rewired every time the grid is repainted."
[event selector & names]
(when-let [element (.closest (.-target event) selector)]
(mapv (fn [n] (js/parseInt (.getAttribute element (str "data-" n)) 10))
names)))
(defn delegate!
"One click handler on a container, resolving to whichever child was
hit. `f` is called with the event and the vector `data` returned."
[node container-selector child-selector names f]
(when-let [container (fedoras.reader/q node container-selector)]
(set! (.-onclick container)
(fn [event]
(when-let [found (apply data event child-selector names)]
(f event found))))))
(defn keys!
"Wire a keymap onto the widget itself. The shell carries the tabindex,
so the keys work when the widget has focus and nowhere else, and the
page still scrolls when it does not."
[node keymap f]
(set! (.-onkeydown node)
(fn [event]
(when-let [what (get keymap (.-key event))]
(.preventDefault event)
(f event what)))))</script><script type="application/x-scittle">(ns fedoras.ui.legend
"The key, built in the page.
Both boards build their own key at runtime rather than taking a
rendered one from the JVM, because Breakout's changes the moment
somebody catches a bad drop. So the renderer lives here and both of
them call it with data.
An entry is {:svg or :letter, :label, :note, :tone}. Nothing here
knows what any of the tones look like — that is the stylesheet's
business and it is the reason a board never contains a hex code."
(:require [clojure.string :as str]))
(defn- glyph [{:keys [svg letter tone] :or {tone :ink}}]
(str "<span class='legend-glyph tone-" (name tone) "'>"
(if svg
(str "<svg viewBox='0 0 24 24'>" svg "</svg>")
(str "<span class='legend-letter'>" letter "</span>"))
"</span>"))
(defn html
"The whole key as one string, ready for innerHTML."
[entries]
(str/join
""
(for [{:keys [label note] :as e} entries]
(str "<div class='legend-item'>"
(glyph e)
"<span class='legend-label'>" label "</span>"
"<span class='legend-note'>" (or note "") "</span>"
"</div>"))))
(defn paint!
"Put a key into an element. `cols` is how many across.
(legend/paint! (reader/q node \".floor-legend\") entries 2)"
([el entries] (paint! el entries 2))
([el entries cols]
(when el
(set! (.-className el) "legend")
(.setProperty (.-style el) "--legend-cols" (str cols))
(set! (.-innerHTML el) (html entries)))))
(defn tone-class
"For the board itself, so a box on the grid is the same brown as the
box in the key."
[tone]
(str "tone-" (name (or tone :ink))))</script><style>/* fedoras/cs_class/walk/walk.css
*
* Layout, and the palette.
*
* The colours are custom properties rather than literals in the
* ClojureScript, and the renderer reads them back out with
* getPropertyValue, so the look of the quad is a stylesheet decision
* and changing it does not mean touching a shader. Ten of them are
* packed into a strip of texture two pixels tall when the settings are
* run; the rest go up as plain uniforms. Either way, this file is
* where the campus is coloured.
*
* A property added here needs a matching entry in walk_world.cljs,
* which is the one place that knows what the names are. */
.widget-walk {
max-width: 38rem;
--walk-sky: #6d7f95;
--walk-haze: #97a1ab;
--walk-sun: #fff4dc;
--walk-lawn: #4a6b39;
--walk-lawn-far: #35502c;
--walk-field: #3f5f2e;
--walk-field-far: #2c4520;
--walk-paving: #7b7a74;
--walk-paving-far: #4e4d49;
--walk-road: #3a3a3d;
--walk-road-far: #232326;
--walk-lot: #45444c;
--walk-lot-far: #2b2a30;
--walk-centreline: #9c7a29;
--walk-centreline-far: #5c481c;
--walk-rail: #4a4741;
--walk-rail-far: #2b2925;
--walk-fill: #5a5148;
--walk-fill-far: #39332c;
--walk-brook: #345c70;
--walk-brook-far: #223c49;
--walk-streambed: #4a4034;
--walk-streambed-far: #2e281f;
--walk-road-mark: #d8d3c4;
--walk-road-mark-far: #8f8b7c;
/* three shades, and the mortar between them */
--walk-brick-a: #8d3f34;
--walk-brick-b: #733029;
--walk-brick-c: #a35a48;
--walk-mortar: #b9b0a2;
--walk-face: #8d3f34; /* what a wall too far off to brick is */
--walk-roof: #43423f;
--walk-leaf: #3f6b34;
--walk-leaf-lit: #5c8f45;
--walk-bark: #4a3a2c;
--walk-bush: #3a5f30;
--walk-bush-lit: #557f3f;
--walk-water: #3c6f86;
--walk-foam: #d8e6ec;
--walk-stone: #9a958a;
--walk-slate: #e4e2dc;
--walk-glass: #9fc2d2;
--walk-post: #5b5a55;
--walk-lamp: #7fb2e0;
--walk-lamp-lit: #f2c14a;
}
body.quarto-light .widget-walk {
--walk-sky: #8fa6bd;
--walk-haze: #c3cbd3;
--walk-lawn: #5f8447;
--walk-lawn-far: #7d9a6c;
--walk-field: #567c3c;
--walk-field-far: #6f9159;
--walk-paving: #9a9891;
--walk-paving-far: #b6b4ad;
--walk-road: #46464a;
--walk-road-far: #6f6f74;
--walk-lot: #52515a;
--walk-lot-far: #7b7a83;
--walk-centreline: #b8902f;
--walk-centreline-far: #cdad63;
--walk-rail: #55514a;
--walk-rail-far: #7d7870;
--walk-fill: #6b6155;
--walk-fill-far: #948b7e;
--walk-brook: #3a6c84;
--walk-brook-far: #6d95a7;
--walk-streambed: #5d5142;
--walk-streambed-far: #877a67;
--walk-road-mark: #f2efe6;
--walk-road-mark-far: #dcd8cc;
--walk-brick-a: #a04a3c;
--walk-brick-b: #8a3b31;
--walk-brick-c: #b96b56;
--walk-mortar: #ddd5c8;
--walk-face: #a04a3c;
--walk-roof: #6d6b66;
--walk-leaf: #588f43;
--walk-leaf-lit: #76ad57;
--walk-bush: #4f8039;
--walk-bush-lit: #6da04e;
--walk-water: #5b93aa;
--walk-stone: #b3ada1;
--walk-slate: #f4f2ec;
--walk-glass: #b9d6e2;
}
/* The far shade is what a surface fades toward at the draw distance,
which in a dark scheme means darker and in a light one means paler.
Getting that backwards is the commonest way to make a quad that
looks like it is on fire at the horizon. */
.walk-canvas {
display: block;
width: 100%;
max-width: 34rem;
margin: 0.6rem 0 0.4rem 0;
border: 1px solid var(--rule, var(--ink));
background: var(--walk-haze);
image-rendering: pixelated;
touch-action: none;
cursor: crosshair;
}
.walk-canvas:focus { outline: 2px solid var(--violet); outline-offset: 1px; }
.widget-walk:focus, .widget-walk:focus-visible { outline: none; }
.widget-walk:focus .walk-canvas { box-shadow: 0 0 0 2px var(--ink); }
.walk-where {
font-variant-numeric: tabular-nums;
font-size: 0.78rem;
opacity: 0.75;
}
.walk-ahead {
font-size: 0.8rem;
margin: 0.2rem 0 0.2rem !important;
min-height: 1.2em;
font-variant-numeric: tabular-nums;
}
/* the only thing on this quad that tells anybody off, and it does
nothing about it */
.walk-keep-off { font-style: italic; opacity: 0.6; }</style><script type="application/x-scittle">(ns fedoras.cs-class.walk.walk-gl
"The thin layer between ClojureScript and the graphics card.
Nothing in here knows anything about a campus. It compiles a program,
hands it a pile of numbers, and draws them, and it is deliberately the
most boring file in the directory: everything interesting should be
either in the plan or in a shader, and if it drifts in here somebody
has put it in the wrong place.
The constants are written out below rather than required from
goog.webgl. This runs under Scittle, which is SCI in the browser and
has no Closure library to require from, so goog.webgl is not a
namespace that exists here. Reading them off the context instead,
as `(.-ARRAY_BUFFER gl)`, would work, but it is a property lookup on
a hot path every frame forever, and the values are frozen by the
WebGL specification and cannot move."
(:require [clojure.string :as str]))
;; ---------------------------------------------------------------------
;; The WebGL 1 enumerants this file uses, at their specified values
;; ---------------------------------------------------------------------
(def DEPTH_BUFFER_BIT 0x00000100)
(def COLOR_BUFFER_BIT 0x00004000)
(def POINTS 0x0000)
(def TRIANGLES 0x0004)
(def SRC_ALPHA 0x0302)
(def ONE_MINUS_SRC_ALPHA 0x0303)
(def ONE 1)
(def LESS 0x0201)
(def CULL_FACE 0x0B44)
(def DEPTH_TEST 0x0B71)
(def BLEND 0x0BE2)
(def UNSIGNED_BYTE 0x1401)
(def FLOAT 0x1406)
(def RGBA 0x1908)
(def NEAREST 0x2600)
(def LINEAR 0x2601)
(def LINEAR_MIPMAP_LINEAR 0x2703)
(def TEXTURE_MAG_FILTER 0x2800)
(def TEXTURE_MIN_FILTER 0x2801)
(def TEXTURE_WRAP_S 0x2802)
(def TEXTURE_WRAP_T 0x2803)
(def TEXTURE_2D 0x0DE1)
(def TEXTURE0 0x84C0)
(def CLAMP_TO_EDGE 0x812F)
(def ARRAY_BUFFER 0x8892)
(def STATIC_DRAW 0x88E4)
(def FRAGMENT_SHADER 0x8B30)
(def VERTEX_SHADER 0x8B31)
(def COMPILE_STATUS 0x8B81)
(def LINK_STATUS 0x8B82)
(def ACTIVE_UNIFORMS 0x8B86)
(def ACTIVE_ATTRIBUTES 0x8B89)
(def ALIASED_POINT_SIZE_RANGE 0x846D)
(def UNPACK_FLIP_Y_WEBGL 0x9240)
;; ---------------------------------------------------------------------
;; Getting a card at all
;; ---------------------------------------------------------------------
(defn context
"A drawing context, or nil. Antialiasing is off on purpose: the canvas
is drawn small and stretched with image-rendering: pixelated, and
smoothing the edges before enlarging them would fight that."
[canvas]
(let [options #js {:alpha false
:antialias false
:depth true
:stencil false
:premultipliedAlpha false
:preserveDrawingBuffer false
:powerPreference "high-performance"}]
(or (.getContext canvas "webgl" options)
(.getContext canvas "experimental-webgl" options))))
(defn biggest-point
"The largest point sprite this card will draw. Some will do a thousand
and some will do sixty-four, and asking for more than it has is not an
error, it is a silently wrong picture."
[gl]
(let [range (.getParameter gl ALIASED_POINT_SIZE_RANGE)]
(if range (aget range 1) 64)))
;; ---------------------------------------------------------------------
;; Programs
;; ---------------------------------------------------------------------
(defn- numbered
"Source with line numbers on it, so a card that says 0:39 is pointing
at something you can read rather than at a line you have to count to."
[source]
(->> (str/split-lines source)
(map-indexed (fn [i line] (str (inc i) " " line)))
(str/join "\n")))
(defn- compile-one
"One shader, named. The card reports a line and a column and nothing
about which of fifteen shaders it was reading, so the name and the
numbered source are put beside the complaint here. Finding that out
by elimination is an afternoon; printing it is four lines."
[gl kind label source]
(let [shader (.createShader gl kind)]
(.shaderSource gl shader source)
(.compileShader gl shader)
(if (.getShaderParameter gl shader COMPILE_STATUS)
shader
(do (js/console.error (str "walk: " label " shader did not compile\n")
(.getShaderInfoLog gl shader)
(str "\n" (numbered source)))
(.deleteShader gl shader)
nil))))
(defn- named-attributes
"Every attribute the linked program actually kept, by name. Asked of
the program rather than listed by hand, so a uniform that stops being
used cannot leave a stale location behind."
[gl program]
(into {}
(for [i (range (.getProgramParameter gl program ACTIVE_ATTRIBUTES))
:let [nm (.-name (.getActiveAttrib gl program i))]]
[nm (.getAttribLocation gl program nm)])))
(defn- named-uniforms
[gl program]
(into {}
(for [i (range (.getProgramParameter gl program ACTIVE_UNIFORMS))
:let [nm (.-name (.getActiveUniform gl program i))]]
[nm (.getUniformLocation gl program nm)])))
(defn program
"Compile, link, and find out what the result answers to. Gives back
{:program :attributes :uniforms}, or nil if anything refused, having
said which one refused and why."
([gl vertex-source fragment-source]
(program gl "unnamed" vertex-source fragment-source))
([gl label vertex-source fragment-source]
(let [vertex (compile-one gl VERTEX_SHADER (str label " vertex") vertex-source)
fragment (compile-one gl FRAGMENT_SHADER (str label " fragment") fragment-source)]
(when (and vertex fragment)
(let [linked (.createProgram gl)]
(.attachShader gl linked vertex)
(.attachShader gl linked fragment)
(.linkProgram gl linked)
(.deleteShader gl vertex)
(.deleteShader gl fragment)
(if (.getProgramParameter gl linked LINK_STATUS)
{:program linked
:attributes (named-attributes gl linked)
:uniforms (named-uniforms gl linked)}
(do (js/console.error (str "walk: " label " did not link")
(.getProgramInfoLog gl linked))
(.deleteProgram gl linked)
nil)))))))
;; ---------------------------------------------------------------------
;; Numbers on the card
;; ---------------------------------------------------------------------
(defn mesh
"A pile of vertex numbers uploaded once and left there.
`layout` names the attributes each vertex carries and how many floats
each of them is, in the order they are packed: [[\"a_position\" 3]
[\"a_patch\" 3]] is six floats per vertex. The stride and the offsets
fall out of that, which is why nothing else in this codebase ever
writes a byte offset down."
[gl layout numbers]
(let [width (reduce + 0 (map second layout))
buffer (.createBuffer gl)]
(.bindBuffer gl ARRAY_BUFFER buffer)
(.bufferData gl ARRAY_BUFFER numbers STATIC_DRAW)
{:buffer buffer
:layout layout
:width width
:count (js/Math.floor (/ (.-length numbers) width))}))
(defn drop-mesh!
[gl {:keys [buffer]}]
(when buffer (.deleteBuffer gl buffer)))
(defn- bind-mesh!
[gl {:keys [attributes]} {:keys [buffer layout width]}]
(.bindBuffer gl ARRAY_BUFFER buffer)
(let [stride (* 4 width)]
(loop [offset 0 remaining layout]
(when-let [[nm size] (first remaining)]
(let [where (get attributes nm)]
(when (and where (>= where 0))
(.enableVertexAttribArray gl where)
(.vertexAttribPointer gl where size FLOAT false stride offset)))
(recur (+ offset (* 4 size)) (next remaining))))))
;; ---------------------------------------------------------------------
;; Uniforms
;;
;; One entry point that works out what it was handed. A Float32Array of
;; sixteen is a matrix, a vector is however many floats long it is, and
;; anything else is a single number.
;; ---------------------------------------------------------------------
(defn uniforms!
[gl {:keys [uniforms]} values]
(doseq [[nm value] values]
(when-let [where (get uniforms nm)]
(cond
(instance? js/Float32Array value)
(.uniformMatrix4fv gl where false value)
(number? value)
(.uniform1f gl where value)
(= 2 (count value))
(.uniform2f gl where (nth value 0) (nth value 1))
(= 3 (count value))
(.uniform3f gl where (nth value 0) (nth value 1) (nth value 2))
(= 4 (count value))
(.uniform4f gl where (nth value 0) (nth value 1) (nth value 2) (nth value 3))
:else nil))))
(defn sampler!
"Point a named sampler at a texture on a given unit.
The program is made current first. A uniform is set on whichever
program is bound at the time, so setting one before using its program
writes it into whatever was drawn last. Every sampler here happens to
want unit zero, which is also the default, so the mistake would have
cost nothing and stayed hidden until the day something wanted unit
one."
[gl {:keys [program uniforms]} nm unit texture]
(when-let [where (get uniforms nm)]
(.useProgram gl program)
(.activeTexture gl (+ TEXTURE0 unit))
(.bindTexture gl TEXTURE_2D texture)
(.uniform1i gl where unit)))
;; ---------------------------------------------------------------------
;; Textures
;; ---------------------------------------------------------------------
(defn- clamp!
[gl smooth?]
(let [filtering (if smooth? LINEAR NEAREST)]
(.texParameteri gl TEXTURE_2D TEXTURE_WRAP_S CLAMP_TO_EDGE)
(.texParameteri gl TEXTURE_2D TEXTURE_WRAP_T CLAMP_TO_EDGE)
(.texParameteri gl TEXTURE_2D TEXTURE_MIN_FILTER filtering)
(.texParameteri gl TEXTURE_2D TEXTURE_MAG_FILTER filtering)))
(defn bytes-texture
"A texture out of raw bytes. Used for the palette, which is one
rectangle of colour two pixels tall: near shades on one row, far
shades on the other. Sampled with no smoothing, so a material never
bleeds into the one filed next to it."
[gl w h data]
(let [texture (.createTexture gl)]
(.bindTexture gl TEXTURE_2D texture)
(.pixelStorei gl UNPACK_FLIP_Y_WEBGL false)
(.texImage2D gl TEXTURE_2D 0 RGBA w h 0 RGBA UNSIGNED_BYTE data)
(clamp! gl false)
texture))
(defn picture-texture
"A texture out of anything the browser can draw: an image, or a canvas
somebody has already rasterized an SVG onto. Flipped on upload so that
the bottom of the picture is the bottom of the wall, and mipmapped so
a door seen from two hundred feet is a small door rather than a
handful of surviving pixels."
[gl source power-of-two?]
(let [texture (.createTexture gl)]
(.bindTexture gl TEXTURE_2D texture)
(.pixelStorei gl UNPACK_FLIP_Y_WEBGL true)
(.texImage2D gl TEXTURE_2D 0 RGBA RGBA UNSIGNED_BYTE source)
(if power-of-two?
(do (.texParameteri gl TEXTURE_2D TEXTURE_WRAP_S CLAMP_TO_EDGE)
(.texParameteri gl TEXTURE_2D TEXTURE_WRAP_T CLAMP_TO_EDGE)
(.texParameteri gl TEXTURE_2D TEXTURE_MIN_FILTER LINEAR_MIPMAP_LINEAR)
(.texParameteri gl TEXTURE_2D TEXTURE_MAG_FILTER LINEAR)
(.generateMipmap gl TEXTURE_2D))
(clamp! gl true))
texture))
;; ---------------------------------------------------------------------
;; Matrices
;;
;; Sixteen numbers in column order, which is what WebGL wants and what
;; every article about this gets wrong once before getting it right.
;; ---------------------------------------------------------------------
(defn perspective
"A left-handed projection, looking down positive z in eye space.
Left-handed because the plan is: x runs east, y runs *south* the way
it does on any drawing with an origin in the top corner, and z runs
up. That is a left-handed world, and the honest thing is to build a
projection that matches it rather than mirror the map to suit a
convention nobody on this campus has ever heard of.
`fov` is measured across, not up, because that is what :fov meant
when it was a raycaster and changing what a number means underneath
somebody is worse than a slightly unusual projection. `aspect` is
width over height and lands on the vertical scale for that reason."
[fov aspect near far]
(let [f (/ 1.0 (js/Math.tan (/ fov 2)))
span (- far near)]
(js/Float32Array.
#js [f 0 0 0
0 (* f aspect) 0 0
0 0 (/ (+ far near) span) 1
0 0 (/ (* -2 far near) span) 0])))
(defn look
"Where the eye is and which way it is pointed, as a matrix. Sideways
is [-sin cos], which is south when you are facing east, which is what
south is when the map has y going down."
[[ex ey ez] facing]
(let [c (js/Math.cos facing)
s (js/Math.sin facing)
rx (- s) ry c
fx c fy s]
(js/Float32Array.
#js [rx 0 fx 0
ry 0 fy 0
0 1 0 0
(- (+ (* rx ex) (* ry ey)))
(- ez)
(- (+ (* fx ex) (* fy ey)))
1])))
(defn times
"One matrix through another, in column order."
[a b]
(let [out (js/Float32Array. 16)]
(dotimes [col 4]
(dotimes [row 4]
(aset out (+ (* col 4) row)
(+ (* (aget a row) (aget b (* col 4)))
(* (aget a (+ row 4)) (aget b (+ (* col 4) 1)))
(* (aget a (+ row 8)) (aget b (+ (* col 4) 2)))
(* (aget a (+ row 12)) (aget b (+ (* col 4) 3)))))))
out))
;; ---------------------------------------------------------------------
;; Drawing
;; ---------------------------------------------------------------------
(defn draw!
"Use a program, set its uniforms, point it at a mesh, draw the lot."
([gl program mesh values] (draw! gl program mesh values TRIANGLES))
([gl program mesh values mode]
(when (and program mesh (pos? (:count mesh)))
(.useProgram gl (:program program))
(uniforms! gl program values)
(bind-mesh! gl program mesh)
(.drawArrays gl mode 0 (:count mesh)))))</script><script type="application/x-scittle">(ns fedoras.cs-class.walk.walk-world
"The plan, turned into the numbers a graphics card wants.
Everything in here runs once, when the settings are run. Nothing in
here runs while you are walking. That division is the entire point of
the rewrite: the old renderer worked out where a brick was every time
it drew one, sixty times a second, and this one works out where every
brick on the campus is exactly once and then never thinks about it
again.
A mesh is a flat run of floats, packed the way the matching shader
expects to read them. The layout each builder packs to is named in a
comment above it and defined nowhere in this file: it comes off the
shader's own attribute vector, worked out when the book was built and
carried here in the plan, so a name or a width is written down once
and the two sides cannot drift apart."
(:require [clojure.string :as str]))
;; ---------------------------------------------------------------------
;; A repeatable number, matching walk.clj's `sprout` and the shaders'
;; hash21 in spirit if not in arithmetic. Same seed, same campus.
;; ---------------------------------------------------------------------
(defn spot
"A repeatable number in 0..1 from a pair of numbers. The same pair
always gives the same answer and neighbouring pairs give unrelated
ones, which is the whole of what is wanted from it."
[first-number second-number]
(let [mixed (+ (* first-number 127.1) (* second-number 311.7))
stretched (* (js/Math.sin mixed) 43758.5453)]
(- stretched (js/Math.floor stretched))))
(defn- ease
"Smoothstep. Takes a fraction and gives it back with the corners taken
off, so two lattice cells meet without a crease down the join."
[fraction]
(let [held (max 0.0 (min 1.0 fraction))]
(* held held (- 3.0 (* 2.0 held)))))
(defn- rolling
"Value noise: the four lattice points round a spot, blended smoothly,
so a hill built out of it has no creases in it."
[px py scale]
(let [east (/ px scale)
south (/ py scale)
cell-east (js/Math.floor east)
cell-south (js/Math.floor south)
across (ease (- east cell-east))
down (ease (- south cell-south))
north-west (spot cell-east cell-south)
north-east (spot (inc cell-east) cell-south)
south-west (spot cell-east (inc cell-south))
south-east (spot (inc cell-east) (inc cell-south))]
(+ (* north-west (- 1 across) (- 1 down))
(* north-east across (- 1 down))
(* south-west (- 1 across) down)
(* south-east across down))))
;; ---------------------------------------------------------------------
;; What you can be standing on
;;
;; The order of this vector is the order of the palette strip and the
;; order of the numbered cases in the ground shader. Change one and
;; change all three, and there is no way for the compiler to tell you
;; that you did not.
;; ---------------------------------------------------------------------
(def materials
[:lawn :field :paving :road :lot :centreline :rail :fill :water :crosswalk
:streambed])
(def material-index
(into {} (map-indexed (fn [slot material] [material slot]) materials)))
(def palette-slots
"Which pair of stylesheet properties each slot reads. Near shade on
one row of the texture, far shade on the other."
[["--walk-lawn" "--walk-lawn-far"]
["--walk-field" "--walk-field-far"]
["--walk-paving" "--walk-paving-far"]
["--walk-road" "--walk-road-far"]
["--walk-lot" "--walk-lot-far"]
["--walk-centreline" "--walk-centreline-far"]
["--walk-rail" "--walk-rail-far"]
["--walk-fill" "--walk-fill-far"]
["--walk-brook" "--walk-brook-far"]
["--walk-road-mark" "--walk-road-mark-far"]
["--walk-streambed" "--walk-streambed-far"]])
(def palette-width
"Sixteen columns, and not all of them used. A power of two, because a texture
that is not one has rules about how it may be sampled and there is no
reason to go and learn them for this."
16)
;; ---------------------------------------------------------------------
;; Colour, read back out of the stylesheet
;; ---------------------------------------------------------------------
(defn css-var
"What the stylesheet says a custom property is, or the fallback. The
page is the palette; nothing in here decides what a colour looks
like."
[node property fallback]
(or (try
(let [declared (.getPropertyValue (js/getComputedStyle node) property)]
(when-not (str/blank? declared) (str/trim declared)))
(catch :default _ nil))
fallback))
(defn rgb
"#rrggbb to [r g b] in 0..1. Anything else gives mid grey rather than
throwing, so a missing custom property does not take the page down."
[hex]
(if (and hex (= "#" (subs hex 0 1)) (= 7 (count hex)))
[(/ (js/parseInt (subs hex 1 3) 16) 255.0)
(/ (js/parseInt (subs hex 3 5) 16) 255.0)
(/ (js/parseInt (subs hex 5 7) 16) 255.0)]
[0.5 0.5 0.5]))
(def swatches
"Every colour the renderer asks for by name, and what to fall back on
when the stylesheet has not got it."
{:sky ["--walk-sky" "#6d7f95"]
:haze ["--walk-haze" "#97a1ab"]
:sun ["--walk-sun" "#fff4dc"]
:face ["--walk-face" "#8d3f34"]
:brick-a ["--walk-brick-a" "#8d3f34"]
:brick-b ["--walk-brick-b" "#733029"]
:brick-c ["--walk-brick-c" "#a35a48"]
:mortar ["--walk-mortar" "#b9b0a2"]
:roof ["--walk-roof" "#43423f"]
:leaf ["--walk-leaf" "#3f6b34"]
:leaf-lit ["--walk-leaf-lit" "#5c8f45"]
:bark ["--walk-bark" "#4a3a2c"]
:bush ["--walk-bush" "#3a5f30"]
:bush-lit ["--walk-bush-lit" "#557f3f"]
:water ["--walk-water" "#3c6f86"]
:foam ["--walk-foam" "#d8e6ec"]
:stone ["--walk-stone" "#9a958a"]
:post ["--walk-post" "#5b5a55"]
:slate ["--walk-slate" "#e8e6e0"]
:glass ["--walk-glass" "#bed6e0"]
:lamp ["--walk-lamp" "#7fb2e0"]
:lamp-lit ["--walk-lamp-lit" "#f2c14a"]})
(defn read-swatches
[node]
(into {} (map (fn [[swatch [property fallback]]]
[swatch (rgb (css-var node property fallback))])
swatches)))
(defn palette-bytes
"The palette strip: `palette-width` columns by two rows, near shades
on the top row and far shades on the bottom."
[node]
(let [data (js/Uint8Array. (* palette-width 2 4))
put! (fn [col row [r g b]]
(let [at (* 4 (+ (* row palette-width) col))]
(aset data at (js/Math.round (* 255 r)))
(aset data (+ at 1) (js/Math.round (* 255 g)))
(aset data (+ at 2) (js/Math.round (* 255 b)))
(aset data (+ at 3) 255)))]
(doseq [[col [near far]] (map-indexed vector palette-slots)]
(put! col 0 (rgb (css-var node near "#808080")))
(put! col 1 (rgb (css-var node far "#404040"))))
data))
;; ---------------------------------------------------------------------
;; Where the ground is
;; ---------------------------------------------------------------------
(defn- within?
[rect px py]
(let [{:keys [x y w h]} rect]
(and rect (>= px x) (<= px (+ x w)) (>= py y) (<= py (+ y h)))))
(defn- inset-of
"How many feet in from its own nearest edge a point is."
[{:keys [x y w h]} px py]
(min (- px x) (- (+ x w) px) (- py y) (- (+ y h) py)))
(def floor-of
"What the bottom of a shaped piece of ground is made of. A brook's
floor is silt: the blue belongs to the water lying on top of it, and
painting the bed that same blue is why the surface looked like there
was not one. There was. It was one shade of water over another."
{:water :streambed :fill :fill})
(def shaped
"The materials that are not flat, and which way each one goes.
Nothing is tagged in the plan for this. A brook is shaped because it
is water, and a landfill is shaped because it is fill, and asking the
surface what it is made of is one question with one answer. It used to
be a `:channel?` key set in campus.clj, carried through a select-keys
in walk.clj, put in the mount map under its own name and read back
here, which is four places to write it down and four places to forget
to."
{:fill :up :water :down})
(defn- relief
"How far off the flat campus one shaped rectangle is at a point. A
landfill and a brook are the same arithmetic with the sign turned
round: one is a hill somebody piled and the other is a hill somebody
dug. Both come back to nothing at their own edges, so each meets the
lawn without a step."
[rect surface settings px py]
(let [inset (inset-of rect px py)]
(case surface
:fill (let [{:keys [peak roughness]} (:fill settings)
skirt (ease (/ inset 26.0))
lumps (+ (* 0.55 (rolling px py 48))
(* 0.30 (rolling px py 19))
(* 0.15 (rolling px py 7)))]
(* peak skirt (+ 0.45 (* roughness lumps))))
:water (let [{:keys [depth bank]} (:water settings)
dish (ease (/ inset (max 1.0 bank)))
ripple (* 0.12 (- (rolling px py 23) 0.5))]
(- (* depth dish (+ 0.94 ripple))))
0.0)))
(defn showing?
"Whether a shaped surface is being dug or piled at all. A reader who
turns the Fill off gets flat ground where it was, not a hole."
[settings surface]
(boolean (get-in settings [surface :show])))
(defn- overlapping?
[one other]
(not (or (<= (+ (:x one) (:w one)) (:x other))
(<= (+ (:x other) (:w other)) (:x one))
(<= (+ (:y one) (:h one)) (:y other))
(<= (+ (:y other) (:h other)) (:y one)))))
(defn terrain
"The shaped ground, and for each piece of it only the things laid over
it. Worked out once when the settings are run and handed to everything
that asks how high the ground is.
It used to be worked out per call, and the call is per vertex: the
brook alone is four thousand of them, each walking two hundred
rectangles to find the three roads that cross it. That is most of what
a reader was waiting for. A road crossing a brook is a bridge, and the
ground on a bridge is the bridge."
[{:keys [surfaces]} settings]
(let [above (filterv (fn [rect] (pos? (:layer rect 0))) surfaces)]
(mapv (fn [rect]
(assoc rect :bridges
(filterv (fn [over] (overlapping? rect over)) above)))
(filter (fn [rect]
(and (shaped (:surface rect))
(showing? settings (:surface rect))))
surfaces))))
(defn round?
[box]
(= :round (:shape box)))
(defn ground-height
"How far the ground at a point is above or below the flat campus.
The renderer builds its triangles out of this and the walker takes its
eye height from it, which is the only way the two can agree about what
the ground is doing."
[terrain settings px py]
(or (some (fn [{:keys [bridges surface] :as rect}]
(when (within? rect px py)
(if (some (fn [over] (within? over px py)) bridges)
0.0
(relief rect surface settings px py))))
terrain)
0.0))
;; ---------------------------------------------------------------------
;; Looking up what is underfoot
;;
;; The same coarse grid the old renderer used, kept because the walker
;; still has to answer "what am I standing on" once a frame and there is
;; no reason to ask the graphics card a question ClojureScript can
;; answer in a dozen comparisons.
;; ---------------------------------------------------------------------
(def ^:private bucket 64.0)
(defn- bucket-of
"Which bucket along one axis a foot position falls in."
[foot origin]
(int (/ (- foot origin) bucket)))
(defn index
"Every surface rectangle filed under the buckets it covers, so working
out what you are standing on is a look at the handful of rectangles
near you rather than a walk through all of them.
A rectangle keeps the order it was laid in, because two of them can
cover the same spot and both be telling the truth. The later one wins,
which is the same rule the depth lift enforces in the picture."
[surfaces]
(let [laid (vec (keep-indexed
(fn [laid-order rect]
(when-let [material (get material-index (:surface rect))]
{:west (:x rect)
:north (:y rect)
:east (+ (:x rect) (:w rect))
:south (+ (:y rect) (:h rect))
:material material
:laid-order laid-order}))
surfaces))
origin-x (- (reduce min 0 (map :west laid)) bucket)
origin-y (- (reduce min 0 (map :north laid)) bucket)
covered (fn [rect]
(for [column (range (bucket-of (:west rect) origin-x)
(inc (bucket-of (:east rect) origin-x)))
row (range (bucket-of (:north rect) origin-y)
(inc (bucket-of (:south rect) origin-y)))]
[column row]))]
{:buckets (reduce (fn [buckets rect]
(reduce (fn [filed cell]
(update filed cell (fnil conj []) rect))
buckets
(covered rect)))
{}
laid)
:origin-x origin-x
:origin-y origin-y}))
(defn surface-at
"The material index under a point, or 0 for open lawn."
[{:keys [buckets origin-x origin-y]} px py]
(let [nearby (get buckets [(bucket-of px origin-x) (bucket-of py origin-y)])
underfoot (filter (fn [{:keys [west north east south]}]
(and (>= px west) (< px east)
(>= py north) (< py south)))
nearby)]
(if (seq underfoot)
(:material (apply max-key :laid-order underfoot))
0)))
;; ---------------------------------------------------------------------
;; Packing triangles
;; ---------------------------------------------------------------------
(def ^:private quad-order [0 1 2 0 2 3])
(defn- floats
"The accumulator, handed to the card as the one thing it will take."
[numbers]
(js/Float32Array. numbers))
;; ---------------------------------------------------------------------
;; The ground
;;
;; layout: [["a_position" 3] ["a_patch" 4]]
;;
;; a_patch is (material, how far along this rectangle, how far off its
;; middle). The offset is signed rather than absolute, because a signed
;; offset is linear across the rectangle and an absolute one is a fold,
;; and a triangle can only carry the linear kind. The shader takes the
;; absolute value at the far end, where it costs nothing.
;; ---------------------------------------------------------------------
(defn- ground-quad!
[vertices {:keys [x y w h axis surface style]} z]
(let [material (get material-index surface 0)
east-west (= :ew axis)
crossing (= :crosswalk surface)
middle (if east-west (+ y (/ h 2.0)) (+ x (/ w 2.0)))
corners [[x y] [(+ x w) y] [(+ x w) (+ y h)] [x (+ y h)]]]
(doseq [corner quad-order]
(let [[px py] (nth corners corner)
run (cond crossing (if east-west px py)
east-west (- px x)
:else (- py y))
off-middle (if east-west (- py middle) (- px middle))]
(.push vertices px) (.push vertices py) (.push vertices z)
(.push vertices material) (.push vertices run)
(.push vertices off-middle) (.push vertices (or style 0))))))
(defn- shaped!
"A piece of ground that has a shape, as a grid of triangles. Every
corner gets its height from the same function the walker's own eye
uses, so the mound and the brook are one piece of arithmetic and
neither can drift from what you are standing on."
[vertices ground rect settings lift]
(let [{:keys [x y w h surface]} rect
step (max 1.0 (get-in settings [surface :step] 6))
material (get material-index (get floor-of surface surface) 0)
across (max 2 (js/Math.ceil (/ w step)))
down (max 2 (js/Math.ceil (/ h step)))
at (fn [column row]
(let [px (+ x (* w (/ column across)))
py (+ y (* h (/ row down)))]
[px py (+ lift (ground-height ground settings px py))]))]
(dotimes [row down]
(dotimes [column across]
(let [corners [(at column row) (at (inc column) row)
(at (inc column) (inc row)) (at column (inc row))]]
(doseq [corner quad-order]
(let [[px py pz] (nth corners corner)]
(.push vertices px) (.push vertices py) (.push vertices pz)
(.push vertices material) (.push vertices 0.0)
(.push vertices 0.0) (.push vertices 0.0))))))))
(defn- without
"A rectangle with a hole cut in it, as up to four rectangles. The hole
is clamped to the rectangle first, so a hole that hangs over an edge
or misses altogether is not a special case anywhere else."
[{:keys [x y w h] :as whole} hole]
(let [east (+ x w)
south (+ y h)
cut-west (max x (:x hole))
cut-north (max y (:y hole))
cut-east (min east (+ (:x hole) (:w hole)))
cut-south (min south (+ (:y hole) (:h hole)))]
(if (or (>= cut-west cut-east) (>= cut-north cut-south))
[whole]
(remove (fn [piece] (or (<= (:w piece) 0) (<= (:h piece) 0)))
[{:x x :y y :w w :h (- cut-north y)}
{:x x :y cut-south :w w :h (- south cut-south)}
{:x x :y cut-north :w (- cut-west x) :h (- cut-south cut-north)}
{:x cut-east :y cut-north :w (- east cut-east)
:h (- cut-south cut-north)}]))))
(defn ground-mesh
"Every flat rectangle, the two shaped ones, and enough lawn under the
lot of it to run out past the draw distance so the horizon is always
ground meeting sky and never an edge.
The lawn has the brook cut out of it. It used to be one unbroken quad
at ground level, which meant the grass and the water were the same
plane in the same place and the card picked between them per pixel,
differently every time you moved. A channel that goes down needs the
grass over it gone, not merely lifted off it."
[{:keys [surfaces]} {:keys [ground] :as settings} shapes reach]
(let [vertices (js/Array.)
lift (:lift ground)
edge (* reach 4)
shaped-here (set (map (fn [rect] (dissoc rect :bridges)) shapes))
sunken (filter (fn [rect] (= :down (shaped (:surface rect)))) shapes)
horizon {:x (- edge) :y (- edge) :w (* 2 edge) :h (* 2 edge)}
lawn (reduce (fn [pieces hole]
(mapcat (fn [piece] (without piece hole)) pieces))
[horizon]
sunken)
total (max 1 (count surfaces))]
(doseq [piece lawn]
(ground-quad! vertices (assoc piece :axis :ew :surface :lawn) 0.0))
;; Within one layer, rectangles are nudged apart by a fraction of a
;; layer step, in the order they were laid. Two roads crossing are
;; both layer 2 and were exactly coplanar, so every intersection on
;; the campus was a coin toss the card made again each frame. The
;; spread stays under one lift, so it cannot reorder the layers.
(doseq [[order rect] (map-indexed vector surfaces)]
(when-not (shaped-here rect)
(ground-quad! vertices rect
(* lift (+ (:layer rect 0) (* 0.5 (/ order total)))))))
(doseq [rect shapes]
(shaped! vertices shapes rect settings lift))
(floats vertices)))
;; ---------------------------------------------------------------------
;; Buildings
;;
;; layout: [["a_position" 3] ["a_wall" 2] ["a_normal" 2] ["a_seed" 1]]
;;
;; a_wall is (feet along this wall from the building's own corner, feet
;; up). Courses stagger from the corner, so two segments of one wall can
;; never disagree about where the brick pattern starts, which was a real
;; bug in the old renderer and is now simply not expressible.
;; ---------------------------------------------------------------------
(defn box-top
[box default]
(cond
(:height box) (:height box)
(:one-story? box) (/ default 2)
:else default))
(defn face-named
"One wall of a building, by which side it is. The faces themselves are
worked out in walk.clj and travel in the plan; there is no second copy
of that arithmetic here for the two to disagree over."
[box side]
(some (fn [face] (when (= side (:side face)) face)) (:faces box)))
(defn wall-mesh
[{:keys [boxes]} default-top]
(let [vertices (js/Array.)]
(doseq [box boxes
:when (not (round? box))
{:keys [a b normal length slot]} (:faces box)]
(let [[from-x from-y] a
[to-x to-y] b
[facing-x facing-y] normal
;; Every face gets a number of its own, out of where its
;; near corner stands and which way it faces. Without it,
;; a_wall starts at zero on every wall in the campus and
;; every wall is laid brick for brick the same as the one
;; round the corner from it.
seed (+ (* 0.017 from-x) (* 0.031 from-y) (* 7.3 slot))
top (box-top box default-top)
corners [[from-x from-y 0.0 0.0] [to-x to-y 0.0 length]
[to-x to-y top length] [from-x from-y top 0.0]]]
(doseq [corner quad-order]
(let [[px py pz along-wall] (nth corners corner)]
(.push vertices px) (.push vertices py) (.push vertices pz)
(.push vertices along-wall) (.push vertices pz)
(.push vertices facing-x) (.push vertices facing-y)
(.push vertices seed)))))
(floats vertices)))
;; ---------------------------------------------------------------------
;; Doors and windows
;;
;; layout: [["a_position" 3] ["a_uv" 2] ["a_normal" 2]]
;; ---------------------------------------------------------------------
(def ^:private proud
"How far a door stands out from the wall it is in. Enough that the
depth buffer never has to choose between them, little enough that
nobody notices from the side."
0.09)
(defn- panel!
[vertices {:keys [a b normal length]} where-along width height z-middle]
(let [[from-x from-y] a
[to-x to-y] b
[facing-x facing-y] normal
step-x (/ (- to-x from-x) length)
step-y (/ (- to-y from-y) length)
half-width (/ width 2.0)
middle-foot (* where-along length)
near-foot (- middle-foot half-width)
far-foot (+ middle-foot half-width)
bottom (- z-middle (/ height 2.0))
top (+ z-middle (/ height 2.0))
at (fn [foot z] [(+ from-x (* step-x foot) (* facing-x proud))
(+ from-y (* step-y foot) (* facing-y proud))
z])
corners [[(at near-foot bottom) 0.0 0.0]
[(at far-foot bottom) 1.0 0.0]
[(at far-foot top) 1.0 1.0]
[(at near-foot top) 0.0 1.0]]]
(doseq [corner quad-order]
(let [[[px py pz] image-u image-v] (nth corners corner)]
(.push vertices px) (.push vertices py) (.push vertices pz)
(.push vertices image-u) (.push vertices image-v)
(.push vertices facing-x) (.push vertices facing-y)))))
(defn- name-seed
"A building's name as one number, so its windows follow from what it
is called and stay where they were put."
[building-name]
(reduce + 0 (map (fn [at] (.charCodeAt building-name at))
(range (count building-name)))))
(defn window-plan
"0, 1 or 2 windows for one wall of one building, each a {:u :floor}.
Deterministic from the building's own name, so the same building has
the same windows every time rather than a new set every frame."
[box face]
(let [seed (+ (name-seed (:name box)) (* 7 (:slot face 0)))
roll (fn [nudge] (spot (+ seed nudge) 4001))
how-many (let [first-roll (roll 0)]
(cond (< first-roll 0.30) 0
(< first-roll 0.70) 1
:else 2))]
(vec
(for [which (range how-many)]
{:u (if (= how-many 2)
(if (zero? which)
(+ 0.12 (* 0.25 (roll (+ 17 which))))
(+ 0.63 (* 0.25 (roll (+ 17 which)))))
(+ 0.35 (* 0.30 (roll 17))))
:floor (if (< (roll (+ 23 (* which 5))) 0.5) 0 1)}))))
(defn- doorways-of
"Every face a building has a door in. Most have one; the library has
two, and nothing else in here has to know which is which."
[box]
(if (round? box)
(:faces box)
(keep (fn [side] (face-named box side))
(or (:doors box) [(:door box)]))))
(defn door-mesh
[{:keys [boxes]} {:keys [doors buildings]}]
(let [vertices (js/Array.)
{:keys [width height sill] :or {sill 0.0}} doors]
(when (:show doors)
(doseq [box boxes
face (doorways-of box)]
(panel! vertices face 0.5 width height (+ sill (/ height 2.0)))))
(floats vertices)))
(defn window-mesh
[{:keys [boxes]} {:keys [windows buildings]}]
(let [vertices (js/Array.)
{:keys [width height]} windows
default-top (:height buildings)
storey (/ default-top 2.0)]
(when (:show windows)
(doseq [box boxes
:when (not (round? box))
face (:faces box)
{:keys [u floor]} (window-plan box face)]
(let [top (box-top box default-top)
middle (+ (* floor storey) (/ storey 2.0))]
(when (<= (+ middle (/ height 2.0)) top)
(panel! vertices face u width height middle)))))
(floats vertices)))
;; ---------------------------------------------------------------------
;; Trees and hedges
;;
;; layout: [["a_root" 3] ["a_corner" 2] ["a_shape" 3]]
;;
;; Four corners and a seed. The vertex shader turns the quad to face
;; you and the fragment shader grows the thing on it.
;; ---------------------------------------------------------------------
(def ^:private billboard-corners [[-1 0] [1 0] [1 1] [-1 1]])
(defn billboard-mesh
[plants spread]
(let [vertices (js/Array.)]
(doseq [{:keys [x y radius height]} plants]
(let [half-width (* radius spread)
seed (+ (* 0.017 x) (* 0.031 y))]
(doseq [corner quad-order]
(let [[sideways upward] (nth billboard-corners corner)]
(.push vertices x) (.push vertices y) (.push vertices 0.0)
(.push vertices sideways) (.push vertices upward)
(.push vertices half-width) (.push vertices height)
(.push vertices seed)))))
(floats vertices)))
;; ---------------------------------------------------------------------
;; Everything else with a shape
;;
;; layout: [["a_position" 3] ["a_colour" 3] ["a_normal" 3] ["a_water" 2]]
;;
;; a_water is (ripple spacing, how opaque). Nearly everything here sets
;; it to (0, 1) and means "plain and solid"; the pool sets the first
;; number and the water bell sets the second.
;; ---------------------------------------------------------------------
(defn- vertex!
"One vertex of the solid layout, packed in declaration order. Every
push in this section goes through here, so the order the numbers land
in is written down once."
[vertices [px py pz] [red green blue] [normal-x normal-y normal-z] ripple alpha]
(.push vertices px) (.push vertices py) (.push vertices pz)
(.push vertices red) (.push vertices green) (.push vertices blue)
(.push vertices normal-x) (.push vertices normal-y) (.push vertices normal-z)
(.push vertices ripple) (.push vertices alpha))
(defn- quad!
[vertices corners colour normal ripple alpha]
(doseq [corner quad-order]
(vertex! vertices (nth corners corner) colour normal ripple alpha)))
(defn- box!
"A rectangular block, four sides and a lid. The underside is never
seen and is never built."
[vertices x y z w h top colour]
(let [east (+ x w)
south (+ y h)
lid (+ z top)]
(quad! vertices [[x y z] [east y z] [east y lid] [x y lid]]
colour [0 -1 0] 0.0 1.0)
(quad! vertices [[east south z] [x south z] [x south lid] [east south lid]]
colour [0 1 0] 0.0 1.0)
(quad! vertices [[x south z] [x y z] [x y lid] [x south lid]]
colour [-1 0 0] 0.0 1.0)
(quad! vertices [[east y z] [east south z] [east south lid] [east y lid]]
colour [1 0 0] 0.0 1.0)
(quad! vertices [[x y lid] [east y lid] [east south lid] [x south lid]]
colour [0 0 1] 0.0 1.0)))
(defn- arc-points
"Points along part of a circle, as [x y angle], with one more point
than there are steps so consecutive pairs cover the whole arc."
[middle-x middle-y radius from-angle to-angle steps]
(let [span (- to-angle from-angle)]
(mapv (fn [step]
(let [angle (+ from-angle (* span (/ step steps)))]
[(+ middle-x (* radius (js/Math.cos angle)))
(+ middle-y (* radius (js/Math.sin angle)))
angle]))
(range (inc steps)))))
(defn- ring-points
"A closed ring, which is an arc that happens to go all the way round."
[middle-x middle-y radius steps]
(arc-points middle-x middle-y radius 0.0 6.2832 steps))
(defn- tube!
"A cylinder wall between two heights, facing outward or in."
[vertices middle-x middle-y radius z-bottom z-top steps colour outward?]
(let [ring (ring-points middle-x middle-y radius steps)
facing (if outward? 1.0 -1.0)]
(dotimes [step steps]
(let [[from-x from-y from-angle] (nth ring step)
[to-x to-y to-angle] (nth ring (inc step))
out-at (fn [angle] [(* facing (js/Math.cos angle))
(* facing (js/Math.sin angle))
0])
corners [[from-x from-y z-bottom] [to-x to-y z-bottom]
[to-x to-y z-top] [from-x from-y z-top]]
normals [(out-at from-angle) (out-at to-angle)
(out-at to-angle) (out-at from-angle)]]
(doseq [corner quad-order]
(vertex! vertices (nth corners corner) colour (nth normals corner)
0.0 1.0))))))
(defn- arc-wall!
"A stretch of curved wall standing between two heights. Used for the
library's glass, which is not a full ring: there is a doorway cut out
of it at each end."
[vertices ring z-bottom z-top colour alpha]
(dotimes [step (dec (count ring))]
(let [[from-x from-y from-angle] (nth ring step)
[to-x to-y to-angle] (nth ring (inc step))
out-at (fn [angle] [(js/Math.cos angle) (js/Math.sin angle) 0])
corners [[from-x from-y z-bottom] [to-x to-y z-bottom]
[to-x to-y z-top] [from-x from-y z-top]]
normals [(out-at from-angle) (out-at to-angle)
(out-at to-angle) (out-at from-angle)]]
(doseq [corner quad-order]
(vertex! vertices (nth corners corner) colour (nth normals corner)
0.0 alpha)))))
(defn- dome!
"A quarter of an ellipsoid, in bands, sitting on the top of a wall.
The top band's upper radius is nothing, so its quads collapse into the
triangles a fan would have given, without needing a fan."
[vertices middle-x middle-y radius z-base height steps rings colour]
(let [quarter (/ 3.14159265 2.0)
profile (fn [fraction]
(let [turn (* fraction quarter)]
{:radius (* radius (js/Math.cos turn))
:z (+ z-base (* height (js/Math.sin turn)))
:lean (js/Math.sin turn)
:rise (js/Math.cos turn)}))]
(dotimes [band rings]
(let [low (profile (/ band rings))
high (profile (/ (inc band) rings))
low-ring (ring-points middle-x middle-y (:radius low) steps)
high-ring (ring-points middle-x middle-y (:radius high) steps)
;; The normal of a squashed dome leans out by the height and
;; up by the radius, which is the profile's own tangent
;; turned a quarter turn.
skin (fn [{:keys [lean rise]} angle]
(let [flat (* height rise)
up (* radius lean)
length (js/Math.sqrt (+ (* flat flat) (* up up)))
scale (/ 1.0 (max length 0.0001))]
[(* flat scale (js/Math.cos angle))
(* flat scale (js/Math.sin angle))
(* up scale)]))]
(dotimes [step steps]
(let [[low-from-x low-from-y low-from-angle] (nth low-ring step)
[low-to-x low-to-y low-to-angle] (nth low-ring (inc step))
[high-to-x high-to-y] (nth high-ring (inc step))
[high-from-x high-from-y] (nth high-ring step)
corners [[low-from-x low-from-y (:z low)]
[low-to-x low-to-y (:z low)]
[high-to-x high-to-y (:z high)]
[high-from-x high-from-y (:z high)]]
normals [(skin low low-from-angle) (skin low low-to-angle)
(skin high low-to-angle) (skin high low-from-angle)]]
(doseq [corner quad-order]
(vertex! vertices (nth corners corner) colour
(nth normals corner) 0.0 1.0))))))))
(defn- disc!
"A flat circle, as a fan from its own middle."
[vertices middle-x middle-y radius z steps colour ripple]
(let [ring (ring-points middle-x middle-y radius steps)]
(dotimes [step steps]
(let [[from-x from-y _] (nth ring step)
[to-x to-y _] (nth ring (inc step))]
(vertex! vertices [middle-x middle-y z] colour [0 0 1] ripple 1.0)
(vertex! vertices [from-x from-y z] colour [0 0 1] ripple 1.0)
(vertex! vertices [to-x to-y z] colour [0 0 1] ripple 1.0)))))
(defn- annulus!
"A flat ring, in wedges, each wedge shifted a little off the base
colour. A paved circle is laid in pieces and pieces do not match."
[vertices middle-x middle-y inner outer z steps colour]
(let [ring (ring-points middle-x middle-y 1.0 steps)]
(dotimes [step steps]
(let [[_ _ from-angle] (nth ring step)
[_ _ to-angle] (nth ring (inc step))
shift (* 0.06 (- (spot step 6301) 0.5))
shade (mapv (fn [channel] (max 0.0 (min 1.0 (+ channel shift))))
colour)
at (fn [radius angle]
[(+ middle-x (* radius (js/Math.cos angle)))
(+ middle-y (* radius (js/Math.sin angle)))
z])]
(quad! vertices
[(at inner from-angle) (at outer from-angle)
(at outer to-angle) (at inner to-angle)]
shade [0 0 1] 0.0 1.0)))))
(defn crest
"How high the water bell is at a fraction of the way out. The same
curve the mist follows, which is why the two look like one thing."
[nozzle rise t]
(+ nozzle (* rise (- (* 2 t) (* 2.6 t t)))))
(defn fountain-parts
"Every number the fountain is built from, worked out once so the
solid mesh, the water sheet and the mist all read the same one."
[{:keys [x y radius rim water pedestal spray]}]
(let [{prad :radius phigh :height} pedestal
{:keys [width height density] :or {width 1.0 height 1.0 density 1.0}} spray
kerb (+ water rim)
inner (- radius rim)
nozzle (+ water phigh)]
{:x x :y y :radius radius :inner inner :kerb kerb :water water
:nozzle nozzle :pedestal-radius prad
:rise (* phigh 0.55 (max 0.4 (min 2.5 height)))
:reach (* inner 0.72 (max 0.4 (min 2.5 width)))
:density (max 0.2 (min 2.5 density))}))
(defn fountain-mesh
"Stone, kerb, pool, pedestal. Solid, opaque, and never redrawn."
[fountain swatches ripples]
(let [{:keys [x y radius inner kerb water nozzle pedestal-radius]} (fountain-parts fountain)
steps 28
stone (:stone swatches)
pool (:water swatches)
foam (:foam swatches)
vertices (js/Array.)]
(tube! vertices x y radius 0.0 kerb steps stone true)
(annulus! vertices x y inner radius kerb steps stone)
(tube! vertices x y inner water kerb steps stone false)
(disc! vertices x y inner water steps pool (/ inner (max 1 ripples)))
(tube! vertices x y pedestal-radius water nozzle 16 stone true)
(disc! vertices x y pedestal-radius nozzle 16 stone 0.0)
(tube! vertices x y (* pedestal-radius 0.45) nozzle (+ nozzle 0.9) 12 foam true)
(disc! vertices x y (* pedestal-radius 0.45) (+ nozzle 0.9) 12 foam 0.0)
(floats vertices)))
(defn water-mesh
"The top of every body of water on the campus, as a grid of triangles
at the waterline. Its ripple spacing is written negative, which is how
the vertex shader knows to make it move: everything else with a
spacing of nothing sits still, and a brook that sits still is a road.
Drawn with the rest of the see-through things, so you look at the
surface and through it to the bottom at the same time, which is what
water does."
[shapes settings swatches]
(let [vertices (js/Array.)
colour (:water swatches)
{:keys [waterline step alpha] :or {waterline -0.6 step 6 alpha 0.72}}
(:water settings)]
(doseq [{:keys [x y w h surface bridges]} shapes
:when (= :down (shaped surface))]
(let [across (max 2 (js/Math.ceil (/ w step)))
down (max 2 (js/Math.ceil (/ h step)))
at (fn [column row]
[(+ x (* w (/ column across)))
(+ y (* h (/ row down)))
waterline])]
(dotimes [row down]
(dotimes [column across]
(let [corners [(at column row) (at (inc column) row)
(at (inc column) (inc row)) (at column (inc row))]
[middle-x middle-y _] (at (+ column 0.5) (+ row 0.5))]
(when-not (some (fn [over] (within? over middle-x middle-y))
bridges)
(doseq [corner quad-order]
(vertex! vertices (nth corners corner) colour [0 0 1]
-1.0 alpha))))))))
(floats vertices)))
(defn rotunda-mesh
"Everything of the library you cannot see through: the floor, the four
blocks standing on it, and the lid.
The blocks are the reason the glass works. A building you can see
clean through is a fish tank; a building with something solid in the
middle of it is a building, and walking round it changes what you can
see of the inside, which is the only thing a glass wall is for."
[{:keys [boxes]} settings swatches]
(let [vertices (js/Array.)
{:keys [show wall dome facets rings]
:or {show true wall 15 dome 12 facets 48 rings 7}} (:rotunda settings)
slate (:slate swatches)
floor (:stone swatches)]
(when show
(doseq [box boxes
:when (round? box)]
(let [[middle-x middle-y] (:middle box)
radius (:radius box)]
(disc! vertices middle-x middle-y radius 0.3 facets floor 0.0)
(doseq [{:keys [x y w h height]} (:rooms box)]
(box! vertices x y 0.3 w h (or height 12) slate))
(dome! vertices middle-x middle-y radius wall dome facets rings
slate))))
(floats vertices)))
(defn glass-mesh
"The wall, which is all of it that is not a doorway.
Two arcs rather than a ring: the span a door stands in is left out, so
the way in is a gap in the glass and not a hole cut in it afterwards."
[{:keys [boxes]} settings swatches]
(let [vertices (js/Array.)
{:keys [show wall facets glass door]
:or {show true wall 15 facets 48 glass 0.26 door 9}}
(:rotunda settings)
colour (:glass swatches)]
(when show
(doseq [box boxes
:when (round? box)]
(let [[middle-x middle-y] (:middle box)
radius (:radius box)
gap (/ (/ door 2.0) (max radius 1.0))
spans (if (< (count (:faces box)) 2)
[[gap (- 6.2832 gap)]]
[[gap (- 3.14159265 gap)]
[(+ 3.14159265 gap) (- 6.2832 gap)]])]
(doseq [[from-angle to-angle] spans]
(let [share (/ (- to-angle from-angle) 6.2832)
steps (max 4 (js/Math.ceil (* facets share)))]
(arc-wall! vertices
(arc-points middle-x middle-y radius
from-angle to-angle steps)
0.3 wall colour glass))))))
(floats vertices)))
(defn step-mesh
"A stone step at every door. A building has one, and it also covers
the daylight under a door picture that has transparent space along its
own bottom edge, which no amount of arithmetic in here can measure."
[{:keys [boxes]} {:keys [doors]} swatches]
(let [vertices (js/Array.)
{:keys [show width]} doors
{:keys [rise tread spread] :or {rise 0.6 tread 3.0 spread 1.5}}
(:step doors)
colour (:stone swatches)]
(when (and show (get-in doors [:step :show] true))
(doseq [box boxes
face (doorways-of box)]
(let [{:keys [a b normal length]} face
[from-x from-y] a
[to-x to-y] b
[away-x away-y] normal
along-x (/ (- to-x from-x) length)
along-y (/ (- to-y from-y) length)
half (+ (/ width 2.0) spread)
middle-x (+ from-x (* along-x (/ length 2.0)) (* away-x tread 0.5))
middle-y (+ from-y (* along-y (/ length 2.0)) (* away-y tread 0.5))
reach-x (max (js/Math.abs (* along-x half))
(js/Math.abs (* away-x tread 0.5)))
reach-y (max (js/Math.abs (* along-y half))
(js/Math.abs (* away-y tread 0.5)))]
(box! vertices (- middle-x reach-x) (- middle-y reach-y) 0.0
(* 2 reach-x) (* 2 reach-y) rise colour))))
(floats vertices)))
(defn sheet-mesh
"The bell of water: a curve turned all the way round, fading out as it
falls away from the nozzle."
[fountain swatches]
(let [{:keys [x y nozzle rise reach]} (fountain-parts fountain)
steps 24
rungs 12
foam (:foam swatches)
vertices (js/Array.)
at (fn [fraction angle] [(+ x (* reach fraction (js/Math.cos angle)))
(+ y (* reach fraction (js/Math.sin angle)))
(crest nozzle rise fraction)])
fade (fn [fraction] (* 0.85 (- 1.0 (* fraction fraction))))]
(dotimes [step steps]
(let [from-angle (* 6.2832 (/ step steps))
to-angle (* 6.2832 (/ (inc step) steps))]
(dotimes [rung rungs]
(let [nearer (/ rung rungs)
further (/ (inc rung) rungs)
corners [(at nearer from-angle) (at nearer to-angle)
(at further to-angle) (at further from-angle)]
alphas [(fade nearer) (fade nearer)
(fade further) (fade further)]]
(doseq [corner quad-order]
(vertex! vertices (nth corners corner) foam [0 0 1] 0.0
(nth alphas corner)))))))
(floats vertices)))
(defn post-mesh
"Help posts: a shaft and a lamp on top of it. Small enough that a box
is a better answer than a billboard, and boxes never turn to look at
you when you walk round them."
[{:keys [posts]}
{:keys [height radius lamp] :or {height 8 radius 0.4 lamp 1.2}}
swatches checked]
(let [vertices (js/Array.)
shaft-colour (:post swatches)
waiting (:lamp swatches)
answered (:lamp-lit swatches)]
(doseq [[at [px py]] (map-indexed vector posts)]
(box! vertices (- px radius) (- py radius) 0.0
(* 2 radius) (* 2 radius) height shaft-colour)
(box! vertices (- px (/ lamp 2)) (- py (/ lamp 2)) height
lamp lamp lamp
(if (contains? checked at) answered waiting)))
(floats vertices)))
(defn roof-mesh
[{:keys [boxes]} default-top swatches]
(let [vertices (js/Array.)
colour (:roof swatches)]
(doseq [{:keys [x y w h] :as box} boxes
:when (not (round? box))]
(let [top (box-top box default-top)]
(quad! vertices
[[x y top] [(+ x w) y top] [(+ x w) (+ y h) top] [x (+ y h) top]]
colour [0 0 1] 0.0 1.0)))
(floats vertices)))
;; ---------------------------------------------------------------------
;; Points: cloud and mist
;;
;; Both are static buffers of seeds. The clock is a uniform, so nothing
;; is ever uploaded again once these exist, and standing still costs
;; the same as walking.
;; ---------------------------------------------------------------------
(defn cloud-lobes
"Each cloud is several soft points rather than one: a bright middle
and a scatter of dimmer ones around it, mostly sideways, because a
cloud is wider than it is tall. The overlaps are what read as lumpy."
[{:keys [cover rows size lobes] :or {cover 0.42 rows 9 size 0.19 lobes 10}} fov]
(let [lobe-seeds (js/Array.)
spread (js/Math.tan (/ fov 2))]
(dotimes [row rows]
(let [toward-horizon (/ (+ row 0.5) rows)
lift (+ 0.08 (* 0.66 (- 1.0 toward-horizon)))
scale (+ 0.4 (* 0.8 (- 1.0 toward-horizon)))
across (max 6 (int (/ 20 scale)))
brightness (* 0.20 (- 1.0 (* 0.5 toward-horizon)))]
(dotimes [cell across]
(let [occupied (spot cell (+ row 7))]
(when (< occupied cover)
(let [turn (* 6.2832 (/ (+ cell occupied) across))
wide (* size scale spread 1.5)]
(dotimes [lobe lobes]
(let [middle? (zero? lobe)
which-way (spot (+ cell 501 (* lobe 37)) (+ row 900))
how-far (spot (+ cell 907 (* lobe 53)) (+ row 331))
how-big (spot (+ cell 613 (* lobe 19)) (+ row 771))
angle (* 6.2832 which-way)
off-middle (if middle? 0.0 (* wide 0.85 how-far))
lobe-turn (+ turn (* off-middle (js/Math.cos angle)))
lobe-lift (- lift (js/Math.abs
(* off-middle 0.42 (js/Math.sin angle))))
girth (* wide (if middle?
(+ 0.9 (* 0.4 how-big))
(+ 0.4 (* 0.5 how-big))))
alpha (* brightness (if middle?
1.0
(+ 0.45 (* 0.5 how-big))))]
(.push lobe-seeds lobe-turn)
(.push lobe-seeds (max 0.02 lobe-lift))
(.push lobe-seeds girth)
(.push lobe-seeds alpha)))))))))
(floats lobe-seeds)))
(def mist-life
"How long a drop of mist lives, in milliseconds."
650)
(defn mist-drops
"Streams of drops thrown out of the nozzle, plus a few thrown straight
up out of it.
The jet used to be a cone of triangles standing over the fountain,
which from any angle read as a stationary spike, because that is what
it was. A jet is water in the air. The vertex shader already knows how
to throw water in the air, and a stream whose reach is almost nothing
goes almost straight up; the shader trades that missing reach for
height, so the narrow ones climb."
[density jets]
(let [streams (max 4 (int (* 58 density)))
upward (max 0 (int (* 12 jets)))
drops-per-stream 13
drop-seeds (js/Array.)
throw! (fn [stream total angle reach-scale]
(dotimes [drop drops-per-stream]
(let [ordinal (+ (* stream drops-per-stream) drop)]
(.push drop-seeds angle)
(.push drop-seeds reach-scale)
(.push drop-seeds (spot ordinal (+ 511 total)))
(.push drop-seeds (+ 1.0 (* 0.37 ordinal))))))]
(dotimes [stream streams]
(let [wobble (* 0.08 (- (spot stream 401) 0.5))]
(throw! stream streams
(+ (* 6.2832 (/ stream streams)) wobble)
(+ 0.85 (* 0.3 (spot stream 733))))))
(dotimes [stream upward]
(throw! (+ streams stream) upward
(* 6.2832 (spot stream 877))
(* 0.06 (+ 0.4 (spot stream 313)))))
(floats drop-seeds)))
;; ---------------------------------------------------------------------
;; The screen-covering rectangle the sky is painted on
;; ---------------------------------------------------------------------
(def screen-mesh
(js/Float32Array. #js [-1 -1 1 -1 1 1 -1 -1 1 1 -1 1]))</script><script type="application/x-scittle">(ns fedoras.cs-class.walk.walk-ui
"First-person walkthrough of the quad.
This file is the part that changes while you are walking, and there
is deliberately not much of it. It holds where you are, what keys are
down, and the order the programs run in for one frame. Everything
else was decided before you pressed a key: the plan is in walk.clj,
the triangles are in walk_world.cljs, the look is in walk_shaders.cljs
and walk.css, and the machinery is in walk_gl.cljs.
ONE FRAME, IN ORDER. Sky first, with no depth test, so it can never
hide anything. Clouds on top of it, added rather than painted over,
which is what makes them look lit from behind. Then the depth buffer
comes on and everything solid goes down in whatever order is
convenient, because the card is sorting it out per pixel now and the
painter's algorithm and its sorting are gone. Then the things that
see through themselves: doors, the water bell, the mist. Those go
last, with depth testing still on and depth writing off, which is the
whole of what used to be four hand-written occlusion tests around the
fountain."
(:require [clojure.string :as str]
[fedoras.reader :as reader]
[fedoras.ui.widget-ui :as widget]
[fedoras.cs-class.walk.walk-gl :as gl]
[fedoras.cs-class.walk.walk-world :as world]))
(def default-settings
{:resolution [420 236]
:fov 1.05
:eye-height 5.6
:draw 520
:bound 1000
:walk-speed 2.6
:back-speed 2.0
:side-speed 2.0
:turn-speed 0.045
:sun {:azimuth 2.2 :height 0.30 :size 44 :ambient 0.62}
:ground {:slab 4 :joint 0.5 :tuft 4 :patch 17 :grain 12
:mow 9 :bay 9 :tie 2.4 :lift 0.05}
:haze {:clarity 1.6 :horizon 0.82}
:fill {:show true :peak 16 :roughness 0.9 :step 5}
:water {:show true :depth 5 :bank 14 :step 6
:waterline -0.6 :alpha 0.72 :swell 0.35 :wavelength 9
:breath 12}
:brick {:width 2.2 :height 0.9 :mortar 0.14 :streak 9 :detail 150}
:doors {:show true :width 7 :height 12 :sill -0.8
:step {:show true :rise 0.6 :tread 3.0 :spread 1.5}}
:windows {:show true :width 3.4 :height 3.4}
:buildings {:height 30}
:rotunda {:show true :wall 15 :dome 12 :facets 48 :rings 7
:glass 0.26 :door 9}
:fountain {:show true :animate true :ripples 3
:x 455 :y 330 :radius 16 :rim 1.4 :water 1.6
:pedestal {:radius 2.4 :height 4.5}
:spray {:width 1.0 :height 1.0 :density 1.0 :jets 1.0}}
:trees {:show true :spread 1.15 :trunk 0.75 :fork 0.34
:taper 0.45 :limbs 0.5}
:bushes {:show true :spread 1.15}
:posts {:show true :height 8 :radius 0.4 :lamp 1.2 :reach 9}
:clouds {:show true :cover 0.42 :drift 0.00004 :rows 9 :size 0.19
:lobes 10 :ragged 0.22 :flat 0.6}
:wraith {:show false :speed 1.7 :width 2.4 :height 7.5
:wake 260 :touch 6 :start [900 700] :quicken 0.35
:sound {:on false :level 0.35 :pitch 70 :file nil}}
:lines {:dash 18 :on 10 :width 0.4 :bay-width 0.7
:centre-inner 0.3 :centre-outer 0.9
:rail-inner 1.9 :rail-outer 2.5
:zebra 5 :bar 2}})
(defonce plan (atom nil))
(defonce settings (atom default-settings))
(defonce eye (atom {:at [450 330] :facing 0.0 :held #{} :stand 0.0 :sank nil}))
(defonce scene (atom nil))
(defonce dirty (atom true))
(defonce ticks (atom 0))
(defonce wraith (atom nil))
(defonce checked (atom #{}))
(defonce noise (atom nil))
(defonce loop-id (atom 0))
(def ^:private walking-keys
#{"arrowup" "arrowdown" "arrowleft" "arrowright" "a" "d"})
(def ^:private near-plane 1.0)
;; ---------------------------------------------------------------------
;; The programs, built once per mount
;; ---------------------------------------------------------------------
(defn- build-programs
"Every program, compiled from the GLSL the build worked out and sent
along in the plan, each told its own name so a card that refuses one
says which one it refused.
Nothing here writes GLSL or reads it. The shaders were written as
Clojure in walk_shaders.clj, typechecked there, and turned into text
by fedoras.glsl before this page existed."
[gl shaders]
(into {}
(map (fn [[which {:keys [vertex fragment]}]]
[which (gl/program gl (name which) vertex fragment)]))
shaders))
;; ---------------------------------------------------------------------
;; Pictures
;;
;; A door arrives as an SVG, which a card cannot sample directly, so it
;; is drawn once onto a canvas of a size the card is happy with and
;; that canvas is what becomes the texture. Nothing waits for it: until
;; it lands there is simply no door mesh drawn, and the wall behind is
;; already correct.
;; ---------------------------------------------------------------------
(defn- data-url
"SVG text as something an Image will load. Percent-encoded rather than
base64, because an SVG is text and btoa refuses anything outside
Latin-1, which one curly quote in a label is enough to trigger."
[svg]
(str "data:image/svg+xml;charset=utf-8," (js/encodeURIComponent svg)))
(defn- load-picture!
"Rasterize an SVG to a power-of-two canvas and hand the result to the
card.
The SVG arrives as text, inlined by the build, and is turned into a
data URL here. Fetching it as a file instead is what a browser calls
cross-origin when the book is opened from disc: the image loads, the
canvas it is drawn on is quietly tainted, and the upload to the card
then throws SecurityError from inside a load callback, several
seconds after anything that could be blamed for it. A data URL is
same-origin and taints nothing.
Width and height are set on the element before the source, because an
SVG without an intrinsic size draws as nothing."
[gl svg picture-width picture-height keep!]
(when (and (string? svg) (seq svg))
(let [image (js/Image.)]
(set! (.-width image) picture-width)
(set! (.-height image) picture-height)
(set! (.-onload image)
(fn [_]
(try
(let [canvas (.createElement js/document "canvas")]
(set! (.-width canvas) picture-width)
(set! (.-height canvas) picture-height)
(.drawImage (.getContext canvas "2d") image 0 0
picture-width picture-height)
(keep! (gl/picture-texture gl canvas true))
(reset! dirty true))
(catch :default problem
(js/console.error "walk: a picture would not go on the card"
problem)))))
(set! (.-onerror image)
(fn [_] (js/console.error "walk: a picture did not decode")))
(set! (.-src image) (data-url svg)))))
;; ---------------------------------------------------------------------
;; The meshes, rebuilt whenever the settings run
;; ---------------------------------------------------------------------
(defn- drop-meshes!
[gl meshes]
(doseq [[_ mesh] meshes]
(when mesh (gl/drop-mesh! gl mesh))))
(defn- build-meshes
"Every buffer the card will hold, built once from the plan and the
settings as they stand. Nothing here is touched again until the
reader presses Run."
[gl node]
(let [settings-now @settings
plan-now @plan
layouts (:layouts plan-now)
ground (world/terrain plan-now settings-now)
swatches (world/read-swatches node)
default-top (:height (:buildings settings-now))
reach (:draw settings-now)
buffer (fn [layout numbers] (gl/mesh gl layout numbers))
showing (fn [which plants] (if (:show (which settings-now)) plants []))]
{:swatches swatches
:terrain ground
:meshes
{:screen (buffer (:sky layouts) world/screen-mesh)
:wraith (buffer (:wraith layouts) world/screen-mesh)
:ground (buffer (:ground layouts)
(world/ground-mesh plan-now settings-now ground reach))
:wall (buffer (:wall layouts)
(world/wall-mesh plan-now default-top))
:door (buffer (:panel layouts)
(world/door-mesh plan-now settings-now))
:window (buffer (:panel layouts)
(world/window-mesh plan-now settings-now))
:trees (buffer (:billboard layouts)
(world/billboard-mesh (showing :trees (:trees plan-now))
(:spread (:trees settings-now))))
:bushes (buffer (:billboard layouts)
(world/billboard-mesh (showing :bushes (:bushes plan-now))
(:spread (:bushes settings-now))))
:step (buffer (:solid layouts)
(world/step-mesh plan-now settings-now swatches))
:water (buffer (:solid layouts)
(world/water-mesh ground settings-now swatches))
:roof (buffer (:solid layouts)
(world/roof-mesh plan-now default-top swatches))
:rotunda (buffer (:solid layouts)
(world/rotunda-mesh plan-now settings-now swatches))
:glass (buffer (:solid layouts)
(world/glass-mesh plan-now settings-now swatches))
:posts (buffer (:solid layouts)
(world/post-mesh (assoc plan-now :posts
(showing :posts (:posts plan-now)))
(:posts settings-now) swatches
@checked))
:fountain (buffer (:solid layouts)
(world/fountain-mesh (:fountain settings-now) swatches
(:ripples (:fountain settings-now))))
:sheet (buffer (:solid layouts)
(world/sheet-mesh (:fountain settings-now) swatches))
:cloud (buffer (:cloud layouts)
(world/cloud-lobes (:clouds settings-now) (:fov settings-now)))
:mist (buffer (:mist layouts)
(world/mist-drops
(:density (:spray (:fountain settings-now)))
(:jets (:spray (:fountain settings-now)) 1.0)))}}))
;; ---------------------------------------------------------------------
;; Where the eye is
;; ---------------------------------------------------------------------
(defn- standing-height
"How high the ground is where you are. Up THE FILL, down into SPRING
BROOK, flat everywhere else."
[px py]
(world/ground-height (:terrain @scene) @settings px py))
(def ^:private cushion
"How far off a wall you are stopped, in feet. You are wider than a
point and the glass is closer than it looks."
3)
(defn- through-a-doorway?
"Whether a point lines up with one of a round building's ways in. A
doorway is a span of angle, and standing in one means the wall is not
there."
[box toward-x toward-y radius]
(let [gap (/ (/ (get-in @settings [:rotunda :door] 9) 2.0) (max radius 1.0))
bearing (js/Math.atan2 toward-y toward-x)]
(some (fn [{:keys [normal]}]
(let [[out-x out-y] normal
facing (js/Math.atan2 out-y out-x)
apart (js/Math.abs
(- (mod (+ (- bearing facing) 9.42478) 6.28318)
3.14159))]
(> apart (- 3.14159 gap))))
(:faces box))))
(defn- stopped-by-rotunda?
"The library stops you at its glass, and inside it at whichever of the
four blocks you have walked into. The way in is the two doorways, and
once you are through one of them the wall behind you is not in the way
either: the test is the ring of glass, not the circle it encloses."
[box px py]
(let [[middle-x middle-y] (:middle box)
radius (:radius box)
toward-x (- px middle-x)
toward-y (- py middle-y)
out (js/Math.sqrt (+ (* toward-x toward-x) (* toward-y toward-y)))]
(or (some (fn [{:keys [x y w h]}]
(and (> px (- x 1.5)) (< px (+ x w 1.5))
(> py (- y 1.5)) (< py (+ y h 1.5))))
(:rooms box))
;; The inner edge of the ring is never thinner than a single
;; step. A reader who winds :walk-speed up past the thickness of
;; the glass would otherwise walk straight through it, which is
;; the one failure a ring has that a disc does not.
(let [band (max cushion (:walk-speed @settings 2.6))]
(and (> out (- radius band))
(< out (+ radius cushion))
(not (through-a-doorway? box toward-x toward-y radius)))))))
(defn- blocked?
"A building plus three feet, or the fountain plus three feet. The Fill
is not in this list, because the Fill is a hill and you are meant to
go up it, and neither is the library, which you are meant to go into."
[[px py]]
(let [{:keys [boxes]} @plan
fountain (:fountain @settings)]
(boolean
(or (some (fn [{:keys [x y w h] :as box}]
(if (= :round (:shape box))
(stopped-by-rotunda? box px py)
(and (> px (- x cushion)) (< px (+ x w cushion))
(> py (- y cushion)) (< py (+ y h cushion)))))
boxes)
(when (:show fountain)
(let [toward-x (- px (:x fountain))
toward-y (- py (:y fountain))
out (+ (:radius fountain) cushion)]
(< (+ (* toward-x toward-x) (* toward-y toward-y)) (* out out))))))))
(defn- move!
"Applies held keys to position and facing for one tick, wrapping at
the world bound with a facing mirror so walking off one edge faces
you back into the world rather than away from it. Gives back true if
anything moved."
[]
(let [{:keys [held at facing]} @eye
{:keys [walk-speed back-speed side-speed turn-speed bound]
:or {bound 1000}} @settings
[x y] at
turn (cond (held "arrowleft") (- turn-speed)
(held "arrowright") turn-speed
:else 0)
forward (cond (held "arrowup") walk-speed
(held "arrowdown") (- back-speed)
:else 0)
sideways (cond (held "a") (- side-speed)
(held "d") side-speed
:else 0)]
(if (and (zero? turn) (zero? forward) (zero? sideways))
false
(let [aimed (+ facing turn)
step-x (+ x (* forward (js/Math.cos aimed)) (* sideways (- (js/Math.sin aimed))))
step-y (+ y (* forward (js/Math.sin aimed)) (* sideways (js/Math.cos aimed)))
over-x (> (js/Math.abs step-x) bound)
over-y (> (js/Math.abs step-y) bound)
wrapped-x (if over-x (- (* 2 bound (js/Math.sign step-x)) step-x) step-x)
wrapped-y (if over-y (- (* 2 bound (js/Math.sign step-y)) step-y) step-y)
mirrored (cond-> aimed
over-x (as-> a (- 3.14159265 a))
over-y (as-> a (- a)))]
(swap! eye assoc :facing mirrored)
(when-not (blocked? [wrapped-x y]) (swap! eye assoc-in [:at 0] wrapped-x))
(when-not (blocked? [(get-in @eye [:at 0]) wrapped-y])
(swap! eye assoc-in [:at 1] wrapped-y))
(let [[nx ny] (:at @eye)]
(swap! eye assoc :stand (standing-height nx ny)))
true))))
;; ---------------------------------------------------------------------
;; Whatever it is that follows you
;;
;; It walks at very nearly your own speed, which is the whole design. You
;; can open a gap and you cannot keep it, and standing still to read a
;; building's name costs you every foot you had. It does not open doors,
;; go round the far side of a building cleverly, or hide. It comes.
;; ---------------------------------------------------------------------
;; ---------------------------------------------------------------------
;; The noise it makes
;;
;; Two ways of making one, and neither is on unless a reader asks. With
;; no file named, a square wave at :pitch, which needs nothing off a
;; disc and is already unpleasant. With a file named, that file, on a
;; loop, played by a plain audio element rather than through the audio
;; graph: routing a file through the graph is what taints it when the
;; book is opened from disc, and a silent monster is worse than a simple
;; one.
;;
;; Either way it is silent until the thing is within :wake, and loudest
;; when it reaches you.
;; ---------------------------------------------------------------------
(defn- hush!
"Stop whatever is making the noise and forget it. Safe to call when
there is nothing making a noise, which is most of the time."
[]
(when-let [{:keys [source element context]} @noise]
(try (when source (.stop source)) (catch :default _ nil))
(try (when element (.pause element)) (catch :default _ nil))
(try (when context (.close context)) (catch :default _ nil)))
(swap! noise dissoc :source :element :context :gain))
(defn- start-noise!
"Build whichever noise the settings ask for, silent, ready to be turned
up. A browser will not let anything make a sound before the reader has
touched the page, so this only gets as far as building it; `wake-noise!`
starts it on the first key or click.
Nothing is rebuilt unless the settings that decide what the noise is
have actually changed. This runs on every render, and tearing an audio
context down and putting it back up again is both audible and enough
to lose the permission the reader already gave."
[]
(let [{:keys [show] :as beast} (:wraith @settings)
{:keys [on pitch file] :or {pitch 70}} (:sound beast)
wanted [show on pitch file]]
(when (not= wanted (:wanted @noise))
(hush!)
(reset! noise {:wanted wanted})
(when (and show on)
(if (and (string? file) (seq file))
(let [element (js/Audio. file)]
(set! (.-loop element) true)
(set! (.-volume element) 0)
(swap! noise assoc :element element))
(try
(let [context (js/AudioContext.)
oscillator (.createOscillator context)
gain (.createGain context)]
(set! (.-type oscillator) "square")
(set! (.-value (.-frequency oscillator)) pitch)
(set! (.-value (.-gain gain)) 0)
(.connect oscillator gain)
(.connect gain (.-destination context))
(.start oscillator)
(swap! noise assoc
:context context :source oscillator :gain gain))
(catch :default problem
(js/console.warn "walk: this browser will not make a noise"
problem))))))))
(defn- wake-noise!
"A browser starts an audio context asleep and will not wake it until
the reader has done something. The first key or click is that."
[]
(when-let [{:keys [context]} @noise]
(try
(when (= "suspended" (.-state context)) (.resume context))
(catch :default _ nil))))
(defn- howl!
"How loud it is, worked out from how close it is. Silent at :wake, all
the way up at :touch, and it climbs faster than the distance closes so
that the last thirty feet are the ones you notice."
[gap]
(when-let [{:keys [gain source element]} @noise]
(let [{:keys [wake touch] :or {wake 260 touch 6}} (:wraith @settings)
{:keys [level pitch] :or {level 0.35 pitch 70}}
(:sound (:wraith @settings))
nearness (if gap
(max 0.0 (min 1.0 (/ (- wake gap) (max 1.0 (- wake touch)))))
0.0)
loudness (* level nearness nearness)]
(try
(if gain
(do (set! (.-value (.-gain gain)) loudness)
(set! (.-value (.-frequency source))
(* pitch (+ 1.0 (* 1.6 nearness)))))
(do (set! (.-volume element) (max 0.0 (min 1.0 loudness)))
(when (and (.-paused element) (pos? loudness))
(.catch (.play element) (fn [_] nil)))))
(catch :default _ nil)))))
;; ---------------------------------------------------------------------
;; The emergency posts
;;
;; Twenty of them, already standing, already lit, and until now the only
;; thing on the campus that did nothing at all. Walk up to one and it
;; answers. That is the whole of it: there is no puzzle, no order to do
;; them in, and no reward for finishing beyond finding out that all
;; twenty of them work.
;;
;; They are also the reason to be careful about what else is out there.
;; Every post you answer makes the thing that follows you a little
;; faster, so the only thing on this campus that gives you a reason to
;; walk about is also the thing that makes walking about worse.
;; ---------------------------------------------------------------------
(defn- relamp!
"Rebuild the posts so the ones that have answered are lit differently.
Thirteen hundred numbers; cheaper than any way of avoiding it."
[]
(when-let [{:keys [gl meshes swatches]} @scene]
(when-let [layout (get-in @plan [:layouts :solid])]
(gl/drop-mesh! gl (:posts meshes))
(swap! scene assoc-in [:meshes :posts]
(gl/mesh gl layout
(world/post-mesh @plan (:posts @settings)
swatches @checked))))))
(defn- answer-posts!
"Any post you are standing next to has now been answered."
[]
(let [posts (:posts @plan)
reach (get-in @settings [:posts :reach] 9)
[eye-x eye-y] (:at @eye)
found (keep-indexed
(fn [at [px py]]
(when (and (not (contains? @checked at))
(< (+ (* (- px eye-x) (- px eye-x))
(* (- py eye-y) (- py eye-y)))
(* reach reach))
(:show (:posts @settings)))
at))
posts)]
(when (seq found)
(swap! checked into found)
(relamp!)
(reset! dirty true)
(when (= (count @checked) (count posts))
;; A point of fortune, once, silently, the same as anything else
;; a reader actually finishes. Guarded because the deed has to be
;; registered in fedoras.endings.standing/deeds before it counts
;; for anything, and a walk that throws because nobody has got
;; round to that yet is worse than a walk that awards nothing.
(try (widget/did! ::every-post)
(catch :default problem
(js/console.warn "walk: no deed registered for the posts"
problem)))))))
(defn- follow!
"One step of whatever it is. It walks straight at you, and when a
building is in the way it takes whichever of the two axes is still
clear, which is enough to get round a rectangle and not enough to look
like it is thinking."
[]
(let [{:keys [show speed start quicken] :or {speed 1.7 quicken 0.0}}
(:wraith @settings)
answered (/ (count @checked) (max 1 (count (:posts @plan))))
speed (* speed (+ 1.0 (* quicken answered)))
[eye-x eye-y] (:at @eye)]
(when show
(when (nil? @wraith)
(reset! wraith (vec (or start [900 700]))))
(let [[at-x at-y] @wraith
toward-x (- eye-x at-x)
toward-y (- eye-y at-y)
gap (js/Math.sqrt (+ (* toward-x toward-x) (* toward-y toward-y)))]
(when (> gap 0.001)
(let [step-x (* speed (/ toward-x gap))
step-y (* speed (/ toward-y gap))
straight [(+ at-x step-x) (+ at-y step-y)]
sideways [(+ at-x step-x) at-y]
lengthways [at-x (+ at-y step-y)]]
(reset! wraith
(cond
(not (blocked? straight)) straight
(not (blocked? sideways)) sideways
(not (blocked? lengthways)) lengthways
:else @wraith))))))))
(defn- how-close
"Feet between you and it, or nil when there is nothing to measure."
[]
(when-let [[at-x at-y] @wraith]
(let [[eye-x eye-y] (:at @eye)]
(js/Math.sqrt (+ (* (- at-x eye-x) (- at-x eye-x))
(* (- at-y eye-y) (- at-y eye-y)))))))
(defn- caught!
"What happens when it reaches you, which is nothing much. You are put
back on the quad and it is put back where it started, and neither of
you says anything about it."
[]
(let [{:keys [touch start] :or {touch 6}} (:wraith @settings)
gap (how-close)]
(when (and gap (< gap touch))
(let [[start-x start-y] (:at @plan)]
(swap! eye assoc
:at [start-x start-y]
:held #{}
:sank nil
:stand (standing-height start-x start-y))
(reset! wraith (vec (or start [900 700])))
(reset! dirty true)))))
(defn- in-the-water?
"Whether the ground you are standing on is below the waterline."
[stand]
(< stand (- (get-in @settings [:water :waterline] -0.6) 0.2)))
(defn- breathe!
"Starts the clock when you step into the brook, stops it when you get
out, and puts you back where you started if it runs out.
There is nothing here that could hurt you. Being returned to the quad
is the entire consequence, and the campus does not comment on it
afterwards."
[now]
(let [{:keys [stand sank]} @eye
breath (* 1000 (get-in @settings [:water :breath] 12))]
(cond
(not (in-the-water? stand))
(when sank (swap! eye assoc :sank nil))
(nil? sank)
(swap! eye assoc :sank now)
(> (- now sank) breath)
(let [[start-x start-y] (:at @plan)]
(swap! eye assoc
:at [start-x start-y]
:sank nil
:held #{}
:stand (standing-height start-x start-y))
(reset! dirty true)))))
(defn- held-breath
"Seconds of air left, or nil when you are on dry land."
[now]
(when-let [sank (:sank @eye)]
(let [breath (get-in @settings [:water :breath] 12)]
(max 0 (js/Math.ceil (- breath (/ (- now sank) 1000)))))))
;; ---------------------------------------------------------------------
;; What is ahead
;; ---------------------------------------------------------------------
(defn- hit-box
"How far along a ray the first face of a box is, or nil.
The slab method. A ray is inside a box exactly when it is between the
two east-west edges and between the two north-south edges at the same
time, so work out the stretch of the ray that satisfies each pair and
intersect the two. Whatever is left is the time inside the box, and
it enters at the start of it."
[[from-x from-y] [toward-x toward-y] {:keys [x y w h]}]
(let [per-foot-east (if (zero? toward-x) 1e9 (/ 1.0 toward-x))
per-foot-south (if (zero? toward-y) 1e9 (/ 1.0 toward-y))
west-edge (* (- x from-x) per-foot-east)
east-edge (* (- (+ x w) from-x) per-foot-east)
north-edge (* (- y from-y) per-foot-south)
south-edge (* (- (+ y h) from-y) per-foot-south)
entering (max (min west-edge east-edge) (min north-edge south-edge))
leaving (min (max west-edge east-edge) (max north-edge south-edge))]
(when (and (>= leaving (max entering 0.0)) (> entering 0.001))
entering)))
(defn- hit-circle
"How far along a ray the edge of a circle is, or nil. The ray's own
direction is a unit step, so the quadratic has no leading term to
divide by. From inside the circle there is no edge in front of you to
meet, which is the right answer: standing in the library, the library
is not something you are looking at."
[[from-x from-y] [toward-x toward-y] {:keys [middle radius]}]
(let [[middle-x middle-y] middle
offset-x (- from-x middle-x)
offset-y (- from-y middle-y)
along (+ (* offset-x toward-x) (* offset-y toward-y))
clearance (- (+ (* offset-x offset-x) (* offset-y offset-y))
(* radius radius))
room (- (* along along) clearance)]
(when (and (>= room 0.0) (pos? clearance))
(let [entering (- (- along) (js/Math.sqrt room))]
(when (> entering 0.001) entering)))))
(defn- hit
[origin toward {:keys [shape] :as landmark}]
(if (= :circle shape)
(hit-circle origin toward landmark)
(hit-box origin toward landmark)))
(defn- gather-landmarks
"Everything on the campus with a name that stands up out of the ground
far enough to be looked at.
It used to be the buildings and nothing else, so a reader could stand
in front of the fountain, or at the foot of a sixteen foot heap of
landfill, and be told that there was nothing in front of them. The
lawns are left out on purpose: you are nearly always standing on one,
and being told so ahead as well as underfoot is noise.
The fountain comes from the settings rather than the plan because a
reader can move it."
[plan settings terrain]
(let [fountain (:fountain settings)]
(vec
(concat
(mapcat (fn [box]
(if (= :round (:shape box))
(cons (assoc (select-keys box [:name :middle :radius])
:shape :circle)
(map (fn [room] (assoc room :shape :box)) (:rooms box)))
[(assoc (select-keys box [:name :x :y :w :h]) :shape :box)]))
(:boxes plan))
(when (:show fountain)
[{:name "THE FOUNTAIN" :shape :circle
:middle [(:x fountain) (:y fountain)] :radius (:radius fountain)}])
(keep (fn [rect]
(when (:name rect)
(assoc (select-keys rect [:name :x :y :w :h]) :shape :box)))
terrain)))))
(defn- nearest
"The first named thing along a ray, and how far off it is."
[origin toward]
(reduce (fn [best landmark]
(if-let [distance (hit origin toward landmark)]
(if (or (nil? best) (< distance (first best)))
[distance landmark]
best)
best))
nil
(:landmarks @scene)))
(defn- hiccup->dom
"A small hiccup subset: [tag attrs? & children], plain strings, or
nil, turned into real DOM nodes."
[node]
(cond
(nil? node) nil
(string? node) (.createTextNode js/document node)
(vector? node)
(let [[tag maybe-attrs & rest-children] node
has-attrs? (map? maybe-attrs)
attrs (if has-attrs? maybe-attrs {})
children (if has-attrs? rest-children (cons maybe-attrs rest-children))
element (.createElement js/document (name tag))]
(doseq [[attribute value] attrs]
(.setAttribute element (name attribute) (str value)))
(doseq [child children]
(when-let [child-node (hiccup->dom child)]
(.appendChild element child-node)))
element)))
(defn- render-into!
[element & pieces]
(while (.-firstChild element)
(.removeChild element (.-firstChild element)))
(doseq [piece pieces]
(when-let [dom-node (hiccup->dom piece)]
(.appendChild element dom-node))))
(defn- say!
[node now]
(let [{:keys [at facing stand]} @eye
[x y] at
found (nearest at [(js/Math.cos facing) (js/Math.sin facing)])
under (if-let [grid (:index @scene)]
(name (nth world/materials (world/surface-at grid x y)))
"lawn")
on-lawn? (= under "lawn")
air (held-breath now)
gap (when (get-in @settings [:wraith :show]) (how-close))
aside (cond air [:span {:class "walk-keep-off"}
(str "· hold your breath · " air)]
(and gap (< gap 90)) [:span {:class "walk-keep-off"}
(str "· " (int gap) " feet behind you")]
on-lawn? [:span {:class "walk-keep-off"}
"· keep off the grass"]
:else nil)]
(widget/state! node
(str (int x) ", " (int y)
(when (:show (:posts @settings))
(str " · posts " (count @checked)
"/" (count (:posts @plan))))))
(when-let [el (reader/q node ".walk-ahead")]
(render-into! el
(if found
(str (:name (second found)) " · " (int (first found)) " feet")
"nothing in front of you")
(str (if air " · standing in " " · standing on ") under
(when (> stand 0.5) (str " · " (int stand) " feet up"))
(when aside " "))
aside
(when (and (:show (:posts @settings))
(seq (:posts @plan))
(= (count @checked) (count (:posts @plan))))
[:span {:class "walk-keep-off"}
" · every post on this campus works"])))))
;; ---------------------------------------------------------------------
;; One frame
;; ---------------------------------------------------------------------
(defn- sun-direction
[{:keys [azimuth height]}]
(let [lift (max -0.98 (min 0.98 height))
flat (js/Math.sqrt (- 1.0 (* lift lift)))]
[(* flat (js/Math.cos azimuth)) (* flat (js/Math.sin azimuth)) lift]))
(defn draw!
"One frame, in the order the header of this file gives."
[canvas now]
(when-let [{:keys [gl programs meshes palette pictures swatches biggest]} @scene]
(let [settings-now @settings
{:keys [at facing stand]} @eye
canvas-width (.-width canvas)
canvas-height (.-height canvas)
aspect (/ canvas-width canvas-height)
fov (:fov settings-now)
spread (js/Math.tan (/ fov 2))
focal (/ (/ canvas-width 2.0) spread)
reach (:draw settings-now)
eye-at [(nth at 0) (nth at 1) (+ stand (:eye-height settings-now))]
camera (gl/times (gl/perspective fov aspect near-plane (max 2600 (* reach 3)))
(gl/look eye-at facing))
eastward (js/Math.cos facing)
southward (js/Math.sin facing)
sun (:sun settings-now)
fountain (world/fountain-parts (:fountain settings-now))
{:keys [ground brick clouds trees lines water]} settings-now
{:keys [clarity horizon]} (:haze settings-now)
;; Every program that fogs takes the same three numbers, and
;; only the middle one is its own business: for the ground it
;; is how far out the haze takes over, and for a wall it is how
;; far out a brick stops being a brick and becomes a moire
;; pattern.
reach-of (fn [own] [reach own clarity])
;; What every program is told about this frame, whether it asks
;; or not. A program ignores a uniform it does not declare, so
;; telling it costs nothing, and a fact about the frame is
;; written down once instead of once per draw. What a program
;; is told on top of this is only what is true of it alone.
shared {"u_camera" camera
"u_eye" eye-at
"u_right" [(- southward) eastward 0]
"u_haze" (:haze swatches)
"u_sky" (:sky swatches)
"u_sun" (:sun swatches)
"u_reach" (reach-of 0.0)
"u_sunward" (sun-direction sun)
"u_ambient" (:ambient sun)
"u_foam" (:foam swatches)
"u_middle" [(:x fountain) (:y fountain) (:nozzle fountain)]
"u_swell" [(:swell water) (:wavelength water)]
"u_focal" focal
"u_biggest" biggest
"u_now" (mod (* now 0.0007) 1.0)}
draw (fn [program mesh own & [mode]]
(gl/draw! gl (get programs program) (get meshes mesh)
(merge shared own) (or mode gl/TRIANGLES)))
haze (:haze swatches)]
(.viewport gl 0 0 canvas-width canvas-height)
(.clearColor gl (nth haze 0) (nth haze 1) (nth haze 2) 1)
(.clear gl (bit-or gl/COLOR_BUFFER_BIT gl/DEPTH_BUFFER_BIT))
(.disable gl gl/CULL_FACE)
(.disable gl gl/DEPTH_TEST)
(.disable gl gl/BLEND)
(draw :sky :screen {"u_up" [0 0 1]
"u_forward" [eastward southward 0]
"u_spread" [spread (/ spread aspect)]
"u_size" (:size sun)})
(when (:show clouds)
(.enable gl gl/BLEND)
(.blendFunc gl gl/SRC_ALPHA gl/ONE)
(draw :cloud :cloud {"u_now" now
"u_drift" (:drift clouds)
"u_dome" 2000.0
"u_shape" [(:ragged clouds) (:flat clouds)]
"u_tint" [1 1 1]}
gl/POINTS)
(.disable gl gl/BLEND))
(.enable gl gl/DEPTH_TEST)
(.depthFunc gl gl/LESS)
(.depthMask gl true)
(gl/sampler! gl (:ground programs) "u_palette" 0 palette)
(draw :ground :ground {"u_reach" (reach-of horizon)
"u_grain" [(:slab ground) (:joint ground)
(:tuft ground) (:patch ground)]
"u_marks" [(:mow ground) (:bay ground)
(:tie ground) (:grain ground)]
"u_lines" [(:dash lines) (:on lines)
(:width lines) (:bay-width lines)]
"u_edges" [(:centre-inner lines) (:centre-outer lines)
(:rail-inner lines) (:rail-outer lines)]
"u_zebra" [(:zebra lines) (:bar lines)]})
(draw :wall :wall {"u_reach" (reach-of (:detail brick))
"u_brick" [(:width brick) (:height brick)
(:mortar brick) (:streak brick)]
"u_brickA" (:brick-a swatches)
"u_brickB" (:brick-b swatches)
"u_brickC" (:brick-c swatches)
"u_mortar" (:mortar swatches)
"u_face" (:face swatches)})
(doseq [mesh [:roof :posts :step :rotunda]]
(draw :solid mesh {}))
(when (:show (:fountain settings-now))
(draw :solid :fountain {}))
(draw :billboard :trees {"u_leaf" (:leaf swatches)
"u_leafLit" (:leaf-lit swatches)
"u_bark" (:bark swatches)
"u_trunk" [(:trunk trees) (:fork trees)
(:taper trees) (:limbs trees)]})
(draw :billboard :bushes {"u_leaf" (:bush swatches)
"u_leafLit" (:bush-lit swatches)
"u_bark" (:bark swatches)
"u_trunk" [0.0 0.0 1.0 0.0]})
(.enable gl gl/BLEND)
(.blendFunc gl gl/SRC_ALPHA gl/ONE_MINUS_SRC_ALPHA)
;; Doors and windows are pictures, and a picture's clear parts are
;; thrown away rather than blended, so what is left of one is
;; solid and has to say so to the depth buffer. It used to be the
;; only see-through thing that did not, which let the far side of
;; the library's glass, drawn after it, paint straight over its
;; door. From outside, the doors looked like more glass.
(.depthMask gl true)
(doseq [kind [:door :window]]
(when-let [picture (get pictures kind)]
(gl/sampler! gl (:panel programs) "u_image" 0 picture)
(draw :panel kind {})))
(.depthMask gl false)
;; The library's wall, then the top of the brook, then the
;; fountain's own water. None of the three is ever in front of
;; another, and the order is the order a reader is likeliest to be
;; standing inside them.
(draw :solid :glass {})
(when (:show water)
(draw :solid :water {}))
(when (:show (:fountain settings-now))
(draw :solid :sheet {})
(.blendFunc gl gl/SRC_ALPHA gl/ONE)
(draw :mist :mist {"u_bell" [(min (* (:reach fountain) 1.35)
(* (:inner fountain) 0.95))
(* (:rise fountain) 1.6)
world/mist-life
(:inner fountain)]
"u_now" now
"u_shape" [0.0 0.0]
"u_tint" (:foam swatches)}
gl/POINTS))
;; Drawn last of everything that can be seen through, and only
;; once it is near enough to be worth the alarm. The three numbers
;; in u_glitch all climb as it closes, so the nearer it is the
;; more of it there is, which is the only warning anybody gets.
(when-let [[at-x at-y] (and (:show (:wraith settings-now)) @wraith)]
(let [{:keys [wake width height] :or {wake 260 width 2.4 height 7.5}}
(:wraith settings-now)
gap (js/Math.sqrt (+ (* (- at-x (nth at 0)) (- at-x (nth at 0)))
(* (- at-y (nth at 1)) (- at-y (nth at 1)))))
near (- 1.0 (min 1.0 (/ gap (max 1.0 wake))))]
(when (pos? near)
(draw :wraith :wraith {"u_root" [at-x at-y (standing-height at-x at-y)]
"u_size" [width height]
"u_tint" haze
"u_glitch" [(* 0.30 (+ 0.25 near))
(+ 0.45 (* 0.5 near))
(+ 0.25 (* 0.7 near))]
"u_now" (mod (* now 0.001) 1.0)}))))
(.depthMask gl true)
(.disable gl gl/BLEND))))
;; ---------------------------------------------------------------------
;; Input
;; ---------------------------------------------------------------------
(defn- wire-keys!
[canvas]
(set! (.-tabIndex canvas) 0)
(set! (.-onkeydown canvas)
(fn [event]
(let [pressed (str/lower-case (.-key event))]
(wake-noise!)
(when (contains? walking-keys pressed)
(.preventDefault event)
(swap! eye update :held conj pressed)))))
(set! (.-onkeyup canvas)
(fn [event]
(swap! eye update :held disj (str/lower-case (.-key event)))))
(set! (.-onblur canvas) (fn [_] (swap! eye assoc :held #{}) (reset! dirty true)))
(set! (.-onfocus canvas) (fn [_] (reset! dirty true)))
(set! (.-onpointerdown canvas) (fn [_] (wake-noise!) (.focus canvas))))
;; ---------------------------------------------------------------------
;; Setting up and running
;; ---------------------------------------------------------------------
(defn- deep-merge
"Like merge, but a key whose value is a map in both gets merged rather
than replaced. Plain merge silently drops any key a reader's own
settings map does not happen to repeat, so a settings file written
before :streak existed would wipe out the default every time, even
though the reader never asked for that and only never knew to
restate it."
[shipped asked-for]
(if (and (map? shipped) (map? asked-for))
(merge-with deep-merge shipped asked-for)
asked-for))
(defn paint!
"Called when the settings are run and on every render. Sizes the
canvas, rebuilds every mesh and the palette, and asks for a frame."
[node]
(when-let [canvas (reader/q node ".walk-canvas")]
(let [[asked-width asked-height] (:resolution @settings)]
(when (or (not= asked-width (.-width canvas))
(not= asked-height (.-height canvas)))
(set! (.-width canvas) asked-width)
(set! (.-height canvas) asked-height)))
(when-let [{:keys [gl meshes palette]} @scene]
(drop-meshes! gl meshes)
(when palette (.deleteTexture gl palette))
(let [{:keys [swatches terrain] :as built} (build-meshes gl node)]
(swap! scene assoc
:meshes (:meshes built)
:swatches swatches
:terrain terrain
:landmarks (gather-landmarks @plan @settings terrain)
:palette (gl/bytes-texture gl world/palette-width 2
(world/palette-bytes node))
:index (world/index (:surfaces @plan)))))
(start-noise!)
(reset! dirty true)))
(defn- advance!
"Everything that happens in a tick whether or not anybody is looking:
you move, the posts you are standing by answer, the thing that follows
you takes its step and makes its noise, and the brook counts. Gives
back whether you moved, which is the only one of these the frame needs
to hear about directly; the rest mark the frame dirty themselves when
they change anything a reader would see."
[now]
(let [moved (move!)]
(answer-posts!)
(follow!)
(howl! (how-close))
(caught!)
(breathe! now)
moved))
(defn- worth-drawing?
"Whether this tick changes anything on the screen. Standing still with
nothing moving costs no frame at all; standing still with the clouds
drifting costs one every third tick, which is as often as a cloud
moves far enough to see."
[moved tick]
(let [settings-now @settings]
(or moved
@dirty
(:sank @eye)
(get-in settings-now [:wraith :show])
(and (get-in settings-now [:fountain :animate])
(get-in settings-now [:fountain :show]))
(and (get-in settings-now [:clouds :show])
(zero? (mod tick 3))))))
(defn- start-loop!
"One tick per animation frame, and a drawing only when the tick changed
something. loop-id lets a loop left over from a previous mount notice
it has been superseded and stop rescheduling itself."
[node canvas]
(let [this-loop (swap! loop-id inc)]
(letfn [(frame []
(when (= this-loop @loop-id)
(try
(when (widget/running? (.-id node))
(let [now (.now js/performance)
moved (advance! now)]
(when (worth-drawing? moved (swap! ticks inc))
(reset! dirty false)
(draw! canvas now)
(say! node now))))
(catch :default problem
(js/console.error "walk: frame" problem)))
(js/requestAnimationFrame frame)))]
(frame))))
(defn setup!
"One-time per-mount setup: resets the eye from the plan, gets a
context, compiles the programs, loads the pictures, wires the
keyboard, registers the settings callback, and starts the loop."
[node]
(let [{:keys [at facing]} @plan]
(reset! eye {:at (vec at) :facing facing :held #{}
:stand (standing-height (first at) (second at))}))
(when-let [canvas (widget/q! node ".walk-canvas")]
(if-let [gl (gl/context canvas)]
(do
(reset! scene {:gl gl
:programs (build-programs gl (:shaders @plan))
:meshes {}
:pictures {}
:swatches (world/read-swatches node)
:biggest (gl/biggest-point gl)
:index (world/index (:surfaces @plan))})
(load-picture! gl (:door-picture @plan) 256 512
(fn [texture] (swap! scene assoc-in [:pictures :door] texture)))
(load-picture! gl (:window-picture @plan) 256 256
(fn [texture] (swap! scene assoc-in [:pictures :window] texture)))
(wire-keys! canvas)
(widget/settings!
node (.-id node)
(fn [setting]
(swap! settings
(fn [current]
(deep-merge current
(select-keys setting
[:resolution :fov :eye-height :draw :bound
:walk-speed :back-speed :side-speed
:turn-speed :sun :ground :haze :fill :water
:brick :doors :windows :buildings
:fountain :trees :bushes :posts
:clouds :lines :wraith
:rotunda]))))
(paint! node)
(.focus canvas)))
(paint! node)
(start-loop! node canvas))
(js/console.error "walk: no WebGL context on this browser"))))
(defn mount!
"Registers this widget with the host app's mount system."
[{:keys [id] :as derived-plan}]
(reset! plan derived-plan)
(widget/mount! ::walk (or id "walk") setup! paint!))</script><div class="widget widget-walk" id="walk"><div class="roll-head"><span class="roll-label">THE QUAD</span><span class="widget-state">press Run</span></div><div class="widget-source"><div class="widget-source-head"><span class="widget-source-name">walk_settings.cljs</span><span class="widget-source-lang">cljs</span></div><textarea class="widget-settings" rows="22" spellcheck="false">;; ---------------------------------------------------------------------
;; WALK_SETTINGS.CLJS. The quad, and what you are on it.
;;
;; Change a number and press Run. Arrows to turn and to walk, A and D to
;; sidestep.
;;
;; The picture is drawn by the graphics card now, so most of the numbers
;; that used to be about affording something are gone. There is no
;; longer a cost to a smaller brick or a larger campus, and the ones
;; that used to buy you frames now only decide what it looks like.
;;
;; :resolution is still the one to reach for if it runs badly. The
;; canvas is drawn at that size and stretched to fit, so halving it
;; quarters the work and costs you nothing but edges.
;; ---------------------------------------------------------------------
{:resolution [420 236] ; the size it is drawn at, not the size it is
; [640 360] is now affordable and much cleaner
; [320 180] is the old look, cheap and blocky
:fov 1.05 ; radians across. 1.05 is about sixty degrees.
:eye-height 5.6 ; feet. you are a person of average height.
:draw 520 ; feet. past this it is all haze.
:bound 1000 ; feet to the edge of the world
:walk-speed 2.6 ; feet a frame forward
:back-speed 2.0
:side-speed 2.0
:turn-speed 0.045 ; radians a frame
;; THE SUN. An azimuth in radians and a height above the horizon, zero
;; at the skyline and one straight overhead. It lights the faces that
;; point at it and it does nothing else: there are no shadows on this
;; quad, which is a decision about the frame budget and not about the
;; weather.
:sun {:azimuth 2.2 :height 0.30 :size 44 :ambient 0.62}
;; THE GROUND. Everything here is in feet, so a slab is a slab and not
;; a number of pixels, and it stays that size as you walk at it.
;; HOW FAR YOU CAN SEE. :draw up there is the distance, in feet, and
;; :clarity is how the haze is spread across it for anything standing
;; up: walls, trees, the fountain, the library. At 1 the fade is even.
;; Above 1 keeps what is near you clear and spends the fade out where
;; the campus ends. The ground does not take :clarity at all; it is the
;; horizon, and it fades evenly to :draw so that it meets the sky
;; softly. :horizon is how far into that fade it gives up and becomes
;; sky.
:haze {:clarity 1.6 :horizon 0.82}
:ground {:slab 4 ; paving slabs, in feet
:joint 0.5 ; and the gaps between them
:tuft 4 ; grass grain
:patch 17 ; and the larger patches it varies in
:grain 12 ; how far the shades wander
:mow 9 ; how wide a mown stripe on Corliss Field is
:bay 9 ; parking bays, in feet
:tie 2.4 ; sleepers on the rail
:lift 0.05} ; how far each layer of ground sits above the
; one under it. a crosswalk really is on a road
; and a road really is on the lawn, and this is
; the whisker that lets the card tell which is
; which. raise it and the campus delaminates.
;; THE FILL. The one piece of ground with a shape. It is a landfill and
;; you can climb it. :peak is how high it gets in the middle, :step is
;; how many feet of it one triangle covers, and :roughness is how much
;; of the height is lumps rather than a smooth dome.
:fill {:show true :peak 16 :roughness 0.9 :step 5}
;; SPRING BROOK. The same arithmetic as the Fill with the sign turned
;; round: a brook is a hill somebody dug rather than piled. :depth is
;; how far down the middle goes, :bank is how many feet it takes to
;; climb back to the lawn, and the grass is cut away above it rather
;; than laid over it, which is what it used to be.
;; :depth is how far down the middle goes and :bank is how many feet it
;; takes to climb back to the lawn. :waterline is where the surface
;; sits, which is below the grass because a brook is not a puddle on
;; it. :swell and :wavelength move that surface; :breath is how many
;; seconds you get before the campus puts you back where you started.
:water {:show true
:depth 5
:bank 14
:step 6
:waterline -0.6
:alpha 0.72
:swell 0.35
:wavelength 9
:breath 12}
;; ROAD MARKINGS. All in feet.
;;
;; There are two kinds of road and each wears one set of markings. A
;; through road gets the pair of unbroken lines between :centre-inner
;; and :centre-outer, which says do not cross. A street between
;; buildings gets the single dashed line of somewhere you are about to
;; turn off: :dash is how often it repeats and :on is how much of that
;; is painted, so 18 and 10 is ten feet of paint and eight of nothing.
;; Which road is which is decided in campus.clj, not here.
;;
;; :width is half the thickness of a painted line and :bay-width is the
;; thickness of a parking bay stripe. A rail's steel runs between
;; :rail-inner and :rail-outer, and a crosswalk lays a :bar wide bar
;; every :zebra feet.
:lines {:dash 18 :on 10 :width 0.4 :bay-width 0.7
:centre-inner 0.3 :centre-outer 0.9
:rail-inner 1.9 :rail-outer 2.5
:zebra 5 :bar 2}
;; THE BRICK. Worked out per pixel from how far along a wall and how
;; far up it that pixel is, so the size of a brick is free and the
;; number of them is free. :streak is how many bricks share a rough
;; batch colour. :detail is how far away a brick stops being drawn as a
;; brick, in feet, because a brick smaller than a pixel is a moire
;; pattern and not a wall.
:brick {:width 2.2 ; feet along the wall
:height 0.9 ; feet, one course
:mortar 0.14 ; feet, the joint
:streak 9
:detail 150}
;; DOORS AND WINDOWS. One face of each building has a door. Which face
;; is worked out from where the building sits, unless the plan says
;; otherwise. Windows are placed from the building's own name, so the
;; same building has the same windows every time.
;;
;; There is no filename here. The pictures are inlined into the page by
;; the build, the same as every other page-side file in this book,
;; because a book opened from disc cannot fetch its own assets and an
;; image that was fetched from disc cannot be put on a graphics card.
;; Sizes are in feet and are yours to change.
;; :sill is where the bottom edge of the door picture sits, in feet off
;; the lawn, and it is below it on purpose: the picture has transparent
;; space along its own bottom edge and nothing in the program can
;; measure how much. Sinking it and putting a stone step in front of it
;; is the answer a building would have given anyway. If the door still
;; hovers, lower :sill further and raise :height by as much.
:doors {:show true :width 7 :height 12 :sill -0.8
:step {:show true :rise 0.6 :tread 3.0 :spread 1.5}}
:windows {:show true :width 3.4 :height 3.4}
:buildings {:height 30}
;; OAKES LIBRARY. The only round thing on the campus with a door in it.
;; :wall is how tall the glass stands and :dome how much higher the lid
;; goes above it. :facets is how many flat pieces the circle is made of
;; and :rings how many bands the dome has; both are cheap and both
;; show. :glass is how solid the wall is, from 0 to 1.
;;
;; There are four blocks inside and they are the reason the glass is
;; worth having. A building you can see clean through is a fish tank.
;; Walk round it and what you can see of the inside changes, which is
;; the only thing a glass wall is for.
;;
;; :door is how wide each doorway is, and it is a gap in the wall
;; rather than a hole cut in it afterwards. Two of them, east and west,
;; which is decided in campus.clj and not here.
:rotunda {:show true
:wall 15
:dome 12
:facets 48
:rings 7
:glass 0.26
:door 9}
;; THE FOUNTAIN. :animate keeps the water moving while you stand still.
;; The mist is a few hundred points whose whole path is a function of
;; the clock, so it costs the same standing as it does walking.
:fountain {:show true
:animate true
:ripples 3
:x 455 :y 330
:radius 16
:rim 1.4
:water 1.6
:pedestal {:radius 2.4
:height 4.5}
;; :jets is how much of the spray goes straight up instead
;; of out. There is no geometry over the nozzle: a stream
;; that is barely thrown outward is thrown upward instead,
;; so the jet is water going up and coming back down rather
;; than a cone standing there pretending to.
:spray {:width 1.0
:height 1.0
:density 1.0
:jets 1.0}}
;; TREES AND HEDGES. Both are flat panels that turn to face you, grown
;; out of overlapping blobs. :spread is how much wider than its own
;; canopy the panel is; :trunk is half a trunk at the root, in feet;
;; :fork is how far up the trunk the leaves start. :taper is how much
;; narrower the trunk is by the fork than at the root, and :limbs is
;; how far out the two branches reach. Zero for both gives back the
;; lollipop this used to be.
:trees {:show true :spread 1.15 :trunk 0.75 :fork 0.34
:taper 0.45 :limbs 0.5}
:bushes {:show true :spread 1.15}
;; THE EMERGENCY POSTS. Twenty of them, and until recently the only
;; thing on this campus that did nothing at all. Walk within :reach feet
;; of one and it answers, and its lamp changes colour. There is no
;; puzzle, no order to do them in, and no reward for finishing beyond
;; finding out that all twenty of them work.
:posts {:show true :height 8 :radius 0.4 :lamp 1.2 :reach 9}
;; WHATEVER IT IS. It walks at :speed feet a frame, which is very
;; slightly under yours, and it never stops. :wake is how far off it
;; starts being drawn at all, :touch is how close it has to get, and
;; :start is where it goes back to afterwards. Nothing happens when it
;; reaches you except that you are put back on the quad. Set :show to
;; false and the campus is a campus again.
;; It is off. Turn it on when you have had a look round.
;;
;; :speed is feet a frame, against your own :walk-speed of 2.6 up
;; there. At 1.7 you can open a gap and hold it if you keep moving; put
;; it over 2.6 and there is no point running at all.
;;
;; :quicken is what ties it to the posts: every post you answer makes it
;; that much faster, so the only thing on this campus that gives you a
;; reason to walk about is also the thing that makes walking about
;; worse. Set it to 0 and answering posts costs you nothing.
;;
;; THE NOISE. Also off. With no :file it is a square wave at :pitch
;; hertz, which needs nothing off a disc and is already unpleasant; the
;; pitch climbs as it closes. Name a .wav or .mp3 in :file and that
;; plays instead, on a loop. Either way it is silent at :wake, loudest
;; when it reaches you, and :level is the loudest it is ever allowed to
;; be, from 0 to 1.
;;
;; A browser will not make a sound until you have pressed a key or
;; clicked the picture. That is not something this can work around and
;; it is not worth wanting to.
:wraith {:show false
:speed 1.7
:quicken 0.35
:width 2.4
:height 7.5
:wake 260
:touch 6
:start [900 700]
:sound {:on false
:level 0.35
:pitch 70
:file nil}}
;; CLOUDS. Each one is :lobes soft points overlapping, sitting on a
;; dome around you, drifting. :cover is what fraction of the sky has
;; any; :rows is how many bands of them there are between overhead and
;; the skyline.
;; :ragged cuts each lobe's edge into wedges and pushes them in and
;; out, because a soft round dot reads as cotton wool. :flat squashes
;; the underside, which is what gives a cumulus its flat bottom. Zero
;; for either gives back the plain dot.
:clouds {:show true
:cover 0.42
:drift 0.00004
:rows 9
:size 0.19
:lobes 10
:ragged 0.22
:flat 0.6}}</textarea><div class="live-bar"><button class="roll-go widget-settings-run">Run</button><button class="roll-rewrite widget-settings-reset">Put it back</button></div><p class="widget-settings-said">Press Run.</p></div><div class="widget-body"><canvas class="walk-canvas"></canvas><p class="walk-ahead"></p></div><p class="roll-foot">It will not let you into a building. It will let you on the grass.</p></div><script type="application/x-scittle">(if-let [mount (resolve (quote fedoras.cs-class.walk.walk-ui/mount!))] (mount {:boxes [{:name "THE OLD CHAPEL", :x 340, :y 80, :w 100, :h 45, :door :south, :faces [{:side :north, :slot 0, :a [340 80], :b [440 80], :normal [0 -1], :length 100} {:side :south, :slot 1, :a [440 125], :b [340 125], :normal [0 1], :length 100} {:side :west, :slot 2, :a [340 125], :b [340 80], :normal [-1 0], :length 45} {:side :east, :slot 3, :a [440 80], :b [440 125], :normal [1 0], :length 45}], :rooms nil} {:name "PRENTISS SCIENCE CENTER", :x 330, :y 175, :w 250, :h 75, :door :south, :faces [{:side :north, :slot 0, :a [330 175], :b [580 175], :normal [0 -1], :length 250} {:side :south, :slot 1, :a [580 250], :b [330 250], :normal [0 1], :length 250} {:side :west, :slot 2, :a [330 250], :b [330 175], :normal [-1 0], :length 75} {:side :east, :slot 3, :a [580 175], :b [580 250], :normal [1 0], :length 75}], :rooms nil} {:name "STERLING CENTER", :x 615, :y 175, :w 145, :h 75, :door :south, :faces [{:side :north, :slot 0, :a [615 175], :b [760 175], :normal [0 -1], :length 145} {:side :south, :slot 1, :a [760 250], :b [615 250], :normal [0 1], :length 145} {:side :west, :slot 2, :a [615 250], :b [615 175], :normal [-1 0], :length 75} {:side :east, :slot 3, :a [760 175], :b [760 250], :normal [1 0], :length 75}], :rooms nil} {:name "BUILDING 14", :x 795, :y 175, :w 115, :h 75, :door :west, :faces [{:side :north, :slot 0, :a [795 175], :b [910 175], :normal [0 -1], :length 115} {:side :south, :slot 1, :a [910 250], :b [795 250], :normal [0 1], :length 115} {:side :west, :slot 2, :a [795 250], :b [795 175], :normal [-1 0], :length 75} {:side :east, :slot 3, :a [910 175], :b [910 250], :normal [1 0], :length 75}], :rooms nil} {:name "MERIDIAN HALL", :x 615, :y 265, :w 145, :h 75, :door :west, :faces [{:side :north, :slot 0, :a [615 265], :b [760 265], :normal [0 -1], :length 145} {:side :south, :slot 1, :a [760 340], :b [615 340], :normal [0 1], :length 145} {:side :west, :slot 2, :a [615 340], :b [615 265], :normal [-1 0], :length 75} {:side :east, :slot 3, :a [760 265], :b [760 340], :normal [1 0], :length 75}], :rooms nil} {:name "MERIDIAN ANNEX", :x 615, :y 350, :w 145, :h 45, :door :west, :faces [{:side :north, :slot 0, :a [615 350], :b [760 350], :normal [0 -1], :length 145} {:side :south, :slot 1, :a [760 395], :b [615 395], :normal [0 1], :length 145} {:side :west, :slot 2, :a [615 395], :b [615 350], :normal [-1 0], :length 45} {:side :east, :slot 3, :a [760 350], :b [760 395], :normal [1 0], :length 45}], :rooms nil} {:name "NORTH HOUSE", :x 795, :y 265, :w 115, :h 55, :door :west, :faces [{:side :north, :slot 0, :a [795 265], :b [910 265], :normal [0 -1], :length 115} {:side :south, :slot 1, :a [910 320], :b [795 320], :normal [0 1], :length 115} {:side :west, :slot 2, :a [795 320], :b [795 265], :normal [-1 0], :length 55} {:side :east, :slot 3, :a [910 265], :b [910 320], :normal [1 0], :length 55}], :rooms nil} {:name "SOUTH HOUSE", :x 795, :y 330, :w 115, :h 55, :door :west, :faces [{:side :north, :slot 0, :a [795 330], :b [910 330], :normal [0 -1], :length 115} {:side :south, :slot 1, :a [910 385], :b [795 385], :normal [0 1], :length 115} {:side :west, :slot 2, :a [795 385], :b [795 330], :normal [-1 0], :length 55} {:side :east, :slot 3, :a [910 330], :b [910 385], :normal [1 0], :length 55}], :rooms nil} {:name "ASHCROFT HOUSE", :x 795, :y 395, :w 115, :h 55, :door :west, :faces [{:side :north, :slot 0, :a [795 395], :b [910 395], :normal [0 -1], :length 115} {:side :south, :slot 1, :a [910 450], :b [795 450], :normal [0 1], :length 115} {:side :west, :slot 2, :a [795 450], :b [795 395], :normal [-1 0], :length 55} {:side :east, :slot 3, :a [910 395], :b [910 450], :normal [1 0], :length 55}], :rooms nil} {:name "WHITLOCK HALL", :x 330, :y 425, :w 155, :h 90, :door :east, :faces [{:side :north, :slot 0, :a [330 425], :b [485 425], :normal [0 -1], :length 155} {:side :south, :slot 1, :a [485 515], :b [330 515], :normal [0 1], :length 155} {:side :west, :slot 2, :a [330 515], :b [330 425], :normal [-1 0], :length 90} {:side :east, :slot 3, :a [485 425], :b [485 515], :normal [1 0], :length 90}], :rooms nil} {:name "HALLORAN UNION", :x 500, :y 425, :w 70, :h 90, :door :north, :faces [{:side :north, :slot 0, :a [500 425], :b [570 425], :normal [0 -1], :length 70} {:side :south, :slot 1, :a [570 515], :b [500 515], :normal [0 1], :length 70} {:side :west, :slot 2, :a [500 515], :b [500 425], :normal [-1 0], :length 90} {:side :east, :slot 3, :a [570 425], :b [570 515], :normal [1 0], :length 90}], :rooms nil} {:name "THE COMMONS", :x 615, :y 425, :w 145, :h 90, :door :north, :faces [{:side :north, :slot 0, :a [615 425], :b [760 425], :normal [0 -1], :length 145} {:side :south, :slot 1, :a [760 515], :b [615 515], :normal [0 1], :length 145} {:side :west, :slot 2, :a [615 515], :b [615 425], :normal [-1 0], :length 90} {:side :east, :slot 3, :a [760 425], :b [760 515], :normal [1 0], :length 90}], :rooms nil} {:name "ASHCROFT FIELD HOUSE", :x 795, :y 460, :w 115, :h 55, :door :west, :faces [{:side :north, :slot 0, :a [795 460], :b [910 460], :normal [0 -1], :length 115} {:side :south, :slot 1, :a [910 515], :b [795 515], :normal [0 1], :length 115} {:side :west, :slot 2, :a [795 515], :b [795 460], :normal [-1 0], :length 55} {:side :east, :slot 3, :a [910 460], :b [910 515], :normal [1 0], :length 55}], :rooms nil} {:name "BECKWITH HALL", :x 135, :y 265, :w 145, :h 85, :door :east, :faces [{:side :north, :slot 0, :a [135 265], :b [280 265], :normal [0 -1], :length 145} {:side :south, :slot 1, :a [280 350], :b [135 350], :normal [0 1], :length 145} {:side :west, :slot 2, :a [135 350], :b [135 265], :normal [-1 0], :length 85} {:side :east, :slot 3, :a [280 265], :b [280 350], :normal [1 0], :length 85}], :rooms nil} {:y 362, :rooms [{:name "ELEVATOR", :x 211.00634918610405, :y 421.00634918610405, :w 23.099999999999998, :h 23.099999999999998, :height 12} {:name "STAIRS", :x 179.89365081389593, :y 421.00634918610405, :w 23.099999999999998, :h 23.099999999999998, :height 12} {:name "CLOSET", :x 179.89365081389593, :y 389.89365081389593, :w 23.099999999999998, :h 23.099999999999998, :height 12} {:name "STAIRS", :x 211.00634918610402, :y 389.89365081389593, :w 23.099999999999998, :h 23.099999999999998, :height 12}], :doors [:east :west], :name "OAKES LIBRARY", :w 110, :radius 55.0, :door :east, :h 110, :shape :round, :faces [{:side :east, :slot 0, :a [262.0 412.5], :b [262.0 421.5], :normal [1.0 0.0], :length 9} {:side :west, :slot 1, :a [152.0 421.5], :b [152.0 412.5], :normal [-1.0 1.2246467991473532E-16], :length 9}], :x 152, :middle [207.0 417.0]} {:name "PLANT SERVICES", :x 135, :y 560, :w 145, :h 60, :door :east, :faces [{:side :north, :slot 0, :a [135 560], :b [280 560], :normal [0 -1], :length 145} {:side :south, :slot 1, :a [280 620], :b [135 620], :normal [0 1], :length 145} {:side :west, :slot 2, :a [135 620], :b [135 560], :normal [-1 0], :length 60} {:side :east, :slot 3, :a [280 560], :b [280 620], :normal [1 0], :length 60}], :rooms nil}], :bushes [{:kind :box, :x 480, :y 330, :height 3.8687707788056285, :radius 3.5424875026059452} {:kind :box, :x 473, :y 348, :height 3.703126723199239, :radius 3.111255083027936} {:kind :box, :x 455, :y 355, :height 3.9738510217663134, :radius 3.4076165506499816} {:kind :box, :x 437, :y 348, :height 3.2954018208882188, :radius 3.0871187695844124} {:kind :box, :x 430, :y 330, :height 3.845535276463488, :radius 3.1188073767596505} {:kind :box, :x 437, :y 312, :height 3.7943302449188194, :radius 3.2747266632068204} {:kind :box, :x 455, :y 305, :height 3.60338926952536, :radius 2.983250061229046} {:kind :box, :x 473, :y 312, :height 3.4187018992413867, :radius 3.613648433840717} {:kind :spirea, :height 5.356955030983954, :radius 3.5026244433356624, :x 350, :y 76} {:kind :juniper, :height 2.247741962753935, :radius 5.108504460804397, :x 365, :y 76} {:kind :box, :height 3.651073190736497, :radius 3.042560992280414, :x 370, :y 129} {:kind :spirea, :height 5.8600740173933445, :radius 3.8315868575264176, :x 355, :y 129} {:kind :box, :height 4.083805855030369, :radius 3.4031715458586405, :x 336, :y 115} {:kind :hedge, :height 3.1616896604537033, :radius 4.742534490680555, :x 340, :y 171} {:kind :box, :height 4.40612748413987, :radius 3.6717729034498916, :x 385, :y 171} {:kind :juniper, :height 2.2395895076697343, :radius 5.089976153794851, :x 475, :y 171} {:kind :spirea, :height 5.196563518750918, :radius 3.397753069952523, :x 490, :y 171} {:kind :spirea, :height 5.018972008451783, :radius 3.2816355439877043, :x 505, :y 171} {:kind :spirea, :height 5.490846310715279, :radius 3.590168741621528, :x 520, :y 171} {:kind :box, :height 4.484922131612257, :radius 3.7374351096768805, :x 550, :y 171} {:kind :hedge, :height 3.3023356701800366, :radius 4.953503505270056, :x 565, :y 171} {:kind :box, :height 4.027160987653224, :radius 3.35596748971102, :x 540, :y 254} {:kind :spirea, :height 5.829132221240434, :radius 3.811355683118745, :x 510, :y 254} {:kind :hedge, :height 2.8825033594272096, :radius 4.323755039140815, :x 495, :y 254} {:kind :box, :height 4.300491135654738, :radius 3.583742613045615, :x 480, :y 254} {:kind :spirea, :height 5.936508417862206, :radius 3.8815631962945187, :x 465, :y 254} {:kind :box, :height 3.7223070139152696, :radius 3.101922511596058, :x 420, :y 254} {:kind :spirea, :height 5.665919288888108, :radius 3.7046395350422245, :x 375, :y 254} {:kind :juniper, :height 2.2431246483909493, :radius 5.098010564524884, :x 360, :y 254} {:kind :hedge, :height 3.3202361564650342, :radius 4.980354234697552, :x 345, :y 254} {:kind :box, :height 3.8995583797295694, :radius 3.2496319831079745, :x 584, :y 185} {:kind :hedge, :height 3.0546460055818896, :radius 4.581969008372835, :x 655, :y 171} {:kind :box, :height 3.988608747928811, :radius 3.323840623274009, :x 670, :y 171} {:kind :juniper, :height 2.6049819847670737, :radius 5.920413601743348, :x 730, :y 171} {:kind :box, :height 3.6421451228066872, :radius 3.0351209356722393, :x 745, :y 171} {:kind :spirea, :height 6.079502487475838, :radius 3.975059318734202, :x 750, :y 254} {:kind :hedge, :height 3.017508817503403, :radius 4.5262632262551055, :x 735, :y 254} {:kind :box, :height 4.039290764195903, :radius 3.366075636829919, :x 720, :y 254} {:kind :juniper, :height 2.448335659802251, :radius 5.564399226823298, :x 705, :y 254} {:kind :box, :height 4.373724113082426, :radius 3.644770094235355, :x 675, :y 254} {:kind :box, :height 3.6465742506861716, :radius 3.0388118755718097, :x 645, :y 254} {:kind :box, :height 4.125948447484553, :radius 3.4382903729037935, :x 630, :y 254} {:kind :juniper, :height 2.4794410230621, :radius 5.635093234232045, :x 611, :y 225} {:kind :spirea, :height 5.044469148136886, :radius 3.2983067507048873, :x 820, :y 171} {:kind :juniper, :height 2.570197365722561, :radius 5.841357649369456, :x 835, :y 171} {:kind :hedge, :height 2.7134532476906315, :radius 4.070179871535948, :x 880, :y 171} {:kind :hedge, :height 3.3015708136954345, :radius 4.952356220543152, :x 895, :y 171} {:kind :juniper, :height 2.220050453098884, :radius 5.045569211588372, :x 870, :y 254} {:kind :juniper, :height 2.5614086817327917, :radius 5.821383367574526, :x 840, :y 254} {:kind :spirea, :height 6.026466728234664, :radius 3.940382091538049, :x 825, :y 254} {:kind :box, :height 4.339555143496546, :radius 3.616295952913788, :x 810, :y 254} {:kind :spirea, :height 5.8827463294794144, :radius 3.846411061582694, :x 791, :y 240} {:kind :spirea, :height 6.3986339870753, :radius 4.183722222318465, :x 791, :y 225} {:kind :juniper, :height 2.7441450097374176, :radius 6.2366932039486755, :x 791, :y 195} {:kind :box, :height 4.154638314195654, :radius 3.462198595163045, :x 914, :y 185} {:kind :hedge, :height 2.804539599116833, :radius 4.20680939867525, :x 914, :y 200} {:kind :box, :height 3.503421660422464, :radius 2.9195180503520533, :x 914, :y 215} {:kind :box, :height 4.22556772763055, :radius 3.5213064396921254, :x 914, :y 230} {:kind :spirea, :height 5.395096889964771, :radius 3.5275633511308113, :x 625, :y 261} {:kind :spirea, :height 6.198478005526704, :radius 4.052851003613614, :x 640, :y 261} {:kind :hedge, :height 3.1612285192808485, :radius 4.741842778921273, :x 655, :y 261} {:kind :spirea, :height 5.887177662353497, :radius 3.849308471538825, :x 700, :y 261} {:kind :hedge, :height 3.40455137820536, :radius 5.106827067308041, :x 715, :y 261} {:kind :spirea, :height 5.767728682428424, :radius 3.7712072154339693, :x 730, :y 261} {:kind :box, :height 4.24729299745668, :radius 3.5394108312138997, :x 750, :y 344} {:kind :spirea, :height 5.540190818690462, :radius 3.622432458374533, :x 735, :y 344} {:kind :hedge, :height 2.8180762963276718, :radius 4.227114444491508, :x 705, :y 344} {:kind :box, :height 3.6041202884618544, :radius 3.003433573718212, :x 611, :y 285} {:kind :box, :height 3.9459480618017553, :radius 3.2882900515014626, :x 700, :y 346} {:kind :spirea, :height 5.698534381221362, :radius 3.7259647877216593, :x 715, :y 346} {:kind :hedge, :height 3.0149304106787893, :radius 4.522395616018184, :x 820, :y 261} {:kind :hedge, :height 3.143387414938479, :radius 4.715081122407719, :x 835, :y 261} {:kind :juniper, :height 2.506632161963671, :radius 5.696891277190161, :x 880, :y 261} {:kind :hedge, :height 3.2427549666404953, :radius 4.864132449960744, :x 895, :y 261} {:kind :spirea, :height 5.827648459444754, :radius 3.810385531175416, :x 900, :y 324} {:kind :hedge, :height 3.415656262343873, :radius 5.12348439351581, :x 885, :y 324} {:kind :spirea, :height 6.100488284253515, :radius 3.988780801242683, :x 791, :y 280} {:kind :spirea, :height 6.249787503274274, :radius 4.08639952137164, :x 914, :y 275} {:kind :juniper, :height 2.7143472163063054, :radius 6.168970946150694, :x 914, :y 305} {:kind :spirea, :height 6.20347282904404, :radius 4.056116849759564, :x 880, :y 326} {:kind :hedge, :height 3.2678481255560197, :radius 4.90177218833403, :x 895, :y 326} {:kind :hedge, :height 2.7648290778237423, :radius 4.147243616735614, :x 900, :y 389} {:kind :juniper, :height 2.253008835284345, :radius 5.120474625646239, :x 870, :y 389} {:kind :box, :height 4.409566194736089, :radius 3.674638495613408, :x 914, :y 355} {:kind :juniper, :height 2.254480608438462, :radius 5.123819564632868, :x 914, :y 370} {:kind :hedge, :height 3.002245630030302, :radius 4.503368445045453, :x 895, :y 391} {:kind :juniper, :height 2.3319313936353865, :radius 5.29984407644406, :x 885, :y 454} {:kind :box, :height 3.876143515984586, :radius 3.2301195966538216, :x 870, :y 454} {:kind :hedge, :height 2.7633667901456644, :radius 4.145050185218497, :x 914, :y 405} {:kind :juniper, :height 2.5587173640236873, :radius 5.81526673641747, :x 914, :y 420} {:kind :juniper, :height 2.191263958727268, :radius 4.980145360743791, :x 460, :y 519} {:kind :hedge, :height 3.2103514507900397, :radius 4.81552717618506, :x 445, :y 519} {:kind :juniper, :height 2.363364158313198, :radius 5.37128217798454, :x 415, :y 519} {:kind :hedge, :height 3.29525392348587, :radius 4.9428808852288055, :x 400, :y 519} {:kind :box, :height 4.024907304654334, :radius 3.3540894205452787, :x 385, :y 519} {:kind :spirea, :height 5.888722296999186, :radius 3.8503184249610056, :x 326, :y 505} {:kind :juniper, :height 2.3529295817076674, :radius 5.347567231153789, :x 326, :y 460} {:kind :hedge, :height 3.4692909212892116, :radius 5.203936381933818, :x 545, :y 519} {:kind :spirea, :height 6.396572137151634, :radius 4.182374089676068, :x 530, :y 519} {:kind :hedge, :height 2.876330067207891, :radius 4.314495100811837, :x 574, :y 495} {:kind :hedge, :height 3.297763963024772, :radius 4.946645944537159, :x 735, :y 519} {:kind :spirea, :height 5.0911528633936545, :radius 3.3288307183727737, :x 675, :y 519} {:kind :spirea, :height 6.273568878350488, :radius 4.101948881998396, :x 645, :y 519} {:kind :spirea, :height 6.440154497314507, :radius 4.2108702482441, :x 630, :y 519} {:kind :spirea, :height 6.002957442043862, :radius 3.925010635182525, :x 611, :y 505} {:kind :hedge, :height 2.83903957815055, :radius 4.258559367225826, :x 611, :y 490} {:kind :spirea, :height 5.362724712688942, :radius 3.5063969275273847, :x 865, :y 456} {:kind :hedge, :height 2.798341984579747, :radius 4.19751297686962, :x 880, :y 456} {:kind :spirea, :height 5.057734280549631, :radius 3.3069801065132194, :x 885, :y 519} {:kind :juniper, :height 2.4904441708298695, :radius 5.6601003882497025, :x 870, :y 519} {:kind :spirea, :height 5.170641665507901, :radius 3.3808041659090122, :x 810, :y 519} {:kind :hedge, :height 3.3726693155520477, :radius 5.059003973328072, :x 914, :y 500} {:kind :spirea, :height 6.032210078818025, :radius 3.9441373592271702, :x 175, :y 261} {:kind :spirea, :height 5.453572163166974, :radius 3.5657971836091744, :x 220, :y 261} {:kind :box, :height 4.220737606703915, :radius 3.517281338919929, :x 235, :y 261} {:kind :spirea, :height 5.648342349851737, :radius 3.6931469210569046, :x 265, :y 261} {:kind :spirea, :height 5.499842473868921, :radius 3.5960508482989098, :x 270, :y 354} {:kind :spirea, :height 5.6702441786112106, :radius 3.7074673475534836, :x 195, :y 354} {:kind :box, :height 3.6698571643340983, :radius 3.0582143036117486, :x 180, :y 354} {:kind :spirea, :height 5.062909463168762, :radius 3.3103638797641906, :x 150, :y 354} {:kind :spirea, :height 6.433513932026108, :radius 4.206528340170917, :x 131, :y 340} {:kind :hedge, :height 2.88656712481752, :radius 4.329850687226281, :x 131, :y 325} {:kind :juniper, :height 2.4561168694388473, :radius 5.582083794179198, :x 131, :y 310} {:kind :spirea, :height 6.041329153681909, :radius 3.9500998312535556, :x 131, :y 295} {:kind :box, :height 4.276443992945133, :radius 3.5637033274542773, :x 131, :y 280} {:kind :spirea, :height 6.387801334357937, :radius 4.176639334003267, :x 284, :y 275} {:kind :spirea, :height 5.956238264076092, :radius 3.8944634803574445, :x 160, :y 556} {:kind :hedge, :height 3.3818910161280655, :radius 5.072836524192098, :x 175, :y 556} {:kind :box, :height 3.8274205260502643, :radius 3.1895171050418867, :x 220, :y 556} {:kind :hedge, :height 3.419674081807898, :radius 5.129511122711847, :x 235, :y 556} {:kind :box, :height 3.6910341119000805, :radius 3.0758617599167337, :x 265, :y 556} {:kind :box, :height 4.146065632390673, :radius 3.4550546936588944, :x 270, :y 624} {:kind :hedge, :height 2.959468292585734, :radius 4.439202438878602, :x 255, :y 624} {:kind :juniper, :height 2.428377110078145, :radius 5.519038886541239, :x 210, :y 624} {:kind :hedge, :height 2.7763475990373987, :radius 4.164521398556099, :x 195, :y 624} {:kind :box, :height 3.7250419473668446, :radius 3.1042016228057037, :x 165, :y 624} {:kind :box, :height 4.437031786292209, :radius 3.697526488576841, :x 150, :y 624} {:kind :box, :height 4.034533060388276, :radius 3.3621108836568965, :x 131, :y 595} {:kind :spirea, :height 5.206050630567188, :radius 3.4039561815246997, :x 131, :y 580} {:kind :juniper, :height 2.2209820451062114, :radius 5.0476864661504806, :x 284, :y 570} {:kind :box, :height 4.399786942329956, :radius 3.666489118608297, :x 284, :y 600}], :window-picture "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"39\" height=\"39\" viewBox=\"0 0 39 39\" shape-rendering=\"crispEdges\" image-rendering=\"pixelated\" xmlns:c2pa=\"http://c2pa.org/manifest\"><metadata><c2pa:manifest>AAAWgmp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAABZcanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTpiY2VhZDgyMC0yOTk1LTRlNmMtYTNjZC1hZmNiZjFmNzZkNzkAAAADl2p1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAALxqdW1iAAAARGp1bWRjYm9yABEAEIAAAKoAOJtxE2MycGEuaW5ncmVkaWVudC52MwAAAAAYYzJzaJeAUIxMISITIi4MupovIiUAAABwY2JvcqNpZGM6Zm9ybWF0bWltYWdlL3N2Zyt4bWxqaW5zdGFuY2VJRHgseG1wOmlpZDoxODgwYjY2Mi03YThhLTQ0YWYtOWE1Zi0yMTEyOGEwNjU1ZjJscmVsYXRpb25zaGlwaHBhcmVudE9mAAAB4mp1bWIAAABBanVtZGNib3IAEQAQgAAAqgA4m3ETYzJwYS5hY3Rpb25zLnYyAAAAABhjMnNoDngoiofkVtizrb51LaEoiAAAAZljYm9yomdhY3Rpb25zgqJmYWN0aW9ua2MycGEub3BlbmVkanBhcmFtZXRlcnOha2luZ3JlZGllbnRzgaJjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFggsiXuFtJ67SPuAvft/KV0A6xrWfanZgmo+RxibN/dC0OkZmFjdGlvbngdY29tLmFudGhyb3BpYy5jbGF1ZGUucHJvdmlkZWRqcGFyYW1ldGVyc6F4H2NvbS5hbnRocm9waWMub3JpZ2luLWNvbmZpZGVuY2VndW5rbm93bmtkZXNjcmlwdGlvbnhmQ2xhdWRlIHByb3ZpZGVkIHRoaXMgZmlsZSBhdCB0aGUgcmVxdWVzdCBvZiBhIHVzZXIgYW5kIG1heSBoYXZlIGNyZWF0ZWQgb3IgbW9kaWZpZWQgdGhlIGZpbGUgY29udGVudHMubXNvZnR3YXJlQWdlbnShZG5hbWVmQ2xhdWRlcmFsbEFjdGlvbnNJbmNsdWRlZPUAAADIanVtYgAAAEBqdW1kY2JvcgARABCAAACqADibcRNjMnBhLmhhc2guZGF0YQAAAAAYYzJzaMao+1L2w2v90aBvPUExRykAAACAY2JvcqVjYWxnZnNoYTI1NmNwYWRNAAAAAAAAAAAAAAAAAGRoYXNoWCABu3mhAFySR9o/lwOZJyzIuetmHhbMH7mASYggA+J9uGRuYW1lbmp1bWJmIG1hbmlmZXN0amV4Y2x1c2lvbnOBomVzdGFydBjLZmxlbmd0aBkeBAAAAj5qdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAACD2Nib3KlY2FsZ2ZzaGEyNTZpc2lnbmF0dXJleE1zZWxmI2p1bWJmPS9jMnBhL3VybjpjMnBhOmJjZWFkODIwLTI5OTUtNGU2Yy1hM2NkLWFmY2JmMWY3NmQ3OS9jMnBhLnNpZ25hdHVyZWppbnN0YW5jZUlEeCx4bXA6aWlkOmE2NGMyMjM1LTUzNWUtNDMxYi04NWQ2LTM2ZTc3MjhjMDZmMHJjcmVhdGVkX2Fzc2VydGlvbnODomN1cmx4LXNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuaW5ncmVkaWVudC52M2RoYXNoWCCyJe4W0nrtI+4C9+38pXQDrGtZ9qdmCaj5HGJs390LQ6JjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFggJgBiC4Xz173pvxdoJ33+XCjtM1oswuXhJEXXedNELd2iY3VybHgpc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5oYXNoLmRhdGFkaGFzaFggld3QEXRvZbfKE8W33yeL4trP4wE7wLwxnS7yjZZP3yl0Y2xhaW1fZ2VuZXJhdG9yX2luZm+jZG5hbWVvQW50aHJvcGljIEZpbGVzZ3ZlcnNpb25lMS4wLjBrc3BlY1ZlcnNpb25lMi40LjAAABA4anVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAABAIY2JvctKEWQISogEmGCFZAgowggIGMIIBjaADAgECAhRA5aAK7sI50L64g/oGQgU9Z1UTADAKBggqhkjOPQQDAzBJMRcwFQYDVQQKEw5BbnRocm9waWMsIFBCQzEuMCwGA1UEAxMlQW50aHJvcGljIENvbnRlbnQgQ3JlZGVudGlhbHMgUm9vdCBDQTAeFw0yNjA4MDcxODQzNTZaFw0yODA4MDYxOTQzNTZaMEQxFzAVBgNVBAoTDkFudGhyb3BpYywgUEJDMSkwJwYDVQQDEyBBbnRocm9waWMgQ2xhdWRlIENvbnRlbnQgU2lnbmluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJh6CmvLUBgFFNU0vUKlOVtE6djd17L5SuwX0LemFisBM3dkd/3cyjxFA3Qo5S46fX0/ihY0VZ7mfb9KF703t5OjWDBWMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUEDjAMBgorBgEEAYPoXgIBMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUzlHiBIFOZFsj+OPEz5o+nMHXXMIwCgYIKoZIzj0EAwMDZwAwZAIwMXMdFJ4BetLLVY7ORuE9noqbbAZOZn/aArXyTwFAZfKrPzxF2vPoJNf1+UCdg1XGAjBwX1zd9WGqYkqmL5SFqw1QySjr1zJfpJM9+1rdDwSPLMOPOjKuiXjoU/pUUeG9RwmhY3BhZFkNngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPZYQFVBkEBX3ScmY4g579LYUkF41wKR4NaQvg+26Shr+RGTvkAcA4cd5anUjmL/tb5vpJQEUnCkprZCIyHGwPLfIGc=</c2pa:manifest></metadata>\n <title>Four pane window</title>\n <path fill=\"#231610\" d=\"M0 0h1v39h-1zM1 0h1v1h-1zM1 38h1v1h-1zM2 0h1v1h-1zM2 38h1v1h-1zM3 0h1v1h-1zM3 38h1v1h-1zM4 0h1v1h-1zM4 38h1v1h-1zM5 0h1v1h-1zM5 38h1v1h-1zM6 0h1v1h-1zM6 38h1v1h-1zM7 0h1v1h-1zM7 38h1v1h-1zM8 0h1v1h-1zM8 38h1v1h-1zM9 0h1v1h-1zM9 38h1v1h-1zM10 0h1v1h-1zM10 38h1v1h-1zM11 0h1v1h-1zM11 38h1v1h-1zM12 0h1v1h-1zM12 38h1v1h-1zM13 0h1v1h-1zM13 38h1v1h-1zM14 0h1v1h-1zM14 38h1v1h-1zM15 0h1v1h-1zM15 38h1v1h-1zM16 0h1v1h-1zM16 38h1v1h-1zM17 0h1v1h-1zM17 38h1v1h-1zM18 0h1v1h-1zM18 38h1v1h-1zM19 0h1v1h-1zM19 38h1v1h-1zM20 0h1v1h-1zM20 38h1v1h-1zM21 0h1v1h-1zM21 38h1v1h-1zM22 0h1v1h-1zM22 38h1v1h-1zM23 0h1v1h-1zM23 38h1v1h-1zM24 0h1v1h-1zM24 38h1v1h-1zM25 0h1v1h-1zM25 38h1v1h-1zM26 0h1v1h-1zM26 38h1v1h-1zM27 0h1v1h-1zM27 38h1v1h-1zM28 0h1v1h-1zM28 38h1v1h-1zM29 0h1v1h-1zM29 38h1v1h-1zM30 0h1v1h-1zM30 38h1v1h-1zM31 0h1v1h-1zM31 38h1v1h-1zM32 0h1v1h-1zM32 38h1v1h-1zM33 0h1v1h-1zM33 38h1v1h-1zM34 0h1v1h-1zM34 38h1v1h-1zM35 0h1v1h-1zM35 38h1v1h-1zM36 0h1v1h-1zM36 38h1v1h-1zM37 0h1v1h-1zM37 38h1v1h-1zM38 0h1v39h-1z\"/>\n <path fill=\"#4d3021\" d=\"M2 37h1v1h-1zM3 36h1v2h-1zM4 36h1v2h-1zM5 5h1v1h-1zM5 36h1v2h-1zM6 5h1v1h-1zM6 18h1v1h-1zM6 36h1v2h-1zM7 5h1v1h-1zM7 18h1v1h-1zM7 36h1v2h-1zM8 5h1v1h-1zM8 18h1v1h-1zM8 36h1v2h-1zM9 5h1v1h-1zM9 18h1v1h-1zM9 36h1v2h-1zM10 5h1v1h-1zM10 18h1v1h-1zM10 36h1v2h-1zM11 5h1v1h-1zM11 18h1v1h-1zM11 36h1v2h-1zM12 5h1v1h-1zM12 18h1v1h-1zM12 36h1v2h-1zM13 5h1v1h-1zM13 18h1v1h-1zM13 36h1v2h-1zM14 5h1v1h-1zM14 18h1v1h-1zM14 36h1v2h-1zM15 5h1v1h-1zM15 18h1v1h-1zM15 36h1v2h-1zM16 5h1v1h-1zM16 18h1v1h-1zM16 36h1v2h-1zM17 5h1v1h-1zM17 18h1v1h-1zM17 36h1v2h-1zM18 5h1v13h-1zM18 21h1v12h-1zM18 36h1v2h-1zM19 5h1v1h-1zM19 36h1v2h-1zM20 5h1v1h-1zM20 36h1v2h-1zM21 5h1v1h-1zM21 18h1v1h-1zM21 36h1v2h-1zM22 5h1v1h-1zM22 18h1v1h-1zM22 36h1v2h-1zM23 5h1v1h-1zM23 18h1v1h-1zM23 36h1v2h-1zM24 5h1v1h-1zM24 18h1v1h-1zM24 36h1v2h-1zM25 5h1v1h-1zM25 18h1v1h-1zM25 36h1v2h-1zM26 5h1v1h-1zM26 18h1v1h-1zM26 36h1v2h-1zM27 5h1v1h-1zM27 18h1v1h-1zM27 36h1v2h-1zM28 5h1v1h-1zM28 18h1v1h-1zM28 36h1v2h-1zM29 5h1v1h-1zM29 18h1v1h-1zM29 36h1v2h-1zM30 5h1v1h-1zM30 18h1v1h-1zM30 36h1v2h-1zM31 5h1v1h-1zM31 18h1v1h-1zM31 36h1v2h-1zM32 5h1v1h-1zM32 18h1v1h-1zM32 36h1v2h-1zM33 5h1v1h-1zM33 36h1v2h-1zM34 36h1v2h-1zM35 36h1v2h-1zM36 37h1v1h-1z\"/>\n <path fill=\"#5f3d2a\" d=\"M5 6h1v28h-1zM36 3h1v34h-1zM37 2h1v36h-1z\"/>\n <path fill=\"#734b33\" d=\"M33 3h1v2h-1z\"/>\n <path fill=\"#87593c\" d=\"M3 3h1v20h-1zM3 34h1v2h-1zM4 3h1v8h-1zM4 23h1v1h-1zM4 27h1v9h-1zM5 34h1v2h-1zM6 3h1v2h-1zM6 19h1v1h-1zM6 34h1v2h-1zM7 3h1v2h-1zM7 19h1v1h-1zM7 34h1v2h-1zM8 3h1v2h-1zM8 19h1v1h-1zM8 34h1v2h-1zM9 3h1v2h-1zM9 19h1v1h-1zM9 34h1v2h-1zM10 3h1v2h-1zM10 19h1v1h-1zM10 34h1v2h-1zM11 3h1v1h-1zM11 19h1v1h-1zM11 34h1v2h-1zM12 3h1v1h-1zM12 19h1v1h-1zM12 34h1v2h-1zM13 3h1v1h-1zM13 19h1v1h-1zM13 34h1v2h-1zM14 3h1v1h-1zM14 19h1v1h-1zM14 34h1v2h-1zM15 3h1v1h-1zM15 19h1v1h-1zM15 34h1v2h-1zM16 3h1v1h-1zM16 19h1v1h-1zM16 34h1v2h-1zM17 3h1v1h-1zM17 19h1v1h-1zM17 34h1v2h-1zM18 3h1v2h-1zM18 18h1v3h-1zM19 3h1v2h-1zM19 6h1v27h-1zM20 3h1v2h-1zM20 18h1v3h-1zM20 34h1v2h-1zM21 3h1v1h-1zM21 19h1v1h-1zM21 34h1v2h-1zM22 19h1v1h-1zM22 34h1v2h-1zM23 19h1v1h-1zM23 34h1v2h-1zM24 19h1v1h-1zM24 34h1v1h-1zM25 19h1v1h-1zM25 34h1v1h-1zM26 19h1v1h-1zM26 34h1v1h-1zM27 19h1v1h-1zM27 34h1v1h-1zM28 19h1v1h-1zM28 34h1v1h-1zM29 19h1v1h-1zM29 34h1v1h-1zM30 4h1v1h-1zM30 19h1v1h-1zM30 34h1v1h-1zM31 4h1v1h-1zM31 19h1v1h-1zM31 34h1v1h-1zM32 4h1v1h-1zM32 19h1v1h-1zM32 34h1v1h-1zM33 34h1v2h-1zM34 3h1v31h-1zM35 3h1v21h-1z\"/>\n <path fill=\"#9d6c49\" d=\"M1 2h1v36h-1zM2 3h1v34h-1zM3 23h1v11h-1zM4 11h1v12h-1zM4 24h1v3h-1zM5 3h1v2h-1zM11 4h1v1h-1zM12 4h1v1h-1zM13 4h1v1h-1zM14 4h1v1h-1zM15 4h1v1h-1zM16 4h1v1h-1zM17 4h1v1h-1zM18 34h1v2h-1zM19 34h1v2h-1zM21 4h1v1h-1zM22 3h1v2h-1zM23 3h1v2h-1zM24 3h1v2h-1zM24 35h1v1h-1zM25 3h1v2h-1zM25 35h1v1h-1zM26 3h1v2h-1zM26 35h1v1h-1zM27 3h1v2h-1zM27 35h1v1h-1zM28 3h1v2h-1zM28 35h1v1h-1zM29 3h1v2h-1zM29 35h1v1h-1zM30 3h1v1h-1zM30 35h1v1h-1zM31 3h1v1h-1zM31 35h1v1h-1zM32 3h1v1h-1zM32 35h1v1h-1zM33 6h1v28h-1zM34 34h1v2h-1zM35 24h1v12h-1z\"/>\n <path fill=\"#b5825b\" d=\"M1 1h1v1h-1zM2 1h1v2h-1zM3 1h1v2h-1zM4 1h1v2h-1zM5 1h1v2h-1zM6 1h1v2h-1zM6 20h1v1h-1zM6 33h1v1h-1zM7 1h1v2h-1zM7 20h1v1h-1zM7 33h1v1h-1zM8 1h1v2h-1zM8 20h1v1h-1zM8 33h1v1h-1zM9 1h1v2h-1zM9 20h1v1h-1zM9 33h1v1h-1zM10 1h1v2h-1zM10 20h1v1h-1zM10 33h1v1h-1zM11 1h1v2h-1zM11 20h1v1h-1zM11 33h1v1h-1zM12 1h1v2h-1zM12 20h1v1h-1zM12 33h1v1h-1zM13 1h1v2h-1zM13 20h1v1h-1zM13 33h1v1h-1zM14 1h1v2h-1zM14 20h1v1h-1zM14 33h1v1h-1zM15 1h1v2h-1zM15 20h1v1h-1zM15 33h1v1h-1zM16 1h1v2h-1zM16 20h1v1h-1zM16 33h1v1h-1zM17 1h1v2h-1zM17 20h1v1h-1zM17 33h1v1h-1zM18 1h1v2h-1zM18 33h1v1h-1zM19 1h1v2h-1zM19 33h1v1h-1zM20 1h1v2h-1zM20 6h1v12h-1zM20 21h1v13h-1zM21 1h1v2h-1zM21 20h1v1h-1zM21 33h1v1h-1zM22 1h1v2h-1zM22 20h1v1h-1zM22 33h1v1h-1zM23 1h1v2h-1zM23 20h1v1h-1zM23 33h1v1h-1zM24 1h1v2h-1zM24 20h1v1h-1zM24 33h1v1h-1zM25 1h1v2h-1zM25 20h1v1h-1zM25 33h1v1h-1zM26 1h1v2h-1zM26 20h1v1h-1zM26 33h1v1h-1zM27 1h1v2h-1zM27 20h1v1h-1zM27 33h1v1h-1zM28 1h1v2h-1zM28 20h1v1h-1zM28 33h1v1h-1zM29 1h1v2h-1zM29 20h1v1h-1zM29 33h1v1h-1zM30 1h1v2h-1zM30 20h1v1h-1zM30 33h1v1h-1zM31 1h1v2h-1zM31 20h1v1h-1zM31 33h1v1h-1zM32 1h1v2h-1zM32 20h1v1h-1zM32 33h1v1h-1zM33 1h1v2h-1zM34 1h1v2h-1zM35 1h1v2h-1zM36 1h1v2h-1zM37 1h1v1h-1z\"/>\n <path fill=\"#263b48\" d=\"M6 31h1v2h-1zM7 31h1v2h-1zM8 30h1v3h-1zM9 30h1v3h-1zM10 31h1v2h-1zM11 31h1v2h-1zM12 31h1v2h-1zM13 31h1v2h-1zM14 31h1v2h-1zM15 31h1v2h-1zM16 32h1v1h-1zM17 32h1v1h-1zM27 32h1v1h-1zM31 31h1v1h-1zM32 32h1v1h-1z\"/>\n <path fill=\"#33505f\" d=\"M6 24h1v7h-1zM7 24h1v7h-1zM8 25h1v5h-1zM9 25h1v5h-1zM10 25h1v6h-1zM11 26h1v5h-1zM12 26h1v5h-1zM13 27h1v4h-1zM14 28h1v3h-1zM15 29h1v2h-1zM16 30h1v2h-1zM17 31h1v1h-1zM21 26h1v1h-1zM22 26h1v2h-1zM23 27h1v3h-1zM24 28h1v3h-1zM25 29h1v4h-1zM26 24h1v3h-1zM26 30h1v3h-1zM27 24h1v4h-1zM27 31h1v1h-1zM28 25h1v4h-1zM28 32h1v1h-1zM29 25h1v5h-1zM30 26h1v5h-1zM31 26h1v5h-1zM32 25h1v7h-1z\"/>\n <path fill=\"#436678\" d=\"M6 21h1v3h-1zM7 21h1v3h-1zM8 22h1v3h-1zM9 23h1v2h-1zM10 24h1v1h-1zM11 25h1v1h-1zM12 25h1v1h-1zM13 26h1v1h-1zM14 27h1v1h-1zM15 21h1v1h-1zM15 28h1v1h-1zM16 21h1v2h-1zM16 29h1v1h-1zM17 21h1v4h-1zM17 29h1v2h-1zM21 21h1v1h-1zM21 25h1v1h-1zM21 27h1v2h-1zM22 21h1v2h-1zM22 28h1v3h-1zM23 21h1v3h-1zM23 30h1v2h-1zM24 21h1v4h-1zM24 27h1v1h-1zM24 31h1v2h-1zM25 21h1v6h-1zM25 28h1v1h-1zM26 21h1v3h-1zM26 27h1v1h-1zM26 29h1v1h-1zM27 21h1v3h-1zM27 28h1v1h-1zM27 30h1v1h-1zM28 21h1v4h-1zM28 29h1v1h-1zM28 31h1v1h-1zM29 21h1v4h-1zM29 30h1v1h-1zM29 32h1v1h-1zM30 21h1v5h-1zM30 31h1v1h-1zM31 21h1v5h-1zM31 32h1v1h-1zM32 21h1v4h-1z\"/>\n <path fill=\"#578294\" d=\"M7 12h1v1h-1zM8 13h1v1h-1zM8 21h1v1h-1zM9 13h1v3h-1zM9 22h1v1h-1zM10 14h1v3h-1zM10 23h1v1h-1zM11 15h1v3h-1zM11 23h1v2h-1zM12 16h1v2h-1zM12 24h1v1h-1zM13 13h1v1h-1zM13 17h1v1h-1zM13 25h1v1h-1zM14 14h1v1h-1zM14 21h1v2h-1zM14 26h1v1h-1zM15 14h1v2h-1zM15 22h1v2h-1zM15 27h1v1h-1zM16 13h1v4h-1zM16 23h1v2h-1zM16 28h1v1h-1zM17 13h1v5h-1zM17 25h1v1h-1zM17 28h1v1h-1zM21 15h1v3h-1zM21 22h1v1h-1zM21 24h1v1h-1zM21 29h1v2h-1zM21 32h1v1h-1zM22 15h1v3h-1zM22 23h1v1h-1zM22 25h1v1h-1zM22 31h1v1h-1zM23 15h1v3h-1zM23 24h1v1h-1zM23 26h1v1h-1zM23 32h1v1h-1zM24 14h1v4h-1zM24 25h1v1h-1zM25 14h1v4h-1zM26 14h1v4h-1zM26 28h1v1h-1zM27 14h1v4h-1zM27 29h1v1h-1zM28 13h1v5h-1zM28 30h1v1h-1zM29 13h1v5h-1zM29 31h1v1h-1zM30 13h1v5h-1zM30 32h1v1h-1zM31 13h1v5h-1zM32 13h1v5h-1z\"/>\n <path fill=\"#6f9fb0\" d=\"M6 6h1v1h-1zM6 10h1v4h-1zM7 6h1v2h-1zM7 11h1v1h-1zM7 13h1v2h-1zM8 7h1v2h-1zM8 12h1v1h-1zM8 14h1v2h-1zM9 7h1v3h-1zM9 16h1v1h-1zM9 21h1v1h-1zM10 7h1v4h-1zM10 17h1v1h-1zM10 21h1v2h-1zM11 7h1v5h-1zM11 14h1v1h-1zM11 22h1v1h-1zM12 7h1v6h-1zM12 15h1v1h-1zM12 23h1v1h-1zM13 7h1v6h-1zM13 14h1v1h-1zM13 16h1v1h-1zM13 21h1v2h-1zM13 24h1v1h-1zM14 6h1v8h-1zM14 15h1v1h-1zM14 17h1v1h-1zM14 23h1v1h-1zM14 25h1v1h-1zM15 6h1v8h-1zM15 16h1v1h-1zM15 24h1v1h-1zM15 26h1v1h-1zM16 7h1v6h-1zM16 17h1v1h-1zM16 25h1v1h-1zM16 27h1v1h-1zM17 7h1v6h-1zM17 26h1v2h-1zM21 7h1v8h-1zM21 23h1v1h-1zM21 31h1v1h-1zM22 7h1v8h-1zM22 24h1v1h-1zM22 32h1v1h-1zM23 7h1v8h-1zM23 25h1v1h-1zM24 7h1v7h-1zM24 26h1v1h-1zM25 7h1v7h-1zM25 27h1v1h-1zM26 7h1v7h-1zM27 7h1v7h-1zM28 8h1v5h-1zM29 8h1v5h-1zM30 6h1v7h-1zM31 6h1v7h-1zM32 6h1v7h-1z\"/>\n <path fill=\"#8dbccb\" d=\"M6 7h1v1h-1zM6 9h1v1h-1zM6 14h1v1h-1zM6 17h1v1h-1zM7 8h1v1h-1zM7 10h1v1h-1zM7 15h1v1h-1zM8 6h1v1h-1zM8 9h1v1h-1zM8 11h1v1h-1zM8 16h1v2h-1zM9 6h1v1h-1zM9 10h1v1h-1zM9 12h1v1h-1zM9 17h1v1h-1zM10 6h1v1h-1zM10 11h1v1h-1zM10 13h1v1h-1zM11 6h1v1h-1zM11 12h1v1h-1zM11 21h1v1h-1zM12 6h1v1h-1zM12 13h1v1h-1zM12 21h1v2h-1zM13 6h1v1h-1zM13 15h1v1h-1zM13 23h1v1h-1zM14 16h1v1h-1zM14 24h1v1h-1zM15 17h1v1h-1zM15 25h1v1h-1zM16 6h1v1h-1zM16 26h1v1h-1zM17 6h1v1h-1zM21 6h1v1h-1zM22 6h1v1h-1zM23 6h1v1h-1zM24 6h1v1h-1zM25 6h1v1h-1zM26 6h1v1h-1zM27 6h1v1h-1zM28 6h1v2h-1zM29 6h1v2h-1z\"/>\n <path fill=\"#b2d7e2\" d=\"M6 15h1v2h-1zM7 16h1v2h-1zM8 10h1v1h-1zM9 11h1v1h-1zM10 12h1v1h-1zM11 13h1v1h-1zM12 14h1v1h-1z\"/>\n <path fill=\"#dcf0f5\" d=\"M6 8h1v1h-1zM7 9h1v1h-1z\"/>\n</svg>", :shaders {:wraith {:vertex "attribute vec2 a_corner;\nuniform mat4 u_camera;\nuniform vec3 u_right;\nuniform vec3 u_root;\nuniform vec2 u_size;\nvarying vec2 v_local;\nvarying vec3 v_world;\n\nfloat hash21(vec2 seed) {\n vec3 scattered = fract((vec3(seed.xyx) * 0.1031));\n vec3 folded = (scattered + dot(scattered, (scattered.yzx + 33.33)));\n return fract(((folded.x + folded.y) * folded.z));\n}\n\nfloat pick(vec2 cell, float steps) {\n return floor((hash21(floor(cell)) * steps));\n}\n\nfloat fogAt(vec3 here, vec3 u_eye_at, vec3 reach) {\n return pow(clamp((distance(here, u_eye_at) / reach.x), 0.0, 1.0), reach.z);\n}\n\nvoid main() {\n float up = ((a_corner.y + 1.0) * 0.5);\n vec3 world = (u_root + (u_right * (a_corner.x * u_size.x)) + vec3(0.0, 0.0, (up * u_size.y)));\n v_local = vec2(a_corner.x, up);\n v_world = world;\n gl_Position = (u_camera * vec4(world, 1.0));\n}\n", :fragment "#ifdef GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\n#else\nprecision mediump float;\n#endif\nuniform vec3 u_tint;\nuniform vec3 u_haze;\nuniform vec3 u_eye;\nuniform vec3 u_reach;\nuniform vec3 u_glitch;\nuniform float u_now;\nvarying vec2 v_local;\nvarying vec3 v_world;\n\nfloat hash21(vec2 seed) {\n vec3 scattered = fract((vec3(seed.xyx) * 0.1031));\n vec3 folded = (scattered + dot(scattered, (scattered.yzx + 33.33)));\n return fract(((folded.x + folded.y) * folded.z));\n}\n\nfloat pick(vec2 cell, float steps) {\n return floor((hash21(floor(cell)) * steps));\n}\n\nfloat fogAt(vec3 here, vec3 u_eye_at, vec3 reach) {\n return pow(clamp((distance(here, u_eye_at) / reach.x), 0.0, 1.0), reach.z);\n}\n\nvoid main() {\n float along = v_local.x;\n float up = v_local.y;\n float shoulder = (1.0 - (0.62 * abs((up - 0.62))));\n float waist = (0.5 * shoulder * (1.0 - (0.35 * smoothstep(0.86, 1.0, up))));\n float band = floor((up * 30.0));\n float frame = floor((u_now * 24.0));\n float slip = (u_glitch.x * (hash21(vec2(band, frame)) - 0.5) * 2.0);\n float edge = (waist - abs((along - slip)));\n if ((edge < 0.0)) {\n discard;\n }\n float torn = hash21(vec2((band + 91.0), frame));\n if ((torn > u_glitch.y)) {\n discard;\n }\n float fog = fogAt(v_world, u_eye, u_reach);\n vec3 shade = mix(u_tint, u_haze, (fog * 0.7));\n gl_FragColor = vec4(shade, (u_glitch.z * smoothstep(0.0, 0.06, edge) * (1.0 - (0.5 * fog))));\n}\n"}, :solid {:vertex "attribute vec3 a_position;\nattribute vec3 a_colour;\nattribute vec3 a_normal;\nattribute vec2 a_water;\nuniform mat4 u_camera;\nuniform float u_now;\nuniform vec2 u_swell;\nvarying vec3 v_colour;\nvarying vec3 v_normal;\nvarying vec3 v_world;\nvarying vec2 v_water;\n\nfloat hash21(vec2 seed) {\n vec3 scattered = fract((vec3(seed.xyx) * 0.1031));\n vec3 folded = (scattered + dot(scattered, (scattered.yzx + 33.33)));\n return fract(((folded.x + folded.y) * folded.z));\n}\n\nfloat pick(vec2 cell, float steps) {\n return floor((hash21(floor(cell)) * steps));\n}\n\nfloat fogAt(vec3 here, vec3 u_eye_at, vec3 reach) {\n return pow(clamp((distance(here, u_eye_at) / reach.x), 0.0, 1.0), reach.z);\n}\n\nvoid main() {\n vec3 place = a_position;\n vec3 facing = a_normal;\n float phase = (u_now * 6.2832);\n float height = u_swell.x;\n float length = u_swell.y;\n if ((a_water.x < 0.0)) {\n float long_wave = (((place.x + place.y) / length) + phase);\n float short_wave = (((place.x - (place.y * 1.7)) / (length * 0.6)) - phase);\n float long_slope = ((height / length) * cos(long_wave));\n float short_slope = (((height * 0.5) / (length * 0.6)) * cos(short_wave));\n place = (place + vec3(0.0, 0.0, ((height * sin(long_wave)) + (height * 0.5 * sin(short_wave)))));\n facing = normalize(vec3(-(long_slope + short_slope), -(long_slope - (1.7 * short_slope)), 1.0));\n }\n v_colour = a_colour;\n v_normal = facing;\n v_world = place;\n v_water = a_water;\n gl_Position = (u_camera * vec4(place, 1.0));\n}\n", :fragment "#ifdef GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\n#else\nprecision mediump float;\n#endif\nuniform vec3 u_haze;\nuniform vec3 u_eye;\nuniform vec3 u_foam;\nuniform vec3 u_middle;\nuniform vec3 u_sunward;\nuniform float u_ambient;\nuniform float u_now;\nuniform vec3 u_reach;\nuniform vec3 u_sky;\nuniform vec3 u_sun;\nvarying vec3 v_colour;\nvarying vec3 v_normal;\nvarying vec3 v_world;\nvarying vec2 v_water;\n\nfloat hash21(vec2 seed) {\n vec3 scattered = fract((vec3(seed.xyx) * 0.1031));\n vec3 folded = (scattered + dot(scattered, (scattered.yzx + 33.33)));\n return fract(((folded.x + folded.y) * folded.z));\n}\n\nfloat pick(vec2 cell, float steps) {\n return floor((hash21(floor(cell)) * steps));\n}\n\nfloat fogAt(vec3 here, vec3 u_eye_at, vec3 reach) {\n return pow(clamp((distance(here, u_eye_at) / reach.x), 0.0, 1.0), reach.z);\n}\n\nvoid main() {\n vec3 shade = v_colour;\n if ((v_water.x > 0.001)) {\n float from_middle = distance(v_world.xy, u_middle.xy);\n float phase = fract(((from_middle / v_water.x) - u_now));\n shade = mix(shade, u_foam, (0.5 * pow((1.0 - phase), 4.0)));\n }\n vec3 surface = normalize(v_normal);\n float lit = (u_ambient + ((1.0 - u_ambient) * max(0.0, dot(surface, u_sunward))));\n float fog = fogAt(v_world, u_eye, u_reach);\n vec3 body = (shade * lit);\n float solidity = v_water.y;\n if ((v_water.x < 0.0)) {\n vec3 toward_eye = normalize((u_eye - v_world));\n float grazing = pow((1.0 - max(dot(toward_eye, surface), 0.0)), 5.0);\n float fresnel = (0.04 + (0.96 * grazing));\n vec3 bounced = reflect(-toward_eye, surface);\n vec3 sky_seen = mix(u_haze, u_sky, smoothstep(0.0, 0.55, max(bounced.z, 0.0)));\n float glint = pow(max(dot(reflect(-u_sunward, surface), toward_eye), 0.0), 90.0);\n body = (mix(body, sky_seen, fresnel) + (u_sun * glint));\n solidity = mix(solidity, 1.0, fresnel);\n }\n gl_FragColor = vec4(mix(body, u_haze, fog), solidity);\n}\n"}, :ground {:vertex "attribute vec3 a_position;\nattribute vec4 a_patch;\nuniform mat4 u_camera;\nvarying vec4 v_patch;\nvarying vec3 v_world;\n\nfloat hash21(vec2 seed) {\n vec3 scattered = fract((vec3(seed.xyx) * 0.1031));\n vec3 folded = (scattered + dot(scattered, (scattered.yzx + 33.33)));\n return fract(((folded.x + folded.y) * folded.z));\n}\n\nfloat pick(vec2 cell, float steps) {\n return floor((hash21(floor(cell)) * steps));\n}\n\nfloat fogAt(vec3 here, vec3 u_eye_at, vec3 reach) {\n return pow(clamp((distance(here, u_eye_at) / reach.x), 0.0, 1.0), reach.z);\n}\n\nvoid main() {\n v_patch = a_patch;\n v_world = a_position;\n gl_Position = (u_camera * vec4(a_position, 1.0));\n}\n", :fragment "#ifdef GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\n#else\nprecision mediump float;\n#endif\nuniform sampler2D u_palette;\nuniform vec4 u_grain;\nuniform vec4 u_marks;\nuniform vec4 u_lines;\nuniform vec4 u_edges;\nuniform vec2 u_zebra;\nuniform vec3 u_reach;\nuniform vec3 u_haze;\nuniform vec3 u_eye;\nvarying vec4 v_patch;\nvarying vec3 v_world;\n\nconst float PALETTE_WIDTH = 16.0;\nconst float ROAD_SLOT = 3.0;\nconst float CENTRE_SLOT = 5.0;\nconst float PAINT_SLOT = 9.0;\n\nfloat hash21(vec2 seed) {\n vec3 scattered = fract((vec3(seed.xyx) * 0.1031));\n vec3 folded = (scattered + dot(scattered, (scattered.yzx + 33.33)));\n return fract(((folded.x + folded.y) * folded.z));\n}\n\nfloat pick(vec2 cell, float steps) {\n return floor((hash21(floor(cell)) * steps));\n}\n\nfloat fogAt(vec3 here, vec3 u_eye_at, vec3 reach) {\n return pow(clamp((distance(here, u_eye_at) / reach.x), 0.0, 1.0), reach.z);\n}\n\nvec3 shadeAt(float slot, float fog) {\n float along = ((slot + 0.5) / PALETTE_WIDTH);\n return mix(texture2D(u_palette, vec2(along, 0.25)).rgb, texture2D(u_palette, vec2(along, 0.75)).rgb, fog);\n}\n\nfloat levelsOf(int material) {\n if ((material == 0)) {\n return 5.0;\n }\n if ((material == 1)) {\n return 4.0;\n }\n if ((material == 2)) {\n return 4.0;\n }\n if ((material == 4)) {\n return 3.0;\n }\n if ((material == 6)) {\n return 3.0;\n }\n if ((material == 7)) {\n return 4.0;\n }\n if ((material == 8)) {\n return 3.0;\n }\n return 2.0;\n}\n\nfloat levelAt(int material, vec2 world, float run, float across, float style) {\n if ((material == 0)) {\n float tuft = pick((world / u_grain.z), 3.0);\n float blotch = pick(((world / u_grain.w) + 31.0), 3.0);\n return min(4.0, (tuft + blotch));\n }\n if ((material == 1)) {\n return ((2.0 * mod(floor((world.y / u_marks.x)), 2.0)) + pick(((world / u_grain.z) + 61.0), 2.0));\n }\n if ((material == 2)) {\n vec2 cell = mod(world, u_grain.x);\n if (((cell.x < u_grain.y) || (cell.y < u_grain.y))) {\n return 0.0;\n }\n return (1.0 + pick(((world / u_grain.x) + 97.0), 3.0));\n }\n if ((material == 3)) {\n if ((style > 0.5)) {\n return (((across < u_lines.z) && (mod(run, u_lines.x) < u_lines.y)) ? 1.0 : 0.0);\n }\n return (((across >= u_edges.x) && (across < u_edges.y)) ? 2.0 : 0.0);\n }\n if ((material == 4)) {\n if (((across < u_lines.z) && (mod(run, u_lines.x) < u_lines.y))) {\n return 2.0;\n }\n if (((across > 2.0) && (mod(run, u_marks.y) < u_lines.w))) {\n return 1.0;\n }\n return 0.0;\n }\n if ((material == 6)) {\n if (((across > u_edges.z) && (across < u_edges.w))) {\n return 2.0;\n }\n return ((mod(run, u_marks.z) < 1.2) ? 1.0 : 0.0);\n }\n if ((material == 7)) {\n return pick((world / 3.0), 4.0);\n }\n if ((material == 8)) {\n return pick(((world / vec2(5.0, 12.0)) + 13.0), 3.0);\n }\n return ((mod(run, u_zebra.x) < u_zebra.y) ? 1.0 : 0.0);\n}\n\nvoid main() {\n int material = int((v_patch.x + 0.5));\n float across = abs(v_patch.z);\n float far = clamp((distance(v_world, u_eye) / u_reach.x), 0.0, 1.0);\n float level = levelAt(material, v_world.xy, v_patch.y, across, v_patch.w);\n float slot = v_patch.x;\n bool painted = false;\n if (((material == 3) && (level > 1.5))) {\n slot = CENTRE_SLOT;\n painted = true;\n }\n if (((material == 3) && (level > 0.5) && (level < 1.5))) {\n slot = PAINT_SLOT;\n painted = true;\n }\n if (((material == 9) && (level < 0.5))) {\n slot = ROAD_SLOT;\n painted = true;\n }\n vec3 shade = shadeAt(slot, far);\n if (!painted) {\n shade = (shade + ((u_marks.w / 255.0) * (level - floor((levelsOf(material) * 0.5)))));\n }\n shade = mix(shade, u_haze, smoothstep(u_reach.y, 1.0, far));\n gl_FragColor = vec4(shade, 1.0);\n}\n"}, :cloud {:vertex "attribute vec4 a_lobe;\nuniform mat4 u_camera;\nuniform vec3 u_eye;\nuniform float u_now;\nuniform float u_drift;\nuniform float u_focal;\nuniform float u_dome;\nuniform float u_biggest;\nvarying float v_alpha;\nvarying float v_seed;\n\nfloat hash21(vec2 seed) {\n vec3 scattered = fract((vec3(seed.xyx) * 0.1031));\n vec3 folded = (scattered + dot(scattered, (scattered.yzx + 33.33)));\n return fract(((folded.x + folded.y) * folded.z));\n}\n\nfloat pick(vec2 cell, float steps) {\n return floor((hash21(floor(cell)) * steps));\n}\n\nfloat fogAt(vec3 here, vec3 u_eye_at, vec3 reach) {\n return pow(clamp((distance(here, u_eye_at) / reach.x), 0.0, 1.0), reach.z);\n}\n\nvoid main() {\n float turn = (a_lobe.x + (u_now * u_drift));\n float lift = a_lobe.y;\n vec3 bearing = normalize(vec3((cos(turn) * (1.0 - lift)), (sin(turn) * (1.0 - lift)), lift));\n vec4 clip = (u_camera * vec4((u_eye + (bearing * u_dome)), 1.0));\n gl_Position = clip;\n gl_PointSize = clamp(((a_lobe.z * u_focal * u_dome) / max(clip.w, 1.0)), 2.0, u_biggest);\n v_alpha = a_lobe.w;\n v_seed = (a_lobe.x * 13.0);\n}\n", :fragment "#ifdef GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\n#else\nprecision mediump float;\n#endif\nuniform vec3 u_tint;\nuniform vec2 u_shape;\nvarying float v_alpha;\nvarying float v_seed;\n\nfloat hash21(vec2 seed) {\n vec3 scattered = fract((vec3(seed.xyx) * 0.1031));\n vec3 folded = (scattered + dot(scattered, (scattered.yzx + 33.33)));\n return fract(((folded.x + folded.y) * folded.z));\n}\n\nfloat pick(vec2 cell, float steps) {\n return floor((hash21(floor(cell)) * steps));\n}\n\nfloat fogAt(vec3 here, vec3 u_eye_at, vec3 reach) {\n return pow(clamp((distance(here, u_eye_at) / reach.x), 0.0, 1.0), reach.z);\n}\n\nvoid main() {\n vec2 offset = (gl_PointCoord - vec2(0.5));\n float bearing = atan(offset.y, offset.x);\n float turns = (((bearing + 3.1416) / 6.2832) * 9.0);\n float wedge = floor(turns);\n float across = smoothstep(0.0, 1.0, fract(turns));\n float here = hash21(vec2(mod(wedge, 9.0), v_seed));\n float next = hash21(vec2(mod((wedge + 1.0), 9.0), v_seed));\n float ragged = (u_shape.x * (mix(here, next, across) - 0.5) * 2.0);\n float squashed = (1.0 + (u_shape.y * max(0.0, offset.y) * 2.0));\n float from_middle = ((length(offset) * 2.0 * squashed) / (1.0 + ragged));\n float soft = (1.0 - smoothstep(0.3, 1.0, from_middle));\n if (((soft * v_alpha) < 0.002)) {\n discard;\n }\n gl_FragColor = vec4(u_tint, (v_alpha * soft));\n}\n"}, :wall {:vertex "attribute vec3 a_position;\nattribute vec2 a_wall;\nattribute vec2 a_normal;\nattribute float a_seed;\nuniform mat4 u_camera;\nvarying vec2 v_wall;\nvarying vec2 v_normal;\nvarying vec3 v_world;\nvarying float v_seed;\n\nfloat hash21(vec2 seed) {\n vec3 scattered = fract((vec3(seed.xyx) * 0.1031));\n vec3 folded = (scattered + dot(scattered, (scattered.yzx + 33.33)));\n return fract(((folded.x + folded.y) * folded.z));\n}\n\nfloat pick(vec2 cell, float steps) {\n return floor((hash21(floor(cell)) * steps));\n}\n\nfloat fogAt(vec3 here, vec3 u_eye_at, vec3 reach) {\n return pow(clamp((distance(here, u_eye_at) / reach.x), 0.0, 1.0), reach.z);\n}\n\nvoid main() {\n v_wall = a_wall;\n v_normal = a_normal;\n v_world = a_position;\n v_seed = a_seed;\n gl_Position = (u_camera * vec4(a_position, 1.0));\n}\n", :fragment "#ifdef GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\n#else\nprecision mediump float;\n#endif\nuniform vec3 u_brickA;\nuniform vec3 u_brickB;\nuniform vec3 u_brickC;\nuniform vec3 u_mortar;\nuniform vec3 u_face;\nuniform vec3 u_haze;\nuniform vec3 u_eye;\nuniform vec4 u_brick;\nuniform vec3 u_sunward;\nuniform float u_ambient;\nuniform vec3 u_reach;\nvarying vec2 v_wall;\nvarying vec2 v_normal;\nvarying vec3 v_world;\nvarying float v_seed;\n\nfloat hash21(vec2 seed) {\n vec3 scattered = fract((vec3(seed.xyx) * 0.1031));\n vec3 folded = (scattered + dot(scattered, (scattered.yzx + 33.33)));\n return fract(((folded.x + folded.y) * folded.z));\n}\n\nfloat pick(vec2 cell, float steps) {\n return floor((hash21(floor(cell)) * steps));\n}\n\nfloat fogAt(vec3 here, vec3 u_eye_at, vec3 reach) {\n return pow(clamp((distance(here, u_eye_at) / reach.x), 0.0, 1.0), reach.z);\n}\n\nvoid main() {\n float along_period = (u_brick.x + u_brick.z);\n float up_period = (u_brick.y + u_brick.z);\n float course = floor((v_wall.y / up_period));\n float stagger = (mod(course, 2.0) * along_period * 0.5);\n float column = floor(((v_wall.x - stagger) / along_period));\n vec2 inside = vec2(mod((v_wall.x - stagger), along_period), mod(v_wall.y, up_period));\n float half_joint = (u_brick.z * 0.5);\n bool in_mortar = ((inside.x < half_joint) || (inside.x > (along_period - half_joint)) || (inside.y < half_joint) || (inside.y > (up_period - half_joint)));\n float this_brick = hash21((vec2(column, course) + 7.0 + v_seed));\n float this_batch = hash21((floor((vec2(column, course) / u_brick.w)) + 23.0 + v_seed));\n float blend = ((0.6 * this_batch) + (0.4 * this_brick));\n vec3 baked = ((blend < 0.34) ? u_brickA : ((blend < 0.67) ? u_brickB : u_brickC));\n float depth = distance(v_world, u_eye);\n float detail = (1.0 - smoothstep((u_reach.y * 0.55), u_reach.y, depth));\n vec3 shade = mix(u_face, (in_mortar ? u_mortar : baked), detail);\n float lit = (u_ambient + ((1.0 - u_ambient) * max(0.0, dot(normalize(v_normal), normalize(u_sunward.xy)))));\n float fog = pow(clamp((depth / u_reach.x), 0.0, 1.0), u_reach.z);\n gl_FragColor = vec4(mix((shade * lit), u_haze, fog), 1.0);\n}\n"}, :billboard {:vertex "attribute vec3 a_root;\nattribute vec2 a_corner;\nattribute vec3 a_shape;\nuniform mat4 u_camera;\nuniform vec3 u_right;\nvarying vec2 v_local;\nvarying vec2 v_size;\nvarying float v_seed;\nvarying vec3 v_world;\n\nfloat hash21(vec2 seed) {\n vec3 scattered = fract((vec3(seed.xyx) * 0.1031));\n vec3 folded = (scattered + dot(scattered, (scattered.yzx + 33.33)));\n return fract(((folded.x + folded.y) * folded.z));\n}\n\nfloat pick(vec2 cell, float steps) {\n return floor((hash21(floor(cell)) * steps));\n}\n\nfloat fogAt(vec3 here, vec3 u_eye_at, vec3 reach) {\n return pow(clamp((distance(here, u_eye_at) / reach.x), 0.0, 1.0), reach.z);\n}\n\nvoid main() {\n vec3 world = (a_root + (u_right * (a_corner.x * a_shape.x)) + vec3(0.0, 0.0, (a_corner.y * a_shape.y)));\n v_local = vec2((a_corner.x * a_shape.x), (a_corner.y * a_shape.y));\n v_size = a_shape.xy;\n v_seed = a_shape.z;\n v_world = world;\n gl_Position = (u_camera * vec4(world, 1.0));\n}\n", :fragment "#ifdef GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\n#else\nprecision mediump float;\n#endif\nuniform vec3 u_leaf;\nuniform vec3 u_leafLit;\nuniform vec3 u_bark;\nuniform vec3 u_haze;\nuniform vec3 u_eye;\nuniform vec3 u_reach;\nuniform vec4 u_trunk;\nvarying vec2 v_local;\nvarying vec2 v_size;\nvarying float v_seed;\nvarying vec3 v_world;\n\nfloat hash21(vec2 seed) {\n vec3 scattered = fract((vec3(seed.xyx) * 0.1031));\n vec3 folded = (scattered + dot(scattered, (scattered.yzx + 33.33)));\n return fract(((folded.x + folded.y) * folded.z));\n}\n\nfloat pick(vec2 cell, float steps) {\n return floor((hash21(floor(cell)) * steps));\n}\n\nfloat fogAt(vec3 here, vec3 u_eye_at, vec3 reach) {\n return pow(clamp((distance(here, u_eye_at) / reach.x), 0.0, 1.0), reach.z);\n}\n\nfloat toSegment(vec2 place, vec2 from, vec2 to) {\n vec2 reach = (place - from);\n vec2 run = (to - from);\n float along = clamp((dot(reach, run) / max(dot(run, run), 1.0E-4)), 0.0, 1.0);\n return length((reach - (run * along)));\n}\n\nvoid main() {\n float top = v_size.y;\n float fork = (top * u_trunk.y);\n float canopy = ((top - fork) * 0.5);\n vec2 unit = vec2(max(v_size.x, 0.001), max(canopy, 0.001));\n vec2 middle = vec2(0.0, (fork + (canopy * 0.95)));\n vec2 place = ((v_local - middle) / unit);\n float up = clamp((v_local.y / max(middle.y, 0.001)), 0.0, 1.0);\n vec3 shade = u_leaf;\n bool standing = false;\n float taper = (u_trunk.x * mix(1.0, u_trunk.z, up));\n float lean = (u_trunk.w * v_size.x);\n float limb = min(toSegment(v_local, vec2(0.0, (fork * 0.75)), vec2(lean, middle.y)), toSegment(v_local, vec2(0.0, (fork * 0.9)), vec2(-lean, (middle.y * 0.86))));\n float grain = hash21(vec2(floor((v_local.y * 3.0)), v_seed));\n if (((v_local.y < middle.y) && ((abs(v_local.x) < taper) || (limb < (u_trunk.x * 0.55))))) {\n shade = mix(u_bark, (u_bark * 1.25), grain);\n standing = true;\n }\n for (int blob = 0; blob < 7; blob++) {\n float which = float(blob);\n float turn = hash21(vec2((v_seed + (which * 3.7)), 1.0));\n float outward = hash21(vec2((v_seed + (which * 5.3)), 2.0));\n float grow = hash21(vec2((v_seed + (which * 7.1)), 3.0));\n float ring = ((blob == 0) ? 0.0 : (0.2 + (0.25 * outward)));\n vec2 at = vec2((cos((turn * 6.2832)) * ring), (sin((turn * 6.2832)) * ring * 0.75));\n float spread = ((blob == 0) ? 0.52 : (0.26 + (0.2 * grow)));\n float into = (spread - length((place - at)));\n if ((into > 0.0)) {\n float lift = clamp(((0.55 * (0.5 - at.y)) + (0.5 * (1.0 - (into / spread))) + (0.35 * grow)), 0.0, 1.0);\n float dapple = hash21((floor((place * 9.0)) + v_seed));\n shade = mix(u_leafLit, u_leaf, clamp((lift + (0.22 * (dapple - 0.5))), 0.0, 1.0));\n standing = true;\n }\n }\n if (!standing) {\n discard;\n }\n float fog = fogAt(v_world, u_eye, u_reach);\n gl_FragColor = vec4(mix(shade, u_haze, fog), 1.0);\n}\n"}, :panel {:vertex "attribute vec3 a_position;\nattribute vec2 a_uv;\nattribute vec2 a_normal;\nuniform mat4 u_camera;\nvarying vec2 v_uv;\nvarying vec2 v_normal;\nvarying vec3 v_world;\n\nfloat hash21(vec2 seed) {\n vec3 scattered = fract((vec3(seed.xyx) * 0.1031));\n vec3 folded = (scattered + dot(scattered, (scattered.yzx + 33.33)));\n return fract(((folded.x + folded.y) * folded.z));\n}\n\nfloat pick(vec2 cell, float steps) {\n return floor((hash21(floor(cell)) * steps));\n}\n\nfloat fogAt(vec3 here, vec3 u_eye_at, vec3 reach) {\n return pow(clamp((distance(here, u_eye_at) / reach.x), 0.0, 1.0), reach.z);\n}\n\nvoid main() {\n v_uv = a_uv;\n v_normal = a_normal;\n v_world = a_position;\n gl_Position = (u_camera * vec4(a_position, 1.0));\n}\n", :fragment "#ifdef GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\n#else\nprecision mediump float;\n#endif\nuniform sampler2D u_image;\nuniform vec3 u_eye;\nuniform vec3 u_sunward;\nuniform float u_ambient;\nuniform vec3 u_reach;\nvarying vec2 v_uv;\nvarying vec2 v_normal;\nvarying vec3 v_world;\n\nfloat hash21(vec2 seed) {\n vec3 scattered = fract((vec3(seed.xyx) * 0.1031));\n vec3 folded = (scattered + dot(scattered, (scattered.yzx + 33.33)));\n return fract(((folded.x + folded.y) * folded.z));\n}\n\nfloat pick(vec2 cell, float steps) {\n return floor((hash21(floor(cell)) * steps));\n}\n\nfloat fogAt(vec3 here, vec3 u_eye_at, vec3 reach) {\n return pow(clamp((distance(here, u_eye_at) / reach.x), 0.0, 1.0), reach.z);\n}\n\nvoid main() {\n vec4 picture = texture2D(u_image, v_uv);\n float fog = fogAt(v_world, u_eye, u_reach);\n float showing = (picture.a * (1.0 - fog));\n if ((showing < 0.02)) {\n discard;\n }\n float lit = (u_ambient + ((1.0 - u_ambient) * max(0.0, dot(normalize(v_normal), normalize(u_sunward.xy)))));\n gl_FragColor = vec4((picture.rgb * lit), showing);\n}\n"}, :mist {:vertex "attribute vec4 a_drop;\nuniform mat4 u_camera;\nuniform vec3 u_middle;\nuniform vec4 u_bell;\nuniform float u_now;\nuniform float u_focal;\nuniform float u_biggest;\nvarying float v_alpha;\nvarying float v_seed;\n\nfloat hash21(vec2 seed) {\n vec3 scattered = fract((vec3(seed.xyx) * 0.1031));\n vec3 folded = (scattered + dot(scattered, (scattered.yzx + 33.33)));\n return fract(((folded.x + folded.y) * folded.z));\n}\n\nfloat pick(vec2 cell, float steps) {\n return floor((hash21(floor(cell)) * steps));\n}\n\nfloat fogAt(vec3 here, vec3 u_eye_at, vec3 reach) {\n return pow(clamp((distance(here, u_eye_at) / reach.x), 0.0, 1.0), reach.z);\n}\n\nvoid main() {\n float life = u_bell.z;\n float age = fract(((u_now + (a_drop.z * life)) / life));\n float scatter = (0.6 * pow(age, 1.6));\n vec3 stray = vec3(((hash21(vec2(a_drop.w, 1.0)) - 0.5) * 2.0 * scatter), ((hash21(vec2(a_drop.w, 2.0)) - 0.5) * 2.0 * scatter), ((hash21(vec2(a_drop.w, 3.0)) - 0.5) * scatter));\n float outward = (u_bell.x * a_drop.y * age);\n float climb = (u_bell.y * mix(2.6, 1.0, clamp(a_drop.y, 0.0, 1.0)));\n float arc = ((2.0 * age) - (2.6 * age * age));\n vec3 world = (vec3((u_middle.x + (outward * cos(a_drop.x))), (u_middle.y + (outward * sin(a_drop.x))), (u_middle.z + (climb * arc))) + stray);\n float from_middle = length((world.xy - u_middle.xy));\n float edge = clamp(((u_bell.w - from_middle) / (u_bell.w * 0.18)), 0.0, 1.0);\n float fading = ((age < 0.8) ? (0.35 + (0.44 * age)) : (0.7 * (1.0 - age) * 5.0));\n vec4 clip = (u_camera * vec4(world, 1.0));\n v_alpha = (fading * edge);\n v_seed = a_drop.w;\n gl_Position = clip;\n gl_PointSize = clamp((((0.1 + (0.1 * age)) * u_focal) / max(clip.w, 0.5)), 1.0, u_biggest);\n}\n", :fragment "#ifdef GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\n#else\nprecision mediump float;\n#endif\nuniform vec3 u_tint;\nuniform vec2 u_shape;\nvarying float v_alpha;\nvarying float v_seed;\n\nfloat hash21(vec2 seed) {\n vec3 scattered = fract((vec3(seed.xyx) * 0.1031));\n vec3 folded = (scattered + dot(scattered, (scattered.yzx + 33.33)));\n return fract(((folded.x + folded.y) * folded.z));\n}\n\nfloat pick(vec2 cell, float steps) {\n return floor((hash21(floor(cell)) * steps));\n}\n\nfloat fogAt(vec3 here, vec3 u_eye_at, vec3 reach) {\n return pow(clamp((distance(here, u_eye_at) / reach.x), 0.0, 1.0), reach.z);\n}\n\nvoid main() {\n vec2 offset = (gl_PointCoord - vec2(0.5));\n float bearing = atan(offset.y, offset.x);\n float turns = (((bearing + 3.1416) / 6.2832) * 9.0);\n float wedge = floor(turns);\n float across = smoothstep(0.0, 1.0, fract(turns));\n float here = hash21(vec2(mod(wedge, 9.0), v_seed));\n float next = hash21(vec2(mod((wedge + 1.0), 9.0), v_seed));\n float ragged = (u_shape.x * (mix(here, next, across) - 0.5) * 2.0);\n float squashed = (1.0 + (u_shape.y * max(0.0, offset.y) * 2.0));\n float from_middle = ((length(offset) * 2.0 * squashed) / (1.0 + ragged));\n float soft = (1.0 - smoothstep(0.3, 1.0, from_middle));\n if (((soft * v_alpha) < 0.002)) {\n discard;\n }\n gl_FragColor = vec4(u_tint, (v_alpha * soft));\n}\n"}, :sky {:vertex "attribute vec2 a_corner;\nuniform vec3 u_right;\nuniform vec3 u_up;\nuniform vec3 u_forward;\nuniform vec2 u_spread;\nvarying vec3 v_ray;\n\nfloat hash21(vec2 seed) {\n vec3 scattered = fract((vec3(seed.xyx) * 0.1031));\n vec3 folded = (scattered + dot(scattered, (scattered.yzx + 33.33)));\n return fract(((folded.x + folded.y) * folded.z));\n}\n\nfloat pick(vec2 cell, float steps) {\n return floor((hash21(floor(cell)) * steps));\n}\n\nfloat fogAt(vec3 here, vec3 u_eye_at, vec3 reach) {\n return pow(clamp((distance(here, u_eye_at) / reach.x), 0.0, 1.0), reach.z);\n}\n\nvoid main() {\n v_ray = (u_forward + (u_right * (a_corner.x * u_spread.x)) + (u_up * (a_corner.y * u_spread.y)));\n gl_Position = vec4(a_corner, 0.0, 1.0);\n}\n", :fragment "#ifdef GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\n#else\nprecision mediump float;\n#endif\nuniform vec3 u_sky;\nuniform vec3 u_haze;\nuniform vec3 u_sun;\nuniform vec3 u_sunward;\nuniform float u_size;\nvarying vec3 v_ray;\n\nfloat hash21(vec2 seed) {\n vec3 scattered = fract((vec3(seed.xyx) * 0.1031));\n vec3 folded = (scattered + dot(scattered, (scattered.yzx + 33.33)));\n return fract(((folded.x + folded.y) * folded.z));\n}\n\nfloat pick(vec2 cell, float steps) {\n return floor((hash21(floor(cell)) * steps));\n}\n\nfloat fogAt(vec3 here, vec3 u_eye_at, vec3 reach) {\n return pow(clamp((distance(here, u_eye_at) / reach.x), 0.0, 1.0), reach.z);\n}\n\nvoid main() {\n vec3 ray = normalize(v_ray);\n float overhead = smoothstep(0.0, 0.55, clamp(ray.z, 0.0, 1.0));\n vec3 ground_up = mix(u_haze, u_sky, overhead);\n float from_sun = acos(clamp(dot(ray, u_sunward), -1.0, 1.0));\n float disc = from_sun;\n float core = (u_size * 0.0022);\n float glare = (1.0 - smoothstep(core, (core * 3.4), disc));\n float glow = (0.35 * pow(max(dot(ray, u_sunward), 0.0), 6.0));\n vec3 lit_sky = mix(ground_up, u_sun, glow);\n gl_FragColor = vec4(mix(lit_sky, u_sun, glare), 1.0);\n}\n"}}, :surfaces [{:x 330, :y 265, :w 250, :h 130, :surface :lawn, :name "THE QUAD", :layer 0, :axis :ew} {:x 615, :y 560, :w 145, :h 65, :surface :field, :name "CORLISS FIELD", :layer 0, :axis :ew} {:x 330, :y 665, :w 250, :h 85, :surface :fill, :name "THE FILL", :layer 0, :axis :ew} {:x 50, :y 90, :w 38, :h 630, :surface :water, :name "SPRING BROOK", :layer 0, :axis :ns} {:x 330, :y 560, :w 130, :h 65, :surface :lot, :layer 1, :axis :ew} {:x 475, :y 560, :w 105, :h 65, :surface :lot, :layer 1, :axis :ew} {:x 135, :y 175, :w 145, :h 75, :surface :lot, :layer 1, :axis :ew} {:x 92.0, :y -999.9999999999998, :w 26, :h 1999.9999999999998, :surface :road, :axis :ns, :style 0, :layer 2} {:x -1000.0, :y 137.0, :w 2000.0, :h 26, :surface :road, :axis :ew, :style 0, :layer 2} {:x -1000.0, :y 527.0, :w 2000.0, :h 26, :surface :road, :axis :ew, :style 0, :layer 2} {:x -1000.0, :y 630.0, :w 2000.0, :h 20, :surface :road, :axis :ew, :style 1, :layer 2} {:x 295.0, :y -1000.0, :w 20, :h 2000.0, :surface :road, :axis :ns, :style 1, :layer 2} {:x 587.0, :y -1000.0, :w 20, :h 2000.0, :surface :road, :axis :ns, :style 1, :layer 2} {:x 765.0, :y -1000.0, :w 20, :h 2000.0, :surface :road, :axis :ns, :style 1, :layer 2} {:x 934.0, :y -1000.0, :w 12, :h 2000.0, :surface :rail, :layer 3, :axis :ns} {:x 340, :y 80, :w 100, :h 45, :surface :paving, :layer 4, :axis :ew} {:x 330, :y 175, :w 250, :h 75, :surface :paving, :layer 4, :axis :ew} {:x 615, :y 175, :w 145, :h 75, :surface :paving, :layer 4, :axis :ew} {:x 795, :y 175, :w 115, :h 75, :surface :paving, :layer 4, :axis :ew} {:x 615, :y 265, :w 145, :h 75, :surface :paving, :layer 4, :axis :ew} {:x 615, :y 350, :w 145, :h 45, :surface :paving, :layer 4, :axis :ew} {:x 795, :y 265, :w 115, :h 55, :surface :paving, :layer 4, :axis :ew} {:x 795, :y 330, :w 115, :h 55, :surface :paving, :layer 4, :axis :ew} {:x 795, :y 395, :w 115, :h 55, :surface :paving, :layer 4, :axis :ew} {:x 330, :y 425, :w 155, :h 90, :surface :paving, :layer 4, :axis :ew} {:x 500, :y 425, :w 70, :h 90, :surface :paving, :layer 4, :axis :ns} {:x 615, :y 425, :w 145, :h 90, :surface :paving, :layer 4, :axis :ew} {:x 795, :y 460, :w 115, :h 55, :surface :paving, :layer 4, :axis :ew} {:x 135, :y 265, :w 145, :h 85, :surface :paving, :layer 4, :axis :ew} {:x 152, :y 362, :w 110, :h 110, :surface :paving, :layer 4, :axis :ew} {:x 135, :y 560, :w 145, :h 60, :surface :paving, :layer 4, :axis :ew} {:x 390, :y 103, :w 18, :h 368, :surface :paving, :layer 4, :axis :ns} {:x 414, :y 103, :w 18, :h 110, :surface :paving, :layer 4, :axis :ns} {:x 455, :y 204, :w 233, :h 18, :surface :paving, :layer 4, :axis :ew} {:x 422, :y 213, :w 18, :h 258, :surface :paving, :layer 4, :axis :ns} {:x 679, :y 213, :w 18, :h 258, :surface :paving, :layer 4, :axis :ns} {:x 455, :y 204, :w 398, :h 18, :surface :paving, :layer 4, :axis :ew} {:x 688, :y 204, :w 165, :h 18, :surface :paving, :layer 4, :axis :ew} {:x 844, :y 213, :w 18, :h 80, :surface :paving, :layer 4, :axis :ns} {:x 844, :y 213, :w 18, :h 145, :surface :paving, :layer 4, :axis :ns} {:x 679, :y 213, :w 18, :h 90, :surface :paving, :layer 4, :axis :ns} {:x 688, :y 289, :w 165, :h 18, :surface :paving, :layer 4, :axis :ew} {:x 688, :y 321, :w 165, :h 18, :surface :paving, :layer 4, :axis :ew} {:x 679, :y 303, :w 18, :h 168, :surface :paving, :layer 4, :axis :ns} {:x 679, :y 213, :w 18, :h 160, :surface :paving, :layer 4, :axis :ns} {:x 679, :y 303, :w 18, :h 70, :surface :paving, :layer 4, :axis :ns} {:x 688, :y 356, :w 165, :h 18, :surface :paving, :layer 4, :axis :ew} {:x 679, :y 373, :w 18, :h 98, :surface :paving, :layer 4, :axis :ns} {:x 207, :y 386, :w 481, :h 18, :surface :paving, :layer 4, :axis :ew} {:x 844, :y 293, :w 18, :h 65, :surface :paving, :layer 4, :axis :ns} {:x 844, :y 213, :w 18, :h 210, :surface :paving, :layer 4, :axis :ns} {:x 688, :y 389, :w 165, :h 18, :surface :paving, :layer 4, :axis :ew} {:x 844, :y 293, :w 18, :h 130, :surface :paving, :layer 4, :axis :ns} {:x 844, :y 358, :w 18, :h 65, :surface :paving, :layer 4, :axis :ns} {:x 408, :y 437, :w 445, :h 18, :surface :paving, :layer 4, :axis :ew} {:x 535, :y 437, :w 318, :h 18, :surface :paving, :layer 4, :axis :ew} {:x 688, :y 437, :w 165, :h 18, :surface :paving, :layer 4, :axis :ew} {:x 207, :y 411, :w 646, :h 18, :surface :paving, :layer 4, :axis :ew} {:x 408, :y 461, :w 128, :h 18, :surface :paving, :layer 4, :axis :ew} {:x 535, :y 461, :w 153, :h 18, :surface :paving, :layer 4, :axis :ew} {:x 207, :y 435, :w 328, :h 18, :surface :paving, :layer 4, :axis :ew} {:x 408, :y 461, :w 280, :h 18, :surface :paving, :layer 4, :axis :ew} {:x 844, :y 213, :w 18, :h 275, :surface :paving, :layer 4, :axis :ns} {:x 844, :y 293, :w 18, :h 195, :surface :paving, :layer 4, :axis :ns} {:x 844, :y 358, :w 18, :h 130, :surface :paving, :layer 4, :axis :ns} {:x 844, :y 423, :w 18, :h 65, :surface :paving, :layer 4, :axis :ns} {:x 408, :y 470, :w 445, :h 18, :surface :paving, :layer 4, :axis :ew} {:x 535, :y 470, :w 318, :h 18, :surface :paving, :layer 4, :axis :ew} {:x 688, :y 470, :w 165, :h 18, :surface :paving, :layer 4, :axis :ew} {:x 208, :y 296, :w 480, :h 18, :surface :paving, :layer 4, :axis :ew} {:x 208, :y 331, :w 480, :h 18, :surface :paving, :layer 4, :axis :ew} {:x 208, :y 291, :w 645, :h 18, :surface :paving, :layer 4, :axis :ew} {:x 208, :y 324, :w 645, :h 18, :surface :paving, :layer 4, :axis :ew} {:x 198, :y 308, :w 18, :h 110, :surface :paving, :layer 4, :axis :ns} {:x 199, :y 308, :w 18, :h 283, :surface :paving, :layer 4, :axis :ns} {:x 207, :y 378, :w 646, :h 18, :surface :paving, :layer 4, :axis :ew} {:x 207, :y 435, :w 201, :h 18, :surface :paving, :layer 4, :axis :ew} {:x 207, :y 435, :w 481, :h 18, :surface :paving, :layer 4, :axis :ew} {:x 198, :y 417, :w 18, :h 173, :surface :paving, :layer 4, :axis :ns} {:x 390, :y 137.0, :w 18, :h 26.0, :surface :crosswalk, :axis :ew, :layer 5} {:x 414, :y 137.0, :w 18, :h 26.0, :surface :crosswalk, :axis :ew, :layer 5} {:x 587.0, :y 204, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 587.0, :y 204, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 765.0, :y 204, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 765.0, :y 204, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 765.0, :y 289, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 765.0, :y 321, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 765.0, :y 356, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 295.0, :y 386, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 587.0, :y 386, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 765.0, :y 389, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 587.0, :y 437, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 765.0, :y 437, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 587.0, :y 437, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 765.0, :y 437, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 765.0, :y 437, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 295.0, :y 411, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 587.0, :y 411, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 765.0, :y 411, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 587.0, :y 461, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 295.0, :y 435, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 587.0, :y 461, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 587.0, :y 470, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 765.0, :y 470, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 587.0, :y 470, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 765.0, :y 470, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 765.0, :y 470, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 295.0, :y 296, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 587.0, :y 296, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 295.0, :y 331, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 587.0, :y 331, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 295.0, :y 291, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 587.0, :y 291, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 765.0, :y 291, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 295.0, :y 324, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 587.0, :y 324, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 765.0, :y 324, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 199, :y 527.0, :w 18, :h 26.0, :surface :crosswalk, :axis :ew, :layer 5} {:x 295.0, :y 378, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 587.0, :y 378, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 765.0, :y 378, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 295.0, :y 435, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 295.0, :y 435, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 587.0, :y 435, :w 20.0, :h 18, :surface :crosswalk, :axis :ns, :layer 5} {:x 198, :y 527.0, :w 18, :h 26.0, :surface :crosswalk, :axis :ew, :layer 5}], :facing 0.0, :route [[305.0 150.0] [775.0 150.0] [775.0 540.0] [305.0 540.0]], :door-picture "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"91\" height=\"156\" viewBox=\"0 0 91 156\" shape-rendering=\"crispEdges\" image-rendering=\"pixelated\" xmlns:c2pa=\"http://c2pa.org/manifest\"><metadata><c2pa:manifest>AAAWgmp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAABZcanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTpjY2VkM2EwMS0wMjQ0LTRiODItOTJhNS04MTVhNzIzZDIzZGQAAAADl2p1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAALxqdW1iAAAARGp1bWRjYm9yABEAEIAAAKoAOJtxE2MycGEuaW5ncmVkaWVudC52MwAAAAAYYzJzaL1pwKZ3xk8aMFLIOXTudbQAAABwY2JvcqNpZGM6Zm9ybWF0bWltYWdlL3N2Zyt4bWxqaW5zdGFuY2VJRHgseG1wOmlpZDo3MDM2OWNjNi0zNTM2LTQ4YzQtOTQxMi01OGJjM2U3NDAyMjlscmVsYXRpb25zaGlwaHBhcmVudE9mAAAB4mp1bWIAAABBanVtZGNib3IAEQAQgAAAqgA4m3ETYzJwYS5hY3Rpb25zLnYyAAAAABhjMnNoqEl2X5EQUkF6GBQbT2MS7AAAAZljYm9yomdhY3Rpb25zgqJmYWN0aW9ua2MycGEub3BlbmVkanBhcmFtZXRlcnOha2luZ3JlZGllbnRzgaJjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFgghKQTIOopivJCUJ9Q/He4Fxo0Bednx2nVrgInUL7dLJqkZmFjdGlvbngdY29tLmFudGhyb3BpYy5jbGF1ZGUucHJvdmlkZWRqcGFyYW1ldGVyc6F4H2NvbS5hbnRocm9waWMub3JpZ2luLWNvbmZpZGVuY2VndW5rbm93bmtkZXNjcmlwdGlvbnhmQ2xhdWRlIHByb3ZpZGVkIHRoaXMgZmlsZSBhdCB0aGUgcmVxdWVzdCBvZiBhIHVzZXIgYW5kIG1heSBoYXZlIGNyZWF0ZWQgb3IgbW9kaWZpZWQgdGhlIGZpbGUgY29udGVudHMubXNvZnR3YXJlQWdlbnShZG5hbWVmQ2xhdWRlcmFsbEFjdGlvbnNJbmNsdWRlZPUAAADIanVtYgAAAEBqdW1kY2JvcgARABCAAACqADibcRNjMnBhLmhhc2guZGF0YQAAAAAYYzJzaOo5jMzn3qY7if7OL3Vj8n4AAACAY2JvcqVjYWxnZnNoYTI1NmNwYWRNAAAAAAAAAAAAAAAAAGRoYXNoWCA0HH0pY4BZ5pq36HbCqGDxxsQWhd6P2WY9Al9yfW7dKWRuYW1lbmp1bWJmIG1hbmlmZXN0amV4Y2x1c2lvbnOBomVzdGFydBjNZmxlbmd0aBkeBAAAAj5qdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAACD2Nib3KlY2FsZ2ZzaGEyNTZpc2lnbmF0dXJleE1zZWxmI2p1bWJmPS9jMnBhL3VybjpjMnBhOmNjZWQzYTAxLTAyNDQtNGI4Mi05MmE1LTgxNWE3MjNkMjNkZC9jMnBhLnNpZ25hdHVyZWppbnN0YW5jZUlEeCx4bXA6aWlkOmJjZjE5ODY0LWU0NTQtNDhmMy05MDM1LWY4ZDU2YmJkMjFmOXJjcmVhdGVkX2Fzc2VydGlvbnODomN1cmx4LXNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuaW5ncmVkaWVudC52M2RoYXNoWCCEpBMg6imK8kJQn1D8d7gXGjQF52fHadWuAidQvt0smqJjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFggxDO5OLEZWqbywChXtaqLkl3+j8mByBeEnOrKec1St9SiY3VybHgpc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5oYXNoLmRhdGFkaGFzaFggIfgLAONRTVSpcLc8HwElhS0pWv+XKbBleponm6zpqHF0Y2xhaW1fZ2VuZXJhdG9yX2luZm+jZG5hbWVvQW50aHJvcGljIEZpbGVzZ3ZlcnNpb25lMS4wLjBrc3BlY1ZlcnNpb25lMi40LjAAABA4anVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAABAIY2JvctKEWQISogEmGCFZAgowggIGMIIBjaADAgECAhRA5aAK7sI50L64g/oGQgU9Z1UTADAKBggqhkjOPQQDAzBJMRcwFQYDVQQKEw5BbnRocm9waWMsIFBCQzEuMCwGA1UEAxMlQW50aHJvcGljIENvbnRlbnQgQ3JlZGVudGlhbHMgUm9vdCBDQTAeFw0yNjA4MDcxODQzNTZaFw0yODA4MDYxOTQzNTZaMEQxFzAVBgNVBAoTDkFudGhyb3BpYywgUEJDMSkwJwYDVQQDEyBBbnRocm9waWMgQ2xhdWRlIENvbnRlbnQgU2lnbmluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJh6CmvLUBgFFNU0vUKlOVtE6djd17L5SuwX0LemFisBM3dkd/3cyjxFA3Qo5S46fX0/ihY0VZ7mfb9KF703t5OjWDBWMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUEDjAMBgorBgEEAYPoXgIBMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUzlHiBIFOZFsj+OPEz5o+nMHXXMIwCgYIKoZIzj0EAwMDZwAwZAIwMXMdFJ4BetLLVY7ORuE9noqbbAZOZn/aArXyTwFAZfKrPzxF2vPoJNf1+UCdg1XGAjBwX1zd9WGqYkqmL5SFqw1QySjr1zJfpJM9+1rdDwSPLMOPOjKuiXjoU/pUUeG9RwmhY3BhZFkNngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPZYQJ8eWIUAUldpqdzU30r9VLtRTrtDyRH0AU5QjHBenEr1EmqbBuBWLTyOslZluj6prNJ85MkzqgmFtjoLJ5nYVJE=</c2pa:manifest></metadata>\n <title>Panelled door - straight on</title>\n <rect x=\"91\" y=\"0\" width=\"1\" height=\"156\" fill=\"#231610\"></rect>\n <g transform=\"translate(0,0) scale(1.000000,1)\">\n<path fill=\"#231610\" d=\"M0 0h1v156h-1zM1 0h1v1h-1zM1 155h1v1h-1zM2 0h1v1h-1zM2 155h1v1h-1zM3 0h1v1h-1zM3 155h1v1h-1zM4 0h1v1h-1zM4 155h1v1h-1zM5 0h1v1h-1zM5 155h1v1h-1zM6 0h1v1h-1zM6 155h1v1h-1zM7 0h1v1h-1zM7 155h1v1h-1zM8 0h1v1h-1zM8 155h1v1h-1zM9 0h1v1h-1zM9 155h1v1h-1zM10 0h1v1h-1zM10 155h1v1h-1zM11 0h1v1h-1zM11 155h1v1h-1zM12 0h1v1h-1zM12 155h1v1h-1zM13 0h1v1h-1zM13 155h1v1h-1zM14 0h1v1h-1zM14 155h1v1h-1zM15 0h1v1h-1zM15 155h1v1h-1zM16 0h1v1h-1zM16 155h1v1h-1zM17 0h1v1h-1zM17 155h1v1h-1zM18 0h1v1h-1zM18 155h1v1h-1zM19 0h1v1h-1zM19 155h1v1h-1zM20 0h1v1h-1zM20 155h1v1h-1zM21 0h1v1h-1zM21 155h1v1h-1zM22 0h1v1h-1zM22 155h1v1h-1zM23 0h1v1h-1zM23 155h1v1h-1zM24 0h1v1h-1zM24 155h1v1h-1zM25 0h1v1h-1zM25 155h1v1h-1zM26 0h1v1h-1zM26 155h1v1h-1zM27 0h1v1h-1zM27 155h1v1h-1zM28 0h1v1h-1zM28 155h1v1h-1zM29 0h1v1h-1zM29 155h1v1h-1zM30 0h1v1h-1zM30 155h1v1h-1zM31 0h1v1h-1zM31 155h1v1h-1zM32 0h1v1h-1zM32 155h1v1h-1zM33 0h1v1h-1zM33 155h1v1h-1zM34 0h1v1h-1zM34 155h1v1h-1zM35 0h1v1h-1zM35 155h1v1h-1zM36 0h1v1h-1zM36 155h1v1h-1zM37 0h1v1h-1zM37 155h1v1h-1zM38 0h1v1h-1zM38 155h1v1h-1zM39 0h1v1h-1zM39 155h1v1h-1zM40 0h1v1h-1zM40 155h1v1h-1zM41 0h1v1h-1zM41 155h1v1h-1zM42 0h1v1h-1zM42 155h1v1h-1zM43 0h1v1h-1zM43 155h1v1h-1zM44 0h1v1h-1zM44 155h1v1h-1zM45 0h1v1h-1zM45 155h1v1h-1zM46 0h1v1h-1zM46 155h1v1h-1zM47 0h1v1h-1zM47 155h1v1h-1zM48 0h1v1h-1zM48 155h1v1h-1zM49 0h1v1h-1zM49 155h1v1h-1zM50 0h1v1h-1zM50 155h1v1h-1zM51 0h1v1h-1zM51 155h1v1h-1zM52 0h1v1h-1zM52 155h1v1h-1zM53 0h1v1h-1zM53 155h1v1h-1zM54 0h1v1h-1zM54 155h1v1h-1zM55 0h1v1h-1zM55 155h1v1h-1zM56 0h1v1h-1zM56 155h1v1h-1zM57 0h1v1h-1zM57 155h1v1h-1zM58 0h1v1h-1zM58 155h1v1h-1zM59 0h1v1h-1zM59 155h1v1h-1zM60 0h1v1h-1zM60 155h1v1h-1zM61 0h1v1h-1zM61 155h1v1h-1zM62 0h1v1h-1zM62 155h1v1h-1zM63 0h1v1h-1zM63 155h1v1h-1zM64 0h1v1h-1zM64 155h1v1h-1zM65 0h1v1h-1zM65 155h1v1h-1zM66 0h1v1h-1zM66 155h1v1h-1zM67 0h1v1h-1zM67 155h1v1h-1zM68 0h1v1h-1zM68 155h1v1h-1zM69 0h1v1h-1zM69 155h1v1h-1zM70 0h1v1h-1zM70 155h1v1h-1zM71 0h1v1h-1zM71 155h1v1h-1zM72 0h1v1h-1zM72 155h1v1h-1zM73 0h1v1h-1zM73 155h1v1h-1zM74 0h1v1h-1zM74 155h1v1h-1zM75 0h1v1h-1zM75 155h1v1h-1zM76 0h1v1h-1zM76 155h1v1h-1zM77 0h1v1h-1zM77 155h1v1h-1zM78 0h1v1h-1zM78 155h1v1h-1zM79 0h1v1h-1zM79 155h1v1h-1zM80 0h1v1h-1zM80 155h1v1h-1zM81 0h1v1h-1zM81 155h1v1h-1zM82 0h1v1h-1zM82 155h1v1h-1zM83 0h1v1h-1zM83 155h1v1h-1zM84 0h1v1h-1zM84 155h1v1h-1zM85 0h1v1h-1zM85 155h1v1h-1zM86 0h1v1h-1zM86 155h1v1h-1zM87 0h1v1h-1zM87 155h1v1h-1zM88 0h1v1h-1zM88 155h1v1h-1zM89 0h1v1h-1zM89 155h1v1h-1zM90 0h1v156h-1z\"></path>\n <path fill=\"#4d3021\" d=\"M2 154h1v1h-1zM3 153h1v2h-1zM4 153h1v2h-1zM5 153h1v2h-1zM6 153h1v2h-1zM7 153h1v2h-1zM8 153h1v2h-1zM9 153h1v2h-1zM10 153h1v2h-1zM11 153h1v2h-1zM12 153h1v2h-1zM13 153h1v2h-1zM14 15h1v1h-1zM14 91h1v1h-1zM14 153h1v2h-1zM15 15h1v2h-1zM15 91h1v2h-1zM15 153h1v2h-1zM16 15h1v3h-1zM16 91h1v3h-1zM16 153h1v2h-1zM17 15h1v3h-1zM17 91h1v3h-1zM17 153h1v2h-1zM18 15h1v3h-1zM18 91h1v3h-1zM18 153h1v2h-1zM19 15h1v3h-1zM19 91h1v3h-1zM19 153h1v2h-1zM20 15h1v3h-1zM20 91h1v3h-1zM20 153h1v2h-1zM21 15h1v3h-1zM21 91h1v3h-1zM21 153h1v2h-1zM22 15h1v3h-1zM22 91h1v3h-1zM22 153h1v2h-1zM23 15h1v3h-1zM23 91h1v3h-1zM23 153h1v2h-1zM24 15h1v3h-1zM24 91h1v3h-1zM24 153h1v2h-1zM25 15h1v3h-1zM25 91h1v3h-1zM25 153h1v2h-1zM26 15h1v3h-1zM26 91h1v3h-1zM26 153h1v2h-1zM27 15h1v3h-1zM27 91h1v3h-1zM27 153h1v2h-1zM28 15h1v3h-1zM28 91h1v3h-1zM28 153h1v2h-1zM29 15h1v3h-1zM29 91h1v3h-1zM29 153h1v2h-1zM30 15h1v3h-1zM30 91h1v3h-1zM30 153h1v2h-1zM31 15h1v3h-1zM31 91h1v3h-1zM31 153h1v2h-1zM32 15h1v3h-1zM32 91h1v3h-1zM32 153h1v2h-1zM33 15h1v3h-1zM33 91h1v3h-1zM33 153h1v2h-1zM34 15h1v3h-1zM34 91h1v3h-1zM34 153h1v2h-1zM35 15h1v3h-1zM35 91h1v3h-1zM35 153h1v2h-1zM36 15h1v3h-1zM36 91h1v3h-1zM36 153h1v2h-1zM37 15h1v3h-1zM37 91h1v3h-1zM37 153h1v2h-1zM38 15h1v3h-1zM38 91h1v3h-1zM38 153h1v2h-1zM39 15h1v3h-1zM39 91h1v3h-1zM39 153h1v2h-1zM40 15h1v3h-1zM40 91h1v3h-1zM40 153h1v2h-1zM41 15h1v3h-1zM41 91h1v3h-1zM41 153h1v2h-1zM42 15h1v3h-1zM42 91h1v3h-1zM42 153h1v2h-1zM43 15h1v3h-1zM43 91h1v3h-1zM43 153h1v2h-1zM44 15h1v3h-1zM44 91h1v3h-1zM44 153h1v2h-1zM45 15h1v3h-1zM45 91h1v3h-1zM45 153h1v2h-1zM46 15h1v3h-1zM46 91h1v3h-1zM46 153h1v2h-1zM47 15h1v3h-1zM47 91h1v3h-1zM47 153h1v2h-1zM48 15h1v3h-1zM48 91h1v3h-1zM48 153h1v2h-1zM49 15h1v3h-1zM49 91h1v3h-1zM49 153h1v2h-1zM50 15h1v3h-1zM50 91h1v3h-1zM50 153h1v2h-1zM51 15h1v3h-1zM51 91h1v3h-1zM51 153h1v2h-1zM52 15h1v3h-1zM52 91h1v3h-1zM52 153h1v2h-1zM53 15h1v3h-1zM53 91h1v3h-1zM53 153h1v2h-1zM54 15h1v3h-1zM54 91h1v3h-1zM54 153h1v2h-1zM55 15h1v3h-1zM55 91h1v3h-1zM55 153h1v2h-1zM56 15h1v3h-1zM56 91h1v3h-1zM56 153h1v2h-1zM57 15h1v3h-1zM57 91h1v3h-1zM57 153h1v2h-1zM58 15h1v3h-1zM58 91h1v3h-1zM58 153h1v2h-1zM59 15h1v3h-1zM59 91h1v3h-1zM59 153h1v2h-1zM60 15h1v3h-1zM60 91h1v3h-1zM60 153h1v2h-1zM61 15h1v3h-1zM61 91h1v3h-1zM61 153h1v2h-1zM62 15h1v3h-1zM62 91h1v3h-1zM62 153h1v2h-1zM63 15h1v3h-1zM63 91h1v3h-1zM63 153h1v2h-1zM64 15h1v3h-1zM64 91h1v3h-1zM64 153h1v2h-1zM65 15h1v3h-1zM65 91h1v3h-1zM65 153h1v2h-1zM66 15h1v3h-1zM66 91h1v3h-1zM66 153h1v2h-1zM67 15h1v3h-1zM67 91h1v3h-1zM67 153h1v2h-1zM68 15h1v3h-1zM68 91h1v3h-1zM68 153h1v2h-1zM69 15h1v3h-1zM69 91h1v3h-1zM69 153h1v2h-1zM70 15h1v3h-1zM70 91h1v3h-1zM70 153h1v2h-1zM71 15h1v3h-1zM71 91h1v3h-1zM71 153h1v2h-1zM72 15h1v3h-1zM72 91h1v3h-1zM72 153h1v2h-1zM73 15h1v3h-1zM73 91h1v3h-1zM73 153h1v2h-1zM74 15h1v3h-1zM74 91h1v3h-1zM74 153h1v2h-1zM75 15h1v2h-1zM75 91h1v2h-1zM75 153h1v2h-1zM76 15h1v1h-1zM76 91h1v1h-1zM76 153h1v2h-1zM77 153h1v2h-1zM78 153h1v2h-1zM79 153h1v2h-1zM80 153h1v2h-1zM81 153h1v2h-1zM82 153h1v2h-1zM83 153h1v2h-1zM84 153h1v2h-1zM85 153h1v2h-1zM86 153h1v2h-1zM87 153h1v2h-1zM88 154h1v1h-1z\"></path>\n <path fill=\"#5f3d2a\" d=\"M14 16h1v57h-1zM14 92h1v43h-1zM15 17h1v55h-1zM15 93h1v41h-1zM16 18h1v53h-1zM16 94h1v39h-1zM18 18h1v7h-1zM18 42h1v14h-1zM19 64h1v4h-1zM20 108h1v24h-1zM21 18h1v43h-1zM21 94h1v6h-1zM22 63h1v7h-1zM23 102h1v30h-1zM24 94h1v6h-1zM27 18h1v12h-1zM27 36h1v20h-1zM28 18h1v51h-1zM29 61h1v9h-1zM29 107h1v25h-1zM30 94h1v38h-1zM31 94h1v10h-1zM34 18h1v8h-1zM34 27h1v1h-1zM34 42h1v3h-1zM34 46h1v7h-1zM35 60h1v8h-1zM36 111h1v21h-1zM37 18h1v41h-1zM37 94h1v12h-1zM37 120h1v6h-1zM37 129h1v3h-1zM38 62h1v8h-1zM38 98h1v4h-1zM39 105h1v27h-1zM40 94h1v7h-1zM43 18h1v3h-1zM44 18h1v50h-1zM45 58h1v12h-1zM45 112h1v20h-1zM46 94h1v38h-1zM47 94h1v13h-1zM50 18h1v6h-1zM51 18h1v49h-1zM52 57h1v13h-1zM52 112h1v20h-1zM53 21h1v4h-1zM53 43h1v2h-1zM53 48h1v3h-1zM53 69h1v1h-1zM53 94h1v38h-1zM54 63h1v5h-1zM54 94h1v13h-1zM55 112h1v20h-1zM56 94h1v8h-1zM60 18h1v44h-1zM61 31h1v4h-1zM61 56h1v14h-1zM61 123h1v3h-1zM61 129h1v3h-1zM62 69h1v1h-1zM62 94h1v5h-1zM62 102h1v30h-1zM63 94h1v16h-1zM66 18h1v3h-1zM67 18h1v48h-1zM68 27h1v16h-1zM68 45h1v1h-1zM68 52h1v18h-1zM68 116h1v1h-1zM68 120h1v7h-1zM69 67h1v3h-1zM69 94h1v38h-1zM70 94h1v19h-1zM88 3h1v151h-1zM89 2h1v153h-1z\"></path>\n <path fill=\"#734b33\" d=\"M3 5h1v1h-1zM3 14h1v1h-1zM3 76h1v1h-1zM3 141h1v1h-1zM3 148h1v1h-1zM4 5h1v1h-1zM4 14h1v1h-1zM4 26h1v16h-1zM4 58h1v11h-1zM4 72h1v1h-1zM4 76h1v1h-1zM4 141h1v1h-1zM4 148h1v1h-1zM5 5h1v1h-1zM5 14h1v2h-1zM5 76h1v1h-1zM5 85h1v1h-1zM5 92h1v4h-1zM5 141h1v1h-1zM5 148h1v1h-1zM6 5h1v1h-1zM6 76h1v1h-1zM6 85h1v1h-1zM6 101h1v5h-1zM6 141h1v1h-1zM6 148h1v1h-1zM7 22h1v46h-1zM7 76h1v1h-1zM7 85h1v1h-1zM7 141h1v1h-1zM7 148h1v1h-1zM8 4h1v1h-1zM8 76h1v1h-1zM8 85h1v1h-1zM8 93h1v5h-1zM8 141h1v1h-1zM8 147h1v2h-1zM9 4h1v1h-1zM9 76h1v1h-1zM9 85h1v1h-1zM9 102h1v8h-1zM9 123h1v12h-1zM9 140h1v2h-1zM9 147h1v2h-1zM10 4h1v1h-1zM10 76h1v1h-1zM10 85h1v1h-1zM10 140h1v1h-1zM10 147h1v2h-1zM11 4h1v1h-1zM11 13h1v1h-1zM11 76h1v1h-1zM11 85h1v1h-1zM11 140h1v1h-1zM11 147h1v1h-1zM12 4h1v1h-1zM12 13h1v1h-1zM12 76h1v1h-1zM12 85h1v1h-1zM12 140h1v1h-1zM12 147h1v1h-1zM13 4h1v1h-1zM13 13h1v1h-1zM13 22h1v51h-1zM13 76h1v1h-1zM13 85h1v1h-1zM13 140h1v1h-1zM13 147h1v1h-1zM14 4h1v1h-1zM14 13h1v1h-1zM14 76h1v1h-1zM14 85h1v1h-1zM14 140h1v1h-1zM14 147h1v1h-1zM15 4h1v1h-1zM15 13h1v1h-1zM15 76h1v1h-1zM15 85h1v1h-1zM15 140h1v1h-1zM15 147h1v1h-1zM16 4h1v1h-1zM16 13h1v1h-1zM16 76h1v1h-1zM16 85h1v1h-1zM16 140h1v1h-1zM16 147h1v1h-1zM17 4h1v1h-1zM17 13h1v1h-1zM17 18h1v6h-1zM17 27h1v33h-1zM17 75h1v2h-1zM17 85h1v1h-1zM17 94h1v38h-1zM17 140h1v1h-1zM17 147h1v1h-1zM18 4h1v1h-1zM18 13h1v1h-1zM18 25h1v17h-1zM18 56h1v14h-1zM18 76h1v1h-1zM18 85h1v1h-1zM18 94h1v10h-1zM18 140h1v1h-1zM18 147h1v1h-1zM19 4h1v1h-1zM19 13h1v1h-1zM19 18h1v46h-1zM19 68h1v2h-1zM19 76h1v1h-1zM19 85h1v1h-1zM19 103h1v29h-1zM19 140h1v1h-1zM19 147h1v1h-1zM20 4h1v1h-1zM20 13h1v1h-1zM20 18h1v52h-1zM20 76h1v1h-1zM20 85h1v1h-1zM20 94h1v14h-1zM20 140h1v1h-1zM20 147h1v1h-1zM21 4h1v1h-1zM21 13h1v1h-1zM21 61h1v9h-1zM21 76h1v1h-1zM21 85h1v1h-1zM21 100h1v32h-1zM21 140h1v1h-1zM21 147h1v1h-1zM22 4h1v1h-1zM22 13h1v1h-1zM22 18h1v45h-1zM22 76h1v1h-1zM22 85h1v1h-1zM22 94h1v38h-1zM22 140h1v1h-1zM22 147h1v1h-1zM23 4h1v1h-1zM23 13h1v1h-1zM23 59h1v11h-1zM23 76h1v1h-1zM23 85h1v1h-1zM23 94h1v8h-1zM23 140h1v1h-1zM23 147h1v1h-1zM24 4h1v1h-1zM24 13h1v1h-1zM24 18h1v43h-1zM24 76h1v1h-1zM24 85h1v1h-1zM24 100h1v32h-1zM24 140h1v1h-1zM24 147h1v1h-1zM25 4h1v1h-1zM25 13h1v1h-1zM25 18h1v52h-1zM25 76h1v1h-1zM25 85h1v1h-1zM25 94h1v12h-1zM25 140h1v1h-1zM25 147h1v1h-1zM26 4h1v1h-1zM26 13h1v1h-1zM26 18h1v52h-1zM26 76h1v1h-1zM26 85h1v1h-1zM26 102h1v30h-1zM26 140h1v1h-1zM26 147h1v1h-1zM27 4h1v1h-1zM27 13h1v1h-1zM27 30h1v6h-1zM27 56h1v14h-1zM27 76h1v1h-1zM27 85h1v1h-1zM27 94h1v38h-1zM27 140h1v1h-1zM27 147h1v2h-1zM28 4h1v1h-1zM28 13h1v1h-1zM28 69h1v1h-1zM28 76h1v1h-1zM28 85h1v1h-1zM28 94h1v38h-1zM28 140h1v1h-1zM28 147h1v2h-1zM29 4h1v1h-1zM29 18h1v43h-1zM29 76h1v1h-1zM29 85h1v1h-1zM29 94h1v13h-1zM29 140h1v1h-1zM29 147h1v2h-1zM30 4h1v1h-1zM30 18h1v52h-1zM30 76h1v1h-1zM30 85h1v1h-1zM30 140h1v2h-1zM30 147h1v2h-1zM31 4h1v1h-1zM31 18h1v52h-1zM31 76h1v1h-1zM31 85h1v1h-1zM31 104h1v28h-1zM31 141h1v1h-1zM31 147h1v2h-1zM32 4h1v1h-1zM32 57h1v13h-1zM32 76h1v1h-1zM32 85h1v1h-1zM32 94h1v38h-1zM32 141h1v1h-1zM32 147h1v2h-1zM33 4h1v1h-1zM33 18h1v6h-1zM33 30h1v6h-1zM33 37h1v8h-1zM33 48h1v13h-1zM33 76h1v1h-1zM33 85h1v1h-1zM33 94h1v38h-1zM33 141h1v1h-1zM33 147h1v2h-1zM34 26h1v1h-1zM34 28h1v14h-1zM34 45h1v1h-1zM34 53h1v17h-1zM34 76h1v1h-1zM34 85h1v1h-1zM34 94h1v13h-1zM34 141h1v1h-1zM34 147h1v2h-1zM35 18h1v42h-1zM35 68h1v2h-1zM35 76h1v1h-1zM35 85h1v1h-1zM35 101h1v31h-1zM35 141h1v1h-1zM35 147h1v2h-1zM36 18h1v52h-1zM36 76h1v1h-1zM36 85h1v1h-1zM36 94h1v17h-1zM36 141h1v1h-1zM36 147h1v2h-1zM37 59h1v11h-1zM37 76h1v1h-1zM37 85h1v1h-1zM37 106h1v14h-1zM37 126h1v3h-1zM37 141h1v1h-1zM37 147h1v2h-1zM38 18h1v44h-1zM38 76h1v1h-1zM38 85h1v1h-1zM38 94h1v4h-1zM38 102h1v30h-1zM38 141h1v1h-1zM38 148h1v1h-1zM39 56h1v1h-1zM39 58h1v12h-1zM39 76h1v1h-1zM39 85h1v1h-1zM39 94h1v11h-1zM39 141h1v1h-1zM39 148h1v1h-1zM40 5h1v1h-1zM40 18h1v40h-1zM40 69h1v1h-1zM40 76h1v1h-1zM40 85h1v1h-1zM40 101h1v31h-1zM40 141h1v1h-1zM40 147h1v2h-1zM41 5h1v1h-1zM41 18h1v52h-1zM41 76h1v1h-1zM41 85h1v1h-1zM41 94h1v14h-1zM41 141h1v1h-1zM41 147h1v2h-1zM42 5h1v1h-1zM42 18h1v52h-1zM42 76h1v1h-1zM42 85h1v1h-1zM42 105h1v27h-1zM42 141h1v1h-1zM42 148h1v1h-1zM43 5h1v1h-1zM43 21h1v49h-1zM43 76h1v1h-1zM43 85h1v1h-1zM43 94h1v38h-1zM43 141h1v1h-1zM43 148h1v1h-1zM44 4h1v2h-1zM44 68h1v2h-1zM44 76h1v1h-1zM44 85h1v1h-1zM44 94h1v38h-1zM44 141h1v1h-1zM44 147h1v2h-1zM45 18h1v40h-1zM45 76h1v1h-1zM45 85h1v1h-1zM45 94h1v18h-1zM45 141h1v1h-1zM45 148h1v1h-1zM46 18h1v52h-1zM46 76h1v1h-1zM46 85h1v1h-1zM46 141h1v1h-1zM46 147h1v2h-1zM47 18h1v52h-1zM47 76h1v1h-1zM47 85h1v1h-1zM47 107h1v25h-1zM47 141h1v1h-1zM47 147h1v2h-1zM48 4h1v1h-1zM48 27h1v21h-1zM48 51h1v19h-1zM48 76h1v1h-1zM48 85h1v1h-1zM48 94h1v38h-1zM48 141h1v1h-1zM48 147h1v2h-1zM49 4h1v1h-1zM49 18h1v3h-1zM49 22h1v2h-1zM49 27h1v35h-1zM49 68h1v2h-1zM49 76h1v1h-1zM49 85h1v1h-1zM49 94h1v38h-1zM49 141h1v1h-1zM49 147h1v2h-1zM50 24h1v41h-1zM50 66h1v4h-1zM50 76h1v1h-1zM50 85h1v1h-1zM50 94h1v20h-1zM50 128h1v4h-1zM50 141h1v1h-1zM50 147h1v2h-1zM51 4h1v1h-1zM51 67h1v3h-1zM51 76h1v1h-1zM51 85h1v1h-1zM51 96h1v36h-1zM51 141h1v1h-1zM51 147h1v2h-1zM52 4h1v1h-1zM52 18h1v39h-1zM52 76h1v1h-1zM52 85h1v1h-1zM52 94h1v18h-1zM52 140h1v2h-1zM52 147h1v2h-1zM53 4h1v1h-1zM53 18h1v3h-1zM53 25h1v18h-1zM53 45h1v3h-1zM53 51h1v18h-1zM53 76h1v1h-1zM53 85h1v1h-1zM53 140h1v2h-1zM53 147h1v2h-1zM54 4h1v1h-1zM54 18h1v45h-1zM54 68h1v2h-1zM54 76h1v1h-1zM54 85h1v1h-1zM54 107h1v25h-1zM54 147h1v2h-1zM55 4h1v1h-1zM55 29h1v14h-1zM55 53h1v1h-1zM55 55h1v15h-1zM55 76h1v1h-1zM55 85h1v1h-1zM55 94h1v18h-1zM55 140h1v1h-1zM55 147h1v2h-1zM56 4h1v1h-1zM56 13h1v1h-1zM56 18h1v12h-1zM56 38h1v1h-1zM56 42h1v13h-1zM56 68h1v2h-1zM56 76h1v1h-1zM56 85h1v1h-1zM56 102h1v30h-1zM56 140h1v2h-1zM56 147h1v2h-1zM57 4h1v1h-1zM57 13h1v1h-1zM57 18h1v51h-1zM57 76h1v1h-1zM57 85h1v1h-1zM57 94h1v19h-1zM57 140h1v1h-1zM57 147h1v2h-1zM58 4h1v1h-1zM58 13h1v1h-1zM58 18h1v52h-1zM58 76h1v1h-1zM58 85h1v1h-1zM58 108h1v24h-1zM58 140h1v1h-1zM58 147h1v2h-1zM59 4h1v1h-1zM59 13h1v1h-1zM59 18h1v52h-1zM59 76h1v1h-1zM59 85h1v1h-1zM59 94h1v38h-1zM59 140h1v1h-1zM59 147h1v2h-1zM60 4h1v1h-1zM60 62h1v8h-1zM60 76h1v1h-1zM60 85h1v1h-1zM60 94h1v38h-1zM60 140h1v1h-1zM60 147h1v2h-1zM61 4h1v1h-1zM61 18h1v13h-1zM61 35h1v21h-1zM61 76h1v1h-1zM61 85h1v1h-1zM61 94h1v29h-1zM61 126h1v3h-1zM61 140h1v1h-1zM61 147h1v2h-1zM62 4h1v1h-1zM62 13h1v1h-1zM62 18h1v51h-1zM62 75h1v2h-1zM62 82h1v1h-1zM62 85h1v1h-1zM62 99h1v3h-1zM62 140h1v2h-1zM62 147h1v2h-1zM63 4h1v1h-1zM63 13h1v1h-1zM63 18h1v52h-1zM63 76h1v1h-1zM63 85h1v1h-1zM63 110h1v22h-1zM63 140h1v2h-1zM63 147h1v2h-1zM64 4h1v1h-1zM64 18h1v52h-1zM64 76h1v1h-1zM64 85h1v1h-1zM64 94h1v38h-1zM64 140h1v1h-1zM64 147h1v2h-1zM65 4h1v1h-1zM65 18h1v52h-1zM65 76h1v1h-1zM65 85h1v1h-1zM65 94h1v6h-1zM65 102h1v30h-1zM65 141h1v1h-1zM65 147h1v2h-1zM66 4h1v1h-1zM66 21h1v49h-1zM66 76h1v1h-1zM66 85h1v1h-1zM66 94h1v38h-1zM66 141h1v1h-1zM66 147h1v2h-1zM67 4h1v1h-1zM67 66h1v4h-1zM67 76h1v1h-1zM67 85h1v1h-1zM67 94h1v38h-1zM67 141h1v1h-1zM67 147h1v2h-1zM68 4h1v1h-1zM68 18h1v9h-1zM68 43h1v2h-1zM68 46h1v6h-1zM68 76h1v1h-1zM68 85h1v1h-1zM68 94h1v22h-1zM68 117h1v3h-1zM68 127h1v5h-1zM68 141h1v1h-1zM68 147h1v2h-1zM69 4h1v2h-1zM69 14h1v1h-1zM69 18h1v49h-1zM69 76h1v1h-1zM69 85h1v1h-1zM69 141h1v1h-1zM69 147h1v2h-1zM70 5h1v1h-1zM70 14h1v1h-1zM70 18h1v52h-1zM70 76h1v1h-1zM70 85h1v1h-1zM70 113h1v19h-1zM70 141h1v1h-1zM70 147h1v2h-1zM71 5h1v1h-1zM71 14h1v1h-1zM71 23h1v47h-1zM71 76h1v1h-1zM71 85h1v1h-1zM71 94h1v38h-1zM71 141h1v1h-1zM71 147h1v2h-1zM72 5h1v1h-1zM72 14h1v1h-1zM72 18h1v6h-1zM72 46h1v5h-1zM72 66h1v4h-1zM72 76h1v1h-1zM72 94h1v38h-1zM72 141h1v1h-1zM72 148h1v1h-1zM73 5h1v1h-1zM73 14h1v1h-1zM73 18h1v50h-1zM73 76h1v1h-1zM73 94h1v24h-1zM73 123h1v9h-1zM73 141h1v1h-1zM73 148h1v1h-1zM74 5h1v1h-1zM74 14h1v1h-1zM74 76h1v2h-1zM74 86h1v1h-1zM74 141h1v1h-1zM74 148h1v1h-1zM75 5h1v1h-1zM75 14h1v1h-1zM75 76h1v2h-1zM75 83h1v1h-1zM75 86h1v1h-1zM75 141h1v1h-1zM75 148h1v1h-1zM76 5h1v1h-1zM76 14h1v1h-1zM76 76h1v2h-1zM76 83h1v1h-1zM76 86h1v1h-1zM76 141h1v1h-1zM76 148h1v1h-1zM77 5h1v1h-1zM77 14h1v1h-1zM77 76h1v2h-1zM77 83h1v1h-1zM77 86h1v1h-1zM77 95h1v5h-1zM77 141h1v1h-1zM77 148h1v2h-1zM78 5h1v1h-1zM78 14h1v1h-1zM78 64h1v2h-1zM78 67h1v6h-1zM78 76h1v2h-1zM78 83h1v1h-1zM78 86h1v1h-1zM78 104h1v31h-1zM78 141h1v2h-1zM78 148h1v2h-1zM79 5h1v1h-1zM79 17h1v8h-1zM79 50h1v6h-1zM79 77h1v1h-1zM79 86h1v1h-1zM79 91h1v3h-1zM79 142h1v1h-1zM79 148h1v2h-1zM80 5h1v2h-1zM80 77h1v1h-1zM80 86h1v1h-1zM80 95h1v9h-1zM80 142h1v1h-1zM80 148h1v2h-1zM81 5h1v2h-1zM81 77h1v1h-1zM81 86h1v1h-1zM81 106h1v29h-1zM81 139h1v1h-1zM81 142h1v1h-1zM81 148h1v2h-1zM82 6h1v1h-1zM82 77h1v1h-1zM82 142h1v1h-1zM82 149h1v1h-1zM83 6h1v1h-1zM83 77h1v2h-1zM83 142h1v1h-1zM83 149h1v1h-1zM84 6h1v1h-1zM84 68h1v1h-1zM84 70h1v3h-1zM84 77h1v2h-1zM84 87h1v1h-1zM84 142h1v1h-1zM84 149h1v1h-1zM85 6h1v1h-1zM85 17h1v56h-1zM85 77h1v2h-1zM85 87h1v1h-1zM85 91h1v2h-1zM85 142h1v1h-1zM85 149h1v1h-1zM86 6h1v1h-1zM86 15h1v14h-1zM86 38h1v1h-1zM86 41h1v21h-1zM86 77h1v2h-1zM86 84h1v1h-1zM86 87h1v1h-1zM86 91h1v11h-1zM86 142h1v1h-1zM86 149h1v2h-1zM87 6h1v1h-1zM87 77h1v2h-1zM87 84h1v1h-1zM87 87h1v1h-1zM87 94h1v41h-1zM87 142h1v2h-1zM87 149h1v2h-1z\"></path>\n <path fill=\"#87593c\" d=\"M3 4h1v1h-1zM3 6h1v3h-1zM3 11h1v3h-1zM3 20h1v56h-1zM3 77h1v4h-1zM3 82h1v6h-1zM3 89h1v2h-1zM3 100h1v36h-1zM3 138h1v3h-1zM3 142h1v1h-1zM3 144h1v2h-1zM3 147h1v1h-1zM3 149h1v3h-1zM4 4h1v1h-1zM4 6h1v3h-1zM4 11h1v3h-1zM4 15h1v11h-1zM4 42h1v16h-1zM4 69h1v3h-1zM4 73h1v3h-1zM4 77h1v3h-1zM4 82h1v6h-1zM4 89h1v10h-1zM4 135h1v1h-1zM4 138h1v3h-1zM4 142h1v1h-1zM4 144h1v2h-1zM4 147h1v1h-1zM4 149h1v3h-1zM5 3h1v2h-1zM5 6h1v3h-1zM5 11h1v3h-1zM5 16h1v58h-1zM5 75h1v1h-1zM5 77h1v3h-1zM5 82h1v3h-1zM5 86h1v2h-1zM5 89h1v3h-1zM5 96h1v17h-1zM5 122h1v14h-1zM5 138h1v3h-1zM5 142h1v1h-1zM5 144h1v4h-1zM5 149h1v3h-1zM6 3h1v2h-1zM6 6h1v3h-1zM6 11h1v63h-1zM6 75h1v1h-1zM6 77h1v3h-1zM6 82h1v3h-1zM6 86h1v1h-1zM6 88h1v13h-1zM6 106h1v30h-1zM6 137h1v4h-1zM6 142h1v1h-1zM6 144h1v4h-1zM6 149h1v3h-1zM7 3h1v6h-1zM7 10h1v12h-1zM7 68h1v6h-1zM7 75h1v1h-1zM7 77h1v3h-1zM7 82h1v3h-1zM7 86h1v1h-1zM7 88h1v48h-1zM7 137h1v4h-1zM7 142h1v1h-1zM7 144h1v4h-1zM7 149h1v3h-1zM8 3h1v1h-1zM8 5h1v4h-1zM8 10h1v20h-1zM8 39h1v30h-1zM8 73h1v1h-1zM8 75h1v1h-1zM8 77h1v3h-1zM8 82h1v3h-1zM8 86h1v1h-1zM8 88h1v5h-1zM8 98h1v38h-1zM8 137h1v4h-1zM8 142h1v1h-1zM8 144h1v3h-1zM8 149h1v3h-1zM9 3h1v1h-1zM9 5h1v3h-1zM9 10h1v5h-1zM9 73h1v1h-1zM9 75h1v1h-1zM9 77h1v3h-1zM9 82h1v3h-1zM9 86h1v1h-1zM9 88h1v3h-1zM9 94h1v8h-1zM9 110h1v13h-1zM9 135h1v1h-1zM9 137h1v3h-1zM9 142h1v1h-1zM9 144h1v3h-1zM9 149h1v3h-1zM10 3h1v1h-1zM10 5h1v3h-1zM10 10h1v5h-1zM10 20h1v54h-1zM10 75h1v1h-1zM10 77h1v3h-1zM10 82h1v3h-1zM10 86h1v1h-1zM10 88h1v2h-1zM10 102h1v34h-1zM10 137h1v3h-1zM10 141h1v6h-1zM10 149h1v3h-1zM11 3h1v1h-1zM11 5h1v3h-1zM11 10h1v3h-1zM11 14h1v60h-1zM11 75h1v1h-1zM11 77h1v3h-1zM11 82h1v3h-1zM11 86h1v1h-1zM11 88h1v11h-1zM11 112h1v2h-1zM11 117h1v3h-1zM11 137h1v3h-1zM11 141h1v6h-1zM11 148h1v4h-1zM12 3h1v1h-1zM12 5h1v3h-1zM12 10h1v3h-1zM12 14h1v62h-1zM12 77h1v3h-1zM12 82h1v3h-1zM12 86h1v1h-1zM12 88h1v25h-1zM12 122h1v13h-1zM12 137h1v3h-1zM12 141h1v6h-1zM12 148h1v4h-1zM13 3h1v1h-1zM13 5h1v3h-1zM13 10h1v3h-1zM13 14h1v8h-1zM13 73h1v3h-1zM13 77h1v3h-1zM13 82h1v3h-1zM13 86h1v1h-1zM13 88h1v47h-1zM13 137h1v3h-1zM13 141h1v6h-1zM13 148h1v4h-1zM14 3h1v1h-1zM14 5h1v3h-1zM14 10h1v3h-1zM14 14h1v1h-1zM14 73h1v3h-1zM14 77h1v3h-1zM14 82h1v3h-1zM14 86h1v1h-1zM14 88h1v3h-1zM14 137h1v3h-1zM14 141h1v6h-1zM14 148h1v4h-1zM15 3h1v1h-1zM15 5h1v3h-1zM15 10h1v3h-1zM15 14h1v1h-1zM15 73h1v3h-1zM15 77h1v3h-1zM15 82h1v3h-1zM15 86h1v1h-1zM15 88h1v3h-1zM15 137h1v3h-1zM15 141h1v6h-1zM15 148h1v3h-1zM16 3h1v1h-1zM16 5h1v3h-1zM16 10h1v3h-1zM16 14h1v1h-1zM16 73h1v3h-1zM16 77h1v3h-1zM16 82h1v3h-1zM16 86h1v1h-1zM16 88h1v3h-1zM16 137h1v3h-1zM16 141h1v6h-1zM16 148h1v4h-1zM17 3h1v1h-1zM17 5h1v3h-1zM17 10h1v3h-1zM17 14h1v1h-1zM17 24h1v3h-1zM17 60h1v10h-1zM17 73h1v2h-1zM17 77h1v3h-1zM17 82h1v3h-1zM17 86h1v1h-1zM17 88h1v3h-1zM17 137h1v3h-1zM17 141h1v6h-1zM17 148h1v3h-1zM18 3h1v1h-1zM18 5h1v3h-1zM18 10h1v3h-1zM18 14h1v1h-1zM18 73h1v3h-1zM18 77h1v3h-1zM18 82h1v3h-1zM18 86h1v1h-1zM18 88h1v3h-1zM18 104h1v28h-1zM18 137h1v3h-1zM18 141h1v1h-1zM18 143h1v2h-1zM18 146h1v1h-1zM18 148h1v3h-1zM19 3h1v1h-1zM19 5h1v3h-1zM19 10h1v3h-1zM19 14h1v1h-1zM19 73h1v3h-1zM19 77h1v3h-1zM19 82h1v3h-1zM19 86h1v1h-1zM19 88h1v3h-1zM19 94h1v9h-1zM19 137h1v3h-1zM19 141h1v1h-1zM19 143h1v2h-1zM19 146h1v1h-1zM19 148h1v3h-1zM20 3h1v1h-1zM20 5h1v3h-1zM20 10h1v3h-1zM20 14h1v1h-1zM20 73h1v3h-1zM20 77h1v3h-1zM20 82h1v3h-1zM20 86h1v1h-1zM20 88h1v3h-1zM20 137h1v3h-1zM20 141h1v1h-1zM20 143h1v2h-1zM20 146h1v1h-1zM20 148h1v3h-1zM21 3h1v1h-1zM21 5h1v3h-1zM21 10h1v3h-1zM21 14h1v1h-1zM21 73h1v3h-1zM21 77h1v3h-1zM21 82h1v3h-1zM21 86h1v1h-1zM21 88h1v3h-1zM21 137h1v3h-1zM21 141h1v1h-1zM21 143h1v4h-1zM21 148h1v3h-1zM22 3h1v1h-1zM22 5h1v3h-1zM22 10h1v3h-1zM22 14h1v1h-1zM22 73h1v3h-1zM22 77h1v3h-1zM22 82h1v3h-1zM22 86h1v1h-1zM22 88h1v3h-1zM22 137h1v3h-1zM22 141h1v6h-1zM22 148h1v4h-1zM23 3h1v1h-1zM23 5h1v3h-1zM23 10h1v3h-1zM23 14h1v1h-1zM23 18h1v41h-1zM23 73h1v3h-1zM23 77h1v3h-1zM23 82h1v3h-1zM23 86h1v1h-1zM23 88h1v3h-1zM23 137h1v3h-1zM23 141h1v6h-1zM23 148h1v4h-1zM24 3h1v1h-1zM24 5h1v3h-1zM24 10h1v3h-1zM24 14h1v1h-1zM24 61h1v9h-1zM24 73h1v3h-1zM24 77h1v3h-1zM24 82h1v3h-1zM24 86h1v1h-1zM24 88h1v3h-1zM24 137h1v3h-1zM24 141h1v6h-1zM24 148h1v4h-1zM25 3h1v1h-1zM25 5h1v3h-1zM25 10h1v3h-1zM25 14h1v1h-1zM25 73h1v3h-1zM25 77h1v3h-1zM25 82h1v3h-1zM25 86h1v1h-1zM25 88h1v3h-1zM25 106h1v26h-1zM25 137h1v3h-1zM25 141h1v6h-1zM25 148h1v4h-1zM26 3h1v1h-1zM26 5h1v3h-1zM26 10h1v3h-1zM26 14h1v1h-1zM26 73h1v3h-1zM26 77h1v3h-1zM26 82h1v3h-1zM26 86h1v1h-1zM26 88h1v3h-1zM26 94h1v8h-1zM26 137h1v3h-1zM26 141h1v6h-1zM26 148h1v4h-1zM27 3h1v1h-1zM27 5h1v3h-1zM27 10h1v3h-1zM27 14h1v1h-1zM27 73h1v1h-1zM27 75h1v1h-1zM27 77h1v3h-1zM27 82h1v3h-1zM27 86h1v1h-1zM27 88h1v3h-1zM27 137h1v3h-1zM27 141h1v6h-1zM27 149h1v3h-1zM28 3h1v1h-1zM28 5h1v3h-1zM28 10h1v3h-1zM28 14h1v1h-1zM28 73h1v1h-1zM28 75h1v1h-1zM28 77h1v3h-1zM28 82h1v3h-1zM28 86h1v1h-1zM28 88h1v3h-1zM28 135h1v1h-1zM28 137h1v3h-1zM28 141h1v6h-1zM28 149h1v3h-1zM29 3h1v1h-1zM29 5h1v3h-1zM29 10h1v5h-1zM29 73h1v1h-1zM29 75h1v1h-1zM29 77h1v3h-1zM29 82h1v3h-1zM29 86h1v1h-1zM29 88h1v3h-1zM29 135h1v1h-1zM29 137h1v3h-1zM29 141h1v6h-1zM29 149h1v3h-1zM30 3h1v1h-1zM30 5h1v3h-1zM30 10h1v5h-1zM30 73h1v1h-1zM30 75h1v1h-1zM30 77h1v3h-1zM30 82h1v3h-1zM30 86h1v1h-1zM30 88h1v3h-1zM30 135h1v1h-1zM30 137h1v3h-1zM30 142h1v5h-1zM30 149h1v3h-1zM31 3h1v1h-1zM31 5h1v3h-1zM31 10h1v5h-1zM31 73h1v1h-1zM31 75h1v1h-1zM31 77h1v3h-1zM31 82h1v3h-1zM31 86h1v1h-1zM31 88h1v3h-1zM31 135h1v1h-1zM31 137h1v4h-1zM31 142h1v1h-1zM31 144h1v3h-1zM31 149h1v3h-1zM32 3h1v1h-1zM32 5h1v3h-1zM32 10h1v5h-1zM32 18h1v39h-1zM32 73h1v1h-1zM32 75h1v1h-1zM32 77h1v3h-1zM32 82h1v3h-1zM32 86h1v1h-1zM32 88h1v3h-1zM32 135h1v1h-1zM32 137h1v4h-1zM32 142h1v1h-1zM32 144h1v3h-1zM32 149h1v3h-1zM33 3h1v1h-1zM33 5h1v4h-1zM33 10h1v5h-1zM33 24h1v6h-1zM33 36h1v1h-1zM33 45h1v3h-1zM33 61h1v9h-1zM33 73h1v1h-1zM33 75h1v1h-1zM33 77h1v3h-1zM33 82h1v3h-1zM33 86h1v1h-1zM33 88h1v3h-1zM33 135h1v1h-1zM33 137h1v4h-1zM33 142h1v1h-1zM33 144h1v3h-1zM33 149h1v3h-1zM34 3h1v6h-1zM34 10h1v5h-1zM34 73h1v1h-1zM34 75h1v1h-1zM34 77h1v3h-1zM34 82h1v3h-1zM34 86h1v1h-1zM34 88h1v3h-1zM34 107h1v25h-1zM34 135h1v1h-1zM34 137h1v4h-1zM34 142h1v1h-1zM34 144h1v3h-1zM34 149h1v3h-1zM35 3h1v6h-1zM35 10h1v5h-1zM35 73h1v1h-1zM35 75h1v1h-1zM35 77h1v3h-1zM35 82h1v3h-1zM35 86h1v1h-1zM35 88h1v3h-1zM35 94h1v7h-1zM35 135h1v1h-1zM35 137h1v4h-1zM35 142h1v1h-1zM35 144h1v3h-1zM35 149h1v3h-1zM36 3h1v6h-1zM36 10h1v5h-1zM36 73h1v1h-1zM36 75h1v1h-1zM36 77h1v3h-1zM36 82h1v3h-1zM36 86h1v1h-1zM36 88h1v3h-1zM36 135h1v1h-1zM36 137h1v4h-1zM36 142h1v1h-1zM36 144h1v3h-1zM36 149h1v3h-1zM37 3h1v6h-1zM37 10h1v5h-1zM37 73h1v1h-1zM37 75h1v1h-1zM37 77h1v3h-1zM37 82h1v3h-1zM37 86h1v1h-1zM37 88h1v3h-1zM37 135h1v1h-1zM37 137h1v4h-1zM37 142h1v1h-1zM37 144h1v3h-1zM37 149h1v3h-1zM38 3h1v6h-1zM38 10h1v5h-1zM38 73h1v1h-1zM38 75h1v1h-1zM38 77h1v3h-1zM38 82h1v3h-1zM38 86h1v1h-1zM38 88h1v3h-1zM38 135h1v1h-1zM38 137h1v4h-1zM38 142h1v1h-1zM38 144h1v4h-1zM38 149h1v3h-1zM39 3h1v6h-1zM39 10h1v5h-1zM39 18h1v38h-1zM39 57h1v1h-1zM39 73h1v1h-1zM39 75h1v1h-1zM39 77h1v3h-1zM39 82h1v3h-1zM39 86h1v1h-1zM39 88h1v3h-1zM39 135h1v1h-1zM39 137h1v4h-1zM39 142h1v1h-1zM39 144h1v4h-1zM39 149h1v3h-1zM40 3h1v2h-1zM40 6h1v3h-1zM40 10h1v5h-1zM40 58h1v11h-1zM40 73h1v1h-1zM40 75h1v1h-1zM40 77h1v3h-1zM40 82h1v3h-1zM40 86h1v1h-1zM40 88h1v3h-1zM40 135h1v1h-1zM40 137h1v4h-1zM40 142h1v1h-1zM40 144h1v3h-1zM40 149h1v3h-1zM41 3h1v2h-1zM41 6h1v3h-1zM41 10h1v5h-1zM41 73h1v1h-1zM41 75h1v1h-1zM41 77h1v3h-1zM41 82h1v3h-1zM41 86h1v1h-1zM41 88h1v3h-1zM41 108h1v24h-1zM41 135h1v1h-1zM41 137h1v4h-1zM41 142h1v1h-1zM41 144h1v3h-1zM41 149h1v3h-1zM42 3h1v2h-1zM42 6h1v3h-1zM42 11h1v4h-1zM42 73h1v1h-1zM42 75h1v1h-1zM42 77h1v3h-1zM42 82h1v3h-1zM42 86h1v1h-1zM42 88h1v3h-1zM42 94h1v11h-1zM42 135h1v1h-1zM42 137h1v4h-1zM42 142h1v1h-1zM42 144h1v4h-1zM42 149h1v3h-1zM43 3h1v2h-1zM43 6h1v3h-1zM43 10h1v5h-1zM43 73h1v1h-1zM43 75h1v1h-1zM43 77h1v3h-1zM43 82h1v3h-1zM43 86h1v1h-1zM43 88h1v3h-1zM43 135h1v1h-1zM43 137h1v4h-1zM43 142h1v1h-1zM43 144h1v4h-1zM43 149h1v3h-1zM44 3h1v1h-1zM44 6h1v3h-1zM44 10h1v5h-1zM44 73h1v1h-1zM44 75h1v1h-1zM44 77h1v3h-1zM44 82h1v3h-1zM44 86h1v1h-1zM44 88h1v3h-1zM44 135h1v1h-1zM44 137h1v4h-1zM44 142h1v1h-1zM44 144h1v3h-1zM44 149h1v3h-1zM45 3h1v6h-1zM45 10h1v5h-1zM45 73h1v1h-1zM45 75h1v1h-1zM45 77h1v3h-1zM45 82h1v3h-1zM45 86h1v1h-1zM45 88h1v3h-1zM45 135h1v1h-1zM45 137h1v4h-1zM45 142h1v1h-1zM45 144h1v4h-1zM45 149h1v3h-1zM46 3h1v6h-1zM46 10h1v5h-1zM46 73h1v1h-1zM46 75h1v1h-1zM46 77h1v3h-1zM46 82h1v3h-1zM46 86h1v1h-1zM46 88h1v3h-1zM46 135h1v1h-1zM46 137h1v4h-1zM46 142h1v1h-1zM46 144h1v3h-1zM46 149h1v3h-1zM47 3h1v6h-1zM47 10h1v5h-1zM47 73h1v1h-1zM47 75h1v1h-1zM47 77h1v3h-1zM47 82h1v3h-1zM47 86h1v1h-1zM47 88h1v3h-1zM47 135h1v1h-1zM47 137h1v4h-1zM47 142h1v1h-1zM47 144h1v3h-1zM47 149h1v3h-1zM48 3h1v1h-1zM48 5h1v4h-1zM48 10h1v5h-1zM48 18h1v9h-1zM48 48h1v3h-1zM48 73h1v1h-1zM48 75h1v1h-1zM48 77h1v3h-1zM48 82h1v3h-1zM48 86h1v1h-1zM48 88h1v3h-1zM48 135h1v1h-1zM48 137h1v4h-1zM48 142h1v1h-1zM48 144h1v3h-1zM48 149h1v3h-1zM49 3h1v1h-1zM49 5h1v4h-1zM49 10h1v5h-1zM49 21h1v1h-1zM49 24h1v3h-1zM49 62h1v6h-1zM49 73h1v1h-1zM49 75h1v1h-1zM49 77h1v3h-1zM49 82h1v3h-1zM49 86h1v1h-1zM49 88h1v3h-1zM49 135h1v1h-1zM49 137h1v4h-1zM49 142h1v1h-1zM49 144h1v3h-1zM49 149h1v3h-1zM50 3h1v6h-1zM50 10h1v5h-1zM50 65h1v1h-1zM50 73h1v1h-1zM50 75h1v1h-1zM50 77h1v3h-1zM50 82h1v3h-1zM50 86h1v1h-1zM50 88h1v3h-1zM50 114h1v14h-1zM50 135h1v1h-1zM50 137h1v4h-1zM50 142h1v1h-1zM50 144h1v3h-1zM50 149h1v3h-1zM51 3h1v1h-1zM51 5h1v4h-1zM51 10h1v5h-1zM51 73h1v1h-1zM51 75h1v1h-1zM51 77h1v3h-1zM51 82h1v3h-1zM51 86h1v1h-1zM51 88h1v3h-1zM51 94h1v2h-1zM51 135h1v1h-1zM51 137h1v4h-1zM51 142h1v1h-1zM51 144h1v3h-1zM51 149h1v3h-1zM52 3h1v1h-1zM52 5h1v4h-1zM52 10h1v5h-1zM52 73h1v1h-1zM52 75h1v1h-1zM52 77h1v3h-1zM52 82h1v3h-1zM52 86h1v1h-1zM52 88h1v3h-1zM52 135h1v1h-1zM52 137h1v3h-1zM52 142h1v5h-1zM52 149h1v3h-1zM53 3h1v1h-1zM53 5h1v4h-1zM53 10h1v5h-1zM53 73h1v1h-1zM53 75h1v1h-1zM53 77h1v3h-1zM53 82h1v3h-1zM53 86h1v1h-1zM53 88h1v3h-1zM53 135h1v1h-1zM53 137h1v3h-1zM53 142h1v5h-1zM53 149h1v3h-1zM54 3h1v1h-1zM54 5h1v3h-1zM54 10h1v5h-1zM54 73h1v1h-1zM54 75h1v1h-1zM54 77h1v3h-1zM54 82h1v3h-1zM54 86h1v1h-1zM54 88h1v3h-1zM54 135h1v1h-1zM54 137h1v6h-1zM54 144h1v3h-1zM54 149h1v3h-1zM55 3h1v1h-1zM55 5h1v3h-1zM55 10h1v5h-1zM55 18h1v11h-1zM55 43h1v10h-1zM55 54h1v1h-1zM55 73h1v1h-1zM55 75h1v1h-1zM55 77h1v3h-1zM55 82h1v3h-1zM55 86h1v1h-1zM55 88h1v3h-1zM55 135h1v1h-1zM55 137h1v3h-1zM55 141h1v6h-1zM55 149h1v3h-1zM56 3h1v1h-1zM56 5h1v3h-1zM56 10h1v3h-1zM56 14h1v1h-1zM56 30h1v8h-1zM56 39h1v3h-1zM56 55h1v13h-1zM56 73h1v3h-1zM56 77h1v3h-1zM56 82h1v3h-1zM56 86h1v1h-1zM56 88h1v3h-1zM56 135h1v1h-1zM56 137h1v3h-1zM56 142h1v5h-1zM56 149h1v3h-1zM57 3h1v1h-1zM57 5h1v3h-1zM57 10h1v3h-1zM57 14h1v1h-1zM57 69h1v1h-1zM57 73h1v1h-1zM57 75h1v1h-1zM57 77h1v3h-1zM57 82h1v3h-1zM57 86h1v1h-1zM57 88h1v3h-1zM57 113h1v19h-1zM57 135h1v1h-1zM57 137h1v3h-1zM57 141h1v6h-1zM57 149h1v3h-1zM58 3h1v1h-1zM58 5h1v3h-1zM58 10h1v3h-1zM58 14h1v1h-1zM58 73h1v1h-1zM58 75h1v1h-1zM58 77h1v3h-1zM58 82h1v3h-1zM58 86h1v1h-1zM58 88h1v3h-1zM58 94h1v14h-1zM58 135h1v1h-1zM58 137h1v3h-1zM58 141h1v6h-1zM58 149h1v3h-1zM59 3h1v1h-1zM59 5h1v3h-1zM59 10h1v3h-1zM59 14h1v1h-1zM59 73h1v1h-1zM59 75h1v1h-1zM59 77h1v3h-1zM59 82h1v3h-1zM59 86h1v1h-1zM59 88h1v3h-1zM59 135h1v1h-1zM59 137h1v3h-1zM59 141h1v6h-1zM59 149h1v3h-1zM60 3h1v1h-1zM60 5h1v3h-1zM60 10h1v5h-1zM60 73h1v1h-1zM60 75h1v1h-1zM60 77h1v3h-1zM60 82h1v3h-1zM60 86h1v1h-1zM60 88h1v3h-1zM60 135h1v1h-1zM60 137h1v3h-1zM60 141h1v6h-1zM60 149h1v3h-1zM61 3h1v1h-1zM61 5h1v3h-1zM61 10h1v5h-1zM61 73h1v1h-1zM61 75h1v1h-1zM61 77h1v3h-1zM61 82h1v3h-1zM61 86h1v1h-1zM61 88h1v3h-1zM61 135h1v1h-1zM61 137h1v3h-1zM61 141h1v6h-1zM61 149h1v3h-1zM62 3h1v1h-1zM62 5h1v3h-1zM62 10h1v3h-1zM62 14h1v1h-1zM62 73h1v2h-1zM62 77h1v3h-1zM62 83h1v2h-1zM62 86h1v1h-1zM62 88h1v3h-1zM62 135h1v1h-1zM62 137h1v3h-1zM62 142h1v5h-1zM62 149h1v3h-1zM63 3h1v1h-1zM63 5h1v3h-1zM63 10h1v3h-1zM63 14h1v1h-1zM63 73h1v1h-1zM63 75h1v1h-1zM63 77h1v3h-1zM63 82h1v3h-1zM63 86h1v1h-1zM63 88h1v3h-1zM63 135h1v1h-1zM63 137h1v3h-1zM63 142h1v5h-1zM63 149h1v3h-1zM64 3h1v1h-1zM64 5h1v3h-1zM64 10h1v5h-1zM64 73h1v1h-1zM64 75h1v1h-1zM64 77h1v3h-1zM64 82h1v3h-1zM64 86h1v1h-1zM64 88h1v3h-1zM64 135h1v1h-1zM64 137h1v3h-1zM64 141h1v2h-1zM64 144h1v3h-1zM64 149h1v3h-1zM65 3h1v1h-1zM65 5h1v3h-1zM65 10h1v5h-1zM65 73h1v1h-1zM65 75h1v1h-1zM65 77h1v3h-1zM65 82h1v3h-1zM65 86h1v1h-1zM65 88h1v3h-1zM65 100h1v2h-1zM65 135h1v1h-1zM65 137h1v4h-1zM65 142h1v1h-1zM65 144h1v3h-1zM65 149h1v3h-1zM66 3h1v1h-1zM66 5h1v4h-1zM66 10h1v5h-1zM66 73h1v1h-1zM66 75h1v1h-1zM66 77h1v3h-1zM66 82h1v3h-1zM66 86h1v1h-1zM66 88h1v3h-1zM66 135h1v1h-1zM66 137h1v4h-1zM66 142h1v1h-1zM66 144h1v3h-1zM66 149h1v3h-1zM67 3h1v1h-1zM67 5h1v4h-1zM67 10h1v5h-1zM67 73h1v1h-1zM67 75h1v1h-1zM67 77h1v3h-1zM67 82h1v3h-1zM67 86h1v1h-1zM67 88h1v3h-1zM67 135h1v1h-1zM67 137h1v4h-1zM67 142h1v1h-1zM67 144h1v3h-1zM67 149h1v3h-1zM68 3h1v1h-1zM68 5h1v4h-1zM68 10h1v5h-1zM68 73h1v1h-1zM68 75h1v1h-1zM68 77h1v3h-1zM68 82h1v3h-1zM68 86h1v1h-1zM68 88h1v3h-1zM68 135h1v1h-1zM68 137h1v4h-1zM68 142h1v1h-1zM68 144h1v3h-1zM68 149h1v3h-1zM69 3h1v1h-1zM69 6h1v3h-1zM69 10h1v4h-1zM69 73h1v1h-1zM69 75h1v1h-1zM69 77h1v3h-1zM69 82h1v3h-1zM69 86h1v1h-1zM69 88h1v3h-1zM69 135h1v1h-1zM69 137h1v4h-1zM69 142h1v1h-1zM69 144h1v3h-1zM69 149h1v3h-1zM70 3h1v2h-1zM70 6h1v3h-1zM70 11h1v3h-1zM70 73h1v1h-1zM70 75h1v1h-1zM70 77h1v3h-1zM70 82h1v3h-1zM70 86h1v5h-1zM70 135h1v1h-1zM70 137h1v4h-1zM70 142h1v1h-1zM70 144h1v3h-1zM70 149h1v3h-1zM71 3h1v2h-1zM71 6h1v3h-1zM71 11h1v3h-1zM71 18h1v5h-1zM71 73h1v3h-1zM71 77h1v3h-1zM71 82h1v3h-1zM71 86h1v5h-1zM71 135h1v1h-1zM71 138h1v3h-1zM71 142h1v1h-1zM71 144h1v3h-1zM71 149h1v3h-1zM72 3h1v2h-1zM72 6h1v3h-1zM72 11h1v3h-1zM72 24h1v22h-1zM72 51h1v15h-1zM72 73h1v3h-1zM72 77h1v4h-1zM72 82h1v6h-1zM72 89h1v2h-1zM72 135h1v1h-1zM72 138h1v3h-1zM72 142h1v1h-1zM72 144h1v2h-1zM72 147h1v1h-1zM72 149h1v3h-1zM73 4h1v1h-1zM73 6h1v3h-1zM73 11h1v3h-1zM73 68h1v2h-1zM73 73h1v3h-1zM73 77h1v4h-1zM73 82h1v6h-1zM73 89h1v2h-1zM73 118h1v5h-1zM73 135h1v1h-1zM73 138h1v3h-1zM73 142h1v1h-1zM73 144h1v2h-1zM73 147h1v1h-1zM73 149h1v3h-1zM74 4h1v1h-1zM74 6h1v3h-1zM74 11h1v3h-1zM74 73h1v3h-1zM74 78h1v3h-1zM74 82h1v4h-1zM74 87h1v1h-1zM74 89h1v2h-1zM74 135h1v1h-1zM74 138h1v3h-1zM74 142h1v1h-1zM74 144h1v4h-1zM74 149h1v3h-1zM75 4h1v1h-1zM75 6h1v3h-1zM75 11h1v3h-1zM75 73h1v3h-1zM75 78h1v3h-1zM75 82h1v1h-1zM75 84h1v2h-1zM75 87h1v1h-1zM75 89h1v2h-1zM75 135h1v1h-1zM75 137h1v4h-1zM75 142h1v6h-1zM75 149h1v4h-1zM76 4h1v1h-1zM76 6h1v3h-1zM76 11h1v3h-1zM76 73h1v3h-1zM76 78h1v3h-1zM76 84h1v2h-1zM76 87h1v1h-1zM76 89h1v2h-1zM76 135h1v1h-1zM76 138h1v3h-1zM76 142h1v6h-1zM76 149h1v4h-1zM77 4h1v1h-1zM77 6h1v3h-1zM77 11h1v3h-1zM77 15h1v61h-1zM77 78h1v3h-1zM77 84h1v2h-1zM77 87h1v1h-1zM77 89h1v6h-1zM77 100h1v37h-1zM77 138h1v3h-1zM77 142h1v6h-1zM77 150h1v3h-1zM78 3h1v2h-1zM78 6h1v4h-1zM78 11h1v3h-1zM78 15h1v49h-1zM78 66h1v1h-1zM78 73h1v3h-1zM78 78h1v3h-1zM78 84h1v2h-1zM78 87h1v1h-1zM78 89h1v15h-1zM78 135h1v2h-1zM78 138h1v3h-1zM78 143h1v5h-1zM78 150h1v3h-1zM79 3h1v2h-1zM79 6h1v4h-1zM79 11h1v6h-1zM79 25h1v25h-1zM79 56h1v19h-1zM79 76h1v1h-1zM79 78h1v3h-1zM79 83h1v3h-1zM79 87h1v1h-1zM79 89h1v2h-1zM79 94h1v43h-1zM79 138h1v4h-1zM79 143h1v1h-1zM79 145h1v3h-1zM79 150h1v3h-1zM80 3h1v2h-1zM80 7h1v3h-1zM80 11h1v18h-1zM80 30h1v3h-1zM80 37h1v28h-1zM80 73h1v2h-1zM80 76h1v1h-1zM80 78h1v3h-1zM80 83h1v3h-1zM80 87h1v1h-1zM80 89h1v6h-1zM80 104h1v33h-1zM80 138h1v4h-1zM80 143h1v1h-1zM80 145h1v3h-1zM80 150h1v3h-1zM81 3h1v2h-1zM81 7h1v3h-1zM81 12h1v3h-1zM81 64h1v13h-1zM81 78h1v3h-1zM81 83h1v3h-1zM81 87h1v4h-1zM81 93h1v13h-1zM81 135h1v2h-1zM81 138h1v1h-1zM81 140h1v2h-1zM81 143h1v1h-1zM81 145h1v3h-1zM81 150h1v3h-1zM82 3h1v3h-1zM82 7h1v3h-1zM82 12h1v3h-1zM82 16h1v61h-1zM82 78h1v4h-1zM82 83h1v6h-1zM82 90h1v4h-1zM82 101h1v36h-1zM82 139h1v3h-1zM82 143h1v1h-1zM82 145h1v4h-1zM82 150h1v3h-1zM83 3h1v1h-1zM83 5h1v1h-1zM83 7h1v3h-1zM83 12h1v65h-1zM83 79h1v3h-1zM83 83h1v6h-1zM83 90h1v14h-1zM83 135h1v2h-1zM83 139h1v3h-1zM83 143h1v1h-1zM83 145h1v4h-1zM83 150h1v3h-1zM84 3h1v1h-1zM84 5h1v1h-1zM84 7h1v3h-1zM84 12h1v56h-1zM84 69h1v1h-1zM84 74h1v3h-1zM84 79h1v3h-1zM84 83h1v4h-1zM84 88h1v1h-1zM84 90h1v47h-1zM84 139h1v3h-1zM84 143h1v1h-1zM84 145h1v4h-1zM84 150h1v3h-1zM85 3h1v1h-1zM85 5h1v1h-1zM85 7h1v3h-1zM85 12h1v5h-1zM85 74h1v3h-1zM85 79h1v3h-1zM85 84h1v3h-1zM85 88h1v1h-1zM85 90h1v1h-1zM85 93h1v44h-1zM85 139h1v3h-1zM85 143h1v6h-1zM85 150h1v3h-1zM86 3h1v1h-1zM86 5h1v1h-1zM86 7h1v3h-1zM86 12h1v3h-1zM86 29h1v9h-1zM86 39h1v2h-1zM86 62h1v11h-1zM86 74h1v3h-1zM86 79h1v3h-1zM86 85h1v2h-1zM86 88h1v1h-1zM86 90h1v1h-1zM86 102h1v36h-1zM86 139h1v3h-1zM86 143h1v6h-1zM86 151h1v2h-1zM87 3h1v3h-1zM87 7h1v4h-1zM87 12h1v61h-1zM87 74h1v3h-1zM87 79h1v3h-1zM87 85h1v2h-1zM87 88h1v1h-1zM87 90h1v4h-1zM87 135h1v7h-1zM87 144h1v5h-1zM87 151h1v2h-1z\"></path>\n <path fill=\"#9d6c49\" d=\"M1 2h1v153h-1zM2 3h1v151h-1zM3 3h1v1h-1zM3 9h1v2h-1zM3 15h1v5h-1zM3 81h1v1h-1zM3 88h1v1h-1zM3 91h1v9h-1zM3 136h1v2h-1zM3 143h1v1h-1zM3 146h1v1h-1zM3 152h1v1h-1zM4 3h1v1h-1zM4 9h1v2h-1zM4 80h1v2h-1zM4 88h1v1h-1zM4 99h1v36h-1zM4 136h1v2h-1zM4 143h1v1h-1zM4 146h1v1h-1zM4 152h1v1h-1zM5 9h1v2h-1zM5 74h1v1h-1zM5 80h1v2h-1zM5 88h1v1h-1zM5 113h1v9h-1zM5 136h1v2h-1zM5 143h1v1h-1zM5 152h1v1h-1zM6 9h1v2h-1zM6 74h1v1h-1zM6 80h1v2h-1zM6 87h1v1h-1zM6 136h1v1h-1zM6 143h1v1h-1zM6 152h1v1h-1zM7 9h1v1h-1zM7 74h1v1h-1zM7 80h1v2h-1zM7 87h1v1h-1zM7 136h1v1h-1zM7 143h1v1h-1zM7 152h1v1h-1zM8 9h1v1h-1zM8 30h1v9h-1zM8 69h1v4h-1zM8 74h1v1h-1zM8 80h1v2h-1zM8 87h1v1h-1zM8 136h1v1h-1zM8 143h1v1h-1zM8 152h1v1h-1zM9 8h1v2h-1zM9 15h1v58h-1zM9 74h1v1h-1zM9 80h1v2h-1zM9 87h1v1h-1zM9 91h1v3h-1zM9 136h1v1h-1zM9 143h1v1h-1zM9 152h1v1h-1zM10 8h1v2h-1zM10 15h1v5h-1zM10 74h1v1h-1zM10 80h1v2h-1zM10 87h1v1h-1zM10 90h1v12h-1zM10 136h1v1h-1zM10 152h1v1h-1zM11 8h1v2h-1zM11 74h1v1h-1zM11 80h1v2h-1zM11 87h1v1h-1zM11 99h1v13h-1zM11 114h1v3h-1zM11 120h1v17h-1zM11 152h1v1h-1zM12 8h1v2h-1zM12 80h1v2h-1zM12 87h1v1h-1zM12 113h1v9h-1zM12 135h1v2h-1zM12 152h1v1h-1zM13 8h1v2h-1zM13 80h1v2h-1zM13 87h1v1h-1zM13 135h1v2h-1zM13 152h1v1h-1zM14 8h1v2h-1zM14 80h1v2h-1zM14 87h1v1h-1zM14 135h1v2h-1zM14 152h1v1h-1zM15 8h1v2h-1zM15 80h1v2h-1zM15 87h1v1h-1zM15 135h1v2h-1zM15 151h1v2h-1zM16 8h1v2h-1zM16 80h1v2h-1zM16 87h1v1h-1zM16 135h1v2h-1zM16 152h1v1h-1zM17 8h1v2h-1zM17 80h1v2h-1zM17 87h1v1h-1zM17 135h1v2h-1zM17 151h1v2h-1zM18 8h1v2h-1zM18 80h1v2h-1zM18 87h1v1h-1zM18 135h1v2h-1zM18 142h1v1h-1zM18 145h1v1h-1zM18 151h1v2h-1zM19 8h1v2h-1zM19 80h1v2h-1zM19 87h1v1h-1zM19 135h1v2h-1zM19 142h1v1h-1zM19 145h1v1h-1zM19 151h1v2h-1zM20 8h1v2h-1zM20 80h1v2h-1zM20 87h1v1h-1zM20 135h1v2h-1zM20 142h1v1h-1zM20 145h1v1h-1zM20 151h1v2h-1zM21 8h1v2h-1zM21 80h1v2h-1zM21 87h1v1h-1zM21 135h1v2h-1zM21 142h1v1h-1zM21 151h1v2h-1zM22 8h1v2h-1zM22 80h1v2h-1zM22 87h1v1h-1zM22 135h1v2h-1zM22 152h1v1h-1zM23 8h1v2h-1zM23 80h1v2h-1zM23 87h1v1h-1zM23 135h1v2h-1zM23 152h1v1h-1zM24 8h1v2h-1zM24 80h1v2h-1zM24 87h1v1h-1zM24 135h1v2h-1zM24 152h1v1h-1zM25 8h1v2h-1zM25 80h1v2h-1zM25 87h1v1h-1zM25 135h1v2h-1zM25 152h1v1h-1zM26 8h1v2h-1zM26 80h1v2h-1zM26 87h1v1h-1zM26 135h1v2h-1zM26 152h1v1h-1zM27 8h1v2h-1zM27 74h1v1h-1zM27 80h1v2h-1zM27 87h1v1h-1zM27 135h1v2h-1zM27 152h1v1h-1zM28 8h1v2h-1zM28 74h1v1h-1zM28 80h1v2h-1zM28 87h1v1h-1zM28 136h1v1h-1zM28 152h1v1h-1zM29 8h1v2h-1zM29 74h1v1h-1zM29 80h1v2h-1zM29 87h1v1h-1zM29 136h1v1h-1zM29 152h1v1h-1zM30 8h1v2h-1zM30 74h1v1h-1zM30 80h1v2h-1zM30 87h1v1h-1zM30 136h1v1h-1zM30 152h1v1h-1zM31 8h1v2h-1zM31 74h1v1h-1zM31 80h1v2h-1zM31 87h1v1h-1zM31 136h1v1h-1zM31 143h1v1h-1zM31 152h1v1h-1zM32 8h1v2h-1zM32 74h1v1h-1zM32 80h1v2h-1zM32 87h1v1h-1zM32 136h1v1h-1zM32 143h1v1h-1zM32 152h1v1h-1zM33 9h1v1h-1zM33 74h1v1h-1zM33 80h1v2h-1zM33 87h1v1h-1zM33 136h1v1h-1zM33 143h1v1h-1zM33 152h1v1h-1zM34 9h1v1h-1zM34 74h1v1h-1zM34 80h1v2h-1zM34 87h1v1h-1zM34 136h1v1h-1zM34 143h1v1h-1zM34 152h1v1h-1zM35 9h1v1h-1zM35 74h1v1h-1zM35 80h1v2h-1zM35 87h1v1h-1zM35 136h1v1h-1zM35 143h1v1h-1zM35 152h1v1h-1zM36 9h1v1h-1zM36 74h1v1h-1zM36 80h1v2h-1zM36 87h1v1h-1zM36 136h1v1h-1zM36 143h1v1h-1zM36 152h1v1h-1zM37 9h1v1h-1zM37 74h1v1h-1zM37 80h1v2h-1zM37 87h1v1h-1zM37 136h1v1h-1zM37 143h1v1h-1zM37 152h1v1h-1zM38 9h1v1h-1zM38 74h1v1h-1zM38 80h1v2h-1zM38 87h1v1h-1zM38 136h1v1h-1zM38 143h1v1h-1zM38 152h1v1h-1zM39 9h1v1h-1zM39 74h1v1h-1zM39 80h1v2h-1zM39 87h1v1h-1zM39 136h1v1h-1zM39 143h1v1h-1zM39 152h1v1h-1zM40 9h1v1h-1zM40 74h1v1h-1zM40 80h1v2h-1zM40 87h1v1h-1zM40 136h1v1h-1zM40 143h1v1h-1zM40 152h1v1h-1zM41 9h1v1h-1zM41 74h1v1h-1zM41 80h1v2h-1zM41 87h1v1h-1zM41 136h1v1h-1zM41 143h1v1h-1zM41 152h1v1h-1zM42 9h1v2h-1zM42 74h1v1h-1zM42 80h1v2h-1zM42 87h1v1h-1zM42 136h1v1h-1zM42 143h1v1h-1zM42 152h1v1h-1zM43 9h1v1h-1zM43 74h1v1h-1zM43 80h1v2h-1zM43 87h1v1h-1zM43 136h1v1h-1zM43 143h1v1h-1zM43 152h1v1h-1zM44 9h1v1h-1zM44 74h1v1h-1zM44 80h1v2h-1zM44 87h1v1h-1zM44 136h1v1h-1zM44 143h1v1h-1zM44 152h1v1h-1zM45 9h1v1h-1zM45 74h1v1h-1zM45 80h1v2h-1zM45 87h1v1h-1zM45 136h1v1h-1zM45 143h1v1h-1zM45 152h1v1h-1zM46 9h1v1h-1zM46 74h1v1h-1zM46 80h1v2h-1zM46 87h1v1h-1zM46 136h1v1h-1zM46 143h1v1h-1zM46 152h1v1h-1zM47 9h1v1h-1zM47 74h1v1h-1zM47 80h1v2h-1zM47 87h1v1h-1zM47 136h1v1h-1zM47 143h1v1h-1zM47 152h1v1h-1zM48 9h1v1h-1zM48 74h1v1h-1zM48 80h1v2h-1zM48 87h1v1h-1zM48 136h1v1h-1zM48 143h1v1h-1zM48 152h1v1h-1zM49 9h1v1h-1zM49 74h1v1h-1zM49 80h1v2h-1zM49 87h1v1h-1zM49 136h1v1h-1zM49 143h1v1h-1zM49 152h1v1h-1zM50 9h1v1h-1zM50 74h1v1h-1zM50 80h1v2h-1zM50 87h1v1h-1zM50 136h1v1h-1zM50 143h1v1h-1zM50 152h1v1h-1zM51 9h1v1h-1zM51 74h1v1h-1zM51 80h1v2h-1zM51 87h1v1h-1zM51 136h1v1h-1zM51 143h1v1h-1zM51 152h1v1h-1zM52 9h1v1h-1zM52 74h1v1h-1zM52 80h1v2h-1zM52 87h1v1h-1zM52 136h1v1h-1zM52 152h1v1h-1zM53 9h1v1h-1zM53 74h1v1h-1zM53 80h1v2h-1zM53 87h1v1h-1zM53 136h1v1h-1zM53 152h1v1h-1zM54 8h1v2h-1zM54 74h1v1h-1zM54 80h1v2h-1zM54 87h1v1h-1zM54 136h1v1h-1zM54 143h1v1h-1zM54 152h1v1h-1zM55 8h1v2h-1zM55 74h1v1h-1zM55 80h1v2h-1zM55 87h1v1h-1zM55 136h1v1h-1zM55 152h1v1h-1zM56 8h1v2h-1zM56 80h1v2h-1zM56 87h1v1h-1zM56 136h1v1h-1zM56 152h1v1h-1zM57 8h1v2h-1zM57 74h1v1h-1zM57 80h1v2h-1zM57 87h1v1h-1zM57 136h1v1h-1zM57 152h1v1h-1zM58 8h1v2h-1zM58 74h1v1h-1zM58 80h1v2h-1zM58 87h1v1h-1zM58 136h1v1h-1zM58 152h1v1h-1zM59 8h1v2h-1zM59 74h1v1h-1zM59 80h1v2h-1zM59 87h1v1h-1zM59 136h1v1h-1zM59 152h1v1h-1zM60 8h1v2h-1zM60 74h1v1h-1zM60 80h1v2h-1zM60 87h1v1h-1zM60 136h1v1h-1zM60 152h1v1h-1zM61 8h1v2h-1zM61 74h1v1h-1zM61 80h1v2h-1zM61 87h1v1h-1zM61 136h1v1h-1zM61 152h1v1h-1zM62 8h1v2h-1zM62 80h1v2h-1zM62 87h1v1h-1zM62 136h1v1h-1zM62 152h1v1h-1zM63 8h1v2h-1zM63 74h1v1h-1zM63 80h1v2h-1zM63 87h1v1h-1zM63 136h1v1h-1zM63 152h1v1h-1zM64 8h1v2h-1zM64 74h1v1h-1zM64 80h1v2h-1zM64 87h1v1h-1zM64 136h1v1h-1zM64 143h1v1h-1zM64 152h1v1h-1zM65 8h1v2h-1zM65 74h1v1h-1zM65 80h1v2h-1zM65 87h1v1h-1zM65 136h1v1h-1zM65 143h1v1h-1zM65 152h1v1h-1zM66 9h1v1h-1zM66 74h1v1h-1zM66 80h1v2h-1zM66 87h1v1h-1zM66 136h1v1h-1zM66 143h1v1h-1zM66 152h1v1h-1zM67 9h1v1h-1zM67 74h1v1h-1zM67 80h1v2h-1zM67 87h1v1h-1zM67 136h1v1h-1zM67 143h1v1h-1zM67 152h1v1h-1zM68 9h1v1h-1zM68 74h1v1h-1zM68 80h1v2h-1zM68 87h1v1h-1zM68 136h1v1h-1zM68 143h1v1h-1zM68 152h1v1h-1zM69 9h1v1h-1zM69 74h1v1h-1zM69 80h1v2h-1zM69 87h1v1h-1zM69 136h1v1h-1zM69 143h1v1h-1zM69 152h1v1h-1zM70 9h1v2h-1zM70 74h1v1h-1zM70 80h1v2h-1zM70 136h1v1h-1zM70 143h1v1h-1zM70 152h1v1h-1zM71 9h1v2h-1zM71 80h1v2h-1zM71 136h1v2h-1zM71 143h1v1h-1zM71 152h1v1h-1zM72 9h1v2h-1zM72 81h1v1h-1zM72 88h1v1h-1zM72 136h1v2h-1zM72 143h1v1h-1zM72 146h1v1h-1zM72 152h1v1h-1zM73 3h1v1h-1zM73 9h1v2h-1zM73 81h1v1h-1zM73 88h1v1h-1zM73 136h1v2h-1zM73 143h1v1h-1zM73 146h1v1h-1zM73 152h1v1h-1zM74 3h1v1h-1zM74 9h1v2h-1zM74 18h1v53h-1zM74 81h1v1h-1zM74 88h1v1h-1zM74 94h1v39h-1zM74 136h1v2h-1zM74 143h1v1h-1zM74 152h1v1h-1zM75 3h1v1h-1zM75 9h1v2h-1zM75 17h1v55h-1zM75 81h1v1h-1zM75 88h1v1h-1zM75 93h1v41h-1zM75 136h1v1h-1zM76 3h1v1h-1zM76 9h1v2h-1zM76 16h1v57h-1zM76 81h1v2h-1zM76 88h1v1h-1zM76 92h1v43h-1zM76 136h1v2h-1zM77 3h1v1h-1zM77 9h1v2h-1zM77 81h1v2h-1zM77 88h1v1h-1zM77 137h1v1h-1zM78 10h1v1h-1zM78 81h1v2h-1zM78 88h1v1h-1zM78 137h1v1h-1zM79 10h1v1h-1zM79 75h1v1h-1zM79 81h1v2h-1zM79 88h1v1h-1zM79 137h1v1h-1zM79 144h1v1h-1zM80 10h1v1h-1zM80 29h1v1h-1zM80 33h1v4h-1zM80 65h1v8h-1zM80 75h1v1h-1zM80 81h1v2h-1zM80 88h1v1h-1zM80 137h1v1h-1zM80 144h1v1h-1zM81 10h1v2h-1zM81 15h1v49h-1zM81 81h1v2h-1zM81 91h1v2h-1zM81 137h1v1h-1zM81 144h1v1h-1zM82 10h1v2h-1zM82 15h1v1h-1zM82 82h1v1h-1zM82 89h1v1h-1zM82 94h1v7h-1zM82 137h1v2h-1zM82 144h1v1h-1zM83 4h1v1h-1zM83 10h1v2h-1zM83 82h1v1h-1zM83 89h1v1h-1zM83 104h1v31h-1zM83 137h1v2h-1zM83 144h1v1h-1zM84 4h1v1h-1zM84 10h1v2h-1zM84 73h1v1h-1zM84 82h1v1h-1zM84 89h1v1h-1zM84 137h1v2h-1zM84 144h1v1h-1zM85 4h1v1h-1zM85 10h1v2h-1zM85 73h1v1h-1zM85 82h1v2h-1zM85 89h1v1h-1zM85 137h1v2h-1zM86 4h1v1h-1zM86 10h1v2h-1zM86 73h1v1h-1zM86 82h1v2h-1zM86 89h1v1h-1zM86 138h1v1h-1zM87 11h1v1h-1zM87 73h1v1h-1zM87 82h1v2h-1zM87 89h1v1h-1z\"></path>\n <path fill=\"#b5825b\" d=\"M1 1h1v1h-1zM2 1h1v2h-1zM3 1h1v2h-1zM4 1h1v2h-1zM5 1h1v2h-1zM6 1h1v2h-1zM7 1h1v2h-1zM8 1h1v2h-1zM9 1h1v2h-1zM10 1h1v2h-1zM11 1h1v2h-1zM12 1h1v2h-1zM13 1h1v2h-1zM14 1h1v2h-1zM15 1h1v2h-1zM15 72h1v1h-1zM15 134h1v1h-1zM16 1h1v2h-1zM16 71h1v2h-1zM16 133h1v2h-1zM17 1h1v2h-1zM17 70h1v3h-1zM17 132h1v3h-1zM18 1h1v2h-1zM18 70h1v3h-1zM18 132h1v3h-1zM19 1h1v2h-1zM19 70h1v3h-1zM19 132h1v3h-1zM20 1h1v2h-1zM20 70h1v3h-1zM20 132h1v3h-1zM21 1h1v2h-1zM21 70h1v3h-1zM21 132h1v3h-1zM22 1h1v2h-1zM22 70h1v3h-1zM22 132h1v3h-1zM23 1h1v2h-1zM23 70h1v3h-1zM23 132h1v3h-1zM24 1h1v2h-1zM24 70h1v3h-1zM24 132h1v3h-1zM25 1h1v2h-1zM25 70h1v3h-1zM25 132h1v3h-1zM26 1h1v2h-1zM26 70h1v3h-1zM26 132h1v3h-1zM27 1h1v2h-1zM27 70h1v3h-1zM27 132h1v3h-1zM28 1h1v2h-1zM28 70h1v3h-1zM28 132h1v3h-1zM29 1h1v2h-1zM29 70h1v3h-1zM29 132h1v3h-1zM30 1h1v2h-1zM30 70h1v3h-1zM30 132h1v3h-1zM31 1h1v2h-1zM31 70h1v3h-1zM31 132h1v3h-1zM32 1h1v2h-1zM32 70h1v3h-1zM32 132h1v3h-1zM33 1h1v2h-1zM33 70h1v3h-1zM33 132h1v3h-1zM34 1h1v2h-1zM34 70h1v3h-1zM34 132h1v3h-1zM35 1h1v2h-1zM35 70h1v3h-1zM35 132h1v3h-1zM36 1h1v2h-1zM36 70h1v3h-1zM36 132h1v3h-1zM37 1h1v2h-1zM37 70h1v3h-1zM37 132h1v3h-1zM38 1h1v2h-1zM38 70h1v3h-1zM38 132h1v3h-1zM39 1h1v2h-1zM39 70h1v3h-1zM39 132h1v3h-1zM40 1h1v2h-1zM40 70h1v3h-1zM40 132h1v3h-1zM41 1h1v2h-1zM41 70h1v3h-1zM41 132h1v3h-1zM42 1h1v2h-1zM42 70h1v3h-1zM42 132h1v3h-1zM43 1h1v2h-1zM43 70h1v3h-1zM43 132h1v3h-1zM44 1h1v2h-1zM44 70h1v3h-1zM44 132h1v3h-1zM45 1h1v2h-1zM45 70h1v3h-1zM45 132h1v3h-1zM46 1h1v2h-1zM46 70h1v3h-1zM46 132h1v3h-1zM47 1h1v2h-1zM47 70h1v3h-1zM47 132h1v3h-1zM48 1h1v2h-1zM48 70h1v3h-1zM48 132h1v3h-1zM49 1h1v2h-1zM49 70h1v3h-1zM49 132h1v3h-1zM50 1h1v2h-1zM50 70h1v3h-1zM50 132h1v3h-1zM51 1h1v2h-1zM51 70h1v3h-1zM51 132h1v3h-1zM52 1h1v2h-1zM52 70h1v3h-1zM52 132h1v3h-1zM53 1h1v2h-1zM53 70h1v3h-1zM53 132h1v3h-1zM54 1h1v2h-1zM54 70h1v3h-1zM54 132h1v3h-1zM55 1h1v2h-1zM55 70h1v3h-1zM55 132h1v3h-1zM56 1h1v2h-1zM56 70h1v3h-1zM56 132h1v3h-1zM57 1h1v2h-1zM57 70h1v3h-1zM57 132h1v3h-1zM58 1h1v2h-1zM58 70h1v3h-1zM58 132h1v3h-1zM59 1h1v2h-1zM59 70h1v3h-1zM59 132h1v3h-1zM60 1h1v2h-1zM60 70h1v3h-1zM60 132h1v3h-1zM61 1h1v2h-1zM61 70h1v3h-1zM61 132h1v3h-1zM62 1h1v2h-1zM62 70h1v3h-1zM62 132h1v3h-1zM63 1h1v2h-1zM63 70h1v3h-1zM63 132h1v3h-1zM64 1h1v2h-1zM64 70h1v3h-1zM64 132h1v3h-1zM65 1h1v2h-1zM65 70h1v3h-1zM65 132h1v3h-1zM66 1h1v2h-1zM66 70h1v3h-1zM66 132h1v3h-1zM67 1h1v2h-1zM67 70h1v3h-1zM67 132h1v3h-1zM68 1h1v2h-1zM68 70h1v3h-1zM68 132h1v3h-1zM69 1h1v2h-1zM69 70h1v3h-1zM69 132h1v3h-1zM70 1h1v2h-1zM70 70h1v3h-1zM70 132h1v3h-1zM71 1h1v2h-1zM71 70h1v3h-1zM71 132h1v3h-1zM72 1h1v2h-1zM72 70h1v3h-1zM72 132h1v3h-1zM73 1h1v2h-1zM73 70h1v3h-1zM73 132h1v3h-1zM74 1h1v2h-1zM74 71h1v2h-1zM74 133h1v2h-1zM75 1h1v2h-1zM75 72h1v1h-1zM75 134h1v1h-1zM76 1h1v2h-1zM77 1h1v2h-1zM78 1h1v2h-1zM79 1h1v2h-1zM80 1h1v2h-1zM81 1h1v2h-1zM82 1h1v2h-1zM83 1h1v2h-1zM84 1h1v2h-1zM85 1h1v2h-1zM86 1h1v2h-1zM87 1h1v2h-1zM88 1h1v2h-1zM89 1h1v1h-1z\"></path>\n <path fill=\"#4d3021\" d=\"M81 77h3v1h-3zM80 78h2v1h-2zM79 79h2v1h-2zM78 80h2v1h-2zM78 81h1v1h-1zM78 82h1v1h-1z\"></path>\n <path fill=\"#3a2a10\" d=\"M82 78h3v1h-3zM81 79h2v1h-2zM80 80h2v1h-2zM79 81h2v1h-2zM79 82h1v1h-1zM79 83h1v1h-1zM80 84h1v1h-1zM81 85h1v1h-1z\"></path>\n <path fill=\"#6b5119\" d=\"M83 79h3v1h-3zM82 80h2v1h-2zM86 80h1v1h-1zM81 81h2v1h-2zM87 81h1v1h-1zM80 82h2v1h-2zM80 83h1v1h-1zM82 86h1v1h-1z\"></path>\n <path fill=\"#9c7a26\" d=\"M84 80h2v1h-2zM83 81h4v1h-4zM82 82h4v1h-4zM87 82h1v1h-1zM81 83h3v1h-3zM87 83h1v1h-1zM81 84h2v1h-2zM82 85h1v1h-1z\"></path>\n <path fill=\"#c9a441\" d=\"M86 82h1v1h-1zM84 83h3v1h-3zM83 84h1v1h-1zM86 84h1v1h-1zM83 85h1v1h-1zM85 85h1v1h-1zM83 86h2v1h-2z\"></path>\n <path fill=\"#e8cf86\" d=\"M84 84h2v1h-2zM84 85h1v1h-1z\"></path>\n </g>\n</svg>", :layouts {:wraith [["a_corner" 2]], :solid [["a_position" 3] ["a_colour" 3] ["a_normal" 3] ["a_water" 2]], :ground [["a_position" 3] ["a_patch" 4]], :cloud [["a_lobe" 4]], :wall [["a_position" 3] ["a_wall" 2] ["a_normal" 2] ["a_seed" 1]], :billboard [["a_root" 3] ["a_corner" 2] ["a_shape" 3]], :panel [["a_position" 3] ["a_uv" 2] ["a_normal" 2]], :mist [["a_drop" 4]], :sky [["a_corner" 2]]}, :id "walk", :at [365 320], :posts [[200 131] [500 131] [700 131] [200 169] [640 169] [324 200] [324 296] [324 470] [286 378] [280 600] [578 296] [578 470] [616 411] [400 515] [660 515] [470 621] [250 659] [795 565] [128 480] [910 400]], :trees [{:kind :oak, :height 34, :radius 13, :trunk 1.3, :x 372, :y 292} {:kind :oak, :height 31, :radius 12, :trunk 1.2, :x 372, :y 368} {:kind :maple, :height 29, :radius 12.5, :trunk 1.0, :x 538, :y 292} {:kind :maple, :height 32, :radius 12, :trunk 1.1, :x 538, :y 368} {:kind :maple, :height 30, :radius 12, :trunk 1.1, :x 672, :y 732} {:kind :maple, :height 30, :radius 12, :trunk 1.1, :x 634, :y 74} {:kind :oak, :height 38, :radius 15, :trunk 1.4, :x 887, :y 105} {:kind :oak, :height 38, :radius 15, :trunk 1.4, :x 910, :y 739} {:kind :maple, :height 30, :radius 12, :trunk 1.1, :x 40, :y 60} {:kind :oak, :height 38, :radius 15, :trunk 1.4, :x 140, :y 499} {:kind :oak, :height 38, :radius 15, :trunk 1.4, :x 126, :y 418} {:kind :birch, :height 26, :radius 8, :trunk 0.8, :x 364, :y 658} {:kind :maple, :height 30, :radius 12, :trunk 1.1, :x 151, :y 79} {:kind :oak, :height 38, :radius 15, :trunk 1.4, :x 530, :y 111} {:kind :maple, :height 30, :radius 12, :trunk 1.1, :x 794, :y 103} {:kind :oak, :height 38, :radius 15, :trunk 1.4, :x 43, :y 463} {:kind :oak, :height 38, :radius 15, :trunk 1.4, :x 923, :y 369} {:kind :birch, :height 26, :radius 8, :trunk 0.8, :x 757, :y 93} {:kind :birch, :height 26, :radius 8, :trunk 0.8, :x 803, :y 685} {:kind :oak, :height 38, :radius 15, :trunk 1.4, :x 139, :y 692} {:kind :birch, :height 26, :radius 8, :trunk 0.8, :x 890, :y 78} {:kind :oak, :height 38, :radius 15, :trunk 1.4, :x 811, :y 737} {:kind :oak, :height 38, :radius 15, :trunk 1.4, :x 181, :y 56} {:kind :birch, :height 26, :radius 8, :trunk 0.8, :x 843, :y 127} {:kind :oak, :height 38, :radius 15, :trunk 1.4, :x 900, :y 692} {:kind :birch, :height 26, :radius 8, :trunk 0.8, :x 52, :y 736} {:kind :maple, :height 30, :radius 12, :trunk 1.1, :x 488, :y 103} {:kind :maple, :height 30, :radius 12, :trunk 1.1, :x 264, :y 485} {:kind :maple, :height 30, :radius 12, :trunk 1.1, :x 281, :y 76} {:kind :maple, :height 30, :radius 12, :trunk 1.1, :x 539, :y 63} {:kind :birch, :height 26, :radius 8, :trunk 0.8, :x 560, :y 272} {:kind :maple, :height 30, :radius 12, :trunk 1.1, :x 636, :y 670} {:kind :birch, :height 26, :radius 8, :trunk 0.8, :x 347, :y 47} {:kind :maple, :height 30, :radius 12, :trunk 1.1, :x 496, :y 266} {:kind :birch, :height 26, :radius 8, :trunk 0.8, :x 127, :y 223} {:kind :maple, :height 30, :radius 12, :trunk 1.1, :x 389, :y 44} {:kind :oak, :height 38, :radius 15, :trunk 1.4, :x 754, :y 120} {:kind :maple, :height 30, :radius 12, :trunk 1.1, :x 877, :y 619} {:kind :oak, :height 38, :radius 15, :trunk 1.4, :x 194, :y 660} {:kind :oak, :height 38, :radius 15, :trunk 1.4, :x 828, :y 76} {:kind :oak, :height 38, :radius 15, :trunk 1.4, :x 686, :y 687} {:kind :maple, :height 30, :radius 12, :trunk 1.1, :x 323, :y 365} {:kind :birch, :height 26, :radius 8, :trunk 0.8, :x 333, :y 284} {:kind :oak, :height 38, :radius 15, :trunk 1.4, :x 921, :y 594} {:kind :birch, :height 26, :radius 8, :trunk 0.8, :x 126, :y 45} {:kind :birch, :height 26, :radius 8, :trunk 0.8, :x 896, :y 130} {:kind :oak, :height 38, :radius 15, :trunk 1.4, :x 500, :y 129} {:kind :birch, :height 26, :radius 8, :trunk 0.8, :x 242, :y 100} {:kind :oak, :height 38, :radius 15, :trunk 1.4, :x 555, :y 98} {:kind :birch, :height 26, :radius 8, :trunk 0.8, :x 378, :y 271} {:kind :oak, :height 38, :radius 15, :trunk 1.4, :x 793, :y 58} {:kind :maple, :height 30, :radius 12, :trunk 1.1, :x 688, :y 111} {:kind :maple, :height 30, :radius 12, :trunk 1.1, :x 255, :y 657} {:kind :maple, :height 30, :radius 12, :trunk 1.1, :x 235, :y 130} {:kind :birch, :height 26, :radius 8, :trunk 0.8, :x 42, :y 288} {:kind :birch, :height 26, :radius 8, :trunk 0.8, :x 207, :y 88} {:kind :birch, :height 26, :radius 8, :trunk 0.8, :x 637, :y 712} {:kind :birch, :height 26, :radius 8, :trunk 0.8, :x 751, :y 689} {:kind :maple, :height 30, :radius 12, :trunk 1.1, :x 726, :y 110} {:kind :maple, :height 30, :radius 12, :trunk 1.1, :x 188, :y 706} {:kind :maple, :height 30, :radius 12, :trunk 1.1, :x 863, :y 709} {:kind :oak, :height 38, :radius 15, :trunk 1.4, :x 821, :y 601} {:kind :birch, :height 26, :radius 8, :trunk 0.8, :x 925, :y 186} {:kind :birch, :height 26, :radius 8, :trunk 0.8, :x 629, :y 737} {:kind :oak, :height 38, :radius 15, :trunk 1.4, :x 158, :y 723} {:kind :birch, :height 26, :radius 8, :trunk 0.8, :x 710, :y 662} {:kind :oak, :height 38, :radius 15, :trunk 1.4, :x 924, :y 433} {:kind :maple, :height 30, :radius 12, :trunk 1.1, :x 694, :y 63} {:kind :maple, :height 30, :radius 12, :trunk 1.1, :x 820, :y 49} {:kind :birch, :height 26, :radius 8, :trunk 0.8, :x 178, :y 94} {:kind :oak, :height 38, :radius 15, :trunk 1.4, :x 203, :y 734} {:kind :birch, :height 26, :radius 8, :trunk 0.8, :x 622, :y 130} {:kind :birch, :height 26, :radius 8, :trunk 0.8, :x 460, :y 268} {:kind :birch, :height 26, :radius 8, :trunk 0.8, :x 253, :y 733}]}) (js/console.error "fedoras: fedoras.cs-class.walk.walk-ui/mount! is not on this page. Either the file that declares it was never shipped, or the namespace inside it does not match the path it was shipped from."))</script></div><div class="screenplay"><p class="action">The list of buildings in that cell is not a copy. It is written out of the plan when the book is built, so if somebody renames a building after a donor falls out of favour, it is renamed in here too, and nobody has to remember.</p></div><div class="screenplay"><p class="slug">EXT. THE QUAD — THE FOLLOWING NIGHT</p></div><div class="screenplay"><p class="action">He walks it. Actually walks it, at two in the morning, from the middle of the quad to the corner of Meridian and back, with the machine shut in his room.</p><p class="action beat">It is the same.</p><p class="action">Not similar. The gap between the Hall and the Annexe is where it is in the picture, and the Commons is behind them at the distance the picture gave, and he stands in the middle of the quad for a while.</p><p class="action beat">Then he turns round and faces south-west, at the low ground past Plant Services, and there is nothing there.</p><p class="action">There is nothing there in the picture either. It is not drawn as empty. It is drawn as ground with a name on it, and the name is on the plan on the wall of every room in Ashcroft House, and it has been there since August.</p></div><div class="screenplay"><p class="transition">Blackout.</p></div><div class="screenplay"><p class="slug">INT. MERIDIAN HALL — REYES 204 — FRIDAY 22 MAY</p></div><div class="screenplay"><p class="action">The door is open because she is in it. The corridor outside is full of people carrying boxes past it in both directions.</p><p class="action">He puts it on the desk on paper because she will not take anything any other way. It is longer than it was in March and it runs the four hundred and nine pages and it does not fall over on any of them.</p><p class="action">She reads it the way she reads things, which is fast and twice.</p></div><div class="screenplay"><div class="dialogue"><p class="character">REYES</p><div class="lines"><p class="line">Is it finished?</p></div></div><div class="dialogue"><p class="character"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span></p><div class="lines"><p class="line">Yes.</p></div></div><div class="dialogue"><p class="character">REYES</p><div class="lines"><p class="line">Say why.</p></div></div></div><div class="screenplay"><p class="action beat">Nobody has asked him to say why about anything all year.</p></div><div class="screenplay"><div class="dialogue"><p class="character"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span></p><div class="lines"><p class="line">Because it does the thing it says on the front and</p><p class="line">it does it on all of them, and everything I've</p><p class="line">added since March has been me tidying it, and I</p><p class="line">stopped.</p></div></div><div class="dialogue"><p class="character">REYES</p><div class="lines"><p class="line">Right.</p></div></div></div><div class="screenplay"><p class="action">She marks it full, which takes about four seconds, and then writes one word in the margin.</p><p class="action beat">The word is AGAIN.</p><p class="action">He looks at it for a moment and cannot tell whether it is an instruction or a criticism, and does not ask, and does not find out for about a week.</p></div><div class="screenplay"><div class="dialogue"><p class="character">REYES</p><div class="lines"><p class="line">Set one, next year, is six programs in three weeks</p><p class="line">and the first of them counts to a hundred and says</p><p class="line">fizz on the threes.</p></div></div><div class="dialogue"><p class="character"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span></p><div class="lines"><p class="line">I know.</p></div></div><div class="dialogue"><p class="character">REYES</p><div class="lines"><p class="line">Yes. You'll be in the room and there'll be</p><p class="line">thirty-one of them who aren't, and about four of</p><p class="line">them will be listening, and you will not be able</p><p class="line">to tell which four and neither can I.</p></div></div></div><div class="screenplay"><p class="action beat">That is the closest she comes all year and she goes back to the screen in the middle of it.</p></div><div class="screenplay"><div class="dialogue"><p class="character">REYES</p><div class="lines"><p class="line">Off you go. Have a summer.</p></div></div></div><div class="screenplay"><p class="transition">Blackout.</p></div><div class="screenplay"><p class="action">He keeps the paper. It is in the bag with the page that has been folded into eight since August.</p></div><div class="screenplay"><p class="slug">INT. STERLING CENTER — AN INNER OFFICE — MONDAY 25 MAY, 11:00 A.M.</p></div><div class="screenplay"><p class="action">One window, one desk, and a screen turned away.</p></div><div class="screenplay"><div class="dialogue"><p class="character">PARR</p><div class="lines"><p class="line">It's the last week.</p></div></div><div class="dialogue"><p class="character"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span></p><div class="lines"><p class="line">I know. I'm not taking it.</p></div></div></div><div class="screenplay"><p class="action beat">Parr types for about two seconds.</p></div><div class="screenplay"><div class="dialogue"><p class="character">PARR</p><div class="lines"><p class="line">Right.</p></div></div><div class="dialogue"><p class="character"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span></p><div class="lines"><p class="line">Don't you want to know why?</p></div></div><div class="dialogue"><p class="character">PARR</p><div class="lines"><p class="line">There isn't a box for why. There's a box that says</p><p class="line">declined and there's a box that says lapsed and</p><p class="line">declined is better for you, so I've put declined.</p></div></div></div><div class="screenplay"><p class="action beat">Four seconds.</p></div><div class="screenplay"><div class="dialogue"><p class="character"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span></p><div class="lines"><p class="line">What's the difference?</p></div></div><div class="dialogue"><p class="character">PARR</p><div class="lines"><p class="line">Declined is a person doing something. Lapsed is a</p><p class="line">date going past.</p></div></div></div><div class="screenplay"><p class="action">He turns the card face down on the desk, which he did in February the other way round.</p></div><div class="screenplay"><div class="dialogue"><p class="character">PARR</p><div class="lines"><p class="line">It'll be open again in February.</p></div></div><div class="dialogue"><p class="character"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span></p><div class="lines"><p class="line">Every year?</p></div></div><div class="dialogue"><p class="character">PARR</p><div class="lines"><p class="line">Meridian takes four from here a year and there are</p><p class="line">four every year, and if you keep declining it</p><p class="line">somebody will eventually want to know what you're</p><p class="line">for instead, and that isn't a threat, that's just</p><p class="line">how a list works.</p></div></div><div class="dialogue"><p class="character"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span></p><div class="lines"><p class="line">Can I have a service assignment over the summer?</p></div></div><div class="dialogue"><p class="character">PARR</p><div class="lines"><p class="line">There isn't one. The whole line's contracted and</p><p class="line">there is nothing in Plant for anybody.</p></div></div><div class="dialogue"><p class="character"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span></p><div class="lines"><p class="line">Then I'll keep Systems.</p></div></div><div class="dialogue"><p class="character">PARR</p><div class="lines"><p class="line">Systems is a post. It doesn't stop for a summer</p><p class="line">and it's paid and you'd have had to tell me if you</p><p class="line">weren't keeping it.</p></div></div></div><div class="screenplay"><p class="action beat">So he keeps it, and this is the first thing he has taken in this building by saying yes to it out loud rather than by not saying no.</p></div><div class="screenplay"><div class="dialogue"><p class="character">PARR</p><div class="lines"><p class="line">Are you going anywhere?</p></div></div><div class="dialogue"><p class="character"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span></p><div class="lines"><p class="line">No.</p></div></div><div class="dialogue"><p class="character">PARR</p><div class="lines"><p class="line">That's most of them.</p></div></div></div><div class="screenplay"><p class="transition">Blackout.</p></div><div class="artifact handout"><p class="handout-title">NEW CARTHAGE UNIVERSITY · SUMMER ARRANGEMENTS · {pc-full}</p><p><span>Accommodation: Ashcroft House, room 214, retained. Single.</span></p><p><span>Systems: continues. Mondays and Wednesdays, 09:00–13:00. Paid.</span></p><p><span>Service assignment: none held.</span></p><p><span>Docket: not assigned.</span></p><p><span>Remote operations: no ballot lodged.</span></p><p><span>Nomination: recorded.</span></p><p><span>Dining allocation is unaffected. The Commons will operate on summer hours.</span></p></div><div class="screenplay"><p class="action">It is the sixth line and it is between the ballot he did not lodge and the dining allocation, and it does not say a name, because the form does not print the name, because the name is not for the subject.</p><p class="action">He did it on the Tuesday, at four in the afternoon, in about eleven seconds, and he did not tell her and he is not going to.</p></div><div class="artifact discord"><div class="discord-head"><span class="discord-server">FEDORA SOCIETY</span><span class="discord-channel">#general</span></div><div class="discord-body"><div class="discord-msg"><p class="discord-meta"><span class="discord-who"><span>Miles</span></span><span class="discord-at">20 May, 6:02 PM</span></p><div class="discord-text"><p><span>review week. good luck everyone. genuinely</span></p></div></div><div class="discord-msg"><p class="discord-meta"><span class="discord-who"><span>Simon</span></span><span class="discord-at">20 May, 6:40 PM</span></p><div class="discord-text"><p><span>good luck</span></p></div></div><div class="discord-msg"><p class="discord-meta"><span class="discord-who"><span>Derek</span></span><span class="discord-at">26 May, 11:12 AM</span></p><div class="discord-text"><p><span>no action</span></p></div></div><div class="discord-msg"><p class="discord-meta"><span class="discord-who"><span>Miles</span></span><span class="discord-at">26 May, 11:14 AM</span></p><div class="discord-text"><p><span>no action</span></p></div></div><div class="discord-msg"><p class="discord-meta"><span class="discord-who"><span>Simon</span></span><span class="discord-at">26 May, 12:31 PM</span></p><div class="discord-text"><p><span>no action</span></p></div></div><div class="discord-msg"><p class="discord-meta"><span class="discord-who"><span>Victor</span></span><span class="discord-role">PRESIDENT</span><span class="discord-at">26 May, 12:33 PM</span></p><div class="discord-text"><p><span>Five of five.</span></p></div></div><div class="discord-msg"><p class="discord-meta"><span class="discord-who"><span>Derek</span></span><span class="discord-at">26 May, 12:34 PM</span></p><div class="discord-text"><p><span>victor thats not what that means</span></p></div></div><div class="discord-msg"><p class="discord-meta"><span class="discord-who"><span>Victor</span></span><span class="discord-role">PRESIDENT</span><span class="discord-at">26 May, 12:35 PM</span></p><div class="discord-text"><p><span>It is this once.</span></p></div></div></div></div><div class="artifact email"><table class="email-head"><tbody><tr><td>From:</td><td><span>Office of the Dean <no-reply@newcarthage.edu></span></td></tr><tr><td>To:</td><td><span>The Fedora Society <c/o V. ASHCROFT></span></td></tr><tr><td>Date:</td><td>26 May, 4:00 AM</td></tr><tr><td>Subject:</td><td><strong><span>Engagement — submission acknowledged</span></strong></td></tr></tbody></table><div class="email-body"><p><span>Thank you for your submission of November concerning the treatment of recognised societies within the engagement figure.</span></p><p><span>The submission was noted at the board. The arithmetic was checked and found to be correct.</span></p><p><span>The methodology of the engagement figure is under review. Societies are not at present within scope. The review has no reporting date.</span></p><p><span>The Dean has asked that the Society be thanked for its interest.</span></p><p><span>Engagement is not a component of review. Engagement is recorded.</span></p></div></div><div class="screenplay"><p class="action">Victor reads it out at the last meeting, standing up, all of it, twice, and the second time slower.</p><p class="action">The arithmetic was checked and found to be correct. He says that part four times over the course of the evening and nobody at that table takes it off him, and Miles, who did the arithmetic, does not say a word about whose it was.</p></div><div class="screenplay"><p class="slug">INT. HALLORAN UNION — ROOM 3C — WEDNESDAY 27 MAY, 7:00 P.M.</p></div><div class="screenplay"><p class="action">The folding table. The tin. The hat, which has been repaired twice this year and is worse each time and which nobody has said anything about since the second week of September.</p><p class="action">Five chairs and five people in them. Simon is in his and has been at three of five since April and is here for this.</p></div><div class="screenplay"><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">Last one. I'm going to finish it properly because</p><p class="line">I have never finished one properly and I have run</p><p class="line">six.</p></div></div></div><div class="screenplay"><p class="action">He takes the screen down, which he has never done, and puts it flat on the table so that everybody can see the notes, which are four pages and are in pencil and have been gone over.</p></div><div class="screenplay"><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">The line in the ledger closed in November. The</p><p class="line">professor is teaching. The class is full.</p></div></div><div class="dialogue"><p class="character">MILES</p><div class="lines"><p class="line">We know.</p></div></div><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">You know. Here's the part I've not told you.</p></div></div><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">The bursar's office has a line in the ledger they</p><p class="line">can't account for. Every month for three years the</p><p class="line">Collegium has paid a salary to a lecturer in the</p><p class="line">natural philosophy faculty, and last week somebody</p><p class="line">in accounts finally asked which lecturer.</p></div></div></div><div class="screenplay"><p class="action beat">It is the first thing he said at the first session in September, word for word, and three of them get it at different speeds.</p></div><div class="screenplay"><div class="dialogue"><p class="character">DEREK</p><div class="lines"><p class="line">It's starting again.</p></div></div><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">It started again in November. It has been starting</p><p class="line">again the whole time. It is a different clerk and</p><p class="line">a different lecturer and the same line.</p></div></div><div class="dialogue"><p class="character">SIMON</p><p class="paren">(quietly)</p><div class="lines"><p class="line">So nothing we did counted.</p></div></div><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">Everything you did counted. It's in the minutes.</p></div></div></div><div class="screenplay"><p class="action beat">Miles puts his hand flat on the ledger and does not say anything.</p></div><div class="screenplay"><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">There's one thing that came out of that corridor</p><p class="line">that was not there when you went in and it is</p><p class="line">still with you, and it is the only thing in nine</p><p class="line">months that nobody has been able to file.</p></div></div></div><div class="screenplay"><p class="action">He looks at the sheet in front of <span class="pc-name" data-case="pc" data-pc="-first">Owen</span>, at the line marked COMPANION, where somebody wrote a name in in September.</p></div><div class="screenplay"><p class="direction">What your animal does at the end of it is below.</p></div><div><div class="session" id="session-four"><p class="session-who"></p><p class="session-text"></p></div><script type="application/x-scittle">(fedoras.reader/on-render! :session/four (fn [] (when-let [node (fedoras.reader/el "session-four")] (if-let [c (fedoras.reader/companion)] (do (fedoras.reader/text! node ".session-who" (clojure.string/upper-case (:name c))) (fedoras.reader/text! node ".session-text" (get {7 "It goes ahead. It has gone ahead into every room for nine months and it goes ahead out of this one, into the corridor, before anybody has said the campaign is over, and it does not come back to say whether there is anything in it. Victor lets it go. In September it is in the room before the party is.", 20 "It is in the doorway. Victor cannot end a session with a bear in the doorway and ends it anyway, and nobody stands up, because nothing gets past it including the end of the campaign. Miles writes the last entry of the year sitting down. In September it is in a doorway again and it is a wider door and it is not wide enough.", 1 "It is on the table when Miles opens the ledger to write the last entry and it is on the table when he closes it, and there is no column in that ledger for a pigeon and there never has been. Victor rules that it goes back to the one roof it knows and comes back on its own. In September it comes back on its own, into a different room, and lands on a different table.", 4 "It is at the door. It was at the door before Victor said the campaign was over and it was at the door before anybody stood up. Miles asks what to put and Victor says put that it was at the door. In September it is at the door of a room it has never been in, waiting, and nobody in that room has met it.", 15 "It is at the gate, saddled, and in nine months nobody has established who saddles it. Victor returns to the question one final time, unprompted, at about half past eleven, and does not answer it. It is at the gate in September and the girth has been done up by somebody with two hands.", 13 "It comes back with something. Victor takes about four seconds and then rules that it is the thing the party lost in the second session and had stopped mentioning by the fourth, and Derek says the name of it out loud before Victor does. In September it brings back something nobody has lost yet.", 6 "Everything in that fiction was put back overnight and the hole in the floor was not, because nobody put the hole there on purpose and the correction only reverses things that were meant. Miles enters it in the column he ruled in October, which is HEARD, and it is the only physical object in that column. The hole is still there in September.", 17 "Still asleep. Still warm. Nine months and it has not opened its eyes since the night it arrived, and Victor has never once been drawn on what happens when it does. Miles asks whether to close the entry. Victor says leave it open. It is asleep in September and it is warmer.", 3 "Nobody can say when it arrived. It was in the pack, and the pack was on the ranger's back, and the ranger cannot tell anybody which day. Miles will not enter a thing without a date on it and Victor will not give him one, and it is the only line in nine months of minutes that is left blank. It is in the pack in September and it is still the size of a loaf.", 12 "It is outside, with everything the party owns still on its back, and it will not come in, and nobody in nine months established why it would not enter the one room it would not enter. Miles has that as an open item and it stays open, and an open item carries forward. It is outside in September.", 2 "It went under the boards in the second session and nobody has seen the whole of it since, and the party has been finding evidence of it all year, and not one piece of that evidence is a thing Miles is allowed to write down. Victor says it is in the fabric now. In September something moves under the floor of a new campaign and Derek says out loud that he knew it.", 19 "The ones the party was never allowed to ask about are not in the room. The one that is the companion is, and is between the ranger and the door, which is where it has been since the corridor. Victor is careful one final time to say that only one of them is the companion, and Simon asks about the rest, and gets the answer he has been getting since October.", 11 "It has not hurried at any point in nine months and it does not hurry now. It is on the beam. Victor says nothing about it at the end and does not need to, and nobody looks up. In September it is on a different beam and got there without anybody seeing it move.", 9 "It is not in the room when Victor finishes, because it is somewhere else doing the afterwards. Victor does not describe it this time, which the table notices and nobody mentions. Miles writes ongoing, and Victor lets him, and it is the only entry in the ledger written in the present tense.", 5 "The last sound in that room is not anybody's voice. Victor makes the noise one more time with his own throat and the whole table joins in badly and the Janitor opens the door before he has been asked to. Miles writes noise, sustained, and rules a line under it. In September somebody makes the noise before Victor has sat down.", 14 "There is a door in that fiction that everybody agreed was closed, and locked, and made good, and it is open. Victor rules that it will not shut and that nobody is going to be able to shut it. Miles asks whether that goes in and Victor says yes, and it is the only thing about the last night that does.", 16 "It cannot turn round. It is facing the way it has faced since the corridor, which is out of the room, along the corridor, towards whatever is next, and there is nothing anybody can do about that and nothing anybody wants to. Simon says so out loud and is right, and Victor writes it on the notes rather than in the minutes.", 10 "It is asleep under the table with its head on somebody's foot and it has been there two hours and whoever's foot it is has not moved. Miles writes the last entry of the year with a dog on his foot. In September it comes into the new room ahead of everybody and is glad to see all four of them and has forgotten nothing.", 18 "It is above all of it and has been the whole time, and the ranger has never had a way of asking it anything, which stopped being funny at about the fourth session. It sees the ledger reconcile and the class fill and the corridor come clean, and there is no way for any of that to reach the table. In September it is still up there.", 8 "Victor says one final time that it has been following the ranger since before the campaign started, and Miles asks one final time for how long, and Victor says one final time that he is not going to say. It is at the edge of the light in September and the light is a different light and the distance is the same."} (:n c) ""))) (do (fedoras.reader/text! node ".session-who" "") (fedoras.reader/text! node ".session-text" "Roll for your animal in Act I, Scene 5, and it will be here."))))))</script></div><div class="screenplay"><p class="slug">INT. ROOM 3C — 10:40 P.M.</p></div><div class="screenplay"><p class="action">Nobody says anything for a while and it is not uncomfortable.</p></div><div class="screenplay"><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">Right. Next year is a new campaign and new</p><p class="line">characters and I would like everybody to think</p><p class="line">about what they want to be, over the summer, and</p><p class="line">to come back in September with an answer.</p></div></div><div class="dialogue"><p class="character">DEREK</p><div class="lines"><p class="line">Can I be the documentarian?</p></div></div><div class="dialogue"><p class="character">MILES</p><div class="lines"><p class="line">No.</p></div></div><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">No.</p></div></div></div><div class="screenplay"><p class="action beat">Simon has not said anything.</p></div><div class="screenplay"><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">Simon.</p></div></div><div class="dialogue"><p class="character">SIMON</p><div class="lines"><p class="line">Can I keep the paladin?</p></div></div></div><div class="screenplay"><p class="action beat">Four seconds.</p></div><div class="screenplay"><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">It's new characters.</p></div></div><div class="dialogue"><p class="character">SIMON</p><div class="lines"><p class="line">I know. Can I keep him anyway.</p></div></div></div><div class="screenplay"><p class="action">Victor looks at his notes for slightly longer than the question needs, which is the third time this year.</p></div><div class="screenplay"><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">Yes.</p></div></div></div><div class="screenplay"><p class="action">Miles writes it down. It is the last entry in the ledger for the year and it is four words long.</p></div><div class="screenplay"><p class="transition">Cut to:</p></div><div class="screenplay"><p class="slug">INT. HALLORAN UNION — THE STAIRWELL — 11:50 P.M.</p></div><div class="screenplay"><p class="action">The pizza-box wall has been taken down because the Union does the corridors over the summer, and there is a clean patch of paint behind where it was, in a different colour, the size of a wall.</p></div><div class="screenplay"><div class="dialogue"><p class="character"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span></p><div class="lines"><p class="line">What are you doing over the summer?</p></div></div><div class="dialogue"><p class="character">SIMON</p><div class="lines"><p class="line">Docket doesn't stop. Unattached students in good</p><p class="line">standing.</p></div></div><div class="dialogue"><p class="character"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span></p><div class="lines"><p class="line">Every week?</p></div></div><div class="dialogue"><p class="character">SIMON</p><div class="lines"><p class="line">Every week.</p></div></div></div><div class="screenplay"><p class="action beat"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span> does not say any of the four things a person says.</p></div><div class="screenplay"><div class="dialogue"><p class="character"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span></p><div class="lines"><p class="line">Is that all right?</p></div></div><div class="dialogue"><p class="character">SIMON</p><div class="lines"><p class="line">It's a warm room.</p></div></div></div><div class="screenplay"><p class="action beat">It is the same four words a member of faculty used to him in August about a nosebleed, and Simon has never heard them said that way and never will.</p><p class="action">THE JANITOR is at the bottom of the stairwell with a bin liner. Nobody hears the door.</p></div><div class="screenplay"><div class="dialogue"><p class="character">THE JANITOR</p><div class="lines"><p class="line">Twelve.</p></div></div><div class="dialogue"><p class="character">VICTOR</p><p class="paren">(from above)</p><div class="lines"><p class="line">Yes.</p></div></div></div><div class="screenplay"><p class="transition">Blackout.</p></div><div class="screenplay"><p class="action">Lena's letter comes on the twenty-eighth. She does not say what is in it and she does not say who it is from and she is in the print shop on the twenty-ninth and on the thirtieth and on the first of June.</p><p class="action">Eli goes on the thirtieth with a bag and four books, and three of the four are the ones that went onto the list in June and one of them is by the woman who is on it twice.</p><p class="action">Derek balloted in March on five years of hours and is Tier Two again and has eleven sessions over the summer, and the ridge has a name and he has still not told anybody what it is.</p><p class="action">Maya is given a run of her own, which is nine hours a day of a thermometer and a column, for six weeks, and it is the most anybody has given her since she got here, and she is pleased about it, and it is a thermometer and a column.</p><p class="action">Hollis goes on the last day of term. There is no leaving do because there is no mechanism for one. He puts his mug in the sink and washes it and leaves it upside down on the drainer, and takes nothing off the wall.</p><p class="action">The last thing he writes this year is a thing that makes a card.</p><p class="action">In March he wrote something that reads a card, which took an evening and most of the evening was an em dash. This one goes the other way and it took eleven minutes.</p></div><div><pre><code class="sourceCode language-clojure printed-clojure">#'act5.s5-held/card
</code></pre></div><div><pre><code class="sourceCode language-clojure printed-clojure">#'act5.s5-held/render
</code></pre></div><div class="screenplay"><p class="action">That is the whole of it and there is nothing clever in it anywhere, and Lena sets it in the print shop on the first of June on card stock because a card is card, and it takes her four goes to get the spacing the way the old ones are.</p><p class="action">They do not put the index on it. The index is four hundred pages and four hundred pages does not go in a drawer.</p><p class="action">They print the index twice, on paper, at night, and one copy goes down the fill on the second of June, and the other one goes under the short leg of the desk in room two fourteen, folded, where it holds a desk level, which is the only reason anything in this book survived at all.</p><p class="action">And then they type one card, and file it in the drawer with the 016s in it, in order, because they were asked to.</p></div><div class="artifact terminal"><p class="term-out"><span>016.13 — catalogue card — NEW CARTHAGE
A FINDING AID TO THE HOLDINGS UNDER THIS BUILDING · compiled from the cards
acc. —
COPIES: 2
LOCATION: C-3/S — held
LOCATION: ASHCROFT 214 — held
— in pencil, along the bottom edge, in a hand that is not the typist's: the water goes in a bucket. every day. the drain's in the corner.</span></p></div><div class="screenplay"><p class="action">There is no accession number on it because they do not have one and will not make one up. There is no date. Nobody in that building will look at it, and somebody will put a bag on the cabinet within a week, and it will sit in order in a drawer in a room where people have supervised study on Mondays and Wednesdays.</p><p class="action">It is the only thing either of them does all year that is meant to be found by somebody who is not born.</p></div><div class="screenplay"><p class="slug">INT. MERIDIAN HALL — BASEMENT — MONDAY 8 JUNE, 6:10 A.M.</p></div><div class="screenplay"><p class="action">The campus is empty in the way it is empty in June, which is not empty at all, and is quieter.</p><p class="action">The tray is gone. There is a hole in the slab with four inches of pipe in it and a sheared bolt beside it and a galvanised bucket under the pipe.</p><p class="action beat">It is nearly full.</p><p class="action"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span> picks it up with both hands, because that is how you pick up a full bucket, and carries it nine feet to the drain in the corner and pours it away, and puts it back under the pipe.</p><p class="action beat">It takes about forty seconds.</p><p class="action">He does it again on Tuesday and on Wednesday and on Thursday.</p></div><div class="screenplay"><p class="transition">Cut to:</p></div><div class="screenplay"><p class="slug">EXT. THE QUAD — 8:00 A.M.</p></div><div class="screenplay"><p class="action">The banner has come down for the summer and the frame is still up.</p><p class="action">A man on a ladder is doing something to the gate and two people are painting the railings and the grass has been cut.</p><p class="action">Somewhere on the north side a tuba is being played very badly by somebody who has been given a room for six weeks and is going to use all of it.</p><p class="action beat">There is nothing wrong with the campus.</p><p class="action"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span> goes up to the fourth floor of the Sterling Center, where there is carpet, and does a page, and it takes twenty minutes, and he looks out of the window at the quad until one o'clock and gets paid for the morning.</p></div><div class="screenplay"><p class="transition">Blackout.</p></div><div class="artifact handout"><p class="handout-title">NEW CARTHAGE UNIVERSITY</p><p><span>WELCOME, CLASS OF 20XX!</span></p><p><span>RANKED #1 IN STUDENT ENGAGEMENT*</span></p><p><span>*According to New Carthage University.</span></p><p><span>INTAKE: 1,024</span></p><p><span>ORIENTATION SUPPORTED BY MERIDIAN SYSTEMS</span></p></div><div class="screenplay"><p class="action">The banner goes back up on the eighteenth of August, on the same frame, and the number on it is the same number.</p><p class="action">One thousand and twenty-four of them come up the drive with suitcases and one of them will be given room two fourteen in Ashcroft House, which is a single, and which has a desk in it that does not rock.</p><p class="action">Curtain.</p></div></div>
```