jarvis-cloud / webar /index.html
Jarvis2345's picture
deploy(S4): Blender headless pipeline + WebAR client + backend fixes (part 2)
4ec6735 verified
Raw
History Blame Contribute Delete
16.5 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, user-scalable=no">
<meta name="description" content="JARVIS real 8th Wall WebAR runtime for OMEGA">
<meta name="theme-color" content="#05070b">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>JARVIS Real AR</title>
<link rel="manifest" href="./assets/manifest-8qualO3q.json">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
<!--
BUG 33 FIX — Explicit compositor layer z-order applied before any JS runs.
camerafeed (XR8/fallback camera canvas) → z-index: 0 (base layer)
css3d-layer (CSS3D AR overlays) → z-index: 1
hand-overlay (MediaPipe hand canvas) → z-index: 2
world-label-layer → z-index: 3
ui-layer (AR panels, HUD) → z-index: 10
loading-screen → z-index: 100
error-host / banner-host → z-index: 200
-->
<style id="jarvis-compositor-layer-order">
#camerafeed {
position: fixed;
inset: 0;
width: 100vw;
height: 100vh;
z-index: 0;
display: block;
background: var(--bg-darker);
/* XR8 will resize the canvas pixel dimensions on xrloaded.
The CSS dimensions are locked here so no flash of 0x0. */
}
#css3d-layer {
position: fixed;
inset: 0;
width: 100vw;
height: 100vh;
z-index: 1;
pointer-events: none;
}
#hand-overlay {
position: fixed;
inset: 0;
width: 100vw;
height: 100vh;
z-index: 2;
pointer-events: none;
}
#world-label-layer {
position: fixed;
inset: 0;
z-index: 3;
pointer-events: none;
}
#ui-layer {
position: fixed;
inset: 0;
z-index: 10;
pointer-events: none;
}
#ui-layer > * { pointer-events: auto; }
#hud-toolbar { z-index: 11; }
#status-panel { z-index: 11; }
#jarvis-bubble { z-index: 11; }
#loading-screen { z-index: 100; }
/* Layout lives in hud.css (#notice-stack) — these two hosts are laid out as one
column there so notices can never overlap. Only the z-index and pointer-events
policy stay here, alongside the rest of the compositor's layer order. */
#notice-stack { z-index: 200; pointer-events: none; }
#error-host > *, #banner-host > * { pointer-events: auto; }
</style>
<!--
This product includes the XR Engine software developed by
Niantic Spatial, Inc.
Copyright © 2026 Niantic Spatial, Inc. All rights reserved.
License: https://github.com/8thwall/engine/blob/main/LICENSE
8th Wall transitioned to open source (MIT framework, closed SLAM binary) on
Feb 28 2026. The hosted CDN was retired. The binary is self-hosted under
external/xr/xr.js — download from https://8th.io/xrjs and place at that path.
-->
<!--
BUG 31 FIX — A-Frame JS loaded here so window.AFRAME is defined before
ar-init.js runs and <a-scene> can initialise normally.
Pinned at v1.6.0; update by changing the version in both src and the integrity hash.
-->
<!--
A-Frame is deliberately NOT loaded. History, so this isn't "fixed" back:
The tag used to point at aframe@1.6.0/dist/aframe.min.js, which 404s on
jsDelivr (the real filename is aframe-master.min.js). So `window.AFRAME` has
never been defined in production, and every page load paid for a failed
request. Its only consumer was the physics tick in ar-physics.js, which
silently no-op'd behind `if (window.AFRAME)` — physics never stepped once.
Correcting the URL was tried and measured in a real browser: A-Frame ships its
own copy of three.js, which produced "WARNING: Multiple instances of Three.js
being imported" plus a CONTEXT_LOST_WEBGL against the app's own renderer.
A-Frame is not the renderer here — <a-scene> below is display:none and the live
context comes from XR8 or the fallback THREE canvas (utils/scene-db.js
getSceneContext/ensureFallbackThree) — so loading it buys nothing and costs a
duplicate WebGL/three stack.
The physics tick now runs on the app's own managed RAF loop instead, so nothing
depends on A-Frame. The <a-scene> block below is inert without it, which is
exactly how production has always behaved.
-->
<!--
BUG 32 FIX — XR binary loaded statically so XR8 is always attempted even
when the page is served without a ?xr= URL parameter.
Since the 8th Wall hosted CDN was retired (Feb 2026) the binary must be
self-hosted. Place the extracted xr-standalone.zip contents at:
phone/eightwall_ar/external/xr/xr.js
Download: https://8th.io/xrjs
The onerror handler fires when the file is absent and falls through
gracefully — ar-init.js will try the ?xr= param next, then WebXR fallback,
then basic camera. No hard failure.
-->
<script id="jarvis-xr-static"
src="./external/xr/xr.js"
async
data-preload-chunks="slam"
data-jarvis-xr-static="true"
onerror="document.getElementById('jarvis-xr-static').dataset.loadFailed='true'"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script>
<script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/3.5.0/model-viewer.min.js" crossorigin="anonymous"></script>
<script type="module" crossorigin src="./assets/index-CyALIax6.js"></script>
<link rel="modulepreload" crossorigin href="./assets/mediapipe-Ck-u_eH7.js">
<link rel="stylesheet" crossorigin href="./assets/index-hHPIeHhK.css">
</head>
<body>
<canvas id="camerafeed" aria-label="Live rear camera AR feed"></canvas>
<a-scene id="ar-scene"
xr-mode-ui="enabled: false"
renderer="alpha: true; antialias: true; colorManagement: true; physicallyCorrectLights: true; logarithmicDepthBuffer: true"
background="color: transparent"
embedded
vr-mode-ui="enabled: false"
style="display:none !important">
<a-entity id="camera-rig">
<a-camera
id="main-camera"
position="0 0 0"
near="0.01"
far="1000"
fov="60"
look-controls="enabled: false"
wasd-controls="enabled: false"></a-camera>
</a-entity>
<a-light type="ambient" color="#ffffff" intensity="0.6"></a-light>
<a-light type="directional" color="#ffffff" intensity="0.8" position="1 2 1" cast-shadow="true"></a-light>
<a-assets id="ar-assets" timeout="30000"></a-assets>
</a-scene>
<div id="css3d-layer" aria-hidden="true"></div>
<canvas id="hand-overlay" aria-label="Hand tracking overlay"></canvas>
<div id="world-label-layer" aria-live="polite"></div>
<div id="ui-layer" aria-live="polite"></div>
<div id="loading-screen" class="loading-screen">
<div class="loading-card glass-panel">
<div class="loading-ring" aria-hidden="true"></div>
<p class="caption">JARVIS REAL AR</p>
<h1>Spatial Runtime</h1>
<p id="loading-text">Preparing secure AR runtime...</p>
<div class="progress-track"><span id="loading-progress"></span></div>
<!-- Stated before the browser's permission prompt appears, not after. An unexplained
camera request is the highest-drop-off moment in any WebAR product, and this one is
asking a subscriber to point a camera around their home. -->
<div class="gate-consent">
<svg class="gate-consent-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2.8 7.8h11.4a2 2 0 0 1 2 2v6.4a2 2 0 0 1-2 2H2.8a2 2 0 0 1-2-2V9.8a2 2 0 0 1 2-2z"/><path d="m16.2 12.2 5-3v7.6l-5-3z"/></svg>
<p>Next you'll be asked for camera access. Video is processed on your device and is never uploaded or stored.</p>
</div>
<button id="start-ar" class="btn-primary" disabled>PREPARING...</button>
<p class="gate-foot">Requires a rear camera &middot; works best standing</p>
</div>
</div>
<!--
Both notice hosts share ONE stacking column so they can never overlap.
They used to be two independently positioned fixed elements at hardcoded
top offsets (78px and 152px). That only holds while an error card stays
under 74px tall — and a real one measured 112px in a live session, so the
fallback-tracking error and the wake-word banner drew on top of each other
and neither was fully readable. The offsets stay separate elements for the
two distinct aria-live politeness levels; only the layout is shared.
-->
<div id="notice-stack">
<aside id="error-host" aria-live="assertive"></aside>
<aside id="banner-host" aria-live="polite"></aside>
</div>
<nav id="hud-toolbar" class="hud-toolbar glass-panel" aria-label="AR controls">
<!-- Mode selector, per the Claude Design `hud/mode-toolbar` component. These six were bare
characters (M S G B VR J) whose only real label lived in title/aria-label. A phone held at
arm's length has no hover, so a tooltip-only label is one the user can never read — the
word is now permanent under each icon, and the glyph carries the meaning. The decorative
svg is aria-hidden so a screen reader announces each mode once, not twice. -->
<button class="btn-symbol selected" data-action="models" title="Models" aria-label="Models" aria-pressed="true">
<span class="icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2.6 21 7v10l-9 4.4L3 17V7z"/><path d="M3 7l9 4.4L21 7M12 11.4V21.4"/></svg></span>
<span class="btn-label">Models</span>
</button>
<button class="btn-symbol" data-action="scan" title="Scan room" aria-label="Scan room" aria-pressed="false">
<span class="icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8V5.2A2.2 2.2 0 0 1 5.2 3H8M16 3h2.8A2.2 2.2 0 0 1 21 5.2V8M21 16v2.8a2.2 2.2 0 0 1-2.2 2.2H16M8 21H5.2A2.2 2.2 0 0 1 3 18.8V16"/><path d="M3 12h18" opacity=".9"/></svg></span>
<span class="btn-label">Scan</span>
</button>
<button class="btn-symbol" data-action="games" title="Games" aria-label="Games" aria-pressed="false">
<span class="icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="2.4" y="7.4" width="19.2" height="9.2" rx="4.4"/><path d="M7 10.6v2.8M8.4 12H5.6M15.6 11.4h.01M17.9 13.2h.01"/></svg></span>
<span class="btn-label">Games</span>
</button>
<button class="btn-symbol" data-action="builder" title="Builder" aria-label="Builder" aria-pressed="false">
<span class="icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a3.6 3.6 0 0 0 4.8 4.7l2 2-4.5 4.5-2-2a3.6 3.6 0 0 0-4.7-4.8z"/><path d="M9.4 14.6 3.6 20.4"/></svg></span>
<span class="btn-label">Builder</span>
</button>
<button class="btn-symbol" data-action="vr" title="VR screen" aria-label="VR screen" aria-pressed="false">
<span class="icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="2.2" y="7.6" width="19.6" height="8.8" rx="3"/><path d="M9.6 16.4c-.5-1.7-1-2.5-2.2-2.5s-2 .9-2 2.2M14.4 16.4c.5-1.7 1-2.5 2.2-2.5s2 .9 2 2.2"/></svg></span>
<span class="btn-label">VR</span>
</button>
<button class="btn-symbol" data-action="media" title="Media" aria-label="Local media viewer" aria-pressed="false">
<span class="icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="2.4" y="4.6" width="19.2" height="13" rx="2.4"/><path d="M10.2 9.2 14.6 11.1 10.2 13z"/><path d="M7 21h10"/></svg></span>
<span class="btn-label">Media</span>
</button>
<button class="btn-symbol" data-action="jarvis" title="JARVIS" aria-label="JARVIS" aria-pressed="false">
<span class="icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3.1"/><circle cx="12" cy="12" r="8.4" opacity=".55"/><path d="M12 1.2v2.4M12 20.4v2.4M22.8 12h-2.4M3.6 12H1.2"/></svg></span>
<span class="btn-label">JARVIS</span>
</button>
</nav>
<section id="status-panel" class="status-panel glass-panel">
<div>
<span>Camera</span>
<strong id="status-camera">standby</strong>
</div>
<div>
<span>Tracking</span>
<strong id="status-tracking">waiting</strong>
</div>
<div>
<span>OMEGA</span>
<strong id="status-omega">offline</strong>
</div>
</section>
<!-- Part 38 Claude Design pass: webar/assistant-bubble.html.
data-state is driven from jarvis/assistant-state.js, which is itself driven by the
REAL SpeechSynthesis utterance lifecycle — so the indicator cannot outlive the thing
it indicates. The glyph is four <i> bars restyled per state (meter / orbit / wave /
still), not an emoji: emoji render differently on every device and a screen reader
announces whatever that device's font happens to call the codepoint. -->
<section id="jarvis-bubble" class="jarvis-bubble glass-panel hidden" aria-live="polite" data-state="idle">
<button type="button" id="jarvis-bubble-close" class="bubble-x" aria-label="Dismiss">&times;</button>
<span><span class="state-glyph" aria-hidden="true"><i></i><i></i><i></i><i></i></span><span id="jarvis-bubble-label">JARVIS</span></span>
<p id="jarvis-bubble-text"></p>
</section>
<template id="panel-template">
<section class="ar-panel glass-panel">
<!-- Part 41 Claude Design pass: webar/info-model-panel.html. This button was EMPTY —
a 52px solid red circle with nothing in it, reading as a warning dot rather than a
control, and sitting top-left while minimise sat top-right. It now carries a real
glyph and moves into the header beside minimise. -->
<button class="panel-close" aria-label="Close panel"><span aria-hidden="true">×</span></button>
<div class="panel-drag-handle" aria-hidden="true"></div>
<header>
<h2></h2>
<button class="panel-minimize btn-symbol" aria-label="Minimize"><span class="icon">-</span></button>
</header>
<div class="panel-body"></div>
<div class="ornament"></div>
<div class="panel-resize-handle" aria-hidden="true"></div>
</section>
</template>
<!-- S4: access gate loads first; it imports ar-init.js only after the
cloud access key is validated server-side. Keeps the Space public
while locking the app behind a real backend-enforced password. -->
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
const scope = new URL('.', window.location.href).pathname
navigator.serviceWorker.register('service-worker.js', { scope })
.catch(error => {
window.dispatchEvent(new CustomEvent('jarvis-ar-service-worker-error', {
detail: { message: error.message || 'Service worker registration failed' },
}))
})
})
}
</script>
</body>
</html>