mirror of
https://github.com/Sped0n/bridget.git
synced 2026-04-14 10:09:31 -07:00
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:
@@ -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 {
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { container } from '../container'
|
||||
|
||||
import { active } from './stage'
|
||||
import { active } from './state'
|
||||
|
||||
/**
|
||||
* variables
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
|
||||
20
assets/ts/desktop/state.ts
Normal file
20
assets/ts/desktop/state.ts
Normal 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)
|
||||
28
assets/ts/desktop/utils.ts
Normal file
28
assets/ts/desktop/utils.ts
Normal 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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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> {
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import { Watchable } from '../utils'
|
||||
|
||||
export const scrollable = new Watchable<boolean>(true)
|
||||
@@ -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
37
assets/ts/mobile/utils.ts
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user