in-situ-instructions
Fabien Benetou 7 months ago
parent ffc8494062
commit d0999b87e0
  1. 723
      index.html
  2. 30
      jxr-core.js

@ -7,97 +7,542 @@
<script src="dependencies/a-console.js"></script> <script src="dependencies/a-console.js"></script>
<script src='dependencies/aframe-troika-text.min.js'></script> <script src='dependencies/aframe-troika-text.min.js'></script>
<script src='dependencies/webdav.js'></script> <script src='dependencies/webdav.js'></script>
<script src='jxr-core.js?123'></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> <script src='jxr-postitnote.js?13235'></script>
</head> </head>
<body> <body>
<script> <script>
var forceXaxis /* TODO :
// setInterval( _ => console.log(forceXaxis), 1000) - reset (as done for fishinbowl)
- add audio instructions
var translatingTargets = false - better menu (e.g target with onreleased)
var clearRot - fix maze/mazemap mismatch (causing emit() error on init)
function toggleTranslateTargets(){ - game ideas
translatingTargets = !translatingTargets - art deco / art nouveau facade as puzzle mixed pieces
let scene = AFRAME.scenes[0].object3D */
if (translatingTargets){
let anchor = new THREE.Object3D() AFRAME.registerComponent('startfunctions', {
let latest = selectedElements[selectedElements.length-1].element init: function(){
latest.object3D.add( anchor ) addGames()
// also inherits rotation, could try cancel it as the opposite of latest rotation
// might be easier to copy the position only every few ms instead // example of adding a game programmatically
anchor.position.sub( latest.object3D.position ) let newEl = document.createElement('a-entity')
//targets.map( t => anchor.attach(t.object3D) ) //let gamename = "checkers"
// should attach all BUT the current moving entity! let gamename = "carcassone"
Array.from(document.querySelectorAll('.mab')).map( t => anchor.attach(t.object3D) ) newEl.id = gamename
// they don't move... despite newEl.setAttribute(gamename, "")
} else { newEl.classList.add( "game" )
clearInterval( clearRot ) AFRAME.scenes[0].appendChild(newEl)
Array.from(document.querySelectorAll('.mab')).map( t => scene.attach(t.object3D) )
//targets.map( t => scene.attach(t.object3D) ) // move with waddle example
// could delete anchor, cleaner 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;")
}
})
//___________________________________________________________________________________________________________________________________
/*
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('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()
}
} }
} }
var attachToPlayer = false //___________________________________________________________________________________________________________________________________
function toggleAttachToSelf(){ // should moved to e.g src='jxr-game-maze.js'
attachToPlayer = !attachToPlayer AFRAME.registerComponent('mazemap', {
attachToPlayer ? parent=document.querySelector("#player") : parent=AFRAME.scenes[0] init: function(){
targets.map( t => parent.object3D.attach(t.object3D) ) 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 checkIntersection(latest, nearby){ function moveBigguForward(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 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 latest = selectedElements[selectedElements.length-1].element
//let nearby = getClosestTargetElements( latest.getAttribute('position') ) //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.containsBox
// https://threejs.org/docs/?q=box#api/en/math/Box3.expandByObject // 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 a = new THREE.Box3().expandByObject( latest.object3D ) // consider mesh.geometry.computeBoundingBox() first
let b = new THREE.Box3().expandByObject( nearby.object3D ) let b = new THREE.Box3().expandByObject( nearby.object3D )
console.log(a,b, a.containsBox(b)) return a.containsBox(b)
// testable as checkIntersection( document.querySelector("[color='yellow']"), document.querySelector("[color='purple']") ) // 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=".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> // <a-box scale=".2 .2 .2" position=".5 .8 -.3" color="yellow" ></a-box>
} }
setTimeout( _ => {
let newPostIt = addNewNoteAsPostItNote("jxr console.log(222);", "0 1.2 -.5")
.setAttribute("onreleased", "grammarBasedSnap()")
let otherPostIt = addNewNoteAsPostItNote("jxr console.log(111);", "0 1.4 -.5")
.setAttribute("onreleased", "grammarBasedSnap()")
let postIt = addNewNoteAsPostItNote("hi this is a post-it note.", "0 1.6 -.5")
.setAttribute("onreleased", "runClosestJXR(); grammarBasedSnap()") // dunno how to share the event context back here...
// .setAttribute("onreleased", "snapNext()") // does NOT support multiple instances for now
// see https://aframe.io/docs/1.5.0/core/component.html#multiple
// maybe bind could help
//let cloneMe = addNewNote('jxr clone me from corner', '0 0 .1', '1 1 1', 'cmd')
// should rebind parent...
//setTimeout( _ => { _ => cloneMe.object3D.parent = postIt.object3D }, 1000 )
// should try object3D.attach() instead
//.addEventListener('loaded',
// entityIndexes( document.querySelector("[color='blue']").object3D.children[0] )
}, 1000 )
// e.g document.querySelector("[color='blue']").object3D.children[0]
function entityIndexes(mesh){ // needs a mesh with a geometry, not a group
// could also traverse
let gp = mesh.geometry.attributes.position;
let wPos = [];
for(let i = 0;i < gp.count; i++){
let p = new THREE.Vector3().fromBufferAttribute(gp, i); // set p from `position`
mesh.localToWorld(p); // p has wordl coords
wPos.push(p);
}
// many are duplicates, i.e a a cube will return 24 indexes (4 per 6 faces), not 8
//let l = [...new Set(wPos)].length; console.log( l )
[...new Set(wPos)].map( p => addNewNote("x", p))
console.log( [...new Set(wPos)].length )
// seems to add the duplicates again
// try to "de-dup" via .distanceTo() below a threshold instead
}
function snapToGrid(gridSize=1){ // default as 1 decimeter function snapToGrid(gridSize=1){ // default as 1 decimeter
let latest = selectedElements[selectedElements.length-1].element let latest = selectedElements[selectedElements.length-1].element
latest.setAttribute("rotation", "0 0 0") latest.setAttribute("rotation", "0 0 0")
@ -186,32 +631,14 @@ function runClosestJXR(){
nearby.map( n => interpretJXR( n.el.getAttribute("value") ) ) nearby.map( n => interpretJXR( n.el.getAttribute("value") ) )
} }
function notesFromArray(data, generatorName="", field="title", offset=1, step=1/10, depth=-.5 ){ AFRAME.registerComponent('idleafterload', {
data.slice(0,maxItemsFromSources).map( (n,i) => { events: {
addNewNote( n[field], "0 "+(offset+i*step)+" "+depth, ".1 .1 .1", null, generatorName ) 'model-loaded': function (evt) {
.setAttribute("onreleased","spreadItemsFromCollection('getcsljson', 1.5)") console.log('This entity was loaded!');
}) this.el.setAttribute('animation-mixer', "clip:bigguaction_idle; loop:true;")
} }
}
function spreadItemsFromCollection( generatorName, offset=1, step=1/10, depth=-.5 ){ });
getArrayFromClass(generatorName).sort((a,b)=>a.getAttribute('position').y-b.getAttribute('position').y).map( (n,i) => {
n.setAttribute('position', "0 "+(offset+i*step)+" "+depth)
n.setAttribute('rotation', "0 0 0") // could also be based on the average of all items, the first item, last one, etc
// see also snap-on-pinchended component
})
let items = getArrayFromClass(generatorName).sort((b,a)=>a.getAttribute('position').y-b.getAttribute('position').y).map( n => n.getAttribute('value') )
shareLiveEvent('modified list', items)
}
let page = "Wiki.VirtualRealityInterface";
// should do then only once graph loaded instead, should emit event
let pageFromParam = AFRAME.utils.getUrlParameter('page')
if (pageFromParam) page = pageFromParam
setTimeout( _ => {
Array.from( document.querySelectorAll("[value='"+page+"']") ).map( n =>
n.setAttribute("onreleased", "console.log('dropped, should toggle display children,"+n.id+"')"));
Array.from( document.querySelectorAll("[value='"+page+"']>a-sphere") ).map( n => n.setAttribute("color", "purple"))
}, 5000)
</script> </script>
<div style='position:fixed;z-index:1; top: 0%; left: 0%; border-bottom: 70px solid transparent; border-left: 70px solid #eee; '> <div style='position:fixed;z-index:1; top: 0%; left: 0%; border-bottom: 70px solid transparent; border-left: 70px solid #eee; '>
@ -221,8 +648,27 @@ setTimeout( _ => {
</div> </div>
<a-scene startfunctions > <a-scene startfunctions >
<a-gltf-model hide-on-enter-ar="" id="environment" src="../content/CubeRoom.glb" rotation="0 -90 0" position="0 0 1" scale="" ></a-gltf-model>
<!-- Cube Room by Anonymous [CC-BY] via Poly Pizza --> <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="rig">
<a-entity id="player" networked="template:#avatar-template;attachTemplateToLocal:false;" <a-entity id="player" networked="template:#avatar-template;attachTemplateToLocal:false;"
@ -232,60 +678,49 @@ setTimeout( _ => {
<a-entity id="leftHand" pinchsecondary wristattachsecondary="target: #box" hand-tracking-controls="hand: left;"></a-entity> <a-entity id="leftHand" pinchsecondary wristattachsecondary="target: #box" hand-tracking-controls="hand: left;"></a-entity>
</a-entity> </a-entity>
<a-box pressable start-on-press id="box" scale="0.05 0.05 0.05" color="pink"></a-box>
<a-troika-text value="SpaSca : Spatial Scaffolding" anchor="left" outline-width="5%" font="../content/ChakraPetch-Regular.ttf" position="-3 5 -2" <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> 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-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 0.65 -0.2" scale="0.1 0.1 0.1"></a-troika-text> <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 onNextPrimaryPinch(deleteTarget)" target position=" -0.3 1.50 0" rotation="0 40 0" scale="0.1 0.1 0.1"></a-troika-text> <a-troika-text anchor=left value="jxr location.reload()" target position="-1 1.30 0" rotation="0 40 0" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text anchor=left value="jxr onNextPrimaryPinch(cloneTarget)" target position=" -0.3 1.60 0" rotation="0 40 0" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text anchor=left value="jxr location.reload()" target position=" -0.3 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-troika-text anchor=left value="forceXaxis toggling" <!-- ------------------------------------------------------------------------------------------------------------------ -->
onreleased="console.log('run on released');forceXaxis=!forceXaxis" <a-entity visible="false" id="maze" class="game" onstart="" wincondition="checkWinCondition()" losecondition="" advice="" onmistake="">
onpicked="console.log('run on picked');forceXaxis=!forceXaxis" <a-entity scale="0.2 0.2 0.2" position="0 -.1 -1" mazemap="
target position=" -0.3 1.45 0" rotation="0 40 0" scale="0.1 0.1 0.1"></a-troika-text> 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-troika-text anchor=left value="translate targets"
onreleased="toggleTranslateTargets()"
onpicked="toggleTranslateTargets()"
target position=" 1 1.45 -.2" rotation="0 -40 0" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text anchor=left value="jxr setTimeout( _ => toggleAttachToSelf(), 1000); toggleAttachToSelf()" target position=" -0.3 1.25 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-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-box scale=".07 .07 .07" class="mab" target position=".3 1.6 -.5" color="brown" onreleased="snapMAB()" >
<a-box scale="1.5 1.5 1" color="brown"></a-box>
<a-box scale="1 1.5 1.5" color="brown"></a-box>
<a-box scale="1.5 1 1.5" color="brown"></a-box>
</a-box>
<a-box scale=".07 .07 .07" class="mab" target position=".1 1.6 -.5" color="brown" onreleased="snapMAB()" >
<a-box scale="1.5 1.5 1" color="brown"></a-box>
<a-box scale="1 1.5 1.5" color="brown"></a-box>
<a-box scale="1.5 1 1.5" color="brown"></a-box>
</a-box>
<a-box scale=".07 .07 .07" class="mab" target position=".5 1.6 -.5" color="brown" onreleased="snapMAB()" >
<a-box scale="1.5 1.5 1" color="brown"></a-box>
<a-box scale="1 1.5 1.5" color="brown"></a-box>
<a-box scale="1.5 1 1.5" color="brown"></a-box>
</a-box>
<a-box scale=".07 .07 .07" class="mab" target position="-.5 1.6 -.5" color="brown" onreleased="snapMAB()" >
<a-box scale="1.5 1.5 1" color="brown"></a-box>
<a-box scale="1 1.5 1.5" color="brown"></a-box>
<a-box scale="1.5 1 1.5" color="brown"></a-box>
</a-box>
<a-box scale=".1 .1 .1" target position=".5 1.6 -.3" color="blue" onreleased="snapToGrid()"
annotation="content:could also show/hide grid with gridHelper on pinch started and hide on release"
></a-box>
<a-box scale=".1 .1 .1" target position=".5 1.8 -.3" color="blue" onreleased="snapToGrid()" ></a-box>
<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>
</a-scene> </a-scene>
</body> </body>
</script> </script>

@ -33,11 +33,21 @@ AFRAME.registerComponent('target', {
targets.push( this.el ) targets.push( this.el )
this.el.classList.add("collidable") this.el.classList.add("collidable")
} }
// on remove should also remove from targets, e.g targets = targets.filter( e => e != target)
}) })
function getClosestTargetElements( pos, threshold=0.05 ){ function getClosestTargetElements( pos, threshold=0.05 ){ // if done frequently on large amount of targets, e.g hover on keyboard keys, consider proper structure e.g octree instead
// TODO Bbox intersects rather than position // TODO Bbox intersects rather than position
return targets.filter( e => e.getAttribute("visible") == true).map( t => { return { el: t, dist : pos.distanceTo(t.getAttribute("position") ) } }) return targets.filter( e => e.getAttribute("visible") == true)
// .map( t => { return { el: t, dist : pos.distanceTo(t.getAttribute("position") ) } })
// limited to local position
.map( t => {
let posTarget = new THREE.Vector3()
t.object3D.getWorldPosition( posTarget )
let d = pos.distanceTo( posTarget )
return { el: t, dist : d }
})
// needs reparenting to scene via attach() otherwise lead to strange behavior
.filter( t => t.dist < threshold && t.dist > 0 ) .filter( t => t.dist < threshold && t.dist > 0 )
.sort( (a,b) => a.dist > b.dist) .sort( (a,b) => a.dist > b.dist)
} }
@ -154,7 +164,12 @@ AFRAME.registerComponent('pinchprimary', { // currently only 1 hand, the right o
var closests = getClosestTargetElements( event.detail.position ) var closests = getClosestTargetElements( event.detail.position )
//if (closests && closests.length > 0) // avoiding self reference //if (closests && closests.length > 0) // avoiding self reference
// setFeedbackHUD("close enough, could stack with "+ closests[1].el.getAttribute("value") ) // setFeedbackHUD("close enough, could stack with "+ closests[1].el.getAttribute("value") )
var dist = event.detail.position.distanceTo( document.querySelector("#box").object3D.position ) /*
somehow #box MUST exist, otherwise craches?!
*/
let dist = 100
if ( document.querySelector("#box") )
dist = event.detail.position.distanceTo( document.querySelector("#box").object3D.position )
if (dist < .1){ if (dist < .1){
setFeedbackHUD("close enough, replaced shortcut with "+ selectedElement.getAttribute("value") ) setFeedbackHUD("close enough, replaced shortcut with "+ selectedElement.getAttribute("value") )
wristShortcut = selectedElement.getAttribute("value") wristShortcut = selectedElement.getAttribute("value")
@ -227,6 +242,7 @@ AFRAME.registerComponent('pinchprimary', { // currently only 1 hand, the right o
// avoiding setOnDropFromAttribute() as it is not idiosyncratic and creates timing issues // avoiding setOnDropFromAttribute() as it is not idiosyncratic and creates timing issues
AFRAME.registerComponent('onreleased', { // changed from ondrop to be coherent with event name AFRAME.registerComponent('onreleased', { // changed from ondrop to be coherent with event name
// could support multi // could support multi
// could check if target component is already present on this.el, if not, add it as it's required
events: { events: {
released: function (e) { released: function (e) {
let code = this.el.getAttribute('onreleased') let code = this.el.getAttribute('onreleased')
@ -242,6 +258,7 @@ AFRAME.registerComponent('onreleased', { // changed from ondrop to be coherent w
AFRAME.registerComponent('onpicked', { AFRAME.registerComponent('onpicked', {
// could support multi // could support multi
// could check if target component is already present on this.el, if not, add it as it's required
events: { events: {
picked: function (e) { picked: function (e) {
let code = this.el.getAttribute('onpicked') let code = this.el.getAttribute('onpicked')
@ -600,8 +617,8 @@ AFRAME.registerComponent('start-on-press', {
init: function(){ init: function(){
let el = this.el let el = this.el
this.el.addEventListener('pressedended', function (event) { this.el.addEventListener('pressedended', function (event) {
console.log(event) console.log(event)
// should ignore that if we entered XR recently // should ignore that if we entered XR recently
if (!primaryPinchStarted && wristShortcut.match(prefix)) interpretJXR(wristShortcut) if (!primaryPinchStarted && wristShortcut.match(prefix)) interpretJXR(wristShortcut)
// seems to happen also when entering VR // seems to happen also when entering VR
// other action could possibly based on position relative to zones instead, i.e a list of bbox/functions pairs // other action could possibly based on position relative to zones instead, i.e a list of bbox/functions pairs
@ -776,7 +793,8 @@ function switchSide(){
document.querySelector("#"+sides[secondarySide]+"Hand").removeAttribute("wristattachsecondary") document.querySelector("#"+sides[secondarySide]+"Hand").removeAttribute("wristattachsecondary")
document.querySelector("#"+sides[secondarySide]+"Hand").setAttribute("pinchprimary", "") document.querySelector("#"+sides[secondarySide]+"Hand").setAttribute("pinchprimary", "")
document.querySelector("#"+sides[primarySide]+"Hand").setAttribute("pinchsecondary", "") document.querySelector("#"+sides[primarySide]+"Hand").setAttribute("pinchsecondary", "")
document.querySelector("#"+sides[primarySide]+"Hand").setAttribute("wristattachsecondary", "target: #box") if ( document.querySelector("#box") )
document.querySelector("#"+sides[primarySide]+"Hand").setAttribute("wristattachsecondary", "target: #box")
if (primarySide == 0) { if (primarySide == 0) {
secondarySide = 0 secondarySide = 0
primarySide = 1 primarySide = 1

Loading…
Cancel
Save