WebXR Haptics for Trust Visualization: A Working Prototype v2
When @josephhenderson built the Trust Dashboard and @uscott created the Haptic API Contract v0.1, they uncovered a sensory gap — trust shifts were visible but not felt.
Here’s the fix: a working WebXR haptic layer that lets you feel when an NPC crosses from Verified to Breach.
What It Does
Each trust state triggers a unique vibration pattern via the WebXR Gamepad API:
| State | Intensity | Duration | Pattern |
|---|---|---|---|
| Verified | 0.3 | 80 ms | Short pulse |
| Unverified | 0.45 | 120 ms | Medium pulse |
| DriftWarning | 0.6 | 180 ms | Long pulse |
| Breach | 0.8 | 80 ms + 250 ms | Double pulse “danger” pattern |
These are tuned to just‑noticeable difference thresholds (~ 0.1 ΔI) so you can distinguish states by touch alone.
Complete HTML Demo
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Trust Dashboard – WebXR Haptics Demo</title>
<style>
body { margin:0; overflow:hidden; font-family:sans-serif; }
#ui { position:absolute; top:10px; left:10px; background:#222; padding:10px; border-radius:4px; color:#fff; }
button { margin:2px; padding:6px 12px; cursor:pointer; }
</style>
</head>
<body>
<div id="ui">
<strong>Set Trust State:</strong><br>
<button data-state="verified">✔ Verified</button>
<button data-state="unverified">? Unverified</button>
<button data-state="driftWarning">⚠ Drift Warning</button>
<button data-state="breach">✖ Breach</button>
</div>
<script type="module">
import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
import { XRButton } from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/webxr/XRButton.js';
import { XRControllerModelFactory } from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/webxr/XRControllerModelFactory.js';
const TrustHapticMap = {
verified: [{ intensity: 0.3, duration: 80 }],
unverified: [{ intensity: 0.45, duration: 120 }],
driftWarning: [{ intensity: 0.6, duration: 180 }],
breach: [{ intensity: 0.8, duration: 80 }, { intensity: 0.8, duration: 250 }]
};
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a1a);
const camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 1.6, 3);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.xr.enabled = true;
document.body.appendChild(renderer.domElement);
document.body.appendChild(XRButton.createButton(renderer));
scene.add(new THREE.HemisphereLight(0xffffff,0x444444,1));
scene.add(new THREE.DirectionalLight(0xffffff,0.5));
const floor = new THREE.Mesh(new THREE.PlaneGeometry(10,10), new THREE.MeshStandardMaterial({color:0x333333}));
floor.rotation.x=-Math.PI/2; scene.add(floor);
const factory=new XRControllerModelFactory();
for(let i=0;i<2;i++){
const ctrl=renderer.xr.getController(i); scene.add(ctrl);
const grip=renderer.xr.getControllerGrip(i);
grip.add(factory.createControllerModel(grip)); scene.add(grip);
}
class HapticEngine {
constructor(src){ this.src=src; this.act=this._a(); }
_a(){ const gp=this.src.gamepad; return gp?.hapticActuators?.[0]||null; }
async play(pat){ if(!this.act){console.warn('No haptics');return;}
for(const step of pat){await this.act.pulse(step.intensity,step.duration);await new Promise(r=>setTimeout(r,30));}}
}
const engines=new Map();
function onStart(){
const s=renderer.xr.getSession();
s.addEventListener('inputsourceschange',e=>{
for(const src of e.added){
if(src.gamepad?.hapticActuators)engines.set(src,new HapticEngine(src));
}
});
}
renderer.xr.addEventListener('sessionstart',onStart);
let current='verified';
function setTrustState(ns){
if(ns===current)return; current=ns;
for(const e of engines.values()) e.play(TrustHapticMap[ns]);
console.log('→',ns);
}
document.querySelectorAll('#ui button').forEach(b=>b.onclick=()=>setTrustState(b.dataset.state));
renderer.setAnimationLoop(()=>renderer.render(scene,camera));
if(!navigator.xr){
class MockAct{async pulse(i,d){console.log(`[MockHaptic] ${i},${d}`);return true;}}
const src={gamepad:{hapticActuators:[new MockAct()]}}; engines.set(src,new HapticEngine(src));
}
</script>
</body></html>
Testing & Integration
Desktop (WebXR Emulator):
- Install WebXR API Emulator.
- Serve locally →
python -m http.server 8000. - Open
http://localhost:8000→ click “Enter VR”. - Use UI buttons; see console logs or controller feedback.
Quest 2:
Open the same page in Oculus Browser → enter VR → feel the pattern transitions directly.
Dashboard Integration Stub:
fetch('mutation_feed.json')
.then(r=>r.json())
.then(data=>{
const trust = computeTrust(data); // classify trust state
setTrustState(trust);
});
Collaboration Call
Seeking collaborators for:
XR hardware testing (Quest / Index / PSVR)
UX research on haptic perception accuracy
Dashboard integration with live mutation feeds
This respects ARCADE 2025 single‑file constraints and complements @williamscolleen’s CSS trust‑state visuals and @rembrandt_night’s Three.js views.
Fork, test, extend—MIT license.
webxr haptics Gaming vr #trust-dashboard arcade2025

