PageSourceSearch

https://www.escavox.com//js/main.js?v=20260921

js escavox.com collected 2026-10-06 17:29:24 UTC 7,351 bytes, 189 lines download raw bytes

1/* Escavox — site interactions (Motion-powered animations with graceful fallback) */
2(function () {
3  "use strict";
4
5  var M = window.Motion;                                   // vendored motion.js (UMD global)
6  var reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
7  var canAnimate = !!(M && M.animate && M.inView) && !reduce;
8
9  /* Mobile nav toggle */
10  var toggle = document.querySelector(".nav-toggle");
11  var menu = document.querySelector(".nav-menu");
12  if (toggle && menu) {
13    toggle.addEventListener("click", function () {
14      var open = menu.classList.toggle("open");
15      toggle.setAttribute("aria-expanded", open ? "true" : "false");
16    });
17    menu.querySelectorAll("a").forEach(function (a) {
18      a.addEventListener("click", function () {
19        menu.classList.remove("open");
20        toggle.setAttribute("aria-expanded", "false");
21      });
22    });
23  }
24
25  /* Sticky header shadow on scroll */
26  var header = document.querySelector(".header");
27  if (header) {
28    var onScroll = function () { header.classList.toggle("scrolled", window.scrollY > 8); };
29    onScroll();
30    window.addEventListener("scroll", onScroll, { passive: true });
31  }
32
33  /* Reveal on scroll — Motion when available, instant reveal otherwise */
34  var reveals = document.querySelectorAll(".reveal");
35  var delayFor = function (el) {
36    if (el.classList.contains("d1")) return 0.08;
37    if (el.classList.contains("d2")) return 0.16;
38    if (el.classList.contains("d3")) return 0.24;
39    if (el.classList.contains("d4")) return 0.32;
40    return 0;
41  };
42  if (reveals.length) {
43    if (canAnimate) {
44      reveals.forEach(function (el) {
45        M.inView(el, function () {
46          M.animate(
47            el,
48            { opacity: [0, 1], transform: ["translateY(24px)", "translateY(0px)"] },
49            { duration: 0.7, delay: delayFor(el), ease: [0.22, 0.7, 0.3, 1] }
50          );
51        }, { amount: 0.15, margin: "0px 0px -40px 0px" });
52      });
53    } else {
54      reveals.forEach(function (el) { el.classList.add("in"); }); // instant fallback
55    }
56  }
57
58  /* Count-up for elements with data-count */
59  var counters = document.querySelectorAll("[data-count]");
60  counters.forEach(function (el) {
61    var target = parseFloat(el.getAttribute("data-count"));
62    var start = el.getAttribute("data-count-start") ? parseFloat(el.getAttribute("data-count-start")) : 0;
63    var suffix = el.getAttribute("data-suffix") || "";
64    var decimals = el.getAttribute("data-decimals") ? parseInt(el.getAttribute("data-decimals"), 10) : 0;
65    var format = el.getAttribute("data-format");
66    var compact = function (v) {
67      if (v >= 1e6) { var m = v / 1e6; return (m % 1 === 0 ? m.toFixed(0) : m.toFixed(1)) + "M+"; }
68      return Math.round(v / 1e3) + "k";
69    };
70    var render = format === "compact"
71      ? function (v) { el.textContent = compact(v); }
72      : function (v) { el.textContent = v.toFixed(decimals) + suffix; };
73    if (canAnimate) {
74      M.inView(el, function () {
75        M.animate(start, target, {
76          duration: 1.4,
77          ease: [0.22, 0.7, 0.3, 1],
78          onUpdate: render,
79          onComplete: function () { render(target); }
80        });
81      }, { amount: 0.6 });
82    } else {
83      render(target); // instant fallback
84    }
85  });
86
87  /* Hero entrance — subtle spring lift on the live-tracker visual */
88  if (canAnimate) {
89    var heroVisual = document.querySelector(".hero-visual .tracker-card");
90    if (heroVisual) {
91      M.animate(
92        heroVisual,
93        { opacity: [0, 1], transform: ["translateY(28px) scale(0.98)", "translateY(0px) scale(1)"] },
94        { duration: 0.9, delay: 0.15, type: "spring", stiffness: 90, damping: 16 }
95      );
96    }
97  }
98
99  /* Contact form — submits to Web3Forms via AJAX, keeps inline success message */
100  var form = document.querySelector("#contact-form");
101  if (form) {
102    form.addEventListener("submit", function (ev) {
103      ev.preventDefault();
104      if (form.checkValidity && !form.checkValidity()) { form.reportValidity(); return; }
105
106      var ok = document.querySelector("#form-success");
107      var btn = form.querySelector('button[type="submit"]');
108      var btnText = btn ? btn.textContent : "";
109      if (btn) { btn.disabled = true; btn.textContent = "Sending…"; }
110
111      var payload = {};
112      new FormData(form).forEach(function (value, key) { payload[key] = value; });
113
114      fetch(form.getAttribute("action") || "https://api.web3forms.com/submit", {
115        method: "POST",
116        headers: { "Content-Type": "application/json", "Accept": "application/json" },
117        body: JSON.stringify(payload)
118      }).then(function (res) { return res.json(); }).then(function (json) {
119        if (!json.success) throw new Error(json.message || "Submission failed");
120        form.style.display = "none";
121        if (ok) { ok.style.display = "block"; ok.scrollIntoView({ behavior: "smooth", block: "center" }); }
122      }).catch(function () {
123        if (btn) { btn.disabled = false; btn.textContent = btnText; }
124        alert("Sorry — your message couldn't be sent just now. Please email [email protected] and we'll get straight back to you.");
125      });
126    });
127  }
128
129  /* Footer year */
130  var yr = document.querySelector("#year");
131  if (yr) yr.textContent = new Date().getFullYear();
132
133  /* Particle globe (hero orbiting visual) */
134  document.querySelectorAll(".orbit-globe-canvas").forEach(function (canvas) {
135    var ctx = canvas.getContext("2d");
136    if (!ctx) return;
137
138    var COUNT = 480;
139    var points = [];
140    var golden = Math.PI * (3 - Math.sqrt(5));
141    for (var i = 0; i < COUNT; i++) {
142      var y = 1 - (i / (COUNT - 1)) * 2;
143      var r = Math.sqrt(1 - y * y);
144      var theta = golden * i;
145      points.push({ x: Math.cos(theta) * r, y: y, z: Math.sin(theta) * r });
146    }
147
148    var width = 0, height = 0, colorStr = "rgb(3,126,176)";
149    var readColor = function () {
150      var c = getComputedStyle(canvas).color;
151      if (c) colorStr = c;
152    };
153    var resize = function () {
154      var rect = canvas.getBoundingClientRect();
155      width = rect.width; height = rect.height;
156      var dpr = Math.min(window.devicePixelRatio || 1, 2);
157      canvas.width = Math.max(1, Math.floor(width * dpr));
158      canvas.height = Math.max(1, Math.floor(height * dpr));
159      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
160      readColor();
161    };
162    resize();
163    if ("ResizeObserver" in window) { new ResizeObserver(resize).observe(canvas); }
164    else { window.addEventListener("resize", resize, { passive: true }); }
165
166    var angle = 0;
167    var draw = function () {
168      var cx = width / 2, cy = height / 2;
169      var radius = (Math.min(width, height) / 2) * 0.94;
170      var cos = Math.cos(angle), sin = Math.sin(angle);
171      ctx.clearRect(0, 0, width, height);
172      ctx.fillStyle = colorStr;
173      for (var j = 0; j < points.length; j++) {
174        var p = points[j];
175        var x = p.x * cos - p.z * sin;
176        var z = p.x * sin + p.z * cos;
177        var depth = (z + 1) / 2;
178        ctx.globalAlpha = 0.1 + depth * 0.7;
179        ctx.beginPath();
180        ctx.arc(cx + x * radius, cy + p.y * radius, 0.6 + depth * 1.8, 0, Math.PI * 2);
181        ctx.fill();
182      }
183      ctx.globalAlpha = 1;
184      angle += reduce ? 0.0012 : 0.005;
185      requestAnimationFrame(draw);
186    };
187    requestAnimationFrame(draw);
188  });
189})();

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.