[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.

first commit

Florian (Mar 26, 2023, 4:45 PM +0200) 728897ad

+691
+2
.gitignore
··· 1 + /.DS_Store 2 + /.prettierrc
+21
Readme.md
··· 1 + # Mandala 2 + 3 + simple browser based vector mandala drawing tool, using [paper.js](http://paperjs.org/) and [bulma](https://bulma.io). 4 + 5 + ### TODO 6 + 7 + - [ ] connect rotations slider to the actual rotation 8 + - [x] save and load settings 9 + 10 + - [ ] add zoom and pan on mobile 11 + - [ ] add stuff for pwa 12 + 13 + - [ ] add icon/favicon 14 + 15 + - [ ] create file modal 16 + - [ ] set background color 17 + - [ ] clear current drawing 18 + 19 + - [ ] save file 20 + - [ ] load file 21 + - [ ] export file as svg/png
+170
index.html
··· 1 + <!DOCTYPE html> 2 + <html> 3 + <head> 4 + <meta charset="utf-8" /> 5 + 6 + <meta name="apple-mobile-web-app-capable" content="yes" /> 7 + 8 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 9 + 10 + <link 11 + rel="stylesheet" 12 + href="https://cdn.jsdelivr.net/npm/@mdi/font@7.2.96/css/materialdesignicons.min.css" 13 + /> 14 + 15 + <link 16 + rel="stylesheet" 17 + href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css" 18 + /> 19 + <script 20 + type="text/javascript" 21 + src="https://cdnjs.cloudflare.com/ajax/libs/paper.js/0.12.17/paper-core.js" 22 + ></script> 23 + <script type="text/javascript" src="./script.js"></script> 24 + 25 + <link rel="stylesheet" type="text/css" href="./style.css" /> 26 + 27 + <meta name="theme-color" content="#AA0000" /> 28 + <title>vector mandalas</title> 29 + </head> 30 + <body> 31 + <canvas id="myCanvas" resize></canvas> 32 + <div id="controls"> 33 + <button 34 + class="button is-danger is-large top-right-button" 35 + id="file-button" 36 + > 37 + <span class="icon is-large"> 38 + <i class="mdi mdi-file mdi-36px"></i> 39 + </span> 40 + </button> 41 + <button 42 + class="button is-danger is-large bottom-right-button" 43 + id="brush-button" 44 + > 45 + <span class="icon is-large"> 46 + <i class="mdi mdi-brush mdi-36px"></i> 47 + </span> 48 + </button> 49 + <button 50 + class="button is-danger is-large bottom-left-button" 51 + id="undo-button" 52 + > 53 + <span class="icon is-large"> 54 + <i class="mdi mdi-undo mdi-36px"></i> 55 + </span> 56 + </button> 57 + </div> 58 + 59 + <div class="modal" id="brush-modal"> 60 + <div class="modal-background"></div> 61 + 62 + <div class="modal-card"> 63 + <header class="modal-card-head"> 64 + <p class="modal-card-title">Brush settings</p> 65 + <button class="delete" aria-label="close"></button> 66 + </header> 67 + <div class="modal-card-body"> 68 + <div class="block"> 69 + <div class="field"> 70 + <div class="control columns is-mobile is-vcentered"> 71 + <div class="column is-narrow"> 72 + <label class="label">Size</label> 73 + </div> 74 + <div class="column"> 75 + <progress 76 + class="progress is-large is-danger" 77 + id="brush-size-bar" 78 + value="80" 79 + max="100" 80 + > 81 + 80% 82 + </progress> 83 + </div> 84 + <div class="column is-3"> 85 + <div class="control"> 86 + <input 87 + class="input" 88 + type="text" 89 + placeholder="0" 90 + value="80" 91 + id="brush-size-input" 92 + /> 93 + </div> 94 + </div> 95 + </div> 96 + </div> 97 + 98 + <div class="field"> 99 + <div class="control columns is-mobile is-vcentered"> 100 + <div class="column is-narrow"> 101 + <label class="label">Color</label> 102 + </div> 103 + <div class="column"> 104 + <div class="control"> 105 + <input 106 + class="input" 107 + type="color" 108 + value="#ff0000" 109 + id="brush-color-input" 110 + style="width: 100px" 111 + /> 112 + </div> 113 + </div> 114 + </div> 115 + </div> 116 + </div> 117 + 118 + <div class="block"> 119 + <div class="field"> 120 + <div class="columns is-mobile is-vcentered"> 121 + <div class="column is-narrow"> 122 + <label class="label">Rotations</label> 123 + </div> 124 + <div class="column"> 125 + <progress 126 + class="progress is-large is-danger" 127 + id="rotations-bar" 128 + value="80" 129 + max="100" 130 + > 131 + 80% 132 + </progress> 133 + </div> 134 + <div class="column is-3"> 135 + <div class="control"> 136 + <input 137 + class="input" 138 + type="text" 139 + placeholder="0" 140 + value="80" 141 + id="rotations-input" 142 + /> 143 + </div> 144 + </div> 145 + </div> 146 + </div> 147 + 148 + <div class="field"> 149 + <div class="buttons"> 150 + <button class="button is-danger" id="mirror-button"> 151 + <span class="icon"> 152 + <i class="mdi mdi-mirror"></i> 153 + </span> 154 + <span>Mirror</span> 155 + </button> 156 + 157 + <button class="button is-danger" id="simplify-button"> 158 + <span class="icon"> 159 + <i class="mdi mdi-chart-bell-curve-cumulative"></i> 160 + </span> 161 + <span>Simplify</span> 162 + </button> 163 + </div> 164 + </div> 165 + </div> 166 + </div> 167 + </div> 168 + </div> 169 + </body> 170 + </html>
+400
script.js
··· 1 + paper.install(window); 2 + 3 + window.onload = function () { 4 + paper.setup("myCanvas"); 5 + 6 + let mandalaDrawer = new MandalaDrawer(); 7 + mandalaDrawer.loadCurrent(); 8 + 9 + view.translate(view.center); 10 + 11 + document.addEventListener( 12 + "wheel", 13 + function (e) { 14 + e.preventDefault(); 15 + if (e.ctrlKey) { 16 + view.zoom -= e.deltaY * 0.005; 17 + } else { 18 + view.translate(-e.deltaX, -e.deltaY); 19 + } 20 + }, 21 + { passive: false } 22 + ); 23 + 24 + let undoButton = document.getElementById("undo-button"); 25 + undoButton.addEventListener("click", function (e) { 26 + mandalaDrawer.undo(); 27 + }); 28 + 29 + let brushButton = document.getElementById("brush-button"); 30 + brushButton.addEventListener("click", openBrushModal); 31 + 32 + let brushSizeInput = document.getElementById("brush-size-input"); 33 + let brushSizeBar = document.getElementById("brush-size-bar"); 34 + 35 + brushSizeBar.value = mandalaDrawer.brushSizePercentage * 100; 36 + brushSizeInput.value = mandalaDrawer.brushSize; 37 + 38 + brushSizeBar.addEventListener("click", function (e) { 39 + let brushSize = e.offsetX / brushSizeBar.offsetWidth; 40 + mandalaDrawer.setPercentageBrushSize(brushSize); 41 + 42 + brushSizeBar.value = mandalaDrawer.brushSizePercentage * 100; 43 + brushSizeInput.value = mandalaDrawer.brushSize; 44 + }); 45 + 46 + let mouseDown = false; 47 + brushSizeBar.addEventListener("pointerdown", function (e) { 48 + mouseDown = true; 49 + }); 50 + document.addEventListener("pointerup", function (e) { 51 + mouseDown = false; 52 + }); 53 + brushSizeBar.addEventListener("pointermove", function (e) { 54 + if (mouseDown == false) return; 55 + let brushSize = e.offsetX / brushSizeBar.offsetWidth; 56 + mandalaDrawer.setPercentageBrushSize(brushSize); 57 + 58 + brushSizeBar.value = mandalaDrawer.brushSizePercentage * 100; 59 + brushSizeInput.value = mandalaDrawer.brushSize; 60 + }); 61 + 62 + brushSizeInput.addEventListener("change", function (e) { 63 + let brushSize = brushSizeInput.value; 64 + mandalaDrawer.brushSize = brushSize; 65 + 66 + brushSizeBar.value = mandalaDrawer.brushSizePercentage * 100; 67 + brushSizeInput.value = mandalaDrawer.brushSize; 68 + }); 69 + 70 + let mirrorButton = document.getElementById("mirror-button"); 71 + if (mandalaDrawer.mirror) { 72 + mirrorButton.classList.remove("is-light"); 73 + } else { 74 + mirrorButton.classList.add("is-light"); 75 + } 76 + 77 + mirrorButton.addEventListener("click", function (e) { 78 + mandalaDrawer.mirror = !mandalaDrawer.mirror; 79 + if (mandalaDrawer.mirror) { 80 + mirrorButton.classList.remove("is-light"); 81 + } else { 82 + mirrorButton.classList.add("is-light"); 83 + } 84 + }); 85 + 86 + let simplifyButton = document.getElementById("simplify-button"); 87 + if (mandalaDrawer.simplify) { 88 + simplifyButton.classList.remove("is-light"); 89 + } else { 90 + simplifyButton.classList.add("is-light"); 91 + } 92 + 93 + simplifyButton.addEventListener("click", function (e) { 94 + mandalaDrawer.simplify = !mandalaDrawer.simplify; 95 + if (mandalaDrawer.simplify) { 96 + simplifyButton.classList.remove("is-light"); 97 + } else { 98 + simplifyButton.classList.add("is-light"); 99 + } 100 + }); 101 + 102 + let brushColorInput = document.getElementById("brush-color-input"); 103 + brushColorInput.value = mandalaDrawer.brushColor; 104 + 105 + brushColorInput.addEventListener("change", function (e) { 106 + mandalaDrawer.brushColor = brushColorInput.value; 107 + }); 108 + }; 109 + 110 + class MandalaDrawer { 111 + constructor() { 112 + this.tool = new Tool(); 113 + 114 + let json = localStorage.getItem("brush-settings"); 115 + let settings = json === null ? {} : JSON.parse(json); 116 + 117 + this.path = undefined; 118 + this.paths = undefined; 119 + 120 + this._simplify = settings.simplify ?? true; 121 + 122 + this._mirror = settings.mirror ?? true; 123 + this._rotations = settings.rotations ?? 8; 124 + 125 + this.maxBrushSize = 15; 126 + this.minBrushSize = 0.2; 127 + 128 + this._brushColor = settings.brushColor ?? "#FF0000"; 129 + 130 + this.brushSize = settings.brushSize ?? 1; 131 + 132 + this.setupTool(); 133 + } 134 + 135 + setPercentageBrushSize(percentage) { 136 + if (isNaN(percentage)) percentage = 0; 137 + 138 + percentage = Math.max(0, percentage); 139 + percentage = Math.min(1, percentage); 140 + 141 + this.brushSizePercentage = percentage; 142 + 143 + this.brushSize = 144 + this.minBrushSize + (this.maxBrushSize - this.minBrushSize) * percentage; 145 + 146 + this.brushSize = Math.round(this.brushSize * 10) / 10; 147 + } 148 + 149 + set mirror(mirror) { 150 + this._mirror = mirror; 151 + this.saveSettings(); 152 + } 153 + get mirror() { 154 + return this._mirror; 155 + } 156 + set rotations(rotations) { 157 + this._rotations = rotations; 158 + this.saveSettings(); 159 + } 160 + get rotations() { 161 + return this._rotations; 162 + } 163 + set simplify(simplify) { 164 + this._simplify = simplify; 165 + this.saveSettings(); 166 + } 167 + get simplify() { 168 + return this._simplify; 169 + } 170 + set brushColor(color) { 171 + this._brushColor = color; 172 + this.saveSettings(); 173 + } 174 + get brushColor() { 175 + return this._brushColor; 176 + } 177 + 178 + set brushSize(size) { 179 + this._brushSize = size; 180 + let percentage = (size - this.minBrushSize) / this.maxBrushSize; 181 + percentage = Math.max(0, percentage); 182 + percentage = Math.min(1, percentage); 183 + 184 + this.brushSizePercentage = percentage; 185 + 186 + this.saveSettings(); 187 + } 188 + get brushSize() { 189 + return this._brushSize; 190 + } 191 + 192 + setupTool() { 193 + this.tool.onMouseDown = (event) => { 194 + if (event.modifiers.shift) { 195 + return; 196 + } 197 + 198 + this.paths = [this.makePath()]; 199 + 200 + if (this.mirror) { 201 + let mirrorPath = this.makePath(); 202 + this.paths.push(mirrorPath); 203 + } 204 + 205 + for (let i = 1; i <= this.rotations; i++) { 206 + let p = this.makePath(); 207 + this.paths.push(p); 208 + 209 + if (this.mirror) { 210 + let mp = this.makePath(); 211 + this.paths.push(mp); 212 + } 213 + } 214 + 215 + let group = new Group(this.paths); 216 + 217 + this.addPoint(event.point); 218 + }; 219 + 220 + this.tool.onMouseDrag = (event) => { 221 + if (event.modifiers.shift) { 222 + view.translate(event.delta); 223 + return; 224 + } 225 + 226 + if (this.paths === undefined) { 227 + return; 228 + } 229 + 230 + this.addPoint(event.point); 231 + }; 232 + 233 + this.tool.onMouseUp = (event) => { 234 + if (event.modifiers.shift || this.paths === undefined) { 235 + return; 236 + } 237 + 238 + if (this.simplify) { 239 + for (let p of this.paths) { 240 + p.simplify(10); 241 + } 242 + } 243 + 244 + this.paths = undefined; 245 + 246 + // save project 247 + this.saveCurrent(); 248 + }; 249 + 250 + this.tool.onKeyDown = (event) => { 251 + if (event.key == "m") { 252 + // Create a blob from the canvas... 253 + view.element.toBlob((blob) => { 254 + // ...and get it as a URL. 255 + const url = URL.createObjectURL(blob); 256 + // Open it in a new tab. 257 + window.open(url, "_blank"); 258 + }); 259 + } 260 + if (event.key == "+") { 261 + view.zoom *= 1.1; 262 + } 263 + if (event.key == "-") { 264 + view.zoom /= 1.1; 265 + } 266 + // move around 267 + if (event.key == "up") { 268 + view.translate(0, -10); 269 + } 270 + if (event.key == "down") { 271 + view.translate(0, 10); 272 + } 273 + if (event.key == "left") { 274 + view.translate(-10, 0); 275 + } 276 + if (event.key == "right") { 277 + view.translate(10, 0); 278 + } 279 + }; 280 + } 281 + 282 + makePath() { 283 + let p = new Path(); 284 + p.strokeColor = this.brushColor; 285 + p.strokeWidth = this.brushSize; 286 + return p; 287 + } 288 + 289 + mirrorPoint(point) { 290 + return new Point(point.x, -point.y); 291 + } 292 + 293 + addPoint(point) { 294 + if (this.paths === undefined || this.paths.length < 1) return; 295 + this.paths[0].add(point); 296 + 297 + let index = 1; 298 + if (this.mirror) { 299 + this.paths[index].add(this.mirrorPoint(point)); 300 + index += 1; 301 + } 302 + 303 + for (let i = 1; i <= this.rotations; i++) { 304 + let p = point.rotate((360 / this.rotations) * i); 305 + this.paths[index].add(p); 306 + index += 1; 307 + 308 + if (this.mirror) { 309 + this.paths[index].add(this.mirrorPoint(p)); 310 + index += 1; 311 + } 312 + } 313 + } 314 + 315 + saveCurrent() { 316 + let json = paper.project.exportJSON(); 317 + localStorage.setItem("current-project", json); 318 + } 319 + 320 + loadCurrent() { 321 + let json = localStorage.getItem("current-project"); 322 + paper.project.importJSON(json); 323 + } 324 + 325 + undo() { 326 + let last = paper.project.activeLayer.lastChild; 327 + if (last == undefined) return; 328 + last.remove(); 329 + 330 + this.saveCurrent(); 331 + } 332 + 333 + saveSettings() { 334 + let settings = {}; 335 + settings.mirror = this.mirror; 336 + settings.simplify = this.simplify; 337 + settings.rotations = this.rotations; 338 + settings.brushSize = this.brushSize; 339 + settings.brushColor = this.brushColor; 340 + 341 + localStorage.setItem("brush-settings", JSON.stringify(settings)); 342 + } 343 + } 344 + 345 + function openModal($el) { 346 + $el.classList.add("is-active"); 347 + } 348 + 349 + function openBrushModal() { 350 + openModal(document.getElementById("brush-modal")); 351 + } 352 + 353 + function openFileModal() { 354 + openModal(document.getElementById("file-modal")); 355 + } 356 + 357 + function closeModal($el) { 358 + $el.classList.remove("is-active"); 359 + } 360 + 361 + document.addEventListener("DOMContentLoaded", () => { 362 + function closeAllModals() { 363 + (document.querySelectorAll(".modal") || []).forEach(($modal) => { 364 + closeModal($modal); 365 + }); 366 + } 367 + 368 + // Add a click event on buttons to open a specific modal 369 + (document.querySelectorAll(".js-modal-trigger") || []).forEach(($trigger) => { 370 + const modal = $trigger.dataset.target; 371 + const $target = document.getElementById(modal); 372 + 373 + $trigger.addEventListener("click", () => { 374 + openModal($target); 375 + }); 376 + }); 377 + 378 + // Add a click event on various child elements to close the parent modal 379 + ( 380 + document.querySelectorAll( 381 + ".modal-background, .modal-close, .modal-card-head .delete, .modal-card-foot .button" 382 + ) || [] 383 + ).forEach(($close) => { 384 + const $target = $close.closest(".modal"); 385 + 386 + $close.addEventListener("click", () => { 387 + closeModal($target); 388 + }); 389 + }); 390 + 391 + // Add a keyboard event to close all modals 392 + document.addEventListener("keydown", (event) => { 393 + const e = event || window.event; 394 + 395 + if (e.keyCode === 27) { 396 + // Escape key 397 + closeAllModals(); 398 + } 399 + }); 400 + });
+98
style.css
··· 1 + html, body { 2 + height: 100%; 3 + margin: 0; 4 + overflow: hidden; 5 + background-color: green; 6 + } 7 + 8 + canvas[resize] { 9 + width: 100%; 10 + height: 100%; 11 + position: absolute; 12 + background-color: black; 13 + } 14 + 15 + .top-right-button { 16 + position: absolute; 17 + top: 0; 18 + right: 0; 19 + z-index: 1; 20 + } 21 + 22 + .bottom-right-button { 23 + position: absolute; 24 + bottom: 0; 25 + right: 0; 26 + z-index: 1; 27 + } 28 + 29 + .bottom-left-button { 30 + position: absolute; 31 + bottom: 0; 32 + left: 0; 33 + z-index: 1; 34 + } 35 + 36 + .slidecontainer { 37 + width: 100%; /* Width of the outside container */ 38 + } 39 + 40 + /* The slider itself */ 41 + .slider { 42 + -webkit-appearance: none; /* Override default CSS styles */ 43 + appearance: none; 44 + width: 100%; /* Full-width */ 45 + height: 25px; /* Specified height */ 46 + background: #d3d3d3; /* Grey background */ 47 + outline: none; /* Remove outline */ 48 + opacity: 0.7; /* Set transparency (for mouse-over effects on hover) */ 49 + -webkit-transition: .2s; /* 0.2 seconds transition on hover */ 50 + transition: opacity .2s; 51 + } 52 + 53 + input[type="range"]::-webkit-slider-runnable-track { 54 + background: #053a5f; 55 + } 56 + 57 + /******** Firefox ********/ 58 + input[type="range"]::-moz-range-track { 59 + background: #053a5f; 60 + } 61 + 62 + /* Mouse-over effects */ 63 + .slider:hover { 64 + opacity: 1; /* Fully shown on mouse-over */ 65 + } 66 + 67 + /* The slider handle (use -webkit- (Chrome, Opera, Safari, Edge) and -moz- (Firefox) to override default look) */ 68 + .slider::-webkit-slider-thumb { 69 + -webkit-appearance: none; /* Override default look */ 70 + appearance: none; 71 + width: 25px; /* Set a specific slider handle width */ 72 + height: 25px; /* Slider handle height */ 73 + background: #04AA6D; /* Green background */ 74 + cursor: pointer; /* Cursor on hover */ 75 + } 76 + 77 + .slider::-moz-range-thumb { 78 + width: 25px; /* Set a specific slider handle width */ 79 + height: 25px; /* Slider handle height */ 80 + background: #04AA6D; /* Green background */ 81 + cursor: pointer; /* Cursor on hover */ 82 + } 83 + 84 + 85 + /** FF*/ 86 + input[type="range"]::-moz-range-progress { 87 + background-color: #43e5f7; 88 + } 89 + input[type="range"]::-moz-range-track { 90 + background-color: #9a905d; 91 + } 92 + /* IE*/ 93 + input[type="range"]::-ms-fill-lower { 94 + background-color: #43e5f7; 95 + } 96 + input[type="range"]::-ms-fill-upper { 97 + background-color: #9a905d; 98 + }