mirror of
https://github.com/Sped0n/bridget.git
synced 2026-04-14 18:19:29 -07:00
Compare commits
160 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
222d97c97c | ||
|
|
b9cbe289c2 | ||
|
|
765bbcb201 | ||
|
|
960e473893 | ||
|
|
4d4cad18b6 | ||
|
|
d8611b834b | ||
|
|
7e7c96be55 | ||
|
|
dc2948a84c | ||
|
|
ac774c68ee | ||
|
|
a606497735 | ||
|
|
4b2af94540 | ||
|
|
e2d5887912 | ||
|
|
27837bcf07 | ||
|
|
f7c811374e | ||
|
|
cb5b0ba312 | ||
|
|
f87d28ed93 | ||
|
|
4abc531e4b | ||
|
|
f69492c2b4 | ||
|
|
6839eb2e7d | ||
|
|
6d99db9e38 | ||
|
|
7515704301 | ||
|
|
71acaeff02 | ||
|
|
b538d984df | ||
|
|
c694cb13b2 | ||
|
|
3a75206ef2 | ||
|
|
a89a551013 | ||
|
|
472f9172ca | ||
|
|
44be2c4a50 | ||
|
|
9589c369f0 | ||
|
|
3e0a489d7d | ||
|
|
5f1204a889 | ||
|
|
0f96c7a8a8 | ||
|
|
0057c472ef | ||
|
|
6d68202b83 | ||
|
|
010406e77e | ||
|
|
758a2d1a62 | ||
|
|
ec1df7f070 | ||
|
|
f09988f32d | ||
|
|
e16aaca42b | ||
|
|
80442eb569 | ||
|
|
9039e04b38 | ||
|
|
56304e09f1 | ||
|
|
0cbaacbc0e | ||
|
|
f78449adb9 | ||
|
|
4d55bca248 | ||
|
|
ad998ba153 | ||
|
|
bd95ab861b | ||
|
|
919489c7e9 | ||
|
|
efa72bb763 | ||
|
|
90f79113c7 | ||
|
|
b5d0754c45 | ||
|
|
8a751b7437 | ||
|
|
212dca53e8 | ||
|
|
989a7f4951 | ||
|
|
f25b71a858 | ||
|
|
1c386386f3 | ||
|
|
797b59a38a | ||
|
|
dbbc063353 | ||
|
|
f7e87c3c15 | ||
|
|
1242146140 | ||
|
|
a8c1f948db | ||
|
|
dc30a8cd16 | ||
|
|
71c7b02c1d | ||
|
|
6cfcfb272a | ||
|
|
c239112cb2 | ||
|
|
637391bb34 | ||
|
|
fe38e1289a | ||
|
|
08bd9a76ef | ||
|
|
14f481b2c0 | ||
|
|
546791e90b | ||
|
|
136c2303f9 | ||
|
|
b13f1bf454 | ||
|
|
313a6f294a | ||
|
|
2804be174d | ||
|
|
fec976ad9e | ||
|
|
582433874c | ||
|
|
037cdbd679 | ||
|
|
50ff7f62bb | ||
|
|
3fe8477646 | ||
|
|
64b43597a4 | ||
|
|
94603999d7 | ||
|
|
c3c42e4aca | ||
|
|
38f4895233 | ||
|
|
636fea3496 | ||
|
|
c15b8a3e46 | ||
|
|
0cd891d16b | ||
|
|
4670c00157 | ||
|
|
032a75f581 | ||
|
|
5e40e9041e | ||
|
|
9c7731b8e5 | ||
|
|
d59475fbdc | ||
|
|
27c3b754ac | ||
|
|
1359adee59 | ||
|
|
b8d7193fb9 | ||
|
|
a06027401b | ||
|
|
5501d10cd0 | ||
|
|
31e20f752a | ||
|
|
fd300d8104 | ||
|
|
e91a7c6633 | ||
|
|
321e4b618a | ||
|
|
f630b85669 | ||
|
|
a431d74e8c | ||
|
|
975e084ea8 | ||
|
|
771b9b34ab | ||
|
|
1e5155c49e | ||
|
|
44750037f3 | ||
|
|
9af0090644 | ||
|
|
b05eec64cb | ||
|
|
284cfa4e84 | ||
|
|
11ea5c101b | ||
|
|
d4b9a4588a | ||
|
|
4ed332314d | ||
|
|
82ab5b996f | ||
|
|
81c14d9f2b | ||
|
|
7cf5f9ad2d | ||
|
|
0f69ec5f96 | ||
|
|
e6a9cacfef | ||
|
|
b24e401a28 | ||
|
|
ca9fef6c2d | ||
|
|
2835d46a57 | ||
|
|
3779bc7ce6 | ||
|
|
de16c6b443 | ||
|
|
d536303f8e | ||
|
|
76d420c2c9 | ||
|
|
c448648127 | ||
|
|
7e0e6244b0 | ||
|
|
c76835d474 | ||
|
|
39c13e242d | ||
|
|
61c86692ee | ||
|
|
cd63e9fec5 | ||
|
|
8bc8fded81 | ||
|
|
8bc54835e5 | ||
|
|
bd632b0ca4 | ||
|
|
b550dcd236 | ||
|
|
1b067c26d0 | ||
|
|
665522c8d1 | ||
|
|
e67865b82b | ||
|
|
6440556242 | ||
|
|
59227fb265 | ||
|
|
d291aab64f | ||
|
|
c290595a35 | ||
|
|
a6a576246f | ||
|
|
93629a4e6b | ||
|
|
a909afee97 | ||
|
|
9c15a367ea | ||
|
|
73ee16c6fb | ||
|
|
91b0314c5d | ||
|
|
d1a1dba210 | ||
|
|
110ff665e7 | ||
|
|
b39d563e77 | ||
|
|
0e74655820 | ||
|
|
8926caed69 | ||
|
|
19f54640f9 | ||
|
|
56b87d6393 | ||
|
|
75d8310953 | ||
|
|
a9f164f2af | ||
|
|
7773f184aa | ||
|
|
bc501934ae | ||
|
|
44b619e49b | ||
|
|
024d013219 |
95
.github/workflows/build.yml
vendored
95
.github/workflows/build.yml
vendored
@@ -12,91 +12,72 @@ permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
filter:
|
||||
runs-on: ubuntu-latest
|
||||
name: Filter
|
||||
outputs:
|
||||
any_changed: ${{ steps.changed-files-specific.outputs.any_changed }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get changed files in scope
|
||||
id: changed-files-specific
|
||||
uses: tj-actions/changed-files@v47
|
||||
with:
|
||||
files: |
|
||||
package.json
|
||||
pnpm-lock.yaml
|
||||
tsconfig.json
|
||||
vite.config.ts
|
||||
assets/**
|
||||
|
||||
build:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
name: Build (Hugo ${{ matrix.hugo-version }})
|
||||
needs: [filter]
|
||||
if: |
|
||||
github.event.repository.fork == false
|
||||
name: Build (Hugo ${{ matrix.hugo-label }})
|
||||
if: github.event.repository.fork == false
|
||||
strategy:
|
||||
matrix:
|
||||
hugo-version: ['latest', '0.114.0']
|
||||
include:
|
||||
- hugo-version: latest
|
||||
hugo-label: Latest
|
||||
- hugo-version: '0.114.0'
|
||||
hugo-label: 'v0.114.0'
|
||||
steps:
|
||||
- name: Set current date as env variable
|
||||
run: |
|
||||
echo "builddate=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
id: version
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
|
||||
- name: Setup Hugo
|
||||
uses: peaceiris/actions-hugo@v2.6.0
|
||||
- name: Setup Mise
|
||||
uses: jdx/mise-action@v4
|
||||
with:
|
||||
hugo-version: ${{ matrix.hugo-version }}
|
||||
extended: true
|
||||
install_args: node@latest pnpm@10 hugo-extended@${{ matrix.hugo-version }}
|
||||
tool_versions: |
|
||||
node latest
|
||||
pnpm 10
|
||||
hugo-extended ${{ matrix.hugo-version }}
|
||||
cache: true
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Get pnpm store directory
|
||||
- name: Get pnpm store path
|
||||
id: pnpm-cache
|
||||
run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
run: 'echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT'
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('./pnpm-lock.yaml') }}
|
||||
key: pnpm-store-${{ hashFiles('./pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
pnpm-store-
|
||||
|
||||
- name: Setup hugo cache
|
||||
uses: actions/cache@v4
|
||||
- name: Setup Hugo cache
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ./exampleSite/resources
|
||||
key: ${{ runner.os }}-hugo-${{ matrix.hugo-version }}-${{ hashFiles('./exampleSite') }}
|
||||
path: exampleSite/resources/_gen
|
||||
key: hugo-${{ matrix.hugo-version }}-${{ hashFiles('./exampleSite/**/*.jpg') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-hugo-${{ matrix.hugo-version }}-
|
||||
hugo-${{ matrix.hugo-version }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
- name: Install project dependencies
|
||||
run: 'pnpm install'
|
||||
|
||||
- name: Pre-build cleanup
|
||||
if: >
|
||||
matrix.hugo-version == 'latest' &&
|
||||
(github.event_name == 'push' || github.event.pull_request.merged == true)
|
||||
run: 'rm -rf bundled'
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
pnpm run build
|
||||
run: 'pnpm run build'
|
||||
|
||||
- name: Push artifacts
|
||||
if: >
|
||||
matrix.hugo-version == 'latest' &&
|
||||
(github.event_name == 'push' || github.event.pull_request.merged == true) &&
|
||||
needs.filter.outputs.any_changed == 'true'
|
||||
uses: stefanzweifel/git-auto-commit-action@v5
|
||||
(github.event_name == 'push' || github.event.pull_request.merged == true)
|
||||
uses: stefanzweifel/git-auto-commit-action@v7
|
||||
with:
|
||||
file_pattern: 'bundled/**/*.js bundled/**/*.css'
|
||||
commit_message: 'ci: update bundled artifacts [skip ci]'
|
||||
|
||||
8
.github/workflows/codeql.yml
vendored
8
.github/workflows/codeql.yml
vendored
@@ -46,11 +46,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
@@ -77,6 +77,6 @@ jobs:
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: '/language:${{matrix.language}}'
|
||||
|
||||
57
.github/workflows/eslint.yml
vendored
57
.github/workflows/eslint.yml
vendored
@@ -1,57 +0,0 @@
|
||||
name: 'ESLint && Prettier'
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
name: Lint
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.PAT || github.token }}
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('./pnpm-lock.yaml') }}
|
||||
restore-keys: ${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Lint Check
|
||||
continue-on-error: true
|
||||
id: check
|
||||
run: pnpm run lint:check
|
||||
|
||||
- name: Format manually
|
||||
id: format
|
||||
if: ${{ steps.check.outcome == 'failure' }}
|
||||
run: pnpm run lint
|
||||
|
||||
- name: Commit
|
||||
if: ${{ steps.format.outcome == 'success' }}
|
||||
uses: stefanzweifel/git-auto-commit-action@v5
|
||||
with:
|
||||
commit_message: 'ci: format code [skip ci]'
|
||||
58
.github/workflows/lint.yml
vendored
Normal file
58
.github/workflows/lint.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
name: 'Lint'
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
name: Lint
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
# github.token as a fallback, since other user might trigger this
|
||||
# workflow in their pull request
|
||||
token: ${{ secrets.PAT || github.token }}
|
||||
|
||||
- name: Setup Mise
|
||||
uses: jdx/mise-action@v4
|
||||
with:
|
||||
install_args: node@latest pnpm@10
|
||||
tool_versions: |
|
||||
node latest
|
||||
pnpm 10
|
||||
cache: true
|
||||
|
||||
- name: Get pnpm store path
|
||||
id: pnpm-cache
|
||||
run: 'echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT'
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: pnpm-store-${{ hashFiles('./pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
pnpm-store-
|
||||
|
||||
- name: Install project dependencies
|
||||
run: 'pnpm install'
|
||||
|
||||
- name: Lint
|
||||
id: lint
|
||||
run: 'pnpm run lint:check || pnpm run lint'
|
||||
|
||||
- name: Commit
|
||||
if: ${{ steps.format.lint == 'success' }}
|
||||
uses: stefanzweifel/git-auto-commit-action@v7
|
||||
with:
|
||||
commit_message: 'ci: lint [skip ci]'
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -27,4 +27,4 @@ jsconfig.json
|
||||
*.css.map
|
||||
|
||||
# dummmy file
|
||||
bundled/css/critical.js
|
||||
bundled/js/critical.js
|
||||
|
||||
@@ -9,7 +9,7 @@ Here is a [live demo](https://bridget-demo.sped0n.com).
|
||||

|
||||
|
||||
> [!NOTE]
|
||||
> This repository is currently in **maintaince mode** for two reasons:
|
||||
> This repository is currently in **maintenance mode** for two reasons:
|
||||
>
|
||||
> 1. I want to keep this theme minimal.
|
||||
> 2. My bandwith after work is limited.
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
position: fixed;
|
||||
top: var(--nav-height);
|
||||
z-index: var(--z-nav-gallery);
|
||||
overflow: hidden;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -25,8 +26,9 @@
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: calc(var(--window-height) - 2 * var(--nav-height));
|
||||
max-width: 100%;
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
@@ -49,6 +51,21 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.navClose {
|
||||
cursor: pointer;
|
||||
z-index: calc(var(--z-nav-gallery) + 1);
|
||||
|
||||
min-width: 25%;
|
||||
height: calc(var(--nav-height) * 2.5);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
margin-right: calc(var(--space-standard) * -1);
|
||||
padding-right: var(--space-standard);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
91
assets/ts/configState.tsx
Normal file
91
assets/ts/configState.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import {
|
||||
createContext,
|
||||
createMemo,
|
||||
createSignal,
|
||||
useContext,
|
||||
type Accessor,
|
||||
type JSX
|
||||
} from 'solid-js'
|
||||
import invariant from 'tiny-invariant'
|
||||
|
||||
import { getThresholdSessionIndex } from './utils'
|
||||
|
||||
export interface ThresholdRelated {
|
||||
threshold: number
|
||||
trailLength: number
|
||||
}
|
||||
|
||||
export interface ConfigState {
|
||||
thresholdIndex: number
|
||||
threshold: number
|
||||
trailLength: number
|
||||
}
|
||||
|
||||
export type ConfigStateContextType = readonly [
|
||||
Accessor<ConfigState>,
|
||||
{
|
||||
readonly incThreshold: () => void
|
||||
readonly decThreshold: () => void
|
||||
}
|
||||
]
|
||||
|
||||
const thresholds: ThresholdRelated[] = [
|
||||
{ threshold: 20, trailLength: 20 },
|
||||
{ threshold: 40, trailLength: 10 },
|
||||
{ threshold: 80, trailLength: 5 },
|
||||
{ threshold: 140, trailLength: 5 },
|
||||
{ threshold: 200, trailLength: 5 }
|
||||
]
|
||||
|
||||
const ConfigStateContext = createContext<ConfigStateContextType>()
|
||||
|
||||
function getSafeThresholdIndex(): number {
|
||||
const index = getThresholdSessionIndex()
|
||||
if (index < 0 || index >= thresholds.length) return 2
|
||||
return index
|
||||
}
|
||||
|
||||
export function ConfigStateProvider(props: { children?: JSX.Element }): JSX.Element {
|
||||
const [thresholdIndex, setThresholdIndex] = createSignal(getSafeThresholdIndex())
|
||||
|
||||
const state = createMemo<ConfigState>(() => {
|
||||
const current = thresholds[thresholdIndex()]
|
||||
|
||||
return {
|
||||
thresholdIndex: thresholdIndex(),
|
||||
threshold: current.threshold,
|
||||
trailLength: current.trailLength
|
||||
}
|
||||
})
|
||||
|
||||
const updateThreshold = (stride: number): void => {
|
||||
const nextIndex = thresholdIndex() + stride
|
||||
if (nextIndex < 0 || nextIndex >= thresholds.length) return
|
||||
sessionStorage.setItem('thresholdsIndex', nextIndex.toString())
|
||||
setThresholdIndex(nextIndex)
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfigStateContext.Provider
|
||||
value={[
|
||||
state,
|
||||
{
|
||||
incThreshold: () => {
|
||||
updateThreshold(1)
|
||||
},
|
||||
decThreshold: () => {
|
||||
updateThreshold(-1)
|
||||
}
|
||||
}
|
||||
]}
|
||||
>
|
||||
{props.children}
|
||||
</ConfigStateContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useConfigState(): ConfigStateContextType {
|
||||
const context = useContext(ConfigStateContext)
|
||||
invariant(context, 'undefined config context')
|
||||
return context
|
||||
}
|
||||
@@ -4,7 +4,6 @@ export default function CustomCursor(props: {
|
||||
children?: JSX.Element
|
||||
active: Accessor<boolean>
|
||||
cursorText: Accessor<string>
|
||||
isOpen: Accessor<boolean>
|
||||
}): JSX.Element {
|
||||
// types
|
||||
interface XY {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Show, createMemo, createSignal, type JSX } from 'solid-js'
|
||||
import { Show, createMemo, type JSX } from 'solid-js'
|
||||
|
||||
import type { ImageJSON } from '../resources'
|
||||
import type { Vector } from '../utils'
|
||||
import { useImageState } from '../imageState'
|
||||
|
||||
import CustomCursor from './customCursor'
|
||||
import Nav from './nav'
|
||||
import Stage from './stage'
|
||||
import StageNav from './stageNav'
|
||||
import { useDesktopState } from './state'
|
||||
|
||||
/**
|
||||
* interfaces and types
|
||||
@@ -23,65 +23,36 @@ export interface DesktopImage extends HTMLImageElement {
|
||||
}
|
||||
}
|
||||
|
||||
export interface HistoryItem {
|
||||
i: number
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
/**
|
||||
* components
|
||||
*/
|
||||
|
||||
export default function Desktop(props: {
|
||||
children?: JSX.Element
|
||||
ijs: ImageJSON[]
|
||||
prevText: string
|
||||
closeText: string
|
||||
nextText: string
|
||||
loadingText: string
|
||||
}): JSX.Element {
|
||||
const [cordHist, setCordHist] = createSignal<HistoryItem[]>([])
|
||||
const [isLoading, setIsLoading] = createSignal(false)
|
||||
const [isOpen, setIsOpen] = createSignal(false)
|
||||
const [isAnimating, setIsAnimating] = createSignal(false)
|
||||
const [hoverText, setHoverText] = createSignal('')
|
||||
const [navVector, setNavVector] = createSignal<Vector>('none')
|
||||
const imageState = useImageState()
|
||||
const [desktop] = useDesktopState()
|
||||
|
||||
const active = createMemo(() => isOpen() && !isAnimating())
|
||||
const cursorText = createMemo(() => (isLoading() ? props.loadingText : hoverText()))
|
||||
const active = createMemo(() => desktop.isOpen() && !desktop.isAnimating())
|
||||
const cursorText = createMemo(() =>
|
||||
desktop.isLoading() ? props.loadingText : desktop.hoverText()
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Nav />
|
||||
<Show when={props.ijs.length > 0}>
|
||||
<Stage
|
||||
ijs={props.ijs}
|
||||
setIsLoading={setIsLoading}
|
||||
isOpen={isOpen}
|
||||
setIsOpen={setIsOpen}
|
||||
isAnimating={isAnimating}
|
||||
setIsAnimating={setIsAnimating}
|
||||
cordHist={cordHist}
|
||||
setCordHist={setCordHist}
|
||||
navVector={navVector}
|
||||
setNavVector={setNavVector}
|
||||
/>
|
||||
<Show when={isOpen()}>
|
||||
<CustomCursor cursorText={cursorText} active={active} isOpen={isOpen} />
|
||||
<Show when={imageState().length > 0}>
|
||||
<Stage />
|
||||
<Show when={desktop.isOpen()}>
|
||||
<CustomCursor cursorText={cursorText} active={active} />
|
||||
<StageNav
|
||||
prevText={props.prevText}
|
||||
closeText={props.closeText}
|
||||
nextText={props.nextText}
|
||||
loadingText={props.loadingText}
|
||||
active={active}
|
||||
isAnimating={isAnimating}
|
||||
setCordHist={setCordHist}
|
||||
isOpen={isOpen}
|
||||
setIsOpen={setIsOpen}
|
||||
setHoverText={setHoverText}
|
||||
navVector={navVector}
|
||||
setNavVector={setNavVector}
|
||||
/>
|
||||
</Show>
|
||||
</Show>
|
||||
|
||||
@@ -1,66 +1,68 @@
|
||||
import { createEffect } from 'solid-js'
|
||||
import { createEffect, onCleanup, onMount } from 'solid-js'
|
||||
|
||||
import { useState } from '../state'
|
||||
import { useConfigState } from '../configState'
|
||||
import { useImageState } from '../imageState'
|
||||
import { expand } from '../utils'
|
||||
|
||||
/**
|
||||
* constants
|
||||
*/
|
||||
|
||||
// threshold div
|
||||
const thresholdDiv = document.getElementsByClassName('threshold')[0] as HTMLDivElement
|
||||
// threshold nums span
|
||||
const thresholdDispNums = Array.from(
|
||||
thresholdDiv.getElementsByClassName('num')
|
||||
) as HTMLSpanElement[]
|
||||
// threshold buttons
|
||||
const decButton = thresholdDiv
|
||||
.getElementsByClassName('dec')
|
||||
.item(0) as HTMLButtonElement
|
||||
const incButton = thresholdDiv
|
||||
.getElementsByClassName('inc')
|
||||
.item(0) as HTMLButtonElement
|
||||
// index div
|
||||
const indexDiv = document.getElementsByClassName('index').item(0) as HTMLDivElement
|
||||
// index nums span
|
||||
const indexDispNums = Array.from(
|
||||
indexDiv.getElementsByClassName('num')
|
||||
) as HTMLSpanElement[]
|
||||
|
||||
/**
|
||||
* helper functions
|
||||
*/
|
||||
|
||||
function updateThresholdText(thresholdValue: string): void {
|
||||
thresholdDispNums.forEach((e: HTMLSpanElement, i: number) => {
|
||||
e.innerText = thresholdValue[i]
|
||||
})
|
||||
}
|
||||
|
||||
function updateIndexText(indexValue: string, indexLength: string): void {
|
||||
indexDispNums.forEach((e: HTMLSpanElement, i: number) => {
|
||||
if (i < 4) {
|
||||
e.innerText = indexValue[i]
|
||||
} else {
|
||||
e.innerText = indexLength[i - 4]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Nav component
|
||||
*/
|
||||
import { useDesktopState } from './state'
|
||||
|
||||
export default function Nav(): null {
|
||||
const [state, { incThreshold, decThreshold }] = useState()
|
||||
let thresholdNums: HTMLSpanElement[] = []
|
||||
let indexNums: HTMLSpanElement[] = []
|
||||
let decButton: HTMLButtonElement | undefined
|
||||
let incButton: HTMLButtonElement | undefined
|
||||
let controller: AbortController | undefined
|
||||
|
||||
createEffect(() => {
|
||||
updateIndexText(expand(state().index + 1), expand(state().length))
|
||||
updateThresholdText(expand(state().threshold))
|
||||
const imageState = useImageState()
|
||||
const [config, { incThreshold, decThreshold }] = useConfigState()
|
||||
const [desktop] = useDesktopState()
|
||||
|
||||
const updateThresholdText = (thresholdValue: string): void => {
|
||||
thresholdNums.forEach((element, i) => {
|
||||
element.innerText = thresholdValue[i]
|
||||
})
|
||||
}
|
||||
|
||||
const updateIndexText = (indexValue: string, indexLength: string): void => {
|
||||
indexNums.forEach((element, i) => {
|
||||
if (i < 4) {
|
||||
element.innerText = indexValue[i]
|
||||
} else {
|
||||
element.innerText = indexLength[i - 4]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const thresholdDiv = document.getElementsByClassName(
|
||||
'threshold'
|
||||
)[0] as HTMLDivElement
|
||||
const indexDiv = document.getElementsByClassName('index').item(0) as HTMLDivElement
|
||||
|
||||
thresholdNums = Array.from(
|
||||
thresholdDiv.getElementsByClassName('num')
|
||||
) as HTMLSpanElement[]
|
||||
indexNums = Array.from(indexDiv.getElementsByClassName('num')) as HTMLSpanElement[]
|
||||
decButton = thresholdDiv.getElementsByClassName('dec').item(0) as HTMLButtonElement
|
||||
incButton = thresholdDiv.getElementsByClassName('inc').item(0) as HTMLButtonElement
|
||||
|
||||
controller = new AbortController()
|
||||
const signal = controller.signal
|
||||
|
||||
decButton.addEventListener('click', decThreshold, { signal })
|
||||
incButton.addEventListener('click', incThreshold, { signal })
|
||||
})
|
||||
|
||||
decButton.onclick = decThreshold
|
||||
incButton.onclick = incThreshold
|
||||
createEffect(() => {
|
||||
if (thresholdNums.length === 0 || indexNums.length === 0) return
|
||||
|
||||
updateIndexText(expand(desktop.index() + 1), expand(imageState().length))
|
||||
updateThresholdText(expand(config().threshold))
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
controller?.abort()
|
||||
})
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,409 +1,167 @@
|
||||
import { type gsap } from 'gsap'
|
||||
import {
|
||||
For,
|
||||
createEffect,
|
||||
on,
|
||||
onMount,
|
||||
type Accessor,
|
||||
type JSX,
|
||||
type Setter
|
||||
} from 'solid-js'
|
||||
import { For, createEffect, on, onMount, type JSX } from 'solid-js'
|
||||
|
||||
import type { ImageJSON } from '../resources'
|
||||
import { useState, type State } from '../state'
|
||||
import { decrement, increment, loadGsap, type Vector } from '../utils'
|
||||
import { useConfigState } from '../configState'
|
||||
import { useImageState } from '../imageState'
|
||||
import { increment, loadGsap } from '../utils'
|
||||
|
||||
import type { DesktopImage, HistoryItem } from './layout'
|
||||
import type { DesktopImage } from './layout'
|
||||
import { expandStage, minimizeStage, syncStagePosition } from './stageAnimations'
|
||||
import { onMutation } from './stageUtils'
|
||||
import { useDesktopState } from './state'
|
||||
|
||||
/**
|
||||
* helper functions
|
||||
*/
|
||||
|
||||
function getTrailElsIndex(cordHistValue: HistoryItem[]): number[] {
|
||||
return cordHistValue.map((el) => el.i)
|
||||
}
|
||||
|
||||
function getTrailCurrentElsIndex(
|
||||
cordHistValue: HistoryItem[],
|
||||
stateValue: State
|
||||
): number[] {
|
||||
return getTrailElsIndex(cordHistValue).slice(-stateValue.trailLength)
|
||||
}
|
||||
|
||||
function getTrailInactiveElsIndex(
|
||||
cordHistValue: HistoryItem[],
|
||||
stateValue: State
|
||||
): number[] {
|
||||
return getTrailCurrentElsIndex(cordHistValue, stateValue).slice(0, -1)
|
||||
}
|
||||
|
||||
function getCurrentElIndex(cordHistValue: HistoryItem[]): number {
|
||||
return getTrailElsIndex(cordHistValue).slice(-1)[0]
|
||||
}
|
||||
|
||||
function getPrevElIndex(cordHistValue: HistoryItem[], stateValue: State): number {
|
||||
return decrement(cordHistValue.slice(-1)[0].i, stateValue.length)
|
||||
}
|
||||
|
||||
function getNextElIndex(cordHistValue: HistoryItem[], stateValue: State): number {
|
||||
return increment(cordHistValue.slice(-1)[0].i, stateValue.length)
|
||||
}
|
||||
|
||||
function getImagesFromIndexes(imgs: DesktopImage[], indexes: number[]): DesktopImage[] {
|
||||
return indexes.map((i) => imgs[i])
|
||||
}
|
||||
|
||||
function hires(imgs: DesktopImage[]): void {
|
||||
imgs.forEach((img) => {
|
||||
if (img.src === img.dataset.hiUrl) return
|
||||
img.src = img.dataset.hiUrl
|
||||
img.height = parseInt(img.dataset.hiImgH)
|
||||
img.width = parseInt(img.dataset.hiImgW)
|
||||
})
|
||||
}
|
||||
|
||||
function lores(imgs: DesktopImage[]): void {
|
||||
imgs.forEach((img) => {
|
||||
if (img.src === img.dataset.loUrl) return
|
||||
img.src = img.dataset.loUrl
|
||||
img.height = parseInt(img.dataset.loImgH)
|
||||
img.width = parseInt(img.dataset.loImgW)
|
||||
})
|
||||
}
|
||||
|
||||
function onMutation<T extends HTMLElement>(
|
||||
element: T,
|
||||
trigger: (arg0: MutationRecord) => boolean,
|
||||
observeOptions: MutationObserverInit = { attributes: true }
|
||||
): void {
|
||||
new MutationObserver((mutations, observer) => {
|
||||
for (const mutation of mutations) {
|
||||
if (trigger(mutation)) {
|
||||
observer.disconnect()
|
||||
break
|
||||
}
|
||||
}
|
||||
}).observe(element, observeOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Stage component
|
||||
*/
|
||||
|
||||
export default function Stage(props: {
|
||||
ijs: ImageJSON[]
|
||||
setIsLoading: Setter<boolean>
|
||||
isOpen: Accessor<boolean>
|
||||
setIsOpen: Setter<boolean>
|
||||
isAnimating: Accessor<boolean>
|
||||
setIsAnimating: Setter<boolean>
|
||||
cordHist: Accessor<HistoryItem[]>
|
||||
setCordHist: Setter<HistoryItem[]>
|
||||
navVector: Accessor<Vector>
|
||||
setNavVector: Setter<Vector>
|
||||
}): JSX.Element {
|
||||
// variables
|
||||
export default function Stage(): JSX.Element {
|
||||
let _gsap: typeof gsap
|
||||
let gsapPromise: Promise<void> | undefined
|
||||
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
const imgs: DesktopImage[] = Array<DesktopImage>(props.ijs.length)
|
||||
const imageState = useImageState()
|
||||
const [config] = useConfigState()
|
||||
const [
|
||||
desktop,
|
||||
{ setIndex, setCordHist, setIsOpen, setIsAnimating, setIsLoading, setNavVector }
|
||||
] = useDesktopState()
|
||||
|
||||
const imgs: DesktopImage[] = Array<DesktopImage>(imageState().length)
|
||||
let last = { x: 0, y: 0 }
|
||||
|
||||
let abortController: AbortController | undefined
|
||||
|
||||
// states
|
||||
let gsapLoaded = false
|
||||
|
||||
const [state, { incIndex }] = useState()
|
||||
const stateLength = state().length
|
||||
|
||||
let mounted = false
|
||||
|
||||
const ensureGsapReady: () => Promise<void> = async () => {
|
||||
if (gsapPromise !== undefined) return await gsapPromise
|
||||
|
||||
gsapPromise = loadGsap()
|
||||
.then((g) => {
|
||||
_gsap = g
|
||||
gsapLoaded = true
|
||||
})
|
||||
.catch((e) => {
|
||||
gsapPromise = undefined
|
||||
console.log(e)
|
||||
})
|
||||
|
||||
await gsapPromise
|
||||
}
|
||||
|
||||
const onMouse: (e: MouseEvent) => void = (e) => {
|
||||
if (props.isOpen() || props.isAnimating() || !gsapLoaded || !mounted) return
|
||||
if (desktop.isOpen() || desktop.isAnimating() || !gsapLoaded || !mounted) return
|
||||
|
||||
const length = imageState().length
|
||||
if (length <= 0) return
|
||||
|
||||
const cord = { x: e.clientX, y: e.clientY }
|
||||
const travelDist = Math.hypot(cord.x - last.x, cord.y - last.y)
|
||||
|
||||
if (travelDist > state().threshold) {
|
||||
last = cord
|
||||
incIndex()
|
||||
if (travelDist > config().threshold) {
|
||||
const nextIndex = increment(desktop.index(), length)
|
||||
|
||||
const _state = state()
|
||||
const newHist = { i: _state.index, ...cord }
|
||||
props.setCordHist((prev) => [...prev, newHist].slice(-stateLength))
|
||||
last = cord
|
||||
setIndex(nextIndex)
|
||||
setCordHist((prev) => [...prev, { i: nextIndex, ...cord }].slice(-length))
|
||||
}
|
||||
}
|
||||
|
||||
const onClick: () => void = () => {
|
||||
if (!props.isAnimating()) props.setIsOpen(true)
|
||||
const onClick: () => Promise<void> = async () => {
|
||||
if (!gsapLoaded) {
|
||||
await ensureGsapReady()
|
||||
}
|
||||
|
||||
if (desktop.isAnimating() || !gsapLoaded) return
|
||||
if (desktop.index() < 0 || desktop.cordHist().length === 0) return
|
||||
setIsOpen(true)
|
||||
}
|
||||
|
||||
const setPosition: () => void = () => {
|
||||
if (!mounted) return
|
||||
if (imgs.length === 0) return
|
||||
const _cordHist = props.cordHist()
|
||||
const trailElsIndex = getTrailElsIndex(_cordHist)
|
||||
if (trailElsIndex.length === 0) return
|
||||
|
||||
const elsTrail = getImagesFromIndexes(imgs, trailElsIndex)
|
||||
|
||||
const _isOpen = props.isOpen()
|
||||
const _state = state()
|
||||
|
||||
_gsap.set(elsTrail, {
|
||||
x: (i: number) => _cordHist[i].x - window.innerWidth / 2,
|
||||
y: (i: number) => _cordHist[i].y - window.innerHeight / 2,
|
||||
opacity: (i: number) =>
|
||||
Math.max(
|
||||
(i + 1 + _state.trailLength <= _cordHist.length ? 0 : 1) - (_isOpen ? 1 : 0),
|
||||
0
|
||||
),
|
||||
zIndex: (i: number) => i,
|
||||
scale: 0.6
|
||||
})
|
||||
|
||||
if (_isOpen) {
|
||||
const elc = getImagesFromIndexes(imgs, [getCurrentElIndex(_cordHist)])[0]
|
||||
const indexArrayToHires: number[] = []
|
||||
const indexArrayToCleanup: number[] = []
|
||||
switch (props.navVector()) {
|
||||
case 'prev':
|
||||
indexArrayToHires.push(getPrevElIndex(_cordHist, _state))
|
||||
indexArrayToCleanup.push(getNextElIndex(_cordHist, _state))
|
||||
break
|
||||
case 'next':
|
||||
indexArrayToHires.push(getNextElIndex(_cordHist, _state))
|
||||
indexArrayToCleanup.push(getPrevElIndex(_cordHist, _state))
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
hires(getImagesFromIndexes(imgs, indexArrayToHires)) // preload
|
||||
_gsap.set(getImagesFromIndexes(imgs, indexArrayToCleanup), { opacity: 0 })
|
||||
_gsap.set(elc, { x: 0, y: 0, scale: 1 }) // set current to center
|
||||
setLoaderForHiresImage(elc) // set loader, if loaded set current opacity to 1
|
||||
} else {
|
||||
lores(elsTrail)
|
||||
}
|
||||
}
|
||||
|
||||
const expandImage: () => Promise<
|
||||
gsap.core.Omit<gsap.core.Timeline, 'then'>
|
||||
> = async () => {
|
||||
// isAnimating is prechecked in isOpen effect
|
||||
if (!mounted || !gsapLoaded) throw new Error('not mounted or gsap not loaded')
|
||||
|
||||
props.setIsAnimating(true)
|
||||
|
||||
const _cordHist = props.cordHist()
|
||||
const _state = state()
|
||||
|
||||
const elcIndex = getCurrentElIndex(_cordHist)
|
||||
const elc = imgs[elcIndex]
|
||||
|
||||
// don't hide here because we want a better transition
|
||||
hires(
|
||||
getImagesFromIndexes(imgs, [
|
||||
elcIndex,
|
||||
getPrevElIndex(_cordHist, _state),
|
||||
getNextElIndex(_cordHist, _state)
|
||||
])
|
||||
)
|
||||
setLoaderForHiresImage(elc)
|
||||
|
||||
const tl = _gsap.timeline()
|
||||
const trailInactiveEls = getImagesFromIndexes(
|
||||
syncStagePosition({
|
||||
gsap: _gsap,
|
||||
imgs,
|
||||
getTrailInactiveElsIndex(_cordHist, _state)
|
||||
)
|
||||
// move down and hide trail inactive
|
||||
tl.to(trailInactiveEls, {
|
||||
y: '+=20',
|
||||
ease: 'power3.in',
|
||||
stagger: 0.075,
|
||||
duration: 0.3,
|
||||
delay: 0.1,
|
||||
opacity: 0
|
||||
})
|
||||
// current move to center
|
||||
tl.to(elc, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
ease: 'power3.inOut',
|
||||
duration: 0.7,
|
||||
delay: 0.3
|
||||
})
|
||||
// current expand
|
||||
tl.to(elc, {
|
||||
delay: 0.1,
|
||||
scale: 1,
|
||||
ease: 'power3.inOut'
|
||||
})
|
||||
// finished
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
return await tl.then(() => {
|
||||
props.setIsAnimating(false)
|
||||
cordHist: desktop.cordHist(),
|
||||
trailLength: config().trailLength,
|
||||
length: imageState().length,
|
||||
isOpen: desktop.isOpen(),
|
||||
navVector: desktop.navVector(),
|
||||
mounted,
|
||||
setIsLoading
|
||||
})
|
||||
}
|
||||
|
||||
const minimizeImage: () => Promise<
|
||||
gsap.core.Omit<gsap.core.Timeline, 'then'>
|
||||
> = async () => {
|
||||
const expandImage: () => Promise<void> = async () => {
|
||||
if (!mounted || !gsapLoaded) throw new Error('not mounted or gsap not loaded')
|
||||
|
||||
props.setIsAnimating(true)
|
||||
props.setNavVector('none') // cleanup
|
||||
|
||||
const _cordHist = props.cordHist()
|
||||
const _state = state()
|
||||
|
||||
const elcIndex = getCurrentElIndex(_cordHist)
|
||||
const elsTrailInactiveIndexes = getTrailInactiveElsIndex(_cordHist, _state)
|
||||
|
||||
lores(getImagesFromIndexes(imgs, [...elsTrailInactiveIndexes, elcIndex]))
|
||||
|
||||
const tl = _gsap.timeline()
|
||||
const elc = getImagesFromIndexes(imgs, [elcIndex])[0]
|
||||
const elsTrailInactive = getImagesFromIndexes(imgs, elsTrailInactiveIndexes)
|
||||
// shrink current
|
||||
tl.to(elc, {
|
||||
scale: 0.6,
|
||||
duration: 0.6,
|
||||
ease: 'power3.inOut'
|
||||
})
|
||||
// move current to original position
|
||||
tl.to(elc, {
|
||||
delay: 0.3,
|
||||
duration: 0.7,
|
||||
ease: 'power3.inOut',
|
||||
x: _cordHist.slice(-1)[0].x - window.innerWidth / 2,
|
||||
y: _cordHist.slice(-1)[0].y - window.innerHeight / 2
|
||||
})
|
||||
// show trail inactive
|
||||
tl.to(elsTrailInactive, {
|
||||
y: '-=20',
|
||||
ease: 'power3.out',
|
||||
stagger: -0.1,
|
||||
duration: 0.3,
|
||||
opacity: 1
|
||||
})
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
return await tl.then(() => {
|
||||
props.setIsAnimating(false)
|
||||
await expandStage({
|
||||
gsap: _gsap,
|
||||
imgs,
|
||||
cordHist: desktop.cordHist(),
|
||||
trailLength: config().trailLength,
|
||||
length: imageState().length,
|
||||
mounted,
|
||||
setIsLoading,
|
||||
setIsAnimating
|
||||
})
|
||||
}
|
||||
|
||||
function setLoaderForHiresImage(img: DesktopImage): void {
|
||||
if (!mounted || !gsapLoaded) return
|
||||
if (!img.complete) {
|
||||
props.setIsLoading(true)
|
||||
// abort controller for cleanup
|
||||
const controller = new AbortController()
|
||||
const abortSignal = controller.signal
|
||||
// event listeners
|
||||
img.addEventListener(
|
||||
'load',
|
||||
() => {
|
||||
_gsap
|
||||
.to(img, { opacity: 1, ease: 'power3.out', duration: 0.5 })
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
.then(() => {
|
||||
props.setIsLoading(false)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
.finally(() => {
|
||||
controller.abort()
|
||||
})
|
||||
},
|
||||
{ once: true, passive: true, signal: abortSignal }
|
||||
)
|
||||
img.addEventListener(
|
||||
'error',
|
||||
() => {
|
||||
_gsap
|
||||
.set(img, { opacity: 1 })
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
.then(() => {
|
||||
props.setIsLoading(false)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
.finally(() => {
|
||||
controller.abort()
|
||||
})
|
||||
},
|
||||
{ once: true, passive: true, signal: abortSignal }
|
||||
)
|
||||
} else {
|
||||
_gsap
|
||||
.set(img, { opacity: 1 })
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
.then(() => {
|
||||
props.setIsLoading(false)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
}
|
||||
const minimizeImage: () => Promise<void> = async () => {
|
||||
if (!mounted || !gsapLoaded) throw new Error('not mounted or gsap not loaded')
|
||||
|
||||
setNavVector('none')
|
||||
|
||||
await minimizeStage({
|
||||
gsap: _gsap,
|
||||
imgs,
|
||||
cordHist: desktop.cordHist(),
|
||||
trailLength: config().trailLength,
|
||||
mounted,
|
||||
setIsAnimating
|
||||
})
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// preload logic
|
||||
imgs.forEach((img, i) => {
|
||||
// preload first 5 images on page load
|
||||
if (i < 5) {
|
||||
img.src = img.dataset.loUrl
|
||||
}
|
||||
// lores preloader for rest of the images
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
|
||||
onMutation(img, (mutation) => {
|
||||
// if open or animating, hold
|
||||
if (props.isOpen() || props.isAnimating()) return false
|
||||
// if mutation is not about style attribute, hold
|
||||
if (desktop.isOpen() || desktop.isAnimating()) return false
|
||||
if (mutation.attributeName !== 'style') return false
|
||||
|
||||
const opacity = parseFloat(img.style.opacity)
|
||||
// if opacity is not 1, hold
|
||||
if (opacity !== 1) return false
|
||||
// preload the i + 5th image, if it exists
|
||||
|
||||
if (i + 5 < imgs.length) {
|
||||
imgs[i + 5].src = imgs[i + 5].dataset.loUrl
|
||||
}
|
||||
// triggered
|
||||
|
||||
return true
|
||||
})
|
||||
})
|
||||
// load gsap on mousemove
|
||||
window.addEventListener(
|
||||
'mousemove',
|
||||
() => {
|
||||
loadGsap()
|
||||
.then((g) => {
|
||||
_gsap = g
|
||||
gsapLoaded = true
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
},
|
||||
{ passive: true, once: true }
|
||||
)
|
||||
// event listeners
|
||||
|
||||
window.addEventListener('pointermove', () => void ensureGsapReady(), {
|
||||
passive: true,
|
||||
once: true
|
||||
})
|
||||
window.addEventListener('pointerdown', () => void ensureGsapReady(), {
|
||||
passive: true,
|
||||
once: true
|
||||
})
|
||||
window.addEventListener('click', () => void ensureGsapReady(), {
|
||||
passive: true,
|
||||
once: true
|
||||
})
|
||||
|
||||
abortController = new AbortController()
|
||||
const abortSignal = abortController.signal
|
||||
window.addEventListener('mousemove', onMouse, {
|
||||
passive: true,
|
||||
signal: abortSignal
|
||||
})
|
||||
// mounted
|
||||
|
||||
mounted = true
|
||||
})
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => props.cordHist(),
|
||||
() => desktop.cordHist(),
|
||||
() => {
|
||||
setPosition()
|
||||
},
|
||||
@@ -413,36 +171,38 @@ export default function Stage(props: {
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => props.isOpen(),
|
||||
async () => {
|
||||
if (props.isAnimating()) return
|
||||
if (props.isOpen()) {
|
||||
// expand image
|
||||
desktop.isOpen,
|
||||
async (isOpen) => {
|
||||
if (desktop.isAnimating()) return
|
||||
|
||||
if (isOpen) {
|
||||
if (desktop.index() < 0 || desktop.cordHist().length === 0) {
|
||||
setIsOpen(false)
|
||||
return
|
||||
}
|
||||
|
||||
await expandImage()
|
||||
.catch(() => {
|
||||
void 0
|
||||
setIsOpen(false)
|
||||
setIsAnimating(false)
|
||||
setIsLoading(false)
|
||||
})
|
||||
.then(() => {
|
||||
// abort controller for cleanup
|
||||
abortController?.abort()
|
||||
})
|
||||
} else {
|
||||
// minimize image
|
||||
await minimizeImage()
|
||||
.catch(() => {
|
||||
void 0
|
||||
})
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
.then(() => {
|
||||
// event listeners and its abort controller
|
||||
abortController = new AbortController()
|
||||
const abortSignal = abortController.signal
|
||||
window.addEventListener('mousemove', onMouse, {
|
||||
passive: true,
|
||||
signal: abortSignal
|
||||
})
|
||||
// cleanup isLoading
|
||||
props.setIsLoading(false)
|
||||
setIsLoading(false)
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -453,7 +213,7 @@ export default function Stage(props: {
|
||||
return (
|
||||
<>
|
||||
<div class="stage" onClick={onClick} onKeyDown={onClick}>
|
||||
<For each={props.ijs}>
|
||||
<For each={imageState().images}>
|
||||
{(ij, i) => (
|
||||
<img
|
||||
ref={imgs[i()]}
|
||||
|
||||
263
assets/ts/desktop/stageAnimations.ts
Normal file
263
assets/ts/desktop/stageAnimations.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
import { type gsap } from 'gsap'
|
||||
|
||||
import type { Vector } from '../utils'
|
||||
|
||||
import type { DesktopImage } from './layout'
|
||||
import {
|
||||
getCurrentElIndex,
|
||||
getImagesFromIndexes,
|
||||
getNextElIndex,
|
||||
getPrevElIndex,
|
||||
getTrailElsIndex,
|
||||
getTrailInactiveElsIndex,
|
||||
hires,
|
||||
lores
|
||||
} from './stageUtils'
|
||||
import type { HistoryItem } from './state'
|
||||
|
||||
type SetLoading = (value: boolean) => void
|
||||
|
||||
export function setLoaderForHiresImage(args: {
|
||||
gsap: typeof gsap
|
||||
img: DesktopImage
|
||||
mounted: boolean
|
||||
setIsLoading: SetLoading
|
||||
}): void {
|
||||
const { gsap, img, mounted, setIsLoading } = args
|
||||
if (!mounted) return
|
||||
|
||||
if (img.complete) {
|
||||
gsap
|
||||
.set(img, { opacity: 1 })
|
||||
.then(() => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
|
||||
const controller = new AbortController()
|
||||
const abortSignal = controller.signal
|
||||
|
||||
img.addEventListener(
|
||||
'load',
|
||||
() => {
|
||||
gsap
|
||||
.to(img, { opacity: 1, ease: 'power3.out', duration: 0.5 })
|
||||
.then(() => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
.finally(() => {
|
||||
controller.abort()
|
||||
})
|
||||
},
|
||||
{ once: true, passive: true, signal: abortSignal }
|
||||
)
|
||||
img.addEventListener(
|
||||
'error',
|
||||
() => {
|
||||
gsap
|
||||
.set(img, { opacity: 1 })
|
||||
.then(() => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
.finally(() => {
|
||||
controller.abort()
|
||||
})
|
||||
},
|
||||
{ once: true, passive: true, signal: abortSignal }
|
||||
)
|
||||
}
|
||||
|
||||
export function syncStagePosition(args: {
|
||||
gsap: typeof gsap
|
||||
imgs: DesktopImage[]
|
||||
cordHist: HistoryItem[]
|
||||
trailLength: number
|
||||
length: number
|
||||
isOpen: boolean
|
||||
navVector: Vector
|
||||
mounted: boolean
|
||||
setIsLoading: SetLoading
|
||||
}): void {
|
||||
const {
|
||||
gsap,
|
||||
imgs,
|
||||
cordHist,
|
||||
trailLength,
|
||||
length,
|
||||
isOpen,
|
||||
navVector,
|
||||
mounted,
|
||||
setIsLoading
|
||||
} = args
|
||||
|
||||
if (!mounted || imgs.length === 0) return
|
||||
|
||||
const trailElsIndex = getTrailElsIndex(cordHist)
|
||||
if (trailElsIndex.length === 0) return
|
||||
|
||||
const elsTrail = getImagesFromIndexes(imgs, trailElsIndex)
|
||||
|
||||
gsap.set(elsTrail, {
|
||||
x: (i: number) => cordHist[i].x - window.innerWidth / 2,
|
||||
y: (i: number) => cordHist[i].y - window.innerHeight / 2,
|
||||
opacity: (i: number) =>
|
||||
Math.max((i + 1 + trailLength <= cordHist.length ? 0 : 1) - (isOpen ? 1 : 0), 0),
|
||||
zIndex: (i: number) => i,
|
||||
scale: 0.6
|
||||
})
|
||||
|
||||
if (!isOpen) {
|
||||
lores(elsTrail)
|
||||
return
|
||||
}
|
||||
|
||||
const current = getImagesFromIndexes(imgs, [getCurrentElIndex(cordHist)])[0]
|
||||
const indexArrayToHires: number[] = []
|
||||
const indexArrayToCleanup: number[] = []
|
||||
|
||||
switch (navVector) {
|
||||
case 'prev':
|
||||
indexArrayToHires.push(getPrevElIndex(cordHist, length))
|
||||
indexArrayToCleanup.push(getNextElIndex(cordHist, length))
|
||||
break
|
||||
case 'next':
|
||||
indexArrayToHires.push(getNextElIndex(cordHist, length))
|
||||
indexArrayToCleanup.push(getPrevElIndex(cordHist, length))
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
hires(getImagesFromIndexes(imgs, indexArrayToHires))
|
||||
gsap.set(getImagesFromIndexes(imgs, indexArrayToCleanup), { opacity: 0 })
|
||||
gsap.set(current, { x: 0, y: 0, scale: 1 })
|
||||
setLoaderForHiresImage({ gsap, img: current, mounted, setIsLoading })
|
||||
}
|
||||
|
||||
export async function expandStage(args: {
|
||||
gsap: typeof gsap
|
||||
imgs: DesktopImage[]
|
||||
cordHist: HistoryItem[]
|
||||
trailLength: number
|
||||
length: number
|
||||
mounted: boolean
|
||||
setIsLoading: SetLoading
|
||||
setIsAnimating: (value: boolean) => void
|
||||
}): Promise<void> {
|
||||
const {
|
||||
gsap,
|
||||
imgs,
|
||||
cordHist,
|
||||
trailLength,
|
||||
length,
|
||||
mounted,
|
||||
setIsLoading,
|
||||
setIsAnimating
|
||||
} = args
|
||||
|
||||
if (!mounted) throw new Error('not mounted')
|
||||
|
||||
setIsAnimating(true)
|
||||
|
||||
const currentIndex = getCurrentElIndex(cordHist)
|
||||
const current = imgs[currentIndex]
|
||||
|
||||
hires(
|
||||
getImagesFromIndexes(imgs, [
|
||||
currentIndex,
|
||||
getPrevElIndex(cordHist, length),
|
||||
getNextElIndex(cordHist, length)
|
||||
])
|
||||
)
|
||||
setLoaderForHiresImage({ gsap, img: current, mounted, setIsLoading })
|
||||
|
||||
const tl = gsap.timeline()
|
||||
const trailInactiveEls = getImagesFromIndexes(
|
||||
imgs,
|
||||
getTrailInactiveElsIndex(cordHist, trailLength)
|
||||
)
|
||||
|
||||
tl.to(trailInactiveEls, {
|
||||
y: '+=20',
|
||||
ease: 'power3.in',
|
||||
stagger: 0.075,
|
||||
duration: 0.3,
|
||||
delay: 0.1,
|
||||
opacity: 0
|
||||
})
|
||||
tl.to(current, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
ease: 'power3.inOut',
|
||||
duration: 0.7,
|
||||
delay: 0.3
|
||||
})
|
||||
tl.to(current, {
|
||||
delay: 0.1,
|
||||
scale: 1,
|
||||
ease: 'power3.inOut'
|
||||
})
|
||||
|
||||
await tl.then(() => {
|
||||
setIsAnimating(false)
|
||||
})
|
||||
}
|
||||
|
||||
export async function minimizeStage(args: {
|
||||
gsap: typeof gsap
|
||||
imgs: DesktopImage[]
|
||||
cordHist: HistoryItem[]
|
||||
trailLength: number
|
||||
mounted: boolean
|
||||
setIsAnimating: (value: boolean) => void
|
||||
}): Promise<void> {
|
||||
const { gsap, imgs, cordHist, trailLength, mounted, setIsAnimating } = args
|
||||
if (!mounted) throw new Error('not mounted')
|
||||
|
||||
setIsAnimating(true)
|
||||
|
||||
const currentIndex = getCurrentElIndex(cordHist)
|
||||
const trailInactiveIndexes = getTrailInactiveElsIndex(cordHist, trailLength)
|
||||
|
||||
lores(getImagesFromIndexes(imgs, [...trailInactiveIndexes, currentIndex]))
|
||||
|
||||
const tl = gsap.timeline()
|
||||
const current = getImagesFromIndexes(imgs, [currentIndex])[0]
|
||||
const trailInactiveEls = getImagesFromIndexes(imgs, trailInactiveIndexes)
|
||||
|
||||
tl.to(current, {
|
||||
scale: 0.6,
|
||||
duration: 0.6,
|
||||
ease: 'power3.inOut'
|
||||
})
|
||||
tl.to(current, {
|
||||
delay: 0.3,
|
||||
duration: 0.7,
|
||||
ease: 'power3.inOut',
|
||||
x: cordHist.slice(-1)[0].x - window.innerWidth / 2,
|
||||
y: cordHist.slice(-1)[0].y - window.innerHeight / 2
|
||||
})
|
||||
tl.to(trailInactiveEls, {
|
||||
y: '-=20',
|
||||
ease: 'power3.out',
|
||||
stagger: -0.1,
|
||||
duration: 0.3,
|
||||
opacity: 1
|
||||
})
|
||||
|
||||
await tl.then(() => {
|
||||
setIsAnimating(false)
|
||||
})
|
||||
}
|
||||
@@ -1,24 +1,15 @@
|
||||
import { For, createEffect, type Accessor, type JSX, type Setter } from 'solid-js'
|
||||
import { For, createEffect, createMemo, on, onCleanup, type JSX } from 'solid-js'
|
||||
|
||||
import { useState } from '../state'
|
||||
import { decrement, increment, type Vector } from '../utils'
|
||||
import { useImageState } from '../imageState'
|
||||
import { decrement, increment } from '../utils'
|
||||
|
||||
import type { HistoryItem } from './layout'
|
||||
import { useDesktopState } from './state'
|
||||
|
||||
export default function StageNav(props: {
|
||||
children?: JSX.Element
|
||||
prevText: string
|
||||
closeText: string
|
||||
nextText: string
|
||||
loadingText: string
|
||||
active: Accessor<boolean>
|
||||
isAnimating: Accessor<boolean>
|
||||
setCordHist: Setter<HistoryItem[]>
|
||||
isOpen: Accessor<boolean>
|
||||
setIsOpen: Setter<boolean>
|
||||
setHoverText: Setter<string>
|
||||
navVector: Accessor<Vector>
|
||||
setNavVector: Setter<Vector>
|
||||
}): JSX.Element {
|
||||
// types
|
||||
type NavItem = (typeof navItems)[number]
|
||||
@@ -29,64 +20,74 @@ export default function StageNav(props: {
|
||||
const navItems = [props.prevText, props.closeText, props.nextText] as const
|
||||
|
||||
// states
|
||||
const [state, { incIndex, decIndex }] = useState()
|
||||
const imageState = useImageState()
|
||||
const [
|
||||
desktop,
|
||||
{ incIndex, decIndex, setCordHist, setHoverText, setIsOpen, setNavVector }
|
||||
] = useDesktopState()
|
||||
|
||||
const stateLength = state().length
|
||||
const active = createMemo(() => desktop.isOpen() && !desktop.isAnimating())
|
||||
|
||||
const prevImage: () => void = () => {
|
||||
props.setNavVector('prev')
|
||||
props.setCordHist((c) =>
|
||||
setNavVector('prev')
|
||||
setCordHist((c) =>
|
||||
c.map((item) => {
|
||||
return { ...item, i: decrement(item.i, stateLength) }
|
||||
return { ...item, i: decrement(item.i, imageState().length) }
|
||||
})
|
||||
)
|
||||
decIndex()
|
||||
}
|
||||
|
||||
const closeImage: () => void = () => {
|
||||
props.setIsOpen(false)
|
||||
setIsOpen(false)
|
||||
}
|
||||
|
||||
const nextImage: () => void = () => {
|
||||
props.setNavVector('next')
|
||||
props.setCordHist((c) =>
|
||||
setNavVector('next')
|
||||
setCordHist((c) =>
|
||||
c.map((item) => {
|
||||
return { ...item, i: increment(item.i, stateLength) }
|
||||
return { ...item, i: increment(item.i, imageState().length) }
|
||||
})
|
||||
)
|
||||
incIndex()
|
||||
}
|
||||
|
||||
const handleClick: (item: NavItem) => void = (item) => {
|
||||
if (!props.isOpen() || props.isAnimating()) return
|
||||
if (!desktop.isOpen() || desktop.isAnimating()) return
|
||||
if (item === navItems[0]) prevImage()
|
||||
else if (item === navItems[1]) closeImage()
|
||||
else nextImage()
|
||||
}
|
||||
|
||||
const handleKey: (e: KeyboardEvent) => void = (e) => {
|
||||
if (!props.isOpen() || props.isAnimating()) return
|
||||
if (!desktop.isOpen() || desktop.isAnimating()) return
|
||||
if (e.key === 'ArrowLeft') prevImage()
|
||||
else if (e.key === 'Escape') closeImage()
|
||||
else if (e.key === 'ArrowRight') nextImage()
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (props.isOpen()) {
|
||||
controller = new AbortController()
|
||||
const abortSignal = controller.signal
|
||||
window.addEventListener('keydown', handleKey, {
|
||||
passive: true,
|
||||
signal: abortSignal
|
||||
})
|
||||
} else {
|
||||
createEffect(
|
||||
on(desktop.isOpen, (isOpen) => {
|
||||
controller?.abort()
|
||||
}
|
||||
|
||||
if (isOpen) {
|
||||
controller = new AbortController()
|
||||
const abortSignal = controller.signal
|
||||
window.addEventListener('keydown', handleKey, {
|
||||
passive: true,
|
||||
signal: abortSignal
|
||||
})
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
onCleanup(() => {
|
||||
controller?.abort()
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="navOverlay" classList={{ active: props.active() }}>
|
||||
<div class="navOverlay" classList={{ active: active() }}>
|
||||
<For each={navItems}>
|
||||
{(item) => (
|
||||
<div
|
||||
@@ -94,8 +95,8 @@ export default function StageNav(props: {
|
||||
onClick={() => {
|
||||
handleClick(item)
|
||||
}}
|
||||
onFocus={() => props.setHoverText(item)}
|
||||
onMouseOver={() => props.setHoverText(item)}
|
||||
onFocus={() => setHoverText(item)}
|
||||
onMouseOver={() => setHoverText(item)}
|
||||
tabIndex="-1"
|
||||
/>
|
||||
)}
|
||||
|
||||
67
assets/ts/desktop/stageUtils.ts
Normal file
67
assets/ts/desktop/stageUtils.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { decrement, increment } from '../utils'
|
||||
|
||||
import type { DesktopImage } from './layout'
|
||||
import type { HistoryItem } from './state'
|
||||
|
||||
export function getTrailElsIndex(cordHistValue: HistoryItem[]): number[] {
|
||||
return cordHistValue.map((el) => el.i)
|
||||
}
|
||||
|
||||
export function getTrailInactiveElsIndex(
|
||||
cordHistValue: HistoryItem[],
|
||||
trailLength: number
|
||||
): number[] {
|
||||
return getTrailElsIndex(cordHistValue).slice(-trailLength).slice(0, -1)
|
||||
}
|
||||
|
||||
export function getCurrentElIndex(cordHistValue: HistoryItem[]): number {
|
||||
return getTrailElsIndex(cordHistValue).slice(-1)[0]
|
||||
}
|
||||
|
||||
export function getPrevElIndex(cordHistValue: HistoryItem[], length: number): number {
|
||||
return decrement(cordHistValue.slice(-1)[0].i, length)
|
||||
}
|
||||
|
||||
export function getNextElIndex(cordHistValue: HistoryItem[], length: number): number {
|
||||
return increment(cordHistValue.slice(-1)[0].i, length)
|
||||
}
|
||||
|
||||
export function getImagesFromIndexes(
|
||||
imgs: DesktopImage[],
|
||||
indexes: number[]
|
||||
): DesktopImage[] {
|
||||
return indexes.map((i) => imgs[i])
|
||||
}
|
||||
|
||||
export function hires(imgs: DesktopImage[]): void {
|
||||
imgs.forEach((img) => {
|
||||
if (img.src === img.dataset.hiUrl) return
|
||||
img.src = img.dataset.hiUrl
|
||||
img.height = parseInt(img.dataset.hiImgH)
|
||||
img.width = parseInt(img.dataset.hiImgW)
|
||||
})
|
||||
}
|
||||
|
||||
export function lores(imgs: DesktopImage[]): void {
|
||||
imgs.forEach((img) => {
|
||||
if (img.src === img.dataset.loUrl) return
|
||||
img.src = img.dataset.loUrl
|
||||
img.height = parseInt(img.dataset.loImgH)
|
||||
img.width = parseInt(img.dataset.loImgW)
|
||||
})
|
||||
}
|
||||
|
||||
export function onMutation<T extends HTMLElement>(
|
||||
element: T,
|
||||
trigger: (arg0: MutationRecord) => boolean,
|
||||
observeOptions: MutationObserverInit = { attributes: true }
|
||||
): void {
|
||||
new MutationObserver((mutations, observer) => {
|
||||
for (const mutation of mutations) {
|
||||
if (trigger(mutation)) {
|
||||
observer.disconnect()
|
||||
break
|
||||
}
|
||||
}
|
||||
}).observe(element, observeOptions)
|
||||
}
|
||||
96
assets/ts/desktop/state.ts
Normal file
96
assets/ts/desktop/state.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import {
|
||||
createComponent,
|
||||
createContext,
|
||||
createSignal,
|
||||
useContext,
|
||||
type Accessor,
|
||||
type JSX,
|
||||
type Setter
|
||||
} from 'solid-js'
|
||||
import invariant from 'tiny-invariant'
|
||||
|
||||
import { useImageState } from '../imageState'
|
||||
import { decrement, increment, type Vector } from '../utils'
|
||||
|
||||
export interface HistoryItem {
|
||||
i: number
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export interface DesktopState {
|
||||
index: Accessor<number>
|
||||
cordHist: Accessor<HistoryItem[]>
|
||||
hoverText: Accessor<string>
|
||||
isOpen: Accessor<boolean>
|
||||
isAnimating: Accessor<boolean>
|
||||
isLoading: Accessor<boolean>
|
||||
navVector: Accessor<Vector>
|
||||
}
|
||||
|
||||
export type DesktopStateContextType = readonly [
|
||||
DesktopState,
|
||||
{
|
||||
readonly setIndex: Setter<number>
|
||||
readonly incIndex: () => void
|
||||
readonly decIndex: () => void
|
||||
readonly setCordHist: Setter<HistoryItem[]>
|
||||
readonly setHoverText: Setter<string>
|
||||
readonly setIsOpen: Setter<boolean>
|
||||
readonly setIsAnimating: Setter<boolean>
|
||||
readonly setIsLoading: Setter<boolean>
|
||||
readonly setNavVector: Setter<Vector>
|
||||
}
|
||||
]
|
||||
|
||||
const DesktopStateContext = createContext<DesktopStateContextType>()
|
||||
|
||||
export function DesktopStateProvider(props: { children?: JSX.Element }): JSX.Element {
|
||||
const imageState = useImageState()
|
||||
|
||||
const [index, setIndex] = createSignal(-1)
|
||||
const [cordHist, setCordHist] = createSignal<HistoryItem[]>([])
|
||||
const [hoverText, setHoverText] = createSignal('')
|
||||
const [isOpen, setIsOpen] = createSignal(false)
|
||||
const [isAnimating, setIsAnimating] = createSignal(false)
|
||||
const [isLoading, setIsLoading] = createSignal(false)
|
||||
const [navVector, setNavVector] = createSignal<Vector>('none')
|
||||
|
||||
const updateIndex = (stride: 1 | -1): void => {
|
||||
const length = imageState().length
|
||||
if (length <= 0) return
|
||||
setIndex((current) =>
|
||||
stride === 1 ? increment(current, length) : decrement(current, length)
|
||||
)
|
||||
}
|
||||
|
||||
return createComponent(DesktopStateContext.Provider, {
|
||||
value: [
|
||||
{ index, cordHist, hoverText, isOpen, isAnimating, isLoading, navVector },
|
||||
{
|
||||
setIndex,
|
||||
incIndex: () => {
|
||||
updateIndex(1)
|
||||
},
|
||||
decIndex: () => {
|
||||
updateIndex(-1)
|
||||
},
|
||||
setCordHist,
|
||||
setHoverText,
|
||||
setIsOpen,
|
||||
setIsAnimating,
|
||||
setIsLoading,
|
||||
setNavVector
|
||||
}
|
||||
],
|
||||
get children() {
|
||||
return props.children
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function useDesktopState(): DesktopStateContextType {
|
||||
const context = useContext(DesktopStateContext)
|
||||
invariant(context, 'undefined desktop context')
|
||||
return context
|
||||
}
|
||||
41
assets/ts/imageState.tsx
Normal file
41
assets/ts/imageState.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
createContext,
|
||||
createMemo,
|
||||
useContext,
|
||||
type Accessor,
|
||||
type JSX
|
||||
} from 'solid-js'
|
||||
import invariant from 'tiny-invariant'
|
||||
|
||||
import type { ImageJSON } from './resources'
|
||||
|
||||
export interface ImageState {
|
||||
images: ImageJSON[]
|
||||
length: number
|
||||
}
|
||||
|
||||
type ImageStateContextType = Accessor<ImageState>
|
||||
|
||||
const ImageStateContext = createContext<ImageStateContextType>()
|
||||
|
||||
export function ImageStateProvider(props: {
|
||||
children?: JSX.Element
|
||||
images: ImageJSON[]
|
||||
}): JSX.Element {
|
||||
const state = createMemo<ImageState>(() => ({
|
||||
images: props.images,
|
||||
length: props.images.length
|
||||
}))
|
||||
|
||||
return (
|
||||
<ImageStateContext.Provider value={state}>
|
||||
{props.children}
|
||||
</ImageStateContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useImageState(): ImageStateContextType {
|
||||
const context = useContext(ImageStateContext)
|
||||
invariant(context, 'undefined image context')
|
||||
return context
|
||||
}
|
||||
@@ -1,17 +1,11 @@
|
||||
import {
|
||||
Match,
|
||||
Show,
|
||||
Switch,
|
||||
createEffect,
|
||||
createResource,
|
||||
createSignal,
|
||||
lazy,
|
||||
type JSX
|
||||
} from 'solid-js'
|
||||
import { Match, Show, Switch, createResource, lazy, type JSX } from 'solid-js'
|
||||
import { render } from 'solid-js/web'
|
||||
|
||||
import { ConfigStateProvider } from './configState'
|
||||
import { DesktopStateProvider } from './desktop/state'
|
||||
import { ImageStateProvider } from './imageState'
|
||||
import { MobileStateProvider } from './mobile/state'
|
||||
import { getImageJSON } from './resources'
|
||||
import { StateProvider } from './state'
|
||||
|
||||
import '../scss/style.scss'
|
||||
|
||||
@@ -35,48 +29,60 @@ const container = document.getElementsByClassName('container')[0] as Container
|
||||
const Desktop = lazy(async () => await import('./desktop/layout'))
|
||||
const Mobile = lazy(async () => await import('./mobile/layout'))
|
||||
|
||||
function AppContent(props: {
|
||||
isMobile: boolean
|
||||
prevText: string
|
||||
closeText: string
|
||||
nextText: string
|
||||
loadingText: string
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<Switch fallback={<div>Error</div>}>
|
||||
<Match when={props.isMobile}>
|
||||
<MobileStateProvider>
|
||||
<Mobile closeText={props.closeText} loadingText={props.loadingText} />
|
||||
</MobileStateProvider>
|
||||
</Match>
|
||||
<Match when={!props.isMobile}>
|
||||
<DesktopStateProvider>
|
||||
<Desktop
|
||||
prevText={props.prevText}
|
||||
closeText={props.closeText}
|
||||
nextText={props.nextText}
|
||||
loadingText={props.loadingText}
|
||||
/>
|
||||
</DesktopStateProvider>
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
function Main(): JSX.Element {
|
||||
// variables
|
||||
const [ijs] = createResource(getImageJSON)
|
||||
const isMobile =
|
||||
window.matchMedia('(hover: none)').matches &&
|
||||
!window.navigator.userAgent.includes('Win')
|
||||
|
||||
// states
|
||||
const [scrollable, setScollable] = createSignal(true)
|
||||
|
||||
createEffect(() => {
|
||||
if (scrollable()) {
|
||||
container.classList.remove('disableScroll')
|
||||
} else {
|
||||
container.classList.add('disableScroll')
|
||||
}
|
||||
})
|
||||
const ua = window.navigator.userAgent.toLowerCase()
|
||||
const hasTouchInput = 'ontouchstart' in window || window.navigator.maxTouchPoints > 0
|
||||
const hasTouchLayout =
|
||||
window.matchMedia('(pointer: coarse)').matches ||
|
||||
window.matchMedia('(hover: none)').matches
|
||||
const isMobileUA = /android|iphone|ipad|ipod|mobile/.test(ua)
|
||||
const isWindowsDesktop = /windows nt/.test(ua)
|
||||
const isMobile = isMobileUA || (hasTouchInput && hasTouchLayout && !isWindowsDesktop)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Show when={ijs.state === 'ready'}>
|
||||
<StateProvider length={ijs()?.length ?? 0}>
|
||||
<Switch fallback={<div>Error</div>}>
|
||||
<Match when={isMobile}>
|
||||
<Mobile
|
||||
ijs={ijs() ?? []}
|
||||
closeText={container.dataset.close}
|
||||
loadingText={container.dataset.loading}
|
||||
setScrollable={setScollable}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={!isMobile}>
|
||||
<Desktop
|
||||
ijs={ijs() ?? []}
|
||||
prevText={container.dataset.prev}
|
||||
closeText={container.dataset.close}
|
||||
nextText={container.dataset.next}
|
||||
loadingText={container.dataset.loading}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
</StateProvider>
|
||||
<ImageStateProvider images={ijs() ?? []}>
|
||||
<ConfigStateProvider>
|
||||
<AppContent
|
||||
isMobile={isMobile}
|
||||
prevText={container.dataset.prev}
|
||||
closeText={container.dataset.close}
|
||||
nextText={container.dataset.next}
|
||||
loadingText={container.dataset.loading}
|
||||
/>
|
||||
</ConfigStateProvider>
|
||||
</ImageStateProvider>
|
||||
</Show>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
import {
|
||||
For,
|
||||
createEffect,
|
||||
on,
|
||||
onMount,
|
||||
type Accessor,
|
||||
type JSX,
|
||||
type Setter
|
||||
} from 'solid-js'
|
||||
import { For, createEffect, on, onMount, type JSX } from 'solid-js'
|
||||
|
||||
import type { ImageJSON } from '../resources'
|
||||
import { useState } from '../state'
|
||||
import { useImageState } from '../imageState'
|
||||
|
||||
import type { MobileImage } from './layout'
|
||||
import { useMobileState } from './state'
|
||||
|
||||
function getRandom(min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min
|
||||
@@ -31,29 +23,26 @@ function onIntersection<T extends HTMLElement>(
|
||||
}).observe(element)
|
||||
}
|
||||
|
||||
export default function Collection(props: {
|
||||
children?: JSX.Element
|
||||
ijs: ImageJSON[]
|
||||
isAnimating: Accessor<boolean>
|
||||
isOpen: Accessor<boolean>
|
||||
setIsOpen: Setter<boolean>
|
||||
}): JSX.Element {
|
||||
export default function Collection(): JSX.Element {
|
||||
// variables
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
const imgs: MobileImage[] = Array<MobileImage>(props.ijs.length)
|
||||
const imageState = useImageState()
|
||||
const imgs: MobileImage[] = Array<MobileImage>(imageState().length)
|
||||
|
||||
// states
|
||||
const [state, { setIndex }] = useState()
|
||||
const [mobile, { setIndex, setIsOpen }] = useMobileState()
|
||||
|
||||
// helper functions
|
||||
const handleClick: (i: number) => void = (i) => {
|
||||
if (props.isAnimating()) return
|
||||
if (mobile.isAnimating()) return
|
||||
setIndex(i)
|
||||
props.setIsOpen(true)
|
||||
setIsOpen(true)
|
||||
}
|
||||
|
||||
const scrollToActive: () => void = () => {
|
||||
imgs[state().index].scrollIntoView({ behavior: 'auto', block: 'center' })
|
||||
const index = mobile.index()
|
||||
|
||||
if (index < 0) return
|
||||
imgs[index].scrollIntoView({ behavior: 'auto', block: 'center' })
|
||||
}
|
||||
|
||||
// effects
|
||||
@@ -94,11 +83,9 @@ export default function Collection(props: {
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
mobile.isOpen,
|
||||
() => {
|
||||
props.isOpen()
|
||||
},
|
||||
() => {
|
||||
if (!props.isOpen()) scrollToActive() // scroll to active when closed
|
||||
if (!mobile.isOpen()) scrollToActive() // scroll to active when closed
|
||||
},
|
||||
{ defer: true }
|
||||
)
|
||||
@@ -107,7 +94,7 @@ export default function Collection(props: {
|
||||
return (
|
||||
<>
|
||||
<div class="collection">
|
||||
<For each={props.ijs}>
|
||||
<For each={imageState().images}>
|
||||
{(ij, i) => (
|
||||
<img
|
||||
ref={imgs[i()]}
|
||||
|
||||
@@ -1,209 +1,170 @@
|
||||
import { type gsap } from 'gsap'
|
||||
import {
|
||||
createEffect,
|
||||
createMemo,
|
||||
createSignal,
|
||||
For,
|
||||
on,
|
||||
onMount,
|
||||
Show,
|
||||
type Accessor,
|
||||
type JSX,
|
||||
type Setter
|
||||
untrack,
|
||||
type JSX
|
||||
} from 'solid-js'
|
||||
import { createStore } from 'solid-js/store'
|
||||
import { type Swiper } from 'swiper'
|
||||
import invariant from 'tiny-invariant'
|
||||
|
||||
import { type ImageJSON } from '../resources'
|
||||
import { useState } from '../state'
|
||||
import { loadGsap, type Vector } from '../utils'
|
||||
import { useImageState } from '../imageState'
|
||||
import { loadGsap, removeDuplicates, type Vector } from '../utils'
|
||||
|
||||
import GalleryImage from './galleryImage'
|
||||
import GalleryNav, { capitalizeFirstLetter } from './galleryNav'
|
||||
|
||||
function removeDuplicates<T>(arr: T[]): T[] {
|
||||
if (arr.length < 2) return arr // optimization
|
||||
return [...new Set(arr)]
|
||||
}
|
||||
|
||||
async function loadSwiper(): Promise<typeof Swiper> {
|
||||
const s = await import('swiper')
|
||||
return s.Swiper
|
||||
}
|
||||
import { closeGallery, openGallery } from './galleryTransitions'
|
||||
import { getActiveImageIndexes, loadSwiper } from './galleryUtils'
|
||||
import { useMobileState } from './state'
|
||||
|
||||
export default function Gallery(props: {
|
||||
children?: JSX.Element
|
||||
ijs: ImageJSON[]
|
||||
closeText: string
|
||||
loadingText: string
|
||||
isAnimating: Accessor<boolean>
|
||||
setIsAnimating: Setter<boolean>
|
||||
isOpen: Accessor<boolean>
|
||||
setIsOpen: Setter<boolean>
|
||||
setScrollable: Setter<boolean>
|
||||
}): JSX.Element {
|
||||
// variables
|
||||
let _gsap: typeof gsap
|
||||
let _swiper: Swiper
|
||||
let _swiper: Swiper | undefined
|
||||
let initPromise: Promise<void> | undefined
|
||||
|
||||
let curtain: HTMLDivElement | undefined
|
||||
let gallery: HTMLDivElement | undefined
|
||||
let galleryInner: HTMLDivElement | undefined
|
||||
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
const _loadingText = capitalizeFirstLetter(props.loadingText)
|
||||
const imageState = useImageState()
|
||||
const [mobile, { setIndex, setIsAnimating, setIsScrollLocked }] = useMobileState()
|
||||
|
||||
const loadingText = createMemo(() => capitalizeFirstLetter(props.loadingText))
|
||||
|
||||
// states
|
||||
let lastIndex = -1
|
||||
let mounted = false
|
||||
let navigateVector: Vector = 'none'
|
||||
|
||||
const [state, { setIndex }] = useState()
|
||||
const [libLoaded, setLibLoaded] = createSignal(false)
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
const [loads, setLoads] = createStore(Array<boolean>(props.ijs.length).fill(false))
|
||||
const [swiperReady, setSwiperReady] = createSignal(false)
|
||||
const [loads, setLoads] = createStore(Array<boolean>(imageState().length).fill(false))
|
||||
|
||||
// helper functions
|
||||
const slideUp: () => void = () => {
|
||||
// isAnimating is prechecked in isOpen effect
|
||||
if (!libLoaded() || !mounted) return
|
||||
props.setIsAnimating(true)
|
||||
|
||||
invariant(curtain, 'curtain is not defined')
|
||||
invariant(gallery, 'gallery is not defined')
|
||||
|
||||
_gsap.to(curtain, {
|
||||
opacity: 1,
|
||||
duration: 1
|
||||
openGallery({
|
||||
gsap: _gsap,
|
||||
curtain,
|
||||
gallery,
|
||||
setIsAnimating,
|
||||
setIsScrollLocked
|
||||
})
|
||||
|
||||
_gsap.to(gallery, {
|
||||
y: 0,
|
||||
ease: 'power3.inOut',
|
||||
duration: 1,
|
||||
delay: 0.4
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
props.setScrollable(false)
|
||||
props.setIsAnimating(false)
|
||||
}, 1200)
|
||||
}
|
||||
|
||||
const slideDown: () => void = () => {
|
||||
// isAnimating is prechecked in isOpen effect
|
||||
props.setIsAnimating(true)
|
||||
|
||||
invariant(gallery, 'curtain is not defined')
|
||||
invariant(curtain, 'gallery is not defined')
|
||||
|
||||
_gsap.to(gallery, {
|
||||
y: '100%',
|
||||
ease: 'power3.inOut',
|
||||
duration: 1
|
||||
closeGallery({
|
||||
gsap: _gsap,
|
||||
curtain,
|
||||
gallery,
|
||||
setIsAnimating,
|
||||
setIsScrollLocked,
|
||||
onClosed: () => {
|
||||
lastIndex = -1
|
||||
}
|
||||
})
|
||||
|
||||
_gsap.to(curtain, {
|
||||
opacity: 0,
|
||||
duration: 1.2,
|
||||
delay: 0.4
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
// cleanup
|
||||
props.setScrollable(true)
|
||||
props.setIsAnimating(false)
|
||||
lastIndex = -1
|
||||
}, 1400)
|
||||
}
|
||||
|
||||
const galleryLoadImages: () => void = () => {
|
||||
let activeImagesIndex: number[] = []
|
||||
const _state = state()
|
||||
const currentIndex = _state.index
|
||||
const nextIndex = Math.min(currentIndex + 1, _state.length - 1)
|
||||
const prevIndex = Math.max(currentIndex - 1, 0)
|
||||
switch (navigateVector) {
|
||||
case 'next':
|
||||
activeImagesIndex = [nextIndex]
|
||||
break
|
||||
case 'prev':
|
||||
activeImagesIndex = [prevIndex]
|
||||
break
|
||||
case 'none':
|
||||
activeImagesIndex = [currentIndex, nextIndex, prevIndex]
|
||||
break
|
||||
}
|
||||
setLoads(removeDuplicates(activeImagesIndex), true)
|
||||
const currentIndex = mobile.index()
|
||||
|
||||
setLoads(
|
||||
removeDuplicates(
|
||||
getActiveImageIndexes(currentIndex, imageState().length, navigateVector)
|
||||
),
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
const changeSlide: (slide: number) => void = (slide) => {
|
||||
// we are already in the gallery, don't need to
|
||||
// check mounted or libLoaded
|
||||
if (!swiperReady() || _swiper === undefined) return
|
||||
galleryLoadImages()
|
||||
_swiper.slideTo(slide, 0)
|
||||
}
|
||||
|
||||
// effects
|
||||
onMount(() => {
|
||||
window.addEventListener(
|
||||
'touchstart',
|
||||
() => {
|
||||
loadGsap()
|
||||
.then((g) => {
|
||||
_gsap = g
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
loadSwiper()
|
||||
.then((S) => {
|
||||
invariant(galleryInner, 'galleryInner is not defined')
|
||||
_swiper = new S(galleryInner, { spaceBetween: 20 })
|
||||
_swiper.on('slideChange', ({ realIndex }) => {
|
||||
setIndex(realIndex)
|
||||
})
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
const ensureGalleryReady: () => Promise<void> = async () => {
|
||||
if (initPromise !== undefined) return await initPromise
|
||||
|
||||
initPromise = (async () => {
|
||||
try {
|
||||
const [g, S] = await Promise.all([loadGsap(), loadSwiper()])
|
||||
|
||||
_gsap = g
|
||||
|
||||
invariant(galleryInner, 'galleryInner is not defined')
|
||||
_swiper = new S(galleryInner, { spaceBetween: 20 })
|
||||
_swiper.on('slideChange', ({ realIndex }) => {
|
||||
setIndex(realIndex)
|
||||
})
|
||||
|
||||
setLibLoaded(true)
|
||||
},
|
||||
{ once: true, passive: true }
|
||||
)
|
||||
setSwiperReady(true)
|
||||
|
||||
const initialIndex = untrack(mobile.index)
|
||||
|
||||
if (initialIndex >= 0) {
|
||||
changeSlide(initialIndex)
|
||||
lastIndex = initialIndex
|
||||
}
|
||||
} catch (e) {
|
||||
initPromise = undefined
|
||||
setSwiperReady(false)
|
||||
console.log(e)
|
||||
}
|
||||
})()
|
||||
|
||||
await initPromise
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('touchstart', () => void ensureGalleryReady(), {
|
||||
once: true,
|
||||
passive: true
|
||||
})
|
||||
mounted = true
|
||||
})
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => {
|
||||
state()
|
||||
},
|
||||
() => {
|
||||
const i = state().index
|
||||
if (i === lastIndex)
|
||||
return // change slide only when index is changed
|
||||
else if (lastIndex === -1)
|
||||
navigateVector = 'none' // lastIndex before set
|
||||
else if (i < lastIndex)
|
||||
navigateVector = 'prev' // set navigate vector for galleryLoadImages
|
||||
else if (i > lastIndex)
|
||||
navigateVector = 'next' // set navigate vector for galleryLoadImages
|
||||
else navigateVector = 'none' // default
|
||||
changeSlide(i) // change slide to new index
|
||||
lastIndex = i // update last index
|
||||
() => [swiperReady(), mobile.index()] as const,
|
||||
([ready, index]) => {
|
||||
if (!ready || index < 0) return
|
||||
if (index === lastIndex) return
|
||||
if (lastIndex === -1) navigateVector = 'none'
|
||||
else if (index < lastIndex) navigateVector = 'prev'
|
||||
else if (index > lastIndex) navigateVector = 'next'
|
||||
else navigateVector = 'none'
|
||||
changeSlide(index)
|
||||
lastIndex = index
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => {
|
||||
props.isOpen()
|
||||
},
|
||||
() => {
|
||||
if (props.isAnimating()) return
|
||||
if (props.isOpen()) slideUp()
|
||||
() => mobile.isOpen(),
|
||||
async (isOpen) => {
|
||||
if (isOpen && !swiperReady()) {
|
||||
await ensureGalleryReady()
|
||||
}
|
||||
|
||||
if (!libLoaded() || !swiperReady()) return
|
||||
if (mobile.isAnimating()) return
|
||||
if (isOpen) slideUp()
|
||||
else slideDown()
|
||||
},
|
||||
{ defer: true }
|
||||
@@ -215,26 +176,16 @@ export default function Gallery(props: {
|
||||
<div ref={gallery} class="gallery">
|
||||
<div ref={galleryInner} class="galleryInner">
|
||||
<div class="swiper-wrapper">
|
||||
<Show when={libLoaded()}>
|
||||
<For each={props.ijs}>
|
||||
{(ij, i) => (
|
||||
<div class="swiper-slide">
|
||||
<GalleryImage
|
||||
load={loads[i()]}
|
||||
ij={ij}
|
||||
loadingText={_loadingText}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
<For each={imageState().images}>
|
||||
{(ij, i) => (
|
||||
<div class="swiper-slide">
|
||||
<GalleryImage load={loads[i()]} ij={ij} loadingText={loadingText()} />
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
<GalleryNav
|
||||
closeText={props.closeText}
|
||||
isAnimating={props.isAnimating}
|
||||
setIsOpen={props.setIsOpen}
|
||||
/>
|
||||
<GalleryNav closeText={props.closeText} />
|
||||
</div>
|
||||
<div ref={curtain} class="curtain" />
|
||||
</>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { onMount, type JSX } from 'solid-js'
|
||||
import { type gsap } from 'gsap'
|
||||
import { createEffect, on, onMount, type JSX } from 'solid-js'
|
||||
import invariant from 'tiny-invariant'
|
||||
|
||||
import type { ImageJSON } from '../resources'
|
||||
import { useState } from '../state'
|
||||
import { loadGsap } from '../utils'
|
||||
|
||||
import { useMobileState } from './state'
|
||||
|
||||
export default function GalleryImage(props: {
|
||||
children?: JSX.Element
|
||||
load: boolean
|
||||
@@ -14,40 +16,83 @@ export default function GalleryImage(props: {
|
||||
let img: HTMLImageElement | undefined
|
||||
let loadingDiv: HTMLDivElement | undefined
|
||||
|
||||
let _gsap: typeof gsap
|
||||
let _gsap: typeof gsap | undefined
|
||||
let gsapPromise: Promise<typeof gsap> | undefined
|
||||
let revealed = false
|
||||
|
||||
const [state] = useState()
|
||||
const [mobile] = useMobileState()
|
||||
|
||||
const revealImage = async (): Promise<void> => {
|
||||
if (revealed) return
|
||||
revealed = true
|
||||
|
||||
invariant(img, 'ref must be defined')
|
||||
invariant(loadingDiv, 'loadingDiv must be defined')
|
||||
|
||||
gsapPromise ??= loadGsap()
|
||||
|
||||
try {
|
||||
_gsap ??= await gsapPromise
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
|
||||
if (_gsap === undefined) {
|
||||
img.style.opacity = '1'
|
||||
loadingDiv.style.opacity = '0'
|
||||
return
|
||||
}
|
||||
|
||||
if (mobile.index() !== props.ij.index) {
|
||||
_gsap.set(img, { opacity: 1 })
|
||||
_gsap.set(loadingDiv, { opacity: 0 })
|
||||
return
|
||||
}
|
||||
|
||||
_gsap.to(img, {
|
||||
opacity: 1,
|
||||
delay: 0.5,
|
||||
duration: 0.5,
|
||||
ease: 'power3.out'
|
||||
})
|
||||
_gsap.to(loadingDiv, { opacity: 0, duration: 0.5, ease: 'power3.in' })
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
loadGsap()
|
||||
gsapPromise = loadGsap()
|
||||
.then((g) => {
|
||||
_gsap = g
|
||||
return g
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
throw e
|
||||
})
|
||||
|
||||
img?.addEventListener(
|
||||
'load',
|
||||
() => {
|
||||
invariant(img, 'ref must be defined')
|
||||
invariant(loadingDiv, 'loadingDiv must be defined')
|
||||
if (state().index !== props.ij.index) {
|
||||
_gsap.set(img, { opacity: 1 })
|
||||
_gsap.set(loadingDiv, { opacity: 0 })
|
||||
} else {
|
||||
_gsap.to(img, {
|
||||
opacity: 1,
|
||||
delay: 0.5,
|
||||
duration: 0.5,
|
||||
ease: 'power3.out'
|
||||
})
|
||||
_gsap.to(loadingDiv, { opacity: 0, duration: 0.5, ease: 'power3.in' })
|
||||
}
|
||||
void revealImage()
|
||||
},
|
||||
{ once: true, passive: true }
|
||||
)
|
||||
|
||||
if (props.load && img?.complete && img.currentSrc !== '') {
|
||||
void revealImage()
|
||||
}
|
||||
})
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => props.load,
|
||||
(load) => {
|
||||
if (!load || img === undefined || !img.complete || img.currentSrc === '') return
|
||||
void revealImage()
|
||||
},
|
||||
{ defer: true }
|
||||
)
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="slideContainer">
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { createMemo, type Accessor, type JSX, type Setter } from 'solid-js'
|
||||
import { createMemo, type JSX } from 'solid-js'
|
||||
|
||||
import { useState } from '../state'
|
||||
import { useImageState } from '../imageState'
|
||||
import { expand } from '../utils'
|
||||
|
||||
import { useMobileState } from './state'
|
||||
|
||||
export function capitalizeFirstLetter(str: string): string {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||
}
|
||||
@@ -10,17 +12,16 @@ export function capitalizeFirstLetter(str: string): string {
|
||||
export default function GalleryNav(props: {
|
||||
children?: JSX.Element
|
||||
closeText: string
|
||||
isAnimating: Accessor<boolean>
|
||||
setIsOpen: Setter<boolean>
|
||||
}): JSX.Element {
|
||||
// states
|
||||
const [state] = useState()
|
||||
const indexValue = createMemo(() => expand(state().index + 1))
|
||||
const indexLength = createMemo(() => expand(state().length))
|
||||
const imageState = useImageState()
|
||||
const [mobile, { setIsOpen }] = useMobileState()
|
||||
const indexValue = createMemo(() => expand(mobile.index() + 1))
|
||||
const indexLength = createMemo(() => expand(imageState().length))
|
||||
|
||||
const onClick: () => void = () => {
|
||||
if (props.isAnimating()) return
|
||||
props.setIsOpen(false)
|
||||
if (mobile.isAnimating()) return
|
||||
setIsOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -37,7 +38,14 @@ export default function GalleryNav(props: {
|
||||
<span class="num">{indexLength()[2]}</span>
|
||||
<span class="num">{indexLength()[3]}</span>
|
||||
</div>
|
||||
<div onClick={onClick} onKeyDown={onClick}>
|
||||
<div
|
||||
class="navClose"
|
||||
onClick={onClick}
|
||||
onTouchEnd={onClick}
|
||||
onKeyDown={onClick}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
>
|
||||
{capitalizeFirstLetter(props.closeText)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
64
assets/ts/mobile/galleryTransitions.ts
Normal file
64
assets/ts/mobile/galleryTransitions.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { type gsap } from 'gsap'
|
||||
|
||||
const OPEN_DELAY_MS = 1200
|
||||
const CLOSE_DELAY_MS = 1400
|
||||
|
||||
export function openGallery(args: {
|
||||
gsap: typeof gsap
|
||||
curtain: HTMLDivElement
|
||||
gallery: HTMLDivElement
|
||||
setIsAnimating: (value: boolean) => void
|
||||
setIsScrollLocked: (value: boolean) => void
|
||||
}): void {
|
||||
const { gsap, curtain, gallery, setIsAnimating, setIsScrollLocked } = args
|
||||
|
||||
setIsAnimating(true)
|
||||
|
||||
gsap.to(curtain, {
|
||||
opacity: 1,
|
||||
duration: 1
|
||||
})
|
||||
|
||||
gsap.to(gallery, {
|
||||
y: 0,
|
||||
ease: 'power3.inOut',
|
||||
duration: 1,
|
||||
delay: 0.4
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
setIsScrollLocked(true)
|
||||
setIsAnimating(false)
|
||||
}, OPEN_DELAY_MS)
|
||||
}
|
||||
|
||||
export function closeGallery(args: {
|
||||
gsap: typeof gsap
|
||||
curtain: HTMLDivElement
|
||||
gallery: HTMLDivElement
|
||||
setIsAnimating: (value: boolean) => void
|
||||
setIsScrollLocked: (value: boolean) => void
|
||||
onClosed: () => void
|
||||
}): void {
|
||||
const { gsap, curtain, gallery, setIsAnimating, setIsScrollLocked, onClosed } = args
|
||||
|
||||
setIsAnimating(true)
|
||||
|
||||
gsap.to(gallery, {
|
||||
y: '100%',
|
||||
ease: 'power3.inOut',
|
||||
duration: 1
|
||||
})
|
||||
|
||||
gsap.to(curtain, {
|
||||
opacity: 0,
|
||||
duration: 1.2,
|
||||
delay: 0.4
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
setIsScrollLocked(false)
|
||||
setIsAnimating(false)
|
||||
onClosed()
|
||||
}, CLOSE_DELAY_MS)
|
||||
}
|
||||
26
assets/ts/mobile/galleryUtils.ts
Normal file
26
assets/ts/mobile/galleryUtils.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { type Swiper } from 'swiper'
|
||||
|
||||
import type { Vector } from '../utils'
|
||||
|
||||
export async function loadSwiper(): Promise<typeof Swiper> {
|
||||
const swiper = await import('swiper')
|
||||
return swiper.Swiper
|
||||
}
|
||||
|
||||
export function getActiveImageIndexes(
|
||||
currentIndex: number,
|
||||
length: number,
|
||||
navigateVector: Vector
|
||||
): number[] {
|
||||
const nextIndex = Math.min(currentIndex + 1, length - 1)
|
||||
const prevIndex = Math.max(currentIndex - 1, 0)
|
||||
|
||||
switch (navigateVector) {
|
||||
case 'next':
|
||||
return [nextIndex]
|
||||
case 'prev':
|
||||
return [prevIndex]
|
||||
case 'none':
|
||||
return [currentIndex, nextIndex, prevIndex]
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Show, createSignal, type JSX, type Setter } from 'solid-js'
|
||||
import { Show, createEffect, onCleanup, type JSX } from 'solid-js'
|
||||
|
||||
import type { ImageJSON } from '../resources'
|
||||
import { useImageState } from '../imageState'
|
||||
|
||||
import Collection from './collection'
|
||||
import Gallery from './gallery'
|
||||
import { useMobileState } from './state'
|
||||
|
||||
/**
|
||||
* interfaces
|
||||
@@ -18,34 +19,33 @@ export interface MobileImage extends HTMLImageElement {
|
||||
|
||||
export default function Mobile(props: {
|
||||
children?: JSX.Element
|
||||
ijs: ImageJSON[]
|
||||
closeText: string
|
||||
loadingText: string
|
||||
setScrollable: Setter<boolean>
|
||||
}): JSX.Element {
|
||||
// states
|
||||
const [isOpen, setIsOpen] = createSignal(false)
|
||||
const [isAnimating, setIsAnimating] = createSignal(false)
|
||||
const imageState = useImageState()
|
||||
const [mobile] = useMobileState()
|
||||
|
||||
createEffect(() => {
|
||||
const container = document.getElementsByClassName('container').item(0)
|
||||
if (container === null) return
|
||||
|
||||
if (mobile.isScrollLocked()) {
|
||||
container.classList.add('disableScroll')
|
||||
} else {
|
||||
container.classList.remove('disableScroll')
|
||||
}
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
const container = document.getElementsByClassName('container').item(0)
|
||||
container?.classList.remove('disableScroll')
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<Show when={props.ijs.length > 0}>
|
||||
<Collection
|
||||
ijs={props.ijs}
|
||||
isAnimating={isAnimating}
|
||||
isOpen={isOpen}
|
||||
setIsOpen={setIsOpen}
|
||||
/>
|
||||
<Gallery
|
||||
ijs={props.ijs}
|
||||
closeText={props.closeText}
|
||||
loadingText={props.loadingText}
|
||||
isAnimating={isAnimating}
|
||||
setIsAnimating={setIsAnimating}
|
||||
isOpen={isOpen}
|
||||
setIsOpen={setIsOpen}
|
||||
setScrollable={props.setScrollable}
|
||||
/>
|
||||
<Show when={imageState().length > 0}>
|
||||
<Collection />
|
||||
<Gallery closeText={props.closeText} loadingText={props.loadingText} />
|
||||
</Show>
|
||||
</>
|
||||
)
|
||||
|
||||
78
assets/ts/mobile/state.ts
Normal file
78
assets/ts/mobile/state.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
createComponent,
|
||||
createContext,
|
||||
createSignal,
|
||||
useContext,
|
||||
type Accessor,
|
||||
type JSX,
|
||||
type Setter
|
||||
} from 'solid-js'
|
||||
import invariant from 'tiny-invariant'
|
||||
|
||||
import { useImageState } from '../imageState'
|
||||
import { decrement, increment } from '../utils'
|
||||
|
||||
export interface MobileState {
|
||||
index: Accessor<number>
|
||||
isOpen: Accessor<boolean>
|
||||
isAnimating: Accessor<boolean>
|
||||
isScrollLocked: Accessor<boolean>
|
||||
}
|
||||
|
||||
export type MobileStateContextType = readonly [
|
||||
MobileState,
|
||||
{
|
||||
readonly setIndex: Setter<number>
|
||||
readonly incIndex: () => void
|
||||
readonly decIndex: () => void
|
||||
readonly setIsOpen: Setter<boolean>
|
||||
readonly setIsAnimating: Setter<boolean>
|
||||
readonly setIsScrollLocked: Setter<boolean>
|
||||
}
|
||||
]
|
||||
|
||||
const MobileStateContext = createContext<MobileStateContextType>()
|
||||
|
||||
export function MobileStateProvider(props: { children?: JSX.Element }): JSX.Element {
|
||||
const imageState = useImageState()
|
||||
|
||||
const [index, setIndex] = createSignal(-1)
|
||||
const [isOpen, setIsOpen] = createSignal(false)
|
||||
const [isAnimating, setIsAnimating] = createSignal(false)
|
||||
const [isScrollLocked, setIsScrollLocked] = createSignal(false)
|
||||
|
||||
const updateIndex = (stride: 1 | -1): void => {
|
||||
const length = imageState().length
|
||||
if (length <= 0) return
|
||||
setIndex((current) =>
|
||||
stride === 1 ? increment(current, length) : decrement(current, length)
|
||||
)
|
||||
}
|
||||
|
||||
return createComponent(MobileStateContext.Provider, {
|
||||
value: [
|
||||
{ index, isOpen, isAnimating, isScrollLocked },
|
||||
{
|
||||
setIndex,
|
||||
incIndex: () => {
|
||||
updateIndex(1)
|
||||
},
|
||||
decIndex: () => {
|
||||
updateIndex(-1)
|
||||
},
|
||||
setIsOpen,
|
||||
setIsAnimating,
|
||||
setIsScrollLocked
|
||||
}
|
||||
],
|
||||
get children() {
|
||||
return props.children
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function useMobileState(): MobileStateContextType {
|
||||
const context = useContext(MobileStateContext)
|
||||
invariant(context, 'undefined mobile context')
|
||||
return context
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
import {
|
||||
createContext,
|
||||
createSignal,
|
||||
useContext,
|
||||
type Accessor,
|
||||
type JSX,
|
||||
type Setter
|
||||
} from 'solid-js'
|
||||
import invariant from 'tiny-invariant'
|
||||
|
||||
import { decrement, getThresholdSessionIndex, increment } from './utils'
|
||||
|
||||
/**
|
||||
* interfaces and types
|
||||
*/
|
||||
|
||||
export interface ThresholdRelated {
|
||||
threshold: number
|
||||
trailLength: number
|
||||
}
|
||||
|
||||
export interface State {
|
||||
index: number
|
||||
length: number
|
||||
threshold: number
|
||||
trailLength: number
|
||||
}
|
||||
|
||||
export type StateContextType = readonly [
|
||||
Accessor<State>,
|
||||
{
|
||||
readonly setIndex: (index: number) => void
|
||||
readonly incIndex: () => void
|
||||
readonly decIndex: () => void
|
||||
readonly incThreshold: () => void
|
||||
readonly decThreshold: () => void
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
* constants
|
||||
*/
|
||||
|
||||
const thresholds: ThresholdRelated[] = [
|
||||
{ threshold: 20, trailLength: 20 },
|
||||
{ threshold: 40, trailLength: 10 },
|
||||
{ threshold: 80, trailLength: 5 },
|
||||
{ threshold: 140, trailLength: 5 },
|
||||
{ threshold: 200, trailLength: 5 }
|
||||
]
|
||||
const makeStateContext: (
|
||||
state: Accessor<State>,
|
||||
setState: Setter<State>
|
||||
) => StateContextType = (state: Accessor<State>, setState: Setter<State>) => {
|
||||
return [
|
||||
state,
|
||||
{
|
||||
setIndex: (index: number) => {
|
||||
setState((s) => {
|
||||
return { ...s, index }
|
||||
})
|
||||
},
|
||||
incIndex: () => {
|
||||
setState((s) => {
|
||||
return { ...s, index: increment(s.index, s.length) }
|
||||
})
|
||||
},
|
||||
decIndex: () => {
|
||||
setState((s) => {
|
||||
return { ...s, index: decrement(s.index, s.length) }
|
||||
})
|
||||
},
|
||||
incThreshold: () => {
|
||||
setState((s) => {
|
||||
return { ...s, ...updateThreshold(s.threshold, thresholds, 1) }
|
||||
})
|
||||
},
|
||||
decThreshold: () => {
|
||||
setState((s) => {
|
||||
return { ...s, ...updateThreshold(s.threshold, thresholds, -1) }
|
||||
})
|
||||
}
|
||||
}
|
||||
] as const
|
||||
}
|
||||
const StateContext = createContext<StateContextType>()
|
||||
|
||||
/**
|
||||
* helper functions
|
||||
*/
|
||||
|
||||
function updateThreshold(
|
||||
currentThreshold: number,
|
||||
thresholds: ThresholdRelated[],
|
||||
stride: number
|
||||
): ThresholdRelated {
|
||||
const i = thresholds.findIndex((t) => t.threshold === currentThreshold) + stride
|
||||
if (i < 0 || i >= thresholds.length) return thresholds[i - stride]
|
||||
// storage the index so we can restore it even if we go to another page
|
||||
sessionStorage.setItem('thresholdsIndex', i.toString())
|
||||
return thresholds[i]
|
||||
}
|
||||
|
||||
/**
|
||||
* StateProvider
|
||||
*/
|
||||
|
||||
export function StateProvider(props: {
|
||||
children?: JSX.Element
|
||||
length: number
|
||||
}): JSX.Element {
|
||||
const defaultState: State = {
|
||||
index: -1,
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
length: props.length,
|
||||
threshold: thresholds[getThresholdSessionIndex()].threshold,
|
||||
trailLength: thresholds[getThresholdSessionIndex()].trailLength
|
||||
}
|
||||
|
||||
const [state, setState] = createSignal(defaultState)
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
const contextValue = makeStateContext(state, setState)
|
||||
return (
|
||||
<StateContext.Provider value={contextValue}>{props.children}</StateContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* use context
|
||||
*/
|
||||
|
||||
export function useState(): StateContextType {
|
||||
const uc = useContext(StateContext)
|
||||
invariant(uc, 'undefined context')
|
||||
return uc
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
bundled/js/BQuRTE.js
Normal file
1
bundled/js/BQuRTE.js
Normal file
File diff suppressed because one or more lines are too long
1
bundled/js/Briij_.js
Normal file
1
bundled/js/Briij_.js
Normal file
File diff suppressed because one or more lines are too long
1
bundled/js/BwPjw0.js
Normal file
1
bundled/js/BwPjw0.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
bundled/js/jjLR4b.js
Normal file
1
bundled/js/jjLR4b.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
bundled/js/v3xXn8.js
Normal file
1
bundled/js/v3xXn8.js
Normal file
File diff suppressed because one or more lines are too long
10
flake.lock
generated
10
flake.lock
generated
@@ -2,12 +2,12 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1762596750,
|
||||
"narHash": "sha256-rXXuz51Bq7DHBlfIjN7jO8Bu3du5TV+3DSADBX7/9YQ=",
|
||||
"rev": "b6a8526db03f735b89dd5ff348f53f752e7ddc8e",
|
||||
"revCount": 891611,
|
||||
"lastModified": 1769461804,
|
||||
"narHash": "sha256-msG8SU5WsBUfVVa/9RPLaymvi5bI8edTavbIq3vRlhI=",
|
||||
"rev": "bfc1b8a4574108ceef22f02bafcf6611380c100d",
|
||||
"revCount": 935279,
|
||||
"type": "tarball",
|
||||
"url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.891611%2Brev-b6a8526db03f735b89dd5ff348f53f752e7ddc8e/019a684c-ea63-75fd-99cc-3b869954e5f9/source.tar.gz"
|
||||
"url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.935279%2Brev-bfc1b8a4574108ceef22f02bafcf6611380c100d/019c02ef-f13d-717e-8527-f1603ec205db/source.tar.gz"
|
||||
},
|
||||
"original": {
|
||||
"type": "tarball",
|
||||
|
||||
29
package.json
29
package.json
@@ -39,30 +39,31 @@
|
||||
},
|
||||
"homepage": "https://github.com/Sped0n/bridget#readme",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@types/node": "^24.10.0",
|
||||
"@typescript-eslint/parser": "^8.46.4",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-love": "^133.0.0",
|
||||
"@eslint/js": "^9.39.4",
|
||||
"@types/node": "^25.5.2",
|
||||
"@typescript-eslint/parser": "^8.58.0",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-love": "^151.0.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"eslint-plugin-solid": "^0.14.5",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "3.6.2",
|
||||
"prettier": "3.8.1",
|
||||
"prettier-plugin-go-template": "^0.0.15",
|
||||
"prettier-plugin-organize-imports": "^4.3.0",
|
||||
"sass-embedded": "^1.93.3",
|
||||
"sass-embedded": "^1.99.0",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.46.4",
|
||||
"vite": "^7.2.2",
|
||||
"vite-plugin-solid": "^2.11.10"
|
||||
"typescript-eslint": "^8.58.1",
|
||||
"vite": "^8.0.5",
|
||||
"vite-plugin-solid": "^2.11.12",
|
||||
"vitefu": "^1.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"gsap": "^3.13.0",
|
||||
"solid-js": "^1.9.10",
|
||||
"swiper": "^12.0.3",
|
||||
"gsap": "^3.15.0",
|
||||
"solid-js": "^1.9.12",
|
||||
"swiper": "^12.1.3",
|
||||
"tiny-invariant": "^1.3.3"
|
||||
}
|
||||
}
|
||||
|
||||
2409
pnpm-lock.yaml
generated
2409
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
node_modules_generated_dir="./node_modules/exampleSite/resources/_gen"
|
||||
project_generated_dir="./exampleSite/resources/_gen"
|
||||
node_modules_generated_dir="./node_modules/exampleSite/resources/_gen/images"
|
||||
project_generated_dir="./exampleSite/resources/_gen/images"
|
||||
|
||||
copy_generated_assets_to_project() {
|
||||
if [ -d "${node_modules_generated_dir}" ]; then
|
||||
@@ -16,7 +16,8 @@ copy_generated_assets_to_project() {
|
||||
}
|
||||
|
||||
run_site_build() {
|
||||
pnpm run vite:build && hugo --logLevel info --source=exampleSite --gc --minify
|
||||
rm -rf bundled
|
||||
pnpm run build
|
||||
}
|
||||
|
||||
copy_generated_assets_to_node_modules() {
|
||||
|
||||
@@ -5,6 +5,7 @@ export default defineConfig({
|
||||
plugins: [solidPlugin()],
|
||||
build: {
|
||||
outDir: './bundled',
|
||||
cssMinify: 'esbuild',
|
||||
watch: process.env.DISABLE_WATCH
|
||||
? null
|
||||
: {
|
||||
@@ -19,8 +20,7 @@ export default defineConfig({
|
||||
format: 'es',
|
||||
entryFileNames: 'js/[name].js',
|
||||
chunkFileNames: 'js/[hash:6].js',
|
||||
assetFileNames: '[ext]/[name].[ext]',
|
||||
compact: true
|
||||
assetFileNames: '[ext]/[name].[ext]'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user