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.