diff --git a/README.md b/README.md index 6682bc3..36453b0 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,90 @@ # 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 ``` + + +## 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. +- `(frame)` Returns a rect of the frame. +- `(center)` Returns a position of the center of the frame. +- `(scale rect w h)` +- `(resize ~w ~h)` +- `(crop rect)` +- `(clone a b)` +- `(of h ...keys)` +- `(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. +- `(ronin)` +- `(time)` Returns timestamp in milliseconds. +- `(animate ~play)` Toggles animation. +- `(js)` +- `(test name a b)` + ## Extras - This application supports the [Ecosystem Theme](https://github.com/hundredrabbits/Themes). 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 c04f194..e9b4b03 100644 --- a/desktop/sources/scripts/library.js +++ b/desktop/sources/scripts/library.js @@ -1,269 +1,88 @@ function Library (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) => { - return arr.filter(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 = (fn, arr, acc = 0) => { - return arr.reduce(fn, acc) - } - - 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}`) - } - - // 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)) } @@ -298,7 +117,183 @@ 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 = (fn, arr, acc = 0) => { + return arr.reduce(fn, acc) + } + + 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] + } + + // 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 = 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) + } + + // Copy/Paste + + this.clone = (a, b) => { + ronin.surface.clone(a, b) + return [a, b] + } + + // 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] + } + + // Misc this.echo = (...args) => { ronin.log(args) @@ -309,26 +304,39 @@ 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) + } + + // Client + this.ronin = ronin + + // Livecoding + this.time = () => { // Returns timestamp in milliseconds. + return Date.now + } + + this.animate = (play = true) => { // Toggles animation. + ronin.animate(play) + } + + // javascript interop + this.js = 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 } - - // Livecoding - this.time = Date.now - - // javascript interop - this.js = window - - // Client - this.ronin = ronin } diff --git a/desktop/sources/scripts/lisp.js b/desktop/sources/scripts/lisp.js index fe9b520..3a38a83 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) { diff --git a/desktop/sources/scripts/ronin.js b/desktop/sources/scripts/ronin.js index 0883f22..ea59299 100644 --- a/desktop/sources/scripts/ronin.js +++ b/desktop/sources/scripts/ronin.js @@ -52,7 +52,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 d1143e0..a1bd5eb 100644 --- a/desktop/sources/scripts/surface.js +++ b/desktop/sources/scripts/surface.js @@ -61,6 +61,15 @@ function Surface (ronin) { context.closePath() } + 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) + }) + return gradient + } + // Tracers this.trace = function (shape, context) { diff --git a/documentation.md b/documentation.md new file mode 100644 index 0000000..706fee5 --- /dev/null +++ b/documentation.md @@ -0,0 +1,159 @@ +# Functions + +## IO + +`(open path)` + +`(export path type quality)` + +`(draw path rect)` + +`(resize width height)` + +`(crop rect)` + +`(folder path)` + +`(exit)` + +## Logic + +`(gt a b)` check if `a` is greater than `b` + +`(lt a b)` check if `a` is lower than `b` + +`(eq a b)` check if `a` is equal to `b` + +`(and a b )` returns true if all conditions are true + +`(or a b )` returns true if at least one condition is true + +## Arrays + +`(map function array)` + +`(filter function array)` + +`(reduce function array accumulator)` + +`(len array)` + +`(first array)` + +`(last array)` + +`(rest array)` + +`(range start end step)` + +## Shapes + +`(pos x y)` + +`(size w h)` + +`(rect x y w h t)` + +`(circle x y r)` + +`(line start end)` + +`(text x y g string font)` + +`(svg data)` + +## Helpers + +`(frame)` + +`(center)` + +`(scale rect width height)` + +## Copy/Paste + +`(clone start end)` clone start `rect` into end `rect` + +`(stroke shape thickness color)` + +`(fill shape color)` + +`(clear shape)` + +## Objects + +`(get item key )` + +`(set item key val)` + +## Colors + +`(theme variable)` + +`(gradient (x1,y1,x2,y2) colors)` + +`(pixels rect function q)` + +`(saturation pixel q)` + +`(contrast pixel q)` + +## Math + +`(add ...values)` + +`(sub...values)` + +`(mul ...values)` + +`(div ...values)` + +`(mod a b)` + +`(clamp value min max)` + +`(step value step)` + +`(min a b)` + +`(max a b)` + +`(ceil value)` + +`(floor value)` + +`(sin a)` + +`(cos a)` + +`PI, TWO_PI` + +`(random)` + +`(random start end)` + +`(random max)` + +## Generics + +`(echo args)` + +`(str args)` + +`(test name value expectedValue)` + +## Livecoding + +`(time)` returns timestamp in milliseconds + +`(animate)` start animation + +`(animate false)` stop animation + +## Javascript interop + +`js` + +## Client + +`ronin` diff --git a/examples/animate.lisp b/examples/animate.lisp index fa12b3c..b43d696 100644 --- a/examples/animate.lisp +++ b/examples/animate.lisp @@ -1,13 +1,12 @@ ; animate ( - (def start (get ronin "animate")) - (def t (sin (div (time) 100))) (def pos (add 200 (mul 30 t))) (defn square (a) (rect a a a a)) (stroke (square pos) 1 "red") - (start) + (animate) + ;(animate false) to stop animation ) \ No newline at end of file diff --git a/examples/g_spiral1.lisp b/examples/g_spiral1.lisp new file mode 100644 index 0000000..0fc82e6 --- /dev/null +++ b/examples/g_spiral1.lisp @@ -0,0 +1,29 @@ +; animated recusive spiral +; by @local_guru +( + (def start (get ronin "animate")) + (clear) + (defn rec + (v) + (if (gt v 0) + ((stroke + (circle + (add 300 + (mul (cos (add (div v 17) (div (time) 2000))) + (div v 2) + ) + ) + (add 300 + (mul (sin (div v 11)) + (div v 2) + ) + ) + (div v 2)) + 1 "rgba(255,255,255,0.1") + (rec (sub v 0.3)) + ) + ) + ) +(start) +(rec 300) +) 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/gradient.lisp b/examples/gradient.lisp new file mode 100644 index 0000000..b11e9aa --- /dev/null +++ b/examples/gradient.lisp @@ -0,0 +1,16 @@ +( +; gradients + +(clear) +(fill + (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 ") + (gradient + (0 -50 600 175) + ("red" "orange" "blue" "green"))) + +(stroke + (svg "M255,60 L255,60 L135,180 L75,60 L195,210 L120,225 L105,225 L165,255 L225,195 L255,135 L285,150") 1 + (gradient + (50 0 180 0) + ("black" "white" "blue" "green"))) +) \ No newline at end of file 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