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.
729 lines
30 KiB
729 lines
30 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 :
|
|
- reset (as done for fishinbowl)
|
|
- add audio instructions
|
|
- 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"
|
|
newEl.id = gamename
|
|
newEl.setAttribute(gamename, "")
|
|
newEl.classList.add( "game" )
|
|
AFRAME.scenes[0].appendChild(newEl)
|
|
|
|
// 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;")
|
|
*/
|
|
}
|
|
})
|
|
|
|
//___________________________________________________________________________________________________________________________________
|
|
|
|
/*
|
|
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()
|
|
}
|
|
}
|
|
}
|
|
|
|
//___________________________________________________________________________________________________________________________________
|
|
// 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){
|
|
// 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="-1 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>
|
|
|