[READ-ONLY] Mirror of https://github.com/flo-bit/mandala. flo-bit.github.io/mandala/
0

Configure Feed

Select the types of activity you want to include in your feed.

mandala / script.js
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});