diff --git a/bun.lockb b/bun.lockb
new file mode 100755
index 0000000..8a4c905
Binary files /dev/null and b/bun.lockb differ
diff --git a/index.html b/index.html
deleted file mode 100644
index ce41bb9..0000000
--- a/index.html
+++ /dev/null
@@ -1,1773 +0,0 @@
-
-
-
-
-
-
- Ronin
-
-
-
-
-
-
diff --git a/open.sh b/open.sh
new file mode 100755
index 0000000..13f8b75
--- /dev/null
+++ b/open.sh
@@ -0,0 +1,4 @@
+localhost . -p 1616 &
+$(command -v ungoogled-chromium ||\
+command -v chromium ||\
+command -v chrome) --new-window --app="http://localhost:1616/src/browser/index.html"
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..f55449e
--- /dev/null
+++ b/package.json
@@ -0,0 +1,6 @@
+{
+ "type": "module",
+ "dependencies": {
+ "@napi-rs/canvas": "^0.1.41"
+ }
+}
\ No newline at end of file
diff --git a/push.sh b/push.sh
deleted file mode 100755
index 3f02b0f..0000000
--- a/push.sh
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/bin/bash
-
-node scripts/lib/build
-rm -r release
-mkdir release
-cp index.html release/index.html
-cp README.txt release/README.txt
-~/Applications/butler push ~/Repositories/Hundredrabbits/Ronin/release hundredrabbits/ronin:main
-~/Applications/butler status hundredrabbits/ronin
-rm -r release
\ No newline at end of file
diff --git a/scripts/lib/build.js b/scripts/lib/build.js
deleted file mode 100644
index c77eec6..0000000
--- a/scripts/lib/build.js
+++ /dev/null
@@ -1,73 +0,0 @@
-'use strict'
-
-const fs = require('fs')
-const libs = fs.readdirSync('./scripts/lib').filter((file) => { return file.indexOf('.js') > 0 && file !== 'build.js' })
-const scripts = fs.readdirSync('./scripts').filter((file) => { return file.indexOf('.js') > 0 })
-const styles = fs.readdirSync('./links').filter((file) => { return file.indexOf('.css') > 0 })
-const id = process.cwd().split('/').slice(-1)[0]
-
-function cleanup (txt) {
- const lines = txt.split('\n')
- let output = ''
- for (const line of lines) {
- if (line.trim() === '') { continue }
- if (line.trim().substr(0, 2) === '//') { continue }
- if (line.indexOf('/*') > -1 && line.indexOf('*/') > -1) { continue }
- output += line + '\n'
- }
- return output
-}
-
-// Create release
-
-fs.writeFileSync('index.html', cleanup(`
-
-
-
-
-
-
- ${id}
-
-
-
-
-
-`))
-
-// Create debug
-
-fs.writeFileSync('debug.html', `
-
-
-
-
-
-
- ${id}
- ${styles.reduce((acc, item) => { return `${acc}\n` }, '')}
- ${libs.reduce((acc, item) => { return `${acc}\n` }, '')}
- ${scripts.reduce((acc, item) => { return `${acc}\n` }, '')}
-
-
-
-
-`)
-
-console.log(`Built ${id}`)
diff --git a/scripts/lib/lain.js b/src/Lain.js
similarity index 64%
rename from scripts/lib/lain.js
rename to src/Lain.js
index 485bdca..acadc17 100644
--- a/scripts/lib/lain.js
+++ b/src/Lain.js
@@ -3,9 +3,11 @@
// In the real world, it didn’t matter if I was there or not.
// When I realized that, I was no longer afraid of losing my body.
+const AsyncFunction = (async () => {}).constructor
+
function Lain (lib = {}) {
const TYPES = { identifier: 0, number: 1, string: 2, bool: 3, symbol: 4 }
-
+
const Context = function (scope, parent) {
this.scope = scope
this.parent = parent
@@ -19,74 +21,57 @@ function Lain (lib = {}) {
}
const special = {
- let: function (input, context) {
- const letContext = input[1].reduce(function (acc, x) {
- acc.scope[x[0].value] = interpret(x[1], context)
- return acc
- }, new Context({}, context))
- return interpret(input[2], letContext)
+ let: async function (input, context) {
+ const letContext = new Context({}, context)
+ for(let x of input[1]) {
+ letContext.scope[x[0].value] = await interpret(x[1], context)
+ }
+ return await interpret(input[2], letContext)
},
- def: function (input, context) {
+ def: async function (input, context) {
if (input.length !== 3) { console.warn('Lain', 'Invalid definition.'); return }
const identifier = input[1].host ? input[1].host : input[1].value
if (input[1].host) {
if (!context.scope[identifier]) { context.scope[identifier] = {} }
- context.scope[identifier][input[1].value] = interpret(input[2], context)
+ context.scope[identifier][input[1].value] = await interpret(input[2], context)
return context.scope[identifier][input[1].value]
}
- context.scope[identifier] = interpret(input[2], context)
+ context.scope[identifier] = await interpret(input[2], context)
return context.scope[identifier]
},
- defn: function (input, context) {
+ defn: async function (input, context) {
const identifier = input[1].value
if (context.scope[identifier]) { console.warn('Lain', `Redefining function: ${identifier}`) }
-
const fnParams = input[2].type === TYPES.string && input[3] ? input[3] : input[2]
- const fnBodyFirstIndex = input[2].type === TYPES.string && input[4] ? 4 : 3
- const fnBody = input.slice(fnBodyFirstIndex)
-
- context.scope[identifier] = function () {
+ const fnBody = input[2].type === TYPES.string && input[4] ? input[4] : input[3]
+ context.scope[identifier] = async function () {
const lambdaArguments = arguments
const lambdaScope = fnParams.reduce(function (acc, x, i) {
acc[x.value] = lambdaArguments[i]
return acc
}, {})
-
- let result = interpret(fnBody, new Context(lambdaScope, context))
- //lisp returns the return value of the last executed function, not a list of all results of all functions.
- return getReturnValue(result)
+ return await interpret(fnBody, new Context(lambdaScope, context))
}
},
- λ: function (input, context) {
- return function () {
+ λ: async function (input, context) {
+ return async function () {
const lambdaArguments = arguments
const lambdaScope = input[1].reduce(function (acc, x, i) {
acc[x.value] = lambdaArguments[i]
return acc
}, {})
-
- let result = interpret(input.slice(2), new Context(lambdaScope, context))
- //lisp returns the return value of the last executed function, not a list of all results of all functions.
- return getReturnValue(result)
+ return await interpret(input[2], new Context(lambdaScope, context))
}
},
- if: function (input, context) {
- return interpret(input[1], context) ? interpret(input[2], context) : input[3] ? interpret(input[3], context) : []
+ if: async function (input, context) {
+ return await interpret(input[1], context) ? await interpret(input[2], context) : input[3] ? interpret(input[3], context) : []
}
}
- const getReturnValue = function (interpretResult) {
- //lisp returns the return value of the last executed function,
- //not a list of all results of all functions.
- if(!interpretResult || !(interpretResult instanceof Array) || !interpretResult.length){
- return interpretResult
- }
- return interpretResult[interpretResult.length - 1]
- }
-
- const interpretList = function (input, context) {
+ const interpretList = async function (input, context) {
if (input.length > 0 && input[0].value in special) {
- return special[input[0].value](input, context)
+ return await special[input[0].value](input, context)
+ .catch(console.error)
}
const list = []
for (let i = 0; i < input.length; i++) {
@@ -100,18 +85,25 @@ function Lain (lib = {}) {
list.push(obj => obj[input[i].value])
}
} else {
- list.push(interpret(input[i], context))
+ list.push(await interpret(input[i], context))
}
}
- return list[0] instanceof Function ? list[0].apply(undefined, list.slice(1)) : list
+ if(list[0] instanceof AsyncFunction) {
+ return await list[0].apply(undefined, list.slice(1))
+ .catch(console.error)
+ } else if(list[0] instanceof Function) {
+ return list[0].apply(undefined, list.slice(1))
+ } else {
+ return list
+ }
}
- const interpret = function (input, context) {
+ const interpret = async function (input, context) {
if (!input) { console.warn('Lain', context.scope); return null }
if (context === undefined) {
- return interpret(input, new Context(lib))
+ return await interpret(input, new Context(lib))
} else if (input instanceof Array) {
- return interpretList(input, context)
+ return await interpretList(input, context)
} else if (input.type === TYPES.identifier) {
return context.get(input.value)
} else if (input.type === TYPES.number || input.type === TYPES.symbol || input.type === TYPES.string || input.type === TYPES.bool) {
@@ -157,7 +149,17 @@ function Lain (lib = {}) {
}).join('"').trim().split(/\s+/).map(function (x) { return x.replace(/!ws!/g, ' ') })
}
- this.run = (input) => {
- return interpret(parenthesize(tokenize(input)))
+ this.run = async (input) => {
+ return await interpret(parenthesize(tokenize(input)))
+ }
+
+ this.runSandbox = async (input, args) => {
+ let context = new Context(lib)
+ for(let i = 0; i < args.length; i++) {
+ context.scope['arg-' + i] = args[i]
+ }
+ return await interpret(parenthesize(tokenize(input)), context)
}
}
+
+export default Lain
\ No newline at end of file
diff --git a/scripts/library.js b/src/Library.js
similarity index 89%
rename from scripts/library.js
rename to src/Library.js
index b0e60cc..2d9b340 100644
--- a/scripts/library.js
+++ b/src/Library.js
@@ -1,28 +1,43 @@
-'use strict'
-
-/* global Image */
-
function Library (client) {
// IO
- this.open = (name, scale = 1) => { // Import a graphic and scale canvas to fit.
- const img = client.cache.get(name)
- if (!img) { client.log('No data for ' + name); return }
+
+ this.open = async (name, scale = 1) => { // Import a graphic and scale canvas to fit.
+ const img = await resolveImage(name)
const rect = this.rect(0, 0, img.width * scale, img.height * scale)
this.resize(rect.w, rect.h)
- this.import(name, rect)
+ await this.import(name, rect)
return rect
}
- this.import = (name, shape, alpha = 1) => { // Imports a graphic file with format.
- const img = client.cache.get(name)
- if (!img) { client.log('No data for ' + name); return }
+ this.import = async (name, shape, alpha = 1) => { // Imports a graphic file with format.
+ const img = await resolveImage(name)
client.surface.draw(img, shape, alpha)
return shape || this.rect(0, 0, img.width, img.height)
}
- this.export = (format = 'jpg', quality = 0.9) => { // Exports a graphic file with format.
+ this.export = async (format = 'jpg', quality = 0.9, name = `ronin-${timestamp()}`) => { // Exports a graphic file with format.
const type = `image/${format === 'jpeg' || format === 'jpg' ? 'jpeg' : 'png'}`
- client.source.write('ronin', format, client.surface.el.toDataURL(type, quality), type)
+ console.log('Exporting!')
+ client.source.write(name, format, client.surface.toDataURL(type, quality), type)
+ }
+
+ function timestamp (d = new Date(), e = new Date(d)) {
+ return `${arvelie()}-${neralie()}`
+ }
+
+ function arvelie (date = new Date()) {
+ const start = new Date(date.getFullYear(), 0, 0)
+ const diff = (date - start) + ((start.getTimezoneOffset() - date.getTimezoneOffset()) * 60 * 1000)
+ const doty = Math.floor(diff / 86400000) - 1
+ const y = date.getFullYear().toString().substr(2, 2)
+ const m = doty === 364 || doty === 365 ? '+' : String.fromCharCode(97 + Math.floor(doty / 14)).toUpperCase()
+ const d = `${(doty === 365 ? 1 : doty === 366 ? 2 : (doty % 14)) + 1}`.padStart(2, '0')
+ return `${y}${m}${d}`
+ }
+
+ function neralie (d = new Date(), e = new Date(d)) {
+ const ms = e - d.setHours(0, 0, 0, 0)
+ return (ms / 8640 / 10000).toFixed(6).substr(2, 6)
}
this.files = () => {
@@ -86,9 +101,9 @@ function Library (client) {
this.resize = (w = client.surface.bounds().w, h = client.surface.bounds().h, fit = true) => { // Resizes the canvas to target w and h, returns the rect.
if (w === this['get-frame']().w && h === this['get-frame']().h) { return }
const rect = { x: 0, y: 0, w, h }
- const a = document.createElement('img')
- const b = document.createElement('img')
- a.src = client.surface.el.toDataURL()
+ const a = new Image()
+ const b = new Image()
+ a.src = client.surface.toDataURL()
client.surface.resizeImage(a, b)
client.surface.resize(rect, fit)
return client.surface.draw(b, rect)
@@ -96,9 +111,9 @@ function Library (client) {
this.rescale = (w = 1, h) => { // Rescales the canvas to target ratio of w and h, returns the rect.
const rect = { x: 0, y: 0, w: this['get-frame']().w * w, h: this['get-frame']().h * (h || w) }
- const a = document.createElement('img')
- const b = document.createElement('img')
- a.src = client.surface.el.toDataURL()
+ const a = new Image()
+ const b = new Image()
+ a.src = client.surface.toDataURL()
client.surface.resizeImage(a, b)
client.surface.resize(rect, true)
return client.surface.draw(b, rect)
@@ -307,23 +322,23 @@ function Library (client) {
// Math
- this.add = (...args) => { // Adds values.
+ this.add = this['+'] = (...args) => { // Adds values.
return args.reduce((sum, val) => sum + val)
}
- this.sub = (...args) => { // Subtracts values.
+ this.sub = this['-'] = (...args) => { // Subtracts values.
return args.reduce((sum, val) => sum - val)
}
- this.mul = (...args) => { // Multiplies values.
+ this.mul = this['*'] = (...args) => { // Multiplies values.
return args.reduce((sum, val) => sum * val)
}
- this.div = (...args) => { // Divides values.
+ this.div = this['/'] = (...args) => { // Divides values.
return args.reduce((sum, val) => sum / val)
}
- this.mod = (a, b) => { // Returns the modulo of a and b.
+ this.mod = this['%'] = (a, b) => { // Returns the modulo of a and b.
return a % b
}
@@ -694,4 +709,19 @@ function Library (client) {
this['get-frame'] = () => { // Get frame shape.
return client.surface.getFrame()
}
+
+ // Extras
+
+ // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation
+ this['blend-mode'] = (operation = 'source-over') => {
+ client.surface.context.globalCompositeOperation = operation
+ }
+
+ async function resolveImage(name) {
+ const img = new Image()
+ img.src = await client.cache.resolve(name)
+ return img
+ }
}
+
+export default Library
\ No newline at end of file
diff --git a/scripts/surface.js b/src/Surface.js
similarity index 79%
rename from scripts/surface.js
rename to src/Surface.js
index 6dd944c..936a6f2 100644
--- a/scripts/surface.js
+++ b/src/Surface.js
@@ -1,33 +1,5 @@
-'use strict'
-
-/* global Path2D */
-/* global Image */
-
function Surface (client) {
- this.el = document.createElement('canvas')
- this.el.id = 'surface'
- this._guide = document.createElement('canvas')
- this._guide.id = 'guide'
- this._guide.setAttribute('tabindex', '1') // focus is necessary to capture keyboard events
- this.ratio = window.devicePixelRatio
-
- // Contexts
- this.context = this.el.getContext('2d')
- this.guide = this._guide.getContext('2d')
-
- this.install = function (host) {
- host.appendChild(this.el)
- host.appendChild(this._guide)
- window.addEventListener('resize', (e) => { this.onResize() }, false)
- this._guide.addEventListener('mousedown', client.onMouseDown, false)
- this._guide.addEventListener('mousemove', client.onMouseMove, false)
- this._guide.addEventListener('mouseup', client.onMouseUp, false)
- this._guide.addEventListener('mouseover', client.onMouseOver, false)
- this._guide.addEventListener('mouseout', client.onMouseOut, false)
- this._guide.addEventListener('keydown', client.onKeyDown, false)
- this._guide.addEventListener('keyup', client.onKeyUp, false)
- this._guide.addEventListener('keypress', client.onKeyPress, false)
- }
+ this.createCanvas = null
this.start = function () {
this.maximize()
@@ -42,8 +14,8 @@ function Surface (client) {
}
// Shape
-
- this.stroke = (shape, color = client.theme.get('f_high'), width = 2, context = this.context) => {
+
+ this.stroke = (shape, color = client.resolveMissingColor('f_high'), width = 2, context = this.context) => {
context.beginPath()
this.trace(shape, context)
context.lineWidth = width
@@ -66,7 +38,7 @@ function Surface (client) {
// Fill
- this.fill = (shape, color = client.theme.get('b_high'), context = this.context) => {
+ this.fill = (shape, color = client.resolveMissingColor('b_high'), context = this.context) => {
context.beginPath()
context.fillStyle = typeof color === 'object' && color.rgba ? color.rgba : color
this.trace(shape, context)
@@ -219,21 +191,15 @@ function Surface (client) {
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
- this.el.style.width = (size.w / this.ratio) + 'px'
- this.el.style.height = (size.h / this.ratio) + 'px'
- this._guide.width = size.w
- this._guide.height = size.h
- this._guide.style.width = (size.w / this.ratio) + 'px'
- this._guide.style.height = (size.h / this.ratio) + 'px'
+ this._canvas.width = size.w
+ this._canvas.height = size.h
}
this.copy = function (rect) {
- const newCanvas = document.createElement('canvas')
+ const newCanvas = this.createCanvas()
newCanvas.width = rect.w
newCanvas.height = rect.h
- newCanvas.getContext('2d').drawImage(this.el, rect.x, rect.y, rect.w, rect.h, 0, 0, rect.w, rect.h)
+ newCanvas.getContext('2d').drawImage(this._canvas, rect.x, rect.y, rect.w, rect.h, 0, 0, rect.w, rect.h)
return newCanvas
}
@@ -249,9 +215,8 @@ function Surface (client) {
let cW = src.naturalWidth
let cH = src.naturalHeight
tmp.src = src.src
- // resolve()
tmp.onload = () => {
- canvas = document.createElement('canvas')
+ canvas = this.createCanvas()
cW /= 2
cH /= 2
if (cW < src.width) {
@@ -262,7 +227,7 @@ function Surface (client) {
}
canvas.width = cW
canvas.height = cH
- context = canvas.getContext('2d')
+ context = canvas.getContext('2d', { colorspace: "srgb" })
context.drawImage(tmp, 0, 0, cW, cH)
dst.src = canvas.toDataURL(type, quality)
if (cW <= src.width || cH <= src.height) { return resolve() }
@@ -271,7 +236,7 @@ function Surface (client) {
}
})
}
-
+
this.maximize = () => {
this.resize(this.bounds())
}
@@ -281,11 +246,13 @@ function Surface (client) {
}
this.getFrame = () => {
- return { x: 0, y: 0, w: this.el.width, h: this.el.height, c: this.el.width / 2, m: this.el.height / 2 }
+ return { x: 0, y: 0, w: this._canvas.width, h: this._canvas.height, c: this._canvas.width / 2, m: this._canvas.height / 2 }
}
this.toggleGuides = function () {
- this._guide.className = this._guide.className === 'hidden' ? '' : 'hidden'
+ if(this._guide) {
+ this._guide.className = this._guide.className === 'hidden' ? '' : 'hidden'
+ }
}
function isRect (shape) {
@@ -325,3 +292,5 @@ function Surface (client) {
}
}
}
+
+export default Surface
\ No newline at end of file
diff --git a/scripts/lib/acels.js b/src/browser/Acels.js
similarity index 99%
rename from scripts/lib/acels.js
rename to src/browser/Acels.js
index 80ab5ed..aafe680 100644
--- a/scripts/lib/acels.js
+++ b/src/browser/Acels.js
@@ -1,5 +1,3 @@
-'use strict'
-
function Acels (client) {
this.el = document.createElement('ul')
this.el.id = 'acels'
@@ -117,3 +115,5 @@ function Acels (client) {
function capitalize (s) { return s.substr(0, 1).toUpperCase() + s.substr(1) }
}
+
+export default Acels
\ No newline at end of file
diff --git a/scripts/client.js b/src/browser/Client.js
similarity index 91%
rename from scripts/client.js
rename to src/browser/Client.js
index a273946..496819e 100644
--- a/scripts/client.js
+++ b/src/browser/Client.js
@@ -1,14 +1,11 @@
-'use strict'
+import Acels from "./Acels.js"
+import Theme from "./Theme.js"
+import Source from "./Source.js"
+import Commander from "./Commander.js"
+import DisplaySurface from "./DisplaySurface.js"
-/* global Acels */
-/* global Theme */
-/* global Source */
-/* global Commander */
-/* global Surface */
-/* global Library */
-/* global Lain */
-/* global Image */
-/* global requestAnimationFrame */
+import Library from "../Library.js"
+import Lain from "../Lain.js"
function Client () {
this.el = document.createElement('div')
@@ -19,7 +16,7 @@ function Client () {
this.source = new Source(this)
this.commander = new Commander(this)
- this.surface = new Surface(this)
+ this.surface = new DisplaySurface(this)
this.library = new Library(this)
this.lain = new Lain(this.library)
@@ -47,13 +44,17 @@ function Client () {
this.acels.set('File', 'Save', 'CmdOrCtrl+S', () => { this.source.write('ronin', 'lisp', this.commander._input.value, 'text/plain') })
this.acels.set('File', 'Export Image', 'CmdOrCtrl+E', () => { this.source.write('ronin', 'png', this.surface.el.toDataURL('image/png', 1.0), 'image/png') })
this.acels.set('File', 'Open', 'CmdOrCtrl+U', () => { this.source.open('lisp', this.whenOpen) })
- this.acels.set('View', 'Toggle Guides', 'CmdOrCtrl+Shift+H', () => { this.surface.toggleGuides() })
+ this.acels.set('View', 'Toggle Guides', 'CmdOrCtrl+H', () => { this.surface.toggleGuides() })
this.acels.set('View', 'Toggle Commander', 'CmdOrCtrl+K', () => { this.commander.toggle() })
this.acels.set('View', 'Expand Commander', 'CmdOrCtrl+Shift+K', () => { this.commander.toggle(true) })
this.acels.set('Project', 'Eval', 'CmdOrCtrl+Enter', () => { this.commander.eval() })
this.acels.set('Project', 'Eval Selection', 'Alt+Enter', () => { this.commander.evalSelection() })
this.acels.set('Project', 'Re-Indent', 'CmdOrCtrl+Shift+I', () => { this.commander.lint() })
this.acels.set('Project', 'Clean', 'Escape', () => { this.commander.cleanup() })
+ this.acels.set('Project', 'Start Function', '`', () => {
+ this.commander.inject('()')
+ this.commander._input.selectionEnd = this.commander._input.selectionStart - 1
+ })
this.acels.route(this)
}
@@ -237,3 +238,5 @@ function Client () {
return { x, y, xy, wh, d, r, a, line, rect, pos, size, circle, arc, type, 'is-down': type !== 'mouse-up' ? true : null }
}
}
+
+export default Client
\ No newline at end of file
diff --git a/scripts/commander.js b/src/browser/Commander.js
similarity index 83%
rename from scripts/commander.js
rename to src/browser/Commander.js
index f940036..aff507c 100644
--- a/scripts/commander.js
+++ b/src/browser/Commander.js
@@ -27,9 +27,57 @@ function Commander (client) {
this._input.addEventListener('click', this.onClick)
this._eval.addEventListener('click', () => { this.eval() })
- this._input.onkeydown = (e) => {
- if (e.keyCode === 9 || e.which === 9) { e.preventDefault(); this.inject(' ') }
- }
+ // this._input.onkeydown = (e) => {
+ // const cursorIndex = this._input.selectionStart
+
+ // switch(e.code) {
+ // case 'Tab':
+ // e.preventDefault()
+
+ // for(let i = cursorIndex - 1; i > 0; i--) {
+ // let char = this._input.value[i]
+
+ // if(char !== ' ') {
+ // if(this._input.value[i] == '\n') {
+ // this.inject(' ')
+ // return
+ // }
+ // // else {
+ // // let start = this._input.value.indexOf(')', cursorIndex)
+
+ // // if(start !== -1) {
+ // // this._input.selectionStart = start
+ // // }
+ // // }
+
+ // break
+ // }
+ // }
+
+ // let jumpTo = this._input.value.indexOf(/[^\w]/g, cursorIndex + 1)
+
+ // if(jumpTo !== -1) {
+ // console.log(jumpTo)
+
+ // this._input.selectionStart = jumpTo
+ // }
+
+ // // if('\n(;)'.includes(this._input.value[cursorIndex])) {
+ // // this._input.selectionStart = cursorIndex + 1
+ // // return
+ // // }
+ // break
+
+ // case 'Backspace':
+ // // If the cursor is in the middle of a "()" expand the cursor to delete both
+ // if(cursorIndex == this._input.selectionEnd &&
+ // this._input.value[cursorIndex -1] == '(' &&
+ // this._input.value[cursorIndex] == ')') {
+ // this._input.selectionStart--
+ // this._input.selectionEnd++
+ // }
+ // }
+ // }
client.surface.maximize()
}
@@ -275,3 +323,5 @@ function lintLISP (str) {
val = val.split('\n').map((line) => { return line.substr(0, 2) === '; ' ? `\n${line}\n` : line }).join('\n')
return val.trim()
}
+
+export default Commander
\ No newline at end of file
diff --git a/src/browser/DisplaySurface.js b/src/browser/DisplaySurface.js
new file mode 100755
index 0000000..5b6f448
--- /dev/null
+++ b/src/browser/DisplaySurface.js
@@ -0,0 +1,54 @@
+import Surface from "../Surface.js"
+import { classExtends } from "../util.js"
+
+const DisplaySurface = function(client) {
+ classExtends(this, Surface, client)
+
+ this._canvas = document.createElement('canvas')
+ this._canvas.id = 'surface'
+ this._guide = document.createElement('canvas')
+ this._guide.id = 'guide'
+ this._guide.setAttribute('tabindex', '1') // focus is necessary to capture keyboard events
+ this.ratio = window.devicePixelRatio
+ this.context = this._canvas.getContext('2d')
+ this.guide = this._guide.getContext('2d')
+
+ this.createCanvas = () =>
+ document.createElement('canvas')
+
+ this.install = function (host) {
+ host.appendChild(this._canvas)
+ host.appendChild(this._guide)
+ window.addEventListener('resize', (e) => { this.onResize() }, false)
+ this._guide.addEventListener('mousedown', client.onMouseDown, false)
+ this._guide.addEventListener('mousemove', client.onMouseMove, false)
+ this._guide.addEventListener('mouseup', client.onMouseUp, false)
+ this._guide.addEventListener('mouseover', client.onMouseOver, false)
+ this._guide.addEventListener('mouseout', client.onMouseOut, false)
+ this._guide.addEventListener('keydown', client.onKeyDown, false)
+ this._guide.addEventListener('keyup', client.onKeyUp, false)
+ this._guide.addEventListener('keypress', client.onKeyPress, false)
+ }
+
+ // Clone the method
+ const genericResize = this.resize.bind(this)
+ this.resize = (size, fit) => {
+ genericResize(size, fit)
+ this._guide.width = size.w
+ this._guide.height = size.h
+ this._canvas.style.width = (size.w / this.ratio) + 'px'
+ this._canvas.style.height = (size.h / this.ratio) + 'px'
+ this._guide.style.width = (size.w / this.ratio) + 'px'
+ this._guide.style.height = (size.h / this.ratio) + 'px'
+ }
+
+ this.toggleGuides = function () {
+ this._guide.className = this._guide.className === 'hidden' ? '' : 'hidden'
+ }
+
+ this.toDataURL = () => {
+ return this._canvas.toDataURL()
+ }
+}
+
+export default DisplaySurface
\ No newline at end of file
diff --git a/scripts/lib/source.js b/src/browser/Source.js
similarity index 98%
rename from scripts/lib/source.js
rename to src/browser/Source.js
index ba2554c..0484758 100644
--- a/scripts/lib/source.js
+++ b/src/browser/Source.js
@@ -1,8 +1,3 @@
-'use strict'
-
-/* global FileReader */
-/* global MouseEvent */
-
function Source (client) {
this.cache = {}
@@ -100,3 +95,5 @@ function Source (client) {
return (ms / 8640 / 10000).toFixed(6).substr(2, 6)
}
}
+
+export default Source
\ No newline at end of file
diff --git a/scripts/lib/theme.js b/src/browser/Theme.js
similarity index 99%
rename from scripts/lib/theme.js
rename to src/browser/Theme.js
index 7d4ac79..59520fb 100644
--- a/scripts/lib/theme.js
+++ b/src/browser/Theme.js
@@ -168,3 +168,5 @@ function Theme (client) {
try { new DOMParser().parseFromString(text, 'text/xml'); return true } catch (error) { return false }
}
}
+
+export default Theme
\ No newline at end of file
diff --git a/src/browser/index.html b/src/browser/index.html
new file mode 100644
index 0000000..d194ffb
--- /dev/null
+++ b/src/browser/index.html
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+Ronin
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/index.js b/src/browser/index.js
new file mode 100644
index 0000000..23757a6
--- /dev/null
+++ b/src/browser/index.js
@@ -0,0 +1,16 @@
+import Path from 'path'
+import FS from 'fs/promises'
+import http from 'http'
+
+const root = Path.join(__dirname, '../')
+
+function respond(request, response) {
+ if(request.url == '/') {
+
+ }
+}
+
+const server = http.createServer()
+
+server.on('request', respond)
+server.listen(8080)
\ No newline at end of file
diff --git a/src/cli/Client.js b/src/cli/Client.js
new file mode 100755
index 0000000..5564c13
--- /dev/null
+++ b/src/cli/Client.js
@@ -0,0 +1,94 @@
+import Lain from "../Lain.js";
+import Library from "../Library.js";
+import NodeSurface from "./NodeSurface.js";
+import Source from "./Source.js";
+import Path from 'path'
+import FS from 'fs/promises'
+import { Image, Path2D } from "@napi-rs/canvas";
+
+// Polyfill
+global.Image = Image
+global.Path2D = Path2D
+
+function Client(directory) {
+ this.workingDirectory = directory
+ this.surface = new NodeSurface(this)
+ this.library = new Library(this)
+ this.lain = new Lain(this.library)
+ this.source = new Source(this)
+
+ this.run = (args, program) => {
+ if(program.indexOf('$') > -1) {
+ program = this.replaceConstants(program, args)
+ }
+
+ this.surface.clear()
+ return this.lain.run('(' + program + ')')
+ }
+
+ this.bind = () => {
+ // Perhaps binding in the cli could work by piping in
+ // CSV. Events could be recorded in the web client and saved
+ // to the format
+ }
+
+ this.log = (...msg) => {
+ console.log('Evaluation:', ...msg)
+ }
+
+ this.cache = {
+ data: {},
+ set: (key, content) => {
+ this.log((this.cache.data[key] ? 'Updated ' : 'Stored ') + key)
+ this.cache.data[key] = content
+ },
+
+ get: (key) => {
+ return this.cache.data[key]
+ },
+
+ // Async
+ load: (pathResolvable) => {
+ let key = resolvePath(pathResolvable)
+
+ return openImage(key)
+ },
+
+ // Async
+ resolve: (pathResolvable) => {
+ let key = resolvePath(pathResolvable),
+ out = this.cache.get(key)
+
+ if(out) {
+ return Promise.resolve(out)
+ } else {
+ return openImage(key)
+ }
+ }
+ }
+
+ const replaceConstants = (txt, args) => {
+ return txt.replaceAll(/\$(\d+)/g, (match, num) => {
+ return JSON.stringify(args[parseInt(num)])
+ })
+ }
+
+ const resolvePath = pathResolvable => {
+ return Path.isAbsolute(pathResolvable) ? Path.normalize(pathResolvable) : Path.join(this.workingDirectory, pathResolvable)
+ }
+
+ const openImage = async (key) => {
+ let buf = await FS.readFile(key)
+ .catch(err => {
+ console.error('Source', `Could not load image at path "${key}"`, err)
+ process.exit()
+ })
+
+ let img = new Image()
+ img.src = buf
+ this.cache.set(key, buf)
+ return buf
+ }
+}
+
+export default Client
\ No newline at end of file
diff --git a/src/cli/NodeSurface.js b/src/cli/NodeSurface.js
new file mode 100755
index 0000000..0a077d1
--- /dev/null
+++ b/src/cli/NodeSurface.js
@@ -0,0 +1,44 @@
+import { createCanvas } from "@napi-rs/canvas"
+import Surface from "../Surface.js"
+import { classExtends } from "../util.js"
+
+function noop () {}
+
+function NodeSurface (client) {
+ classExtends(this, Surface, client)
+
+ // TODO: Find proper default canvas size
+ this._canvas = createCanvas(500, 500)
+ this._canvas.id = 'surface'
+ this.context = this._canvas.getContext('2d')
+
+ this.createCanvas = () =>
+ createCanvas()
+
+ this.install = noop
+
+ this.clearGuide = noop
+
+ // this.toStream = (type, quality) => {
+ // switch(type) {
+ // case 'image/png':
+ // return this._canvas.toBuffer(type)
+
+ // case 'image/jpg':
+ // case 'image/jpeg':
+ // return this._canvas.createJPEGStream()
+
+ // case 'image/pdf':
+ // return this._canvas.createPDFStream()
+
+ // default:
+ // throw new Error(`Cannot handle filetype with extension "${ext}"`)
+ // }
+ // }
+
+ this.toDataURL = (type = 'image/png') => {
+ return this._canvas.toBuffer(type)
+ }
+}
+
+export default NodeSurface
\ No newline at end of file
diff --git a/src/cli/Source.js b/src/cli/Source.js
new file mode 100755
index 0000000..84ac809
--- /dev/null
+++ b/src/cli/Source.js
@@ -0,0 +1,15 @@
+import FS from 'fs'
+import Path from 'path'
+
+function Source (client) {
+ // Needed by Library
+ this.write = (name, ext, content, type, settings = 'charset=utf-8') => {
+ let path = Path.extname(name) == '' ? `${name}.${ext}` : name
+
+ path = Path.isAbsolute(`${name}.${ext}`) ? path : Path.join(client.workingDirectory, path)
+
+ return FS.writeFileSync(path, content)
+ }
+}
+
+export default Source
\ No newline at end of file
diff --git a/src/cli/index.js b/src/cli/index.js
new file mode 100755
index 0000000..07cdf93
--- /dev/null
+++ b/src/cli/index.js
@@ -0,0 +1,13 @@
+import Path from 'path'
+import FS from 'fs'
+import Client from './Client.js'
+
+let target = process.argv[2],
+ client = new Client(process.cwd())
+
+if(!Path.isAbsolute(target) )
+ target = Path.join(client.workingDirectory, target)
+
+let program = FS.readFileSync(target, 'utf8')
+
+client.run(process.argv.slice(3), program)
\ No newline at end of file
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 0000000..75b30c0
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,9 @@
+import Path from 'path'
+
+let target = process.argv[2]
+
+if(target == undefined || target == 'edit') {
+ import('./browser/index.js')
+} else {
+ import('./cli/index.js')
+}
\ No newline at end of file
diff --git a/src/util.js b/src/util.js
new file mode 100755
index 0000000..afc139b
--- /dev/null
+++ b/src/util.js
@@ -0,0 +1,17 @@
+function classExtends (context, parentConstructor, ...superArgs) {
+ let parent = parentConstructor.call(context, ...superArgs)
+
+ for(let key in parent) {
+ let value = parent[key]
+
+ if(typeof value == 'function') {
+ context[key] = value.bind(context)
+ } else {
+ context[key] = value
+ }
+ }
+}
+
+export {
+ classExtends
+}
\ No newline at end of file