diff --git a/assets/ts/customCursor.ts b/assets/ts/customCursor.ts new file mode 100644 index 0000000..43cc50e --- /dev/null +++ b/assets/ts/customCursor.ts @@ -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 +} diff --git a/assets/ts/dataFetch.ts b/assets/ts/dataFetch.ts deleted file mode 100644 index 5a4e92c..0000000 --- a/assets/ts/dataFetch.ts +++ /dev/null @@ -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 diff --git a/assets/ts/desktop.ts b/assets/ts/desktop.ts deleted file mode 100644 index daea4a3..0000000 --- a/assets/ts/desktop.ts +++ /dev/null @@ -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 { - // 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]) - } -} diff --git a/assets/ts/elemGen.ts b/assets/ts/elemGen.ts deleted file mode 100644 index 6b54071..0000000 --- a/assets/ts/elemGen.ts +++ /dev/null @@ -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 - -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 -} - -const passMobileElements = (): void => { - imagesDivNodes = document.getElementsByClassName('imagesMobile')[0] - .childNodes as NodeListOf -} - -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() -} diff --git a/assets/ts/imageCache.ts b/assets/ts/imageCache.ts deleted file mode 100644 index 46320c3..0000000 --- a/assets/ts/imageCache.ts +++ /dev/null @@ -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 -} diff --git a/assets/ts/indexDisp.ts b/assets/ts/indexDisp.ts deleted file mode 100644 index e772230..0000000 --- a/assets/ts/indexDisp.ts +++ /dev/null @@ -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] - } - } -} diff --git a/assets/ts/main.ts b/assets/ts/main.ts index 19b6283..2d58579 100644 --- a/assets/ts/main.ts +++ b/assets/ts/main.ts @@ -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() diff --git a/assets/ts/mobile.ts b/assets/ts/mobile.ts deleted file mode 100644 index b3e93ee..0000000 --- a/assets/ts/mobile.ts +++ /dev/null @@ -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' - }) -} diff --git a/assets/ts/nav.ts b/assets/ts/nav.ts new file mode 100644 index 0000000..6a8b235 --- /dev/null +++ b/assets/ts/nav.ts @@ -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] + } + }) +} diff --git a/assets/ts/overlay.ts b/assets/ts/overlay.ts deleted file mode 100644 index 544a013..0000000 --- a/assets/ts/overlay.ts +++ /dev/null @@ -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 { - // 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 } - ) -} diff --git a/assets/ts/resources.ts b/assets/ts/resources.ts new file mode 100644 index 0000000..4eb0b06 --- /dev/null +++ b/assets/ts/resources.ts @@ -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 + } + ) +} diff --git a/assets/ts/stage.ts b/assets/ts/stage.ts new file mode 100644 index 0000000..7d0ee37 --- /dev/null +++ b/assets/ts/stage.ts @@ -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) +} diff --git a/assets/ts/stageNav.ts b/assets/ts/stageNav.ts new file mode 100644 index 0000000..cba6d97 --- /dev/null +++ b/assets/ts/stageNav.ts @@ -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() +} diff --git a/assets/ts/state.ts b/assets/ts/state.ts new file mode 100644 index 0000000..4aebfe9 --- /dev/null +++ b/assets/ts/state.ts @@ -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 } +} diff --git a/assets/ts/thresholdCtl.ts b/assets/ts/thresholdCtl.ts deleted file mode 100644 index 30f2a21..0000000 --- a/assets/ts/thresholdCtl.ts +++ /dev/null @@ -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 } - ) -} diff --git a/assets/ts/utils.ts b/assets/ts/utils.ts index 04da9fa..63743a6 100644 --- a/assets/ts/utils.ts +++ b/assets/ts/utils.ts @@ -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 { - 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, - 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 }