Nick Montfort and Stephanie Strickland: Sea and Spar Between
/ Sea and Spar Between
// by Nick Montfort and Stephanie Strickland
//
// a poetry generator which defines a space of language
// populated by a number of stanzas comparable to the number
// of fish in the sea, around 225 trillion
//
// index.html, reading.html, and style.css are part of Sea and Spar Between
// canvastext.js is a required, public domain file by Jim Studt
// Use the version of canvastext.js that accompanies this file, as
// minor changes have been made to it.
//
//
// Copyright (c) 2010, Nick Montfort and Stephanie Strickland
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
// * Neither the names of the copyright holders nor the names of any other
// contributor may be used to endorse or promote products derived from this
// software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
// The following two lines are used for checking the code with jslint:
/*jslint browser: true*/
/*global window unescape CanvasTextFunctions*/
var mouseX = 0, mouseY = 0;
// The following two lines set the initial lattice coordinates. This is the
// only code that involves randomness. If you prefer a completely
// deterministic experience, you can add something of the form "?X,Y"
// (for example, "?0,0" or "?12345,22680") to the URL to start Sea and Spar
// at the same specified place:
var baseI = Math.round(Math.random() * 14992383);
var baseJ = Math.round(Math.random() * 14992383);
var canvas, context;
var fontSize = 12;
var lineHeight, column, stanzaHeight, spacingX, spacingY;
// Data: Words and short phrases that are combined by the generator --
var shortPhrase = ['circle on', 'dash on', 'let them', 'listen now', 'loop on', 'oh time', 'plunge on', 'reel on',
'roll on', 'run on', 'spool on', 'steady', 'swerve me?', 'turn on', 'wheel on', 'whirl on',
'you--too--', 'fast-fish', 'loose-fish'];
// Common Dickinson nouns grouped by number of syllables:
var dickinsonNoun = [
['air', 'art', 'care', 'door', 'dust', 'each', 'ear', 'earth', 'fair', 'faith', 'fear', 'friend', 'gold', 'grace',
'grass', 'grave', 'hand', 'hill', 'house', 'joy', 'keep', 'leg', 'might', 'mind', 'morn', 'name', 'need', 'noon', 'pain',
'place', 'play', 'rest', 'rose', 'show', 'sight', 'sky', 'snow', 'star', 'thought', 'tree', 'well', 'wind', 'world', 'year'],
['again', 'alone', 'better', 'beyond', 'delight', 'dying', 'easy', 'enough', 'ever', 'father', 'flower', 'further',
'himself', 'human', 'morning', 'myself', 'power', 'purple', 'single', 'spirit', 'today'],
['another', 'paradise'],
['eternity'],
['immortality']
];
var courseStart = ['fix upon the ', 'cut to fit the ', 'how to withstand the '];
// The following syllables are combined into compound words:
var dickinsonSyllable = ['bard', 'bead', 'bee', 'bin', 'bliss', 'blot', 'blur', 'buzz', 'curl', 'dirt', 'disk', 'doll',
'drum', 'fern', 'film', 'folk', 'germ', 'hive', 'hood', 'husk', 'jay', 'pink', 'plot', 'spun', 'toll', 'web'];
var melvilleSyllable = ['ash', 'bag', 'buck', 'bull', 'bunk', 'cane', 'chap', 'chop', 'clam', 'cock', 'cone',
'dash', 'dock', 'edge', 'eel', 'fin', 'goat', 'hag', 'hawk', 'hook', 'hoop', 'horn', 'howl', 'iron', 'jack',
'jaw', 'kick', 'kin', 'lime', 'loon', 'lurk', 'milk', 'net', 'pike', 'rag', 'rail', 'ram', 'sack', 'salt', 'tool'];
var syllable = dickinsonSyllable;
syllable.concat(melvilleSyllable);
syllable.sort();
// Dickinson's poems include many words ending in "less," such as "artless."
// Some of their stems (such as "art") follow, grouped by number of syllables.
var dickinsonLessLess = [
['art', 'base', 'blame', 'crumb', 'cure', 'date', 'death', 'drought', 'fail', 'flesh', 'floor', 'foot',
'frame', 'fruit', 'goal', 'grasp', 'guile', 'guilt', 'hue', 'key', 'league', 'list', 'need', 'note', 'pang',
'pause', 'phrase', 'pier', 'plash', 'price', 'shame', 'shape', 'sight', 'sound', 'star', 'stem', 'stint',
'stir', 'stop', 'swerve', 'tale', 'taste', 'thread', 'worth'],
['arrest', 'blanket', 'concern', 'costume', 'cypher', 'degree', 'desire', 'dower', 'efface', 'enchant',
'escape', 'fashion', 'flavor', 'honor', 'kinsman', 'marrow', 'perceive', 'perturb', 'plummet', 'postpone', 'recall',
'record', 'reduce', 'repeal', 'report', 'retrieve', 'tenant'],
['latitude', 'retriever']
];
// Makes one long list of all "less" stems, combining stems of any number of
// syllables:
var dickinsonFlatLessLess = dickinsonLessLess[0];
dickinsonFlatLessLess.concat(dickinsonLessLess[1], dickinsonLessLess[2]);
dickinsonFlatLessLess.sort();
// Verbs that can suggest a positive mood:
var upVerb = ['bask', 'chime', 'dance', 'go', 'leave', 'move', 'rise', 'sing', 'speak', 'step', 'turn', 'walk'];
var butBeginning = ['but', 'for', 'then'];
var butEnding = ['earth', 'sea', 'sky', 'sun'];
var threeToFiveSyllable = dickinsonNoun[2];
threeToFiveSyllable.concat(dickinsonNoun[3] + dickinsonNoun[4] + dickinsonLessLess[2]);
var twoSyllable = dickinsonNoun[1];
twoSyllable.concat(dickinsonLessLess[1]);
var nailedEnding = ['coffin', 'deck', 'desk', 'groove', 'mast', 'spar', 'pole', 'plank', 'rail', 'room', 'sash'];
// Functions: These generate each type of line, assemble stanzas, draw the
// lattice of stanzas in the browser window, and handle input and other events.
// First line functions:
function shortLine(n)
{
return shortPhrase[n % shortPhrase.length];
}
function oneNounLine(n)
{
var a, b, c, d = n % dickinsonNoun[0].length;
n = Math.floor(n / dickinsonNoun[0].length);
c = n % dickinsonNoun[0].length;
n = Math.floor(n / dickinsonNoun[0].length);
b = n % dickinsonNoun[0].length;
n = Math.floor(n / dickinsonNoun[0].length);
a = n % dickinsonNoun[0].length;
return 'one ' + dickinsonNoun[0][a] + ' one ' + dickinsonNoun[0][b] + ' one ' + dickinsonNoun[0][c] + ' one ' + dickinsonNoun[0][d];
}
function compoundCourseLine(n)
{
var a, b, c = n % syllable.length;
n = Math.floor(n / syllable.length);
b = n % syllable.length;
n = Math.floor(n / syllable.length);
a = n % courseStart.length;
return courseStart[a] + syllable[b] + syllable[c] + ' course';
}
function firstLine(n)
// The first line of a pair is one of the three types above.
{
var m = Math.floor(n / 4);
if (n % 4 < 2)
{
return shortLine(m);
}
if (n % 4 === 2)
{
return oneNounLine(m);
}
return compoundCourseLine(m);
}
// Second line functions:
function riseAndGoLine(n)
{
var a, b, c = n % upVerb.length, dash = '';
n = Math.floor(n / upVerb.length);
b = n % upVerb.length;
n = Math.floor(n / upVerb.length);
a = n % dickinsonFlatLessLess.length;
if (dickinsonFlatLessLess[a] in dickinsonLessLess[0])
{
dash = ' --';
}
return dickinsonFlatLessLess[a] + 'less ' + upVerb[b] + ' and ' + upVerb[c] + dash;
}
function butLine(n)
{
var a, b, c = n % butEnding.length;
n = Math.floor(n / butEnding.length);
b = n % dickinsonFlatLessLess.length;
n = Math.floor(n / dickinsonFlatLessLess.length);
a = n % butBeginning.length;
return butBeginning[a] + ' ' + dickinsonFlatLessLess[b] + 'less is the ' + butEnding[c];
}
function exclaimLine(n)
{
var a, b = n % twoSyllable.length;
n = Math.floor(n / twoSyllable.length);
a = n % threeToFiveSyllable.length;
return threeToFiveSyllable[a] + '! ' + twoSyllable[b] + '!';
}
function nailedLine(n)
{
var a = n % nailedEnding.length;
return 'nailed to the ' + nailedEnding[a];
}
function secondLine(n)
// The second line of a pair is one of the four types above.
{
var m = Math.floor(n / 4);
if (n % 4 === 0)
{
return riseAndGoLine(m);
}
if (n % 4 === 1)
{
return butLine(m);
}
if (n % 4 === 2)
{
return exclaimLine(m);
}
return nailedLine(m);
}
// Functions related to drawing text and handling events:
function drawPair(i, j, x, y)
// Displays two lines in the browser window. The drawing of these is done
// by calling the drawText method (in canvastext.js) using the graphical
// coordinates x, y. The lines themselves are determined by the functions
// firstLine and secondLine (above), which are given the lattice
// coordinates i, j.
{
y += lineHeight;
context.drawText('sans', fontSize, x, y, firstLine(i + j + 1));
y += lineHeight;
context.drawText('sans', fontSize, x, y, ' ' + secondLine(Math.abs(i - j) + 1));
}
function readCoords()
// Parses the coordinates in the URL (if there are any) and uses those as
// the base lattice coordinates.
{
var params = window.location.search, a;
if (params.substring(0, 1) === '?')
{
params = params.substring(1);
}
params = params.split(',');
for (a = 0; a < params.length; a += 1)
{
params[a] = unescape(params[a]);
}
return params;
}
function drawCoords(i, j, x, y)
// Displays the lattice coordinates of the central stanza above that stanza.
{
var stroke = context.strokeStyle;
context.strokeStyle = "rgba(255,255,255,1.2)";
context.drawText('sans', 12, x, y, i + ' : ' + j);
context.strokeStyle = stroke;
}
function canonical(value)
// Converts any integer to a "canonical" lattice coordinate -- a value
// at least 0 and at most 14992383 -- to handle negative and very large
// inputs. This makes the "sea" a torus, looping in both the right/left
// direction and in the up/down direction.
// The large number of lines of the form "one _ one _ one _ one _"
// determined the number of possible values in each direction, 14992384.
{
value = value % 14992384;
if (value < 0)
{
value = value + 14992384;
}
return value;
}
function drawLattice(startI, startJ)
// The main function that draws the entire visible portion of the lattice
// in the browser window.
{
var startX, startY, i, j, x, y;
// Draw the background:
context.fillStyle = "rgba(199,220,254,1)";
context.fillRect(0, 0, canvas.width, canvas.height);
startX = (canvas.width - column) / 2; // X position of central stanza.
startY = (canvas.height - stanzaHeight) / 2; // Y position.
// Draw the coordinate of that stanza:
drawCoords(canonical(baseI + startI), canonical(baseJ + startJ), startX, startY);
// At this point startX and startY indicate where the central stanza
// will be drawn. They need to be adjusted if the window is large
// enough or font small enough to accomodate other stanzas.
while (startX > 0) { // Until we are at 0 or off the page to the left,
startX -= spacingX; // step back one space ...
startI -= 1; // so we can draw the previous, (i-1)th stanza
} // to the left.
while (startY > 0) { // Until we are at 0 or off the top of the page,
startY -= spacingY; // step up one stanza ...
startJ -= 1; // so we can draw the previous stanza with
} // the pair of lines (j-2) and (j-1) up above.
i = canonical(baseI + startI);
// i now holds the correct first lattice coordinate for the upper left
// stanza.
for (x = startX; x <= canvas.width; x += spacingX)
{
j = canonical((baseJ + startJ) * 2);
// The multiplication by two is so that the lattice moves up and down
// two pairs (one stanza) at a time. If this weren't done the breaks
// between stanzas would not be maintained.
for (y = startY; y <= canvas.height; y += spacingY - lineHeight * 3)
{
// A stanza is drawn by drawing one pair of lines, then another.
drawPair(i, j, x, y, lineHeight);
j = canonical(j + 1);
y += lineHeight * 3;
drawPair(i, j, x, y, lineHeight);
j = canonical(j + 1);
}
i = canonical(i + 1);
}
}
function changeFontSize(delta)
// Change the font size by adding delta, determine the new spacing, redraw.
{
fontSize += delta;
fontSize = Math.max(4, fontSize);
lineHeight = context.fontAscent('sans', fontSize) + context.fontDescent('sans', fontSize);
column = fontSize * 22;
stanzaHeight = lineHeight * 5;
spacingX = fontSize * 38;
spacingY = stanzaHeight * 2;
drawLattice(parseInt(mouseX / 3, 10), parseInt(mouseY / 3, 10));
}
function updateWheel(e)
// Make the appropriate change if the mouse wheel has moved.
{
var evt, wheel;
evt = window.event || e; // Select available event object.
wheel = evt.detail ? evt.detail * (-120) : evt.wheelDelta;
if (wheel > 0)
{
changeFontSize(1);
}
else
{
changeFontSize(-1);
}
}
function markStanza()
// Place the coordinates of the central stanza in the box.
{
var textInput = document.getElementById("coords");
textInput.value = canonical(baseI + parseInt(mouseX / 3, 10)) + ",";
textInput.value += canonical(baseJ + parseInt(mouseY / 3, 10));
}
function keyDown(e)
// Handle key presses.
{
var key = String.fromCharCode(e.keyCode);
if (key === "a" || key === "A")
{
changeFontSize(1);
}
else if (key === "z" || key === "Z")
{
changeFontSize(-1);
}
else if (key === ' ')
{
markStanza();
}
// The rest of these if statements handle the arrow keys.
else if (e.keyCode === 37)
{
baseI = canonical(baseI - 1);
drawLattice(parseInt(mouseX / 3, 10), parseInt(mouseY / 3, 10));
}
else if (e.keyCode === 38)
{
baseJ = canonical(baseJ - 1);
drawLattice(parseInt(mouseX / 3, 10), parseInt(mouseY / 3, 10));
}
else if (e.keyCode === 39)
{
baseI = canonical(baseI + 1);
drawLattice(parseInt(mouseX / 3, 10), parseInt(mouseY / 3, 10));
}
else if (e.keyCode === 40)
{
baseJ = canonical(baseJ + 1);
drawLattice(parseInt(mouseX / 3, 10), parseInt(mouseY / 3, 10));
}
}
function mouseMove(e)
// Handle mouse movement.
{
mouseX = e.clientX;
mouseY = e.clientY;
drawLattice(parseInt(mouseX / 3, 10), parseInt(mouseY / 3, 10));
return false;
}
function mouseClick(e)
// Shift to a new region if a click happened near an edge,
// redraw in any case.
{
if (mouseX > canvas.width * 2 / 3)
{
baseI += parseInt(canvas.width / 3, 10);
}
else if (mouseX < canvas.width / 3)
{
baseI -= parseInt(canvas.width / 3, 10);
}
if (mouseY > canvas.height * 2 / 3)
{
baseJ += parseInt(canvas.height / 3, 10);
}
else if (mouseY < canvas.height / 3)
{
baseJ -= parseInt(canvas.height / 3, 10);
}
drawLattice(parseInt(mouseX / 3, 10), parseInt(mouseY / 3, 10));
return false;
}
function resizeCanvas(e)
// When the browser window is resized, resize the canvas.
{
var div = document.getElementsByTagName('div')[0];
canvas.width = div.scrollWidth;
canvas.height = div.scrollHeight;
context.strokeStyle = "rgba(0,0,128,0.75)";
drawLattice(0, 0);
}
function setBase(coords)
// Sets the base lattice coordinates if the new array of strings is valid.
// Otherwise, the existing baseI and baseJ values remain.
{
newI = parseInt(coords[0], 10);
newJ = parseInt(coords[1], 10);
if (!isNaN(newI) && !isNaN(newJ))
{
baseI = newI;
baseJ = newJ;
}
}
function setup()
// Runs when the page is loaded. Initializes the canvas, event listeners, etc.
{
var div, newI, newJ, mouseWheelEvent, params = readCoords();
if (params.length === 2)
{
setBase(params);
}
canvas = document.getElementsByTagName('canvas')[0];
if (!canvas.getContext)
{
return;
}
// Add event listeners for mouse movement, mouse click, key down:
canvas.onmousemove = mouseMove;
canvas.onclick = mouseClick;
document.onkeydown = keyDown;
mouseWheelEvent = (/Firefox/i.test(navigator.userAgent)) ? "DOMMouseScroll" : "mousewheel";
if (document.attachEvent) // For IE (and Opera depending on user setting).
{
document.attachEvent("on" + mouseWheelEvent, updateWheel);
}
else if (document.addEventListener) // For WC3 browsers.
{
document.addEventListener(mouseWheelEvent, updateWheel, false);
}
// Add the text functions to the context:
context = canvas.getContext('2d');
CanvasTextFunctions.enable(context);
changeFontSize(0);
window.onresize = resizeCanvas;
resizeCanvas(null);
markStanza();
}
function go()
// Called when someone has pressed "enter" in the navigation box; updates coordinates.
{
var textInput, coordPair, URL;
textInput = document.getElementById("coords");
coordPair = textInput.value;
coordPair = coordPair.split(' ').join('');
coordPair = coordPair.split(':').join(',');
setBase(coordPair.split(','));
drawLattice(0, 0);
}