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