diff --git a/.env b/.env new file mode 100644 index 0000000..5b5d4c4 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +git remote | grep codeberg || git remote add codeberg git@codeberg.org:xrsh/xrsh-com.git +git remote | grep c-frame || git remote add c-frame git@github.com:c-frame/xrsh-com.git diff --git a/README.awk b/README.awk new file mode 100755 index 0000000..f4b64de --- /dev/null +++ b/README.awk @@ -0,0 +1,41 @@ +#!/usr/bin/env -S awk -f +# a no-nonsense source-to-markdown generator which scans for: +# +# /** +# * # foo +# * +# * this is markdown $(cat bar.md) +# */ +# +# var foo; // comment with 2 leading spaces is markdown too $(date) +# +# easily refactorable to hash-based languages (py/bash/perl/lua e.g.) +# by changing the regexes +# + +BEGIN{ + # printf README.md until '# Component List' + system("grep -B9999 '# Component List' README.md") + print "" +} + +/\$\(/ { cmd=$0; + gsub(/^.*\$\(/,"",cmd); + gsub(/\).*/,"",cmd); + cmd | getline stdout; close(cmd); + sub(/\$\(.*\)/,stdout); + } +/\/\*\*/ { doc=1; sub(/^.*\/\*/,""); } +doc && /\*\// { doc=0; + sub(/[[:space:]]*\*\/.*/,""); + sub(/^[[:space:]]*\*[[:space:]]?/,""); + print + } +doc && /^[[:space:]]*\*/ { sub(/^[[:space:]]*\*[[:space:]]?/,""); + print + } +#!doc && /\/\/ / { sub(".*// ",""); +# sub("# ","\n# "); +# sub("> ","\n> "); +# print +# } diff --git a/README.md b/README.md index 231cd31..aa6c801 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,10 @@ Characteristics: ``` +See component list below + +> this README.md is generated by running `echo "$(./README.awk com/*.js)" > README.md` + ## Funding This project is partially funded through [NGI0 Entrust](https://nlnet.nl/entrust), a fund established by [NLnet](https://nlnet.nl) with financial support from the European Commission's [Next Generation Internet](https://ngi.eu) program. Learn more at the [NLnet project page](https://nlnet.nl/project/xrsh). @@ -24,3 +28,148 @@ This project is partially funded through [NGI0 Entrust](https://nlnet.nl/entrust [NLnet foundation logo](https://nlnet.nl) [NGI Zero Logo](https://nlnet.nl/entrust) +# Component List + + +## [data_events](com/data_events.js) + +allows components to react to data changes + +```html + + + +``` + + + +## [html-as-texture-in-xr](com/html-as-texture-in-xr.js) + +shows domid **only** in immersive mode +(wrapper around [aframe-htmlmesh](https://ada.is/aframe-htmlmesh/) + +It also sets class 'XR' to the (HTML) body-element in immersive mode. +This allows CSS (in [dom component](com/dom.js)) to visually update accordingly. + +> depends on [AFRAME.utils.require](com/require.js) + +```html + + + + hello + +``` + +| property | type | +|--------------|--------------------| +| `domid` | `string` | + +| event | target | info | +|--------------|------------|--------------------------------------| +| `3D` | a-scene | fired when going into immersive mode | +| `2D` | a-scene | fired when leaving immersive mode | + + + +## [isoterminal](com/isoterminal.js) + +Renders a windowed terminal in both (non)immersive mode. +It displays an interactive javascript console or boots into +a Linux ISO image (via WASM). + +```html + +``` + +> depends on [AFRAME.utils.require](com/require.js) + +| property | type | default | info | +|------------------|-----------|------------------------|------| +| `iso` | `string` | https`//forgejo.isvery.ninja/assets/xrsh-buildroot/main/xrsh.iso" | | +| `overlayfs` | `string` | *WORK-IN-PROGRESS* | | +| `width` | `number` | 800 || +| `height` | `number` | 600 || +| `depth` | `number` | 0.03 || +| `lineHeight` | `number` | 18 || +| `prompt` | `boolean` | true | boot straight into ISO or give user choice | +| `padding` | `number`` | 18 | | +| `maximized` | `boolean` | false | | +| `minimized` | `boolean` | false | | +| `muteUntilPrompt`| `boolean` | true | mute stdout until a prompt is detected in ISO | +| `HUD` | `boolean` | false | link to camera movement | +| `transparent` | `boolean` | false | heavy, needs good gpu | +| `memory` | `number` | 60 | VM memory (in MB) [NOTE` quest or smartphone webworker might crash > 40mb ] | +| `bufferLatency` | `number` | 1 | in ms` bufferlatency from webworker to term (batch-update every char to texture) | +| `debug` | `boolean` | false | | +| `emulator` | `string` | fbterm | terminal emulator | + +> for more info see [xrsh.isvery.ninja](https://xrsh.isvery.ninja) + +Component design: +``` + css/html template + + ┌─────────┐ ┌────────────┐ exit-AR + ┌───────►│ com/dom ┼──►│ com/window ├───────────────── exit-VR ◄─┐ + │ └─────────┘ └───────────┬┘ │ + │ │ │ +┌──────────┴────────┐ │ ┌───────────┐ ┌─────────────────────────────┐ +│ com/isoterminal ├────────────────────────────►│com/term.js│ │com/html-as-texture-in-XR.js │ +└────────┬─┬────────┘ │ └──┬─────┬▲─┘ └─────────────────────────────┘ + │ │ ┌────────┐ ┌──▼──────▼──────┐ ││ │ + │ └───────►│ plane ├─────►text───┼►div#isoterminal│◄────────────────── enter-VR │ + │ └────────┘ └────────────────┘ enter-AR ◄─┘ + │ │ + │ │ + │ ISOTerminal.js + │ ┌───────────────────────────┐ + │ │ com/isoterminal/worker.js ├ + │ └──────────────┌────────────┤ + │ │ │ v86.js │ + │ │ │ feat/*.js │ + │ │ │ libv86.js │ + │ │ └────────────┘ + │ │ + └─────────────────────┘ + +NOTE: For convenience reasons, events are forwarded between com/isoterminal.js, worker.js and ISOTerminal + Instead of a melting pot of different functionnames, events are flowing through everything (ISOTerminal.emit()) +``` + + +## [pastedrop](com/pastedrop.js) + +detects user copy/paste and file dragdrop action +and clipboard functions + +```html + +``` + +| event | target | info | +|--------------|--------|------------------------------------------| +| `pasteFile` | self | always translates input to a File object | + + +## [require](com/require('').js) + +automatically requires dependencies + +```javascript +await AFRAME.utils.require( this.dependencies ) (*) autoload missing components +await AFRAME.utils.require( this.el.getAttributeNames() ) (*) autoload missing components +await AFRAME.utils.require({foo: "https://foo.com/aframe/components/foo.js"},this) +await AFRAME.utils.require(["./app/foo.js","foo.css"],this) +``` + +> (*) = prefixes baseURL AFRAME.utils.require.baseURL ('./com/' e.g.) diff --git a/com/data2event.js b/com/data2event.js index 54118a5..2bdbfe8 100644 --- a/com/data2event.js +++ b/com/data2event.js @@ -1,5 +1,5 @@ -/* - * ## data_events +/** + * ## [data_events](com/data_events.js) * * allows components to react to data changes * diff --git a/com/dom.js b/com/dom.js index 3241417..70ada40 100644 --- a/com/dom.js +++ b/com/dom.js @@ -1,5 +1,5 @@ /* - * ## dom + * ## [dom](com/dom.js) * * instances reactive DOM component from AFRAME component's `dom` metadata * @@ -36,10 +36,6 @@ if( !AFRAME.components.dom ){ AFRAME.registerComponent('dom',{ - requires: { - "requestAnimationFrameXR": "com/requestAnimationFrameXR.js" - }, - init: function(){ Object.values(this.el.components) .map( (c) => { diff --git a/com/html-as-texture-in-xr.js b/com/html-as-texture-in-xr.js index afb0e8d..8a5405c 100644 --- a/com/html-as-texture-in-xr.js +++ b/com/html-as-texture-in-xr.js @@ -1,3 +1,35 @@ +/** + * ## [html-as-texture-in-xr](com/html-as-texture-in-xr.js) + * + * shows domid **only** in immersive mode + * (wrapper around [aframe-htmlmesh](https://ada.is/aframe-htmlmesh/) + * + * It also sets class 'XR' to the (HTML) body-element in immersive mode. + * This allows CSS (in [dom component](com/dom.js)) to visually update accordingly. + * + * > depends on [AFRAME.utils.require](com/require.js) + * + * ```html + * + * + * + * hello + * + * ``` + * + * | property | type | + * |--------------|--------------------| + * | `domid` | `string` | + * + * | event | target | info | + * |--------------|------------|--------------------------------------| + * | `3D` | a-scene | fired when going into immersive mode | + * | `2D` | a-scene | fired when leaving immersive mode | + * + */ + if( !AFRAME.components['html-as-texture-in-xr'] ){ AFRAME.registerComponent('html-as-texture-in-xr', { diff --git a/com/isoterminal.js b/com/isoterminal.js index 7c6b647..116165f 100644 --- a/com/isoterminal.js +++ b/com/isoterminal.js @@ -1,5 +1,40 @@ -/* +/** + * ## [isoterminal](com/isoterminal.js) * + * Renders a windowed terminal in both (non)immersive mode. + * It displays an interactive javascript console or boots into + * a Linux ISO image (via WASM). + * + * ```html + * + * ``` + * + * > depends on [AFRAME.utils.require](com/require.js) + * + * | property | type | default | info | + * |------------------|-----------|------------------------|------| + * | `iso` | `string` | https`//forgejo.isvery.ninja/assets/xrsh-buildroot/main/xrsh.iso" | | + * | `overlayfs` | `string` | *WORK-IN-PROGRESS* | | + * | `width` | `number` | 800 || + * | `height` | `number` | 600 || + * | `depth` | `number` | 0.03 || + * | `lineHeight` | `number` | 18 || + * | `prompt` | `boolean` | true | boot straight into ISO or give user choice | + * | `padding` | `number`` | 18 | | + * | `maximized` | `boolean` | false | | + * | `minimized` | `boolean` | false | | + * | `muteUntilPrompt`| `boolean` | true | mute stdout until a prompt is detected in ISO | + * | `HUD` | `boolean` | false | link to camera movement | + * | `transparent` | `boolean` | false | heavy, needs good gpu | + * | `memory` | `number` | 60 | VM memory (in MB) [NOTE` quest or smartphone webworker might crash > 40mb ] | + * | `bufferLatency` | `number` | 1 | in ms` bufferlatency from webworker to term (batch-update every char to texture) | + * | `debug` | `boolean` | false | | + * | `emulator` | `string` | fbterm | terminal emulator | + * + * > for more info see [xrsh.isvery.ninja](https://xrsh.isvery.ninja) + * + * Component design: + * ``` * css/html template * * ┌─────────┐ ┌────────────┐ exit-AR @@ -27,6 +62,7 @@ * * NOTE: For convenience reasons, events are forwarded between com/isoterminal.js, worker.js and ISOTerminal * Instead of a melting pot of different functionnames, events are flowing through everything (ISOTerminal.emit()) + * ``` */ if( typeof AFRAME != 'undefined '){ diff --git a/com/pastedrop.js b/com/pastedrop.js index a714aef..eb75dad 100644 --- a/com/pastedrop.js +++ b/com/pastedrop.js @@ -1,3 +1,18 @@ +/** + * ## [pastedrop](com/pastedrop.js) + * + * detects user copy/paste and file dragdrop action + * and clipboard functions + * + * ```html + * + * ``` + * + * | event | target | info | + * |--------------|--------|------------------------------------------| + * | `pasteFile` | self | always translates input to a File object | + */ + AFRAME.registerComponent('pastedrop', { schema: { foo: { type:"string"} @@ -22,16 +37,16 @@ AFRAME.registerComponent('pastedrop', { }) }, - //getClipboard: function(){ - // navigator.clipboard.readText() - // .then( async (base64) => { - // let mimetype = base64.replace(/;base64,.*/,'') - // let data = base64.replace(/.*;base64,/,'') - // let type = this.textHeuristic(data) - // const term = document.querySelector('[isoterminal]').components.isoterminal.term - // this.el.emit('pasteFile',{}) /*TODO* data incompatible */ - // }) - //}, + getClipboard: function(){ + navigator.clipboard.readText() + .then( async (base64) => { + let mimetype = base64.replace(/;base64,.*/,'') + let data = base64.replace(/.*;base64,/,'') + let type = this.textHeuristic(data) + const term = document.querySelector('[isoterminal]').components.isoterminal.term + this.el.emit('pasteFile',{}) /*TODO* data incompatible */ + }) + }, onDrop: function(e){ e.preventDefault() diff --git a/com/require.js b/com/require.js index 4e16fc9..f7c4bec 100644 --- a/com/require.js +++ b/com/require.js @@ -1,12 +1,17 @@ -// usage: -// -// await AFRAME.utils.require( this.dependencies ) (*) autoload missing components -// await AFRAME.utils.require( this.el.getAttributeNames() ) (*) autoload missing components -// await AFRAME.utils.require({foo: "https://foo.com/aframe/components/foo.js"},this) -// await AFRAME.utils.require(["./app/foo.js","foo.css"],this) -// -// (*) = prefixes baseURL AFRAME.utils.require.baseURL ('./com/' e.g.) -// +/** + * ## [require](com/require('').js) + * + * automatically requires dependencies + * + * ```javascript + * await AFRAME.utils.require( this.dependencies ) (*) autoload missing components + * await AFRAME.utils.require( this.el.getAttributeNames() ) (*) autoload missing components + * await AFRAME.utils.require({foo: "https://foo.com/aframe/components/foo.js"},this) + * await AFRAME.utils.require(["./app/foo.js","foo.css"],this) + * ``` + * + * > (*) = prefixes baseURL AFRAME.utils.require.baseURL ('./com/' e.g.) + */ AFRAME.utils.require = function(arr_or_obj,opts){ opts = opts || {} let i = 0 diff --git a/com/window.js b/com/window.js index 7015de2..364626d 100644 --- a/com/window.js +++ b/com/window.js @@ -1,3 +1,28 @@ + * ## [window](com/window.js) + * + * wraps a draggable window around a dom id or [dom](com/dom.js) component. + * + * ```html + * + * ``` + * + * > depends on [AFRAME.utils.require](com/require.js) + * + * | property | type | default | info | + * |------------------|-----------|------------------------|------| + * | `title` |`string` | "" | | + * | `width` |`string` | | | + * | `height` |`string` | 260px | | + * | `uid` |`string` | | | + * | `attach` |`selector` | | | + * | `dom` |`selector` | | | + * | `max` |`boolean` | false | | + * | `min` |`boolean` | false | | + * | `x` |`string` | "center" | | + * | `y` |`string` | "center" | | + * | `class` |`array` | [] | | + */ + AFRAME.registerComponent('window', { schema:{ title: {type:'string',"default":"title"}, diff --git a/com/xterm.js b/com/xterm.js deleted file mode 100644 index 04ae169..0000000 --- a/com/xterm.js +++ /dev/null @@ -1,268 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2019 - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - * 2019 Mauve Ranger - * 2024 Leon van Kammen - */ - -let terminalInstance = 0 - -const TERMINAL_THEME = { - theme_foreground: { - // 'default': '#ffffff' - }, - theme_background: { - // 'default': '#000' - }, - theme_cursor: { - // 'default': '#ffffff' - }, - theme_selection: { - // 'default': 'rgba(255, 255, 255, 0.3)' - }, - theme_black: { - // 'default': '#000000' - }, - theme_red: { - // 'default': '#e06c75' - }, - theme_brightRed: { - // 'default': '#e06c75' - }, - theme_green: { - // 'default': '#A4EFA1' - }, - theme_brightGreen: { - // 'default': '#A4EFA1' - }, - theme_brightYellow: { - // 'default': '#EDDC96' - }, - theme_yellow: { - // 'default': '#EDDC96' - }, - theme_magenta: { - // 'default': '#e39ef7' - }, - theme_brightMagenta: { - // 'default': '#e39ef7' - }, - theme_cyan: { - // 'default': '#5fcbd8' - }, - theme_brightBlue: { - // 'default': '#5fcbd8' - }, - theme_brightCyan: { - // 'default': '#5fcbd8' - }, - theme_blue: { - // 'default': '#5fcbd8' - }, - theme_white: { - // 'default': '#d0d0d0' - }, - theme_brightBlack: { - // 'default': '#808080' - }, - theme_brightWhite: { - // 'default': '#ffffff' - } -} - -AFRAME.registerComponent('xterm', { - schema: Object.assign({ - XRrenderer: { type: 'string', default: 'canvas', }, - cols: { type: 'number', default: 110, }, - rows: { type: 'number', default: Math.floor( (window.innerHeight * 0.7 ) * 0.054 ) }, - canvasLatency:{ type:'number', default: 200 } - }, TERMINAL_THEME), - - write: function(message) { - this.term.write(message) - }, - init: function () { - const terminalElement = document.createElement('div') - terminalElement.setAttribute('style', ` - width: 800px; - height: ${Math.floor( 800 * 0.527 )}px; - overflow: hidden; - `) - - this.el.terminalElement = terminalElement - - if( this.data.XRrenderer == 'canvas' ){ - // setup slightly bigger black backdrop (this.el.getObject3D("mesh")) - // and terminal text (this.el.planeText.getObject("mesh")) - const w = 2; - const h = (this.data.rows*5/this.data.cols) - this.el.setAttribute("geometry",`primitive: box; width:${w}; height:${h}; depth: -0.12`) - this.el.setAttribute("material","shader:flat; color:black; opacity:0.5; transparent:true; ") - this.el.planeText = document.createElement('a-entity') - this.el.planeText.setAttribute("geometry",`primitive: plane; width:${w}; height:${h}`) - this.el.appendChild(this.el.planeText) - - // we switch between dom/canvas rendering because canvas looks pixely in nonimmersive mode - this.el.sceneEl.addEventListener('enter-vr', this.enterImmersive.bind(this) ) - this.el.sceneEl.addEventListener('enter-ar', this.enterImmersive.bind(this) ) - this.el.sceneEl.addEventListener('exit-vr', this.exitImmersive.bind(this) ) - this.el.sceneEl.addEventListener('exit-ar', this.exitImmersive.bind(this) ) - - } - - this.tick = AFRAME.utils.throttleLeadingAndTrailing( () => { - if( this.el.sceneEl.renderer.xr.isPresenting ){ - // workaround - // xterm relies on window.requestAnimationFrame (which is not called WebXR immersive mode) - //this.term._core.viewport._innerRefresh() - this.term._core.renderer._renderDebouncer._innerRefresh() - } - }, this.data.canvasLatency) - - // Build up a theme object - const theme = Object.keys(this.data).reduce((theme, key) => { - if (!key.startsWith('theme_')) return theme - const data = this.data[key] - if(!data) return theme - theme[key.slice('theme_'.length)] = data - return theme - }, {}) - - this.fontSize = 14 - - const term = this.term = new Terminal({ - logLevel:"off", - theme: theme, - allowTransparency: true, - cursorBlink: true, - disableStdin: false, - rows: this.data.rows, - cols: this.data.cols, - fontFamily: 'Cousine, monospace', - fontSize: this.fontSize, - lineHeight: 1.15, - useFlowControl: true, - rendererType: this.renderType // 'dom' // 'canvas' - }) - - this.term.open(terminalElement) - this.term.focus() - this.setRenderType('dom') - - terminalElement.querySelector('.xterm-viewport').style.background = 'transparent' - - // now we can scale canvases to the parent element - const $screen = terminalElement.querySelector('.xterm-screen') - $screen.style.width = '100%' - - term.on('refresh', AFRAME.utils.throttleLeadingAndTrailing( () => this.update(), 150 ) ) - term.on('data', (data) => { - this.el.emit('xterm-input', data) - }) - - this.el.addEventListener('serial-output-byte', (e) => { - const byte = e.detail - var chr = String.fromCharCode(byte); - this.term.write(chr) - }) - - this.el.addEventListener('serial-output-string', (e) => { - this.term.write(e.detail) - }) - - }, - - update: function(){ - if( this.renderType == 'canvas' ){ - const material = this.el.planeText.getObject3D('mesh').material - if (!material.map ) return - if( this.cursorCanvas ) this.canvasContext.drawImage(this.cursorCanvas, 0,0) - else console.log("no cursorCanvas") - material.map.needsUpdate = true - //material.needsUpdate = true - } - }, - - setRenderType: function(type){ - - - if( type.match(/(dom|canvas)/) ){ - - if( type == 'dom'){ - this.el.dom.appendChild(this.el.terminalElement) - this.term.setOption('fontSize', this.fontSize ) - this.term.setOption('rendererType',type ) - this.renderType = type - } - - if( type == 'canvas'){ - this.el.appendChild(this.el.terminalElement) - this.term.setOption('fontSize', this.fontSize * 3 ) - this.term.setOption('rendererType',type ) - this.renderType = type - this.update() - setTimeout( () => { - this.canvas = this.el.terminalElement.querySelector('.xterm-text-layer') - this.canvas.id = "xterm-canvas" - this.canvasContext = this.canvas.getContext('2d') - this.cursorCanvas = this.el.terminalElement.querySelector('.xterm-cursor-layer') - // Create a texture from the canvas - const canvasTexture = new THREE.Texture(this.canvas) - //canvasTexture.minFilter = THREE.NearestFilter //LinearFilter - //canvasTexture.magFilter = THREE.LinearMipMapLinearFilter //THREE.NearestFilter //LinearFilter - canvasTexture.needsUpdate = true; // Ensure the texture updates - let plane = this.el.planeText.getObject3D("mesh") //this.el.getObject3D('mesh') - if( plane.material ) plane.material.dispose() - plane.material = new THREE.MeshBasicMaterial({ - map: canvasTexture, // Set the texture from the canvas - transparent: false, // Set transparency - //side: THREE.DoubleSide // Set to double-sided rendering - //blending: THREE.AdditiveBlending - }); - this.el.object3D.scale.x = 0.2 - this.el.object3D.scale.y = 0.2 - this.el.object3D.scale.z = 0.2 - },100) - } - - this.el.terminalElement.style.opacity = type == 'canvas' ? 0 : 1 - - } - }, - - enterImmersive: function(){ - if( this.mode == 'immersive' ) return - this.el.object3D.visible = true - this.mode = "immersive" - this.setRenderType('canvas') - this.term.focus() - }, - - exitImmersive: function(){ - if( this.mode == 'nonimmersive' ) return - this.el.object3D.visible = false - this.mode = "nonimmersive" - this.setRenderType('dom') - }, - -})