From e897561afad3bb1645d36c39b7c9cfbcd75ebb9e Mon Sep 17 00:00:00 2001 From: Spedon Date: Fri, 24 Mar 2023 16:11:00 +0800 Subject: [PATCH] initial version of transition toward image elements --- assets/css/_partial/_image.scss | 48 ++++++++++++ assets/ts/desktop.ts | 133 +++++++++++++++++++++++--------- assets/ts/elemGen.ts | 32 +++----- assets/ts/overlay.ts | 76 +++++++++++------- assets/ts/utils.ts | 34 +------- 5 files changed, 207 insertions(+), 116 deletions(-) diff --git a/assets/css/_partial/_image.scss b/assets/css/_partial/_image.scss index efd0023..b0fae78 100644 --- a/assets/css/_partial/_image.scss +++ b/assets/css/_partial/_image.scss @@ -97,4 +97,52 @@ &#layer1 { z-index: 5; } +} + +.images { + + img { + position: absolute; + top: 0; + left: 0; + object-fit: contain; + max-height: 55vmin; + max-width: 100vw; + + &[data-status='null'] { + opacity: 1; + } + + &[data-status='top'] { + opacity: 1; + max-height: calc(100vh - var(--footer-height));; + transition-property: transform, max-height; + transition-timing-function: ease-in-out; + transition-duration: 0.5s, 0.5s; + } + + &[data-status='trail'] { + opacity: 0; + margin-top: 40px; + transition-property: opacity, margin-top; + transition-timing-function: ease-out; + transition-duration: 0.2s; + } + + &[data-status='resumeTop'] { + opacity: 1; + max-height: 55vmin; + transition-property: max-height, transform; + transition-timing-function: ease-in-out; + transition-duration: 0.7s, 0.5s; + } + + &[data-status='resume'] { + opacity: 1; + margin-top: 0; + transition-property: opacity, margin-top; + transition-timing-function: ease-out; + transition-duration: 0.2s; + } + } } \ No newline at end of file diff --git a/assets/ts/desktop.ts b/assets/ts/desktop.ts index 440b270..cbfc030 100644 --- a/assets/ts/desktop.ts +++ b/assets/ts/desktop.ts @@ -1,25 +1,9 @@ import { overlayEnable } from './overlay' -import { - calcImageIndex, - center, - createImgElement, - delay, - FIFO, - layerPosSet, - type position -} from './utils' +import { calcImageIndex, center, delay, mouseToTransform, type position } from './utils' import { thresholdIndex, thresholdSensitivityArray } from './thresholdCtl' import { imgIndexSpanUpdate } from './indexDisp' -import { imagesArray, imagesArrayLen } from './dataFetch' -import { layers } from './elemGen' - -// top layer position caching -let topLayerPos: number[] = [0, 0] - -// set top layer position -export const topLayerPosSet = (): void => { - layerPosSet(topLayerPos[0], topLayerPos[1], layers[0]) -} +import { imagesArrayLen } from './dataFetch' +import { imagesDivNodes as images } from './elemGen' // global index for "activated" export let globalIndex: number = 0 @@ -27,13 +11,79 @@ export let globalIndex: number = 0 // last position set as "activated" let last: position = { x: 0, y: 0 } +export let trailingImageIndexes: number[] = [] + +export let transformCache: string[] = [] + +let EnterOverlayClickAbCtl = new AbortController() + +export const stackDepth: number = 5 + +export const pushIndex = ( + index: number, + invert: boolean = false, + autoHide: boolean = true +): number => { + let indexesNum: number = trailingImageIndexes.length + let overflow: number + if (!invert) { + // push the tail index out and hide the image + if (indexesNum === stackDepth) { + trailingImageIndexes.push(index) + overflow = trailingImageIndexes.shift() as number + if (autoHide) { + images[overflow].style.display = 'none' + images[overflow].dataset.status = 'trail' + } + } else { + trailingImageIndexes.push(index) + indexesNum += 1 + } + } else { + if (indexesNum === stackDepth) { + trailingImageIndexes.unshift( + calcImageIndex(index - stackDepth + 1, imagesArrayLen) + ) + overflow = trailingImageIndexes.pop() as number + if (autoHide) { + images[overflow].style.display = 'none' + images[overflow].dataset.status = 'trail' + } + } else { + trailingImageIndexes.unshift( + calcImageIndex(index - indexesNum + 1, imagesArrayLen) + ) + indexesNum += 1 + } + } + return indexesNum +} + // activate top image -const activate = (index: number, x: number, y: number): void => { - const imgElem: HTMLImageElement = createImgElement(imagesArray[index]) - topLayerPos = [x, y] - FIFO(imgElem, layers, true) - topLayerPosSet() - last = { x, y } +const activate = (index: number, mouseX: number, mouseY: number): void => { + EnterOverlayClickAbCtl.abort() + EnterOverlayClickAbCtl = new AbortController() + const indexesNum: number = pushIndex(index) + // set img position + images[index].style.transform = mouseToTransform(mouseX, mouseY, true, true) + images[index].dataset.status = 'null' + // reset z index + for (let i = 0; i < indexesNum; i++) { + images[trailingImageIndexes[i]].style.zIndex = `${i}` + } + images[index].style.display = 'block' + images[index].addEventListener( + 'click', + () => { + void enterOverlay() + }, + { + passive: true, + once: true, + signal: EnterOverlayClickAbCtl.signal + } + ) + last = { x: mouseX, y: mouseY } } // Compare the current mouse position with the last activated position @@ -61,10 +111,18 @@ export const handleOnMove = (e: MouseEvent): void => { async function enterOverlay(): Promise { // stop images animation window.removeEventListener('mousemove', handleOnMove) - // set top image - center(layers[0]) - for (let i = 0; i <= 4; i++) { - layers[i].dataset.status = `t${i}` + const indexesNum: number = trailingImageIndexes.length + for (let i = 0; i < indexesNum; i++) { + const e: HTMLImageElement = images[trailingImageIndexes[i]] + transformCache.push(e.style.transform) + 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.dataset.status = 'trail' + e.style.transitionDelay = `${0.1 * i}s` + } } await delay(1600) // Offset previous self increment of global index (by handleOnMove) @@ -76,15 +134,6 @@ async function enterOverlay(): Promise { // initialization export const trackMouseInit = (): void => { window.addEventListener('mousemove', handleOnMove) - layers[0].addEventListener( - 'click', - () => { - void enterOverlay() - }, - { - passive: true - } - ) } export const globalIndexDec = (): void => { @@ -94,3 +143,11 @@ export const globalIndexDec = (): void => { export const globalIndexInc = (): void => { globalIndex++ } + +export const emptyTransformCache = (): void => { + transformCache = [] +} + +export const emptyTrailingImageIndexes = (): void => { + trailingImageIndexes = [] +} diff --git a/assets/ts/elemGen.ts b/assets/ts/elemGen.ts index 632b58a..cdda2b2 100644 --- a/assets/ts/elemGen.ts +++ b/assets/ts/elemGen.ts @@ -1,7 +1,10 @@ +import { imagesArray, imagesArrayLen } from './dataFetch' +import { createImgElement } from './utils' + // get components of overlay export let overlayCursor: HTMLDivElement export let cursorInnerContent: HTMLDivElement -export let layers: HTMLDivElement[] +export let imagesDivNodes: NodeListOf const passDesktopElements = (): void => { overlayCursor = document @@ -10,21 +13,8 @@ const passDesktopElements = (): void => { cursorInnerContent = document .getElementsByClassName('cursor_innerText') .item(0) as HTMLDivElement - layers = [ - document.getElementById('layer1') as HTMLDivElement, - document.getElementById('layer2') as HTMLDivElement, - document.getElementById('layer3') as HTMLDivElement, - document.getElementById('layer4') as HTMLDivElement, - document.getElementById('layer5') as HTMLDivElement - ] -} - -const createLayerDiv = (layerID: number): HTMLDivElement => { - const layerDiv: HTMLDivElement = document.createElement('div') - layerDiv.className = 'image_container' - layerDiv.id = `layer${layerID}` - layerDiv.dataset.status = 'null' - return layerDiv + imagesDivNodes = document.getElementsByClassName('images')[0] + .childNodes as NodeListOf } const createCursorDiv = (): HTMLDivElement => { @@ -39,11 +29,11 @@ const createCursorDiv = (): HTMLDivElement => { export const createDesktopElements = (): void => { const mainDiv = document.getElementById('main') as HTMLDivElement mainDiv.appendChild(createCursorDiv()) - const desktopWrapper: HTMLDivElement = document.createElement('div') - desktopWrapper.className = 'desktopWrapper' - for (let i = 0; i < 15; i++) { - desktopWrapper.appendChild(createLayerDiv(i)) + const imagesDiv: HTMLDivElement = document.createElement('div') + imagesDiv.className = 'images' + for (let i = 0; i < imagesArrayLen; i++) { + imagesDiv.appendChild(createImgElement(imagesArray[i])) } - mainDiv.appendChild(desktopWrapper) + mainDiv.appendChild(imagesDiv) passDesktopElements() } diff --git a/assets/ts/overlay.ts b/assets/ts/overlay.ts index 2117a53..81f86ef 100644 --- a/assets/ts/overlay.ts +++ b/assets/ts/overlay.ts @@ -1,21 +1,20 @@ -import { - delay, - center, - createImgElement, - calcImageIndex, - FIFO, - mouseToTransform -} from './utils' +import { delay, center, calcImageIndex, mouseToTransform } from './utils' import { handleOnMove, globalIndex, globalIndexDec, globalIndexInc, - topLayerPosSet + trailingImageIndexes, + transformCache, + pushIndex, + emptyTransformCache, + emptyTrailingImageIndexes } from './desktop' -import { imagesArray, imagesArrayLen } from './dataFetch' +import { imagesArrayLen } from './dataFetch' import { imgIndexSpanUpdate } from './indexDisp' -import { overlayCursor, cursorInnerContent, layers } from './elemGen' +import { overlayCursor, cursorInnerContent, imagesDivNodes as images } from './elemGen' + +let oneThird: number = Math.round(window.innerWidth / 3) // set cursor text const setCursorText = (text: string): void => { @@ -35,7 +34,7 @@ const disableListener = (): void => { // enable overlay export const overlayEnable = (): void => { - overlayCursor.style.zIndex = '7' + overlayCursor.style.zIndex = '100' setListener() } @@ -44,41 +43,62 @@ export const overlayDisable = (): void => { overlayCursor.style.zIndex = '-1' setCursorText('') disableListener() - // Add back previous self increment of global index (by handleOnMove) - globalIndexInc() } // handle close click async function handleCloseClick(): Promise { overlayDisable() - topLayerPosSet() - for (let i: number = 0; i <= 4; i++) { - layers[i].dataset.status = `r${i}` + const indexesNum = trailingImageIndexes.length + emptyTrailingImageIndexes() + for (let i: number = 0; i < indexesNum; i++) { + const e: HTMLImageElement = images[calcImageIndex(globalIndex - i, imagesArrayLen)] + trailingImageIndexes.unshift(calcImageIndex(globalIndex - i, imagesArrayLen)) + if (i === 0) { + e.style.transitionDelay = '0s, 0.7s' + } else { + e.style.transitionDelay = `${1.2 + 0.1 * i - 0.1}s` + e.style.display = 'block' + } + e.style.transform = transformCache[indexesNum - i - 1] + e.style.zIndex = `${indexesNum - i - 1}` + e.dataset.status = i === 0 ? 'resumeTop' : 'resume' } await delay(1700) - for (let i: number = 0; i <= 4; i++) { - layers[i].dataset.status = 'null' + for (let i: number = 0; i < indexesNum; i++) { + images[calcImageIndex(globalIndex - i, imagesArrayLen)].dataset.status = 'null' } + // Add back previous self increment of global index (by handleOnMove) + globalIndexInc() window.addEventListener('mousemove', handleOnMove, { passive: true }) + emptyTransformCache() } const handlePrevClick = (): void => { + const imgIndex: number = calcImageIndex(globalIndex, imagesArrayLen) globalIndexDec() - const imgIndex = calcImageIndex(globalIndex, imagesArrayLen) - FIFO(createImgElement(imagesArray[imgIndex]), layers, false) - imgIndexSpanUpdate(imgIndex + 1, imagesArrayLen) + const prevImgIndex = calcImageIndex(globalIndex, imagesArrayLen) + pushIndex(prevImgIndex, true, false) + images[imgIndex].style.display = 'none' + center(images[prevImgIndex]) + images[prevImgIndex].dataset.status = 'top' + images[prevImgIndex].style.display = 'block' + imgIndexSpanUpdate(prevImgIndex + 1, imagesArrayLen) } const handleNextClick = (): void => { + const imgIndex: number = calcImageIndex(globalIndex, imagesArrayLen) globalIndexInc() - const imgIndex = calcImageIndex(globalIndex, imagesArrayLen) - FIFO(createImgElement(imagesArray[imgIndex]), layers, false) - imgIndexSpanUpdate(imgIndex + 1, imagesArrayLen) + const nextImgIndex = calcImageIndex(globalIndex, imagesArrayLen) + pushIndex(nextImgIndex, false, false) + images[imgIndex].style.display = 'none' + center(images[nextImgIndex]) + images[nextImgIndex].dataset.status = 'top' + images[nextImgIndex].style.display = 'block' + imgIndexSpanUpdate(nextImgIndex + 1, imagesArrayLen) } const handleOverlayMouseMove = (e: MouseEvent): void => { setTextPos(e) - const oneThird: number = Math.round(window.innerWidth / 3) if (e.clientX < oneThird) { setCursorText('PREV') overlayCursor.dataset.status = 'PREV' @@ -114,6 +134,7 @@ export const vwRefreshInit = (): void => { window.addEventListener( 'resize', () => { + oneThird = Math.round(window.innerWidth / 3) // reset footer height const r = document.querySelector(':root') as HTMLStyleElement if (window.innerWidth > 768) { @@ -122,7 +143,8 @@ export const vwRefreshInit = (): void => { r.style.setProperty('--footer-height', '31px') } // recenter image (only in overlay) - if (layers[0].dataset.status === 't0') center(layers[0]) + const i: HTMLImageElement = images[calcImageIndex(globalIndex, imagesArrayLen)] + if (i.dataset.status === 'top') center(i) }, { passive: true } ) diff --git a/assets/ts/utils.ts b/assets/ts/utils.ts index ba9e27d..7958f5b 100644 --- a/assets/ts/utils.ts +++ b/assets/ts/utils.ts @@ -23,29 +23,6 @@ export const duper = (num: number): string => { return ('0000' + num.toString()).slice(-4) } -// FIFO data array for image display -export const FIFO = ( - element: HTMLImageElement, - layersArray: HTMLDivElement[], - passPosition: boolean = true -): void => { - function layerProcess(layerL: HTMLDivElement, layerH: HTMLDivElement): void { - if (layerL.childElementCount !== 0) - layerL.removeChild(layerL.lastElementChild as HTMLImageElement) - if (layerH.childElementCount !== 0) { - const layerHNode = layerH.lastElementChild as HTMLImageElement - layerL.appendChild(layerHNode.cloneNode(true)) - if (passPosition) layerL.style.transform = layerH.style.transform - } - } - for (let i: number = 4; i >= 1; i--) { - layerProcess(layersArray[i], layersArray[i - 1]) - } - if (layersArray[0].childElementCount !== 0) - layersArray[0].removeChild(layersArray[0].lastElementChild as HTMLImageElement) - layersArray[0].appendChild(element) -} - export const mouseToTransform = ( x: number, y: number, @@ -57,11 +34,6 @@ export const mouseToTransform = ( }, ${centerCorrection ? `calc(${y}px - 50%)` : `${y}px`}${accelerate ? ', 0' : ''})` } -// set position for layer -export const layerPosSet = (x: number, y: number, layer: HTMLDivElement): void => { - layer.style.transform = mouseToTransform(x, y) -} - // eslint-disable-next-line @typescript-eslint/promise-function-async export function delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)) @@ -73,7 +45,7 @@ export const removeAllEventListeners = (e: Node): Node => { } // center top div -export const center = (e: HTMLDivElement): void => { +export const center = (e: HTMLElement): void => { const x: number = window.innerWidth / 2 let y: number if (window.innerWidth > 768) { @@ -90,7 +62,9 @@ export const createImgElement = (input: ImageData): HTMLImageElement => { img.setAttribute('alt', '') img.setAttribute('height', input.imgH) img.setAttribute('width', input.imgW) - img.style.backgroundImage = `linear-gradient(15deg, ${input.pColor}, ${input.sColor})` + img.style.display = 'none' + img.dataset.status = 'trail' + // img.style.backgroundImage = `linear-gradient(15deg, ${input.pColor}, ${input.sColor})` return img }