diff --git a/README.md b/README.md index 9035af7..9c436af 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ | package | description | |-|-| +| [drumkeyboard](drumkeyboard) | A customizable keyboard component | | [fzy](fzy) | lightweight + fast fuzzyfinder alternative | | [joe](joe) | Set of battleproof lightweight text/code editors | | [nano](nano) | Lightweight code editor with syntax highlighting | diff --git a/drumkeyboard/.env b/drumkeyboard/.env new file mode 100755 index 0000000..93fa739 --- /dev/null +++ b/drumkeyboard/.env @@ -0,0 +1,18 @@ +#!/bin/sh +dir=$(pwd) + +# overlay our directories +run(){ + test -f /.nano || { + find . -type d -mindepth 1 | while read dir; do + cp -d -r ./$dir/* /$dir/. + done + echo 1 > /.nano + cp ./root/.nanorc ~/. + echo "[i] imported overlay fs" + echo "[i] editor 'nano' installed" | logger + } +} + +test -n "$BROWSER" && run + diff --git a/drumkeyboard/README.md b/drumkeyboard/README.md new file mode 100644 index 0000000..2e2e5bb --- /dev/null +++ b/drumkeyboard/README.md @@ -0,0 +1,5 @@ +# Drumkeyboard + +A customizable keyboard component + +> NOTE: after importing package run `cd ~/scene-drumkeyboard` to load the demoscene diff --git a/drumkeyboard/build.sh b/drumkeyboard/build.sh new file mode 100755 index 0000000..1539bf6 --- /dev/null +++ b/drumkeyboard/build.sh @@ -0,0 +1,4 @@ +#!/bin/sh +set -e +name=drumkeyboard +zip -r package.$name.zip root .env .env.leave diff --git a/drumkeyboard/package.drumkeyboard.zip b/drumkeyboard/package.drumkeyboard.zip new file mode 100644 index 0000000..987ddd1 Binary files /dev/null and b/drumkeyboard/package.drumkeyboard.zip differ diff --git a/drumkeyboard/root/scene-drumkeyboard/.env b/drumkeyboard/root/scene-drumkeyboard/.env new file mode 100755 index 0000000..480541d --- /dev/null +++ b/drumkeyboard/root/scene-drumkeyboard/.env @@ -0,0 +1,4 @@ +#!/bin/sh +echo "loading drumkeyboard demo scene" +cp index.html /root/index.html +cat drumkbd.js index.js > /root/index.js diff --git a/drumkeyboard/root/scene-drumkeyboard/.env.leave b/drumkeyboard/root/scene-drumkeyboard/.env.leave new file mode 100644 index 0000000..a9f4150 --- /dev/null +++ b/drumkeyboard/root/scene-drumkeyboard/.env.leave @@ -0,0 +1 @@ +echo "" > /root/index.html # cleanup diff --git a/drumkeyboard/root/scene-drumkeyboard/drumkbd.js b/drumkeyboard/root/scene-drumkeyboard/drumkbd.js new file mode 100644 index 0000000..ed3b56f --- /dev/null +++ b/drumkeyboard/root/scene-drumkeyboard/drumkbd.js @@ -0,0 +1,229 @@ +// let sequentialFilters = [] +// + +let currentFilter = null + +function applyNextFilter( filename ){ + if ( currentFilter == null ) currentFilter = -1 + currentFilter++ + if ( sequentialFilters[currentFilter] ){ + sequentialFilters[ currentFilter ]( filename ) + } else { + console.log( "done filtering for", filename ) + currentFilter = null + } +} + +function showFile( filename, openingOptions = {}){ + console.log('showFile', filename) + fetch( '/mnt/root/scene-drumkeyboard/'+filename ).then( r => { + let idFromFilename = filename.replaceAll('.','') // has to remove from proper CSS ID + if (!filesWithMetadata[filename] ) filesWithMetadata[filename] = {} + filesWithMetadata[filename].contentType = r.headers.get('Content-Type') + filesWithMetadata[filename].idFromFilename = idFromFilename + filesWithMetadata[filename].openingOptions = openingOptions + console.log( 'ct metadata', filename, filesWithMetadata[filename].contentType ) + + applyNextFilter( filename ) + // const showdefinitions = urlParams.get('showdefinitions'); + // should be showFile() configuration parameter instead + // can be used via e.g. showFile("https://fabien.benetou.fr/?action=source",{ mereology:"whole"}) + + // should emit an event when done, for now just console.log which isn't programmatic + }) +} + +function addDrumKeyboard(){ + // get keymap + showFile('fabien_corneish_zen.keymap') + // showFile('fabien_corneish_zen.keymap') + + const swiping = urlParams.get('swiping'); + + const keyclass = "keys_from_drumsticks" + // transform keymap to keys of keyboard + let keyboardEl = document.createElement("a-entity") + keyboardEl.id = 'keyboard' + AFRAME.scenes[0].appendChild( keyboardEl ) + AFRAME.scenes[0].addEventListener("keymaploaded", e => { + applyToClass("keymap_layer", el => el.setAttribute("visible", "false") ) + // alternatively other layers could be displayed but with lower opacity + // might be very busy but easier to recall + Array.from( document.querySelectorAll('.keymap_layer') ).map( (layerToAdd, layerNumber) => { + layerToAdd.getAttribute("value").split('\n').map( (l,y) => { + l.split("|").map( (k,x) => { + if (k.trim()) { + // zIndex could be once deep once shallow to potentially go faster between keys, kind of straggered vs ortho + let xOffset = -.2 + let yOffset = 1.3 + let zOffset = -.4 + let keysPerRow = l.split("|").length -1 + let ratio = 1/20 + zOffset -= Math.abs( keysPerRow/2 - x ) * ratio/5 // arguably more ergonomic + if (swiping){ zOffset = zOffset + Math.abs( x - keysPerRow/2 ) * ratio } // opposite from swiping + // somehow the center isn't... the center + if (l.length < 70) xOffset += .15 // 80 was fine for layer 0 but not layer 1 and 2 + let labelEl = document.createElement("a-troika-text") + labelEl.setAttribute("value",k.trim()) + labelEl.setAttribute("position", "0 .51 0") + labelEl.setAttribute("font-size", "1") + labelEl.setAttribute("color", "black") + labelEl.setAttribute("rotation", "-90 0 0") + + let keyEl = document.createElement("a-cylinder") + keyEl.setAttribute("segments-height", "2") + keyEl.setAttribute("segments-radial", "24") + keyEl.setAttribute("scale", ".01 .01 .01") + keyEl.setAttribute("rotation", "60 0 0") + keyEl.setAttribute("position", ""+(xOffset+x*ratio)+" " +(yOffset-y*ratio)+" "+(zOffset + y/50) ) + keyEl.classList.add( keyclass ) + keyEl.classList.add( "layer_"+ layerNumber ) + keyEl.appendChild( labelEl ) + keyboardEl.appendChild( keyEl ) + // keyEl.id = keyclass+'_'+k.trim() // not correct anymore as multiple layers can have the same key + keyEl.id = keyclass+'_'+layerNumber+'_'+k.trim() + // setTimeout( _ => keyEl.object3D.lookAt( new THREE.Vector3(0, 1.5, 0)), 100 ) + // not great as they have 90 deg rotation already, could find a better way + // ... but arguably should be the opposite based on resting hand positions + } + }) + }) + }) + + keys_from_drumsticks_0_SHFT.setAttribute("wireframe", shiftFromVirtualKeyboard) // arguable, could do so for all layers + // hide all layers but the current one + Array.from( document.querySelectorAll(".keys_from_drumsticks") ).map( k => k.setAttribute("visible", "false") ) + Array.from( document.querySelectorAll(".keys_from_drumsticks"+".layer_"+layerFromVirtualKeyboard) ).map( k => k.setAttribute("visible", "true") ) + keyBoardCheck() + }) + + const threshold = .02 // distance + const refractionPeriod = 500 // ms until next keypress + + // add visible contact points + let jointTestEl1 = document.createElement("a-sphere") + jointTestEl1.setAttribute("radius", .01) + jointTestEl1.id = "jointtest1" + AFRAME.scenes[0].appendChild( jointTestEl1 ) + let jointTestEl2 = document.createElement("a-sphere") + jointTestEl2.setAttribute("radius", .01) + jointTestEl2.id = "jointtest2" + AFRAME.scenes[0].appendChild( jointTestEl2 ) + AFRAME.scenes[0].setAttribute("bind-element-to-finger__test1", { hand: 'r_handMeshNode', finger: 'index-finger-tip', target: '#jointtest1' } ) + AFRAME.scenes[0].setAttribute("bind-element-to-finger__test2", { hand: 'l_handMeshNode', finger: 'index-finger-tip', target: '#jointtest2' } ) + + // thumb tip test + let jointTestEl3 = document.createElement("a-sphere") + jointTestEl3.setAttribute("radius", .01) + jointTestEl3.id = "jointtest3" + AFRAME.scenes[0].appendChild( jointTestEl3 ) + let jointTestEl4 = document.createElement("a-sphere") + jointTestEl4.setAttribute("radius", .01) + jointTestEl4.id = "jointtest4" + AFRAME.scenes[0].appendChild( jointTestEl4 ) + AFRAME.scenes[0].setAttribute("bind-element-to-finger__test3", { hand: 'r_handMeshNode', finger: 'thumb-tip', target: '#jointtest3' } ) + AFRAME.scenes[0].setAttribute("bind-element-to-finger__test4", { hand: 'l_handMeshNode', finger: 'thumb-tip', target: '#jointtest4' } ) + + const forcecontrollers = urlParams.get('forcecontrollers'); + if (forcecontrollers){ + setTimeout( _ => { + jointtest1.object3D.parent = document.querySelector("[meta-touch-controls]").object3D + jointtest2.object3D.parent = document.querySelector("[oculus-touch-controls]").object3D + }, 2000 ) + // TODO does not work with pen + // see that does not seem to show anything, even in AFrame 1.7.1 nor recent master build + } + + // if done a la bind-element-to-finger consider + // const joint = AFRAME.scenes[0].object3D.getObjectByName(this.data.hand)?.parent.getObjectByName(this.data.finger) + // if ( joint && this.data.target.object3D.parent == AFRAME.scenes[0].object3D ) this.data.target.object3D.parent = joint + + let pos1 = new THREE.Vector3() + let pos2 = new THREE.Vector3() + let pos3 = new THREE.Vector3() + let pos4 = new THREE.Vector3() + + let shiftFromVirtualKeyboard = true + let layerFromVirtualKeyboard = 0 + + window.keyboardTarget = typinghud + + // check for potential contact + let lastKeypress = Date.now() + function keyBoardCheck() { + return setInterval( _ => { + jointTestEl1.object3D.getWorldPosition( pos1 ) + jointTestEl2.object3D.getWorldPosition( pos2 ) + jointTestEl3.object3D.getWorldPosition( pos3 ) + jointTestEl4.object3D.getWorldPosition( pos4 ) + // could also check only when jointTestEl1 / jointTestEl2 are visible, ignore otherwise + + // to do with all keys instead + Array.from( document.querySelectorAll(".keys_from_drumsticks"+".layer_"+layerFromVirtualKeyboard) ) + .concat( [keys_from_drumsticks_0_LWR, keys_from_drumsticks_0_RSE] ) + .filter( k => k.getAttribute("visible") ) + .map( k => { + // should only look at visible keys, could limit via .filter( k => k.getAttribute("visible") == "true") + // this way one wouldn't type on an invisible keyboard + let d1 = k.object3D.position.distanceTo( pos1 ) + let d2 = k.object3D.position.distanceTo( pos2 ) + let d3 = k.object3D.position.distanceTo( pos3 ) + let d4 = k.object3D.position.distanceTo( pos4 ) + if ( d1 < threshold || d2 < threshold || d3 < threshold || d4 < threshold) { + //if ( d1 < threshold || d2 < threshold) { + k.setAttribute("color", "pink") + if (keyboardTarget == typinghud) typinghud.setAttribute("material","opacity", .5) + if ( Date.now() - lastKeypress < refractionPeriod ){ + // console.warn('ignoring, executed during the last 500ms already') + let x = 42 // added just to ignore + } else { + lastKeypress = Date.now() + let value = k.firstChild.getAttribute("value") + if ( keyboardTarget.getAttribute("value") == "[]" ) keyboardTarget.setAttribute("value", "" ) // for typinghud starting value + if (value == "TAB") console.warn('does not complete yet') // TODO add completion + if (value == "SPC") value = " " + if (value == "ENT") { + if (keyboardTarget == typinghud) { + parseKeys("keydown", "Enter") + keyboardTarget.setAttribute("value", "" ) + } else { + keyboardTarget.setAttribute("value", keyboardTarget.getAttribute("value") + '\n' ) + } + } else if (value == "SHFT") { + shiftFromVirtualKeyboard = !shiftFromVirtualKeyboard + // visual highlight, also note that is closer to CAPSLOCK behavior + keys_from_drumsticks_0_SHFT.setAttribute("wireframe", shiftFromVirtualKeyboard) // arguable, could do so for all layers + } else if (value == "RSE") { + if (layerFromVirtualKeyboard<2) layerFromVirtualKeyboard++ // hardcoded max + console.log('should raise layer', layerFromVirtualKeyboard) // a la CAPSLOCK too + Array.from( document.querySelectorAll(".keys_from_drumsticks") ).map( k => k.setAttribute("visible", "false") ) + Array.from( document.querySelectorAll(".keys_from_drumsticks"+".layer_"+layerFromVirtualKeyboard) ).map( k => k.setAttribute("visible", "true") ) + // forcing visibility yet get ignored as on wrong layer + keys_from_drumsticks_0_LWR.setAttribute("visible", "true") + keys_from_drumsticks_0_RSE.setAttribute("visible", "true") + } else if (value == "LWR") { + if (layerFromVirtualKeyboard>0) layerFromVirtualKeyboard-- + console.log('should lower layer', layerFromVirtualKeyboard) // a la CAPSLOCK too + Array.from( document.querySelectorAll(".keys_from_drumsticks") ).map( k => k.setAttribute("visible", "false") ) + Array.from( document.querySelectorAll(".keys_from_drumsticks"+".layer_"+layerFromVirtualKeyboard) ).map( k => k.setAttribute("visible", "true") ) + keys_from_drumsticks_0_LWR.setAttribute("visible", "true") + keys_from_drumsticks_0_RSE.setAttribute("visible", "true") + } else if (value == "BKSP") { + keyboardTarget.setAttribute("value", keyboardTarget.getAttribute("value").slice(0,-1) ) + } else { + if (!shiftFromVirtualKeyboard) value = value.toLowerCase() + keyboardTarget.setAttribute("value", keyboardTarget.getAttribute("value") + value ) + } + keyboardEl.emit( "keypressed", {key: value} ) + } + } else if ( d1 < threshold*1.2 || d2 < threshold*1.2) { // arguably, not convinced it brings value more than confusion + k.setAttribute("color", "#ffe4e1") + } else { + k.setAttribute("color", "white") + } + }) + }, 20) + } + + return keyboardEl +} diff --git a/drumkeyboard/root/scene-drumkeyboard/fabien_corneish_zen.keymap b/drumkeyboard/root/scene-drumkeyboard/fabien_corneish_zen.keymap new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/drumkeyboard/root/scene-drumkeyboard/fabien_corneish_zen.keymap @@ -0,0 +1 @@ + diff --git a/drumkeyboard/root/scene-drumkeyboard/index.html b/drumkeyboard/root/scene-drumkeyboard/index.html new file mode 100755 index 0000000..5d3c393 --- /dev/null +++ b/drumkeyboard/root/scene-drumkeyboard/index.html @@ -0,0 +1,2 @@ +#!/bin/html + diff --git a/drumkeyboard/root/scene-drumkeyboard/index.js b/drumkeyboard/root/scene-drumkeyboard/index.js new file mode 100644 index 0000000..8f29233 --- /dev/null +++ b/drumkeyboard/root/scene-drumkeyboard/index.js @@ -0,0 +1,22 @@ +AFRAME.registerComponent('bind-element-to-finger', { + multiple: true, + schema: { + hand: {type: 'string', default: 'r_handMeshNode'}, + finger: {type: 'string', default: 'index-finger-tip'}, + target : {type: 'selector'}, + }, + tick: function (time, timeDelta) { + const joint = AFRAME.scenes[0].object3D.getObjectByName(this.data.hand)?.parent.getObjectByName(this.data.finger) + if ( joint && this.data.target.object3D.parent == AFRAME.scenes[0].object3D ) this.data.target.object3D.parent = joint + } +}) + +window.keyboardTarget = typinghud +let kbd = addDrumKeyboard() +kbd.addEventListener( "keypressed", e => { + document.querySelector("[isoterminal]").components.isoterminal.term.send(e.detail.key) + // consider executing the content on ENTER + // document.querySelector("[isoterminal]").components.isoterminal.term.exec("ls") + // raw keypress, this getting SHFT, ENT, etc +}) +