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.
288 lines
15 KiB
288 lines
15 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='jxr-core.js?12345'></script>
|
|
<script src='jxr-postitnote.js?13235'></script>
|
|
</head>
|
|
<body>
|
|
|
|
<script>
|
|
/*
|
|
SpaSca circular menu inspired by https://www.olegfrolov.design/spatialcomputing or the Meta OS menu
|
|
|
|
on empty pinch moving, if (moving duration above threshold 500ms)
|
|
then
|
|
show menu nearby position with JXR commands that hides on released,
|
|
on moving
|
|
highlight closest snippet
|
|
rewrite nearby getClosestTargetElements() to be only over a subset or targets, i.e the ones from active menu
|
|
on release
|
|
if (nearby one of the created JXR snippets), execute it
|
|
remove those JXR commands
|
|
|
|
<a-troika-text id="spatial-introspection-test" anchor=left value="console.log('executing from secondary pinch');"
|
|
onreleased="console.log('run on released')"
|
|
onpicked="console.log('run on picked')"
|
|
target position=" -0.3 1.35 0" rotation="0 40 0" scale="0.1 0.1 0.1">
|
|
</a-troika-text>
|
|
*/
|
|
var library
|
|
AFRAME.registerComponent('library-load', {
|
|
init: function(){
|
|
//fetch("../content/library-acm22.json").then( res => res.json() ).then( res => {
|
|
fetch("https://webdav.benetou.fr/fotsave/test_newfot.json").then( res => res.json() ).then( res => {
|
|
library = res
|
|
console.log(res)
|
|
res.documents.map( (doc,i) => {
|
|
if (doc.customData[0].position)
|
|
addNewNote( doc.title, doc.customData[0].position )
|
|
else
|
|
addNewNote( doc.title, '.5 '+(1+i/50)+' -.5' )
|
|
.setAttribute("onreleased", "updateDatasetThenSave(e.detail.element)")
|
|
})
|
|
|
|
})
|
|
}
|
|
})
|
|
|
|
function updateDatasetThenSave(element){
|
|
if (!library) return
|
|
let pos = element.getAttribute('position')
|
|
let title = element.getAttribute('value')
|
|
console.log(pos, title)
|
|
library.documents.filter( el => el.title == title ).map( found => found.customData[0].position = pos )
|
|
//library.documents.filter( el => el.title == title ).map( found => console.log( found.customData[0] ) )
|
|
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/test_newfot.json", JSON.stringify(library) )
|
|
setFeedbackHUD( "file saved" )
|
|
}
|
|
|
|
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 el = getClosestTargetElement( latest.getAttribute('position') )
|
|
if (el) interpretJXR( el.getAttribute("value") )
|
|
}
|
|
|
|
// should move to jxr-core.js
|
|
AFRAME.registerComponent('onemptypinch', { // changed from ondrop to be coherent with event name
|
|
init: function(){
|
|
AFRAME.scenes[0].addEventListener('enter-vr', e => {
|
|
console.log('entered vr')
|
|
document.querySelector("[cursor]").setAttribute("visible", "true")
|
|
document.querySelector("[camera]").setAttribute("cursor", "")
|
|
})
|
|
this.menuTargets = []
|
|
|
|
this.menuEl = document.createElement("a-box")
|
|
this.menuEl.setAttribute("width", ".01")
|
|
this.menuEl.setAttribute("height", ".01")
|
|
this.menuEl.setAttribute("depth", ".01")
|
|
//this.menuEl.setAttribute("scale", ".01 .01 .01") // breaking moving targets as children
|
|
this.menuEl.setAttribute("opacity", 0)
|
|
this.menuEl.setAttribute("color", "red")
|
|
this.menuEl.setAttribute("wireframe", "true")
|
|
this.menuEl.id = "handprimarymenu"
|
|
this.menuOpacity = 0
|
|
AFRAME.scenes[0].appendChild(this.menuEl)
|
|
|
|
let namedCSSColors = [ "aliceblue", "antiquewhite", "aqua", "aquamarine", "azure", "beige", "bisque", "black", "blanchedalmond", "blue", "blueviolet", "brown", "burlywood", "cadetblue", "chartreuse", "chocolate", "coral", "cornflowerblue", "cornsilk", "crimson", "cyan", "darkblue", "darkcyan", "darkgoldenrod", "darkgray", "darkgreen", "darkkhaki", "darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darkred", "darksalmon", "darkseagreen", "darkslateblue", "darkslategray", "darkturquoise", "darkviolet", "deeppink", "deepskyblue", "dimgray", "dodgerblue", "firebrick", "floralwhite", "forestgreen", "fuchsia", "gainsboro", "ghostwhite", "gold", "goldenrod", "gray", "green", "greenyellow", "honeydew", "hotpink", "indianred", "indigo", "ivory", "khaki", "lavender", "lavenderblush", "lawngreen", "lemonchiffon", "lightblue", "lightcoral", "lightcyan", "lightgoldenrodyellow", "lightgreen", "lightgrey", "lightpink", "lightsalmon", "lightseagreen", "lightskyblue", "lightslategray", "lightsteelblue", "lightyellow", "lime", "limegreen", "linen", "magenta", "maroon", "mediumaquamarine", "mediumblue", "mediumorchid", "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen", "mediumturquoise", "mediumvioletred", "midnightblue", "mintcream", "mistyrose", "moccasin", "navajowhite", "navy", "navyblue", "oldlace", "olive", "olivedrab", "orange", "orangered", "orchid", "palegoldenrod", "palegreen", "paleturquoise", "palevioletred", "papayawhip", "peachpuff", "peru", "pink", "plum", "powderblue", "purple", "red", "rosybrown", "royalblue", "saddlebrown", "salmon", "sandybrown", "seagreen", "seashell", "sienna", "silver", "skyblue", "slateblue", "slategray", "snow", "springgreen", "steelblue", "tan", "teal", "thistle", "tomato", "turquoise", "violet", "wheat", "white", "whitesmoke", "yellow", "yellowgreen"]
|
|
|
|
//let colors =["green", "blue", "orange", "yellow", "brown", "black"].map( (c,i) => {
|
|
let colors = namedCSSColors.slice(0,20).map( (c,i) => {
|
|
let newEl = document.createElement("a-box")
|
|
let x = (i/100+.01)
|
|
let z = -.1 // i%10
|
|
newEl.setAttribute("position", "" + x + " 0 " + z)
|
|
newEl.setAttribute("opacity", 0)
|
|
newEl.setAttribute("scale", ".01 .01 .01")
|
|
newEl.setAttribute("wireframe", "true")
|
|
newEl.setAttribute("color", c )
|
|
newEl.setAttribute("target", "true" ) // does not work as on same hand
|
|
// requires to enter a mode, i.e edit mode, that would let it be visible and movable
|
|
// seems to lose position while rotation does work
|
|
// wondering if truly empty, should be exclusive events, can't have emptypinch and pinched at the same time
|
|
// note that getClosestElements() filter out non visible element, but not when opacity is 0
|
|
// currently we can move now elements that are with opacity 0 yet visible
|
|
// maybe position works but scale is not taking into account the parent scale, only its position, so we move too little
|
|
// try to then stick to scale 1 for now then
|
|
this.menuEl.appendChild( newEl )
|
|
this.menuTargets.push( newEl )
|
|
})
|
|
this.menuTargets.push( this.menuEl )
|
|
|
|
},
|
|
// could support multi
|
|
events: {
|
|
emptypinch: function (e) {
|
|
this.menuEl.setAttribute("position", e.detail.position)
|
|
// works with AFRAME.scenes[0].emit('emptypinch', {position:"0 0 0"})
|
|
let code = this.el.getAttribute('onemptypinch')
|
|
// if multi, should also look for onreleased__ not just onreleased
|
|
try {
|
|
eval( code ) // should be jxr too e.g if (txt.match(prefix)) interpretJXR(txt)
|
|
} catch (error) {
|
|
console.error(`Evaluation failed with ${error}`);
|
|
}
|
|
},
|
|
emptypinchmoved: function (e) {
|
|
let foundElement = getClosestElement( e.detail.position, threshold=0.01, this.menuTargets )
|
|
this.menuTargets.map( mt => mt.setAttribute("wireframe", "true") )
|
|
if ( foundElement ) foundElement.setAttribute("wireframe", "false")
|
|
|
|
if (this.menuOpacity < 1) {
|
|
this.menuOpacity += .01
|
|
this.menuTargets.map( mt => mt.setAttribute("opacity", this.menuOpacity) )
|
|
}
|
|
let code = this.el.getAttribute('onemptypinchmoved')
|
|
// if multi, should also look for onreleased__ not just onreleased
|
|
try {
|
|
eval( code ) // should be jxr too e.g if (txt.match(prefix)) interpretJXR(txt)
|
|
} catch (error) {
|
|
console.error(`Evaluation failed with ${error}`);
|
|
}
|
|
},
|
|
emptypinchreleased: function (e) {
|
|
// could also check if above this.menuOpacity threshold, e.g not doing below .3 to avoid unexpected action
|
|
let foundElement = getClosestElement( e.detail.position, threshold=0.02, this.menuTargets )
|
|
// does not seem to execute anymore after teleporting, did work before though
|
|
if ( foundElement ){
|
|
document.getElementById("box").setAttribute("color", foundElement.getAttribute("color") )
|
|
}
|
|
|
|
this.menuOpacity = 0
|
|
this.menuEl.setAttribute("opacity", this.menuOpacity)
|
|
this.menuTargets.map( mt => mt.setAttribute("opacity", this.menuOpacity) )
|
|
let code = this.el.getAttribute('onemptypinchreleased')
|
|
// if multi, should also look for onreleased__ not just onreleased
|
|
try {
|
|
eval( code ) // should be jxr too e.g if (txt.match(prefix)) interpretJXR(txt)
|
|
} catch (error) {
|
|
console.error(`Evaluation failed with ${error}`);
|
|
}
|
|
},
|
|
}
|
|
})
|
|
|
|
function onHoveredTeleport(){
|
|
// iterate over targets
|
|
// see instead of teleportable https://aframe.io/docs/1.5.0/components/cursor.html#configuring-the-cursor-through-the-raycaster-component
|
|
Array.from( document.querySelectorAll("[teleporter]") ).map( target => {
|
|
if ( target.states.includes( "cursor-hovered" ) ){
|
|
target.setAttribute("material", "color", "magenta") // visited
|
|
document.getElementById('rig').setAttribute('position', target.getAttribute("position") )
|
|
}
|
|
})
|
|
}
|
|
|
|
AFRAME.registerComponent('teleporter', {
|
|
init: function(){
|
|
this.el.setAttribute("opacity", .5)
|
|
},
|
|
events: {
|
|
mouseenter: function (e) { this.el.setAttribute("opacity", .8) },
|
|
mouseleave: function (e) { this.el.setAttribute("opacity", .5) },
|
|
click: function (e) { document.getElementById('rig').setAttribute('position', this.el.getAttribute("position") ) }
|
|
// makes it compatible with mouse on desktop ... but also somehow enable the wrist shortcut?!
|
|
}
|
|
});
|
|
|
|
</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 library-load onemptypinch="onHoveredTeleport()" >
|
|
<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-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 cursor position="0 0 -1"
|
|
geometry="primitive: ring; radiusInner: 0.005; radiusOuter: 0.01"
|
|
material="color: black; shader: flat; opacity:.05;"
|
|
></a-entity>
|
|
</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-console position="2 2 0" rotation="0 -45 0" font-size="34" height=1 skip-intro=true></a-console>
|
|
</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"
|
|
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 0.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 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-troika-text anchor=left value="jxr makeAnchorsVisibleOnTargets()" target position=" -0.3 1.20 0" rotation="0 40 0" scale="0.1 0.1 0.1"></a-troika-text>
|
|
|
|
<a-troika-text anchor=left value="forceXaxis toggling"
|
|
onreleased="console.log('run on released');forceXaxis=!forceXaxis"
|
|
onpicked="console.log('run on picked');forceXaxis=!forceXaxis"
|
|
target position=" -0.3 1.45 0" rotation="0 40 0" scale="0.1 0.1 0.1"></a-troika-text>
|
|
|
|
<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-box teleporter height=".1" class="teleportable" material="color: cyan" position="3.5 0 -3.5" ></a-box>
|
|
<a-box teleporter height=".1" class="teleportable" material="color: cyan" position="-4 0 4" ></a-box>
|
|
<a-box teleporter height=".1" class="teleportable" material="color: cyan" position="3 3 4" >
|
|
<a-troika-text anchor=left value="jxr location.reload()" target position="0 1.30 -.5" rotation="0 0 0" scale="0.1 0.1 0.1"></a-troika-text>
|
|
<!-- works to execute but not to move, should either reparent or take into account the parent offset while moving -->
|
|
<!-- see pinchmoved in primary pinch in jxr-core.js as potential solution -->
|
|
</a-box>
|
|
<a-box teleporter height=".1" class="teleportable" material="color: cyan" position="0 0 0" ></a-box>
|
|
|
|
<a-troika-text id="spatial-introspection-test" anchor=left value="console.log('executing from secondary pinch');"
|
|
onreleased="console.log('run on released')"
|
|
onpicked="console.log('run on picked')"
|
|
target position=" -0.3 1.35 0" rotation="0 40 0" scale="0.1 0.1 0.1">
|
|
</a-troika-text>
|
|
|
|
<!-- should probably do so for the menu root too -->
|
|
<a-troika-text anchor=left value="jxr Array.from( document.getElementById('handprimarymenu').children ).map( el => el.setAttribute('opacity', 0))"
|
|
target position=" -0.3 1.53 0" rotation="0 40 0" scale="0.1 0.1 0.1">
|
|
</a-troika-text>
|
|
<a-troika-text anchor=left value="jxr Array.from( document.getElementById('handprimarymenu').children ).map( el => el.setAttribute('opacity', 1))"
|
|
target position=" -0.3 1.55 0" rotation="0 40 0" scale="0.1 0.1 0.1">
|
|
</a-troika-text>
|
|
|
|
</a-scene>
|
|
</body>
|
|
</script>
|
|
</html>
|
|
|