PageSourceSearch

https://safety.snipershopping.com/shared/js/switch.js

js snipershopping.com collected 2026-10-06 10:33:09 UTC 7,767 bytes, 190 lines download raw bytes

1/* Sniper - the division switch animation.
2   The signature transition from the plan (section 4) and the prototype in
3   prototypes/division-switch/, adapted to two separate sites: the circle opens
4   out of the header block on the page you are leaving, covers the screen, and
5   the browser navigates underneath it. The page you land on finds the curtain
6   already covering, holds the emblem for a beat, then contracts it back into
7   the same corner - so the two halves read as one movement across a real page
8   load.
9
10   Timings are the storyboard's: expand 450ms, emblem at 290, hold 110, and on
11   arrival hold 140 then contract 610. Only clip-path and opacity animate, which
12   is what keeps it on the compositor.
13
14   Reduced motion gets a 200ms cross-fade with no expansion and no emblem.
15   If the destination never arrives the curtain retreats rather than leaving
16   anyone on a coloured void. */
17(function () {
18  "use strict";
19
20  var KEY = "sniper.switch.v1";
21  var EASE = "cubic-bezier(.77,0,.18,1)";
22  var T = { expand: 450, emblemAt: 290, emblemIn: 160, hold: 110,
23            arriveHold: 140, contract: 610, fade: 200 };
24
25  function reduced() {
26    return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
27  }
28  function cssVar(name, fallback) {
29    var v = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
30    return v || fallback;
31  }
32
33  /* Cover from the very first paint. This runs while the head is still parsing,
34     so there is no frame in which the arriving page is visible before the
35     curtain lands on it - a pseudo-element on <html> does the covering until
36     the real layer can be appended to <body>. */
37  var arriving = false;
38  try { arriving = sessionStorage.getItem(KEY) === "1"; } catch (e) { /* ignore */ }
39  if (arriving) document.documentElement.setAttribute("data-arriving", "1");
40
41  /* ---------------------------------------------------------- the layer */
42  function curtain(colour, markSrc) {
43    var c = document.getElementById("divCurtain");
44    if (c) return c;
45    c = document.createElement("div");
46    c.id = "divCurtain";
47    c.className = "divcurtain";
48    c.setAttribute("aria-hidden", "true");
49    c.style.background = colour;
50    c.innerHTML = markSrc
51      ? '<img class="divcurtain__mark" src="' + markSrc + '" alt="">'
52      : "";
53    document.body.appendChild(c);
54    return c;
55  }
56
57  function radiusFrom(x, y) {
58    return Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y));
59  }
60  function centreOf(node) {
61    if (!node) return { x: innerWidth - 80, y: 40 };
62    var r = node.getBoundingClientRect();
63    return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
64  }
65
66  /* ------------------------------------------------------------- leaving */
67  function leave(link) {
68    var href = link.getAttribute("href");
69    if (!href) return;
70
71    /* the destination's colour is the wash already in this header, and the mark
72       is the one the block is already showing - no extra configuration */
73    var colour = cssVar("--cross-bg", "#182957");
74    var markEl = link.querySelector(".crossnav__logo");
75    var mark = markEl ? markEl.getAttribute("src") : "";
76
77    var c = curtain(colour, mark);
78    var pt = centreOf(link);
79    var R = radiusFrom(pt.x, pt.y);
80    var emblem = c.querySelector(".divcurtain__mark");
81
82    try { sessionStorage.setItem(KEY, "1"); } catch (e) { /* ignore */ }
83
84    if (reduced()) {
85      c.style.clipPath = "none";
86      c.animate([{ opacity: 0 }, { opacity: 1 }], { duration: T.fade, fill: "forwards" })
87        .finished.then(function () { location.href = href; });
88      return;
89    }
90
91    c.style.clipPath = "circle(0px at " + pt.x + "px " + pt.y + "px)";
92    c.animate(
93      [{ clipPath: "circle(0px at " + pt.x + "px " + pt.y + "px)" },
94       { clipPath: "circle(" + R + "px at " + pt.x + "px " + pt.y + "px)" }],
95      { duration: T.expand, easing: EASE, fill: "forwards" }
96    );
97    if (emblem) {
98      emblem.animate(
99        [{ opacity: 0, transform: "scale(.92)" }, { opacity: 1, transform: "none" }],
100        { duration: T.emblemIn, delay: T.emblemAt, easing: "ease-out", fill: "forwards" }
101      );
102    }
103    setTimeout(function () { location.href = href; }, T.expand + T.hold);
104
105    /* if the page is still here well after it should have gone, retreat */
106    setTimeout(function () {
107      if (!document.getElementById("divCurtain")) return;
108      c.animate(
109        [{ clipPath: "circle(" + R + "px at " + pt.x + "px " + pt.y + "px)" },
110         { clipPath: "circle(0px at " + pt.x + "px " + pt.y + "px)" }],
111        { duration: 380, easing: EASE, fill: "forwards" }
112      ).finished.then(function () {
113        c.remove();
114        try { sessionStorage.removeItem(KEY); } catch (e) {}
115        if (window.sniperToast) {
116          window.sniperToast("That division did not load. Nothing lost - you are still here.");
117        }
118      });
119    }, 2600);
120  }
121
122  /* ------------------------------------------------------------ arriving */
123  function arrive() {
124    if (!arriving) return;
125    try { sessionStorage.removeItem(KEY); } catch (e) {}
126
127    var d = window.SNIPER_DIVISION || {};
128    var colour = cssVar("--division-bg", "#182957");
129    var c = curtain(colour, d.mark || "");
130    var emblem = c.querySelector(".divcurtain__mark");
131
132    if (reduced()) {
133      document.documentElement.removeAttribute("data-arriving");
134      c.animate([{ opacity: 1 }, { opacity: 0 }], { duration: T.fade, fill: "forwards" })
135        .finished.then(function () { c.remove(); });
136      return;
137    }
138
139    /* contract back into the block in this header, so the movement that opened
140       on the last page closes on this one */
141    var target = document.querySelector(".crossnav") || document.querySelector(".header__tools");
142    var pt = centreOf(target);
143    var R = radiusFrom(pt.x, pt.y);
144
145    c.style.clipPath = "circle(" + R + "px at " + pt.x + "px " + pt.y + "px)";
146    if (emblem) emblem.style.opacity = "1";
147    /* the real layer is in place, so the stand-in can go */
148    document.documentElement.removeAttribute("data-arriving");
149
150    setTimeout(function () {
151      if (emblem) {
152        emblem.animate([{ opacity: 1 }, { opacity: 0 }],
153          { duration: 200, easing: "ease-in", fill: "forwards" });
154      }
155      c.animate(
156        [{ clipPath: "circle(" + R + "px at " + pt.x + "px " + pt.y + "px)" },
157         { clipPath: "circle(0px at " + pt.x + "px " + pt.y + "px)" }],
158        { duration: T.contract, easing: EASE, fill: "forwards" }
159      ).finished.then(function () { c.remove(); });
160
161      /* the page itself rises the last 18px as the curtain clears */
162      var rise = document.querySelectorAll(".header, .hero, .vhero");
163      rise.forEach(function (n) {
164        n.animate([{ opacity: 0, transform: "translateY(18px)" }, { opacity: 1, transform: "none" }],
165          { duration: 320, easing: EASE, fill: "backwards" });
166      });
167    }, T.arriveHold);
168  }
169
170  /* --------------------------------------------------------------- wiring */
171  document.addEventListener("click", function (e) {
172    var link = e.target.closest(".crossnav, .js-division-switch");
173    if (!link) return;
174    /* let modified clicks behave normally - people open these in new tabs */
175    if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;
176    e.preventDefault();
177    leave(link);
178  });
179
180  /* coming back with the Back button must never show a stale curtain */
181  window.addEventListener("pageshow", function (ev) {
182    if (!ev.persisted) return;
183    var c = document.getElementById("divCurtain");
184    if (c) c.remove();
185    try { sessionStorage.removeItem(KEY); } catch (e) {}
186  });
187
188  if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", arrive);
189  else arrive();
190})();

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.