Merge branch 'dev' into main

This commit is contained in:
Sped0n
2023-10-29 22:15:47 +08:00
31 changed files with 1375 additions and 314 deletions

View File

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

View File

@@ -0,0 +1,6 @@
@font-face {
font-family: HelveticaNow;
src: url('/fonts/HelveticaNowText-Regular.woff') format('woff');
font-weight: 400;
font-style: normal;
}

View 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)}.";
}
}

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

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

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

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

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

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

View File

@@ -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
View 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')
}
})
}

View 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
View 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)
}

View 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()
}

View File

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

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,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]

View File

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

View File

@@ -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)[] = []