Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions webxr-aframe/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>A-Frame WebXR Elements Demo</title>
<script src="https://aframe.io/releases/1.5.0/aframe.min.js"></script>
<script src="https://unpkg.com/aframe-htmlembed-component@1.0.0/dist/build.js"></script>
<script src="https://unpkg.com/aframe-gesture-detector-component@3.2.2/dist/aframe-gesture-detector-component.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet" />
<link rel="stylesheet" href="styles.css" />
<script defer src="main.js"></script>
</head>
<body>
<a-scene renderer="antialias: true; colorManagement: true"
background="color: #000"
webxr="optionalFeatures: hit-test, hand-tracking, depth-sensing, local-floor"
embedded>
<a-assets>
<a-asset-item id="gltf-model" src="https://cdn.aframe.io/test-models/models/box/box.gltf"></a-asset-item>
<video id="stream" autoplay muted playsinline></video>
</a-assets>

<a-entity id="cameraRig">
<a-entity id="camera" camera look-controls
cursor="fuse:true; fuseTimeout:1500; rayOrigin:mouse"
raycaster="objects: .interactive"></a-entity>
</a-entity>

<!-- Flat spatial panel -->
<a-entity id="mainPanel"
class="panel interactive"
position="0 1.5 -2"
htmlembed="ppu:512"
panel>
<div class="embed">
<h1>Spatial Panel</h1>
<p>Glassmorphic panel with embedded HTML.</p>
<button class="ui-btn" id="dialogBtn">Open Dialog</button>
</div>
</a-entity>

<!-- Curved spatial panel -->
<a-entity id="curvedPanel"
class="panel interactive"
position="1.5 1.5 -2"
htmlembed="ppu:512"
panel="curve:0.5; radius:1">
<div class="embed">
<h1>Curved Panel</h1>
</div>
</a-entity>

<!-- Orbiting element -->
<a-entity id="orbiter"
class="orbiter interactive"
geometry="primitive: sphere; radius: 0.05"
material="color: #00A8E8; opacity: 0.8"
position="0 0.2 -2"
orbiter="radius:0.3; speed:45">
</a-entity>

<!-- Dialog -->
<a-entity id="dialog"
class="panel interactive hidden"
position="0 1.8 -1.5"
htmlembed="ppu:512"
elevate-on-enter>
<div class="embed">
<h2>Dialog Title</h2>
<p>Dialog content here.</p>
<button class="ui-btn" id="closeDialog">Close</button>
</div>
</a-entity>

<!-- Button -->
<a-entity id="playButton"
class="panel button interactive"
position="-0.6 1.2 -1.5"
htmlembed="ppu:256"
button-actions>
<div class="embed">
<span class="material-symbols-outlined">play_arrow</span>
</div>
</a-entity>

<!-- 3D model -->
<a-entity id="model"
class="interactive"
position="0 1 -1"
gltf-model="#gltf-model"
gesture-handler
annotation="text:3D Model">
</a-entity>

<!-- Video panel -->
<a-entity id="videoPanel"
class="panel interactive"
position="-1.5 1.5 -2"
geometry="primitive: plane; height:1; width:1.5"
material="shader: flat; src: #stream">
</a-entity>

<!-- Safe zone -->
<a-entity safe-zone="radius:1.5"></a-entity>

<!-- Hands -->
<a-entity hand-tracking-controls="hand: left" pointer-feedback></a-entity>
<a-entity hand-tracking-controls="hand: right" pointer-feedback></a-entity>

<!-- Text label -->
<a-entity id="label"
position="0 2 -1.5"
text="value:XR UI; align:center; width:4"
auto-scale-text></a-entity>

<!-- Lights -->
<a-entity light="type: ambient; intensity: 0.5"></a-entity>
<a-entity light="type: point; intensity: 1" position="0 2 -1"></a-entity>

<!-- Sky -->
<a-sky color="#1a1a1a"></a-sky>
</a-scene>
</body>
</html>
208 changes: 208 additions & 0 deletions webxr-aframe/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
// Panel component for flat or curved panels
AFRAME.registerComponent('panel', {
schema: {
curve: {type: 'number', default: 0}, // 0 for flat, fraction of 180deg for curved
radius: {type: 'number', default: 1},
width: {type: 'number', default: 1.5},
height: {type: 'number', default: 1}
},
update: function () {
if (this.data.curve > 0) {
this.el.setAttribute('geometry', {
primitive: 'cylinder',
radius: this.data.radius,
height: this.data.height,
openEnded: true,
thetaLength: this.data.curve * 180,
thetaStart: -this.data.curve * 90
});
} else {
this.el.setAttribute('geometry', {
primitive: 'plane',
width: this.data.width,
height: this.data.height
});
}
}
});

// Orbiter component for rotating elements
AFRAME.registerComponent('orbiter', {
schema: { radius: {default: 0.3}, speed: {default: 45} }, // speed in deg/s
tick: function (time) {
var angle = THREE.Math.degToRad((time / 1000) * this.data.speed);
var x = Math.cos(angle) * this.data.radius;
var z = Math.sin(angle) * this.data.radius - 2; // orbit around panel at z=-2
this.el.object3D.position.set(x, 0.2, z);
}
});

// Elevation animation for dialogs/popups
AFRAME.registerComponent('elevate-on-enter', {
schema: { from: {default: -0.1}, to: {default: 0.3}, dur: {default: 300} },
init: function () {
var pos = this.el.object3D.position;
this.startZ = pos.z + this.data.from;
pos.z = this.startZ;
this.el.setAttribute('visible', false);
this.el.addEventListener('show', () => {
this.el.setAttribute('visible', true);
this.el.setAttribute('animation__elevate', {
property: 'position',
to: `${pos.x} ${pos.y} ${this.data.to}`,
dur: this.data.dur,
easing: 'easeOutQuad'
});
});
this.el.addEventListener('hide', () => {
this.el.setAttribute('animation__elevate', {
property: 'position',
to: `${pos.x} ${pos.y} ${this.startZ}`,
dur: this.data.dur,
easing: 'easeInQuad'
});
setTimeout(() => this.el.setAttribute('visible', false), this.data.dur);
});
}
});

// Interactive button states
AFRAME.registerComponent('button-actions', {
init: function () {
this.el.addEventListener('raycaster-intersected', () => this.el.addState('hovered'));
this.el.addEventListener('raycaster-intersected-cleared', () => {
this.el.removeState('hovered');
this.el.removeState('pressed');
});
this.el.addEventListener('mousedown', () => this.el.addState('pressed'));
this.el.addEventListener('mouseup', () => this.el.removeState('pressed'));
},
tick: function () {
var scale = this.el.is('pressed') ? 0.95 : this.el.is('hovered') ? 1.1 : 1;
this.el.object3D.scale.set(scale, scale, scale);
}
});

// Auto-scale text based on camera distance
AFRAME.registerComponent('auto-scale-text', {
schema: { factor: {default: 0.5} },
tick: function () {
var cam = this.el.sceneEl.camera;
if (!cam) return;
var pos = new THREE.Vector3();
var camPos = new THREE.Vector3();
this.el.object3D.getWorldPosition(pos);
cam.getWorldPosition(camPos);
var dist = pos.distanceTo(camPos);
var s = dist * this.data.factor;
this.el.object3D.scale.set(s, s, s);
}
});

// Gesture handler for models
AFRAME.registerComponent('gesture-handler', {
schema: { min: {default: 0.5}, max: {default: 3} },
init: function () {
this.initialScale = this.el.object3D.scale.clone();
this.el.sceneEl.addEventListener('onefingermove', e => {
this.el.object3D.position.add(e.detail.positionChange);
});
this.el.sceneEl.addEventListener('twofingermove', e => {
var current = this.el.object3D.scale.x;
var newScale = THREE.Math.clamp(current + e.detail.spreadChange / 400, this.data.min, this.data.max);
this.el.object3D.scale.set(newScale, newScale, newScale);
});
}
});

// Annotation component
AFRAME.registerComponent('annotation', {
schema: { text: {type: 'string'} },
init: function () {
var ann = document.createElement('a-entity');
ann.setAttribute('position', '0 0.5 0');
ann.setAttribute('htmlembed', 'ppu:256');
ann.innerHTML = `<div class="embed annotation">${this.data.text}</div>`;
this.el.appendChild(ann);
}
});

// Pointer feedback on hands/controllers
AFRAME.registerComponent('pointer-feedback', {
init: function () {
var ring = document.createElement('a-ring');
ring.setAttribute('radius-inner', '0.0075');
ring.setAttribute('radius-outer', '0.01');
ring.setAttribute('color', '#00A8E8');
ring.setAttribute('material', 'shader:flat; opacity:0.8');
ring.setAttribute('position', '0 0 -0.05');
this.el.appendChild(ring);
this.ring = ring;
this.el.addEventListener('triggerdown', () => ring.setAttribute('color', '#fff'));
this.el.addEventListener('triggerup', () => ring.setAttribute('color', '#00A8E8'));
}
});

// Safe zone visual
AFRAME.registerComponent('safe-zone', {
schema: { radius: {default: 1.5} },
init: function () {
var r = this.data.radius;
var ring = document.createElement('a-ring');
ring.setAttribute('radius-inner', r - 0.005);
ring.setAttribute('radius-outer', r + 0.005);
ring.setAttribute('rotation', '-90 0 0');
ring.setAttribute('material', 'color:#00A8E8; opacity:0.25; side:double; wireframe:true');
this.el.appendChild(ring);
}
});

// Voice command helper
AFRAME.registerComponent('voice-command', {
init: function () {
var SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SpeechRecognition) return;
var rec = new SpeechRecognition();
rec.continuous = true;
rec.lang = 'en-US';
rec.onresult = e => {
var last = e.results[e.results.length - 1][0].transcript.trim().toLowerCase();
this.el.emit('voice', {text: last});
};
rec.start();
}
});

// Start local camera stream for video panel
async function startStream() {
var video = document.getElementById('stream');
if (!navigator.mediaDevices) return;
try {
var media = await navigator.mediaDevices.getUserMedia({video: true, audio: false});
video.srcObject = media;
} catch (e) {
console.warn('Stream failed', e);
}
}

// Initialize interactions after DOM is ready
document.addEventListener('DOMContentLoaded', function () {
startStream();
var scene = document.querySelector('a-scene');
scene.setAttribute('voice-command', '');
var dialog = document.getElementById('dialog');
var openBtn = document.getElementById('dialogBtn');
var closeBtn = document.getElementById('closeDialog');
openBtn.addEventListener('click', function () {
dialog.classList.remove('hidden');
dialog.emit('show');
});
closeBtn.addEventListener('click', function () {
dialog.emit('hide');
dialog.classList.add('hidden');
});
scene.addEventListener('voice', function (e) {
if (e.detail.text.includes('open dialog')) openBtn.click();
if (e.detail.text.includes('close dialog')) closeBtn.click();
});
});
61 changes: 61 additions & 0 deletions webxr-aframe/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
body {
margin: 0;
font-family: system-ui, sans-serif;
background: transparent;
}

.embed {
width: 512px;
color: #fff;
font-size: 1.2rem;
line-height: 1.5;
}

.panel {
background: rgba(30,30,30,0.8);
border-radius: 12px;
padding: 16px;
backdrop-filter: blur(10px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}

.ui-btn {
background: rgba(255,255,255,0.1);
border: 1px solid rgba(255,255,255,0.2);
color: #fff;
padding: 0.5rem 1rem;
border-radius: 8px;
cursor: pointer;
transition: background 0.2s, transform 0.2s, box-shadow 0.2s;
}

.ui-btn:hover {
background: rgba(255,255,255,0.2);
box-shadow: 0 0 8px #00A8E8;
}

.ui-btn:active {
background: #00A8E8;
color: #1e1e1e;
}

.button .material-symbols-outlined {
font-size: 48px;
color: #fff;
transition: color 0.2s, text-shadow 0.2s;
}

.button:hover .material-symbols-outlined {
color: #00A8E8;
text-shadow: 0 0 8px #00A8E8;
}

.hidden {
display: none;
}

.annotation {
padding: 4px 8px;
background: rgba(30,30,30,0.8);
border-radius: 8px;
}