mirror of
https://github.com/Sped0n/bridget.git
synced 2026-04-14 10:09:31 -07:00
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:
38
assets/ts/customCursor.ts
Normal file
38
assets/ts/customCursor.ts
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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
59
assets/ts/nav.ts
Normal 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]
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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
21
assets/ts/resources.ts
Normal 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
217
assets/ts/stage.ts
Normal 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
93
assets/ts/stageNav.ts
Normal 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
68
assets/ts/state.ts
Normal 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 }
|
||||
}
|
||||
@@ -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 }
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user