fix(main.ts): import correct functions from utils module

feat(stage.ts): implement stage navigation functionality
feat(stageNav.ts): implement stage navigation overlay functionality
feat(state.ts): implement state management for index and threshold
feat(utils.ts): add utility functions for increment and decrement
This commit is contained in:
Sped0n
2023-10-29 00:58:53 +08:00
parent 2bc6d213ee
commit d32d5b5e4f
16 changed files with 515 additions and 712 deletions

38
assets/ts/customCursor.ts Normal file
View File

@@ -0,0 +1,38 @@
import { addActiveCallback } from './stage'
let cursor: HTMLDivElement
// create cursor
cursor = document.createElement('div')
cursor.className = 'cursor'
cursor.classList.add('active')
// create cursor inner
const cursorInner = document.createElement('div')
cursorInner.className = 'cursorInner'
// append cursor inner to cursor
cursor.append(cursorInner)
function onMouse(e: MouseEvent) {
const x = e.clientX
const y = e.clientY
cursor.style.transform = `translate3d(${x}px, ${y}px, 0)`
}
export function initCustomCursor(): void {
// append cursor to main
document.getElementById('main')!.append(cursor)
// bind mousemove event to window
window.addEventListener('mousemove', onMouse)
// add active callback
addActiveCallback((active) => {
if (active) {
cursor.classList.add('active')
} else {
cursor.classList.remove('active')
}
})
}
export function setCustomCursor(text: string): void {
cursorInner.innerText = text
}

View File

@@ -1,14 +0,0 @@
import { type ImageData } from './utils'
// fetch images info from JSON
const imageArrayElement = document.getElementById('images_info') as HTMLScriptElement
const rawImagesInfo = imageArrayElement.textContent as string
export const imagesInfo: ImageData[] = JSON.parse(rawImagesInfo).sort(
(a: ImageData, b: ImageData) => {
if (a.index < b.index) {
return -1
}
return 1
}
)
export const imagesLen: number = imagesInfo.length

View File

@@ -1,165 +0,0 @@
import { overlayEnable } from './overlay'
import {
calcImageIndex,
center,
delay,
mouseToTransform,
pushIndex,
type position,
hideImage
} from './utils'
import { thresholdIndex, thresholdSensitivityArray } from './thresholdCtl'
import { imgIndexSpanUpdate } from './indexDisp'
import { imagesLen } from './dataFetch'
import { imagesDivNodes as images } from './elemGen'
// global index for "activated"
export let globalIndex: number = 0
// last position set as "activated"
let last: position = { x: 0, y: 0 }
export let trailingImageIndexes: number[] = []
// only used in overlay disable, for storing positions temporarily
export let transformCache: string[] = []
// abort controller for enter overlay event listener
let EnterOverlayClickAbCtl = new AbortController()
// stack depth of images array
export let stackDepth: number = 5
export let lastStackDepth: number = 5
export const addEnterOverlayEL = (e: HTMLImageElement): void => {
EnterOverlayClickAbCtl.abort()
EnterOverlayClickAbCtl = new AbortController()
e.addEventListener(
'click',
() => {
void enterOverlay()
},
{
passive: true,
once: true,
signal: EnterOverlayClickAbCtl.signal
}
)
}
// activate top image
const activate = (index: number, mouseX: number, mouseY: number): void => {
addEnterOverlayEL(images[index])
if (stackDepth !== lastStackDepth) {
trailingImageIndexes.push(index)
refreshStack()
lastStackDepth = stackDepth
}
const indexesNum: number = pushIndex(
index,
trailingImageIndexes,
stackDepth,
images,
imagesLen
)
// set img position
images[index].style.transform = mouseToTransform(mouseX, mouseY, true, true)
images[index].dataset.status = 'null'
// reset z index
for (let i = indexesNum; i > 0; i--) {
images[trailingImageIndexes[i - 1]].style.zIndex = `${i}`
}
images[index].style.visibility = 'visible'
last = { x: mouseX, y: mouseY }
}
// Compare the current mouse position with the last activated position
const distanceFromLast = (x: number, y: number): number => {
return Math.hypot(x - last.x, y - last.y)
}
// handle mouse move
export const handleOnMove = (e: MouseEvent): void => {
// meet threshold
if (
distanceFromLast(e.clientX, e.clientY) >
window.innerWidth / thresholdSensitivityArray[thresholdIndex]
) {
// calculate the actual index
const imageIndex = calcImageIndex(globalIndex, imagesLen)
// show top image and change index
activate(imageIndex, e.clientX, e.clientY)
imgIndexSpanUpdate(imageIndex + 1, imagesLen)
// self increment
globalIndexInc()
}
}
async function enterOverlay(): Promise<void> {
// stop images animation
window.removeEventListener('mousemove', handleOnMove)
// get index array length
const indexesNum: number = trailingImageIndexes.length
for (let i = 0; i < indexesNum; i++) {
// create image element
const e: HTMLImageElement = images[trailingImageIndexes[i]]
// cache images' position
transformCache.push(e.style.transform)
// set style for the images
if (i === indexesNum - 1) {
e.style.transitionDelay = `${0.1 * i + 0.2}s, ${0.1 * i + 0.2 + 0.5}s`
e.dataset.status = 'top'
center(e)
} else {
e.style.transitionDelay = `${0.1 * i}s`
e.dataset.status = 'trail'
}
}
// sleep
await delay(stackDepth * 100 + 100 + 1000)
// post process
for (let i = 0; i < indexesNum; i++) {
images[trailingImageIndexes[i]].style.transitionDelay = ''
if (i === indexesNum - 1) {
images[trailingImageIndexes[i]].dataset.status = 'overlay'
} else {
images[trailingImageIndexes[i]].style.visibility = 'hidden'
}
}
// Offset previous self increment of global index (by handleOnMove)
globalIndexDec()
// overlay init
overlayEnable()
}
// initialization
export const trackMouseInit = (): void => {
window.addEventListener('mousemove', handleOnMove)
}
export const globalIndexDec = (): void => {
globalIndex--
}
export const globalIndexInc = (): void => {
globalIndex++
}
export const emptyTransformCache = (): void => {
transformCache = []
}
export const emptyTrailingImageIndexes = (): void => {
trailingImageIndexes = []
}
export const setStackDepth = (newStackDepth: number): void => {
if (stackDepth !== newStackDepth) {
lastStackDepth = stackDepth
stackDepth = newStackDepth
}
}
export const refreshStack = (): void => {
const l: number = trailingImageIndexes.length
if (stackDepth < lastStackDepth && l > stackDepth) {
const times: number = l - stackDepth
for (let i = 0; i < times; i++)
hideImage(images[trailingImageIndexes.shift() as number])
}
}

View File

@@ -1,55 +0,0 @@
import { imagesInfo, imagesLen } from './dataFetch'
import { createImgElement } from './utils'
// get components of overlay
export let overlayCursor: HTMLDivElement
export let cursorInnerContent: HTMLDivElement
export let imagesDivNodes: NodeListOf<HTMLImageElement>
const mainDiv = document.getElementById('main') as HTMLDivElement
const passDesktopElements = (): void => {
overlayCursor = document
.getElementsByClassName('overlay_cursor')
.item(0) as HTMLDivElement
cursorInnerContent = document
.getElementsByClassName('cursor_innerText')
.item(0) as HTMLDivElement
imagesDivNodes = document.getElementsByClassName('imagesDesktop')[0]
.childNodes as NodeListOf<HTMLImageElement>
}
const passMobileElements = (): void => {
imagesDivNodes = document.getElementsByClassName('imagesMobile')[0]
.childNodes as NodeListOf<HTMLImageElement>
}
const createCursorDiv = (): HTMLDivElement => {
const cursorDiv: HTMLDivElement = document.createElement('div')
cursorDiv.className = 'overlay_cursor'
const innerTextDiv: HTMLDivElement = document.createElement('div')
innerTextDiv.className = 'cursor_innerText'
cursorDiv.appendChild(innerTextDiv)
return cursorDiv
}
export const createDesktopElements = (): void => {
mainDiv.appendChild(createCursorDiv())
const imagesDiv: HTMLDivElement = document.createElement('div')
imagesDiv.className = 'imagesDesktop'
for (let i = 0; i < imagesLen; i++) {
imagesDiv.appendChild(createImgElement(imagesInfo[i]))
}
mainDiv.appendChild(imagesDiv)
passDesktopElements()
}
export const createMobileElements = (): void => {
const imagesDiv: HTMLDivElement = document.createElement('div')
imagesDiv.className = 'imagesMobile'
for (let i = 0; i < imagesLen; i++) {
imagesDiv.appendChild(createImgElement(imagesInfo[i]))
}
mainDiv.appendChild(imagesDiv)
passMobileElements()
}

View File

@@ -1,18 +0,0 @@
import { imagesInfo, imagesLen } from './dataFetch'
import { preloadImage, calcImageIndex } from './utils'
let lastIndex: number = 0
export const preloader = (index: number): void => {
if (lastIndex === index) {
for (let i: number = -2; i <= 1; i++)
preloadImage(imagesInfo[calcImageIndex(index + i, imagesLen)].url)
} else if (lastIndex > index) {
for (let i: number = 1; i <= 3; i++)
preloadImage(imagesInfo[calcImageIndex(index - i, imagesLen)].url)
} else {
for (let i: number = 1; i <= 3; i++)
preloadImage(imagesInfo[calcImageIndex(index + i, imagesLen)].url)
}
lastIndex = index
}

View File

@@ -1,17 +0,0 @@
import { duper } from './utils'
// update index of displaying image
export const imgIndexSpanUpdate = (numOne: number, numTwo: number): void => {
// footer index number display module
const footerIndexDisp = document.getElementsByClassName('ftid')
const numOneString: string = duper(numOne)
const numTwoString: string = duper(numTwo)
for (let i: number = 0; i <= 7; i++) {
const footerIndex = footerIndexDisp[i] as HTMLSpanElement
if (i > 3) {
footerIndex.innerText = numTwoString[i - 4]
} else {
footerIndex.innerText = numOneString[i]
}
}
}

View File

@@ -1,28 +1,13 @@
import { createDesktopElements, createMobileElements } from './elemGen'
import { imgIndexSpanUpdate } from './indexDisp'
import { trackMouseInit } from './desktop'
import { thresholdCtlInit } from './thresholdCtl'
import { imagesLen } from './dataFetch'
import { vwRefreshInit } from './overlay'
import { preloader } from './imageCache'
import { getDeviceType } from './utils'
import { renderImages } from './mobile'
import { initResources } from './resources'
import { initState } from './state'
import { initCustomCursor } from './customCursor'
import { initNav } from './nav'
import { initStage } from './stage'
import { initStageNav } from './stageNav'
const desktopInit = (): void => {
createDesktopElements()
preloader(0)
vwRefreshInit()
imgIndexSpanUpdate(0, imagesLen)
thresholdCtlInit()
trackMouseInit()
}
const mobileInit = (): void => {
createMobileElements()
vwRefreshInit()
imgIndexSpanUpdate(0, imagesLen)
renderImages()
console.log('mobile')
}
getDeviceType().desktop ? mobileInit() : desktopInit()
initCustomCursor()
const ijs = initResources()
initState(ijs.length)
initStage(ijs)
initStageNav()
initNav()

View File

@@ -1,24 +0,0 @@
import { imagesDivNodes as images } from './elemGen'
import { imagesLen } from './dataFetch'
export const renderImages = (): void => {
images.forEach((img: HTMLImageElement, idx: number): void => {
const randomX: number = Math.floor(Math.random() * 35) + 2
let randomY: number
// random Y calculation
if (idx === 0) {
randomY = 68
} else if (idx === 1) {
randomY = 44
} else if (idx === imagesLen - 1) {
randomY = 100
} else {
randomY = Math.floor(Math.random() * 51) + 2
}
img.style.transform = `translate(${randomX}vw, -${randomY}%)`
img.style.marginTop = `${idx === 1 ? 70 : 0}vh`
img.style.visibility = 'visible'
})
}

59
assets/ts/nav.ts Normal file
View File

@@ -0,0 +1,59 @@
import { getState, incThreshold, decThreshold } from './state'
import { expand } from './utils'
// threshold div
const thresholdDiv = document
.getElementsByClassName('threshold')
.item(0) as HTMLDivElement
// threshold nums span
const thresholdDispNums = Array.from(
thresholdDiv.getElementsByClassName('num')
) as HTMLSpanElement[]
// threshold buttons
const decButton = thresholdDiv
.getElementsByClassName('dec')
.item(0) as HTMLButtonElement
const incButton = thresholdDiv
.getElementsByClassName('inc')
.item(0) as HTMLButtonElement
// index div
const indexDiv = document.getElementsByClassName('index').item(0) as HTMLDivElement
// index nums span
const indexDispNums = Array.from(
indexDiv.getElementsByClassName('num')
) as HTMLSpanElement[]
export function initNav() {
// init threshold text
updateThresholdText()
// init index text
updateIndexText()
// event listeners
decButton.addEventListener('click', () => decThreshold())
incButton.addEventListener('click', () => incThreshold())
}
// helper
export function updateThresholdText(): void {
const thresholdValue: string = expand(getState().threshold)
thresholdDispNums.forEach((e: HTMLSpanElement, i: number) => {
e.innerText = thresholdValue[i]
})
}
export function updateIndexText(): void {
const indexValue: string = expand(getState().index + 1)
const indexLength: string = expand(getState().length)
indexDispNums.forEach((e: HTMLSpanElement, i: number) => {
if (i < 4) {
e.innerText = indexValue[i]
} else {
e.innerText = indexLength[i - 4]
}
})
}

View File

@@ -1,204 +0,0 @@
import { delay, center, calcImageIndex, mouseToTransform, pushIndex } from './utils'
import {
handleOnMove,
globalIndex,
globalIndexDec,
globalIndexInc,
trailingImageIndexes,
transformCache,
emptyTransformCache,
emptyTrailingImageIndexes,
stackDepth,
addEnterOverlayEL
} from './desktop'
import { imagesLen } from './dataFetch'
import { imgIndexSpanUpdate } from './indexDisp'
import { overlayCursor, cursorInnerContent, imagesDivNodes as images } from './elemGen'
let oneThird: number = Math.round(window.innerWidth / 3)
// set cursor text
const setCursorText = (text: string): void => {
cursorInnerContent.innerText = text
}
// overlay cursor event handler
const setTextPos = (e: MouseEvent): void => {
overlayCursor.style.transform = mouseToTransform(e.clientX, e.clientY, false, true)
}
// disable listeners
const disableListener = (): void => {
window.removeEventListener('mousemove', handleOverlayMouseMove)
overlayCursor.removeEventListener('click', handleOverlayClick)
}
// enable overlay
export const overlayEnable = (): void => {
// show the overlay components
overlayCursor.style.zIndex = '21'
// set overlay event listeners
setListener()
}
// disable overlay
export const overlayDisable = (): void => {
// hide the overlay components
overlayCursor.style.zIndex = '-1'
// set overlay cursor text content to none
setCursorText('')
// disable overlay event listeners
disableListener()
}
// handle close click
async function handleCloseClick(): Promise<void> {
// disable overlay
overlayDisable()
// get length of indexes and empty indexes array
const indexesNum = trailingImageIndexes.length
emptyTrailingImageIndexes()
// prepare animation
for (let i: number = 0; i < indexesNum; i++) {
// get element from index and store the index
const index: number = calcImageIndex(globalIndex - i, imagesLen)
const e: HTMLImageElement = images[index]
trailingImageIndexes.unshift(index)
// set z index for the image element
e.style.zIndex = `${indexesNum - i - 1}`
// set different style for trailing and top image
if (i === 0) {
// set position
e.style.transform = transformCache[indexesNum - i - 1]
// set transition delay
e.style.transitionDelay = '0s, 0.7s'
// set status for css
e.dataset.status = 'resumeTop'
} else {
// set position
e.style.transform = transformCache[indexesNum - i - 1]
// set transition delay
e.style.transitionDelay = `${1.2 + 0.1 * i - 0.1}s`
// set status for css
e.dataset.status = 'resume'
}
// style process complete, show the image
e.style.visibility = 'visible'
}
// halt the function while animation is running
await delay(1200 + stackDepth * 100 + 100)
// add back enter overlay event listener to top image
addEnterOverlayEL(images[calcImageIndex(globalIndex, imagesLen)])
// clear unused status and transition delay
for (let i: number = 0; i < indexesNum; i++) {
const index: number = calcImageIndex(globalIndex - i, imagesLen)
images[index].dataset.status = 'null'
images[index].style.transitionDelay = ''
}
// Add back previous self increment of global index (by handleOnMove)
globalIndexInc()
// add back mousemove event listener
window.addEventListener('mousemove', handleOnMove, { passive: true })
// empty the position array cache
emptyTransformCache()
}
const handleSideClick = (CLD: boolean): void => {
// get last displayed image's index
const imgIndex: number = calcImageIndex(globalIndex, imagesLen)
// change global index and get current displayed image's index
CLD ? globalIndexInc() : globalIndexDec()
const currImgIndex: number = calcImageIndex(globalIndex, imagesLen)
// store current displayed image's index
CLD
? pushIndex(
currImgIndex,
trailingImageIndexes,
stackDepth,
images,
imagesLen,
false,
false
)
: pushIndex(
currImgIndex,
trailingImageIndexes,
stackDepth,
images,
imagesLen,
true,
false
)
// hide last displayed image
images[imgIndex].style.visibility = 'hidden'
images[imgIndex].dataset.status = 'trail'
// process the image going to display
center(images[currImgIndex])
images[currImgIndex].dataset.status = 'overlay'
// process complete, show the image
images[currImgIndex].style.visibility = 'visible'
// change index display
imgIndexSpanUpdate(currImgIndex + 1, imagesLen)
}
// change text and position of overlay cursor
const handleOverlayMouseMove = (e: MouseEvent): void => {
// set text position
setTextPos(e)
// set text content
if (e.clientX < oneThird) {
setCursorText('PREV')
overlayCursor.dataset.status = 'PREV'
} else if (e.clientX < oneThird * 2) {
setCursorText('CLOSE')
overlayCursor.dataset.status = 'CLOSE'
} else {
setCursorText('NEXT')
overlayCursor.dataset.status = 'NEXT'
}
}
const handleOverlayClick = (): void => {
switch (overlayCursor.dataset.status) {
case 'PREV':
handleSideClick(false)
break
case 'CLOSE':
void handleCloseClick()
break
case 'NEXT':
handleSideClick(true)
break
}
}
// set event listener
const setListener = (): void => {
// add mouse move event listener (for overlay text cursor)
window.addEventListener('mousemove', handleOverlayMouseMove, { passive: true })
// add close/prev/next click event listener
overlayCursor.addEventListener('click', handleOverlayClick, { passive: true })
}
export const vwRefreshInit = (): void => {
window.addEventListener(
'resize',
() => {
// refresh value of one third
oneThird = Math.round(window.innerWidth / 3)
// reset footer height
const r = document.querySelector(':root') as HTMLStyleElement
if (window.innerWidth > 768) {
r.style.setProperty('--footer-height', '38px')
} else {
r.style.setProperty('--footer-height', '31px')
}
// recenter image (only in overlay)
if (
images[calcImageIndex(globalIndex, imagesLen)].dataset.status === 'overlay'
)
center(images[calcImageIndex(globalIndex, imagesLen)])
},
{ passive: true }
)
}

21
assets/ts/resources.ts Normal file
View File

@@ -0,0 +1,21 @@
// data structure for images info
export interface ImageJSON {
index: number
url: string
imgH: number
imgW: number
pColor: string
sColor: string
}
export function initResources(): ImageJSON[] {
const imagesJson = document.getElementById('imagesSource') as HTMLScriptElement
return JSON.parse(imagesJson.textContent as string).sort(
(a: ImageJSON, b: ImageJSON) => {
if (a.index < b.index) {
return -1
}
return 1
}
)
}

217
assets/ts/stage.ts Normal file
View File

@@ -0,0 +1,217 @@
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 CordHist {
private obj: HistoryItem[] = []
get(): HistoryItem[] {
return this.obj
}
set(e: HistoryItem[]): void {
this.obj = e
setPositions()
}
}
class IsOpen {
private obj = false
get(): boolean {
return this.obj
}
set(e: boolean): void {
this.obj = e
activeCallbacks.forEach((callback) => callback(getActive()))
}
}
let last = { x: 0, y: 0 }
export let cordHist = new CordHist()
export let isOpen = new IsOpen()
let isAnimating = false
let activeCallbacks: ((active: boolean) => void)[] = []
// 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]
}
export function getIsAnimating(): boolean {
return isAnimating
}
export function getActive(): boolean {
return isOpen.get() && !getIsAnimating()
}
// setter
export function addActiveCallback(callback: (active: boolean) => void): void {
activeCallbacks.push(callback)
}
export function setIsAnimating(e: boolean): void {
isAnimating = e
activeCallbacks.forEach((callback) => callback(getActive()))
}
// main functions
// on mouse
function onMouse(e: MouseEvent): void {
if (isOpen.get() || getIsAnimating()) 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 (getIsAnimating()) return
isOpen.set(true)
setIsAnimating(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(() => {
setIsAnimating(false)
})
}
// close navigation and back to stage
export function minimizeImage(): void {
if (isAnimating) return
isOpen.set(false)
setIsAnimating(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(() => {
setIsAnimating(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)
}
// 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)
}

93
assets/ts/stageNav.ts Normal file
View File

@@ -0,0 +1,93 @@
import { setCustomCursor } from './customCursor'
import { decIndex, incIndex, getState } from './state'
import { increment, decrement } from './utils'
import {
cordHist,
isOpen,
getIsAnimating,
minimizeImage,
addActiveCallback
} from './stage'
type NavItem = (typeof navItems)[number]
const navItems = ['prev', 'close', 'next'] as const
// main functions
function handleClick(type: NavItem) {
switch (type) {
case 'prev':
prevImage()
break
case 'close':
minimizeImage()
break
case 'next':
nextImage()
break
}
}
function handleKey(e: KeyboardEvent) {
if (isOpen.get() || getIsAnimating()) return
switch (e.key) {
case 'ArrowLeft':
prevImage()
break
case 'Escape':
minimizeImage()
break
case 'ArrowRight':
nextImage()
break
}
}
// init
export function initStageNav() {
const navOverlay = document.createElement('div')
navOverlay.className = 'navOverlay'
for (let navItem of navItems) {
const overlay = document.createElement('div')
overlay.className = 'overlay'
overlay.addEventListener('click', () => handleClick(navItem))
overlay.addEventListener('keydown', () => handleClick(navItem))
overlay.addEventListener('mouseover', () => setCustomCursor(navItem))
overlay.addEventListener('focus', () => setCustomCursor(navItem))
navOverlay.append(overlay)
}
addActiveCallback((active) => {
if (active) {
navOverlay.classList.add('active')
} else {
navOverlay.classList.remove('active')
}
})
document.getElementById('main')!.append(navOverlay)
window.addEventListener('keydown', handleKey)
}
// hepler
function nextImage() {
if (getIsAnimating()) return
cordHist.set(
cordHist.get().map((item) => {
return { ...item, i: increment(item.i, getState().length) }
})
)
incIndex()
}
function prevImage() {
if (getIsAnimating()) return
cordHist.set(
cordHist.get().map((item) => {
return { ...item, i: decrement(item.i, getState().length) }
})
)
decIndex()
}

68
assets/ts/state.ts Normal file
View File

@@ -0,0 +1,68 @@
import { increment, decrement } from './utils'
import { updateIndexText, updateThresholdText } from './nav'
const thresholds = [
{ threshold: 20, trailLength: 20 },
{ threshold: 40, trailLength: 10 },
{ threshold: 80, trailLength: 5 },
{ threshold: 140, trailLength: 5 },
{ threshold: 200, trailLength: 5 }
]
const defaultState = {
index: -1,
length: 0,
threshold: thresholds[2].threshold,
trailLength: thresholds[2].trailLength
}
export type State = typeof defaultState
let state = defaultState
export function getState(): State {
// return a copy of state
return Object.create(
Object.getPrototypeOf(state),
Object.getOwnPropertyDescriptors(state)
)
}
export function initState(length: number): void {
state.length = length
}
export function setIndex(index: number): void {
state.index = index
updateIndexText()
}
export function incIndex(): void {
state.index = increment(state.index, state.length)
updateIndexText()
}
export function decIndex(): void {
state.index = decrement(state.index, state.length)
updateIndexText()
}
export function incThreshold(): void {
state = updateThreshold(state, 1)
updateThresholdText()
}
export function decThreshold(): void {
state = updateThreshold(state, -1)
updateThresholdText()
}
// helper
function updateThreshold(state: State, inc: number): State {
const i = thresholds.findIndex((t) => state.threshold === t.threshold)
const newItems = thresholds[i + inc]
// out of range
if (!newItems) return state
return { ...state, ...newItems }
}

View File

@@ -1,51 +0,0 @@
import { duper } from './utils'
import { setStackDepth } from './desktop'
// get threshold display element
const thresholdDisp = document.getElementsByClassName('thid').item(0) as HTMLSpanElement
// threshold data
const threshold: number[] = [0, 40, 80, 120, 160, 200]
export const thresholdSensitivityArray: number[] = [100, 40, 18, 14, 9, 5]
export let thresholdIndex: number = 2
export const stackDepthArray: number[] = [15, 8, 5, 5, 5, 5]
// update inner text of threshold display element
const thresholdUpdate = (): void => {
thresholdDisp.innerText = duper(threshold[thresholdIndex])
}
const stackDepthUpdate = (): void => {
setStackDepth(stackDepthArray[thresholdIndex])
}
// threshold control initialization
export const thresholdCtlInit = (): void => {
thresholdUpdate()
const dec = document.getElementById('thresholdDec') as HTMLButtonElement
dec.addEventListener(
'click',
function () {
if (thresholdIndex > 0) {
thresholdIndex--
thresholdUpdate()
stackDepthUpdate()
}
},
{ passive: true }
)
const inc = document.getElementById('thresholdInc') as HTMLButtonElement
inc.addEventListener(
'click',
function () {
if (thresholdIndex < 5) {
thresholdIndex++
thresholdUpdate()
stackDepthUpdate()
}
},
{ passive: true }
)
}

View File

@@ -1,145 +1,15 @@
export interface ImageData {
index: string
url: string
imgH: string
imgW: string
pColor: string
sColor: string
export function increment(num: number, length: number): number {
return (num + 1) % length
}
export interface position {
x: number
y: number
export function decrement(num: number, length: number): number {
return (num + length - 1) % length
}
export interface deviceType {
mobile: boolean
tablet: boolean
desktop: boolean
}
// 0 to 0001, 25 to 0025
export const duper = (num: number): string => {
export function expand(num: number): string {
return ('0000' + num.toString()).slice(-4)
}
export const mouseToTransform = (
x: number,
y: number,
centerCorrection: boolean = true,
accelerate: boolean = false
): string => {
return `translate${accelerate ? '3d' : ''}(${
centerCorrection ? `calc(${x}px - 50%)` : `${x}px`
}, ${centerCorrection ? `calc(${y}px - 50%)` : `${y}px`}${accelerate ? ', 0' : ''})`
}
// eslint-disable-next-line @typescript-eslint/promise-function-async
export function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms))
}
// remove all event listeners from a node
export const removeAllEventListeners = (e: Node): Node => {
return e.cloneNode(true)
}
// center top div
export const center = (e: HTMLElement): void => {
const x: number = window.innerWidth / 2
let y: number
if (window.innerWidth > 768) {
y = (window.innerHeight - 38) / 2
} else {
y = (window.innerHeight - 31) / 2 + 31
}
e.style.transform = mouseToTransform(x, y)
}
export const createImgElement = (input: ImageData): HTMLImageElement => {
const img = document.createElement('img')
img.setAttribute('src', input.url)
img.setAttribute('alt', '')
img.setAttribute('height', input.imgH)
img.setAttribute('width', input.imgW)
img.style.visibility = 'hidden'
img.dataset.status = 'trail'
// img.style.backgroundImage = `linear-gradient(15deg, ${input.pColor}, ${input.sColor})`
return img
}
export const calcImageIndex = (index: number, imgCounts: number): number => {
if (index >= 0) {
return index % imgCounts
} else {
return (imgCounts + (index % imgCounts)) % imgCounts
}
}
export const preloadImage = (src: string): void => {
const cache = new Image()
cache.src = src
}
export const getDeviceType = (): deviceType => {
const ua: string = navigator.userAgent
const result: deviceType = { mobile: false, tablet: false, desktop: false }
if (/(tablet|ipad|playbook|silk)|(android(?!.*mobi))/i.test(ua)) {
result.mobile = true
result.tablet = true
} else if (
/Mobile|iP(hone|od)|Android|BlackBerry|IEMobile|Kindle|Silk-Accelerated|(hpw|web)OS|Opera M(obi|ini)/.test(
ua
)
) {
result.mobile = true
} else result.desktop = true
return result
}
export const hideImage = (e: HTMLImageElement): void => {
e.style.visibility = 'hidden'
e.dataset.status = 'trail'
}
export const pushIndex = (
index: number,
indexesArray: number[],
stackDepth: number,
imagesArray: NodeListOf<HTMLImageElement>,
imagesArrayLen: number,
invertFlag: boolean = false,
autoHideFlag: boolean = true
): number => {
let indexesNum: number = indexesArray.length
// create variable overflow to store the tail index
let overflow: number
if (!invertFlag) {
// if stack is full, push the tail index out and hide the image
if (indexesNum === stackDepth) {
// insert
indexesArray.push(index)
// pop out
overflow = indexesArray.shift() as number
// auto hide tail image
if (autoHideFlag) hideImage(imagesArray[overflow])
} else {
indexesArray.push(index)
indexesNum += 1
}
} else {
// if stack is full, push the tail index out and hide the image
if (indexesNum === stackDepth) {
// insert
indexesArray.unshift(calcImageIndex(index - stackDepth + 1, imagesArrayLen))
// pop out
overflow = indexesArray.pop() as number
// auto hide tail image
if (autoHideFlag) hideImage(imagesArray[overflow])
} else {
indexesArray.unshift(calcImageIndex(index - indexesNum + 1, imagesArrayLen))
indexesNum += 1
}
}
return indexesNum
export function isMobile(): boolean {
return window.matchMedia('(hover: none)').matches
}