diff --git a/README.md b/README.md index 6c8cf4e..c953b45 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,89 @@ # Ronin -_All I wanted, was a way of resizing photos._ +_"All I wanted, was a quick way of resizing a few photos.."_ -Ronin is a LISP repl to create generative graphics currently under development. You can follow the daily progress on [Mastodon](https://merveilles.town/@neauoire/). Until we have documented the library, you can find a list of available functions [here](https://github.com/hundredrabbits/Ronin/blob/master/desktop/sources/scripts/library.js). +Ronin is a [LISP](https://en.wikipedia.org/wiki/Lisp_(programming_language)) repl to create generative graphics currently under development. You can follow the daily progress on [Mastodon](https://merveilles.town/@neauoire/). - +## Install & Run -## Electron Build +You can download [builds](https://hundredrabbits.itch.io/ronin) for **OSX, Windows and Linux**, or if you wish to build it yourself, follow these steps: ``` -cd desktop +git clone https://github.com/hundredrabbits/Ronin.git +cd Ronin/desktop/ npm install npm start ``` -## Documentation + -Documentation is accessible [here](./documentation.md) +## Library + +- `(import path rect)` Imports a graphic file with format. +- `(export path ~format ~quality)` Exports a graphic file with format. +- `(pos x y ~t)` Returns a position shape. +- `(size w h ~t)` Returns a size shape. +- `(rect x y w h ~t)` Returns a rect shape. +- `(circle x y r ~t)` Returns a circle shape. +- `(line a b ~t)` Returns a line shape. +- `(text x y g s ~f ~t)` Returns a text shape. +- `(svg d ~t)` Returns a svg shape. +- `(stroke ~shape)` Strokes a shape. +- `(fill ~rect)` Fills a shape. +- `(clear ~rect)` Clears a rect. +- `(add ...args)` Adds values. +- `(sub ...args)` Subtracts values. +- `(mul ...args)` Multiplies values. +- `(div ...args)` Divides values. +- `(mod a b)` Returns the modulo of a and b. +- `(clamp val min max)` Clamps a value between min and max. +- `(step val step)` +- `(min)` +- `(max)` +- `(ceil)` +- `(floor)` +- `(sin)` +- `(cos)` +- `(PI)` +- `(TWO_PI)` +- `(random ...args)` +- `(gt a b)` Returns true if a is greater than b, else false. +- `(lt a b)` Returns true if a is less than b, else false. +- `(eq a b)` Returns true if a is equal to b, else false. +- `(and a b ...rest)` Returns true if all conditions are true. +- `(or a b ...rest)` Returns true if at least one condition is true. +- `(map fn arr)` +- `(filter fn arr)` +- `(reduce fn arr ~acc)` +- `(len item)` Returns the length of a list. +- `(first arr)` Returns the first item of a list. +- `(last arr)` Returns the last +- `(rest [_ ...arr])` +- `(range start end ~step)` +- `(get item key)` Gets an object's parameter with name. +- `(set item key val)` Sets an object's parameter with name as value. +- `(of h ...keys)` +- `(frame)` Returns a rect of the frame. +- `(center)` Returns a position of the center of the frame. +- `(scale rect w h)` +- `(resize w h)` Resizes the canvas to target w and h, returns the rect. +- `(rescale w h)` Rescales the canvas to target ratio of w and h, returns the rect. +- `(crop rect)` +- `(clone a b)` +- `(theme variable ~el)` +- `(gradient [x1 y1 x2 y2] ~colors 'black'])` +- `(pixels rect fn q)` +- `(saturation pixel ~q)` +- `(contrast pixel ~q)` +- `(echo ...args)` +- `(str ...args)` +- `(open path)` Imports a graphic file and resizes the frame. +- `(folder ~path)` Returns the content of a folder path. +- `(exit ~force)` Exits Ronin. +- `(time)` Returns timestamp in milliseconds. +- `(animate ~play)` Toggles animation. +- `(js)` Javascript interop. +- `(test name a b)` ## Extras diff --git a/desktop/sources/index.html b/desktop/sources/index.html index 6a57f64..d3a5353 100644 --- a/desktop/sources/index.html +++ b/desktop/sources/index.html @@ -40,6 +40,7 @@ ronin.controller.addRole('default', 'Edit', 'paste') ronin.controller.addRole('default', 'Edit', 'delete') ronin.controller.addRole('default', 'Edit', 'selectall') + ronin.controller.add("default","Edit","Re-Indent",() => { ronin.commander.reindent() },"CmdOrCtrl+Shift+I") ronin.controller.add("default","View","Zoom In",() => { ronin.modZoom(0.25) },"CmdOrCtrl+=") ronin.controller.add("default","View","Zoom Out",() => { ronin.modZoom(-0.25) },"CmdOrCtrl+-") ronin.controller.add("default","View","Zoom Reset",() => { ronin.modZoom(1,true) },"CmdOrCtrl+0") diff --git a/desktop/sources/links/main.css b/desktop/sources/links/main.css index 350ac06..8abe5c2 100644 --- a/desktop/sources/links/main.css +++ b/desktop/sources/links/main.css @@ -5,12 +5,12 @@ body { margin:0px; padding:0px; overflow:hidden; font-family:"input_mono_regular #ronin { height: calc(100vh - 60px); width:calc(100vw - 60px); -webkit-app-region: drag; padding: 30px;overflow: hidden; } #ronin #wrapper { overflow: hidden; position: relative; } #ronin #wrapper #commander { z-index: 9000;position: relative;width: 310px;height: calc(100vh - 60px);-webkit-app-region: no-drag;padding-right: 30px;transition: margin-left 250ms;} -#ronin #wrapper #commander textarea { background: none; width: 100%; height: calc(100vh - 80px); resize: none; font-size: 12px;line-height: 15px; padding-right: 15px} -#ronin #wrapper #commander div#status { position: absolute; bottom: 0px; } +#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 div#status { position: absolute; bottom: 0px; text-transform: lowercase;} #ronin.hidden #wrapper #commander { margin-left:-331px; } #ronin canvas#surface,#ronin canvas#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%; left:340px; transition: left 250ms} #ronin canvas#guide { background:none; } #ronin canvas#surface { border-radius: 2px } -#ronin.hidden canvas#surface, #ronin.hidden canvas#guide { left:0px; } \ No newline at end of file +#ronin.hidden canvas#surface, #ronin.hidden canvas#guide { left:10px; } \ No newline at end of file diff --git a/desktop/sources/scripts/commander.js b/desktop/sources/scripts/commander.js index ccf9f1f..b7aae9b 100644 --- a/desktop/sources/scripts/commander.js +++ b/desktop/sources/scripts/commander.js @@ -4,18 +4,28 @@ function Commander (ronin) { this._input = document.createElement('textarea') this._status = document.createElement('div') this._status.id = 'status' + this._log = document.createElement('div') + this._log.id = 'log' + this._source = document.createElement('div') + this._source.id = 'source' + this._docs = document.createElement('div') + this._docs.id = 'help' this.isVisible = true this.install = function (host) { this.el.appendChild(this._input) + this._status.appendChild(this._log) + this._status.appendChild(this._source) + this._status.appendChild(this._docs) this.el.appendChild(this._status) host.appendChild(this.el) - this._input.addEventListener('input', this.onInput) + this._input.addEventListener('click', this.onClick) + this.docs.install() } this.start = function () { - this._status.textContent = 'Idle. (zoom 100%)' + this.setStatus('Ready.') this._input.focus() this.run() this.hide() @@ -33,21 +43,59 @@ function Commander (ronin) { this.run() } + this.reindent = function () { + let val = this._input.value.replace(/\n/g, '').replace(/ +(?= )/g, '').replace(/\( \(/g, '((').replace(/\) \)/g, '))').trim() + let depth = 0 + 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 = val.insert(indent, i) + i += indent.length + } + if (c === '(') { + const indent = '\n' + (' '.repeat(depth - 1)) + val = val.insert(indent, i) + i += indent.length + } + } + this._input.value = val.trim() + } + this.setStatus = function (msg) { - if (!msg) { return } - this._status.textContent = `${(msg + '').substr(0, 40)}` + // Logs + if (msg && msg !== this._log.textContent) { + this._log.textContent = `${msg}` + console.log(msg) + } + // Source + const _source = `${ronin.source} ${this._input.value.split('\n').length} lines` + if (_source !== this._source.textContent) { + this._source.textContent = _source + } + // Docs + const _docs = this.docs.print(this.getLastfn()) + if (_docs !== this._docs.textContent) { + this._docs.textContent = `${_docs}` + } } this.update = function () { } - this.onInput = function () { - + this.onInput = () => { + this.setStatus() } - this.getQuery = function () { + this.onClick = () => { + this.setStatus() + } + this.getLastfn = function () { + const pos = this._input.value.substr(0, this._input.selectionStart).lastIndexOf('(') + return this._input.value.substr(pos).split(' ')[0].replace(/\(/g, '').replace(/\)/g, '').trim() } // Mouse @@ -62,7 +110,6 @@ function Commander (ronin) { this.mouseRect.a.x = e.offsetX this.mouseRect.a.y = e.offsetY this.mouseRect.t = 'pos' - this._status.textContent = `${this.mouseRect.x},${this.mouseRect.y} ${this.mouseRect.w},${this.mouseRect.h}` this.capture() this.show() } @@ -73,7 +120,6 @@ function Commander (ronin) { this.mouseRect.h = e.offsetY - this.mouseRect.y this.mouseRect.b.x = e.offsetX this.mouseRect.b.y = e.offsetY - this._status.textContent = `${this.mouseRect.x},${this.mouseRect.y} ${this.mouseRect.w},${this.mouseRect.h}` this.commit() } } @@ -85,7 +131,6 @@ function Commander (ronin) { this.mouseRect.b.x = e.offsetX this.mouseRect.b.y = e.offsetY this.mouseRect.t = '' - this._status.textContent = `${this.mouseRect.x},${this.mouseRect.y} ${this.mouseRect.w},${this.mouseRect.h}` this.commit() this._input.focus() ronin.surface.clearGuide() @@ -162,4 +207,44 @@ function Commander (ronin) { this.hide() } } + + // Docs micro-module + + this.docs = { + dict: {}, + load: function () { + const fs = require('fs') + const path = require('path') + const p = path.join(__dirname, 'scripts/', 'library.js') + if (!fs.existsSync(p)) { console.warn('Docs', 'File does not exist: ' + p); return } + const lines = fs.readFileSync(p, 'utf8').split('\n').filter((line) => { return line.substr(0, 7) === ' this.' }) + return lines.map((line) => { return line.trim().substr(5).trim() }) + }, + install: function (payload = this.load()) { + for (const id in payload) { + const name = payload[id].substr(0, payload[id].indexOf(' = ')) + const parent = payload[id].substr(payload[id].indexOf(' = ')).match(/\(([^)]+)\)/) + const params = parent ? parent[1].split(',').map((word) => { return word.indexOf(' = ') > -1 ? '~' + (word.split(' = ')[0]).trim() : word.trim() }) : [] + const note = payload[id].indexOf('// ') > -1 ? payload[id].split('//')[1].trim() : '' + this.dict[name] = { note, params } + if (params.length < 1) { console.warn('Docs', 'Missing params for ' + name) } + if (note === '') { console.warn('Docs', 'Missing note for ' + name) } + } + console.log('Docs', `Loaded ${Object.keys(this.dict).length} functions.`) + console.log(this.toMarkdown()) + }, + toMarkdown: function () { + return Object.keys(this.dict).reduce((acc, item, key) => { + const example = `${item} ${this.dict[item].params.reduce((acc, item) => { + return `${acc}${item} ` + }, '').trim()}` + return `${acc}- \`(${example.trim()})\` ${this.dict[item].note}\n` + }, '') + }, + print: function (name) { + return this.dict[name] ? `(${name} ${this.dict[name].params.reduce((acc, item) => { return `${acc}${item} ` }, '').trim()})` : '' + } + } + + String.prototype.insert = function (s, i) { return [this.slice(0, i), `${s}`, this.slice(i)].join('') } } diff --git a/desktop/sources/scripts/library.js b/desktop/sources/scripts/library.js index f639907..794af27 100644 --- a/desktop/sources/scripts/library.js +++ b/desktop/sources/scripts/library.js @@ -1,278 +1,88 @@ function Library (ronin) { - console.log(ronin) - this.open = async (path) => { - return ronin.surface.open(path) - } - - this.export = (path, type = 'image/png', quality = 1.0) => { - if (!path) { console.warn('Missing export path'); return path } - var dataUrl = ronin.surface.el.toDataURL(type, quality) - const data = dataUrl.replace(/^data:image\/png;base64,/, '') - fs.writeFileSync(path, data, 'base64') - return path - } - - this.draw = async (path, rect) => { + this.import = async (path, rect) => { // Imports a graphic file with format. const img = new Image() img.src = path return ronin.surface.draw(img, rect) } - this.resize = async (w = 1, h = 1) => { - const rect = w <= 1 || h <= 1 ? { x: 0, y: 0, w: this.frame().w * w, h: this.frame().h * h } : { x: 0, y: 0, w, h } - const a = document.createElement('img') - const b = document.createElement('img') - a.src = ronin.surface.el.toDataURL() - ronin.surface.resizeImage(a, b) - ronin.surface.resize(rect, true) - return ronin.surface.draw(b, rect) - } - - this.crop = async (rect) => { - return ronin.surface.crop(rect) - } - - this.folder = (path = ronin.source.path) => { - return fs.existsSync(path) ? fs.readdirSync(path) : [] - } - - this.exit = () => { - ronin.source.quit() - } - - // Logic - - this.gt = (a, b) => { - return a > b - } - - this.lt = (a, b) => { - return a < b - } - - this.eq = (a, b) => { - return a === b - } - - this.and = (a, b, ...rest) => { - let args = [a, b].concat(rest) - for (let i = 0; i < args.length; i++) { - if (!args[i]) { - return args[i] - } - } - return args[args.length - 1] - } - - this.or = (a, b, ...rest) => { - let args = [a, b].concat(rest) - for (let i = 0; i < args.length; i++) { - if (args[i]) { - return args[i] - } - } - return args[args.length - 1] - } - - // Arrays - - this.map = async (fn, arr) => { - return Promise.all(arr.map(fn)) - } - - this.filter = (fn, arr) => { - const list = Array.from(arr) - return Promise.all(list.map((element, index) => fn(element, index, list))) - .then(result => { - return list.filter((_, index) => { - return result[index] - }) - }) - } - - this.reduce = async (fn, arr, acc) => { - const length = arr.length - let result = acc === undefined ? subject[0] : acc - for (let i = acc === undefined ? 1 : 0; i < length; i++) { - result = await fn(result, arr[i], i, arr) - } - return result; - } - - this.len = (item) => { - return item.length - } - - this.first = (arr) => { - return arr[0] - } - - this.last = (arr) => { - return arr[arr.length - 1] - } - - this.rest = ([_, ...arr]) => { - return arr - } - - this.range = (start, end, step = 1) => { - let arr = [] - if (step > 0) { - for (let i = start; i <= end; i += step) { - arr.push(i) - } - } else { - for (let i = start; i >= end; i += step) { - arr.push(i) - } - } - return arr + this.export = (path, format = 'image/png', quality = 1.0) => { // Exports a graphic file with format. + if (!path) { console.warn('Missing export path'); return path } + var dataUrl = ronin.surface.el.toDataURL(format, quality) + const data = dataUrl.replace(/^data:image\/png;base64,/, '') + fs.writeFileSync(path, data, 'base64') + return path } // Shapes - this.pos = (x, y, t = 'pos') => { + this.pos = (x, y, t = 'pos') => { // Returns a position shape. return { x, y, t } } - this.size = (w, h, t = 'size') => { + this.size = (w, h, t = 'size') => { // Returns a size shape. return { w, h, t } } - this.rect = (x, y, w, h, t = 'rect') => { + this.rect = (x, y, w, h, t = 'rect') => { // Returns a rect shape. return { x, y, w, h, t } } - this.circle = (x, y, r, t = 'circle') => { + this.circle = (x, y, r, t = 'circle') => { // Returns a circle shape. return { x, y, r, t } } - this.line = (a, b, t = 'line') => { + this.line = (a, b, t = 'line') => { // Returns a line shape. return { a, b, t } } - this.text = (x, y, g, s, f = 'Arial', t = 'text') => { + this.text = (x, y, g, s, f = 'Arial', t = 'text') => { // Returns a text shape. return { x, y, g, s, f, t } } - this.svg = (d, t = 'svg') => { + this.svg = (d, t = 'svg') => { // Returns a svg shape. return { d, t } } - // Helpers + // Actions - this.frame = () => { - return ronin.surface.getFrame() - } - - this.center = () => { - const rect = this.frame() - return this.pos(rect.w / 2, rect.h / 2) - } - - this.scale = (rect, w, h) => { - return { x: rect.x, y: rect.y, w: rect.w * w, h: rect.h * h } - } - - // Copy/Paste - - this.clone = (a, b) => { - ronin.surface.clone(a, b) - return [a, b] - } - - this.stroke = (shape = this.frame(), thickness, color) => { + this.stroke = (shape = this.frame(), thickness, color) => { // Strokes a shape. ronin.surface.stroke(shape, thickness, color) return shape } - this.fill = (rect = this.frame(), color) => { + this.fill = (rect = this.frame(), color) => { // Fills a shape. ronin.surface.fill(rect, color) return rect } - this.clear = (rect = this.frame()) => { + this.clear = (rect = this.frame()) => { // Clears a rect. ronin.surface.clear(rect) return rect } - this.get = (item, key) => { - return item[key] - } - - this.set = (item, key, val) => { - item[key] = val - return item[key] - } - - // TODO: Should remove (of) for (get)? - - this.of = (h, ...keys) => { - return keys.reduce((acc, key) => { - return acc[key] - }, h) - } - - this.theme = (variable, el = document.documentElement) => { - // ex. (theme "f_main") -> :root { --f_main: "#fff" } - return getComputedStyle(el).getPropertyValue(`--${variable}`) - } - - // Gradients - - this.gradient = ([x1,y1,x2,y2], colors=['white','black']) => { - return ronin.surface.linearGradient(x1, y1, x2, y2, colors) - } - - // Pixels - - this.pixels = (rect, fn, q) => { - const img = ronin.surface.context.getImageData(0, 0, rect.w, rect.h) - for (let i = 0, loop = img.data.length; i < loop; i += 4) { - const pixel = { r: img.data[i], g: img.data[i + 1], b: img.data[i + 2], a: img.data[i + 3] } - const processed = fn(pixel, q) - img.data[i] = processed[0] - img.data[i + 1] = processed[1] - img.data[i + 2] = processed[2] - img.data[i + 3] = processed[3] - } - ronin.surface.context.putImageData(img, 0, 0) - return rect - } - - this.saturation = (pixel, q = 1) => { - const color = 0.2126 * pixel.r + 0.7152 * pixel.g + 0.0722 * pixel.b - return [(color * (1 - q)) + (pixel.r * q), (color * (1 - q)) + (pixel.g * q), (color * (1 - q)) + (pixel.b * q), pixel.a] - } - - this.contrast = (pixel, q = 1) => { - const intercept = 128 * (1 - q) - return [pixel.r * q + intercept, pixel.g * q + intercept, pixel.b * q + intercept, pixel.a] - } - // Math - this.add = (...args) => { + this.add = (...args) => { // Adds values. return args.reduce((sum, val) => sum + val) } - this.sub = (...args) => { + this.sub = (...args) => { // Subtracts values. return args.reduce((sum, val) => sum - val) } - this.mul = (...args) => { + this.mul = (...args) => { // Multiplies values. return args.reduce((sum, val) => sum * val) } - this.div = (...args) => { + this.div = (...args) => { // Divides values. return args.reduce((sum, val) => sum / val) } - this.mod = (a, b) => { + this.mod = (a, b) => { // Returns the modulo of a and b. return a % b } - this.clamp = (val, min, max) => { + this.clamp = (val, min, max) => { // Clamps a value between min and max. return Math.min(max, Math.max(min, val)) } @@ -307,7 +117,194 @@ function Library (ronin) { return Math.random() } - // Generics + // Logic + + this.gt = (a, b) => { // Returns true if a is greater than b, else false. + return a > b + } + + this.lt = (a, b) => { // Returns true if a is less than b, else false. + return a < b + } + + this.eq = (a, b) => { // Returns true if a is equal to b, else false. + return a === b + } + + this.and = (a, b, ...rest) => { // Returns true if all conditions are true. + let args = [a, b].concat(rest) + for (let i = 0; i < args.length; i++) { + if (!args[i]) { + return args[i] + } + } + return args[args.length - 1] + } + + this.or = (a, b, ...rest) => { // Returns true if at least one condition is true. + let args = [a, b].concat(rest) + for (let i = 0; i < args.length; i++) { + if (args[i]) { + return args[i] + } + } + return args[args.length - 1] + } + + // Arrays + + this.map = async (fn, arr) => { + return Promise.all(arr.map(fn)) + } + + this.filter = (fn, arr) => { + const list = Array.from(arr) + return Promise.all(list.map((element, index) => fn(element, index, list))) + .then(result => { + return list.filter((_, index) => { + return result[index] + }) + }) + } + + this.reduce = async (fn, arr, acc) => { + const length = arr.length + let result = acc === undefined ? subject[0] : acc + for (let i = acc === undefined ? 1 : 0; i < length; i++) { + result = await fn(result, arr[i], i, arr) + } + return result; + } + + this.len = (item) => { // Returns the length of a list. + return item.length + } + + this.first = (arr) => { // Returns the first item of a list. + return arr[0] + } + + this.last = (arr) => { // Returns the last + return arr[arr.length - 1] + } + + this.rest = ([_, ...arr]) => { + return arr + } + + this.range = (start, end, step = 1) => { + let arr = [] + if (step > 0) { + for (let i = start; i <= end; i += step) { + arr.push(i) + } + } else { + for (let i = start; i >= end; i += step) { + arr.push(i) + } + } + return arr + } + + // Objects + + this.get = (item, key) => { // Gets an object's parameter with name. + return item[key] + } + + this.set = (item, key, val) => { // Sets an object's parameter with name as value. + item[key] = val + return item[key] + } + + this.of = (h, ...keys) => { + return keys.reduce((acc, key) => { + return acc[key] + }, h) + } + + // Frame + + this.frame = () => { // Returns a rect of the frame. + return ronin.surface.getFrame() + } + + this.center = () => { // Returns a position of the center of the frame. + const rect = this.frame() + return this.pos(rect.w / 2, rect.h / 2) + } + + this.scale = (rect, w, h) => { + return { x: rect.x, y: rect.y, w: rect.w * w, h: rect.h * h } + } + + this.resize = async (w, h) => { // Resizes the canvas to target w and h, returns the rect. + const rect = { x: 0, y: 0, w, h } + const a = document.createElement('img') + const b = document.createElement('img') + a.src = ronin.surface.el.toDataURL() + ronin.surface.resizeImage(a, b) + ronin.surface.resize(rect, true) + return ronin.surface.draw(b, rect) + } + + this.rescale = async (w, h) => { // Rescales the canvas to target ratio of w and h, returns the rect. + const rect = { x: 0, y: 0, w: this.frame().w * w, h: this.frame().h * h } + const a = document.createElement('img') + const b = document.createElement('img') + a.src = ronin.surface.el.toDataURL() + ronin.surface.resizeImage(a, b) + ronin.surface.resize(rect, true) + return ronin.surface.draw(b, rect) + } + + this.crop = async (rect) => { + return ronin.surface.crop(rect) + } + + this.clone = (a, b) => { + ronin.surface.clone(a, b) + return [a, b] + } + + this.theme = (variable, el = document.documentElement) => { + // ex. (theme "f_main") -> :root { --f_main: "#fff" } + return getComputedStyle(el).getPropertyValue(`--${variable}`) + } + + // Gradients + + this.gradient = ([x1, y1, x2, y2], colors = ['white', 'black']) => { + return ronin.surface.linearGradient(x1, y1, x2, y2, colors) + } + + // Pixels + + this.pixels = (rect, fn, q) => { + const img = ronin.surface.context.getImageData(0, 0, rect.w, rect.h) + for (let i = 0, loop = img.data.length; i < loop; i += 4) { + const pixel = { r: img.data[i], g: img.data[i + 1], b: img.data[i + 2], a: img.data[i + 3] } + const processed = fn(pixel, q) + img.data[i] = processed[0] + img.data[i + 1] = processed[1] + img.data[i + 2] = processed[2] + img.data[i + 3] = processed[3] + } + ronin.surface.context.putImageData(img, 0, 0) + return rect + } + + this.saturation = (pixel, q = 1) => { + const color = 0.2126 * pixel.r + 0.7152 * pixel.g + 0.0722 * pixel.b + return [(color * (1 - q)) + (pixel.r * q), (color * (1 - q)) + (pixel.g * q), (color * (1 - q)) + (pixel.b * q), pixel.a] + } + + this.contrast = (pixel, q = 1) => { + const intercept = 128 * (1 - q) + return [pixel.r * q + intercept, pixel.g * q + intercept, pixel.b * q + intercept, pixel.a] + } + + // Misc this.echo = (...args) => { ronin.log(args) @@ -318,29 +315,36 @@ function Library (ronin) { return args.reduce((acc, val) => { return acc + val }, '') } + this.open = async (path) => { // Imports a graphic file and resizes the frame. + return ronin.surface.open(path) + } + + this.folder = (path = ronin.source.path) => { // Returns the content of a folder path. + return fs.existsSync(path) ? fs.readdirSync(path) : [] + } + + this.exit = (force = false) => { // Exits Ronin. + ronin.source.quit(force) + } + + this.time = () => { // Returns timestamp in milliseconds. + return Date.now + } + + this.animate = (play = true) => { // Toggles animation. + ronin.animate(play) + } + + this.js = () => { // Javascript interop. + return window + } + this.test = (name, a, b) => { - if (Array.isArray(a)) { - // TODO: make testing more solid - a = a.toString() - b = b.toString() - } - if (a !== b) { + if (`${a}` !== `${b}`) { console.warn('failed ' + name, a, b) } else { console.log('passed ' + name, a) } return a === b } - - // Client - this.ronin = ronin - - // Livecoding - this.time = Date.now - - this.animate = (b = true) => ronin.animate(b) - - // javascript interop - this.js = window - } diff --git a/desktop/sources/scripts/lisp.js b/desktop/sources/scripts/lisp.js index fe9b520..ddc4b97 100644 --- a/desktop/sources/scripts/lisp.js +++ b/desktop/sources/scripts/lisp.js @@ -20,9 +20,8 @@ function Lisp (input, lib) { const special = { include: (input, context) => { - const p = input[1].value - if (!fs.existsSync(p)) { console.warn('Source', p); return [] } - const file = fs.readFileSync(p, { encoding: 'utf-8' }) + if (!input[1].value || !fs.existsSync(input[1].value)) { console.warn('Source', input[1].value); return [] } + const file = fs.readFileSync(input[1].value, { encoding: 'utf-8' }) return interpret(this.parse(file), context) }, let: function (input, context) { @@ -34,29 +33,21 @@ function Lisp (input, lib) { }, def: function (input, context) { const identifier = input[1].value - const value = (input[2].type === TYPES.string) ? input[3] : input[2] - if (input[2].type === TYPES.string) { - // docstring - console.log(input[2].value) - } + const value = input[2].type === TYPES.string && input[3] ? input[3] : input[2] context.scope[identifier] = interpret(value, context) return value }, defn: function (input, context) { - const identifier = input[1].value - const argumentNames = (input[2].type === TYPES.string) ? input[3] : input[2] - const functionBody = (input[2].type === TYPES.string) ? input[4] : input[3] - if (input[2].type === TYPES.string) { - // docstring - console.log(input[2].value) - } - context.scope[identifier] = async function () { + const fnName = input[1].value + 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[fnName] = async function () { const lambdaArguments = arguments - const lambdaScope = argumentNames.reduce(function (acc, x, i) { + const lambdaScope = fnParams.reduce(function (acc, x, i) { acc[x.value] = lambdaArguments[i] return acc }, {}) - return interpret(functionBody, new Context(lambdaScope, context)) + return interpret(fnBody, new Context(lambdaScope, context)) } }, lambda: function (input, context) { diff --git a/desktop/sources/scripts/ronin.js b/desktop/sources/scripts/ronin.js index 0883f22..1e229c7 100644 --- a/desktop/sources/scripts/ronin.js +++ b/desktop/sources/scripts/ronin.js @@ -15,7 +15,6 @@ function Ronin () { this.el.id = 'ronin' this.theme = new Theme(defaultTheme) - this.source = new Source(this) this.commander = new Commander(this) this.surface = new Surface(this) @@ -52,7 +51,6 @@ function Ronin () { } this.log = function (...msg) { - console.log(...msg) this.commander.setStatus(msg.reduce((acc, val) => { return acc + val + ' ' }, '')) } diff --git a/desktop/sources/scripts/source.js b/desktop/sources/scripts/source.js index 29a6457..c6bfd72 100644 --- a/desktop/sources/scripts/source.js +++ b/desktop/sources/scripts/source.js @@ -15,6 +15,7 @@ function Source (ronin) { console.log('Source', 'Make a new file..') this.path = null ronin.surface.clear() + ronin.log(`New file.`) } this.open = function () { @@ -57,6 +58,7 @@ function Source (ronin) { if (quitAfter === true) { app.exit() } + ronin.log(`Writing file.`) } this.read = function (loc = this.path) { @@ -65,6 +67,7 @@ function Source (ronin) { console.log('Source', 'Reading ' + loc) this.path = loc this.load(fs.readFileSync(this.path, 'utf8')) + ronin.log(`Reading file.`) } this.run = function () { @@ -75,8 +78,8 @@ function Source (ronin) { ronin.commander._input.value = data } - this.quit = function () { - if (this.hasChanges() === true) { + this.quit = function (force = false) { + if (this.hasChanges() === true && force === false) { this.verify() } else { app.exit() @@ -144,7 +147,7 @@ function Source (ronin) { } this.toString = function () { - return this.path ? this.name() : 'unsaved' + return this.path ? this.name() + '.lisp' : 'unsaved' } function isDifferent (a, b) { diff --git a/desktop/sources/scripts/surface.js b/desktop/sources/scripts/surface.js index e7de660..7945b97 100644 --- a/desktop/sources/scripts/surface.js +++ b/desktop/sources/scripts/surface.js @@ -61,11 +61,11 @@ function Surface (ronin) { context.closePath() } - this.linearGradient = function(x1, y1, x2, y2, colors, context = this.context) { + this.linearGradient = function (x1, y1, x2, y2, colors, context = this.context) { const gradient = context.createLinearGradient(x1, y1, x2, y2) - const step = 1/(colors.length - 1) - colors.forEach((color,i) => { - gradient.addColorStop(i*step, color) + const step = 1 / (colors.length - 1) + colors.forEach((color, i) => { + gradient.addColorStop(i * step, color) }) return gradient } @@ -159,6 +159,8 @@ function Surface (ronin) { } this.resize = function (size, fit = false) { + const frame = this.getFrame() + if (frame.w === size.w && frame.h === size.h) { return } console.log('Surface', `Resize: ${size.w}x${size.h}`) this.el.width = size.w this.el.height = size.h @@ -173,10 +175,14 @@ function Surface (ronin) { } } + this.getFrame = function () { + return { x: 0, y: 0, w: this.el.width, h: this.el.height, t: 'rect' } + } + this.fitWindow = function (size) { const win = require('electron').remote.getCurrentWindow() const pad = { w: ronin.commander.isVisible === true ? 400 : 60, h: 60 } - win.setSize(size.w + pad.w, size.h + pad.h, false) + win.setSize(size.w + pad.w, size.h + pad.h, true) } this.maximize = function () { @@ -191,10 +197,6 @@ function Surface (ronin) { ronin.log(`resize ${f.w}x${f.h}`) } - this.getFrame = function () { - return { x: 0, y: 0, w: this.el.width, h: this.el.height, t: 'rect' } - } - this.getCrop = function (rect) { const newCanvas = document.createElement('canvas') newCanvas.width = rect.w @@ -203,7 +205,7 @@ function Surface (ronin) { return newCanvas } - this.resizeImage = function (src, dst, type = 'image/jpeg', quality = 0.92) { + this.resizeImage = function (src, dst, type = 'image/png', quality = 1.0) { const tmp = new Image() let canvas let context diff --git a/examples/basics.lisp b/examples/basics.lisp new file mode 100644 index 0000000..c1832ea --- /dev/null +++ b/examples/basics.lisp @@ -0,0 +1,9 @@ +; basics +( + ; define a variable + (def a 25) + (echo a) + + ; define a function + (defn add-two (a) (add 2 a)) + (echo (add-two 4))) \ No newline at end of file diff --git a/examples/glitch.lisp b/examples/glitch.lisp index f84b501..b5f723e 100644 --- a/examples/glitch.lisp +++ b/examples/glitch.lisp @@ -17,7 +17,7 @@ ; Draw photo - (draw + (import "../static/crystal.jpg" (rect 0 0 400 400)) diff --git a/examples/guides.lisp b/examples/guides.lisp index c6bff1a..f956c75 100644 --- a/examples/guides.lisp +++ b/examples/guides.lisp @@ -1,36 +1,57 @@ -; guides file - -((clear) - (stroke (frame) 1 "red") - +; guides +( + (clear) + (stroke + (frame) 1 "red") (stroke (line (pos 0 0) - (pos (of (frame) "w") (of (frame) "h"))) - 1 "red") - - + (pos + (of + (frame) "w") + (of + (frame) "h"))) 1 "red") (stroke (line - (pos (of (frame) "w") 0) - (pos 0 (of (frame) "h"))) - 1 "red") - + (pos + (of + (frame) "w") 0) + (pos 0 + (of + (frame) "h"))) 1 "red") (stroke (line - (pos (div (of (frame) "w") 2) 0) - (pos (div (of (frame) "w") 2) (of (frame) "h"))) - 1 "red") - + (pos + (div + (of + (frame) "w") 2) 0) + (pos + (div + (of + (frame) "w") 2) + (of + (frame) "h"))) 1 "red") (stroke (line - (pos 0 (div (of (frame) "h") 2)) - (pos (div (of (frame) "w") 2) (of (frame) "h"))) - 1 "#72dec2") - + (pos 0 + (div + (of + (frame) "h") 2)) + (pos + (div + (of + (frame) "w") 2) + (of + (frame) "h"))) 1 "#72dec2") (stroke (line - (pos (div (of (frame) "w") 2) 0) - (pos (of (frame) "w") (div (of (frame) "h") 2))) - 1 "#72dec2") -) \ No newline at end of file + (pos + (div + (of + (frame) "w") 2) 0) + (pos + (of + (frame) "w") + (div + (of + (frame) "h") 2))) 1 "#72dec2")) \ No newline at end of file diff --git a/examples/include.lisp b/examples/include.lisp new file mode 100644 index 0000000..bb1fdb9 --- /dev/null +++ b/examples/include.lisp @@ -0,0 +1,4 @@ +; include +( + (include "../examples/recursive.lisp") + (echo line-color)) \ No newline at end of file diff --git a/examples/pixels.lisp b/examples/pixels.lisp index d45baa9..d4edbff 100644 --- a/examples/pixels.lisp +++ b/examples/pixels.lisp @@ -1,12 +1,7 @@ ; pixels - ( - (clear) - (draw - "../../PREVIEW.jpg" - (frame)) + (clear) + (import "../../PREVIEW.jpg" + (frame)) (pixels - (rect 0 0 500 500) - saturation - 0.5) -) \ No newline at end of file + (rect 0 0 500 500) saturation 0.5)) \ No newline at end of file diff --git a/examples/random.lisp b/examples/random.lisp index 0753a05..ff3754c 100644 --- a/examples/random.lisp +++ b/examples/random.lisp @@ -1,21 +1,17 @@ ; random - ( - (clear) - - (defn place - (rec) - (if (gt rec 0) + (clear) + (defn place + (rec) + (if + (gt rec 0) ( - (draw "../static/crystal.jpg" + (import "../static/crystal.jpg" (rect - (random 200) (random 200) (random 200) - (random 200))) - (place (sub rec 1)) - )) - ) - - (place 30) -) \ No newline at end of file + (random 200) + (random 200))) + (place + (sub rec 1))))) + (place 30)) \ No newline at end of file diff --git a/examples/recursive.lisp b/examples/recursive.lisp index 7c4cd6c..fb7003f 100644 --- a/examples/recursive.lisp +++ b/examples/recursive.lisp @@ -1,17 +1,16 @@ ; recursive - ( - (clear) - (def line-color "red") + (clear) (defn rec - (v) - (if (gt v 0) - ((stroke (circle - (mul 5 v) - (mul 5 v) - (mul 5 v)) 1 line-color) - (rec (sub v 5)))) - ) - - (rec 100) -) \ No newline at end of file + (v) + (if + (gt v 0) + ( + (stroke + (circle + (mul 5 v) + (mul 5 v) + (mul 5 v)) 1 "red") + (rec + (sub v 5))))) + (rec 100)) \ No newline at end of file diff --git a/examples/resize.lisp b/examples/resize.lisp index 620a36c..5dbd409 100644 --- a/examples/resize.lisp +++ b/examples/resize.lisp @@ -1,7 +1,5 @@ ; resize - ( - (clear) - (open "../../PREVIEW.jpg") - (resize 0.5 0.5) -) \ No newline at end of file + (clear) + (open "../../PREVIEW.jpg") + (resize 0.5 0.5)) \ No newline at end of file diff --git a/examples/run.lisp b/examples/run.lisp deleted file mode 100644 index 1877cf0..0000000 --- a/examples/run.lisp +++ /dev/null @@ -1,4 +0,0 @@ -( - (include "../examples/recursive.lisp") - (echo line-color) -) \ No newline at end of file diff --git a/examples/svg.lisp b/examples/svg.lisp index 2a81943..497871b 100644 --- a/examples/svg.lisp +++ b/examples/svg.lisp @@ -1,5 +1,8 @@ -((fill - (svg "M255,60 L255,60 L135,180 L75,60 L195,210 L120,225 L105,225 L165,255 L225,195 L255,135 L285,150") "white") - -(stroke - (svg "M405,15 L405,15 L150,150 L195,90 L240,135 L120,195 L75,90 L135,165 L120,225 L90,240 L60,210 L90,150 L255,180 L285,180 L285,165 ") "pink")) \ No newline at end of file +( + (clear) + ; ronin path + (stroke + (svg "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 ") 2 "white") + ; outline + (stroke + (svg "M15,15 L15,15 L285,15 L285,285 L15,285 Z") 1 "#555")) \ No newline at end of file diff --git a/examples/theme.lisp b/examples/theme.lisp index 498e19f..9c50830 100644 --- a/examples/theme.lisp +++ b/examples/theme.lisp @@ -1,30 +1,38 @@ -((clear) - (def col - (lambda - (i) - (of - ((theme "f_high") - (theme "f_med") - (theme "f_low") - (theme "f_inv") - (theme "b_high") - (theme "b_med") - (theme "b_low") - (theme "b_inv")) - (mod i 8)))) - (def rec - (lambda - (v i) - (if (gt v 0) - ((fill - (circle - (add - (div (of (frame) "w") 1.6) - (mul 1.5 v)) - (mul 10 v) - (mul v (div v 5))) - (col i)) - (rec - (sub v 3) - (add i 1)))))) - (rec 40 0)) \ No newline at end of file +; theme +( + (clear) + (def col + (lambda + (i) + (of + ( + (theme "f_high") + (theme "f_med") + (theme "f_low") + (theme "f_inv") + (theme "b_high") + (theme "b_med") + (theme "b_low") + (theme "b_inv")) + (mod i 8)))) + (def rec + (lambda + (v i) + (if + (gt v 0) + ( + (fill + (circle + (add + (div + (of + (frame) "w") 1.6) + (mul 1.5 v)) + (mul 10 v) + (mul v + (div v 5))) + (col i)) + (rec + (sub v 3) + (add i 1)))))) + (rec 40 0)) \ No newline at end of file