diff --git a/src/app/home/home.component.html b/src/app/home/home.component.html index 0a157c5..0024ab8 100644 --- a/src/app/home/home.component.html +++ b/src/app/home/home.component.html @@ -69,7 +69,7 @@ [src]="currentPortraitHighlight.image" width="560" height="420" - [alt]="global.firstname + ' highlight'" + [alt]="currentPortraitHighlight.text + ' Image'" /> @if (currentPortraitHighlightIndex === 0) { Click me! @@ -82,12 +82,7 @@ -
+

About me

diff --git a/src/app/home/home.component.ts b/src/app/home/home.component.ts index a1d7528..aab5313 100644 --- a/src/app/home/home.component.ts +++ b/src/app/home/home.component.ts @@ -25,10 +25,10 @@ export class HomeComponent implements OnInit, AfterViewInit, OnDestroy { private preloadedImages: HTMLImageElement[] = []; private tallestPortraitAspectRatio: number | null = null; private lockedHeroSectionMinHeight: number = 0; - private projectEntryObserver: IntersectionObserver | null = null; - private aboutSectionObserver: IntersectionObserver | null = null; private scrollAnimationFrameId: number | null = null; private heroHeightAnimationFrameId: number | null = null; + private projectEntryAnimationFrameId: number | null = null; + private aboutSectionAnimationFrameId: number | null = null; private readonly scheduleNameGradientUpdate: () => void = (): void => { if (this.scrollAnimationFrameId !== null || typeof window === 'undefined') { return; @@ -50,14 +50,32 @@ export class HomeComponent implements OnInit, AfterViewInit, OnDestroy { this.updateHeroSectionMinHeight(); }); }; + private readonly scheduleProjectEntryRevealUpdate: () => void = (): void => { + if (this.projectEntryAnimationFrameId !== null || typeof window === 'undefined') { + return; + } + + this.projectEntryAnimationFrameId = window.requestAnimationFrame((): void => { + this.projectEntryAnimationFrameId = null; + this.updateProjectEntryRevealProgress(); + }); + }; + private readonly scheduleAboutSectionScrollAnimationUpdate: () => void = (): void => { + if (this.aboutSectionAnimationFrameId !== null || typeof window === 'undefined') { + return; + } + + this.aboutSectionAnimationFrameId = window.requestAnimationFrame((): void => { + this.aboutSectionAnimationFrameId = null; + this.updateAboutSectionScrollProgress(); + }); + }; protected readonly global: Global = global; protected readonly age: number = this.calculateAge(global.birthdate); protected isLongBioShown: boolean = false; protected isLongBioMounted: boolean = false; - protected isAboutSectionInView: boolean = true; - protected isAboutSectionAnimationReady: boolean = true; protected readonly faArrowRight: IconDefinition = faArrowRight; protected readonly faArrowDown: IconDefinition = faArrowDown; protected readonly faEnvelope: IconDefinition = faEnvelope; @@ -283,50 +301,28 @@ export class HomeComponent implements OnInit, AfterViewInit, OnDestroy { return; } - const projectEntries: NodeListOf = document.querySelectorAll('.project-entry'); - projectEntries.forEach((entry: Element): void => { - entry.classList.add('project-entry-reveal-pending'); - }); - - if (typeof IntersectionObserver === 'undefined') { - projectEntries.forEach((entry: Element): void => { - entry.classList.add('project-entry-visible'); - entry.classList.remove('project-entry-reveal-pending'); - }); - return; - } - - this.projectEntryObserver = new IntersectionObserver( - (entries: IntersectionObserverEntry[]): void => { - for (const entry of entries) { - const element: Element = entry.target; - if (entry.isIntersecting) { - window.requestAnimationFrame((): void => { - element.classList.add('project-entry-visible'); - element.classList.remove('project-entry-reveal-pending'); - }); - continue; - } - - element.classList.add('project-entry-reveal-pending'); - element.classList.remove('project-entry-visible'); - } - }, - { - root: null, - threshold: 0.18, - rootMargin: '0px 0px -8% 0px', - }, - ); - - projectEntries.forEach((entry: Element): void => { - this.projectEntryObserver?.observe(entry); - }); + window.addEventListener('scroll', this.scheduleProjectEntryRevealUpdate, { passive: true }); + window.addEventListener('resize', this.scheduleProjectEntryRevealUpdate, { passive: true }); + this.scheduleProjectEntryRevealUpdate(); } private destroyProjectEntryRevealObserver(): void { - this.projectEntryObserver?.disconnect(); - this.projectEntryObserver = null; + if (typeof window === 'undefined' || typeof document === 'undefined') { + return; + } + + window.removeEventListener('scroll', this.scheduleProjectEntryRevealUpdate); + window.removeEventListener('resize', this.scheduleProjectEntryRevealUpdate); + + if (this.projectEntryAnimationFrameId !== null) { + window.cancelAnimationFrame(this.projectEntryAnimationFrameId); + this.projectEntryAnimationFrameId = null; + } + + const projectEntries: NodeListOf = document.querySelectorAll('.project-entry'); + projectEntries.forEach((entry: HTMLElement): void => { + entry.style.removeProperty('--project-entry-progress'); + }); } private initAboutSectionScrollAnimation(): void { @@ -334,49 +330,30 @@ export class HomeComponent implements OnInit, AfterViewInit, OnDestroy { return; } - const aboutSection: HTMLElement | null = document.getElementById('about'); - if (aboutSection === null) { - return; - } - - if (typeof IntersectionObserver === 'undefined') { - return; - } - - this.aboutSectionObserver = new IntersectionObserver( - (entries: IntersectionObserverEntry[]): void => { - for (const entry of entries) { - if (!entry.isIntersecting) { - this.zone.run((): void => { - this.isAboutSectionInView = false; - }); - continue; - } - - this.zone.run((): void => { - this.isAboutSectionInView = false; - }); - - window.requestAnimationFrame((): void => { - this.zone.run((): void => { - this.isAboutSectionInView = true; - }); - }); - } - }, - { - root: null, - threshold: 0.05, - rootMargin: '0px 0px -6% 0px', - }, - ); - - this.aboutSectionObserver.observe(aboutSection); + window.addEventListener('scroll', this.scheduleAboutSectionScrollAnimationUpdate, { + passive: true, + }); + window.addEventListener('resize', this.scheduleAboutSectionScrollAnimationUpdate, { + passive: true, + }); + this.scheduleAboutSectionScrollAnimationUpdate(); } private destroyAboutSectionScrollAnimation(): void { - this.aboutSectionObserver?.disconnect(); - this.aboutSectionObserver = null; + if (typeof window === 'undefined' || typeof document === 'undefined') { + return; + } + + window.removeEventListener('scroll', this.scheduleAboutSectionScrollAnimationUpdate); + window.removeEventListener('resize', this.scheduleAboutSectionScrollAnimationUpdate); + + if (this.aboutSectionAnimationFrameId !== null) { + window.cancelAnimationFrame(this.aboutSectionAnimationFrameId); + this.aboutSectionAnimationFrameId = null; + } + + const aboutSection: HTMLElement | null = document.getElementById('about'); + aboutSection?.style.removeProperty('--about-scroll-progress'); } private destroyNameGradientScrollAnimation(): void { @@ -474,6 +451,46 @@ export class HomeComponent implements OnInit, AfterViewInit, OnDestroy { ); } + private updateAboutSectionScrollProgress(): void { + if (typeof window === 'undefined') { + return; + } + + const aboutSection: HTMLElement | null = document.getElementById('about'); + if (aboutSection === null) { + return; + } + + const sectionRect: DOMRect = aboutSection.getBoundingClientRect(); + const viewportHeight: number = window.innerHeight || 1; + const startY: number = viewportHeight * 0.96; + const endY: number = viewportHeight * 0.46; + const totalDistance: number = Math.max(startY - endY, 1); + const rawProgress: number = (startY - sectionRect.top) / totalDistance; + const clampedProgress: number = Math.min(Math.max(rawProgress, 0), 1); + + aboutSection.style.setProperty('--about-scroll-progress', clampedProgress.toFixed(4)); + } + + private updateProjectEntryRevealProgress(): void { + if (typeof window === 'undefined' || typeof document === 'undefined') { + return; + } + + const projectEntries: NodeListOf = document.querySelectorAll('.project-entry'); + const viewportHeight: number = window.innerHeight || 1; + const startY: number = viewportHeight * 0.98; + const endY: number = viewportHeight * 0.56; + const totalDistance: number = Math.max(startY - endY, 1); + + projectEntries.forEach((entry: HTMLElement): void => { + const entryRect: DOMRect = entry.getBoundingClientRect(); + const rawProgress: number = (startY - entryRect.top) / totalDistance; + const clampedProgress: number = Math.min(Math.max(rawProgress, 0), 1); + entry.style.setProperty('--project-entry-progress', clampedProgress.toFixed(4)); + }); + } + private calculateAge(birthDateString: string): number { const birthDate: Date = new Date(birthDateString); const now: Date = new Date(); diff --git a/src/styles.scss b/src/styles.scss index 4a58e2d..aed467d 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -483,59 +483,30 @@ hr { margin-top: var(--space-5); } +.about-scroll-anim-enabled { + --about-scroll-progress: 0; +} + .about-scroll-anim-enabled .story-copy, .about-scroll-anim-enabled .story-detail { - opacity: 1; - transform: translate3d(0, 0, 0); + opacity: calc(0.12 + (var(--about-scroll-progress) * 0.88)); + will-change: opacity, transform; } -.about-scroll-anim-enabled:not(.about-scroll-anim-ready) .story-copy, -.about-scroll-anim-enabled:not(.about-scroll-anim-ready) .story-detail { - opacity: 1; - transform: translate3d(0, 0, 0); +.about-scroll-anim-enabled .story-copy { + transform: translate3d( + calc((1 - var(--about-scroll-progress)) * -44px), + calc((1 - var(--about-scroll-progress)) * 26px), + 0 + ); } -.about-scroll-anim-enabled.about-scroll-anim-ready .story-copy, -.about-scroll-anim-enabled.about-scroll-anim-ready .story-detail { - opacity: 1; - transform: translate3d(0, 0, 0); -} - -.about-scroll-anim-enabled.about-scroll-anim-ready.about-scroll-in-view .story-copy, -.about-scroll-anim-enabled.about-scroll-anim-ready.about-scroll-in-view .story-detail { - animation-duration: 680ms; - animation-fill-mode: both; - animation-timing-function: cubic-bezier(0.16, 0.84, 0.24, 1); -} - -.about-scroll-anim-enabled.about-scroll-anim-ready.about-scroll-in-view .story-copy { - animation-name: about-copy-reveal; -} - -.about-scroll-anim-enabled.about-scroll-anim-ready.about-scroll-in-view .story-detail { - animation-name: about-detail-reveal; -} - -@keyframes about-copy-reveal { - from { - opacity: 0.12; - transform: translate3d(-44px, 26px, 0); - } - to { - opacity: 1; - transform: translate3d(0, 0, 0); - } -} - -@keyframes about-detail-reveal { - from { - opacity: 0.12; - transform: translate3d(44px, 26px, 0); - } - to { - opacity: 1; - transform: translate3d(0, 0, 0); - } +.about-scroll-anim-enabled .story-detail { + transform: translate3d( + calc((1 - var(--about-scroll-progress)) * 44px), + calc((1 - var(--about-scroll-progress)) * 26px), + 0 + ); } .story-copy p:first-child, @@ -678,6 +649,7 @@ hr { } .project-entry { + --project-entry-progress: 0; display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; @@ -685,33 +657,49 @@ hr { padding: clamp(1.4rem, 3vw, 2rem); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: var(--radius-lg); - opacity: 1; - transform: translate3d(0, 0, 0); + opacity: calc(0.12 + (var(--project-entry-progress) * 0.88)); transition: - opacity 520ms ease, - transform 620ms cubic-bezier(0.2, 0.8, 0.2, 1), border-color var(--transition-fast), background var(--transition-fast), box-shadow var(--transition-fast); + will-change: opacity, transform; } -.project-entry-reveal-pending.project-entry-from-right { - opacity: 0; - transform: translate3d(54px, 16px, 0); +.project-entry.project-entry-from-right { + transform: translate3d( + calc((1 - var(--project-entry-progress)) * 54px), + calc((1 - var(--project-entry-progress)) * 16px), + 0 + ); } -.project-entry-reveal-pending.project-entry-from-left { - opacity: 0; - transform: translate3d(-54px, 16px, 0); +.project-entry.project-entry-from-left { + transform: translate3d( + calc((1 - var(--project-entry-progress)) * -54px), + calc((1 - var(--project-entry-progress)) * 16px), + 0 + ); } -.project-entry-visible { - opacity: 1; - transform: translate3d(0, 0, 0); +.project-entry.project-entry-from-right:hover { + transform: translate3d( + calc((1 - var(--project-entry-progress)) * 54px), + calc((1 - var(--project-entry-progress)) * 16px - 2px), + 0 + ); + border-color: rgba(255, 255, 255, 0.24); + background: linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.04)); + box-shadow: + 0 0 0 1px rgba(255, 255, 255, 0.26), + var(--shadow-soft); } -.project-entry:hover { - transform: translateY(-2px); +.project-entry.project-entry-from-left:hover { + transform: translate3d( + calc((1 - var(--project-entry-progress)) * -54px), + calc((1 - var(--project-entry-progress)) * 16px - 2px), + 0 + ); border-color: rgba(255, 255, 255, 0.24); background: linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.04)); box-shadow: