├── README.md ├── config.json ├── draw.js ├── drawbot.gif ├── modules ├── BotClient.js ├── BotController.js ├── Config.js ├── LocalServer.js └── arcToBezier.js ├── package-lock.json ├── package.json ├── public ├── control │ ├── index.html │ └── js │ │ └── radialIndicator.min.js └── index.html └── wiring ├── drawbot_wiring.fzz └── drawbot_wiring.jpg /README.md: -------------------------------------------------------------------------------- 1 | ![](drawbot.gif) 2 | 3 | # Drawbot ✏️🤖 4 | 5 | Drawing robot capable of rendering SVG paths over WebSockets. Powered by a Raspberry Pi running Node.js. 6 | 7 | ## Parts List 8 | ### Printable Parts 9 | * [Motor Mounts](https://www.thingiverse.com/thing:3054707) (two of each) 10 | * [Electronics Case](https://www.thingiverse.com/thing:3058312) 11 | * [Pen Holder Gondola](https://www.thingiverse.com/thing:372244) 12 | * [Servo Horn Extension](https://www.thingiverse.com/thing:2427037) 13 | 14 | ### Everything Else (~$150 or less) 15 | 16 | * [NEMA 17 Stepper Motors, 36.8oz.in 12v 0.4a](https://www.amazon.com/Stepping-Motor-26Ncm-36-8oz-Printer/dp/B00PNEQ9T4) (need 2) - $23.98 17 | * [Suction Cup with Quick Release](https://www.harborfreight.com/suction-cup-with-quick-release-62715.html) (x2) - $5.98 18 | * [Spiderwire 80-pound fishing line](https://www.amazon.com/gp/product/B00LDYLVVO/ref=oh_aui_search_detailpage?ie=UTF8&psc=1) - $17.56 19 | * [Micro USB cable 10-ft. 2-pack](https://www.amazon.com/Android-Extremely-Durable-Charging-Generation/dp/B01NAG0YPD/ref=sr_1_8) - $8.99 20 | * [Micro USB breakout 10-pcs](https://www.amazon.com/MagiDeal-10pcs-Female-Pinboard-2-54mm/dp/B0183KF7TM/ref=sr_1_4) (only need 2) - $6.59 21 | * [USB type a female breakout 2-pack](https://www.ebay.com/itm/USB-2-0-Type-A-Female-Breakout-Board-2-54mm-Header-Gold-plated-Pack-of-2/302540094106) - $4.50 22 | * [Raspberry Pi Zero W](http://www.microcenter.com/product/486575/Zero_W) - $10) 23 | * [Easydriver Stepper Motor Driver](https://www.amazon.com/Cylewet-Easydriver-Stepper-Headers-Screwdriver/dp/B073D4H4F4/ref=sr_1_cc_1) (2-pack) - $9.99 24 | * [SG92R](http://www.microcenter.com/product/454408/Micro_Servo) Micro Servo motor - $6.99 25 | * [6003zz bearing 17x35x10](https://www.amazon.com/VXB-6003ZZ-Bearing-17x35x10-Shielded/dp/B002BBCO32/ref=sr_1_1) (need 2) - $11 26 | * [10pcs 500mm 3-pin servo extensions](https://www.amazon.com/White-Terminal-Female-Extension-Airplane/dp/B01HLUZO4S/) (need several) - $7.69 27 | * [2.1mm x 5.5mm barrel jack 15pcs](https://www.amazon.com/15PCS-Power-Socket-Barrel-Type-DC-005/dp/B00W944ACE/ref=sr_1_5) (need 1) - $7.59 28 | * [12v 1a power adapter 2.1mm/5.5mm lead](https://www.amazon.com/Dericam-Power-Adapter-Supply-1000mA/dp/B01N3SNRE4/ref=sr_1_3) - $9.99 29 | * [USB Micro Power Adapter](https://www.amazon.com/Keyestudio-Raspberry-Supply-Adapter-Charger/dp/B073RBXX2G/ref=sr_1_20) - $5.99 30 | * [Pololu Universal Mounting Hub for 5mm shaft, #4-40 holes (2-pack)](https://www.pololu.com/product/1203) - $11.44 31 | * [USB fan (40mm)](https://www.amazon.com/gp/product/B071JB9WYB/) - $7.99 32 | * \#8-32 x 1-5/8 in. Eye Bolts (2-pack) - $0.98 33 | * 8 screws for spools (#4-40 x ~½") 34 | * 8 screws for motors (M3-.50 x 6mm metric machine screws) 35 | * 1-2 small screws for gondola to secure pen/marker 36 | * [Mounting Putty](https://www.amazon.com/Loctite-Fun-Tak-Mounting-2-Ounce-1087306/dp/B001F57ZPW) - $4.87 37 | * Standard wire or jumper wires (and headers for components if using jumpers) 38 | * Dry erase markers (or other drawing implements) 39 | * Micro SD card (class 10 or better) 40 | 41 | ## Wiring 42 | 43 | ![](wiring/drawbot_wiring.jpg) 44 | 45 | ## Hardware Assembly 46 | 47 | Thank you to the kind people at the Johnson County Library's Black & Veatch [MakerSpace](https://www.jocolibrary.org/makerspace) for documenting the assembly process at [instructables.com](https://www.instructables.com/id/Drawbot/). 48 | 49 | ## Raspberry Pi Zero W Basic Setup 50 | 1. **Download and install [Etcher](https://etcher.io/)**. 51 | 2. **Download and install latest [Raspbian OS](https://www.raspberrypi.org/downloads/raspbian/)** and flash it onto your SD card with Etcher. 52 | 3. **Enable SSH** by adding a blank file named `ssh` (no extension) to the `boot` directory on the Pi's SD card. (Last tested with Raspbian Stretch Lite 2018-06-27 version.) 53 | 4. **Set up Wifi** by adding a file named `wpa_supplicant.conf` to the same `boot` directory with the following contents (replace `MySSID` and `MyPassword` with your credentials): 54 | ``` 55 | ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev 56 | update_config=1 57 | 58 | network={ 59 | ssid="MySSID" 60 | psk="MyPassword" 61 | } 62 | ``` 63 | 64 | ## Software Prerequisites 65 | From a device connected to the same network as the Drawbot Pi, SSH into the Pi with `ssh pi@raspberrypi.local`. The default password is `raspberry`. 66 | 67 | Then, on the Drawbot Pi: 68 | 69 | 1. **Update, upgrade, and install NPM, Git.** (Automatically answer "yes" to everything.): 70 | * `sudo apt-get update` 71 | * `sudo apt-get upgrade` 72 | * `sudo apt-get install npm` 73 | * `sudo apt-get install git` 74 | 75 | 2. **Install Node.js.** 76 | * `sudo npm install -g n` (Install **n** for node.js version management. [github.com/tj/n](https://github.com/tj/n)) 77 | * `sudo n stable` (Install latest stable version of Node.js. Last tested with v10.8.0) 78 | 79 | 3. **Upgrade NPM.** (and remove old apt-get version). 80 | * `sudo npm install npm@latest -g` 81 | * `sudo apt-get remove npm` 82 | * `sudo reboot` (After rebooting, you'll have to SSH into the Pi again.) 83 | 84 | 4. **Install pigpio C library.** [npmjs.com/package/pigpio](https://www.npmjs.com/package/pigpio) 85 | * `sudo apt-get install pigpio` (Only if you're using Raspbian *Lite*.) 86 | * `npm install pigpio` 87 | 88 | ## Installation 89 | On the Drawbot Pi: 90 | 91 | 1. `git clone https://github.com/andywise/drawbot.git` to clone this repository. 92 | 2. `cd drawbot` 93 | 3. `npm i` 94 | 95 | ## Use 96 | On the Drawbot Pi: 97 | 98 | * Run `npm start` or `sudo node draw.js` to start the Drawbot controller. 99 | 100 | ## Controlling the Drawbot 101 | On a device connected to the same local network as the Drawbot Pi: 102 | 103 | * Go to [raspberrypi.local/control](http://raspberrypi.local/control) to access the Drawbot control interface. 104 | * Use the "bullseye" interface to manually position the gondola, or raise/lower the pen/marker. 105 | * Use the settings panel to configure the Drawbot's `D`, `X`, and `Y` values (see "Configuration" below). 106 | * Drag and drop an SVG file onto the control interface to send artwork to the Drawbot! 107 | 108 | ### SVG Artwork Notes: 109 | * **Only the first `` element will be drawn**, so if necessary, combine artwork into a single compound path. 110 | * The Drawbot will scale artwork so that **1 pixel = 1 millimeter**. 111 | 112 | ## Configuration 113 | * **Enter value for `D`:** measure distance between string starting points (in millimeters). 114 | * **Enter starting `X` and `Y` values:** measure distance from left and top (respectively) of left string starting point to initial pen position (also in mm). 115 | * Note: Values will be stored in the `config.json` file. 116 | 117 | ## Rendering Raster Artwork (Coming Soon!) 118 | * For now, go check out ["TSP Art."](https://wiki.evilmadscientist.com/TSP_art) 119 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "botID": "drawbot", 3 | "swapDirections": [ 4 | true, 5 | true 6 | ], 7 | "localPort": 80, 8 | "baseDelay": 2, 9 | "d": 1000, 10 | "startPos": { 11 | "x": 100, 12 | "y": 100 13 | }, 14 | "stepsPerMM": [ 15 | 10, 16 | 10 17 | ], 18 | "penPauseDelay": 400, 19 | "remoteURL": "", 20 | "pins": { 21 | "leftDir": 20, 22 | "rightDir": 6, 23 | "leftStep": 21, 24 | "rightStep": 13, 25 | "penServo": 18 26 | }, 27 | "stepResolutionPins": { 28 | "leftMotor": { 29 | "ms1": 2, 30 | "ms2": 3, 31 | "ms3": 4 32 | }, 33 | "rightMotor": { 34 | "ms1": 17, 35 | "ms2": 27, 36 | "ms3": 22 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /draw.js: -------------------------------------------------------------------------------- 1 | // 2 | // ,--. ,--. ,--. 3 | // ,-| ,--.--.,--,--,--. ,--| |-. ,---.,-' '-. 4 | // ' .-. | .--' ,-. | |.'.| | .-. | .-. '-. .-' 5 | // \ `-' | | \ '-' | .'. | `-' ' '-' ' | | 6 | // `---'`--' `--`--'--' '--'`---' `---' `--' 7 | // Created by Andy Wise 8 | // 9 | 10 | // import external and node-specific modules 11 | var Config = require('./modules/Config') 12 | var BotController = require('./modules/BotController') 13 | var LocalServer = require('./modules/LocalServer') 14 | // var BotClient = require('./modules/BotClient') // for optional remote drawbot relay server client 15 | 16 | // SETUP 17 | var botController, botClient, localServer 18 | var config = Config('config.json', () => { 19 | 20 | // Main Controller 21 | botController = BotController(config) 22 | 23 | // Local Server 24 | localServer = LocalServer(config, botController) 25 | botController.localio = localServer.io 26 | 27 | // Optional: Remote Drawbot Relay Server (requires "BotClient" import above, and "remoteURL" value in config.json) 28 | // botClient = BotClient(config, botController) 29 | // botController.client = botClient 30 | 31 | // Initialize! 32 | go() 33 | }) 34 | 35 | // START 36 | var go = () => { 37 | botController.updateStringLengths() 38 | localServer.start() 39 | } 40 | 41 | // GRACEFUL EXIT 42 | // per http://joseoncode.com/2014/07/21/graceful-shutdown-in-node-dot-js/ 43 | // and https://github.com/fivdi/pigpio/issues/6 44 | var shutdown = () => { 45 | console.log('stopping the drawbot app...') 46 | localServer.server.close() 47 | process.exit(0) 48 | } 49 | process.on('SIGHUP', shutdown) 50 | process.on('SIGINT', shutdown) 51 | process.on('SIGTERM', shutdown) 52 | process.on('SIGCONT', shutdown) -------------------------------------------------------------------------------- /drawbot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andywise/drawbot/d2767b4d380ca4a06da9470a63d92077945ee56c/drawbot.gif -------------------------------------------------------------------------------- /modules/BotClient.js: -------------------------------------------------------------------------------- 1 | // socket.io Bot Client (as opposed to an "Input Client") 2 | // BotClient.js 3 | 4 | const { exec } = require('child_process') // https://nodejs.org/api/child_process.html 5 | var ioc = require( 'socket.io-client' ) 6 | 7 | var BotClient = (cfg, controller) => { 8 | var c = controller 9 | var config = cfg.data 10 | var client = ioc.connect(config.remoteURL) 11 | 12 | client.once('connect',function(){ 13 | console.log('bot connected to '+config.remoteURL+'!') 14 | client.emit('registerBot',config.botID) 15 | 16 | client.on('drawpath',function(data){ 17 | c.addPath(data.path) 18 | }) 19 | client.on('drawart',function(data){ 20 | c.paths = [] 21 | c.drawingPath = false 22 | c.addPath(data.path) 23 | }) 24 | client.on('r',function(data){ 25 | console.log('r',data) 26 | c.rotate(Number(data.m), Number(data.dir), Number(data.d), Number(data.steps)) 27 | }) 28 | client.on('pen',function(data){ 29 | c.pen(data.up) 30 | }) 31 | client.on('setStartPos',function(data){ 32 | c.setStartPos(data) 33 | client.emit('DXY',{ 34 | d: c._D, 35 | x: c.startPos.x, 36 | y: c.startPos.y, 37 | strings: c.startStringLengths, 38 | botID: c._BOT_ID 39 | }) 40 | }) 41 | client.on('setD',function(data){ 42 | c.setD(Number(data.d)) 43 | client.emit('DXY',{ 44 | d: c._D, 45 | x: c.startPos.x, 46 | y: c.startPos.y, 47 | strings: c.startStringLengths, 48 | botID: c._BOT_ID 49 | }) 50 | }) 51 | client.on('moveto',function(data){ 52 | c.moveTo(data.x,data.y) 53 | }) 54 | client.on('getDXY', function(data){ 55 | client.emit('DXY',{ 56 | d: c._D, 57 | x: c.startPos.x, 58 | y: c.startPos.y, 59 | strings: c.startStringLengths, 60 | botID: c._BOT_ID 61 | }) 62 | }) 63 | client.on('pause', function(data){ 64 | if(data.botID == c._BOT_ID) pause() 65 | }) 66 | client.on('reboot', function(data){ 67 | console.log('reboot',data) 68 | if(data.botID == c._BOT_ID) exec('sudo reboot') 69 | }) 70 | }) 71 | 72 | return client 73 | } 74 | 75 | 76 | 77 | module.exports = BotClient -------------------------------------------------------------------------------- /modules/BotController.js: -------------------------------------------------------------------------------- 1 | var Gpio = require('pigpio').Gpio 2 | var cBezier = require('adaptive-bezier-curve') 3 | var qBezier = require('adaptive-quadratic-curve') 4 | var svgParse = require('svg-path-parser') 5 | var arcToBezier = require('./arcToBezier')// copied from svg-arc-to-bezier npm library, because it uses es6 import instead of require 6 | 7 | var BotController = (cfg) => { 8 | 9 | var bc = {} 10 | var config = cfg.data 11 | 12 | 13 | ///////////////////////////////// 14 | // MAIN SETUP VARIABLES 15 | bc._BOT_ID = config.botID // || 'drawbot' 16 | bc._DIRSWAP = config.swapDirections // || [true, true] 17 | bc.baseDelay = config.baseDelay // || 2 18 | bc._D = config.d // || 1000// default distance between string starts 19 | bc.startPos = config.startPos // || { x: 100, y: 100 } 20 | bc.stepsPerMM = config.stepsPerMM // || [5000/500, 5000/500] // steps / mm 21 | bc.penPause = config.penPauseDelay // || 200 // pause for pen up/down movement (in ms) 22 | 23 | 24 | ///////////////////////////////// 25 | // GPIO SETUP 26 | var gmOut = {mode: Gpio.OUTPUT} 27 | var dirPins = [ 28 | new Gpio(config.pins.leftDir, gmOut), 29 | new Gpio(config.pins.rightDir, gmOut) 30 | ] 31 | var stepPins = [ 32 | new Gpio(config.pins.leftStep, gmOut), 33 | new Gpio(config.pins.rightStep, gmOut) 34 | ] 35 | // set up servo GPIO pin 36 | var servo = new Gpio(config.pins.penServo, gmOut) 37 | 38 | // ^ Step resolution Pins 39 | const leftMotorMs1= new Gpio(config.stepResolutionPins.leftMotor.ms1, gmOut) 40 | const leftMotorMs2= new Gpio(config.stepResolutionPins.leftMotor.ms2, gmOut) 41 | const leftMotorMs3= new Gpio(config.stepResolutionPins.leftMotor.ms3, gmOut) 42 | const rightMotorMs1= new Gpio(config.stepResolutionPins.rightMotor.ms1, gmOut) 43 | const rightMotorMs2= new Gpio(config.stepResolutionPins.rightMotor.ms2, gmOut) 44 | const rightMotorMs3= new Gpio(config.stepResolutionPins.rightMotor.ms3, gmOut) 45 | 46 | // ^ Step resolution settings 47 | // * Were configuring our driver for an Eighth Step resolution. Also note that these pinouts 48 | // * correspond to the A4988 StepStick stepper motor driver: 49 | // ? https://www.pololu.com/product/1182 50 | // 51 | // * We're not adjusting the values on the fly so they can be set here and not touched, but if your resolution 52 | // * needs to vary at runtime you can adjust the values of these pins. More information 53 | // * on pin configurations can be found here: 54 | // ? https://howtomechatronics.com/tutorials/arduino/how-to-control-stepper-motor-with-a4988-driver-and-arduino/ 55 | leftMotorMs1.digitalWrite(1) 56 | leftMotorMs2.digitalWrite(1) 57 | leftMotorMs3.digitalWrite(0) 58 | rightMotorMs1.digitalWrite(1) 59 | rightMotorMs2.digitalWrite(1) 60 | rightMotorMs3.digitalWrite(0) 61 | 62 | ///////////////////////////////// 63 | // CONTROLLER VARIABLES 64 | 65 | // TODO: isolate private/public stuff 66 | 67 | bc.pos = {x:0, y:0} 68 | bc.penPos = 0 69 | bc.paused = false 70 | 71 | // string length stuff 72 | bc.startStringLengths = [0, 0] 73 | bc.stringLengths = [0, 0] 74 | bc.startSteps = [0, 0] 75 | bc.currentSteps = [0, 0] 76 | bc.stepCounts = [0, 0] 77 | bc.steppeds = [0, 0] 78 | bc.paths = [] 79 | bc.drawingPath = false 80 | 81 | 82 | ///////////////////////////////// 83 | // HARDWARE METHODS 84 | 85 | bc.updateStringLengths = () => { 86 | bc.startStringLengths = [ 87 | Math.sqrt( (bc.startPos.x * bc.startPos.x) + (bc.startPos.y * bc.startPos.y) ), 88 | Math.sqrt( ( (bc._D - bc.startPos.x) * (bc._D - bc.startPos.x) ) + (bc.startPos.y * bc.startPos.y) ) 89 | ] 90 | bc.stringLengths = [bc.startStringLengths[0],bc.startStringLengths[1]] 91 | bc.startSteps = [Math.round(bc.stringLengths[0] * bc.stepsPerMM[0]), Math.round(bc.stringLengths[1] * bc.stepsPerMM[1])] 92 | bc.currentSteps = [bc.startSteps[0], bc.startSteps[1]] 93 | 94 | console.log('bc.startPos',JSON.stringify(bc.startPos)) 95 | console.log('startStringLengths', JSON.stringify(bc.startStringLengths)) 96 | return bc.startStringLengths 97 | } 98 | 99 | bc.setStartPos = (data) => { 100 | cfg.data.startPos.x = bc.startPos.x = Number(data.x)// set values and store in config 101 | cfg.data.startPos.y = bc.startPos.y = Number(data.y)// set values and store in config 102 | cfg.save()// save to local config.json file 103 | bc.updateStringLengths() 104 | } 105 | bc.setD = (data) => { 106 | cfg.data.d = bc._D = Number(data)// set value and store in config 107 | cfg.save()// save to local config.json file 108 | bc.updateStringLengths() 109 | } 110 | 111 | bc.pen = (dir) => { 112 | bc.penPos = dir 113 | // 0=down, 1=up 114 | // 544 to 2400 115 | var servoMin = 544 116 | var servoMax = 2400 117 | var servoD = servoMax-servoMin 118 | var servoUpPos = servoMin+Math.floor(servoD*0.35) 119 | var servoDnPos = servoMin 120 | if(dir){ 121 | // lift pen up 122 | // console.log('up') 123 | servo.servoWrite(servoUpPos) 124 | }else{ 125 | // put pen down 126 | // console.log('down') 127 | servo.servoWrite(servoDnPos) 128 | // servo.digitalWrite(0) 129 | } 130 | } 131 | bc.penThen = (dir, callback) => { 132 | if(dir!=bc.penPos){ 133 | bc.pen(dir) 134 | if (callback!=undefined){ 135 | setTimeout(callback, bc.penPause) 136 | } 137 | }else{ 138 | callback() 139 | } 140 | } 141 | 142 | bc.makeStep = (m, d) => { 143 | // console.log('step',d) 144 | if(bc._DIRSWAP[m]) d = !d// swap direction if that setting is on 145 | dirPins[m].digitalWrite(d) 146 | stepPins[m].digitalWrite(1) 147 | setTimeout(function(){ 148 | stepPins[m].digitalWrite(0) 149 | },1) 150 | } 151 | 152 | // TODO: This could move to a python script for faster execution (faster than bc.baseDelay=2 miliseconds) 153 | bc.rotateBoth = (s1, s2, d1, d2, callback) => { 154 | // console.log('bc.rotateBoth',s1,s2,d1,d2) 155 | var steps = Math.round(Math.max(s1,s2)) 156 | var a1 = 0 157 | var a2 = 0 158 | var stepped = 0 159 | 160 | var doStep = function(){ 161 | if(!bc.paused){ 162 | setTimeout(function(){ 163 | // console.log(stepped,steps) 164 | if(stepped=steps){ 170 | a1 -= steps 171 | bc.makeStep(0,d1) 172 | } 173 | 174 | a2 += s2 175 | if(a2>=steps){ 176 | a2 -= steps 177 | bc.makeStep(1,d2) 178 | } 179 | 180 | doStep() 181 | 182 | }else{ 183 | // console.log('bc.rotateBoth done!') 184 | if (callback!=undefined) callback() 185 | } 186 | }, bc.baseDelay) 187 | }else{ 188 | // paused! 189 | console.log('paused!') 190 | bc.paused = false 191 | } 192 | } 193 | doStep() 194 | } 195 | 196 | bc.rotate = (motorIndex, dirIndex, delay, steps, callback) => { 197 | // console.log('bc.rotate',motorIndex, dirIndex, delay, steps) 198 | bc.stepCounts[motorIndex] = Math.round(steps) 199 | bc.steppeds[motorIndex] = 0 200 | // var dir = (dirIndex==1) ? 0 : 1// reverses direction 201 | 202 | // doStep, then wait for delay d 203 | var doStep = function(d, m){ 204 | bc.makeStep(m, dirIndex)// changed to dirIndex from dir 205 | bc.steppeds[m]++ 206 | if(bc.steppeds[m] < bc.stepCounts[m]){ 207 | setTimeout(function(){ 208 | // console.log(m, bc.steppeds[m], "/", bc.stepCounts[m], d*bc.steppeds[m], "/", bc.stepCounts[m]*d) 209 | doStep(d, m) 210 | }, d) 211 | }else{ 212 | // done 213 | if(callback!=undefined) callback() 214 | } 215 | } 216 | doStep(delay,motorIndex) 217 | } 218 | 219 | 220 | ///////////////////////////////// 221 | // DRAWING METHODS 222 | 223 | bc.moveTo = (x, y, callback, penDir = 1) => { 224 | // console.log('---------- bc.moveTo',x,y,' ----------') 225 | 226 | // convert x,y to l1,l2 (ideal, precise string lengths) 227 | var X = x + bc.startPos.x 228 | var Y = y + bc.startPos.y 229 | var X2 = X * X 230 | var Y2 = Y * Y 231 | var DsubX = bc._D - X 232 | var DsubX2 = DsubX * DsubX 233 | L1 = Math.sqrt( X2 + Y2 ) 234 | L2 = Math.sqrt( DsubX2 + Y2 ) 235 | 236 | // console.log('L:',L1,L2) 237 | 238 | // convert string lengths to motor steps (float to int) 239 | var s1 = Math.round(L1 * bc.stepsPerMM[0]) 240 | var s2 = Math.round(L2 * bc.stepsPerMM[1]) 241 | // console.log('s:',s1,s2) 242 | // console.log('bc.currentSteps:',bc.currentSteps[0],bc.currentSteps[1]) 243 | 244 | // get difference between target steps and current steps (+/- int) 245 | var sd1 = s1 - bc.currentSteps[0] 246 | var sd2 = s2 - bc.currentSteps[1] 247 | // console.log('sd:',sd1,sd2) 248 | 249 | // get directions from steps difference 250 | var sdir1 = (sd1>0) ? 0 : 1 251 | var sdir2 = (sd2>0) ? 1 : 0 252 | // console.log('sdir:',sdir1,sdir2) 253 | 254 | // get steps with absolute value of steps difference 255 | var ssteps1 = Math.abs(sd1) 256 | var ssteps2 = Math.abs(sd2) 257 | // console.log('ssteps:',ssteps1,ssteps2) 258 | 259 | 260 | function doRotation(){ 261 | // do the rotation! 262 | bc.rotateBoth(ssteps1,ssteps2,sdir1,sdir2,callback) 263 | 264 | // store new current steps 265 | bc.currentSteps[0] = s1 266 | bc.currentSteps[1] = s2 267 | 268 | // store new bc.pos 269 | bc.pos.x = x 270 | bc.pos.y = y 271 | } 272 | 273 | if(penDir != 0){ 274 | // MOVETO (default) 275 | // pen up, then 276 | bc.penThen(1, doRotation) 277 | }else{ 278 | // LINETO 279 | doRotation() 280 | } 281 | 282 | } 283 | 284 | bc.lineTo = (x,y,callback) => { 285 | // pen down, then 286 | 287 | bc.penThen(0,function(){ 288 | bc.moveTo(Number(x), Number(y), callback, 0)// 0 makes bc.moveTo happen with pen down instead of up 289 | }) 290 | } 291 | 292 | 293 | bc.addPath = (pathString) => { 294 | console.log('bc.addPath') 295 | bc.paths.push(pathString) 296 | console.log('pathcount: ',bc.paths.length) 297 | if(bc.paths.length==1 && bc.drawingPath==false){ 298 | bc.drawNextPath() 299 | } 300 | } 301 | 302 | bc.pause = () => { 303 | bc.paused = true 304 | } 305 | 306 | bc.drawNextPath = () => { 307 | if(bc.paths.length>0){ 308 | bc.drawPath(bc.paths.shift())// return/remove first path from array 309 | }else{ 310 | console.log("Done drawing all the paths. :)") 311 | } 312 | } 313 | 314 | bc.drawPath = (pathString) => { 315 | bc.drawingPath = true 316 | console.log('drawing path...') 317 | var commands = svgParse(pathString) 318 | // var commands = pathString.split(/(?=[MmLlHhVvZz])/) 319 | var cmdCount = commands.length 320 | console.log(cmdCount) 321 | var cmdIndex = 0 322 | var prevCmd 323 | function doCommand(){ 324 | if(cmdIndex { 619 | var n=0 620 | var cCount = curves.length 621 | function doCommand(){ 622 | if(n { 641 | var n = 0// curret bezier step in iteration 642 | var pts = cBezier(points[0], points[1], points[2], points[3], scale) 643 | var ptCount = pts.length 644 | function doCommand(){ 645 | if(n { 657 | var n = 0// curret bezier step in iteration 658 | var pts = qBezier(points[0], points[1], points[2], scale) 659 | var ptCount = pts.length 660 | function doCommand(){ 661 | if(n { 674 | // http://jsfiddle.net/heygrady/X5fw4/ 675 | // Calculate a point on a circle 676 | function circle(t, radius) { 677 | var r = radius || 100, 678 | arc = Math.PI * 2 679 | 680 | // calculate current angle 681 | var alpha = t * arc 682 | 683 | // calculate current coords 684 | var x = Math.sin(alpha) * r, 685 | y = Math.cos(alpha) * r 686 | 687 | // return coords 688 | return [x, y * -1] 689 | } 690 | 691 | var n = 0 //current step 692 | var pi = 3.1415926 693 | var C = 2*pi*r 694 | var seg = C 695 | 696 | function doCommand(){ 697 | if(n<=seg){ 698 | var t = n/seg 699 | var p = circle(t, r) 700 | if(n==0){ 701 | bc.moveTo(x+p[0], y+p[1], doCommand) 702 | }else{ 703 | bc.lineTo(x+p[0], y+p[1], doCommand) 704 | } 705 | n++ 706 | }else{ 707 | if (callback!=undefined) callback() 708 | } 709 | } 710 | doCommand() 711 | } 712 | bc.drawCircles = (o) => { 713 | console.log(o.count) 714 | var count = o.count 715 | var n = 0 716 | function doCommand(){ 717 | if(n { 4 | var c = {} 5 | 6 | c.data = {} 7 | c.save = (cb) => { 8 | jsonfile.writeFile(file, c.data, {spaces: 2, EOL: '\r\n'}, function (err) { 9 | console.error(err) 10 | if(cb!=undefined) cb() 11 | }) 12 | } 13 | 14 | var open = (cb) => { 15 | jsonfile.readFile(file, (err, o) => { 16 | c.data = o 17 | if(cb!=undefined) cb() 18 | }) 19 | } 20 | open(callback) 21 | 22 | return c 23 | } 24 | module.exports = Config -------------------------------------------------------------------------------- /modules/LocalServer.js: -------------------------------------------------------------------------------- 1 | 2 | // LOCAL SERVER 3 | // LocalServer.js 4 | 5 | var express = require('express') 6 | var app = express() 7 | var server = require('http').Server(app) 8 | var io = require('socket.io')(server) 9 | 10 | var LocalServer = (cfg, controller) => { 11 | var c = controller 12 | var config = cfg.data 13 | 14 | var ls = { 15 | express: express, 16 | app: app, 17 | server: server, 18 | io: io 19 | } 20 | 21 | app.use(express.static('public')) 22 | 23 | io.on('connection', function (socket) { 24 | console.log('connection!') 25 | socket.emit('connected', { hello: 'world' }) 26 | 27 | socket.on('pen',function(data){ 28 | c.pen(data.up) 29 | }) 30 | socket.on('r',function(data){ 31 | c.rotate(Number(data.m), Number(data.dir), Number(data.d), Number(data.steps)) 32 | }) 33 | socket.on('drawpath',function(data){ 34 | c.addPath(data.path) 35 | }) 36 | socket.on('drawart',function(data){ 37 | c.paths = [] 38 | c.drawingPath = false 39 | c.addPath(data.path) 40 | }) 41 | socket.on('setStartPos',function(data){ 42 | c.setStartPos(data) 43 | }) 44 | socket.on('setD',function(data){ 45 | c.setD(Number(data.d)) 46 | }) 47 | socket.on('moveto',function(data){ 48 | c.moveTo(data.x,data.y) 49 | }) 50 | socket.on('getDXY', function(data){ 51 | socket.emit('DXY',{ 52 | d: c._D, 53 | x: c.startPos.x, 54 | y: c.startPos.y, 55 | strings: c.startStringLengths 56 | }) 57 | }) 58 | socket.on('pause', function(data){ 59 | pause() 60 | }) 61 | socket.on('reboot', function(data){ 62 | exec('sudo reboot') 63 | }) 64 | }) 65 | 66 | ls.start = () => { 67 | server.listen(config.localPort, function(){ 68 | console.log('listening on port '+config.localPort+'...') 69 | }) 70 | } 71 | 72 | return ls 73 | } 74 | module.exports = LocalServer -------------------------------------------------------------------------------- /modules/arcToBezier.js: -------------------------------------------------------------------------------- 1 | // from https://github.com/colinmeinke/svg-arc-to-cubic-bezier 2 | // doing it this way since NPM version uses import instead of require 3 | 4 | const TAU = Math.PI * 2 5 | 6 | const mapToEllipse = ({ x, y }, rx, ry, cosphi, sinphi, centerx, centery) => { 7 | x *= rx 8 | y *= ry 9 | 10 | const xp = cosphi * x - sinphi * y 11 | const yp = sinphi * x + cosphi * y 12 | 13 | return { 14 | x: xp + centerx, 15 | y: yp + centery 16 | } 17 | } 18 | 19 | const approxUnitArc = (ang1, ang2) => { 20 | const a = 4 / 3 * Math.tan(ang2 / 4) 21 | 22 | const x1 = Math.cos(ang1) 23 | const y1 = Math.sin(ang1) 24 | const x2 = Math.cos(ang1 + ang2) 25 | const y2 = Math.sin(ang1 + ang2) 26 | 27 | return [ 28 | { 29 | x: x1 - y1 * a, 30 | y: y1 + x1 * a 31 | }, 32 | { 33 | x: x2 + y2 * a, 34 | y: y2 - x2 * a 35 | }, 36 | { 37 | x: x2, 38 | y: y2 39 | } 40 | ] 41 | } 42 | 43 | const vectorAngle = (ux, uy, vx, vy) => { 44 | const sign = (ux * vy - uy * vx < 0) ? -1 : 1 45 | const umag = Math.sqrt(ux * ux + uy * uy) 46 | const vmag = Math.sqrt(ux * ux + uy * uy) 47 | const dot = ux * vx + uy * vy 48 | 49 | let div = dot / (umag * vmag) 50 | 51 | if (div > 1) { 52 | div = 1 53 | } 54 | 55 | if (div < -1) { 56 | div = -1 57 | } 58 | 59 | return sign * Math.acos(div) 60 | } 61 | 62 | const getArcCenter = ( 63 | px, 64 | py, 65 | cx, 66 | cy, 67 | rx, 68 | ry, 69 | largeArcFlag, 70 | sweepFlag, 71 | sinphi, 72 | cosphi, 73 | pxp, 74 | pyp 75 | ) => { 76 | const rxsq = Math.pow(rx, 2) 77 | const rysq = Math.pow(ry, 2) 78 | const pxpsq = Math.pow(pxp, 2) 79 | const pypsq = Math.pow(pyp, 2) 80 | 81 | let radicant = (rxsq * rysq) - (rxsq * pypsq) - (rysq * pxpsq) 82 | 83 | if (radicant < 0) { 84 | radicant = 0 85 | } 86 | 87 | radicant /= (rxsq * pypsq) + (rysq * pxpsq) 88 | radicant = Math.sqrt(radicant) * (largeArcFlag === sweepFlag ? -1 : 1) 89 | 90 | const centerxp = radicant * rx / ry * pyp 91 | const centeryp = radicant * -ry / rx * pxp 92 | 93 | const centerx = cosphi * centerxp - sinphi * centeryp + (px + cx) / 2 94 | const centery = sinphi * centerxp + cosphi * centeryp + (py + cy) / 2 95 | 96 | const vx1 = (pxp - centerxp) / rx 97 | const vy1 = (pyp - centeryp) / ry 98 | const vx2 = (-pxp - centerxp) / rx 99 | const vy2 = (-pyp - centeryp) / ry 100 | 101 | let ang1 = vectorAngle(1, 0, vx1, vy1) 102 | let ang2 = vectorAngle(vx1, vy1, vx2, vy2) 103 | 104 | if (sweepFlag === 0 && ang2 > 0) { 105 | ang2 -= TAU 106 | } 107 | 108 | if (sweepFlag === 1 && ang2 < 0) { 109 | ang2 += TAU 110 | } 111 | 112 | return [ centerx, centery, ang1, ang2 ] 113 | } 114 | 115 | const arcToBezier = ({ 116 | px, 117 | py, 118 | cx, 119 | cy, 120 | rx, 121 | ry, 122 | xAxisRotation = 0, 123 | largeArcFlag = 0, 124 | sweepFlag = 0 125 | }) => { 126 | const curves = [] 127 | 128 | if (rx === 0 || ry === 0) { 129 | return [] 130 | } 131 | 132 | const sinphi = Math.sin(xAxisRotation * TAU / 360) 133 | const cosphi = Math.cos(xAxisRotation * TAU / 360) 134 | 135 | const pxp = cosphi * (px - cx) / 2 + sinphi * (py - cy) / 2 136 | const pyp = -sinphi * (px - cx) / 2 + cosphi * (py - cy) / 2 137 | 138 | if (pxp === 0 && pyp === 0) { 139 | return [] 140 | } 141 | 142 | rx = Math.abs(rx) 143 | ry = Math.abs(ry) 144 | 145 | const lambda = 146 | Math.pow(pxp, 2) / Math.pow(rx, 2) + 147 | Math.pow(pyp, 2) / Math.pow(ry, 2) 148 | 149 | if (lambda > 1) { 150 | rx *= Math.sqrt(lambda) 151 | ry *= Math.sqrt(lambda) 152 | } 153 | 154 | let [ centerx, centery, ang1, ang2 ] = getArcCenter( 155 | px, 156 | py, 157 | cx, 158 | cy, 159 | rx, 160 | ry, 161 | largeArcFlag, 162 | sweepFlag, 163 | sinphi, 164 | cosphi, 165 | pxp, 166 | pyp 167 | ) 168 | 169 | const segments = Math.max(Math.ceil(Math.abs(ang2) / (TAU / 4)), 1) 170 | 171 | ang2 /= segments 172 | 173 | for (let i = 0; i < segments; i++) { 174 | curves.push(approxUnitArc(ang1, ang2)) 175 | ang1 += ang2 176 | } 177 | 178 | return curves.map(curve => { 179 | const { x: x1, y: y1 } = mapToEllipse(curve[ 0 ], rx, ry, cosphi, sinphi, centerx, centery) 180 | const { x: x2, y: y2 } = mapToEllipse(curve[ 1 ], rx, ry, cosphi, sinphi, centerx, centery) 181 | const { x, y } = mapToEllipse(curve[ 2 ], rx, ry, cosphi, sinphi, centerx, centery) 182 | 183 | return { x1, y1, x2, y2, x, y } 184 | }) 185 | } 186 | module.exports = arcToBezier -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drawbot", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "graceful-fs": { 8 | "version": "4.1.11", 9 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", 10 | "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", 11 | "optional": true 12 | }, 13 | "jsonfile": { 14 | "version": "4.0.0", 15 | "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", 16 | "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", 17 | "requires": { 18 | "graceful-fs": "^4.1.6" 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drawbot", 3 | "version": "1.0.0", 4 | "description": "a drawing robot", 5 | "main": "draw.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "sudo node draw.js" 9 | }, 10 | "keywords": [ 11 | "drawbot" 12 | ], 13 | "author": "Andy Wise", 14 | "license": "ISC", 15 | "dependencies": { 16 | "adaptive-bezier-curve": "^1.0.3", 17 | "adaptive-quadratic-curve": "^1.0.2", 18 | "express": "^4.15.3", 19 | "jsonfile": "^4.0.0", 20 | "socket.io": "^2.0.2", 21 | "socket.io-client": "^2.0.3", 22 | "svg-path-parser": "^1.0.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /public/control/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Drawbot Control 9 | 10 | 11 | 12 | 239 | 240 | 241 |
242 | 243 |
244 |
245 | 246 | 247 | 248 |
249 |
250 |
251 |
252 | 253 | 254 | 1000 255 | 256 |
257 |
258 |
259 |
260 | 261 | 262 | 100 263 | 264 |
265 |
266 |
267 |
268 | 269 | 270 | 10 271 | 272 |
273 |
274 | PEN 275 |
276 |
277 |
278 | 279 |
280 |
281 | 282 | 283 |
284 |
285 | 286 | 287 |
288 |
289 | 290 | 291 |
292 |
293 | 294 | 295 |
296 |
297 | 298 | 299 | 300 | 301 | 302 | 510 | 511 | 512 | -------------------------------------------------------------------------------- /public/control/js/radialIndicator.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | radialIndicator.js v 1.3.1 3 | Author: Sudhanshu Yadav 4 | Copyright (c) 2015 Sudhanshu Yadav - ignitersworld.com , released under the MIT license. 5 | Demo on: ignitersworld.com/lab/radialIndicator.html 6 | */ 7 | !function(t){var e=Function("return this")()||(42,eval)("this");"function"==typeof define&&define.amd?define(["jquery"],function(n){return e.radialIndicator=t(n,e)}):"object"==typeof module&&module.exports?module.exports=e.document?t(require("jquery"),e):function(e){if(!e.document)throw new Error("radialIndiactor requires a window with a document");return t(require("jquery")(e),e)}:e.radialIndicator=t(e.jQuery,e)}(function(t,e,n){function r(t){var e=/^#?([a-f\d])([a-f\d])([a-f\d])$/i;t=t.replace(e,function(t,e,n,r){return e+e+n+n+r+r});var n=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(t);return n?[parseInt(n[1],16),parseInt(n[2],16),parseInt(n[3],16)]:null}function i(t,e,n,r){return Math.round(n+(r-n)*t/e)}function a(t,e,n,a,o){var u=-1!=o.indexOf("#")?r(o):o.match(/\d+/g),l=-1!=a.indexOf("#")?r(a):a.match(/\d+/g),s=n-e,c=t-e;return u&&l?"rgb("+i(c,s,l[0],u[0])+","+i(c,s,l[1],u[1])+","+i(c,s,l[2],u[2])+")":null}function o(){for(var t=arguments,e=t[0],n=1,r=t.length;r>n;n++){var i=t[n];for(var a in i)i.hasOwnProperty(a)&&(e[a]=i[a])}return e}function u(t){return function(e){if(!t)return e.toString();e=e||0;for(var n=e.toString().split("").reverse(),r=t.split("").reverse(),i=0,a=0,o=r.length;o>i&&n.length;i++)"#"==r[i]&&(a=i,r[i]=n.shift());return r.splice(a+1,r.lastIndexOf("#")-a,n.reverse().join("")),r.reverse().join("")}}function l(t,e){function n(t){if(e.interaction){t.preventDefault();var n=-Math.max(-1,Math.min(1,t.wheelDelta||-t.detail)),i=null!=e.precision?e.precision:0,a=Math.pow(10,i),o=e.maxValue-e.minValue,u=r.current_value+Math.round(a*n*o/Math.min(o,100))/a;return r.value(u),!1}}var r=this;e=e||{},e=o({},s.defaults,e),this.indOption=e,"string"==typeof t&&(t=c.querySelector(t)),t.length&&(t=t[0]),this.container=t;var i=c.createElement("canvas");t.appendChild(i),this.canElm=i,this.ctx=i.getContext("2d"),this.current_value=e.initValue||e.minValue||0;var a=function(t){if(e.interaction){var n="touchstart"==t.type?"touchmove":"mousemove",a="touchstart"==t.type?"touchend":"mouseup",o=i.getBoundingClientRect(),u=o.top+i.offsetHeight/2,l=o.left+i.offsetWidth/2,s=function(t){t.preventDefault();var n=t.clientX||t.touches[0].clientX,i=t.clientY||t.touches[0].clientY,a=(h+d+Math.atan2(i-u,n-l))%(h+.0175),o=e.radius-1+e.barWidth/2,s=h*o,c=null!=e.precision?e.precision:0,f=Math.pow(10,c),v=Math.round(f*a*o*(e.maxValue-e.minValue)/s)/f;r.value(v)},f=function(){c.removeEventListener(n,s,!1),c.removeEventListener(a,f,!1)};c.addEventListener(n,s,!1),c.addEventListener(a,f,!1)}};i.addEventListener("touchstart",a,!1),i.addEventListener("mousedown",a,!1),i.addEventListener("mousewheel",n,!1),i.addEventListener("DOMMouseScroll",n,!1)}function s(t,e){var n=new l(t,e);return n._init(),n}var c=e.document,h=2*Math.PI,d=Math.PI/2,f=function(){var t=c.createElement("canvas").getContext("2d"),n=e.devicePixelRatio||1,r=t.webkitBackingStorePixelRatio||t.mozBackingStorePixelRatio||t.msBackingStorePixelRatio||t.oBackingStorePixelRatio||t.backingStorePixelRatio||1,i=n/r;return function(t,e,n){var r=n||c.createElement("canvas");return r.width=t*i,r.height=e*i,r.style.width=t+"px",r.style.height=e+"px",r.getContext("2d").setTransform(i,0,0,i,0,0),r}}();return l.prototype={constructor:s,_init:function(){var t=this.indOption,e=this.canElm,n=(this.ctx,2*(t.radius+t.barWidth));return this.formatter="function"==typeof t.format?t.format:u(t.format),this.maxLength=t.percentage?4:this.formatter(t.maxValue).length,f(n,n,e),this._drawBarBg(),this.value(this.current_value),this},_drawBarBg:function(){var t=this.indOption,e=this.ctx,n=2*(t.radius+t.barWidth),r=n/2;e.strokeStyle=t.barBgColor,e.lineWidth=t.barWidth,"transparent"!=t.barBgColor&&(e.beginPath(),e.arc(r,r,t.radius-1+t.barWidth/2,0,2*Math.PI),e.stroke())},value:function(t){if(t===n||isNaN(t))return this.current_value;t=parseFloat(t);var e=this.ctx,r=this.indOption,i=r.barColor,o=2*(r.radius+r.barWidth),u=r.minValue,l=r.maxValue,s=o/2;t=u>t?u:t>l?l:t;var c=null!=r.precision?r.precision:0,f=Math.pow(10,c),v=Math.round((t-u)*f/(l-u)*100)/f,m=r.percentage?v+"%":this.formatter(t);if(this.current_value=t,e.clearRect(0,0,o,o),this._drawBarBg(),"object"==typeof i)for(var p=Object.keys(i),g=1,x=p.length;x>g;g++){var b=p[g-1],y=p[g],C=i[b],M=i[y],w=t==b?C:t==y?M:t>b&&y>t?r.interpolate?a(t,b,y,C,M):M:!1;if(0!=w){i=w;break}}if(e.strokeStyle=i,r.roundCorner&&(e.lineCap="round"),e.beginPath(),e.arc(s,s,r.radius-1+r.barWidth/2,-d,h*v/100-d,!1),e.stroke(),r.displayNumber){var B=e.font.split(" "),I=r.fontWeight,V=r.fontSize||o/(this.maxLength-(Math.floor(1.4*this.maxLength/4)-1));B=r.fontFamily||B[B.length-1],e.fillStyle=r.fontColor||i,e.font=I+" "+V+"px "+B,e.textAlign="center",e.textBaseline=r.textBaseline,e.fillText(m,s,s)}return r.onChange.call(this.container,t),this},animate:function(t){var e=this.indOption,n=this.current_value||e.minValue,r=this,i=e.minValue,a=e.maxValue,o=e.frameNum||(e.percentage?100:500),u=null!=e.precision?e.precision:Math.ceil(Math.log(a-i/o)),l=Math.pow(10,u),s=Math.round((a-i)*l/o)/l;t=i>t?i:t>a?a:t;var c=n>t;return this.intvFunc&&clearInterval(this.intvFunc),this.intvFunc=setInterval(function(){if(!c&&n>=t||c&&t>=n){if(r.current_value==n)return clearInterval(r.intvFunc),void(e.onAnimationComplete&&e.onAnimationComplete(r.current_value));n=t}r.value(n),n!=t&&(n+=c?-s:s)},e.frameTime),this},option:function(t,e){return e===n?this.option[t]:(-1!=["radius","barWidth","barBgColor","format","maxValue","percentage"].indexOf(t)&&(this.indOption[t]=e,this._init().value(this.current_value)),void(this.indOption[t]=e))}},s.defaults={radius:50,barWidth:5,barBgColor:"#eeeeee",barColor:"#99CC33",format:null,frameTime:10,frameNum:null,fontColor:null,fontFamily:null,fontWeight:"bold",fontSize:null,textBaseline:"middle",interpolate:!0,percentage:!1,precision:null,displayNumber:!0,roundCorner:!1,minValue:0,maxValue:100,initValue:0,interaction:!1,onChange:function(){}},e.radialIndicator=s,t&&(t.fn.radialIndicator=function(e){return this.each(function(){var n=s(this,e);t.data(this,"radialIndicator",n)})}),s}); -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | Drawbot! -------------------------------------------------------------------------------- /wiring/drawbot_wiring.fzz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andywise/drawbot/d2767b4d380ca4a06da9470a63d92077945ee56c/wiring/drawbot_wiring.fzz -------------------------------------------------------------------------------- /wiring/drawbot_wiring.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andywise/drawbot/d2767b4d380ca4a06da9470a63d92077945ee56c/wiring/drawbot_wiring.jpg --------------------------------------------------------------------------------