PageSourceSearch

https://webcade.fun/orient.js?v=1025436e

js webcade.fun collected 2026-10-08 08:26:46 UTC 14,490 bytes, 299 lines download raw bytes

1// Which way up a page wants the phone.
2//
3// Every cabinet on this site is portrait or close enough to square, and play
4// mode was built for a thumb on each side of a tall screen. CRYPTO SMASH is
5// the exception: its world is 480x320 and its canvas 960x640, so on a 390px
6// portrait phone the fight gets 390x260 — thirty-one per cent of the screen
7// height — while its own controls sit on top of that strip. Turned sideways
8// the same fight gets the full height and 2.3x the area, and the control
9// layout stops being cramped: #touch pins the pad hard left and the strikes
10// hard right, which is 390px apart in portrait and 844px apart in landscape.
11// The second number is where thumbs actually are when you hold a phone to
12// play something.
13//
14// The ask comes at the door rather than at the fight. Being told to rotate
15// while a match is counting down is a worse moment than being told before you
16// have picked a fighter, so the gate goes up on arrival and everything past
17// it — menu, character select, the fight — is already the right way round.
18//
19// Two mechanisms, because neither is enough alone:
20//
21//   screen.orientation.lock() actually turns the app. It works in an installed
22//   PWA or the Seeker TWA, needs fullscreen first in an ordinary browser tab,
23//   and is simply absent on iOS Safari. So it is attempted and never relied on.
24//
25//   The gate is the fallback and the honest one: ask, show which way, and get
26//   out of the way the moment the phone is round. It is also what a player
27//   sees if they decline or revoke the lock.
28//
29// Phantom's in-app browser can do neither. Its WebView is portrait-locked at
30// the native level, and no web API overrides a host app's orientation policy —
31// lock() rejects, and the gate asks for something the player physically cannot
32// give. A gate that cannot be satisfied is the worst screen on the site.
33//
34// So there the CONTENT turns instead of the phone: <main> is rotated ninety
35// degrees with its width and height swapped, and the player turns the handset
36// to meet it. The OS never rotates; the game is simply already sideways when
37// they get there.
38//
39// This costs nothing in brawl.js, which is luck rather than design. Its
40// controls are DOM buttons whose handlers only set a bitmask — no clientX, no
41// coordinate maths — so the browser hit-tests them through the transform on its
42// own. And fitCabinet measures #cab-screen's own clientWidth/clientHeight
43// rather than the window's, so it reads the swapped box and scales correctly
44// with no changes. A canvas that read raw pointer coordinates would have needed
45// every one of them mapped back through the rotation.
46(() => {
47  const WANT = {                                   // everything else stays portrait
48    '/brawl': 'landscape',
49    // The cabinet is 16:9 and its UI is authored against a 960x540 reference
50    // that matches on HEIGHT. On a 390px portrait phone that is a 219px-tall
51    // strip with the rod, the reel button and the joystick sharing it; turned,
52    // the same game gets the whole 390 and 2.2x the area.
53    '/harbormaster': 'landscape',
54    // BREAK's table is 2:1 across the bed and its canvas 1040x640. On a 390px
55    // portrait phone that is a 326x201 strip in the top corner of an 844-tall
56    // page — a fifth of the screen, with balls four pixels across, which is
57    // not a table anybody can aim on. Turned, it gets the full 844 and four
58    // times the area.
59    '/break': 'landscape',
60    '/break/live': 'landscape',
61  };
62  // Why each one asks, in its own words. The gate used to name CRYPTO SMASH
63  // whatever page it was on, which is the sort of copy that reads as a bug on
64  // every page but the one it was written for.
65  const WHY = {
66    '/brawl': 'CRYPTO SMASH is a sideways fight \u2014 landscape gives it '
67      + 'the whole screen instead of a third of it.',
68    '/harbormaster': 'The bay is wider than it is tall \u2014 landscape gives the '
69      + 'water the whole screen, and puts the reel under your thumb.',
70    '/break': 'A pool table is twice as wide as it is deep \u2014 landscape gives '
71      + 'it the whole screen instead of a fifth of it.',
72    '/break/live': 'A pool table is twice as wide as it is deep \u2014 landscape '
73      + 'gives it the whole screen instead of a fifth of it.',
74  };
75  // Pages whose gate WAITS. The ask normally comes at the door, because being
76  // told to rotate mid-countdown is worse than being told before you have
77  // chosen anything. /break broke that rule the other way: its door is now a
78  // menu, and a menu reads perfectly well upright — so the first thing a phone
79  // met was a demand to turn it, made before it had been shown a single thing
80  // worth turning for. Here the gate is armed by the page instead, at the
81  // moment a mode is chosen and a table is actually about to appear.
82  //
83  // Deferring is only ever safe where the pre-armed view is genuinely usable
84  // in portrait. Adding a path here without checking that is how you ship a
85  // game that runs in a strip.
86  const DEFER = {
87    '/break': true,
88  };
89
90  const path = location.pathname.replace(/\/$/, '') || '/';
91  const want = WANT[path] || 'portrait';
92
93  // Desktop has no orientation to speak of, and a mouse does not care.
94  const touch = matchMedia('(pointer: coarse)').matches || 'ontouchstart' in window;
95  if (!touch) return;
96
97  // THE QUESTION IS ABOUT THE BOX, NOT THE HANDSET.
98  //
99  // This used to prefer screen.orientation.type, and that is a statement about
100  // the DEVICE. It is the same statement whether the page owns the whole screen
101  // or a panel in the middle of somebody else's app — and inside a Telegram
102  // Mini App it is routinely 'portrait-primary' while the sheet the game has
103  // been handed is 844x390. So a player holding the phone sideways was told to
104  // rotate it, and the gate — a full-screen overlay — sat on top of the fight
105  // swallowing every tap. They could see the match and could not touch it.
106  //
107  // The media query is the same engine that lays the page out answering about
108  // the same viewport, so it cannot disagree with what the player sees. It is
109  // also what the change listener below already watches, which until now
110  // decided WHEN to re-check while something else decided WHAT the answer was.
111  // innerWidth/innerHeight is the fallback for anything without matchMedia.
112  //
113  // screen.orientation keeps its one honest job: tryLock() asks the platform to
114  // turn the device, which is a question about the device.
115  const isLandscape = () => {
116    try {
117      const mq = matchMedia('(orientation: landscape)');
118      if (mq && typeof mq.matches === 'boolean') return mq.matches;
119    } catch (_) { /* fall through to measuring it */ }
120    return window.innerWidth > window.innerHeight;
121  };
122  const correct = () => (want === 'landscape' ? isLandscape() : !isLandscape());
123  const MUST_LANDSCAPE = want === 'landscape';
124
125  // ------------------------------------------------ the portrait-locked case
126
127  // Phantom's in-app browser. The provider object alone is not proof — the
128  // desktop extension injects the identical thing — but this module has already
129  // returned unless the pointer is coarse, so a Phantom provider here means the
130  // handset app's own browser.
131  //
132  // Named explicitly, and that is a real cost: Solflare, Coinbase and whatever
133  // wallet ships next are locked the same way and are not covered by this. A
134  // check on the failure rather than the name would catch all of them.
135  function inPhantom() {
136    try {
137      if (/Phantom/i.test(navigator.userAgent || '')) return true;
138      const p = window.phantom && window.phantom.solana;
139      return !!(p && p.isPhantom);
140    } catch (_) { return false; }
141  }
142
143  const ROTATE = MUST_LANDSCAPE && inPhantom();
144
145  // Armed on arrival everywhere except a deferred path, where the page says
146  // when. Everything the sideways treatment does — the gate, the content
147  // rotation and the landscape layout class — waits on this one flag, because
148  // turning the menu ninety degrees is exactly as wrong as demanding the
149  // player turn the handset for it.
150  let armed = !DEFER[path];
151
152  // Ask the platform to turn. Failure is normal — a browser tab that is not
153  // fullscreen refuses, and iOS has no such API at all — so it is fired and
154  // forgotten, with the gate below carrying the case either way.
155  function tryLock() {
156    try {
157      const o = window.screen && screen.orientation;
158      if (o && typeof o.lock === 'function') o.lock(want).catch(() => {});
159    } catch (_) { /* not supported here; the gate does the asking */ }
160  }
161
162  // ------------------------------------------------------------- the gate
163
164  // Only a page that genuinely cannot be played the other way blocks. Nothing
165  // on this site needs to insist on portrait — a portrait page in landscape is
166  // merely wide — so the gate belongs to the landscape one alone.
167  // The gate belongs to the landscape page alone, and never where the content
168  // is being turned instead — asking a player to rotate a screen that is
169  // already sideways is asking them to undo it.
170  const MUST = MUST_LANDSCAPE && !ROTATE;
171  let gate = null;
172
173  function build() {
174    if (gate) return gate;
175    gate = document.createElement('div');
176    gate.id = 'rotate-gate';
177    gate.innerHTML = `
178      <div class="rg-box">
179        <div class="rg-phone" aria-hidden="true">
180          <svg viewBox="0 0 64 64">
181            <rect x="20" y="6" width="24" height="42" rx="4" stroke-width="3" fill="none"/>
182            <line x1="27" y1="43" x2="37" y2="43" stroke-width="3" stroke-linecap="round"/>
183          </svg>
184        </div>
185        <p class="rg-title">TURN YOUR PHONE</p>
186        <p class="rg-sub"></p>
187        <button type="button" class="rg-anyway">Play in portrait anyway</button>
188      </div>`;
189    gate.querySelector('.rg-sub').textContent = WHY[path]
190      || 'This one plays better sideways.';
191    // Dismissible on purpose. A gate with no way past it is a wall, and a
192    // player who wants to try it upright is entitled to a worse view.
193    gate.querySelector('.rg-anyway').addEventListener('click', () => {
194      dismissed = true;
195      apply();
196    });
197    document.body.appendChild(gate);
198    return gate;
199  }
200
201  let dismissed = false;
202
203  function apply() {
204    const ok = correct();
205    const block = MUST && armed && !ok && !dismissed;
206    document.body.classList.toggle('needs-rotate', block);
207    if (block) build();
208    // Rotating after dismissing puts the player where they were headed, and
209    // there is nothing left to ask, so the dismissal is spent.
210    if (ok) dismissed = false;
211  }
212
213  function applyRotation() {
214    // Only while the handset is upright. Turned already, the OS has done the
215    // job and rotating on top of it would stand the game on its head.
216    const on = ROTATE && armed && !isLandscape();
217    document.body.classList.toggle('phantom-landscape', on);
218    if (on) addExit();
219  }
220
221  // Sideways, however it got there.
222  //
223  // The landscape layout — pad hard left, strikes hard right, the select laid
224  // along the long axis, the artwork sized off the short one — was written for
225  // the rotated case and keyed to it, so it only ever ran inside Phantom. A
226  // phone that simply turned got the portrait stylesheet instead, which sizes
227  // everything off the long axis: the menu landed below the fold and behind
228  // the tab bar, and the fight ran taller than the screen with its controls
229  // off the bottom of it. This is the class that layout actually belongs to;
230  // the rotation keeps its own for the transform alone.
231  function applySideways() {
232    const rotated = document.body.classList.contains('phantom-landscape');
233    document.body.classList.toggle('sideways', MUST_LANDSCAPE && armed && (rotated || isLandscape()));
234  }
235
236  // A way out.
237  //
238  // Turning the content means hiding the header, the footer and the tab bar —
239  // they are outside <main> and would sit sideways across a turned page. Every
240  // one of them was also the only way off this page, so the rotated view had no
241  // exit at all: the cabinet's own MENU goes back to its title screen, and the
242  // title screen offers three game modes and nothing else. The only way back to
243  // the arcade was the browser's back button, which an in-app wallet browser
244  // does not always give you.
245  //
246  // Built here rather than in the page because this belongs to the rotation,
247  // not to any one cabinet — whatever else ends up in WANT gets it too. Inside
248  // <main> so it turns with everything else.
249  function addExit() {
250    if (document.getElementById('rot-exit')) return;
251    const host = document.querySelector('main');
252    if (!host) return;
253    const a = document.createElement('a');
254    a.id = 'rot-exit';
255    a.href = '/arcade';
256    a.textContent = '\u2190 ARCADE';
257    host.appendChild(a);
258  }
259
260  function start() {
261    if (ROTATE) applyRotation();
262    applySideways();
263    tryLock();
264    apply();
265    // orientationchange is the one that fires on the phones that matter; the
266    // media query catches the rest, including a desktop window being dragged
267    // narrow with touch emulation on.
268    const onTurn = () => { applyRotation(); applySideways(); apply(); };
269    window.addEventListener('orientationchange', () => setTimeout(onTurn, 80));
270    matchMedia('(orientation: landscape)').addEventListener('change', onTurn);
271    window.addEventListener('resize', onTurn);
272    // A lock is dropped when the app is backgrounded on some platforms; ask
273    // again on the way back rather than leaving the player sideways-locked to
274    // the wrong thing.
275    document.addEventListener('visibilitychange', () => {
276      if (!document.hidden) { tryLock(); applySideways(); apply(); }
277    });
278  }
279
280  // What a deferred page calls when it is finally about to show a table.
281  // Idempotent: a player who backs out to the menu and picks again must not
282  // get a second gate, and there is nothing to disarm — once a table has been
283  // on screen, the page is a landscape page for the rest of the visit.
284  window.ARCADE_ORIENT = {
285    arm() {
286      if (armed) return;
287      armed = true;
288      if (ROTATE) applyRotation();
289      applySideways();
290      tryLock();
291      apply();
292    },
293    get armed() { return armed; },
294    get deferred() { return !!DEFER[path]; },
295  };
296
297  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start);
298  else start();
299})();

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.