├── .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 |
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