1<!DOCTYPE html> 2<html lang="en"> 3<head> 4 <meta charset="utf-8" /> 5 <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" /> 6 <meta name="apple-mobile-web-app-capable" content="yes" /> 7 <!-- Pattern from the 8th Wall export's generated index.html ("TWCFI Antoinette"). 8 overlay-ui.js overwrites this with the storyteller and take, e.g. "TWCFI Marina 2", 9 so nine tabs and nine history entries are distinguishable. This is the pre-script 10 value and is the same on all nine pages. --> 11 <title>TWCFI Storytellers</title> 12 13 <!-- WHICH OF THE NINE EXPERIENCES THIS PAGE IS, as {storyteller}-{take}. 14 *** THIS LINE IS THE ONLY DIFFERENCE BETWEEN THE NINE EXPERIENCE PAGES. *** 15 Everything else that varies -- the scan URL, the image target name, the title, the 16 intro heading, the nav bar, the marker guide, the narration -- is written from 17 experiences.js at runtime by ar-boot.js and overlay-ui.js. Keep it that way: nine 18 hand-maintained copies of the same markup is nine chances for them to drift, and the 19 drift is silent. CLAUDE.md carries a one-line diff check for exactly this. 20 21 MUST come before experiences.js, and experiences.js MUST come before ar-boot.js and 22 hologram-4ds.js, which both read the resolved config at load time. --> 23
23<script>window.EXPERIENCE_ID = 'tom-3'</script>
23 24
24<script src="experiences.js"></script>
24 25 26 <!-- Load order matters and everything is synchronous so it executes before 27 <a-scene> is parsed: 28 1. 8frame (A-Frame 1.3.0, the fork the XR engine was built against; 29 defines AFRAME + window.THREE at three.js r137) 30 2. 8th Wall engine (XR8; registers xrweb/xrconfig once its async boot finishes) 31 3. three.js compat (aliases BufferGeometry.addAttribute for 4DViews) 32 4-5. 4DViews player (Model4D / ResourceManagerXHR / Decoder4D globals) 33 6. hologram-4ds.js (registers the <hologram-4ds> primitive) 34 7. world-anchor.js (registers the world-anchor component) 35 36 xr.js is deliberately NOT async: the export shipped it that way, which races 37 A-Frame, and if xr.js wins then window.AFRAME is undefined when it tries to 38 register xrweb and the component never exists. data-preload-chunks="slam" pulls in 39 xr-slam.js (SLAM + image targets) during the intro gate; the face chunk is not 40 shipped. xr.js and xr-slam.js must stay byte-identical and adjacent to each other: 41 the engine resolves the chunk and its resources relative to its own script src, and 42 the licence requires the binary be distributed unmodified. 43 44 lib/web4dvResource.js MUST keep that exact filename and stay a classic script: it 45 derives the WASM codec path from 46 document.querySelector('script[src*="web4dvResource.js"]').src, and a rename, a 47 module or a bundle all yield null and silently break the decode worker. --> 48
48<script src="lib/8frame-1.3.0.min.js"></script>
48 49
49<script src="lib/xr/xr.js" data-preload-chunks="slam"></script>
49 50
50<script src="lib/4dviews-three-compat.js"></script>
50 51
51<script src="lib/model4D_Three.js"></script>
51 52
52<script src="lib/web4dvResource.js"></script>
52 53
53<script src="hologram-4ds.js"></script>
53 54
54<script src="world-anchor.js"></script>
54 55 56 <!-- Fills in every per-page part of the overlay (the nav bar's takes, the marker guide 57 image, the narration's src, the intro heading and the document title) from 58 experiences.js, so the nine pages can stay identical to each other. Must come after 59 experiences.js; order relative to the engine scripts does not matter, it only touches 60 plain DOM. --> 61
61<script src="overlay-ui.js" defer></script>
61 62 63 <link rel="stylesheet" href="index.css" /> 64</head> 65<body> 66 <!-- Intro / Continue gate. Shown immediately on load; the Continue tap is the 67 single user gesture that unlocks audio playback. 68 69 This gate stays on the experience page rather than moving to the menu, even though 70 tapping a menu card is itself a gesture: navigating creates a NEW document whose 71 WebAudio context starts suspended again, and iOS motion permission does not carry 72 across either. So the card tap is the choice and this tap is the unlock. --> 73 <div id="intro-overlay"> 74 <div id="intro-content"> 75 <!-- This gate is here for the TAP, not for the words, and it deliberately carries no 76 instructions. "Point camera at image target." lives on #scan-prompt over the 77 camera feed, which is where the originals put it. Repeating it here made this read as a second 78 instruction screen saying the same thing, which is what the client reported. 79 80 The tap itself cannot go. It does three things that all have to happen on THIS 81 document, inside a real user gesture: resume the 4DViews WebAudio context so the 82 narration can start on its own, call DeviceMotionEvent.requestPermission() for 83 iOS 13+, and emit runreality to start the camera. Tapping a menu card or a nav 84 thumbnail is a gesture on the PREVIOUS document and carries across to none of 85 them. See "the Continue gate" in ar-boot.js. --> 86 <!-- Filled by overlay-ui.js from the storyteller's label. The bare name, without the 87 take number, which is how the originals had it: the visitor chose the number one 88 screen ago and this gate is here for the tap rather than for the words. --> 89 <h1 id="intro-title"></h1> 90 <!-- The client's own introduction to this scan, from 91 their StorytellersNewInterface.pdf, which is nine screenshots of this very gate 92 with their copy set into it. River and place are the storyteller's; the description 93 is their sentence for THIS take. All three are filled by overlay-ui.js from 94 experiences.js (see the INTRO COPY block there) rather than being written here, 95 because nine hand-maintained copies of a paragraph is nine chances to drift. 96 97 Two <span>s rather than a <br>: each line is set with textContent, so no part of 98 the client's copy is ever round-tripped through innerHTML. --> 99 <p id="intro-location"> 100 <span id="intro-river"></span> 101 <span id="intro-place"></span> 102 </p> 103 <p id="intro-description"></p> 104 <!-- The one instruction the gate does carry, and the only one that earns its place: 105 tapping Continue opens the browser's camera permission prompt, and nothing else in 106 the build warns anyone that it is coming. The rule this looks like it breaks (see 107 "the two-tap flow" in CLAUDE.md) is about repeating what the camera feed is about to 108 say a moment later on #scan-prompt; the feed never mentions 109 permission, and #camera-error-overlay only speaks up once access has ALREADY been 110 refused. So this says something new, at the only moment it is useful. 111 112 Static markup rather than config: it is identical on all nine pages, like Continue 113 itself, so there is nothing per-experience for experiences.js to hold. It is also 114 NOT in the client's interface PDF, which is the source of truth for the rest of the 115 copy on this screen. --> 116 <p id="intro-camera-note">Please allow camera access.</p> 117 <button id="continue-button" class="gate-btn">Continue</button> 118 <!-- Attribution required by the XR Engine License Agreement (lib/xr/LICENSE): 119 identify Niantic Spatial as creator, carry a copyright notice, reference the 120 agreement, and reference the warranty disclaimer. Must stay visible. --> 121 <p id="attribution"> 122 AR powered by the 8th Wall XR Engine, created by Niantic Spatial, Inc. 123 Copyright © Niantic Spatial, Inc. Used under the 124 <a href="lib/xr/LICENSE" target="_blank" rel="noopener">XR Engine License Agreement</a>,
125 which provides the software “as is”, without warranties of any kind. 126 </p> 127 </div> 128 </div> 129 130 <!-- Camera permission / access error gate. Hidden until ar-boot.js sees the engine 131 report a denied/failed camera, or an unrecoverable realityerror. --> 132 <div id="camera-error-overlay" class="hidden"> 133 <div id="camera-error-content"> 134 <h1 id="camera-error-title">Camera access needed</h1> 135 <p id="camera-error-text"> 136 This experience uses your camera, but access was blocked or no camera is 137 available. Allow camera access over a secure (https) connection and try again. 138 </p> 139 <button id="camera-retry-button" class="gate-btn">Try again</button> 140 </div> 141 </div> 142 143 <!-- AR overlay UI, reproduced from the LIVE 8th Wall apps this build replaces 144 (https://isobelandvan.8thwall.app/twcfi-{antoinette,marina,ar}/, still up, and newer 145 than any export we hold): a dashed framing rectangle with this storyteller's own 146 marker faded into it, the scan prompt, and bottom-centre playback controls, under a 147 storyteller bar in the shape of their nav bar. Their headphone prompt was dropped in 148 the client's redesign. 149 IDs (scan-prompt, ar-overlay-container, ar-outline, 150 ar-background-image, headphone-prompt, headphones-icon, audio-controls, play-button, 151 progress-fill, current-time, total-time, audio-player) are theirs, and are a contract read by 152 overlay-ui.js and by the holo4ds component in hologram-4ds.js. Identical across the 153 nine pages: everything per-page is filled in by overlay-ui.js. 154 155 The whole block is hidden until the engine fires realityready, which is what the 156 originals did too. --> 157 <div id="ui-container"> 158 <!-- Storyteller bar, the only navigation over the camera feed (the client's design, 159 replacing the back arrow). One link per storyteller, to one fixed take each; take 160 selection stays on the menu. Laid out by #topbar in index.css. --> 161 <nav id="topbar" aria-label="Storytellers"> 162 <a aria-label='Antoinette' href='/antoinette-1'><img src="assets/images/A.png" alt="" /></a> 163 <a aria-label='Marina' href='/marina-1'><img src="assets/images/M.png" alt="" /></a> 164 <a aria-label='Tom' href='/tom-3'><img src="assets/images/T.png" alt="" /></a> 165 </nav> 166 167 <!-- Copy is the live pages' scan prompt minus its middle line, "Keep image in view to 168 see hologram." That line described marker-tracked behaviour and is now false: SLAM 169 holds the scan in place once the marker has put it there, which is the entire 170 point of this rebuild. --> 171 <div id="scan-prompt"> 172 Point camera at image target.<br /> 173 Audio will continue until paused. 174 </div> 175 <!-- Marker framing rectangle. #ar-background-image is left src-less on purpose: 176 overlay-ui.js sets it to this storyteller's guide image, and a hard-coded one here 177 would be one more thing to keep in step across the nine pages. --> 178 <div id="ar-overlay-container"> 179 <div id="ar-outline"> 180 <img id="ar-background-image" alt="" /> 181 </div> 182 <!-- No #headphone-prompt / #headphones-icon: removed in the client's redesign. 183 hologram-4ds.js null-checks both. Do not leave an empty #headphone-prompt here: 184 its prompt styling draws a blank black bar across the guide. --> 185 </div> 186 187 <div id="audio-controls" style="display: none"> 188 <div id="audio-controls-container"> 189 <div id="button-controls"> 190 <!-- Play/pause toggle, always visible once the scan is placed, as on the live 191 pages. It is not just a fallback here: playback starts by itself off the 192 Continue gesture, so this button opens showing "Pause audio". If WebAudio is 193 somehow still suspended when the scan is placed it reads "Play audio" and a 194 tap resumes it, which is what it was doing before. --> 195 <button id="play-button" class="control-btn" style="display: flex">Play audio</button> 196 </div> 197 <div id="time-progress-container"> 198 <div id="progress-container"> 199 <div id="progress-bar"> 200 <div id="progress-fill"></div> 201 </div> 202 <div id="time-display">
203 <span id="current-time">00:00</span> 204 <span id="total-time">00:00</span> 205 </div> 206 </div> 207 </div> 208 </div> 209 </div> 210 211 <!-- The narration. A plain <audio> element, deliberately NOT routed through 212 <hologram-4ds audio-4ds="...">: the 4DViews player assumes its audio is the same 213 length as the scan and restarts it at every mesh loop, which chopped a 3:19 214 narration into a repeating 14-second fragment. The originals used a plain <audio> 215 for exactly this reason. src is set by overlay-ui.js; full explanation in the 216 NARRATION block in experiences.js. Kept inside #ui-container purely for grouping; 217 it has no visual presence (index.css parks it off-screen). --> 218 <audio id="audio-player" preload="auto"></audio> 219 </div> 220 221 <!-- The xrweb component (camera + SLAM + image targets) is NOT declared here. xr.js 222 registers it from inside its own async boot chain, and A-Frame does not 223 retro-initialise components registered after the scene has loaded, so declaring it 224 in markup is a race. ar-boot.js attaches it on 'xrloaded' instead, which is also 225 what lets it pick 6DoF vs 3DoF before XR8.run(). It configures exactly ONE image 226 target, this experience's, so an Antoinette marker held up to marina-2.html correctly 227 does nothing. A storyteller's three takes all share that storyteller's one marker. 228 229 renderer and light below are the 8th Wall exports' own declaration, verbatim 230 (twcfi-{antoinette,marina,tom}/src/body.html), and must stay that way. This used to 231 read renderer="colorManagement: true, physicallyCorrectLights, webgl2: true", 232 inherited from the MindAR build, which copied it from MindAR's own examples. That 233 line had two faults, and the client reported the first as a colour grade change: 234 235 colorManagement. Defaults to false in this A-Frame fork and the exports left it 236 off. Turning it on sets renderer.outputEncoding = sRGBEncoding, so the shader 237 applies LinearTosRGB (pow(rgb, 0.41666)) on output. The 4DViews scan is a 238 MeshBasicMaterial whose texture is left at LinearEncoding (lib/model4D_Three.js), 239 so nothing decodes it on the way in and the already-sRGB texture picks up a second 240 gamma: lifted blacks, washed out, desaturated. The camera feed does NOT shift with 241 it, because 8th Wall draws that with GlTextureRenderer as a raw GL pass outside 242 three.js, so the scan ends up visibly graded against the room it is standing in. 243 244 The commas. A-Frame splits component strings on SEMICOLONS only, so the whole 245 thing parsed as a single property: colorManagement = "true, 246 physicallyCorrectLights, webgl2: true", which the boolean parser reads as true. 247 physicallyCorrectLights and webgl2 were both silently dropped to their false 248 defaults, so the build ran on WebGL 1 while appearing to ask for WebGL 2. 249 250 light="defaultLightsEnabled: false" is the exports' too. It changes nothing visible 251 here, since every material in the scene is MeshBasicMaterial or ShadowMaterial and 252 neither is lit, and is kept only so this scene matches what shipped. --> 253 <a-scene 254 renderer="webgl2: true" 255 light="defaultLightsEnabled: false" 256 vr-mode-ui="enabled: false" 257 device-orientation-permission-ui="enabled: false"> 258 <a-camera position="0 0 0" look-controls="enabled: false"></a-camera> 259 260 <!-- World anchor: a child of the SCENE ROOT, not of any image target. The marker 261 writes a world-space pose into this entity once and SLAM holds it there, so the 262 scan survives the marker leaving the camera frame. hologram-4ds.js listens for 263 targetFound on `this.el.parentEl`, which is exactly this entity, so the player 264 itself is unchanged from the MindAR build. 265 266 Do NOT add position/rotation/scale attributes here: world-anchor.js writes 267 straight to object3D and A-Frame's own transform components would fight it. 268 269 SINGLE HOLOGRAM PER PAGE, and this is why the site is nine pages rather than one. 270 WEB4DS.initSequence() mounts the decoded mesh with 271 document.querySelector('hologram-4ds') (the FIRST match in the DOM, whichever 272 instance owns the player), resourceManager is a module-level singleton, Decoder4D's 273 caches are global, and a second load() fires 274 alert('A sequence is already loaded. One sequence at a time.'). 275 Never add a second <hologram-4ds> to this document. 276 277 rotation="180 90 90" is load-bearing. The image target's frame puts +Z along the 278 marker's normal, while the -1.57 bake leaves the scan Y-up, so without the +90 the 279 figure lies flat in the plane of the marker. 280 281 position="0 1 0" is one anchor unit along the marker's own +Y (up the printed 282 image), and both it and size are multiplied by detail.scale, the target's metric 283 size. So these are marker-relative, not absolute: reprinting the marker at a 284 different size moves and resizes the scan with it, which is the intent. 285 286 These are the values the client signed off on, tuned on Antoinette's marker, and all 287 nine pages ship them. They are marker-relative and there are still only three 288 markers, so they transfer across a storyteller's three takes by construction. What 289 does NOT transfer for free is framing: a take where the figure stands differently may 290 want its own pass. Retune with #pos= and #size= (see ar-boot.js), then write the
291 result back HERE if it is right for all nine, or as a `transform` on that one take in 292 experiences.js if it is not. Do not edit a single page: the nine are meant to stay 293 identical. rotation is exempt either way -- it is structural, not tuning. 294 295 There is no audio-4ds on any page, deliberately. The narration is a plain <audio> 296 element above, and routing it back through the 4DViews player would cut a 2-3 minute 297 story to the scan's 8-17 second loop. See the NARRATION block in experiences.js. 298 299 NEITHER main-4ds NOR the anchor's target name is written here. ar-boot.js sets both 300 from experiences.js before the scene loads, which is what lets these nine pages stay 301 identical to each other; EXPERIENCE_ID at the top of the file is the only thing that 302 says which experience this is. secondary-4ds stays, because the 4DViews fire demo is 303 what every non-ASTC desktop plays on all nine pages. --> 304 <a-entity id="world-anchor" world-anchor> 305 <hologram-4ds 306 position="1 0 0" 307 rotation="180 90 90" 308 size="0.8" 309 secondary-4ds="https://dev.fourdviews.com/ltd/8th%20Wall_fire_desktop.4ds"> 310 </hologram-4ds> 311 </a-entity> 312 </a-scene> 313 314 <!-- All tracking wiring: engine config, the Continue gate (narration unlock + audio 315 context + motion permission + camera start), the 316 realityready/realityerror/camerastatuschange event mapping, and the 3DoF fallback. 317 Loaded last so the scene and overlay DOM exist. --> 318
318<script src="ar-boot.js"></script>
318 319</body> 320</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.