<!DOCTYPE html> <html> <title>SpaSca : Spatial Scaffolding</title> <head> <!-- Suggestions? https://git.benetou.fr/utopiah/text-code-xr-engine/issues/ --> <script src='dependencies/aframe.offline.min.js'></script> <script src="dependencies/a-console.js"></script> <script src='dependencies/aframe-troika-text.min.js'></script> <script src='dependencies/webdav.js'></script> <script src="https://cdn.jsdelivr.net/gh/c-frame/aframe-extras@7.1.0/dist/aframe-extras.min.js"></script> <script src="https://cdn.jsdelivr.net/gh/c-frame/aframe-physics-system@v4.2.2/dist/aframe-physics-system.min.js"></script> <script src='jxr-core.js?12345'></script> <script src='jxr-postitnote.js?13235'></script> </head> <body> <script> /* TODO : - refactor to use emit('eventname', {eventdata:'data'}) for onreleased and onpicked rather than latest element e.g newEl.setAttribute("onreleased", 'document.querySelector("['+generatorName+']").emit("check",{color:"'+color+'"})') let latest = selectedElements[selectedElements.length-1].element check for this pattern and replace by event.detail.element instead (for now e.detail.element) might want to reconsider the el convention and do e.g let eventFromEl = e.detail.element over the eval() scope - insure scene setup, e.g starting position and orientation of environment and main character (until now assumed unchanged) - isolate emit('eventname', {test:0}) versus same with onreleased (which does NOT work) and same without event detail (which works) - add audio instructions ../content/voicesBigguJulia/simon_instructions_fr.mp3 ../content/voicesBigguJulia/labyrinthe_instructions_fr.mp3 ../content/voicesBigguJulia/lettresprenom_instructions_fr.mp3 ../content/voicesBigguJulia/tableauformes_instructions_fr.mp3 already there ../content/voicesBigguJulia/instructions.mp3 ../content/voicesBigguJulia/bravojulia.mp3 ../content/voicesBigguJulia/continu.mp3 ../content/voicesBigguJulia/biggu-fem.mp3 example : tts --text "Regarde la couleur, ecoute le son, et pince avec ta main droite chaque cube dans le meme ordre" --model_name "tts_models/fr/mai/tacotron2-DDC" --out_path story.wav Regarde la couleur, ecoute le son, et pince avec ta main droite chaque cube dans le meme ordre Aide-moi a atteindre le poisson a la sortie du labyrinthe. Pour cela trouve les instructions pour me faire avancer, il y a en 4. Avec ta main gauche pince la premiere lettre et je bougerais dans cette direction ! Oops, les formes sont toutes melangees. Merci de les remettre a leur place en faisant attention que chaque fois ai la bonne couleur, comme indique par la ligne du haut et la colonne de droite. Pour deplacer les formes colorees pince les avec ta main droite. Oh non, les lettres de ton prenom ont ete melangees. Pince chacune avec ta main droite et pose les dans le cube dans le bon ordre - reset (as done for fishinbowl) - better menu (e.g target with onreleased) - fix maze/mazemap mismatch (causing emit() error on init) - game ideas - art deco / art nouveau facade as puzzle mixed pieces - philosophical experimentation, cf https://video.benetou.fr/w/9KGbaxtAEx4JLnhAkwQKC1 featuring then the trolley problem */ AFRAME.registerComponent('startfunctions', { init: function(){ let untestedGames = ["checkers", "carcassone"] addGame("voxelpaint") addGame("simon") addGame("physics-construct") addGames() } }) //___________________________________________________________________________________________________________________________________ /* game manager component parent entity where each game itself is another child entity menu show/hide each game bookmark filter on e.g age range, last played, not completed has listener to unify animation and audio e.g yes/win or try again but also lets custom content be presented, e.g custom audio instructions */ function addGame(gamename, visible="false"){ let newEl = document.createElement('a-entity') newEl.id = gamename newEl.setAttribute(gamename, "") newEl.setAttribute("visible", visible) newEl.classList.add( "game" ) AFRAME.scenes[0].appendChild(newEl) } function addGames(){ const imgPath = "../content/games/previews/" const imgExtension = ".jpg" // show/hide should be enough (target should only work when shown iirc) Array.from( document.querySelectorAll('.game') ).map( (g,i) => { let n = addNewNote("jxr showOnlyThisGame('"+g.id+"')") AFRAME.scenes[0].appendChild(n) setTimeout( _ => { let newEl = document.createElement("a-image") newEl.setAttribute("src", imgPath+g.id+imgExtension) //newEl.setAttribute("position", "-1 0 0") newEl.setAttribute("target", "true") // now works despite relative position... but weird newEl.setAttribute("onreleased", "showOnlyThisGame('"+g.id+"')") n.appendChild( newEl ) n.object3D.position.y+=i/10 // n.setAttribute("annotation", "content:...") // e.g to add French, would need to add specific data e.g full name, translation with language name e.g FR, etc }, 500 ) }) // also need to add reset state! // could add a reset event listener on each component } function showOnlyThisGame(name){ // should also work via URL, e.g hash or query parameter Array.from( document.querySelectorAll('.game') ).map( (g,i) => g.setAttribute("visible", "false") ) document.getElementById(name).setAttribute("visible", "true") document.querySelector("["+name+"]").emit("reset") } //___________________________________________________________________________________________________________________________________ /* physics https://github.com/c-frame/aframe-physics-system and setup docs https://github.com/c-frame/aframe-physics-system/blob/master/CannonDriver.md#installation should append to head script with src="https://cdn.jsdelivr.net/gh/c-frame/aframe-physics-system@v4.2.2/dist/aframe-physics-system.min.js" <!-- Floor --> <a-plane static-body></a-plane> <!-- Immovable box --> <a-box static-body position="0 0.5 -5" width="3" height="1" depth="1"></a-box> <!-- Dynamic box --> <a-box dynamic-body position="5 0.5 0" width="1" height="1" depth="1"></a-box> todo test getVoxelPoses() to hash equivalent, in order to be able to share builds */ AFRAME.registerComponent('physics-construct', { init: function(){ let generatorName = this.attrName let el = this.el let sphereEl = document.createElement('a-sphere') sphereEl.setAttribute('radius', '.01') sphereEl.setAttribute('target', '') sphereEl.setAttribute('position', '0 1 -.5') sphereEl.setAttribute("onpicked", "window.pfb = selectedElements.at(-1).element.getAttribute('position').clone();") sphereEl.setAttribute("onreleased", "document.querySelector('["+generatorName+"]').components['"+generatorName+"'].twoPosToBox(window.pfb, selectedElements.at(-1).element.getAttribute('position'), '"+generatorName+"');") // really verbose... consider rebinding or more helpers in such events, facilitating access to this.el and component overall AFRAME.scenes[0].appendChild(sphereEl) let ballEl = document.createElement('a-sphere') ballEl.setAttribute('radius', '.01') ballEl.setAttribute('color', 'blue') ballEl.setAttribute('target', '') ballEl.setAttribute('position', '0 1.5 -0.2') //ballEl.setAttribute('dynamic-body', '') // does not fall? ballEl.setAttribute("onpicked", 'e.detail.element.removeAttribute("dynamic-body")') // untested ballEl.setAttribute("onreleased", 'e.detail.element.setAttribute("dynamic-body","")') AFRAME.scenes[0].appendChild(ballEl) AFRAME.scenes[0].setAttribute("physics", "debug:true") //let script = document.createElement("script") //script.setAttribute("src", "https://cdn.jsdelivr.net/gh/c-frame/aframe-physics-system@v4.2.2/dist/aframe-physics-system.min.js") //document.head.appendChild(script) // does not work, check older tricks if (window.location.hash) { let poses = JSON.parse(decodeURI(window.location.hash.replace("#",'')))[generatorName] // prefixed by generatorName in order to support saving/sharing of the state of other games poses?.map( p => { let newEl = document.createElement('a-box') newEl.setAttribute("scale", p.scale) newEl.setAttribute("position", p.position) newEl.setAttribute("rotation", p.rotation) newEl.setAttribute("target","true") newEl.setAttribute('static-body', '') newEl.setAttribute("onreleased", "document.querySelector('["+generatorName+"]').emit('getVoxelPoses')") newEl.classList.add( "voxel_" + generatorName ) newEl.classList.add( "voxel" ) newEl.classList.add( generatorName ) AFRAME.scenes[0].appendChild(newEl) }) } }, events: { reset: function (evt) { console.log(this.attrName, 'component was resetted!'); let generatorName = this.attrName Array.from( this.el.querySelectorAll("."+generatorName) ).map( el => deleteTarget(el) ) }, check: function (evt) { }, getVoxelPoses: function (evt){ console.log('generating poses') let generatorName = this.attrName let poses = [] // to clarify querySelectorAll here is done on the element and thus should no interfer with other components using the same mechanism // Array.from( this.el.querySelectorAll(".voxel") ).map( el => { Array.from( AFRAME.scenes[0].querySelectorAll(".voxel_" + generatorName ) ).map( el => { poses.push( { position: this.shortenVector3( el.getAttribute("position") ), rotation: this.shortenVector3( el.getAttribute("rotation") ), scale: this.shortenVector3( el.getAttribute("scale") ), } ) }) let data = {} data[generatorName] = poses window.location.hash = JSON.stringify(data) console.log('generated poses:', poses.length) // prefixed by generatorName in order to support saving/sharing of the state of other games }, }, shortenVector3: function ( v ){ let o = new THREE.Vector3() o.x = v.x.toFixed(3) o.y = v.y.toFixed(3) o.z = v.z.toFixed(3) return o }, twoPosToBox(A, B, generatorName){ let center = A.clone() center.add(B) center.divideScalar(2) let lengthes = A.clone() lengthes.sub(B) let newEl = document.createElement("a-box") newEl.setAttribute("position", center ) newEl.setAttribute('target', '') newEl.setAttribute('static-body', '') newEl.classList.add( "voxel" ) newEl.classList.add( generatorName ) newEl.classList.add( "voxel_" + generatorName ) newEl.setAttribute("scale", lengthes.toArray().map( i => Math.abs(i) ).join(" ") ) newEl.setAttribute("onreleased", "document.querySelector('["+generatorName+"]').emit('getVoxelPoses')") AFRAME.scenes[0].appendChild(newEl) // should parent to the component element instead... return newEl } }) //___________________________________________________________________________________________________________________________________ AFRAME.registerComponent('voxelpaint', { init: function(){ let generatorName = this.attrName let el = this.el this.colors = ["red", "green", "blue", "yellow" ] this.scale = 1/10 this.yOffset = 1.5 let j = 0 this.colors.map( (color, i) => { let newEl = document.createElement('a-box') newEl.setAttribute("scale", ""+this.scale+" "+this.scale+" "+this.scale) newEl.setAttribute("color",color) newEl.setAttribute("position", ""+(i%2)*this.scale+" "+(this.yOffset+j*this.scale)+" -.5") newEl.setAttribute("target","true") newEl.setAttribute("onpicked", "document.querySelector('["+generatorName+"]').emit('check')") newEl.setAttribute("onreleased", "document.querySelector('["+generatorName+"]').emit('getVoxelPoses')") newEl.classList.add( generatorName ) el.appendChild(newEl) if (i==1) j++ }) // check if data is present, as hash or query parameter, and if so, display accordingly if (window.location.hash) { let poses = JSON.parse(decodeURI(window.location.hash.replace("#",'')))[generatorName] // prefixed by generatorName in order to support saving/sharing of the state of other games poses.map( p => { let newEl = document.createElement('a-box') newEl.setAttribute("scale", ""+this.scale+" "+this.scale+" "+this.scale) newEl.setAttribute("color",p.color) newEl.setAttribute("position", p.position) newEl.setAttribute("rotation", p.rotation) newEl.setAttribute("target","true") newEl.setAttribute("onreleased", "document.querySelector('["+generatorName+"]').emit('getVoxelPoses')") newEl.classList.add( "voxel" ) newEl.classList.add( generatorName ) el.appendChild(newEl) }) } }, events: { reset: function (evt) { console.log(this.attrName, 'component was resetted!'); let generatorName = this.attrName Array.from( this.el.querySelectorAll(".voxel") ).map( el => el.remove() ) }, check: function (evt) { let generatorName = this.attrName let latest = selectedElements[selectedElements.length-1].element let newEl = latest.cloneNode(true) // does not seem to properly clone all attributes, e.g color works but not position or scale // ["scale", "position", "onpicked"].map( prop => console.log( prop)) //newEl.setAttribute(prop, latest.getAttribute(prop) ) newEl.setAttribute("scale", latest.getAttribute("scale") ) newEl.setAttribute("position", latest.getAttribute("position") ) newEl.setAttribute("onpicked", latest.getAttribute("onpicked") ) latest.removeAttribute("onpicked") latest.classList.add( "voxel" ) this.el.appendChild( newEl ) // could also snap it back, e.g clear rotation, possibily use initially position }, getVoxelPoses: function (evt){ let generatorName = this.attrName let poses = [] Array.from( this.el.querySelectorAll(".voxel") ).map( el => { poses.push( { position: this.shortenVector3( el.getAttribute("position") ), rotation: this.shortenVector3( el.getAttribute("rotation") ), color: el.getAttribute("color") } ) }) let data = {} data[generatorName] = poses window.location.hash = JSON.stringify(data) // prefixed by generatorName in order to support saving/sharing of the state of other games } }, shortenVector3: function ( v ){ let o = new THREE.Vector3() o.x = v.x.toFixed(3) o.y = v.y.toFixed(3) o.z = v.z.toFixed(3) return o } }) //___________________________________________________________________________________________________________________________________ AFRAME.registerComponent('simon', { init: function(){ let generatorName = this.attrName let el = this.el this.colors = ["red", "green", "blue", "yellow" ] const notePrefix = '../content/notes/t' const noteSuffix = '.mp3' this.sequence = [] this.posInSeq = -1 this.userSeq = [] this.scale = 1/10 let j = 0 this.colors.map( (color, i) => { let newEl = document.createElement('a-box') newEl.setAttribute("scale", ""+this.scale+" "+this.scale+" "+this.scale) newEl.setAttribute("color",color) newEl.setAttribute("opacity", .5) newEl.setAttribute("sound", "src:url("+notePrefix+(i+1)+noteSuffix+")") newEl.setAttribute("position", ""+(i%2)*this.scale+" 1.1 "+j*this.scale) newEl.setAttribute("target","true") newEl.setAttribute("onreleased", 'document.querySelector("['+generatorName+']").emit("check",{color:"'+color+'"})') newEl.classList.add( generatorName ) el.appendChild(newEl) if (i==1) j++ }) //setTimeout( _ => { document.querySelector("["+generatorName+"]").emit('playSequence') }, 1000) }, events: { reset: function (evt) { console.log(this.attrName, 'component was resetted!'); let generatorName = this.attrName this.sequence = [] this.posInSeq = -1 this.userSeq = [] // could also reposition the boxes clearInterval( this.interval ) setTimeout( _ => { document.querySelector("["+generatorName+"]").emit('playSequence') }, 1000) }, check: function (evt) { // could also snap it back, e.g clear rotation, possibily use initially position let generatorName = this.attrName this.userSeq.push(evt.detail.color) let box = this.el.querySelector("a-box[color="+evt.detail.color+"]") box.components.sound.playSound() console.log ('seq:', this.sequence.at( this.userSeq.length-1) ,'user:', this.userSeq.at(-1) ) if (this.userSeq.at(-1) == this.sequence.at( this.userSeq.length-1) ){ console.log('same', this.sequence, this.userSeq) if (this.userSeq.length == this.sequence.length){ console.log('entire sequence complete', this.sequence, this.userSeq) animateThenIdle( document.querySelector("#biggu"), 'bigguaction_win') this.userSeq = [] document.querySelector("["+generatorName+"]").emit('playSequence') // grow sequence } else { console.log('partial sequence only, waiting for new input') animateThenIdle( document.querySelector("#biggu"), 'bigguaction_yes') } } else { document.querySelector("["+generatorName+"]").emit('reset') animateThenIdle( document.querySelector("#biggu"), 'bigguaction_no') console.log('failed, should reset') } }, playSequence: function(evt) { this.interval = setInterval( _ => { this.posInSeq++ if (this.posInSeq == this.sequence.length){ this.sequence.push( this.colors.at( this.colors.length * Math.random() ) ) clearInterval( this.interval ) setTimeout( _ => { this.posInSeq=-1 }, 100) // should also a timeout to start giving answers } let box = this.el.querySelector("a-box[color="+ this.sequence[ this.posInSeq ] + "]") box.setAttribute("opacity", 1) box.components.sound.playSound() setTimeout( _ => { box.setAttribute("opacity", .5) }, 700) }, 1000) } } }) //___________________________________________________________________________________________________________________________________ AFRAME.registerComponent('carcassone', { init: function(){ // written vertically then joined, corners, bridges, crosses let tiles = [ "00100 01100 11011 00110 00100", "00100 00100 10101 00100 00100", "00100 00100 11111 00100 00100", ] this.colors = ['red', 'green', 'blue', 'yellow'] let generatorName = this.attrName let el = this.el let deckOfTiles = [] for (let i=0; i<4; i++) deckOfTiles.push( tiles[2] ) for (let i=0; i<3; i++) this.colors.map( (c,i) => { let t = tiles[1].replace('10101','10'+(i+2)+'01') // put in the center, easier, but could be a random one bridge, center vertical deckOfTiles.push( t ) }) let colorMixes = [] for (let i=1; i<4; i++) colorMixes.push( [0,i] ) for (let i=2; i<4; i++) colorMixes.push( [1,i] ) colorMixes.push( [2,3] ) for (let i=0; i<2; i++) colorMixes.map( cs => { let t = tiles[0].replace('11011','1'+(cs[0]+2)+'0'+(cs[1]+2)+'1') deckOfTiles.push( t ) }) for (let i=0; i<2; i++) this.colors.map( (c,i) => { let t = tiles[0].replace('01100','01'+(i+2)+'00') deckOfTiles.push( t ) }) // TODO add the item per color, should try to make minimalist fishes, e.g cone for tail the flatten sphere for body // test to generate tiles let stepSize = 1/2 deckOfTiles.map( (tile,n) => { let t = this.tileFromData( tile ) t.setAttribute("position", "0 0 "+(n*stepSize)) el.appendChild( t ) }) }, tileFromData: function(tileData){ let generatorName = this.attrName let tileEl = document.createElement("a-entity") tileData.split(" ").filter(l=>l.length>0).map( (line,i) => { let whatever = [...line.trim()].map( (c,j) =>{ let newEl = document.createElement("a-box") newEl.setAttribute("scale", ".1 .1 .1") let color let pieceColor switch (Number(c)){ case 0: color="blue" newEl.setAttribute("height", 2) break; case 1: color="white" break; // could do Number(c) to be able to check if >1 as fish on tile (with potential a random rotation) case 2: case 3: case 4: case 5: color="white" pieceColor = this.colors[Number(c)-2] break; } if (pieceColor){ let pieceEl = document.createElement('a-cylinder') pieceEl.setAttribute("radius", .4) pieceEl.setAttribute("height", .1) pieceEl.setAttribute("color", pieceColor) pieceEl.setAttribute("position", "0 1 0") pieceEl.classList.add( generatorName ) newEl.appendChild(pieceEl) } newEl.setAttribute("color", color) newEl.setAttribute("position", ""+j/10+" 0 "+i/10) tileEl.appendChild(newEl) }) }) return tileEl }, events: { reset: function (evt) { console.log(this.attrName, 'component was resetted!'); }, check: function (evt) { let generatorName = this.attrName } } }) //___________________________________________________________________________________________________________________________________ AFRAME.registerComponent('checkers', { init: function(){ let generatorName = this.attrName let el = this.el let color = "white" this.scale = 1/10 for (let j=0;j<8;j++){ for (let i=0;i<8;i++){ let newEl = document.createElement('a-box') newEl.setAttribute("scale", ""+this.scale+" "+this.scale/10+" "+this.scale) color=="white"?color="black":color="white" newEl.setAttribute("color",color) newEl.setAttribute("position", ""+i*this.scale+" 0.1 "+j*this.scale) newEl.classList.add( generatorName ) el.appendChild(newEl) if (j<2){ let pieceEl = document.createElement('a-cylinder') pieceEl.setAttribute("radius", .04) pieceEl.setAttribute("height", .1) pieceEl.setAttribute("target", "true") pieceEl.setAttribute("color","#555555") pieceEl.setAttribute("position", ""+i*this.scale+" 0.1 "+j*this.scale) pieceEl.classList.add( generatorName ) el.appendChild(pieceEl) } if (j>=6){ let pieceEl = document.createElement('a-cylinder') pieceEl.setAttribute("radius", .04) pieceEl.setAttribute("height", .1) pieceEl.setAttribute("target", "true") pieceEl.setAttribute("color","#EEEEEE") pieceEl.setAttribute("position", ""+i*this.scale+" 0.1 "+j*this.scale) pieceEl.classList.add( generatorName ) el.appendChild(pieceEl) } } color=="white"?color="black":color="white" } }, events: { reset: function (evt) { console.log(this.attrName, 'component was resetted!'); }, check: function (evt) { let generatorName = this.attrName } } }) //___________________________________________________________________________________________________________________________________ // model component so far, single setup and single check AFRAME.registerComponent('fishinbowl', { init: function(){ let generatorName = this.attrName let el = this.el this.correctlyPlacedFishes = 0 this.maxFishes = 5 this.xOffset = -.1 this.yOffset = .5 this.zOffset = -.1 this.scale = 1/1 for (let i=0;i<this.maxFishes;i++){ let newEl = document.createElement('a-gltf-model') newEl.setAttribute("onreleased", 'document.querySelector("['+generatorName+']").emit("check")') // this is THE key part, namely that when we pinch, move then release that target, it checks the state of the component newEl.setAttribute("target","true") newEl.setAttribute("scale",".001 .001 .001") newEl.setAttribute("src","../content/winterset/Fish.glb") newEl.setAttribute("position", ""+(Math.random()+this.xOffset)+" "+(Math.random()*this.scale+this.yOffset)+" "+(-Math.random()*this.scale+this.zOffset)) newEl.classList.add( generatorName ) el.appendChild(newEl) } }, events: { reset: function (evt) { console.log(this.attrName, 'component was resetted!'); this.correctlyPlacedFishes = 0 Array.from( document.querySelectorAll('.'+this.attrName) ).map( (e,i) => { e.setAttribute("position", ""+(Math.random()+this.xOffset)+" "+(Math.random()*this.scale+this.yOffset)+" "+(-Math.random()*this.scale+this.zOffset)) }) }, check: function (evt) { let generatorName = this.attrName //used via onrelease="..." if (!selectedElements || selectedElements.length < 1) { console.warn(generatorName, 'check failed, should be called after entity moves, e.g onreleased="..."') return // should only happen after something has been moved } let latest = selectedElements[selectedElements.length-1].element let target = document.getElementById(generatorName+"_target") let posA = new THREE.Vector3(); let posB = new THREE.Vector3(); latest.object3D.getWorldPosition( posA ) target.object3D.getWorldPosition( posB ) if ( posA.distanceTo( posB ) < .2 ){ ++this.correctlyPlacedFishes console.log( this.correctlyPlacedFishes ) // forcing immovable latest.removeAttribute("target") // doesn't unmount the component iirc, not sure it works though! targets = targets.filter( e => e != target) if ( this.correctlyPlacedFishes < 3 ){ animateThenIdle( document.querySelector("#biggu"), 'bigguaction_yes') document.getElementById("biggucontinu").play() } if ( this.correctlyPlacedFishes == 3 ){ animateThenIdle( document.querySelector("#biggu"), 'bigguaction_win') document.getElementById("biggubravojulia").play() } } } } }) //___________________________________________________________________________________________________________________________________ let correctlyPlacedLetters = 0 // should show the target or a box around the letter otherwise can be tricky to grab for some letters without a top left corner // e.g B is easy, J is hard AFRAME.registerComponent('letterstoword', { init: function(){ correctlyPlacedLetters = 0 let generatorName = this.attrName let word = "JULIA" // assumes 1 letter per word, should index position instead const scale = 1/3.5 const xOffset = -.5 const yOffset = .5 const zOffset = -.1 let el = this.el let whatever = [...word].map( (c,i) =>{ let newEl = document.createElement('a-text') newEl.setAttribute("target", "") newEl.setAttribute("value", c) newEl.setAttribute("scale", ".5 .5 .5") newEl.setAttribute("onreleased", "lettersCheckDistanceToDedicatedTargetSpot('"+generatorName+"')") newEl.setAttribute("position", ""+(Math.random()+xOffset)+" "+(Math.random()*scale+yOffset)+" "+(-Math.random()*scale+zOffset)) newEl.classList.add( generatorName ) el.appendChild(newEl) let targetEl = document.createElement('a-box') targetEl.setAttribute("position", ""+(xOffset+i*scale)+" "+(yOffset)+" "+zOffset) targetEl.id = generatorName+"_"+c targetEl.setAttribute("scale", ".05 .05 .05") targetEl.setAttribute("opacity", ".5") el.appendChild(targetEl) }) } }) function lettersCheckDistanceToDedicatedTargetSpot(generatorName){ //used via onrelease="..." let latest = selectedElements[selectedElements.length-1].element let target = document.getElementById(generatorName+"_"+latest.getAttribute("value")) // should also be params, getting complicated... let posA = new THREE.Vector3(); let posB = new THREE.Vector3(); latest.object3D.getWorldPosition( posA ) target.object3D.getWorldPosition( posB ) if ( posA.distanceTo( posB ) < .2 ){ latest.setAttribute("color", "green") ++correctlyPlacedLetters console.log( correctlyPlacedLetters ) // forcing immovable latest.removeAttribute("target") // doesn't unmount the component iirc, not sure it works though! targets = targets.filter( e => e != target) if ( correctlyPlacedLetters < 5 ){ animateThenIdle( document.querySelector("#biggu"), 'bigguaction_yes') document.getElementById("biggucontinu").play() } if ( correctlyPlacedLetters == 5 ) { animateThenIdle( document.querySelector("#biggu"), 'bigguaction_win') document.getElementById("biggubravojulia").play() } } } //___________________________________________________________________________________________________________________________________ let correctlyPlacedPrimitives = 0 AFRAME.registerComponent('table2entries', { init: function(){ correctlyPlacedPrimitives = 0 let generatorName = this.attrName // generate grid and models tool, with target positions to check against const colors = ["red", "green", "blue"] const primitive = ["box", "sphere", "cylinder"] const scale = 1/3.5 const xOffset = .1 const yOffset = .2 const zOffset = -.6 let el = this.el colors.map( (c,j) => { let cel = document.createElement('a-plane') cel.setAttribute("color", c) cel.setAttribute("position", ""+(xOffset+colors.length*scale)+" "+(yOffset+j*scale)+" "+zOffset) cel.setAttribute("scale", ".1 .1 .1") el.appendChild(cel) }) primitive.map( (p,i) => { let pel = document.createElement('a-'+p) pel.setAttribute("position", ""+(xOffset+i*scale)+" "+(yOffset+primitive.length*scale)+" "+zOffset) pel.setAttribute("scale", ".05 .05 .05") if (p=="box") pel.setAttribute("scale", ".1 .1 .1") el.appendChild(pel) colors.map( (c,j) => { let newEl = document.createElement('a-'+p) newEl.setAttribute("target", "") newEl.setAttribute("color", c) newEl.setAttribute("scale", ".05 .05 .05") newEl.setAttribute("onreleased", "checkDistanceToDedicatedTargetSpot('"+generatorName+"')") if (p=="box") newEl.setAttribute("scale", ".1 .1 .1") newEl.setAttribute("position", ""+Math.random()+" "+Math.random()+" "+(Math.random()+zOffset)) newEl.classList.add( generatorName ) el.appendChild(newEl) let targetEl = document.createElement('a-box') targetEl.setAttribute("position", ""+(xOffset+i*scale)+" "+(yOffset+j*scale)+" "+zOffset) targetEl.id = generatorName+"_"+p+"_"+c targetEl.setAttribute("scale", ".05 .05 .05") targetEl.setAttribute("opacity", ".5") el.appendChild(targetEl) }) }) } }) function checkDistanceToDedicatedTargetSpot(generatorName){ //used via onrelease="..." let latest = selectedElements[selectedElements.length-1].element let target = document.getElementById(generatorName+"_"+latest.localName.split('-')[1]+"_"+latest.getAttribute("color")) // should also be params, getting complicated... let posA = new THREE.Vector3(); let posB = new THREE.Vector3(); latest.object3D.getWorldPosition( posA ) target.object3D.getWorldPosition( posB ) let idCheck = generatorName+"_"+latest.localName.split('-')[1]+"_"+latest.getAttribute("color") // should also be params, getting complicated... console.log (idCheck, posA.distanceTo( posB ), posA.distanceTo( posB ) < .2 ) if ( posA.distanceTo( posB ) < .2 ){ latest.setAttribute("wireframe", true) ++correctlyPlacedPrimitives console.log( correctlyPlacedPrimitives ) // forcing immovable latest.removeAttribute("target") // doesn't unmount the component iirc, not sure it works though! targets = targets.filter( e => e != target) if ( correctlyPlacedPrimitives < 9 ){ animateThenIdle( document.querySelector("#biggu"), 'bigguaction_yes') document.getElementById("biggucontinu").play() } if ( correctlyPlacedPrimitives == 9 ) { animateThenIdle( document.querySelector("#biggu"), 'bigguaction_win') document.getElementById("biggubravojulia").play() } } } //___________________________________________________________________________________________________________________________________ // should moved to e.g src='jxr-game-maze.js' AFRAME.registerComponent('mazemap', { init: function(){ let el = this.el this.data.split("\n").filter(l=>l.length>0).map( (line,i) => { let whatever = [...line].map( (c,j) =>{ let newEl = document.createElement("a-box") let color switch (c){ case "1": color="blue" newEl.setAttribute("height", 2) break; case "0": color="white" newEl.setAttribute("material", "metalness:.2") // no big difference break; case "S": color="grey" break; case "E": color="grey" newEl.id = "mazeend" break; } newEl.setAttribute("color", color) newEl.setAttribute("position", ""+j+" 0 "+i) el.appendChild(newEl) }) }) } }) // could also get from parameter URL e.g mazemap=S1111,00001,10111,10001,1110E as suggested by Leon function forbiddenSpots(){ // should only be done once return Array.from( document.querySelectorAll("#maze>a-entity>a-box[color=blue]") ) .map( el => { let pos = new THREE.Vector3(); el.object3D.getWorldPosition(pos); return pos}) } function overForbiddenSpot(selectorA="#biggu", distanceThreshold=.2){ let posA = new THREE.Vector3(); document.querySelector(selectorA).object3D.getWorldPosition( posA ) let over = false forbiddenSpots().map( posB => { if ( posA.distanceTo( posB ) < distanceThreshold ) over = true }) return over } function moveBigguForward(step=.2){ /* // move with waddle example let biggu = document.querySelector("#biggu") biggu.setAttribute("animation__translation", "property: position; to: 0 0 0.5; dur: 10000;") biggu.setAttribute("animation__waddle", "property: rotation; from: 0 -20 -10; to: 0 20 10; dur: 1000; loop:true; easing: linear; dir:alternate;") */ // could also first if within maze boundaries document.querySelector("#biggu").object3D.translateZ(step) if (overForbiddenSpot()) setTimeout( _ => document.querySelector("#biggu").object3D.translateZ(-step), 500 ) } function moveBigguBackward(step=-.2){ // could also first if within maze boundaries document.querySelector("#biggu").object3D.translateZ(step) if (overForbiddenSpot()) setTimeout( _ => document.querySelector("#biggu").object3D.translateZ(-step), 500 ) } function moveBigguRight(step=.2){ // could also first if within maze boundaries document.querySelector("#biggu").object3D.translateX(step) if (overForbiddenSpot()) setTimeout( _ => document.querySelector("#biggu").object3D.translateX(-step), 500 ) } function moveBigguLeft(step=-.2){ // could also first if within maze boundaries document.querySelector("#biggu").object3D.translateX(step) if (overForbiddenSpot()) setTimeout( _ => document.querySelector("#biggu").object3D.translateX(-step), 500 ) } function checkWinCondition(selectorA="#biggu", selectorB="#mazeend", distanceThreshold=.2){ let posA = new THREE.Vector3(); let posB = new THREE.Vector3(); document.querySelector(selectorA).object3D.getWorldPosition( posA ) document.querySelector(selectorB).object3D.getWorldPosition( posB ) if ( posA.distanceTo( posB ) < distanceThreshold ){ animateThenIdle( document.querySelector("#biggu"), 'bigguaction_win') document.getElementById("biggubravojulia").play() } return ( posA.distanceTo( posB ) < distanceThreshold ) } function animateThenIdle(mainCharacter, animationName, timeScale='1'){ mainCharacter.setAttribute('animation-mixer', "clip:"+animationName+";loop:once; timeScale:"+timeScale) mainCharacter.addEventListener('animation-finished', _ => { mainCharacter.setAttribute('animation-mixer', "clip:bigguaction_idle; loop:true;") }) // could return the animation duration or an event when done } // end src='jxr-game-maze.js' //___________________________________________________________________________________________________________________________________ function doesThisContainThat(latest, nearby){ //let latest = selectedElements[selectedElements.length-1].element //let nearby = getClosestTargetElements( latest.getAttribute('position') ) // https://threejs.org/docs/?q=box#api/en/math/Box3.containsBox // https://threejs.org/docs/?q=box#api/en/math/Box3.expandByObject let a = new THREE.Box3().expandByObject( latest.object3D ) // consider mesh.geometry.computeBoundingBox() first let b = new THREE.Box3().expandByObject( nearby.object3D ) return a.containsBox(b) // testable as doesThisContainThat( document.querySelector("[color='yellow']"), document.querySelector("[color='purple']") ) // <a-box scale=".1 .1 .1" position=".5 .8 -.3" color="purple" ></a-box> // <a-box scale=".2 .2 .2" position=".5 .8 -.3" color="yellow" ></a-box> } function snapToGrid(gridSize=1){ // default as 1 decimeter let latest = selectedElements[selectedElements.length-1].element latest.setAttribute("rotation", "0 0 0") let pos = latest.getAttribute("position") pos.multiplyScalar(gridSize*10).round().divideScalar(gridSize*10) latest.setAttribute("position", pos ) } // deeper question, making the rules themselves manipulable? JXR? // So the result of the grammar becomes manipulable, but could you make the rules of the grammar itself visual? Even manipulable? // could start by visualizing examples first e.g https://writer.com/wp-content/uploads/2024/03/grammar-1.webp function snapMAB(){ // multibase arithmetic blocks aka MAB cf https://en.wikipedia.org/wiki/Base_ten_block let latest = selectedElements[selectedElements.length-1].element let nearby = getClosestTargetElements( latest.getAttribute('position') ) let linked = [] if (nearby.length>0){ latest.setAttribute("rotation", AFRAME.utils.coordinates.stringify( nearby[0].el.getAttribute("rotation") ) ) latest.setAttribute("position", AFRAME.utils.coordinates.stringify( nearby[0].el.getAttribute("position") ) ) latest.object3D.translateX( 1/10 ) linked.push( latest ) linked.push( nearby[0].el ) let overlap = Array.from( document.querySelectorAll(".mab") ).filter( e => e.object3D.position.distanceTo( latest.object3D.position ) < 0.01 && e!=latest ) while (overlap.length > 0 ){ latest.object3D.translateX( 1/10 ) linked.push( overlap[0] ) overlap = Array.from( document.querySelectorAll(".mab") ).filter( e => e.object3D.position.distanceTo( latest.object3D.position ) < 0.01 && e!=latest ) } // do something special if it becomes 10, e.g become a single line, removing the "ridges" if (linked.length > 3) linked.map( e => Array.from( e.querySelectorAll("a-box") ).setAttribute("color", "orange") ) // also need to go backward too to see if it's the latest added } } function snapRightOf(){ let latest = selectedElements[selectedElements.length-1].element let nearby = getClosestTargetElements( latest.getAttribute('position') ) if (nearby.length>0){ latest.setAttribute("rotation", AFRAME.utils.coordinates.stringify( nearby[0].el.getAttribute("rotation") ) ) latest.setAttribute("position", AFRAME.utils.coordinates.stringify( nearby[0].el.getAttribute("position") ) ) latest.object3D.translateX( 1/10 ) // somehow... works only the 2nd time, not the 1st?! } } function grammarBasedSnap(){ // verify if snappable, e.g of same type (or not) // e.g check if both have .getAttribute('value').match(prefix) or not let latest = selectedElements[selectedElements.length-1].element let nearby = getClosestTargetElements( latest.getAttribute('position') ) if (nearby.length>0){ let closest = nearby[0].el let latestTypeJXR = latest.getAttribute('value').match(prefix) let closestTypeJXR = latest.getAttribute('value').match(prefix) latest.setAttribute("rotation", AFRAME.utils.coordinates.stringify( closest.getAttribute("rotation") ) ) latest.setAttribute("position", AFRAME.utils.coordinates.stringify( closest.getAttribute("position") ) ) if ( latestTypeJXR && closestTypeJXR ) latest.object3D.translateX( 1/10 ) // same JXR type, snap close else latest.object3D.translateX( 2/10 ) // different types, snap away // somehow... works only the 2nd time, not the 1st?! } } function cloneTarget(target){ let el = target.cloneNode(true) if (!el.id) el.id = "clone_" + crypto.randomUUID() else el.id += "_clone_" + crypto.randomUUID() AFRAME.scenes[0].appendChild(el) } function deleteTarget(target){ targets = targets.filter( e => e != target) target.remove() } function runClosestJXR(){ // ideally this would come from event details let latest = selectedElements[selectedElements.length-1].element let nearby = getClosestTargetElements( latest.getAttribute('position') ) // if (nearby.length>0){ interpretJXR( nearby[0].el.getAttribute("value") ) } nearby.map( n => interpretJXR( n.el.getAttribute("value") ) ) } AFRAME.registerComponent('idleafterload', { events: { 'model-loaded': function (evt) { console.log('This entity was loaded!'); this.el.setAttribute('animation-mixer', "clip:bigguaction_idle; loop:true;") } } }); </script> <div style='position:fixed;z-index:1; top: 0%; left: 0%; border-bottom: 70px solid transparent; border-left: 70px solid #eee; '> <a href="https://git.benetou.fr/utopiah/text-code-xr-engine/issues/"> <img style='position:fixed;left:10px;' title='code repository' src='gitea_logo.svg'> </a> </div> <a-scene startfunctions > <a-assets> <audio id="biggucestmoi" src="../content/voicesBigguJulia/biggu-fem.mp3"></audio> <audio id="biggubravojulia" src="../content/voicesBigguJulia/bravojulia.mp3"></audio> <audio id="biggucontinu" src="../content/voicesBigguJulia/continu.mp3"></audio> <audio id="bigguinstructions" src="../content/voicesBigguJulia/instructions.mp3"></audio> <template id="avatar-template"></template> <template id="left-hand-default-template"> <a-entity networked-hand-controls="hand:left"></a-entity> </template> <template id="right-hand-default-template"> <a-entity networked-hand-controls="hand:right"></a-entity> </template> </a-assets> <a-gltf-model id="environment" hide-on-enter-ar="" src="../content/winterset/WinterIsland.glb" rotation="0 20 0" position="2 -4.5 -3" ></a-gltf-model> <a-gltf-model src="../content/winterset/Crystal_iPoly3D.glb" position="-0.4 -0.2 -3" scale="0.1 0.1 0.1"></a-gltf-model> <a-gltf-model idleafterload id="biggu" src="../content/winterset/SK_Biggu_v029_optimized.glb" position="0 0 -1"> <!-- <a-sound src="#bigguinstructions"></a-sound> --> </a-gltf-model> <a-entity id="rig"> <a-entity id="player" networked="template:#avatar-template;attachTemplateToLocal:false;" hud camera look-controls wasd-controls position="0 1.6 0"> </a-entity> <a-entity id="rightHand" pinchprimary hand-tracking-controls="hand: right;"></a-entity> <a-entity id="leftHand" pinchsecondary wristattachsecondary="target: #box" hand-tracking-controls="hand: left;"></a-entity> </a-entity> <a-troika-text value="SpaSca : Spatial Scaffolding" anchor="left" outline-width="5%" font="../content/ChakraPetch-Regular.ttf" position="-3 5 -2" scale="3 3 3" rotation="80 0 0" troika-text="outlineWidth: 0.01; strokeColor: #ffffff" material="flatShading: true; blending: additive; emissive: #c061cb"></a-troika-text> <a-sky hide-on-enter-ar color="lightgray"></a-sky> <a-troika-text anchor=left target value="instructions : \n--right pinch to move\n--left pinch to execute" position="0 1.65 -0.2" scale="0.1 0.1 0.1"></a-troika-text> <a-troika-text anchor=left value="jxr location.reload()" target position="-.5 1.30 0" rotation="0 40 0" scale="0.1 0.1 0.1"></a-troika-text> <a-troika-text anchor=left value="jxr window.location.hash = ''" target position="-.5 1.40 0" rotation="0 40 0" scale="0.1 0.1 0.1"></a-troika-text> <a-console position="2 2 0" rotation="0 -45 0" font-size="34" height=1 skip-intro=true></a-console> <!-- ------------------------------------------------------------------------------------------------------------------ --> <a-entity visible="false" id="maze" class="game" onstart="" wincondition="checkWinCondition()" losecondition="" advice="" onmistake=""> <a-entity scale="0.2 0.2 0.2" position="0 -.1 -1" mazemap=" S1111 00001 10111 10001 1110E"> </a-entity> <a-gltf-model class="bigguEndPoint" scale="0.002 0.002 0.002" position=".9 0.1 -.2" gltf-model="../content/winterset/Fish.glb"></a-gltf-model> <!-- bad offset... so leaving the end position as part of the maze itself--> <a-troika-text anchor=left value="jxr moveBigguForward(); checkWinCondition()" target position="-0.3 .60 -.3" rotation="90 0 0" annotation="content: BIGGU DEVANT" scale="0.1 0.1 0.1"></a-troika-text> <a-troika-text anchor=left value="jxr moveBigguBackward(); checkWinCondition()" target position="-0.3 .40 -.3" rotation="90 0 0" annotation="content: BIGGU DERRIERE" scale="0.1 0.1 0.1"></a-troika-text> <a-troika-text anchor=left value="jxr moveBigguRight(); checkWinCondition()" target position="-0.1 .50 -.3" rotation="90 0 0" annotation="content: BIGGU DROITE" scale="0.1 0.1 0.1"></a-troika-text> <a-troika-text anchor=left value="jxr moveBigguLeft(); checkWinCondition()" target position="-0.5 .50 -.3" rotation="90 0 0" annotation="content: BIGGU GAUCHE" scale="0.1 0.1 0.1"></a-troika-text> </a-entity> <!-- restart? location.reload() for now --> <!-- ------------------------------------------------------------------------------------------------------------------ --> <a-entity visible="false" id="table2entries" class="game" onstart="" wincondition="" losecondition="" advice="" onmistake="" table2entries></a-entity> <!-- ------------------------------------------------------------------------------------------------------------------ --> <a-entity visible="false" id="letterstoword" class="game" onstart="" wincondition="" losecondition="" advice="" onmistake="" letterstoword></a-entity> <!-- ------------------------------------------------------------------------------------------------------------------ --> <a-entity visible="false" id="fishinbowl" class="game" onstart="" wincondition="" losecondition="" advice="" onmistake="" fishinbowl> <a-gltf-model id="fishinbowl_target" src="../content/winterset/FruitBowl.glb" position="0.00055 -0.01343 -0.4778" scale="0.1 0.1 0.1"></a-gltf-model> </a-entity> <!-- ------------------------------------------------------------------------------------------------------------------ --> <a-box id="box" visible="false"></a-box> <!-- bug if #box missing, so hiding for now --> <!-- bug in start-on-press after XR init, as mentioned there --> </a-scene> </body> </script> </html>