pointvec/index.html

1621 lines
112 KiB
HTML
Raw Permalink Normal View History

2018-08-17 17:34:24 -04:00
<!DOCTYPE html>
<html lang="en">
2018-08-17 15:58:01 -04:00
<html>
<head>
2018-08-17 17:34:24 -04:00
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
2019-01-07 17:11:30 -05:00
<title>Dotgrid</title>
2018-08-17 15:58:01 -04:00
</head>
2020-03-23 21:41:27 -04:00
<body>
2018-09-11 23:27:01 -04:00
<script>
2020-03-23 21:41:27 -04:00
'use strict'
function Acels (client) {
2020-03-24 04:00:10 -04:00
this.el = document.createElement('ul')
this.el.id = 'acels'
this.order = []
2020-03-23 21:41:27 -04:00
this.all = {}
this.pipe = null
2020-03-24 04:00:10 -04:00
this.install = (host = document.body) => {
window.addEventListener('keydown', this.onKeyDown, false)
window.addEventListener('keyup', this.onKeyUp, false)
host.appendChild(this.el)
}
this.start = () => {
const cats = this.sort()
for (const cat of this.order) {
const main = document.createElement('li')
const head = document.createElement('a')
head.innerText = cat
const subs = document.createElement('ul')
for (const item of cats[cat]) {
const option = document.createElement('li')
option.onclick = item.downfn
option.innerHTML = item.accelerator ? `${item.name} <i>${item.accelerator.replace('CmdOrCtrl+', '^')}</i>` : `${item.name}`
subs.appendChild(option)
}
main.appendChild(head)
main.appendChild(subs)
this.el.appendChild(main)
}
2020-03-23 21:41:27 -04:00
}
this.set = (cat, name, accelerator, downfn, upfn) => {
if (this.all[accelerator]) { console.warn('Acels', `Trying to overwrite ${this.all[accelerator].name}, with ${name}.`) }
2020-03-24 04:00:10 -04:00
if (this.order.indexOf(cat) < 0) { this.order.push(cat) }
2020-03-23 21:41:27 -04:00
this.all[accelerator] = { cat, name, downfn, upfn, accelerator }
}
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) => {
2020-03-24 20:53:18 -04:00
const accelerator = event.key === ' ' ? 'Space' : capitalize(event.key.replace('Arrow', ''))
2020-03-23 21:41:27 -04:00
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
}
2020-03-24 04:00:10 -04:00
this.route = (obj) => {
2020-03-23 21:41:27 -04:00
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 = ''
2020-03-24 04:00:10 -04:00
for (const cat of this.order) {
2020-03-23 21:41:27 -04:00
for (const item of cats[cat]) {
2020-03-24 04:00:10 -04:00
text += item.accelerator ? `${cat.padEnd(8, ' ')} ${item.name.padEnd(16, ' ')} ${item.accelerator.replace('CmdOrCtrl+', '^')}\n` : ''
2020-03-23 21:41:27 -04:00
}
}
return text.trim()
}
2020-03-24 04:00:10 -04:00
this.toggle = () => {
this.el.className = this.el.className === 'hidden' ? '' : 'hidden'
}
2020-03-24 20:53:18 -04:00
function capitalize (s) { return s.substr(0, 1).toUpperCase() + s.substr(1) }
2020-03-23 21:41:27 -04:00
}
'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 }
}
'use strict'
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)
}
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)
}
}
'use strict'
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'
}
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
}
2020-03-25 20:32:58 -04:00
this.get = (key) => {
2020-03-23 21:41:27 -04:00
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) }
}
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')
}
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 }
}
}
'use strict'
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)
2020-03-24 04:00:10 -04:00
this.acels.set('∷', 'Toggle Menubar', 'Tab', () => { this.acels.toggle() })
this.acels.set('∷', 'Open Theme', 'CmdOrCtrl+Shift+O', () => { this.theme.open() })
this.acels.set('∷', 'Reset Theme', 'CmdOrCtrl+Backspace', () => { this.theme.reset() })
2020-03-24 05:35:43 -04:00
this.acels.set('File', 'New', 'CmdOrCtrl+N', () => { this.tool.erase(); this.update(); this.source.new() })
2020-03-23 21:41:27 -04:00
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') }) })
2020-03-24 20:53:18 -04:00
this.acels.set('Edit', 'Undo', 'CmdOrCtrl+Z', () => { this.tool.undo() })
this.acels.set('Edit', 'Redo', 'CmdOrCtrl+Shift+Z', () => { this.tool.redo() })
this.acels.set('View', 'Color Picker', 'G', () => { this.picker.start() })
this.acels.set('View', 'Toggle Grid', 'H', () => { this.renderer.toggle() })
this.acels.set('View', 'Toggle Tools', 'CmdOrCtrl+H', () => { this.interface.toggle() })
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() })
2020-03-23 21:41:27 -04:00
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() })
2020-03-24 20:53:18 -04:00
this.acels.set('Stroke', 'Erase Segment', 'Backspace', () => { this.tool.removeSegment() })
this.acels.set('Control', 'Add Point', 'Enter', () => { this.tool.addVertex(this.cursor.pos); this.renderer.update() })
this.acels.set('Control', 'Move Up', 'Up', () => { this.cursor.pos.y -= 15; this.renderer.update() })
this.acels.set('Control', 'Move Right', 'Right', () => { this.cursor.pos.x += 15; this.renderer.update() })
this.acels.set('Control', 'Move Down', 'Down', () => { this.cursor.pos.y += 15; this.renderer.update() })
this.acels.set('Control', 'Move Left', 'Left', () => { this.cursor.pos.x -= 15; this.renderer.update() })
this.acels.set('Control', 'Remove Point', 'X', () => { this.tool.removeSegmentsAt(this.cursor.pos) })
this.acels.set('Style', 'Linecap', 'Q', () => { this.tool.toggle('linecap') })
this.acels.set('Style', 'Linejoin', 'W', () => { this.tool.toggle('linejoin') })
this.acels.set('Style', 'Mirror', 'E', () => { this.tool.toggle('mirror') })
this.acels.set('Style', 'Fill', 'R', () => { this.tool.toggle('fill') })
this.acels.set('Style', 'Thicker', '}', () => { this.tool.toggle('thickness', 1) })
this.acels.set('Style', 'Thinner', '{', () => { this.tool.toggle('thickness', -1) })
this.acels.set('Style', 'Thicker +5', ']', () => { this.tool.toggle('thickness', 5) })
this.acels.set('Style', 'Thinner -5', '[', () => { this.tool.toggle('thickness', -5) })
2020-03-24 04:00:10 -04:00
this.acels.route(this)
2020-03-23 21:41:27 -04:00
this.manager.install()
this.interface.install(host)
this.theme.install(host, () => { this.update() })
2020-03-24 04:00:10 -04:00
this.acels.install(host)
2020-03-23 21:41:27 -04:00
}
this.start = () => {
console.log('Client', 'Starting..')
console.info(`${this.acels}`)
this.theme.start()
2020-03-24 04:00:10 -04:00
this.acels.start()
2020-03-23 21:41:27 -04:00
this.tool.start()
this.renderer.start()
this.interface.start()
2020-03-26 07:35:30 -04:00
this.history.push(this.layers) // initial state
2020-03-23 21:41:27 -04:00
this.source.new()
this.onResize()
2020-03-24 07:24:49 -04:00
this.interface.update(true) // force an update
2020-03-24 07:40:17 -04:00
setTimeout(() => { document.body.className += ' ready' }, 250)
2020-03-23 21:41:27 -04:00
}
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()
}
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()
}
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 }
}
'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 }
}
'use strict'
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 }
seg.vertices[k2].x += offset.x
seg.vertices[k2].y += offset.y
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)
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) } }
}
'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
2020-03-24 07:24:49 -04:00
const options = {
2020-03-24 07:40:17 -04:00
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 ' }
}
}
const mirrorPaths = [
'M60,60 L60,60 L120,120 M180,180 L180,180 L240,240 M210,90 L210,90 L180,120 M120,180 L120,180 L90,210',
'M60,60 L240,240 M180,120 L210,90 M120,180 L90,210',
'M210,90 L210,90 L90,210 M60,60 L60,60 L120,120 M180,180 L180,180 L240,240',
'M60,60 L60,60 L120,120 L180,120 L210,90 M240,240 L240,240 L180,180 L120,180 L90,210',
'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 '
]
2020-03-24 07:24:49 -04:00
this.install = function (host) {
host.appendChild(this.el)
}
this.start = function (host) {
let html = ''
2020-03-23 21:41:27 -04:00
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
2020-03-24 07:40:17 -04:00
for (const type in options) {
const tools = options[type]
for (const name in tools) {
const tool = tools[name]
tool.el = document.getElementById('option_' + name)
}
}
2020-03-23 21:41:27 -04:00
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 }
}
2020-03-24 07:40:17 -04:00
options.cast.line.el.className.baseVal = !client.tool.canCast('line') ? 'icon inactive' : 'icon'
options.cast.arc_c.el.className.baseVal = !client.tool.canCast('arc_c') ? 'icon inactive' : 'icon'
options.cast.arc_r.el.className.baseVal = !client.tool.canCast('arc_r') ? 'icon inactive' : 'icon'
options.cast.bezier.el.className.baseVal = !client.tool.canCast('bezier') ? 'icon inactive' : 'icon'
options.cast.close.el.className.baseVal = !client.tool.canCast('close') ? 'icon inactive' : 'icon'
options.toggle.thickness.el.className.baseVal = client.tool.layer().length < 1 ? 'icon inactive' : 'icon'
options.toggle.linecap.el.className.baseVal = client.tool.layer().length < 1 ? 'icon inactive' : 'icon'
options.toggle.linejoin.el.className.baseVal = client.tool.layer().length < 1 || !multiVertices ? 'icon inactive' : 'icon'
options.toggle.mirror.el.className.baseVal = client.tool.layer().length < 1 ? 'icon inactive' : 'icon'
options.toggle.fill.el.className.baseVal = client.tool.layer().length < 1 ? 'icon inactive' : 'icon'
options.misc.color.el.children[0].style.fill = client.tool.style().color
options.misc.color.el.children[0].style.stroke = client.tool.style().color
options.misc.color.el.className.baseVal = 'icon'
options.source.save.el.className.baseVal = sumSegments < 1 ? 'icon inactive source' : 'icon source'
options.source.export.el.className.baseVal = sumSegments < 1 ? 'icon inactive source' : 'icon source'
options.source.render.el.className.baseVal = sumSegments < 1 ? 'icon inactive source' : 'icon source'
options.source.grid.el.className.baseVal = client.renderer.showExtras ? 'icon inactive source' : 'icon source'
document.getElementById('grid_path').setAttribute('d', client.renderer.showExtras ? '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 ' : 'M65,155 Q155,245 245,155 M65,155 ')
document.getElementById('mirror_path').setAttribute('d', mirrorPaths[client.tool.style().mirror_style])
2020-03-23 21:41:27 -04:00
this.prev_operation = client.cursor.operation
}
this.toggle = function () {
this.isVisible = !this.isVisible
this.el.className = this.isVisible ? 'visible' : 'hidden'
}
function capitalize (str) {
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase()
}
}
'use strict'
function Manager (client) {
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
}
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)
}
}
'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()
}
}
'use strict'
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'
}
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) }
this.context.beginPath()
this.context.lineWidth = 2
this.context.fillStyle = client.theme.active.b_med
2020-03-23 21:41:27 -04:00
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
if (x === 0 || y === 0) { continue }
const pos = {
2020-03-23 21:41:27 -04:00
x: parseInt(x * 15),
y: parseInt(y * 15)
}
const radius = isStep ? 2.5 : 1.5
this.context.moveTo(pos.x * this.scale, pos.y * this.scale)
this.context.arc(pos.x * this.scale, pos.y * this.scale, radius, 0, 2 * Math.PI, false)
2020-03-23 21:41:27 -04:00
}
}
this.context.fill()
this.context.closePath()
2020-03-23 21:41:27 -04:00
}
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)
}
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)
}
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 }
}
'use strict'
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.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
}
2020-03-26 07:35:30 -04:00
this.erase = function () {
this.layers = [[], [], []]
this.vertices = []
client.renderer.update()
client.interface.update(true)
}
2020-03-23 21:41:27 -04:00
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
}
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)
}
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 }
if (type === 'close') {
const prev = this.layer()[this.layer().length - 1]
2020-03-24 07:17:13 -04:00
if (!prev || prev.type === 'close' || this.vertices.length !== 0) {
2020-03-23 21:41:27 -04:00
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()
}
this.style = function () {
if (!this.styles[this.index]) {
this.styles[this.index] = []
}
return this.styles[this.index]
}
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 }
}
2019-11-10 10:48:31 -05:00
const client = new Client()
client.install(document.body)
2019-11-03 13:36:58 -05:00
window.addEventListener('load', () => {
2019-11-10 10:48:31 -05:00
client.start()
2019-11-03 13:36:58 -05:00
})
2018-09-11 23:27:01 -04:00
</script>
2020-03-23 21:41:27 -04:00
<style>
* { margin:0;padding:0;border:0;outline:0;text-decoration:none;font-weight:inherit;font-style:inherit;color:inherit;font-size:100%;font-family:inherit;vertical-align:baseline;list-style:none;border-collapse:collapse;border-spacing:0; -webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale;}
2020-03-24 04:00:10 -04:00
@font-face {
font-family: 'input_mono_regular';
src: url(data:application/font-woff2;charset=utf-8;base64,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
font-weight: normal;
font-style: normal;
font-display: swap;
}
body { padding: 0px; font-family:"input_mono_regular",courier,monospace; -webkit-user-select: none; overflow: hidden; transition: background-color 500ms; -webkit-app-region: drag; padding: 30px;width:calc(100vw - 60px);height:calc(100vh - 60px)}
*:focus {outline: none; }
2020-03-23 21:41:27 -04:00
#guide { position: absolute;width: 300px;height: 300px; transition: opacity 150ms; -webkit-app-region: no-drag; border-radius: 3px;}
#render { display: none }
#vector { z-index: 1000;position: relative;width:300px; height:300px; }
#interface { font-size: 11px;line-height: 30px;text-transform: uppercase;-webkit-app-region: no-drag; transition: all 150ms; width: 100%; position:fixed; bottom:30px; left:40px; height:30px; max-width:calc(100vw - 75px); overflow: hidden;}
#interface.hidden { bottom:10px !important;opacity: 0 !important }
#interface.visible { bottom:28px !important; opacity: 1 !important}
#interface #menu { opacity: 1; position: absolute; top:0px; transition: all 250ms; z-index: 900; overflow: hidden; height:30px; width:100%;}
#interface #menu svg.icon { width:30px; height:30px; margin-right:-9px; opacity: 0.6; transition: opacity 250ms; }
#interface #menu svg.icon.inactive { opacity: 0.2 }
#interface #menu svg.icon:hover { cursor: pointer; opacity: 1.0 }
#interface #menu svg.icon:last-child { margin-right: 0; }
#interface #menu svg.icon path { fill:none; stroke-linecap: round; stroke-linejoin: round; stroke-width:12px; }
#interface #menu svg.icon.source { float:right; margin-left:-2px; margin-right:0px; }
#interface #menu svg.icon#option_color { opacity: 1.0; z-index:1001; position: relative; }
#interface #menu svg.icon#option_color:hover { opacity: 0.8 }
2020-03-24 04:00:10 -04:00
#interface #picker { position: absolute; line-height: 20px; z-index: 0; width: 30px; opacity: 0; transition: all 250ms; font-size: 11px; border-radius: 3px; left: 200px; top: 0px; text-transform: uppercase; height:20px; padding:5px 0px;left:280px; overflow:hidden;}
2020-03-23 21:41:27 -04:00
#interface #picker:before { content:"#"; position: absolute; left:10px; opacity: 0; transition: opacity 500ms}
#interface #picker input { background:transparent; position: absolute; left: 20px; height: 20px; width: 60px; line-height: 20px; opacity: 0; transition: opacity 500ms; text-transform: uppercase;}
#interface #color_path { transition: all 500ms; }
#interface.picker #menu { z-index: 0 }
#interface.picker #picker { width:30px; padding: 5px 15px; padding-right: 45px; opacity: 1; z-index: 900; width: 50px; left:200px; opacity: 1}
#interface.picker #picker:before { opacity: 1; }
#interface.picker #picker input { opacity: 1 }
#interface.picker #option_thickness { opacity: 0 !important }
#interface.picker #option_mirror { opacity: 0 !important }
#interface.picker #option_fill { opacity: 0 !important }
body.web #interface #menu #option_open { display: none; }
body #guide { opacity: 0; transition: opacity 500ms; }
body.ready #guide { opacity: 1 }
body #interface { opacity: 0; transition: opacity 250ms, bottom 500ms; bottom:15px; }
body.ready #interface { opacity: 1; bottom:30px; }
@media (max-width: 560px) {
#interface #menu svg.icon.source { opacity: 0; }
}
2020-03-24 04:00:10 -04:00
#acels { position: fixed;width: 30px;background: red;top: 0;left: 0; width: 100vw; color:black; background:white; font-size:11px; line-height: 20px; transition: margin-top 0.25s; z-index: 9999; padding-left: 25px; }
#acels.hidden { margin-top:-20px; }
#acels.hidden > li > ul > li { display: none }
#acels > li { float: left; position: relative; cursor: pointer; padding:0px 5px; display: inline-block; }
#acels > li:hover { background: black; color:white; }
#acels > li > ul { display: none; position: absolute; background:white; position: absolute; top:20px; left:0px; color:black; width: 200px}
#acels > li:hover > ul { display: block; }
#acels > li > ul > li { padding: 0px 10px; display: block }
#acels > li > ul > li:hover { background: #ccc; }
#acels > li > ul > li > i { display: inline-block; float: right; color: #aaa; }
2020-03-23 21:41:27 -04:00
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; }
</style>
2018-08-17 15:58:01 -04:00
</body>
</html>