SpaSca : open SCAffolding to SPAcially and textualy explore interfaces https://fabien.benetou.fr/pub/home/future_of_text_demo/engine/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
text-code-xr-engine/index.html

908 lines
38 KiB

<!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='jxr-core.js?1234567'></script>
<script src='jxr-postitnote.js?13235'></script>
</head>
<body>
<script>
/* TODO :
- 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
- 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
*/
AFRAME.registerComponent('startfunctions', {
init: function(){
addGames()
// example of adding a game programmatically
let newEl = document.createElement('a-entity')
//let gamename = "checkers"
//let gamename = "carcassone"
//let gamename = "simon"
let gamename = "voxelpaint"
newEl.id = gamename
newEl.setAttribute(gamename, "")
newEl.classList.add( "game" )
AFRAME.scenes[0].appendChild(newEl)
}
})
//___________________________________________________________________________________________________________________________________
/*
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 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){
Array.from( document.querySelectorAll('.game') ).map( (g,i) => g.setAttribute("visible", "false") )
document.getElementById(name).setAttribute("visible", "true")
document.querySelector("["+name+"]").emit("reset")
}
//___________________________________________________________________________________________________________________________________
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: el.getAttribute("position"),
rotation: 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
}
}
})
//___________________________________________________________________________________________________________________________________
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')")
// somehow unable to pass parameters via emit() ?!
//newEl.setAttribute("onreleased", "document.querySelector('["+generatorName+"]').emit('check', {'color':'red'})")
//newEl.setAttribute("onreleased", 'document.querySelector("[simon]").emit("check", {color: "red"})')
//newEl.setAttribute("onreleased", 'document.querySelector("[simon]").emit("check", {color: "green"})')
//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) {
let latest = selectedElements[selectedElements.length-1].element
// could also snap it back, e.g clear rotation, possibily use initially position
let generatorName = this.attrName
let color = latest.getAttribute("color")
//this.userSeq.push(evt.detail.color)
//let box = this.el.querySelector("a-box[color="+evt.detail.color+"]")
this.userSeq.push(color)
let box = this.el.querySelector("a-box[color="+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
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-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>