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