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.