diff --git a/README.txt b/README.txt
index 1da749d..1f9e8a6 100644
--- a/README.txt
+++ b/README.txt
@@ -29,23 +29,29 @@ Project Clean Escape
Example
-; clear screen
(clear)
-
-; draw red square
(stroke
(rect 30 30 100 100) "red" 2)
-
-; download result
(export)
Helpers
-Ronin helpers are keywords that facilitates adding coordinates from the canvas into your script. The currently supported helpers are `$rect`, `$pos`, `$circle`, `$line`, `$drag` and `$view`. Holding right-click while using a $helper will run the script as the mouse is injecting coordinates into the script. Paste the following script, and trace a shape in the canvas:
+Ronin helpers are keywords that facilitates adding coordinates from the canvas into your script. The currently supported helpers are $rect, $pos, $line, $circle & $arc. Holding right-click while using a $helper will run the script as the mouse is injecting coordinates into the script. Paste the following script, and trace a shape in the canvas:
-; draw a red circle
(fill $circle "red")
+Additional helpers are also available to change parts of a shape, these are as follow: $x, $y, $xy, $wh, $a & $r. Paste the following script, and change the position and radius of a circle:
+
+(clear)
+(fill
+ (circle $xy $r) "red")
+
+Extra helpers are available for various transformations, these are as follow: $drag, $view, $poly, $move & $rotate. Paste the following script, and draw the vertices of a line, press escape to stop:
+
+(clear)
+(stroke
+ $poly "red")
+
Import/Export
To save an image in memory, open an image file with Ronin, or drag an image file on the window. You will then be able to import it by using the file image's name. If the image file is `preview.png`, you can import it as follow:
diff --git a/debug.html b/debug.html
index a542bd5..1abce14 100644
--- a/debug.html
+++ b/debug.html
@@ -10,12 +10,13 @@
-
+
+
diff --git a/examples/projects/elementary-theme.lisp b/examples/projects/elementary-theme.lisp
index 79252b3..8509562 100644
--- a/examples/projects/elementary-theme.lisp
+++ b/examples/projects/elementary-theme.lisp
@@ -1,80 +1,78 @@
(clear)
;
-(open $path)
+(open $path 0.75)
; picker to elementary
-(def unit 40)
+(def unit 10)
(def f
(get-frame))
(def pos-row-1
- (sub f:h
- (mul unit 4)))
+ (sub f:h 80))
(def pos-row-2
- (sub f:h
- (mul unit 2)))
+ (sub f:h 45))
; color picker
(def color-1
(pick
(guide
- (rect $xy unit unit))))
+ (rect 846 220 unit unit))))
(def color-2
(pick
(guide
- (rect $xy unit unit))))
+ (rect 584 364 unit unit))))
(def color-3
(pick
(guide
- (rect $xy unit unit))))
+ (rect 70 538 unit unit))))
(def color-4
(pick
(guide
- (rect $xy unit unit))))
+ (rect 468 650 unit unit))))
(def color-5
(pick
(guide
- (rect $xy unit unit))))
+ (rect 254 246 unit unit))))
(def color-6
(pick
(guide
- (rect $xy unit unit))))
+ (rect 190 502 unit unit))))
(def color-7
(pick
(guide
- (rect $xy unit unit))))
+ (rect 1084 446 unit unit))))
(def color-8
(pick
(guide
- (rect $xy unit unit))))
+ (rect 1068 730 unit unit))))
; display
(fill
(circle
- (mul unit 2) pos-row-1 unit) color-1)
+ (mul 20 2) pos-row-1 18) color-1)
(fill
(circle
- (mul unit 4) pos-row-1 unit) color-2)
+ (mul 20 4) pos-row-1 18) color-2)
(fill
(circle
- (mul unit 6) pos-row-1 unit) color-3)
+ (mul 20 6) pos-row-1 18) color-3)
(fill
(circle
- (mul unit 8) pos-row-1 unit) color-4)
+ (mul 20 8) pos-row-1 18) color-4)
(fill
(circle
- (mul unit 3) pos-row-2 unit) color-5)
+ (mul 20 3) pos-row-2 18) color-5)
(fill
(circle
- (mul unit 5) pos-row-2 unit) color-6)
+ (mul 20 5) pos-row-2 18) color-6)
(fill
(circle
- (mul unit 7) pos-row-2 unit) color-7)
+ (mul 20 7) pos-row-2 18) color-7)
(fill
(circle
- (mul unit 9) pos-row-2 unit) color-8)
+ (mul 20 9) pos-row-2 18) color-8)
;
(def res
(add color-1:hex ":" color-2:hex ":" color-3:hex ":" color-4:hex ":" color-5:hex ":" color-6:hex ":" color-7:hex ":" color-8:hex))
diff --git a/index.html b/index.html
index cedf814..8e8e6a2 100644
--- a/index.html
+++ b/index.html
@@ -116,6 +116,778 @@ function Acels (client) {
function capitalize (s) { return s.substr(0, 1).toUpperCase() + s.substr(1) }
}
'use strict'
+function Lain (lib = {}) {
+ const TYPES = { identifier: 0, number: 1, string: 2, bool: 3, symbol: 4 }
+ const Context = function (scope, parent) {
+ this.scope = scope
+ this.parent = parent
+ this.get = function (identifier) {
+ if (identifier in this.scope) {
+ return this.scope[identifier]
+ } else if (this.parent !== undefined) {
+ return this.parent.get(identifier)
+ }
+ }
+ }
+ const special = {
+ let: function (input, context) {
+ const letContext = input[1].reduce(function (acc, x) {
+ acc.scope[x[0].value] = interpret(x[1], context)
+ return acc
+ }, new Context({}, context))
+ return interpret(input[2], letContext)
+ },
+ def: function (input, context) {
+ if (input.length !== 3) { console.warn('Lain', 'Invalid definition.'); return }
+ const identifier = input[1].host ? input[1].host : input[1].value
+ if (input[1].host) {
+ if (!context.scope[identifier]) { context.scope[identifier] = {} }
+ context.scope[identifier][input[1].value] = interpret(input[2], context)
+ return context.scope[identifier][input[1].value]
+ }
+ context.scope[identifier] = interpret(input[2], context)
+ return context.scope[identifier]
+ },
+ defn: function (input, context) {
+ const identifier = input[1].value
+ if (context.scope[identifier]) { console.warn('Lain', `Redefining function: ${identifier}`) }
+ const fnParams = input[2].type === TYPES.string && input[3] ? input[3] : input[2]
+ const fnBody = input[2].type === TYPES.string && input[4] ? input[4] : input[3]
+ context.scope[identifier] = function () {
+ const lambdaArguments = arguments
+ const lambdaScope = fnParams.reduce(function (acc, x, i) {
+ acc[x.value] = lambdaArguments[i]
+ return acc
+ }, {})
+ return interpret(fnBody, new Context(lambdaScope, context))
+ }
+ },
+ λ: function (input, context) {
+ return function () {
+ const lambdaArguments = arguments
+ const lambdaScope = input[1].reduce(function (acc, x, i) {
+ acc[x.value] = lambdaArguments[i]
+ return acc
+ }, {})
+ return interpret(input[2], new Context(lambdaScope, context))
+ }
+ },
+ if: function (input, context) {
+ return interpret(input[1], context) ? interpret(input[2], context) : input[3] ? interpret(input[3], context) : []
+ }
+ }
+ const interpretList = function (input, context) {
+ if (input.length > 0 && input[0].value in special) {
+ return special[input[0].value](input, context)
+ }
+ const list = []
+ for (let i = 0; i < input.length; i++) {
+ if (input[i].type === TYPES.symbol) {
+ if (input[i].host) {
+ const host = context.get(input[i].host)
+ if (host) {
+ list.push(host[input[i].value])
+ }
+ } else {
+ list.push(obj => obj[input[i].value])
+ }
+ } else {
+ list.push(interpret(input[i], context))
+ }
+ }
+ return list[0] instanceof Function ? list[0].apply(undefined, list.slice(1)) : list
+ }
+ const interpret = function (input, context) {
+ if (!input) { console.warn('Lain', context.scope); return null }
+ if (context === undefined) {
+ return interpret(input, new Context(lib))
+ } else if (input instanceof Array) {
+ return interpretList(input, context)
+ } else if (input.type === TYPES.identifier) {
+ return context.get(input.value)
+ } else if (input.type === TYPES.number || input.type === TYPES.symbol || input.type === TYPES.string || input.type === TYPES.bool) {
+ return input.value
+ }
+ }
+ const categorize = function (input) {
+ if (!isNaN(parseFloat(input))) {
+ return { type: TYPES.number, value: parseFloat(input) }
+ } else if (input[0] === '"' && input.slice(-1) === '"') {
+ return { type: TYPES.string, value: input.slice(1, -1) }
+ } else if (input[0] === ':') {
+ return { type: TYPES.symbol, value: input.slice(1) }
+ } else if (input.indexOf(':') > 0) {
+ return { type: TYPES.symbol, host: input.split(':')[0], value: input.split(':')[1] }
+ } else if (input === 'true' || input === 'false') {
+ return { type: TYPES.bool, value: input === 'true' }
+ } else {
+ return { type: TYPES.identifier, value: input }
+ }
+ }
+ const parenthesize = function (input, list) {
+ if (list === undefined) { return parenthesize(input, []) }
+ const token = input.shift()
+ if (token === undefined) {
+ return list.pop()
+ } else if (token === '(') {
+ list.push(parenthesize(input, []))
+ return parenthesize(input, list)
+ } else if (token === ')') {
+ return list
+ } else {
+ return parenthesize(input, list.concat(categorize(token)))
+ }
+ }
+ const tokenize = function (input) {
+ const i = input.replace(/^[\s]*;.*\n?/gm, '').split('"')
+ return i.map(function (x, i) {
+ return i % 2 === 0 ? x.replace(/\(/g, ' ( ').replace(/\)/g, ' ) ') : x.replace(/ /g, '!ws!')
+ }).join('"').trim().split(/\s+/).map(function (x) { return x.replace(/!ws!/g, ' ') })
+ }
+ this.run = (input) => {
+ return interpret(parenthesize(tokenize(input)))
+ }
+}
+'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
+ }
+ this.get = (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) }
+ }
+ 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.el = document.createElement('div')
+ this.el.id = 'ronin'
+ this.acels = new Acels(this)
+ this.theme = new Theme(this)
+ this.source = new Source(this)
+ this.commander = new Commander(this)
+ this.surface = new Surface(this)
+ this.library = new Library(this)
+ this.lain = new Lain(this.library)
+ this.bindings = {}
+ this.install = function (host = document.body) {
+ this._wrapper = document.createElement('div')
+ this._wrapper.id = 'wrapper'
+ this.commander.install(this._wrapper)
+ this.surface.install(this._wrapper)
+ this.el.appendChild(this._wrapper)
+ host.appendChild(this.el)
+ this.theme.install(host)
+ this.acels.install(host)
+ window.addEventListener('dragover', this.onDrag)
+ window.addEventListener('drop', this.onDrop)
+ 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() })
+ this.acels.set('File', 'New', 'CmdOrCtrl+N', () => { this.source.new(); this.surface.clear(); this.commander.clear() })
+ this.acels.set('File', 'Save', 'CmdOrCtrl+S', () => { this.source.write('ronin', 'lisp', this.commander._input.value, 'text/plain') })
+ this.acels.set('File', 'Export Image', 'CmdOrCtrl+E', () => { this.source.write('ronin', 'png', this.surface.el.toDataURL('image/png', 1.0), 'image/png') })
+ this.acels.set('File', 'Open', 'CmdOrCtrl+U', () => { this.source.open('lisp', this.whenOpen) })
+ this.acels.add('Edit', 'undo')
+ this.acels.add('Edit', 'redo')
+ this.acels.add('Edit', 'cut')
+ this.acels.add('Edit', 'copy')
+ this.acels.add('Edit', 'paste')
+ this.acels.add('Edit', 'selectAll')
+ this.acels.set('View', 'Toggle Guides', 'CmdOrCtrl+Shift+H', () => { this.surface.toggleGuides() })
+ this.acels.set('View', 'Toggle Commander', 'CmdOrCtrl+K', () => { this.commander.toggle() })
+ this.acels.set('View', 'Expand Commander', 'CmdOrCtrl+Shift+K', () => { this.commander.toggle(true) })
+ this.acels.set('Project', 'Run', 'CmdOrCtrl+R', () => { this.commander.run() })
+ this.acels.set('Project', 'Re-Indent', 'CmdOrCtrl+Shift+I', () => { this.commander.reindent() })
+ this.acels.set('Project', 'Clean', 'Escape', () => { this.commander.cleanup() })
+ this.acels.route(this)
+ }
+ this.start = function () {
+ console.log('Ronin', 'Starting..')
+ console.info(`${this.acels}`)
+ this.theme.start()
+ this.acels.start()
+ this.source.start()
+ this.commander.start()
+ this.surface.start()
+ this.loop()
+ }
+ this.whenOpen = (file, res) => {
+ console.log(file, res)
+ this.commander.load(res)
+ this.commander.show()
+ }
+ this.loop = () => {
+ if (this.bindings.animate && typeof this.bindings.animate === 'function') {
+ this.bindings.animate()
+ }
+ requestAnimationFrame(() => this.loop())
+ }
+ this.log = (...msg) => {
+ this.commander.setStatus(msg.reduce((acc, val) => {
+ return acc + JSON.stringify(val).replace(/"/g, '').trim() + ' '
+ }, ''))
+ }
+ this.bind = (event, fn) => {
+ this.bindings[event] = fn
+ }
+ this.mouseOrigin = null
+ this.onMouseDown = (e, id = 'mouse-down') => {
+ const pos = { x: e.offsetX * this.surface.ratio, y: e.offsetY * this.surface.ratio }
+ this.mouseOrigin = pos
+ const shape = this.mouseShape(pos, id)
+ if (this.bindings[id]) {
+ this.bindings[id](shape)
+ }
+ this.commander.capture()
+ this.surface.clearGuide()
+ this.surface.drawGuide(shape)
+ }
+ this.onKeyPress = (e, id = 'key-press') => {
+ if (this.bindings[id]) {
+ this.bindings[id](e)
+ }
+ }
+ this.onKeyDown = (e, id = 'key-down') => {
+ if (this.bindings[id]) {
+ this.bindings[id](e)
+ }
+ }
+ this.onKeyUp = (e, id = 'key-up') => {
+ if (this.bindings[id]) {
+ this.bindings[id](e)
+ }
+ }
+ this.onMouseMove = (e, id = 'mouse-move') => {
+ const pos = { x: e.offsetX * this.surface.ratio, y: e.offsetY * this.surface.ratio }
+ const shape = this.mouseShape(pos, id)
+ if (this.bindings[id]) {
+ this.bindings[id](shape)
+ }
+ if (this.mouseOrigin) {
+ this.commander.commit(shape, false, e.which !== 1)
+ this.surface.clearGuide()
+ this.surface.drawGuide(shape)
+ }
+ }
+ this.onMouseUp = (e, id = 'mouse-up') => {
+ const pos = { x: e.offsetX * this.surface.ratio, y: e.offsetY * this.surface.ratio }
+ const shape = this.mouseShape(pos, id)
+ if (this.bindings[id]) {
+ this.bindings[id](shape)
+ }
+ if (this.mouseOrigin) {
+ this.commander.commit(shape, true, e.which !== 1)
+ }
+ this.mouseOrigin = null
+ this.surface.clearGuide()
+ }
+ this.onMouseOver = (e) => {
+ this.mouseOrigin = null
+ }
+ this.onMouseOut = (e) => {
+ this.mouseOrigin = null
+ }
+ this.onDrag = (e) => {
+ e.stopPropagation()
+ e.preventDefault()
+ e.dataTransfer.dropEffect = 'copy'
+ }
+ this.onDrop = (e) => {
+ e.preventDefault()
+ e.stopPropagation()
+ const file = e.dataTransfer.files[0]
+ if (file.name.indexOf('.lisp') > -1) {
+ this.source.read(file, this.whenOpen)
+ this.log('Loaded ' + file.name)
+ }
+ if (file.type === 'image/jpeg' || file.type === 'image/png') {
+ const img = new Image()
+ img.onload = () => {
+ this.cache.set(file.name, img)
+ this.commander.injectPath(file.name)
+ this.log('Loaded ' + file.name)
+ }
+ img.src = URL.createObjectURL(file)
+ }
+ }
+ this.cache = {
+ data: {},
+ set: (key, content) => {
+ this.log((this.cache.data[key] ? 'Updated ' : 'Stored ') + key)
+ this.cache.data[key] = content
+ },
+ get: (key) => {
+ return this.cache.data[key]
+ }
+ }
+ this.mouseShape = (position, type) => {
+ if (!this.mouseOrigin) { return }
+ const x = position.x
+ const y = position.y
+ const xy = x + ' ' + y
+ const pos = { x, y }
+ const line = {
+ a: { x: this.mouseOrigin.x, y: this.mouseOrigin.y },
+ b: { x: pos.x, y: pos.y }
+ }
+ const size = { w: line.a.x ? pos.x - line.a.x : 0, h: line.a.y ? pos.y - line.a.y : 0 }
+ const rect = {
+ x: line.a.x,
+ y: line.a.y,
+ w: size.w,
+ h: size.h
+ }
+ const wh = rect.w + ' ' + rect.h
+ const d = Math.sqrt(((line.a.x - line.b.x) * (line.a.x - line.b.x)) + ((line.a.y - line.b.y) * (line.a.y - line.b.y))).toFixed(2)
+ const r = d
+ const a = Math.atan2(pos.y - line.a.y, pos.x - line.a.x).toFixed(2)
+ const circle = {
+ cx: line.a.x,
+ cy: line.a.y,
+ r: d
+ }
+ const arc = {
+ cx: line.a.x,
+ cy: line.a.y,
+ r: d,
+ sa: 0,
+ ea: a
+ }
+ return { x, y, xy, wh, d, r, a, line, rect, pos, size, circle, arc, type, 'is-down': type !== 'mouse-up' ? true : null }
+ }
+}
+'use strict'
+function Commander (client) {
+ this.el = document.createElement('div')
+ this.el.id = 'commander'
+ this._input = document.createElement('textarea')
+ this._status = document.createElement('div'); this._status.id = 'status'
+ this._log = document.createElement('div'); this._log.id = 'log'
+ this._docs = document.createElement('div'); this._docs.id = 'help'
+ this._run = document.createElement('a'); this._run.id = 'run'
+ this.isVisible = true
+ this.install = function (host) {
+ this.el.appendChild(this._input)
+ this._status.appendChild(this._log)
+ this._status.appendChild(this._docs)
+ this._status.appendChild(this._run)
+ this.el.appendChild(this._status)
+ host.appendChild(this.el)
+ this._run.setAttribute('title', 'Run(c-R)')
+ this._input.setAttribute('autocomplete', 'off')
+ this._input.setAttribute('autocorrect', 'off')
+ this._input.setAttribute('autocapitalize', 'off')
+ this._input.setAttribute('spellcheck', 'false')
+ this._input.addEventListener('input', this.onInput)
+ this._input.addEventListener('click', this.onClick)
+ this._run.addEventListener('click', () => { this.run() })
+ this._input.onkeydown = (e) => {
+ if (e.keyCode === 9 || e.which === 9) { e.preventDefault(); this.inject(' ') }
+ }
+ client.surface.maximize()
+ }
+ this.start = function () {
+ this.show()
+ this._input.value = this.splash
+ setTimeout(() => { this.run() }, 1000)
+ this.setStatus('Ready.')
+ }
+ this.run = (txt = this._input.value) => {
+ if (this._input.value.indexOf('$') > -1) { txt = this.clean(txt) }
+ client.bindings = {}
+ if (this._input.value.trim() === '') {
+ }
+ client.lain.run(`(${txt})`)
+ this.feedback()
+ }
+ this.load = function (txt) {
+ this._input.value = txt
+ this.run(txt)
+ }
+ this.clear = function () {
+ this.load('')
+ }
+ this.cleanup = function () {
+ this._input.value = this.clean(this._input.value)
+ this.reindent()
+ this.run()
+ }
+ this.update = function () {
+ }
+ this.onInput = () => {
+ this.setStatus()
+ }
+ this.onClick = () => {
+ this.setStatus()
+ }
+ this.reindent = function () {
+ let val = this._input.value.replace(/\n/g, '').replace(/ \)/g, ')').replace(/ +(?= )/g, '').replace(/\( \(/g, '((').replace(/\) \)/g, '))').trim()
+ let depth = 0
+ if (val.split('(').length !== val.split(')').length) {
+ client.log('Uneven number of parens.')
+ return
+ }
+ for (let i = 0; i < val.length; i++) {
+ const c = val.charAt(i)
+ if (c === '(') { depth++ } else if (c === ')') { depth-- }
+ if (c === ';') {
+ const indent = '\n' + (' '.repeat(depth))
+ val = insert(val, indent, i)
+ i += indent.length
+ }
+ if (c === '(') {
+ const indent = '\n' + (' '.repeat(depth - 1))
+ val = insert(val, indent, i)
+ i += indent.length
+ }
+ }
+ val = val.split('\n').map((line) => { return line.substr(0, 2) === '; ' ? `\n${line}\n` : line }).join('\n')
+ this._input.value = val.trim()
+ }
+ this.clean = function (input) {
+ const keywords = ['$pos+', '$pos', '$rect', '$line', '$x', '$y', '$xy']
+ for (const word of keywords) {
+ input = input.replace(word, '').trim()
+ }
+ return input
+ }
+ this.setStatus = function (msg) {
+ if (msg && msg !== this._log.textContent) {
+ this._log.textContent = `${msg}`
+ }
+ this._docs.textContent = this.getDocs()
+ }
+ this.cache = this._input.value
+ this.capture = function () {
+ if (this._input.value.indexOf('$') < 0) { return }
+ this.cache = this._input.value
+ }
+ this.inject = function (injection, at = this._input.selectionStart) {
+ this._input.value = this._input.value.substring(0, this._input.selectionStart) + injection + this._input.value.substring(this._input.selectionEnd)
+ this._input.selectionEnd = at + injection.length
+ }
+ this.injectPath = function (path) {
+ if (this._input.value.indexOf('$') < 0) { return }
+ this._input.value = this._input.value.replace('$path', `"${path}"`)
+ }
+ this.commit = function (shape, end = false, run = false) {
+ if (this.cache.indexOf('$') < 0) { return }
+ const segs = this.cache.split('$')
+ const words = segs[1].split(' ')
+ const word = words[0].split(/[^A-Za-z]/)[0]
+ const append = words[0].indexOf('+') > -1
+ if (word === 'drag') {
+ this.cache = this.cache.replace('$drag', '(drag $rect $line)')
+ } else if (word === 'view') {
+ this.cache = this.cache.replace('$view', '(view $rect $rect)')
+ } else if (word === 'poly') {
+ this.cache = this.cache.replace('$poly', '(poly $pos+)')
+ } else if (word === 'move') {
+ this.cache = this.cache.replace('$move', '(transform:move $wh)')
+ } else if (word === 'rotate') {
+ this.cache = this.cache.replace('$rotate', '(transform:rotate $a)')
+ }
+ if (shape[word]) {
+ if (append) {
+ this._input.value = this.cache.replace('$' + word + '+', this.template(shape[word], word) + ' $' + word + '+')
+ } else {
+ this._input.value = this.cache.replace('$' + word, this.template(shape[word], word))
+ }
+ }
+ if (end === true) {
+ this.cache = this._input.value
+ }
+ if (run === true) {
+ this.run()
+ }
+ }
+ this.template = function (shape, word) {
+ if (word === 'rect') { return `(rect ${shape.x} ${shape.y} ${shape.w} ${shape.h})` }
+ if (word === 'pos') { return `(pos ${shape.x} ${shape.y})` }
+ if (word === 'line') { return `(line ${shape.a.x} ${shape.a.y} ${shape.b.x} ${shape.b.y})` }
+ if (word === 'circle') { return `(circle ${shape.cx} ${shape.cy} ${shape.r})` }
+ if (word === 'arc') { return `(arc ${shape.cx} ${shape.cy} ${shape.r} ${shape.sa} ${shape.ea})` }
+ if (word === 'x' || word === 'y' || word === 'xy' || word === 'wh' || word === 'a' || word === 'r') { return `${shape}` }
+ return ''
+ }
+ this.show = function (expand = false) {
+ if (this.isVisible === true) { return }
+ client.el.className = expand ? 'expand' : ''
+ this.isVisible = true
+ this._input.focus()
+ }
+ this.hide = function () {
+ if (this.isVisible !== true) { return }
+ client.el.className = 'hidden'
+ this.isVisible = false
+ this._input.blur()
+ }
+ this.toggle = function (expand = false) {
+ if (this.isVisible !== true) {
+ this.show(expand)
+ } else {
+ this.hide()
+ }
+ }
+ this.length = function () {
+ return this._input.value.split('\n').length
+ }
+ this.feedback = function () {
+ this._run.className = 'active'
+ setTimeout(() => { this._run.className = '' }, 150)
+ }
+ this.getCurrentWord = () => {
+ const pos = this._input.value.substr(0, this._input.selectionStart).lastIndexOf('(')
+ return this._input.value.substr(pos).split(' ')[0].replace(/\(/g, '').replace(/\)/g, '').trim()
+ }
+ this.getCurrentFunction = () => {
+ const word = this.getCurrentWord()
+ let mostSimilar = ''
+ if (client.library[word]) { return word }
+ for (const id of Object.keys(client.library)) {
+ if (id.substr(0, word.length) === word) {
+ mostSimilar = id
+ }
+ }
+ return mostSimilar
+ }
+ this.getDocs = (id) => {
+ const name = this.getCurrentFunction()
+ const fn = client.library[name]
+ if (!fn) { return }
+ const fnString = fn.toString()
+ if (fnString.indexOf(') => {') < 0) { return }
+ const fnParams = fnString.split(') => {')[0].substr(1).split(',').reduce((acc, item) => { return `${acc}${item.indexOf('=') > -1 ? '~' + item.split('=')[0].trim() : item} ` }, '').trim()
+ return `(${(name + ' ' + fnParams).trim()})`
+ }
+ this.splash = `; Ronin v2.50
+(def logo-path "M60,60 L195,60 A45,45 0 0,1 240,105 A45,45 0 0,1 195,150 L60,150 M195,150 A45,45 0 0,1 240,195 L240,240 ")
+;
+(clear)
+(resize 600 600)
+(stroke
+ (svg 140 140 logo-path) "black" 7)`
+ function insert (str, add, i) {
+ return [str.slice(0, i), `${add}`, str.slice(i)].join('')
+ }
+}
+'use strict'
function Lisp (lib = {}) {
const TYPES = { identifier: 0, number: 1, string: 2, bool: 3, symbol: 4 }
const Context = function (scope, parent) {
@@ -286,662 +1058,22 @@ function Lisp (lib = {}) {
}
}
'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
- }
- 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) }
- }
- 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.el = document.createElement('div')
- this.el.id = 'ronin'
- this.acels = new Acels(this)
- this.theme = new Theme(this)
- this.source = new Source(this)
- this.commander = new Commander(this)
- this.surface = new Surface(this)
- this.library = new Library(this)
- this.lisp = new Lisp(this.library)
- this.bindings = {}
- this.install = function (host = document.body) {
- this._wrapper = document.createElement('div')
- this._wrapper.id = 'wrapper'
- this.commander.install(this._wrapper)
- this.surface.install(this._wrapper)
- this.el.appendChild(this._wrapper)
- host.appendChild(this.el)
- this.theme.install(host)
- this.acels.install(host)
- window.addEventListener('dragover', this.onDrag)
- window.addEventListener('drop', this.onDrop)
- 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() })
- this.acels.set('File', 'New', 'CmdOrCtrl+N', () => { this.source.new(); this.surface.clear(); this.commander.clear() })
- this.acels.set('File', 'Save', 'CmdOrCtrl+S', () => { this.source.write('ronin', 'lisp', this.commander._input.value, 'text/plain') })
- this.acels.set('File', 'Export Image', 'CmdOrCtrl+E', () => { this.source.write('ronin', 'png', this.surface.el.toDataURL('image/png', 1.0), 'image/png') })
- this.acels.set('File', 'Open', 'CmdOrCtrl+U', () => { this.source.open('lisp', this.whenOpen) })
- this.acels.add('Edit', 'undo')
- this.acels.add('Edit', 'redo')
- this.acels.add('Edit', 'cut')
- this.acels.add('Edit', 'copy')
- this.acels.add('Edit', 'paste')
- this.acels.add('Edit', 'selectAll')
- this.acels.set('View', 'Toggle Guides', 'CmdOrCtrl+Shift+H', () => { this.surface.toggleGuides() })
- this.acels.set('View', 'Toggle Commander', 'CmdOrCtrl+K', () => { this.commander.toggle() })
- this.acels.set('View', 'Expand Commander', 'CmdOrCtrl+Shift+K', () => { this.commander.toggle(true) })
- this.acels.set('Project', 'Run', 'CmdOrCtrl+R', () => { this.commander.run() })
- this.acels.set('Project', 'Re-Indent', 'CmdOrCtrl+Shift+I', () => { this.commander.reindent() })
- this.acels.set('Project', 'Clean', 'Escape', () => { this.commander.cleanup() })
- this.acels.route(this)
- }
- this.start = function () {
- console.log('Ronin', 'Starting..')
- console.info(`${this.acels}`)
- this.theme.start()
- this.acels.start()
- this.source.start()
- this.commander.start()
- this.surface.start()
- this.loop()
- }
- this.whenOpen = (file, res) => {
- console.log(file, res)
- this.commander.load(res)
- this.commander.show()
- }
- this.loop = () => {
- if (this.bindings.animate && typeof this.bindings.animate === 'function') {
- this.bindings.animate()
- }
- requestAnimationFrame(() => this.loop())
- }
- this.log = (...msg) => {
- this.commander.setStatus(msg.reduce((acc, val) => {
- return acc + JSON.stringify(val).replace(/"/g, '').trim() + ' '
- }, ''))
- }
- this.bind = (event, fn) => {
- this.bindings[event] = fn
- }
- this.mouseOrigin = null
- this.onMouseDown = (e, id = 'mouse-down') => {
- const pos = { x: e.offsetX * this.surface.ratio, y: e.offsetY * this.surface.ratio }
- this.mouseOrigin = pos
- const shape = this.mouseShape(pos, id)
- if (this.bindings[id]) {
- this.bindings[id](shape)
- }
- this.commander.capture()
- this.surface.clearGuide()
- this.surface.drawGuide(shape)
- }
- this.onKeyPress = (e, id = 'key-press') => {
- if (this.bindings[id]) {
- this.bindings[id](e)
- }
- }
- this.onKeyDown = (e, id = 'key-down') => {
- if (this.bindings[id]) {
- this.bindings[id](e)
- }
- }
- this.onKeyUp = (e, id = 'key-up') => {
- if (this.bindings[id]) {
- this.bindings[id](e)
- }
- }
- this.onMouseMove = (e, id = 'mouse-move') => {
- const pos = { x: e.offsetX * this.surface.ratio, y: e.offsetY * this.surface.ratio }
- const shape = this.mouseShape(pos, id)
- if (this.bindings[id]) {
- this.bindings[id](shape)
- }
- if (this.mouseOrigin) {
- this.commander.commit(shape, false, e.which !== 1)
- this.surface.clearGuide()
- this.surface.drawGuide(shape)
- }
- }
- this.onMouseUp = (e, id = 'mouse-up') => {
- const pos = { x: e.offsetX * this.surface.ratio, y: e.offsetY * this.surface.ratio }
- const shape = this.mouseShape(pos, id)
- if (this.bindings[id]) {
- this.bindings[id](shape)
- }
- if (this.mouseOrigin) {
- this.commander.commit(shape, true, e.which !== 1)
- }
- this.mouseOrigin = null
- this.surface.clearGuide()
- }
- this.onMouseOver = (e) => {
- this.mouseOrigin = null
- }
- this.onMouseOut = (e) => {
- this.mouseOrigin = null
- }
- this.onDrag = (e) => {
- e.stopPropagation()
- e.preventDefault()
- e.dataTransfer.dropEffect = 'copy'
- }
- this.onDrop = (e) => {
- e.preventDefault()
- e.stopPropagation()
- const file = e.dataTransfer.files[0]
- if (file.name.indexOf('.lisp') > -1) {
- this.source.read(file, this.whenOpen)
- this.log('Loaded ' + file.name)
- }
- if (file.type === 'image/jpeg' || file.type === 'image/png') {
- const img = new Image()
- img.onload = () => {
- this.cache.set(file.name, img)
- this.commander.injectPath(file.name)
- this.log('Loaded ' + file.name)
- }
- img.src = URL.createObjectURL(file)
- }
- }
- this.cache = {
- data: {},
- set: (key, content) => {
- this.log((this.cache.data[key] ? 'Updated ' : 'Stored ') + key)
- this.cache.data[key] = content
- },
- get: (key) => {
- return this.cache.data[key]
- }
- }
- this.mouseShape = (position, type) => {
- if (!this.mouseOrigin) { return }
- const x = position.x
- const y = position.y
- const xy = x + ' ' + y
- const pos = { x, y }
- const line = {
- a: { x: this.mouseOrigin.x, y: this.mouseOrigin.y },
- b: { x: pos.x, y: pos.y }
- }
- const size = { w: line.a.x ? pos.x - line.a.x : 0, h: line.a.y ? pos.y - line.a.y : 0 }
- const rect = {
- x: line.a.x,
- y: line.a.y,
- w: size.w,
- h: size.h
- }
- const wh = rect.w + ' ' + rect.h
- const d = Math.sqrt(((line.a.x - line.b.x) * (line.a.x - line.b.x)) + ((line.a.y - line.b.y) * (line.a.y - line.b.y))).toFixed(2)
- const a = Math.atan2(pos.y - line.a.y, pos.x - line.a.x).toFixed(2)
- const circle = {
- cx: line.a.x,
- cy: line.a.y,
- r: d
- }
- const arc = {
- cx: line.a.x,
- cy: line.a.y,
- r: d,
- sa: 0,
- ea: a
- }
- return { x, y, xy, wh, d, a, line, rect, pos, size, circle, arc, type, 'is-down': type !== 'mouse-up' ? true : null }
- }
-}
-'use strict'
-function Commander (client) {
- this.el = document.createElement('div')
- this.el.id = 'commander'
- this._input = document.createElement('textarea')
- this._status = document.createElement('div'); this._status.id = 'status'
- this._log = document.createElement('div'); this._log.id = 'log'
- this._docs = document.createElement('div'); this._docs.id = 'help'
- this._run = document.createElement('a'); this._run.id = 'run'
- this.isVisible = true
- this.install = function (host) {
- this.el.appendChild(this._input)
- this._status.appendChild(this._log)
- this._status.appendChild(this._docs)
- this._status.appendChild(this._run)
- this.el.appendChild(this._status)
- host.appendChild(this.el)
- this._run.setAttribute('title', 'Run(c-R)')
- this._input.setAttribute('autocomplete', 'off')
- this._input.setAttribute('autocorrect', 'off')
- this._input.setAttribute('autocapitalize', 'off')
- this._input.setAttribute('spellcheck', 'false')
- this._input.addEventListener('input', this.onInput)
- this._input.addEventListener('click', this.onClick)
- this._run.addEventListener('click', () => { this.run() })
- this._input.onkeydown = (e) => {
- if (e.keyCode === 9 || e.which === 9) { e.preventDefault(); this.inject(' ') }
- }
- }
- this.start = function () {
- this.setStatus('Ready.')
- this.load(this.splash)
- this.show()
- }
- this.run = (txt = this._input.value) => {
- if (this._input.value.indexOf('$') > -1) { txt = this.clean(txt) }
- client.bindings = {}
- if (this._input.value.trim() === '') {
- client.surface.maximize()
- }
- client.lisp.run(txt)
- this.feedback()
- }
- this.load = function (txt) {
- this._input.value = txt
- this.run(txt)
- }
- this.clear = function () {
- this.load('')
- }
- this.cleanup = function () {
- this._input.value = this.clean(this._input.value)
- this.reindent()
- this.run()
- }
- this.update = function () {
- }
- this.onInput = () => {
- this.setStatus()
- }
- this.onClick = () => {
- this.setStatus()
- }
- this.reindent = function () {
- let val = this._input.value.replace(/\n/g, '').replace(/ \)/g, ')').replace(/ +(?= )/g, '').replace(/\( \(/g, '((').replace(/\) \)/g, '))').trim()
- let depth = 0
- if (val.split('(').length !== val.split(')').length) {
- client.log('Uneven number of parens.')
- return
- }
- for (let i = 0; i < val.length; i++) {
- const c = val.charAt(i)
- if (c === '(') { depth++ } else if (c === ')') { depth-- }
- if (c === ';') {
- const indent = '\n' + (' '.repeat(depth))
- val = insert(val, indent, i)
- i += indent.length
- }
- if (c === '(') {
- const indent = '\n' + (' '.repeat(depth - 1))
- val = insert(val, indent, i)
- i += indent.length
- }
- }
- val = val.split('\n').map((line) => { return line.substr(0, 2) === '; ' ? `\n${line}\n` : line }).join('\n')
- this._input.value = val.trim()
- }
- this.clean = function (input) {
- const keywords = ['$pos+', '$pos', '$rect', '$line', '$x', '$y', '$xy']
- for (const word of keywords) {
- input = input.replace(word, '').trim()
- }
- return input
- }
- this.setStatus = function (msg) {
- if (msg && msg !== this._log.textContent) {
- this._log.textContent = `${msg}`
- }
- this._docs.textContent = this.getDocs()
- }
- this.cache = this._input.value
- this.capture = function () {
- if (this._input.value.indexOf('$') < 0) { return }
- this.cache = this._input.value
- }
- this.inject = function (injection, at = this._input.selectionStart) {
- this._input.value = this._input.value.substring(0, this._input.selectionStart) + injection + this._input.value.substring(this._input.selectionEnd)
- this._input.selectionEnd = at + injection.length
- }
- this.injectPath = function (path) {
- if (this._input.value.indexOf('$') < 0) { return }
- this._input.value = this._input.value.replace('$path', `"${path}"`)
- }
- this.commit = function (shape, end = false, run = false) {
- if (this.cache.indexOf('$') < 0) { return }
- const segs = this.cache.split('$')
- const words = segs[1].split(' ')
- const word = words[0].split(/[^A-Za-z]/)[0]
- const append = words[0].indexOf('+') > -1
- if (word === 'drag') {
- this.cache = this.cache.replace('$drag', '(drag $rect $line)')
- } else if (word === 'view') {
- this.cache = this.cache.replace('$view', '(view $rect $rect)')
- } else if (word === 'poly') {
- this.cache = this.cache.replace('$poly', '(poly $pos+)')
- } else if (word === 'move') {
- this.cache = this.cache.replace('$move', '(transform:move $wh)')
- } else if (word === 'rotate') {
- this.cache = this.cache.replace('$rotate', '(transform:rotate $a)')
- }
- if (shape[word]) {
- if (append) {
- this._input.value = this.cache.replace('$' + word + '+', this.template(shape[word], word) + ' $' + word + '+')
- } else {
- this._input.value = this.cache.replace('$' + word, this.template(shape[word], word))
- }
- }
- if (end === true) {
- this.cache = this._input.value
- }
- if (run === true) {
- this.run()
- }
- }
- this.template = function (shape, word) {
- if (word === 'rect') { return `(rect ${shape.x} ${shape.y} ${shape.w} ${shape.h})` }
- if (word === 'pos') { return `(pos ${shape.x} ${shape.y})` }
- if (word === 'line') { return `(line ${shape.a.x} ${shape.a.y} ${shape.b.x} ${shape.b.y})` }
- if (word === 'circle') { return `(circle ${shape.cx} ${shape.cy} ${shape.r})` }
- if (word === 'arc') { return `(arc ${shape.cx} ${shape.cy} ${shape.r} ${shape.sa} ${shape.ea})` }
- if (word === 'x' || word === 'y' || word === 'xy' || word === 'wh' || word === 'a') { return `${shape}` }
- return ''
- }
- this.show = function (expand = false) {
- if (this.isVisible === true) { return }
- client.el.className = expand ? 'expand' : ''
- this.isVisible = true
- this._input.focus()
- }
- this.hide = function () {
- if (this.isVisible !== true) { return }
- client.el.className = 'hidden'
- this.isVisible = false
- this._input.blur()
- }
- this.toggle = function (expand = false) {
- if (this.isVisible !== true) {
- this.show(expand)
- } else {
- this.hide()
- }
- }
- this.length = function () {
- return this._input.value.split('\n').length
- }
- this.feedback = function () {
- this._run.className = 'active'
- setTimeout(() => { this._run.className = '' }, 150)
- }
- this.getCurrentWord = () => {
- const pos = this._input.value.substr(0, this._input.selectionStart).lastIndexOf('(')
- return this._input.value.substr(pos).split(' ')[0].replace(/\(/g, '').replace(/\)/g, '').trim()
- }
- this.getCurrentFunction = () => {
- const word = this.getCurrentWord()
- let mostSimilar = ''
- if (client.library[word]) { return word }
- for (const id of Object.keys(client.library)) {
- if (id.substr(0, word.length) === word) {
- mostSimilar = id
- }
- }
- return mostSimilar
- }
- this.getDocs = (id) => {
- const name = this.getCurrentFunction()
- const fn = client.library[name]
- if (!fn) { return }
- const fnString = fn.toString().replace('async ', '')
- if (fnString.indexOf(') => {') < 0) { return }
- const fnParams = fnString.split(') => {')[0].substr(1).split(',').reduce((acc, item) => { return `${acc}${item.indexOf('=') > -1 ? '~' + item.split('=')[0].trim() : item} ` }, '').trim()
- return `(${(name + ' ' + fnParams).trim()})`
- }
- this.splash = `
-; Ronin v2.40
-(clear)
-(def logo-path "M60,60 L195,60 A45,45 0 0,1 240,105 A45,45 0 0,1 195,150 L60,150 M195,150 A45,45 0 0,1 240,195 L240,240 ")
-(def pos-x
- (mul frame:c 0.25))
-(def pos-y
- (sub frame:m 150))
-(stroke
- (svg pos-x pos-y logo-path) theme:f_high 5)
-`
- function insert (str, add, i) {
- return [str.slice(0, i), `${add}`, str.slice(i)].join('')
- }
-}
-'use strict'
function Library (client) {
- this.open = async (name, scale = 1) => { // Import a graphic and scale canvas to fit.
+ this.open = (name, scale = 1) => { // Import a graphic and scale canvas to fit.
const img = client.cache.get(name)
if (!img) { client.log('No data for ' + name); return }
const rect = this.rect(0, 0, img.width * scale, img.height * scale)
- await this.resize(rect.w, rect.h).then(this.import(name, rect))
+ this.resize(rect.w, rect.h)
+ this.import(name, rect)
return rect
}
- this.import = async (name, shape, alpha = 1) => { // Imports a graphic file with format.
+ this.import = (name, shape, alpha = 1) => { // Imports a graphic file with format.
const img = client.cache.get(name)
if (!img) { client.log('No data for ' + name); return }
client.surface.draw(img, shape, alpha)
return shape || this.rect(0, 0, img.width, img.height)
}
- this.export = async (name = 'ronin', type = 'image/png', quality = 1.0) => { // Exports a graphic file with format.
+ this.export = (name = 'ronin', type = 'image/png', quality = 1.0) => { // Exports a graphic file with format.
const ext = type === 'image/png' ? name + '.png' : name + '.jpg'
client.source.write(name, ext, client.surface.el.toDataURL(type, 1.0), type)
}
@@ -983,32 +1115,32 @@ function Library (client) {
this.hsl = (h, s, l, a = 1) => { // returns a HSL color object
return { h, s, l, a, toString: () => { return `hsla(${h},${s}%,${l}%,${a})` }, 0: h, 1: s, 2: l, 3: a, f: [h / 360, s / 100, l / 100, a] }
}
- this.resize = async (w = client.surface.bounds().w, h = client.surface.bounds().h, fit = true) => { // Resizes the canvas to target w and h, returns the rect.
+ this.resize = (w = client.surface.bounds().w, h = client.surface.bounds().h, fit = true) => { // Resizes the canvas to target w and h, returns the rect.
if (w === this['get-frame']().w && h === this['get-frame']().h) { return }
const rect = { x: 0, y: 0, w, h }
const a = document.createElement('img')
const b = document.createElement('img')
a.src = client.surface.el.toDataURL()
- await client.surface.resizeImage(a, b)
+ client.surface.resizeImage(a, b)
client.surface.resize(rect, fit)
return client.surface.draw(b, rect)
}
- this.rescale = async (w = 1, h) => { // Rescales the canvas to target ratio of w and h, returns the rect.
+ this.rescale = (w = 1, h) => { // Rescales the canvas to target ratio of w and h, returns the rect.
const rect = { x: 0, y: 0, w: this['get-frame']().w * w, h: this['get-frame']().h * (h || w) }
const a = document.createElement('img')
const b = document.createElement('img')
a.src = client.surface.el.toDataURL()
- await client.surface.resizeImage(a, b)
+ client.surface.resizeImage(a, b)
client.surface.resize(rect, true)
return client.surface.draw(b, rect)
}
- this.crop = async (rect = this['get-frame']()) => { // Crop canvas to rect.
+ this.crop = (rect = this['get-frame']()) => { // Crop canvas to rect.
return client.surface.crop(rect)
}
- this.copy = async (rect = this['get-frame']()) => { // Copy a section of the canvas.
+ this.copy = (rect = this['get-frame']()) => { // Copy a section of the canvas.
return client.surface.copy(rect)
}
- this.paste = async (copy, rect = this['get-frame']()) => { // Paste a section of the canvas.
+ this.paste = (copy, rect = this['get-frame']()) => { // Paste a section of the canvas.
return client.surface.paste(copy, rect)
}
this.drag = (rect = this['get-frame'](), line = this.line()) => { // Drag a part of the canvas.
@@ -1021,6 +1153,7 @@ function Library (client) {
client.surface.context.drawImage(crop, rect.x, rect.y)
}
this.view = (a, b) => { // View a part of the canvas.
+ if (!a || !b) { return }
this.guide({ a: { x: a.x, y: a.y }, b: { x: b.x, y: b.y } })
this.guide(a)
this.guide(b)
@@ -1038,8 +1171,8 @@ function Library (client) {
}
return this.color(this.floor(sum[0] / count), this.floor(sum[1] / count), this.floor(sum[2] / count))
}
- this.orient = async (deg = 0) => { // Orient canvas with angle in degrees.
- const copy = await this.copy()
+ this.orient = (deg = 0) => { // Orient canvas with angle in degrees.
+ const copy = this.copy()
const frame = this['get-frame']()
const mode = Math.floor(deg / 90) % 4
const offset = { x: [0, 0, -frame.w, -frame.w], y: [0, -frame.h, -frame.h, 0] }
@@ -1052,8 +1185,8 @@ function Library (client) {
client.surface.context.restore()
}
this.mirror = { // Mirror canvas, methods: `x`, `y`.
- x: async (j = 0) => {
- const copy = await this.copy()
+ x: (j = 0) => {
+ const copy = this.copy()
const frame = this['get-frame']()
client.surface.context.save()
client.surface.context.translate(frame.w, 0)
@@ -1061,8 +1194,8 @@ function Library (client) {
client.surface.context.drawImage(copy, 0, 0)
client.surface.context.restore()
},
- y: async (j = 0) => {
- const copy = await this.copy()
+ y: (j = 0) => {
+ const copy = this.copy()
const frame = this['get-frame']()
client.surface.context.save()
client.surface.context.translate(0, frame.h)
@@ -1122,12 +1255,12 @@ function Library (client) {
client.surface.drawGuide(shape, color)
return shape
}
- this.pixels = async (fn, q = 1, rect = this['get-frame']()) => {
+ this.pixels = (fn, q = 1, rect = this['get-frame']()) => {
if (!fn) { console.warn('Unknown function'); return rect }
const img = client.surface.context.getImageData(rect.x, rect.y, rect.w, rect.h)
for (let i = 0, loop = img.data.length; i < loop; i += 4) {
const pixel = [img.data[i], img.data[i + 1], img.data[i + 2], img.data[i + 3]]
- const processed = await fn(pixel, q)
+ const processed = fn(pixel, q)
img.data[i] = this.clamp(parseInt(processed[0]), 0, 255)
img.data[i + 1] = this.clamp(parseInt(processed[1]), 0, 255)
img.data[i + 2] = this.clamp(parseInt(processed[2]), 0, 255)
@@ -1244,10 +1377,10 @@ function Library (client) {
}
return args[args.length - 1]
}
- this.each = async (arr, fn) => { // Run a function for each element in a list.
+ this.each = (arr, fn) => { // Run a function for each element in a list.
for (let i = 0; i < arr.length; i++) {
const arg = arr[i]
- await fn(arg)
+ fn(arg)
}
}
this.map = (arr, fn) => { // Run a function on each element in a list.
@@ -1262,11 +1395,11 @@ function Library (client) {
})
})
}
- this.reduce = async (arr, fn, acc) => {
+ this.reduce = (arr, fn, acc) => {
const length = arr.length
let result = acc === undefined ? arr[0] : acc
for (let i = acc === undefined ? 1 : 0; i < length; i++) {
- result = await fn(result, arr[i], i, arr)
+ result = fn(result, arr[i], i, arr)
}
return result
}
@@ -1390,9 +1523,9 @@ function Library (client) {
}
return a === b
}
- this.benchmark = async (fn) => { // Logs time taken to execute a function.
+ this.benchmark = (fn) => { // Logs time taken to execute a function.
const start = Date.now()
- const result = await fn()
+ const result = fn()
console.log(`time taken: ${Date.now() - start}ms`)
return result
}
@@ -1543,24 +1676,21 @@ function Surface (client) {
this.traceSVG = function (text, context) {
}
this.draw = function (img, shape = this.getFrame(), alpha = 1) {
- return new Promise(resolve => {
- this.context.globalAlpha = alpha
- if (isLine(shape)) {
- this.context.drawImage(img, shape.a.x, shape.a.y, shape.b.x - shape.a.x, shape.b.y - shape.a.y)
- } else if (isRect(shape)) {
- const fit = fitRect({ w: img.width, h: img.height }, { w: shape.w, h: shape.h })
- this.context.drawImage(img, shape.x, shape.y, fit.w, fit.h)
- } else if (isCircle(shape)) {
- const side = Math.sqrt(Math.pow(shape.r, 2) / 2)
- const rect = { x: shape.cx - (side), y: shape.cy - (side), w: side * 2, h: side * 2 }
- const fit = fitRect({ w: img.width, h: img.height }, { w: rect.w, h: rect.h })
- this.context.drawImage(img, rect.x, rect.y, fit.w, fit.h)
- } else {
- this.context.drawImage(img, shape.x, shape.y, img.width, img.height)
- }
- this.context.globalAlpha = 1
- resolve()
- })
+ this.context.globalAlpha = alpha
+ if (isLine(shape)) {
+ this.context.drawImage(img, shape.a.x, shape.a.y, shape.b.x - shape.a.x, shape.b.y - shape.a.y)
+ } else if (isRect(shape)) {
+ const fit = fitRect({ w: img.width, h: img.height }, { w: shape.w, h: shape.h })
+ this.context.drawImage(img, shape.x, shape.y, fit.w, fit.h)
+ } else if (isCircle(shape)) {
+ const side = Math.sqrt(Math.pow(shape.r, 2) / 2)
+ const rect = { x: shape.cx - (side), y: shape.cy - (side), w: side * 2, h: side * 2 }
+ const fit = fitRect({ w: img.width, h: img.height }, { w: rect.w, h: rect.h })
+ this.context.drawImage(img, rect.x, rect.y, fit.w, fit.h)
+ } else {
+ this.context.drawImage(img, shape.x, shape.y, img.width, img.height)
+ }
+ this.context.globalAlpha = 1
}
this.crop = function (rect) {
if (!isRect(rect)) { return }
@@ -1707,6 +1837,7 @@ body { margin:0px; padding:0px; overflow:hidden; font-family:"input_mono_regular
#ronin #wrapper #commander textarea { background: none; width: 100%; height: calc(100vh - 105px); resize: none; font-size: 12px;line-height: 15px; padding-right: 15px}
#ronin #wrapper #commander #status { position: absolute; bottom: 0px; line-height: 15px; height: 30px; overflow: hidden; width: calc(100% - 75px); padding-left:45px;}
#ronin #wrapper #commander #status #run { display: block; width: 26px; height: 26px; position: absolute; top: 0px; border-radius: 15px; left:0px; cursor: pointer; border:2px solid #fff; transition: background-color 250ms, border-color 250ms}
+#ronin #wrapper #commander #status #run:hover { background: none }
#ronin.expand #wrapper #commander { width:100%; }
#ronin #surface, #ronin #guide { position: absolute; top:0px; -webkit-user-select: none;-webkit-app-region: no-drag; background-image: url("data:image/svg+xml;utf8,"); background-size: 10px 10px; background-position: -4px -4px; width:100%; height:100%; transition: left 250ms, opacity 250ms; opacity: 1; }
#ronin.hidden #wrapper #commander { margin-left:-40vw; }
diff --git a/links/main.css b/links/main.css
index d20fa65..518e04b 100644
--- a/links/main.css
+++ b/links/main.css
@@ -20,6 +20,7 @@ body { margin:0px; padding:0px; overflow:hidden; font-family:"input_mono_regular
#ronin #wrapper #commander textarea { background: none; width: 100%; height: calc(100vh - 105px); resize: none; font-size: 12px;line-height: 15px; padding-right: 15px}
#ronin #wrapper #commander #status { position: absolute; bottom: 0px; line-height: 15px; height: 30px; overflow: hidden; width: calc(100% - 75px); padding-left:45px;}
#ronin #wrapper #commander #status #run { display: block; width: 26px; height: 26px; position: absolute; top: 0px; border-radius: 15px; left:0px; cursor: pointer; border:2px solid #fff; transition: background-color 250ms, border-color 250ms}
+#ronin #wrapper #commander #status #run:hover { background: none }
#ronin.expand #wrapper #commander { width:100%; }
#ronin #surface, #ronin #guide { position: absolute; top:0px; -webkit-user-select: none;-webkit-app-region: no-drag; background-image: url("data:image/svg+xml;utf8,"); background-size: 10px 10px; background-position: -4px -4px; width:100%; height:100%; transition: left 250ms, opacity 250ms; opacity: 1; }
diff --git a/scripts/client.js b/scripts/client.js
index 5ad1bf9..86a9a24 100644
--- a/scripts/client.js
+++ b/scripts/client.js
@@ -6,7 +6,7 @@
/* global Commander */
/* global Surface */
/* global Library */
-/* global Lisp */
+/* global Lain */
/* global Image */
/* global requestAnimationFrame */
@@ -21,7 +21,7 @@ function Client () {
this.commander = new Commander(this)
this.surface = new Surface(this)
this.library = new Library(this)
- this.lisp = new Lisp(this.library)
+ this.lain = new Lain(this.library)
this.bindings = {}
@@ -226,6 +226,7 @@ function Client () {
}
const wh = rect.w + ' ' + rect.h
const d = Math.sqrt(((line.a.x - line.b.x) * (line.a.x - line.b.x)) + ((line.a.y - line.b.y) * (line.a.y - line.b.y))).toFixed(2)
+ const r = d
const a = Math.atan2(pos.y - line.a.y, pos.x - line.a.x).toFixed(2)
const circle = {
cx: line.a.x,
@@ -239,6 +240,6 @@ function Client () {
sa: 0,
ea: a
}
- return { x, y, xy, wh, d, a, line, rect, pos, size, circle, arc, type, 'is-down': type !== 'mouse-up' ? true : null }
+ return { x, y, xy, wh, d, r, a, line, rect, pos, size, circle, arc, type, 'is-down': type !== 'mouse-up' ? true : null }
}
}
diff --git a/scripts/commander.js b/scripts/commander.js
index e801d79..4eb9bf4 100644
--- a/scripts/commander.js
+++ b/scripts/commander.js
@@ -30,21 +30,23 @@ function Commander (client) {
this._input.onkeydown = (e) => {
if (e.keyCode === 9 || e.which === 9) { e.preventDefault(); this.inject(' ') }
}
+ client.surface.maximize()
}
this.start = function () {
- this.setStatus('Ready.')
- this.load(this.splash)
this.show()
+ this._input.value = this.splash
+ setTimeout(() => { this.run() }, 1000)
+ this.setStatus('Ready.')
}
this.run = (txt = this._input.value) => {
if (this._input.value.indexOf('$') > -1) { txt = this.clean(txt) }
client.bindings = {}
if (this._input.value.trim() === '') {
- client.surface.maximize()
+
}
- client.lisp.run(txt)
+ client.lain.run(`(${txt})`)
this.feedback()
}
@@ -180,7 +182,7 @@ function Commander (client) {
if (word === 'line') { return `(line ${shape.a.x} ${shape.a.y} ${shape.b.x} ${shape.b.y})` }
if (word === 'circle') { return `(circle ${shape.cx} ${shape.cy} ${shape.r})` }
if (word === 'arc') { return `(arc ${shape.cx} ${shape.cy} ${shape.r} ${shape.sa} ${shape.ea})` }
- if (word === 'x' || word === 'y' || word === 'xy' || word === 'wh' || word === 'a') { return `${shape}` }
+ if (word === 'x' || word === 'y' || word === 'xy' || word === 'wh' || word === 'a' || word === 'r') { return `${shape}` }
return ''
}
@@ -240,7 +242,7 @@ function Commander (client) {
const name = this.getCurrentFunction()
const fn = client.library[name]
if (!fn) { return }
- const fnString = fn.toString().replace('async ', '')
+ const fnString = fn.toString()
if (fnString.indexOf(') => {') < 0) { return }
const fnParams = fnString.split(') => {')[0].substr(1).split(',').reduce((acc, item) => { return `${acc}${item.indexOf('=') > -1 ? '~' + item.split('=')[0].trim() : item} ` }, '').trim()
return `(${(name + ' ' + fnParams).trim()})`
@@ -248,17 +250,14 @@ function Commander (client) {
// Splash
- this.splash = `
-; Ronin v2.40
-(clear)
+ this.splash = `; Ronin v2.50
+
(def logo-path "M60,60 L195,60 A45,45 0 0,1 240,105 A45,45 0 0,1 195,150 L60,150 M195,150 A45,45 0 0,1 240,195 L240,240 ")
-(def pos-x
- (mul frame:c 0.25))
-(def pos-y
- (sub frame:m 150))
+;
+(clear)
+(resize 600 600)
(stroke
- (svg pos-x pos-y logo-path) theme:f_high 5)
-`
+ (svg 140 140 logo-path) "black" 7)`
function insert (str, add, i) {
return [str.slice(0, i), `${add}`, str.slice(i)].join('')
diff --git a/scripts/lib/lisp.js b/scripts/lain(old).js
similarity index 100%
rename from scripts/lib/lisp.js
rename to scripts/lain(old).js
diff --git a/scripts/lib/lain.js b/scripts/lib/lain.js
new file mode 100644
index 0000000..660fca4
--- /dev/null
+++ b/scripts/lib/lain.js
@@ -0,0 +1,145 @@
+'use strict'
+
+// In the real world, it didn’t matter if I was there or not.
+// When I realized that, I was no longer afraid of losing my body.
+
+function Lain (lib = {}) {
+ const TYPES = { identifier: 0, number: 1, string: 2, bool: 3, symbol: 4 }
+
+ const Context = function (scope, parent) {
+ this.scope = scope
+ this.parent = parent
+ this.get = function (identifier) {
+ if (identifier in this.scope) {
+ return this.scope[identifier]
+ } else if (this.parent !== undefined) {
+ return this.parent.get(identifier)
+ }
+ }
+ }
+
+ const special = {
+ let: function (input, context) {
+ const letContext = input[1].reduce(function (acc, x) {
+ acc.scope[x[0].value] = interpret(x[1], context)
+ return acc
+ }, new Context({}, context))
+ return interpret(input[2], letContext)
+ },
+ def: function (input, context) {
+ if (input.length !== 3) { console.warn('Lain', 'Invalid definition.'); return }
+ const identifier = input[1].host ? input[1].host : input[1].value
+ if (input[1].host) {
+ if (!context.scope[identifier]) { context.scope[identifier] = {} }
+ context.scope[identifier][input[1].value] = interpret(input[2], context)
+ return context.scope[identifier][input[1].value]
+ }
+ context.scope[identifier] = interpret(input[2], context)
+ return context.scope[identifier]
+ },
+ defn: function (input, context) {
+ const identifier = input[1].value
+ if (context.scope[identifier]) { console.warn('Lain', `Redefining function: ${identifier}`) }
+ const fnParams = input[2].type === TYPES.string && input[3] ? input[3] : input[2]
+ const fnBody = input[2].type === TYPES.string && input[4] ? input[4] : input[3]
+ context.scope[identifier] = function () {
+ const lambdaArguments = arguments
+ const lambdaScope = fnParams.reduce(function (acc, x, i) {
+ acc[x.value] = lambdaArguments[i]
+ return acc
+ }, {})
+ return interpret(fnBody, new Context(lambdaScope, context))
+ }
+ },
+ λ: function (input, context) {
+ return function () {
+ const lambdaArguments = arguments
+ const lambdaScope = input[1].reduce(function (acc, x, i) {
+ acc[x.value] = lambdaArguments[i]
+ return acc
+ }, {})
+ return interpret(input[2], new Context(lambdaScope, context))
+ }
+ },
+ if: function (input, context) {
+ return interpret(input[1], context) ? interpret(input[2], context) : input[3] ? interpret(input[3], context) : []
+ }
+ }
+
+ const interpretList = function (input, context) {
+ if (input.length > 0 && input[0].value in special) {
+ return special[input[0].value](input, context)
+ }
+ const list = []
+ for (let i = 0; i < input.length; i++) {
+ if (input[i].type === TYPES.symbol) {
+ if (input[i].host) {
+ const host = context.get(input[i].host)
+ if (host) {
+ list.push(host[input[i].value])
+ }
+ } else {
+ list.push(obj => obj[input[i].value])
+ }
+ } else {
+ list.push(interpret(input[i], context))
+ }
+ }
+ return list[0] instanceof Function ? list[0].apply(undefined, list.slice(1)) : list
+ }
+
+ const interpret = function (input, context) {
+ if (!input) { console.warn('Lain', context.scope); return null }
+ if (context === undefined) {
+ return interpret(input, new Context(lib))
+ } else if (input instanceof Array) {
+ return interpretList(input, context)
+ } else if (input.type === TYPES.identifier) {
+ return context.get(input.value)
+ } else if (input.type === TYPES.number || input.type === TYPES.symbol || input.type === TYPES.string || input.type === TYPES.bool) {
+ return input.value
+ }
+ }
+
+ const categorize = function (input) {
+ if (!isNaN(parseFloat(input))) {
+ return { type: TYPES.number, value: parseFloat(input) }
+ } else if (input[0] === '"' && input.slice(-1) === '"') {
+ return { type: TYPES.string, value: input.slice(1, -1) }
+ } else if (input[0] === ':') {
+ return { type: TYPES.symbol, value: input.slice(1) }
+ } else if (input.indexOf(':') > 0) {
+ return { type: TYPES.symbol, host: input.split(':')[0], value: input.split(':')[1] }
+ } else if (input === 'true' || input === 'false') {
+ return { type: TYPES.bool, value: input === 'true' }
+ } else {
+ return { type: TYPES.identifier, value: input }
+ }
+ }
+
+ const parenthesize = function (input, list) {
+ if (list === undefined) { return parenthesize(input, []) }
+ const token = input.shift()
+ if (token === undefined) {
+ return list.pop()
+ } else if (token === '(') {
+ list.push(parenthesize(input, []))
+ return parenthesize(input, list)
+ } else if (token === ')') {
+ return list
+ } else {
+ return parenthesize(input, list.concat(categorize(token)))
+ }
+ }
+
+ const tokenize = function (input) {
+ const i = input.replace(/^[\s]*;.*\n?/gm, '').split('"')
+ return i.map(function (x, i) {
+ return i % 2 === 0 ? x.replace(/\(/g, ' ( ').replace(/\)/g, ' ) ') : x.replace(/ /g, '!ws!')
+ }).join('"').trim().split(/\s+/).map(function (x) { return x.replace(/!ws!/g, ' ') })
+ }
+
+ this.run = (input) => {
+ return interpret(parenthesize(tokenize(input)))
+ }
+}
diff --git a/scripts/lib/theme.js b/scripts/lib/theme.js
index 0556fa0..7d4ac79 100644
--- a/scripts/lib/theme.js
+++ b/scripts/lib/theme.js
@@ -86,7 +86,7 @@ function Theme (client) {
this.active[key] = hex
}
- this.read = (key) => {
+ this.get = (key) => {
return this.active[key]
}
diff --git a/scripts/library.js b/scripts/library.js
index 61a7908..177fe04 100644
--- a/scripts/library.js
+++ b/scripts/library.js
@@ -5,22 +5,23 @@
function Library (client) {
// IO
- this.open = async (name, scale = 1) => { // Import a graphic and scale canvas to fit.
+ this.open = (name, scale = 1) => { // Import a graphic and scale canvas to fit.
const img = client.cache.get(name)
if (!img) { client.log('No data for ' + name); return }
const rect = this.rect(0, 0, img.width * scale, img.height * scale)
- await this.resize(rect.w, rect.h).then(this.import(name, rect))
+ this.resize(rect.w, rect.h)
+ this.import(name, rect)
return rect
}
- this.import = async (name, shape, alpha = 1) => { // Imports a graphic file with format.
+ this.import = (name, shape, alpha = 1) => { // Imports a graphic file with format.
const img = client.cache.get(name)
if (!img) { client.log('No data for ' + name); return }
client.surface.draw(img, shape, alpha)
return shape || this.rect(0, 0, img.width, img.height)
}
- this.export = async (name = 'ronin', type = 'image/png', quality = 1.0) => { // Exports a graphic file with format.
+ this.export = (name = 'ronin', type = 'image/png', quality = 1.0) => { // Exports a graphic file with format.
const ext = type === 'image/png' ? name + '.png' : name + '.jpg'
client.source.write(name, ext, client.surface.el.toDataURL(type, 1.0), type)
}
@@ -79,36 +80,36 @@ function Library (client) {
// Frame
- this.resize = async (w = client.surface.bounds().w, h = client.surface.bounds().h, fit = true) => { // Resizes the canvas to target w and h, returns the rect.
+ this.resize = (w = client.surface.bounds().w, h = client.surface.bounds().h, fit = true) => { // Resizes the canvas to target w and h, returns the rect.
if (w === this['get-frame']().w && h === this['get-frame']().h) { return }
const rect = { x: 0, y: 0, w, h }
const a = document.createElement('img')
const b = document.createElement('img')
a.src = client.surface.el.toDataURL()
- await client.surface.resizeImage(a, b)
+ client.surface.resizeImage(a, b)
client.surface.resize(rect, fit)
return client.surface.draw(b, rect)
}
- this.rescale = async (w = 1, h) => { // Rescales the canvas to target ratio of w and h, returns the rect.
+ this.rescale = (w = 1, h) => { // Rescales the canvas to target ratio of w and h, returns the rect.
const rect = { x: 0, y: 0, w: this['get-frame']().w * w, h: this['get-frame']().h * (h || w) }
const a = document.createElement('img')
const b = document.createElement('img')
a.src = client.surface.el.toDataURL()
- await client.surface.resizeImage(a, b)
+ client.surface.resizeImage(a, b)
client.surface.resize(rect, true)
return client.surface.draw(b, rect)
}
- this.crop = async (rect = this['get-frame']()) => { // Crop canvas to rect.
+ this.crop = (rect = this['get-frame']()) => { // Crop canvas to rect.
return client.surface.crop(rect)
}
- this.copy = async (rect = this['get-frame']()) => { // Copy a section of the canvas.
+ this.copy = (rect = this['get-frame']()) => { // Copy a section of the canvas.
return client.surface.copy(rect)
}
- this.paste = async (copy, rect = this['get-frame']()) => { // Paste a section of the canvas.
+ this.paste = (copy, rect = this['get-frame']()) => { // Paste a section of the canvas.
return client.surface.paste(copy, rect)
}
@@ -123,6 +124,7 @@ function Library (client) {
}
this.view = (a, b) => { // View a part of the canvas.
+ if (!a || !b) { return }
this.guide({ a: { x: a.x, y: a.y }, b: { x: b.x, y: b.y } })
this.guide(a)
this.guide(b)
@@ -142,8 +144,8 @@ function Library (client) {
return this.color(this.floor(sum[0] / count), this.floor(sum[1] / count), this.floor(sum[2] / count))
}
- this.orient = async (deg = 0) => { // Orient canvas with angle in degrees.
- const copy = await this.copy()
+ this.orient = (deg = 0) => { // Orient canvas with angle in degrees.
+ const copy = this.copy()
const frame = this['get-frame']()
const mode = Math.floor(deg / 90) % 4
const offset = { x: [0, 0, -frame.w, -frame.w], y: [0, -frame.h, -frame.h, 0] }
@@ -157,8 +159,8 @@ function Library (client) {
}
this.mirror = { // Mirror canvas, methods: `x`, `y`.
- x: async (j = 0) => {
- const copy = await this.copy()
+ x: (j = 0) => {
+ const copy = this.copy()
const frame = this['get-frame']()
client.surface.context.save()
client.surface.context.translate(frame.w, 0)
@@ -166,8 +168,8 @@ function Library (client) {
client.surface.context.drawImage(copy, 0, 0)
client.surface.context.restore()
},
- y: async (j = 0) => {
- const copy = await this.copy()
+ y: (j = 0) => {
+ const copy = this.copy()
const frame = this['get-frame']()
client.surface.context.save()
client.surface.context.translate(0, frame.h)
@@ -240,12 +242,12 @@ function Library (client) {
// Pixels
- this.pixels = async (fn, q = 1, rect = this['get-frame']()) => {
+ this.pixels = (fn, q = 1, rect = this['get-frame']()) => {
if (!fn) { console.warn('Unknown function'); return rect }
const img = client.surface.context.getImageData(rect.x, rect.y, rect.w, rect.h)
for (let i = 0, loop = img.data.length; i < loop; i += 4) {
const pixel = [img.data[i], img.data[i + 1], img.data[i + 2], img.data[i + 3]]
- const processed = await fn(pixel, q)
+ const processed = fn(pixel, q)
img.data[i] = this.clamp(parseInt(processed[0]), 0, 255)
img.data[i + 1] = this.clamp(parseInt(processed[1]), 0, 255)
img.data[i + 2] = this.clamp(parseInt(processed[2]), 0, 255)
@@ -412,10 +414,10 @@ function Library (client) {
// Arrays
- this.each = async (arr, fn) => { // Run a function for each element in a list.
+ this.each = (arr, fn) => { // Run a function for each element in a list.
for (let i = 0; i < arr.length; i++) {
const arg = arr[i]
- await fn(arg)
+ fn(arg)
}
}
@@ -433,11 +435,11 @@ function Library (client) {
})
}
- this.reduce = async (arr, fn, acc) => {
+ this.reduce = (arr, fn, acc) => {
const length = arr.length
let result = acc === undefined ? arr[0] : acc
for (let i = acc === undefined ? 1 : 0; i < length; i++) {
- result = await fn(result, arr[i], i, arr)
+ result = fn(result, arr[i], i, arr)
}
return result
}
@@ -589,9 +591,9 @@ function Library (client) {
return a === b
}
- this.benchmark = async (fn) => { // Logs time taken to execute a function.
+ this.benchmark = (fn) => { // Logs time taken to execute a function.
const start = Date.now()
- const result = await fn()
+ const result = fn()
console.log(`time taken: ${Date.now() - start}ms`)
return result
}
diff --git a/scripts/surface.js b/scripts/surface.js
index a1391d4..6dd944c 100644
--- a/scripts/surface.js
+++ b/scripts/surface.js
@@ -174,24 +174,21 @@ function Surface (client) {
// IO
this.draw = function (img, shape = this.getFrame(), alpha = 1) {
- return new Promise(resolve => {
- this.context.globalAlpha = alpha
- if (isLine(shape)) {
- this.context.drawImage(img, shape.a.x, shape.a.y, shape.b.x - shape.a.x, shape.b.y - shape.a.y)
- } else if (isRect(shape)) {
- const fit = fitRect({ w: img.width, h: img.height }, { w: shape.w, h: shape.h })
- this.context.drawImage(img, shape.x, shape.y, fit.w, fit.h)
- } else if (isCircle(shape)) {
- const side = Math.sqrt(Math.pow(shape.r, 2) / 2)
- const rect = { x: shape.cx - (side), y: shape.cy - (side), w: side * 2, h: side * 2 }
- const fit = fitRect({ w: img.width, h: img.height }, { w: rect.w, h: rect.h })
- this.context.drawImage(img, rect.x, rect.y, fit.w, fit.h)
- } else {
- this.context.drawImage(img, shape.x, shape.y, img.width, img.height)
- }
- this.context.globalAlpha = 1
- resolve()
- })
+ this.context.globalAlpha = alpha
+ if (isLine(shape)) {
+ this.context.drawImage(img, shape.a.x, shape.a.y, shape.b.x - shape.a.x, shape.b.y - shape.a.y)
+ } else if (isRect(shape)) {
+ const fit = fitRect({ w: img.width, h: img.height }, { w: shape.w, h: shape.h })
+ this.context.drawImage(img, shape.x, shape.y, fit.w, fit.h)
+ } else if (isCircle(shape)) {
+ const side = Math.sqrt(Math.pow(shape.r, 2) / 2)
+ const rect = { x: shape.cx - (side), y: shape.cy - (side), w: side * 2, h: side * 2 }
+ const fit = fitRect({ w: img.width, h: img.height }, { w: rect.w, h: rect.h })
+ this.context.drawImage(img, rect.x, rect.y, fit.w, fit.h)
+ } else {
+ this.context.drawImage(img, shape.x, shape.y, img.width, img.height)
+ }
+ this.context.globalAlpha = 1
}
this.crop = function (rect) {