diff --git a/desktop/sources/scripts/tool.js b/desktop/sources/scripts/tool.js index 0c168b9..8f779aa 100644 --- a/desktop/sources/scripts/tool.js +++ b/desktop/sources/scripts/tool.js @@ -23,7 +23,6 @@ function Tool() this.layers = [[],[],[]]; this.vertices = []; this.index = 0; - dotgrid.set_size({width:300,height:300}) } this.clear = function() diff --git a/index.html b/index.html new file mode 100644 index 0000000..b413db1 --- /dev/null +++ b/index.html @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + Dotgrid + + +
+ +
+ + diff --git a/web/links/fonts.css b/web/links/fonts.css new file mode 100644 index 0000000..be688de --- /dev/null +++ b/web/links/fonts.css @@ -0,0 +1,15 @@ +/* Input */ + +@font-face { + font-family: 'input_mono_regular'; + src: url('../media/fonts/input_mono_regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'input_mono_medium'; + src: url('../media/fonts/input_mono_medium.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} \ No newline at end of file diff --git a/web/links/main.css b/web/links/main.css new file mode 100644 index 0000000..e7f9f3d --- /dev/null +++ b/web/links/main.css @@ -0,0 +1,42 @@ +body { padding: 5px; font-family: 'input_mono_regular'; -webkit-user-select: none; overflow: hidden; padding-left:5px; transition: background 500ms} + +/* Core */ + +#app { display: flex; flex-direction: column; align-items: center; -webkit-app-region: drag; padding-top:30px;} +#guide { position: absolute;width: 300px;height: 300px; transition: opacity 150ms; margin-left:50%;} +#render { display: none } +#vector { z-index: 1000;position: relative;width:300px; height:300px; } + +/* Interface */ + +#interface { font-size: 11px;line-height: 30px;text-transform: uppercase;-webkit-app-region: no-drag; transition: all 150ms; width: 315px; position:fixed; bottom:20px; left:calc(50vw - 155px); height:30px;} +#interface svg.inactive { opacity: 0.2 } +#interface svg path.inactive { opacity: 0.2 } +#interface svg:hover { opacity: 0.5 } +#interface svg.icon:last-child { margin-right: 0; } +#interface svg path { fill:none; stroke-linecap: round; stroke-linejoin: round; stroke-width:12px; } +#interface.hidden { bottom:10px;opacity: 0 } +#interface.visible { bottom:20px; opacity: 1 } +#interface #menu { opacity: 1; position: absolute; top:0px; transition: all 250ms; z-index: 900} +#interface #picker { background:red; position: absolute; line-height: 30px; z-index: 0; width:250px; top:5px; opacity: 0; transition: all 250ms;} +#interface.picker #menu { opacity: 0; top:-5px; z-index: 0 } +#interface.picker #picker { opacity: 1; top:0px; z-index: 900 } +#interface .icon { width:30px; height:30px; margin-right:-2px; opacity: 1} +#interface .icon:hover { cursor: pointer; opacity: 1 } + +/* Theme Overrides */ + +:root { --background: "#222"; --f_high: "#fff";--f_med: "#777";--f_low: "#444";--f_inv: "#000";--b_high: "#000";--b_med: "#affec7";--b_low: "#000";--b_inv: "#affec7"; } + +body { background:var(--background) !important; } +#picker { background:var(--background) !important; color:var(--f_high) !important; } +.fh { color:var(--f_high) !important; stroke:var(--f_high) !important; } +.fm { color:var(--f_med) !important ; stroke:var(--f_med) !important; } +.fl { color:var(--f_low) !important ; stroke:var(--f_low) !important; } +.f_inv { color:var(--f_inv) !important ; stroke:var(--f_inv) !important; } +.f_inv { color:var(--f_inv) !important ; stroke:var(--f_inv) !important; } +.bh { background:var(--b_high) !important; } +.bm { background:var(--b_med) !important ; } +.bl { background:var(--b_low) !important ; } +.b_inv { background:var(--b_inv) !important ; ; } +.icon { color:var(--f_high) !important; stroke:var(--f_high) !important; } \ No newline at end of file diff --git a/web/links/reset.css b/web/links/reset.css new file mode 100644 index 0000000..9f57584 --- /dev/null +++ b/web/links/reset.css @@ -0,0 +1 @@ +* { margin:0;padding:0;border:0;outline:0;text-decoration:none;font-weight:inherit;font-style:inherit;color:inherit;font-size:100%;font-family:inherit;vertical-align:baseline;list-style:none;border-collapse:collapse;border-spacing:0; -webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale;} \ No newline at end of file diff --git a/web/media/fonts/input_mono_medium.ttf b/web/media/fonts/input_mono_medium.ttf new file mode 100644 index 0000000..0d488bf Binary files /dev/null and b/web/media/fonts/input_mono_medium.ttf differ diff --git a/web/media/fonts/input_mono_regular.ttf b/web/media/fonts/input_mono_regular.ttf new file mode 100644 index 0000000..c19c287 Binary files /dev/null and b/web/media/fonts/input_mono_regular.ttf differ diff --git a/web/scripts/dotgrid.js b/web/scripts/dotgrid.js new file mode 100644 index 0000000..9cc0a8c --- /dev/null +++ b/web/scripts/dotgrid.js @@ -0,0 +1,410 @@ +function Dotgrid(width,height,grid_x,grid_y,block_x,block_y) +{ + this.theme = new Theme(); + this.interface = new Interface(); + this.history = new History(); + this.guide = new Guide(); + this.renderer = new Renderer(); + this.tool = new Tool(); + this.picker = new Picker(); + + this.grid_x = grid_x; + this.grid_y = grid_y; + this.block_x = block_x; + this.block_y = block_y; + + this.cursor = { pos:{x:0,y:0},translation:null,multi:false,updated:0 } + + this.install = function() + { + document.getElementById("app").appendChild(this.guide.el); + + this.theme.start(); + this.tool.start(); + this.guide.start(); + this.interface.start(); + + document.addEventListener('mousedown', function(e){ dotgrid.mouse_down(e); }, false); + document.addEventListener('mousemove', function(e){ dotgrid.mouse_move(e); }, false); + document.addEventListener('contextmenu', function(e){ dotgrid.mouse_alt(e); }, false); + document.addEventListener('mouseup', function(e){ dotgrid.mouse_up(e);}, false); + document.addEventListener('copy', function(e){ dotgrid.copy(e); e.preventDefault(); }, false); + document.addEventListener('cut', function(e){ dotgrid.cut(e); e.preventDefault(); }, false); + document.addEventListener('paste', function(e){ dotgrid.paste(e); e.preventDefault(); }, false); + window.addEventListener('drop', dotgrid.drag); + + this.new(); + } + + // File + + this.new = function() + { + this.history.push(this.tool.layers); + this.clear(); + } + + this.open = function() + { + var paths = dialog.showOpenDialog({properties: ['openFile'],filters:[{name:"Dotgrid Image",extensions:["dot","grid"]}]}); + + if(!paths){ console.log("Nothing to load"); return; } + + fs.readFile(paths[0], 'utf-8', (err, data) => { + if(err){ alert("An error ocurred reading the file :" + err.message); return; } + this.tool.replace(JSON.parse(data.toString().trim())); + this.guide.refresh(); + }); + } + + this.save = function(content = this.tool.export()) + { + dialog.showSaveDialog({ + title:"Save to .grid", + filters: [{name: "Dotgrid", extensions: ["grid", "dot"]}] + },(fileName) => { + if (fileName === undefined){ return; } + fileName = fileName.substr(-5,5) != ".grid" ? fileName+".grid" : fileName; + fs.writeFileSync(fileName, content); + this.guide.refresh() + }); + } + + this.refresh = function() + { + this.set_size({width:window.innerWidth - 120,height:window.innerHeight- 120}) + } + + this.render = function(content = this.renderer.to_png({width:dotgrid.tool.settings.size.width*2,height:dotgrid.tool.settings.size.height*2}), ready = null, size = null) + { + if(!ready){return; } + + dialog.showSaveDialog({title:"Render to .png"},(fileName) => { + if (fileName === undefined){ return; } + fileName = fileName.substr(-4,4) != ".png" ? fileName+".png" : fileName; + console.log(`Rendered ${size.width}x${size.height}`) + fs.writeFileSync(fileName, ready); + }); + } + + this.export = function(content = this.renderer.to_svg()) + { + dialog.showSaveDialog({title:"Export to .svg"},(fileName) => { + if (fileName === undefined){ return; } + fileName = fileName.substr(-4,4) != ".svg" ? fileName+".svg" : fileName; + fs.writeFileSync(fileName, content); + this.guide.refresh() + }); + } + + this.bundle = {} + + this.build = function() + { + this.bundle = {} + + var sizes = [ + {width:16,height:16}, + {width:32,height:32}, + {width:52,height:52}, + {width:64,height:64}, + {width:72,height:72}, + {width:96,height:96}, + {width:128,height:128}, + {width:256,height:256}, + {width:512,height:512} + ] + + for(id in sizes){ + this.renderer.to_png(sizes[id],dotgrid.package) + } + } + + this.package = function(n = null, ready,size) + { + dotgrid.bundle[`${size.width}x${size.height}`] = ready + + console.log(`Rendered ${size.width}x${size.height}`,`${Object.keys(dotgrid.bundle).length}/9`) + + if(Object.keys(dotgrid.bundle).length == 9){ + dialog.showSaveDialog({title:"Export to Icons"},(fileName) => { + if (fileName === undefined){ return; } + for(id in dotgrid.bundle){ + fs.writeFileSync(`${fileName}.${id}.png`, dotgrid.bundle[id]); + } + }); + } + } + + // Cursor + + this.mouse_down = function(e) + { + var o = e.target.getAttribute("ar"); + + if(o){ + if(o == "line"){ this.tool.cast("line"); return; } + if(o == "arc_c"){ this.tool.cast("arc_c"); return;} + if(o == "arc_r"){ this.tool.cast("arc_r"); return; } + if(o == "bezier"){ this.tool.cast("bezier"); return; } + if(o == "close"){ this.tool.cast("close"); return; } + + if(o == "thickness"){ this.mod_thickness(10,true,true); return; } + if(o == "linecap"){ this.mod_linecap(); return; } + if(o == "linejoin"){ this.mod_linejoin(); return; } + if(o == "mirror"){ this.tool.toggle_mirror(); return; } + if(o == "fill"){ this.mod_fill(); return; } + if(o == "color"){ setTimeout(()=>{ this.picker.start(); }, 100); return; } + if(o == "depth"){ this.tool.select_next_layer(); return; } + + e.preventDefault(); + } + + var pos = this.position_in_grid({x:e.clientX+5,y:e.clientY-5}); + pos = this.position_on_grid(pos); + + if(e.altKey){ dotgrid.tool.remove_segments_at(pos); return; } + + if(dotgrid.tool.vertex_at(pos)){ + console.log("Begin translation"); dotgrid.cursor.translation = {from:pos,to:pos}; + if(e.shiftKey){ console.log("Begin translation(multi)"); dotgrid.cursor.multi = true; } + } + + dotgrid.guide.refresh(); + dotgrid.interface.refresh(); + } + + this.mouse_move = function(e) + { + var pos = this.position_in_grid({x:e.clientX+5,y:e.clientY-5}); pos = this.position_on_grid(pos); + + this.cursor.pos = pos; + this.cursor.updated = new Date().getTime(); + this.cursor.operation = e.target.getAttribute("ar"); + + if(dotgrid.cursor.translation && (Math.abs(dotgrid.cursor.translation.from.x) != Math.abs(pos.x) || Math.abs(dotgrid.cursor.translation.from.y) != Math.abs(pos.y))){ dotgrid.cursor.translation.to = pos; } + + dotgrid.guide.refresh(); + dotgrid.interface.refresh(); + e.preventDefault(); + } + + this.mouse_up = function(e) + { + if(e.target.getAttribute("ar")){ return } // If clicking on interface + + var pos = this.position_in_grid({x:e.clientX+5,y:e.clientY-5}); + pos = this.position_on_grid(pos); + + if(e.altKey || e.target.id != "guide"){ return; } + + if(pos.x > 0) { dotgrid.cursor.translation = null; return; } + + if(dotgrid.cursor.translation && (Math.abs(dotgrid.cursor.translation.from.x) != Math.abs(dotgrid.cursor.translation.to.x) || Math.abs(dotgrid.cursor.translation.from.y) != Math.abs(dotgrid.cursor.translation.to.y))){ + if(dotgrid.cursor.multi){ + dotgrid.tool.translate_multi(dotgrid.cursor.translation.from,dotgrid.cursor.translation.to); + } + else{ + dotgrid.tool.translate(dotgrid.cursor.translation.from,dotgrid.cursor.translation.to); + } + dotgrid.cursor.translation = null; + dotgrid.cursor.multi = null; + dotgrid.guide.refresh(); + e.preventDefault(); + return; + } + + this.tool.add_vertex({x:pos.x * -1,y:pos.y}); + dotgrid.cursor.translation = null; + + dotgrid.interface.refresh(); + dotgrid.guide.refresh(); + + e.preventDefault(); + } + + this.mouse_alt = function(e) + { + var pos = this.position_in_grid({x:e.clientX+5,y:e.clientY-5}); pos = this.position_on_grid(pos); + dotgrid.tool.remove_segments_at(pos); + e.preventDefault(); + + setTimeout(() => { dotgrid.tool.clear(); },150); + } + + // Toggles + + this.mod_thickness = function(mod = 10,step = false,cap = false) + { + if(cap){ + this.tool.style().thickness = this.tool.style().thickness > 40 ? 1 : this.tool.style().thickness + } + if(step){ + this.tool.style().thickness = parseInt(this.tool.style().thickness/5) * 5; + } + + this.tool.style().thickness = clamp(this.tool.style().thickness+mod,1,40); + dotgrid.guide.refresh(); + } + + this.mod_linecap_index = 1; + + this.mod_linecap = function(mod) + { + var a = ["butt","square","round"]; + this.mod_linecap_index += 1; + this.tool.style().strokeLinecap = a[this.mod_linecap_index % a.length]; + dotgrid.guide.refresh(); + } + + this.mod_linejoin_index = 1; + + this.mod_linejoin = function(mod) + { + var a = ["miter","round","bevel"]; + this.mod_linejoin_index += 1; + this.tool.style().strokeLinejoin = a[this.mod_linejoin_index % a.length]; + dotgrid.guide.refresh(); + } + + this.mod_fill = function() + { + this.tool.style().fill = this.tool.style().fill == "none" ? this.tool.style().color : "none"; + dotgrid.guide.refresh(); + } + + // Basics + + this.set_size = function(size = {width:300,height:300},interface = true,scale = 1) + { + size = { width:clamp(parseInt(size.width/15)*15,120,1000),height:clamp(parseInt(size.height/15)*15,120,1000)} + + if(this.tool.settings.size.width == size.width && this.tool.settings.size.height == size.height){ return; } + + console.log(`Setting size: ${size.width}x${size.height}`) + + this.tool.settings.size.width = size.width + this.tool.settings.size.height = size.height + + this.grid_x = size.width/15 + this.grid_y = size.height/15 + + this.grid_width = this.tool.settings.size.width/this.grid_x; + this.grid_height = this.tool.settings.size.height/this.grid_y; + + dotgrid.guide.resize(size); + + this.interface.refresh(); + dotgrid.guide.refresh(); + } + + // Draw + + this.reset = function() + { + this.tool.clear(); + } + + this.clear = function() + { + this.refresh(); + this.history.clear(); + this.tool.reset(); + this.reset(); + dotgrid.guide.refresh(); + dotgrid.interface.refresh(true); + } + + this.drag = function(e) + { + e.preventDefault(); + e.stopPropagation(); + + var file = e.dataTransfer.files[0]; + + if(!file.path || file.path.indexOf(".dot") < 0 && file.path.indexOf(".grid") < 0){ console.log("Dotgrid","Not a dot file"); return; } + + var reader = new FileReader(); + reader.onload = function(e){ + dotgrid.tool.replace(JSON.parse(e.target.result.toString().trim())); + dotgrid.guide.refresh(); + }; + reader.readAsText(file); + } + + this.copy = function(e) + { + dotgrid.guide.refresh(); + + e.clipboardData.setData('text/source', dotgrid.tool.export(dotgrid.tool.layer())); + e.clipboardData.setData('text/plain', dotgrid.tool.path()); + e.clipboardData.setData('text/html', dotgrid.renderer.to_svg()); + e.clipboardData.setData('text/svg+xml', dotgrid.renderer.to_svg()); + + dotgrid.guide.refresh(); + } + + this.cut = function(e) + { + dotgrid.guide.refresh(); + + e.clipboardData.setData('text/plain', dotgrid.tool.export(dotgrid.tool.layer())); + e.clipboardData.setData('text/html', dotgrid.renderer.to_svg()); + e.clipboardData.setData('text/svg+xml', dotgrid.renderer.to_svg()); + + dotgrid.tool.layers[dotgrid.tool.index] = []; + + dotgrid.guide.refresh(); + } + + this.paste = function(e) + { + var data = e.clipboardData.getData("text/source"); + if(is_json(data)){ + data = JSON.parse(data.trim()); + dotgrid.tool.import(data); + } + + dotgrid.guide.refresh(); + } + + // Normalizers + + this.position_in_grid = function(pos) + { + return {x:(window.innerWidth/2) - (this.tool.settings.size.width/2) - pos.x,y:pos.y - (30+10)} + } + + this.position_on_grid = function(pos) + { + pos.y = pos.y - 7.5 + pos.x = pos.x + 7.5 + x = Math.round(pos.x/this.grid_width)*this.grid_width + y = Math.round(pos.y/this.grid_height)*this.grid_height + + x = clamp(x * -1,0,this.tool.settings.size.width) + y = clamp(y,0,this.tool.settings.size.height) + return {x:x*-1,y:y}; + } + + function is_json(text){ try{ JSON.parse(text);return true; } catch(error){ return false; }} + function pos_is_equal(a,b){ return a && b && a.x == b.x && a.y == b.y } + function clamp(v, min, max) { return v < min ? min : v > max ? max : v; } +} + +window.addEventListener('resize', function(e) +{ + dotgrid.refresh() +}, false); + +window.addEventListener('dragover',function(e) +{ + e.stopPropagation(); + e.preventDefault(); + e.dataTransfer.dropEffect = 'copy'; +}); + +String.prototype.capitalize = function() +{ + return this.charAt(0).toUpperCase() + this.slice(1).toLowerCase(); +} diff --git a/web/scripts/generator.js b/web/scripts/generator.js new file mode 100644 index 0000000..8dd1764 --- /dev/null +++ b/web/scripts/generator.js @@ -0,0 +1,114 @@ +function Generator(layer,style) +{ + this.layer = layer; + this.style = style; + + function operate(layer,offset,scale,mirror = 0,angle = 0) + { + var l = copy(layer) + + for(k1 in l){ + var seg = l[k1]; + for(k2 in seg.vertices){ + if(mirror == 1){ seg.vertices[k2].x = (dotgrid.tool.settings.size.width) - seg.vertices[k2].x } + if(mirror == 2){ seg.vertices[k2].y = (dotgrid.tool.settings.size.height) - seg.vertices[k2].y } + + // Offset + seg.vertices[k2].x += offset.x + seg.vertices[k2].y += offset.y + + // Rotate + var center = {x:(dotgrid.tool.settings.size.width/2)+offset.x,y:(dotgrid.tool.settings.size.height/2)+offset.y} + seg.vertices[k2] = rotate_point(seg.vertices[k2],center,angle) + + // Scale + seg.vertices[k2].x *= scale + seg.vertices[k2].y *= scale + } + } + return l; + } + + this.render = function(prev,segment,mirror = 0) + { + var type = segment.type; + var vertices = segment.vertices; + var html = ''; + var skip = 0; + + for(id in vertices){ + if(skip > 0){ skip -= 1; continue; } + + var vertex = vertices[id] + var next = vertices[parseInt(id)+1] + var after_next = vertices[parseInt(id)+2] + + if(id == 0 && !prev || id == 0 && prev && (prev.x != vertex.x || prev.y != vertex.y)){ + html += `M${vertex.x},${vertex.y} ` + } + + if(type == "line"){ + html += `L${vertex.x},${vertex.y} `; + } + else if(type == "arc_c"){ + var clock = mirror > 0 ? '0,0' : '0,1' + html += next ? `A${Math.abs(next.x - vertex.x)},${Math.abs(next.y - vertex.y)} 0 ${clock} ${next.x},${next.y} ` : ''; + } + else if(type == "arc_r"){ + var clock = mirror > 0 ? '0,1' : '0,0' + html += next ? `A${Math.abs(next.x - vertex.x)},${Math.abs(next.y - vertex.y)} 0 ${clock} ${next.x},${next.y} ` : ''; + } + else if(type == "bezier"){ + html += next && after_next ?`Q${next.x},${next.y} ${after_next.x},${after_next.y} ` : ''; + skip = 1 + } + else{ + console.warn(`unknown type:${type}`) + } + } + + if(segment.type == "close"){ + html += "Z " + } + + return html + } + + this.convert = function(layer,mirror,angle) + { + var s = "" + var prev = null + for(id in layer){ + var seg = layer[id]; + s += `${this.render(prev,seg,mirror)}` + prev = seg.vertices ? seg.vertices[seg.vertices.length-1] : null + } + + return s; + } + + this.toString = function(offset = {x:0,y:0}, scale = 1, mirror = this.style && this.style.mirror_style ? this.style.mirror_style : 0) + { + var s = this.convert(operate(this.layer,offset,scale)) + + if(mirror == 1 || mirror == 2){ + s += this.convert(operate(this.layer,offset,scale,mirror),mirror) + } + + if(mirror == 3){ + s += this.convert(operate(this.layer,offset,scale,mirror,120),mirror) + s += this.convert(operate(this.layer,offset,scale,mirror,240),mirror) + } + if(mirror == 4){ + s += this.convert(operate(this.layer,offset,scale,mirror,72),mirror) + s += this.convert(operate(this.layer,offset,scale,mirror,144),mirror) + s += this.convert(operate(this.layer,offset,scale,mirror,216),mirror) + s += this.convert(operate(this.layer,offset,scale,mirror,288),mirror) + } + + return s + } + + function copy(data){ return data ? JSON.parse(JSON.stringify(data)) : []; } + function rotate_point(point, origin, angle){ angle = angle * Math.PI / 180.0; return { x: (Math.cos(angle) * (point.x-origin.x) - Math.sin(angle) * (point.y-origin.y) + origin.x).toFixed(1), y: (Math.sin(angle) * (point.x-origin.x) + Math.cos(angle) * (point.y-origin.y) + origin.y).toFixed(1) }; } +} \ No newline at end of file diff --git a/web/scripts/guide.js b/web/scripts/guide.js new file mode 100644 index 0000000..5bfd3bd --- /dev/null +++ b/web/scripts/guide.js @@ -0,0 +1,232 @@ +function Guide() +{ + this.el = document.createElement("canvas"); + this.el.id = "guide"; + this.el.width = 640; + this.el.height = 640; + this.el.style.width = "320px"; + this.el.style.height = "320px"; + this.show_extras = true; + + var scale = 2; + + this.start = function() + { + this.clear(); + this.refresh(); + } + + this.refresh = function() + { + this.clear(); + + if(dotgrid.tool.index == 2){ this.draw_markers() ; this.draw_vertices() } + this.draw_path(new Generator(dotgrid.tool.layers[2],dotgrid.tool.styles[2]).toString({x:15,y:15},scale),dotgrid.tool.styles[2]) + if(dotgrid.tool.index == 1){ this.draw_markers() ; this.draw_vertices() } + this.draw_path(new Generator(dotgrid.tool.layers[1],dotgrid.tool.styles[1]).toString({x:15,y:15},scale),dotgrid.tool.styles[1]) + if(dotgrid.tool.index == 0){ this.draw_markers(); this.draw_vertices() } + this.draw_path(new Generator(dotgrid.tool.layers[0],dotgrid.tool.styles[0]).toString({x:15,y:15},scale),dotgrid.tool.styles[0]) + + this.draw_handles() + this.draw_translation(); + this.draw_cursor(); + this.draw_preview(); + } + + this.clear = function() + { + this.el.getContext('2d').clearRect(0, 0, this.el.width*scale, this.el.height*scale); + } + + this.toggle = function() + { + this.show_extras = this.show_extras ? false : true; + this.refresh() + } + + this.resize = function(size) + { + var offset = 30 + this.el.width = (size.width+offset)*scale; + this.el.height = (size.height+offset)*scale; + this.el.style.width = (size.width+offset)+"px"; + this.el.style.height = (size.height+offset)+"px"; + + this.el.style.left = (size.width/2) * -1 + this.refresh(); + } + + this.draw_handles = function() + { + if(!this.show_extras){ return; } + + for(segment_id in dotgrid.tool.layer()){ + var segment = dotgrid.tool.layer()[segment_id]; + for(vertex_id in segment.vertices){ + var vertex = segment.vertices[vertex_id]; + this.draw_handle(vertex); + } + } + } + + this.draw_vertices = function() + { + for(id in dotgrid.tool.vertices){ + this.draw_vertex(dotgrid.tool.vertices[id]); + } + } + + this.draw_markers = function() + { + if(!this.show_extras){ return; } + + for (var x = dotgrid.grid_x; x >= 0; x--) { + for (var y = dotgrid.grid_y; y >= 0; y--) { + var pos_x = parseInt(x * dotgrid.grid_width) + dotgrid.grid_width ; + var pos_y = parseInt(y * dotgrid.grid_height) + dotgrid.grid_height ; + var is_step = x % dotgrid.block_x == 0 && y % dotgrid.block_y == 0; + var radius = is_step ? 2.5 : 1.5; + this.draw_marker({x:pos_x,y:pos_y},radius,is_step); + } + } + } + + this.draw_vertex = function(pos, radius = 5) + { + var ctx = this.el.getContext('2d'); + ctx.beginPath(); + ctx.lineWidth = 2; + ctx.arc((pos.x * scale)+30, (pos.y * scale)+30, radius, 0, 2 * Math.PI, false); + ctx.fillStyle = dotgrid.theme.active.f_med; + ctx.fill(); + ctx.closePath(); + } + + this.draw_handle = function(pos, radius = 6) + { + var ctx = this.el.getContext('2d'); + + ctx.beginPath(); + ctx.setLineDash([0,0]); + ctx.lineWidth = 3; + ctx.lineCap="round"; + ctx.arc(Math.abs(pos.x * -scale)+30, Math.abs(pos.y * scale)+30, radius+3, 0, 2 * Math.PI, false); + ctx.fillStyle = dotgrid.theme.active.f_high; + ctx.fill(); + ctx.strokeStyle = dotgrid.theme.active.f_high; + ctx.stroke(); + ctx.closePath(); + + ctx.beginPath(); + ctx.arc((pos.x * scale)+30, (pos.y * scale)+30, radius, 0, 2 * Math.PI, false); + ctx.fillStyle = dotgrid.theme.active.f_low; + ctx.fill(); + ctx.closePath(); + + ctx.beginPath(); + ctx.arc((pos.x * scale)+30, (pos.y * scale)+30, radius-3, 0, 2 * Math.PI, false); + ctx.fillStyle = dotgrid.theme.active.f_high; + ctx.fill(); + ctx.closePath(); + } + + this.draw_marker = function(pos,radius = 1,step) + { + var ctx = this.el.getContext('2d'); + ctx.beginPath(); + ctx.lineWidth = 2; + ctx.arc(pos.x * scale, pos.y * scale, radius, 0, 2 * Math.PI, false); + ctx.fillStyle = step ? dotgrid.theme.active.f_med : dotgrid.theme.active.f_low; + ctx.fill(); + ctx.closePath(); + } + + this.draw_path = function(path,style) + { + var ctx = this.el.getContext('2d'); + var p = new Path2D(path); + + ctx.setLineDash([0,0]); + + ctx.strokeStyle = style.color; + ctx.lineWidth = style.thickness * scale; + ctx.lineCap = style.strokeLinecap; + ctx.lineJoin = style.strokeLinejoin; + + if(style.fill && style.fill != "none"){ + ctx.fillStyle = style.color + ctx.fill(p); + } + if(style.strokeLineDash){ + ctx.setLineDash(style.strokeLineDash); + } + + ctx.stroke(p); + } + + this.draw_translation = function() + { + if(!dotgrid.cursor.translation){ return; } + // From + var ctx = this.el.getContext('2d'); + var from = dotgrid.cursor.translation.from; + var to = dotgrid.cursor.translation.to; + + if(to.x<=0) { + ctx.beginPath(); + ctx.setLineDash([0,0]); + ctx.moveTo((from.x * -scale)+30,(from.y * scale)+30); + ctx.lineTo((to.x * -scale)+30,(to.y * scale)+30); + ctx.lineCap="round"; + ctx.lineWidth = 5; + ctx.strokeStyle = dotgrid.theme.active.b_inv; + ctx.stroke(); + ctx.closePath(); + } + } + + this.draw_cursor = function(pos = dotgrid.cursor.pos,radius = dotgrid.tool.style().thickness-1) + { + var ctx = this.el.getContext('2d'); + + ctx.beginPath(); + ctx.setLineDash([0,0]); + ctx.lineWidth = 3; + ctx.lineCap="round"; + ctx.arc(Math.abs(pos.x * -scale)+30, Math.abs(pos.y * scale)+30, 3, 0, 2 * Math.PI, false); + ctx.fillStyle = dotgrid.theme.active.f_low; + ctx.fill(); + ctx.closePath(); + + ctx.beginPath(); + ctx.setLineDash([0,0]); + ctx.lineWidth = 3; + ctx.lineCap="round"; + ctx.arc(Math.abs(pos.x * -scale)+30, Math.abs(pos.y * scale)+30, clamp(radius,5,100), 0, 2 * Math.PI, false); + ctx.strokeStyle = dotgrid.theme.active.f_med; + ctx.stroke(); + ctx.closePath(); + } + + this.draw_preview = function() + { + var operation = dotgrid.cursor.operation + + if(!dotgrid.tool.can_cast(operation)){ return; } + if(operation == "close"){ return; } + + var path = new Generator([{vertices:dotgrid.tool.vertices,type:operation}]).toString({x:15,y:15},2) + var style = { + color:dotgrid.theme.active.f_med, + thickness:2, + strokeLinecap:"round", + strokeLinejoin:"round", + strokeLineDash:[5, 15] + } + + this.draw_path(path,style) + } + + function pos_is_equal(a,b){ return a && b && Math.abs(a.x) == Math.abs(b.x) && Math.abs(a.y) == Math.abs(b.y) } + function clamp(v, min, max) { return v < min ? min : v > max ? max : v; } +} diff --git a/web/scripts/interface.js b/web/scripts/interface.js new file mode 100644 index 0000000..a40b153 --- /dev/null +++ b/web/scripts/interface.js @@ -0,0 +1,79 @@ +function Interface() +{ + this.el = document.createElement("div"); + this.el.id = "interface"; + + this.el.appendChild(this.menu_el = document.createElement("div")); + this.menu_el.id = "menu"; + + this.is_visible = true; + this.zoom = false; + + this.start = function() + { + document.getElementById("app").appendChild(this.el); + this.el.appendChild(dotgrid.picker.el); + + var html = "" + var tools = { + line: ["line","M60,60 L240,240","A"], + arc_c: ["arc clockwise","M60,60 A180,180 0 0,1 240,240","S"], + arc_r: ["arc reverse","M60,60 A180,180 0 0,0 240,240","D"], + bezier: ["bezier","M60,60 Q60,150 150,150 Q240,150 240,240","F"], + close: ["close","M60,60 A180,180 0 0,1 240,240 M60,60 A180,180 0 0,0 240,240","Z"], + + linecap: ["linecap","M60,60 L60,60 L180,180 L240,180 L240,240 L180,240 L180,180","Q"], + linejoin: ["linejoin","M60,60 L120,120 L180,120 M120,180 L180,180 L240,240","W"], + thickness: ["thickness","M120,90 L120,90 L90,120 L180,210 L210,180 Z M105,105 L105,105 L60,60 M195,195 L195,195 L240,240"], + + mirror: ["mirror","M60,60 L60,60 L120,120 M180,180 L180,180 L240,240 M210,90 L210,90 L180,120 M120,180 L120,180 L90,210","E"], + fill: ["fill","M60,60 L60,150 L150,150 L240,150 L240,240 Z","R"], + color: ["color","M150,60 A90,90 0 0,1 240,150 A-90,90 0 0,1 150,240 A-90,-90 0 0,1 60,150 A90,-90 0 0,1 150,60","G"], + } + + for(id in tools){ + var tool = tools[id]; + var shortcut = tool[2]; + html += `${id == "depth" ? `` : ""}${id.capitalize()}${shortcut ? '('+shortcut+')' : ''}` + } + this.menu_el.innerHTML = html + } + + this.prev_operation = null; + + this.refresh = function(force = false) + { + if(this.prev_operation == dotgrid.cursor.operation && force == false){ return; } + + document.getElementById("line").className.baseVal = !dotgrid.tool.can_cast("line") ? "icon inactive" : "icon"; + document.getElementById("arc_c").className.baseVal = !dotgrid.tool.can_cast("arc_c") ? "icon inactive" : "icon"; + document.getElementById("arc_r").className.baseVal = !dotgrid.tool.can_cast("arc_r") ? "icon inactive" : "icon"; + document.getElementById("bezier").className.baseVal = !dotgrid.tool.can_cast("bezier") ? "icon inactive" : "icon"; + document.getElementById("close").className.baseVal = !dotgrid.tool.can_cast("close") ? "icon inactive" : "icon"; + + document.getElementById("thickness").className.baseVal = dotgrid.tool.layer().length < 1 ? "icon inactive" : "icon"; + document.getElementById("linecap").className.baseVal = dotgrid.tool.layer().length < 1 ? "icon inactive" : "icon"; + document.getElementById("linejoin").className.baseVal = dotgrid.tool.layer().length < 1 ? "icon inactive" : "icon"; + document.getElementById("mirror").className.baseVal = dotgrid.tool.layer().length < 1 ? "icon inactive" : "icon"; + document.getElementById("fill").className.baseVal = dotgrid.tool.layer().length < 1 ? "icon inactive" : "icon"; + + document.getElementById("color").children[0].style.fill = dotgrid.tool.style().color; + document.getElementById("color").children[0].style.stroke = dotgrid.tool.style().color; + document.getElementById("color").className.baseVal = "icon"; + + // Mirror + if(dotgrid.tool.style().mirror_style == 0){ document.getElementById("mirror_path").setAttribute("d","M60,60 L60,60 L120,120 M180,180 L180,180 L240,240 M210,90 L210,90 L180,120 M120,180 L120,180 L90,210") } + else if(dotgrid.tool.style().mirror_style == 1){ document.getElementById("mirror_path").setAttribute("d","M60,60 L240,240 M180,120 L210,90 M120,180 L90,210") } + else if(dotgrid.tool.style().mirror_style == 2){ document.getElementById("mirror_path").setAttribute("d","M210,90 L210,90 L90,210 M60,60 L60,60 L120,120 M180,180 L180,180 L240,240") } + else if(dotgrid.tool.style().mirror_style == 3){ document.getElementById("mirror_path").setAttribute("d","M60,60 L60,60 L120,120 L120,120 L180,120 M120,150 L120,150 L180,150 M120,180 L120,180 L180,180 L180,180 L240,240 ") } + else if(dotgrid.tool.style().mirror_style == 4){ document.getElementById("mirror_path").setAttribute("d","M120,120 L120,120 L120,120 L180,120 M120,150 L120,150 L180,150 M120,180 L120,180 L180,180 L180,180 L180,180 L240,240 M120,210 L120,210 L180,210 M120,90 L120,90 L180,90 M60,60 L60,60 L120,120 ") } + + this.prev_operation = dotgrid.cursor.operation; + } + + this.toggle = function() + { + this.is_visible = this.is_visible ? false : true; + this.el.className = this.is_visible ? "visible" : "hidden"; + } +} diff --git a/web/scripts/lib/history.js b/web/scripts/lib/history.js new file mode 100644 index 0000000..a3578cb --- /dev/null +++ b/web/scripts/lib/history.js @@ -0,0 +1,50 @@ +function History() +{ + this.index = 0; + this.a = []; + + this.clear = function() + { + this.a = []; + this.index = 0; + } + + this.push = function(data) + { + if(this.index < this.a.length-1){ + this.fork(); + } + this.index = this.a.length; + this.a = this.a.slice(0,this.index); + this.a.push(copy(data)); + + if(this.a.length > 20){ + this.a.shift(); + } + } + + this.fork = function() + { + this.a = this.a.slice(0,this.index+1); + } + + this.pop = function() + { + return this.a.pop(); + } + + this.prev = function() + { + this.index = clamp(this.index-1,0,this.a.length-1); + return copy(this.a[this.index]); + } + + this.next = function() + { + this.index = clamp(this.index+1,0,this.a.length-1); + return copy(this.a[this.index]); + } + + function copy(data){ return data ? JSON.parse(JSON.stringify(data)) : []; } + function clamp(v, min, max) { return v < min ? min : v > max ? max : v; } +} \ No newline at end of file diff --git a/web/scripts/lib/theme.js b/web/scripts/lib/theme.js new file mode 100644 index 0000000..a3b3180 --- /dev/null +++ b/web/scripts/lib/theme.js @@ -0,0 +1,86 @@ +function Theme() +{ + var app = this; + + this.el = document.createElement("style"); + this.el.type = 'text/css'; + this.default = {meta:{}, data: { background: "#222", f_high: "#fff", f_med: "#777", f_low: "#444", f_inv: "#000", b_high: "#000", b_med: "#affec7", b_low: "#000", b_inv: "#affec7" }} + this.active = this.default; + + this.start = function() + { + this.load(localStorage.theme ? localStorage.theme : this.default, this.default); + window.addEventListener('dragover',this.drag_enter); + window.addEventListener('drop', this.drag); + document.head.appendChild(this.el) + } + + this.load = function(t, fall_back) + { + var theme = is_json(t) ? JSON.parse(t).data : t.data; + + if(!theme || !theme.background){ + if(fall_back) { + theme = fall_back.data; + } else { + return; + } + } + + var css = ` + :root { + --background: ${theme.background}; + --f_high: ${theme.f_high}; + --f_med: ${theme.f_med}; + --f_low: ${theme.f_low}; + --f_inv: ${theme.f_inv}; + --b_high: ${theme.b_high}; + --b_med: ${theme.b_med}; + --b_low: ${theme.b_low}; + --b_inv: ${theme.b_inv}; + }`; + + this.active = theme; + this.el.textContent = css; + localStorage.setItem("theme", JSON.stringify({data: theme})); + } + + this.reset = function() + { + this.load(this.default); + } + + this.drag_enter = function(e) + { + e.stopPropagation(); + e.preventDefault(); + e.dataTransfer.dropEffect = 'copy'; + } + + this.drag = function(e) + { + e.preventDefault(); + e.stopPropagation(); + + var file = e.dataTransfer.files[0]; + + if(!file.name || !file.name.indexOf(".thm") < 0){ console.log("Theme","Not a theme"); return; } + + var reader = new FileReader(); + reader.onload = function(e){ + app.load(e.target.result); + }; + reader.readAsText(file); + } + + function is_json(text) + { + try{ + JSON.parse(text); + return true; + } + catch (error){ + return false; + } + } +} \ No newline at end of file diff --git a/web/scripts/picker.js b/web/scripts/picker.js new file mode 100644 index 0000000..936ce5c --- /dev/null +++ b/web/scripts/picker.js @@ -0,0 +1,114 @@ +function Picker() +{ + this.memory = ""; + this.el = document.createElement("input"); + this.el.id = "picker" + this.original = null; + + this.start = function() + { + this.el.setAttribute("placeholder",`${dotgrid.tool.style().color} ${dotgrid.tool.settings.size.width}x${dotgrid.tool.settings.size.height}`) + + dotgrid.controller.set("picker"); + dotgrid.interface.el.className = "picker" + this.el.focus() + this.original = dotgrid.tool.style().color + this.el.value = "" + } + + this.stop = function() + { + this.cancel(); + dotgrid.controller.set(); + dotgrid.interface.el.className = "" + this.el.blur() + this.el.value = "" + } + + this.validate = function() + { + var parts = this.parse(this.el.value) + + if(parts.color){ this.set_color(parts.color); } + if(parts.size){ this.set_size(parts.size); } + + dotgrid.guide.refresh(); + dotgrid.controller.set(); + dotgrid.interface.el.className = "" + this.el.blur() + this.el.value = "" + } + + this.set_color = function(color) + { + dotgrid.tool.style().color = color; + dotgrid.tool.style().fill = dotgrid.tool.style().fill != "none" ? color : "none"; + } + + this.set_size = function(size) + { + dotgrid.set_size(size); + } + + this.cancel = function() + { + if(!this.original){ return; } + dotgrid.tool.style().color = this.original; + dotgrid.tool.style().fill = dotgrid.tool.style().fill != "none" ? this.original : "none"; + dotgrid.guide.refresh(); + } + + this.update = function() + { + var parts = this.parse(this.el.value) + if(!parts.color){ return; } + + dotgrid.tool.style().color = parts.color; + dotgrid.tool.style().fill = dotgrid.tool.style().fill != "none" ? parts.color : "none"; + dotgrid.guide.refresh(); + } + + this.listen = function(e) + { + if(e.key == "Enter"){ + this.validate(); + e.preventDefault(); + return; + } + + this.update(); + } + + this.parse = function(value) + { + var parts = value.split(" "); + var color = null; + var size = null; + + for(id in parts){ + var part = parts[id]; + if(is_color(part) && !color){ color = part; } + if(is_size(part) && !size){ size = { width:parseInt(part.toLowerCase().split("x")[0]),height:parseInt(part.toLowerCase().split("x")[1]) }; } + } + return {color:color,size:size} + } + + function is_size(val) + { + if(val.toLowerCase().indexOf("x") < 1){ return false; } + + return true + } + + function is_color(val) + { + if(val.length != 4 && val.length != 7){ + return false + } + + var re = /\#[0-9A-Fa-f]/g; + return re.test(val) + } + + this.el.onkeyup = function(event){ dotgrid.picker.listen(event); }; +} \ No newline at end of file diff --git a/web/scripts/renderer.js b/web/scripts/renderer.js new file mode 100644 index 0000000..7c59cb6 --- /dev/null +++ b/web/scripts/renderer.js @@ -0,0 +1,87 @@ +function Renderer() +{ + // Create SVG parts + this.svg_el = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + this.svg_el.setAttribute("xmlns","http://www.w3.org/2000/svg"); + this.svg_el.setAttribute("baseProfile","full"); + this.svg_el.setAttribute("version","1.1"); + this.svg_el.style.fill = "none"; + + this.layer_1 = document.createElementNS("http://www.w3.org/2000/svg", "path"); + this.layer_2 = document.createElementNS("http://www.w3.org/2000/svg", "path"); + this.layer_3 = document.createElementNS("http://www.w3.org/2000/svg", "path"); + + this.svg_el.appendChild(this.layer_3); + this.svg_el.appendChild(this.layer_2); + this.svg_el.appendChild(this.layer_1); + + this.refresh = function() + { + this.svg_el.setAttribute("width",dotgrid.tool.settings.size.width+"px"); + this.svg_el.setAttribute("height",dotgrid.tool.settings.size.height+"px"); + this.svg_el.style.width = dotgrid.tool.settings.size.width; + this.svg_el.style.height = dotgrid.tool.settings.size.height; + this.svg_el.style.strokeWidth = dotgrid.tool.style().thickness; + + var styles = dotgrid.tool.styles + var paths = dotgrid.tool.paths() + + this.layer_1.style.strokeWidth = styles[0].thickness; + this.layer_1.style.strokeLinecap = styles[0].strokeLinecap; + this.layer_1.style.strokeLinejoin = styles[0].strokeLinejoin; + this.layer_1.style.stroke = styles[0].color; + this.layer_1.style.fill = styles[0].fill; + this.layer_1.setAttribute("d",paths[0]) + + this.layer_2.style.strokeWidth = styles[1].thickness; + this.layer_2.style.strokeLinecap = styles[1].strokeLinecap; + this.layer_2.style.strokeLinejoin = styles[1].strokeLinejoin; + this.layer_2.style.stroke = styles[1].color; + this.layer_2.style.fill = styles[1].fill; + this.layer_2.setAttribute("d",paths[1]) + + this.layer_3.style.strokeWidth = styles[2].thickness; + this.layer_3.style.strokeLinecap = styles[2].strokeLinecap; + this.layer_3.style.strokeLinejoin = styles[2].strokeLinejoin; + this.layer_3.style.stroke = styles[2].color; + this.layer_3.style.fill = styles[2].fill; + this.layer_3.setAttribute("d",paths[2]) + } + + this.to_png = function(size = dotgrid.tool.settings.size,callback = dotgrid.render) + { + this.refresh(); + + var xml = new XMLSerializer().serializeToString(this.svg_el); + var svg64 = btoa(xml); + var b64Start = 'data:image/svg+xml;base64,'; + var image64 = b64Start + svg64; + var img = new Image; + + var canvas = document.createElement("canvas"); + + canvas.width = size.width; + canvas.height = size.height; + + var ctx = canvas.getContext('2d'); + + img.onload = function(){ + ctx.drawImage(img, 0, 0, size.width, size.height); + var data = canvas.toDataURL('image/png').replace(/^data:image\/\w+;base64,/, ""); + dotgrid.renderer.to_png_ready(callback, new Buffer(data, 'base64'),size) + }; + img.src = image64; + } + + this.to_png_ready = function(callback, buffer, size) + { + callback(null,buffer,size) + } + + this.to_svg = function() + { + this.refresh(); + + return this.svg_el.outerHTML; + } +} \ No newline at end of file diff --git a/web/scripts/tool.js b/web/scripts/tool.js new file mode 100644 index 0000000..8f779aa --- /dev/null +++ b/web/scripts/tool.js @@ -0,0 +1,286 @@ +function Tool() +{ + this.index = 0; + this.settings = { size:{width:300,height:300} } + this.layers = [[],[],[]]; + this.styles = [ + { thickness:10,strokeLinecap:"round",strokeLinejoin:"round",color:"#f00",fill:"none",mirror_style:0 }, + { thickness:10,strokeLinecap:"round",strokeLinejoin:"round",color:"#0f0",fill:"none",mirror_style:0 }, + { thickness:10,strokeLinecap:"round",strokeLinejoin:"round",color:"#00f",fill:"none",mirror_style:0 } + ]; + this.vertices = []; + this.reqs = { line:2,arc_c:2,arc_r:2,bezier:3,close:0 }; + + this.start = function() + { + this.styles[0].color = dotgrid.theme.active.f_high + this.styles[1].color = dotgrid.theme.active.f_med + this.styles[2].color = dotgrid.theme.active.f_low + } + + this.reset = function() + { + this.layers = [[],[],[]]; + this.vertices = []; + this.index = 0; + } + + this.clear = function() + { + this.vertices = []; + dotgrid.guide.refresh(); + dotgrid.interface.refresh(true); + } + + this.undo = function() + { + this.layers = dotgrid.history.prev(); + dotgrid.guide.refresh(); + dotgrid.interface.refresh(true); + } + + this.redo = function() + { + this.layers = dotgrid.history.next(); + dotgrid.guide.refresh(); + dotgrid.interface.refresh(true); + } + + // I/O + + this.export = function(target = {settings:this.settings,layers:this.layers,styles:this.styles}) + { + return JSON.stringify(copy(target), null, 2); + } + + this.import = function(layer) + { + this.layers[this.index] = this.layers[this.index].concat(layer) + dotgrid.history.push(this.layers); + this.clear(); + dotgrid.guide.refresh(); + dotgrid.interface.refresh(true); + } + + this.replace = function(dot) + { + if(!dot.layers || dot.layers.length != 3){ console.warn("Incompatible version"); return; } + + if(dot.settings.width && dot.settings.height){ + dot.settings.size = {width:dot.settings.width,height:dot.settings.height} + } + if(this.settings && (this.settings.size.width != dot.settings.size.width || this.settings.size.height != dot.settings.size.height)){ + dotgrid.set_size({width:dot.settings.size.width,height:dot.settings.size.height}) + } + + this.layers = dot.layers; + this.styles = dot.styles; + this.settings = dot.settings; + + this.clear(); + dotgrid.guide.refresh(); + dotgrid.interface.refresh(true); + dotgrid.history.push(this.layers); + } + + // EDIT + + this.remove_segment = function() + { + if(this.vertices.length > 0){ this.clear(); return; } + + this.layer().pop(); + this.clear(); + dotgrid.guide.refresh(); + dotgrid.interface.refresh(true); + } + + this.remove_segments_at = function(pos) + { + for(segment_id in this.layer()){ + var segment = this.layer()[segment_id]; + for(vertex_id in segment.vertices){ + var vertex = segment.vertices[vertex_id]; + if(Math.abs(pos.x) == Math.abs(vertex.x) && Math.abs(pos.y) == Math.abs(vertex.y)){ + segment.vertices.splice(vertex_id,1) + } + } + if(segment.vertices.length < 2){ + this.layers[this.index].splice(segment_id,1) + } + } + this.clear(); + dotgrid.guide.refresh(); + dotgrid.interface.refresh(true); + } + + this.add_vertex = function(pos) + { + pos = {x:Math.abs(pos.x),y:Math.abs(pos.y)} + this.vertices.push(pos); + dotgrid.interface.refresh(true); + } + + this.vertex_at = function(pos) + { + for(segment_id in this.layer()){ + var segment = this.layer()[segment_id]; + for(vertex_id in segment.vertices){ + var vertex = segment.vertices[vertex_id]; + if(vertex.x == Math.abs(pos.x) && vertex.y == Math.abs(pos.y)){ + return vertex; + } + } + } + return null; + } + + this.cast = function(type) + { + if(!this.layer()){ this.layers[this.index] = []; } + if(!this.can_cast(type)){ console.warn("Cannot cast"); return; } + + var append_target = this.can_append({type:type,vertices:this.vertices.slice()}) + + if(append_target){ + this.layers[this.index][append_target].vertices = this.layers[this.index][append_target].vertices.concat(this.vertices.slice()) + } + else{ + this.layer().push({type:type,vertices:this.vertices.slice()}) + } + + dotgrid.history.push(this.layers); + + this.clear(); + dotgrid.guide.refresh(); + dotgrid.interface.refresh(true); + + console.log(`Casted ${type} -> ${this.layer().length} elements`); + } + + this.can_append = function(content) + { + for(id in this.layer()){ + var stroke = this.layer()[id]; + if(stroke.type != content.type){ continue; } + if(!stroke.vertices){ continue; } + if(!stroke.vertices[stroke.vertices.length-1]){ continue; } + if(stroke.vertices[stroke.vertices.length-1].x != content.vertices[0].x){ continue; } + if(stroke.vertices[stroke.vertices.length-1].y != content.vertices[0].y){ continue; } + return id; + } + return false; + } + + this.can_cast = function(type) + { + if(!type){ return false; } + // Cannot cast close twice + if(type == "close"){ + var prev = this.layer()[this.layer().length-1]; + if(!prev || prev.type == "close"){ + return false; + } + } + if(type == "bezier"){ + if(this.vertices.length != 3 && this.vertices.length != 5 && this.vertices.length != 7 && this.vertices.length != 9){ + return false; + } + } + return this.vertices.length >= this.reqs[type]; + } + + this.paths = function() + { + var l1 = new Generator(dotgrid.tool.layers[0],dotgrid.tool.styles[0]).toString({x:0,y:0},1) + var l2 = new Generator(dotgrid.tool.layers[1],dotgrid.tool.styles[1]).toString({x:0,y:0},1) + var l3 = new Generator(dotgrid.tool.layers[2],dotgrid.tool.styles[2]).toString({x:0,y:0},1) + + return [l1,l2,l3] + } + + this.path = function() + { + return new Generator(dotgrid.tool.layer(),dotgrid.tool.style()).toString({x:0,y:0},1) + } + + this.translate = function(a,b) + { + for(segment_id in this.layer()){ + var segment = this.layer()[segment_id]; + for(vertex_id in segment.vertices){ + var vertex = segment.vertices[vertex_id]; + if(vertex.x == Math.abs(a.x) && vertex.y == Math.abs(a.y)){ + segment.vertices[vertex_id] = {x:Math.abs(b.x),y:Math.abs(b.y)}; + } + } + } + dotgrid.history.push(this.layers); + this.clear(); + dotgrid.guide.refresh(); + } + + this.translate_multi = function(a,b) + { + var offset = {x:a.x - b.x,y:a.y - b.y} + + for(segment_id in this.layer()){ + var segment = this.layer()[segment_id]; + for(vertex_id in segment.vertices){ + var vertex = segment.vertices[vertex_id]; + segment.vertices[vertex_id] = {x:vertex.x+offset.x,y:vertex.y-offset.y}; + } + } + dotgrid.history.push(this.layers); + this.clear(); + dotgrid.guide.refresh(); + } + + // Toggles + + this.toggle_mirror = function() + { + this.style().mirror_style = this.style().mirror_style > 3 ? 0 : this.style().mirror_style+1; + + dotgrid.guide.refresh(); + dotgrid.interface.refresh(true); + } + + // Style + + this.style = function() + { + if(!this.styles[this.index]){ + this.styles[this.index] = []; + } + return this.styles[this.index]; + } + + // Layers + + this.layer = function() + { + if(!this.layers[this.index]){ + this.layers[this.index] = []; + } + return this.layers[this.index]; + } + + this.select_layer = function(id) + { + this.index = clamp(id,0,2); + this.clear(); + dotgrid.guide.refresh(); + dotgrid.interface.refresh(true); + console.log(`layer:${this.index}`) + } + + this.select_next_layer = function() + { + this.index = this.index >= 2 ? 0 : this.index+1 + this.select_layer(this.index); + } + + function copy(data){ return data ? JSON.parse(JSON.stringify(data)) : []; } + function clamp(v, min, max) { return v < min ? min : v > max ? max : v; } +}