Skip to content

Instantly share code, notes, and snippets.

@mattdesl
Last active June 11, 2019 01:36
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mattdesl/444f1c967f31535bc5982012c74c63df to your computer and use it in GitHub Desktop.
Save mattdesl/444f1c967f31535bc5982012c74c63df to your computer and use it in GitHub Desktop.
pen plotterable circle pack demo with D3 + canvas-sketch + utils https://github.com/mattdesl/canvas-sketch

To run with latest canvas-sketch-cli, copy the contents of sketch.js and then:

mkdir my-sketch
cd my-sketch

# Run a development server
pbpaste | canvas-sketch sketch.js --new --open

# Later, you can build it to a static HTML site
canvas-sketch sketch.js --name=index --build --inline --dir=build/ --title=circles

See canvas-sketch for details/docs.

// 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);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment