diff --git a/assets/scss/_partial/_gallery.scss b/assets/scss/_partial/_gallery.scss index a161edc..a6a263f 100644 --- a/assets/scss/_partial/_gallery.scss +++ b/assets/scss/_partial/_gallery.scss @@ -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 { diff --git a/assets/ts/container.ts b/assets/ts/container.ts index 63fd864..01b80c2 100644 --- a/assets/ts/container.ts +++ b/assets/ts/container.ts @@ -1,9 +1,24 @@ -import { scrollable } from './mobile/scroll' +import { Watchable } from './globalUtils' -export let container: HTMLDivElement +export const scrollable = new Watchable(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') diff --git a/assets/ts/desktop/customCursor.ts b/assets/ts/desktop/customCursor.ts index 6b1bf4d..283de88 100644 --- a/assets/ts/desktop/customCursor.ts +++ b/assets/ts/desktop/customCursor.ts @@ -1,6 +1,6 @@ import { container } from '../container' -import { active } from './stage' +import { active } from './state' /** * variables diff --git a/assets/ts/desktop/init.ts b/assets/ts/desktop/init.ts index 923752b..1ee7a64 100644 --- a/assets/ts/desktop/init.ts +++ b/assets/ts/desktop/init.ts @@ -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) diff --git a/assets/ts/desktop/stage.ts b/assets/ts/desktop/stage.ts index c8efe54..013f930 100644 --- a/assets/ts/desktop/stage.ts +++ b/assets/ts/desktop/stage.ts @@ -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([]) -export const isOpen = new Watchable(false) -export const isAnimating = new Watchable(false) -export const active = new Watchable(false) -export const isLoading = new Watchable(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) + }) +} diff --git a/assets/ts/desktop/stageNav.ts b/assets/ts/desktop/stageNav.ts index ab5676d..2f5beb8 100644 --- a/assets/ts/desktop/stageNav.ts +++ b/assets/ts/desktop/stageNav.ts @@ -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) } diff --git a/assets/ts/desktop/state.ts b/assets/ts/desktop/state.ts new file mode 100644 index 0000000..7a115fd --- /dev/null +++ b/assets/ts/desktop/state.ts @@ -0,0 +1,20 @@ +import { Watchable } from '../globalUtils' + +/** + * types + */ + +export interface HistoryItem { + i: number + x: number + y: number +} + +/** + * variables + */ + +export const cordHist = new Watchable([]) +export const isOpen = new Watchable(false) +export const active = new Watchable(false) +export const isLoading = new Watchable(false) diff --git a/assets/ts/desktop/utils.ts b/assets/ts/desktop/utils.ts new file mode 100644 index 0000000..50b4d02 --- /dev/null +++ b/assets/ts/desktop/utils.ts @@ -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( + element: T, + callback: (arg0: MutationRecord[], arg1: MutationObserver) => void, + observeOptions: MutationObserverInit = { attributes: true } +): void { + new MutationObserver((mutations, observer) => { + callback(mutations, observer) + }).observe(element, observeOptions) +} diff --git a/assets/ts/state.ts b/assets/ts/globalState.ts similarity index 86% rename from assets/ts/state.ts rename to assets/ts/globalState.ts index 21eaa58..30650ad 100644 --- a/assets/ts/state.ts +++ b/assets/ts/globalState.ts @@ -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(defaultState) +export const isAnimating = new Watchable(false) +export const navigateVector = new Watchable('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) -} diff --git a/assets/ts/utils.ts b/assets/ts/globalUtils.ts similarity index 50% rename from assets/ts/utils.ts rename to assets/ts/globalUtils.ts index fda8dc2..433c590 100644 --- a/assets/ts/utils.ts +++ b/assets/ts/globalUtils.ts @@ -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( - 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 { - 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(arr: T[]): T[] { + if (arr.length < 2) return arr // optimization + return [...new Set(arr)] } /** - * custom types + * custom "reactive" object */ export class Watchable { diff --git a/assets/ts/main.ts b/assets/ts/main.ts index 455ae77..cd4001d 100644 --- a/assets/ts/main.ts +++ b/assets/ts/main.ts @@ -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 { + 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 +} diff --git a/assets/ts/mobile/collection.ts b/assets/ts/mobile/collection.ts index 261bb5d..0c7865b 100644 --- a/assets/ts/mobile/collection.ts +++ b/assets/ts/mobile/collection.ts @@ -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 diff --git a/assets/ts/mobile/gallery.ts b/assets/ts/mobile/gallery.ts index d53b4da..be511b6 100644 --- a/assets/ts/mobile/gallery.ts +++ b/assets/ts/mobile/gallery.ts @@ -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(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 - } -} diff --git a/assets/ts/mobile/scroll.ts b/assets/ts/mobile/scroll.ts deleted file mode 100644 index 46ecb41..0000000 --- a/assets/ts/mobile/scroll.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Watchable } from '../utils' - -export const scrollable = new Watchable(true) diff --git a/assets/ts/mobile/mounted.ts b/assets/ts/mobile/state.ts similarity index 55% rename from assets/ts/mobile/mounted.ts rename to assets/ts/mobile/state.ts index 461e9d3..6be25aa 100644 --- a/assets/ts/mobile/mounted.ts +++ b/assets/ts/mobile/state.ts @@ -1,3 +1,3 @@ -import { Watchable } from '../utils' +import { Watchable } from '../globalUtils' export const mounted = new Watchable(false) diff --git a/assets/ts/mobile/utils.ts b/assets/ts/mobile/utils.ts new file mode 100644 index 0000000..79a624a --- /dev/null +++ b/assets/ts/mobile/utils.ts @@ -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( + 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 { + const s = await import('swiper') + return s.Swiper +} diff --git a/assets/ts/nav.ts b/assets/ts/nav.ts index 6435ebe..ef12a53 100644 --- a/assets/ts/nav.ts +++ b/assets/ts/nav.ts @@ -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 diff --git a/layouts/_default/single.html b/layouts/_default/single.html index fa2b905..97e85a2 100644 --- a/layouts/_default/single.html +++ b/layouts/_default/single.html @@ -1,11 +1,11 @@ {{- define "main" -}} - -
+
{{- partial "nav.html" . -}} {{- with .Content -}}