diff --git a/assets/scss/_partial/_collection.scss b/assets/scss/_partial/_collection.scss new file mode 100644 index 0000000..ec63f67 --- /dev/null +++ b/assets/scss/_partial/_collection.scss @@ -0,0 +1,29 @@ +.collection { + display: flex; + flex-direction: column; + gap: 20vh; + + padding-top: 50vh; + margin-top: calc(var(--nav-height) * -1); + + img { + position: sticky; + top: 50vh; + + width: 60vw; + height: 20vh; + + object-fit: contain; + + transform: translate3d(0, -50%, 0); + align-self: center; + + &:last-child { + margin-bottom: 20vh; + } + } +} + +.hidden { + display: none; +} diff --git a/assets/scss/_partial/_gallery.scss b/assets/scss/_partial/_gallery.scss new file mode 100644 index 0000000..1d82815 --- /dev/null +++ b/assets/scss/_partial/_gallery.scss @@ -0,0 +1,56 @@ +.gallery { + pointer-events: all; + + position: fixed; + top: var(--nav-height); + z-index: var(--z-nav-gallery); + + display: flex; + flex-direction: column; + + width: 100vw; + height: calc(var(--window-height) - var(--nav-height)); + background: white; + transform: translate3d(0, 100%, 0); + + .galleryInner { + flex: 1; + height: 0; + + .swiper-slide { + display: flex; + align-items: center; + justify-content: center; + + img { + width: 100%; + height: 100%; + object-fit: contain; + } + } + } + + .nav { + height: var(--nav-height); + padding: var(--space-standard); + + display: flex; + justify-content: space-between; + align-items: center; + } +} + +.curtain { + position: fixed; + top: 0; + left: 0; + z-index: var(--z-curtain); + + width: 100vw; + height: var(--window-height); + + background: rgba(0, 0, 0, 0.5); + opacity: 0; + + pointer-events: none; +} diff --git a/assets/ts/mobile/collection.ts b/assets/ts/mobile/collection.ts index e69de29..a661bc2 100644 --- a/assets/ts/mobile/collection.ts +++ b/assets/ts/mobile/collection.ts @@ -0,0 +1,73 @@ +import { container } from '../container' +import { ImageJSON } from '../resources' +import { setIndex } from '../state' +import { getRandom, Watchable } from '../utils' +import { slideUp } from './gallery' + +/** + * variables + */ + +export let imgs: HTMLImageElement[] = [] +export const mounted = new Watchable(false) + +/** + * main functions + */ + +function handleClick(i: number): void { + setIndex(i) + slideUp() +} + +/** + * init + */ + +export function initCollection(ijs: ImageJSON[]): void { + createCollection(ijs) + // get container + const container = document + .getElementsByClassName('collection') + .item(0) as HTMLDivElement + // add watcher + mounted.addWatcher(() => { + if (mounted.get()) { + container.classList.remove('hidden') + } else { + container.classList.add('hidden') + } + }) + // get image elements + imgs = Array.from(container.getElementsByTagName('img')) + // add event listeners + imgs.forEach((img, i) => { + img.addEventListener('click', () => handleClick(i)) + img.addEventListener('keydown', () => handleClick(i)) + }) +} + +/** + * helper + */ + +function createCollection(ijs: ImageJSON[]): void { + // create container for images + const collection: HTMLDivElement = document.createElement('div') + collection.className = 'collection' + // append images to container + for (let [i, ij] of ijs.entries()) { + // random x and y + const x = i !== 0 ? getRandom(-25, 25) : 0 + const y = i !== 0 ? getRandom(-30, 30) : 0 + // element + const e = document.createElement('img') + e.src = ij.url + e.height = ij.imgH + e.width = ij.imgW + e.alt = 'image' + e.style.transform = `translate3d(${x}%, ${y - 50}%, 0)` + collection.append(e) + } + container.append(collection) +} diff --git a/assets/ts/mobile/gallery.ts b/assets/ts/mobile/gallery.ts new file mode 100644 index 0000000..a9167ae --- /dev/null +++ b/assets/ts/mobile/gallery.ts @@ -0,0 +1,198 @@ +import { Power3, gsap } from 'gsap' +import Swiper from 'swiper' +import { container } from '../container' +import { ImageJSON } from '../resources' +import { setIndex, state } from '../state' +import { Watchable, expand } from '../utils' +import { imgs, mounted } from './collection' +import { scrollable } from './scroll' + +/** + * variables + */ + +let swiperNode: HTMLDivElement +let gallery: HTMLDivElement +let curtain: HTMLDivElement +let swiper: Swiper +const isAnimating = new Watchable(false) +let lastIndex = -1 +let indexDispNums: HTMLSpanElement[] = [] + +/** + * main functions + */ + +export function slideUp(): void { + if (isAnimating.get()) return + isAnimating.set(true) + + gsap.to(curtain, { + opacity: 1, + duration: 1 + }) + + gsap.to(gallery, { + y: 0, + ease: Power3.easeInOut, + duration: 1, + delay: 0.4 + }) + + setTimeout(() => { + scrollable.set(false) + isAnimating.set(false) + }, 1200) +} + +function slideDown(): void { + scrollable.set(true) + scrollToActive() + + gsap.to(gallery, { + y: '100%', + ease: Power3.easeInOut, + duration: 1 + }) + + gsap.to(curtain, { + opacity: 0, + duration: 1.2, + delay: 0.4 + }) +} + +/** + * init + */ + +export function initGallery(ijs: ImageJSON[]): void { + // create gallery + createGallery(ijs) + // get elements + indexDispNums = Array.from( + document.getElementsByClassName('nav').item(0)!.getElementsByClassName('num') + ) as HTMLSpanElement[] + swiperNode = document.getElementsByClassName('galleryInner').item(0) as HTMLDivElement + gallery = document.getElementsByClassName('gallery').item(0) as HTMLDivElement + curtain = document.getElementsByClassName('curtain').item(0) as HTMLDivElement + // state watcher + state.addWatcher(() => { + const s = state.get() + // change slide only when index is changed + if (s.index === lastIndex) return + changeSlide(s.index) + updateIndexText() + lastIndex = s.index + }) + // mounted watcher + mounted.addWatcher(() => { + if (!mounted.get()) return + scrollable.set(true) + swiper = new Swiper(swiperNode, { spaceBetween: 20 }) + swiper.on('slideChange', ({ realIndex }) => { + setIndex(realIndex) + }) + }) + + // mounted + mounted.set(true) +} + +/** + * helper + */ + +function changeSlide(slide: number): void { + console.log(slide) + swiper?.slideTo(slide, 0) +} + +function scrollToActive(): void { + imgs[state.get().index].scrollIntoView({ + block: 'center', + behavior: 'auto' + }) +} + +function updateIndexText(): void { + const indexValue: string = expand(state.get().index + 1) + const indexLength: string = expand(state.get().length) + indexDispNums.forEach((e: HTMLSpanElement, i: number) => { + if (i < 4) { + e.innerText = indexValue[i] + } else { + e.innerText = indexLength[i - 4] + } + }) +} + +function createGallery(ijs: ImageJSON[]): void { + /** + * gallery + * |- galleryInner + * |- swiper-wrapper + * |- swiper-slide + * |- img + * |- swiper-slide + * |- img + * |- ... + * |- nav + * |- index + * |- close + */ + // swiper wrapper + const _swiperWrapper = document.createElement('div') + _swiperWrapper.className = 'swiper-wrapper' + // swiper slide + for (let ij of ijs) { + const _swiperSlide = document.createElement('div') + _swiperSlide.className = 'swiper-slide' + // img + const e = document.createElement('img') + e.src = ij.url + e.alt = 'image' + // append + _swiperSlide.append(e) + _swiperWrapper.append(_swiperSlide) + } + // swiper node + const _swiperNode = document.createElement('div') + _swiperNode.className = 'galleryInner' + _swiperNode.append(_swiperWrapper) + // index + const _index = document.createElement('div') + _index.insertAdjacentHTML( + 'afterbegin', + ` + / + ` + ) + // close + const _close = document.createElement('div') + _close.innerText = 'Close' + _close.addEventListener('click', () => slideDown()) + _close.addEventListener('keydown', () => slideDown()) + // nav + const _navDiv = document.createElement('div') + _navDiv.className = 'nav' + _navDiv.append(_index, _close) + // gallery + const _gallery = document.createElement('div') + _gallery.className = 'gallery' + _gallery.append(_swiperNode) + _gallery.append(_navDiv) + + /** + * curtain + */ + const _curtain = document.createElement('div') + _curtain.className = 'curtain' + + /** + * container + * |- gallery + * |- curtain + */ + container.append(_gallery, _curtain) +} diff --git a/assets/ts/mobile/scroll.ts b/assets/ts/mobile/scroll.ts new file mode 100644 index 0000000..46ecb41 --- /dev/null +++ b/assets/ts/mobile/scroll.ts @@ -0,0 +1,3 @@ +import { Watchable } from '../utils' + +export const scrollable = new Watchable(true) diff --git a/layouts/partials/head.html b/layouts/partials/head.html index c3d84e6..44d92c6 100644 --- a/layouts/partials/head.html +++ b/layouts/partials/head.html @@ -1,9 +1,13 @@ {{- $fingerprint := .Scratch.Get "fingerprint" | default "" -}} -{{- $style := dict "Source" "css/style.scss" "Fingerprint" $fingerprint -}} +{{- $style := dict "Source" "scss/style.scss" "Fingerprint" $fingerprint -}} {{- $options := dict "targetPath" "css/style.min.css" "enableSourceMap" true -}} {{- $style = dict "Context" . "ToCSS" $options | merge $style -}} {{- partial "plugin/style.html" $style -}} {{- $esBuildOpts := dict "minify" hugo.IsProduction -}} {{- $script := resources.Get "ts/main.ts" | js.Build $esBuildOpts -}} +