From 0812a5a6b83090e366691fcc694cdfcbd75da2b8 Mon Sep 17 00:00:00 2001 From: Spedon <70063177+Sped0n@users.noreply.github.com> Date: Tue, 6 Feb 2024 23:12:44 +0800 Subject: [PATCH] feat: loading transition (#277) * refactor: change hires loader function name * feat: add loading transition animation and improve performance * refactor: refactor mutation handling in desktop codebase - Modify the `initStage` function in `assets/ts/desktop/stage.ts`: - Change the `onMutation` callback to accept a single mutation instead of an array of mutations. - Update the conditions inside the callback to use `hold` instead of `skip`. - Modify the `onMutation` function in `assets/ts/desktop/utils.ts`: - Change the `callback` parameter to `trigger`. - Update the implementation of the function to iterate over each mutation and check if it triggers the `trigger` function. If it does, disconnect the observer and break the loop. * style: refactor state section and remove unnecessary code - Remove the declaration of `lastIndex` on line 21 - Add a comment block for the state section - Add a declaration of `lastIndex` for the state section * refactor: refactor mobile collection and intersection functions - Modify the `initCollection` function in `assets/ts/mobile/collection.ts` - Remove the nested loop in the `initCollection` function - Modify the `onIntersection` function in `assets/ts/mobile/utils.ts` - Replace the callback parameter with a trigger parameter in the `onIntersection` function - Remove the nested loop in the `onIntersection` function * refactor: refactor Watchable class constructor to include lazy parameter - Add a second parameter `lazy` in the constructor of the `Watchable` class in `globalUtils.ts` - Set the default value of `lazy` to `true` in the constructor - Add a condition to check if `e` is equal to `this.obj` and `this.lazy` is `true` to return in `watch` - Delete the previous constructor definition in the `Watchable` class in `globalUtils.ts` * fix: set state's lazy param to false * refactor: refactor third party lib import --- assets/ts/desktop/stage.ts | 112 ++++++++++++++++++++------------- assets/ts/desktop/utils.ts | 9 ++- assets/ts/globalState.ts | 2 +- assets/ts/globalUtils.ts | 13 ++-- assets/ts/mobile/collection.ts | 21 +++---- assets/ts/mobile/gallery.ts | 39 +++++++----- assets/ts/mobile/utils.ts | 9 ++- 7 files changed, 125 insertions(+), 80 deletions(-) diff --git a/assets/ts/desktop/stage.ts b/assets/ts/desktop/stage.ts index 013f930..f96df68 100644 --- a/assets/ts/desktop/stage.ts +++ b/assets/ts/desktop/stage.ts @@ -1,4 +1,4 @@ -import { type Power3, type gsap } from 'gsap' +import { type gsap } from 'gsap' import { container } from '../container' import { incIndex, isAnimating, navigateVector, state } from '../globalState' @@ -17,7 +17,10 @@ let imgs: DesktopImage[] = [] let last = { x: 0, y: 0 } let _gsap: typeof gsap -let _Power3: typeof Power3 + +/** + * state + */ let gsapLoaded = false @@ -85,33 +88,43 @@ function setPositions(): void { const elsTrail = getImagesWithIndexArray(trailElsIndex) + // cached state + const _isOpen = isOpen.get() + const _cordHist = cordHist.get() + const _state = state.get() + _gsap.set(elsTrail, { - x: (i: number) => cordHist.get()[i].x - window.innerWidth / 2, - y: (i: number) => cordHist.get()[i].y - window.innerHeight / 2, + x: (i: number) => _cordHist[i].x - window.innerWidth / 2, + y: (i: number) => _cordHist[i].y - window.innerHeight / 2, opacity: (i: number) => - i + 1 + state.get().trailLength <= cordHist.get().length ? 0 : 1, + Math.max( + (i + 1 + _state.trailLength <= _cordHist.length ? 0 : 1) - (_isOpen ? 1 : 0), + 0 + ), zIndex: (i: number) => i, scale: 0.6 }) - if (isOpen.get()) { + if (_isOpen) { const elc = getImagesWithIndexArray([getCurrentElIndex()])[0] - elc.classList.add('hide') // hide image to prevent flash const indexArrayToHires: number[] = [] + const indexArrayToCleanup: number[] = [] switch (navigateVector.get()) { case 'prev': indexArrayToHires.push(getPrevElIndex()) + indexArrayToCleanup.push(getNextElIndex()) break case 'next': indexArrayToHires.push(getNextElIndex()) + indexArrayToCleanup.push(getPrevElIndex()) break default: break } hires(getImagesWithIndexArray(indexArrayToHires)) // preload - setLoaderForImage(elc) - _gsap.set(imgs, { opacity: 0 }) - _gsap.set(elc, { opacity: 1, x: 0, y: 0, scale: 1 }) + _gsap.set(getImagesWithIndexArray(indexArrayToCleanup), { opacity: 0 }) + _gsap.set(elc, { x: 0, y: 0, scale: 1 }) // set current to center + setLoaderForHiresImage(elc) // set loader, if loaded set current opacity to 1 } else { lores(elsTrail) } @@ -130,14 +143,14 @@ function expandImage(): void { // elc.classList.add('hide') hires(getImagesWithIndexArray([elcIndex, getPrevElIndex(), getNextElIndex()])) - setLoaderForImage(elc) + setLoaderForHiresImage(elc) const tl = _gsap.timeline() const trailInactiveEls = getImagesWithIndexArray(getTrailInactiveElsIndex()) // move down and hide trail inactive tl.to(trailInactiveEls, { y: '+=20', - ease: _Power3.easeIn, + ease: 'power3.in', stagger: 0.075, duration: 0.3, delay: 0.1, @@ -147,7 +160,7 @@ function expandImage(): void { tl.to(elc, { x: 0, y: 0, - ease: _Power3.easeInOut, + ease: 'power3.inOut', duration: 0.7, delay: 0.3 }) @@ -155,7 +168,7 @@ function expandImage(): void { tl.to(elc, { delay: 0.1, scale: 1, - ease: _Power3.easeInOut + ease: 'power3.inOut' }) // finished tl.then(() => { @@ -184,20 +197,20 @@ export function minimizeImage(): void { tl.to(elc, { scale: 0.6, duration: 0.6, - ease: _Power3.easeInOut + ease: 'power3.inOut' }) // move current to original position tl.to(elc, { delay: 0.3, duration: 0.7, - ease: _Power3.easeInOut, + ease: 'power3.inOut', x: cordHist.get()[cordHist.get().length - 1].x - window.innerWidth / 2, y: cordHist.get()[cordHist.get().length - 1].y - window.innerHeight / 2 }) // show trail inactive tl.to(elsTrailInactive, { y: '-=20', - ease: _Power3.easeOut, + ease: 'power3.out', stagger: -0.1, duration: 0.3, opacity: 1 @@ -227,23 +240,20 @@ export function initStage(ijs: ImageJSON[]): void { img.src = img.dataset.loUrl } // lores preloader for rest of the images - onMutation(img, (mutations, observer) => { - mutations.every((mutation) => { - // if open or animating, skip - if (isOpen.get() || isAnimating.get()) return true - // if mutation is not about style attribute, skip - if (mutation.attributeName !== 'style') return true - const opacity = parseFloat(img.style.opacity) - // if opacity is not 1, skip - if (opacity !== 1) return true - // preload the i + 5th image - if (i + 5 < imgs.length) { - imgs[i + 5].src = imgs[i + 5].dataset.loUrl - } - // disconnect observer and return false to break the loop - observer.disconnect() - return false - }) + onMutation(img, (mutation) => { + // if open or animating, hold + if (isOpen.get() || isAnimating.get()) return false + // if mutation is not about style attribute, hold + if (mutation.attributeName !== 'style') return false + const opacity = parseFloat(img.style.opacity) + // if opacity is not 1, hold + if (opacity !== 1) return false + // preload the i + 5th image, if it exists + if (i + 5 < imgs.length) { + imgs[i + 5].src = imgs[i + 5].dataset.loUrl + } + // triggered + return true }) }) // event listeners @@ -331,35 +341,53 @@ function lores(imgs: DesktopImage[]): void { }) } -function setLoaderForImage(e: HTMLImageElement): void { +function setLoaderForHiresImage(e: HTMLImageElement): void { if (!e.complete) { isLoading.set(true) e.addEventListener( 'load', () => { - isLoading.set(false) - e.classList.remove('hide') + _gsap + .to(e, { opacity: 1, ease: 'power3.out', duration: 0.5 }) + .then(() => { + isLoading.set(false) + }) + .catch((e) => { + console.log(e) + }) }, { once: true, passive: true } ) e.addEventListener( 'error', () => { - isLoading.set(false) + _gsap + .set(e, { opacity: 1 }) + .then(() => { + isLoading.set(false) + }) + .catch((e) => { + console.log(e) + }) }, { once: true, passive: true } ) } else { - e.classList.remove('hide') - isLoading.set(false) + _gsap + .set(e, { opacity: 1 }) + .then(() => { + isLoading.set(false) + }) + .catch((e) => { + console.log(e) + }) } } function loadLib(): void { loadGsap() .then((g) => { - _gsap = g[0] - _Power3 = g[1] + _gsap = g gsapLoaded = true }) .catch((e) => { diff --git a/assets/ts/desktop/utils.ts b/assets/ts/desktop/utils.ts index 50b4d02..eff3b23 100644 --- a/assets/ts/desktop/utils.ts +++ b/assets/ts/desktop/utils.ts @@ -19,10 +19,15 @@ export interface DesktopImage extends HTMLImageElement { export function onMutation( element: T, - callback: (arg0: MutationRecord[], arg1: MutationObserver) => void, + trigger: (arg0: MutationRecord) => boolean, observeOptions: MutationObserverInit = { attributes: true } ): void { new MutationObserver((mutations, observer) => { - callback(mutations, observer) + for (const mutation of mutations) { + if (trigger(mutation)) { + observer.disconnect() + break + } + } }).observe(element, observeOptions) } diff --git a/assets/ts/globalState.ts b/assets/ts/globalState.ts index 30650ad..56461f6 100644 --- a/assets/ts/globalState.ts +++ b/assets/ts/globalState.ts @@ -31,7 +31,7 @@ const defaultState = { trailLength: thresholds[getThresholdSessionIndex()].trailLength } -export const state = new Watchable(defaultState) +export const state = new Watchable(defaultState, false) export const isAnimating = new Watchable(false) export const navigateVector = new Watchable('none') diff --git a/assets/ts/globalUtils.ts b/assets/ts/globalUtils.ts index 433c590..6fa22ec 100644 --- a/assets/ts/globalUtils.ts +++ b/assets/ts/globalUtils.ts @@ -1,4 +1,4 @@ -import { type Power3, type gsap } from 'gsap' +import { type gsap } from 'gsap' /** * utils @@ -16,9 +16,9 @@ export function expand(num: number): string { return ('0000' + num.toString()).slice(-4) } -export async function loadGsap(): Promise<[typeof gsap, typeof Power3]> { +export async function loadGsap(): Promise { const g = await import('gsap') - return [g.gsap, g.Power3] + return g.gsap } export function getThresholdSessionIndex(): number { @@ -37,7 +37,11 @@ export function removeDuplicates(arr: T[]): T[] { */ export class Watchable { - constructor(private obj: T) {} + constructor( + private obj: T, + private readonly lazy: boolean = true + ) {} + private readonly watchers: Array<(arg0: T) => void> = [] get(): T { @@ -45,6 +49,7 @@ export class Watchable { } set(e: T): void { + if (e === this.obj && this.lazy) return this.obj = e this.watchers.forEach((watcher) => { watcher(this.obj) diff --git a/assets/ts/mobile/collection.ts b/assets/ts/mobile/collection.ts index 0c7865b..c787fbf 100644 --- a/assets/ts/mobile/collection.ts +++ b/assets/ts/mobile/collection.ts @@ -64,18 +64,15 @@ export function initCollection(ijs: ImageJSON[]): void { { passive: true } ) // preload - onIntersection(img, (entries, observer) => { - entries.every((entry) => { - // no intersection, skip - if (entry.intersectionRatio <= 0) return true - // preload the i + 5th image - if (i + 5 < imgs.length) { - imgs[i + 5].src = imgs[i + 5].dataset.src - } - // disconnect observer and return false to break the loop - observer.disconnect() - return false - }) + onIntersection(img, (entry) => { + // no intersection, hold + if (entry.intersectionRatio <= 0) return false + // preload the i + 5th image, if it exists + if (i + 5 < imgs.length) { + imgs[i + 5].src = imgs[i + 5].dataset.src + } + // triggered + return true }) }) } diff --git a/assets/ts/mobile/gallery.ts b/assets/ts/mobile/gallery.ts index 290da84..b2c4552 100644 --- a/assets/ts/mobile/gallery.ts +++ b/assets/ts/mobile/gallery.ts @@ -1,4 +1,4 @@ -import { type Power3, type gsap } from 'gsap' +import { type gsap } from 'gsap' import { type Swiper } from 'swiper' import { container, scrollable } from '../container' @@ -17,16 +17,18 @@ import { capitalizeFirstLetter, loadSwiper, type MobileImage } from './utils' let swiperNode: HTMLDivElement let gallery: HTMLDivElement let curtain: HTMLDivElement -let swiper: Swiper -let lastIndex = -1 let indexDispNums: HTMLSpanElement[] = [] let galleryImages: MobileImage[] = [] let collectionImages: MobileImage[] = [] -let _Swiper: typeof Swiper let _gsap: typeof gsap -let _Power3: typeof Power3 +let _swiper: Swiper +/** + * state + */ + +let lastIndex = -1 let libLoaded = false /** @@ -44,7 +46,7 @@ export function slideUp(): void { _gsap.to(gallery, { y: 0, - ease: _Power3.easeInOut, + ease: 'power3.inOut', duration: 1, delay: 0.4 }) @@ -63,7 +65,7 @@ function slideDown(): void { _gsap.to(gallery, { y: '100%', - ease: _Power3.easeInOut, + ease: 'power3.inOut', duration: 1 }) @@ -128,17 +130,15 @@ export function initGallery(ijs: ImageJSON[]): void { () => { loadGsap() .then((g) => { - _gsap = g[0] - _Power3 = g[1] + _gsap = g }) .catch((e) => { console.log(e) }) loadSwiper() - .then((s) => { - _Swiper = s - swiper = new _Swiper(swiperNode, { spaceBetween: 20 }) - swiper.on('slideChange', ({ realIndex }) => { + .then((S) => { + _swiper = new S(swiperNode, { spaceBetween: 20 }) + _swiper.on('slideChange', ({ realIndex }) => { setIndex(realIndex) }) }) @@ -159,7 +159,7 @@ export function initGallery(ijs: ImageJSON[]): void { function changeSlide(slide: number): void { galleryLoadImages() - swiper.slideTo(slide, 0) + _swiper.slideTo(slide, 0) } function scrollToActive(): void { @@ -237,13 +237,18 @@ function createGallery(ijs: ImageJSON[]): void { e.height = ij.hiImgH e.width = ij.hiImgW e.alt = ij.alt - e.classList.add('hide') + e.style.opacity = '0' // load event e.addEventListener( 'load', () => { - e.classList.remove('hide') - l.classList.add('hide') + if (state.get().index !== ij.index) { + _gsap.set(e, { opacity: 1 }) + _gsap.set(l, { opacity: 0 }) + } else { + _gsap.to(e, { opacity: 1, delay: 0.5, duration: 0.5, ease: 'power3.out' }) + _gsap.to(l, { opacity: 0, duration: 0.5, ease: 'power3.in' }) + } }, { once: true, passive: true } ) diff --git a/assets/ts/mobile/utils.ts b/assets/ts/mobile/utils.ts index 79a624a..e6b4a4f 100644 --- a/assets/ts/mobile/utils.ts +++ b/assets/ts/mobile/utils.ts @@ -20,10 +20,15 @@ export function getRandom(min: number, max: number): number { export function onIntersection( element: T, - callback: (arg0: IntersectionObserverEntry[], arg1: IntersectionObserver) => void + trigger: (arg0: IntersectionObserverEntry) => boolean ): void { new IntersectionObserver((entries, observer) => { - callback(entries, observer) + for (const entry of entries) { + if (trigger(entry)) { + observer.disconnect() + break + } + } }).observe(element) }