Skip to content

Instantly share code, notes, and snippets.

@mattdesl
Created July 10, 2018 18:18
Show Gist options
  • Save mattdesl/ad219d45fb2baa6c85f0b10a4e57e73a to your computer and use it in GitHub Desktop.
Save mattdesl/ad219d45fb2baa6c85f0b10a4e57e73a to your computer and use it in GitHub Desktop.
code dump of "Light Rail" sketch — https://twitter.com/mattdesl/status/1016337483594850304
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