xxxxxxxxxx
172
// Implementation of Anders Hoff's Spline Script algorithm
// https://inconvergent.net/2017/spline-script/
// Left click to draw a variation of the current alphabet.
// Right click to generate a new alphabet.
const alphabetLength = 26;
const glyphSize = 1;
const glyphPoints = 5; // must be at least 3
const glyphSquashX = 0.25;
const glyphRotation = 0.3;
const zoom = 15.0 / glyphSize;
const lineHeight = 2.5;
const letterSpacing = 0;
const spaceSize = 1.5;
// the source text itself isn't that important -
// it's there mostly to give a realistic sense of
// word length distribution and spacing
const sourceText = `You walk for days among trees and among stones.
Rarely does the eye light on a thing, and then only
when it has recognized that thing as the sign of
another thing: a print in the sand indicates the
tiger's passage; a marsh announces a vein of water;
the hibiscus flower, the end of winter. All the rest is
silent and interchangeable; trees and stones are only
what they are`;
const sourceWords = sourceText
.toLowerCase()
.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, '')
.replace(/\s{2,}/g, ' ')
.split(' ');
let alphabet = [];
function setup() {
createCanvas(windowWidth, windowHeight);
createAlphabet();
noLoop();
}
function draw() {
background(255);
stroke(0);
push();
translate(glyphSize * zoom, glyphSize * zoom);
let offsetX = 0;
let offsetY = 0;
for (let word of sourceWords) {
const wordCharCodes = word.split('').map(c => c.charCodeAt(0) - 'a'.charCodeAt(0));
const wordGlyphs = wordCharCodes.map(charCode => alphabet[constrain(charCode, 0, alphabet.length - 1)]);
const wordSize = wordGlyphs.reduce((sum, g) => sum + g.size, 0);
if ((offsetX + wordSize + spaceSize * glyphSize) * zoom > windowWidth) {
offsetX = 0;
offsetY += glyphSize * lineHeight;
if (offsetY > windowHeight) {
break;
}
}
push();
scale(zoom);
strokeWeight(1 / zoom);
translate(offsetX, offsetY);
drawWord(wordGlyphs);
offsetX += wordSize + spaceSize * glyphSize;
pop();
}
pop();
}
function mousePressed() {
if (mouseButton === LEFT) {
redraw();
} else {
createAlphabet();
redraw();
}
}
function createAlphabet() {
alphabet = [];
for (let i = 0; i < alphabetLength; i++) {
alphabet.push(createGlyph(random(glyphSize + 0.25 * glyphSize, glyphSize + 0.25 * glyphSize), max(glyphPoints, 3)));
}
}
function createGlyph(size, numCentroids) {
const centroids = [];
for (let i = 0; i < numCentroids; i++) {
centroids.push(randFromTiltedEllipsoid(size));
}
centroids.sort(angleToOriginComparator);
const delaunay = d3.Delaunay.from(centroids, p => p.x, p => p.y);
const voronoi = delaunay.voronoi([0, 0, width, height]);
return {
size: size,
centroids: centroids,
voronoi: voronoi
}
}
function drawWord(glyphs) {
noFill();
beginShape();
for (let i = 0; i < glyphs.length; i++) {
const glyph = glyphs[i];
const pts = generateGlyphVariation(glyph);
for (let pt of pts) {
let x = pt.x + i * glyph.size;
if (i > 0) {
x += letterSpacing * glyphSize;
}
curveVertex(x, pt.y);
}
}
endShape();
}
function generateGlyphVariation(glyph) {
const pts = [];
// for each centroid, find a random point inside
// that centroid's voronoi cell
for (let i = 0; i < glyph.centroids.length; i++) {
let pt = randFromTiltedEllipsoid(glyph.size);
while(!glyph.voronoi.contains(i, pt.x, pt.y)) {
pt = randFromTiltedEllipsoid(glyph.size);
}
pts.push(pt);
}
return pts;
}
// squash and rotate a random point from a circle
function randFromTiltedEllipsoid(radius) {
const pt = randFromCircle(radius);
pt.x *= glyphSquashX;
pt.rotate(glyphRotation * PI);
return pt;
}
// https://stackoverflow.com/questions/5837572/generate-a-random-point-within-a-circle-uniformly#answer-50746409
function randFromCircle(radius) {
const r = radius * sqrt(random());
const theta = random() * 2 * PI;
return createVector(r * cos(theta), r * sin(theta));
}
// sort points by their angle to the origin
function angleToOriginComparator(a, b) {
const thetaA = atan2(a.y - 0, a.x - 0);
const thetaB = atan2(b.y - 0, b.x - 0);
if (thetaA < thetaB) {
return -1;
} else if (thetaA > thetaB) {
return 1;
}
return 0;
}