PageSourceSearch

https://gex-levels.com/assets/account-intro.js?v=cs4

js gex-levels.com collected 2026-10-02 01:31:20 UTC 9,541 bytes, 220 lines download raw bytes

1/* account-intro.js (2026-08-22) — the member area's opening film.
2 *
3 * Plays ONCE per browser, and only for a signed-in member: it waits for
4 * [data-account-email] to be filled with a real address by app.js before opening,
5 * so a logged-out visitor bouncing through /account never sees "Welcome back".
6 * A "Replay the intro" button in the dashboard calls GEXAccountIntro.play().
7 *
8 * CSP: external file, no inline <style>/<script>, no style attributes in markup.
9 * The only JS-set styles are the canvas size and the progress bar width, applied
10 * through the CSSOM — allowed under the strict style-src 'self'.
11 *
12 * Audio autoplay is blocked everywhere, so the film opens on a "Begin" click. The
13 * picture follows the VOICE (audio.currentTime) and falls back to a wall clock if
14 * playback is blocked or the file fails, so it can never freeze mid-scene.
15 */
16(function () {
17  "use strict";
18
19  var SEEN_KEY = "gex_acct_intro_seen";
20  var VO = "/assets/account-intro.mp3";
21  var END = 19.8;                        // VO is ~19.1s; hold the last frame briefly
22
23  var root = null, CV = null, CX = null, raf = 0, audio = null;
24  var started = false, sceneIdx = -1, t0 = 0;
25
26  function seen() { try { return localStorage.getItem(SEEN_KEY) === "1"; } catch (e) { return false; } }
27  function markSeen() { try { localStorage.setItem(SEEN_KEY, "1"); } catch (e) {} }
28
29  /* ---- the grid: a control surface coming online, drawn line by line ---- */
30  function sizeCanvas() {
31    if (!CV) return;
32    var d = Math.min(window.devicePixelRatio || 1, 2);
33    CV.width = window.innerWidth * d; CV.height = window.innerHeight * d;
34    CV.style.width = window.innerWidth + "px"; CV.style.height = window.innerHeight + "px";
35    CX.setTransform(d, 0, 0, d, 0, 0);
36  }
37  function draw(t) {
38    if (!CX) return;
39    var w = window.innerWidth, h = window.innerHeight, cx = w / 2, cy = h / 2;
40    CX.fillStyle = "#070504";
41    CX.fillRect(0, 0, w, h);
42
43    var step = 58;
44    var reach = Math.min(1, t / 6);                       // the grid draws itself in
45    var maxR = Math.sqrt(cx * cx + cy * cy) * reach;
46
47    CX.lineWidth = 1;
48    for (var x = cx % step; x < w; x += step) {
49      var dx = Math.abs(x - cx);
50      if (dx > maxR) continue;
51      CX.strokeStyle = "rgba(200,168,120," + (0.10 * (1 - dx / (maxR + 1))).toFixed(3) + ")";
52      CX.beginPath(); CX.moveTo(x, 0); CX.lineTo(x, h); CX.stroke();
53    }
54    for (var y = cy % step; y < h; y += step) {
55      var dy = Math.abs(y - cy);
56      if (dy > maxR) continue;
57      CX.strokeStyle = "rgba(200,168,120," + (0.10 * (1 - dy / (maxR + 1))).toFixed(3) + ")";
58      CX.beginPath(); CX.moveTo(0, y); CX.lineTo(w, y); CX.stroke();
59    }
60
61    // a slow sweep of light across the surface, like a panel refreshing
62    var sweep = ((t * 150) % (w + 460)) - 230;
63    var g = CX.createLinearGradient(sweep - 230, 0, sweep + 230, 0);
64    g.addColorStop(0, "transparent");
65    g.addColorStop(0.5, "rgba(232,210,164,.055)");
66    g.addColorStop(1, "transparent");
67    CX.fillStyle = g; CX.fillRect(0, 0, w, h);
68
69    // warm pool under the copy
70    var pool = CX.createRadialGradient(cx, cy, 0, cx, cy, Math.max(w, h) * 0.6);
71    pool.addColorStop(0, "rgba(200,168,120,.07)");
72    pool.addColorStop(1, "transparent");
73    CX.fillStyle = pool; CX.fillRect(0, 0, w, h);
74  }
75
76  /* ---- scenes, timed to the voice ---- */
77  var SCENES = [
78    { a: 0.0,  b: 3.2,  html: '<p class="ai__line">Welcome <em>back</em>.</p>' },
79    { a: 3.2,  b: 7.4,  html: '<p class="ai__line">This is your desk.<br>Everything you have access to, <em>in one place</em>.</p>' },
80    { a: 7.4,  b: 12.4, kind: "tiles" },
81    { a: 12.4, b: 16.2, html: '<p class="ai__line">Your journal. Your levels.<br>Your <em>library</em>.</p>' },
82    { a: 16.2, b: 20.5, html: '<p class="ai__line ai__line--hero">Now, get to <em>work</em>.</p>' }
83  ];
84  function stage() { return root ? root.querySelector(".ai__stage") : null; }
85  function setScene(i) {
86    if (i === sceneIdx) return;
87    sceneIdx = i;
88    var st = stage(), s = SCENES[i];
89    if (!st || !s) return;
90    if (s.kind === "tiles") {
91      st.innerHTML =
92        '<div class="ai__tiles">' +
93          '<div class="ai__tile"><span>Your products</span><b>Unlocked</b><p>The Library and the Indicator, and exactly what each one gives you.</p></div>' +
94          '<div class="ai__tile"><span>The regime</span><b>Today</b><p>How dealers are positioned, read before the open.</p></div>' +
95          '<div class="ai__tile"><span>Your rank</span><b>Earned</b><p>Progress that moves you up, and the price it has earned you.</p></div>' +
96        '</div>';
97      var tiles = st.querySelectorAll(".ai__tile");
98      for (var k = 0; k < tiles.length; k++) tiles[k].style.animationDelay = (k * 0.17) + "s";
99    } else {
100      st.innerHTML = s.html;
101    }
102  }
103
104  function clock() {
105    var wall = (performance.now() - t0) / 1000;
106    return (audio && audio.currentTime > 0.05) ? audio.currentTime : wall;
107  }
108  function tick() {
109    var t = clock();
110    draw(t);
111    var bar = root && root.querySelector(".ai__bar i");
112    if (bar) bar.style.width = Math.min(100, (t / END) * 100) + "%";
113    for (var i = 0; i < SCENES.length; i++) {
114      if (t >= SCENES[i].a && t < SCENES[i].b) { setScene(i); break; }
115    }
116    if (started && t >= END) { close(); return; }
117    raf = window.requestAnimationFrame(tick);
118  }
119
120  function begin() {
121    if (started) return;
122    started = true; t0 = performance.now();
123    var gate = root.querySelector(".ai__gate");
124    if (gate && gate.parentNode) gate.parentNode.removeChild(gate);
125    var skip = root.querySelector(".ai__skip");
126    if (skip) skip.hidden = false;
127    try {
128      audio = new Audio(VO);
129      audio.preload = "auto";
130      audio.play().catch(function () {});
131      audio.addEventListener("error", function () { audio = null; });
132      audio.addEventListener("ended", function () { window.setTimeout(close, 650); });
133    } catch (e) { audio = null; }
134    window.cancelAnimationFrame(raf);
135    raf = window.requestAnimationFrame(tick);
136  }
137
138  function close() {
139    if (!root) return;
140    window.cancelAnimationFrame(raf);
141    if (audio) { try { audio.pause(); } catch (e) {} audio = null; }
142    markSeen();
143    var node = root;
144    root = null; CV = null; CX = null; started = false; sceneIdx = -1;
145    document.body.classList.remove("ai-lock");
146    try {
147      var shut = function () { if (node.parentNode) node.parentNode.removeChild(node); };
148      node.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 600, easing: "ease" }).onfinish = shut;
149      /* a hidden tab pauses the fade forever — the teardown must not depend on it */
150      window.setTimeout(shut, 850);
151    } catch (e) { if (node.parentNode) node.parentNode.removeChild(node); }
152  }
153
154  function open() {
155    if (root) return;
156    root = document.createElement("div");
157    root.id = "acctIntro";
158    root.className = "is-on";
159    root.setAttribute("aria-label", "Member area opening");
160    root.innerHTML =
161      '<canvas></canvas><div class="ai__vig"></div>' +
162      '<div class="ai__stage">' +
163        '<div class="ai__gate">' +
164          '<div class="ai__mark">&#9650;</div>' +
165          '<h2 class="ai__t">Your member area</h2>' +
166          '<p class="ai__s">A quick tour of what lives here. Twenty seconds, once.</p>' +
167          '<button class="ai__btn" type="button">Begin</button>' +
168          '<button class="ai__skipgate" type="button">Skip &rarr;</button>' +
169        '</div>' +
170      '</div>' +
171      '<button class="ai__skip" type="button" hidden>Skip</button>' +
172      '<div class="ai__bar"><i></i></div>';
173    document.body.appendChild(root);
174    document.body.classList.add("ai-lock");
175    CV = root.querySelector("canvas"); CX = CV.getContext("2d");
176    sizeCanvas();
177    root.querySelector(".ai__btn").addEventListener("click", begin);
178    root.querySelector(".ai__skipgate").addEventListener("click", close);
179    root.querySelector(".ai__skip").addEventListener("click", close);
180    draw(0);
181    (function idle() {                    // the grid keeps breathing behind the gate
182      if (root && !started) { draw((performance.now() % 60000) / 1000); window.requestAnimationFrame(idle); }
183    })();
184  }
185
186  /* Only for a signed-in member: app.js replaces the "Loading…" placeholder in
187     [data-account-email] with the real address once the session resolves. */
188  function signedIn() {
189    var el = document.querySelector("[data-account-email]");
190    return !!(el && el.textContent && el.textContent.indexOf("@") > -1);
191  }
192  function waitForMember(tries) {
193    if (seen() || root) return;
194    if (signedIn()) { open(); return; }
195    if (tries <= 0) return;
196    window.setTimeout(function () { waitForMember(tries - 1); }, 400);
197  }
198
199  window.addEventListener("resize", function () { if (root) sizeCanvas(); });
200
201  /* The dashboard's "Replay" control — delegated so it works no matter when the
202     panel is rendered, and needs no inline handler (strict script-src). */
203  document.addEventListener("click", function (e) {
204    var b = e.target && e.target.closest ? e.target.closest("[data-intro-replay]") : null;
205    if (!b) return;
206    e.preventDefault();
207    open();
208  });
209
210  window.GEXAccountIntro = {
211    play: function () { open(); },              // the dashboard's "Replay" button
212    maybePlay: function () { waitForMember(30); } // ~12s of polling, then gives up
213  };
214
215  if (document.readyState === "loading") {
216    document.addEventListener("DOMContentLoaded", function () { waitForMember(30); });
217  } else {
218    waitForMember(30);
219  }
220})();

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.