Files
bridget/assets/ts/stage.ts

194 lines
4.6 KiB
TypeScript

import { incIndex, getState } from './state'
import { gsap, Power3 } from 'gsap'
import { ImageJSON } from './resources'
export type HistoryItem = { i: number; x: number; y: number }
let imgs: HTMLImageElement[] = []
class watchable<T> {
constructor(private obj: T) {}
private watchers: (() => void)[] = []
get(): T {
return this.obj
}
set(e: T): void {
this.obj = e
this.watchers.forEach((watcher) => watcher())
}
addWatcher(watcher: () => void): void {
this.watchers.push(watcher)
}
}
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)
// getter
function getElTrail(): HTMLImageElement[] {
return cordHist.get().map((item) => imgs[item.i])
}
function getElTrailCurrent(): HTMLImageElement[] {
return getElTrail().slice(-getState().trailLength)
}
function getElTrailInactive(): HTMLImageElement[] {
const elTrailCurrent = getElTrailCurrent()
return elTrailCurrent.slice(0, elTrailCurrent.length - 1)
}
function getElCurrent(): HTMLImageElement {
const elTrail = getElTrail()
return elTrail[elTrail.length - 1]
}
// main functions
// on mouse
function onMouse(e: MouseEvent): void {
if (isOpen.get() || isAnimating.get()) return
const cord = { x: e.clientX, y: e.clientY }
const travelDist = Math.hypot(cord.x - last.x, cord.y - last.y)
if (travelDist > getState().threshold) {
last = cord
incIndex()
const newHist = { i: getState().index, ...cord }
cordHist.set([...cordHist.get(), newHist].slice(-getState().length))
}
}
// set image position with gsap
function setPositions(): void {
const elTrail = getElTrail()
if (!elTrail.length) return
gsap.set(elTrail, {
x: (i: number) => cordHist.get()[i].x - window.innerWidth / 2,
y: (i: number) => cordHist.get()[i].y - window.innerHeight / 2,
opacity: (i: number) =>
i + 1 + getState().trailLength <= cordHist.get().length ? 0 : 1,
zIndex: (i: number) => i,
scale: 0.6
})
if (isOpen.get()) {
gsap.set(imgs, { opacity: 0 })
gsap.set(getElCurrent(), { opacity: 1, x: 0, y: 0, scale: 1 })
}
}
// open image into navigation
function expandImage(): void {
if (isAnimating.get()) return
isOpen.set(true)
isAnimating.set(true)
const tl = gsap.timeline()
// move down and hide trail inactive
tl.to(getElTrailInactive(), {
y: '+=20',
ease: Power3.easeIn,
stagger: 0.075,
duration: 0.3,
delay: 0.1,
opacity: 0
})
// current move to center
tl.to(getElCurrent(), {
x: 0,
y: 0,
ease: Power3.easeInOut,
duration: 0.7,
delay: 0.3
})
// current expand
tl.to(getElCurrent(), {
delay: 0.1,
scale: 1,
ease: Power3.easeInOut
})
// finished
tl.then(() => {
isAnimating.set(false)
})
}
// close navigation and back to stage
export function minimizeImage(): void {
if (isAnimating.get()) return
isOpen.set(false)
isAnimating.set(true)
const tl = gsap.timeline()
// shrink current
tl.to(getElCurrent(), {
scale: 0.6,
duration: 0.6,
ease: Power3.easeInOut
})
// move current to original position
tl.to(getElCurrent(), {
delay: 0.3,
duration: 0.7,
ease: Power3.easeInOut,
x: cordHist.get()[cordHist.get().length - 1].x - window.innerWidth / 2,
y: cordHist.get()[cordHist.get().length - 1].y - window.innerHeight / 2
})
// show trail inactive
tl.to(getElTrailInactive(), {
y: '-=20',
ease: Power3.easeOut,
stagger: -0.1,
duration: 0.3,
opacity: 1
})
// finished
tl.then(() => {
isAnimating.set(false)
})
}
// init
export function initStage(ijs: ImageJSON[]): void {
createStage(ijs)
const stage = document.getElementsByClassName('stage').item(0) as HTMLDivElement
imgs = Array.from(stage.getElementsByTagName('img'))
stage.addEventListener('click', () => expandImage())
stage.addEventListener('keydown', () => expandImage())
window.addEventListener('mousemove', onMouse)
isOpen.addWatcher(() => active.set(isOpen.get() && !isAnimating.get()))
isAnimating.addWatcher(() => active.set(isOpen.get() && !isAnimating.get()))
cordHist.addWatcher(() => setPositions())
}
// hepler
function createStage(ijs: ImageJSON[]): void {
// create container for images
const stage: HTMLDivElement = document.createElement('div')
stage.className = 'stage'
// append images to container
for (let ij of ijs) {
const e = document.createElement('img')
e.src = ij.url
e.height = ij.imgH
e.width = ij.imgW
e.alt = 'image'
stage.append(e)
}
document.getElementById('main')!.append(stage)
}