Removed traces of Electron

This commit is contained in:
neauoire 2020-03-24 10:41:27 +09:00
parent fdb0054579
commit 2eca003904
18 changed files with 3540 additions and 40 deletions

18
README.txt Normal file
View File

@ -0,0 +1,18 @@
DOTGRID
Dotgrid is a grid-based vector drawing software designed to create logos, icons and type.
It supports layers, the full SVG specs and additional effects such as mirroring and radial drawing.
Dotgrid exports to both PNG and SVG files.
The application was initially created for internal use,
and later made available as a free and open source software.
- Guide: https://100r.co/site/dotgrid.html
- Video Tutorial: https://www.youtube.com/watch?v=Xt1zYHhpypk
- Community: https://hundredrabbits.itch.io/dotgrid/community
Extras
- Themes: https://github.com/hundredrabbits/Themes
- Support: https://patreon.com/100)
- Pull Requests are welcome!

1585
index.html

File diff suppressed because one or more lines are too long

57
links/main.css Normal file

File diff suppressed because one or more lines are too long

13
links/theme.css Normal file
View File

@ -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; }

11
push.sh Executable file
View File

@ -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'

266
scripts/client.js Normal file
View File

@ -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 }
}

85
scripts/cursor.js Normal file
View File

@ -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 }
}

118
scripts/generator.js Normal file
View File

@ -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) } }
}

159
scripts/interface.js Normal file
View File

@ -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()
}
}

92
scripts/lib/acels.js Normal file
View File

@ -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()
}
}

48
scripts/lib/build.js Normal file
View File

@ -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}`)

45
scripts/lib/history.js Normal file
View File

@ -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 }
}

102
scripts/lib/source.js Normal file
View File

@ -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)
}
}

170
scripts/lib/theme.js Normal file
View File

@ -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 }
}
}

90
scripts/manager.js Normal file
View File

@ -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)
}
}

92
scripts/picker.js Normal file
View File

@ -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()
}
}

263
scripts/renderer.js Normal file
View File

@ -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 }
}

366
scripts/tool.js Normal file
View File

@ -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 }
}