PageSourceSearch

https://rorybowcott.com/hero-motion.js?v=22

js rorybowcott.com collected 2026-10-07 03:25:42 UTC 11,043 bytes, 237 lines download raw bytes

1function setupHeroImageMotion() {
2  const heroSection = document.querySelector(".home-about-section");
3  const heroImage = heroSection?.querySelector(".about-image img");
4
5  if (!heroSection || !heroImage) {
6    return;
7  }
8
9  const heroImageWrap = heroImage.parentElement;
10  const splatter = document.createElement("div");
11  splatter.className = "hero-splatter";
12  splatter.setAttribute("aria-hidden", "true");
13  splatter.innerHTML = `
14  <img src="public/Images/me_splat_2.webp" alt="">
15    <img src="public/Images/me_splat_1.webp" alt="">
16  `;
17  heroImageWrap.insertBefore(splatter, heroImage);
18
19  const desktopInteraction = window.matchMedia("(min-width: 821px) and (pointer: fine)");
20
21  if (!desktopInteraction.matches) {
22    splatter.classList.add("is-visible");
23    heroSection.style.setProperty("--hero-image-scale", "0.98");
24    heroSection.style.setProperty("--hero-shadow-opacity", "0");
25    heroSection.style.setProperty("--hero-splatter-scale", "1");
26    return;
27  }
28
29  let splatSound;
30
31  const maxMove = {
32    x: 8,
33    y: 8,
34  };
35
36  const current = {
37    splatterScale: 1.2,
38    scale: 1,
39    shadow: 0.25,
40    x: 0,
41    y: 0,
42  };
43  const target = {
44    splatterScale: 1.2,
45    scale: 1,
46    shadow: 0.25,
47    x: 0,
48    y: 0,
49  };
50  const ease = 0.08;
51  const pressDownEase = 1;
52  const releaseEase = 0.15;
53  const alphaMask = {
54    width: 128,
55    height: 192,
56    bounds: {
57      minX: 10,
58      minY: 12,
59      maxX: 118,
60      maxY: 188,
61    },
62    data: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAMAwAAAAAAAAAAAAAAAAAAkf/gAAAAAAAAAAAAAAAAAJ//8AAAAAAAAAAAAAAAAAB///gAAAAAAAAAAAAAAAAD///8AAAAAAAAAAAAAAAAB////hwAAAAAAAAAAAAAAB////+/AAAAAAAAAAAAAAA//////4AAAAAAAAAAAAAAP/////+AAAAAAAAAAAAAAL//////yAAAAAAAAAAAAAB///////AAAAAAAAAAAAAAf//////wAAAAAAAAAAAAAP//////+gAAAAAAAAAAAAD///////gAAAAAAAAAAAAA///////4AAAAAAAAAAAAAf///////QAAAAAAAAAAAAH///////0AAAAAAAAAAADH///////8AAAAAAAAAAAAp////////gAAAAAAAAAAAG////////4AAAAAAAAAAAEv///////+AAAAAAAAAAAAT////////wAAAAAAAAAAAH////////8AAAAAAAAAAAB////////+wAAAAAAAAAAA/////////sAAAAAAAAAAAP////////4AAAAAAAAAAAD/////////AAAAAAAAAAAA/////////wAAAAAAAAAAAH////////8AAAAAAAAAAAA////////+AAAAAAAAAAAAP////////wAAAAAAAAAAAD////////4AAAAAAAAAAAA////////+AAAAAAAAAAAAP////////xAAAAAAAAAAAD////////8AAAAAAAAAAAA/////////AAAAAAAAAAAAD////////AAAAAAAAAAAAAf///////gAAAAAAAAAAAAH///////4gAAAAAAAAAAAN///////+YAAAAAAAAAAAB////////gAAAAAAAAAAAAD///////+AAAAAAAAAAAAB///////+AAAAAAAAAAAAAf///////gAAAAAAAAAAAAH///////4AAAAAAAAAAAAA///////+gAAAAAAAAAAAAP///////IAAAAAAAAAAAAF///////wAAAAAAAAAAAAB///////4AAAAAAAAAAAABv///////gAAAAAAAAAAAAb///////YAAAAAAAAAAAAC///////wYAAAAAAAAAAAAf//////4CAAAAAAAAAAAAH//////+AAAAAAAAAAAAAD//////8AAAAAAAAAAAAAAv//////sIAAAAAAAAAAACD///////AAAAAAAAAAAABi//////4AAAAAAAAAAAAAc//////+YgAAAAAAAAAAAD///////YYAAAAAAAAAAAAP//////+nAAAAAAAAAAAAH///////DQAAAAAAAAAAAb///////2wAAAAAAAAAAAH///////wgAAAAAAAAAAAA3///////SAAAAAAAAAAAAP///////BgAAAAAAAAAAAE///////zgAAAAAAAAAAAA////////egAAAAAAAAAAA////////8oAAAAAAAAAABP////////wAAAAAAAAAAAd////////8AAAAAAAAAAAD/////////sAAAAAAAAAAA/////////zAAAAAAAAAAA/////////wAAAAAAAAAAD/////////+wAAAAAAAAAD//////////8AAAAAAAAADf/////////+AAAAAAAAAA3//////////4AAAAAAAAAP///////////AAAAAAAAAH///////////wAAAAAAAAD///////////8AAAAAAAAA////////////wAAAAAAAAf///////////8AAAAAAAAH////////////AAAAAAAAH////////////wAAAAAAAH////////////+AAAAAAAJ/////////////gAAAAAAC/////////////4AAAAAAAf/////////////AAAAAAB3/////////////wAAAAAAf/////////////8AAAAAAz//////////////AAAAAAA//////////////4AAAAAAH/////////////+AAAAAAb//////////////gAAAAAX//////////////6AAAAAA//////////////+AAAAAAf//////////////gAAAAAP//////////////4AAAAAD//////////////+AAAAAA///////////////gAAAAAP//////////////4AAAAAL//////////////+AAAAAA///////////////gAAAAAf//////////////wAAAAAv//////
62////////8AAAAAf///////////////AAAAAB///////////////wAAAAAf//////////////8AAAAAH///////////////AAAAAB///////////////gAAAAAf//////////////4AAAAAP///////////////AAAAAD///////////////wAAAAw///////////////9gAAAMf///////////////QAAAAH///////////////wAAAAP///////////////8AAAAD////////////////AAAAAP///////////////wAAAAP///////////////8AAAAB///////////////+AAAAA////////////////gAAAAP///////////////4AAAAD///////////////+gAAAB////////////////gAAAFf///////////////4AAABn///////////////+AAAAD////////////////gAAAIf///////////////4AAADv///////////////+AAAAf////////////////gAAAH////////////////4AAAAf///////////////+AAAAP////////////////gAAAb////////////////4AAAP////////////////+AAAD3////////////////gAAAb////////////////8AAAB/////////////////AAABv////////////////wAAAL////////////////8AAAH/////////////////gAAA/////////////////4AAAP////////////////+AAAF/////////////////gAAAP////////////////4AAAPf///////////////+AAAD/////////////////gAAAf////////////////4AAAH////////////////+AAAJ/////////////////gAAAf/v//////////////gAAAHNb//////////////4AAABhW//////////////8AAAAYFOPs7////
62//////+AAAAABAn+B/////////9/AAAAAA0Afgf////////+fwAAAAAEMEwP/////////HwAAAAAADCABv//f/////xsAAAAAAAAgAL/uz/////+DAAAAAAAAAAANn8I/////mwAAAAAAAAAAAAGED/j/+xsAAAAAAAAAQADAgBfjP/wDAAAAAAAAAAAAAAChwxDoAgAAAAAAAAAAAAAAgYII6AAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
63  };
64  const alphaBytes = Uint8Array.from(atob(alphaMask.data), (character) => character.charCodeAt(0));
65  let isPressed = false;
66
67  function parseObjectPositionValue(value) {
68    if (value === "left" || value === "top") {
69      return 0;
70    }
71
72    if (value === "right" || value === "bottom") {
73      return 1;
74    }
75
76    if (value === "center") {
77      return 0.5;
78    }
79
80    if (value.endsWith("%")) {
81      return Number.parseFloat(value) / 100;
82    }
83
84    return 0.5;
85  }
86
87  function getObjectPosition() {
88    const values = window.getComputedStyle(heroImage).objectPosition.split(/\s+/);
89
90    if (values.length === 1) {
91      const value = values[0];
92
93      if (value === "top" || value === "bottom") {
94        return { x: 0.5, y: parseObjectPositionValue(value) };
95      }
96
97      return { x: parseObjectPositionValue(value), y: 0.5 };
98    }
99
100    return {
101      x: parseObjectPositionValue(values[0]),
102      y: parseObjectPositionValue(values[1]),
103    };
104  }
105
106  function getRenderedImageBox(rect) {
107    const naturalRatio = heroImage.naturalWidth / heroImage.naturalHeight;
108    const boxRatio = rect.width / rect.height;
109    const position = getObjectPosition();
110    let width = rect.width;
111    let height = rect.height;
112    let left = 0;
113    let top = 0;
114
115    if (naturalRatio > boxRatio) {
116      height = rect.width / naturalRatio;
117      top = (rect.height - height) * position.y;
118    } else {
119      width = rect.height * naturalRatio;
120      left = (rect.width - width) * position.x;
121    }
122
123    return { left, top, width, height };
124  }
125
126  function updateTransformOrigin() {
127    if (heroImage.naturalWidth === 0 || heroImage.naturalHeight === 0) {
128      return;
129    }
130
131    const imageBox = getRenderedImageBox({
132      width: heroImage.offsetWidth,
133      height: heroImage.offsetHeight,
134    });
135    const centerX = ((alphaMask.bounds.minX + alphaMask.bounds.maxX) / 2 / alphaMask.width) * imageBox.width;
136    const centerY = ((alphaMask.bounds.minY + alphaMask.bounds.maxY) / 2 / alphaMask.height) * imageBox.height;
137
138    heroSection.style.setProperty("--hero-origin-x", `${(imageBox.left + centerX).toFixed(2)}px`);
139    heroSection.style.setProperty("--hero-origin-y", `${(imageBox.top + centerY).toFixed(2)}px`);
140  }
141
142  function isMaskPixelOpaque(maskX, maskY) {
143    const bitIndex = maskY * alphaMask.width + maskX;
144    const byte = alphaBytes[Math.floor(bitIndex / 8)];
145    const bit = 7 - (bitIndex % 8);
146
147    return ((byte >> bit) & 1) === 1;
148  }
149
150  function isOpaquePixel(event) {
151    if (heroImage.naturalWidth === 0 || heroImage.naturalHeight === 0) {
152      return false;
153    }
154
155    const bounds = heroImage.getBoundingClientRect();
156    const imageBox = getRenderedImageBox(bounds);
157    const localX = event.clientX - bounds.left - imageBox.left;
158    const localY = event.clientY - bounds.top - imageBox.top;
159
160    if (localX < 0 || localY < 0 || localX >= imageBox.width || localY >= imageBox.height) {
161      return false;
162    }
163
164    const maskX = Math.floor((localX / imageBox.width) * alphaMask.width);
165    const maskY = Math.floor((localY / imageBox.height) * alphaMask.height);
166
167    if (maskX < 0 || maskY < 0 || maskX >= alphaMask.width || maskY >= alphaMask.height) {
168      return false;
169    }
170
171    return isMaskPixelOpaque(maskX, maskY);
172  }
173
174  function moveImage(event) {
175    if (isPressed) {
176      return;
177    }
178
179    const bounds = heroSection.getBoundingClientRect();
180    const cursorX = Math.min(Math.max((event.clientX - bounds.left) / bounds.width, 0), 1);
181    const cursorY = Math.min(Math.max((event.clientY - bounds.top) / bounds.height, 0), 1);
182
183    target.x = (cursorX - 0.5) * 2 * maxMove.x;
184    target.y = (cursorY - 0.5) * 2 * maxMove.y;
185  }
186
187  function animateImage() {
188    current.x += (target.x - current.x) * ease;
189    current.y += (target.y - current.y) * ease;
190    const slapEase = isPressed ? pressDownEase : releaseEase;
191
192    current.scale += (target.scale - current.scale) * slapEase;
193    current.shadow += (target.shadow - current.shadow) * slapEase;
194
195    heroSection.style.setProperty("--hero-image-x", `${current.x.toFixed(2)}px`);
196    heroSection.style.setProperty("--hero-image-y", `${current.y.toFixed(2)}px`);
197    heroSection.style.setProperty("--hero-image-scale", current.scale.toFixed(3));
198    heroSection.style.setProperty("--hero-shadow-opacity", current.shadow.toFixed(3));
199    heroSection.style.setProperty("--hero-splatter-scale", current.splatterScale.toFixed(3));
200    requestAnimationFrame(animateImage);
201  }
202
203  function pressImage(event) {
204    if (isPressed || !isOpaquePixel(event)) {
205      return;
206    }
207
208    if (!splatSound) {
209      splatSound = new Audio("public/Audio/squishy-slap.mp3");
210      splatSound.preload = "none";
211      splatSound.volume = 0.75;
212    }
213
214    splatSound.currentTime = 0;
215    splatSound.play().catch(() => {});
216    isPressed = true;
217    splatter.classList.add("is-visible");
218    current.splatterScale = 1;
219    target.scale = 0.98;
220    target.shadow = 0;
221    target.splatterScale = 1;
222  }
223
224  function updateCursor(event) {
225    heroImage.style.cursor = isOpaquePixel(event) ? "pointer" : "default";
226  }
227
228  document.addEventListener("mousemove", moveImage);
229  heroImage.addEventListener("mousemove", updateCursor);
230  heroImage.addEventListener("pointerdown", pressImage);
231  heroImage.addEventListener("load", updateTransformOrigin, { once: true });
232  window.addEventListener("resize", updateTransformOrigin);
233  updateTransformOrigin();
234  animateImage();
235}
236
237setupHeroImageMotion();

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.