1/* claudiaschwartz.de â menu, waveform players, gallery, lightbox */ 2(function () { 3 "use strict"; 4 5 var reduce = matchMedia("(prefers-reduced-motion: reduce)").matches; 6 var $ = function (s, r) { return (r || document).querySelector(s); }; 7 var $$ = function (s, r) { return [].slice.call((r || document).querySelectorAll(s)); }; 8 9 /* ======================================================== hero entrance */ 10 requestAnimationFrame(function () { document.body.classList.add("is-ready"); }); 11 12 /* ================================================================= nav */ 13 var burger = $(".burger"); 14 var menu = $("#menu"); 15 var topBar = $(".top"); 16 var beforeMenu = null; 17 18 function setMenu(open) { 19 document.body.classList.toggle("menu-open", open); 20 burger.setAttribute("aria-expanded", open ? "true" : "false"); 21 burger.setAttribute("aria-label", open ? burger.dataset.close : burger.dataset.open); 22 document.documentElement.style.overflow = open ? "hidden" : ""; 23 if (open) { 24 beforeMenu = document.activeElement; 25 menu.removeAttribute("inert"); 26 var first = $(".menu__list a", menu); 27 if (first) setTimeout(function () { first.focus(); }, 80); 28 } else { 29 menu.setAttribute("inert", ""); 30 if (beforeMenu) beforeMenu.focus(); 31 } 32 } 33 34 if (burger && menu) { 35 burger.addEventListener("click", function () { 36 setMenu(!document.body.classList.contains("menu-open")); 37 }); 38 menu.addEventListener("click", function (e) { 39 if (e.target.closest("a")) setMenu(false); 40 }); 41 addEventListener("keydown", function (e) { 42 if (e.key === "Escape" && document.body.classList.contains("menu-open")) setMenu(false); 43 }); 44 /* keep tabbing inside the overlay while it is open */ 45 menu.addEventListener("keydown", function (e) { 46 if (e.key !== "Tab") return; 47 var f = $$("a", menu); 48 if (!f.length) return; 49 var first = f[0], last = f[f.length - 1]; 50 if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); } 51 else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); } 52 }); 53 } 54 55 /* the burger takes the colour of whatever band sits under it */ 56 if (topBar) { 57 var bands = $$(".hero, .band, [data-band]"); 58 var paint = function () { 59 var y = 34, over = "light"; 60 for (var i = 0; i < bands.length; i++) { 61 var r = bands[i].getBoundingClientRect(); 62 if (r.top <= y && r.bottom > y) { 63 over = bands[i].dataset.band === "dark" ? "dark" : "light"; 64 break; 65 } 66 } 67 topBar.dataset.over = over; 68 }; 69 addEventListener("scroll", paint, { passive: true }); 70 addEventListener("resize", paint);
71 paint(); 72 } 73 74 /* section spy */ 75 var sections = $$("main section[id]"); 76 var navLinks = $$('.menu__list a[href*="#"]'); 77 if (sections.length && navLinks.length && "IntersectionObserver" in window) { 78 var spy = new IntersectionObserver(function (entries) { 79 entries.forEach(function (en) { 80 if (!en.isIntersecting) return; 81 navLinks.forEach(function (a) { 82 var href = a.getAttribute("href"); 83 a.setAttribute("aria-current", 84 href.slice(href.indexOf("#")) === "#" + en.target.id ? "true" : "false"); 85 }); 86 }); 87 }, { rootMargin: "-48% 0px -48% 0px" }); 88 sections.forEach(function (s) { spy.observe(s); }); 89 } 90 91 /* ============================================================== reveal */ 92 var revealables = $$(".rise, .fade"); 93 if ("IntersectionObserver" in window && !reduce) { 94 var rev = new IntersectionObserver(function (entries, obs) { 95 entries.forEach(function (en) { 96 if (!en.isIntersecting) return; 97 en.target.classList.add("is-in"); 98 obs.unobserve(en.target); 99 }); 100 }, { rootMargin: "0px 0px -10% 0px", threshold: 0.08 }); 101 revealables.forEach(function (el) { rev.observe(el); }); 102 } else { 103 revealables.forEach(function (el) { el.classList.add("is-in"); }); 104 } 105 106 /* ==================================================== waveform players */ 107 /* Peaks are baked at build time from the real recordings, so the shape is 108 on screen before a single byte of audio is fetched. */ 109 var players = []; 110 var live = null; 111 112 function fmt(t) { 113 if (!isFinite(t)) t = 0; 114 var m = Math.floor(t / 60), s = Math.floor(t % 60); 115 return m + ":" + (s < 10 ? "0" : "") + s; 116 } 117 118 function Player(root) { 119 var self = this; 120 this.root = root; 121 this.canvas = $(".wave__canvas", root); 122 this.ctx = this.canvas.getContext("2d"); 123 this.track = $(".wave__track", root); 124 this.btn = $(".wave__play", root); 125 this.time = $(".wave__time", root); 126 this.peaks = (root.dataset.peaks || "").split(",").map(Number); 127 this.duration = parseFloat(root.dataset.duration) || 0; 128 this.src = root.dataset.src; 129 this.audio = null; 130 this.progress = 0; 131 this.raf = 0; 132 133 this.colors(); 134 this.resize(); 135 addEventListener("resize", function () { self.resize(); }); 136 137 this.btn.addEventListener("click", function () { self.toggle(); }); 138 139 var seek = function (e) { 140 var r = self.track.getBoundingClientRect(); 141 var p = Math.min(1, Math.max(0, (e.clientX - r.left) / r.width)); 142 self.progress = p; 143 if (self.audio && self.audio.duration) self.audio.currentTime = p * self.audio.duration; 144 self.draw(); 145 self.label(); 146 }; 147 this.track.addEventListener("click", seek); 148 this.track.addEventListener("keydown", function (e) { 149 if (e.key === "Enter" || e.key === " ") { e.preventDefault(); self.toggle(); } 150 if (e.key === "ArrowRight" || e.key === "ArrowLeft") { 151 e.preventDefault(); 152 var d = (e.key === "ArrowRight" ? 5 : -5); 153 if (self.audio && self.audio.duration) { 154 self.audio.currentTime = Math.min(self.audio.duration, Math.max(0, self.audio.currentTime + d)); 155 self.progress = self.audio.currentTime / self.audio.duration; 156 self.draw(); self.label(); 157 } 158 } 159 }); 160 161 this.label(); 162 } 163 164 Player.prototype.colors = function () { 165 var cs = getComputedStyle(this.root); 166 this.cIdle = cs.getPropertyValue("--wave-idle").trim() || "rgba(139,144,156,.55)"; 167 this.cLive = cs.getPropertyValue("--wave-live").trim() || "#E3A03C"; 168 }; 169 170 Player.prototype.resize = function () { 171 var dpr = Math.min(2, devicePixelRatio || 1); 172 var r = this.track.getBoundingClientRect(); 173 if (!r.width) return; 174 this.canvas.width = Math.round(r.width * dpr); 175 this.canvas.height = Math.round(r.height * dpr); 176 this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0); 177 this.w = r.width; this.h = r.height; 178 this.draw(); 179 }; 180 181 Player.prototype.draw = function () { 182 if (!this.w) return; 183 var ctx = this.ctx, n = this.peaks.length; 184 var gap = 2, bar = Math.max(1.5, (this.w - gap * (n - 1)) / n); 185 var mid = this.h / 2, cut = this.progress * this.w; 186 187 ctx.clearRect(0, 0, this.w, this.h); 188 for (var i = 0; i < n; i++) { 189 var x = i * (bar + gap); 190 var hh = Math.max(1.5, this.peaks[i] * (this.h - 6)); 191 ctx.fillStyle = (x + bar / 2) <= cut ? this.cLive : this.cIdle; 192 ctx.fillRect(x, mid - hh / 2, bar, hh); 193 } 194 }; 195 196 Player.prototype.label = function () { 197 var d = (this.audio && this.audio.duration) || this.duration; 198 var cur = this.progress * d; 199 this.time.textContent = (this.progress > 0 ? fmt(cur) + " / " : "") + fmt(d); 200 }; 201 202 Player.prototype.tick = function () { 203 var self = this; 204 if (!this.audio) return; 205 this.progress = this.audio.duration ? this.audio.currentTime / this.audio.duration : 0; 206 this.draw(); 207 this.label(); 208 this.raf = requestAnimationFrame(function () { self.tick(); }); 209 }; 210 211 Player.prototype.toggle = function () { 212 var self = this; 213 if (live && live !== this) live.pause(); 214 215 if (!this.audio) { 216 this.audio = new Audio(); 217 this.audio.preload = "none"; 218 this.audio.src = this.src; 219 this.audio.addEventListener("ended", function () { self.pause(true); }); 220 /* rAF is frozen in background tabs; timeupdate keeps the readout honest */ 221 this.audio.addEventListener("timeupdate", function () { 222 if (!self.audio.duration) return; 223 self.progress = self.audio.currentTime / self.audio.duration; 224 self.label(); 225 }); 226 this.audio.addEventListener("loadedmetadata", function () { self.label(); }); 227 this.audio.addEventListener("error", function () { 228 self.pause(); 229 self.time.textContent = "â"; 230 }); 231 } 232 if (this.audio.paused) { 233 if (this.progress >= 0.999) this.progress = 0; 234 if (this.audio.duration) this.audio.currentTime = this.progress * this.audio.duration; 235 this.audio.play().then(function () { 236 live = self; 237 self.root.classList.add("is-playing"); 238 self.btn.setAttribute("aria-label", self.btn.dataset.pause || "Pause"); 239 var li = self.root.closest("li"); 240 if (li) li.classList.add("is-live"); 241 self.tick(); 242 }).catch(function () { self.pause(); }); 243 } else { 244 this.pause(); 245 } 246 }; 247 248 Player.prototype.pause = function (reset) { 249 if (this.audio) this.audio.pause(); 250 cancelAnimationFrame(this.raf); 251 this.root.classList.remove("is-playing"); 252 this.btn.setAttribute("aria-label", this.btn.dataset.play || "Play"); 253 var li = this.root.closest("li"); 254 if (li) li.classList.remove("is-live"); 255 if (reset) { this.progress = 0; this.draw(); this.label(); } 256 if (live === this) live = null; 257 }; 258 259 $$(".wave").forEach(function (el) { players.push(new Player(el)); }); 260 261 addEventListener("load", function () {
262 players.forEach(function (p) { p.resize(); }); 263 }); 264 document.addEventListener("visibilitychange", function () { 265 if (document.hidden) return; 266 players.forEach(function (p) { p.draw(); p.label(); }); 267 if (live) live.tick(); 268 }); 269 270 /* =================================================== justified gallery */ 271 function layout(grid) { 272 var items = [].slice.call(grid.children); 273 var width = grid.clientWidth; 274 if (!items.length || !width) return; 275 276 var gap = parseFloat(getComputedStyle(grid).gap) || 6; 277 var ideal = width < 700 ? 180 : width < 1000 ? 230 : 300; 278 var row = [], sum = 0; 279 280 function flush(last) { 281 if (!row.length) return; 282 var avail = width - gap * (row.length - 1); 283 var h = avail / sum; 284 if (last && h > ideal * 1.35) h = ideal; 285 var used = 0; 286 row.forEach(function (it, i) { 287 var w = (i === row.length - 1 && !last) ? avail - used : Math.floor(it.r * h); 288 used += w; 289 it.el.style.width = w + "px"; 290 it.el.style.height = Math.round(h) + "px"; 291 }); 292 row = []; sum = 0; 293 } 294 295 items.forEach(function (el) { 296 var r = parseFloat(el.dataset.ratio) || 1; 297 row.push({ el: el, r: r }); 298 sum += r; 299 if (sum * ideal >= width - gap * (row.length - 1)) flush(false); 300 }); 301 flush(true); 302 } 303 304 var grids = $$(".gal__grid"); 305 function layoutAll() { grids.forEach(function (g) { if (!g.hidden) layout(g); }); } 306 layoutAll(); 307 308 var rafL; 309 addEventListener("resize", function () { 310 cancelAnimationFrame(rafL); 311 rafL = requestAnimationFrame(layoutAll); 312 }); 313 314 $$(".gal__bar").forEach(function (bar) { 315 bar.addEventListener("click", function (e) { 316 var btn = e.target.closest(".gal__tab"); 317 if (!btn) return; 318 $$(".gal__tab", bar).forEach(function (b) { 319 b.setAttribute("aria-selected", b === btn ? "true" : "false"); 320 }); 321 grids.forEach(function (g) { 322 g.hidden = g.id !== btn.dataset.target; 323 if (!g.hidden) layout(g); 324 }); 325 }); 326 }); 327 328 /* ============================================================ lightbox */ 329 var lb = $(".lb"); 330 if (lb) { 331 var stage = $(".lb__stage", lb); 332 var count = $(".lb__count", lb); 333 var prevBtn = $(".lb__prev", lb); 334 var nextBtn = $(".lb__next", lb); 335 var set = [], idx = 0, restore = null; 336 337 /* Stop a self-hosted clip at once â the stage is only emptied after the 338 fade-out, and a still-playing video would keep sounding through it. */ 339 function stopMedia() { 340 var vd = $("video", stage); 341 if (vd) { vd.pause(); vd.removeAttribute("src"); vd.load(); } 342 } 343 344 function render() { 345 var it = set[idx]; 346 stopMedia(); 347 stage.innerHTML = ""; 348 if (it.file) { 349 var vd = document.createElement("video"); 350 vd.src = it.file; 351 if (it.poster) vd.poster = it.poster; 352 vd.controls = true; 353 vd.autoplay = true; 354 vd.playsInline = true; 355 vd.preload = "metadata"; 356 vd.setAttribute("controlslist", "nodownload"); 357 vd.title = it.alt || "Video"; 358 stage.appendChild(vd); 359 } else if (it.video) { 360 var f = document.createElement("iframe"); 361 f.src = "https://www.youtube-nocookie.com/embed/" + it.video + "?autoplay=1&rel=0"; 362 f.allow = "accelerometer; autoplay; encrypted-media; picture-in-picture; fullscreen"; 363 f.allowFullscreen = true; 364 f.title = it.alt || "Video"; 365 stage.appendChild(f); 366 } else { 367 var img = new Image(); 368 img.src = it.src; 369 img.alt = it.alt || ""; 370 stage.appendChild(img); 371 } 372 count.textContent = set.length > 1 ? (idx + 1) + " / " + set.length : ""; 373 prevBtn.hidden = nextBtn.hidden = set.length < 2; 374 } 375 376 function open(items, start) { 377 if (live) live.pause(); 378 if (document.body.classList.contains("menu-open")) setMenu(false); 379 restore = document.activeElement; 380 set = items; idx = start; 381 lb.classList.add("is-open"); 382 requestAnimationFrame(function () { lb.classList.add("is-vis"); }); 383 document.documentElement.style.overflow = "hidden"; 384 render(); 385 $(".lb__close", lb).focus(); 386 } 387 388 function close() { 389 stopMedia(); 390 lb.classList.remove("is-vis"); 391 setTimeout(function () { lb.classList.remove("is-open"); stage.innerHTML = ""; }, 350); 392 document.documentElement.style.overflow = ""; 393 if (restore) restore.focus(); 394 } 395 396 function step(d) { idx = (idx + d + set.length) % set.length; render(); } 397 398 lb.addEventListener("click", function (e) { 399 if (e.target === lb || e.target.closest(".lb__close")) return close(); 400 if (e.target.closest(".lb__prev")) return step(-1); 401 if (e.target.closest(".lb__next")) return step(1); 402 }); 403 addEventListener("keydown", function (e) { 404 if (!lb.classList.contains("is-open")) return; 405 if (e.key === "Escape") close(); 406 else if (e.key === "ArrowLeft") step(-1); 407 else if (e.key === "ArrowRight") step(1); 408 else if (e.key === "Tab") { 409 var f = $$("button:not([hidden])", lb); 410 var first = f[0], last = f[f.length - 1]; 411 if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); } 412 else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); } 413 } 414 }); 415 416 grids.forEach(function (g) { 417 g.addEventListener("click", function (e) { 418 var a = e.target.closest(".gal__item"); 419 if (!a) return; 420 e.preventDefault(); 421 var all = $$(".gal__item", g); 422 open(all.map(function (l) { 423 var i = $("img", l); 424 return { src: l.href, alt: i ? i.alt : "" }; 425 }), all.indexOf(a)); 426 }); 427 }); 428 429 $$(".film__row").forEach(function (row) { 430 row.addEventListener("click", function (e) { 431 var b = e.target.closest(".film__item"); 432 if (!b) return; 433 var all = $$(".film__item", row); 434 open(all.map(function (x) { 435 return { 436 video: x.dataset.video,
437 file: x.dataset.file, 438 poster: x.dataset.poster, 439 alt: x.getAttribute("aria-label") 440 }; 441 }), all.indexOf(b)); 442 }); 443 }); 444 } 445 446 /* ================================================================ form */ 447 var form = $(".form"); 448 if (form) { 449 var status = $(".form__status", form); 450 var send = $(".send", form); 451 452 /* contact.php only accepts a message carrying a token it issued, which 453 costs a round trip and means running this script -- the form-spam bots 454 hitting the endpoint just POST the field names blind and never get one. 455 The token is requested as soon as someone starts filling the form in, 456 and must be a few seconds old when it comes back: a person spends that 457 long typing, a script does not. If no token can be had we say so rather 458 than posting into a void, so a real enquiry is never lost silently. */ 459 var tokenReq = null; 460 var getToken = function () { 461 if (!tokenReq) { 462 tokenReq = fetch(form.action + "?token=1", { headers: { Accept: "application/json" } }) 463 .then(function (r) { return r.ok ? r.json() : null; }) 464 .then(function (d) { return d && d.token ? { v: d.token, at: Date.now() } : null; }) 465 .catch(function () { tokenReq = null; return null; }); 466 } 467 return tokenReq; 468 }; 469 form.addEventListener("focusin", getToken); 470 form.addEventListener("input", getToken); 471 472 var valid = function () { 473 var ok = true; 474 $$("[data-required]", form).forEach(function (f) { 475 var err = $('[data-err="' + f.name + '"]', form); 476 var msg = ""; 477 if (!f.value.trim()) msg = form.dataset.msgRequired; 478 else if (f.type === "email" && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(f.value)) msg = form.dataset.msgEmail; 479 if (err) err.textContent = msg; 480 if (msg) ok = false; 481 }); 482 return ok; 483 }; 484 485 form.addEventListener("submit", function (e) { 486 e.preventDefault(); 487 status.textContent = ""; 488 if (!valid()) return; 489 490 var data = new FormData(form); 491 data.append("lang", form.dataset.lang || "de"); 492 send.disabled = true; 493 status.textContent = form.dataset.msgSending; 494 495 getToken() 496 .then(function (tok) { 497 if (!tok) throw new Error("no token"); 498 data.append("t", tok.v); 499 // hold back until the token is comfortably past the server's floor 500 var wait = 4000 - (Date.now() - tok.at); 501 return wait > 0 ? new Promise(function (r) { setTimeout(r, wait); }) : null; 502 }) 503 .then(function () { return fetch(form.action, { method: "POST", body: data }); }) 504 .then(function (r) { return r.ok ? r.json() : Promise.reject(r); }) 505 .then(function (res) { 506 if (!res || !res.ok) throw new Error("rejected"); 507 status.textContent = form.dataset.msgOk; 508 form.reset(); 509 tokenReq = null; // one token per message 510 }) 511 .catch(function () { status.textContent = form.dataset.msgErr; }) 512 .then(function () { send.disabled = false; }); 513 }); 514 } 515})();
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.