Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
265 changes: 172 additions & 93 deletions static/scripts/hero-glass.js
Original file line number Diff line number Diff line change
@@ -1,99 +1,178 @@
const root = document.documentElement;
const hero = document.querySelector("#hero");
const tiltTargets = document.querySelectorAll("[data-tilt]");
const floaters = document.querySelectorAll("[data-float]");

let frame;
let heroInView = true;
let heroRect = null;
let heroRectDirty = true;
const pointer = {
x: window.innerWidth / 2,
y: window.innerHeight / 2,
};

const scheduleUpdateScene = () => {
if (!frame) {
frame = requestAnimationFrame(updateScene);
}
};

const refreshHeroRect = () => {
if (!hero || !heroInView) return;
heroRect = hero.getBoundingClientRect();
heroRectDirty = false;
};

const updateScene = () => {
root.style.setProperty("--cursor-x", `${pointer.x}px`);
root.style.setProperty("--cursor-y", `${pointer.y}px`);

if (hero && heroInView) {
if (heroRectDirty || !heroRect) {
refreshHeroRect();
(() => {
"use strict";

const root = document.documentElement;
const hero = document.querySelector("#hero");
const tiltTargets = document.querySelectorAll("[data-tilt]");
const floaters = document.querySelectorAll("[data-float]");

const state = {
pointer: {
x: window.innerWidth / 2,
y: window.innerHeight / 2
},
visibleTargets: new Set(),
rectCache: new Map(),
frame: null,
isDirty: true
};

/**
* Intersection Observer
* Tracks elements that are visible.
*/
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {

if (entry.isIntersecting) {
state.visibleTargets.add(entry.target);
state.isDirty = true;
} else {
state.visibleTargets.delete(entry.target);
state.rectCache.delete(entry.target);

entry.target.style.setProperty("--tilt-x", "0deg");
entry.target.style.setProperty("--tilt-y", "0deg");
}

});
}, { threshold: 0.1 });

if (hero) observer.observe(hero);
tiltTargets.forEach(el => observer.observe(el));

/**
* Resize Observer
* Marks layout as dirty only when elements actually resize.
*/
const resizeObserver = new ResizeObserver(() => {
state.isDirty = true;
});

if (hero) resizeObserver.observe(hero);
tiltTargets.forEach(el => resizeObserver.observe(el));

/**
* GPU hint for smoother animations
*/
tiltTargets.forEach(el => {
el.style.willChange = "transform";
});

/**
* Measurement Phase (Read)
*/
const refreshRects = () => {
state.visibleTargets.forEach(el => {
state.rectCache.set(el, el.getBoundingClientRect());
});

state.isDirty = false;
};

/**
* Render Phase (Write)
*/
const updateScene = () => {
root.style.setProperty("--cursor-x", `${state.pointer.x}px`);
root.style.setProperty("--cursor-y", `${state.pointer.y}px`);

if (state.visibleTargets.size === 0) {
state.frame = null;
return;
}
const relX = (pointer.x - heroRect.left) / heroRect.width - 0.5;
const relY = (pointer.y - heroRect.top) / heroRect.height - 0.5;
hero.style.setProperty("--tilt-x", `${(-relY * 7).toFixed(2)}deg`);
hero.style.setProperty("--tilt-y", `${(relX * 9).toFixed(2)}deg`);
}

tiltTargets.forEach((target) => {
const rect = target.getBoundingClientRect();
const relX = (pointer.x - rect.left) / rect.width - 0.5;
const relY = (pointer.y - rect.top) / rect.height - 0.5;
target.style.setProperty("--tilt-x", `${(-relY * 6).toFixed(2)}deg`);
target.style.setProperty("--tilt-y", `${(relX * 6).toFixed(2)}deg`);

if (state.isDirty) refreshRects();

state.visibleTargets.forEach((el) => {

const rect = state.rectCache.get(el);
if (!rect) return;

const relX = (state.pointer.x - rect.left) / rect.width - 0.5;
const relY = (state.pointer.y - rect.top) / rect.height - 0.5;

const intensity = el === hero ? 10 : 6;

const tiltX = Math.round(-relY * intensity * 100) / 100;
const tiltY = Math.round(relX * intensity * 100) / 100;

el.style.setProperty("--tilt-x", `${tiltX}deg`);
el.style.setProperty("--tilt-y", `${tiltY}deg`);
});

state.frame = null;
};

const scheduleUpdate = () => {
if (!state.frame) {
state.frame = requestAnimationFrame(updateScene);
}
};

/**
* Pointer Movement
*/
window.addEventListener("pointermove", (e) => {

if (
e.clientX === state.pointer.x &&
e.clientY === state.pointer.y
) return;

state.pointer.x = e.clientX;
state.pointer.y = e.clientY;

scheduleUpdate();

}, { passive: true });

/**
* Touch / pointer down responsiveness
*/
window.addEventListener("pointerdown", (e) => {
state.pointer.x = e.clientX;
state.pointer.y = e.clientY;
scheduleUpdate();
}, { passive: true });

/**
* Reset pointer when leaving window
*/
window.addEventListener("pointerleave", () => {
state.pointer.x = window.innerWidth / 2;
state.pointer.y = window.innerHeight / 2;
scheduleUpdate();
});

frame = null;
};

const handlePointer = (event) => {
pointer.x = event.clientX;
pointer.y = event.clientY;
scheduleUpdateScene();
};

window.addEventListener("pointermove", handlePointer, { passive: true });
window.addEventListener("pointerdown", handlePointer, { passive: true });
window.addEventListener("pointerleave", () => {
pointer.x = window.innerWidth / 2;
pointer.y = window.innerHeight / 2;
scheduleUpdateScene();
});
window.addEventListener("resize", () => {
pointer.x = window.innerWidth / 2;
pointer.y = window.innerHeight / 2;
heroRectDirty = true;
scheduleUpdateScene();
}, { passive: true });
window.addEventListener("scroll", () => {
heroRectDirty = true;
}, { passive: true });

if (hero) {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
heroInView = entry.isIntersecting;
if (!heroInView) {
hero.style.setProperty("--tilt-x", "0deg");
hero.style.setProperty("--tilt-y", "0deg");
heroRect = null;
} else {
heroRectDirty = true;
scheduleUpdateScene();
}
/**
* Scroll Throttling
*/
let scrollTick = false;

window.addEventListener("scroll", () => {

if (!scrollTick) {
scrollTick = true;

requestAnimationFrame(() => {
state.isDirty = true;
scrollTick = false;
});
},
{ threshold: 0.2 },
);
observer.observe(hero);
}
}

}, { passive: true });

/**
* Floater Animation Delays
*/
floaters.forEach((item, index) => {
item.style.animationDelay = `${index * -0.5}s`;
});

floaters.forEach((item, index) => {
item.style.animationDelay = `${index * -2.5}s`;
});
/**
* Initial Run
*/
updateScene();

updateScene();
})();
Loading