|
|
|
@ -30,7 +30,7 @@ |
|
|
|
|
|
|
|
|
|
<!-- 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> |
|
|
|
@ -58,198 +58,66 @@ |
|
|
|
|
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) |
|
|
|
|
writecsljsonback(reader.result) |
|
|
|
|
}, 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" ] |
|
|
|
|
} |
|
|
|
|
if (file) { |
|
|
|
|
reader.readAsText(file); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// much too complex |
|
|
|
|
AFRAME.registerComponent('exercise', { |
|
|
|
|
const libraryURL = 'https://webdav.benetou.fr/fotsave/ExportedItems-FromZoteroAsCSLJSON.json' |
|
|
|
|
AFRAME.registerComponent('getcsljson', { |
|
|
|
|
schema: { |
|
|
|
|
instructions: {type: 'string'}, |
|
|
|
|
win: {type: 'string'}, |
|
|
|
|
failed: {type: 'string'}, |
|
|
|
|
next: {type: 'selector'}, |
|
|
|
|
first: {type: 'boolean', default: false}, |
|
|
|
|
url: {type: 'string', default: libraryURL }, |
|
|
|
|
}, |
|
|
|
|
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() |
|
|
|
|
}) |
|
|
|
|
let generatorName = this.attrName |
|
|
|
|
fetch(this.data.url).then(res => res.json() ).then(res => { notesFromArray(res, generatorName, "title", 2, -1/10) }) |
|
|
|
|
//fetch(this.data.url).then(res => res.json() ).then(res => { notesFromArray(res, generatorName) }) |
|
|
|
|
// could use citeproc instead |
|
|
|
|
// see also https://citation.js.org |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
function notesFromArray(data, generatorName="", field="title", offset=1, ratio=1/10, depth=-.5 ){ |
|
|
|
|
data.slice(0,maxItemsFromSources).map( (n,i) => addNewNote( n[field], "0 "+(offset+i*ratio)+" "+depth, ".1 .1 .1", null, generatorName ) ) |
|
|
|
|
} |
|
|
|
|
function getDataToSaveBack(){ |
|
|
|
|
let dataToSave = [] |
|
|
|
|
let unsorted = [] |
|
|
|
|
fetch(libraryURL).then(res => res.json() ).then(res => { |
|
|
|
|
// assume unique title always present |
|
|
|
|
getArrayFromClass('getcsljson').map(i=>{ |
|
|
|
|
unsorted.push( {data: res.filter(citation=>citation.title==i.getAttribute('value'))[0], |
|
|
|
|
position: i.getAttribute('position') |
|
|
|
|
}) |
|
|
|
|
}) |
|
|
|
|
dataToSave = unsorted.sort((a,b)=>b.position.y-a.position.y) |
|
|
|
|
.map(i=> { if (!i.data.note) { i.data.note = JSON.stringify(i.position) } else {i.data.note += '\n' + JSON.stringify(i.position) } ; return i.data } ) |
|
|
|
|
// this is also where piggy-backing on CSL-JSON could be tested, e.g spatial position (or stringifoied pose) field added |
|
|
|
|
// could consider appending to the .note field instead |
|
|
|
|
// .position does get saved, and does not prevent to be loaded from Zotero, but does get lost after when exported again |
|
|
|
|
// cf https://citeproc-js.readthedocs.io/en/latest/csl-json/markup.html#cheater-syntax-for-odd-fields |
|
|
|
|
writecsljsonback(JSON.stringify(dataToSave)) |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
AFRAME.registerComponent('warmup', { |
|
|
|
|
init: function(){ |
|
|
|
|
startExercise() |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
// data could come from parsing back order from getArrayFromClass('getcsljson').map(i=>i.getAttribute('position').y) |
|
|
|
|
// cf https://gist.github.com/Utopiah/26bae9fecc7a921f8bfd38cf5fc91612#file-logo_vr_hubs-js-L44 |
|
|
|
|
// yet still needs the actual data itself and adding a comment field for position if to be used back here rather than e.g Zotero |
|
|
|
|
function writecsljsonback(data){ |
|
|
|
|
const webdavurl = "https://webdav.benetou.fr"; |
|
|
|
|
const client = window.WebDAV.createClient(webdavurl) |
|
|
|
|
async function w(path, data){ return await client.putFileContents(path, data); } |
|
|
|
|
w("/fotsave/ExportedItems-FromZoteroAsCSLJSON.json", data ) |
|
|
|
|
setFeedbackHUD( "file saved" ) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
</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)" /> |
|
|
|
|
<input style='position:fixed;z-index:1; top: 0%; left: 20%; display:float' |
|
|
|
|
type="file" name="file-input" accept=".json" 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'> |
|
|
|
@ -258,7 +126,7 @@ AFRAME.registerComponent('warmup', { |
|
|
|
|
|
|
|
|
|
<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> |
|
|
|
|
<a-scene startfunctions getcsljson> |
|
|
|
|
<!-- 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;" |
|
|
|
@ -266,10 +134,6 @@ AFRAME.registerComponent('warmup', { |
|
|
|
|
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> |
|
|
|
@ -279,44 +143,13 @@ AFRAME.registerComponent('warmup', { |
|
|
|
|
</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-gltf-model hide-on-enter-ar="" src="../content/MinimalisticJapaneseRoom.glb" rotation="0 -90 0" position="2 1 -1" scale="" ></a-gltf-model> |
|
|
|
|
<!-- from https://poly.pizza/m/8cWuXx5BASV --> |
|
|
|
|
<!-- alt https://poly.pizza/m/cA_lcvRC4NA --> |
|
|
|
|
|
|
|
|
|
<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-troika-text anchor=left target annotation="content:saves data back to Zotero library over WebDAV backend" |
|
|
|
|
value="jxr getDataToSaveBack()" position=" -0.3 0.60 -.5" rotation="0 0 0" scale="0.1 0.1 0.1"></a-troika-text> |
|
|
|
|
|
|
|
|
|
<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"> |
|
|
|
|