Paper.js is a JavaScript Tool for Generative Art . This is a Literate Programming environment for building little toys in Paper.js. Eventually, I would like to have different AxiDraw AxiDraw projects in their own files, input presented with output as with all my other creative output.
I have looked at some other libraries for Generative Art like axi's turtle library and various other turtle implementations, Quill, snek, and other lisp libraries ... and here I am. Paper.js seems like a pretty straightforward thing to do what I want, which is to be able to program in the literate fashion, present the source code and a rendering of the artwork, and also be able to bring that artwork to physical medium with the AxiDraw. For this I need to either be able to directly drive the AxiDraw with my code, or I need to be able to capture an SVG of the work. Paper.js provides the latter, while conseving the former, an optimal design, but implemented in JavaScript, an ecosystem I am not so excited for in its current state. I could write ClojureScript but that leaves me similarly unexcited.
And so I tread carefully in to the territory; I'm terribly disinterested in setting up a full webpack gruntfile source map buildsystem madlads -- this is going to be terribly simple, and I want to be able to integrate this work in to Arcology -- the Arcology must be able to render this creative output.
But for now: the bare minimum. Run the server below after org-exporting this document, navigate to http://127.0.0.1:8081/
Server Setup and simple index.html for play
I try to contain all of this in the paper subdirectory, so that it's easy enough to start over or exclude these files from sync to certain devices. I'm being really half-assed here, installing paper with the Node Package Mangler and then pulling it out and shoving it in my public web directory under js.
shell source: :results none :exports codemkdir -p paper/public; pushd paper npm install paper http-server mkdir public/js/ cp node_modules/paper/dist/paper-full.js public/js/
To start the static server, run elisp:(async-shell-command "paper/node_modules/.bin/http-server ./paper/public/"). I don't want to really deal with a server-side aspect yet, and optimally this is built in to Arcology , so I'm just using this standard http-server package.
At this point, I can include Paper.js in web-exports here, and some CSS to make sure my canvases look good.
css source: :tangle paper/public/stylesheet.css :exports codehtml, body { height: 100%; } /* Scale canvas with resize attribute to full size */ canvas[resize] { width: 840px; height: 594px; background-color: white; }
And this document is the "index.html=" referenced in the heading title. It's a little bit janky right now, but I can sort of keep this system running. It's a mix of org-babel and org-export, and that sort of sucks. I have this python function, right? It's got a javascript template in it with the curly braces doubled so that Python's =format function doesn't mind. How stupid is that? I'd like to rewrite this eventually, but figuring out how to get download buttons working requires a fair bit of mess. This creates a window.globals.projects object that is shared between the paperscript and the parent context so that I can export the SVGs from Javascript. What fun what fun.
python source: :noweb yes :results none :session paperscript-exportdef paperscript_it(canvas, code): return """ <script type="text/paperscript" canvas="{canvas}"> {input} // shuttle the project out globals.projects["{canvas}"] = paper.project; </script> <script type="text/javascript"> window.globals = {{projects:{{}}}}; var download_fn = function() {{ var svg = window.globals.projects["{canvas}"].exportSVG({{asString:true}}); var fileName = "{canvas}.svg" var url = "data:image/svg+xml;utf8," + encodeURIComponent(svg); var link = document.createElement("a"); link.download = fileName; link.href = url; link.click(); }} document.addEventListener("DOMContentLoaded", function(event) {{ var els = document.getElementById("b_{canvas}"); els.onclick = download_fn; }}); </script> <button id="b_{canvas}" class="download-btn">Download</button> """.format(canvas=canvas, input=code)
It's simple, it prints some HTML given a string of code and a target canvas's ID. That can be included in the export, then,
org source: :tangle paper/org-capture-template* %? #+HTML: <canvas id="%^{name}" resize></canvas> #+name: %^{name} #+begin_src javascript #+end_src #+begin_src python :noweb yes :exports results :results html :session paperscript-export f = """ <<%^{name}>>""" paperscript_it("%^{name}", f) #+end_src
emacs-lisp source: :results none(provide 'cce/paper-js) (with-eval-after-load 'org-capture (add-to-list 'org-capture-templates '("P" "Paper.js Entry" entry (file "~/org/paper_js.org") (file "~/org/paper/org-capture-template") :clockin t)))
Tutorials, Getting Started, etc..
javascript source:function rand(max) { return Math.floor(Math.random() * Math.floor(2*max))-max; } function jitter(dist) { return new Point(rand(dist), rand(dist)); } var path = new Path(); path.strokeColor = 'black'; path.moveTo(new Point(200,200)) for(var i=0; i<=6; i++) { path.lineBy(jitter(50)) }
python source: :noweb yes :exports results :results html :session paperscript-exportf = """ <<tutorial>>""" paperscript_it("tutorial", f)
Spiral Example
You can feed an object with angle and length included in it to make relative polar movements! mind the end, the smoothing struggles a bit.
javascript source:var path = new Path(); path.strokeColor = 'black'; path.add(view.center); for (var i = 0; i < 300; i++) { // oh wow! var vector = new Point({ angle: i * 45, length: i / 2 }); path.lineBy(vector); } // Smooth the handles of the path: path.smooth();
python source: :noweb yes :exports results :results html :session paperscript-exportf = """ <<spiral>>""" paperscript_it("spiral", f)
generative wander
this is an attempt at doing more complicated things, including relative movements. Sort of just meandering around. Being able to feed an angle and a distance is pretty neat. Next, I'd like to figure out how to get these movements to be relative.
javascript source:function rand(max) { return Math.floor(Math.random() * Math.floor(max)); } function recenter(val, max) { return val - max/2; } for (var h = 0; h < 10; h++) { var path = new Path(); path.strokeColor = 'black'; path.add(view.center); var angle = rand(360); for (var i = 0; i < 50; i++) { angle = angle+recenter(rand(180), 90) % 360; var vector = new Point({ angle: angle, length: 25 }); path.lineBy(vector); } // Smooth the handles of the path: path.smooth(); }
python source: :noweb yes :exports results :results html :session paperscript-exportf = """ <<gen_wander>>""" paperscript_it("gen_wander", f)
Grid Functions
some circle patterns, clipping.
this is a pattern I should probably move in to a library of babel.
javascript source:var w = view.size.width; var h = view.size.height; var dw = 20, dh = 20; var colors = ['black', 'green', 'red', 'blue']; var cgroups = {}; for (cid in colors) { var color = colors[cid]; cgroups[color] = new Layer({name: "" + (cid+1) + " " + color}); } for (var y = 0; y < h; y=y+dh) { for (var x = 0; x < w; x+=dw) { var color = colors[(y/dh % 4)]; var path = new Path.Circle({ center: [x, y], radius: (3*x + y) % 50, strokeColor: color, }); cgroups[color].addChild(path); } }
python source: :noweb yes :exports results :results html :session paperscript-exportf = """ <<gridfuncs>>""" paperscript_it("gridfuncs", f)
Grid Functions 2
Playing more with this theme, I could build a little harnes out of this...
javascript source:var w = view.size.width; var h = view.size.height; var dw = 20, dh = 20; var colors = ['black', 'green', 'red', 'blue']; var cgroups = {}; for (cid in colors) { var color = colors[cid]; cgroups[color] = new Layer({name: "" + cid + " " + color}); } for (var y = 0; y < h; y=y+dh) { for (var x = 0; x < w; x+=dw) { var color = colors[(Math.abs(-1 * x/dw % 10) + (y/dh % 17)) % 4]; var path = new Path.Rectangle({ center: [x, y], size: [ (3*x + y) % 25, (3*x + y) % 25 ], strokeColor: color, }); cgroups[color].addChild(path); } }
python source: :noweb yes :exports results :results html :session paperscript-exportf = """ <<gridfuncs2>>""" paperscript_it("gridfuncs2", f)
Grid Functions Harness
Let's make this a bit more repeatable...
javascript source:ColorGridFunction = function(props) { this.width = props.width || 250; this.height = props.height || 250; this.dw = props.dw || 250; this.dh = props.dh || 25; this.colors = props.colors || ['black']; this.project = props.project; this.drawFn = props.drawFn; this.cgroups = {}; this.makeLayers() } ColorGridFunction.prototype.makeLayers = function() { for (cid in this.colors) { var color = this.colors[cid], name = "" + cid + " " + color; this.cgroups[color] = new Layer({ name: name, }); this.project.addLayer(this.cgroups[color]); } } ColorGridFunction.prototype.draw = function() { for (var y = 0; y < this.height; y=y+this.dh) { for (var x = 0; x < this.width; x+=this.dw) { this.drawFn(x,y); } } paper.view.draw(); } ColorGridFunction.prototype.color = function(idx) { return this.colors[idx]; } ColorGridFunction.prototype.path = function(color, path) { this.cgroups[color].addChild(path); } cgf = function(props) { return new ColorGridFunction(props); } cgf({ project: project, width: view.size.width, height: view.size.height, dw: 20, dh: 20, colors: ['black', 'green', 'red', 'blue'], drawFn: function(x,y) { var color = this.color( (Math.abs(-1 * x/this.dw % 10) + (y/this.dh % 17)) % 4 ); var path = new Path.Rectangle({ strokeStyle: color, center: [x, y], size: [ (3*x + y) % 25, (3*x + y) % 25 ], }); this.path(color, path); } }).draw();
python source: :noweb yes :exports none :results html :session paperscript-exportf = """ <<grid_harnessed>>""" paperscript_it("grid_harnessed", f)
I don't know why this works and I don't feel like making it... this tool is baroque and so is writing eczema script 5. sketch online.
Hatching
Some code to generate hatching suitable for the plotter. This hatching pattern is a +.
The masking didn't work at all when I ran this in axicli...
javascript source:var hatch = function(rectangle, lineLength, lineDensity) { var g = new Group({}); var spacing = 10 * lineDensity; var slip = false; var startx = rectangle.x, starty = rectangle.y, lx = rectangle.width, ly = rectangle.height; for (var y=starty; y<starty + ly; y+=spacing) { var x = startx; if (slip) x += (spacing+lineLength) / 2; for (; x<startx + lx; x+=(spacing+lineLength)) { g.addChild(new Path.Line({ from: [x, y], to: [x+lineLength, y], strokeColor: 'blue', })); var path = new Path.Line({ from: [x, y], to: [x+lineLength, y], strokeColor: 'blue', }).rotate(90); g.addChild(path); } if (slip) { x-=(lineLength / 2); slip = false; } else slip = true; } return g; } var lines = [ new Path.Circle({ center: view.center, radius: 240, strokeColor: 'black', fillColor: 'white', }), new Path.Circle({ center: view.center, radius: 120, strokeColor: 'black', fillColor: 'white', }) ] for (var y=view.center.y-100; y<=view.center.y+100; y+=20) { lines.concat(new Path.Line({ from: [0,y], to: [view.size.width,y], strokeColor: 'black', })); } blackLines = new Layer({ name: '3 outline', children: lines, }); // hatching goes last, gets pushed to top. outerClip = new Layer({ name: '1 outer clip', children: [ new Path.Circle({ center: view.center, radius: 240, strokeColor: 'black', fillColor: 'white', clipMask: true, }), hatch(new Rectangle([0, 0], [view.size.width, view.size.height]), 20, 2.0), // this is the inner path of the donut, used for occluding these hashes new Path.Circle({ center: view.center, radius: 120, strokeColor: 'black', fillColor: 'white', }), ], clipped: true }); innerClip = new Layer({ name: '2 inner clip', children: [ new Path.Circle({ center: view.center, radius: 120, strokeColor: 'black', fillColor: 'white', clipMask: true, }), hatch(new Rectangle([200,200], [400,400]), 10, 1.0) .rotate(45), ], clipped: true }); outerClip.bringToFront(); innerClip.bringToFront();
python source: :noweb yes :exports results :results html :session paperscript-exportf = """ <<hatching>>""" paperscript_it("hatching", f)