Created
July 10, 2018 18:18
-
-
Save mattdesl/ad219d45fb2baa6c85f0b10a4e57e73a to your computer and use it in GitHub Desktop.
code dump of "Light Rail" sketch — https://twitter.com/mattdesl/status/1016337483594850304
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const canvasSketch = require('canvas-sketch'); // not yet released – DM me for details ! | |
const { vec2 } = require('gl-matrix'); | |
const { grid } = require('./util/procedural'); | |
const { clamp01 } = require('./util/math'); | |
const painter = require('./util/canvas-painter'); | |
const Random = require('./util/random'); | |
const { Harmonizer } = require('color-harmony'); | |
Random.setSeed(Random.getRandomSeed()); | |
const settings = { | |
scaleToFit: true, | |
prefix: 'light-rail', | |
suffix: Random.getSeed(), | |
dimensions: [ 2048, 2048 ] | |
}; | |
const manhattan = (a, b) => { | |
return Math.abs(a[0] - b[0]) + Math.abs(a[1] - b[1]); | |
}; | |
const sketch = ({ context, width, height }) => { | |
const getNeighbour = (point, points) => { | |
if (points.length <= 0) return; | |
const max = typeof maxNeighbourDistance === 'function' ? maxNeighbourDistance(point) : maxNeighbourDistance; | |
const neighbours = points | |
.filter(p => p !== point) | |
.map(other => { | |
return { | |
position: other, | |
distance: manhattan(point, other) | |
}; | |
}) | |
.filter(p => p.distance > 0 && p.distance <= max); | |
if (neighbours.length <= 0) return null; | |
return Random.pick(neighbours).position; | |
}; | |
const kill = (list, point) => { | |
const idx = list.indexOf(point); | |
if (idx >= 0) list.splice(idx, 1); | |
}; | |
const seek = (list, point) => { | |
const neighbour = getNeighbour(point, remaining); | |
if (neighbour) { | |
const direction = vec2.sub([], point, neighbour); | |
vec2.normalize(direction, direction); | |
let angleDegrees = Math.round(Math.atan2(direction[1], direction[0]) * 180 / Math.PI); | |
if (angleDegrees % 45 === 0) { | |
return neighbour; | |
} | |
} | |
return null; | |
}; | |
const paint = painter(context); | |
const margin = width * 0.1; | |
const center = [ width / 2, height / 2 ]; | |
const target = center; | |
const stationsLow = 20; | |
const stationsHigh = 50; | |
const minStations = 0; | |
const dropoff = 0.65; | |
const thickness = 20; | |
const square = false; | |
const drawCircles = true; | |
const count = 25; | |
const monochrome = false; | |
const killSurrounding = 10; | |
const killSurroundingChance = 0.2; | |
const maxNeighbourDistance = (p) => { | |
const center2 = vec2.add([], center, Random.insideCircle(width / 8)); | |
const dist = 1 - clamp01(vec2.distance(p, center2) / (width * 0.45)); | |
return dist * Math.abs(Random.gaussian(0, 1)) * width * Random.range(0.5, 2); | |
}; | |
const hsl = () => { | |
const hue = Random.range(0, 1); | |
const sat = clamp01(Random.range(0.45, 0.55) + Math.abs(Random.gaussian(0, 1 / 3.14 / 2))); | |
const light = clamp01(Random.range(0.45, 0.55) + Math.abs(Random.gaussian(0, 1 / 3.14 / 2))); | |
return `hsl(${Math.floor(hue * 360)}, ${Math.floor(sat * 100)}%, ${Math.floor(light * 100)}%)`; | |
}; | |
const getMonochrome = (dark) => { | |
const palette = [ 'white', 'black' ]; | |
if (dark) palette.reverse(); | |
return palette; | |
}; | |
const dark = Random.boolean(); | |
let palette = monochrome | |
? getMonochrome(dark) | |
: new Harmonizer()[dark ? 'shades' : 'tints'](hsl(), 5); | |
const background = palette.shift(); | |
const gridOpts = { | |
min: [ margin, margin ], | |
max: [ width - margin, height - margin ], | |
count | |
}; | |
let paletteIndex = 0; | |
let points = grid(gridOpts).map(p => p.position); | |
points = Random.shuffle(points).filter(p => Random.chance(dropoff)); | |
const lines = []; | |
let remaining = (points.slice()); | |
remaining = remaining.map(position => { | |
return { | |
position, | |
distance: vec2.distance(target, position) | |
}; | |
}).sort((a, b) => a.distance - b.distance).map(p => p.position); | |
remaining.forEach(point => { | |
let stations = Random.rangeFloor(stationsLow, stationsHigh); | |
let head = point; | |
const line = [ head.slice() ]; | |
for (let i = 0; i < stations; i++) { | |
const found = seek(remaining, head); | |
if (!found) break; | |
if (i > 0) kill(remaining, head); | |
kill(remaining, found); | |
head = found; | |
line.push(head.slice()); | |
} | |
if (line.length > 1 && line.length > minStations) { | |
kill(remaining, point); | |
line.forEach(point => kill(remaining, point)); | |
if (Random.chance(killSurroundingChance)) { | |
line.forEach(station => { | |
for (let i = 0; i < killSurrounding; i++) { | |
const nearest = getNeighbour(station, remaining); | |
if (nearest) kill(remaining, nearest); | |
} | |
}); | |
} | |
const color = palette[paletteIndex++ % palette.length] | |
const colorIndex = palette.indexOf(color); | |
lines.push({ | |
lineWidth: 1, | |
color, | |
colorIndex, | |
path: line | |
}); | |
} | |
}); | |
return () => { | |
paint.clear({ width, height, fill: background }); | |
if (drawCircles) { | |
remaining.forEach(position => { | |
if (square) { | |
const boxSize = thickness; | |
paint.rect({ | |
position: [ position[0] - boxSize / 2, position[1] - boxSize / 2 ], | |
fill: palette[0], | |
width: boxSize, | |
height: boxSize | |
}); | |
} else { | |
paint.circle({ position, radius: thickness / 2, alpha: 1, fill: palette[0] }); | |
} | |
}); | |
} | |
lines.forEach(({ path, lineWidth, color }) => paint.polyline(path, { | |
stroke: color, | |
lineJoin: 'round', | |
lineCap: square ? 'butt' : 'round', | |
lineWidth: thickness * lineWidth | |
})); | |
}; | |
}; | |
canvasSketch(sketch, settings); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment