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 { 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
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, skip
if (opacity !== 1) return true
// preload the i + 5th image
// 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
}
// disconnect observer and return false to break the loop
observer.disconnect()
return false
})
// 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',
() => {
_gsap
.to(e, { opacity: 1, ease: 'power3.out', duration: 0.5 })
.then(() => {
isLoading.set(false)
e.classList.remove('hide')
})
.catch((e) => {
console.log(e)
})
},
{ once: true, passive: true }
)
e.addEventListener(
'error',
() => {
_gsap
.set(e, { opacity: 1 })
.then(() => {
isLoading.set(false)
})
.catch((e) => {
console.log(e)
})
},
{ once: true, passive: true }
)
} else {
e.classList.remove('hide')
_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) => {

View File

@@ -19,10 +19,15 @@ export interface DesktopImage extends HTMLImageElement {
export function onMutation<T extends HTMLElement>(
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)
}

View File

@@ -31,7 +31,7 @@ const defaultState = {
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 navigateVector = new Watchable<NavVec>('none')

View File

@@ -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<typeof gsap> {
const g = await import('gsap')
return [g.gsap, g.Power3]
return g.gsap
}
export function getThresholdSessionIndex(): number {
@@ -37,7 +37,11 @@ export function removeDuplicates<T>(arr: T[]): 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> = []
get(): T {
@@ -45,6 +49,7 @@ export class Watchable<T> {
}
set(e: T): void {
if (e === this.obj && this.lazy) return
this.obj = e
this.watchers.forEach((watcher) => {
watcher(this.obj)

View File

@@ -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
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
}
// disconnect observer and return false to break the loop
observer.disconnect()
return false
})
// triggered
return true
})
})
}

View File

@@ -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 }
)

View File

@@ -20,10 +20,15 @@ export function getRandom(min: number, max: number): number {
export function onIntersection<T extends HTMLElement>(
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)
}