0 reported it broke · the ▶ count is distinct visitors who ran it, counted once each, anonymously
Paste any text and it becomes a word cloud: every word counted, the glue words dropped, and the rest packed outward from the centre along a spiral until each one finds a gap it fits in. Size and colour both come from the count, and the whole set of sizes is binary-searched until the largest scale that still fits the canvas is found — so the cloud fills the frame at any window width and saves straight to PNG.
It opens on the first lines of A Tale of Two Cities, which is the perfect thing to try skip common words on: switch it off and watch the and it eat the entire picture. Then drag used at least to strip the one-offs, hit re-shuffle layout for a different packing of the same numbers, and hover a word for its real count next to the honest table — because a cloud is a lovely picture and a terrible ruler.
Source — the code you see is the code that just ran 1066 lines 49.0 KB index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Word Cloud Builder — sloprun.dev</title>
<style>
/* sloprun design tokens — inline this block into every demo (self-contained rule).
Identity: instrument-panel. Machine facts in mono; human words in sans.
Green is EARNED: only for "it ran / it worked" states, never decoration. */
:root {
--bg: #F6F6F3; /* neutral paper, not cream */
--surface: #FFFFFF;
--ink: #1A1D21;
--muted: #5A6068;
--line: #E3E2DC;
--accent: #C05600; /* phosphor amber, darkened for light bg */
--accent-soft: #FFF3E6;
--run: #1A7F37; /* earned green */
--run-soft: #E7F4EA;
--danger: #C93C37;
--radius: 10px;
--font-sans: system-ui, -apple-system, "Segoe UI", sans-serif;
--font-mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
--line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
--run: #3FB950; --run-soft: #16281B; --danger: #F47067;
}
}
:root[data-theme="dark"] {
--bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
--line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
--run: #3FB950; --run-soft: #16281B; --danger: #F47067;
}
body { background: var(--bg); color: var(--ink); font-family: var(--font-sans); }
/* ---- cloud ramp: most frequent → least. Amber down into ink, then muted.
No green in here: green is earned. ---- */
:root {
--w0: #C05600; --w1: #A05512; --w2: #2B3038; --w3: #575D65; --w4: #767C84;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--w0: #FFB454; --w1: #E0913C; --w2: #E4E1DC; --w3: #A7ADB5; --w4: #838A93;
}
}
:root[data-theme="dark"] {
--w0: #FFB454; --w1: #E0913C; --w2: #E4E1DC; --w3: #A7ADB5; --w4: #838A93;
}
/* ---- shell ---- */
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body { font-size: 16px; line-height: 1.55; -webkit-text-size-adjust: 100%; overflow-x: hidden; }
.wrap { max-width: 1080px; margin: 0 auto; padding: 26px 18px 34px; }
h1 { font-size: 1.5rem; line-height: 1.2; margin: 0 0 7px; letter-spacing: -.015em; }
h2 { font-size: .95rem; margin: 0; font-weight: 600; }
.lede { margin: 0; color: var(--muted); font-size: .95rem; max-width: 72ch; }
.lede b { color: var(--ink); font-weight: 600; }
.sr-only {
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.k-lab {
font-family: var(--font-mono); font-size: .63rem; letter-spacing: .12em;
text-transform: uppercase; color: var(--muted);
}
/* ---- cards ---- */
.card {
background: var(--surface); border: 1px solid var(--line);
border-radius: var(--radius); padding: 14px; margin-top: 14px;
}
.cardhead { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px; margin-bottom: 11px; }
.cardhead .note { font-size: .78rem; color: var(--muted); margin-left: auto; }
/* ---- buttons ---- */
.btn {
font-family: var(--font-mono); font-size: .72rem; letter-spacing: .02em;
background: var(--bg); color: var(--ink); border: 1px solid var(--line);
border-radius: 999px; padding: 6px 11px; min-height: 32px; cursor: pointer;
white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis;
text-decoration: none; display: inline-flex; align-items: center; justify-content: center;
transition: border-color .15s, color .15s, background .15s;
}
.btn:hover { border-color: var(--accent); color: var(--accent); }
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.btn[aria-pressed="true"] { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
.btn[disabled], .btn[aria-disabled="true"] { opacity: .45; cursor: not-allowed; }
.btn[disabled]:hover, .btn[aria-disabled="true"]:hover { border-color: var(--line); color: var(--ink); }
.row { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.row.tight { gap: 5px; }
.pill {
font-family: var(--font-mono); font-size: .64rem; letter-spacing: .07em;
border-radius: 999px; padding: 3px 9px; border: 1px solid var(--line);
white-space: nowrap; color: var(--muted);
}
.pill.ok { background: var(--run-soft); border-color: var(--run); color: var(--run); }
.pill.warn { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
/* ---- cloud ---- */
.stage {
position: relative; border: 1px solid var(--line); border-radius: 9px;
background: var(--surface); overflow: hidden;
}
canvas#cloud { display: block; width: 100%; height: auto; touch-action: pan-y; }
canvas#cloud:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.empty {
position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
text-align: center; padding: 18px; color: var(--muted); font-size: .9rem;
pointer-events: none; max-width: 42ch; margin: 0 auto;
}
.empty[hidden] { display: none; }
.empty em { color: var(--ink); font-style: normal; font-family: var(--font-mono); font-size: .85em; }
.readout {
display: flex; flex-wrap: wrap; gap: 4px 14px; align-items: baseline;
margin-top: 9px; min-height: 1.5em;
}
.readout .rw { font-family: var(--font-mono); font-size: .92rem; color: var(--accent); }
.readout .rf { font-family: var(--font-mono); font-size: .72rem; color: var(--muted); }
.readout .rf b { color: var(--ink); font-weight: 500; font-variant-numeric: tabular-nums; }
.ramp { display: flex; align-items: center; gap: 6px; margin-left: auto; }
.ramp i { width: 15px; height: 9px; border-radius: 2px; display: block; }
.ramp .r0 { background: var(--w0); } .ramp .r1 { background: var(--w1); }
.ramp .r2 { background: var(--w2); } .ramp .r3 { background: var(--w3); }
.ramp .r4 { background: var(--w4); }
/* ---- controls ---- */
.controls { margin-top: 13px; border-top: 1px dashed var(--line); padding-top: 12px; }
.trow { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.trow label { font-family: var(--font-mono); font-size: .66rem; letter-spacing: .07em; text-transform: uppercase; color: var(--muted); }
input[type="range"] { flex: 1 1 150px; min-width: 120px; accent-color: var(--accent); height: 22px; }
input[type="range"]:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }
.tval { font-family: var(--font-mono); font-size: .84rem; min-width: 2.2em; }
.hint { font-family: var(--font-mono); font-size: .68rem; color: var(--muted); }
.hint b { color: var(--ink); font-weight: 500; font-variant-numeric: tabular-nums; }
/* ---- lower grid ---- */
.grid { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); gap: 14px; align-items: start; }
.grid > .card { margin-top: 0; }
@media (max-width: 780px) { .grid { grid-template-columns: minmax(0, 1fr); } }
.gridwrap { margin-top: 14px; }
/* ---- text input ---- */
textarea#ta {
width: 100%; min-height: 190px; resize: vertical; display: block;
font-family: var(--font-sans); font-size: .95rem; line-height: 1.6;
padding: 12px; border: 1px solid var(--line); border-radius: 9px;
background: var(--bg); color: var(--ink); caret-color: var(--accent);
}
textarea#ta:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.src { font-family: var(--font-mono); font-size: .66rem; color: var(--muted); margin-top: 7px; }
.src b { color: var(--ink); font-weight: 500; }
/* ---- table ---- */
.tablescroll { max-height: 340px; overflow: auto; margin-top: 2px; }
table.top { width: 100%; border-collapse: collapse; font-family: var(--font-mono); font-size: .74rem; }
table.top th {
text-align: left; color: var(--muted); font-weight: 400; letter-spacing: .08em;
text-transform: uppercase; font-size: .57rem; padding: 4px 6px; border-bottom: 1px solid var(--line);
position: sticky; top: 0; background: var(--surface); z-index: 1;
}
table.top th.num, table.top td.num { text-align: right; font-variant-numeric: tabular-nums; }
table.top td { padding: 0; border-bottom: 1px solid var(--line); }
table.top td.cell { padding: 4px 6px; font-variant-numeric: tabular-nums; color: var(--muted); }
table.top tr.sel td.cell { color: var(--ink); }
table.top tr.sel { background: var(--accent-soft); }
.wbtn {
display: block; width: 100%; text-align: left; background: none; border: 0;
font: inherit; color: var(--ink); padding: 4px 6px; cursor: pointer;
border-radius: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.wbtn:hover { color: var(--accent); }
.wbtn:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
tr.sel .wbtn { color: var(--accent); }
.bar { display: block; height: 5px; border-radius: 999px; background: var(--line); overflow: hidden; min-width: 34px; }
.bar > i { display: block; height: 100%; background: var(--accent); }
td.barcell { padding: 4px 6px 4px 0; width: 24%; }
.tnote { font-family: var(--font-mono); font-size: .66rem; color: var(--muted); margin-top: 9px; }
/* ---- method ---- */
details.method {
margin-top: 14px; border: 1px solid var(--line); border-radius: var(--radius);
background: var(--surface); padding: 0;
}
details.method > summary {
cursor: pointer; padding: 12px 14px; font-family: var(--font-mono); font-size: .74rem;
letter-spacing: .04em; color: var(--muted); list-style: none;
}
details.method > summary::-webkit-details-marker { display: none; }
details.method > summary::before { content: "▸ "; color: var(--accent); }
details.method[open] > summary::before { content: "▾ "; }
details.method > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: var(--radius); }
.method .body { padding: 0 14px 14px; font-size: .88rem; color: var(--muted); max-width: 74ch; }
.method .body p { margin: 9px 0; }
.method .body b { color: var(--ink); font-weight: 600; }
.method .body code { font-family: var(--font-mono); font-size: .85em; color: var(--ink); }
.stoplist {
font-family: var(--font-mono); font-size: .68rem; color: var(--muted); background: var(--bg);
border: 1px solid var(--line); border-radius: 7px; padding: 9px 11px; margin: 9px 0;
max-height: 130px; overflow: auto; line-height: 1.7; word-spacing: .1em;
}
footer { margin-top: 24px; font-family: var(--font-mono); font-size: .7rem; color: var(--muted); }
@media (max-width: 380px) {
.wrap { padding: 20px 13px 30px; }
.card { padding: 12px; }
td.barcell { display: none; }
table.top th.barh { display: none; }
}
@media (prefers-reduced-motion: reduce) {
* { transition: none !important; animation: none !important; scroll-behavior: auto !important; }
}
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>Word Cloud Builder</h1>
<p class="lede">Paste any text. It counts every word, drops the ones that carry no meaning, and
packs the rest into a cloud — <b>bigger word, more often used</b>. Runs entirely in this page,
and the picture saves as a PNG.</p>
</header>
<section class="card" aria-labelledby="cloud-h">
<div class="cardhead">
<h2 id="cloud-h">The cloud</h2>
<span class="note mono" id="fit-pill-wrap"></span>
</div>
<p class="sr-only" id="cloud-help">The cloud is a picture of the counts. Focus it and use the arrow keys to
step through its words one at a time; the same numbers are listed in the Top words table further down.</p>
<div class="stage">
<canvas id="cloud" role="img" tabindex="0" aria-label="Word cloud" aria-describedby="cloud-help"></canvas>
<div class="empty" id="empty" hidden></div>
</div>
<div class="readout">
<span class="rw" id="ro-word">—</span>
<span class="rf" id="ro-fact"></span>
<span class="ramp" aria-hidden="true">
<span class="k-lab" style="letter-spacing:.08em">often</span>
<i class="r0"></i><i class="r1"></i><i class="r2"></i><i class="r3"></i><i class="r4"></i>
<span class="k-lab" style="letter-spacing:.08em">rarely</span>
</span>
</div>
<div class="controls">
<div class="trow">
<label for="minc">used at least</label>
<input type="range" id="minc" min="1" max="6" step="1" value="1" aria-describedby="minc-hint">
<span class="tval mono" id="minc-val">1×</span>
<span class="hint" id="minc-hint"></span>
</div>
<div class="row tight" style="margin-top:11px">
<button class="btn" type="button" id="stop-toggle" aria-pressed="true">skip common words</button>
<button class="btn" type="button" id="shuffle">re‑shuffle layout</button>
<a class="btn" id="dl" download="word-cloud.png" href="#" role="button">download PNG</a>
<span class="hint" id="dl-note"></span>
</div>
</div>
<p class="sr-only" aria-live="polite" id="live"></p>
</section>
<div class="gridwrap">
<div class="grid">
<section class="card" aria-labelledby="text-h">
<div class="cardhead">
<h2 id="text-h">Your text</h2>
<span class="note mono" id="text-counts">—</span>
</div>
<div class="row tight" role="group" aria-label="Example passages" style="margin-bottom:9px">
<span class="k-lab" style="margin-right:2px">try</span>
<button class="btn" type="button" data-sample="dickens" aria-pressed="true">two cities</button>
<button class="btn" type="button" data-sample="austen" aria-pressed="false">pride & prejudice</button>
<button class="btn" type="button" data-sample="carroll" aria-pressed="false">alice</button>
<button class="btn" type="button" data-sample="melville" aria-pressed="false">moby‑dick</button>
</div>
<label class="sr-only" for="ta">Text to build the cloud from</label>
<textarea id="ta" spellcheck="false"></textarea>
<p class="src" id="src"></p>
</section>
<section class="card" aria-labelledby="tbl-h">
<div class="cardhead">
<h2 id="tbl-h">Top words</h2>
<span class="note">pick one to find it in the cloud</span>
</div>
<div class="tablescroll">
<table class="top">
<thead>
<tr>
<th class="num" style="width:2.4em">#</th>
<th>word</th>
<th class="num">count</th>
<th class="num">share</th>
<th class="barh"><span class="sr-only">relative frequency</span></th>
</tr>
</thead>
<tbody id="tbody"></tbody>
</table>
</div>
<p class="tnote" id="tnote"></p>
</section>
</div>
</div>
<details class="method">
<summary>how the words are counted and packed — and why clouds lie a little</summary>
<div class="body">
<p><b>Counting.</b> The text is lowercased and cut into words on anything that is not a letter, a digit
or an apostrophe. A trailing <code>'s</code> is dropped, so <code>sister's</code> and <code>sister</code>
are the same word. Nothing is stemmed beyond that: <code>times</code> and <code>time</code> stay apart,
because guessing at stems quietly invents counts that are not in the text.</p>
<p><b>Common words.</b> With <i>skip common words</i> on, the words below are thrown away before counting.
They are the glue of English — frequent everywhere, telling you nothing about this particular text.
Switch it off and watch <code>the</code> swallow the picture: that is what the list is for.</p>
<div class="stoplist" id="stoplist"></div>
<p><b>Sizing and colour.</b> A word’s size follows <code>(count ÷ top count)</code> raised to the
power <code>0.55</code> — a ratio, not a stretch from the rarest word to the busiest one, so a text
whose most-used word appears twice does not read like a landslide. Colour is the other half of the story:
each word is dropped into one of five bands by where its count sits between the rarest and the most
frequent, amber for the top band down to muted grey for the bottom.</p>
<p><b>Packing.</b> Words are laid down largest first. Each one walks out from the centre along an
Archimedean spiral until it finds a spot where its box touches nothing already placed, then creeps back
toward the centre while it still fits. After the first three, about one word in five is turned on its
side. The whole set of sizes is then binary-searched up or down until the biggest scale that fits the
canvas is found — which is why the cloud fills the frame at any window width.</p>
<p><b>Where it lies.</b> A word cloud is a bad instrument and a good picture. Area grows faster than the
eye reads it, so a word used ten times does not look ten times bigger than one used once; long words
take more space than short ones at the same count; and position means nothing at all — a word near
the middle just got placed early. The table on the right holds the honest numbers. Use the cloud to
notice, the table to compare.</p>
<p>Everything here runs in this page: no network requests, no storage, no analytics. Your text stays in the tab,
and the PNG is drawn from the same canvas you are looking at.</p>
</div>
</details>
<footer>demo · sloprun.dev</footer>
</div>
<script>
(function () {
"use strict";
var $ = function (id) { return document.getElementById(id); };
function clamp(v, a, b) { return v < a ? a : v > b ? b : v; }
function plural(n, word) { return n === 1 ? word : word + "s"; }
function esc(s) {
return String(s).replace(/[&<>"]/g, function (c) {
return c === "&" ? "&" : c === "<" ? "<" : c === ">" ? ">" : """;
});
}
var SANS = 'system-ui, -apple-system, "Segoe UI", sans-serif';
var WEIGHTS = [700, 600, 600, 500, 500];
var TAU = Math.PI * 2;
/* ------------------------------------------------------------------ *
* public-domain passages
* ------------------------------------------------------------------ */
var SAMPLES = {
dickens: {
src: "Charles Dickens · A Tale of Two Cities (1859) · opening lines",
text: "It was the best of times, it was the worst of times, it was the age of wisdom, it was the age of foolishness, it was the epoch of belief, it was the epoch of incredulity, it was the season of Light, it was the season of Darkness, it was the spring of hope, it was the winter of despair, we had everything before us, we had nothing before us, we were all going direct to Heaven, we were all going direct the other way."
},
austen: {
src: "Jane Austen · Pride and Prejudice (1813) · chapter one",
text: "It is a truth universally acknowledged, that a single man in possession of a good fortune, must be in want of a wife.\n\nHowever little known the feelings or views of such a man may be on his first entering a neighbourhood, this truth is so well fixed in the minds of the surrounding families, that he is considered as the rightful property of some one or other of their daughters.\n\n“My dear Mr. Bennet,” said his lady to him one day, “have you heard that Netherfield Park is let at last?”\n\nMr. Bennet replied that he had not.\n\n“But it is,” returned she; “for Mrs. Long has just been here, and she told me all about it.”\n\nMr. Bennet made no answer.\n\n“Do you not want to know who has taken it?” cried his wife impatiently.\n\n“You want to tell me, and I have no objection to hearing it.”\n\nThis was invitation enough.\n\n“Why, my dear, you must know, Mrs. Long says that Netherfield is taken by a young man of large fortune from the north of England; that he came down on Monday in a chaise and four to see the place, and was so much delighted with it that he agreed with Mr. Morris immediately; that he is to take possession before Michaelmas, and some of his servants are to be in the house by the end of next week.”\n\n“What is his name?”\n\n“Bingley.”\n\n“Is he married or single?”\n\n“Oh! single, my dear, to be sure! A single man of large fortune; four or five thousand a year. What a fine thing for our girls!”"
},
carroll: {
src: "Lewis Carroll · Alice's Adventures in Wonderland (1865) · chapter one",
text: "Alice was beginning to get very tired of sitting by her sister on the bank, and of having nothing to do: once or twice she had peeped into the book her sister was reading, but it had no pictures or conversations in it, “and what is the use of a book,” thought Alice “without pictures or conversations?”\n\nSo she was considering in her own mind (as well as she could, for the hot day made her feel very sleepy and stupid), whether the pleasure of making a daisy-chain would be worth the trouble of getting up and picking the daisies, when suddenly a White Rabbit with pink eyes ran close by her.\n\nThere was nothing so very remarkable in that; nor did Alice think it so very much out of the way to hear the Rabbit say to itself, “Oh dear! Oh dear! I shall be late!” But when the Rabbit actually took a watch out of its waistcoat-pocket, and looked at it, and then hurried on, Alice started to her feet, for it flashed across her mind that she had never before seen a rabbit with either a waistcoat-pocket, or a watch to take out of it, and burning with curiosity, she ran across the field after it, and fortunately was just in time to see it pop down a large rabbit-hole under the hedge.\n\nIn another moment down went Alice after it, never once considering how in the world she was to get out again. The rabbit-hole went straight on like a tunnel for some way, and then dipped suddenly down, so suddenly that Alice had not a moment to think about stopping herself before she found herself falling down a very deep well."
},
melville: {
src: "Herman Melville · Moby-Dick (1851) · opening of “Loomings”",
text: "Call me Ishmael. Some years ago — never mind how long precisely — having little or no money in my purse, and nothing particular to interest me on shore, I thought I would sail about a little and see the watery part of the world. It is a way I have of driving off the spleen and regulating the circulation.\n\nWhenever I find myself growing grim about the mouth; whenever it is a damp, drizzly November in my soul; whenever I find myself involuntarily pausing before coffin warehouses, and bringing up the rear of every funeral I meet; and especially whenever my hypos get such an upper hand of me, that it requires a strong moral principle to prevent me from deliberately stepping into the street, and methodically knocking people's hats off — then, I account it high time to get to sea as soon as I can.\n\nThere now is your insular city of the Manhattoes, belted round by wharves, and commerce surrounds it with her surf. Right and left, the streets take you waterward. Look at the crowds of water-gazers there. There is nothing surprising in this. If they but knew it, almost all men in their degree, some time or other, cherish very nearly the same feelings towards the ocean with me."
}
};
/* ------------------------------------------------------------------ *
* stopwords — the glue of English
* ------------------------------------------------------------------ */
var STOPWORDS = ("a about above after again against all also am an and any are aren't as at be because been " +
"before being below between both but by can can't cannot could couldn't did didn't do does doesn't doing " +
"don't down during each few for from further had hadn't has hasn't have haven't having he he'd he'll he's " +
"her here here's hers herself him himself his how how's i i'd i'll i'm i've if in into is isn't it it's its " +
"itself just let's me more most mustn't my myself no nor not of off on once only or other ought our ours " +
"ourselves out over own same shan't she she'd she'll she's should shouldn't so some such than that that's " +
"the their theirs them themselves then there there's these they they'd they'll they're they've this those " +
"through to too under until up upon very was wasn't we we'd we'll we're we've were weren't what what's when " +
"when's where where's which while who who's whom why why's with won't would wouldn't you you'd you'll " +
"you're you've your yours yourself yourselves o s t").split(" ");
var STOP = Object.create(null);
for (var si = 0; si < STOPWORDS.length; si++) { STOP[STOPWORDS[si]] = 1; }
/* ------------------------------------------------------------------ *
* tokenising
* ------------------------------------------------------------------ */
var TOKEN_RE;
try { TOKEN_RE = new RegExp("[\\p{L}\\p{N}][\\p{L}\\p{N}'’]*", "gu"); }
catch (e) { TOKEN_RE = /[A-Za-z0-9][A-Za-z0-9'’]*/g; }
function tokenize(text) {
var raw = String(text).toLowerCase().replace(/’/g, "'");
var out = raw.match(TOKEN_RE) || [];
var res = [], i, w;
for (i = 0; i < out.length; i++) {
w = out[i].replace(/'+$/, "");
if (/'s$/.test(w) && w.length > 2) { w = w.slice(0, -2); }
if (w) { res.push(w); }
}
return res;
}
function tally(text, useStop) {
var toks = tokenize(text);
var map = Object.create(null), i, w, skipped = 0, kept = 0;
for (i = 0; i < toks.length; i++) {
w = toks[i];
if (useStop && STOP[w]) { skipped++; continue; }
kept++;
map[w] = (map[w] || 0) + 1;
}
var list = [];
for (w in map) { list.push({ w: w, n: map[w] }); }
list.sort(function (a, b) { return b.n - a.n || (a.w < b.w ? -1 : a.w > b.w ? 1 : 0); });
return { list: list, total: toks.length, kept: kept, skipped: skipped, unique: list.length };
}
/* ------------------------------------------------------------------ *
* seeded rng
* ------------------------------------------------------------------ */
function mulberry32(a) {
return function () {
a |= 0; a = (a + 0x6D2B79F5) | 0;
var t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
/* ------------------------------------------------------------------ *
* layout — spiral packing with an axis-aligned box test
* ------------------------------------------------------------------ */
var cv = $("cloud");
var ctx = cv.getContext("2d");
var mcv = document.createElement("canvas");
var mctx = mcv.getContext("2d");
function hits(list, x, y, w, h) {
for (var i = list.length - 1; i >= 0; i--) {
var b = list[i];
if (x < b.x + b.w && x + w > b.x && y < b.y + b.h && y + h > b.y) { return true; }
}
return false;
}
/* how many words a canvas this size can hold without turning to grit */
function wordBudget(W, H) {
return clamp(Math.round((W * H) / 1900), 30, 90);
}
function tryLayout(items, W, H, seed, scale, abortOnFail) {
var rng = mulberry32(seed >>> 0);
var cx = W / 2, cy = H / 2;
/* soften the aspect stretch so the cloud fills height as well as width */
var ex = W >= H ? Math.pow(W / H, 0.6) : 1;
var ey = W >= H ? 1 : Math.pow(H / W, 0.6);
var maxR = Math.max(W, H) * 0.66;
var maxSize = Math.min(H * 0.24, W * 0.34);
var minSize = Math.max(11, Math.min(21, H * 0.038));
var growth = Math.max(0.85, minSize * 0.13);
var placed = [], boxes = [], unplaced = 0;
var n = items.length;
var maxN = items[0].n, minN = items[n - 1].n;
for (var i = 0; i < n; i++) {
var it = items[i];
/* size on the ratio of counts, not on min-max stretch: a text whose
busiest word appears twice must not read like a 4:1 landslide. */
var ratio = Math.pow(it.n / maxN, 0.55);
var t = (maxN === minN) ? 1 : (it.n - minN) / (maxN - minN);
var size = Math.max(8, Math.round(clamp(maxSize * ratio, minSize, maxSize) * scale));
var band = clamp(Math.floor((1 - t) * 5), 0, 4);
var weight = WEIGHTS[band];
mctx.font = weight + " " + size + "px " + SANS;
var m = mctx.measureText(it.w);
var asc = m.actualBoundingBoxAscent || size * 0.74;
var desc = m.actualBoundingBoxDescent || size * 0.22;
var cw = m.width, ch = asc + desc;
var pad = Math.max(3, size * 0.16);
var flip = rng(), a0 = rng() * TAU;
var rot = (i >= 3 && flip < 0.2) ? 1 : 0;
var bw = (rot ? ch : cw) + pad;
var bh = (rot ? cw : ch) + pad;
if (rot && (bw > W - 2 || bh > H - 2)) {
rot = 0; bw = cw + pad; bh = ch + pad;
}
if (bw > W - 2 || bh > H - 2) {
unplaced++;
if (abortOnFail) { return { placed: placed, boxes: boxes, unplaced: unplaced }; }
continue;
}
var tt = 0, r = 0, ok = false, px = 0, py = 0, iter = 0;
while (iter++ < 3000) {
tt += Math.max(0.05, (size * 0.30) / Math.max(8, r));
r = growth * tt;
if (r > maxR) { break; }
var x = cx + r * ex * Math.cos(tt + a0) - bw / 2;
var y = cy + r * ey * Math.sin(tt + a0) - bh / 2;
if (x < 1 || y < 1 || x + bw > W - 1 || y + bh > H - 1) { continue; }
if (!hits(boxes, x, y, bw, bh)) { ok = true; px = x; py = y; break; }
}
if (!ok) {
unplaced++;
if (abortOnFail) { return { placed: placed, boxes: boxes, unplaced: unplaced }; }
continue;
}
/* creep back toward the centre while it still fits */
var tx = cx - (px + bw / 2), ty = cy - (py + bh / 2);
var d = Math.sqrt(tx * tx + ty * ty);
if (d > 1) {
var sx = (tx / d) * 2, sy = (ty / d) * 2;
for (var s = 0; s < 24; s++) {
var nx = px + sx, ny = py + sy;
if (nx < 1 || ny < 1 || nx + bw > W - 1 || ny + bh > H - 1) { break; }
if (hits(boxes, nx, ny, bw, bh)) { break; }
if ((cx - (nx + bw / 2)) * tx + (cy - (ny + bh / 2)) * ty <= 0) { break; }
px = nx; py = ny;
}
}
boxes.push({ x: px, y: py, w: bw, h: bh });
placed.push({
word: it.w, n: it.n, x: px, y: py, bw: bw, bh: bh, size: size, weight: weight,
band: band, rot: rot, asc: asc, desc: desc, src: i
});
}
return { placed: placed, boxes: boxes, unplaced: unplaced };
}
function layout(items, W, H, seed) {
if (!items.length || W < 40 || H < 40) { return { placed: [], boxes: [], unplaced: 0 }; }
var lo = 0.28, hi = 2.1, found = false, bestScale = lo, mid, res, i;
for (i = 0; i < 6; i++) {
mid = (lo + hi) / 2;
res = tryLayout(items, W, H, seed, mid, true);
if (res.unplaced === 0) { found = true; bestScale = mid; lo = mid; } else { hi = mid; }
}
return tryLayout(items, W, H, seed, found ? bestScale : lo, false);
}
/* ------------------------------------------------------------------ *
* palette
* ------------------------------------------------------------------ */
var PAL = { ramp: ["#C05600", "#A05512", "#2B3038", "#575D65", "#767C84"], surface: "#FFFFFF", accent: "#C05600", soft: "#FFF3E6" };
function readPalette() {
var cs = getComputedStyle(document.documentElement);
function v(name, fb) { var s = cs.getPropertyValue(name); s = s && s.trim(); return s || fb; }
PAL.ramp = [v("--w0", "#C05600"), v("--w1", "#A05512"), v("--w2", "#2B3038"), v("--w3", "#575D65"), v("--w4", "#767C84")];
PAL.surface = v("--surface", "#FFFFFF");
PAL.accent = v("--accent", "#C05600");
PAL.soft = v("--accent-soft", "#FFF3E6");
PAL.muted = v("--muted", "#5A6068");
}
/* ------------------------------------------------------------------ *
* state
* ------------------------------------------------------------------ */
var state = {
text: SAMPLES.dickens.text,
sample: "dickens",
useStop: true,
minCount: 1,
seed: 20250814,
placed: [], boxes: [], posOf: {}, unplaced: 0,
counted: null, shown: [],
sel: -1, hover: -1,
W: 0, H: 0, dpr: 1,
reveal: 1
};
var reduceMotion = false;
try { reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; } catch (e) { reduceMotion = false; }
/* ------------------------------------------------------------------ *
* drawing
* ------------------------------------------------------------------ */
function roundRect(c, x, y, w, h, r) {
if (typeof c.roundRect === "function") { c.beginPath(); c.roundRect(x, y, w, h, r); return; }
c.beginPath();
c.moveTo(x + r, y);
c.arcTo(x + w, y, x + w, y + h, r);
c.arcTo(x + w, y + h, x, y + h, r);
c.arcTo(x, y + h, x, y, r);
c.arcTo(x, y, x + w, y, r);
c.closePath();
}
function draw() {
var W = state.W, H = state.H;
ctx.setTransform(state.dpr, 0, 0, state.dpr, 0, 0);
ctx.fillStyle = PAL.surface;
ctx.fillRect(0, 0, W, H);
var list = state.placed;
var upto = Math.ceil(list.length * clamp(state.reveal, 0, 1));
/* selection/hover are indexes into state.shown; map them onto the cloud */
var wanted = state.hover >= 0 ? state.hover : state.sel;
var mark = (wanted >= 0 && state.posOf[wanted] !== undefined) ? state.posOf[wanted] : -1;
for (var i = 0; i < upto; i++) {
var p = list[i];
if (i === mark) {
ctx.fillStyle = PAL.soft;
ctx.strokeStyle = PAL.accent;
ctx.lineWidth = 1.5;
roundRect(ctx, p.x - 1, p.y - 1, p.bw + 2, p.bh + 2, 5);
ctx.fill();
ctx.stroke();
}
ctx.save();
ctx.translate(p.x + p.bw / 2, p.y + p.bh / 2);
if (p.rot) { ctx.rotate(-Math.PI / 2); }
ctx.font = p.weight + " " + p.size + "px " + SANS;
ctx.textAlign = "center";
ctx.textBaseline = "alphabetic";
ctx.fillStyle = (i === mark) ? PAL.accent : PAL.ramp[p.band];
ctx.fillText(p.word, 0, (p.asc - p.desc) / 2);
ctx.restore();
}
}
var revealRAF = 0;
function animateIn() {
if (revealRAF) { cancelAnimationFrame(revealRAF); revealRAF = 0; }
if (reduceMotion || !state.placed.length) { state.reveal = 1; draw(); return; }
var t0 = 0, dur = 420;
state.reveal = 0;
function step(ts) {
if (!t0) { t0 = ts; }
var k = clamp((ts - t0) / dur, 0, 1);
state.reveal = 1 - Math.pow(1 - k, 2);
draw();
if (k < 1) { revealRAF = requestAnimationFrame(step); } else { revealRAF = 0; }
}
revealRAF = requestAnimationFrame(step);
}
/* ------------------------------------------------------------------ *
* PNG export
* ------------------------------------------------------------------ */
var pngTimer = 0, pngUrl = "";
function schedulePNG() {
if (pngTimer) { clearTimeout(pngTimer); }
var dl = $("dl");
dl.setAttribute("aria-disabled", "true");
$("dl-note").innerHTML = "";
pngTimer = setTimeout(function () {
pngTimer = 0;
if (!state.placed.length) {
/* keep the anchor focusable — the click handler blocks the dead link */
dl.setAttribute("href", "#");
dl.setAttribute("aria-disabled", "true");
$("dl-note").innerHTML = "";
pngUrl = "";
return;
}
/* guarantee a complete frame in the export, whatever the reveal is doing */
if (state.reveal < 1) { state.reveal = 1; draw(); }
var url;
try { url = cv.toDataURL("image/png"); } catch (e) { url = ""; }
if (!url) { return; }
pngUrl = url;
dl.href = url;
dl.removeAttribute("aria-disabled");
var bytes = Math.round((url.length - (url.indexOf(",") + 1)) * 0.75);
var kb = bytes > 1024 * 1024
? (bytes / 1048576).toFixed(1) + " MB"
: Math.round(bytes / 1024) + " KB";
$("dl-note").innerHTML = cv.width + "×" + cv.height + " · <b>" + kb + "</b>";
}, reduceMotion ? 240 : 560);
}
/* ------------------------------------------------------------------ *
* render pipeline
* ------------------------------------------------------------------ */
function sizeCanvas() {
var stage = cv.parentNode;
var w = Math.max(200, Math.round(stage.clientWidth));
var h = Math.round(clamp(w * 0.60, 250, 480));
var dpr = Math.min(2, window.devicePixelRatio || 1);
state.W = w; state.H = h; state.dpr = dpr;
cv.style.height = h + "px";
cv.width = Math.round(w * dpr);
cv.height = Math.round(h * dpr);
}
function fmtPct(x) {
if (x >= 10) { return Math.round(x) + "%"; }
return (Math.round(x * 10) / 10) + "%";
}
function renderTable() {
var rows = state.shown.slice(0, 40);
var tb = $("tbody");
if (!rows.length) {
tb.innerHTML = '<tr><td class="cell" colspan="5">no words at this setting</td></tr>';
$("tnote").textContent = "";
return;
}
var top = rows[0].n, html = "", i;
/* exactly one row must stay in the tab order, even when the selection
lives past the 40 rows this table shows */
var focusIdx = (state.sel >= 0 && state.sel < rows.length) ? state.sel : 0;
for (i = 0; i < rows.length; i++) {
var r = rows[i];
var share = state.counted.kept ? (r.n / state.counted.kept) * 100 : 0;
html += '<tr data-i="' + i + '"' + (i === state.sel ? ' class="sel"' : "") + ">" +
'<td class="cell num">' + (i + 1) + "</td>" +
'<td><button type="button" class="wbtn" data-i="' + i + '" tabindex="' +
(i === focusIdx ? "0" : "-1") + '">' + esc(r.w) + "</button></td>" +
'<td class="cell num">' + r.n + "</td>" +
'<td class="cell num">' + fmtPct(share) + "</td>" +
'<td class="barcell"><span class="bar"><i style="width:' +
Math.max(4, Math.round((r.n / top) * 100)) + '%"></i></span></td>' +
"</tr>";
}
tb.innerHTML = html;
var more = state.shown.length - rows.length;
$("tnote").innerHTML = more > 0
? "showing top 40 of <b>" + state.shown.length + "</b> words · share is of counted words"
: "<b>" + state.shown.length + "</b> word" + (state.shown.length === 1 ? "" : "s") + " · share is of counted words";
}
function describeCloud() {
if (!state.placed.length) { return "Word cloud: nothing to show yet."; }
var names = [];
for (var i = 0; i < Math.min(5, state.placed.length); i++) {
names.push(state.placed[i].word + " (" + state.placed[i].n + ")");
}
return "Word cloud of " + state.placed.length + plural(state.placed.length, " word") +
". Most frequent: " + names.join(", ") + ".";
}
function setReadout(idx) {
var p = idx >= 0 && idx < state.shown.length ? state.shown[idx] : null;
if (!p) {
$("ro-word").textContent = state.shown.length ? state.shown[0].w : "—";
var f = "";
if (state.shown.length) {
f = state.shown[0].n === 1
? "every word here is used once"
: "the most used word here · <b>" + state.shown[0].n + "</b> times";
}
$("ro-fact").innerHTML = f;
return;
}
var share = state.counted.kept ? (p.n / state.counted.kept) * 100 : 0;
$("ro-word").textContent = p.w;
$("ro-fact").innerHTML = "used <b>" + p.n + "</b>" + plural(p.n, " time") +
" · <b>" + fmtPct(share) + "</b> of counted words · rank <b>" + (idx + 1) + "</b>";
}
var liveTimer = 0;
function announce(msg) {
if (liveTimer) { clearTimeout(liveTimer); }
liveTimer = setTimeout(function () { $("live").textContent = msg; }, 500);
}
function build(keepSel) {
sizeCanvas();
var budget = wordBudget(state.W, state.H);
var counted = tally(state.text, state.useStop);
state.counted = counted;
/* min-count slider bounds follow the text */
var maxN = counted.list.length ? counted.list[0].n : 1;
var sliderMax = clamp(maxN, 1, 8);
var minc = $("minc");
minc.max = String(sliderMax);
if (state.minCount > sliderMax) { state.minCount = sliderMax; }
minc.value = String(state.minCount);
minc.disabled = sliderMax <= 1;
$("minc-val").innerHTML = state.minCount + "×";
var shown = [], i;
for (i = 0; i < counted.list.length; i++) {
if (counted.list[i].n >= state.minCount) { shown.push(counted.list[i]); }
}
var overflow = Math.max(0, shown.length - budget);
if (overflow) { shown = shown.slice(0, budget); }
state.shown = shown;
$("minc-hint").innerHTML = state.minCount === 1
? "every word counts once or more · <b>" + counted.unique + "</b> unique"
: "<b>" + shown.length + "</b> of <b>" + counted.unique + "</b> unique words survive";
if (overflow) {
$("minc-hint").innerHTML += " · biggest <b>" + budget + "</b> fit this canvas";
}
$("text-counts").innerHTML = counted.total
? counted.total + plural(counted.total, " word") + " · " + counted.unique + " unique"
: "empty";
/* layout + paint */
var res = layout(shown, state.W, state.H, state.seed);
state.placed = res.placed;
state.boxes = res.boxes;
state.unplaced = res.unplaced;
state.posOf = {};
for (i = 0; i < res.placed.length; i++) { state.posOf[res.placed[i].src] = i; }
if (!keepSel) { state.sel = -1; }
if (state.sel >= state.shown.length) { state.sel = -1; }
state.hover = -1;
var empty = $("empty");
if (!shown.length) {
empty.hidden = false;
empty.innerHTML = counted.total
? "Every word was filtered out. Lower the <em>used at least</em> slider, or switch off <em>skip common words</em>."
: "Paste some text on the left, or pick one of the passages.";
} else {
empty.hidden = true;
}
cv.setAttribute("aria-label", describeCloud());
/* fit pill — green only when the packer really did place everything */
var pill = $("fit-pill-wrap");
if (!shown.length) {
pill.innerHTML = "";
} else if (state.unplaced === 0) {
pill.innerHTML = '<span class="pill ok">' + (state.placed.length === 1
? "1 word placed"
: "all " + state.placed.length + " words placed") + "</span>";
} else {
pill.innerHTML = '<span class="pill warn">' + state.placed.length + " placed · " +
state.unplaced + " would not fit</span>";
}
renderTable();
setReadout(state.sel);
animateIn();
schedulePNG();
announce(shown.length
? state.placed.length + plural(state.placed.length, " word") + " in the cloud, most frequent " +
shown[0].w + " at " + shown[0].n + plural(shown[0].n, " use") + "."
: "No words to show at this setting.");
}
/* redraw only (theme swap, selection) */
function repaint() {
if (revealRAF) { return; }
draw();
}
/* ------------------------------------------------------------------ *
* interaction
* ------------------------------------------------------------------ */
/* returns an index into state.shown, or -1 */
function hitTest(x, y) {
for (var i = state.boxes.length - 1; i >= 0; i--) {
var b = state.boxes[i];
if (x >= b.x && x <= b.x + b.w && y >= b.y && y <= b.y + b.h) { return state.placed[i].src; }
}
return -1;
}
cv.addEventListener("mousemove", function (e) {
var r = cv.getBoundingClientRect();
var i = hitTest(e.clientX - r.left, e.clientY - r.top);
if (i !== state.hover) {
state.hover = i;
cv.style.cursor = i >= 0 ? "pointer" : "default";
setReadout(i >= 0 ? i : state.sel);
repaint();
}
});
cv.addEventListener("mouseleave", function () {
if (state.hover !== -1) {
state.hover = -1;
cv.style.cursor = "default";
setReadout(state.sel);
repaint();
}
});
cv.addEventListener("click", function (e) {
var r = cv.getBoundingClientRect();
var i = hitTest(e.clientX - r.left, e.clientY - r.top);
if (i >= 0) { select(i, true); }
});
cv.addEventListener("keydown", function (e) {
var n = state.shown.length;
if (!n) { return; }
var i = state.sel, moved = true;
if (e.key === "ArrowRight" || e.key === "ArrowDown") { i = i < 0 ? 0 : Math.min(n - 1, i + 1); }
else if (e.key === "ArrowLeft" || e.key === "ArrowUp") { i = i <= 0 ? 0 : i - 1; }
else if (e.key === "Home") { i = 0; }
else if (e.key === "End") { i = n - 1; }
else { moved = false; }
if (!moved) { return; }
e.preventDefault();
select(i, false);
});
function select(i, scrollRow) {
state.sel = i;
state.hover = -1;
setReadout(i);
var rows = $("tbody").querySelectorAll("tr"), k, row = null;
for (k = 0; k < rows.length; k++) {
if (Number(rows[k].getAttribute("data-i")) === i) { rows[k].classList.add("sel"); row = rows[k]; }
else { rows[k].classList.remove("sel"); }
}
var btns = $("tbody").querySelectorAll(".wbtn");
for (k = 0; k < btns.length; k++) {
btns[k].tabIndex = Number(btns[k].getAttribute("data-i")) === i ? 0 : -1;
}
if (!row && btns.length) { btns[0].tabIndex = 0; }
if (scrollRow && row) {
try { row.scrollIntoView({ block: "nearest" }); } catch (err) { /* ignore */ }
}
var p = state.shown[i];
if (p) {
announce(p.w + ", used " + p.n + plural(p.n, " time") + ", rank " + (i + 1) + ".");
}
repaint();
}
$("tbody").addEventListener("click", function (e) {
var b = e.target.closest ? e.target.closest(".wbtn") : null;
if (!b) { return; }
select(Number(b.getAttribute("data-i")), false);
});
$("tbody").addEventListener("keydown", function (e) {
var btns = $("tbody").querySelectorAll(".wbtn");
if (!btns.length) { return; }
var cur = document.activeElement && document.activeElement.classList.contains("wbtn")
? Number(document.activeElement.getAttribute("data-i")) : -1;
if (cur < 0) { return; }
var i = cur, moved = true;
if (e.key === "ArrowDown") { i = Math.min(btns.length - 1, cur + 1); }
else if (e.key === "ArrowUp") { i = Math.max(0, cur - 1); }
else if (e.key === "Home") { i = 0; }
else if (e.key === "End") { i = btns.length - 1; }
else { moved = false; }
if (!moved) { return; }
e.preventDefault();
select(i, true);
var next = $("tbody").querySelectorAll(".wbtn")[i];
if (next) { next.focus(); }
});
/* text input */
var inputTimer = 0;
$("ta").addEventListener("input", function () {
state.text = this.value;
if (state.sample) {
state.sample = "";
document.querySelectorAll("[data-sample]").forEach(function (b) { b.setAttribute("aria-pressed", "false"); });
$("src").innerHTML = "source · <b>your own text</b>";
}
if (inputTimer) { clearTimeout(inputTimer); }
inputTimer = setTimeout(function () { build(false); }, 260);
});
document.querySelectorAll("[data-sample]").forEach(function (b) {
b.addEventListener("click", function () {
var key = b.getAttribute("data-sample");
state.sample = key;
state.text = SAMPLES[key].text;
$("ta").value = state.text;
$("src").innerHTML = "source · <b>" + esc(SAMPLES[key].src) + "</b> · public domain";
document.querySelectorAll("[data-sample]").forEach(function (o) {
o.setAttribute("aria-pressed", o === b ? "true" : "false");
});
state.minCount = 1;
build(false);
});
});
$("minc").addEventListener("input", function () {
state.minCount = parseInt(this.value, 10) || 1;
$("minc-val").innerHTML = state.minCount + "×";
build(false);
});
$("stop-toggle").addEventListener("click", function () {
state.useStop = !state.useStop;
this.setAttribute("aria-pressed", state.useStop ? "true" : "false");
build(false);
});
$("shuffle").addEventListener("click", function () {
state.seed = (Math.imul(state.seed, 1103515245) + 12345) >>> 0;
build(true);
});
$("dl").addEventListener("click", function (e) {
if (this.getAttribute("aria-disabled") === "true" || !pngUrl) { e.preventDefault(); }
});
/* resize */
var resizeTimer = 0, lastW = 0;
function onResize() {
var w = Math.round(cv.parentNode.clientWidth);
if (Math.abs(w - lastW) < 4) { return; }
lastW = w;
if (resizeTimer) { clearTimeout(resizeTimer); }
resizeTimer = setTimeout(function () { build(true); }, 160);
}
if (typeof ResizeObserver === "function") {
new ResizeObserver(onResize).observe(cv.parentNode);
} else {
window.addEventListener("resize", onResize);
}
/* theme: platform message, OS preference, or data-theme attribute */
window.addEventListener("message", function (e) {
var d = e.data;
if (d && d.type === "sloprun:theme" && (d.theme === "light" || d.theme === "dark")) {
document.documentElement.setAttribute("data-theme", d.theme);
}
});
function themeChanged() { readPalette(); draw(); schedulePNG(); }
new MutationObserver(themeChanged).observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });
try {
var mq = window.matchMedia("(prefers-color-scheme: dark)");
if (mq.addEventListener) { mq.addEventListener("change", themeChanged); }
else if (mq.addListener) { mq.addListener(themeChanged); }
} catch (e2) { /* ignore */ }
/* ------------------------------------------------------------------ *
* boot — never an empty form
* ------------------------------------------------------------------ */
(function boot() {
var list = [], i;
for (i = 0; i < STOPWORDS.length; i++) { list.push(STOPWORDS[i]); }
list.sort();
$("stoplist").textContent = list.join(" ");
$("ta").value = state.text;
$("src").innerHTML = "source · <b>" + esc(SAMPLES.dickens.src) + "</b> · public domain";
readPalette();
lastW = Math.round(cv.parentNode.clientWidth);
build(false);
})();
})();
</script>
</body>
</html>
source-visible by construction · nothing is published here without its code
Comments
0 totalNo comments yet. If you ran it, say what happened.