PageSourceSearch

https://www.readingtheworldpodcast.com/static/js/pv-hero-motion.2f219bb77669.js

js readingtheworldpodcast.com collected 2026-10-04 07:33:31 UTC 5,142 bytes, 121 lines download raw bytes

1/* PodView custom-hero motion layer.
2 *
3 * Small, always-shipped, no dependencies. Activates ONLY on a custom hero that
4 * opts in via `data-hero-motion` (an allowlist set server-side in theme_tags.py).
5 * The sandboxed hero blocks never ship JS themselves; this trusted module is the
6 * only script, so there is no block-authored code-execution surface.
7 *
8 * Effects (space-separated tokens):
9 *   tilt      - the cover image tilts in 3D toward the pointer
10 *   parallax  - foreground copy drifts opposite the pointer for depth
11 *   spotlight - a soft radial highlight follows the pointer
12 *
13 * Respects prefers-reduced-motion and only runs on pointer-capable devices.
14 */
15(function () {
16  "use strict";
17
18  var reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
19  var fine = window.matchMedia && window.matchMedia("(pointer: fine)").matches;
20  if (reduce || !fine) return;
21
22  function ready(fn) {
23    if (document.readyState !== "loading") fn();
24    else document.addEventListener("DOMContentLoaded", fn);
25  }
26
27  function firstBg(hero) {
28    return hero.querySelector('[class$="-bg"], [class*="-art"] img, [class*="-screen"] img, [class*="-photo"] img, [class*="-panel"] img, [class*="-disc"] img');
29  }
30
31  function initHero(hero) {
32    // Idempotence guard. This module was DOM-ready-only, so re-running it after a
33    // boosted swap would bind a SECOND pointermove/enter/leave set (and insert a
34    // second spotlight span) on every navigation back to a hero page.
35    if (hero.dataset.pvHeroMotionReady) return;
36    hero.dataset.pvHeroMotionReady = "1";
37
38    var effects = (hero.getAttribute("data-hero-motion") || "").split(/\s+/);
39    var wantTilt = effects.indexOf("tilt") !== -1;
40    var wantParallax = effects.indexOf("parallax") !== -1;
41    var wantSpotlight = effects.indexOf("spotlight") !== -1;
42
43    var art = (wantTilt || wantParallax) ? firstBg(hero) : null;
44    if (art && wantTilt) {
45      art.style.transformStyle = "preserve-3d";
46      art.style.transition = "transform .18s ease-out";
47      art.style.willChange = "transform";
48    }
49    var copy = wantParallax ? hero.querySelector('[class$="-copy"], [class$="-inner"], [class$="-lead"]') : null;
50    if (copy) copy.style.transition = "transform .2s ease-out";
51
52    if (wantSpotlight) {
53      var glow = document.createElement("span");
54      glow.className = "pv-hero-spotlight";
55      glow.style.cssText =
56        "position:absolute;inset:0;z-index:0;pointer-events:none;opacity:0;" +
57        "transition:opacity .3s ease;mix-blend-mode:soft-light;" +
58        "background:radial-gradient(240px circle at var(--mx,50%) var(--my,50%)," +
59        "rgba(255,255,255,.45),transparent 60%)";
60      if (getComputedStyle(hero).position === "static") hero.style.position = "relative";
61      hero.insertBefore(glow, hero.firstChild);
62      hero._pvGlow = glow;
63    }
64
65    var raf = 0;
66    var lastX = 0, lastY = 0;
67    function apply() {
68      raf = 0;
69      var px = lastX, py = lastY; // -0.5 .. 0.5
70      if (art && wantTilt) {
71        art.style.transform = "perspective(900px) rotateY(" + (px * 10) + "deg) rotateX(" + (-py * 10) + "deg)";
72      }
73      if (copy) {
74        copy.style.transform = "translate3d(" + (-px * 14) + "px," + (-py * 10) + "px,0)";
75      }
76      if (hero._pvGlow) {
77        hero.style.setProperty("--mx", (px + 0.5) * 100 + "%");
78        hero.style.setProperty("--my", (py + 0.5) * 100 + "%");
79      }
80    }
81    function onMove(e) {
82      var r = hero.getBoundingClientRect();
83      lastX = (e.clientX - r.left) / r.width - 0.5;
84      lastY = (e.clientY - r.top) / r.height - 0.5;
85      if (!raf) raf = requestAnimationFrame(apply);
86    }
87    function onEnter() { if (hero._pvGlow) hero._pvGlow.style.opacity = "1"; }
88    function onLeave() {
89      if (hero._pvGlow) hero._pvGlow.style.opacity = "0";
90      if (art && wantTilt) art.style.transform = "";
91      if (copy) copy.style.transform = "";
92    }
93    hero.addEventListener("pointermove", onMove);
94    hero.addEventListener("pointerenter", onEnter);
95    hero.addEventListener("pointerleave", onLeave);
96  }
97
98  function initAll(root) {
99    var scope = root && root.querySelectorAll ? root : document;
100    var heroes = scope.querySelectorAll(".pv-custom-hero[data-hero-motion]");
101    for (var i = 0; i < heroes.length; i++) initHero(heroes[i]);
102  }
103
104  ready(function () {
105    initAll(document);
106  });
107
108  // Boosted navigation swaps a new hero in. `historyRestore` matters too: Back
109  // restores cached markup and fires only that event (pv-boost-nav.js clears
110  // data-pv-hero-motion-ready first, so this re-binds rather than no-ops).
111  document.addEventListener("htmx:afterSwap", function () {
112    // Always the document, never `evt.detail.target`. Under an `outerHTML` swap —
113    // which is exactly what the boost contract uses — detail.target is the OLD
114    // element, already detached, so scoping to it queries dead DOM and binds
115    // nothing. The guard attribute makes a document-wide pass cheap and idempotent.
116    initAll(document);
117  });
118  document.addEventListener("htmx:historyRestore", function () {
119    initAll(document);
120  });
121})();

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.