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