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
+})
+