mirror of
https://github.com/Sped0n/bridget.git
synced 2026-04-14 10:09:31 -07:00
Merge branch 'dev' into main
This commit is contained in:
@@ -1,21 +0,0 @@
|
||||
.cursor {
|
||||
position: fixed;
|
||||
z-index: var(--z-cursor);
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
display: none;
|
||||
cursor: none;
|
||||
pointer-events: none;
|
||||
|
||||
color: white;
|
||||
mix-blend-mode: difference;
|
||||
}
|
||||
|
||||
.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cursorInner {
|
||||
transform: translate3d(-50%, -50%, 0);
|
||||
}
|
||||
6
assets/scss/_core/_font.scss
Normal file
6
assets/scss/_core/_font.scss
Normal file
@@ -0,0 +1,6 @@
|
||||
@font-face {
|
||||
font-family: HelveticaNow;
|
||||
src: url('/fonts/HelveticaNowText-Regular.woff') format('woff');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
28
assets/scss/_core/_mixins.scss
Normal file
28
assets/scss/_core/_mixins.scss
Normal file
@@ -0,0 +1,28 @@
|
||||
$breakpoints: (
|
||||
'mobile': 375px,
|
||||
'tablet': 768px,
|
||||
'laptop': 1024px,
|
||||
'desktop': 1440px
|
||||
) !default;
|
||||
|
||||
// Breakpoints
|
||||
|
||||
@mixin min-width($breakpoint) {
|
||||
@if map-has-key($breakpoints, $breakpoint) {
|
||||
@media (min-width: map-get($breakpoints, $breakpoint)) {
|
||||
@content;
|
||||
}
|
||||
} @else {
|
||||
@error "Unfortunately, no value could be retrieved from `#{$breakpoint}`. " + "Available breakpoints are: #{map-keys($breakpoints)}.";
|
||||
}
|
||||
}
|
||||
|
||||
@mixin max-width($breakpoint) {
|
||||
@if map-has-key($breakpoints, $breakpoint) {
|
||||
@media (max-width: (map-get($breakpoints, $breakpoint) - 1px)) {
|
||||
@content;
|
||||
}
|
||||
} @else {
|
||||
@error "Unfortunately, no value could be retrieved from `#{$breakpoint}`. " + "Available breakpoints are: #{map-keys($breakpoints)}.";
|
||||
}
|
||||
}
|
||||
109
assets/scss/_core/_reset.scss
Normal file
109
assets/scss/_core/_reset.scss
Normal file
@@ -0,0 +1,109 @@
|
||||
/***
|
||||
The new CSS reset - version 1.11.1 (last updated 24.10.2023)
|
||||
GitHub page: https://github.com/elad2412/the-new-css-reset
|
||||
***/
|
||||
|
||||
/*
|
||||
Remove all the styles of the "User-Agent-Stylesheet", except for the 'display' property
|
||||
- The "symbol *" part is to solve Firefox SVG sprite bug
|
||||
- The "html" element is excluded, otherwise a bug in Chrome breaks the CSS hyphens property (https://github.com/elad2412/the-new-css-reset/issues/36)
|
||||
*/
|
||||
*:where(:not(html, iframe, canvas, img, svg, video, audio):not(svg *, symbol *)) {
|
||||
all: unset;
|
||||
display: revert;
|
||||
}
|
||||
|
||||
/* Preferred box-sizing value */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Fix mobile Safari increase font-size on landscape mode */
|
||||
html {
|
||||
-moz-text-size-adjust: none;
|
||||
-webkit-text-size-adjust: none;
|
||||
text-size-adjust: none;
|
||||
}
|
||||
|
||||
/* Reapply the pointer cursor for anchor tags */
|
||||
a,
|
||||
button {
|
||||
cursor: revert;
|
||||
}
|
||||
|
||||
/* Remove list styles (bullets/numbers) */
|
||||
ol,
|
||||
ul,
|
||||
menu,
|
||||
summary {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
/* For images to not be able to exceed their container */
|
||||
img {
|
||||
max-inline-size: 100%;
|
||||
max-block-size: 100%;
|
||||
}
|
||||
|
||||
/* removes spacing between cells in tables */
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
/* Safari - solving issue when using user-select:none on the <body> text input doesn't working */
|
||||
input,
|
||||
textarea {
|
||||
-webkit-user-select: auto;
|
||||
}
|
||||
|
||||
/* revert the 'white-space' property for textarea elements on Safari */
|
||||
textarea {
|
||||
white-space: revert;
|
||||
}
|
||||
|
||||
/* minimum style to allow to style meter element */
|
||||
meter {
|
||||
-webkit-appearance: revert;
|
||||
appearance: revert;
|
||||
}
|
||||
|
||||
/* preformatted text - use only for this feature */
|
||||
:where(pre) {
|
||||
all: revert;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* reset default text opacity of input placeholder */
|
||||
::placeholder {
|
||||
color: unset;
|
||||
}
|
||||
|
||||
/* fix the feature of 'hidden' attribute.
|
||||
display:revert; revert to element instead of attribute */
|
||||
:where([hidden]) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* revert for bug in Chromium browsers
|
||||
- fix for the content editable attribute will work properly.
|
||||
- webkit-user-select: auto; added for Safari in case of using user-select:none on wrapper element*/
|
||||
:where([contenteditable]:not([contenteditable='false'])) {
|
||||
-moz-user-modify: read-write;
|
||||
-webkit-user-modify: read-write;
|
||||
overflow-wrap: break-word;
|
||||
-webkit-line-break: after-white-space;
|
||||
-webkit-user-select: auto;
|
||||
}
|
||||
|
||||
/* apply back the draggable feature - exist only in Chromium and Safari */
|
||||
:where([draggable='true']) {
|
||||
-webkit-user-drag: element;
|
||||
}
|
||||
|
||||
/* Revert Modal native behavior */
|
||||
:where(dialog:modal) {
|
||||
all: revert;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
14
assets/scss/_core/_typography.scss
Normal file
14
assets/scss/_core/_typography.scss
Normal file
@@ -0,0 +1,14 @@
|
||||
@import 'mixins';
|
||||
|
||||
body {
|
||||
line-height: 1.2;
|
||||
font-size: 16px;
|
||||
font-family: HelveticaNow, helvetica, arial, sans-serif;
|
||||
|
||||
@include min-width('tablet') {
|
||||
font-size: 18px;
|
||||
}
|
||||
@include min-width('laptop') {
|
||||
font-size: 19px;
|
||||
}
|
||||
}
|
||||
29
assets/scss/_partial/_collection.scss
Normal file
29
assets/scss/_partial/_collection.scss
Normal 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;
|
||||
}
|
||||
19
assets/scss/_partial/_container.scss
Normal file
19
assets/scss/_partial/_container.scss
Normal file
@@ -0,0 +1,19 @@
|
||||
.container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
z-index: 0;
|
||||
|
||||
width: 100vw;
|
||||
height: var(--window-height);
|
||||
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
background: white;
|
||||
|
||||
overscroll-behavior: none;
|
||||
-webkit-overflow-scrolling: none;
|
||||
}
|
||||
|
||||
.disableScroll {
|
||||
pointer-events: none;
|
||||
}
|
||||
56
assets/scss/_partial/_gallery.scss
Normal file
56
assets/scss/_partial/_gallery.scss
Normal 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;
|
||||
}
|
||||
43
assets/scss/_partial/_nav.scss
Normal file
43
assets/scss/_partial/_nav.scss
Normal file
@@ -0,0 +1,43 @@
|
||||
@import '../_core/mixins';
|
||||
$tablet: map-get($breakpoints, 'tablet') - 1;
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
width: 100%;
|
||||
height: var(--nav-height);
|
||||
padding: 0 var(--space-standard);
|
||||
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
background: white;
|
||||
|
||||
z-index: var(--z-nav);
|
||||
|
||||
// Maintain functionality while container is locked
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.num {
|
||||
width: 0.625em;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.current {
|
||||
font-style: italic;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: $tablet), (hover: none) {
|
||||
nav {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.index,
|
||||
.threshold {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
22
assets/scss/_partial/_stage.scss
Normal file
22
assets/scss/_partial/_stage.scss
Normal file
@@ -0,0 +1,22 @@
|
||||
.stage {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100vw;
|
||||
height: calc(var(--window-height) - var(--nav-height));
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
width: 100vw;
|
||||
height: var(--window-height);
|
||||
object-fit: contain;
|
||||
|
||||
transform: scale(0.6);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
21
assets/scss/_partial/_stageNav.scss
Normal file
21
assets/scss/_partial/_stageNav.scss
Normal file
@@ -0,0 +1,21 @@
|
||||
.navOverlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: var(--z-nav-gallery);
|
||||
|
||||
width: 100vw;
|
||||
height: calc(var(--window-height) - var(--nav-height));
|
||||
|
||||
display: flex;
|
||||
cursor: none;
|
||||
|
||||
&:not(.active) {
|
||||
pointer-events: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,11 @@
|
||||
@import '_variables';
|
||||
@import '_core/base';
|
||||
|
||||
@import '_partial/customCursor';
|
||||
@import '_partial/nav';
|
||||
|
||||
@import '_partial/customCursor';
|
||||
@import '_partial/stage';
|
||||
@import '_partial/stageNav';
|
||||
|
||||
@import '_partial/collection';
|
||||
@import '_partial/gallery';
|
||||
14
assets/ts/container.ts
Normal file
14
assets/ts/container.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { scrollable } from './mobile/scroll'
|
||||
|
||||
export let container: HTMLDivElement
|
||||
|
||||
export function initContainer(): void {
|
||||
container = document.getElementsByClassName('container').item(0) as HTMLDivElement
|
||||
scrollable.addWatcher(() => {
|
||||
if (scrollable.get()) {
|
||||
container.classList.remove('disableScroll')
|
||||
} else {
|
||||
container.classList.add('disableScroll')
|
||||
}
|
||||
})
|
||||
}
|
||||
47
assets/ts/desktop/customCursor.ts
Normal file
47
assets/ts/desktop/customCursor.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { active } from './stage'
|
||||
import { container } from '../container'
|
||||
|
||||
/**
|
||||
* variables
|
||||
*/
|
||||
|
||||
const cursor = document.createElement('div')
|
||||
const cursorInner = document.createElement('div')
|
||||
|
||||
/**
|
||||
* main functions
|
||||
*/
|
||||
|
||||
function onMouse(e: MouseEvent) {
|
||||
const x = e.clientX
|
||||
const y = e.clientY
|
||||
cursor.style.transform = `translate3d(${x}px, ${y}px, 0)`
|
||||
}
|
||||
|
||||
export function setCustomCursor(text: string): void {
|
||||
cursorInner.innerText = text
|
||||
}
|
||||
|
||||
/**
|
||||
* init
|
||||
*/
|
||||
export function initCustomCursor(): void {
|
||||
// cursor class name
|
||||
cursor.className = 'cursor'
|
||||
// cursor inner class name
|
||||
cursorInner.className = 'cursorInner'
|
||||
// append cursor inner to cursor
|
||||
cursor.append(cursorInner)
|
||||
// append cursor to main
|
||||
container.append(cursor)
|
||||
// bind mousemove event to window
|
||||
window.addEventListener('mousemove', onMouse)
|
||||
// add active callback
|
||||
active.addWatcher(() => {
|
||||
if (active.get()) {
|
||||
cursor.classList.add('active')
|
||||
} else {
|
||||
cursor.classList.remove('active')
|
||||
}
|
||||
})
|
||||
}
|
||||
197
assets/ts/desktop/stage.ts
Normal file
197
assets/ts/desktop/stage.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
import { incIndex, state } from '../state'
|
||||
import { gsap, Power3 } from 'gsap'
|
||||
import { ImageJSON } from '../resources'
|
||||
import { Watchable } from '../utils'
|
||||
import { container } from '../container'
|
||||
|
||||
/**
|
||||
* types
|
||||
*/
|
||||
|
||||
export type HistoryItem = { i: number; x: number; y: number }
|
||||
|
||||
/**
|
||||
* variables
|
||||
*/
|
||||
|
||||
let imgs: HTMLImageElement[] = []
|
||||
let last = { x: 0, y: 0 }
|
||||
export const cordHist = new Watchable<HistoryItem[]>([])
|
||||
export const isOpen = new Watchable<boolean>(false)
|
||||
export const isAnimating = new Watchable<boolean>(false)
|
||||
export const active = new Watchable<boolean>(false)
|
||||
|
||||
/**
|
||||
* getter
|
||||
*/
|
||||
|
||||
function getElTrail(): HTMLImageElement[] {
|
||||
return cordHist.get().map((item) => imgs[item.i])
|
||||
}
|
||||
|
||||
function getElTrailCurrent(): HTMLImageElement[] {
|
||||
return getElTrail().slice(-state.get().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]
|
||||
}
|
||||
|
||||
/**
|
||||
* main functions
|
||||
*/
|
||||
|
||||
// on mouse
|
||||
function onMouse(e: MouseEvent): void {
|
||||
if (isOpen.get() || isAnimating.get()) return
|
||||
const cord = { x: e.clientX, y: e.clientY }
|
||||
const travelDist = Math.hypot(cord.x - last.x, cord.y - last.y)
|
||||
|
||||
if (travelDist > state.get().threshold) {
|
||||
last = cord
|
||||
incIndex()
|
||||
|
||||
const newHist = { i: state.get().index, ...cord }
|
||||
cordHist.set([...cordHist.get(), newHist].slice(-state.get().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 + state.get().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 (isAnimating.get()) return
|
||||
|
||||
isOpen.set(true)
|
||||
isAnimating.set(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(() => {
|
||||
isAnimating.set(false)
|
||||
})
|
||||
}
|
||||
|
||||
// close navigation and back to stage
|
||||
export function minimizeImage(): void {
|
||||
if (isAnimating.get()) return
|
||||
|
||||
isOpen.set(false)
|
||||
isAnimating.set(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(() => {
|
||||
isAnimating.set(false)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* init
|
||||
*/
|
||||
|
||||
export function initStage(ijs: ImageJSON[]): void {
|
||||
// create stage element
|
||||
createStage(ijs)
|
||||
// get stage
|
||||
const stage = document.getElementsByClassName('stage').item(0) as HTMLDivElement
|
||||
// get image elements
|
||||
imgs = Array.from(stage.getElementsByTagName('img'))
|
||||
// event listeners
|
||||
stage.addEventListener('click', () => expandImage())
|
||||
stage.addEventListener('keydown', () => expandImage())
|
||||
window.addEventListener('mousemove', onMouse)
|
||||
// watchers
|
||||
isOpen.addWatcher(() => active.set(isOpen.get() && !isAnimating.get()))
|
||||
isAnimating.addWatcher(() => active.set(isOpen.get() && !isAnimating.get()))
|
||||
cordHist.addWatcher(() => setPositions())
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
container.append(stage)
|
||||
}
|
||||
103
assets/ts/desktop/stageNav.ts
Normal file
103
assets/ts/desktop/stageNav.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { setCustomCursor } from './customCursor'
|
||||
import { decIndex, incIndex, state } from '../state'
|
||||
import { increment, decrement } from '../utils'
|
||||
import { cordHist, isOpen, isAnimating, active, minimizeImage } from './stage'
|
||||
import { container } from '../container'
|
||||
|
||||
/**
|
||||
* types
|
||||
*/
|
||||
|
||||
type NavItem = (typeof navItems)[number]
|
||||
|
||||
/**
|
||||
* variables
|
||||
*/
|
||||
|
||||
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() || isAnimating.get()) 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)
|
||||
}
|
||||
active.addWatcher(() => {
|
||||
if (active.get()) {
|
||||
navOverlay.classList.add('active')
|
||||
} else {
|
||||
navOverlay.classList.remove('active')
|
||||
}
|
||||
})
|
||||
container.append(navOverlay)
|
||||
window.addEventListener('keydown', handleKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* hepler
|
||||
*/
|
||||
|
||||
function nextImage() {
|
||||
if (isAnimating.get()) return
|
||||
cordHist.set(
|
||||
cordHist.get().map((item) => {
|
||||
return { ...item, i: increment(item.i, state.get().length) }
|
||||
})
|
||||
)
|
||||
|
||||
incIndex()
|
||||
}
|
||||
|
||||
function prevImage() {
|
||||
if (isAnimating.get()) return
|
||||
cordHist.set(
|
||||
cordHist.get().map((item) => {
|
||||
return { ...item, i: decrement(item.i, state.get().length) }
|
||||
})
|
||||
)
|
||||
|
||||
decIndex()
|
||||
}
|
||||
@@ -1,13 +1,25 @@
|
||||
import { initContainer } from './container'
|
||||
import { initCustomCursor } from './desktop/customCursor'
|
||||
import { initStage } from './desktop/stage'
|
||||
import { initStageNav } from './desktop/stageNav'
|
||||
import { initCollection } from './mobile/collection'
|
||||
import { initGallery } from './mobile/gallery'
|
||||
import { initNav } from './nav'
|
||||
import { initResources } from './resources'
|
||||
import { initState } from './state'
|
||||
import { initCustomCursor } from './customCursor'
|
||||
import { initNav } from './nav'
|
||||
import { initStage } from './stage'
|
||||
import { initStageNav } from './stageNav'
|
||||
import { isMobile } from './utils'
|
||||
|
||||
initContainer()
|
||||
initCustomCursor()
|
||||
const ijs = initResources()
|
||||
initState(ijs.length)
|
||||
initStage(ijs)
|
||||
initStageNav()
|
||||
|
||||
initNav()
|
||||
|
||||
if (!isMobile()) {
|
||||
initStage(ijs)
|
||||
initStageNav()
|
||||
} else {
|
||||
initCollection(ijs)
|
||||
initGallery(ijs)
|
||||
}
|
||||
|
||||
73
assets/ts/mobile/collection.ts
Normal file
73
assets/ts/mobile/collection.ts
Normal 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
198
assets/ts/mobile/gallery.ts
Normal 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)
|
||||
}
|
||||
3
assets/ts/mobile/scroll.ts
Normal file
3
assets/ts/mobile/scroll.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { Watchable } from '../utils'
|
||||
|
||||
export const scrollable = new Watchable<boolean>(true)
|
||||
@@ -1,6 +1,10 @@
|
||||
import { getState, incThreshold, decThreshold } from './state'
|
||||
import { decThreshold, incThreshold, state } from './state'
|
||||
import { expand } from './utils'
|
||||
|
||||
/**
|
||||
* variables
|
||||
*/
|
||||
|
||||
// threshold div
|
||||
const thresholdDiv = document
|
||||
.getElementsByClassName('threshold')
|
||||
@@ -27,6 +31,10 @@ const indexDispNums = Array.from(
|
||||
indexDiv.getElementsByClassName('num')
|
||||
) as HTMLSpanElement[]
|
||||
|
||||
/**
|
||||
* init
|
||||
*/
|
||||
|
||||
export function initNav() {
|
||||
// init threshold text
|
||||
updateThresholdText()
|
||||
@@ -40,15 +48,15 @@ export function initNav() {
|
||||
// helper
|
||||
|
||||
export function updateThresholdText(): void {
|
||||
const thresholdValue: string = expand(getState().threshold)
|
||||
const thresholdValue: string = expand(state.get().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)
|
||||
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]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { increment, decrement } from './utils'
|
||||
import { updateIndexText, updateThresholdText } from './nav'
|
||||
import { Watchable, decrement, increment } from './utils'
|
||||
|
||||
const thresholds = [
|
||||
{ threshold: 20, trailLength: 20 },
|
||||
@@ -18,43 +18,46 @@ const defaultState = {
|
||||
|
||||
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 const state = new Watchable<State>(defaultState)
|
||||
|
||||
export function initState(length: number): void {
|
||||
state.length = length
|
||||
const s = state.get()
|
||||
s.length = length
|
||||
state.set(s)
|
||||
state.addWatcher(() => {
|
||||
updateIndexText()
|
||||
updateThresholdText()
|
||||
})
|
||||
}
|
||||
|
||||
export function setIndex(index: number): void {
|
||||
state.index = index
|
||||
updateIndexText()
|
||||
const s = state.get()
|
||||
s.index = index
|
||||
state.set(s)
|
||||
}
|
||||
|
||||
export function incIndex(): void {
|
||||
state.index = increment(state.index, state.length)
|
||||
updateIndexText()
|
||||
const s = state.get()
|
||||
s.index = increment(s.index, s.length)
|
||||
state.set(s)
|
||||
}
|
||||
|
||||
export function decIndex(): void {
|
||||
state.index = decrement(state.index, state.length)
|
||||
updateIndexText()
|
||||
const s = state.get()
|
||||
s.index = decrement(s.index, s.length)
|
||||
state.set(s)
|
||||
}
|
||||
|
||||
export function incThreshold(): void {
|
||||
state = updateThreshold(state, 1)
|
||||
updateThresholdText()
|
||||
let s = state.get()
|
||||
s = updateThreshold(s, 1)
|
||||
state.set(s)
|
||||
}
|
||||
|
||||
export function decThreshold(): void {
|
||||
state = updateThreshold(state, -1)
|
||||
updateThresholdText()
|
||||
let s = state.get()
|
||||
s = updateThreshold(s, 1)
|
||||
state.set(s)
|
||||
}
|
||||
|
||||
// helper
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* custom helpers
|
||||
*/
|
||||
|
||||
export function increment(num: number, length: number): number {
|
||||
return (num + 1) % length
|
||||
}
|
||||
@@ -14,6 +18,14 @@ export function isMobile(): boolean {
|
||||
return window.matchMedia('(hover: none)').matches
|
||||
}
|
||||
|
||||
export function getRandom(min: number, max: number) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min
|
||||
}
|
||||
|
||||
/**
|
||||
* custom types
|
||||
*/
|
||||
|
||||
export class Watchable<T> {
|
||||
constructor(private obj: T) {}
|
||||
private watchers: (() => void)[] = []
|
||||
|
||||
Reference in New Issue
Block a user