Working toward PWA.

This commit is contained in:
neauoire 2019-11-02 14:55:38 -04:00
parent ee1cdfa167
commit 707f056649
16 changed files with 322 additions and 451 deletions

View File

@ -16,9 +16,6 @@
"push_status": "~/butler status hundredrabbits/ronin",
"push": "npm run build ; npm run push_theme ; npm run push_osx ; npm run push_linux ; npm run push_win ; npm run clean ; npm run push_status"
},
"dependencies": {
"node-osc": "^4.1.0"
},
"devDependencies": {
"electron": "^5.0.8",
"electron-packager": "^13.1.1"

View File

@ -10,7 +10,6 @@
<script type="text/javascript" src="scripts/surface.js"></script>
<script type="text/javascript" src="scripts/lisp.js"></script>
<script type="text/javascript" src="scripts/library.js"></script>
<script type="text/javascript" src="scripts/osc.js"></script>
<link rel="stylesheet" type="text/css" href="links/reset.css"/>
<link rel="stylesheet" type="text/css" href="links/fonts.css"/>
<link rel="stylesheet" type="text/css" href="links/main.css"/>
@ -19,6 +18,7 @@
</head>
<body>
<script type="text/javascript">
const remote = require('electron').remote
const {dialog,app} = require('electron').remote;
const fs = require('fs')
const ronin = new Ronin()

View File

@ -8,7 +8,6 @@ body { margin:0px; padding:0px; overflow:hidden; font-family:"input_mono_regular
#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; text-transform: lowercase; line-height: 15px; height: 30px; overflow: hidden; width: calc(100% - 75px); padding-left:45px;}
#ronin #wrapper #commander #status #run { 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 #run:hover { background: none; transition: 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; }

View File

@ -1,5 +1,4 @@
function Commander (ronin) {
this.docs = new Docs(ronin)
this.el = document.createElement('div')
this.el.id = 'commander'
this._input = document.createElement('textarea')
@ -18,15 +17,17 @@ function Commander (ronin) {
this.el.appendChild(this._status)
host.appendChild(this.el)
this._run.setAttribute('title', 'Run(c-R)')
this._input.setAttribute('autocomplete', 'off')
this._input.setAttribute('autocorrect', 'off')
this._input.setAttribute('autocapitalize', 'off')
this._input.setAttribute('spellcheck', 'false')
this._input.addEventListener('input', this.onInput)
this._input.addEventListener('click', this.onClick)
this._run.addEventListener('click', () => { this.run() })
this._input.onkeydown = (e) => {
if (e.keyCode == 9 || e.which == 9) { e.preventDefault(); this.inject(' ') }
if (e.keyCode === 9 || e.which === 9) { e.preventDefault(); this.inject(' ') }
}
this.docs.install()
}
this.start = function () {
@ -41,7 +42,7 @@ function Commander (ronin) {
if (this._input.value.trim() === '') {
ronin.surface.maximize()
}
ronin.interpreter.run(txt)
ronin.lisp.run(txt)
this.feedback()
}
@ -89,12 +90,12 @@ function Commander (ronin) {
if (c === '(') { depth++ } else if (c === ')') { depth-- }
if (c === ';') {
const indent = '\n' + (' '.repeat(depth))
val = val.insert(indent, i)
val = insert(val, indent, i)
i += indent.length
}
if (c === '(') {
const indent = '\n' + (' '.repeat(depth - 1))
val = val.insert(indent, i)
val = insert(val, indent, i)
i += indent.length
}
}
@ -103,7 +104,7 @@ function Commander (ronin) {
this.clean = function (input) {
const keywords = ['$pos+', '$pos', '$rect', '$line', '$x', '$y', '$xy']
for (word of keywords) {
for (const word of keywords) {
input = input.replace(word, '').trim()
}
return input
@ -115,12 +116,13 @@ function Commander (ronin) {
this._log.textContent = `${msg}`
}
// Docs
const lstFn = this.getLastfn()
const rect = ronin.surface.getFrame()
const _docs = this.docs.hasDocs(lstFn) === true ? this.docs.print(lstFn) : `${ronin.source}:${this.length()} ${rect.w}x${rect.h}`
if (_docs !== this._docs.textContent) {
this._docs.textContent = `${_docs}`
}
// const lstFn = this.getLastfn()
// const rect = ronin.surface.getFrame()
// TODO
// const _docs = this.docs.hasDocs(lstFn) === true ? this.docs.print(lstFn) : `${ronin.source}:${this.length()} ${rect.w}x${rect.h}`
// if (_docs !== this._docs.textContent) {
// this._docs.textContent = `${_docs}`
// }
}
// Injection
@ -152,15 +154,15 @@ function Commander (ronin) {
const append = words[0].indexOf('+') > -1
if (word === 'drag') {
this.cache = this.cache.replace('$drag', `(drag $rect $line)`)
this.cache = this.cache.replace('$drag', '(drag $rect $line)')
} else if (word === 'view') {
this.cache = this.cache.replace('$view', `(view $rect $rect)`)
this.cache = this.cache.replace('$view', '(view $rect $rect)')
} else if (word === 'poly') {
this.cache = this.cache.replace('$poly', `(poly $pos+)`)
this.cache = this.cache.replace('$poly', '(poly $pos+)')
} else if (word === 'move') {
this.cache = this.cache.replace('$move', `(transform:move $wh)`)
this.cache = this.cache.replace('$move', '(transform:move $wh)')
} else if (word === 'rotate') {
this.cache = this.cache.replace('$rotate', `(transform:rotate $a)`)
this.cache = this.cache.replace('$rotate', '(transform:rotate $a)')
}
if (shape[word]) {
@ -225,7 +227,7 @@ function Commander (ronin) {
// Splash
this.splash = `; welcome to ronin
; v2.30
; v2.40
(clear)
(def logo-path "M60,60 L195,60 A45,45 0 0,1 240,105 A45,45 0 0,1 195,150 L60,150 M195,150 A45,45 0 0,1 240,195 L240,240 ")
(def pos-x
@ -235,5 +237,7 @@ function Commander (ronin) {
(stroke
(svg pos-x pos-y logo-path) theme:b_high 5)`
String.prototype.insert = function (s, i) { return [this.slice(0, i), `${s}`, this.slice(i)].join('') }
function insert (str, add, i) {
return [str.slice(0, i), `${add}`, str.slice(i)].join('')
}
}

View File

@ -1,43 +0,0 @@
function Docs (ronin) {
this.dict = {}
this.load = () => {
const fs = require('fs')
const path = require('path')
const p = path.join(__dirname, 'scripts/', 'library.js')
if (!fs.existsSync(p)) { console.warn('Docs', 'File does not exist: ' + p); return }
const lines = fs.readFileSync(p, 'utf8').split('\n').filter((line) => { return line.substr(0, 7) === ' this.' })
return lines.map((line) => { return line.trim().substr(5).trim() })
}
this.install = (payload = this.load()) => {
for (const id in payload) {
const name = payload[id].substr(0, payload[id].indexOf(' = '))
const parent = payload[id].substr(payload[id].indexOf(' = ')).match(/\(([^)]+)\)/)
const params = parent ? parent[1].split(',').map((word) => { return word.indexOf(' = ') > -1 ? '~' + (word.split(' = ')[0]).trim() : word.trim() }) : []
const note = payload[id].indexOf('// ') > -1 ? payload[id].split('//')[1].trim() : ''
this.dict[name] = { note, params }
if (params.length < 1) { console.warn('Docs', 'Missing params for ' + name) }
if (note === '') { console.warn('Docs', 'Missing note for ' + name) }
}
console.log('Docs', `Loaded ${Object.keys(this.dict).length} functions.`)
console.log(this.toMarkdown())
}
this.toMarkdown = () => {
return Object.keys(this.dict).reduce((acc, item, key) => {
const example = `${item} ${this.dict[item].params.reduce((acc, item) => {
return `${acc}${item} `
}, '').trim()}`
return `${acc}- \`(${example.trim()})\` ${this.dict[item].note}\n`
}, '')
}
this.hasDocs = (name) => {
return !!this.dict[name]
}
this.print = (name) => {
return `(${name} ${this.dict[name].params.reduce((acc, item) => { return `${acc}${item} ` }, '').trim()})`
}
}

View File

@ -0,0 +1,79 @@
'use strict'
function Acels () {
this.all = {}
this.install = (host = window) => {
host.addEventListener('keydown', this.onKeyDown, false)
host.addEventListener('keyup', this.onKeyUp, false)
}
this.set = (type, name, key, downfn, upfn) => {
if (this.all[key]) { console.warn('Acels', `Trying to overwrite ${this.all[key].name}, with ${name}.`) }
this.all[key] = { type, name, downfn, upfn, key }
}
this.get = (key) => {
return this.all[key]
}
this.sort = () => {
const h = {}
for (const item of Object.values(this.all)) {
if (!h[item.type]) { h[item.type] = [] }
h[item.type].push(item)
}
return h
}
this.convert = (event) => {
const key = event.key.substr(0, 1).toUpperCase() + event.key.substr(1)
if ((event.ctrlKey || event.metaKey) && event.shiftKey) {
return `CmdOrCtrl+Shift+${key}`
}
if (event.shiftKey) {
return `Shift+${key}`
}
if (event.ctrlKey || event.metaKey) {
return `CmdOrCtrl+${key}`
}
return key
}
this.onKeyDown = (e) => {
const target = this.get(this.convert(e))
if (!target || !target.downfn) { return }
target.downfn()
e.preventDefault()
}
this.onKeyUp = (e) => {
const target = this.get(this.convert(e))
if (!target || !target.upfn) { return }
target.upfn()
e.preventDefault()
}
this.toMarkdown = () => {
const types = this.sort()
let text = ''
for (const type in types) {
text += `\n### ${type}\n\n`
for (const item of types[type]) {
text += `- \`${item.key}\`: ${item.info}\n`
}
}
return text.trim()
}
this.toString = () => {
const types = this.sort()
let text = ''
for (const type in types) {
for (const item of types[type]) {
text += `${type}: ${item.name} | ${item.key}\n`
}
}
return text.trim()
}
}

View File

@ -1,90 +0,0 @@
'use strict'
function Controller () {
const fs = require('fs')
const { dialog, app } = require('electron').remote
this.menu = { default: {} }
this.mode = 'default'
this.app = require('electron').remote.app
this.start = function () {
}
this.add = function (mode, cat, label, fn, accelerator) {
if (!this.menu[mode]) { this.menu[mode] = {} }
if (!this.menu[mode][cat]) { this.menu[mode][cat] = {} }
this.menu[mode][cat][label] = { fn: function (_menuItem, browserWindow) {
if (browserWindow) {
browserWindow.webContents.focus()
}
fn.apply(this, arguments)
},
accelerator: accelerator }
}
this.addRole = function (mode, cat, label) {
if (!this.menu[mode]) { this.menu[mode] = {} }
if (!this.menu[mode][cat]) { this.menu[mode][cat] = {} }
this.menu[mode][cat][label] = { role: label }
}
this.addSpacer = function (mode, cat, label, type = 'separator') {
if (!this.menu[mode]) { this.menu[mode] = {} }
if (!this.menu[mode][cat]) { this.menu[mode][cat] = {} }
this.menu[mode][cat][label] = { type: type }
}
this.clearCat = function (mode, cat) {
if (this.menu[mode]) { this.menu[mode][cat] = {} }
}
this.set = function (mode = 'default') {
this.mode = mode
this.commit()
}
this.format = function () {
const f = []
const m = this.menu[this.mode]
for (const cat in m) {
const submenu = []
for (const name in m[cat]) {
const option = m[cat][name]
if (option.role) {
submenu.push({ role: option.role })
} else if (option.type) {
submenu.push({ type: option.type })
} else {
submenu.push({ label: name, accelerator: option.accelerator, click: option.fn })
}
}
f.push({ label: cat, submenu: submenu })
}
return f
}
this.commit = function () {
console.log('Controller', 'Changing..')
this.app.injectMenu(this.format())
}
this.accelerator = function (key, menu) {
const acc = { basic: null, ctrl: null }
for (cat in menu) {
const options = menu[cat]
for (const id in options.submenu) {
const option = options.submenu[id]; if (option.role) { continue }
acc.basic = (option.accelerator.toLowerCase() === key.toLowerCase()) ? option.label.toUpperCase().replace('TOGGLE ', '').substr(0, 8).trim() : acc.basic
acc.ctrl = (option.accelerator.toLowerCase() === ('CmdOrCtrl+' + key).toLowerCase()) ? option.label.toUpperCase().replace('TOGGLE ', '').substr(0, 8).trim() : acc.ctrl
}
}
return acc
}
this.docs = function () {
// TODO
console.log(this.menu.default)
}
}

View File

@ -1,9 +1,6 @@
'use strict'
function Lisp (lib = {}) {
const path = require('path')
const fs = require('fs')
const TYPES = { identifier: 0, number: 1, string: 2, bool: 3, symbol: 4 }
const Context = function (scope, parent) {
@ -19,11 +16,6 @@ function Lisp (lib = {}) {
}
const special = {
include: (input, context) => {
if (!input[1].value || !fs.existsSync(input[1].value)) { console.warn('Lisp', 'No file: ' + input[1].value); return [] }
const file = fs.readFileSync(input[1].value, { encoding: 'utf-8' })
return interpret(this.parse(`(${file})`), context)
},
let: function (input, context) {
const letContext = input[1].reduce(function (acc, x) {
acc.scope[x[0].value] = interpret(x[1], context)
@ -164,7 +156,7 @@ function Lisp (lib = {}) {
}
const tokenize = function (input) {
const i = input.replace(/^\;.*\n?/gm, '').replace(/λ /g, 'lambda ').split('"')
const i = input.replace(/^;.*\n?/gm, '').replace(/λ /g, 'lambda ').split('"')
return i.map(function (x, i) {
return i % 2 === 0
? x.replace(/\(/g, ' ( ')

View File

@ -0,0 +1,62 @@
'use strict'
/* global FileReader */
/* global MouseEvent */
function Source () {
this.cache = {}
this.install = () => {
}
this.start = () => {
this.new()
}
this.new = () => {
console.log('Source', 'New file..')
this.cache = {}
}
this.open = (callback) => {
console.log('Source', 'Open file..')
const input = document.createElement('input')
input.type = 'file'
input.onchange = (e) => {
this.cache = e.target.files[0]
this.load(this.cache, callback)
}
input.click()
}
this.save = (name, content, type = 'text/plain', callback) => {
this.saveAs(name, content, type, callback)
}
this.saveAs = (name, content, type = 'text/plain', callback) => {
console.log('Source', 'Save new file..')
this.download(name, content, type, callback)
}
this.revert = () => {
}
// I/O
this.load = (file, callback) => {
const reader = new FileReader()
reader.onload = (event) => {
const res = event.target.result
callback(res)
}
reader.readAsText(file, 'UTF-8')
}
this.download = (name, content, type) => {
const pom = document.createElement('a')
pom.setAttribute('download', name)
pom.setAttribute('href', 'data:' + type + ';charset=utf-8,' + encodeURIComponent(content))
pom.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }))
}
}

View File

@ -1,5 +1,9 @@
'use strict'
/* global localStorage */
/* global FileReader */
/* global DOMParser */
function Theme (_default) {
const themer = this
@ -8,12 +12,12 @@ function Theme (_default) {
this.el = document.createElement('style')
this.el.type = 'text/css'
this.install = function (host = document.body, callback) {
this.install = (host = document.body, callback) => {
host.appendChild(this.el)
this.callback = callback
}
this.start = function () {
this.start = () => {
console.log('Theme', 'Starting..')
if (isJson(localStorage.theme)) {
const storage = JSON.parse(localStorage.theme)
@ -26,10 +30,10 @@ function Theme (_default) {
this.load(_default)
}
this.load = function (data) {
this.load = (data) => {
const theme = parse(data)
if (!validate(theme)) { return }
console.log('Theme', `Loaded theme!`)
console.log('Theme', 'Loaded theme!')
this.el.innerHTML = `:root { --background: ${theme.background}; --f_high: ${theme.f_high}; --f_med: ${theme.f_med}; --f_low: ${theme.f_low}; --f_inv: ${theme.f_inv}; --b_high: ${theme.b_high}; --b_med: ${theme.b_med}; --b_low: ${theme.b_low}; --b_inv: ${theme.b_inv}; }`
localStorage.setItem('theme', JSON.stringify(theme))
this.active = theme
@ -38,11 +42,11 @@ function Theme (_default) {
}
}
this.reset = function () {
this.reset = () => {
this.load(_default)
}
this.get = function (key) {
this.get = (key) => {
return this.active[key]
}
@ -53,13 +57,13 @@ function Theme (_default) {
// Drag
this.drag = function (e) {
this.drag = (e) => {
e.stopPropagation()
e.preventDefault()
e.dataTransfer.dropEffect = 'copy'
}
this.drop = function (e) {
this.drop = (e) => {
e.preventDefault()
e.stopPropagation()
const file = e.dataTransfer.files[0]
@ -72,10 +76,10 @@ function Theme (_default) {
reader.readAsText(file)
}
this.open = function () {
this.open = () => {
const fs = require('fs')
const { dialog, app } = require('electron').remote
let paths = dialog.showOpenDialog(app.win, { properties: ['openFile'], filters: [{ name: 'Themes', extensions: ['svg'] }] })
const paths = dialog.showOpenDialog(app.win, { properties: ['openFile'], filters: [{ name: 'Themes', extensions: ['svg'] }] })
if (!paths) { console.log('Nothing to load'); return }
fs.readFile(paths[0], 'utf8', function (err, data) {
if (err) throw err
@ -106,15 +110,15 @@ function Theme (_default) {
const svg = new DOMParser().parseFromString(text, 'text/xml')
try {
return {
'background': svg.getElementById('background').getAttribute('fill'),
'f_high': svg.getElementById('f_high').getAttribute('fill'),
'f_med': svg.getElementById('f_med').getAttribute('fill'),
'f_low': svg.getElementById('f_low').getAttribute('fill'),
'f_inv': svg.getElementById('f_inv').getAttribute('fill'),
'b_high': svg.getElementById('b_high').getAttribute('fill'),
'b_med': svg.getElementById('b_med').getAttribute('fill'),
'b_low': svg.getElementById('b_low').getAttribute('fill'),
'b_inv': svg.getElementById('b_inv').getAttribute('fill')
background: svg.getElementById('background').getAttribute('fill'),
f_high: svg.getElementById('f_high').getAttribute('fill'),
f_med: svg.getElementById('f_med').getAttribute('fill'),
f_low: svg.getElementById('f_low').getAttribute('fill'),
f_inv: svg.getElementById('f_inv').getAttribute('fill'),
b_high: svg.getElementById('b_high').getAttribute('fill'),
b_med: svg.getElementById('b_med').getAttribute('fill'),
b_low: svg.getElementById('b_low').getAttribute('fill'),
b_inv: svg.getElementById('b_inv').getAttribute('fill')
}
} catch (err) {
console.warn('Theme', 'Incomplete SVG Theme', err)

View File

@ -1,3 +1,7 @@
'use strict'
/* global Image */
function Library (ronin) {
// Modularity: Write simple parts connected by clean interfaces.
// Composition: Design programs to be connected to other programs.
@ -12,11 +16,11 @@ function Library (ronin) {
}
this.export = (path, quality = 1.0) => { // Exports a graphic file with format.
if (!path) { console.warn('Missing export path'); return path }
const dataUrl = ronin.surface.el.toDataURL(path.indexOf('.jpg') > -1 ? 'image/jpeg' : path.indexOf('.png') > -1 ? 'image/png' : format, quality)
const data = dataUrl.replace(/^data:image\/png;base64,/, '').replace(/^data:image\/jpeg;base64,/, '')
fs.writeFileSync(path, data, 'base64')
return path
// if (!path) { console.warn('Missing export path'); return path }
// const dataUrl = ronin.surface.el.toDataURL(path.indexOf('.jpg') > -1 ? 'image/jpeg' : path.indexOf('.png') > -1 ? 'image/png' : format, quality)
// const data = dataUrl.replace(/^data:image\/png;base64,/, '').replace(/^data:image\/jpeg;base64,/, '')
// fs.writeFileSync(path, data, 'base64')
// return path
}
this.open = async (path, ratio = 1) => { // Imports a graphic file and resizes the frame.
@ -403,7 +407,7 @@ function Library (ronin) {
}
this.or = (a, b, ...rest) => { // Returns true if at least one condition is true.
let args = [a, b].concat(rest)
const args = [a, b].concat(rest)
for (let i = 0; i < args.length; i++) {
if (args[i]) {
return args[i]
@ -437,7 +441,7 @@ function Library (ronin) {
this.reduce = async (arr, fn, acc) => {
const length = arr.length
let result = acc === undefined ? subject[0] : acc
let result = acc === undefined ? arr[0] : acc
for (let i = acc === undefined ? 1 : 0; i < length; i++) {
result = await fn(result, arr[i], i, arr)
}
@ -461,7 +465,7 @@ function Library (ronin) {
}
this.range = (start, end, step = 1) => {
let arr = []
const arr = []
if (step > 0) {
for (let i = start; i <= end; i += step) {
arr.push(i)
@ -534,9 +538,9 @@ function Library (ronin) {
}
this.sharpen = () => { // Returns the sharpen kernel.
return [[ 0, -1, 0],
return [[0, -1, 0],
[-1, 5, -1],
[ 0, -1, 0]]
[0, -1, 0]]
}
this.edge = () => { // Returns the edge kernel.
@ -548,27 +552,27 @@ function Library (ronin) {
// File System
this.dir = (path = this.dirpath()) => { // Returns the content of a directory.
return fs.existsSync(path) ? fs.readdirSync(path) : []
// return fs.existsSync(path) ? fs.readdirSync(path) : []
}
this.file = (path = this.filepath()) => { // Returns the content of a file.
return fs.existsSync(path) ? fs.readFileSync(path, 'utf8') : ''
// return fs.existsSync(path) ? fs.readFileSync(path, 'utf8') : ''
}
this.dirpath = (path = this.filepath()) => { // Returns the path of a directory.
return require('path').dirname(path)
// return require('path').dirname(path)
}
this.filepath = (path = ronin.source.path) => { // Returns the path of a file.
return path
// return path
}
this.dirname = (path = this.filepath()) => { // Returns the name of a folder.
return require('path').basename(require('path').dirname(path))
// return require('path').basename(require('path').dirname(path))
}
this.filename = (path = this.filepath()) => { // Returns the name of a file.
return require('path').parse(path).name
// return require('path').parse(path).name
}
this.offset = (a, b) => { // Offsets pos a with pos b, returns a.
@ -578,7 +582,7 @@ function Library (ronin) {
}
this.distance = (a, b) => { // Get distance between positions.
return Math.sqrt(((ax - bx) * (ax - bx)) + ((ay - by) * (ay - by)))
return Math.sqrt(((a.x - b.x) * (a.x - b.x)) + ((a.y - b.y) * (a.y - b.y)))
}
this.echo = (...args) => { // Print arguments to interface.

View File

@ -1,18 +0,0 @@
'use strict'
function Osc (ronin) {
const osc = require('node-osc')
this.port = 49162
this.start = function () {
const server = new osc.Server(49162, '0.0.0.0')
server.on('message', this.onMsg)
}
this.onMsg = (msg) => {
const address = msg.shift()
if (ronin.bindings[address]) {
ronin.bindings[address](msg)
}
}
}

View File

@ -1,26 +1,25 @@
function Ronin () {
const defaultTheme = {
background: '#111',
f_high: '#fff',
f_med: '#999',
f_low: '#444',
f_inv: '#000',
b_high: '#ffffff',
b_med: '#72dec2',
b_low: '#aaaaaa',
b_inv: '#ffb545'
}
'use strict'
/* global Acels */
/* global Theme */
/* global Source */
/* global Commander */
/* global Surface */
/* global Library */
/* global Lisp */
/* global requestAnimationFrame */
function Ronin () {
this.el = document.createElement('div')
this.el.id = 'ronin'
this.theme = new Theme(defaultTheme)
this.source = new Source(this)
this.acels = new Acels()
this.theme = new Theme()
this.source = new Source()
this.commander = new Commander(this)
this.surface = new Surface(this)
this.library = new Library(this)
this.interpreter = new Lisp(this.library)
this.osc = new Osc(this)
this.lisp = new Lisp(this.library)
this.bindings = {}
@ -34,22 +33,52 @@ function Ronin () {
host.appendChild(this.el)
this.theme.install()
window.addEventListener('dragover', this.drag)
window.addEventListener('drop', this.drop)
window.addEventListener('dragover', this.onDrag)
window.addEventListener('drop', this.onDrop)
this.acels.set('File', 'New', 'CmdOrCtrl+N', () => { this.source.new(); this.surface.clear(); this.commander.clear() })
this.acels.set('File', 'Save', 'CmdOrCtrl+S', () => { this.source.save('export.lisp', this.commander._input.value, 'text/plain') })
this.acels.set('File', 'Save As', 'CmdOrCtrl+Shift+S', () => { this.source.saveAs() })
this.acels.set('File', 'Open', 'CmdOrCtrl+O', () => { this.source.open(this.whenOpen) })
this.acels.set('File', 'Revert', 'CmdOrCtrl+W', () => { this.source.revert() })
this.acels.set('View', 'Toggle Guides', 'CmdOrCtrl+Shift+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', 'Run', 'CmdOrCtrl+R', () => { this.commander.run() })
this.acels.set('Project', 'Reload Run', 'CmdOrCtrl+Shift+R', () => { this.source.revert(); this.commander.run() })
this.acels.set('Project', 'Re-Indent', 'CmdOrCtrl+Shift+I', () => { this.commander.reindent() })
this.acels.set('Project', 'Clean', 'Escape', () => { this.commander.cleanup() })
this.acels.install(window)
}
this.start = function () {
this.theme.start()
console.log('Ronin', 'Starting..')
console.info(`${this.acels}`)
this.theme.start({
background: '#111',
f_high: '#fff',
f_med: '#999',
f_low: '#444',
f_inv: '#000',
b_high: '#ffffff',
b_med: '#72dec2',
b_low: '#aaaaaa',
b_inv: '#ffb545'
})
this.source.start()
this.commander.start()
this.surface.start()
this.osc.start()
this.loop()
}
this.whenOpen = (res) => {
this.commander.load(res)
this.commander.show()
}
this.loop = () => {
if (this.bindings['animate'] && typeof this.bindings['animate'] === 'function') {
this.bindings['animate']()
if (this.bindings.animate && typeof this.bindings.animate === 'function') {
this.bindings.animate()
}
requestAnimationFrame(() => this.loop())
}
@ -60,7 +89,7 @@ function Ronin () {
this.log = function (...msg) {
this.commander.setStatus(msg.reduce((acc, val) => {
return acc + JSON.stringify(val).replace(/\"/g, '').trim() + ' '
return acc + JSON.stringify(val).replace(/"/g, '').trim() + ' '
}, ''))
}
@ -77,7 +106,7 @@ function Ronin () {
this.mouseOrigin = null
this.onMouseDown = (e, id = 'mouse-down') => {
const pos = { x: e.offsetX * ronin.surface.ratio, y: e.offsetY * ronin.surface.ratio }
const pos = { x: e.offsetX * this.surface.ratio, y: e.offsetY * this.surface.ratio }
this.mouseOrigin = pos
const shape = this.mouseShape(pos, id)
if (this.bindings[id]) {
@ -94,20 +123,20 @@ function Ronin () {
}
}
this.onKeyUp = (e, id = 'key-up') => {
if (this.bindings[id]) {
this.bindings[id](e)
}
}
this.onKeyDown = (e, id = 'key-down') => {
if (this.bindings[id]) {
this.bindings[id](e)
}
}
this.onKeyUp = (e, id = 'key-up') => {
if (this.bindings[id]) {
this.bindings[id](e)
}
}
this.onMouseMove = (e, id = 'mouse-move') => {
const pos = { x: e.offsetX * ronin.surface.ratio, y: e.offsetY * ronin.surface.ratio }
const pos = { x: e.offsetX * this.surface.ratio, y: e.offsetY * this.surface.ratio }
const shape = this.mouseShape(pos, id)
if (this.bindings[id]) {
this.bindings[id](shape)
@ -120,7 +149,7 @@ function Ronin () {
}
this.onMouseUp = (e, id = 'mouse-up') => {
const pos = { x: e.offsetX * ronin.surface.ratio, y: e.offsetY * ronin.surface.ratio }
const pos = { x: e.offsetX * this.surface.ratio, y: e.offsetY * this.surface.ratio }
const shape = this.mouseShape(pos, id)
if (this.bindings[id]) {
this.bindings[id](shape)
@ -140,6 +169,18 @@ function Ronin () {
this.mouseOrigin = null
}
this.onDrag = (e) => {
e.stopPropagation()
e.preventDefault()
e.dataTransfer.dropEffect = 'copy'
}
this.onDrop = (e) => {
e.preventDefault()
e.stopPropagation()
this.source.load(e.dataTransfer.files[0], this.whenOpen)
}
this.mouseShape = (position, type) => {
if (!this.mouseOrigin) { return }
const x = position.x
@ -174,48 +215,4 @@ function Ronin () {
}
return { x, y, xy, wh, d, a, line, rect, pos, size, circle, arc, type, 'is-down': type !== 'mouse-up' ? true : null }
}
// Zoom
this.modZoom = function (mod = 0, set = false) {
try {
const { webFrame } = require('electron')
const currentZoomFactor = webFrame.getZoomFactor()
webFrame.setZoomFactor(set ? mod : currentZoomFactor + mod)
console.log(window.devicePixelRatio)
} catch (err) {
console.log('Cannot zoom')
}
}
this.setZoom = function (scale) {
try {
webFrame.setZoomFactor(scale)
} catch (err) {
console.log('Cannot zoom')
}
}
// Events
this.drag = (e) => {
e.stopPropagation()
e.preventDefault()
e.dataTransfer.dropEffect = 'copy'
}
this.drop = (e) => {
e.preventDefault()
e.stopPropagation()
const file = e.dataTransfer.files[0]
if (!file || !file.name) { console.warn('File', 'Not a valid file.'); return }
const path = file.path ? file.path : file.name
if (this.commander._input.value.indexOf('$path') > -1) {
this.commander.injectPath(file.path)
this.commander.show()
} else if (path.indexOf('.lisp') > -1) {
this.source.read(path)
this.commander.show()
}
}
}

View File

@ -1,151 +0,0 @@
'use strict'
function Source (ronin) {
const fs = require('fs')
const path = require('path')
const { dialog, app } = require('electron').remote
this.path = null
this.start = function () {
this.new()
}
this.new = function () {
console.log('Source', 'Make a new file..')
this.path = null
ronin.surface.clear()
ronin.commander.clear()
ronin.log(`New file.`)
}
this.open = function () {
console.log('Source', 'Open a file..')
let paths = dialog.showOpenDialog(app.win, { properties: ['openFile'], filters: [{ name: 'Ronin Lisp', extensions: ['lisp'] }] })
if (!paths) { console.log('Nothing to load'); return }
this.read(paths[0])
}
this.save = function (quitAfter = false) {
console.log('Source', 'Save..', this.path)
if (this.path) {
this.write(this.path, this.generate(), quitAfter)
} else {
this.saveAs(quitAfter)
}
}
this.saveAs = function (quitAfter = false) {
console.log('Source', 'Save a file as..')
dialog.showSaveDialog((loc) => {
if (loc === undefined) { return }
if (loc.indexOf('.lisp') < 0) { loc += '.lisp' }
this.write(loc, this.generate(), quitAfter)
this.path = loc
})
}
this.revert = function () {
if (!this.path) { return }
console.log('Source', 'Revert a file..')
this.read(this.path)
}
// I/O
this.write = function (loc, data = this.generate(), quitAfter = false) {
console.log('Source', 'Writing ' + loc)
fs.writeFileSync(loc, data)
if (quitAfter === true) {
app.exit()
}
ronin.log(`Writing file.`)
}
this.read = function (loc = this.path) {
if (!loc) { return }
if (!fs.existsSync(loc)) { console.warn('Source', 'File does not exist: ' + loc); return }
console.log('Source', 'Reading ' + loc)
this.path = loc
ronin.commander.load(fs.readFileSync(this.path, 'utf8'))
ronin.log(`Reading file.`)
}
this.run = function () {
ronin.commander.run()
}
this.quit = function (force = false) {
if (this.hasChanges() === true && force === false) {
this.verify()
} else {
app.exit()
}
}
this.verify = function () {
let response = dialog.showMessageBox(app.win, {
type: 'question',
buttons: ['Cancel', 'Discard', 'Save'],
title: 'Confirm',
message: 'Unsaved data will be lost. Would you like to save your changes before leaving?',
icon: path.join(__dirname, '../icon.png')
})
if (response === 2) {
this.save(true)
} else if (response === 1) {
app.exit()
}
}
this.hasChanges = function () {
console.log('Source', 'Looking for changes..')
if (!this.path) {
console.log('Source', 'File is unsaved..')
if (ronin.commander._input.value.length > 2) {
console.log('Source', `File is not empty.`)
return true
}
} else {
if (fs.existsSync(this.path)) {
console.log('Source', 'Comparing with last saved copy..')
const diff = isDifferent(fs.readFileSync(this.path, 'utf8'), this.generate())
if (diff === true) {
console.log('Source', 'File has been changed.')
return true
}
} else {
console.log('Source', 'File does not exist.')
return true
}
}
}
// Converters
this.generate = function (str = ronin.commander._input.value) {
return `${str}`
}
// Etc
this.name = function () {
return this.path ? path.basename(this.path, '.lisp') : null
}
this.folder = function (p = this.path) {
return p ? path.dirname(p) : null
}
this.toString = function () {
return this.path ? this.name() + '.lisp' : 'new'
}
function isDifferent (a, b) {
return a.trim() !== b.trim()
}
function clean (s) {
return s
}
}

View File

@ -1,3 +1,8 @@
'use strict'
/* global Path2D */
/* global Image */
function Surface (ronin) {
this.el = document.createElement('canvas')
this.el.id = 'surface'
@ -5,6 +10,7 @@ function Surface (ronin) {
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')
@ -132,7 +138,7 @@ function Surface (ronin) {
const positions = Object.values(poly)
const origin = positions.shift()
context.moveTo(origin.x, origin.y)
for (pos of positions) {
for (const pos of positions) {
context.lineTo(pos.x, pos.y)
}
}
@ -173,7 +179,7 @@ function Surface (ronin) {
img.src = path
img.onload = () => {
const rect = { x: 0, y: 0, w: img.width * ratio, h: img.height * ratio }
this.resize(rect,true)
this.resize(rect, true)
this.context.drawImage(img, rect.x, rect.y, rect.w, rect.h)
resolve()
}

29
index.html Normal file
View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<head>
<meta charset='UTF-8'>
<script type="text/javascript" src="desktop/sources/scripts/lib/theme.js"></script>
<script type="text/javascript" src="desktop/sources/scripts/lib/acels.js"></script>
<script type="text/javascript" src="desktop/sources/scripts/lib/lisp.js"></script>
<script type="text/javascript" src="desktop/sources/scripts/lib/source.js"></script>
<script type="text/javascript" src="desktop/sources/scripts/ronin.js"></script>
<script type="text/javascript" src="desktop/sources/scripts/commander.js"></script>
<script type="text/javascript" src="desktop/sources/scripts/surface.js"></script>
<script type="text/javascript" src="desktop/sources/scripts/library.js"></script>
<link rel="stylesheet" type="text/css" href="desktop/sources/links/reset.css"/>
<link rel="stylesheet" type="text/css" href="desktop/sources/links/fonts.css"/>
<link rel="stylesheet" type="text/css" href="desktop/sources/links/main.css"/>
<link rel="stylesheet" type="text/css" href="desktop/sources/links/theme.css"/>
<title>Ronin</title>
</head>
<body>
<script type="text/javascript">
const ronin = new Ronin()
ronin.install(document.body)
window.addEventListener('load', () => {
ronin.start()
})
</script>
</body>
</html>