[READ-ONLY] Mirror of https://github.com/flo-bit/mandala.
flo-bit.github.io/mandala/
1paper.install(window);
2
3var isTouchGesture = false;
4var mandalaDrawer;
5
6window.onload = function () {
7 paper.setup("myCanvas");
8
9 mandalaDrawer = new MandalaDrawer();
10
11 initPinchZoom();
12 setupEventListeners();
13};
14
15function setupEventListeners() {
16 let canvas = document.getElementById("myCanvas");
17
18 canvas.addEventListener(
19 "wheel",
20 function (e) {
21 e.preventDefault();
22 if (e.ctrlKey) {
23 view.zoom -= e.deltaY * 0.005;
24 } else {
25 view.translate(-e.deltaX, -e.deltaY);
26 }
27 },
28 { passive: false }
29 );
30
31 document.addEventListener("keydown", function (event) {
32 if (event.key == "s" && (event.metaKey || event.ctrlKey)) {
33 event.preventDefault();
34
35 mandalaDrawer.saveAsSVG();
36 }
37
38 if (event.key == "f" && (event.metaKey || event.ctrlKey)) {
39 openSettingsModal();
40 }
41
42 if (event.key == "b" && (event.metaKey || event.ctrlKey)) {
43 openBrushModal();
44 }
45
46 if (event.key == "+" && (event.metaKey || event.ctrlKey)) {
47 event.preventDefault();
48 view.zoom *= 1.1;
49 }
50 if (event.key == "-" && (event.metaKey || event.ctrlKey)) {
51 event.preventDefault();
52 view.zoom /= 1.1;
53 }
54
55 // move around
56 if (event.key == "ArrowUp") {
57 view.translate(0, 10);
58 }
59 if (event.key == "ArrowDown") {
60 view.translate(0, -10);
61 }
62 if (event.key == "ArrowLeft") {
63 view.translate(10, 0);
64 }
65 if (event.key == "ArrowRight") {
66 view.translate(-10, 0);
67 }
68
69 if (event.key == "z" && (event.metaKey || event.ctrlKey)) {
70 mandalaDrawer.undo();
71 }
72 });
73
74 let mirrorButton = document.getElementById("mirror-button");
75 mirrorButton.addEventListener("click", function (e) {
76 mandalaDrawer.mirror = !mandalaDrawer.mirror;
77 if (mandalaDrawer.mirror) {
78 mirrorButton.classList.remove("is-light");
79 } else {
80 mirrorButton.classList.add("is-light");
81 }
82 });
83
84 let simplifyButton = document.getElementById("simplify-button");
85 simplifyButton.addEventListener("click", function (e) {
86 mandalaDrawer.simplify = !mandalaDrawer.simplify;
87 if (mandalaDrawer.simplify) {
88 simplifyButton.classList.remove("is-light");
89 } else {
90 simplifyButton.classList.add("is-light");
91 }
92 });
93
94 let brushColorInput = document.getElementById("brush-color-input");
95 brushColorInput.addEventListener("change", function (e) {
96 mandalaDrawer.brushColor = brushColorInput.value;
97 });
98
99 setupBrushSizeSlider();
100 setupRotationsSlider();
101
102 let backgroundColorInput = document.getElementById("background-color-input");
103 backgroundColorInput.addEventListener("change", function (e) {
104 mandalaDrawer.backgroundColor = backgroundColorInput.value;
105 closeBrushModal();
106 });
107
108 // file modal stuff
109 let fileInput = document.getElementById("svg-file-input");
110 fileInput.onchange = () => {
111 if (fileInput.files.length > 0) {
112 console.log(fileInput.files[0].name);
113 project.clear();
114 project.importSVG(fileInput.files[0], {
115 applyMatrix: false,
116 expandShapes: true,
117 insert: false,
118 onLoad: (item) => {
119 if (
120 item.children.length == 2 &&
121 item.children[1].children.length > 0
122 ) {
123 let groups = item.children[1].children;
124 for (let g of groups) {
125 project.activeLayer.addChild(g);
126 }
127 }
128 },
129 onError: (error) => {
130 console.log(error);
131 },
132 });
133
134 closeFileModal();
135 }
136 };
137
138 let exportPNGButton = document.getElementById("png-export-button");
139 exportPNGButton.addEventListener("click", () => {
140 mandalaDrawer.saveAsPNG();
141 closeFileModal();
142 });
143
144 let exportSVGButton = document.getElementById("svg-export-button");
145 exportSVGButton.addEventListener("click", () => {
146 mandalaDrawer.saveAsSVG();
147 closeFileModal();
148 });
149
150 let clearButton = document.getElementById("clear-button");
151 clearButton.addEventListener("click", () => {
152 project.clear();
153 closeFileModal();
154 mandalaDrawer.unsavedChanges = false;
155 });
156}
157
158function initPinchZoom() {
159 const canvasElement = paper.view.element;
160 const box = canvasElement.getBoundingClientRect();
161 const offset = new paper.Point(box.left, box.top);
162
163 const hammer = new Hammer(canvasElement, {});
164 hammer.get("pinch").set({ enable: true });
165 hammer.get("tap").set({ enable: true, pointers: 2 });
166
167 let startMatrix, startMatrixInverted, p0ProjectCoords;
168
169 hammer.on("pinchstart", (e) => {
170 isTouchGesture = true;
171
172 startMatrix = paper.view.matrix.clone();
173 startMatrixInverted = startMatrix.inverted();
174 const p0 = getCenterPoint(e);
175 p0ProjectCoords = paper.view.viewToProject(p0);
176 });
177
178 hammer.on("pinch", (e) => {
179 // Translate and scale view using pinch event's 'center' and 'scale' properties.
180 // Translation computes center's distance from initial center (considering current scale).
181 const p = getCenterPoint(e);
182 const pProject0 = p.transform(startMatrixInverted);
183 const delta = pProject0.subtract(p0ProjectCoords).divide(e.scale);
184 paper.view.matrix = startMatrix
185 .clone()
186 .scale(e.scale, p0ProjectCoords)
187 .translate(delta);
188 });
189
190 hammer.on("pinchend", (e) => {
191 isTouchGesture = false;
192 });
193
194 hammer.on("tap", (e) => {
195 mandalaDrawer.undo();
196 });
197
198 function getCenterPoint(e) {
199 return new paper.Point(e.center.x, e.center.y).subtract(offset);
200 }
201}
202
203function setupBrushSizeSlider() {
204 let brushSizeInput = document.getElementById("brush-size-input");
205 let brushSizeBar = document.getElementById("brush-size-bar");
206
207 brushSizeBar.addEventListener("click", function (e) {
208 let brushSize = e.offsetX / brushSizeBar.offsetWidth;
209 mandalaDrawer.percentageBrushSize = brushSize;
210
211 brushSizeBar.value = mandalaDrawer.percentageBrushSize * 100;
212 brushSizeInput.value = mandalaDrawer.brushSize;
213 });
214
215 let mouseDown = false;
216 brushSizeBar.addEventListener("pointerdown", function (e) {
217 mouseDown = true;
218 });
219 document.addEventListener("pointerup", function (e) {
220 mouseDown = false;
221 });
222 brushSizeBar.addEventListener("pointermove", function (e) {
223 if (mouseDown == false) return;
224 let brushSize = e.offsetX / brushSizeBar.offsetWidth;
225 mandalaDrawer.percentageBrushSize = brushSize;
226
227 brushSizeBar.value = mandalaDrawer.percentageBrushSize * 100;
228 brushSizeInput.value = mandalaDrawer.brushSize;
229 });
230
231 brushSizeInput.addEventListener("change", function (e) {
232 let brushSize = brushSizeInput.value;
233 mandalaDrawer.brushSize = brushSize;
234
235 brushSizeBar.value = mandalaDrawer.percentageBrushSize * 100;
236 brushSizeInput.value = mandalaDrawer.brushSize;
237 });
238}
239
240function setupRotationsSlider() {
241 let rotationsInput = document.getElementById("rotations-input");
242 let rotationsBar = document.getElementById("rotations-bar");
243
244 rotationsBar.addEventListener("click", function (e) {
245 let rotations = e.offsetX / rotationsBar.offsetWidth;
246 mandalaDrawer.percentageRotations = rotations;
247
248 rotationsBar.value = mandalaDrawer.percentageRotations * 100;
249 rotationsInput.value = mandalaDrawer.rotations;
250 });
251
252 let mouseDown = false;
253 rotationsBar.addEventListener("pointerdown", function (e) {
254 mouseDown = true;
255 });
256 document.addEventListener("pointerup", function (e) {
257 mouseDown = false;
258 });
259 rotationsBar.addEventListener("pointermove", function (e) {
260 if (mouseDown == false) return;
261 let rotations = e.offsetX / rotationsBar.offsetWidth;
262 mandalaDrawer.percentageRotations = rotations;
263
264 rotationsBar.value = mandalaDrawer.percentageRotations * 100;
265 rotationsInput.value = mandalaDrawer.rotations;
266 });
267 rotationsInput.addEventListener("change", function (e) {
268 let rotations = rotationsInput.value;
269 mandalaDrawer.rotations = rotations;
270
271 rotationsBar.value = mandalaDrawer.percentageRotations * 100;
272 rotationsInput.value = mandalaDrawer.rotations;
273 });
274}
275
276function downloadURI(uri, name) {
277 var link = document.createElement("a");
278 link.download = name;
279 link.href = uri;
280 document.body.appendChild(link);
281 link.click();
282 document.body.removeChild(link);
283 delete link;
284}
285
286class MandalaDrawer {
287 constructor() {
288 this.tool = new Tool();
289
290 this.path = undefined;
291 this.paths = undefined;
292
293 this.minRotations = 0;
294 this.maxRotations = 32;
295
296 this.maxBrushSize = 15;
297 this.minBrushSize = 0.2;
298
299 this.unsavedChanges = false;
300
301 this.loadSettings();
302 this.setupTool();
303
304 view.translate(view.center);
305 }
306
307 saveSettings() {
308 let settings = {
309 backgroundColor: this.backgroundColor,
310 brushColor: this.brushColor,
311 brushSize: this.brushSize,
312 mirror: this.mirror,
313 rotations: this.rotations,
314 simplify: this.simplify,
315 };
316
317 localStorage.setItem("settings", JSON.stringify(settings));
318 }
319
320 loadSettings() {
321 let settings = JSON.parse(localStorage.getItem("settings"));
322
323 this._simplify = settings?.simplify ?? false;
324 this._mirror = settings?.mirror ?? true;
325
326 this._brushColor = settings?.brushColor ?? "#000000";
327
328 this._rotations = settings?.rotations ?? 16;
329 this._brushSize = settings?.brushSize ?? 1;
330
331 this._backgroundColor = settings?.backgroundColor ?? "#FFFFFF";
332 view.element.style.backgroundColor = this._backgroundColor;
333
334 this.updateUI();
335 }
336
337 updateUI() {
338 let brushSizeSlider = document.getElementById("brush-size-bar");
339 brushSizeSlider.value = this.percentageBrushSize * 100;
340
341 let brushSizeLabel = document.getElementById("brush-size-input");
342 brushSizeLabel.value = this.brushSize;
343
344 let rotationsSlider = document.getElementById("rotations-bar");
345 rotationsSlider.value = this.percentageRotations * 100;
346
347 let rotationsLabel = document.getElementById("rotations-input");
348 rotationsLabel.value = this.rotations;
349
350 let brushColorInput = document.getElementById("brush-color-input");
351 brushColorInput.value = this.brushColor;
352
353 let backgroundColorInput = document.getElementById(
354 "background-color-input"
355 );
356 backgroundColorInput.value = this.backgroundColor;
357
358 let mirrorButton = document.getElementById("mirror-button");
359 if (this.mirror) {
360 mirrorButton.classList.remove("is-light");
361 } else {
362 mirrorButton.classList.add("is-light");
363 }
364
365 let simplifyButton = document.getElementById("simplify-button");
366 if (this.simplify) {
367 simplifyButton.classList.remove("is-light");
368 } else {
369 simplifyButton.classList.add("is-light");
370 }
371 }
372
373 saveAsPNG() {
374 view.element.toBlob((blob) => {
375 let dataURL = URL.createObjectURL(blob);
376 downloadURI(dataURL, "mandala.png");
377 });
378 }
379
380 saveAsSVG() {
381 var fileName = "mandala.svg";
382 var url =
383 "data:image/svg+xml;utf8," +
384 encodeURIComponent(
385 project.exportSVG({
386 asString: true,
387 bounds: "content",
388 })
389 );
390 downloadURI(url, fileName);
391
392 this.unsavedChanges = false;
393 }
394
395 saveAsJSON() {
396 let json = project.exportJSON();
397 let dataURL = "data:text/json;charset=utf-8," + encodeURIComponent(json);
398 downloadURI(dataURL, "mandala.json");
399 }
400
401 set rotations(rotations) {
402 this._rotations = rotations;
403 this.saveSettings();
404 }
405 get rotations() {
406 return this._rotations;
407 }
408
409 set percentageRotations(percentage) {
410 if (isNaN(percentage)) percentage = 0;
411 percentage = Math.max(0, percentage);
412 percentage = Math.min(1, percentage);
413
414 let rotations =
415 this.minRotations + (this.maxRotations - this.minRotations) * percentage;
416
417 this.rotations = Math.round(rotations);
418 }
419 get percentageRotations() {
420 let percentage =
421 (this.rotations - this.minRotations) /
422 (this.maxRotations - this.minRotations);
423 percentage = Math.max(0, percentage);
424 percentage = Math.min(1, percentage);
425
426 return percentage;
427 }
428
429 set mirror(mirror) {
430 this._mirror = mirror;
431 this.saveSettings();
432 }
433 get mirror() {
434 return this._mirror;
435 }
436
437 set simplify(simplify) {
438 this._simplify = simplify;
439 this.saveSettings();
440 }
441 get simplify() {
442 return this._simplify;
443 }
444
445 set brushColor(color) {
446 this._brushColor = color;
447 this.saveSettings();
448 }
449 get brushColor() {
450 return this._brushColor;
451 }
452
453 set brushSize(size) {
454 this._brushSize = size;
455 this.saveSettings();
456 }
457 get brushSize() {
458 return this._brushSize;
459 }
460
461 set percentageBrushSize(percentage) {
462 if (isNaN(percentage)) percentage = 0;
463
464 percentage = Math.max(0, percentage);
465 percentage = Math.min(1, percentage);
466
467 this.brushSizePercentage = percentage;
468
469 let brushSize =
470 this.minBrushSize + (this.maxBrushSize - this.minBrushSize) * percentage;
471
472 this.brushSize = Math.round(brushSize * 10) / 10;
473 }
474 get percentageBrushSize() {
475 let percentage =
476 (this.brushSize - this.minBrushSize) /
477 (this.maxBrushSize - this.minBrushSize);
478 percentage = Math.max(0, percentage);
479 percentage = Math.min(1, percentage);
480
481 return percentage;
482 }
483
484 set backgroundColor(color) {
485 this._backgroundColor = color;
486 view.element.style.backgroundColor = color;
487 this.saveSettings();
488 }
489 get backgroundColor() {
490 return this._backgroundColor;
491 }
492
493 setupTool() {
494 this.tool.onMouseDown = (event) => {
495 if (event.modifiers.shift || isTouchGesture) {
496 return;
497 }
498
499 this.paths = [this.makePath()];
500
501 if (this.mirror) {
502 let mirrorPath = this.makePath();
503 this.paths.push(mirrorPath);
504 }
505
506 for (let i = 1; i <= this.rotations; i++) {
507 let p = this.makePath();
508 this.paths.push(p);
509
510 if (this.mirror) {
511 let mp = this.makePath();
512 this.paths.push(mp);
513 }
514 }
515
516 let group = new Group(this.paths);
517
518 this.addPoint(event.point);
519
520 this.lastPathOnePoint = true;
521 };
522
523 this.tool.onMouseDrag = (event) => {
524 if (event.modifiers.shift || isTouchGesture) {
525 view.translate(event.delta);
526 return;
527 }
528
529 if (this.paths === undefined) {
530 return;
531 }
532
533 this.addPoint(event.point);
534
535 this.lastPathOnePoint = false;
536 };
537
538 this.tool.onMouseUp = (event) => {
539 if (event.modifiers.shift || this.paths === undefined || isTouchGesture) {
540 return;
541 }
542
543 if (this.simplify) {
544 for (let p of this.paths) {
545 p.simplify(10);
546 }
547 }
548
549 this.paths = undefined;
550
551 // remove path if it only has one point
552 if (this.lastPathOnePoint) {
553 this.undo();
554 return;
555 }
556
557 this.unsavedChanges = true;
558 };
559 }
560
561 makePath() {
562 let p = new Path();
563 p.strokeColor = this.brushColor;
564 p.strokeWidth = this.brushSize;
565 return p;
566 }
567
568 mirrorPoint(point) {
569 return new Point(point.x, -point.y);
570 }
571
572 addPoint(point) {
573 if (this.paths === undefined || this.paths.length < 1) return;
574 this.paths[0].add(point);
575
576 let index = 1;
577 if (this.mirror) {
578 this.paths[index].add(this.mirrorPoint(point));
579 index += 1;
580 }
581
582 for (let i = 1; i <= this.rotations; i++) {
583 let p = point.rotate((360 / this.rotations) * i);
584 this.paths[index].add(p);
585 index += 1;
586
587 if (this.mirror) {
588 this.paths[index].add(this.mirrorPoint(p));
589 index += 1;
590 }
591 }
592 }
593
594 undo() {
595 let last = paper.project.activeLayer.lastChild;
596 if (last == undefined) return;
597 last.remove();
598 }
599}
600
601window.onbeforeunload = function (e) {
602 e = e || window.event;
603
604 if (!mandalaDrawer.unsavedChanges) return;
605
606 // For IE and Firefox prior to version 4
607 if (e) {
608 e.returnValue = "Sure?";
609 }
610
611 // For Safari
612 return "Sure?";
613};
614
615function openModal($el) {
616 $el.classList.add("is-active");
617}
618
619function closeModal($el) {
620 $el.classList.remove("is-active");
621}
622
623function openBrushModal() {
624 openModal(document.getElementById("brush-modal"));
625}
626
627function closeBrushModal() {
628 closeModal(document.getElementById("brush-modal"));
629}
630
631function closeFileModal() {
632 closeModal(document.getElementById("file-modal"));
633}
634
635function openFileModal() {
636 openModal(document.getElementById("file-modal"));
637}
638
639document.addEventListener("DOMContentLoaded", () => {
640 function closeAllModals() {
641 (document.querySelectorAll(".modal") || []).forEach(($modal) => {
642 closeModal($modal);
643 });
644 }
645
646 // Add a click event on buttons to open a specific modal
647 (document.querySelectorAll(".js-modal-trigger") || []).forEach(($trigger) => {
648 const modal = $trigger.dataset.target;
649 const $target = document.getElementById(modal);
650
651 $trigger.addEventListener("click", () => {
652 openModal($target);
653 });
654 });
655
656 // Add a click event on various child elements to close the parent modal
657 (
658 document.querySelectorAll(
659 ".modal-background, .modal-close, .modal-card-head .delete, .modal-card-foot .button"
660 ) || []
661 ).forEach(($close) => {
662 const $target = $close.closest(".modal");
663
664 $close.addEventListener("click", () => {
665 closeModal($target);
666 });
667 });
668
669 // Add a keyboard event to close all modals
670 document.addEventListener("keydown", (event) => {
671 const e = event || window.event;
672
673 if (e.keyCode === 27) {
674 // Escape key
675 closeAllModals();
676 }
677 });
678});