Fork
This commit is contained in:
parent
53ea20cb58
commit
bb7bc4f9ea
1773
index.html
1773
index.html
File diff suppressed because one or more lines are too long
4
open.sh
Executable file
4
open.sh
Executable file
@ -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"
|
6
package.json
Normal file
6
package.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@napi-rs/canvas": "^0.1.41"
|
||||
}
|
||||
}
|
10
push.sh
10
push.sh
@ -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
|
@ -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(`
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${id}</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
${libs.reduce((acc, item) => { return `${acc}// Including Library ${item}\n\n${fs.readFileSync('./scripts/lib/' + item, 'utf8')}\n` }, '')}
|
||||
${scripts.reduce((acc, item) => { return `${acc}// Including Script ${item}\n\n${fs.readFileSync('./scripts/' + item, 'utf8')}\n` }, '')}
|
||||
const client = new Client()
|
||||
client.install(document.body)
|
||||
window.addEventListener('load', () => {
|
||||
client.start()
|
||||
})
|
||||
</script>
|
||||
<style>
|
||||
${styles.reduce((acc, item) => { return `${acc}/* Including Style ${item} */ \n\n${fs.readFileSync('./links/' + item, 'utf8')}\n` }, '')}
|
||||
</style>
|
||||
</body>
|
||||
</html>`))
|
||||
|
||||
// Create debug
|
||||
|
||||
fs.writeFileSync('debug.html', `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${id}</title>
|
||||
${styles.reduce((acc, item) => { return `${acc}<link rel="stylesheet" type="text/css" href="./links/${item}"/>\n` }, '')}
|
||||
${libs.reduce((acc, item) => { return `${acc}<script type="text/javascript" src="./scripts/lib/${item}"></script>\n` }, '')}
|
||||
${scripts.reduce((acc, item) => { return `${acc}<script type="text/javascript" src="./scripts/${item}"></script>\n` }, '')}
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
const client = new Client()
|
||||
client.install(document.body)
|
||||
window.addEventListener('load', () => {
|
||||
client.start()
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>`)
|
||||
|
||||
console.log(`Built ${id}`)
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
54
src/browser/DisplaySurface.js
Executable file
54
src/browser/DisplaySurface.js
Executable file
@ -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
|
@ -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
|
@ -168,3 +168,5 @@ function Theme (client) {
|
||||
try { new DOMParser().parseFromString(text, 'text/xml'); return true } catch (error) { return false }
|
||||
}
|
||||
}
|
||||
|
||||
export default Theme
|
76
src/browser/index.html
Normal file
76
src/browser/index.html
Normal file
@ -0,0 +1,76 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ronin</title>
|
||||
<style>
|
||||
|
||||
* { margin:0;padding:0;border:0;outline:0;text-decoration:none;font-weight:inherit;font-style:inherit;color:inherit;font-size:100%;font-family:inherit;vertical-align:baseline;list-style:none;border-collapse:collapse;border-spacing:0; -webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale;}
|
||||
body { margin:0px; padding:0px; overflow:hidden; font-family: monospace; background:000; -webkit-app-region: drag; -webkit-user-select: none; font-size:12px; transition: background 500ms}
|
||||
*:focus { outline: none; }
|
||||
#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: calc(50vw - 30px);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 - 105px); resize: none; font-size: 12px;line-height: 15px; padding-right: 15px}
|
||||
#ronin #wrapper #commander #status { position: absolute; bottom: 0px; line-height: 15px; height: 30px; overflow: hidden; width: calc(100% - 75px); padding-left:45px;}
|
||||
#ronin #wrapper #commander #status #eval { display: block; width: 26px; height: 26px; position: absolute; top: 0px; border-radius: 15px; left:0px; cursor: pointer; border:2px solid #fff; transition: background-color 250ms, border-color 250ms}
|
||||
#ronin #wrapper #commander #status #eval:hover { background: none }
|
||||
#ronin.expand #wrapper #commander { width:100%; }
|
||||
#ronin #surface, #ronin #guide { position: absolute; top:0px; -webkit-user-select: none;-webkit-app-region: no-drag; background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20'><circle cx='10' cy='10' r='1' fill='%23555'></circle></svg>"); background-size: 10px 10px; background-position: -4px -4px; width:100%; height:100%; transition: left 250ms, opacity 250ms; opacity: 1; }
|
||||
#ronin.hidden #wrapper #commander { margin-left:-40vw; }
|
||||
#ronin.hidden #surface, #ronin.hidden #guide { left:0; }
|
||||
#ronin #guide.hidden { opacity: 0 }
|
||||
#ronin.hidden #wrapper #commander { margin-left:-50vw; }
|
||||
#ronin #surface,#ronin #guide { left:50vw; }
|
||||
#ronin #guide { background:none; }
|
||||
#ronin #surface { border-radius: 2px }
|
||||
#acels { position: fixed;width: 30px;background: red;top: 0;left: 0; width: 100vw; color:black; background:white; font-size:11px; line-height: 20px; transition: margin-top 0.25s; z-index: 9999; padding-left: 25px; }
|
||||
#acels.hidden { margin-top:-20px; }
|
||||
#acels.hidden > li > ul > li { display: none }
|
||||
#acels > li { float: left; position: relative; cursor: pointer; padding:0px 5px; display: inline-block; }
|
||||
#acels > li:hover { background: black; color:white; }
|
||||
#acels > li > ul { display: none; position: absolute; background:white; position: absolute; top:20px; left:0px; color:black; width: 200px}
|
||||
#acels > li:hover > ul { display: block; }
|
||||
#acels > li > ul > li { padding: 0px 10px; display: block }
|
||||
#acels > li > ul > li:hover { background: #ccc; }
|
||||
#acels > li > ul > li > i { display: inline-block; float: right; color: #aaa; }
|
||||
body { background:var(--background); }
|
||||
#ronin #wrapper { background: var(--background); }
|
||||
#ronin #wrapper #commander { background:var(--background); }
|
||||
#ronin #wrapper #commander textarea { color:var(--f_high); }
|
||||
#ronin #wrapper #commander #status { color:var(--f_med); }
|
||||
#ronin #wrapper #commander #status #source { color:var(--f_low); }
|
||||
#ronin #wrapper #commander #status #eval { background-color: var(--b_inv); border-color: var(--b_inv) }
|
||||
#ronin #wrapper #commander #status #eval.active { background:var(--f_high); border-color:var(--f_high); transition: none }
|
||||
::selection { background-color:var(--b_inv); color:var(--f_inv); text-decoration:none }
|
||||
@media (min-width: 720px) {
|
||||
#ronin #wrapper #commander { width:350px; }
|
||||
#ronin.hidden #wrapper #commander { margin-left:-380px; }
|
||||
#ronin #surface,#ronin #guide { left:380px; }
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<script type="module">
|
||||
|
||||
import Client from './Client.js'
|
||||
|
||||
const client = new Client()
|
||||
client.install(document.body)
|
||||
window.addEventListener('load', () => {
|
||||
client.start()
|
||||
})
|
||||
|
||||
window.client = client
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
16
src/browser/index.js
Normal file
16
src/browser/index.js
Normal file
@ -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)
|
94
src/cli/Client.js
Executable file
94
src/cli/Client.js
Executable file
@ -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
|
44
src/cli/NodeSurface.js
Executable file
44
src/cli/NodeSurface.js
Executable file
@ -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
|
15
src/cli/Source.js
Executable file
15
src/cli/Source.js
Executable file
@ -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
|
13
src/cli/index.js
Executable file
13
src/cli/index.js
Executable file
@ -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)
|
9
src/index.js
Normal file
9
src/index.js
Normal file
@ -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')
|
||||
}
|
17
src/util.js
Executable file
17
src/util.js
Executable file
@ -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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user