|
// Import various utils |
|
const canvasSketch = require('canvas-sketch'); |
|
const d3 = require('d3'); |
|
const Random = require('canvas-sketch-util/random'); |
|
const { clamp, linspace } = require('canvas-sketch-util/math'); |
|
const { renderPolylines } = require('canvas-sketch-util/penplot'); |
|
const { createHatchLines, clipLineToCircle, clipPolylinesToBox } = require('canvas-sketch-util/geometry'); |
|
const risoColors = require('riso-colors').map(c => c.hex); |
|
|
|
const settings = { |
|
dimensions: [ 2048, 2048 ] |
|
}; |
|
|
|
const sketch = ({ canvas, width, height, render }) => { |
|
document.body.style.cursor = 'pointer'; |
|
|
|
// Clip margin around entire artwork |
|
const paperMargin = width * 0.05; |
|
|
|
// Padding between each circle |
|
const padding = width * 0.005; |
|
|
|
// Global scale of our simulation, smaller = tighter |
|
const graphScale = 0.025; |
|
|
|
// A base line width for all circles |
|
const lineWidth = width * 0.005; |
|
|
|
// Some factors to modulate the line width per circle |
|
const minLineSpaceFactor = 1.5; |
|
const maxLineSpaceFactor = 6; |
|
const spaceFactorMean = 1; |
|
const spaceFactorDeviation = 2; |
|
|
|
// Factors to scale the circles |
|
const circleMean = 0.25; |
|
const circleDeviation = 3; |
|
|
|
// Colors which will be set on new generations |
|
let background, foreground; |
|
|
|
// Start with an empty array, we will re-populate on new generations |
|
let nodes = []; |
|
|
|
// Setup a D3 simulation that re-renders each tick |
|
const simulation = d3.forceSimulation().on('tick', () => { |
|
// re-render the frame |
|
render(); |
|
}); |
|
|
|
// Setup the simulation parameters |
|
simulation |
|
.force('charge', d3.forceManyBody().strength(10).distanceMin(padding / 4)) |
|
.force('center', d3.forceCenter(width / 2, height / 2)) |
|
.force('collision', d3.forceCollide().radius(d => d.radius + padding) |
|
.strength(0.75) |
|
.iterations(5) |
|
) |
|
.stop(); |
|
|
|
const reset = () => { |
|
// Choose new colors, randomly picking 2 riso colors |
|
const colors = Random.shuffle(risoColors).slice(0, 2); |
|
background = colors[0]; |
|
foreground = colors[1]; |
|
|
|
// Setup a new array of N nodes |
|
const numNodes = Random.rangeFloor(25, 200); |
|
const curGraphScale = graphScale; |
|
nodes = linspace(numNodes).map(() => { |
|
// Determine a nice value to modulaate the line spacing |
|
const spaceFactor = Math.abs(Random.gaussian(spaceFactorMean, spaceFactorDeviation)); |
|
// Get a starting point within a circle |
|
const [ x, y ] = Random.insideCircle(width * 0.25); |
|
return { |
|
// Updated by D3 |
|
x, |
|
y, |
|
// circle radius for collision |
|
radius: width * Math.abs(Random.gaussian(circleMean, circleDeviation)) * curGraphScale, |
|
// line spacing |
|
spacing: lineWidth * clamp(spaceFactor, minLineSpaceFactor, maxLineSpaceFactor), |
|
// hatch angle |
|
angle: Random.range(-1, 1) * Math.PI * 2 |
|
}; |
|
}); |
|
|
|
// Re-run the simulation from the start |
|
simulation |
|
.nodes(nodes) |
|
.alpha(1) |
|
.restart(); |
|
}; |
|
|
|
// Reset initially & on click |
|
canvas.addEventListener('click', reset); |
|
reset(); |
|
|
|
return (props) => { |
|
const { width, height } = props; |
|
|
|
// Gather a list of polylines from each node |
|
let polylines = []; |
|
nodes.map(node => { |
|
const { x, y, radius, angle, spacing } = node; |
|
|
|
// Get the bounding box of the circle |
|
const bounds = [ |
|
[ x - radius, y - radius ], |
|
[ x + radius, y + radius ] |
|
]; |
|
|
|
const circlePosition = [ x, y ]; |
|
|
|
// Get a series of 'hatch fill' lines from the boundin gbox |
|
const hatchLines = createHatchLines(bounds, angle, spacing); |
|
|
|
// Now clip each hatch line to a segment within the circle |
|
hatchLines.forEach(hatch => { |
|
// Here we clip the line to the circle |
|
const hits = []; |
|
const clipped = clipLineToCircle(hatch[0], hatch[1], circlePosition, radius, hits); |
|
// If the hatch isn't inside the circle, or we don't receive 2 intersections, skip this |
|
if (!clipped || hits.length !== 2) { |
|
return; |
|
} |
|
// Otherwise, add this segment |
|
polylines.push(hits); |
|
}); |
|
}); |
|
|
|
// Let's clip all lines to a margin around the artwork |
|
// otherwise we may have lines hitting the very edge of the page |
|
const pageBounds = [ |
|
[ paperMargin, paperMargin ], |
|
[ width - paperMargin, height - paperMargin ] |
|
]; |
|
polylines = clipPolylinesToBox(polylines, pageBounds); |
|
return renderPolylines(polylines, { |
|
...props, |
|
lineWidth, |
|
background, |
|
foreground |
|
}); |
|
}; |
|
}; |
|
|
|
canvasSketch(sketch, settings); |