├── .gitignore ├── README.md ├── css └── style.css ├── images ├── favicon.png ├── icon-add.svg ├── icon-creator-selected.svg ├── icon-creator.svg ├── icon-delete.svg ├── icon-move-selected.svg ├── icon-move.svg ├── icon-paintbrush-selected.svg ├── icon-paintbrush.svg ├── icon-pick.svg ├── icon-selector-selected.svg ├── icon-selector.svg ├── polypal-logo.svg └── screenshot.png ├── index.html ├── js └── editor │ ├── colors.js │ ├── draw.js │ ├── grid.js │ ├── index.js │ ├── keys.js │ ├── listeners.js │ ├── memory.js │ ├── savedpics.js │ ├── scale.js │ ├── settings.js │ ├── tool-create.js │ ├── tools.js │ ├── utils.js │ ├── vars.js │ └── zindex.js └── todo.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ▲◼ PolyPal 2 | ### Your friendly web-based SVG editor for low-poly style illustrations 3 | 4 | ### [→ Try it out!](https://flukeout.github.io/PolyPal/) 5 | 6 | _Warning: It's a work in progress—it might have bugs!!_ 7 | 8 | 9 | 10 | #### Features! 11 | 12 | * Create & save real SVG files right to your computer's hard-drive! 13 | * Customizable, indexed color palette! 14 | * Change a color in the palette and all shapes of that color also get changed! 15 | * Change the z-index of shapes! 16 | * Undo things up to 20 times! 17 | * Rotate, scale, and move things! 18 | * ...and more to come! 19 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | overflow: hidden; 4 | width: 100vw; 5 | height: 100vh; 6 | user-select: none; 7 | -moz-user-select: none; 8 | } 9 | 10 | .logo { 11 | color: black; 12 | margin: 0 15px; 13 | font-weight: bold; 14 | font-size: 16px; 15 | float: left; 16 | position: relative; 17 | top: 10px; 18 | background-image: url(../images/polypal-logo.svg); 19 | background-position: top left; 20 | background-repeat: no-repeat; 21 | background-size: 34px; 22 | padding-left: 38px; 23 | } 24 | 25 | .top-ui { 26 | position: absolute; 27 | top: 10px; 28 | width: 100%; 29 | font-family: sans-serif; 30 | font-size: 12px; 31 | color: rgba(0,0,0,.6); 32 | z-index: 9999; 33 | } 34 | 35 | .top-ui button { 36 | background: white; 37 | color: black; 38 | border: solid 1px #BBB; 39 | font-size: 12px; 40 | padding: 10px 12px; 41 | border-radius: 20px; 42 | cursor: pointer; 43 | font-weight: bold; 44 | } 45 | 46 | .top-ui .delete, .top-ui .undo { 47 | padding: 10px 18px 10px 12px; 48 | } 49 | 50 | .top-ui button:hover { 51 | background: #EEE; 52 | } 53 | 54 | .top-ui button:active { 55 | background: black; 56 | color: white; 57 | border: solid 1px black; 58 | } 59 | 60 | .bottom-ui { 61 | position: absolute; 62 | bottom: 20px; 63 | width: 100%; 64 | text-align: center; 65 | font-family: sans-serif; 66 | font-size: 12px; 67 | color: rgba(0,0,0,.6); 68 | } 69 | 70 | .bottom-ui input { 71 | width: 300px; 72 | } 73 | 74 | .bottom-ui span { 75 | margin-left: 20px; 76 | position: relative; 77 | top: -4px; 78 | margin-right: 5px; 79 | } 80 | 81 | button { 82 | padding: 3px 10px; 83 | margin: 0 3px; 84 | user-select: none; 85 | } 86 | 87 | button:focus { 88 | outline: none; 89 | } 90 | 91 | button.selected { 92 | box-shadow: 0px 0px 0px 3px rgba(0,255,0,.6); 93 | } 94 | 95 | h1 { 96 | position: absolute; 97 | width: 100%; 98 | text-align: center; 99 | color: white; 100 | top: 0; 101 | line-height: 26px; 102 | font-size: 24px; 103 | color: rgba(255,255,255,.4); 104 | font-weight: 300; 105 | transform: rotateX(-20deg); 106 | padding-bottom: 10px; 107 | } 108 | 109 | .scene { 110 | position: relative; 111 | background: white; 112 | } 113 | 114 | .svg-canvas { 115 | background: white; 116 | position: absolute; 117 | top: 0; 118 | left: 0; 119 | width: calc(100vw - 200px); 120 | height: calc(100vh - 200px); 121 | width: 100vw; 122 | height: 100vh; 123 | } 124 | 125 | .svg-points svg { 126 | overflow: visible; 127 | } 128 | 129 | .svg-image, .svg-points { 130 | width: 100%; 131 | height: 100%; 132 | position: absolute; 133 | top: 0; 134 | left: 0; 135 | } 136 | 137 | .colors { 138 | display: block; 139 | height: 100%; 140 | position: absolute; 141 | display: flex; 142 | align-items: flex-end; 143 | flex-direction: column; 144 | justify-content: center; 145 | right: 0; 146 | bottom: 0; 147 | top: 0; 148 | padding-right: 20px; 149 | } 150 | 151 | .colors .color-wrapper { 152 | position: relative; 153 | margin-bottom: 10px; 154 | display: flex; 155 | flex-direction: row; 156 | } 157 | 158 | 159 | .colors .colorpicker-wrapper input { 160 | position: absolute; 161 | opacity: 0; 162 | background: none; 163 | border: none; 164 | width: 100%; 165 | height: 100%; 166 | padding: 0; 167 | margin: 0; 168 | } 169 | 170 | .colors .color-ui { 171 | width: 25px; 172 | height: 25px; 173 | margin: 0 5px; 174 | border-radius: 50%; 175 | position: relative; 176 | align-self: center; 177 | background-color: transparent; 178 | background-position: center; 179 | background-repeat: no-repeat; 180 | opacity: .2; 181 | cursor: pointer; 182 | display: none; 183 | } 184 | 185 | 186 | .colors .selected .color-ui { 187 | display: block; 188 | } 189 | 190 | 191 | .colors .delete { 192 | background-image: url(../images/icon-delete.svg); 193 | } 194 | 195 | .colors .add { 196 | background-image: url(../images/icon-add.svg); 197 | } 198 | 199 | .colors .colorpicker-wrapper { 200 | background-image: url(../images/icon-pick.svg); 201 | } 202 | 203 | .colors .color-ui:hover { 204 | opacity: .5; 205 | } 206 | 207 | .colors .color-wrapper:only-child .delete { 208 | display: none; 209 | } 210 | 211 | 212 | 213 | 214 | .colors .selected .swatch:after { 215 | content: ""; 216 | position: absolute; 217 | width: 30px; 218 | height: 30px; 219 | left: calc(50% - 15px); 220 | top: calc(50% - 15px); 221 | background-image: url(../images/icon-paintbrush-selected.svg); 222 | } 223 | 224 | .tools { 225 | position: absolute; 226 | left: 0; 227 | padding-left: 20px; 228 | height: 100vh; 229 | text-align: center; 230 | display: flex; 231 | align-items: center; 232 | justify-content: center; 233 | flex-direction: column; 234 | } 235 | 236 | .tools .tool.selected { 237 | background: black; 238 | border: solid 1px black; 239 | animation: boop .2s ease-in-out; 240 | } 241 | 242 | @keyframes boop { 243 | 0% { 244 | transform: scale(1); 245 | } 246 | 40% { 247 | transform: scale(.8); 248 | } 249 | 75% { 250 | transform: scale(1.1); 251 | } 252 | } 253 | 254 | .colors .swatch { 255 | width: 36px; 256 | height: 36px; 257 | margin: 0 5px; 258 | display: block; 259 | cursor: pointer; 260 | position: relative; 261 | border-radius: 50%; 262 | border: solid 2px transparent; 263 | } 264 | 265 | .colors .selected .swatch { 266 | border: solid 2px rgba(0, 0, 0, .2); 267 | } 268 | 269 | .tools .tool { 270 | width: 40px; 271 | height: 40px; 272 | margin-bottom: 10px; 273 | display: block; 274 | cursor: pointer; 275 | position: relative; 276 | border-radius: 50%; 277 | } 278 | 279 | .tools .tool { 280 | display: block; 281 | margin-bottom: 18px; 282 | background: white; 283 | border: solid 1px #BBB; 284 | box-shadow: none; 285 | border-radius: 50%; 286 | color: black; 287 | font-family: sans-serif; 288 | font-weight: bold; 289 | } 290 | 291 | .tools .tool:after { 292 | position: absolute; 293 | bottom: -9px; 294 | right: -9px; 295 | font-size: 12px; 296 | border: solid 1px #BBB; 297 | padding: 3px 5px 2px 5px; 298 | background: white; 299 | border-radius: 12px; 300 | } 301 | 302 | .tools .tool .label { 303 | position: absolute; 304 | left: 50px; 305 | background: #DDD; 306 | padding: 13px 15px 12px 15px; 307 | color: black; 308 | border-radius: 20px; 309 | display: none; 310 | text-transform: capitalize; 311 | white-space: nowrap; 312 | font-weight: normal; 313 | } 314 | 315 | .tools .tool:hover .label { 316 | display: block; 317 | opacity: .4; 318 | } 319 | 320 | .tools .tool.selector:after { 321 | content: "S"; 322 | } 323 | 324 | .tool.selector { 325 | background-image: url(../images/icon-selector.svg); 326 | } 327 | 328 | .tool.selector.selected { 329 | background-image: url(../images/icon-selector-selected.svg); 330 | } 331 | 332 | .tools .tool.paintbrush:after { 333 | content: "B"; 334 | } 335 | 336 | .tool.creator { 337 | background-image: url(../images/icon-creator.svg); 338 | } 339 | 340 | .tool.creator.selected { 341 | background-image: url(../images/icon-creator-selected.svg); 342 | } 343 | 344 | .tools .tool.creator:after { 345 | content: "C"; 346 | } 347 | 348 | .tool.paintbrush { 349 | background-image: url(../images/icon-paintbrush.svg); 350 | } 351 | 352 | .tool.paintbrush.selected { 353 | background-image: url(../images/icon-paintbrush-selected.svg); 354 | } 355 | 356 | .tools .tool.move:after { 357 | content: "M"; 358 | } 359 | 360 | .tool.move { 361 | background-image: url(../images/icon-move.svg); 362 | } 363 | 364 | .tool.move.selected { 365 | background-image: url(../images/icon-move-selected.svg); 366 | } 367 | 368 | .svg-canvas[tool="creator"] { 369 | cursor: pointer; 370 | } 371 | 372 | .svg-canvas[tool="selector"] { 373 | cursor: default; 374 | } 375 | 376 | .svg-canvas[tool="paintbrush"] { 377 | cursor: crosshair; 378 | } 379 | 380 | .svg-canvas[tool="move"] { 381 | cursor: move; 382 | } 383 | 384 | .top-ui button { 385 | position: relative; 386 | } 387 | 388 | .top-ui button:hover:after { 389 | position: absolute; 390 | top: 44px; 391 | padding: 8px 12px; 392 | content: attr(text); 393 | left: calc(50% - 60px); 394 | width: 120px; 395 | background: black; 396 | color: white; 397 | border-radius: 3px; 398 | box-sizing: border-box; 399 | animation: tooltip 2s ease-in; 400 | transform: translateY(5px); 401 | } 402 | 403 | @keyframes tooltip { 404 | 0% { 405 | opacity: 0; 406 | transform: translateY(0); 407 | } 408 | 20% { 409 | opacity: 0; 410 | transform: translateY(0); 411 | } 412 | 25% { 413 | opacity: 1; 414 | transform: translateY(7px); 415 | } 416 | 30% { 417 | transform: translateY(5px); 418 | } 419 | } 420 | 421 | .svg-image polygon.pulse { 422 | animation: pulse .3s ease-in-out; 423 | animation-iteration-count: 2; 424 | } 425 | 426 | @keyframes pulse { 427 | 50% { 428 | opacity: .75; 429 | } 430 | } 431 | 432 | .top-ui button:before { 433 | line-height: 13px; 434 | position: absolute; 435 | bottom: -6px; 436 | right: -6px; 437 | font-size: 12px; 438 | border: solid 1px #BBB; 439 | padding: 3px 5px; 440 | background: white; 441 | border-radius: 12px; 442 | } 443 | 444 | 445 | button.delete:before { 446 | content: "D"; 447 | } 448 | 449 | button.undo:before { 450 | content: "Z"; 451 | } 452 | -------------------------------------------------------------------------------- /images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flukeout/PolyPal/fcc384abdde199521b174543048f7ec854d2f32e/images/favicon.png -------------------------------------------------------------------------------- /images/icon-add.svg: -------------------------------------------------------------------------------- 1 | icon-add -------------------------------------------------------------------------------- /images/icon-creator-selected.svg: -------------------------------------------------------------------------------- 1 | icon-creator-selected -------------------------------------------------------------------------------- /images/icon-creator.svg: -------------------------------------------------------------------------------- 1 | icon-creator -------------------------------------------------------------------------------- /images/icon-delete.svg: -------------------------------------------------------------------------------- 1 | icon-delete -------------------------------------------------------------------------------- /images/icon-move-selected.svg: -------------------------------------------------------------------------------- 1 | icon-move-selected -------------------------------------------------------------------------------- /images/icon-move.svg: -------------------------------------------------------------------------------- 1 | icon-move -------------------------------------------------------------------------------- /images/icon-paintbrush-selected.svg: -------------------------------------------------------------------------------- 1 | icon-paintbrush-selected -------------------------------------------------------------------------------- /images/icon-paintbrush.svg: -------------------------------------------------------------------------------- 1 | icon-paintbrush -------------------------------------------------------------------------------- /images/icon-pick.svg: -------------------------------------------------------------------------------- 1 | icon-pick -------------------------------------------------------------------------------- /images/icon-selector-selected.svg: -------------------------------------------------------------------------------- 1 | icon-selector-selected -------------------------------------------------------------------------------- /images/icon-selector.svg: -------------------------------------------------------------------------------- 1 | icon-selector -------------------------------------------------------------------------------- /images/polypal-logo.svg: -------------------------------------------------------------------------------- 1 | polypal-logo -------------------------------------------------------------------------------- /images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flukeout/PolyPal/fcc384abdde199521b174543048f7ec854d2f32e/images/screenshot.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PolyPal SVG Editor 6 | 7 | 8 | 9 | 10 |
11 | 18 | 19 | 20 |
21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | — 32 | Extrude type 33 | 34 | 35 | — 36 | 37 | 38 | — 39 | Z-index 40 | 41 | 42 |
43 | 44 |
45 | Scale 46 | 47 | Rotate 48 | 49 |
50 | 51 | 52 |
53 |
54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /js/editor/colors.js: -------------------------------------------------------------------------------- 1 | let availableColors = [ 2 | "#8F3D61", 3 | "#B94B5D", 4 | "#DD7E5F", 5 | "#EB9762", 6 | "#EDBD77", 7 | "#DDDDDD" 8 | ]; 9 | 10 | // availableColors = [ 11 | // "#D3C663", 12 | // "#AEB160", 13 | // "#97904A", 14 | // "#5AA99D", 15 | // "#38797A", 16 | // "#0C5878", 17 | // "#BBBBBB" 18 | // ] 19 | 20 | let colorWrapper = dQ(".colors"); 21 | let selectedColor = availableColors[0]; 22 | let selectedColorIndex = 0; 23 | 24 | 25 | const buildColorUI = () => { 26 | let index = 0; 27 | 28 | colorWrapper.innerHTML = ""; 29 | 30 | availableColors.map(color => { 31 | 32 | let html = ` 33 |
34 |
35 |
36 | 37 |
38 |
44 |
45 | `; 46 | 47 | let colorEl = document.createElement("div"); 48 | colorEl.classList.add("color-wrapper"); 49 | if(index === selectedColorIndex) { 50 | colorEl.classList.add("selected"); 51 | } 52 | colorEl.innerHTML = html; 53 | colorEl.setAttribute("index", index); 54 | 55 | // Set up clickable swatch 56 | let swatchEl = colorEl.querySelector(".swatch"); 57 | swatchEl.addEventListener("click", function(el){ 58 | selectColor(el.target.getAttribute("index")); 59 | }); 60 | 61 | colorEl.querySelector(".colorpicker-wrapper").addEventListener("mouseover", function(el){ 62 | let index = el.target.getAttribute("index"); 63 | highlightGridsByIndex(index); 64 | }); 65 | 66 | colorEl.querySelector(".colorpicker-wrapper").addEventListener("mouseout", function(el){ 67 | clearGridHighlight(); 68 | }); 69 | 70 | 71 | let deleteEl = colorEl.querySelector(".delete"); 72 | deleteEl.addEventListener("click", function(el){ 73 | let parent = el.target.closest(".color-wrapper"); 74 | let index = parseInt(parent.getAttribute("index")); 75 | deleteColor(index); 76 | }); 77 | 78 | let addEl = colorEl.querySelector(".add"); 79 | addEl.addEventListener("click", function(el){ 80 | let parent = el.target.closest(".color-wrapper"); 81 | let index = parseInt(parent.getAttribute("index")); 82 | addColor(index); 83 | }); 84 | 85 | 86 | // Set up colorpicker input 87 | let colorPicker = colorEl.querySelector("input"); 88 | colorPicker.addEventListener("change",function(e){ 89 | let index = parseInt(e.target.getAttribute("index")); 90 | changeColor(index, e.target.value); 91 | }); 92 | 93 | colorWrapper.appendChild(colorEl); 94 | index++; 95 | }); 96 | 97 | } 98 | 99 | const addColor = index => { 100 | currentColor = availableColors[index]; 101 | availableColors.splice(index, 0, currentColor); 102 | 103 | grids = grids.map(grid => { 104 | 105 | if(grid.fillColorIndex > index) { 106 | grid.fillColorIndex++; 107 | } 108 | 109 | return grid; 110 | }) 111 | 112 | buildColorUI(); 113 | frameLoop(); 114 | } 115 | 116 | const deleteColor = index => { 117 | availableColors.splice(index, 1); 118 | console.log(availableColors); 119 | 120 | grids = grids.map(grid => { 121 | 122 | if(grid.fillColorIndex == index) { 123 | grid.fillColorIndex = false; 124 | } 125 | 126 | if(grid.fillColorIndex >= index) { 127 | grid.fillColorIndex--; 128 | } 129 | return grid; 130 | }); 131 | 132 | buildColorUI(); 133 | frameLoop(); 134 | } 135 | 136 | const changeColor = (index, value) => { 137 | availableColors[index] = value; 138 | frameLoop(); 139 | updateColors(); 140 | } 141 | 142 | const updateColors = () => { 143 | let swatches = document.querySelectorAll(".swatch"); 144 | let index = 0; 145 | 146 | swatches.forEach(el => { 147 | el.setAttribute("color", availableColors[index]); 148 | el.style.background = availableColors[index]; 149 | index++; 150 | }); 151 | } 152 | 153 | const selectColor = colorIndex => { 154 | selectedColorIndex = parseInt(colorIndex); 155 | 156 | if(grids) { 157 | grids = grids.map(grid => { 158 | if(grid.selected) { 159 | grid.fillColorIndex = colorIndex; 160 | } 161 | return grid; 162 | }); 163 | } 164 | 165 | document.querySelectorAll(".colors .color-wrapper").forEach(el => { 166 | el.classList.remove("selected"); 167 | if(selectedColorIndex === parseInt(el.getAttribute("index"))) { 168 | el.classList.add("selected"); 169 | } 170 | }); 171 | 172 | if(typeof frameLoop !== "undefined") { 173 | frameLoop(); 174 | } 175 | } 176 | 177 | buildColorUI(); 178 | 179 | selectColor(selectedColorIndex); 180 | 181 | const highlightGridsByIndex = colorIndex => { 182 | grids.map(g => { 183 | if(g.fillColorIndex == colorIndex) { 184 | g.svgEl.classList.add("pulse"); 185 | } 186 | }); 187 | } 188 | 189 | const clearGridHighlight = () => { 190 | grids.map(g => { 191 | g.svgEl.classList.remove("pulse"); 192 | }) 193 | } 194 | -------------------------------------------------------------------------------- /js/editor/draw.js: -------------------------------------------------------------------------------- 1 | const drawVertex = (p) => { 2 | 3 | drawSvgVertex(p); 4 | 5 | if(p.hovered) { 6 | hoveredVertex = true; 7 | } 8 | } 9 | 10 | const drawSvgVertex = (p) => { 11 | if(!p.svgEl) { return } 12 | 13 | if(p.selected) { 14 | p.svgEl.querySelector(".bigcircle").setAttribute("stroke", "rgba(0,0,0,.2)"); 15 | p.svgEl.querySelector(".smallcircle").setAttribute("fill", "rgba(0,0,0,1)"); 16 | } else if (p.hovered) { 17 | p.svgEl.querySelector(".bigcircle").setAttribute("stroke", "rgba(0,0,0,.2)"); 18 | p.svgEl.querySelector(".smallcircle").setAttribute("fill", "none"); 19 | } else if (p.stickyHovered) { 20 | p.svgEl.querySelector(".bigcircle").setAttribute("stroke", "rgba(0,0,0,0)"); 21 | p.svgEl.querySelector(".smallcircle").setAttribute("fill", "rgba(0,0,0,.65)"); 22 | } else { 23 | p.svgEl.querySelector(".bigcircle").setAttribute("stroke", "none"); 24 | p.svgEl.querySelector(".smallcircle").setAttribute("fill", "none"); 25 | } 26 | 27 | p.svgEl.setAttribute("x",p.x); 28 | p.svgEl.setAttribute("y",p.y); 29 | } 30 | 31 | 32 | let hoverSegmentSvg = false; 33 | // Draw the segment that is being hovered 34 | const drawHoverSegment = (show) => { 35 | 36 | let closestSegment = hoveredSegments.reduce((segment, closestSeg) => { 37 | if(segment.distance < closestSeg.distance) { 38 | return segment; 39 | } else { 40 | return closestSeg; 41 | } 42 | }, hoveredSegments[0]); 43 | 44 | if(hoverSegmentSvg == false) { 45 | let attributes = { 46 | "fill" : "transparent", 47 | "stroke-width" : "2", 48 | "stroke-linecap" : "round" 49 | } 50 | hoverSegmentSvg = makeSvg("line", attributes, ".svg-points"); 51 | } 52 | 53 | if(closestSegment && show) { 54 | hoverSegmentSvg.setAttribute("stroke", "rgba(0,0,0,.4"); 55 | updateHoverSegment({ 56 | "x1" : closestSegment.start.x, 57 | "y1" : closestSegment.start.y, 58 | "x2" : closestSegment.end.x, 59 | "y2" : closestSegment.end.y, 60 | }); 61 | } else { 62 | hoverSegmentSvg.setAttribute("stroke", "rgba(0,0,0,0"); 63 | } 64 | 65 | } 66 | 67 | const updateHoverSegment = attrs => { 68 | hoverSegmentSvg.setAttribute("x1", attrs.x1); 69 | hoverSegmentSvg.setAttribute("y1", attrs.y1); 70 | hoverSegmentSvg.setAttribute("x2", attrs.x2); 71 | hoverSegmentSvg.setAttribute("y2", attrs.y2); 72 | } 73 | -------------------------------------------------------------------------------- /js/editor/grid.js: -------------------------------------------------------------------------------- 1 | 2 | class Grid { 3 | 4 | constructor(points) { 5 | this.points = points; 6 | 7 | this.lineWidth = 1; 8 | this.fillLineColor = "#AAA"; 9 | this.numFillLines = shapeFillLineCount; 10 | 11 | this.outlineWidth = shapeOutlineLineWidth; 12 | this.outlineColor = shapeOutlineColor; 13 | this.fillStartPoint = 0; 14 | 15 | this.fillColorIndex = 0; 16 | 17 | this.hovered = false; 18 | this.selected = false; 19 | this.svgCreated = false; 20 | this.svgEl = false; 21 | this.uiEl = false; 22 | } 23 | 24 | createSvg() { 25 | 26 | this.svgCreated = true; 27 | 28 | // This is the actual image element 29 | this.svgEl = document.createElementNS("http://www.w3.org/2000/svg","polygon"); 30 | this.svgEl.setAttribute("stroke-width", "1"); 31 | this.svgEl.setAttribute("stroke", "rgba(0,0,0,0"); 32 | this.svgEl.setAttribute("stroke-linejoin", "round"); 33 | 34 | this.zIndex = (svgImage.querySelectorAll("polygon").length || 0)+ 1; 35 | 36 | svgImage.appendChild(this.svgEl); 37 | 38 | // This is for displaying selections, etc 39 | this.uiEl = document.createElementNS("http://www.w3.org/2000/svg","polygon"); 40 | this.uiEl.setAttribute("stroke-width", "2"); 41 | this.uiEl.setAttribute("stroke-linejoin", "round"); 42 | this.uiEl.setAttribute("fill", "transparent"); 43 | svgPoints.appendChild(this.uiEl); 44 | 45 | this.updatePoly(); 46 | } 47 | 48 | // Update both the UI element 49 | updatePoly() { 50 | this.uiEl.setAttribute("stroke-width", "1"); 51 | let pointsString = this.points.reduce((string, point) => { 52 | return string + parseInt(point.x) + "," + parseInt(point.y) + " "; 53 | }, ""); 54 | 55 | if(this.mode == "ghost") { 56 | this.svgEl.setAttribute("fill", "transparent"); 57 | this.svgEl.setAttribute("stroke", "rgba(0,0,0,.2"); 58 | } else if (this.mode == "invisible") { 59 | this.svgEl.setAttribute("fill", "transparent"); 60 | } else { 61 | let color = availableColors[this.fillColorIndex] || "rgba(255,255,255,0)" 62 | this.svgEl.setAttribute("fill", color); 63 | this.svgEl.setAttribute("stroke", "rgba(0,0,0,.2"); 64 | } 65 | 66 | if(this.selected) { 67 | this.uiEl.setAttribute("stroke-width", "2"); 68 | this.uiEl.setAttribute("stroke", "rgba(0,0,0,1"); 69 | } else if(this.showHovered && this.showHover) { 70 | this.uiEl.setAttribute("stroke-width", "1"); 71 | this.uiEl.setAttribute("stroke", "rgba(0,0,0,.3"); 72 | } else if(this.mode == "invisible") { 73 | this.uiEl.setAttribute("stroke-width", "0"); 74 | this.uiEl.setAttribute("stroke", "red"); 75 | } else { 76 | this.uiEl.setAttribute("stroke-width", "1"); 77 | this.uiEl.setAttribute("stroke", "transparent"); 78 | } 79 | 80 | this.svgEl.setAttribute("points", pointsString); 81 | this.uiEl.setAttribute("points", pointsString); 82 | } 83 | 84 | canvasDraw() { 85 | 86 | if(this.svgCreated == false ){ 87 | this.createSvg(); 88 | } 89 | this.updatePoly(); 90 | } 91 | 92 | checkShapeHover() { 93 | let shapePoints = []; 94 | for(var i = 0; i < this.points.length; i++) { 95 | let p = this.points[i]; 96 | shapePoints.push([p.x, p.y]); 97 | } 98 | this.hovered = testWithin([mouse.x, mouse.y], shapePoints); 99 | } 100 | 101 | click() { 102 | this.selected = !this.selected; 103 | } 104 | 105 | // Add hovered segments 106 | checkHoverSegments() { 107 | for(var i = 0; i < this.points.length; i++){ 108 | 109 | let thisP = this.points[i]; 110 | let nextP = this.points[i + 1]; 111 | let start, end, dist; 112 | dist = 0; 113 | 114 | if(!nextP) { 115 | nextP = this.points[0]; 116 | } 117 | 118 | start = {x: thisP.x, y: thisP.y}; 119 | end = {x: nextP.x, y: nextP.y}; 120 | 121 | dist = distToSegment({x : mouse.x, y : mouse.y}, start, end); 122 | 123 | if(dist <= lineHoverDistance) { 124 | hoveredSegments.push( 125 | { 126 | start : { x : start.x, y: start.y }, 127 | end : { x : end.x, y: end.y }, 128 | distance : dist 129 | } 130 | ) 131 | } 132 | } 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /js/editor/index.js: -------------------------------------------------------------------------------- 1 | // Basic Config 2 | let canvasWidth = svgImage.getBoundingClientRect().width; 3 | let canvasHeight = svgImage.getBoundingClientRect().height; 4 | 5 | let cloning = false; 6 | let cloners = []; 7 | let wobble = false; 8 | let pointSelected = false; 9 | let gridSelected = false; 10 | let newGrid; 11 | let clickedGrids; 12 | let distanceTraveled; 13 | 14 | let clonedGrid = { 15 | newGrid : "", 16 | startPoint : { 17 | x : 0, 18 | y : 0 19 | }, 20 | distanceTraveled : 0 21 | } 22 | 23 | window.addEventListener("mousedown", e => { 24 | mouse.pressedAnywhere = true; 25 | frameLoop(); 26 | }); 27 | 28 | svgScene.addEventListener("mousedown", (e) => { 29 | 30 | mouse.pressed = true; 31 | cloning = false; 32 | clickedGrids = []; 33 | 34 | if(selectedTool == "paintbrush") { 35 | clickedGrids = []; 36 | grids.map(grid => { 37 | if(grid.hovered) { 38 | clickedGrids.push(grid) 39 | } 40 | }); 41 | if(clickedGrids.length >0 ) { 42 | pushHistory(); 43 | highestZIndexItem(clickedGrids).fillColorIndex = selectedColorIndex; 44 | } 45 | } 46 | 47 | if(selectedTool == "creator") { 48 | toolCreate.mouseDown(e); 49 | } 50 | 51 | if(selectedTool == "selector") { 52 | 53 | cloners = []; 54 | cloning = false; 55 | 56 | pointSelected = false; 57 | gridSelected = false; 58 | 59 | let gridClicked = false; 60 | let clickedSelectedPoint = false; 61 | 62 | // Check if we are clicking a selected 63 | points.map(p => { 64 | if(p.hovered && p.selected) { 65 | clickedSelectedPoint = true; 66 | } 67 | }); 68 | 69 | // If a non-selected point is clicked 70 | // clear all selected points. 71 | if(clickedSelectedPoint == false && mouse.shiftPressed == false) { 72 | points = points.map(p => { 73 | p.selected = false; 74 | return p; 75 | }); 76 | } 77 | 78 | // Select clicked point. 79 | points = points.map(p => { 80 | if(p.hovered) { 81 | p.selected = true; 82 | pointSelected = true; 83 | } 84 | return p; 85 | }); 86 | 87 | if(pointSelected == false && mouse.shiftPressed == false) { 88 | points = points.map(p => { 89 | p.selected = false; 90 | p.hovered = false; 91 | return p; 92 | }); 93 | } 94 | 95 | 96 | // If no points are selected 97 | if(pointSelected == false) { 98 | 99 | clickedGrids = []; 100 | 101 | clickedGrids = grids.filter(grid => grid.hovered); 102 | 103 | if(mouse.shiftPressed == false ) { 104 | deselectGrids(); 105 | } 106 | 107 | grids.map(grid => { 108 | 109 | // Figure out segment hovering - ! 110 | for(var i = 0; i < grid.points.length; i++){ 111 | 112 | let thisP = grid.points[i]; 113 | let nextP = grid.points[i + 1]; 114 | let start, end, dist; 115 | dist = 0; 116 | 117 | if(!nextP) { 118 | nextP = grid.points[0]; 119 | } 120 | 121 | start = {x: thisP.x, y: thisP.y}; 122 | end = {x: nextP.x, y: nextP.y}; 123 | 124 | dist = distToSegment({x : mouse.x, y : mouse.y}, start, end); 125 | 126 | if(dist <= lineHoverDistance) { 127 | if(cloners.length < 2) { 128 | 129 | cloners.push(thisP); 130 | cloners.push(nextP); 131 | } 132 | } 133 | } 134 | }); // end grid.map... 135 | 136 | // Click the Grid with the highest z index 137 | if(clickedGrids.length > 0 && cloners.length === 0) { 138 | highestZIndexItem(clickedGrids).click(); 139 | gridClicked = true; 140 | } 141 | 142 | } else { 143 | deselectGrids(); 144 | } 145 | 146 | if(pointSelected == false) { 147 | mouse.dragging = true; 148 | mouse.dragZone.start.x = e.offsetX; 149 | mouse.dragZone.start.y = e.offsetY; 150 | mouse.dragZone.end.x = e.offsetX; 151 | mouse.dragZone.end.y = e.offsetY; 152 | } 153 | 154 | // For cloning 155 | if(cloners.length == 2 && pointSelected == false && mouse.shiftPressed == false) { 156 | 157 | deselectGrids(); 158 | deselectPoints(); 159 | frameLoop(); 160 | cloning = true; 161 | 162 | // Add new points to the points array 163 | let newPoints = []; 164 | let newOne, newTwo; 165 | 166 | if(settings.extrudeMode == "line") { 167 | newOne = { x: parseInt(cloners[0].x), y: parseInt(cloners[0].y)} 168 | newTwo = { x: parseInt(cloners[1].x), y: parseInt(cloners[1].y)} 169 | 170 | newOne = createPoint(newOne); 171 | newTwo = createPoint(newTwo); 172 | 173 | newOne.cloning = true; 174 | newTwo.cloning = true; 175 | 176 | points.push(newTwo); 177 | points.push(newOne); 178 | newPoints.push(newTwo); 179 | newPoints.push(newOne); 180 | 181 | } else if (settings.extrudeMode == "point") { 182 | newOne = { x: parseInt(mouse.x), y: parseInt(mouse.y)} 183 | 184 | newOne = createPoint(newOne); 185 | newOne.cloning = true; 186 | 187 | points.push(newOne); 188 | newPoints.push(newOne); 189 | } 190 | 191 | newPoints.push(cloners[0]); 192 | newPoints.push(cloners[1]); 193 | 194 | mouse.dragging = false; 195 | 196 | // Create a grid tile from it 197 | let newGrid = createGrid(newPoints, { 198 | fillColorIndex : selectedColorIndex, 199 | mode : "invisible" 200 | }); 201 | 202 | // Keep track of the cloned grid... 203 | clonedGrid.grid = newGrid; 204 | clonedGrid.distanceTraveled = 0; 205 | clonedGrid.startPoint.x = parseInt(mouse.x); 206 | clonedGrid.startPoint.y = parseInt(mouse.y); 207 | 208 | pushHistory(); 209 | 210 | grids.push(newGrid); 211 | } 212 | } 213 | 214 | snapshotTaken = false; 215 | 216 | }); 217 | 218 | 219 | window.addEventListener("mousemove", (e) => { 220 | 221 | if(mouse.pressed && snapshotTaken == false && clonedGrid.grid == false) { 222 | pushHistory(); 223 | snapshotTaken = true; 224 | } 225 | 226 | let dX = e.clientX - mouse.x; 227 | let dY = e.clientY - mouse.y; 228 | 229 | if(selectedTool === "creator" && mouse.pressed == true) { 230 | toolCreate.mouseMove(e); 231 | } 232 | 233 | if(selectedTool === "selector") { 234 | 235 | if(clonedGrid.grid) { 236 | let cloneDist = distPoints(clonedGrid.startPoint, { x: mouse.x, y : mouse.y}); 237 | if(cloneDist > 18) { 238 | clonedGrid.grid.mode = "normal"; 239 | } else { 240 | clonedGrid.grid.mode = "ghost"; 241 | } 242 | } 243 | 244 | if(mouse.dragging) { 245 | mouse.dragZone.end.x += dX; 246 | mouse.dragZone.end.y += dY; 247 | } 248 | 249 | points = points.map(p => { 250 | 251 | if(mouse.dragging) { 252 | p.hovered = false; 253 | p.stickyHovered = checkDragZone(p); 254 | } else { 255 | let distance = Math.sqrt(Math.pow(p.x - mouse.x, 2) + Math.pow(p.y - mouse.y, 2)); 256 | 257 | if(distance < hoverRadius) { 258 | p.hovered = true; 259 | } else { 260 | p.hovered = false; 261 | } 262 | } 263 | 264 | if((p.selected || p.cloning) && mouse.pressed && mouse.shiftPressed == false) { 265 | p.x += dX; 266 | p.y += dY; 267 | moveSticky(dX,dY); 268 | } 269 | 270 | return p; 271 | }); 272 | } 273 | 274 | if(selectedTool === "move" && mouse.pressed) { 275 | points = points.map(p => { 276 | p.x += dX; 277 | p.y += dY; 278 | return p; 279 | }); 280 | } 281 | 282 | if(selectedTool === "paintbrush" && mouse.pressed) { 283 | clickedGrids = []; 284 | grids.map(grid => { 285 | if(grid.hovered) { 286 | clickedGrids.push(grid) 287 | } 288 | }); 289 | if(clickedGrids.length > 0) { 290 | highestZIndexItem(clickedGrids).fillColorIndex = selectedColorIndex; 291 | } 292 | } 293 | 294 | mouse.x = e.clientX; 295 | mouse.y = e.clientY; 296 | 297 | frameLoop(); 298 | }); 299 | 300 | // Deselect all points on mouseup 301 | window.addEventListener("mouseup", (e) => { 302 | mouse.pressed = false; 303 | mouse.dragging = false; 304 | mouse.pressedAnywhere = false; 305 | 306 | // If we were cloning, but didn't create a new grid, 307 | // select the original grid we clicked instead. 308 | if(cloning && clickedGrids.length > 0) { 309 | if(clonedGrid.grid.mode == "invisible") { 310 | highestZIndexItem(clickedGrids).click(); 311 | } 312 | } 313 | 314 | // SVG 315 | dragSvg.remove(); 316 | dragSvg = false; 317 | cloning = false; 318 | 319 | points = points.map(p => { 320 | if(p.stickyHovered) { 321 | p.stickyHovered = false; 322 | p.selected = true; 323 | } 324 | p.cloning = false; 325 | return p; 326 | }); 327 | 328 | if(selectedTool === "creator") { 329 | toolCreate.mouseUp(e); 330 | } 331 | 332 | roundPoints(); 333 | frameLoop(); 334 | }); 335 | 336 | const moveSticky = (dX, dY) => { 337 | points = points.map(p => { 338 | if(p.stickyHovered && !p.selected) { 339 | p.x += dX; 340 | p.y += dY; 341 | } 342 | return p; 343 | }); 344 | } 345 | 346 | const deleteSelectedGrids = () => { 347 | grids = customFilter(grids, (g => g.selected)); 348 | } 349 | 350 | let frameCount = 0; 351 | let hoveredVertex = false; 352 | let hoveredSegments; 353 | let hoveredGrids; 354 | 355 | const frameLoop = () => { 356 | 357 | if(mouse.pressed == false && mouse.pressedAnywhere == false) { 358 | killGhosts(); // Kill shapes that are ghosts 359 | cleanupPoints(); // Get rid of orphan points 360 | mergeSamePoints(); // Make points close to each other have the same x,y values 361 | consolidatePoints(); // Make points with same x,y be the same points 362 | cleanupGrids(); // Throw out grids with less than 3 points 363 | cleanupPoints(); // Get rid of orphan points 364 | } 365 | 366 | hoveredVertex = false; 367 | frameCount++; 368 | 369 | hoveredSegments = []; 370 | hoveredGrids = []; 371 | // Get all the hover segments 372 | 373 | if(selectedTool == "selector" || selectedTool == "paintbrush") { 374 | grids.map(grid => { 375 | if(mouse.shiftPressed == false) { 376 | grid.checkHoverSegments(); 377 | } 378 | if(grid.hovered) { 379 | hoveredGrids.push(grid); 380 | } 381 | }); 382 | } 383 | 384 | 385 | 386 | grids.map(grid => { 387 | grid.showHover = hoveredSegments.length > 0 ? false : true; 388 | grid.checkShapeHover(); // sets 'grid.hovered' 389 | grid.showHovered = false; 390 | 391 | if(hoveredGrids.length > 0) { 392 | hoveredGrids = hoveredGrids.sort((a,b) => { 393 | return a.zIndex < b.zIndex ? 1 : -1; 394 | }); 395 | hoveredGrids[0].showHovered = true; 396 | } 397 | 398 | if(selectedTool == "paintbrush") { 399 | grid.showHover = true; 400 | } 401 | 402 | if(mouse.dragging || cloning) { 403 | grid.showHover = false; 404 | } 405 | 406 | grid.canvasDraw(); 407 | }); 408 | 409 | points.map(p => drawVertex(p)); // These are just UI points 410 | 411 | // Draw the hovered line segment closest ot pointer 412 | let showHoverSegment = true; 413 | if(selectedTool === "selector" && cloning == true) { 414 | showHoverSegment = false; 415 | } 416 | 417 | drawHoverSegment(showHoverSegment); 418 | 419 | if(hoverSegmentSvg) { 420 | if(hoveredVertex == true) { 421 | hoverSegmentSvg.setAttribute("stroke", "none"); 422 | } 423 | } 424 | 425 | drawDragZone(); 426 | } 427 | 428 | 429 | let dragSvg = false; 430 | 431 | const drawDragZone = () => { 432 | 433 | if(dragSvg == false) { 434 | let attributes = { 435 | "fill" : "rgba(255,0,0,.2)" 436 | } 437 | dragSvg = makeSvg("polygon", attributes, ".svg-points"); 438 | } 439 | 440 | if(mouse.dragging) { 441 | 442 | let dragPoints = [ 443 | { 444 | x: mouse.dragZone.start.x, 445 | y: mouse.dragZone.start.y 446 | },{ 447 | x: mouse.dragZone.end.x, 448 | y: mouse.dragZone.start.y 449 | },{ 450 | x: mouse.dragZone.end.x, 451 | y: mouse.dragZone.end.y 452 | },{ 453 | x: mouse.dragZone.start.x, 454 | y: mouse.dragZone.end.y 455 | }, 456 | ] 457 | 458 | // SVG 459 | let pointsString = dragPoints.reduce((string, point) => { 460 | return string + parseInt(point.x) + "," + parseInt(point.y) + " "; 461 | }, ""); 462 | dragSvg.setAttribute("points", pointsString); 463 | } 464 | } 465 | 466 | const checkDragZone = p => { 467 | let startX = Math.min(mouse.dragZone.start.x, mouse.dragZone.end.x); 468 | let endX = Math.max(mouse.dragZone.start.x, mouse.dragZone.end.x); 469 | let startY = Math.min(mouse.dragZone.start.y, mouse.dragZone.end.y); 470 | let endY = Math.max(mouse.dragZone.start.y, mouse.dragZone.end.y); 471 | 472 | return ( 473 | p.x > startX 474 | && p.x < endX 475 | && p.y > startY 476 | && p.y < endY 477 | ) 478 | } 479 | 480 | const start = () => { 481 | let picture = window.localStorage.getItem("picture"); 482 | let loaded = loadPicture(JSON.parse(picture)); 483 | 484 | if(loaded == false) { 485 | resetPicture(); 486 | } 487 | 488 | frameLoop(); 489 | } 490 | 491 | 492 | 493 | start(); 494 | -------------------------------------------------------------------------------- /js/editor/keys.js: -------------------------------------------------------------------------------- 1 | const keyMap = { 2 | 37 : "left", 3 | 38 : "up", 4 | 39 : "right", 5 | 40 : "down", 6 | 16 : "shift", 7 | 68 : "delete", 8 | 8 : "delete", 9 | 187 : "plus", 10 | 189 : "minus", 11 | 83 : "selector", 12 | 66 : "paintbrush", 13 | 77 : "move", 14 | 16 : "shift", 15 | 90 : "z", 16 | 67 : "creator", 17 | } 18 | 19 | const getKey = keyCode => { 20 | // console.log(keyCode); 21 | return keyMap[keyCode]; 22 | } 23 | 24 | window.addEventListener("keydown", e => { 25 | 26 | let key = getKey(e.keyCode); 27 | 28 | if(key == "selector" || key == "paintbrush" || key == "move" || key == "creator") { 29 | selectTool(key); 30 | } 31 | 32 | if(key == "shift") { 33 | mouse.shiftPressed = true; 34 | } 35 | 36 | if(key == "z") { 37 | undo(); 38 | } 39 | 40 | if(key == "delete") { 41 | e.preventDefault(); 42 | deleteSelected(); 43 | } 44 | 45 | if(key == "plus") { 46 | scalePoints(.05); 47 | } 48 | 49 | if(key == "minus") { 50 | scalePoints(-.05); 51 | } 52 | 53 | frameLoop(); 54 | }); 55 | 56 | window.addEventListener("keyup", e => { 57 | mouse.shiftPressed = false; 58 | frameLoop(); 59 | }); 60 | -------------------------------------------------------------------------------- /js/editor/listeners.js: -------------------------------------------------------------------------------- 1 | // dQ(".plus").addEventListener("click", () => scalePoints(.05)); 2 | // dQ(".minus").addEventListener("click", () => scalePoints(-.05)); 3 | 4 | // dQ(".rotate[type='left'").addEventListener("click", () => rotatePoints(10)); 5 | // dQ(".rotate[type='right'").addEventListener("click", () => rotatePoints(-10)); 6 | 7 | const setExtrudeMode = (mode) => { 8 | settings.extrudeMode = mode; 9 | dQ(".extrude[type='point']").classList.remove("selected"); 10 | dQ(".extrude[type='line']").classList.remove("selected"); 11 | dQ(".extrude[type='"+mode+"']").classList.add("selected"); 12 | } 13 | 14 | const toggleExtrudeMode = () => { 15 | if(settings.extrudeMode == "point") { 16 | setExtrudeMode("line"); 17 | } else { 18 | setExtrudeMode("point");; 19 | } 20 | } 21 | 22 | setExtrudeMode("point"); 23 | 24 | document.querySelectorAll(".extrude").forEach((el) => { 25 | el.addEventListener("click", (e) => setExtrudeMode(e.target.getAttribute("type"))); 26 | }); 27 | 28 | document.querySelector(".download").addEventListener("click", () => { 29 | downloadSvg(); 30 | }); 31 | 32 | function downloadSvg() { 33 | var svgData = dQ(".svg-image").outerHTML; 34 | var svgBlob = new Blob([svgData], {type:"image/svg+xml;charset=utf-8"}); 35 | var svgUrl = URL.createObjectURL(svgBlob); 36 | var downloadLink = document.createElement("a"); 37 | downloadLink.href = svgUrl; 38 | downloadLink.download = "polypal.svg"; 39 | document.body.appendChild(downloadLink); 40 | downloadLink.click(); 41 | document.body.removeChild(downloadLink); 42 | } 43 | 44 | let previousScale = 0; 45 | 46 | document.querySelector(".bottom-ui .scale").addEventListener("input",function(e){ 47 | let scale = e.target.value; 48 | let scaleDelta = scale - previousScale; 49 | scalePoints(scaleDelta, false); // false is to not take a history snapshot 50 | previousScale = scale; 51 | }) 52 | 53 | let previousRotation = 0; 54 | 55 | document.querySelector(".bottom-ui .rotate").addEventListener("mousedown", function(e){ 56 | pushHistory(); 57 | }); 58 | 59 | document.querySelector(".bottom-ui .scale").addEventListener("mousedown", function(e){ 60 | pushHistory(); 61 | }); 62 | 63 | 64 | document.querySelector(".bottom-ui .rotate").addEventListener("input",function(e){ 65 | let rotation = e.target.value; 66 | let rotationDelta = rotation - previousRotation; 67 | rotatePoints(-rotationDelta, false); // False is to not push history 68 | previousRotation = rotation; 69 | }); 70 | 71 | document.querySelector(".bottom-ui .rotate").addEventListener("change",function(e){ 72 | e.target.value = 0; 73 | previousRotation = 0; 74 | }); 75 | 76 | document.querySelector(".bottom-ui .scale").addEventListener("change",function(e){ 77 | e.target.value = 0; 78 | previousScale = 0; 79 | }); -------------------------------------------------------------------------------- /js/editor/memory.js: -------------------------------------------------------------------------------- 1 | // Save & load a picture from localstorage 2 | 3 | const saveButton = document.querySelector(".save") 4 | , loadButton = document.querySelector(".load") 5 | , resetButton = document.querySelector(".reset") 6 | , undoButton = dQ(".undo") 7 | , deleteButton = dQ(".delete"); 8 | 9 | const resetPicture = () => { 10 | console.log("resetPicture()"); 11 | clearExistingPicture(); 12 | selectTool("creator"); 13 | frameLoop(); 14 | } 15 | 16 | const clearExistingPicture = () => { 17 | points = customFilter(points, (() => true)); 18 | grids = customFilter(grids, (() => true)); 19 | } 20 | 21 | const undo = () => { 22 | console.log("undo()"); 23 | 24 | if(pictureHistory.length > 0) { 25 | console.log("undo(): Undoing last change") 26 | clearExistingPicture(); 27 | let lastStep = pictureHistory[pictureHistory.length - 1]; 28 | loadPicture(JSON.parse(lastStep)); 29 | pictureHistory.pop(); 30 | } else { 31 | console.log("undo(): No undo states left"); 32 | } 33 | } 34 | 35 | const pushHistory = () => { 36 | console.log("pushHistory()"); 37 | 38 | let currentState = { grids : getPictureData() } 39 | pictureHistory.push(JSON.stringify(currentState)); 40 | if(pictureHistory.length > 20) { 41 | console.log("pushHistory(): More than 20 snapshots, nuking one."); 42 | pictureHistory.shift(); 43 | } 44 | } 45 | 46 | const getPictureData = () => { 47 | return grids.map(grid => { 48 | return { 49 | points : grid.points.map(p => { 50 | return { x: p.x, y: p.y}; 51 | }), 52 | fillColorIndex : grid.fillColorIndex 53 | } 54 | }); 55 | } 56 | 57 | const savePicture = () => { 58 | console.log("savePicture()"); 59 | let savedGrids = getPictureData(); 60 | window.localStorage.setItem("picture", JSON.stringify({ 61 | grids : savedGrids, 62 | colors : availableColors 63 | })); 64 | } 65 | 66 | const loadPicture = (picture) => { 67 | console.log("loadPicture()"); 68 | clearExistingPicture(); 69 | 70 | if(picture) { 71 | let pictureData = picture; 72 | let savedGrids = pictureData.grids; 73 | 74 | 75 | if(pictureData.colors) { 76 | availableColors = pictureData.colors; 77 | updateColors(); 78 | } 79 | 80 | grids = []; 81 | 82 | let newPoints = []; 83 | 84 | points = pictureData.grids.map(grid => { 85 | grid.points.map(p => { 86 | let thisPoint = { x : p.x, y: p.y}; 87 | let found = false; 88 | for(var i = 0; i < newPoints.length; i++) { 89 | if(comparePoints(thisPoint, newPoints[i])) { 90 | found = true; 91 | } 92 | } 93 | if(found == false) { 94 | newPoints.push(thisPoint); 95 | } 96 | }); 97 | }); 98 | 99 | points = newPoints.map(p => { 100 | return createPoint(p); 101 | }); 102 | 103 | savedGrids.map(grid => { 104 | let newArray = []; 105 | 106 | grid.points = grid.points.map(p => { 107 | for(var i = 0; i < points.length; i++) { 108 | let existingPoint = points[i]; 109 | if(comparePoints(p,existingPoint)) { 110 | return existingPoint; 111 | } 112 | } 113 | }); 114 | 115 | grids.push( 116 | createGrid(grid.points, { fillColorIndex : grid.fillColorIndex}) 117 | ); 118 | }); 119 | 120 | buildColorUI(); 121 | 122 | frameLoop(); 123 | return true; 124 | } else { 125 | return false; 126 | } 127 | } 128 | 129 | saveButton.addEventListener("click", () => { 130 | savePicture(); 131 | }); 132 | 133 | loadButton.addEventListener("click", () => { 134 | let picture = window.localStorage.getItem("picture"); 135 | loadPicture(JSON.parse(picture)); 136 | }); 137 | 138 | resetButton.addEventListener("click", () => { 139 | resetPicture(); 140 | }); 141 | 142 | undoButton.addEventListener("click", () => { 143 | undo(); 144 | }); 145 | 146 | deleteButton.addEventListener("click", () => { 147 | deleteSelected(); 148 | }); 149 | 150 | -------------------------------------------------------------------------------- /js/editor/savedpics.js: -------------------------------------------------------------------------------- 1 | let gemGrids = [{"points":[{"x":459,"y":297},{"x":558,"y":298},{"x":562,"y":403},{"x":457,"y":403}],"fillColorIndex":3},{"points":[{"x":507,"y":518},{"x":562,"y":403},{"x":457,"y":403}],"fillColorIndex":2},{"points":[{"x":683,"y":362},{"x":558,"y":298},{"x":562,"y":403}],"fillColorIndex":2},{"points":[{"x":507,"y":518},{"x":457,"y":403},{"x":380,"y":465}],"fillColorIndex":2},{"points":[{"x":640,"y":467},{"x":507,"y":518},{"x":562,"y":403}],"fillColorIndex":1},{"points":[{"x":640,"y":467},{"x":562,"y":403},{"x":683,"y":362}],"fillColorIndex":1},{"points":[{"x":683,"y":362},{"x":626,"y":235},{"x":558,"y":298}],"fillColorIndex":2},{"points":[{"x":341,"y":345},{"x":457,"y":403},{"x":459,"y":297}],"fillColorIndex":3},{"points":[{"x":380,"y":465},{"x":341,"y":345},{"x":457,"y":403}],"fillColorIndex":3},{"points":[{"x":511,"y":185},{"x":459,"y":297},{"x":558,"y":298}],"fillColorIndex":3},{"points":[{"x":626,"y":235},{"x":558,"y":298},{"x":511,"y":185}],"fillColorIndex":3},{"points":[{"x":391,"y":237},{"x":511,"y":185},{"x":459,"y":297}],"fillColorIndex":4},{"points":[{"x":341,"y":345},{"x":459,"y":297},{"x":391,"y":237}],"fillColorIndex":4},{"points":[{"x":592,"y":549},{"x":716,"y":494},{"x":640,"y":467},{"x":507,"y":518}],"fillColorIndex":5},{"points":[{"x":716,"y":494},{"x":756,"y":387},{"x":683,"y":362},{"x":640,"y":467}],"fillColorIndex":5},{"points":[{"x":721,"y":281},{"x":756,"y":387},{"x":683,"y":362},{"x":626,"y":235}],"fillColorIndex":5}]; 2 | let blankPic = {"grids":[{"points":[{"x":466,"y":272},{"x":563,"y":272},{"x":563,"y":369},{"x":466,"y":369}],"fillColorIndex":2}],"colors":["#8F3D61","#B94B5D","#DD7E5F","#EB9762","#EDBD77","#DDDDDD"]}; 3 | 4 | -------------------------------------------------------------------------------- /js/editor/scale.js: -------------------------------------------------------------------------------- 1 | const scalePoints = (scalar, shouldPushHistory) => { 2 | if(shouldPushHistory != false) { 3 | pushHistory(); 4 | } 5 | 6 | 7 | let selectedPoints = getSelectedPoints(); 8 | 9 | if(selectedPoints.length == 1) { 10 | deselectPoints(); 11 | } 12 | 13 | let midX = canvasWidth / 2; 14 | let midY = canvasHeight / 2; 15 | 16 | if(selectedPoints.length > 1) { 17 | let midPoint = getMidpoint(selectedPoints); 18 | midX = midPoint.x; 19 | midY = midPoint.y; 20 | } 21 | 22 | points = points.map(p => { 23 | if(selectedPoints.length < 2 ) { 24 | p.x = p.x + (p.x - midX) * scalar; 25 | p.y = p.y + (p.y - midY) * scalar; 26 | } else { 27 | if(p.selected || selectedPoints.indexOf(p) > -1) { 28 | p.x = p.x + (p.x - midX) * scalar; 29 | p.y = p.y + (p.y - midY) * scalar; 30 | } 31 | } 32 | return p; 33 | }); 34 | 35 | frameLoop(); 36 | } 37 | 38 | 39 | const rotatePoints = (angle, shouldPushHistory) => { 40 | if(shouldPushHistory != false) { 41 | pushHistory(); 42 | } 43 | 44 | let midX = canvasWidth / 2; 45 | let midY = canvasHeight / 2; 46 | 47 | let selectedPoints = getSelectedPoints(); 48 | 49 | if(selectedPoints.length === 1) { 50 | deselectPoints(); 51 | } 52 | 53 | if(selectedPoints.length > 1) { 54 | let midPoint = getMidpoint(selectedPoints); 55 | midX = midPoint.x; 56 | midY = midPoint.y; 57 | } 58 | 59 | points = points.map(p => { 60 | let cx = midX; 61 | let cy = midY; 62 | let x = p.x; 63 | let y = p.y; 64 | 65 | var radians = (Math.PI / 180) * angle, 66 | cos = Math.cos(radians), 67 | sin = Math.sin(radians), 68 | nx = (cos * (x - cx)) + (sin * (y - cy)) + cx, 69 | ny = (cos * (y - cy)) - (sin * (x - cx)) + cy; 70 | 71 | if(selectedPoints.length < 2) { 72 | p.x = nx; 73 | p.y = ny; 74 | } else { 75 | if(p.selected || selectedPoints.indexOf(p) > -1) { 76 | p.x = nx; 77 | p.y = ny; 78 | } 79 | } 80 | return p; 81 | }); 82 | frameLoop(); 83 | } 84 | 85 | 86 | const getSelectedPoints = () => { 87 | let selectedPoints = points.filter(p => p.selected); 88 | 89 | // Try loading girds 90 | if(selectedPoints.length === 0) { 91 | grids.map(grid => { 92 | if(grid.selected) { 93 | grid.points.map(p => { 94 | selectedPoints.push(p); 95 | }); 96 | } 97 | }) 98 | } 99 | return selectedPoints; 100 | } -------------------------------------------------------------------------------- /js/editor/settings.js: -------------------------------------------------------------------------------- 1 | let dragZoneFillStyle = "rgba(255,0,0,.15)"; 2 | let hoverStrokeStyle = "rgba(0,0,0,1)"; 3 | 4 | let hoverRadius = 14; // Size of vertex selection radius 5 | let mergeDistance = 14; // Distance before we auto merge points 6 | let lineHoverDistance = 12; 7 | 8 | // Vertex 9 | let hoveredVertexFillStyle = "rgba(255,255,255,.1)"; 10 | 11 | let selectedVertexFillStyle = "rgba(255,255,255,.2)"; 12 | let vertexSize = 4; 13 | let vertexFillStyle = "rgba(0,0,0,0)"; 14 | 15 | // Shape 16 | let shapeFillLineCount = 15; 17 | 18 | let shapeOutlineLineWidth = 1; 19 | let shapeOutlineColor = "rgba(255,255,255,.2)"; 20 | shapeOutlineColor = "rgba(0,0,0,.1)"; 21 | 22 | let settings = { 23 | extrudeMode : "point" 24 | }; 25 | -------------------------------------------------------------------------------- /js/editor/tool-create.js: -------------------------------------------------------------------------------- 1 | const toolCreate = { 2 | start : { 3 | x : 0, 4 | y : 0 5 | }, 6 | activeGrid : false, 7 | distanceTraveled : 0, 8 | shapeStarted : false, 9 | size : 40, 10 | 11 | mouseDown : function(e) { 12 | this.start.x = e.clientX; 13 | this.start.y = e.clientY; 14 | 15 | let newPoints = this.getGridPoints(); 16 | 17 | newPoints = newPoints.map(p => { 18 | let newPoint = createPoint(p); 19 | points.push(newPoint); 20 | return newPoint; 21 | }); 22 | 23 | this.activeGrid = createGrid(newPoints, { fillColorIndex : selectedColorIndex}); 24 | grids.push(this.activeGrid); 25 | }, 26 | 27 | getGridPoints : function(e) { 28 | 29 | let points = []; 30 | for(var i = 0; i < 4; i++) { 31 | points.push( 32 | { 33 | x : this.start.x, 34 | y : this.start.y, 35 | }); 36 | } 37 | return points; 38 | }, 39 | 40 | mouseMove : function(e) { 41 | let current = { 42 | x: e.clientX, 43 | y: e.clientY 44 | } 45 | 46 | this.activeGrid.points[1].x = e.clientX; 47 | 48 | this.activeGrid.points[2].x = e.clientX; 49 | this.activeGrid.points[2].y = e.clientY; 50 | 51 | this.activeGrid.points[3].y = e.clientY; 52 | 53 | this.distanceTraveled = distPoints(this.start, current); 54 | }, 55 | 56 | mouseUp : function(e) { 57 | if(this.distanceTraveled < 50) { 58 | 59 | let vals = [ 60 | { 61 | x : -this.size, 62 | y : -this.size 63 | },{ 64 | x : this.size, 65 | y : -this.size 66 | },{ 67 | x : this.size, 68 | y : this.size 69 | },{ 70 | x : -this.size, 71 | y : this.size 72 | } 73 | ] 74 | 75 | let index = 0; 76 | if(this.activeGrid) { 77 | this.activeGrid.points.map(p => { 78 | p.x = this.start.x + vals[index].x; 79 | p.y = this.start.y + vals[index].y; 80 | index++; 81 | return p; 82 | }); 83 | } 84 | } 85 | 86 | this.activeGrid = false; 87 | selectTool("selector"); 88 | } 89 | 90 | } -------------------------------------------------------------------------------- /js/editor/tools.js: -------------------------------------------------------------------------------- 1 | let tools = [ 2 | { 3 | name : "creator", 4 | description: "Create a shape" 5 | },{ 6 | name : "selector", 7 | description: "Select & Extrude" 8 | },{ 9 | name : "paintbrush", 10 | description: "Fill things with color" 11 | },{ 12 | name : "move", 13 | description: "Move Canvas" 14 | }]; 15 | 16 | let selectedTool; 17 | 18 | tools = tools.map(tool => { 19 | tool.selected = false; 20 | return tool; 21 | }); 22 | 23 | let toolbarEl = dQ(".tools") 24 | 25 | tools.map(tool => { 26 | 27 | let toolEl = document.createElement("div"); 28 | toolEl.classList.add("tool"); 29 | toolEl.classList.add(tool.name); 30 | toolEl.style.background = tool.name; 31 | toolEl.setAttribute("name", tool.name); 32 | 33 | let toolLabelEl = document.createElement("div"); 34 | toolLabelEl.classList.add("label"); 35 | toolLabelEl.innerText = tool.description; 36 | toolEl.appendChild(toolLabelEl); 37 | 38 | toolbarEl.appendChild(toolEl); 39 | toolEl.addEventListener("click", function(el){ 40 | selectTool(el.target.getAttribute("name")); 41 | }); 42 | }); 43 | 44 | const selectTool = toolName => { 45 | document.querySelectorAll(".tools .tool").forEach(el => { 46 | el.classList.remove("selected"); 47 | if(toolName === el.getAttribute("name")) { 48 | el.classList.add("selected"); 49 | if(selectedTool === "selector" && toolName === "selector") { 50 | toggleExtrudeMode(); 51 | } 52 | if(selectedTool != toolName) { 53 | deselectGrids(); 54 | deselectPoints(); 55 | } 56 | selectedTool = toolName; 57 | svgScene.setAttribute("tool", toolName); 58 | } 59 | }); 60 | 61 | } 62 | 63 | selectTool("selector"); -------------------------------------------------------------------------------- /js/editor/utils.js: -------------------------------------------------------------------------------- 1 | // Cool utility functions 2 | 3 | // Distance from a point to a line segment 4 | // p = point {x,y} 5 | // v, w = start and and points {x,y}, {x,y} 6 | function distToSegment(p, v, w) { return Math.sqrt(distToSegmentSquared(p, v, w)); } 7 | function distToSegmentSquared(p, v, w) { 8 | var l2 = dist2(v, w); 9 | if (l2 == 0) return dist2(p, v); 10 | var t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2; 11 | t = Math.max(0, Math.min(1, t)); 12 | return dist2(p, { x: v.x + t * (w.x - v.x), 13 | y: v.y + t * (w.y - v.y) }); 14 | } 15 | function sqr(x) { return x * x } 16 | function dist2(v, w) { return sqr(v.x - w.x) + sqr(v.y - w.y) } 17 | 18 | function distPoints(v, w) { 19 | let deltaX = w.x - v.x; 20 | let deltaY = w.y - v.y; 21 | return Math.sqrt(Math.pow(deltaX,2 ) + Math.pow(deltaY ,2)); 22 | 23 | } 24 | 25 | 26 | function getRandom(min, max){ 27 | return min + Math.random() * (max-min); 28 | } 29 | 30 | // Check if a point is within a polygon 31 | // * point = [x,y] 32 | // * polygon = [[x,y], [x,y], [x,y]] 33 | function testWithin(point, vs) { 34 | 35 | var x = point[0], y = point[1]; 36 | 37 | var inside = false; 38 | for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) { 39 | var xi = vs[i][0], yi = vs[i][1]; 40 | var xj = vs[j][0], yj = vs[j][1]; 41 | 42 | var intersect = ((yi > y) != (yj > y)) 43 | && (x < (xj - xi) * (y - yi) / (yj - yi) + xi); 44 | if (intersect) inside = !inside; 45 | } 46 | 47 | return inside; 48 | }; 49 | 50 | function getRandom(min, max){ 51 | return min + Math.random() * (max-min); 52 | } 53 | 54 | 55 | const comparePoints = (point, otherPoint) => { 56 | return point.x == otherPoint.x && point.y == otherPoint.y; 57 | } 58 | 59 | const dQ = (selector) => { 60 | return document.querySelector(selector); 61 | } 62 | 63 | 64 | 65 | const getMidpoint = (points) => { 66 | 67 | let bounds = points.reduce((bounds, point) => { 68 | if(point.x < bounds.minX) { 69 | bounds.minX = point.x; 70 | } 71 | if(point.x > bounds.maxX) { 72 | bounds.maxX = point.x; 73 | } 74 | if(point.y > bounds.maxY) { 75 | bounds.maxY = point.y; 76 | } 77 | if(point.y < bounds.minY) { 78 | bounds.minY = point.y; 79 | } 80 | return bounds; 81 | }, { 82 | minX : points[0].x, 83 | maxX : points[0].x, 84 | minY : points[0].y, 85 | maxY : points[0].y 86 | }); 87 | 88 | return { 89 | x : bounds.minX + (bounds.maxX - bounds.minX) / 2, 90 | y : bounds.minY + (bounds.maxY - bounds.minY) / 2 91 | } 92 | 93 | } 94 | 95 | function shadeColor2(color, percent) { 96 | var f=parseInt(color.slice(1),16),t=percent<0?0:255,p=percent<0?percent*-1:percent,R=f>>16,G=f>>8&0x00FF,B=f&0x0000FF; 97 | return "#"+(0x1000000+(Math.round((t-R)*p)+R)*0x10000+(Math.round((t-G)*p)+G)*0x100+(Math.round((t-B)*p)+B)).toString(16).slice(1); 98 | } 99 | 100 | // Removes a point or grid from an array, including its 101 | // * uiEl 102 | // * svgEl 103 | const customFilter = (items, conditional) => { 104 | return items.filter(item => { 105 | let killItem = conditional(item); 106 | if(killItem == true) { 107 | if(item.svgEl) { 108 | item.svgEl.remove(); 109 | } 110 | if(item.uiEl) { 111 | item.uiEl.remove(); 112 | } 113 | } 114 | return !killItem; 115 | }); 116 | } 117 | 118 | const makeSvg = (type = "polygon", options = {}, appendEl) => { 119 | let svgEl = document.createElementNS("http://www.w3.org/2000/svg", type); 120 | Object.keys(options).forEach(key => { 121 | svgEl.setAttribute(key, options[key]); 122 | }); 123 | document.querySelector(appendEl).appendChild(svgEl); 124 | return svgEl; 125 | } 126 | 127 | const createGrid = (points, options) => { 128 | // console.log("cg",points); 129 | let newGrid = new Grid(points); 130 | Object.keys(options).forEach(key => newGrid[key] = options[key]); 131 | return newGrid; 132 | } 133 | 134 | const highestZIndexItem = items => { 135 | let sortedItems = items.sort((a,b) => { 136 | return a.zIndex < b.zIndex ? 1 : -1; 137 | }); 138 | return sortedItems[0]; 139 | } 140 | 141 | 142 | const deselectGrids = () => { 143 | console.log("deselectGrids()"); 144 | grids = grids.map(grid => { 145 | grid.selected = false; 146 | return grid; 147 | }); 148 | } 149 | 150 | const deselectPoints = () => { 151 | points = points.map(point => { 152 | point.selected = false; 153 | point.hovered = false; 154 | point.stickyHovered = false; 155 | return point; 156 | }); 157 | } 158 | 159 | 160 | const roundPoints = () => { 161 | points = points.map(p => { 162 | p.x = Math.round(p.x); 163 | p.y = Math.round(p.y); 164 | return p; 165 | }) 166 | } 167 | 168 | const createPoint = p => { 169 | 170 | let group = document.createElementNS("http://www.w3.org/2000/svg","svg"); 171 | group.setAttribute("x", p.x); 172 | group.setAttribute("y", p.y); 173 | 174 | let circle = document.createElementNS("http://www.w3.org/2000/svg","circle"); 175 | circle.setAttribute("cx", 0); 176 | circle.classList.add("bigcircle"); 177 | circle.setAttribute("cy", 0); 178 | circle.setAttribute("r", 14); 179 | circle.setAttribute("stroke", "transparent"); 180 | circle.setAttribute("stroke-width", 2); 181 | circle.setAttribute("fill", "transparent"); 182 | 183 | let smallCircle = document.createElementNS("http://www.w3.org/2000/svg","circle"); 184 | smallCircle.classList.add("smallcircle"); 185 | smallCircle.setAttribute("cx", 0); 186 | smallCircle.setAttribute("cy", 0); 187 | smallCircle.setAttribute("r", 3); 188 | smallCircle.setAttribute("fill", "transparent"); 189 | 190 | group.append(circle); 191 | group.append(smallCircle); 192 | 193 | svgPoints.appendChild(group); 194 | 195 | return { 196 | x : p.x, 197 | y : p.y, 198 | svgEl : group 199 | } 200 | } 201 | 202 | 203 | const mergeSamePoints = () => { 204 | points = points.map(p => { 205 | points.map(otherP => { 206 | if(p != otherP) { 207 | let distance = Math.sqrt(Math.pow(p.x - otherP.x, 2) + Math.pow(p.y - otherP.y, 2)); 208 | if(distance <= mergeDistance) { 209 | p.x = otherP.x; 210 | p.y = otherP.y; 211 | } 212 | } 213 | }) 214 | return p; 215 | }); 216 | } 217 | 218 | 219 | 220 | // Check if there are any overlapping points... 221 | const consolidatePoints = () => { 222 | 223 | let x, y; 224 | let haveNewPoint = false; 225 | 226 | let samePoints = points.filter(thisPoint => { 227 | for(var i = 0; i < points.length; i++) { 228 | let otherPoint = points[i]; 229 | if(otherPoint != thisPoint) { 230 | if(otherPoint.x == thisPoint.x && otherPoint.y == thisPoint.y) { 231 | x = thisPoint.x; 232 | y = thisPoint.y; 233 | haveNewPoint = true; 234 | return thisPoint; 235 | } 236 | } 237 | } 238 | }); 239 | 240 | if(haveNewPoint == false) { 241 | return; 242 | } 243 | 244 | // replace with new reference... 245 | let newPoint = { x: x, y: y}; 246 | newPoint = createPoint(newPoint); 247 | newPoint.new = true; 248 | let alreadyReturned; 249 | 250 | // this does NOT update the 'grids value' 251 | // might as well do it the mapped way... 252 | 253 | points = points.filter(p => { 254 | if(p.x == newPoint.x && p.y == newPoint.y) { 255 | p.svgEl.remove(); 256 | return false; 257 | } else { 258 | return true; 259 | } 260 | }); 261 | 262 | grids = grids.map(grid => { 263 | grid.points = grid.points.map(p => { 264 | if(p.x == newPoint.x && p.y == newPoint.y) { 265 | return newPoint; 266 | } else { 267 | return p; 268 | } 269 | }); 270 | 271 | return grid; 272 | }) 273 | 274 | points.push(newPoint); 275 | } 276 | 277 | const killGhosts = () => { 278 | if(clonedGrid.grid) { 279 | clonedGrid.grid.click(); 280 | } 281 | clonedGrid.grid = false; 282 | grids = customFilter(grids, (g => g.mode === "ghost" || g.mode === "invisible")); 283 | 284 | } 285 | 286 | 287 | // Get rid of shapes with 2 or fewer points 288 | // If a shape has two points that are the same..., consolidate those too? 289 | const cleanupGrids = () => { 290 | 291 | // Get rid of shapes in a grid that don't exist in the points array 292 | grids = grids.map(grid => { 293 | grid.points = customFilter(grid.points, (p => points.indexOf(p) === -1)); 294 | return grid; 295 | }); 296 | 297 | // Filter out duplicate points from grids 298 | grids = grids.map(grid => { 299 | 300 | let dupeIndexes = []; 301 | 302 | for(var i = 0; i < grid.points.length; i++) { 303 | let thisPoint = grid.points[i]; 304 | for(var j = 0; j < grid.points.length; j++) { 305 | let otherPoint = grid.points[j]; 306 | if(i != j && thisPoint == otherPoint) { 307 | if(dupeIndexes.indexOf(i) < 0 && dupeIndexes.indexOf(j) < 0) { 308 | dupeIndexes.push(i); 309 | } 310 | } 311 | } 312 | } 313 | 314 | let mapIndex = -1; 315 | grid.points = grid.points.filter(p => { 316 | mapIndex++; 317 | if(dupeIndexes.indexOf(mapIndex) > -1) { 318 | return false; 319 | } else { 320 | return true; 321 | } 322 | }) 323 | return grid; 324 | }) 325 | 326 | // Get rid of shapes that have fewer than 3 pints 327 | grids = customFilter(grids, (grid => grid.points.length < 3)); 328 | } 329 | 330 | // Filter out points that aren't associated with any shapes 331 | const cleanupPoints = () => { 332 | points = points.filter(p => { 333 | let contained = false; 334 | for(var i = 0; i < grids.length; i++) { 335 | let gridPoints = grids[i].points; 336 | if(gridPoints.includes(p)) { 337 | contained = true; 338 | } 339 | } 340 | if(contained == false) { 341 | p.svgEl.remove(); 342 | } 343 | return contained; 344 | }); 345 | } 346 | 347 | const deleteSelected = () => { 348 | let anythingSelected = false; 349 | 350 | points.map(p => { 351 | if(p.selected) { 352 | anythingSelected = true; 353 | } 354 | }); 355 | 356 | grids.map(g => { 357 | if(g.selected) { 358 | anythingSelected = true; 359 | } 360 | }) 361 | 362 | if(anythingSelected) { 363 | pushHistory(); 364 | } 365 | 366 | points = customFilter(points, (p => p.selected)); 367 | deleteSelectedGrids(); 368 | } 369 | -------------------------------------------------------------------------------- /js/editor/vars.js: -------------------------------------------------------------------------------- 1 | // Element references 2 | const bodyEl = document.querySelector("body") 3 | , svgScene = document.querySelector(".svg-canvas") 4 | , svgImage = document.querySelector(".svg-image") 5 | , svgPoints = document.querySelector(".svg-points"); 6 | 7 | let points = []; 8 | let grids = []; 9 | let selectedGrids = []; 10 | let pictureHistory = []; 11 | 12 | let snapshotTaken; 13 | 14 | let mouse = { 15 | x : 0, 16 | y: 0, 17 | 18 | pressed : false, 19 | 20 | // For keeping track of clicks anywhere 21 | // like when you are using the scale slider, so points dont merge 22 | pressedAnywhere : false, 23 | 24 | dragging : false, 25 | shiftPressed : false, 26 | 27 | dragZone : { 28 | start : { 29 | x : 0, 30 | y : 0, 31 | }, 32 | end : { 33 | x : 0, 34 | y : 0 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /js/editor/zindex.js: -------------------------------------------------------------------------------- 1 | // Increases or decreases the z-index of shapes 2 | 3 | const zUpButton = dQ(".z-up") 4 | , zDownButton = dQ(".z-down"); 5 | 6 | zUpButton.addEventListener("click", () => { 7 | changeZindex("up"); 8 | }); 9 | 10 | zDownButton.addEventListener("click", () => { 11 | changeZindex("down"); 12 | }); 13 | 14 | const changeZindex = (direction) => { 15 | let selectedGrids = grids.filter(grid => grid.selected); 16 | 17 | if(selectedGrids.length > 0) { 18 | 19 | if(mouse.shiftPressed == true ) { 20 | 21 | selectedGrids.map(g => { 22 | let thisEl = g.svgEl; 23 | 24 | let nextElement = direction == "down" ? dQ(".svg-image polygon:first-child") : dQ(".svg-image polygon:last-child"); 25 | let type = direction == "down" ? "beforebegin" : "afterend"; 26 | 27 | if(nextElement) { 28 | pushHistory(); 29 | let nextGrid = findGridByElement(nextElement); 30 | nextElement.insertAdjacentElement(type, thisEl); 31 | } 32 | }); 33 | 34 | } else { 35 | 36 | selectedGrids.map(g => { 37 | let thisEl = g.svgEl; 38 | console.log(g.zIndex); 39 | let nextZindex = direction == "up" ? g.zIndex + 1 : g.zIndex - 1; 40 | console.log(nextZindex); 41 | let nextElement = dQ(".svg-image polygon:nth-child("+ nextZindex+")"); 42 | 43 | if(nextElement) { 44 | pushHistory(); 45 | let nextGrid = findGridByElement(nextElement); 46 | let type = direction == "up" ? "afterend" : "beforebegin"; 47 | nextElement.insertAdjacentElement(type, thisEl); 48 | } 49 | }); 50 | } 51 | } 52 | 53 | let zIndex = 1; 54 | document.querySelectorAll(".svg-image polygon").forEach(el => { 55 | let thisGrid = findGridByElement(el); 56 | thisGrid.zIndex = zIndex; 57 | zIndex++; 58 | }); 59 | 60 | // Sort the grids array by zIndex so when we save the pictur the order persists 61 | grids = grids.sort((a,b) => { 62 | if (a.zIndex > b.zIndex) { 63 | return 1; 64 | } else { 65 | return -1; 66 | } 67 | }); 68 | } 69 | 70 | const findGridByElement = svgEl => { 71 | return grids.find(g => { 72 | return g.svgEl == svgEl; 73 | }); 74 | } 75 | -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flukeout/PolyPal/fcc384abdde199521b174543048f7ec854d2f32e/todo.md --------------------------------------------------------------------------------