diff --git a/app/components/ProjectsSection.vue b/app/components/ProjectsSection.vue index 680a4bd..d314270 100644 --- a/app/components/ProjectsSection.vue +++ b/app/components/ProjectsSection.vue @@ -6,6 +6,8 @@ const cardStates = reactive>( Object.fromEntries(resume.projects.map((p) => [p.name, { x: 50, y: 50, hover: false }])), ) +const { registerCard } = useScrollActivatedCards(cardStates) + const onCardMove = (name: string, e: MouseEvent) => { const el = e.currentTarget as HTMLElement const rect = el.getBoundingClientRect() @@ -41,6 +43,7 @@ const onFaviconError = (e: Event) => {
  • >( Object.fromEntries(resume.skills.map((g) => [g.label, { x: 50, y: 50, hover: false }])), ) +const { registerCard } = useScrollActivatedCards(cardStates) + const onCardMove = (label: string, e: MouseEvent) => { const el = e.currentTarget as HTMLElement const rect = el.getBoundingClientRect() @@ -26,6 +28,7 @@ const onCardLeave = (label: string) => {
    registerCard(el, key)" …> + */ +export function useScrollActivatedCards(cardStates: Record) { + const pending = new Map() + const observers: IntersectionObserver[] = [] + + const registerCard = (el: Element | unknown, key: string) => { + if (el instanceof Element) pending.set(key, el) + } + + onMounted(() => { + if (!window.matchMedia('(pointer: coarse)').matches) return + + for (const [key, el] of pending) { + const state = cardStates[key] + if (!state) continue + + const observer = new IntersectionObserver( + ([entry]) => { + if (!entry) return + state.hover = entry.isIntersecting + if (entry.isIntersecting) { + state.x = 50 + state.y = 50 + } + }, + { + // Shrink the effective viewport by 25% from the bottom so cards only + // activate once they've scrolled well clear of the bottom edge. + rootMargin: '0px 0px -25% 0px', + threshold: 0.1, + }, + ) + + observer.observe(el) + observers.push(observer) + } + }) + + onBeforeUnmount(() => { + observers.forEach((o) => o.disconnect()) + }) + + return { registerCard } +}