├── .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 |
--------------------------------------------------------------------------------
/images/icon-creator-selected.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/images/icon-creator.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/images/icon-delete.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/images/icon-move-selected.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/images/icon-move.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/images/icon-paintbrush-selected.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/images/icon-paintbrush.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/images/icon-pick.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/images/icon-selector-selected.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/images/icon-selector.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/images/polypal-logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
19 |
20 |
21 |
22 |
23 | PolyPal Editor
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
--------------------------------------------------------------------------------