refactor: refactor the pile of crap I wrote before 🤡 (#259)

* feat: refactor file structure and imports in mobile and desktop components

- Removed the import of `scrollable` from `assets/ts/mobile/scroll.ts`
- Renamed `assets/ts/mobile/mounted.ts` to `assets/ts/mobile/state.ts`
- Changed the import of `active` from `./stage` to `./state` in `assets/ts/desktop/customCursor.ts`
- Changed the import of `active` from `../state` to `../globalState` in `assets/ts/desktop/stage.ts`
- Changed the import of `active` from `./stage` to `./state` in `assets/ts/desktop/stageNav.ts`
- Renamed `assets/ts/state.ts` to `assets/ts/globalState.ts`
- Created a new file `assets/ts/desktop/state.ts`
- Added the interface `HistoryItem` to `assets/ts/desktop/state.ts`
- Added the variables `cordHist`, `isOpen`, `active`, and `isLoading` to `assets/ts/desktop/state.ts`
- Deleted the function `loader` from `assets/ts/desktop/stage.ts` and replaced it with `setLoaderForImage`
- Deleted the import of `./stage` from `assets/ts/desktop/stageNav.ts`
- Added the import of `minimizeImage` from `./stage` in `assets/ts/desktop/stageNav.ts`
- Deleted the import of `./mounted` from `assets/ts/mobile/collection.ts`
- Changed the import of `mounted` from `./mounted` to `./state` in `

* refactor: refactor the `onVisible` function to improve performance

- Modify the type of the `onVisible` function parameter `T` to extend `HTMLElement`
- Change the `entries.forEach` loop in the `onVisible` function to `entries.every`

* feat: add new function for detecting opacity changes in element

- Add a new function `onOpacityOne` in `assets/ts/utils.ts`
- The function uses a `MutationObserver` to check for opacity changes on an element
- When the element's opacity reaches `1`, the provided callback function is called
- The `MutationObserver` is disconnected after the callback is executed

* refactor: refactor function names and parameters in intersection and mutation observers

- Change the function name `onVisible` to `onIntersection`
- Modify the `callback` parameter in the `onIntersection` function to accept `IntersectionObserverEntry[]` and `IntersectionObserver` parameters
- Remove the code block that checks for intersection ratio and immediately calls the `callback` function in the `onIntersection` function
- Modify the function name `onOpacityOne` to `onMutation`
- Modify the `callback` parameter in the `onMutation` function to accept `MutationRecord[]` and `MutationObserver` parameters
- Add a default value for the `observeOptions` parameter in the `onMutation` function

* refactor: refine preload logic on both mobile and desktop

* refactor: refactor import statements and add new files

- The import statement for `Watchable` in `assets/ts/globalState.ts` has been changed from `../utils` to `../globalUtils`
- The import statement for `Watchable` in `assets/ts/desktop/state.ts` has been changed from `../utils` to `../globalUtils`
- The import statement for `decrement` and `increment` in `assets/ts/desktop/stageNav.ts` has been changed from `../utils` to `../globalUtils`
- A new file `utils.ts` has been added in the `assets/ts/desktop` directory
- The import statements for `getRandom`, `onIntersection`, and `type MobileImage` in `assets/ts/mobile/collection.ts` have been changed from `../utils` to `./utils`
- The `imgs` array in `assets/ts/mobile/collection.ts` has been changed from an array of `HTMLImageElement` to an array of `MobileImage`
- The import statements for `expand`, `loadGsap`, `loadSwiper`, and `removeDuplicates` in `assets/ts/mobile/gallery.ts` have been changed from `../utils` to `../globalUtils`
- The import statement for `type MobileImage` in `assets/ts/mobile/gallery.ts` has been changed from `./utils` to `../mobile/utils`
- The `galleryLoadImages` function in `assets/ts/mobile/gallery.ts` has been removed
- A new file `utils.ts`

* refactor: refactor swiper import and functions in mobile and global utils

* refactor: refactor navigation and image loading logic in desktop and mobile

* refactor: remove print function and optimize removeDuplicates return

* refactor: update text variable assignments and attributes

* refactor: update variable types in galleryImages and collectionImages in mobile/gallery.ts

* refactor: refactor variable types for consistency with naming conventions

* refactor: update animation durations and types in gallery functions

* refactor: refactor image loading logic and add console logs

* refactor: refactor sass hierarchy

* refactor: remove console logs in multiple files
This commit is contained in:
Spedon
2024-02-03 23:17:16 +08:00
committed by GitHub
parent 9bfaac25f5
commit 1f65b08b56
18 changed files with 415 additions and 248 deletions

View File

@@ -16,30 +16,30 @@
.galleryInner {
flex: 1;
height: 0;
}
.swiper-slide {
display: flex;
align-items: center;
justify-content: center;
.swiper-slide {
display: flex;
align-items: center;
justify-content: center;
}
img {
width: 100%;
height: 100%;
object-fit: contain;
}
img {
width: 100%;
height: 100%;
object-fit: contain;
}
.loadingText {
position: absolute;
top: 50%;
left: 50%;
transform: translate3d(-50%, -50%, 0);
}
.loadingText {
position: absolute;
top: 50%;
left: 50%;
transform: translate3d(-50%, -50%, 0);
}
.slideContainer {
position: relative;
display: inline-block;
}
}
.slideContainer {
position: relative;
display: inline-block;
}
.nav {

View File

@@ -1,9 +1,24 @@
import { scrollable } from './mobile/scroll'
import { Watchable } from './globalUtils'
export let container: HTMLDivElement
export const scrollable = new Watchable<boolean>(true)
export let container: Container
/**
* interfaces
*/
export interface Container extends HTMLDivElement {
dataset: {
next: string
close: string
prev: string
loading: string
}
}
export function initContainer(): void {
container = document.getElementsByClassName('container').item(0) as HTMLDivElement
container = document.getElementsByClassName('container').item(0) as Container
scrollable.addWatcher((o) => {
if (o) {
container.classList.remove('disableScroll')

View File

@@ -1,6 +1,6 @@
import { container } from '../container'
import { active } from './stage'
import { active } from './state'
/**
* variables

View File

@@ -4,6 +4,10 @@ import { initCustomCursor } from './customCursor'
import { initStage } from './stage'
import { initStageNav } from './stageNav'
/**
* main functions
*/
export function initDesktop(ijs: ImageJSON[]): void {
initCustomCursor()
initStage(ijs)

View File

@@ -1,31 +1,20 @@
import { type Power3, type gsap } from 'gsap'
import { container } from '../container'
import { incIndex, isAnimating, navigateVector, state } from '../globalState'
import { decrement, increment, loadGsap } from '../globalUtils'
import { type ImageJSON } from '../resources'
import { incIndex, state } from '../state'
import { Watchable, decrement, increment, loadGsap } from '../utils'
/**
* types
*/
export interface HistoryItem {
i: number
x: number
y: number
}
import { active, cordHist, isLoading, isOpen } from './state'
// eslint-disable-next-line sort-imports
import { onMutation, type DesktopImage } from './utils'
/**
* variables
*/
let imgs: HTMLImageElement[] = []
let imgs: DesktopImage[] = []
let last = { x: 0, y: 0 }
export const cordHist = new Watchable<HistoryItem[]>([])
export const isOpen = new Watchable<boolean>(false)
export const isAnimating = new Watchable<boolean>(false)
export const active = new Watchable<boolean>(false)
export const isLoading = new Watchable<boolean>(false)
let _gsap: typeof gsap
let _Power3: typeof Power3
@@ -36,45 +25,34 @@ let gsapLoaded = false
* getter
*/
function getElTrail(): HTMLImageElement[] {
return cordHist.get().map((item) => imgs[item.i])
function getTrailElsIndex(): number[] {
return cordHist.get().map((item) => item.i)
}
function getElTrailCurrent(): HTMLImageElement[] {
return getElTrail().slice(-state.get().trailLength)
function getTrailCurrentElsIndex(): number[] {
return getTrailElsIndex().slice(-state.get().trailLength)
}
function getElTrailInactive(): HTMLImageElement[] {
const elTrailCurrent = getElTrailCurrent()
return elTrailCurrent.slice(0, elTrailCurrent.length - 1)
function getTrailInactiveElsIndex(): number[] {
const trailCurrentElsIndex = getTrailCurrentElsIndex()
return trailCurrentElsIndex.slice(0, trailCurrentElsIndex.length - 1)
}
function getElCurrent(): HTMLImageElement {
const elTrail = getElTrail()
return elTrail[elTrail.length - 1]
function getCurrentElIndex(): number {
const trailElsIndex = getTrailElsIndex()
return trailElsIndex[trailElsIndex.length - 1]
}
function getElNextSeven(): HTMLImageElement[] {
function getPrevElIndex(): number {
const c = cordHist.get()
const s = state.get()
const c0 = c.length > 0 ? c[c.length - 1].i : s.index
const els = []
for (let i = 0; i < 7; i++) {
els.push(imgs[increment(c0 + i, s.length)])
}
return els
return decrement(c[c.length - 1].i, s.length)
}
function getElPrev(): HTMLImageElement {
function getNextElIndex(): number {
const c = cordHist.get()
const s = state.get()
return imgs[decrement(c[c.length - 1].i, s.length)]
}
function getElNext(): HTMLImageElement {
const c = cordHist.get()
const s = state.get()
return imgs[increment(c[c.length - 1].i, s.length)]
return increment(c[c.length - 1].i, s.length)
}
/**
@@ -83,7 +61,11 @@ function getElNext(): HTMLImageElement {
// on mouse
function onMouse(e: MouseEvent): void {
if (isOpen.get() || isAnimating.get() || !gsapLoaded) return
if (isOpen.get() || isAnimating.get()) return
if (!gsapLoaded) {
loadLib()
return
}
const cord = { x: e.clientX, y: e.clientY }
const travelDist = Math.hypot(cord.x - last.x, cord.y - last.y)
@@ -96,15 +78,14 @@ function onMouse(e: MouseEvent): void {
}
}
// set image position with gsap
// set image position with gsap (in both stage and navigation)
function setPositions(): void {
const elTrail = getElTrail()
if (elTrail.length === 0 || !gsapLoaded) return
const trailElsIndex = getTrailElsIndex()
if (trailElsIndex.length === 0 || !gsapLoaded) return
// preload
lores(getElNextSeven())
const elsTrail = getImagesWithIndexArray(trailElsIndex)
_gsap.set(elTrail, {
_gsap.set(elsTrail, {
x: (i: number) => cordHist.get()[i].x - window.innerWidth / 2,
y: (i: number) => cordHist.get()[i].y - window.innerHeight / 2,
opacity: (i: number) =>
@@ -114,33 +95,47 @@ function setPositions(): void {
})
if (isOpen.get()) {
lores(getElTrail())
const elc = getElCurrent()
elc.src = '' // reset src to ensure we only display hires images
elc.classList.add('hide')
hires([elc, getElPrev(), getElNext()])
const elc = getImagesWithIndexArray([getCurrentElIndex()])[0]
elc.classList.add('hide') // hide image to prevent flash
const indexArrayToHires: number[] = []
switch (navigateVector.get()) {
case 'prev':
indexArrayToHires.push(getPrevElIndex())
break
case 'next':
indexArrayToHires.push(getNextElIndex())
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 })
loader(elc)
} else {
lores(elsTrail)
}
}
// open image into navigation
function expandImage(): void {
if (isAnimating.get() || !gsapLoaded) return
if (isAnimating.get()) return
isOpen.set(true)
isAnimating.set(true)
const elc = getElCurrent()
// don't clear src here because we want a better transition
const elcIndex = getCurrentElIndex()
const elc = getImagesWithIndexArray([elcIndex])[0]
// don't hide here because we want a better transition
// elc.classList.add('hide')
hires([elc, getElPrev(), getElNext()])
loader(elc)
hires(getImagesWithIndexArray([elcIndex, getPrevElIndex(), getNextElIndex()]))
setLoaderForImage(elc)
const tl = _gsap.timeline()
const trailInactiveEls = getImagesWithIndexArray(getTrailInactiveElsIndex())
// move down and hide trail inactive
tl.to(getElTrailInactive(), {
tl.to(trailInactiveEls, {
y: '+=20',
ease: _Power3.easeIn,
stagger: 0.075,
@@ -149,7 +144,7 @@ function expandImage(): void {
opacity: 0
})
// current move to center
tl.to(getElCurrent(), {
tl.to(elc, {
x: 0,
y: 0,
ease: _Power3.easeInOut,
@@ -157,7 +152,7 @@ function expandImage(): void {
delay: 0.3
})
// current expand
tl.to(getElCurrent(), {
tl.to(elc, {
delay: 0.1,
scale: 1,
ease: _Power3.easeInOut
@@ -172,23 +167,27 @@ function expandImage(): void {
// close navigation and back to stage
export function minimizeImage(): void {
if (isAnimating.get() || !gsapLoaded) return
if (isAnimating.get()) return
isOpen.set(false)
isAnimating.set(true)
navigateVector.set('none') // cleanup
lores([getElCurrent()])
lores(getElTrailInactive())
lores(
getImagesWithIndexArray([...getTrailInactiveElsIndex(), ...[getCurrentElIndex()]])
)
const tl = _gsap.timeline()
const elc = getImagesWithIndexArray([getCurrentElIndex()])[0]
const elsTrailInactive = getImagesWithIndexArray(getTrailInactiveElsIndex())
// shrink current
tl.to(getElCurrent(), {
tl.to(elc, {
scale: 0.6,
duration: 0.6,
ease: _Power3.easeInOut
})
// move current to original position
tl.to(getElCurrent(), {
tl.to(elc, {
delay: 0.3,
duration: 0.7,
ease: _Power3.easeInOut,
@@ -196,7 +195,7 @@ export function minimizeImage(): void {
y: cordHist.get()[cordHist.get().length - 1].y - window.innerHeight / 2
})
// show trail inactive
tl.to(getElTrailInactive(), {
tl.to(elsTrailInactive, {
y: '-=20',
ease: _Power3.easeOut,
stagger: -0.1,
@@ -221,14 +220,47 @@ export function initStage(ijs: ImageJSON[]): void {
// get stage
const stage = document.getElementsByClassName('stage').item(0) as HTMLDivElement
// get image elements
imgs = Array.from(stage.getElementsByTagName('img'))
imgs = Array.from(stage.getElementsByTagName('img')) as DesktopImage[]
imgs.forEach((img, i) => {
// preload first 5 images on page load
if (i < 5) {
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
})
})
})
// event listeners
stage.addEventListener('click', () => {
expandImage()
})
stage.addEventListener('keydown', () => {
expandImage()
})
stage.addEventListener(
'click',
() => {
expandImage()
},
{ passive: true }
)
stage.addEventListener(
'keydown',
() => {
expandImage()
},
{ passive: true }
)
window.addEventListener('mousemove', onMouse, { passive: true })
// watchers
isOpen.addWatcher((o) => {
@@ -240,21 +272,11 @@ export function initStage(ijs: ImageJSON[]): void {
cordHist.addWatcher((_) => {
setPositions()
})
// preload
lores(getElNextSeven())
// dynamic import
window.addEventListener(
'mousemove',
() => {
loadGsap()
.then((g) => {
_gsap = g[0]
_Power3 = g[1]
gsapLoaded = true
})
.catch((e) => {
console.log(e)
})
loadLib()
},
{ once: true, passive: true }
)
@@ -270,7 +292,7 @@ function createStage(ijs: ImageJSON[]): void {
stage.className = 'stage'
// append images to container
for (const ij of ijs) {
const e = document.createElement('img')
const e = document.createElement('img') as DesktopImage
e.height = ij.loImgH
e.width = ij.loImgW
// set data attributes
@@ -281,28 +303,35 @@ function createStage(ijs: ImageJSON[]): void {
e.dataset.loImgH = ij.loImgH.toString()
e.dataset.loImgW = ij.loImgW.toString()
e.alt = ij.alt
// append
stage.append(e)
}
container.append(stage)
}
function hires(imgs: HTMLImageElement[]): void {
function getImagesWithIndexArray(indexArray: number[]): DesktopImage[] {
return indexArray.map((i) => imgs[i])
}
function hires(imgs: DesktopImage[]): void {
imgs.forEach((img) => {
img.src = img.dataset.hiUrl as string
img.height = parseInt(img.dataset.hiImgH as string)
img.width = parseInt(img.dataset.hiImgW as string)
if (img.src === img.dataset.hiUrl) return
img.src = img.dataset.hiUrl
img.height = parseInt(img.dataset.hiImgH)
img.width = parseInt(img.dataset.hiImgW)
})
}
function lores(imgs: HTMLImageElement[]): void {
function lores(imgs: DesktopImage[]): void {
imgs.forEach((img) => {
img.src = img.dataset.loUrl as string
img.height = parseInt(img.dataset.loImgH as string)
img.width = parseInt(img.dataset.loImgW as string)
if (img.src === img.dataset.loUrl) return
img.src = img.dataset.loUrl
img.height = parseInt(img.dataset.loImgH)
img.width = parseInt(img.dataset.loImgW)
})
}
function loader(e: HTMLImageElement): void {
function setLoaderForImage(e: HTMLImageElement): void {
if (!e.complete) {
isLoading.set(true)
e.addEventListener(
@@ -325,3 +354,15 @@ function loader(e: HTMLImageElement): void {
isLoading.set(false)
}
}
function loadLib(): void {
loadGsap()
.then((g) => {
_gsap = g[0]
_Power3 = g[1]
gsapLoaded = true
})
.catch((e) => {
console.log(e)
})
}

View File

@@ -1,16 +1,10 @@
import { container } from '../container'
import { decIndex, incIndex, state } from '../state'
import { decrement, increment } from '../utils'
import { decIndex, incIndex, isAnimating, navigateVector, state } from '../globalState'
import { decrement, increment } from '../globalUtils'
import { setCustomCursor } from './customCursor'
import {
active,
cordHist,
isAnimating,
isLoading,
isOpen,
minimizeImage
} from './stage'
import { minimizeImage } from './stage'
import { active, cordHist, isLoading, isOpen } from './state'
/**
* types
@@ -22,13 +16,12 @@ type NavItem = (typeof navItems)[number]
* variables
*/
const mainDiv = document.getElementById('main') as HTMLDivElement
const navItems = [
mainDiv.getAttribute('prevText') as string,
mainDiv.getAttribute('closeText') as string,
mainDiv.getAttribute('nextText') as string
container.dataset.next,
container.dataset.close,
container.dataset.prev
] as const
const loadingText = (mainDiv.getAttribute('loadingText') as string) + '...'
const loadingText = container.dataset.loading + '...'
let loadedText = ''
/**
@@ -158,6 +151,7 @@ export function initStageNav(): void {
function nextImage(): void {
if (isAnimating.get()) return
navigateVector.set('next')
cordHist.set(
cordHist.get().map((item) => {
return { ...item, i: increment(item.i, state.get().length) }
@@ -169,6 +163,7 @@ function nextImage(): void {
function prevImage(): void {
if (isAnimating.get()) return
navigateVector.set('prev')
cordHist.set(
cordHist.get().map((item) => {
return { ...item, i: decrement(item.i, state.get().length) }

View File

@@ -0,0 +1,20 @@
import { Watchable } from '../globalUtils'
/**
* types
*/
export interface HistoryItem {
i: number
x: number
y: number
}
/**
* variables
*/
export const cordHist = new Watchable<HistoryItem[]>([])
export const isOpen = new Watchable<boolean>(false)
export const active = new Watchable<boolean>(false)
export const isLoading = new Watchable<boolean>(false)

View File

@@ -0,0 +1,28 @@
/**
* interfaces
*/
export interface DesktopImage extends HTMLImageElement {
dataset: {
hiUrl: string
hiImgH: string
hiImgW: string
loUrl: string
loImgH: string
loImgW: string
}
}
/**
* utils
*/
export function onMutation<T extends HTMLElement>(
element: T,
callback: (arg0: MutationRecord[], arg1: MutationObserver) => void,
observeOptions: MutationObserverInit = { attributes: true }
): void {
new MutationObserver((mutations, observer) => {
callback(mutations, observer)
}).observe(element, observeOptions)
}

View File

@@ -1,10 +1,16 @@
import { Watchable, decrement, increment } from './utils'
import {
Watchable,
decrement,
getThresholdSessionIndex,
increment
} from './globalUtils'
/**
* types
*/
export type State = typeof defaultState
export type NavVec = 'next' | 'none' | 'prev'
/**
* variables
@@ -26,6 +32,8 @@ const defaultState = {
}
export const state = new Watchable<State>(defaultState)
export const isAnimating = new Watchable<boolean>(false)
export const navigateVector = new Watchable<NavVec>('none')
/**
* main functions
@@ -81,9 +89,3 @@ function updateThreshold(state: State, inc: number): State {
const newItems = thresholds[i]
return { ...state, ...newItems }
}
function getThresholdSessionIndex(): number {
const s = sessionStorage.getItem('thresholdsIndex')
if (s === null) return 2
return parseInt(s)
}

View File

@@ -1,8 +1,7 @@
import { type Power3, type gsap } from 'gsap'
import { type Swiper } from 'swiper'
/**
* custom helpers
* utils
*/
export function increment(num: number, length: number): number {
@@ -17,44 +16,24 @@ export function expand(num: number): string {
return ('0000' + num.toString()).slice(-4)
}
export function isMobile(): boolean {
return window.matchMedia('(hover: none)').matches
}
export function getRandom(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min
}
export function onVisible<T extends Element>(
element: T,
callback: (arg0: T) => void
): void {
new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.intersectionRatio > 0) {
callback(element)
observer.disconnect()
}
})
}).observe(element)
}
export function capitalizeFirstLetter(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1)
}
export async function loadGsap(): Promise<[typeof gsap, typeof Power3]> {
const g = await import('gsap')
return [g.gsap, g.Power3]
}
export async function loadSwiper(): Promise<typeof Swiper> {
const s = await import('swiper')
return s.Swiper
export function getThresholdSessionIndex(): number {
const s = sessionStorage.getItem('thresholdsIndex')
if (s === null) return 2
return parseInt(s)
}
export function removeDuplicates<T>(arr: T[]): T[] {
if (arr.length < 2) return arr // optimization
return [...new Set(arr)]
}
/**
* custom types
* custom "reactive" object
*/
export class Watchable<T> {

View File

@@ -1,19 +1,33 @@
import { initContainer } from './container'
import { initState } from './globalState'
import { initNav } from './nav'
import { initResources } from './resources'
import { initState } from './state'
import { isMobile } from './utils'
initContainer()
const ijs = await initResources()
initState(ijs.length)
initNav()
// this is the main entry point for the app
document.addEventListener('DOMContentLoaded', () => {
main().catch((e) => {
console.log(e)
})
})
// NOTE: it seems firefox and chromnium don't like top layer await
// so we are using import then instead
if (ijs.length > 0) {
/**
* main functions
*/
async function main(): Promise<void> {
initContainer()
const ijs = await initResources()
initState(ijs.length)
initNav()
if (ijs.length === 0) {
return
}
// NOTE: it seems firefox and chromnium don't like top layer await
// so we are using import then instead
if (!isMobile()) {
import('./desktop/init')
await import('./desktop/init')
.then((d) => {
d.initDesktop(ijs)
})
@@ -21,7 +35,7 @@ if (ijs.length > 0) {
console.log(e)
})
} else {
import('./mobile/init')
await import('./mobile/init')
.then((m) => {
m.initMobile(ijs)
})
@@ -30,3 +44,11 @@ if (ijs.length > 0) {
})
}
}
/**
* hepler
*/
function isMobile(): boolean {
return window.matchMedia('(hover: none)').matches
}

View File

@@ -1,16 +1,17 @@
import { container } from '../container'
import { setIndex } from '../globalState'
import { type ImageJSON } from '../resources'
import { setIndex } from '../state'
import { getRandom, onVisible } from '../utils'
import { slideUp } from './gallery'
import { mounted } from './mounted'
import { mounted } from './state'
// eslint-disable-next-line sort-imports
import { getRandom, onIntersection, type MobileImage } from './utils'
/**
* variables
*/
export let imgs: HTMLImageElement[] = []
export let imgs: MobileImage[] = []
/**
* main functions
@@ -40,9 +41,14 @@ export function initCollection(ijs: ImageJSON[]): void {
}
})
// get image elements
imgs = Array.from(collection.getElementsByTagName('img'))
imgs = Array.from(collection.getElementsByTagName('img')) as MobileImage[]
// add event listeners
imgs.forEach((img, i) => {
// preload first 5 images on page load
if (i < 5) {
img.src = img.dataset.src
}
// event listeners
img.addEventListener(
'click',
() => {
@@ -58,12 +64,18 @@ export function initCollection(ijs: ImageJSON[]): void {
{ passive: true }
)
// preload
onVisible(img, () => {
for (let _i = 0; _i < 5; _i++) {
const n = i + _i
if (n < 0 || n > imgs.length - 1) continue
imgs[n].src = imgs[n].dataset.src as string
}
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
})
})
})
}
@@ -82,7 +94,7 @@ function createCollection(ijs: ImageJSON[]): void {
const x = i !== 0 ? getRandom(-25, 25) : 0
const y = i !== 0 ? getRandom(-30, 30) : 0
// element
const e = document.createElement('img')
const e = document.createElement('img') as MobileImage
e.dataset.src = ij.loUrl
e.height = ij.loImgH
e.width = ij.loImgW

View File

@@ -1,19 +1,14 @@
import { type Power3, type gsap } from 'gsap'
import { type Swiper } from 'swiper'
import { container } from '../container'
import { container, scrollable } from '../container'
import { isAnimating, navigateVector, setIndex, state } from '../globalState'
import { expand, loadGsap, removeDuplicates } from '../globalUtils'
import { type ImageJSON } from '../resources'
import { setIndex, state } from '../state'
import {
Watchable,
capitalizeFirstLetter,
expand,
loadGsap,
loadSwiper
} from '../utils'
import { mounted } from './mounted'
import { scrollable } from './scroll'
import { mounted } from './state'
// eslint-disable-next-line sort-imports
import { capitalizeFirstLetter, loadSwiper, type MobileImage } from './utils'
/**
* variables
@@ -23,11 +18,10 @@ let swiperNode: HTMLDivElement
let gallery: HTMLDivElement
let curtain: HTMLDivElement
let swiper: Swiper
const isAnimating = new Watchable<boolean>(false)
let lastIndex = -1
let indexDispNums: HTMLSpanElement[] = []
let galleryImages: HTMLImageElement[] = []
let collectionImages: HTMLImageElement[] = []
let galleryImages: MobileImage[] = []
let collectionImages: MobileImage[] = []
let _Swiper: typeof Swiper
let _gsap: typeof gsap
@@ -44,7 +38,7 @@ export function slideUp(): void {
isAnimating.set(true)
// load active image
loadImages()
galleryLoadImages()
_gsap.to(curtain, {
opacity: 1,
@@ -61,11 +55,12 @@ export function slideUp(): void {
setTimeout(() => {
scrollable.set(false)
isAnimating.set(false)
}, 1200)
}, 1400)
}
function slideDown(): void {
scrollable.set(true)
if (isAnimating.get()) return
isAnimating.set(true)
scrollToActive()
_gsap.to(gallery, {
@@ -79,6 +74,11 @@ function slideDown(): void {
duration: 1.2,
delay: 0.4
})
setTimeout(() => {
scrollable.set(true)
isAnimating.set(false)
}, 1600)
}
/**
@@ -95,18 +95,22 @@ export function initGallery(ijs: ImageJSON[]): void {
swiperNode = document.getElementsByClassName('galleryInner').item(0) as HTMLDivElement
gallery = document.getElementsByClassName('gallery').item(0) as HTMLDivElement
curtain = document.getElementsByClassName('curtain').item(0) as HTMLDivElement
galleryImages = Array.from(gallery.getElementsByTagName('img'))
galleryImages = Array.from(gallery.getElementsByTagName('img')) as MobileImage[]
collectionImages = Array.from(
document
.getElementsByClassName('collection')
.item(0)
?.getElementsByTagName('img') ?? []
)
) as MobileImage[]
// state watcher
state.addWatcher(() => {
const s = state.get()
// change slide only when index is changed
if (s.index === lastIndex) return
else if (lastIndex === -1)
navigateVector.set('none') // lastIndex before first set
else if (s.index < lastIndex) navigateVector.set('prev')
else navigateVector.set('next')
changeSlide(s.index)
updateIndexText()
lastIndex = s.index
@@ -152,7 +156,7 @@ export function initGallery(ijs: ImageJSON[]): void {
*/
function changeSlide(slide: number): void {
loadImages()
galleryLoadImages()
swiper.slideTo(slide, 0)
}
@@ -175,6 +179,29 @@ function updateIndexText(): void {
})
}
function galleryLoadImages(): void {
let activeImagesIndex: number[] = []
const currentIndex = state.get().index
const nextIndex = Math.min(currentIndex + 1, state.get().length - 1)
const prevIndex = Math.max(currentIndex - 1, 0)
switch (navigateVector.get()) {
case 'next':
activeImagesIndex = [nextIndex]
break
case 'prev':
activeImagesIndex = [prevIndex]
break
case 'none':
activeImagesIndex = [currentIndex, nextIndex, prevIndex]
break
}
removeDuplicates(activeImagesIndex).forEach((i) => {
const e = galleryImages[i]
if (e.src === e.dataset.src) return // already loaded
e.src = e.dataset.src
})
}
function createGallery(ijs: ImageJSON[]): void {
/**
* gallery
@@ -192,17 +219,18 @@ function createGallery(ijs: ImageJSON[]): void {
// swiper wrapper
const _swiperWrapper = document.createElement('div')
_swiperWrapper.className = 'swiper-wrapper'
// swiper slide
// loading text
const loadingText = container.dataset.loading
for (const ij of ijs) {
// swiper slide
const _swiperSlide = document.createElement('div')
_swiperSlide.className = 'swiper-slide'
// loading indicator
const l = document.createElement('div')
l.className = 'loadingText'
l.innerText =
(document.getElementById('main')?.getAttribute('loadingText') as string) + '...'
l.innerText = loadingText
// img
const e = document.createElement('img')
const e = document.createElement('img') as MobileImage
e.dataset.src = ij.hiUrl
e.height = ij.hiImgH
e.width = ij.hiImgW
@@ -281,16 +309,3 @@ function createGallery(ijs: ImageJSON[]): void {
*/
container.append(_gallery, _curtain)
}
function loadImages(): void {
const activeImages: HTMLImageElement[] = []
// load current, next, prev image
activeImages.push(galleryImages[swiper.activeIndex])
activeImages.push(
galleryImages[Math.min(swiper.activeIndex + 1, swiper.slides.length - 1)]
)
activeImages.push(galleryImages[Math.max(swiper.activeIndex - 1, 0)])
for (const e of activeImages) {
e.src = e.dataset.src as string
}
}

View File

@@ -1,3 +0,0 @@
import { Watchable } from '../utils'
export const scrollable = new Watchable<boolean>(true)

View File

@@ -1,3 +1,3 @@
import { Watchable } from '../utils'
import { Watchable } from '../globalUtils'
export const mounted = new Watchable<boolean>(false)

37
assets/ts/mobile/utils.ts Normal file
View File

@@ -0,0 +1,37 @@
import { type Swiper } from 'swiper'
/**
* interfaces
*/
export interface MobileImage extends HTMLImageElement {
dataset: {
src: string
}
}
/**
* utils
*/
export function getRandom(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min
}
export function onIntersection<T extends HTMLElement>(
element: T,
callback: (arg0: IntersectionObserverEntry[], arg1: IntersectionObserver) => void
): void {
new IntersectionObserver((entries, observer) => {
callback(entries, observer)
}).observe(element)
}
export function capitalizeFirstLetter(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1)
}
export async function loadSwiper(): Promise<typeof Swiper> {
const s = await import('swiper')
return s.Swiper
}

View File

@@ -1,5 +1,5 @@
import { decThreshold, incThreshold, state } from './state'
import { expand } from './utils'
import { decThreshold, incThreshold, state } from './globalState'
import { expand } from './globalUtils'
/**
* variables