feat: add new SCSS partials for collection and gallery components

- Added a new SCSS partial `_collection.scss` to define styles for the collection component.
- Added a new SCSS partial `_gallery.scss` to define styles for the gallery component.

The new partials contain styles for the collection and gallery components, including layout, positioning, and animations.

---

feat: add support for gallery functionality in mobile

- Created a new file `gallery.ts` to handle the gallery functionality in the mobile view.
- Added functions `slideUp()` and `slideDown()` to handle the sliding animation of the gallery.
- Initialized the gallery with the provided image data in the `initGallery()` function.
- Added event listeners to update the active slide and index text when the slide is changed.
- Created a helper function `changeSlide()` to change the active slide in the gallery.
- Created a helper function `scrollToActive()` to scroll to the active image in the collection.
- Created a helper function `updateIndexText()` to update the index text in the navigation.
- Created a helper function `createGallery()` to dynamically create the gallery HTML structure.

---

feat: add scrollable flag for mobile view

- Created a new file `scroll.ts` to handle the scrollable flag for the mobile view.
- Added a `scrollable` variable as a Watchable object to control the scrollability of the view.

---

fix: change port variable case from lowercase `port` to uppercase `PORT` to improve semantics

- Changed the variable name `port` to `PORT` in the `server.ts` file to improve code readability and semantics.
- The variable `PORT` is now used to define the port number for the server to listen on.
This commit is contained in:
Sped0n
2023-10-29 22:11:04 +08:00
parent 2e3fc3d7b6
commit bd2354e2f5
6 changed files with 364 additions and 1 deletions

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<boolean>(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)
}

198
assets/ts/mobile/gallery.ts Normal file
View File

@@ -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<boolean>(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',
`<span class="num"></span><span class="num"></span><span class="num"></span><span class="num"></span>
<span>/</span>
<span class="num"></span><span class="num"></span><span class="num"></span><span class="num"></span>`
)
// 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)
}

View File

@@ -0,0 +1,3 @@
import { Watchable } from '../utils'
export const scrollable = new Watchable<boolean>(true)

View File

@@ -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 -}}
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css"
/>
<script type="text/javascript" src="{{ $script.RelPermalink }}" defer></script>