PageSourceSearch

https://storytellers-twcfi.netlify.app/tom-3

html storytellers-twcfi.netlify.app collected 2026-10-03 11:27:21 UTC 19,593 bytes, 320 lines download raw bytes

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 &copy; 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 &ldquo;as is&rdquo;, 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.