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
This commit is contained in:
Spedon
2024-02-06 23:12:44 +08:00
committed by GitHub
parent 7fd971eb13
commit 0812a5a6b8
7 changed files with 125 additions and 80 deletions

View File

@@ -1,4 +1,4 @@
import { type Power3, type gsap } from 'gsap' import { type gsap } from 'gsap'
import { container } from '../container' import { container } from '../container'
import { incIndex, isAnimating, navigateVector, state } from '../globalState' import { incIndex, isAnimating, navigateVector, state } from '../globalState'
@@ -17,7 +17,10 @@ let imgs: DesktopImage[] = []
let last = { x: 0, y: 0 } let last = { x: 0, y: 0 }
let _gsap: typeof gsap let _gsap: typeof gsap
let _Power3: typeof Power3
/**
* state
*/
let gsapLoaded = false let gsapLoaded = false
@@ -85,33 +88,43 @@ function setPositions(): void {
const elsTrail = getImagesWithIndexArray(trailElsIndex) const elsTrail = getImagesWithIndexArray(trailElsIndex)
// cached state
const _isOpen = isOpen.get()
const _cordHist = cordHist.get()
const _state = state.get()
_gsap.set(elsTrail, { _gsap.set(elsTrail, {
x: (i: number) => cordHist.get()[i].x - window.innerWidth / 2, x: (i: number) => _cordHist[i].x - window.innerWidth / 2,
y: (i: number) => cordHist.get()[i].y - window.innerHeight / 2, y: (i: number) => _cordHist[i].y - window.innerHeight / 2,
opacity: (i: number) => 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, zIndex: (i: number) => i,
scale: 0.6 scale: 0.6
}) })
if (isOpen.get()) { if (_isOpen) {
const elc = getImagesWithIndexArray([getCurrentElIndex()])[0] const elc = getImagesWithIndexArray([getCurrentElIndex()])[0]
elc.classList.add('hide') // hide image to prevent flash
const indexArrayToHires: number[] = [] const indexArrayToHires: number[] = []
const indexArrayToCleanup: number[] = []
switch (navigateVector.get()) { switch (navigateVector.get()) {
case 'prev': case 'prev':
indexArrayToHires.push(getPrevElIndex()) indexArrayToHires.push(getPrevElIndex())
indexArrayToCleanup.push(getNextElIndex())
break break
case 'next': case 'next':
indexArrayToHires.push(getNextElIndex()) indexArrayToHires.push(getNextElIndex())
indexArrayToCleanup.push(getPrevElIndex())
break break
default: default:
break break
} }
hires(getImagesWithIndexArray(indexArrayToHires)) // preload hires(getImagesWithIndexArray(indexArrayToHires)) // preload
setLoaderForImage(elc) _gsap.set(getImagesWithIndexArray(indexArrayToCleanup), { opacity: 0 })
_gsap.set(imgs, { opacity: 0 }) _gsap.set(elc, { x: 0, y: 0, scale: 1 }) // set current to center
_gsap.set(elc, { opacity: 1, x: 0, y: 0, scale: 1 }) setLoaderForHiresImage(elc) // set loader, if loaded set current opacity to 1
} else { } else {
lores(elsTrail) lores(elsTrail)
} }
@@ -130,14 +143,14 @@ function expandImage(): void {
// elc.classList.add('hide') // elc.classList.add('hide')
hires(getImagesWithIndexArray([elcIndex, getPrevElIndex(), getNextElIndex()])) hires(getImagesWithIndexArray([elcIndex, getPrevElIndex(), getNextElIndex()]))
setLoaderForImage(elc) setLoaderForHiresImage(elc)
const tl = _gsap.timeline() const tl = _gsap.timeline()
const trailInactiveEls = getImagesWithIndexArray(getTrailInactiveElsIndex()) const trailInactiveEls = getImagesWithIndexArray(getTrailInactiveElsIndex())
// move down and hide trail inactive // move down and hide trail inactive
tl.to(trailInactiveEls, { tl.to(trailInactiveEls, {
y: '+=20', y: '+=20',
ease: _Power3.easeIn, ease: 'power3.in',
stagger: 0.075, stagger: 0.075,
duration: 0.3, duration: 0.3,
delay: 0.1, delay: 0.1,
@@ -147,7 +160,7 @@ function expandImage(): void {
tl.to(elc, { tl.to(elc, {
x: 0, x: 0,
y: 0, y: 0,
ease: _Power3.easeInOut, ease: 'power3.inOut',
duration: 0.7, duration: 0.7,
delay: 0.3 delay: 0.3
}) })
@@ -155,7 +168,7 @@ function expandImage(): void {
tl.to(elc, { tl.to(elc, {
delay: 0.1, delay: 0.1,
scale: 1, scale: 1,
ease: _Power3.easeInOut ease: 'power3.inOut'
}) })
// finished // finished
tl.then(() => { tl.then(() => {
@@ -184,20 +197,20 @@ export function minimizeImage(): void {
tl.to(elc, { tl.to(elc, {
scale: 0.6, scale: 0.6,
duration: 0.6, duration: 0.6,
ease: _Power3.easeInOut ease: 'power3.inOut'
}) })
// move current to original position // move current to original position
tl.to(elc, { tl.to(elc, {
delay: 0.3, delay: 0.3,
duration: 0.7, duration: 0.7,
ease: _Power3.easeInOut, ease: 'power3.inOut',
x: cordHist.get()[cordHist.get().length - 1].x - window.innerWidth / 2, x: cordHist.get()[cordHist.get().length - 1].x - window.innerWidth / 2,
y: cordHist.get()[cordHist.get().length - 1].y - window.innerHeight / 2 y: cordHist.get()[cordHist.get().length - 1].y - window.innerHeight / 2
}) })
// show trail inactive // show trail inactive
tl.to(elsTrailInactive, { tl.to(elsTrailInactive, {
y: '-=20', y: '-=20',
ease: _Power3.easeOut, ease: 'power3.out',
stagger: -0.1, stagger: -0.1,
duration: 0.3, duration: 0.3,
opacity: 1 opacity: 1
@@ -227,23 +240,20 @@ export function initStage(ijs: ImageJSON[]): void {
img.src = img.dataset.loUrl img.src = img.dataset.loUrl
} }
// lores preloader for rest of the images // lores preloader for rest of the images
onMutation(img, (mutations, observer) => { onMutation(img, (mutation) => {
mutations.every((mutation) => { // if open or animating, hold
// if open or animating, skip if (isOpen.get() || isAnimating.get()) return false
if (isOpen.get() || isAnimating.get()) return true // if mutation is not about style attribute, hold
// if mutation is not about style attribute, skip if (mutation.attributeName !== 'style') return false
if (mutation.attributeName !== 'style') return true const opacity = parseFloat(img.style.opacity)
const opacity = parseFloat(img.style.opacity) // if opacity is not 1, hold
// if opacity is not 1, skip if (opacity !== 1) return false
if (opacity !== 1) return true // preload the i + 5th image, if it exists
// preload the i + 5th image if (i + 5 < imgs.length) {
if (i + 5 < imgs.length) { imgs[i + 5].src = imgs[i + 5].dataset.loUrl
imgs[i + 5].src = imgs[i + 5].dataset.loUrl }
} // triggered
// disconnect observer and return false to break the loop return true
observer.disconnect()
return false
})
}) })
}) })
// event listeners // event listeners
@@ -331,35 +341,53 @@ function lores(imgs: DesktopImage[]): void {
}) })
} }
function setLoaderForImage(e: HTMLImageElement): void { function setLoaderForHiresImage(e: HTMLImageElement): void {
if (!e.complete) { if (!e.complete) {
isLoading.set(true) isLoading.set(true)
e.addEventListener( e.addEventListener(
'load', 'load',
() => { () => {
isLoading.set(false) _gsap
e.classList.remove('hide') .to(e, { opacity: 1, ease: 'power3.out', duration: 0.5 })
.then(() => {
isLoading.set(false)
})
.catch((e) => {
console.log(e)
})
}, },
{ once: true, passive: true } { once: true, passive: true }
) )
e.addEventListener( e.addEventListener(
'error', 'error',
() => { () => {
isLoading.set(false) _gsap
.set(e, { opacity: 1 })
.then(() => {
isLoading.set(false)
})
.catch((e) => {
console.log(e)
})
}, },
{ once: true, passive: true } { once: true, passive: true }
) )
} else { } else {
e.classList.remove('hide') _gsap
isLoading.set(false) .set(e, { opacity: 1 })
.then(() => {
isLoading.set(false)
})
.catch((e) => {
console.log(e)
})
} }
} }
function loadLib(): void { function loadLib(): void {
loadGsap() loadGsap()
.then((g) => { .then((g) => {
_gsap = g[0] _gsap = g
_Power3 = g[1]
gsapLoaded = true gsapLoaded = true
}) })
.catch((e) => { .catch((e) => {

View File

@@ -19,10 +19,15 @@ export interface DesktopImage extends HTMLImageElement {
export function onMutation<T extends HTMLElement>( export function onMutation<T extends HTMLElement>(
element: T, element: T,
callback: (arg0: MutationRecord[], arg1: MutationObserver) => void, trigger: (arg0: MutationRecord) => boolean,
observeOptions: MutationObserverInit = { attributes: true } observeOptions: MutationObserverInit = { attributes: true }
): void { ): void {
new MutationObserver((mutations, observer) => { new MutationObserver((mutations, observer) => {
callback(mutations, observer) for (const mutation of mutations) {
if (trigger(mutation)) {
observer.disconnect()
break
}
}
}).observe(element, observeOptions) }).observe(element, observeOptions)
} }

View File

@@ -31,7 +31,7 @@ const defaultState = {
trailLength: thresholds[getThresholdSessionIndex()].trailLength trailLength: thresholds[getThresholdSessionIndex()].trailLength
} }
export const state = new Watchable<State>(defaultState) export const state = new Watchable<State>(defaultState, false)
export const isAnimating = new Watchable<boolean>(false) export const isAnimating = new Watchable<boolean>(false)
export const navigateVector = new Watchable<NavVec>('none') export const navigateVector = new Watchable<NavVec>('none')

View File

@@ -1,4 +1,4 @@
import { type Power3, type gsap } from 'gsap' import { type gsap } from 'gsap'
/** /**
* utils * utils
@@ -16,9 +16,9 @@ export function expand(num: number): string {
return ('0000' + num.toString()).slice(-4) return ('0000' + num.toString()).slice(-4)
} }
export async function loadGsap(): Promise<[typeof gsap, typeof Power3]> { export async function loadGsap(): Promise<typeof gsap> {
const g = await import('gsap') const g = await import('gsap')
return [g.gsap, g.Power3] return g.gsap
} }
export function getThresholdSessionIndex(): number { export function getThresholdSessionIndex(): number {
@@ -37,7 +37,11 @@ export function removeDuplicates<T>(arr: T[]): T[] {
*/ */
export class Watchable<T> { export class Watchable<T> {
constructor(private obj: T) {} constructor(
private obj: T,
private readonly lazy: boolean = true
) {}
private readonly watchers: Array<(arg0: T) => void> = [] private readonly watchers: Array<(arg0: T) => void> = []
get(): T { get(): T {
@@ -45,6 +49,7 @@ export class Watchable<T> {
} }
set(e: T): void { set(e: T): void {
if (e === this.obj && this.lazy) return
this.obj = e this.obj = e
this.watchers.forEach((watcher) => { this.watchers.forEach((watcher) => {
watcher(this.obj) watcher(this.obj)

View File

@@ -64,18 +64,15 @@ export function initCollection(ijs: ImageJSON[]): void {
{ passive: true } { passive: true }
) )
// preload // preload
onIntersection(img, (entries, observer) => { onIntersection(img, (entry) => {
entries.every((entry) => { // no intersection, hold
// no intersection, skip if (entry.intersectionRatio <= 0) return false
if (entry.intersectionRatio <= 0) return true // preload the i + 5th image, if it exists
// preload the i + 5th image if (i + 5 < imgs.length) {
if (i + 5 < imgs.length) { imgs[i + 5].src = imgs[i + 5].dataset.src
imgs[i + 5].src = imgs[i + 5].dataset.src }
} // triggered
// disconnect observer and return false to break the loop return true
observer.disconnect()
return false
})
}) })
}) })
} }

View File

@@ -1,4 +1,4 @@
import { type Power3, type gsap } from 'gsap' import { type gsap } from 'gsap'
import { type Swiper } from 'swiper' import { type Swiper } from 'swiper'
import { container, scrollable } from '../container' import { container, scrollable } from '../container'
@@ -17,16 +17,18 @@ import { capitalizeFirstLetter, loadSwiper, type MobileImage } from './utils'
let swiperNode: HTMLDivElement let swiperNode: HTMLDivElement
let gallery: HTMLDivElement let gallery: HTMLDivElement
let curtain: HTMLDivElement let curtain: HTMLDivElement
let swiper: Swiper
let lastIndex = -1
let indexDispNums: HTMLSpanElement[] = [] let indexDispNums: HTMLSpanElement[] = []
let galleryImages: MobileImage[] = [] let galleryImages: MobileImage[] = []
let collectionImages: MobileImage[] = [] let collectionImages: MobileImage[] = []
let _Swiper: typeof Swiper
let _gsap: typeof gsap let _gsap: typeof gsap
let _Power3: typeof Power3 let _swiper: Swiper
/**
* state
*/
let lastIndex = -1
let libLoaded = false let libLoaded = false
/** /**
@@ -44,7 +46,7 @@ export function slideUp(): void {
_gsap.to(gallery, { _gsap.to(gallery, {
y: 0, y: 0,
ease: _Power3.easeInOut, ease: 'power3.inOut',
duration: 1, duration: 1,
delay: 0.4 delay: 0.4
}) })
@@ -63,7 +65,7 @@ function slideDown(): void {
_gsap.to(gallery, { _gsap.to(gallery, {
y: '100%', y: '100%',
ease: _Power3.easeInOut, ease: 'power3.inOut',
duration: 1 duration: 1
}) })
@@ -128,17 +130,15 @@ export function initGallery(ijs: ImageJSON[]): void {
() => { () => {
loadGsap() loadGsap()
.then((g) => { .then((g) => {
_gsap = g[0] _gsap = g
_Power3 = g[1]
}) })
.catch((e) => { .catch((e) => {
console.log(e) console.log(e)
}) })
loadSwiper() loadSwiper()
.then((s) => { .then((S) => {
_Swiper = s _swiper = new S(swiperNode, { spaceBetween: 20 })
swiper = new _Swiper(swiperNode, { spaceBetween: 20 }) _swiper.on('slideChange', ({ realIndex }) => {
swiper.on('slideChange', ({ realIndex }) => {
setIndex(realIndex) setIndex(realIndex)
}) })
}) })
@@ -159,7 +159,7 @@ export function initGallery(ijs: ImageJSON[]): void {
function changeSlide(slide: number): void { function changeSlide(slide: number): void {
galleryLoadImages() galleryLoadImages()
swiper.slideTo(slide, 0) _swiper.slideTo(slide, 0)
} }
function scrollToActive(): void { function scrollToActive(): void {
@@ -237,13 +237,18 @@ function createGallery(ijs: ImageJSON[]): void {
e.height = ij.hiImgH e.height = ij.hiImgH
e.width = ij.hiImgW e.width = ij.hiImgW
e.alt = ij.alt e.alt = ij.alt
e.classList.add('hide') e.style.opacity = '0'
// load event // load event
e.addEventListener( e.addEventListener(
'load', 'load',
() => { () => {
e.classList.remove('hide') if (state.get().index !== ij.index) {
l.classList.add('hide') _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 } { once: true, passive: true }
) )

View File

@@ -20,10 +20,15 @@ export function getRandom(min: number, max: number): number {
export function onIntersection<T extends HTMLElement>( export function onIntersection<T extends HTMLElement>(
element: T, element: T,
callback: (arg0: IntersectionObserverEntry[], arg1: IntersectionObserver) => void trigger: (arg0: IntersectionObserverEntry) => boolean
): void { ): void {
new IntersectionObserver((entries, observer) => { new IntersectionObserver((entries, observer) => {
callback(entries, observer) for (const entry of entries) {
if (trigger(entry)) {
observer.disconnect()
break
}
}
}).observe(element) }).observe(element)
} }