├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── code ├── patchwork.js ├── squares.js ├── test-print.js └── triangulation.js ├── images ├── 3d.jpg ├── canvas-all.jpg ├── canvas1.png ├── canvas2.png ├── canvas3.png ├── code1.png ├── code10.png ├── code11.png ├── code12.png ├── code13.png ├── code2.png ├── code3.png ├── code4.png ├── code5.png ├── code6.png ├── code7.png ├── code8.png ├── code9.png ├── ex-1.png ├── ex-2.png ├── ex-3.png ├── exb1.jpg ├── exb2.jpg ├── exb3.jpg ├── fracture.gif ├── header.jpg ├── molnar.jpg ├── patchwork.jpg ├── penplot1.png ├── plotter-wide.jpg ├── render.png └── tess-v3.jpg ├── markdown ├── Part-1.md ├── Part-2.md └── README.md ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | *.log 4 | .DS_Store 5 | bundle.js 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | *.log 4 | .DS_Store 5 | bundle.js 6 | test 7 | test.js 8 | demo/ 9 | .npmignore 10 | LICENSE.md -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2017 Matt DesLauriers 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 20 | OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pen-plotter-blog-post 2 | 3 | This repo contains the source code and images for a two-part blog post, _"Pen Plotter Art & Algorithms."_ You can read the posts on my blog: 4 | 5 | - [Pen Plotter Art & Algorithms, Part 1](https://mattdesl.svbtle.com/pen-plotter-1) 6 | - [Pen Plotter Art & Algorithms, Part 2](https://mattdesl.svbtle.com/pen-plotter-2) 7 | 8 | ## Usage 9 | 10 | If you want to run from source, make sure you have `node@8.4.x` and `npm@5.3.x` or higher. Then clone this repo, `cd` into it, and `npm install` to grab the dependencies. 11 | 12 | Once the dependencies are installed, you can run one of the demos: 13 | 14 | ```sh 15 | # the default penplot print template 16 | npx penplot code/test-print.js --open 17 | 18 | # simple concentric squares 19 | npx penplot code/squares.js --open 20 | 21 | # Delaunay triangulation example 22 | npx penplot code/triangulation.js --open 23 | 24 | # Patchwork (fractures with convex hull + k-means clustering) 25 | npx penplot code/patchwork.js --open 26 | ``` 27 | 28 | ## License 29 | 30 | MIT, see [LICENSE.md](http://github.com/mattdesl/pen-plotter-blog-post/blob/master/LICENSE.md) for details. -------------------------------------------------------------------------------- /code/patchwork.js: -------------------------------------------------------------------------------- 1 | import { PaperSize, Orientation } from 'penplot'; 2 | import { polylinesToSVG } from 'penplot/util/svg'; 3 | import { randomFloat } from 'penplot/util/random'; 4 | import newArray from 'new-array'; 5 | import clustering from 'density-clustering'; 6 | import convexHull from 'convex-hull'; 7 | 8 | export const orientation = Orientation.LANDSCAPE; 9 | export const dimensions = PaperSize.SQUARE_POSTER; 10 | 11 | const debug = false; 12 | 13 | export default function createPlot (context, dimensions) { 14 | const [ width, height ] = dimensions; 15 | 16 | // A large point count will produce more defined results 17 | const pointCount = 50000; 18 | let points = newArray(pointCount).map(() => { 19 | const margin = 2; 20 | return [ 21 | randomFloat(margin, width - margin), 22 | randomFloat(margin, height - margin) 23 | ]; 24 | }); 25 | 26 | // We will add to this over time 27 | const lines = []; 28 | 29 | // The N value for k-means clustering 30 | // Lower values will produce bigger chunks 31 | const clusterCount = 3; 32 | 33 | // Run our generative algorithm at 30 FPS 34 | setInterval(update, 1000 / 30); 35 | 36 | return { 37 | draw, 38 | print, 39 | background: 'white', 40 | animate: true // start a render loop 41 | }; 42 | 43 | function update () { 44 | // Not enough points in our data set 45 | if (points.length <= clusterCount) return; 46 | 47 | // k-means cluster our data 48 | const scan = new clustering.KMEANS(); 49 | const clusters = scan.run(points, clusterCount) 50 | .filter(c => c.length >= 3); 51 | 52 | // Ensure we resulted in some clusters 53 | if (clusters.length === 0) return; 54 | 55 | // Sort clusters by density 56 | clusters.sort((a, b) => a.length - b.length); 57 | 58 | // Select the least dense cluster 59 | const cluster = clusters[0]; 60 | const positions = cluster.map(i => points[i]); 61 | 62 | // Find the hull of the cluster 63 | const edges = convexHull(positions); 64 | 65 | // Ensure the hull is large enough 66 | if (edges.length <= 2) return; 67 | 68 | // Create a closed polyline from the hull 69 | let path = edges.map(c => positions[c[0]]); 70 | path.push(path[0]); 71 | 72 | // Add to total list of polylines 73 | lines.push(path); 74 | 75 | // Remove those points from our data set 76 | points = points.filter(p => !positions.includes(p)); 77 | } 78 | 79 | function draw () { 80 | lines.forEach(points => { 81 | context.beginPath(); 82 | points.forEach(p => context.lineTo(p[0], p[1])); 83 | context.strokeStyle = debug ? 'blue' : 'black'; 84 | context.stroke(); 85 | }); 86 | 87 | // Turn on debugging if you want to see the points 88 | if (debug) { 89 | points.forEach(p => { 90 | context.beginPath(); 91 | context.arc(p[0], p[1], 0.2, 0, Math.PI * 2); 92 | context.strokeStyle = 'red'; 93 | context.stroke(); 94 | }); 95 | } 96 | } 97 | 98 | function print () { 99 | return polylinesToSVG(lines, { 100 | dimensions 101 | }); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /code/squares.js: -------------------------------------------------------------------------------- 1 | import { PaperSize, Orientation } from 'penplot'; 2 | import { polylinesToSVG } from 'penplot/util/svg'; 3 | 4 | export const orientation = Orientation.LANDSCAPE; 5 | export const dimensions = PaperSize.SQUARE_POSTER; 6 | 7 | export default function createPlot (context, dimensions) { 8 | const [ width, height ] = dimensions; 9 | 10 | // Function to create a square 11 | const square = (x, y, size) => { 12 | // Define rectangle vertices 13 | const path = [ 14 | [ x - size, y - size ], 15 | [ x + size, y - size ], 16 | [ x + size, y + size ], 17 | [ x - size, y + size ] 18 | ]; 19 | // Close the path 20 | path.push(path[0]); 21 | return path; 22 | }; 23 | 24 | // Get centre of the print 25 | const cx = width / 2; 26 | const cy = height / 2; 27 | 28 | // Create 12 concentric pairs of squares 29 | const lines = []; 30 | for (let i = 0; i < 12; i++) { 31 | const size = i + 1; 32 | const margin = 0.25; 33 | lines.push(square(cx, cy, size)); 34 | lines.push(square(cx, cy, size + margin)); 35 | } 36 | 37 | return { 38 | draw, 39 | print, 40 | background: 'white' 41 | }; 42 | 43 | function draw () { 44 | lines.forEach(points => { 45 | context.beginPath(); 46 | points.forEach(p => context.lineTo(p[0], p[1])); 47 | context.stroke(); 48 | }); 49 | } 50 | 51 | function print () { 52 | return polylinesToSVG(lines, { 53 | dimensions 54 | }); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /code/test-print.js: -------------------------------------------------------------------------------- 1 | // Import some penplot utilities 2 | import { PaperSize, Orientation } from 'penplot'; 3 | import { polylinesToSVG } from 'penplot/util/svg'; 4 | import { clipPolylinesToBox } from 'penplot/util/geom'; 5 | 6 | // Export orientation of your print 7 | export const orientation = Orientation.LANDSCAPE; 8 | 9 | // Export [ width, height ] dimensions in centimetres 10 | export const dimensions = PaperSize.LETTER; 11 | 12 | // The plot function takes care of setup & rendering 13 | export default function createPlot (context, dimensions) { 14 | const [ width, height ] = dimensions; 15 | let lines = []; 16 | 17 | // ... Your algorithmic code usually goes here. ... 18 | // Draw some circles expanding outward 19 | const steps = 5; 20 | const count = 20; 21 | const spacing = 1; 22 | const radius = 2; 23 | for (let j = 0; j < count; j++) { 24 | const r = radius + j * spacing; 25 | const circle = []; 26 | for (let i = 0; i < steps; i++) { 27 | const t = i / Math.max(1, steps - 1); 28 | const angle = Math.PI * 2 * t; 29 | circle.push([ 30 | width / 2 + Math.cos(angle) * r, 31 | height / 2 + Math.sin(angle) * r 32 | ]); 33 | } 34 | lines.push(circle); 35 | } 36 | 37 | // Clip all the lines to a margin 38 | const margin = 1.5; 39 | const box = [ margin, margin, width - margin, height - margin ]; 40 | lines = clipPolylinesToBox(lines, box); 41 | 42 | // Return some settings for penplot 43 | return { 44 | draw, 45 | print, 46 | background: 'white', 47 | animate: false, 48 | clear: true 49 | }; 50 | 51 | // For the browser and PNG export, draw the plot to a Canvas2D context 52 | function draw () { 53 | lines.forEach(points => { 54 | context.beginPath(); 55 | points.forEach(p => context.lineTo(p[0], p[1])); 56 | context.stroke(); 57 | }); 58 | } 59 | 60 | // For SVG export, returns a string that makes up the SVG file contents 61 | function print () { 62 | return polylinesToSVG(lines, { 63 | dimensions 64 | }); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /code/triangulation.js: -------------------------------------------------------------------------------- 1 | import { PaperSize, Orientation } from 'penplot'; 2 | import { polylinesToSVG } from 'penplot/util/svg'; 3 | import { randomFloat, setSeed } from 'penplot/util/random'; 4 | import newArray from 'new-array'; 5 | import triangulate from 'delaunay-triangulate'; 6 | 7 | export const orientation = Orientation.LANDSCAPE; 8 | export const dimensions = PaperSize.SQUARE_POSTER; 9 | 10 | // Uncomment this for predictable randomness on each run 11 | // setSeed(16); 12 | 13 | const debug = false; 14 | 15 | export default function createPlot (context, dimensions) { 16 | const [ width, height ] = dimensions; 17 | 18 | const pointCount = 6000; 19 | const positions = newArray(pointCount).map(() => { 20 | // Margin from print edge in centimeters 21 | const margin = 2; 22 | // Return a random 2D point inset by this margin 23 | return [ 24 | randomFloat(margin, width - margin), 25 | randomFloat(margin, height - margin) 26 | ]; 27 | }); 28 | const cells = triangulate(positions); 29 | 30 | const lines = cells.map(cell => { 31 | // Get vertices for this cell 32 | const triangle = cell.map(i => positions[i]); 33 | // Close the path 34 | triangle.push(triangle[0]); 35 | return triangle; 36 | }); 37 | 38 | return { 39 | draw, 40 | print, 41 | background: 'white' 42 | }; 43 | 44 | function draw () { 45 | lines.forEach(points => { 46 | context.beginPath(); 47 | points.forEach(p => context.lineTo(p[0], p[1])); 48 | context.stroke(); 49 | }); 50 | 51 | // Turn on debugging if you want to see the random points 52 | if (debug) { 53 | positions.forEach(p => { 54 | context.beginPath(); 55 | context.arc(p[0], p[1], 0.2, 0, Math.PI * 2); 56 | context.strokeStyle = 'red'; 57 | context.stroke(); 58 | }); 59 | } 60 | } 61 | 62 | function print () { 63 | return polylinesToSVG(lines, { 64 | dimensions 65 | }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /images/3d.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/pen-plotter-blog-post/9729c4494202deaa723c940a1ccfc73f1dfed11a/images/3d.jpg -------------------------------------------------------------------------------- /images/canvas-all.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/pen-plotter-blog-post/9729c4494202deaa723c940a1ccfc73f1dfed11a/images/canvas-all.jpg -------------------------------------------------------------------------------- /images/canvas1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/pen-plotter-blog-post/9729c4494202deaa723c940a1ccfc73f1dfed11a/images/canvas1.png -------------------------------------------------------------------------------- /images/canvas2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/pen-plotter-blog-post/9729c4494202deaa723c940a1ccfc73f1dfed11a/images/canvas2.png -------------------------------------------------------------------------------- /images/canvas3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/pen-plotter-blog-post/9729c4494202deaa723c940a1ccfc73f1dfed11a/images/canvas3.png -------------------------------------------------------------------------------- /images/code1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/pen-plotter-blog-post/9729c4494202deaa723c940a1ccfc73f1dfed11a/images/code1.png -------------------------------------------------------------------------------- /images/code10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/pen-plotter-blog-post/9729c4494202deaa723c940a1ccfc73f1dfed11a/images/code10.png -------------------------------------------------------------------------------- /images/code11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/pen-plotter-blog-post/9729c4494202deaa723c940a1ccfc73f1dfed11a/images/code11.png -------------------------------------------------------------------------------- /images/code12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/pen-plotter-blog-post/9729c4494202deaa723c940a1ccfc73f1dfed11a/images/code12.png -------------------------------------------------------------------------------- /images/code13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/pen-plotter-blog-post/9729c4494202deaa723c940a1ccfc73f1dfed11a/images/code13.png -------------------------------------------------------------------------------- /images/code2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/pen-plotter-blog-post/9729c4494202deaa723c940a1ccfc73f1dfed11a/images/code2.png -------------------------------------------------------------------------------- /images/code3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/pen-plotter-blog-post/9729c4494202deaa723c940a1ccfc73f1dfed11a/images/code3.png -------------------------------------------------------------------------------- /images/code4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/pen-plotter-blog-post/9729c4494202deaa723c940a1ccfc73f1dfed11a/images/code4.png -------------------------------------------------------------------------------- /images/code5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/pen-plotter-blog-post/9729c4494202deaa723c940a1ccfc73f1dfed11a/images/code5.png -------------------------------------------------------------------------------- /images/code6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/pen-plotter-blog-post/9729c4494202deaa723c940a1ccfc73f1dfed11a/images/code6.png -------------------------------------------------------------------------------- /images/code7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/pen-plotter-blog-post/9729c4494202deaa723c940a1ccfc73f1dfed11a/images/code7.png -------------------------------------------------------------------------------- /images/code8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/pen-plotter-blog-post/9729c4494202deaa723c940a1ccfc73f1dfed11a/images/code8.png -------------------------------------------------------------------------------- /images/code9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/pen-plotter-blog-post/9729c4494202deaa723c940a1ccfc73f1dfed11a/images/code9.png -------------------------------------------------------------------------------- /images/ex-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/pen-plotter-blog-post/9729c4494202deaa723c940a1ccfc73f1dfed11a/images/ex-1.png -------------------------------------------------------------------------------- /images/ex-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/pen-plotter-blog-post/9729c4494202deaa723c940a1ccfc73f1dfed11a/images/ex-2.png -------------------------------------------------------------------------------- /images/ex-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/pen-plotter-blog-post/9729c4494202deaa723c940a1ccfc73f1dfed11a/images/ex-3.png -------------------------------------------------------------------------------- /images/exb1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/pen-plotter-blog-post/9729c4494202deaa723c940a1ccfc73f1dfed11a/images/exb1.jpg -------------------------------------------------------------------------------- /images/exb2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/pen-plotter-blog-post/9729c4494202deaa723c940a1ccfc73f1dfed11a/images/exb2.jpg -------------------------------------------------------------------------------- /images/exb3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/pen-plotter-blog-post/9729c4494202deaa723c940a1ccfc73f1dfed11a/images/exb3.jpg -------------------------------------------------------------------------------- /images/fracture.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/pen-plotter-blog-post/9729c4494202deaa723c940a1ccfc73f1dfed11a/images/fracture.gif -------------------------------------------------------------------------------- /images/header.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/pen-plotter-blog-post/9729c4494202deaa723c940a1ccfc73f1dfed11a/images/header.jpg -------------------------------------------------------------------------------- /images/molnar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/pen-plotter-blog-post/9729c4494202deaa723c940a1ccfc73f1dfed11a/images/molnar.jpg -------------------------------------------------------------------------------- /images/patchwork.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/pen-plotter-blog-post/9729c4494202deaa723c940a1ccfc73f1dfed11a/images/patchwork.jpg -------------------------------------------------------------------------------- /images/penplot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/pen-plotter-blog-post/9729c4494202deaa723c940a1ccfc73f1dfed11a/images/penplot1.png -------------------------------------------------------------------------------- /images/plotter-wide.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/pen-plotter-blog-post/9729c4494202deaa723c940a1ccfc73f1dfed11a/images/plotter-wide.jpg -------------------------------------------------------------------------------- /images/render.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/pen-plotter-blog-post/9729c4494202deaa723c940a1ccfc73f1dfed11a/images/render.png -------------------------------------------------------------------------------- /images/tess-v3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/pen-plotter-blog-post/9729c4494202deaa723c940a1ccfc73f1dfed11a/images/tess-v3.jpg -------------------------------------------------------------------------------- /markdown/Part-1.md: -------------------------------------------------------------------------------- 1 | 2 | — You can find the source code for this blog series [here](https://github.com/mattdesl/pen-plotter-blog-post). 3 | 4 |  5 | 6 | Over the last several months, I've been looking for ways to produce *physical* outputs from my generative code. I'm interested in the idea of developing real, tangible objects that are no longer bound by the generative systems that shaped them. Eventually I plan to experiment with 3D printing, laser cutting, CNC milling, and other ways of realizing my algorithms in the real-world. 7 | 8 | My interest in this began in March 2017, when I purchased my first pen plotter: the [AxiDraw V3](https://shop.evilmadscientist.com/productsmenu/846) by Evil Mad Scientist Laboratories. It's a fantastic machine, and has opened a whole new world of thinking for me. For those unaware, a pen plotter is a piece of hardware that acts like a robotic arm on which you can attach a regular pen. Software sends commands to the device to raise, reposition, and lower its arm across a 2D surface. With this, the plotter can be programmed to produce intricate and accurate prints with a pen and paper of your choice. 9 | 10 |  11 | 12 | — Early prints, March 2017 13 | 14 | Unlike a typical printer, a plotter produces prints with a strangely human quality: occasional imperfections arise as the pen catches an edge or momentarily dries up, and the quality of the ink has the subtle texture and emboss that you normally only see in an original drawing. 15 | 16 |
17 | 18 | Often, these plotters and mechanical devices are controlled by formats like [HP-GL](https://en.wikipedia.org/wiki/HP-GL) or G-code. These formats that specify how the machine should lift, move, and place itself over time. For convenience, AxiDraw handles most of the mechanical operation for you, providing an Inkscape SVG plugin that accepts paths, lines, shapes, and even fills (through hatching). 19 | 20 |  21 | 22 | — Tesselations, August 2017 23 | 24 | You don't need to be a programmer to use the AxiDraw pen plotter. You can create SVG files in Adobe Illustrator or find SVGs online to print. However, the machine is very well suited to programmatic and algorithmic line art, as it can run for hours at a time and produce incredibly detailed outputs that would be too tedious to illustrate by hand. 25 | 26 | One recent series I developed, *Natural Systems*, is composed of 4 different algorithms. Each time these algorithms run, they produce different outputs, allowing for an infinite number of unique prints. 27 | 28 |  29 | 30 | — Natural Systems, November 2017 31 | 32 | This isn't a new concept; Vera Molnár, an early pioneer of computer art, was rendering pen plotter prints in the 1960s! 33 | 34 |  35 | 36 | — Vera Molnár, No Title, 1968 37 | 38 | In this post, I'll try to explain some of my workflow when developing new pen plotter prints, and show some of the tools I've been building to help organize my process. 39 | 40 | # Development Environment 41 | 42 | So far, all of my work with the AxiDraw has been with JavaScript and an experimental tool I've been building, aptly named [penplot](https://github.com/mattdesl/penplot). The tool primarily acts as a development environment, making it easier to organize and develop new prints with minimal configuration. 43 | 44 |🖨 Plotters have been around for a while — if you are just starting off, you might be interested in some of the older but more affordable HP plotters.
45 | 46 | You can try the tool out yourself with `node@8.4.x` and `npm@5.3.x` or higher. 47 | 48 | ```sh 49 | # install the CLI app globally 50 | npm install penplot -g 51 | 52 | # run it, generating a new file and opening the browser 53 | penplot test-print.js --write --open 54 | ``` 55 | 56 | The `--write` flag will generate a new `test-print.js` file and `--open` will launch your browser to `localhost:9966`. It starts you off with a basic print: 57 | 58 |  59 | 60 | ✏️ See [here](https://github.com/mattdesl/pen-plotter-blog-post/blob/master/code/test-print.js) to see the generated source code of this print. 61 | 62 | The generated `test-print.js` file is ready to go; you can edit the ES2015 code to see changes reflected in your browser. When you are happy, hit `Cmd + S` (save PNG) or `Cmd + P` (save SVG) to export your print from the browser — the files will save to your Downloads folder. 63 | 64 | # Geometry & Primitives 65 | 66 | For algorithmic work with AxiDraw and its SVG plugin, I tend to distill all my visuals into a series of polylines composed of nested arrays. 67 | 68 | ```js 69 | const lines = [ 70 | [ 71 | [ 1, 1 ], [ 2, 1 ] 72 | ], 73 | [ 74 | [ 1, 2 ], [ 2, 2 ] 75 | ] 76 | ] 77 | ``` 78 | 79 | This creates two horizontal lines in the top left of our print, each 1 cm wide. Here, points are defined by `[ x, y ]` and a polyline (i.e. path) is defined by the points `[ a, b, c, d, .... ]`. Our list of polylines is defined as `[ A, B, ... ]`, allowing us to create multiple disconnected segments (i.e. where the pen lifts to create a new line). 80 | 81 |🚨 This tool is highly experimental and subject to change as my workflow evolves.
82 | 83 |  84 | 85 | So far, the code above doesn't feel very intuitive, but you will hardly ever hardcode coordinates like this. Instead, you should try to think in geometric primitives: points, squares, lines, circles, triangles, etc. For example, to draw some squares in the centre of the print: 86 | 87 | ```js 88 | // Function to create a square 89 | const square = (x, y, size) => { 90 | // Define rectangle vertices 91 | const path = [ 92 | [ x - size, y - size ], 93 | [ x + size, y - size ], 94 | [ x + size, y + size ], 95 | [ x - size, y + size ] 96 | ]; 97 | // Close the path 98 | path.push(path[0]); 99 | return path; 100 | }; 101 | 102 | // Get centre of the print 103 | const cx = width / 2; 104 | const cy = height / 2; 105 | 106 | // Create 12 concentric pairs of squares 107 | const lines = []; 108 | for (let i = 0; i < 12; i++) { 109 | const size = i + 1; 110 | const margin = 0.25; 111 | lines.push(square(cx, cy, size)); 112 | lines.push(square(cx, cy, size + margin)); 113 | } 114 | ``` 115 | 116 | Once the lines are in place, they are easy to render to the Canvas2D context with `beginPath()` and `stroke()`, or save to an SVG with the `penplot` utility, `polylinesToSVG()`. 117 | 118 | The result of our code looks like this: 119 | 120 |  121 | 122 | ✏️ See [here](https://github.com/mattdesl/pen-plotter-blog-post/blob/master/code/squares.js) for the final source code of this print. 123 | 124 | This is starting to get a bit more interesting, but you may be wondering why not just reproduce this by hand in Illustrator. So, let's see if we can create something more complex in code. 125 | 126 | # Delaunay Triangulation 127 | 128 | A simple starting task would be to explore [Delaunay triangulation](https://en.wikipedia.org/wiki/Delaunay_triangulation). For this, we will use [delaunay-triangulate](http://npmjs.com/package/delaunay-triangulate), a robust triangulation library by Mikola Lysenko that works in 2D and 3D. We will also use the [new-array](https://www.npmjs.com/package/new-array) module, a simple array creation utility. 129 | 130 | Before we begin, you'll need to install these dependencies locally: 131 | 132 | ```sh 133 | # first ensure you have a package.json in your folder 134 | npm init -y 135 | 136 | # now you can install the required dependencies 137 | npm install delaunay-triangulate new-array 138 | ``` 139 | 140 | In our JavaScript code, let's `import` some of our modules and define a set of 2D points randomly distributed across the print, inset by a small margin. 141 | 142 | We use the built-in penplot `random` library here, which has the function `randomFloat(min, max)` for convenience. 143 | 144 | ```js 145 | import newArray from 'new-array'; 146 | import { randomFloat } from 'penplot/util/random'; 147 | 148 | // ... 149 | 150 | const pointCount = 200; 151 | const positions = newArray(pointCount).map(() => { 152 | // Margin from print edge in centimeters 153 | const margin = 2; 154 | // Return a random 2D point inset by this margin 155 | return [ 156 | randomFloat(margin, width - margin), 157 | randomFloat(margin, height - margin) 158 | ]; 159 | }); 160 | ``` 161 | 162 |📐 Penplot scales the Canvas2D context before drawing, so all of your units should be in centimeters.
163 | 164 | If we were to visualize our points as circles, it might look like this: 165 | 166 |  167 | 168 | The next step is to triangulate these points, i.e. turn them into triangles. Simply feed the array of points into the `triangulate` function and it returns a list of "cells." 169 | 170 | ```js 171 | import triangulate from 'delaunay-triangulate'; 172 | 173 | // ... 174 | 175 | const cells = triangulate(positions); 176 | ``` 177 | 178 | The return value is an array of triangles, but instead of giving us the 2D positions of each vertex in the triangle, it gives us the index into the `positions` array that we passed in. 179 | 180 | ```js 181 | [ 182 | [ 0, 1, 2 ], 183 | [ 2, 3, 4 ], 184 | ... 185 | ] 186 | ``` 187 | 188 | For example, to get the 3 vertices of the first triangle: 189 | 190 | ```js 191 | const triangle = cells[0].map(i => positions[i]); 192 | 193 | // log each 2D point in the triangle 194 | console.log(triangle[0], triangle[1], triangle[2]); 195 | ``` 196 | 197 | For our final print, we want to map each triangle to a polyline that the pen plotter can draw out. 198 | 199 | ```js 200 | const lines = cells.map(cell => { 201 | // Get vertices for this cell 202 | const triangle = cell.map(i => positions[i]); 203 | // Close the path 204 | triangle.push(triangle[0]); 205 | return triangle; 206 | }); 207 | ``` 208 | 209 | Now we have all the lines we need to send the SVG to AxiDraw. In the browser, hit `Cmd + S` and `Cmd + P` to save a PNG and SVG file, respectively, into your Downloads folder. 210 | 211 |  212 | 213 | For reference, below you can see how our original random points have now become the vertices for each triangle: 214 | 215 |  216 | 217 |💡 I often use
new-array
andmap
to create a list of objects, as I find it more modular and functional than a for loop.
218 | 219 | If we increase the `pointCount` to a higher value, we start to get a more well-defined edge, and potentially a more interesting print. 220 | 221 |  222 | 223 | ✏️ See [here](https://github.com/mattdesl/pen-plotter-blog-post/blob/master/code/triangulation.js) for the final source code of this print. 224 | 225 | # Part Two 226 | 227 | In the next instalment of this series, we'll attempt to reproduce a fracturing algorithm for a more interesting composition. 228 | 229 | **Continue reading:** [Pen Plotter Art & Algorithms, Part 2](https://mattdesl.svbtle.com/pen-plotter-2) -------------------------------------------------------------------------------- /markdown/Part-2.md: -------------------------------------------------------------------------------- 1 | — This post is a continuation of [Pen Plotter Art & Algorithms, Part 1](https://mattdesl.svbtle.com/pen-plotter-1). 2 | 3 |  4 | 5 | — [Patchwork](https://www.behance.net/gallery/60288255/Patchwork), printed with AxiDraw, December 2017 6 | 7 | In our [previous post](https://mattdesl.svbtle.com/pen-plotter-1), we learned to develop some basic prints with [penplot](https://github.com/mattdesl/penplot), an experimental tool I'm building for my own pen plotter artwork. 8 | 9 | In this post, let's aim for something more challenging, and attempt to develop an algorithm from the ground up. I'm calling this algorithm "Patchwork," although I won't claim to have invented it. I'm sure many before me have discovered the same algorithm. 10 | 11 |💡 The
random
module includes asetSeed(n)
function, which is useful if you want predictable randomness every time the page reloads.
12 | 13 | The algorithm we will try to implement works like so: 14 | 15 | 1. Start with a set of *N* initial points. 16 | 2. Select a cluster of points and draw the [convex hull](https://en.wikipedia.org/wiki/Convex_hull) that surrounds all of them. 17 | 3. Remove the points contained by the convex hull from our data set. 18 | 4. Repeat the process from step 2. 19 | 20 | The "convex hull" is a convex polygon that encapsulates a set of points; it's a bit like if we hammered nails down at each point, and then tied a string around them to create a closed shape. 21 | 22 | To select a cluster, we will use [k-means](https://en.wikipedia.org/wiki/K-means_clustering) to partition the data into N clusters, and then select whichever cluster has the least amount of points. There are likely many ways you can randomly select clusters, perhaps more optimally than with k-means. 23 | 24 | # Initial Setup 25 | 26 | Install the required libraries first, and then generate a new script with [penplot](https://github.com/mattdesl/penplot). 27 | 28 | ```sh 29 | # install dependencies 30 | npm install density-clustering convex-hull 31 | 32 | # generate a new plot 33 | penplot patchwork.js --write --open 34 | ``` 35 | 36 | Now, let's begin by adding the same random points code from [Part 1](https://mattdesl.svbtle.com/pen-plotter-1) and stubbing out an `update` function for our algorithm. We also need to return `{ animate: true }`, so that `penplot` will start a render loop instead of just drawing one frame. 37 | 38 | ```js 39 | // ... 40 | 41 | import { PaperSize, Orientation } from 'penplot'; 42 | import { randomFloat } from 'penplot/util/random'; 43 | import newArray from 'new-array'; 44 | import clustering from 'density-clustering'; 45 | import convexHull from 'convex-hull'; 46 | 47 | export const orientation = Orientation.LANDSCAPE; 48 | export const dimensions = PaperSize.SQUARE_POSTER; 49 | 50 | export default function createPlot (context, dimensions) { 51 | const [ width, height ] = dimensions; 52 | 53 | // A large point count will produce more defined results 54 | const pointCount = 500; 55 | let points = newArray(pointCount).map(() => { 56 | const margin = 2; 57 | return [ 58 | randomFloat(margin, width - margin), 59 | randomFloat(margin, height - margin) 60 | ]; 61 | }); 62 | 63 | // We will add to this over time 64 | const lines = []; 65 | 66 | // The N value for k-means clustering 67 | // Lower values will produce bigger chunks 68 | const clusterCount = 3; 69 | 70 | // Run our generative algorithm at 30 FPS 71 | setInterval(update, 1000 / 30); 72 | 73 | return { 74 | draw, 75 | print, 76 | background: 'white', 77 | animate: true // start a render loop 78 | }; 79 | 80 | function update () { 81 | // Our generative algorithm... 82 | } 83 | 84 | // ... draw / print functions ... 85 | } 86 | ``` 87 | 88 | You won't see anything yet if you run the code, that's because our `lines` array is empty. If we were to visualize our random points, they would look like this: 89 | 90 |  91 | 92 | # Generating New Polygons 93 | 94 | Let's make it so that each time `update` runs, it adds a new polygon to the `lines` array. The second step in our algorithm is to select a cluster of points from our data set. For this we will use the [density-clustering](https://www.npmjs.com/package/density-clustering) module, filtering the results to ensure we select a cluster with at least 3 points. Then, we sort by ascending density to select the cluster with the least number of points (i.e. the first). 95 | 96 | Like with `triangulate()`, the density clustering returns lists of *indices*, not points, so we need to map the indices to their corresponding positions. 97 | 98 | ```js 99 | function update () { 100 | // Not enough points in our data set 101 | if (points.length <= clusterCount) return; 102 | 103 | // k-means cluster our data 104 | const scan = new clustering.KMEANS(); 105 | const clusters = scan.run(points, clusterCount) 106 | .filter(c => c.length >= 3); 107 | 108 | // Ensure we resulted in some clusters 109 | if (clusters.length === 0) return; 110 | 111 | // Sort clusters by density 112 | clusters.sort((a, b) => a.length - b.length); 113 | 114 | // Select the least dense cluster 115 | const cluster = clusters[0]; 116 | const positions = cluster.map(i => points[i]); 117 | 118 | // ... 119 | } 120 | ``` 121 | 122 | Now that we have a cluster, we can find the convex hull of its points, and removes those points from our original data set. The [convex-hull](https://www.npmjs.com/package/convex-hull) module returns a list of `edges` (i.e. line segments), and by taking the first vertex in each edge, we can form a closed polyline (polygon) for that cluster. 123 | 124 | ```js 125 | function update () { 126 | // Select a cluster 127 | // ... 128 | 129 | // Find the hull of the cluster 130 | const edges = convexHull(positions); 131 | 132 | // Ensure the hull is large enough 133 | if (edges.length <= 2) return; 134 | 135 | // Create a closed polyline from the hull 136 | let path = edges.map(c => positions[c[0]]); 137 | path.push(path[0]); 138 | 139 | // Add to total list of polylines 140 | lines.push(path); 141 | 142 | // Remove those points from our data set 143 | points = points.filter(p => !positions.includes(p)); 144 | } 145 | ``` 146 | 147 | Below, we can see the set of blue points (a cluster) and their convex hull being defined around them. 148 | 149 |  150 | 151 | Once the points from that cluster are removed from the data set, we are left with a polygon in their place. 152 | 153 |  154 | 155 | As we continue stepping the algorithm forward, we end up with more polygons filling in the empty space. 156 | 157 |  158 | 159 | Until eventually the algorithm converges, and we can find no more suitable clusters. 160 | 161 |  162 | 163 | Like in the triangulation example from [Part 1](https://mattdesl.svbtle.com/pen-plotter-1), let's increase our `pointCount` to get a more refined output. With a high number, like 50,000 points, we will get more detail and smoother polygons. 164 | 165 |  166 | 167 | ✏️ See [here](https://github.com/mattdesl/pen-plotter-blog-post/blob/master/code/patchwork.js) for the final source code of this print. 168 | 169 | # Recursion 170 | 171 | The real elegance in this algorithm comes from recursing it; after it converges, you can select a new polygon, fill it with points, and re-run the algorithm again from step 2. After many iterations, you end up with incredibly detailed patterns. 172 | 173 | Below are a few other examples after spending an evening refining and tweaking a recursive version of this algorithm. These particular outputs use Canvas2D `fill()`, thus aren't suitable for a pen plotter. 174 | 175 |  176 | 177 | — [Patchwork](https://www.behance.net/gallery/60288255/Patchwork), December 2017 178 | 179 | # Other Applications 180 | 181 | It's worth noting that the "Patchwork" algorithm can also be extend to 3D. The below model was exported from ThreeJS and rendered in Blender. 182 | 183 |  184 | 185 | Since my original tweet, others have also implemented this algorithm in Houdini: see [@cargoneblina](https://twitter.com/cargoneblina/status/946057676160516097), [@sugiggy](https://twitter.com/sugiggy/status/946929168247377920) and [@yone80](https://twitter.com/yone80/status/946172960238313473). 186 | 187 | # Thinking Physically 188 | 189 | The biggest takeaway from learning to use a pen plotter is how I am starting to think in more *physical* terms — even something as simple as using centimetre units instead of pixels. 190 | 191 | As you can see from the earlier 3D render, this algorithmic pen plotter work is naturally leading me to other *physical* outputs with JavaScript. In a future post, I hope to detail my workflow for parametric 3D modelling in ThreeJS to create foldable paper models, laser cut artwork, and more. 192 | 193 | # Further Reading 194 | 195 | If you enjoyed this blog post, you should take a look at some other artists working with pen plotters and generative code. 196 | 197 | - [Anders Hoff](http://inconvergent.net/) (Inconvergent) writes a lot about his process in Python and Lisp. 198 | - [Michael Fogleman](https://medium.com/@fogleman/pen-plotter-programming-the-basics-ec0407ab5929) wrote a blog article on pen plotter basics. He's also written tools like [`ln`](https://github.com/fogleman/ln), a 3D to 2D line art engine for Go. 199 | - [Tyler Hobbs](http://www.tylerlhobbs.com/writings) writes about generative art and programming, and his work shares many parallels with my process here. 200 | - [Paul Butler](https://bitaesthetics.com/posts/surface-projection.html) recently wrote a blog post on his pen plotter work in Python. 201 | - [Tobias Toft](http://www.tobiastoft.com/posts/an-intro-to-pen-plotters) explains how to use Processing with traditional HP plotters. These use a different file format, but are often more affordable than the AxiDraw. 202 | 203 | You can find lots more pen plotter work through the Twitter hashtag, [#plottertwitter](https://twitter.com/hashtag/plottertwitter?src=hash). -------------------------------------------------------------------------------- /markdown/README.md: -------------------------------------------------------------------------------- 1 | # Markdown 2 | 3 | These posts are best viewed on my Svbtle blog: 4 | 5 | - [Pen Plotter Art & Algorithms, Part 1](https://mattdesl.svbtle.com/pen-plotter-1) 6 | - [Pen Plotter Art & Algorithms, Part 2](https://mattdesl.svbtle.com/pen-plotter-2) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pen-plotter-blog-post", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "simple.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Matt DesLauriers💡 You can find more discussion and images in this Twitter thread, where I first posted about it.