|
|
|
<!DOCTYPE html>
|
|
|
|
<html>
|
|
|
|
<title>SpaSca : Spatial Scaffolding</title>
|
|
|
|
<head>
|
|
|
|
<!-- Suggestions? https://git.benetou.fr/utopiah/text-code-xr-engine/issues/ -->
|
|
|
|
|
|
|
|
<!--<script src='dependencies/aframe.min.js'></script>-->
|
|
|
|
<script src='dependencies/aframe.offline.min.js'></script>
|
|
|
|
<script src="dependencies/a-console.js"></script>
|
|
|
|
<script src='dependencies/aframe-html.js'></script>
|
|
|
|
<script src='dependencies/aframe-mirror.js'></script>
|
|
|
|
<script src='dependencies/aframe-troika-text.min.js'></script>
|
|
|
|
<!--<script type="module" id=immersbundle src='dependencies/immers-client.js?save=true'></script>-->
|
|
|
|
<!--<script type="module" id=immersbundle src="https://cdn.jsdelivr.net/npm/immers-client/dist/destination.bundle.js?role=modFull"></script>-->
|
|
|
|
<!--<script src="https://threejs.org/examples/js/exporters/GLTFExporter.js"></script>-->
|
|
|
|
|
|
|
|
<script src="https://cdn.jsdelivr.net/gh/c-frame/aframe-extras@7.1.0/dist/aframe-extras.min.js"></script>
|
|
|
|
|
|
|
|
<script src="dependencies/shiki0.14.1.js"></script>
|
|
|
|
|
|
|
|
<!-- for input sharing -->
|
|
|
|
<script src='dependencies/peerjs.min.js'></script>
|
|
|
|
<!-- for content sharing, using NAF
|
|
|
|
<script src='dependencies/socket.io.slim.js'></script>
|
|
|
|
<script src="https://naf.benetou.fr/easyrtc/easyrtc.js"></script>
|
|
|
|
<script src='dependencies/networked-aframe.min.js'></script>
|
|
|
|
-->
|
|
|
|
|
|
|
|
<script src="dependencies/aframe-physics-system.min.js"></script>
|
|
|
|
|
|
|
|
<!-- still experimenting, see webdav.html -->
|
|
|
|
<script src='dependencies/webdav.js'></script>
|
|
|
|
|
|
|
|
<script src='jxr.js?212345'></script>
|
|
|
|
<!-- replacing with local copies as CDNs are like unpkg tend to be slow
|
|
|
|
<script type="module" src="https://unpkg.com/immers-client/dist/destination.bundle.js"></script>
|
|
|
|
<script src="https://aframe.io/releases/1.3.0/aframe.min.js"></script>
|
|
|
|
|
|
|
|
<script src="https://unpkg.com/aframe-troika-text/dist/aframe-troika-text.min.js"></script>
|
|
|
|
<script src="aframe-html.js"></script>
|
|
|
|
|
|
|
|
<script src="https://unpkg.com/peerjs@1.4.5/dist/peerjs.min.js"></script>
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.4.0/socket.io.slim.js"></script>
|
|
|
|
<script src="https://naf.benetou.fr/easyrtc/easyrtc.js"></script>
|
|
|
|
<script src="https://unpkg.com/networked-aframe@^0.10.0/dist/networked-aframe.min.js"></script>
|
|
|
|
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/aframe-mirror@latest/index.js"></script>
|
|
|
|
-->
|
|
|
|
|
|
|
|
<link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png">
|
|
|
|
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png">
|
|
|
|
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png">
|
|
|
|
<link rel="manifest" href="site.webmanifest">
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
|
|
|
|
<script>
|
|
|
|
function loadFile(element){
|
|
|
|
const file = element.files[0]
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
|
|
|
let gltfEl = document.createElement('a-gltf-model')
|
|
|
|
gltfEl.setAttribute('position', '0 1 -1')
|
|
|
|
AFRAME.scenes[0].appendChild(gltfEl)
|
|
|
|
reader.addEventListener( "load", () => {
|
|
|
|
gltfEl.addEventListener("model-loaded", e => inspectModel(gltfEl.object3D) )
|
|
|
|
// never seems to fire
|
|
|
|
gltfEl.setAttribute('src', reader.result)
|
|
|
|
gltfEl.setAttribute('animation-mixer', "")
|
|
|
|
setTimeout( _ => {
|
|
|
|
console.log( inspectModel(gltfEl.object3D) )
|
|
|
|
}, 1000)
|
|
|
|
}, false,);
|
|
|
|
|
|
|
|
if (file) {
|
|
|
|
reader.readAsDataURL(file);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
function inspectModel(selectedModel){
|
|
|
|
let animations=[];
|
|
|
|
let foundBones=[];
|
|
|
|
let skinnedMeshes=[];
|
|
|
|
let morphTargets=[];
|
|
|
|
selectedModel.traverse( t => {
|
|
|
|
if (t.animations?.length) animations.push(t)
|
|
|
|
if (t.type == "Bone") foundBones.push(t)
|
|
|
|
if (t.type == "SkinnedMesh") skinnedMeshes.push(t)
|
|
|
|
if (t.morphTargetInfluences) morphTargets.push(t) // modelInspected.morphTargets.map(mt => mt.morphTargetInfluences.fill(0))
|
|
|
|
});
|
|
|
|
return {animations:animations, bones:foundBones, skinnedMeshes: skinnedMeshes, morphTargets:morphTargets}
|
|
|
|
}
|
|
|
|
|
|
|
|
function manualAnimate(selector="#biggu"){
|
|
|
|
inspectModel( document.querySelector(selector).object3D ).animations[0].animations.map( (a,n) =>
|
|
|
|
addNewNote('jxr document.querySelector('+selector+').setAttribute("animation-mixer", "clip:'+a.name+';loop:once")', '-1 '+(n/10+1)+' -1')
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
let sessionId = self.crypto.randomUUID()
|
|
|
|
|
|
|
|
// see https://biggu-backend-collab.glitch.me/ to insure steps are done correctly
|
|
|
|
function shareLiveEvent(eventName, eventData, server='https://biggu-backend-collab.glitch.me/'){
|
|
|
|
if (!eventName) return
|
|
|
|
let data = { eventName, sessionId }
|
|
|
|
if (eventData) data.eventData = eventData
|
|
|
|
let playername = AFRAME.utils.getUrlParameter('playername')
|
|
|
|
if (playername) data.playername = playername
|
|
|
|
fetch(server+'/newevent/'+JSON.stringify(data))
|
|
|
|
}
|
|
|
|
// should then become a container hosted on benetou.fr
|
|
|
|
|
|
|
|
function saveTargets(server='https://biggu-backend-collab.glitch.me/'){
|
|
|
|
// might try to generate a hash as ID, should be reproducible though
|
|
|
|
let data = []
|
|
|
|
Array.from( document.querySelectorAll("[target]") )
|
|
|
|
.filter( el => el.id != '')
|
|
|
|
.map( el => {
|
|
|
|
// limited to location/position for now as that's only what target does modify
|
|
|
|
if( hasBeenManipulated(el.getAttribute('position')) || hasBeenManipulated(el.getAttribute('rotation')) )
|
|
|
|
data.push({id:el.id, position:el.getAttribute('position'), rotation:el.getAttribute('rotation')})
|
|
|
|
})
|
|
|
|
if (data.length > 0) fetch(server+'/save?data='+JSON.stringify(data))
|
|
|
|
return data
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
function checkExerciseCompletion(targetNumber=2){
|
|
|
|
const instructions = document.querySelector("#instructions>a-troika-text")
|
|
|
|
let counter = 0
|
|
|
|
const mainCharacter = document.getElementById("biggu")
|
|
|
|
mainCharacter.addEventListener("animation-finished", _ => console.log('anim done'))
|
|
|
|
Array.from( document.querySelector("#fishes").children ).map( f => {
|
|
|
|
if (f.object3D.position.distanceTo( document.querySelector('#plate').object3D.position ) < .3 ) counter++ })
|
|
|
|
if (counter == targetNumber) {
|
|
|
|
instructions.emit('win')
|
|
|
|
shareLiveEvent('win')
|
|
|
|
document.getElementById("biggubravojulia").play()
|
|
|
|
animateThenIdle(mainCharacter, 'bigguaction_win')
|
|
|
|
} else {
|
|
|
|
document.getElementById("biggucontinu").play()
|
|
|
|
instructions.emit('failed', {counter:counter})
|
|
|
|
shareLiveEvent('failed', {counter:counter})
|
|
|
|
animateThenIdle(mainCharacter, 'bigguaction_yes')
|
|
|
|
// anims = [ "bigguaction_no", "Bigguaction_pl", "bigguaction_pr", "bigguaction_talk", "bigguaction_win", "bigguaction_win", "bigguaction_yes" ]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// much too complex
|
|
|
|
AFRAME.registerComponent('exercise', {
|
|
|
|
schema: {
|
|
|
|
instructions: {type: 'string'},
|
|
|
|
win: {type: 'string'},
|
|
|
|
failed: {type: 'string'},
|
|
|
|
next: {type: 'selector'},
|
|
|
|
first: {type: 'boolean', default: false},
|
|
|
|
},
|
|
|
|
init: function(){
|
|
|
|
const mainCharacter = document.getElementById("biggu")
|
|
|
|
mainCharacter.setAttribute('animation-mixer', "clip:bigguaction_idle; loop:true;")
|
|
|
|
const emittedExerciseId = this.el.id
|
|
|
|
const instructions = document.querySelector("#instructions>a-troika-text")
|
|
|
|
if (!this.data.first)
|
|
|
|
this.el.setAttribute('position', '0 0 9999')
|
|
|
|
else
|
|
|
|
instructions.setAttribute("value", this.data.instructions )
|
|
|
|
instructions.addEventListener('win', _ => {
|
|
|
|
instructions.setAttribute("value", this.data.win )
|
|
|
|
// use whatever last exercise set, not correct
|
|
|
|
// should setTimeout or let the player actively move on
|
|
|
|
this.el.setAttribute('position', '0 0 9999')
|
|
|
|
// must filter on id, making sure it's emited by matching excercise
|
|
|
|
if (this.data.next) {
|
|
|
|
console.log( this.data.next )
|
|
|
|
console.log( this.data.next.id )
|
|
|
|
this.data.next.setAttribute('position', '0 0 0')
|
|
|
|
}
|
|
|
|
})
|
|
|
|
instructions.addEventListener('failed', ev => {
|
|
|
|
instructions.setAttribute("value", this.data.failed )
|
|
|
|
console.log(emittedExerciseId, this.id)
|
|
|
|
})
|
|
|
|
// should be replaced by drawings or even scaled down models with "5" and arrow or hand model
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
// for more complete supervision consider remote scrcpy
|
|
|
|
AFRAME.registerComponent('start-with-supervision', {
|
|
|
|
init: function(){
|
|
|
|
const server='https://biggu-backend-collab.glitch.me'
|
|
|
|
// CORS enabled needed
|
|
|
|
const source = new EventSource(server+'/events');
|
|
|
|
shareLiveEvent('connected')
|
|
|
|
|
|
|
|
source.addEventListener('message', message => {
|
|
|
|
console.log('Got', message);
|
|
|
|
let json = JSON.parse(message.data)
|
|
|
|
if (json && json.eventName == 'start-with-supervision') startExercise()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
function startExercise(){
|
|
|
|
const maxFishes = 5
|
|
|
|
for (let i=0;i<maxFishes;i++){
|
|
|
|
let gltfEl = document.createElement('a-gltf-model')
|
|
|
|
gltfEl.setAttribute("ondrop","checkExerciseCompletion()")
|
|
|
|
gltfEl.setAttribute("target","true")
|
|
|
|
gltfEl.setAttribute("scale",".001 .001 .001")
|
|
|
|
gltfEl.setAttribute("src","../content/winterset/Fish.glb")
|
|
|
|
let x = Math.random()*1-1
|
|
|
|
let y = Math.random()*1-.5
|
|
|
|
let z = Math.random()*1-1
|
|
|
|
gltfEl.setAttribute("position",`${x} ${y} ${z}`)
|
|
|
|
//console.log("position",`${x} ${y} ${z}`)
|
|
|
|
document.querySelector("#fishes").appendChild(gltfEl)
|
|
|
|
} // might want to appear from the start
|
|
|
|
// audio does not match asset, should be a white bowl, not black
|
|
|
|
|
|
|
|
document.getElementById("exerciseA").setAttribute("visible", false)
|
|
|
|
// hide everything until this is done, otherwise overwhelming
|
|
|
|
const mainCharacter = document.getElementById("biggu")
|
|
|
|
setTimeout(_=>{
|
|
|
|
document.getElementById("biggucestmoi").play()
|
|
|
|
// works in XR on headset, not on desktop (needs user action)
|
|
|
|
animateThenIdle(mainCharacter, 'bigguaction_talk', .5)
|
|
|
|
},1000)
|
|
|
|
// should focus on learning pinch (thumb and index) before doing the exercise itself
|
|
|
|
setTimeout(_=>{
|
|
|
|
document.getElementById("exerciseA").setAttribute("visible", true)
|
|
|
|
document.getElementById("bigguinstructions").play()
|
|
|
|
animateThenIdle(mainCharacter, 'bigguaction_talk', .5)
|
|
|
|
},5000)
|
|
|
|
}
|
|
|
|
|
|
|
|
AFRAME.registerComponent('warmup', {
|
|
|
|
init: function(){
|
|
|
|
startExercise()
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
</script>
|
|
|
|
<input style='position:fixed;z-index:1; top: 0%; left: 20%; display:none'
|
|
|
|
type="file" name="file-input" accept=".gltf, .glb" id="file-input" onchange="loadFile(this)" />
|
|
|
|
<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>
|
|
|
|
|
|
|
|
<button id=mainbutton style="display:none; z-index: 1; position: absolute; width:50%; margin: auto; text-align:center; top:45%; left:30%; height:30%;" onclick="startExperience()">Start the experience (hand tracking recommended)</button>
|
|
|
|
|
|
|
|
<a-scene startfunctions start-with-supervision>
|
|
|
|
<!-- screenstack dynamic-view selectionboxonpinches glossary timeline issues fot
|
|
|
|
toolbox commands-from-external-json disable-components-via-url enable-components-via-url
|
|
|
|
physics="debug:true; friction: 0.01;"
|
|
|
|
networked-scene="serverURL: https://naf.benetou.fr/; adapter: easyrtc; audio: true;"
|
|
|
|
refresh-text-content-from-wiki-page="pagename:TestingPairCollaboration"
|
|
|
|
-->
|
|
|
|
<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>
|
|
|
|
|
|
|
|
<!--
|
|
|
|
../content/winterset/Pinetree.glb
|
|
|
|
../content/winterset/Penguin.glb
|
|
|
|
../content/winterset/Mountains.glb
|
|
|
|
../content/winterset/Crystal.glb
|
|
|
|
<a-entity hide-on-enter-ar="" id="environment" class="hidableenvironment" ></a-entity>
|
|
|
|
<a-entity hide-on-enter-ar="" id="environmentsky" class="hidableenvironment" ></a-entity>
|
|
|
|
-->
|
|
|
|
<a-gltf-model hide-on-enter-ar="" src="../content/winterset/WinterIsland.glb" position="2.1 -4.5 -1.7" ></a-gltf-model>
|
|
|
|
<a-gltf-model src="../content/winterset/WinterSled.glb" position="0.11322 0.14197 0.27573" ></a-gltf-model>
|
|
|
|
|
|
|
|
<a-entity id="bubble" position="0 1 -1" scale=".1 .1 .1">
|
|
|
|
<a-sphere scale="2 1 .1" color="white"></a-sphere>
|
|
|
|
<a-cone scale="2 1 .1" position="-.5 -.7 0" rotation="0 0 45" color="white"></a-cone>
|
|
|
|
<a-troika-text value="C'est moi Biggu!" font-size=".4" outline-color="gray" outline-width=".02"
|
|
|
|
align="center" color="black" position="0 0 .2"></a-troika-text>
|
|
|
|
</a-entity>
|
|
|
|
|
|
|
|
<a-entity id="instructions" position="0.5 0.7 -1" rotation="0 -20 0" scale=".07 .07 .07">
|
|
|
|
<a-sphere scale="2 1 .1" color="white"></a-sphere>
|
|
|
|
<a-cone scale="2 1 .1" position="-.5 -.7 0" rotation="0 0 45" color="white"></a-cone>
|
|
|
|
<a-troika-text value="" font-size=".3" outline-color="gray" outline-width=".02"
|
|
|
|
align="center" color="black" position="0 0 .2"></a-troika-text>
|
|
|
|
</a-entity>
|
|
|
|
|
|
|
|
<a-troika-text anchor=left target annotation="content:RECOMMENCER"
|
|
|
|
value="jxr startExercise()" position=" -0.3 0.60 .5" rotation="90 180 0" scale="0.1 0.1 0.1"></a-troika-text>
|
|
|
|
|
|
|
|
<a-entity id="exerciseA" exercise="first:true;next:#exerciseB;instructions:Pose 2 poissons\ndans l'assiette;win:Bravo Julia!;failed:presque Julia, continue;">
|
|
|
|
<a-entity id="fishes"></a-entity>
|
|
|
|
<a-gltf-model id="plate" src="../content/winterset/BowlWhite.glb" position="-0.397 0.14354 -0.508" scale="0.1 0.05 0.1" ></a-gltf-model>
|
|
|
|
</a-entity>
|
|
|
|
<a-gltf-model id="biggu" src="../content/winterset/SK_Biggu_v029_optimized.glb" position="-0.3 0.5 -1">
|
|
|
|
<!-- <a-sound src="#bigguinstructions"></a-sound> -->
|
|
|
|
</a-gltf-model>
|
|
|
|
|
|
|
|
<a-gltf-model src="../content/winterset/Crystal_iPoly3D.glb" position="-0.29409 -0.23524 -0.83481" scale="0.1 0.1 0.1"></a-gltf-model>
|
|
|
|
<a-gltf-model src="../content/winterset/FruitBowl.glb" position="-0.29409 -.23524 -0.83481" scale="0.1 0.1 0.1"></a-gltf-model>
|
|
|
|
<a-entity id="rig">
|
|
|
|
<a-entity id="player" networked="template:#avatar-template;attachTemplateToLocal:false;"
|
|
|
|
hud camera look-controls wasd-controls waistattach="target: .movebypinch" 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-box pressable start-on-press id="box" scale="0.05 0.05 0.05" color="pink"></a-box>
|
|
|
|
<!-- could attach functions here... BUT then they have to be activable with the other hand! -->
|
|
|
|
<!-- visual reminders of shortcuts, a poster on the far left/right of keyboard shortcuts -->
|
|
|
|
|
|
|
|
<a-entity light="type: ambient; color: #BBB; intensity: 0.6"></a-entity>
|
|
|
|
<a-entity light="type: directional; color: #FFF; intensity: 1.4" position="-0.5 1 1"></a-entity>
|
|
|
|
|
|
|
|
<a-troika-text value="SpaSca : Spatial Scaffolding" anchor="left" outline-width="5%" font="../content/ChakraPetch-Regular.ttf" position="-5.26197 6.54224 -1.81284"
|
|
|
|
scale="4 4 5" rotation="90 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 0.65 -0.2" scale="0.1 0.1 0.1"></a-troika-text>
|
|
|
|
|
|
|
|
<!--
|
|
|
|
<a-troika-text anchor=left target id="startdraw2d" annotation="content:dessiner en 2D"
|
|
|
|
value="jxr startDraw2D()" position="0 1.45 -0.1" scale="0.1 0.1 0.1"></a-troika-text>
|
|
|
|
|
|
|
|
<a-troika-text anchor=left target id="displaypred" value="jxr displayPred()" position="0 1.40 -0.1" scale="0.1 0.1 0.1"></a-troika-text>
|
|
|
|
<a-troika-text anchor=left target value="jxr tiltUpId('codeditor')" position=" -0.3 1.65 0" rotation="0 90 0" scale="0.1 0.1 0.1"></a-troika-text>
|
|
|
|
<a-troika-text anchor=left target value="jxr tiltDownId('codeditor')" position=" -0.3 1.60 0" rotation="0 90 0" scale="0.1 0.1 0.1"></a-troika-text>
|
|
|
|
-->
|
|
|
|
|
|
|
|
<!-- somehow disable hand interaction despite, according to the documentation, it should rely on world position
|
|
|
|
<a-text target value="jxr qs #rig sa position 0 0 10" position="0 1.55 .5" rotation="0 180 0" scale="0.1 0.1 0.1"></a-text>
|
|
|
|
<a-console position="2 2 0" rotation="0 -45 0" font-size="34" height=1 skip-intro=true></a-console>
|
|
|
|
-->
|
|
|
|
|
|
|
|
<!-- for Wolvic on Lynx support test -->
|
|
|
|
<a-entity thumbstick-shifting oculus-touch-controls="hand: left"></a-entity>
|
|
|
|
<a-entity thumbstick-shifting oculus-touch-controls="hand: right"></a-entity>
|
|
|
|
|
|
|
|
</a-scene>
|
|
|
|
</body>
|
|
|
|
</script>
|
|
|
|
</html>
|