| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| body { background:var(--background) !important; } | ||
| #picker { background-color:var(--b_inv) !important; color:var(--f_inv) !important; } | ||
| #picker:before { color:var(--f_med) !important; } | ||
| #picker input::placeholder { color:var(--f_med) !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; } | ||
| .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; } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| #!/bin/bash | ||
|
|
||
| rm -r 'release' | ||
| mkdir 'release' | ||
| cp 'index.html' 'release/index.html' | ||
| cp 'README.txt' 'release/README.txt' | ||
| ~/Applications/butler push ~/Repositories/Hundredrabbits/Dotgrid/release hundredrabbits/dotgrid:osx-64 | ||
| ~/Applications/butler push ~/Repositories/Hundredrabbits/Dotgrid/release hundredrabbits/dotgrid:linux-64 | ||
| ~/Applications/butler push ~/Repositories/Hundredrabbits/Dotgrid/release hundredrabbits/dotgrid:windows-64 | ||
| ~/Applications/butler status hundredrabbits/dotgrid | ||
| rm -r 'release' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,266 @@ | ||
| 'use strict' | ||
|
|
||
| /* global Acels */ | ||
| /* global Theme */ | ||
| /* global Source */ | ||
| /* global History */ | ||
|
|
||
| /* global Manager */ | ||
| /* global Renderer */ | ||
| /* global Tool */ | ||
| /* global Interface */ | ||
| /* global Picker */ | ||
| /* global Cursor */ | ||
|
|
||
| /* global FileReader */ | ||
|
|
||
| function Client () { | ||
| this.install = function (host) { | ||
| console.info('Client', 'Installing..') | ||
|
|
||
| this.acels = new Acels(this) | ||
| this.theme = new Theme(this) | ||
| this.history = new History(this) | ||
| this.source = new Source(this) | ||
|
|
||
| this.manager = new Manager(this) | ||
| this.renderer = new Renderer(this) | ||
| this.tool = new Tool(this) | ||
| this.interface = new Interface(this) | ||
| this.picker = new Picker(this) | ||
| this.cursor = new Cursor(this) | ||
|
|
||
| host.appendChild(this.renderer.el) | ||
|
|
||
| document.addEventListener('mousedown', (e) => { this.cursor.down(e) }, false) | ||
| document.addEventListener('mousemove', (e) => { this.cursor.move(e) }, false) | ||
| document.addEventListener('contextmenu', (e) => { this.cursor.alt(e) }, false) | ||
| document.addEventListener('mouseup', (e) => { this.cursor.up(e) }, false) | ||
| document.addEventListener('copy', (e) => { this.copy(e) }, false) | ||
| document.addEventListener('cut', (e) => { this.cut(e) }, false) | ||
| document.addEventListener('paste', (e) => { this.paste(e) }, false) | ||
| window.addEventListener('resize', (e) => { this.onResize() }, false) | ||
| window.addEventListener('dragover', (e) => { e.stopPropagation(); e.preventDefault(); e.dataTransfer.dropEffect = 'copy' }) | ||
| window.addEventListener('drop', this.onDrop) | ||
|
|
||
| this.acels.set('File', 'New', 'CmdOrCtrl+N', () => { this.source.new() }) | ||
| this.acels.set('File', 'Open', 'CmdOrCtrl+O', () => { this.source.open('grid', this.whenOpen) }) | ||
| this.acels.set('File', 'Save', 'CmdOrCtrl+S', () => { this.source.write('dotgrid', 'grid', this.tool.export(), 'text/plain') }) | ||
| this.acels.set('File', 'Export Vector', 'CmdOrCtrl+E', () => { this.source.write('dotgrid', 'svg', this.manager.toString(), 'image/svg+xml') }) | ||
| this.acels.set('File', 'Export Image', 'CmdOrCtrl+Shift+E', () => { this.manager.toPNG(this.tool.settings.size, (dataUrl) => { this.source.write('dotgrid', 'png', dataUrl, 'image/png') }) }) | ||
| this.acels.set('History', 'Undo', 'CmdOrCtrl+Z', () => { this.tool.undo() }) | ||
| this.acels.set('History', 'Redo', 'CmdOrCtrl+Shift+Z', () => { this.tool.redo() }) | ||
| this.acels.set('Stroke', 'Line', 'A', () => { this.tool.cast('line') }) | ||
| this.acels.set('Stroke', 'Arc', 'S', () => { this.tool.cast('arc_c') }) | ||
| this.acels.set('Stroke', 'Arc Rev', 'D', () => { this.tool.cast('arc_r') }) | ||
| this.acels.set('Stroke', 'Bezier', 'F', () => { this.tool.cast('bezier') }) | ||
| this.acels.set('Stroke', 'Close', 'Z', () => { this.tool.cast('close') }) | ||
| this.acels.set('Stroke', 'Arc(full)', 'T', () => { this.tool.cast('arc_c_full') }) | ||
| this.acels.set('Stroke', 'Arc Rev(full)', 'Y', () => { this.tool.cast('arc_r_full') }) | ||
| this.acels.set('Stroke', 'Clear Selection', 'Escape', () => { this.tool.clear() }) | ||
| this.acels.set('Effect', 'Linecap', 'Q', () => { this.tool.toggle('linecap') }) | ||
| this.acels.set('Effect', 'Linejoin', 'W', () => { this.tool.toggle('linejoin') }) | ||
| this.acels.set('Effect', 'Mirror', 'E', () => { this.tool.toggle('mirror') }) | ||
| this.acels.set('Effect', 'Fill', 'R', () => { this.tool.toggle('fill') }) | ||
| this.acels.set('Effect', 'Thicker', '}', () => { this.tool.toggle('thickness', 1) }) | ||
| this.acels.set('Effect', 'Thinner', '{', () => { this.tool.toggle('thickness', -1) }) | ||
| this.acels.set('Effect', 'Thicker +5', ']', () => { this.tool.toggle('thickness', 5) }) | ||
| this.acels.set('Effect', 'Thinner -5', '[', () => { this.tool.toggle('thickness', -5) }) | ||
| this.acels.set('Manual', 'Add Point', 'Enter', () => { this.tool.addVertex(this.cursor.pos); this.renderer.update() }) | ||
| this.acels.set('Manual', 'Move Up', 'Up', () => { this.cursor.pos.y -= 15; this.renderer.update() }) | ||
| this.acels.set('Manual', 'Move Right', 'Right', () => { this.cursor.pos.x += 15; this.renderer.update() }) | ||
| this.acels.set('Manual', 'Move Down', 'Down', () => { this.cursor.pos.y += 15; this.renderer.update() }) | ||
| this.acels.set('Manual', 'Move Left', 'Left', () => { this.cursor.pos.x -= 15; this.renderer.update() }) | ||
| this.acels.set('Manual', 'Remove Point', 'Shift+Backspace', () => { this.tool.removeSegmentsAt(this.cursor.pos) }) | ||
| this.acels.set('Manual', 'Remove Segment', 'Backspace', () => { this.tool.removeSegment() }) | ||
| this.acels.set('Layers', 'Foreground', 'CmdOrCtrl+1', () => { this.tool.selectLayer(0) }) | ||
| this.acels.set('Layers', 'Middleground', 'CmdOrCtrl+2', () => { this.tool.selectLayer(1) }) | ||
| this.acels.set('Layers', 'Background', 'CmdOrCtrl+3', () => { this.tool.selectLayer(2) }) | ||
| this.acels.set('Layers', 'Merge Layers', 'CmdOrCtrl+M', () => { this.tool.merge() }) | ||
| this.acels.set('View', 'Color Picker', 'G', () => { this.picker.start() }) | ||
| this.acels.set('View', 'Toggle Grid', 'H', () => { this.renderer.toggle() }) | ||
| this.acels.install(window) | ||
| this.acels.pipe(this) | ||
|
|
||
| this.manager.install() | ||
| this.interface.install(host) | ||
| this.theme.install(host, () => { this.update() }) | ||
| } | ||
|
|
||
| this.start = () => { | ||
| console.log('Client', 'Starting..') | ||
| console.info(`${this.acels}`) | ||
|
|
||
| this.theme.start() | ||
| this.tool.start() | ||
| this.renderer.start() | ||
| this.interface.start() | ||
|
|
||
| this.source.new() | ||
| this.onResize() | ||
|
|
||
| setTimeout(() => { document.body.className += ' ready' }, 250) | ||
| } | ||
|
|
||
| this.update = () => { | ||
| this.manager.update() | ||
| this.interface.update() | ||
| this.renderer.update() | ||
| } | ||
|
|
||
| this.clear = () => { | ||
| this.history.clear() | ||
| this.tool.reset() | ||
| this.reset() | ||
| this.renderer.update() | ||
| this.interface.update(true) | ||
| } | ||
|
|
||
| this.reset = () => { | ||
| this.tool.clear() | ||
| this.update() | ||
| } | ||
|
|
||
| this.whenOpen = (file, data) => { | ||
| this.tool.replace(JSON.parse(data)) | ||
| this.onResize() | ||
| } | ||
|
|
||
| // Resize Tools | ||
|
|
||
| this.fitSize = () => { | ||
| if (this.requireResize() === false) { return } | ||
| console.log('Client', `Will resize to: ${printSize(this.getRequiredSize())}`) | ||
| this.update() | ||
| } | ||
|
|
||
| this.getPadding = () => { | ||
| return { x: 60, y: 90 } | ||
| } | ||
|
|
||
| this.getWindowSize = () => { | ||
| return { width: window.innerWidth, height: window.innerHeight } | ||
| } | ||
|
|
||
| this.getProjectSize = () => { | ||
| return this.tool.settings.size | ||
| } | ||
|
|
||
| this.getPaddedSize = () => { | ||
| const rect = this.getWindowSize() | ||
| const pad = this.getPadding() | ||
| return { width: step(rect.width - pad.x, 15), height: step(rect.height - pad.y, 15) } | ||
| } | ||
|
|
||
| this.getRequiredSize = () => { | ||
| const rect = this.getProjectSize() | ||
| const pad = this.getPadding() | ||
| return { width: step(rect.width, 15) + pad.x, height: step(rect.height, 15) + pad.y } | ||
| } | ||
|
|
||
| this.requireResize = () => { | ||
| const _window = this.getWindowSize() | ||
| const _required = this.getRequiredSize() | ||
| const offset = sizeOffset(_window, _required) | ||
| if (offset.width !== 0 || offset.height !== 0) { | ||
| console.log('Client', `Require ${printSize(_required)}, but window is ${printSize(_window)}(${printSize(offset)})`) | ||
| return true | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| this.onResize = () => { | ||
| const _project = this.getProjectSize() | ||
| const _padded = this.getPaddedSize() | ||
| const offset = sizeOffset(_padded, _project) | ||
| if (offset.width !== 0 || offset.height !== 0) { | ||
| console.log('Client', `Resize project to ${printSize(_padded)}`) | ||
| this.tool.settings.size = _padded | ||
| } | ||
| this.update() | ||
| } | ||
|
|
||
| // Events | ||
|
|
||
| this.drag = function (e) { | ||
| e.preventDefault() | ||
| e.stopPropagation() | ||
|
|
||
| const file = e.dataTransfer.files[0] | ||
| const filename = file.path ? file.path : file.name ? file.name : '' | ||
|
|
||
| if (filename.indexOf('.grid') < 0) { console.warn('Client', 'Not a .grid file'); return } | ||
|
|
||
| const reader = new FileReader() | ||
|
|
||
| reader.onload = function (e) { | ||
| const data = e.target && e.target.result ? e.target.result : '' | ||
| this.source.load(filename, data) | ||
| this.fitSize() | ||
| } | ||
| reader.readAsText(file) | ||
| } | ||
|
|
||
| this.onDrop = (e) => { | ||
| e.preventDefault() | ||
| e.stopPropagation() | ||
| const file = e.dataTransfer.files[0] | ||
|
|
||
| if (file.name.indexOf('.grid') > -1) { | ||
| this.source.read(e.dataTransfer.files[0], this.whenOpen) | ||
| } | ||
| } | ||
|
|
||
| this.copy = function (e) { | ||
| this.renderer.update() | ||
|
|
||
| if (e.target !== this.picker.input) { | ||
| e.clipboardData.setData('text/source', this.tool.export(this.tool.layer())) | ||
| e.clipboardData.setData('text/plain', this.tool.path()) | ||
| e.clipboardData.setData('text/html', this.manager.el.outerHTML) | ||
| e.clipboardData.setData('text/svg+xml', this.manager.el.outerHTML) | ||
| e.preventDefault() | ||
| } | ||
|
|
||
| this.renderer.update() | ||
| } | ||
|
|
||
| this.cut = function (e) { | ||
| this.renderer.update() | ||
|
|
||
| if (e.target !== this.picker.input) { | ||
| e.clipboardData.setData('text/source', this.tool.export(this.tool.layer())) | ||
| e.clipboardData.setData('text/plain', this.tool.export(this.tool.layer())) | ||
| e.clipboardData.setData('text/html', this.manager.el.outerHTML) | ||
| e.clipboardData.setData('text/svg+xml', this.manager.el.outerHTML) | ||
| this.tool.layers[this.tool.index] = [] | ||
| e.preventDefault() | ||
| } | ||
|
|
||
| this.renderer.update() | ||
| } | ||
|
|
||
| this.paste = function (e) { | ||
| if (e.target !== this.picker.el) { | ||
| let data = e.clipboardData.getData('text/source') | ||
| if (isJson(data)) { | ||
| data = JSON.parse(data.trim()) | ||
| this.tool.import(data) | ||
| } | ||
| e.preventDefault() | ||
| } | ||
|
|
||
| this.renderer.update() | ||
| } | ||
|
|
||
| this.onKeyDown = (e) => { | ||
| } | ||
|
|
||
| this.onKeyUp = (e) => { | ||
| } | ||
|
|
||
| function sizeOffset (a, b) { return { width: a.width - b.width, height: a.height - b.height } } | ||
| function printSize (size) { return `${size.width}x${size.height}` } | ||
| function isJson (text) { try { JSON.parse(text); return true } catch (error) { return false } } | ||
| function step (v, s) { return Math.round(v / s) * s } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| 'use strict' | ||
|
|
||
| function Cursor (client) { | ||
| this.pos = { x: 0, y: 0 } | ||
| this.lastPos = { x: 0, y: 0 } | ||
| this.translation = null | ||
| this.operation = null | ||
|
|
||
| this.translate = function (from = null, to = null, multi = false, copy = false, layer = false) { | ||
| if ((from || to) && this.translation === null) { this.translation = { multi: multi, copy: copy, layer: layer } } | ||
| if (from) { this.translation.from = from } | ||
| if (to) { this.translation.to = to } | ||
| if (!from && !to) { | ||
| this.translation = null | ||
| } | ||
| } | ||
|
|
||
| this.down = function (e) { | ||
| this.pos = this.atEvent(e) | ||
| if (client.tool.vertexAt(this.pos)) { | ||
| this.translate(this.pos, this.pos, e.shiftKey, e.ctrlKey || e.metaKey, e.altKey) | ||
| } | ||
| client.renderer.update() | ||
| client.interface.update() | ||
| e.preventDefault() | ||
| } | ||
|
|
||
| this.move = function (e) { | ||
| this.pos = this.atEvent(e) | ||
| if (this.translation) { | ||
| this.translate(null, this.pos) | ||
| } | ||
| if (this.lastPos.x !== this.pos.x || this.lastPos.y !== this.pos.y) { | ||
| client.renderer.update() | ||
| } | ||
| client.interface.update() | ||
| this.lastPos = this.pos | ||
| e.preventDefault() | ||
| } | ||
|
|
||
| this.up = function (e) { | ||
| this.pos = this.atEvent(e) | ||
| if (this.translation && !isEqual(this.translation.from, this.translation.to)) { | ||
| if (this.translation.layer === true) { client.tool.translateLayer(this.translation.from, this.translation.to) } else if (this.translation.copy) { client.tool.translateCopy(this.translation.from, this.translation.to) } else if (this.translation.multi) { client.tool.translateMulti(this.translation.from, this.translation.to) } else { client.tool.translate(this.translation.from, this.translation.to) } | ||
| } else if (e.target.id === 'guide') { | ||
| client.tool.addVertex({ x: this.pos.x, y: this.pos.y }) | ||
| client.picker.stop() | ||
| } | ||
| this.translate() | ||
| client.interface.update() | ||
| client.renderer.update() | ||
| e.preventDefault() | ||
| } | ||
|
|
||
| this.alt = function (e) { | ||
| this.pos = this.atEvent(e) | ||
| client.tool.removeSegmentsAt(this.pos) | ||
| e.preventDefault() | ||
| setTimeout(() => { | ||
| client.tool.clear() | ||
| }, 150) | ||
| } | ||
|
|
||
| this.atEvent = function (e) { | ||
| return this.snapPos(this.relativePos({ x: e.clientX, y: e.clientY })) | ||
| } | ||
|
|
||
| this.relativePos = function (pos) { | ||
| return { | ||
| x: pos.x - client.renderer.el.offsetLeft, | ||
| y: pos.y - client.renderer.el.offsetTop | ||
| } | ||
| } | ||
|
|
||
| this.snapPos = function (pos) { | ||
| return { | ||
| x: clamp(step(pos.x, 15), 15, client.tool.settings.size.width - 15), | ||
| y: clamp(step(pos.y, 15), 15, client.tool.settings.size.height - 15) | ||
| } | ||
| } | ||
|
|
||
| function isEqual (a, b) { return a.x === b.x && a.y === b.y } | ||
| function clamp (v, min, max) { return v < min ? min : v > max ? max : v } | ||
| function step (v, s) { return Math.round(v / s) * s } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| 'use strict' | ||
|
|
||
| /* global client */ | ||
|
|
||
| function Generator (layer, style) { | ||
| this.layer = layer | ||
| this.style = style | ||
|
|
||
| function operate (layer, offset, scale, mirror = 0, angle = 0) { | ||
| const l = copy(layer) | ||
|
|
||
| for (const k1 in l) { | ||
| const seg = l[k1] | ||
| for (const k2 in seg.vertices) { | ||
| if (mirror === 1 || mirror === 3) { seg.vertices[k2].x = (client.tool.settings.size.width) - seg.vertices[k2].x } | ||
| if (mirror === 2 || mirror === 3) { seg.vertices[k2].y = (client.tool.settings.size.height) - seg.vertices[k2].y } | ||
| // Offset | ||
| seg.vertices[k2].x += offset.x | ||
| seg.vertices[k2].y += offset.y | ||
| // Rotate | ||
| const center = { x: (client.tool.settings.size.width / 2) + offset.x + (7.5), y: (client.tool.settings.size.height / 2) + offset.y + 30 } | ||
| seg.vertices[k2] = rotatePoint(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) { | ||
| const type = segment.type | ||
| const vertices = segment.vertices | ||
| let html = '' | ||
| let skip = 0 | ||
|
|
||
| for (const id in vertices) { | ||
| if (skip > 0) { skip -= 1; continue } | ||
|
|
||
| const vertex = vertices[parseInt(id)] | ||
| const next = vertices[parseInt(id) + 1] | ||
| const afterNext = vertices[parseInt(id) + 2] | ||
|
|
||
| if (parseInt(id) === 0 && !prev) { | ||
| html += `M${vertex.x},${vertex.y} ` | ||
| } else if (parseInt(id) === 0 && prev && (prev.x !== vertex.x || prev.y !== vertex.y)) { | ||
| html += `M${vertex.x},${vertex.y} ` | ||
| } | ||
|
|
||
| if (type === 'line') { | ||
| html += this._line(vertex) | ||
| } else if (type === 'arc_c') { | ||
| const clock = mirror > 0 && mirror < 3 ? '0,0' : '0,1' | ||
| html += this._arc(vertex, next, clock) | ||
| } else if (type === 'arc_r') { | ||
| const clock = mirror > 0 && mirror < 3 ? '0,1' : '0,0' | ||
| html += this._arc(vertex, next, clock) | ||
| } else if (type === 'arc_c_full') { | ||
| const clock = mirror > 0 ? '1,0' : '1,1' | ||
| html += this._arc(vertex, next, clock) | ||
| } else if (type === 'arc_r_full') { | ||
| const clock = mirror > 0 ? '1,1' : '1,0' | ||
| html += this._arc(vertex, next, clock) | ||
| } else if (type === 'bezier') { | ||
| html += this._bezier(next, afterNext) | ||
| skip = 1 | ||
| } | ||
| } | ||
|
|
||
| if (segment.type === 'close') { | ||
| html += 'Z ' | ||
| } | ||
|
|
||
| return html | ||
| } | ||
|
|
||
| this._line = function (a) { | ||
| return `L${a.x},${a.y} ` | ||
| } | ||
|
|
||
| this._arc = function (a, b, c) { | ||
| if (!a || !b || !c) { return '' } | ||
|
|
||
| const offset = { x: b.x - a.x, y: b.y - a.y } | ||
|
|
||
| if (offset.x === 0 || offset.y === 0) { return this._line(b) } | ||
| return `A${Math.abs(b.x - a.x)},${Math.abs(b.y - a.y)} 0 ${c} ${b.x},${b.y} ` | ||
| } | ||
|
|
||
| this._bezier = function (a, b) { | ||
| if (!a || !b) { return '' } | ||
| return `Q${a.x},${a.y} ${b.x},${b.y} ` | ||
| } | ||
|
|
||
| this.convert = function (layer, mirror, angle) { | ||
| let s = '' | ||
| let prev = null | ||
| for (const id in layer) { | ||
| const seg = layer[parseInt(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) { | ||
| let s = this.convert(operate(this.layer, offset, scale)) | ||
|
|
||
| if (mirror === 1 || mirror === 2 || mirror === 3) { | ||
| s += this.convert(operate(this.layer, offset, scale, mirror), mirror) | ||
| } | ||
|
|
||
| return s | ||
| } | ||
|
|
||
| function copy (data) { return data ? JSON.parse(JSON.stringify(data)) : [] } | ||
| function rotatePoint (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) } } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| 'use strict' | ||
|
|
||
| function Interface (client) { | ||
| this.el = document.createElement('div') | ||
| this.el.id = 'interface' | ||
|
|
||
| this.el.appendChild(this.menu_el = document.createElement('div')) | ||
| this.menu_el.id = 'menu' | ||
|
|
||
| this.isVisible = true | ||
| this.zoom = false | ||
|
|
||
| this.install = function (host) { | ||
| host.appendChild(this.el) | ||
| } | ||
|
|
||
| this.start = function (host) { | ||
| let html = '' | ||
| const options = { | ||
| cast: { | ||
| line: { key: 'A', icon: 'M60,60 L240,240' }, | ||
| arc_c: { key: 'S', icon: 'M60,60 A180,180 0 0,1 240,240' }, | ||
| arc_r: { key: 'D', icon: 'M60,60 A180,180 0 0,0 240,240' }, | ||
| bezier: { key: 'F', icon: 'M60,60 Q60,150 150,150 Q240,150 240,240' }, | ||
| close: { key: 'Z', icon: 'M60,60 A180,180 0 0,1 240,240 M60,60 A180,180 0 0,0 240,240' } | ||
| }, | ||
| toggle: { | ||
| linecap: { key: 'Q', icon: 'M60,60 L60,60 L180,180 L240,180 L240,240 L180,240 L180,180' }, | ||
| linejoin: { key: 'W', icon: 'M60,60 L120,120 L180,120 M120,180 L180,180 L240,240' }, | ||
| thickness: { key: '', icon: 'M120,90 L120,90 L90,120 L180,210 L210,180 Z M105,105 L105,105 L60,60 M195,195 L195,195 L240,240' }, | ||
| mirror: { key: 'E', icon: 'M60,60 L60,60 L120,120 M180,180 L180,180 L240,240 M210,90 L210,90 L180,120 M120,180 L120,180 L90,210' }, | ||
| fill: { key: 'R', icon: 'M60,60 L60,150 L150,150 L240,150 L240,240 Z' } | ||
| }, | ||
| misc: { | ||
| color: { key: 'G', icon: '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' } | ||
| }, | ||
| source: { | ||
| open: { key: 'c-O', icon: 'M155,65 A90,90 0 0,1 245,155 A90,90 0 0,1 155,245 A90,90 0 0,1 65,155 A90,90 0 0,1 155,65 M155,95 A60,60 0 0,1 215,155 A60,60 0 0,1 155,215 A60,60 0 0,1 95,155 A60,60 0 0,1 155,95 ' }, | ||
| render: { key: 'c-R', icon: 'M155,65 A90,90 0 0,1 245,155 A90,90 0 0,1 155,245 A90,90 0 0,1 65,155 A90,90 0 0,1 155,65 M110,155 L110,155 L200,155 ' }, | ||
| export: { key: 'c-E', icon: 'M155,65 A90,90 0 0,1 245,155 A90,90 0 0,1 155,245 A90,90 0 0,1 65,155 A90,90 0 0,1 155,65 M110,140 L110,140 L200,140 M110,170 L110,170 L200,170' }, | ||
| save: { key: 'c-S', icon: 'M155,65 A90,90 0 0,1 245,155 A90,90 0 0,1 155,245 A90,90 0 0,1 65,155 A90,90 0 0,1 155,65 M110,155 L110,155 L200,155 M110,185 L110,185 L200,185 M110,125 L110,125 L200,125' }, | ||
| grid: { key: 'H', icon: 'M65,155 Q155,245 245,155 M65,155 Q155,65 245,155 M155,125 A30,30 0 0,1 185,155 A30,30 0 0,1 155,185 A30,30 0 0,1 125,155 A30,30 0 0,1 155,125 ' } | ||
| } | ||
| } | ||
|
|
||
| for (const type in options) { | ||
| const tools = options[type] | ||
| for (const name in tools) { | ||
| const tool = tools[name] | ||
| html += ` | ||
| <svg | ||
| id="option_${name}" | ||
| title="${capitalize(name)}" | ||
| onmouseout="client.interface.out('${type}','${name}')" | ||
| onmouseup="client.interface.up('${type}','${name}')" | ||
| onmousedown="client.interface.down('${type}','${name}', event)" | ||
| onmouseover="client.interface.over('${type}','${name}')" | ||
| viewBox="0 0 300 300" | ||
| class="icon ${type}"> | ||
| <path id="${name}_path" class="icon_path" d="${tool.icon}"/>${name === 'depth' ? '<path class="icon_path inactive" d=""/>' : ''} | ||
| <rect ar="${name}" width="300" height="300" opacity="0"> | ||
| <title>${capitalize(name)}${tool.key ? '(' + tool.key + ')' : ''}</title> | ||
| </rect> | ||
| </svg>` | ||
| } | ||
| } | ||
| this.menu_el.innerHTML = html | ||
| this.menu_el.appendChild(client.picker.el) | ||
| } | ||
|
|
||
| this.over = function (type, name) { | ||
| client.cursor.operation = {} | ||
| client.cursor.operation[type] = name | ||
| this.update(true) | ||
| client.renderer.update(true) | ||
| } | ||
|
|
||
| this.out = function (type, name) { | ||
| client.cursor.operation = '' | ||
| client.renderer.update(true) | ||
| } | ||
|
|
||
| this.up = function (type, name) { | ||
| if (!client.tool[type]) { console.warn(`Unknown option(type): ${type}.${name}`, client.tool); return } | ||
|
|
||
| this.update(true) | ||
| client.renderer.update(true) | ||
| } | ||
|
|
||
| this.down = function (type, name, event) { | ||
| if (!client.tool[type]) { console.warn(`Unknown option(type): ${type}.${name}`, client.tool); return } | ||
| const mod = event.button === 2 ? -1 : 1 | ||
| client.tool[type](name, mod) | ||
| this.update(true) | ||
| client.renderer.update(true) | ||
| } | ||
|
|
||
| this.prev_operation = null | ||
|
|
||
| this.update = function (force = false, id) { | ||
| if (this.prev_operation === client.cursor.operation && force === false) { return } | ||
|
|
||
| let multiVertices = null | ||
| const segments = client.tool.layer() | ||
| const sumSegments = client.tool.length() | ||
|
|
||
| for (const i in segments) { | ||
| if (segments[i].vertices.length > 2) { multiVertices = true; break } | ||
| } | ||
|
|
||
| document.getElementById('option_line').className.baseVal = !client.tool.canCast('line') ? 'icon inactive' : 'icon' | ||
| document.getElementById('option_arc_c').className.baseVal = !client.tool.canCast('arc_c') ? 'icon inactive' : 'icon' | ||
| document.getElementById('option_arc_r').className.baseVal = !client.tool.canCast('arc_r') ? 'icon inactive' : 'icon' | ||
| document.getElementById('option_bezier').className.baseVal = !client.tool.canCast('bezier') ? 'icon inactive' : 'icon' | ||
| document.getElementById('option_close').className.baseVal = !client.tool.canCast('close') ? 'icon inactive' : 'icon' | ||
|
|
||
| document.getElementById('option_thickness').className.baseVal = client.tool.layer().length < 1 ? 'icon inactive' : 'icon' | ||
| document.getElementById('option_linecap').className.baseVal = client.tool.layer().length < 1 ? 'icon inactive' : 'icon' | ||
| document.getElementById('option_linejoin').className.baseVal = client.tool.layer().length < 1 || !multiVertices ? 'icon inactive' : 'icon' | ||
| document.getElementById('option_mirror').className.baseVal = client.tool.layer().length < 1 ? 'icon inactive' : 'icon' | ||
| document.getElementById('option_fill').className.baseVal = client.tool.layer().length < 1 ? 'icon inactive' : 'icon' | ||
|
|
||
| document.getElementById('option_color').children[0].style.fill = client.tool.style().color | ||
| document.getElementById('option_color').children[0].style.stroke = client.tool.style().color | ||
| document.getElementById('option_color').className.baseVal = 'icon' | ||
|
|
||
| // Source | ||
|
|
||
| document.getElementById('option_save').className.baseVal = sumSegments < 1 ? 'icon inactive source' : 'icon source' | ||
| document.getElementById('option_export').className.baseVal = sumSegments < 1 ? 'icon inactive source' : 'icon source' | ||
| document.getElementById('option_render').className.baseVal = sumSegments < 1 ? 'icon inactive source' : 'icon source' | ||
|
|
||
| document.getElementById('option_grid').className.baseVal = client.renderer.showExtras ? 'icon inactive source' : 'icon source' | ||
|
|
||
| // Grid | ||
| if (client.renderer.showExtras) { document.getElementById('grid_path').setAttribute('d', 'M65,155 Q155,245 245,155 M65,155 Q155,65 245,155 M155,125 A30,30 0 0,1 185,155 A30,30 0 0,1 155,185 A30,30 0 0,1 125,155 A30,30 0 0,1 155,125 ') } else { document.getElementById('grid_path').setAttribute('d', 'M65,155 Q155,245 245,155 M65,155 ') } | ||
|
|
||
| // Mirror | ||
| if (client.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 (client.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 (client.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 (client.tool.style().mirror_style === 3) { document.getElementById('mirror_path').setAttribute('d', 'M60,60 L60,60 L120,120 L180,120 L210,90 M240,240 L240,240 L180,180 L120,180 L90,210') } else if (client.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 = client.cursor.operation | ||
| } | ||
|
|
||
| this.toggle = function () { | ||
| this.isVisible = !this.isVisible | ||
| this.el.className = this.isVisible ? 'visible' : 'hidden' | ||
| } | ||
|
|
||
| document.onkeydown = function (e) { | ||
| if (e.key === 'Tab') { | ||
| client.interface.toggle() | ||
| e.preventDefault() | ||
| } | ||
| } | ||
|
|
||
| function capitalize (str) { | ||
| return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase() | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| 'use strict' | ||
|
|
||
| function Acels (client) { | ||
| this.all = {} | ||
| this.roles = {} | ||
| this.pipe = null | ||
|
|
||
| this.install = (host = window) => { | ||
| host.addEventListener('keydown', this.onKeyDown, false) | ||
| host.addEventListener('keyup', this.onKeyUp, false) | ||
| } | ||
|
|
||
| this.set = (cat, name, accelerator, downfn, upfn) => { | ||
| if (this.all[accelerator]) { console.warn('Acels', `Trying to overwrite ${this.all[accelerator].name}, with ${name}.`) } | ||
| this.all[accelerator] = { cat, name, downfn, upfn, accelerator } | ||
| } | ||
|
|
||
| this.add = (cat, role) => { | ||
| this.all[':' + role] = { cat, name: role, role } | ||
| } | ||
|
|
||
| this.get = (accelerator) => { | ||
| return this.all[accelerator] | ||
| } | ||
|
|
||
| this.sort = () => { | ||
| const h = {} | ||
| for (const item of Object.values(this.all)) { | ||
| if (!h[item.cat]) { h[item.cat] = [] } | ||
| h[item.cat].push(item) | ||
| } | ||
| return h | ||
| } | ||
|
|
||
| this.convert = (event) => { | ||
| const accelerator = event.key === ' ' ? 'Space' : event.key.substr(0, 1).toUpperCase() + event.key.substr(1) | ||
| if ((event.ctrlKey || event.metaKey) && event.shiftKey) { | ||
| return `CmdOrCtrl+Shift+${accelerator}` | ||
| } | ||
| if (event.shiftKey && event.key.toUpperCase() !== event.key) { | ||
| return `Shift+${accelerator}` | ||
| } | ||
| if (event.altKey && event.key.length !== 1) { | ||
| return `Alt+${accelerator}` | ||
| } | ||
| if (event.ctrlKey || event.metaKey) { | ||
| return `CmdOrCtrl+${accelerator}` | ||
| } | ||
| return accelerator | ||
| } | ||
|
|
||
| this.pipe = (obj) => { | ||
| this.pipe = obj | ||
| } | ||
|
|
||
| this.onKeyDown = (e) => { | ||
| const target = this.get(this.convert(e)) | ||
| if (!target || !target.downfn) { return this.pipe ? this.pipe.onKeyDown(e) : null } | ||
| target.downfn() | ||
| e.preventDefault() | ||
| } | ||
|
|
||
| this.onKeyUp = (e) => { | ||
| const target = this.get(this.convert(e)) | ||
| if (!target || !target.upfn) { return this.pipe ? this.pipe.onKeyUp(e) : null } | ||
| target.upfn() | ||
| e.preventDefault() | ||
| } | ||
|
|
||
| this.toMarkdown = () => { | ||
| const cats = this.sort() | ||
| let text = '' | ||
| for (const cat in cats) { | ||
| text += `\n### ${cat}\n\n` | ||
| for (const item of cats[cat]) { | ||
| text += item.accelerator ? `- \`${item.accelerator}\`: ${item.name}\n` : '' | ||
| } | ||
| } | ||
| return text.trim() | ||
| } | ||
|
|
||
| this.toString = () => { | ||
| const cats = this.sort() | ||
| let text = '' | ||
| for (const cat in cats) { | ||
| for (const item of cats[cat]) { | ||
| text += item.accelerator ? `${cat}: ${item.name} | ${item.accelerator}\n` : '' | ||
| } | ||
| } | ||
| return text.trim() | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| 'use strict' | ||
|
|
||
| const fs = require('fs') | ||
| const libs = fs.readdirSync('./scripts/lib').filter((file) => { return file.indexOf('.js') > 0 && file !== 'build.js' }) | ||
| const scripts = fs.readdirSync('./scripts').filter((file) => { return file.indexOf('.js') > 0 }) | ||
| const styles = fs.readdirSync('./links').filter((file) => { return file.indexOf('.css') > 0 }) | ||
| const id = process.cwd().split('/').slice(-1)[0] | ||
|
|
||
| function cleanup (txt) { | ||
| const lines = txt.split('\n') | ||
| let output = '' | ||
| for (const line of lines) { | ||
| if (line.trim() === '') { continue } | ||
| if (line.trim().substr(0, 2) === '//') { continue } | ||
| if (line.indexOf('/*') > -1 && line.indexOf('*/') > -1) { continue } | ||
| output += line + '\n' | ||
| } | ||
| return output | ||
| } | ||
|
|
||
| const wrapper = ` | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <html> | ||
| <head> | ||
| <meta charset="utf-8"> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| <title>${id}</title> | ||
| </head> | ||
| <body> | ||
| <script> | ||
| ${libs.reduce((acc, item) => { return `${acc}// Including Library ${item}\n\n${fs.readFileSync('./scripts/lib/' + item, 'utf8')}\n` }, '')} | ||
| ${scripts.reduce((acc, item) => { return `${acc}// Including Script ${item}\n\n${fs.readFileSync('./scripts/' + item, 'utf8')}\n` }, '')} | ||
| const client = new Client() | ||
| client.install(document.body) | ||
| window.addEventListener('load', () => { | ||
| client.start() | ||
| }) | ||
| </script> | ||
| <style> | ||
| ${styles.reduce((acc, item) => { return `${acc}/* Including Style ${item} */ \n\n${fs.readFileSync('./links/' + item, 'utf8')}\n` }, '')} | ||
| </style> | ||
| </body> | ||
| </html>` | ||
|
|
||
| fs.writeFileSync('index.html', cleanup(wrapper)) | ||
|
|
||
| console.log(`Built ${id}`) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| 'use strict' | ||
|
|
||
| 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 } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| 'use strict' | ||
|
|
||
| /* global FileReader */ | ||
| /* global MouseEvent */ | ||
|
|
||
| function Source (client) { | ||
| this.cache = {} | ||
|
|
||
| this.install = () => { | ||
| } | ||
|
|
||
| this.start = () => { | ||
| this.new() | ||
| } | ||
|
|
||
| this.new = () => { | ||
| console.log('Source', 'New file..') | ||
| this.cache = {} | ||
| } | ||
|
|
||
| this.open = (ext, callback, store = false) => { | ||
| console.log('Source', 'Open file..') | ||
| const input = document.createElement('input') | ||
| input.type = 'file' | ||
| input.onchange = (e) => { | ||
| const file = e.target.files[0] | ||
| if (file.name.indexOf('.' + ext) < 0) { console.warn('Source', `Skipped ${file.name}`); return } | ||
| this.read(file, callback, store) | ||
| } | ||
| input.click() | ||
| } | ||
|
|
||
| this.load = (ext, callback) => { | ||
| console.log('Source', 'Load files..') | ||
| const input = document.createElement('input') | ||
| input.type = 'file' | ||
| input.setAttribute('multiple', 'multiple') | ||
| input.onchange = (e) => { | ||
| for (const file of e.target.files) { | ||
| if (file.name.indexOf('.' + ext) < 0) { console.warn('Source', `Skipped ${file.name}`); continue } | ||
| this.read(file, this.store) | ||
| } | ||
| } | ||
| input.click() | ||
| } | ||
|
|
||
| this.store = (file, content) => { | ||
| console.info('Source', 'Stored ' + file.name) | ||
| this.cache[file.name] = content | ||
| } | ||
|
|
||
| this.save = (name, content, type = 'text/plain', callback) => { | ||
| this.saveAs(name, content, type, callback) | ||
| } | ||
|
|
||
| this.saveAs = (name, ext, content, type = 'text/plain', callback) => { | ||
| console.log('Source', 'Save new file..') | ||
| this.write(name, ext, content, type, callback) | ||
| } | ||
|
|
||
| // I/O | ||
|
|
||
| this.read = (file, callback, store = false) => { | ||
| const reader = new FileReader() | ||
| reader.onload = (event) => { | ||
| const res = event.target.result | ||
| if (callback) { callback(file, res) } | ||
| if (store) { this.store(file, res) } | ||
| } | ||
| reader.readAsText(file, 'UTF-8') | ||
| } | ||
|
|
||
| this.write = (name, ext, content, type, settings = 'charset=utf-8') => { | ||
| const link = document.createElement('a') | ||
| link.setAttribute('download', `${name}-${timestamp()}.${ext}`) | ||
| if (type === 'image/png' || type === 'image/jpeg') { | ||
| link.setAttribute('href', content) | ||
| } else { | ||
| link.setAttribute('href', 'data:' + type + ';' + settings + ',' + encodeURIComponent(content)) | ||
| } | ||
| link.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window })) | ||
| } | ||
|
|
||
| function timestamp (d = new Date(), e = new Date(d)) { | ||
| return `${arvelie()}-${neralie()}` | ||
| } | ||
|
|
||
| function arvelie (date = new Date()) { | ||
| const start = new Date(date.getFullYear(), 0, 0) | ||
| const diff = (date - start) + ((start.getTimezoneOffset() - date.getTimezoneOffset()) * 60 * 1000) | ||
| const doty = Math.floor(diff / 86400000) - 1 | ||
| const y = date.getFullYear().toString().substr(2, 2) | ||
| const m = doty === 364 || doty === 365 ? '+' : String.fromCharCode(97 + Math.floor(doty / 14)).toUpperCase() | ||
| const d = `${(doty === 365 ? 1 : doty === 366 ? 2 : (doty % 14)) + 1}`.padStart(2, '0') | ||
| return `${y}${m}${d}` | ||
| } | ||
|
|
||
| function neralie (d = new Date(), e = new Date(d)) { | ||
| const ms = e - d.setHours(0, 0, 0, 0) | ||
| return (ms / 8640 / 10000).toFixed(6).substr(2, 6) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,170 @@ | ||
| 'use strict' | ||
|
|
||
| /* global localStorage */ | ||
| /* global FileReader */ | ||
| /* global DOMParser */ | ||
|
|
||
| function Theme (client) { | ||
| this.el = document.createElement('style') | ||
| this.el.type = 'text/css' | ||
|
|
||
| this.active = {} | ||
| this.default = { | ||
| background: '#eeeeee', | ||
| f_high: '#0a0a0a', | ||
| f_med: '#4a4a4a', | ||
| f_low: '#6a6a6a', | ||
| f_inv: '#111111', | ||
| b_high: '#a1a1a1', | ||
| b_med: '#c1c1c1', | ||
| b_low: '#ffffff', | ||
| b_inv: '#ffb545' | ||
| } | ||
|
|
||
| // Callbacks | ||
| this.onLoad = () => {} | ||
|
|
||
| this.install = (host = document.body) => { | ||
| window.addEventListener('dragover', this.drag) | ||
| window.addEventListener('drop', this.drop) | ||
| host.appendChild(this.el) | ||
| } | ||
|
|
||
| this.start = () => { | ||
| console.log('Theme', 'Starting..') | ||
| if (isJson(localStorage.theme)) { | ||
| const storage = JSON.parse(localStorage.theme) | ||
| if (isValid(storage)) { | ||
| console.log('Theme', 'Loading theme in localStorage..') | ||
| this.load(storage) | ||
| return | ||
| } | ||
| } | ||
| this.load(this.default) | ||
| } | ||
|
|
||
| this.open = () => { | ||
| console.log('Theme', 'Open theme..') | ||
| const input = document.createElement('input') | ||
| input.type = 'file' | ||
| input.onchange = (e) => { | ||
| this.read(e.target.files[0], this.load) | ||
| } | ||
| input.click() | ||
| } | ||
|
|
||
| this.load = (data) => { | ||
| const theme = this.parse(data) | ||
| if (!isValid(theme)) { console.warn('Theme', 'Invalid format'); return } | ||
| console.log('Theme', 'Loaded theme!') | ||
| this.el.innerHTML = `: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}; | ||
| }` | ||
| localStorage.setItem('theme', JSON.stringify(theme)) | ||
| this.active = theme | ||
| if (this.onLoad) { | ||
| this.onLoad(data) | ||
| } | ||
| } | ||
|
|
||
| this.reset = () => { | ||
| this.load(this.default) | ||
| } | ||
|
|
||
| this.set = (key, val) => { | ||
| if (!val) { return } | ||
| const hex = (`${val}`.substr(0, 1) !== '#' ? '#' : '') + `${val}` | ||
| if (!isColor(hex)) { console.warn('Theme', `${hex} is not a valid color.`); return } | ||
| this.active[key] = hex | ||
| } | ||
|
|
||
| this.read = (key) => { | ||
| return this.active[key] | ||
| } | ||
|
|
||
| this.parse = (any) => { | ||
| if (isValid(any)) { return any } | ||
| if (isJson(any)) { return JSON.parse(any) } | ||
| if (isHtml(any)) { return extract(any) } | ||
| } | ||
|
|
||
| // Drag | ||
|
|
||
| this.drag = (e) => { | ||
| e.stopPropagation() | ||
| e.preventDefault() | ||
| e.dataTransfer.dropEffect = 'copy' | ||
| } | ||
|
|
||
| this.drop = (e) => { | ||
| e.preventDefault() | ||
| const file = e.dataTransfer.files[0] | ||
| if (file.name.indexOf('.svg') > -1) { | ||
| this.read(file, this.load) | ||
| } | ||
| e.stopPropagation() | ||
| } | ||
|
|
||
| this.read = (file, callback) => { | ||
| const reader = new FileReader() | ||
| reader.onload = (event) => { | ||
| callback(event.target.result) | ||
| } | ||
| reader.readAsText(file, 'UTF-8') | ||
| } | ||
|
|
||
| // Helpers | ||
|
|
||
| function extract (xml) { | ||
| const svg = new DOMParser().parseFromString(xml, 'text/xml') | ||
| try { | ||
| return { | ||
| background: svg.getElementById('background').getAttribute('fill'), | ||
| f_high: svg.getElementById('f_high').getAttribute('fill'), | ||
| f_med: svg.getElementById('f_med').getAttribute('fill'), | ||
| f_low: svg.getElementById('f_low').getAttribute('fill'), | ||
| f_inv: svg.getElementById('f_inv').getAttribute('fill'), | ||
| b_high: svg.getElementById('b_high').getAttribute('fill'), | ||
| b_med: svg.getElementById('b_med').getAttribute('fill'), | ||
| b_low: svg.getElementById('b_low').getAttribute('fill'), | ||
| b_inv: svg.getElementById('b_inv').getAttribute('fill') | ||
| } | ||
| } catch (err) { | ||
| console.warn('Theme', 'Incomplete SVG Theme', err) | ||
| } | ||
| } | ||
|
|
||
| function isValid (json) { | ||
| if (!json) { return false } | ||
| if (!json.background || !isColor(json.background)) { return false } | ||
| if (!json.f_high || !isColor(json.f_high)) { return false } | ||
| if (!json.f_med || !isColor(json.f_med)) { return false } | ||
| if (!json.f_low || !isColor(json.f_low)) { return false } | ||
| if (!json.f_inv || !isColor(json.f_inv)) { return false } | ||
| if (!json.b_high || !isColor(json.b_high)) { return false } | ||
| if (!json.b_med || !isColor(json.b_med)) { return false } | ||
| if (!json.b_low || !isColor(json.b_low)) { return false } | ||
| if (!json.b_inv || !isColor(json.b_inv)) { return false } | ||
| return true | ||
| } | ||
|
|
||
| function isColor (hex) { | ||
| return /^#([0-9A-F]{3}){1,2}$/i.test(hex) | ||
| } | ||
|
|
||
| function isJson (text) { | ||
| try { JSON.parse(text); return true } catch (error) { return false } | ||
| } | ||
|
|
||
| function isHtml (text) { | ||
| try { new DOMParser().parseFromString(text, 'text/xml'); return true } catch (error) { return false } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| 'use strict' | ||
|
|
||
| /* global XMLSerializer */ | ||
| /* global btoa */ | ||
| /* global Image */ | ||
| /* global Blob */ | ||
|
|
||
| function Manager (client) { | ||
| // Create SVG parts | ||
| this.el = document.createElementNS('http://www.w3.org/2000/svg', 'svg') | ||
| this.el.setAttribute('xmlns', 'http://www.w3.org/2000/svg') | ||
| this.el.setAttribute('baseProfile', 'full') | ||
| this.el.setAttribute('version', '1.1') | ||
| this.el.style.fill = 'none' | ||
|
|
||
| this.layers = [] | ||
|
|
||
| this.install = function () { | ||
| this.el.appendChild(this.layers[2] = document.createElementNS('http://www.w3.org/2000/svg', 'path')) | ||
| this.el.appendChild(this.layers[1] = document.createElementNS('http://www.w3.org/2000/svg', 'path')) | ||
| this.el.appendChild(this.layers[0] = document.createElementNS('http://www.w3.org/2000/svg', 'path')) | ||
| } | ||
|
|
||
| this.update = function () { | ||
| this.el.setAttribute('width', (client.tool.settings.size.width) + 'px') | ||
| this.el.setAttribute('height', (client.tool.settings.size.height) + 'px') | ||
| this.el.style.width = (client.tool.settings.size.width) | ||
| this.el.style.height = client.tool.settings.size.height | ||
|
|
||
| const styles = client.tool.styles | ||
| const paths = client.tool.paths() | ||
|
|
||
| for (const id in this.layers) { | ||
| const style = styles[id] | ||
| const path = paths[id] | ||
| const layer = this.layers[id] | ||
|
|
||
| layer.style.strokeWidth = style.thickness | ||
| layer.style.strokeLinecap = style.strokeLinecap | ||
| layer.style.strokeLinejoin = style.strokeLinejoin | ||
| layer.style.stroke = style.color | ||
| layer.style.fill = style.fill | ||
|
|
||
| layer.setAttribute('d', path) | ||
| } | ||
| } | ||
|
|
||
| this.svg64 = function () { | ||
| const xml = new XMLSerializer().serializeToString(this.el) | ||
| const svg64 = btoa(xml) | ||
| const b64Start = 'data:image/svg+xml;base64,' | ||
| return b64Start + svg64 | ||
| } | ||
|
|
||
| // Exporters | ||
|
|
||
| this.toPNG = function (size = client.tool.settings.size, callback) { | ||
| this.update() | ||
|
|
||
| const image64 = this.svg64() | ||
| const img = new Image() | ||
| const canvas = document.createElement('canvas') | ||
| canvas.width = (size.width) * 2 | ||
| canvas.height = (size.height) * 2 | ||
| img.onload = function () { | ||
| canvas.getContext('2d').drawImage(img, 0, 0, (size.width) * 2, (size.height) * 2) | ||
| callback(canvas.toDataURL('image/png')) | ||
| } | ||
| img.src = image64 | ||
| } | ||
|
|
||
| this.toSVG = function (callback) { | ||
| this.update() | ||
|
|
||
| const image64 = this.svg64() | ||
| callback(image64, 'export.svg') | ||
| } | ||
|
|
||
| this.toGRID = function (callback) { | ||
| this.update() | ||
|
|
||
| const text = client.tool.export() | ||
| const file = new Blob([text], { type: 'text/plain' }) | ||
| callback(URL.createObjectURL(file), 'export.grid') | ||
| } | ||
|
|
||
| this.toString = () => { | ||
| return new XMLSerializer().serializeToString(this.el) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| 'use strict' | ||
|
|
||
| function Picker (client) { | ||
| this.memory = '' | ||
| this.el = document.createElement('div') | ||
| this.el.id = 'picker' | ||
| this.isActive = false | ||
| this.input = document.createElement('input') | ||
| this.input.id = 'picker_input' | ||
|
|
||
| this.el.appendChild(this.input) | ||
|
|
||
| this.start = function () { | ||
| if (this.isActive) { return } | ||
|
|
||
| this.isActive = true | ||
|
|
||
| this.input.setAttribute('placeholder', `${client.tool.style().color.replace('#', '').trim()}`) | ||
| this.input.setAttribute('maxlength', 6) | ||
|
|
||
| this.input.addEventListener('keydown', this.onKeyDown, false) | ||
| this.input.addEventListener('keyup', this.onKeyUp, false) | ||
|
|
||
| client.interface.el.className = 'picker' | ||
| this.input.focus() | ||
| this.input.value = '' | ||
|
|
||
| try { client.controller.set('picker') } catch (err) { } | ||
| } | ||
|
|
||
| this.update = function () { | ||
| if (!this.isActive) { return } | ||
| if (!isColor(this.input.value)) { return } | ||
|
|
||
| const hex = `#${this.input.value}` | ||
|
|
||
| document.getElementById('option_color').children[0].style.fill = hex | ||
| document.getElementById('option_color').children[0].style.stroke = hex | ||
| } | ||
|
|
||
| this.stop = function () { | ||
| if (!this.isActive) { return } | ||
|
|
||
| this.isActive = false | ||
|
|
||
| client.interface.el.className = '' | ||
| this.input.blur() | ||
| this.input.value = '' | ||
|
|
||
| try { client.controller.set() } catch (err) { console.log('No controller') } | ||
|
|
||
| setTimeout(() => { client.interface.update(true); client.renderer.update() }, 250) | ||
| } | ||
|
|
||
| this.validate = function () { | ||
| if (!isColor(this.input.value)) { return } | ||
|
|
||
| const hex = `#${this.input.value}` | ||
|
|
||
| client.tool.style().color = hex | ||
| client.tool.style().fill = client.tool.style().fill !== 'none' ? hex : 'none' | ||
|
|
||
| this.stop() | ||
| } | ||
|
|
||
| function isColor (val) { | ||
| if (val.length !== 3 && val.length !== 6) { | ||
| return false | ||
| } | ||
|
|
||
| const re = /[0-9A-Fa-f]/g | ||
| return re.test(val) | ||
| } | ||
|
|
||
| this.onKeyDown = (e) => { | ||
| e.stopPropagation() | ||
| if (e.key === 'Enter') { | ||
| this.validate() | ||
| e.preventDefault() | ||
| return | ||
| } | ||
| if (e.key === 'Escape') { | ||
| this.stop() | ||
| e.preventDefault() | ||
| } | ||
| } | ||
|
|
||
| this.onKeyUp = (e) => { | ||
| e.stopPropagation() | ||
| this.update() | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,263 @@ | ||
| 'use strict' | ||
|
|
||
| /* global Image */ | ||
| /* global Path2D */ | ||
| /* global Generator */ | ||
|
|
||
| function Renderer (client) { | ||
| 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.context = this.el.getContext('2d') | ||
| this.showExtras = true | ||
|
|
||
| this.scale = 2 // window.devicePixelRatio | ||
|
|
||
| this.start = function () { | ||
| this.update() | ||
| } | ||
|
|
||
| this.update = function (force = false) { | ||
| this.resize() | ||
| client.manager.update() | ||
| const render = new Image() | ||
| render.onload = () => { | ||
| this.draw(render) | ||
| } | ||
| render.src = client.manager.svg64() | ||
| } | ||
|
|
||
| this.draw = function (render) { | ||
| this.clear() | ||
| this.drawMirror() | ||
| this.drawGrid() | ||
| this.drawRulers() | ||
| this.drawRender(render) // | ||
| this.drawVertices() | ||
| this.drawHandles() | ||
| this.drawTranslation() | ||
| this.drawCursor() | ||
| this.drawPreview() | ||
| } | ||
|
|
||
| this.clear = function () { | ||
| this.context.clearRect(0, 0, this.el.width * this.scale, this.el.height * this.scale) | ||
| } | ||
|
|
||
| this.toggle = function () { | ||
| this.showExtras = !this.showExtras | ||
| this.update() | ||
| client.interface.update(true) | ||
| } | ||
|
|
||
| this.resize = function () { | ||
| const _target = client.getPaddedSize() | ||
| const _current = { width: this.el.width / this.scale, height: this.el.height / this.scale } | ||
| const offset = sizeOffset(_target, _current) | ||
| if (offset.width === 0 && offset.height === 0) { | ||
| return | ||
| } | ||
| console.log('Renderer', `Require resize: ${printSize(_target)}, from ${printSize(_current)}`) | ||
| this.el.width = (_target.width) * this.scale | ||
| this.el.height = (_target.height) * this.scale | ||
| this.el.style.width = (_target.width) + 'px' | ||
| this.el.style.height = (_target.height) + 'px' | ||
| } | ||
|
|
||
| // Collections | ||
|
|
||
| this.drawMirror = function () { | ||
| if (!this.showExtras) { return } | ||
|
|
||
| if (client.tool.style().mirror_style === 0) { return } | ||
|
|
||
| const middle = { x: client.tool.settings.size.width, y: client.tool.settings.size.height } | ||
|
|
||
| if (client.tool.style().mirror_style === 1 || client.tool.style().mirror_style === 3) { | ||
| this.drawRule({ x: middle.x, y: 15 * this.scale }, { x: middle.x, y: (client.tool.settings.size.height) * this.scale }) | ||
| } | ||
| if (client.tool.style().mirror_style === 2 || client.tool.style().mirror_style === 3) { | ||
| this.drawRule({ x: 15 * this.scale, y: middle.y }, { x: (client.tool.settings.size.width) * this.scale, y: middle.y }) | ||
| } | ||
| } | ||
|
|
||
| this.drawHandles = function () { | ||
| if (!this.showExtras) { return } | ||
|
|
||
| for (const segmentId in client.tool.layer()) { | ||
| const segment = client.tool.layer()[segmentId] | ||
| for (const vertexId in segment.vertices) { | ||
| const vertex = segment.vertices[vertexId] | ||
| this.drawHandle(vertex) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| this.drawVertices = function () { | ||
| for (const id in client.tool.vertices) { | ||
| this.drawVertex(client.tool.vertices[id]) | ||
| } | ||
| } | ||
|
|
||
| this.drawGrid = function () { | ||
| if (!this.showExtras) { return } | ||
|
|
||
| const markers = { w: parseInt(client.tool.settings.size.width / 15), h: parseInt(client.tool.settings.size.height / 15) } | ||
|
|
||
| for (let x = markers.w - 1; x >= 0; x--) { | ||
| for (let y = markers.h - 1; y >= 0; y--) { | ||
| const isStep = x % 4 === 0 && y % 4 === 0 | ||
| // Don't draw margins | ||
| if (x === 0 || y === 0) { continue } | ||
| this.drawMarker({ | ||
| x: parseInt(x * 15), | ||
| y: parseInt(y * 15) | ||
| }, isStep ? 2.5 : 1.5, client.theme.active.b_med) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| this.drawRulers = function () { | ||
| if (!client.cursor.translation) { return } | ||
|
|
||
| const pos = client.cursor.translation.to | ||
| const bottom = (client.tool.settings.size.height * this.scale) | ||
| const right = (client.tool.settings.size.width * this.scale) | ||
|
|
||
| this.drawRule({ x: pos.x * this.scale, y: 0 }, { x: pos.x * this.scale, y: bottom }) | ||
| this.drawRule({ x: 0, y: pos.y * this.scale }, { x: right, y: pos.y * this.scale }) | ||
| } | ||
|
|
||
| this.drawPreview = function () { | ||
| const operation = client.cursor.operation && client.cursor.operation.cast ? client.cursor.operation.cast : null | ||
|
|
||
| if (!client.tool.canCast(operation)) { return } | ||
| if (operation === 'close') { return } | ||
|
|
||
| const path = new Generator([{ vertices: client.tool.vertices, type: operation }]).toString({ x: 0, y: 0 }, 2) | ||
| const style = { | ||
| color: client.theme.active.f_med, | ||
| thickness: 2, | ||
| strokeLinecap: 'round', | ||
| strokeLinejoin: 'round', | ||
| strokeLineDash: [5, 15] | ||
| } | ||
| this.drawPath(path, style) | ||
| } | ||
|
|
||
| // Elements | ||
|
|
||
| this.drawMarker = function (pos, radius = 1, color) { | ||
| this.context.beginPath() | ||
| this.context.lineWidth = 2 | ||
| this.context.arc(pos.x * this.scale, pos.y * this.scale, radius, 0, 2 * Math.PI, false) | ||
| this.context.fillStyle = color | ||
| this.context.fill() | ||
| this.context.closePath() | ||
| } | ||
|
|
||
| this.drawVertex = function (pos, radius = 5) { | ||
| this.context.beginPath() | ||
| this.context.lineWidth = 2 | ||
| this.context.arc((pos.x * this.scale), (pos.y * this.scale), radius, 0, 2 * Math.PI, false) | ||
| this.context.fillStyle = client.theme.active.f_low | ||
| this.context.fill() | ||
| this.context.closePath() | ||
| } | ||
|
|
||
| this.drawRule = function (from, to) { | ||
| this.context.beginPath() | ||
| this.context.moveTo(from.x, from.y) | ||
| this.context.lineTo(to.x, to.y) | ||
| this.context.lineCap = 'round' | ||
| this.context.lineWidth = 3 | ||
| this.context.strokeStyle = client.theme.active.b_low | ||
| this.context.stroke() | ||
| this.context.closePath() | ||
| } | ||
|
|
||
| this.drawHandle = function (pos, radius = 6) { | ||
| this.context.beginPath() | ||
| this.context.arc(Math.abs(pos.x * -this.scale), Math.abs(pos.y * this.scale), radius + 3, 0, 2 * Math.PI, false) | ||
| this.context.fillStyle = client.theme.active.f_high | ||
| this.context.fill() | ||
| this.context.closePath() | ||
| this.context.beginPath() | ||
| this.context.arc((pos.x * this.scale), (pos.y * this.scale), radius - 3, 0, 2 * Math.PI, false) | ||
| this.context.fillStyle = client.theme.active.b_low | ||
| this.context.fill() | ||
| this.context.closePath() | ||
| } | ||
|
|
||
| this.drawPath = function (path, style) { | ||
| const p = new Path2D(path) | ||
|
|
||
| this.context.strokeStyle = style.color | ||
| this.context.lineWidth = style.thickness * this.scale | ||
| this.context.lineCap = style.strokeLinecap | ||
| this.context.lineJoin = style.strokeLinejoin | ||
|
|
||
| if (style.fill && style.fill !== 'none') { | ||
| this.context.fillStyle = style.color | ||
| this.context.fill(p) | ||
| } | ||
|
|
||
| // Dash | ||
| this.context.save() | ||
| if (style.strokeLineDash) { this.context.setLineDash(style.strokeLineDash) } else { this.context.setLineDash([]) } | ||
| this.context.stroke(p) | ||
| this.context.restore() | ||
| } | ||
|
|
||
| this.drawTranslation = function () { | ||
| if (!client.cursor.translation) { return } | ||
|
|
||
| this.context.save() | ||
|
|
||
| this.context.beginPath() | ||
| this.context.moveTo((client.cursor.translation.from.x * this.scale), (client.cursor.translation.from.y * this.scale)) | ||
| this.context.lineTo((client.cursor.translation.to.x * this.scale), (client.cursor.translation.to.y * this.scale)) | ||
| this.context.lineCap = 'round' | ||
| this.context.lineWidth = 5 | ||
| this.context.strokeStyle = client.cursor.translation.multi === true ? client.theme.active.b_inv : client.cursor.translation.copy === true ? client.theme.active.f_med : client.theme.active.f_low | ||
| this.context.setLineDash([5, 10]) | ||
| this.context.stroke() | ||
| this.context.closePath() | ||
|
|
||
| this.context.setLineDash([]) | ||
| this.context.restore() | ||
| } | ||
|
|
||
| this.drawCursor = function (pos = client.cursor.pos, radius = client.tool.style().thickness - 1) { | ||
| this.context.save() | ||
|
|
||
| this.context.beginPath() | ||
| this.context.lineWidth = 3 | ||
| this.context.lineCap = 'round' | ||
| this.context.arc(Math.abs(pos.x * -this.scale), Math.abs(pos.y * this.scale), 5, 0, 2 * Math.PI, false) | ||
| this.context.strokeStyle = client.theme.active.background | ||
| this.context.stroke() | ||
| this.context.closePath() | ||
|
|
||
| this.context.beginPath() | ||
| this.context.lineWidth = 3 | ||
| this.context.lineCap = 'round' | ||
| this.context.arc(Math.abs(pos.x * -this.scale), Math.abs(pos.y * this.scale), clamp(radius, 5, 100), 0, 2 * Math.PI, false) | ||
| this.context.strokeStyle = client.theme.active.f_med | ||
| this.context.stroke() | ||
| this.context.closePath() | ||
|
|
||
| this.context.restore() | ||
| } | ||
|
|
||
| this.drawRender = function (render) { | ||
| this.context.drawImage(render, 0, 0, this.el.width, this.el.height) | ||
| } | ||
|
|
||
| function printSize (size) { return `${size.width}x${size.height}` } | ||
| function sizeOffset (a, b) { return { width: a.width - b.width, height: a.height - b.height } } | ||
| function clamp (v, min, max) { return v < min ? min : v > max ? max : v } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,366 @@ | ||
| 'use strict' | ||
|
|
||
| /* global Generator */ | ||
|
|
||
| function Tool (client) { | ||
| this.index = 0 | ||
| this.settings = { size: { width: 600, height: 300 } } | ||
| this.layers = [[], [], []] | ||
| this.styles = [ | ||
| { thickness: 15, strokeLinecap: 'round', strokeLinejoin: 'round', color: '#f00', fill: 'none', mirror_style: 0, transform: 'rotate(45)' }, | ||
| { thickness: 15, strokeLinecap: 'round', strokeLinejoin: 'round', color: '#0f0', fill: 'none', mirror_style: 0, transform: 'rotate(45)' }, | ||
| { thickness: 15, strokeLinecap: 'round', strokeLinejoin: 'round', color: '#00f', fill: 'none', mirror_style: 0, transform: 'rotate(45)' } | ||
| ] | ||
| this.vertices = [] | ||
| this.reqs = { line: 2, arc_c: 2, arc_r: 2, arc_c_full: 2, arc_r_full: 2, bezier: 3, close: 0 } | ||
|
|
||
| this.start = function () { | ||
| this.styles[0].color = client.theme.active.f_high | ||
| this.styles[1].color = client.theme.active.f_med | ||
| this.styles[2].color = client.theme.active.f_low | ||
| } | ||
|
|
||
| this.erase = function () { | ||
| this.layers = [[], [], []] | ||
| } | ||
|
|
||
| this.reset = function () { | ||
| this.styles[0].mirror_style = 0 | ||
| this.styles[1].mirror_style = 0 | ||
| this.styles[2].mirror_style = 0 | ||
| this.styles[0].fill = 'none' | ||
| this.styles[1].fill = 'none' | ||
| this.styles[2].fill = 'none' | ||
| this.erase() | ||
| this.vertices = [] | ||
| this.index = 0 | ||
| } | ||
|
|
||
| this.clear = function () { | ||
| this.vertices = [] | ||
| client.renderer.update() | ||
| client.interface.update(true) | ||
| } | ||
|
|
||
| this.undo = function () { | ||
| this.layers = client.history.prev() | ||
| client.renderer.update() | ||
| client.interface.update(true) | ||
| } | ||
|
|
||
| this.redo = function () { | ||
| this.layers = client.history.next() | ||
| client.renderer.update() | ||
| client.interface.update(true) | ||
| } | ||
|
|
||
| this.length = function () { | ||
| return this.layers[0].length + this.layers[1].length + this.layers[2].length | ||
| } | ||
|
|
||
| // 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) | ||
| client.history.push(this.layers) | ||
| this.clear() | ||
| client.renderer.update() | ||
| client.interface.update(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 } | ||
| } | ||
|
|
||
| this.layers = dot.layers | ||
| this.styles = dot.styles | ||
| this.settings = dot.settings | ||
|
|
||
| this.clear() | ||
| client.fitSize() | ||
| client.renderer.update() | ||
| client.interface.update(true) | ||
| client.history.push(this.layers) | ||
| } | ||
|
|
||
| // EDIT | ||
|
|
||
| this.removeSegment = function () { | ||
| if (this.vertices.length > 0) { this.clear(); return } | ||
|
|
||
| this.layer().pop() | ||
| this.clear() | ||
| client.renderer.update() | ||
| client.interface.update(true) | ||
| } | ||
|
|
||
| this.removeSegmentsAt = function (pos) { | ||
| for (const segmentId in this.layer()) { | ||
| const segment = this.layer()[segmentId] | ||
| for (const vertexId in segment.vertices) { | ||
| const vertex = segment.vertices[vertexId] | ||
| if (Math.abs(pos.x) === Math.abs(vertex.x) && Math.abs(pos.y) === Math.abs(vertex.y)) { | ||
| segment.vertices.splice(vertexId, 1) | ||
| } | ||
| } | ||
| if (segment.vertices.length < 2) { | ||
| this.layers[this.index].splice(segmentId, 1) | ||
| } | ||
| } | ||
| this.clear() | ||
| client.renderer.update() | ||
| client.interface.update(true) | ||
| } | ||
|
|
||
| this.selectSegmentAt = function (pos, source = this.layer()) { | ||
| for (const segmentId in source) { | ||
| const segment = source[segmentId] | ||
| for (const vertexId in segment.vertices) { | ||
| const vertex = segment.vertices[vertexId] | ||
| if (vertex.x === Math.abs(pos.x) && vertex.y === Math.abs(pos.y)) { | ||
| return segment | ||
| } | ||
| } | ||
| } | ||
| return null | ||
| } | ||
|
|
||
| this.addVertex = function (pos) { | ||
| pos = { x: Math.abs(pos.x), y: Math.abs(pos.y) } | ||
| this.vertices.push(pos) | ||
| client.interface.update(true) | ||
| } | ||
|
|
||
| this.vertexAt = function (pos) { | ||
| for (const segmentId in this.layer()) { | ||
| const segment = this.layer()[segmentId] | ||
| for (const vertexId in segment.vertices) { | ||
| const vertex = segment.vertices[vertexId] | ||
| if (vertex.x === Math.abs(pos.x) && vertex.y === Math.abs(pos.y)) { | ||
| return vertex | ||
| } | ||
| } | ||
| } | ||
| return null | ||
| } | ||
|
|
||
| this.addSegment = function (type, vertices, index = this.index) { | ||
| const appendTarget = this.canAppend({ type: type, vertices: vertices }, index) | ||
| if (appendTarget) { | ||
| this.layer(index)[appendTarget].vertices = this.layer(index)[appendTarget].vertices.concat(vertices) | ||
| } else { | ||
| this.layer(index).push({ type: type, vertices: vertices }) | ||
| } | ||
| } | ||
|
|
||
| this.cast = function (type) { | ||
| if (!this.layer()) { this.layers[this.index] = [] } | ||
| if (!this.canCast(type)) { console.warn('Cannot cast'); return } | ||
|
|
||
| this.addSegment(type, this.vertices.slice()) | ||
|
|
||
| client.history.push(this.layers) | ||
|
|
||
| this.clear() | ||
| client.renderer.update() | ||
| client.interface.update(true) | ||
|
|
||
| console.log(`Casted ${type} -> ${this.layer().length} elements`) | ||
| } | ||
|
|
||
| this.i = { linecap: 0, linejoin: 0, thickness: 5 } | ||
|
|
||
| this.toggle = function (type, mod = 1) { | ||
| if (type === 'linecap') { | ||
| const a = ['butt', 'square', 'round'] | ||
| this.i.linecap += mod | ||
| this.style().strokeLinecap = a[this.i.linecap % a.length] | ||
| } else if (type === 'linejoin') { | ||
| const a = ['miter', 'round', 'bevel'] | ||
| this.i.linejoin += mod | ||
| this.style().strokeLinejoin = a[this.i.linejoin % a.length] | ||
| } else if (type === 'fill') { | ||
| this.style().fill = this.style().fill === 'none' ? this.style().color : 'none' | ||
| } else if (type === 'thickness') { | ||
| this.style().thickness = clamp(this.style().thickness + mod, 1, 100) | ||
| } else if (type === 'mirror') { | ||
| this.style().mirror_style = this.style().mirror_style > 2 ? 0 : this.style().mirror_style + 1 | ||
| } else { | ||
| console.warn('Unknown', type) | ||
| } | ||
| client.interface.update(true) | ||
| client.renderer.update() | ||
| } | ||
|
|
||
| this.misc = function (type) { | ||
| client.picker.start() | ||
| } | ||
|
|
||
| this.source = function (type) { | ||
| if (type === 'grid') { client.renderer.toggle() } | ||
| if (type === 'open') { client.source.open('grid', client.whenOpen) } | ||
| if (type === 'save') { client.source.write('dotgrid', 'grid', client.tool.export(), 'text/plain') } | ||
| if (type === 'export') { client.source.write('dotgrid', 'svg', client.manager.toString(), 'image/svg+xml') } | ||
| if (type === 'render') { client.manager.toPNG(client.tool.settings.size, (dataUrl) => { client.source.write('dotgrid', 'png', dataUrl, 'image/png') }) } | ||
| } | ||
|
|
||
| this.canAppend = function (content, index = this.index) { | ||
| for (const id in this.layer(index)) { | ||
| const stroke = this.layer(index)[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.canCast = function (type) { | ||
| if (!type) { return false } | ||
| // Cannot cast close twice | ||
| if (type === 'close') { | ||
| const 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 () { | ||
| const l1 = new Generator(client.tool.layers[0], client.tool.styles[0]).toString({ x: 0, y: 0 }, 1) | ||
| const l2 = new Generator(client.tool.layers[1], client.tool.styles[1]).toString({ x: 0, y: 0 }, 1) | ||
| const l3 = new Generator(client.tool.layers[2], client.tool.styles[2]).toString({ x: 0, y: 0 }, 1) | ||
|
|
||
| return [l1, l2, l3] | ||
| } | ||
|
|
||
| this.path = function () { | ||
| return new Generator(client.tool.layer(), client.tool.style()).toString({ x: 0, y: 0 }, 1) | ||
| } | ||
|
|
||
| this.translate = function (a, b) { | ||
| for (const segmentId in this.layer()) { | ||
| const segment = this.layer()[segmentId] | ||
| for (const vertexId in segment.vertices) { | ||
| const vertex = segment.vertices[vertexId] | ||
| if (vertex.x === Math.abs(a.x) && vertex.y === Math.abs(a.y)) { | ||
| segment.vertices[vertexId] = { x: Math.abs(b.x), y: Math.abs(b.y) } | ||
| } | ||
| } | ||
| } | ||
| client.history.push(this.layers) | ||
| this.clear() | ||
| client.renderer.update() | ||
| } | ||
|
|
||
| this.translateMulti = function (a, b) { | ||
| const offset = { x: a.x - b.x, y: a.y - b.y } | ||
| const segment = this.selectSegmentAt(a) | ||
|
|
||
| if (!segment) { return } | ||
|
|
||
| for (const vertexId in segment.vertices) { | ||
| const vertex = segment.vertices[vertexId] | ||
| segment.vertices[vertexId] = { x: vertex.x - offset.x, y: vertex.y - offset.y } | ||
| } | ||
|
|
||
| client.history.push(this.layers) | ||
| this.clear() | ||
| client.renderer.update() | ||
| } | ||
|
|
||
| this.translateLayer = function (a, b) { | ||
| const offset = { x: a.x - b.x, y: a.y - b.y } | ||
| for (const segmentId in this.layer()) { | ||
| const segment = this.layer()[segmentId] | ||
| for (const vertexId in segment.vertices) { | ||
| const vertex = segment.vertices[vertexId] | ||
| segment.vertices[vertexId] = { x: vertex.x - offset.x, y: vertex.y - offset.y } | ||
| } | ||
| } | ||
| client.history.push(this.layers) | ||
| this.clear() | ||
| client.renderer.update() | ||
| } | ||
|
|
||
| this.translateCopy = function (a, b) { | ||
| const offset = { x: a.x - b.x, y: a.y - b.y } | ||
| const segment = this.selectSegmentAt(a, copy(this.layer())) | ||
|
|
||
| if (!segment) { return } | ||
|
|
||
| for (const vertexId in segment.vertices) { | ||
| const vertex = segment.vertices[vertexId] | ||
| segment.vertices[vertexId] = { x: vertex.x - offset.x, y: vertex.y - offset.y } | ||
| } | ||
| this.layer().push(segment) | ||
|
|
||
| client.history.push(this.layers) | ||
| this.clear() | ||
| client.renderer.update() | ||
| } | ||
|
|
||
| this.merge = function () { | ||
| const merged = [].concat(this.layers[0]).concat(this.layers[1]).concat(this.layers[2]) | ||
| this.erase() | ||
| this.layers[this.index] = merged | ||
|
|
||
| client.history.push(this.layers) | ||
| this.clear() | ||
| client.renderer.update() | ||
| } | ||
|
|
||
| // Style | ||
|
|
||
| this.style = function () { | ||
| if (!this.styles[this.index]) { | ||
| this.styles[this.index] = [] | ||
| } | ||
| return this.styles[this.index] | ||
| } | ||
|
|
||
| // Layers | ||
|
|
||
| this.layer = function (index = this.index) { | ||
| if (!this.layers[index]) { | ||
| this.layers[index] = [] | ||
| } | ||
| return this.layers[index] | ||
| } | ||
|
|
||
| this.selectLayer = function (id) { | ||
| this.index = clamp(id, 0, 2) | ||
| this.clear() | ||
| client.renderer.update() | ||
| client.interface.update(true) | ||
| console.log(`layer:${this.index}`) | ||
| } | ||
|
|
||
| this.selectNextLayer = function () { | ||
| this.index = this.index >= 2 ? 0 : this.index++ | ||
| this.selectLayer(this.index) | ||
| } | ||
|
|
||
| this.selectPrevLayer = function () { | ||
| this.index = this.index >= 0 ? 2 : this.index-- | ||
| this.selectLayer(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 } | ||
| } |