1/* Party extras for the homepage illustration (Content.HeroAnimation) and the 2 * party spotlights (Content.EventSpotlight), until the party is over. 3 * 4 * - Hero: data-party-until in the future -> .is-party shows the party popper, 5 * which fires a burst of confetti after the build-up and then every few 6 * seconds while the illustration is on screen. 7 * - [data-confetti]: one burst the first time the element comes into view. 8 * 9 * Confetti is a few dozen spans moved by the Web Animations API, in the brand 10 * colours. Nothing happens with prefers-reduced-motion. */ 11(function () { 12 "use strict"; 13 14 var COLORS = ["#0069b4", "#ffd618", "#e94e1b", "#f29fc5", "#7e65aa", "#85c9f0", "#6ebd8f"]; 15 var reduced = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; 16 17 function random(min, max) { 18 return min + Math.random() * (max - min); 19 } 20 21 /* One burst into `host` (positioned), from origin (fractions of its size), 22 * in directions between minAngle and maxAngle (degrees, 0 = right, 23 * -90 = up). */ 24 function burst(host, options) { 25 if (reduced || !host.animate) { 26 return; 27 } 28 var layer = host.querySelector(":scope > .confetti"); 29 if (!layer) { 30 layer = document.createElement("div"); 31 layer.className = "confetti"; 32 layer.setAttribute("aria-hidden", "true"); 33 host.appendChild(layer); 34 } 35 var width = host.clientWidth; 36 var height = host.clientHeight; 37 var count = options.count || 40; 38 for (var i = 0; i < count; i++) { 39 var piece = document.createElement("span"); 40 var size = random(8, 14); 41 var round = Math.random() < 0.3; 42 piece.className = "confetti__piece"; 43 piece.style.left = options.x * width + "px"; 44 piece.style.top = options.y * height + "px"; 45 piece.style.width = size + "px"; 46 piece.style.height = (round ? size : size * 0.45) + "px"; 47 piece.style.borderRadius = round ? "50%" : "1px"; 48 piece.style.background = COLORS[i % COLORS.length]; 49 layer.appendChild(piece); 50 51 var angle = random(options.minAngle, options.maxAngle) * Math.PI / 180; 52 var speed = random(0.25, 0.6) * Math.max(width, 260); 53 var dx = Math.cos(angle) * speed; 54 var dy = Math.sin(angle) * speed; 55 var fall = random(0.45, 0.9) * Math.max(height, 220); 56 var spin = random(-720, 720); 57 var duration = random(1600, 2600); 58 59 piece.animate([ 60 { transform: "translate(0, 0) rotate(0deg)", opacity: 1 }, 61 { transform: "translate(" + dx + "px, " + dy + "px) rotate(" + spin / 2 + "deg)", opacity: 1, offset: 0.35 }, 62 { transform: "translate(" + dx * 1.3 + "px, " + (dy + fall) + "px) rotate(" + spin + "deg)", opacity: 0 } 63 ], { 64 duration: duration, 65 easing: "cubic-bezier(0.2, 0.7, 0.4, 1)", 66 fill: "forwards" 67 }).onfinish = (function (p) { 68 return function () { p.remove(); }; 69 })(piece); 70 } 71 } 72 73 window.happylabConfetti = burst; 74 75 function inView(element, callback, options) { 76 if (!("IntersectionObserver" in window)) { 77 callback(true); 78 return; 79 } 80 new IntersectionObserver(function (entries) { 81 callback(entries[0].isIntersecting); 82 }, options || { threshold: 0.4 }).observe(element); 83 } 84 85 function setUpHero(hero) { 86 var until = Date.parse(hero.getAttribute("data-party-until") || ""); 87 if (!until || Date.now() >= until) { 88 return; 89 } 90 hero.classList.add("is-party"); 91 if (reduced) { 92 return; 93 } 94 var popper = hero.querySelector(".hero__popper"); 95 var visible = false; 96 var fire = function () { 97 if (!visible || document.hidden || Date.now() >= until) { 98 return; 99 } 100 if (popper) { 101 popper.classList.remove("is-firing"); 102 // Restart the jerk animation. 103 void popper.getBoundingClientRect(); 104 popper.classList.add("is-firing"); 105 } 106 // From the popper's mouth (top middle of the illustration), fanning out. 107 burst(hero, { x: 0.5, y: 0.04, minAngle: 150, maxAngle: 330, count: 60 }); 108 }; 109 inView(hero, function (isVisible) { visible = isVisible; }, { threshold: 0.2 }); 110 // First shot once the icons and the popper are in (see hero.css). 111 window.setTimeout(fire, 5000); 112 window.setInterval(fire, 9000); 113 } 114 115 function setUpBurst(element) { 116 var done = false;
117 inView(element, function (isVisible) { 118 if (!isVisible || done) { 119 return; 120 } 121 done = true; 122 // Bursts fill the surrounding coloured band where there is one. 123 var host = element.closest("[class^='bg-'], [class*=' bg-']") || element; 124 if (window.getComputedStyle(host).position === "static") { 125 host.style.position = "relative"; 126 } 127 var hostBox = host.getBoundingClientRect(); 128 var box = element.getBoundingClientRect(); 129 burst(host, { 130 x: (box.left + Math.min(box.width, 260) / 2 - hostBox.left) / hostBox.width, 131 y: (box.top - hostBox.top + 20) / hostBox.height, 132 minAngle: -160, 133 maxAngle: -20, 134 count: 50 135 }); 136 }); 137 } 138 139 function init() { 140 Array.prototype.forEach.call(document.querySelectorAll("[data-hero-animation]"), setUpHero); 141 Array.prototype.forEach.call(document.querySelectorAll("[data-confetti]"), setUpBurst); 142 } 143 144 if (document.readyState === "loading") { 145 document.addEventListener("DOMContentLoaded", init); 146 } else { 147 init(); 148 } 149})();
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.