├── .gitignore ├── README.md ├── index.html └── src ├── img ├── Mies.png └── cloud.svg ├── js ├── FileSaver.min.js ├── draw.js └── paper.js └── stylesheets └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /*.log 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Demo](https://tamg.github.io/mies/) 2 | 3 | alt text 4 | 5 | * **Mies** is a simple vector graphics editing tool based on the awesome **PaperJs** library. 6 | * Once you're done drawing you can download an SVG of your masterpiece. 7 | 8 | * This project is made at the [Recurse Center](https://www.recurse.com/), where I am currently learning programming. 9 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mies: Vector Editing Tool Using Paper.JS 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/img/Mies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tamg/mies/38a67f401fe6e5e87ac89c3ce329c4d734e9c891/src/img/Mies.png -------------------------------------------------------------------------------- /src/img/cloud.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/js/FileSaver.min.js: -------------------------------------------------------------------------------- 1 | /*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */ 2 | var saveAs=saveAs||function(e){"use strict";if(typeof e==="undefined"||typeof navigator!=="undefined"&&/MSIE [1-9]\./.test(navigator.userAgent)){return}var t=e.document,n=function(){return e.URL||e.webkitURL||e},r=t.createElementNS("http://www.w3.org/1999/xhtml","a"),o="download"in r,a=function(e){var t=new MouseEvent("click");e.dispatchEvent(t)},i=/constructor/i.test(e.HTMLElement)||e.safari,f=/CriOS\/[\d]+/.test(navigator.userAgent),u=function(t){(e.setImmediate||e.setTimeout)(function(){throw t},0)},s="application/octet-stream",d=1e3*40,c=function(e){var t=function(){if(typeof e==="string"){n().revokeObjectURL(e)}else{e.remove()}};setTimeout(t,d)},l=function(e,t,n){t=[].concat(t);var r=t.length;while(r--){var o=e["on"+t[r]];if(typeof o==="function"){try{o.call(e,n||e)}catch(a){u(a)}}}},p=function(e){if(/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(e.type)){return new Blob([String.fromCharCode(65279),e],{type:e.type})}return e},v=function(t,u,d){if(!d){t=p(t)}var v=this,w=t.type,m=w===s,y,h=function(){l(v,"writestart progress write writeend".split(" "))},S=function(){if((f||m&&i)&&e.FileReader){var r=new FileReader;r.onloadend=function(){var t=f?r.result:r.result.replace(/^data:[^;]*;/,"data:attachment/file;");var n=e.open(t,"_blank");if(!n)e.location.href=t;t=undefined;v.readyState=v.DONE;h()};r.readAsDataURL(t);v.readyState=v.INIT;return}if(!y){y=n().createObjectURL(t)}if(m){e.location.href=y}else{var o=e.open(y,"_blank");if(!o){e.location.href=y}}v.readyState=v.DONE;h();c(y)};v.readyState=v.INIT;if(o){y=n().createObjectURL(t);setTimeout(function(){r.href=y;r.download=u;a(r);h();c(y);v.readyState=v.DONE});return}S()},w=v.prototype,m=function(e,t,n){return new v(e,t||e.name||"download",n)};if(typeof navigator!=="undefined"&&navigator.msSaveOrOpenBlob){return function(e,t,n){t=t||e.name||"download";if(!n){e=p(e)}return navigator.msSaveOrOpenBlob(e,t)}}w.abort=function(){};w.readyState=w.INIT=0;w.WRITING=1;w.DONE=2;w.error=w.onwritestart=w.onprogress=w.onwrite=w.onabort=w.onerror=w.onwriteend=null;return m}(typeof self!=="undefined"&&self||typeof window!=="undefined"&&window||this.content);if(typeof module!=="undefined"&&module.exports){module.exports.saveAs=saveAs}else if(typeof define!=="undefined"&&define!==null&&define.amd!==null){define("FileSaver.js",function(){return saveAs})} 3 | -------------------------------------------------------------------------------- /src/js/draw.js: -------------------------------------------------------------------------------- 1 | 2 | var currentColor = 'SpringGreen' 3 | var currentStrokeWidth = 3 4 | 5 | var previousTool = '' 6 | var currentTool = 'line' 7 | var myPath 8 | var cursor 9 | 10 | //the layer where most things are inside of 11 | var baseLayer = new Layer({ 12 | name: 'baseLayer' 13 | }) 14 | 15 | //layer to hold the colorwheel tool 16 | var colorWheelLayer = new Layer({ 17 | name: 'colorWheelLayer' 18 | }) 19 | 20 | //activate baseLayer 21 | baseLayer.activate() 22 | 23 | //draw our artboard based on browser window size (view) 24 | var artBoardSize = new Size (900, 600) 25 | var artBoardTopX = Math.max(180,(view.center.x - artBoardSize.width/2)) 26 | var artBoardPoint = new Point (artBoardTopX, view.center.y - artBoardSize.height/2) 27 | 28 | var artBoard = new Path.Rectangle({ 29 | point: artBoardPoint, 30 | size: artBoardSize, 31 | strokeColor: '#cbcbcb', 32 | strokeWidth: .5, 33 | fillColor: 'ghostwhite', 34 | name: 'artboard', 35 | shadowColor: 'rgb(134, 149, 147)', 36 | shadowBlur: 30, 37 | shadowOffset: new Point(10, 10), 38 | }) 39 | 40 | //draw all the ui elements 41 | var ui = { 42 | 43 | info: new PointText({ 44 | point: artBoardPoint - [0, 10], 45 | fillColor: 'black', 46 | fontSize: 24, 47 | content: 'Click and Drag to draw a line' 48 | }), 49 | 50 | line: new Path.Line({ 51 | from: [50, 10], 52 | to: [100, -40], 53 | strokeWidth: 8, 54 | strokeColor: 'red', 55 | onClick: function(event) { 56 | currentTool = 'line' 57 | draw.line.activate() 58 | ui.info.content = 'Click and Drag to draw a ' + currentTool 59 | } 60 | }), 61 | 62 | brush: new CompoundPath({ 63 | children: [ 64 | new Path.Line({ 65 | segments: [[80,25], [80,75]], 66 | }), 67 | new Path.Line({ 68 | segments: [[60,75], [100,75]], 69 | }), 70 | new Path.Line({ 71 | segments: [[60, 75],[60, 90]], 72 | }), 73 | new Path.Line({ 74 | segments: [[70, 75],[70, 90]], 75 | }), 76 | new Path.Line({ 77 | segments: [[80, 75],[80, 90]], 78 | }), 79 | new Path.Line({ 80 | segments: [[90, 75],[90, 90]], 81 | }), 82 | new Path.Line({ 83 | segments: [[100, 75],[100, 90]], 84 | }), 85 | ], 86 | strokeCap: 'round', 87 | strokeJoin: 'round', 88 | strokeWidth: 10, 89 | strokeColor: 'red', 90 | fillColor: 'white', 91 | onClick: function(event) { 92 | currentTool = 'brush' 93 | draw.brush.activate() 94 | ui.info.content = 'Click and Drag to paint with a ' + currentTool 95 | } 96 | }), 97 | 98 | circle: new Path.Circle({ 99 | center: [80, 150], 100 | radius: 30, 101 | fillColor: 'red', 102 | onClick: function(event) { 103 | currentTool = 'circle' 104 | draw.circle.activate() 105 | ui.info.content = 'Click and Drag to draw a ' + currentTool 106 | } 107 | }), 108 | 109 | rect: new Path.Rectangle({ 110 | point: [50, 200], 111 | size: [60,60], 112 | fillColor: 'red', 113 | onClick: function(event) { 114 | currentTool = 'rectangle' 115 | draw.rectangle.activate() 116 | ui.info.content = 'Click and Drag to draw a ' + currentTool 117 | } 118 | }), 119 | 120 | arc: new Path.Arc({ 121 | from: [50, 320], 122 | through: [80, 290], 123 | to: [110, 320], 124 | strokeColor: 'red', 125 | strokeWidth: 7, 126 | onClick: function(event) { 127 | currentTool = 'arc' 128 | draw.arc.activate() 129 | ui.info.content = 'Click and Drag to draw an ' + currentTool 130 | } 131 | }), 132 | 133 | cloud: new Path({ 134 | segments:[[50, 370],[60, 350],[70, 370],[80, 350], 135 | [90, 370],[100, 350],[110,370]], 136 | strokeColor: 'red', 137 | strokeWidth: 8, 138 | onClick: function(event) { 139 | currentTool = 'cloud' 140 | draw.cloud.activate() 141 | ui.info.content = 'Click and Drag to draw a ' + currentTool 142 | } 143 | }), 144 | 145 | text: new PointText({ 146 | point: [60, 450], 147 | fillColor: 'red', 148 | fontSize: 70, 149 | fontFamily: 'Arial Bold', 150 | content: 'T', 151 | onClick: function(event) { 152 | currentTool = 'text' 153 | draw.text.activate() 154 | ui.info.content = 'Click to insert text and type' 155 | } 156 | }), 157 | 158 | transform: new Path({ 159 | segments:[[60, 490],[110, 470],[90, 520], [83, 497]], 160 | fillColor: 'red', 161 | onClick: function(event) { 162 | currentTool = 'transform' 163 | draw.transform.activate() 164 | ui.info.content = 'Mouse Drag = Move Up/Down = Scale Left/Right = Rotate Space = Delete' 165 | } 166 | }), 167 | 168 | color: new Path.Circle({ 169 | center: [80, 570], 170 | radius: 30, 171 | fillColor: currentColor, 172 | strokeColor: 'whitesmoke', 173 | strokeWidth: 1.5, 174 | onClick: function(event) { 175 | previousTool = currentTool 176 | previousUiInfo = ui.info.content 177 | currentTool = 'color' 178 | showColorUi() 179 | draw.color.activate() 180 | project.layers.colorWheelLayer.visible = true 181 | ui.info.content = 'Click to pick a color. Space Bar to Exit' 182 | } 183 | }), 184 | 185 | random: new Path.Star({ 186 | center: [80, 650], 187 | points: 10, 188 | radius1: 25, 189 | radius2: 35, 190 | fillColor: 'red', 191 | onClick: function(event) { 192 | currentTool = 'random' 193 | generateRandomDrawing() 194 | draw.transform.activate() 195 | ui.info.content = 'Randomly Generated Masterpiece' 196 | } 197 | }), 198 | 199 | download: new CompoundPath({ 200 | children: [ 201 | new Path.Line({ 202 | segments: [[65,740], [65,747], [95,747], [95,740]], 203 | }), 204 | new Path.Line({ 205 | segments: [[70,730], [80,745], [90,730]], 206 | }), 207 | new Path.Line({ 208 | segments: [[80,710], [80,745]], 209 | }), 210 | new Path.Circle({ 211 | center: new Point(80, 730), 212 | radius: 30, 213 | fillColor: 'white', 214 | }), 215 | ], 216 | strokeColor: 'red', 217 | strokeWidth: 5, 218 | onClick: function(event) { 219 | artBoard.visible = false 220 | var svgData = paper.project.exportSVG({ asString: true, bounds: artBoard }) 221 | var blob = new Blob([svgData], {type: "image/svg+xml"}) 222 | saveAs(blob, 'Mies' +'.svg') 223 | artBoard.visible = true 224 | } 225 | }), 226 | 227 | tempColorDisplay: new Path.Line({ 228 | from: artBoard.bounds.bottomLeft, 229 | to: artBoard.bounds.bottomRight, 230 | strokeWidth: 20, 231 | strokeColor: currentColor, 232 | onClick: function(event) { 233 | previousTool = currentTool 234 | previousUiInfo = ui.info.content 235 | currentTool = 'color' 236 | showColorUi() 237 | draw.color.activate() 238 | project.layers.colorWheelLayer.visible = true 239 | ui.info.content = 'Click to pick a color' 240 | } 241 | }), 242 | 243 | keyboard: new PointText({ 244 | position: artBoard.bounds.bottomLeft + [0, 45], 245 | fillColor: 'black', 246 | fontSize: 16, 247 | content: 'keyboard shortcuts: l:line b:brush c:circle r:rectangle a:arc d:cloud t:text m:transform x:color', 248 | onClick: function(event) { 249 | window.open('https://github.com/tamg/mies','_blank') 250 | } 251 | }), 252 | 253 | created: new PointText({ 254 | position: artBoard.bounds.bottomLeft + [0, 70], 255 | fillColor: 'black', 256 | fontSize: 16, 257 | content: 'Github Source [Created by @tamrrat at The Recurse Center]', 258 | onClick: function(event) { 259 | window.open('https://github.com/tamg/mies','_blank') 260 | } 261 | }) 262 | 263 | }//window.ui 264 | 265 | //add style to cloud and tempColorDisplay ui 266 | ui.cloud.simplify() 267 | ui.tempColorDisplay.position.y += 10 268 | 269 | // modified from Paperjs.org examples 270 | function showColorUi() { 271 | 272 | colorWheelLayer.activate() 273 | var steps = { 274 | hue: 36, 275 | saturation: 5, 276 | lightness: 3 277 | } 278 | 279 | var colorGroup = new Group() 280 | 281 | //lightness 282 | for (var l = 0; l < steps.lightness; l++) { 283 | var radius = artBoard.size.width / steps.lightness * 0.40 284 | var offset = new Point(artBoard.size.width / steps.lightness, 0) 285 | var center = artBoard.bounds.leftCenter + offset * (l + 0.5) 286 | var lightness = 1 - (l + 1) / (steps.lightness + 1) 287 | 288 | //hue 289 | var hUnit = 360 / steps.hue 290 | for (var h = 0; h < steps.hue; h++) { 291 | var hue = h * hUnit; 292 | var vector = new Point({ 293 | angle: hue - 90, 294 | length: radius 295 | }) 296 | 297 | //saturation 298 | for (var i = 0; i < steps.saturation; i++) { 299 | var saturation = i / steps.saturation 300 | var color = { hue: hue, saturation: saturation, lightness: lightness } 301 | } 302 | 303 | colorPath = new Path(new Point(), vector.rotate(hUnit / 2)) 304 | colorPath.closed = true 305 | colorPath.arcTo(vector, vector.rotate(hUnit / -2)) 306 | colorPath.position += center 307 | 308 | colorPath.onClick = function(event) { 309 | currentTool = previousTool 310 | ui.info.content = previousUiInfo 311 | draw[currentTool].activate() 312 | project.layers.colorWheelLayer.visible = false 313 | } 314 | 315 | colorPath.fillColor = colorPath.strokeColor = color 316 | colorPath.name = 'colorPath' + colorPath.id 317 | project.layers.colorWheelLayer.addChild(colorPath) 318 | colorGroup.addChild(colorPath) 319 | } 320 | 321 | //activate the base layer back after creating colorWheelLayer 322 | baseLayer.activate() 323 | } 324 | }//showColorUi() 325 | 326 | // Group all the UI stuff together 327 | var uiGroup = new Group({ 328 | children: [ui.line, ui.brush, ui.circle, ui.rect, ui.arc, ui.cloud, 329 | ui.text, ui.transform, ui.color, ui.random, ui.download] 330 | }) 331 | 332 | //position UI relative to the artboard 333 | //TODO make responsive to resize 334 | uiGroup.position.y = artBoard.position.y 335 | uiGroup.position.x = artBoard.position.x - 500 336 | uiGroup.scale(.82) 337 | 338 | // check/clip if a drawn object is outside the bounds of an artBoard 339 | function checkIfBoardContains(object, index) { 340 | if (!artBoard.bounds.contains(object.bounds)) { 341 | var clipper = new Path.Rectangle(artBoard.bounds) 342 | var clippedGroup = new Group(clipper, object) 343 | clippedGroup.clipped = true 344 | baseLayer.insertChild(index, clippedGroup) 345 | } 346 | } 347 | 348 | //all the drawing and editing tools 349 | window.draw = { 350 | line: new Tool({ 351 | onMouseDown: function(event) { 352 | path = new Path() 353 | path.strokeColor = currentColor 354 | path.add(event.point) 355 | path.strokeWidth = currentStrokeWidth 356 | }, 357 | onMouseDrag: function(event) { 358 | if(artBoard.bounds.contains(event.point)) { 359 | path.add(event.point) 360 | } 361 | } 362 | }), 363 | 364 | //modifed from paperJs examples 365 | brush: new Tool({ 366 | minDistance: 10, 367 | maxDistance: 45, 368 | 369 | onMouseDown: function(event) { 370 | path = new Path() 371 | path.fillColor = currentColor 372 | path.add(event.point) 373 | }, 374 | 375 | onMouseDrag: function(event) { 376 | if(artBoard.bounds.contains(event.point)) { 377 | var step = event.delta / 2 378 | step.angle += 90 379 | 380 | var top = event.middlePoint + step 381 | var bottom = event.middlePoint - step 382 | 383 | path.add(top) 384 | path.insert(0, bottom) 385 | path.smooth() 386 | 387 | checkIfBoardContains(path) 388 | } 389 | }, 390 | 391 | onMouseUp: function(event) { 392 | path.add(event.point) 393 | path.closed = true 394 | path.smooth() 395 | } 396 | }), 397 | 398 | circle: new Tool({ 399 | onMouseDrag: function(event) { 400 | if(artBoard.bounds.contains(event.point)) { 401 | var radius = (event.downPoint - event.point).length 402 | path = new Path.Circle({ 403 | center: event.downPoint, 404 | radius: radius, 405 | name: 'circle' + this.id, 406 | fillColor: currentColor, 407 | strokeColor: 'black', 408 | }) 409 | path.removeOnDrag() 410 | checkIfBoardContains(path) 411 | } 412 | } 413 | }), 414 | 415 | rectangle: new Tool({ 416 | onMouseDrag: function(event) { 417 | if(artBoard.bounds.contains(event.point)) { 418 | var from = event.downPoint 419 | var to = event.point 420 | path = new Path.Rectangle({ 421 | from: from, 422 | to: to, 423 | }) 424 | path.fillColor = currentColor 425 | path.strokeColor = 'black' 426 | path.name = 'rect' + path.id 427 | path.removeOnDrag() 428 | 429 | checkIfBoardContains(path, path.index) 430 | } 431 | } 432 | }), 433 | 434 | arc: new Tool({ 435 | onMouseDrag: function(event) { 436 | if(artBoard.bounds.contains(event.point)) { 437 | path = new Path() 438 | path.strokeColor = currentColor 439 | path.strokeWidth = currentStrokeWidth, 440 | path.add(event.downPoint) 441 | path.arcTo(event.middlePoint, event.point) 442 | path.selected = true 443 | path.removeOnDrag() 444 | 445 | checkIfBoardContains(path) 446 | } 447 | }, 448 | onMouseUp: function(event) { 449 | path.selected = false 450 | } 451 | }), 452 | 453 | cloud: new Tool({ 454 | minDistance: 20, 455 | onMouseDown: function(event) { 456 | path = new Path() 457 | path.strokeColor = currentColor 458 | path.strokeWidth = currentStrokeWidth, 459 | path.add(event.point) 460 | }, 461 | onMouseDrag: function(event) { 462 | if(artBoard.bounds.contains(event.point)) { 463 | path.arcTo(event.point, true) 464 | 465 | checkIfBoardContains(path) 466 | } 467 | } 468 | }), 469 | 470 | transform: new Tool({ 471 | onMouseDown: function(event) { 472 | var hitOptions = { 473 | segments: false, 474 | stroke: true, 475 | fill: true, 476 | tolerance: 5 477 | } 478 | 479 | if (artBoard.bounds.contains(event.point)) { 480 | var hitResult = project.hitTest(event.point, hitOptions) 481 | } 482 | 483 | if (hitResult && hitResult.item !== artBoard) { 484 | path = hitResult.item 485 | index = path.index 486 | } 487 | 488 | }, 489 | onMouseMove: function(event) { 490 | project.activeLayer.selected = false 491 | if (event.item && 492 | event.item !== artBoard && 493 | event.item.layer.name !== 'colorWheelLayer' && 494 | artBoard.bounds.contains(event.point) ) { 495 | 496 | if(event.item.children) { 497 | event.item.children[1].selected = true 498 | } else { 499 | event.item.selected = true 500 | } 501 | 502 | } 503 | }, 504 | onMouseDrag: function(event) { 505 | if (event.item && 506 | event.item !== artBoard && 507 | event.item.layer.name !== 'colorWheelLayer' && 508 | artBoard.bounds.contains(event.point) ) { 509 | 510 | path.position += event.delta 511 | 512 | checkIfBoardContains(path, index) 513 | } 514 | }, 515 | onKeyDown: function(event) { 516 | 517 | } 518 | }),// Move tool 519 | 520 | text: new Tool({ 521 | onMouseDown: function(event) { 522 | if(artBoard.bounds.contains(event.point)) { 523 | var textPoint = event.downPoint 524 | newText = new PointText({ 525 | point: textPoint, 526 | fillColor: currentColor, 527 | fontSize: 60, 528 | fontFamily: 'Arial Bold', 529 | content: '' 530 | }) 531 | 532 | if(cursor) { 533 | cursor.remove() 534 | } 535 | 536 | cursor = new Path.Line({ 537 | from: newText.bounds.bottomRight, 538 | to: newText.bounds.topRight, 539 | strokeWidth: 1, 540 | strokeColor: 'red', 541 | }) 542 | //fix 543 | cursorBlink = setInterval(function(){ 544 | if(currentTool === 'text') { 545 | cursor.visible = cursor.visible ? false : true 546 | } 547 | }, 500) 548 | } 549 | },//mousedown 550 | onKeyDown: function(event) { 551 | if (event.key === 'backspace') { 552 | if(newText.content.length > 0) { 553 | var tempTxt = newText.content 554 | newText.content = tempTxt.substring(0, tempTxt.length - 1) 555 | cursor.position.x = newText.bounds.bottomRight.x + 5 556 | } 557 | } else if (event.key === 'space') { 558 | newText.content += ' ' 559 | cursor.position.x = newText.bounds.bottomRight.x + 5 560 | checkIfBoardContains(newText) 561 | } else if ( 'abcdefghijklmonpqrstuvwxyz0123456789-[]:?/,~!@#$%^&*()_+-'.indexOf(event.key) > -1) { 562 | newText.content += event.key 563 | cursor.position.x = newText.bounds.bottomRight.x + 5 564 | checkIfBoardContains(newText) 565 | } else if ( event.key === 'enter') { 566 | clearInterval(cursorBlink) 567 | cursor.remove() 568 | } 569 | }//mousedown 570 | 571 | }), 572 | 573 | color: new Tool({ 574 | onMouseDown: function(event) { 575 | if (artBoard.bounds.contains(event.point)) { 576 | var hitResult = project.hitTestAll(event.point) 577 | 578 | for(i=0; i