mirror of
https://github.com/Sped0n/bridget.git
synced 2026-04-17 03:29:31 -07:00
Compare commits
163 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b52801b00 | ||
|
|
4e2347fcef | ||
|
|
17d3f8ba6f | ||
|
|
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
|
contents: write
|
||||||
|
|
||||||
jobs:
|
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:
|
build:
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Build (Hugo ${{ matrix.hugo-version }})
|
name: Build (Hugo ${{ matrix.hugo-label }})
|
||||||
needs: [filter]
|
if: github.event.repository.fork == false
|
||||||
if: |
|
|
||||||
github.event.repository.fork == false
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
hugo-version: ['latest', '0.114.0']
|
hugo-version: ['latest', '0.114.0']
|
||||||
|
include:
|
||||||
|
- hugo-version: latest
|
||||||
|
hugo-label: Latest
|
||||||
|
- hugo-version: '0.114.0'
|
||||||
|
hugo-label: 'v0.114.0'
|
||||||
steps:
|
steps:
|
||||||
- name: Set current date as env variable
|
|
||||||
run: |
|
|
||||||
echo "builddate=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
|
||||||
id: version
|
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.PAT }}
|
token: ${{ secrets.PAT }}
|
||||||
|
|
||||||
- name: Setup Hugo
|
- name: Setup Mise
|
||||||
uses: peaceiris/actions-hugo@v2.6.0
|
uses: jdx/mise-action@v4
|
||||||
with:
|
with:
|
||||||
hugo-version: ${{ matrix.hugo-version }}
|
install_args: node@latest pnpm@10 hugo-extended@${{ matrix.hugo-version }}
|
||||||
extended: true
|
tool_versions: |
|
||||||
|
node latest
|
||||||
|
pnpm 10
|
||||||
|
hugo-extended ${{ matrix.hugo-version }}
|
||||||
|
cache: true
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Get pnpm store path
|
||||||
uses: pnpm/action-setup@v3
|
|
||||||
with:
|
|
||||||
version: 10
|
|
||||||
|
|
||||||
- name: Get pnpm store directory
|
|
||||||
id: pnpm-cache
|
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
|
- name: Setup pnpm cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
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: |
|
restore-keys: |
|
||||||
${{ runner.os }}-pnpm-store-
|
pnpm-store-
|
||||||
|
|
||||||
- name: Setup hugo cache
|
- name: Setup Hugo cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ./exampleSite/resources
|
path: exampleSite/resources/_gen
|
||||||
key: ${{ runner.os }}-hugo-${{ matrix.hugo-version }}-${{ hashFiles('./exampleSite') }}
|
key: hugo-${{ matrix.hugo-version }}-${{ hashFiles('./exampleSite/**/*.jpg') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-hugo-${{ matrix.hugo-version }}-
|
hugo-${{ matrix.hugo-version }}-
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install project dependencies
|
||||||
run: pnpm install
|
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
|
- name: Build
|
||||||
run: |
|
run: 'pnpm run build'
|
||||||
pnpm run build
|
|
||||||
|
|
||||||
- name: Push artifacts
|
- name: Push artifacts
|
||||||
if: >
|
if: >
|
||||||
matrix.hugo-version == 'latest' &&
|
matrix.hugo-version == 'latest' &&
|
||||||
(github.event_name == 'push' || github.event.pull_request.merged == true) &&
|
(github.event_name == 'push' || github.event.pull_request.merged == true)
|
||||||
needs.filter.outputs.any_changed == 'true'
|
uses: stefanzweifel/git-auto-commit-action@v7
|
||||||
uses: stefanzweifel/git-auto-commit-action@v5
|
|
||||||
with:
|
with:
|
||||||
|
file_pattern: 'bundled/**/*.js bundled/**/*.css'
|
||||||
commit_message: 'ci: update bundled artifacts [skip ci]'
|
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:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v2
|
uses: github/codeql-action/init@v3
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# 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).
|
# 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)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v2
|
uses: github/codeql-action/autobuild@v3
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ 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
|
# 📚 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
|
# ./location_of_script_within_repo/buildscript.sh
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v2
|
uses: github/codeql-action/analyze@v3
|
||||||
with:
|
with:
|
||||||
category: '/language:${{matrix.language}}'
|
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
|
*.css.map
|
||||||
|
|
||||||
# dummmy file
|
# 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]
|
> [!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.
|
> 1. I want to keep this theme minimal.
|
||||||
> 2. My bandwith after work is limited.
|
> 2. My bandwith after work is limited.
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
top: var(--nav-height);
|
top: var(--nav-height);
|
||||||
z-index: var(--z-nav-gallery);
|
z-index: var(--z-nav-gallery);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -25,8 +26,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 100%;
|
max-height: calc(var(--window-height) - 2 * var(--nav-height));
|
||||||
height: 100%;
|
max-width: 100%;
|
||||||
|
width: auto;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +51,21 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
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
|
children?: JSX.Element
|
||||||
active: Accessor<boolean>
|
active: Accessor<boolean>
|
||||||
cursorText: Accessor<string>
|
cursorText: Accessor<string>
|
||||||
isOpen: Accessor<boolean>
|
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
// types
|
// types
|
||||||
interface XY {
|
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 { useImageState } from '../imageState'
|
||||||
import type { Vector } from '../utils'
|
|
||||||
|
|
||||||
import CustomCursor from './customCursor'
|
import CustomCursor from './customCursor'
|
||||||
import Nav from './nav'
|
import Nav from './nav'
|
||||||
import Stage from './stage'
|
import Stage from './stage'
|
||||||
import StageNav from './stageNav'
|
import StageNav from './stageNav'
|
||||||
|
import { useDesktopState } from './state'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* interfaces and types
|
* interfaces and types
|
||||||
@@ -23,65 +23,36 @@ export interface DesktopImage extends HTMLImageElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HistoryItem {
|
|
||||||
i: number
|
|
||||||
x: number
|
|
||||||
y: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* components
|
* components
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default function Desktop(props: {
|
export default function Desktop(props: {
|
||||||
children?: JSX.Element
|
children?: JSX.Element
|
||||||
ijs: ImageJSON[]
|
|
||||||
prevText: string
|
prevText: string
|
||||||
closeText: string
|
closeText: string
|
||||||
nextText: string
|
nextText: string
|
||||||
loadingText: string
|
loadingText: string
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const [cordHist, setCordHist] = createSignal<HistoryItem[]>([])
|
const imageState = useImageState()
|
||||||
const [isLoading, setIsLoading] = createSignal(false)
|
const [desktop] = useDesktopState()
|
||||||
const [isOpen, setIsOpen] = createSignal(false)
|
|
||||||
const [isAnimating, setIsAnimating] = createSignal(false)
|
|
||||||
const [hoverText, setHoverText] = createSignal('')
|
|
||||||
const [navVector, setNavVector] = createSignal<Vector>('none')
|
|
||||||
|
|
||||||
const active = createMemo(() => isOpen() && !isAnimating())
|
const active = createMemo(() => desktop.isOpen() && !desktop.isAnimating())
|
||||||
const cursorText = createMemo(() => (isLoading() ? props.loadingText : hoverText()))
|
const cursorText = createMemo(() =>
|
||||||
|
desktop.isLoading() ? props.loadingText : desktop.hoverText()
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Nav />
|
<Nav />
|
||||||
<Show when={props.ijs.length > 0}>
|
<Show when={imageState().length > 0}>
|
||||||
<Stage
|
<Stage />
|
||||||
ijs={props.ijs}
|
<Show when={desktop.isOpen()}>
|
||||||
setIsLoading={setIsLoading}
|
<CustomCursor cursorText={cursorText} active={active} />
|
||||||
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} />
|
|
||||||
<StageNav
|
<StageNav
|
||||||
prevText={props.prevText}
|
prevText={props.prevText}
|
||||||
closeText={props.closeText}
|
closeText={props.closeText}
|
||||||
nextText={props.nextText}
|
nextText={props.nextText}
|
||||||
loadingText={props.loadingText}
|
|
||||||
active={active}
|
|
||||||
isAnimating={isAnimating}
|
|
||||||
setCordHist={setCordHist}
|
|
||||||
isOpen={isOpen}
|
|
||||||
setIsOpen={setIsOpen}
|
|
||||||
setHoverText={setHoverText}
|
|
||||||
navVector={navVector}
|
|
||||||
setNavVector={setNavVector}
|
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</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'
|
import { expand } from '../utils'
|
||||||
|
|
||||||
/**
|
import { useDesktopState } from './state'
|
||||||
* 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default function Nav(): null {
|
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(() => {
|
const imageState = useImageState()
|
||||||
updateIndexText(expand(state().index + 1), expand(state().length))
|
const [config, { incThreshold, decThreshold }] = useConfigState()
|
||||||
updateThresholdText(expand(state().threshold))
|
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
|
createEffect(() => {
|
||||||
incButton.onclick = incThreshold
|
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
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,409 +1,167 @@
|
|||||||
import { type gsap } from 'gsap'
|
import { type gsap } from 'gsap'
|
||||||
import {
|
import { For, createEffect, on, onMount, type JSX } from 'solid-js'
|
||||||
For,
|
|
||||||
createEffect,
|
|
||||||
on,
|
|
||||||
onMount,
|
|
||||||
type Accessor,
|
|
||||||
type JSX,
|
|
||||||
type Setter
|
|
||||||
} from 'solid-js'
|
|
||||||
|
|
||||||
import type { ImageJSON } from '../resources'
|
import { useConfigState } from '../configState'
|
||||||
import { useState, type State } from '../state'
|
import { useImageState } from '../imageState'
|
||||||
import { decrement, increment, loadGsap, type Vector } from '../utils'
|
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'
|
||||||
|
|
||||||
/**
|
export default function Stage(): JSX.Element {
|
||||||
* 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
|
|
||||||
let _gsap: typeof gsap
|
let _gsap: typeof gsap
|
||||||
|
let gsapPromise: Promise<void> | undefined
|
||||||
|
|
||||||
// eslint-disable-next-line solid/reactivity
|
const imageState = useImageState()
|
||||||
const imgs: DesktopImage[] = Array<DesktopImage>(props.ijs.length)
|
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 last = { x: 0, y: 0 }
|
||||||
|
|
||||||
let abortController: AbortController | undefined
|
let abortController: AbortController | undefined
|
||||||
|
|
||||||
// states
|
|
||||||
let gsapLoaded = false
|
let gsapLoaded = false
|
||||||
|
|
||||||
const [state, { incIndex }] = useState()
|
|
||||||
const stateLength = state().length
|
|
||||||
|
|
||||||
let mounted = false
|
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) => {
|
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 cord = { x: e.clientX, y: e.clientY }
|
||||||
const travelDist = Math.hypot(cord.x - last.x, cord.y - last.y)
|
const travelDist = Math.hypot(cord.x - last.x, cord.y - last.y)
|
||||||
|
|
||||||
if (travelDist > state().threshold) {
|
if (travelDist > config().threshold) {
|
||||||
last = cord
|
const nextIndex = increment(desktop.index(), length)
|
||||||
incIndex()
|
|
||||||
|
|
||||||
const _state = state()
|
last = cord
|
||||||
const newHist = { i: _state.index, ...cord }
|
setIndex(nextIndex)
|
||||||
props.setCordHist((prev) => [...prev, newHist].slice(-stateLength))
|
setCordHist((prev) => [...prev, { i: nextIndex, ...cord }].slice(-length))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClick: () => void = () => {
|
const onClick: () => Promise<void> = async () => {
|
||||||
if (!props.isAnimating()) props.setIsOpen(true)
|
if (!gsapLoaded) {
|
||||||
|
await ensureGsapReady()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (desktop.isAnimating() || !gsapLoaded) return
|
||||||
|
if (desktop.index() < 0 || desktop.cordHist().length === 0) return
|
||||||
|
setIsOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const setPosition: () => void = () => {
|
const setPosition: () => void = () => {
|
||||||
if (!mounted) return
|
syncStagePosition({
|
||||||
if (imgs.length === 0) return
|
gsap: _gsap,
|
||||||
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(
|
|
||||||
imgs,
|
imgs,
|
||||||
getTrailInactiveElsIndex(_cordHist, _state)
|
cordHist: desktop.cordHist(),
|
||||||
)
|
trailLength: config().trailLength,
|
||||||
// move down and hide trail inactive
|
length: imageState().length,
|
||||||
tl.to(trailInactiveEls, {
|
isOpen: desktop.isOpen(),
|
||||||
y: '+=20',
|
navVector: desktop.navVector(),
|
||||||
ease: 'power3.in',
|
mounted,
|
||||||
stagger: 0.075,
|
setIsLoading
|
||||||
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)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const minimizeImage: () => Promise<
|
const expandImage: () => Promise<void> = async () => {
|
||||||
gsap.core.Omit<gsap.core.Timeline, 'then'>
|
|
||||||
> = async () => {
|
|
||||||
if (!mounted || !gsapLoaded) throw new Error('not mounted or gsap not loaded')
|
if (!mounted || !gsapLoaded) throw new Error('not mounted or gsap not loaded')
|
||||||
|
|
||||||
props.setIsAnimating(true)
|
await expandStage({
|
||||||
props.setNavVector('none') // cleanup
|
gsap: _gsap,
|
||||||
|
imgs,
|
||||||
const _cordHist = props.cordHist()
|
cordHist: desktop.cordHist(),
|
||||||
const _state = state()
|
trailLength: config().trailLength,
|
||||||
|
length: imageState().length,
|
||||||
const elcIndex = getCurrentElIndex(_cordHist)
|
mounted,
|
||||||
const elsTrailInactiveIndexes = getTrailInactiveElsIndex(_cordHist, _state)
|
setIsLoading,
|
||||||
|
setIsAnimating
|
||||||
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)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLoaderForHiresImage(img: DesktopImage): void {
|
const minimizeImage: () => Promise<void> = async () => {
|
||||||
if (!mounted || !gsapLoaded) return
|
if (!mounted || !gsapLoaded) throw new Error('not mounted or gsap not loaded')
|
||||||
if (!img.complete) {
|
|
||||||
props.setIsLoading(true)
|
setNavVector('none')
|
||||||
// abort controller for cleanup
|
|
||||||
const controller = new AbortController()
|
await minimizeStage({
|
||||||
const abortSignal = controller.signal
|
gsap: _gsap,
|
||||||
// event listeners
|
imgs,
|
||||||
img.addEventListener(
|
cordHist: desktop.cordHist(),
|
||||||
'load',
|
trailLength: config().trailLength,
|
||||||
() => {
|
mounted,
|
||||||
_gsap
|
setIsAnimating
|
||||||
.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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// preload logic
|
|
||||||
imgs.forEach((img, i) => {
|
imgs.forEach((img, i) => {
|
||||||
// preload first 5 images on page load
|
|
||||||
if (i < 5) {
|
if (i < 5) {
|
||||||
img.src = img.dataset.loUrl
|
img.src = img.dataset.loUrl
|
||||||
}
|
}
|
||||||
// lores preloader for rest of the images
|
|
||||||
// eslint-disable-next-line solid/reactivity
|
|
||||||
onMutation(img, (mutation) => {
|
onMutation(img, (mutation) => {
|
||||||
// if open or animating, hold
|
if (desktop.isOpen() || desktop.isAnimating()) return false
|
||||||
if (props.isOpen() || props.isAnimating()) return false
|
|
||||||
// if mutation is not about style attribute, hold
|
|
||||||
if (mutation.attributeName !== 'style') return false
|
if (mutation.attributeName !== 'style') return false
|
||||||
|
|
||||||
const opacity = parseFloat(img.style.opacity)
|
const opacity = parseFloat(img.style.opacity)
|
||||||
// if opacity is not 1, hold
|
|
||||||
if (opacity !== 1) return false
|
if (opacity !== 1) return false
|
||||||
// preload the i + 5th image, if it exists
|
|
||||||
if (i + 5 < imgs.length) {
|
if (i + 5 < imgs.length) {
|
||||||
imgs[i + 5].src = imgs[i + 5].dataset.loUrl
|
imgs[i + 5].src = imgs[i + 5].dataset.loUrl
|
||||||
}
|
}
|
||||||
// triggered
|
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
// load gsap on mousemove
|
|
||||||
window.addEventListener(
|
window.addEventListener('pointermove', () => void ensureGsapReady(), {
|
||||||
'mousemove',
|
passive: true,
|
||||||
() => {
|
once: true
|
||||||
loadGsap()
|
})
|
||||||
.then((g) => {
|
window.addEventListener('pointerdown', () => void ensureGsapReady(), {
|
||||||
_gsap = g
|
passive: true,
|
||||||
gsapLoaded = true
|
once: true
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
window.addEventListener('click', () => void ensureGsapReady(), {
|
||||||
console.log(e)
|
passive: true,
|
||||||
})
|
once: true
|
||||||
},
|
})
|
||||||
{ passive: true, once: true }
|
|
||||||
)
|
|
||||||
// event listeners
|
|
||||||
abortController = new AbortController()
|
abortController = new AbortController()
|
||||||
const abortSignal = abortController.signal
|
const abortSignal = abortController.signal
|
||||||
window.addEventListener('mousemove', onMouse, {
|
window.addEventListener('mousemove', onMouse, {
|
||||||
passive: true,
|
passive: true,
|
||||||
signal: abortSignal
|
signal: abortSignal
|
||||||
})
|
})
|
||||||
// mounted
|
|
||||||
mounted = true
|
mounted = true
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => props.cordHist(),
|
() => desktop.cordHist(),
|
||||||
() => {
|
() => {
|
||||||
setPosition()
|
setPosition()
|
||||||
},
|
},
|
||||||
@@ -413,36 +171,38 @@ export default function Stage(props: {
|
|||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => props.isOpen(),
|
desktop.isOpen,
|
||||||
async () => {
|
async (isOpen) => {
|
||||||
if (props.isAnimating()) return
|
if (desktop.isAnimating()) return
|
||||||
if (props.isOpen()) {
|
|
||||||
// expand image
|
if (isOpen) {
|
||||||
|
if (desktop.index() < 0 || desktop.cordHist().length === 0) {
|
||||||
|
setIsOpen(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
await expandImage()
|
await expandImage()
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
void 0
|
setIsOpen(false)
|
||||||
|
setIsAnimating(false)
|
||||||
|
setIsLoading(false)
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// abort controller for cleanup
|
|
||||||
abortController?.abort()
|
abortController?.abort()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// minimize image
|
|
||||||
await minimizeImage()
|
await minimizeImage()
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
void 0
|
void 0
|
||||||
})
|
})
|
||||||
// eslint-disable-next-line solid/reactivity
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// event listeners and its abort controller
|
|
||||||
abortController = new AbortController()
|
abortController = new AbortController()
|
||||||
const abortSignal = abortController.signal
|
const abortSignal = abortController.signal
|
||||||
window.addEventListener('mousemove', onMouse, {
|
window.addEventListener('mousemove', onMouse, {
|
||||||
passive: true,
|
passive: true,
|
||||||
signal: abortSignal
|
signal: abortSignal
|
||||||
})
|
})
|
||||||
// cleanup isLoading
|
setIsLoading(false)
|
||||||
props.setIsLoading(false)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -453,7 +213,7 @@ export default function Stage(props: {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div class="stage" onClick={onClick} onKeyDown={onClick}>
|
<div class="stage" onClick={onClick} onKeyDown={onClick}>
|
||||||
<For each={props.ijs}>
|
<For each={imageState().images}>
|
||||||
{(ij, i) => (
|
{(ij, i) => (
|
||||||
<img
|
<img
|
||||||
ref={imgs[i()]}
|
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 { useImageState } from '../imageState'
|
||||||
import { decrement, increment, type Vector } from '../utils'
|
import { decrement, increment } from '../utils'
|
||||||
|
|
||||||
import type { HistoryItem } from './layout'
|
import { useDesktopState } from './state'
|
||||||
|
|
||||||
export default function StageNav(props: {
|
export default function StageNav(props: {
|
||||||
children?: JSX.Element
|
children?: JSX.Element
|
||||||
prevText: string
|
prevText: string
|
||||||
closeText: string
|
closeText: string
|
||||||
nextText: 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 {
|
}): JSX.Element {
|
||||||
// types
|
// types
|
||||||
type NavItem = (typeof navItems)[number]
|
type NavItem = (typeof navItems)[number]
|
||||||
@@ -29,64 +20,74 @@ export default function StageNav(props: {
|
|||||||
const navItems = [props.prevText, props.closeText, props.nextText] as const
|
const navItems = [props.prevText, props.closeText, props.nextText] as const
|
||||||
|
|
||||||
// states
|
// 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 = () => {
|
const prevImage: () => void = () => {
|
||||||
props.setNavVector('prev')
|
setNavVector('prev')
|
||||||
props.setCordHist((c) =>
|
setCordHist((c) =>
|
||||||
c.map((item) => {
|
c.map((item) => {
|
||||||
return { ...item, i: decrement(item.i, stateLength) }
|
return { ...item, i: decrement(item.i, imageState().length) }
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
decIndex()
|
decIndex()
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeImage: () => void = () => {
|
const closeImage: () => void = () => {
|
||||||
props.setIsOpen(false)
|
setIsOpen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextImage: () => void = () => {
|
const nextImage: () => void = () => {
|
||||||
props.setNavVector('next')
|
setNavVector('next')
|
||||||
props.setCordHist((c) =>
|
setCordHist((c) =>
|
||||||
c.map((item) => {
|
c.map((item) => {
|
||||||
return { ...item, i: increment(item.i, stateLength) }
|
return { ...item, i: increment(item.i, imageState().length) }
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
incIndex()
|
incIndex()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleClick: (item: NavItem) => void = (item) => {
|
const handleClick: (item: NavItem) => void = (item) => {
|
||||||
if (!props.isOpen() || props.isAnimating()) return
|
if (!desktop.isOpen() || desktop.isAnimating()) return
|
||||||
if (item === navItems[0]) prevImage()
|
if (item === navItems[0]) prevImage()
|
||||||
else if (item === navItems[1]) closeImage()
|
else if (item === navItems[1]) closeImage()
|
||||||
else nextImage()
|
else nextImage()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleKey: (e: KeyboardEvent) => void = (e) => {
|
const handleKey: (e: KeyboardEvent) => void = (e) => {
|
||||||
if (!props.isOpen() || props.isAnimating()) return
|
if (!desktop.isOpen() || desktop.isAnimating()) return
|
||||||
if (e.key === 'ArrowLeft') prevImage()
|
if (e.key === 'ArrowLeft') prevImage()
|
||||||
else if (e.key === 'Escape') closeImage()
|
else if (e.key === 'Escape') closeImage()
|
||||||
else if (e.key === 'ArrowRight') nextImage()
|
else if (e.key === 'ArrowRight') nextImage()
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(
|
||||||
if (props.isOpen()) {
|
on(desktop.isOpen, (isOpen) => {
|
||||||
controller = new AbortController()
|
|
||||||
const abortSignal = controller.signal
|
|
||||||
window.addEventListener('keydown', handleKey, {
|
|
||||||
passive: true,
|
|
||||||
signal: abortSignal
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
controller?.abort()
|
controller?.abort()
|
||||||
}
|
|
||||||
|
if (isOpen) {
|
||||||
|
controller = new AbortController()
|
||||||
|
const abortSignal = controller.signal
|
||||||
|
window.addEventListener('keydown', handleKey, {
|
||||||
|
passive: true,
|
||||||
|
signal: abortSignal
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
controller?.abort()
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div class="navOverlay" classList={{ active: props.active() }}>
|
<div class="navOverlay" classList={{ active: active() }}>
|
||||||
<For each={navItems}>
|
<For each={navItems}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<div
|
<div
|
||||||
@@ -94,8 +95,8 @@ export default function StageNav(props: {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleClick(item)
|
handleClick(item)
|
||||||
}}
|
}}
|
||||||
onFocus={() => props.setHoverText(item)}
|
onFocus={() => setHoverText(item)}
|
||||||
onMouseOver={() => props.setHoverText(item)}
|
onMouseOver={() => setHoverText(item)}
|
||||||
tabIndex="-1"
|
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 {
|
import { Match, Show, Switch, createResource, lazy, type JSX } from 'solid-js'
|
||||||
Match,
|
|
||||||
Show,
|
|
||||||
Switch,
|
|
||||||
createEffect,
|
|
||||||
createResource,
|
|
||||||
createSignal,
|
|
||||||
lazy,
|
|
||||||
type JSX
|
|
||||||
} from 'solid-js'
|
|
||||||
import { render } from 'solid-js/web'
|
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 { getImageJSON } from './resources'
|
||||||
import { StateProvider } from './state'
|
|
||||||
|
|
||||||
import '../scss/style.scss'
|
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 Desktop = lazy(async () => await import('./desktop/layout'))
|
||||||
const Mobile = lazy(async () => await import('./mobile/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 {
|
function Main(): JSX.Element {
|
||||||
// variables
|
// variables
|
||||||
const [ijs] = createResource(getImageJSON)
|
const [ijs] = createResource(getImageJSON)
|
||||||
const isMobile =
|
const ua = window.navigator.userAgent.toLowerCase()
|
||||||
window.matchMedia('(hover: none)').matches &&
|
const hasTouchInput = 'ontouchstart' in window || window.navigator.maxTouchPoints > 0
|
||||||
!window.navigator.userAgent.includes('Win')
|
const hasTouchLayout =
|
||||||
|
window.matchMedia('(pointer: coarse)').matches ||
|
||||||
// states
|
window.matchMedia('(hover: none)').matches
|
||||||
const [scrollable, setScollable] = createSignal(true)
|
const isMobileUA = /android|iphone|ipad|ipod|mobile/.test(ua)
|
||||||
|
const isWindowsDesktop = /windows nt/.test(ua)
|
||||||
createEffect(() => {
|
const isMobile = isMobileUA || (hasTouchInput && hasTouchLayout && !isWindowsDesktop)
|
||||||
if (scrollable()) {
|
|
||||||
container.classList.remove('disableScroll')
|
|
||||||
} else {
|
|
||||||
container.classList.add('disableScroll')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Show when={ijs.state === 'ready'}>
|
<Show when={ijs.state === 'ready'}>
|
||||||
<StateProvider length={ijs()?.length ?? 0}>
|
<ImageStateProvider images={ijs() ?? []}>
|
||||||
<Switch fallback={<div>Error</div>}>
|
<ConfigStateProvider>
|
||||||
<Match when={isMobile}>
|
<AppContent
|
||||||
<Mobile
|
isMobile={isMobile}
|
||||||
ijs={ijs() ?? []}
|
prevText={container.dataset.prev}
|
||||||
closeText={container.dataset.close}
|
closeText={container.dataset.close}
|
||||||
loadingText={container.dataset.loading}
|
nextText={container.dataset.next}
|
||||||
setScrollable={setScollable}
|
loadingText={container.dataset.loading}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</ConfigStateProvider>
|
||||||
<Match when={!isMobile}>
|
</ImageStateProvider>
|
||||||
<Desktop
|
|
||||||
ijs={ijs() ?? []}
|
|
||||||
prevText={container.dataset.prev}
|
|
||||||
closeText={container.dataset.close}
|
|
||||||
nextText={container.dataset.next}
|
|
||||||
loadingText={container.dataset.loading}
|
|
||||||
/>
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
</StateProvider>
|
|
||||||
</Show>
|
</Show>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,17 +1,9 @@
|
|||||||
import {
|
import { For, createEffect, on, onMount, type JSX } from 'solid-js'
|
||||||
For,
|
|
||||||
createEffect,
|
|
||||||
on,
|
|
||||||
onMount,
|
|
||||||
type Accessor,
|
|
||||||
type JSX,
|
|
||||||
type Setter
|
|
||||||
} from 'solid-js'
|
|
||||||
|
|
||||||
import type { ImageJSON } from '../resources'
|
import { useImageState } from '../imageState'
|
||||||
import { useState } from '../state'
|
|
||||||
|
|
||||||
import type { MobileImage } from './layout'
|
import type { MobileImage } from './layout'
|
||||||
|
import { useMobileState } from './state'
|
||||||
|
|
||||||
function getRandom(min: number, max: number): number {
|
function getRandom(min: number, max: number): number {
|
||||||
return Math.floor(Math.random() * (max - min + 1)) + min
|
return Math.floor(Math.random() * (max - min + 1)) + min
|
||||||
@@ -31,29 +23,26 @@ function onIntersection<T extends HTMLElement>(
|
|||||||
}).observe(element)
|
}).observe(element)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Collection(props: {
|
export default function Collection(): JSX.Element {
|
||||||
children?: JSX.Element
|
|
||||||
ijs: ImageJSON[]
|
|
||||||
isAnimating: Accessor<boolean>
|
|
||||||
isOpen: Accessor<boolean>
|
|
||||||
setIsOpen: Setter<boolean>
|
|
||||||
}): JSX.Element {
|
|
||||||
// variables
|
// variables
|
||||||
// eslint-disable-next-line solid/reactivity
|
const imageState = useImageState()
|
||||||
const imgs: MobileImage[] = Array<MobileImage>(props.ijs.length)
|
const imgs: MobileImage[] = Array<MobileImage>(imageState().length)
|
||||||
|
|
||||||
// states
|
// states
|
||||||
const [state, { setIndex }] = useState()
|
const [mobile, { setIndex, setIsOpen }] = useMobileState()
|
||||||
|
|
||||||
// helper functions
|
// helper functions
|
||||||
const handleClick: (i: number) => void = (i) => {
|
const handleClick: (i: number) => void = (i) => {
|
||||||
if (props.isAnimating()) return
|
if (mobile.isAnimating()) return
|
||||||
setIndex(i)
|
setIndex(i)
|
||||||
props.setIsOpen(true)
|
setIsOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollToActive: () => void = () => {
|
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
|
// effects
|
||||||
@@ -94,11 +83,9 @@ export default function Collection(props: {
|
|||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
|
mobile.isOpen,
|
||||||
() => {
|
() => {
|
||||||
props.isOpen()
|
if (!mobile.isOpen()) scrollToActive() // scroll to active when closed
|
||||||
},
|
|
||||||
() => {
|
|
||||||
if (!props.isOpen()) scrollToActive() // scroll to active when closed
|
|
||||||
},
|
},
|
||||||
{ defer: true }
|
{ defer: true }
|
||||||
)
|
)
|
||||||
@@ -107,7 +94,7 @@ export default function Collection(props: {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div class="collection">
|
<div class="collection">
|
||||||
<For each={props.ijs}>
|
<For each={imageState().images}>
|
||||||
{(ij, i) => (
|
{(ij, i) => (
|
||||||
<img
|
<img
|
||||||
ref={imgs[i()]}
|
ref={imgs[i()]}
|
||||||
|
|||||||
@@ -1,209 +1,170 @@
|
|||||||
import { type gsap } from 'gsap'
|
import { type gsap } from 'gsap'
|
||||||
import {
|
import {
|
||||||
createEffect,
|
createEffect,
|
||||||
|
createMemo,
|
||||||
createSignal,
|
createSignal,
|
||||||
For,
|
For,
|
||||||
on,
|
on,
|
||||||
onMount,
|
onMount,
|
||||||
Show,
|
untrack,
|
||||||
type Accessor,
|
type JSX
|
||||||
type JSX,
|
|
||||||
type Setter
|
|
||||||
} from 'solid-js'
|
} from 'solid-js'
|
||||||
import { createStore } from 'solid-js/store'
|
import { createStore } from 'solid-js/store'
|
||||||
import { type Swiper } from 'swiper'
|
import { type Swiper } from 'swiper'
|
||||||
import invariant from 'tiny-invariant'
|
import invariant from 'tiny-invariant'
|
||||||
|
|
||||||
import { type ImageJSON } from '../resources'
|
import { useImageState } from '../imageState'
|
||||||
import { useState } from '../state'
|
import { loadGsap, removeDuplicates, type Vector } from '../utils'
|
||||||
import { loadGsap, type Vector } from '../utils'
|
|
||||||
|
|
||||||
import GalleryImage from './galleryImage'
|
import GalleryImage from './galleryImage'
|
||||||
import GalleryNav, { capitalizeFirstLetter } from './galleryNav'
|
import GalleryNav, { capitalizeFirstLetter } from './galleryNav'
|
||||||
|
import { closeGallery, openGallery } from './galleryTransitions'
|
||||||
function removeDuplicates<T>(arr: T[]): T[] {
|
import { getActiveImageIndexes, loadSwiper } from './galleryUtils'
|
||||||
if (arr.length < 2) return arr // optimization
|
import { useMobileState } from './state'
|
||||||
return [...new Set(arr)]
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadSwiper(): Promise<typeof Swiper> {
|
|
||||||
const s = await import('swiper')
|
|
||||||
return s.Swiper
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Gallery(props: {
|
export default function Gallery(props: {
|
||||||
children?: JSX.Element
|
children?: JSX.Element
|
||||||
ijs: ImageJSON[]
|
|
||||||
closeText: string
|
closeText: string
|
||||||
loadingText: string
|
loadingText: string
|
||||||
isAnimating: Accessor<boolean>
|
|
||||||
setIsAnimating: Setter<boolean>
|
|
||||||
isOpen: Accessor<boolean>
|
|
||||||
setIsOpen: Setter<boolean>
|
|
||||||
setScrollable: Setter<boolean>
|
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
// variables
|
|
||||||
let _gsap: typeof gsap
|
let _gsap: typeof gsap
|
||||||
let _swiper: Swiper
|
let _swiper: Swiper | undefined
|
||||||
|
let initPromise: Promise<void> | undefined
|
||||||
|
|
||||||
let curtain: HTMLDivElement | undefined
|
let curtain: HTMLDivElement | undefined
|
||||||
let gallery: HTMLDivElement | undefined
|
let gallery: HTMLDivElement | undefined
|
||||||
let galleryInner: HTMLDivElement | undefined
|
let galleryInner: HTMLDivElement | undefined
|
||||||
|
|
||||||
// eslint-disable-next-line solid/reactivity
|
const imageState = useImageState()
|
||||||
const _loadingText = capitalizeFirstLetter(props.loadingText)
|
const [mobile, { setIndex, setIsAnimating, setIsScrollLocked }] = useMobileState()
|
||||||
|
|
||||||
|
const loadingText = createMemo(() => capitalizeFirstLetter(props.loadingText))
|
||||||
|
|
||||||
// states
|
|
||||||
let lastIndex = -1
|
let lastIndex = -1
|
||||||
let mounted = false
|
let mounted = false
|
||||||
let navigateVector: Vector = 'none'
|
let navigateVector: Vector = 'none'
|
||||||
|
|
||||||
const [state, { setIndex }] = useState()
|
|
||||||
const [libLoaded, setLibLoaded] = createSignal(false)
|
const [libLoaded, setLibLoaded] = createSignal(false)
|
||||||
// eslint-disable-next-line solid/reactivity
|
const [swiperReady, setSwiperReady] = createSignal(false)
|
||||||
const [loads, setLoads] = createStore(Array<boolean>(props.ijs.length).fill(false))
|
const [loads, setLoads] = createStore(Array<boolean>(imageState().length).fill(false))
|
||||||
|
|
||||||
// helper functions
|
|
||||||
const slideUp: () => void = () => {
|
const slideUp: () => void = () => {
|
||||||
// isAnimating is prechecked in isOpen effect
|
|
||||||
if (!libLoaded() || !mounted) return
|
if (!libLoaded() || !mounted) return
|
||||||
props.setIsAnimating(true)
|
|
||||||
|
|
||||||
invariant(curtain, 'curtain is not defined')
|
invariant(curtain, 'curtain is not defined')
|
||||||
invariant(gallery, 'gallery is not defined')
|
invariant(gallery, 'gallery is not defined')
|
||||||
|
|
||||||
_gsap.to(curtain, {
|
openGallery({
|
||||||
opacity: 1,
|
gsap: _gsap,
|
||||||
duration: 1
|
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 = () => {
|
const slideDown: () => void = () => {
|
||||||
// isAnimating is prechecked in isOpen effect
|
|
||||||
props.setIsAnimating(true)
|
|
||||||
|
|
||||||
invariant(gallery, 'curtain is not defined')
|
invariant(gallery, 'curtain is not defined')
|
||||||
invariant(curtain, 'gallery is not defined')
|
invariant(curtain, 'gallery is not defined')
|
||||||
|
|
||||||
_gsap.to(gallery, {
|
closeGallery({
|
||||||
y: '100%',
|
gsap: _gsap,
|
||||||
ease: 'power3.inOut',
|
curtain,
|
||||||
duration: 1
|
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 = () => {
|
const galleryLoadImages: () => void = () => {
|
||||||
let activeImagesIndex: number[] = []
|
const currentIndex = mobile.index()
|
||||||
const _state = state()
|
|
||||||
const currentIndex = _state.index
|
setLoads(
|
||||||
const nextIndex = Math.min(currentIndex + 1, _state.length - 1)
|
removeDuplicates(
|
||||||
const prevIndex = Math.max(currentIndex - 1, 0)
|
getActiveImageIndexes(currentIndex, imageState().length, navigateVector)
|
||||||
switch (navigateVector) {
|
),
|
||||||
case 'next':
|
true
|
||||||
activeImagesIndex = [nextIndex]
|
)
|
||||||
break
|
|
||||||
case 'prev':
|
|
||||||
activeImagesIndex = [prevIndex]
|
|
||||||
break
|
|
||||||
case 'none':
|
|
||||||
activeImagesIndex = [currentIndex, nextIndex, prevIndex]
|
|
||||||
break
|
|
||||||
}
|
|
||||||
setLoads(removeDuplicates(activeImagesIndex), true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const changeSlide: (slide: number) => void = (slide) => {
|
const changeSlide: (slide: number) => void = (slide) => {
|
||||||
// we are already in the gallery, don't need to
|
if (!swiperReady() || _swiper === undefined) return
|
||||||
// check mounted or libLoaded
|
|
||||||
galleryLoadImages()
|
galleryLoadImages()
|
||||||
_swiper.slideTo(slide, 0)
|
_swiper.slideTo(slide, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// effects
|
const ensureGalleryReady: () => Promise<void> = async () => {
|
||||||
onMount(() => {
|
if (initPromise !== undefined) return await initPromise
|
||||||
window.addEventListener(
|
|
||||||
'touchstart',
|
initPromise = (async () => {
|
||||||
() => {
|
try {
|
||||||
loadGsap()
|
const [g, S] = await Promise.all([loadGsap(), loadSwiper()])
|
||||||
.then((g) => {
|
|
||||||
_gsap = g
|
_gsap = g
|
||||||
})
|
|
||||||
.catch((e) => {
|
invariant(galleryInner, 'galleryInner is not defined')
|
||||||
console.log(e)
|
_swiper = new S(galleryInner, { spaceBetween: 20 })
|
||||||
})
|
_swiper.on('slideChange', ({ realIndex }) => {
|
||||||
loadSwiper()
|
setIndex(realIndex)
|
||||||
.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)
|
|
||||||
})
|
|
||||||
setLibLoaded(true)
|
setLibLoaded(true)
|
||||||
},
|
setSwiperReady(true)
|
||||||
{ once: true, passive: 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
|
mounted = true
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => {
|
() => [swiperReady(), mobile.index()] as const,
|
||||||
state()
|
([ready, index]) => {
|
||||||
},
|
if (!ready || index < 0) return
|
||||||
() => {
|
if (index === lastIndex) return
|
||||||
const i = state().index
|
if (lastIndex === -1) navigateVector = 'none'
|
||||||
if (i === lastIndex)
|
else if (index < lastIndex) navigateVector = 'prev'
|
||||||
return // change slide only when index is changed
|
else if (index > lastIndex) navigateVector = 'next'
|
||||||
else if (lastIndex === -1)
|
else navigateVector = 'none'
|
||||||
navigateVector = 'none' // lastIndex before set
|
changeSlide(index)
|
||||||
else if (i < lastIndex)
|
lastIndex = index
|
||||||
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
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => {
|
() => mobile.isOpen(),
|
||||||
props.isOpen()
|
async (isOpen) => {
|
||||||
},
|
if (isOpen && !swiperReady()) {
|
||||||
() => {
|
await ensureGalleryReady()
|
||||||
if (props.isAnimating()) return
|
}
|
||||||
if (props.isOpen()) slideUp()
|
|
||||||
|
if (!libLoaded() || !swiperReady()) return
|
||||||
|
if (mobile.isAnimating()) return
|
||||||
|
if (isOpen) slideUp()
|
||||||
else slideDown()
|
else slideDown()
|
||||||
},
|
},
|
||||||
{ defer: true }
|
{ defer: true }
|
||||||
@@ -215,26 +176,16 @@ export default function Gallery(props: {
|
|||||||
<div ref={gallery} class="gallery">
|
<div ref={gallery} class="gallery">
|
||||||
<div ref={galleryInner} class="galleryInner">
|
<div ref={galleryInner} class="galleryInner">
|
||||||
<div class="swiper-wrapper">
|
<div class="swiper-wrapper">
|
||||||
<Show when={libLoaded()}>
|
<For each={imageState().images}>
|
||||||
<For each={props.ijs}>
|
{(ij, i) => (
|
||||||
{(ij, i) => (
|
<div class="swiper-slide">
|
||||||
<div class="swiper-slide">
|
<GalleryImage load={loads[i()]} ij={ij} loadingText={loadingText()} />
|
||||||
<GalleryImage
|
</div>
|
||||||
load={loads[i()]}
|
)}
|
||||||
ij={ij}
|
</For>
|
||||||
loadingText={_loadingText}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<GalleryNav
|
<GalleryNav closeText={props.closeText} />
|
||||||
closeText={props.closeText}
|
|
||||||
isAnimating={props.isAnimating}
|
|
||||||
setIsOpen={props.setIsOpen}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div ref={curtain} class="curtain" />
|
<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 invariant from 'tiny-invariant'
|
||||||
|
|
||||||
import type { ImageJSON } from '../resources'
|
import type { ImageJSON } from '../resources'
|
||||||
import { useState } from '../state'
|
|
||||||
import { loadGsap } from '../utils'
|
import { loadGsap } from '../utils'
|
||||||
|
|
||||||
|
import { useMobileState } from './state'
|
||||||
|
|
||||||
export default function GalleryImage(props: {
|
export default function GalleryImage(props: {
|
||||||
children?: JSX.Element
|
children?: JSX.Element
|
||||||
load: boolean
|
load: boolean
|
||||||
@@ -14,40 +16,83 @@ export default function GalleryImage(props: {
|
|||||||
let img: HTMLImageElement | undefined
|
let img: HTMLImageElement | undefined
|
||||||
let loadingDiv: HTMLDivElement | 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(() => {
|
onMount(() => {
|
||||||
loadGsap()
|
gsapPromise = loadGsap()
|
||||||
.then((g) => {
|
.then((g) => {
|
||||||
_gsap = g
|
_gsap = g
|
||||||
|
return g
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.log(e)
|
console.log(e)
|
||||||
|
throw e
|
||||||
})
|
})
|
||||||
|
|
||||||
img?.addEventListener(
|
img?.addEventListener(
|
||||||
'load',
|
'load',
|
||||||
() => {
|
() => {
|
||||||
invariant(img, 'ref must be defined')
|
void revealImage()
|
||||||
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' })
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{ once: true, passive: true }
|
{ 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div class="slideContainer">
|
<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 { expand } from '../utils'
|
||||||
|
|
||||||
|
import { useMobileState } from './state'
|
||||||
|
|
||||||
export function capitalizeFirstLetter(str: string): string {
|
export function capitalizeFirstLetter(str: string): string {
|
||||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||||
}
|
}
|
||||||
@@ -10,17 +12,16 @@ export function capitalizeFirstLetter(str: string): string {
|
|||||||
export default function GalleryNav(props: {
|
export default function GalleryNav(props: {
|
||||||
children?: JSX.Element
|
children?: JSX.Element
|
||||||
closeText: string
|
closeText: string
|
||||||
isAnimating: Accessor<boolean>
|
|
||||||
setIsOpen: Setter<boolean>
|
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
// states
|
// states
|
||||||
const [state] = useState()
|
const imageState = useImageState()
|
||||||
const indexValue = createMemo(() => expand(state().index + 1))
|
const [mobile, { setIsOpen }] = useMobileState()
|
||||||
const indexLength = createMemo(() => expand(state().length))
|
const indexValue = createMemo(() => expand(mobile.index() + 1))
|
||||||
|
const indexLength = createMemo(() => expand(imageState().length))
|
||||||
|
|
||||||
const onClick: () => void = () => {
|
const onClick: () => void = () => {
|
||||||
if (props.isAnimating()) return
|
if (mobile.isAnimating()) return
|
||||||
props.setIsOpen(false)
|
setIsOpen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -37,7 +38,14 @@ export default function GalleryNav(props: {
|
|||||||
<span class="num">{indexLength()[2]}</span>
|
<span class="num">{indexLength()[2]}</span>
|
||||||
<span class="num">{indexLength()[3]}</span>
|
<span class="num">{indexLength()[3]}</span>
|
||||||
</div>
|
</div>
|
||||||
<div onClick={onClick} onKeyDown={onClick}>
|
<div
|
||||||
|
class="navClose"
|
||||||
|
onClick={onClick}
|
||||||
|
onTouchEnd={onClick}
|
||||||
|
onKeyDown={onClick}
|
||||||
|
role="button"
|
||||||
|
tabIndex="0"
|
||||||
|
>
|
||||||
{capitalizeFirstLetter(props.closeText)}
|
{capitalizeFirstLetter(props.closeText)}
|
||||||
</div>
|
</div>
|
||||||
</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 Collection from './collection'
|
||||||
import Gallery from './gallery'
|
import Gallery from './gallery'
|
||||||
|
import { useMobileState } from './state'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* interfaces
|
* interfaces
|
||||||
@@ -18,34 +19,33 @@ export interface MobileImage extends HTMLImageElement {
|
|||||||
|
|
||||||
export default function Mobile(props: {
|
export default function Mobile(props: {
|
||||||
children?: JSX.Element
|
children?: JSX.Element
|
||||||
ijs: ImageJSON[]
|
|
||||||
closeText: string
|
closeText: string
|
||||||
loadingText: string
|
loadingText: string
|
||||||
setScrollable: Setter<boolean>
|
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
// states
|
const imageState = useImageState()
|
||||||
const [isOpen, setIsOpen] = createSignal(false)
|
const [mobile] = useMobileState()
|
||||||
const [isAnimating, setIsAnimating] = createSignal(false)
|
|
||||||
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Show when={props.ijs.length > 0}>
|
<Show when={imageState().length > 0}>
|
||||||
<Collection
|
<Collection />
|
||||||
ijs={props.ijs}
|
<Gallery closeText={props.closeText} loadingText={props.loadingText} />
|
||||||
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>
|
</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
1
bundled/js/1zh_gO.js
Normal file
1
bundled/js/1zh_gO.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
File diff suppressed because one or more lines are too long
1
bundled/js/BfssZJ.js
Normal file
1
bundled/js/BfssZJ.js
Normal file
File diff suppressed because one or more lines are too long
1
bundled/js/Br6LVr.js
Normal file
1
bundled/js/Br6LVr.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{B as e,D as t,E as n,M as r,n as i,o as a,r as o,u as s}from"./BfssZJ.js";var c=t();function l(e){let t=i(),[s,l]=r(-1),[u,d]=r(!1),[f,p]=r(!1),[m,h]=r(!1),g=e=>{let n=t().length;n<=0||l(t=>e===1?a(t,n):o(t,n))};return n(c.Provider,{value:[{index:s,isOpen:u,isAnimating:f,isScrollLocked:m},{setIndex:l,incIndex:()=>{g(1)},decIndex:()=>{g(-1)},setIsOpen:d,setIsAnimating:p,setIsScrollLocked:h}],get children(){return e.children}})}function u(){let t=e(c);return s(t,`undefined mobile context`),t}export{u as n,l as t};
|
||||||
1
bundled/js/CYDxmA.js
Normal file
1
bundled/js/CYDxmA.js
Normal file
File diff suppressed because one or more lines are too long
1
bundled/js/DIvfwZ.js
Normal file
1
bundled/js/DIvfwZ.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/Dh98wV.js
Normal file
1
bundled/js/Dh98wV.js
Normal file
File diff suppressed because one or more lines are too long
1
bundled/js/WcOp_5.js
Normal file
1
bundled/js/WcOp_5.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{B as e,D as t,E as n,M as r,a as i,k as a,n as o,o as s,r as c,u as l}from"./BfssZJ.js";var u=[{threshold:20,trailLength:20},{threshold:40,trailLength:10},{threshold:80,trailLength:5},{threshold:140,trailLength:5},{threshold:200,trailLength:5}],d=t();function f(){let e=i();return e<0||e>=u.length?2:e}function p(e){let[t,i]=r(f()),o=a(()=>{let e=u[t()];return{thresholdIndex:t(),threshold:e.threshold,trailLength:e.trailLength}}),s=e=>{let n=t()+e;n<0||n>=u.length||(sessionStorage.setItem(`thresholdsIndex`,n.toString()),i(n))};return n(d.Provider,{value:[o,{incThreshold:()=>{s(1)},decThreshold:()=>{s(-1)}}],get children(){return e.children}})}function m(){let t=e(d);return l(t,`undefined config context`),t}var h=t();function g(e){let t=o(),[i,a]=r(-1),[l,u]=r([]),[d,f]=r(``),[p,m]=r(!1),[g,_]=r(!1),[v,y]=r(!1),[b,x]=r(`none`),S=e=>{let n=t().length;n<=0||a(t=>e===1?s(t,n):c(t,n))};return n(h.Provider,{value:[{index:i,cordHist:l,hoverText:d,isOpen:p,isAnimating:g,isLoading:v,navVector:b},{setIndex:a,incIndex:()=>{S(1)},decIndex:()=>{S(-1)},setCordHist:u,setHoverText:f,setIsOpen:m,setIsAnimating:_,setIsLoading:y,setNavVector:x}],get children(){return e.children}})}function _(){let t=e(h);return l(t,`undefined desktop context`),t}export{m as i,_ as n,p as r,g as t};
|
||||||
File diff suppressed because one or more lines are too long
10
flake.lock
generated
10
flake.lock
generated
@@ -2,12 +2,12 @@
|
|||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1762596750,
|
"lastModified": 1769461804,
|
||||||
"narHash": "sha256-rXXuz51Bq7DHBlfIjN7jO8Bu3du5TV+3DSADBX7/9YQ=",
|
"narHash": "sha256-msG8SU5WsBUfVVa/9RPLaymvi5bI8edTavbIq3vRlhI=",
|
||||||
"rev": "b6a8526db03f735b89dd5ff348f53f752e7ddc8e",
|
"rev": "bfc1b8a4574108ceef22f02bafcf6611380c100d",
|
||||||
"revCount": 891611,
|
"revCount": 935279,
|
||||||
"type": "tarball",
|
"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": {
|
"original": {
|
||||||
"type": "tarball",
|
"type": "tarball",
|
||||||
|
|||||||
29
package.json
29
package.json
@@ -39,30 +39,31 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/Sped0n/bridget#readme",
|
"homepage": "https://github.com/Sped0n/bridget#readme",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.4",
|
||||||
"@types/node": "^24.10.0",
|
"@types/node": "^25.5.2",
|
||||||
"@typescript-eslint/parser": "^8.46.4",
|
"@typescript-eslint/parser": "^8.58.0",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.2",
|
||||||
"eslint-config-love": "^133.0.0",
|
"eslint-config-love": "^151.0.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-import-resolver-typescript": "^4.4.4",
|
"eslint-import-resolver-typescript": "^4.4.4",
|
||||||
"eslint-plugin-import": "^2.32.0",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
"eslint-plugin-prettier": "^5.5.4",
|
"eslint-plugin-prettier": "^5.5.5",
|
||||||
"eslint-plugin-solid": "^0.14.5",
|
"eslint-plugin-solid": "^0.14.5",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.8.1",
|
||||||
"prettier-plugin-go-template": "^0.0.15",
|
"prettier-plugin-go-template": "^0.0.15",
|
||||||
"prettier-plugin-organize-imports": "^4.3.0",
|
"prettier-plugin-organize-imports": "^4.3.0",
|
||||||
"sass-embedded": "^1.93.3",
|
"sass-embedded": "^1.99.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.46.4",
|
"typescript-eslint": "^8.58.1",
|
||||||
"vite": "^7.2.2",
|
"vite": "^8.0.8",
|
||||||
"vite-plugin-solid": "^2.11.10"
|
"vite-plugin-solid": "^2.11.12",
|
||||||
|
"vitefu": "^1.1.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"gsap": "^3.13.0",
|
"gsap": "^3.15.0",
|
||||||
"solid-js": "^1.9.10",
|
"solid-js": "^1.9.12",
|
||||||
"swiper": "^12.0.3",
|
"swiper": "^12.1.3",
|
||||||
"tiny-invariant": "^1.3.3"
|
"tiny-invariant": "^1.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2283
pnpm-lock.yaml
generated
2283
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
node_modules_generated_dir="./node_modules/exampleSite/resources/_gen"
|
node_modules_generated_dir="./node_modules/exampleSite/resources/_gen/images"
|
||||||
project_generated_dir="./exampleSite/resources/_gen"
|
project_generated_dir="./exampleSite/resources/_gen/images"
|
||||||
|
|
||||||
copy_generated_assets_to_project() {
|
copy_generated_assets_to_project() {
|
||||||
if [ -d "${node_modules_generated_dir}" ]; then
|
if [ -d "${node_modules_generated_dir}" ]; then
|
||||||
@@ -16,7 +16,8 @@ copy_generated_assets_to_project() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
run_site_build() {
|
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() {
|
copy_generated_assets_to_node_modules() {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export default defineConfig({
|
|||||||
plugins: [solidPlugin()],
|
plugins: [solidPlugin()],
|
||||||
build: {
|
build: {
|
||||||
outDir: './bundled',
|
outDir: './bundled',
|
||||||
|
cssMinify: 'esbuild',
|
||||||
watch: process.env.DISABLE_WATCH
|
watch: process.env.DISABLE_WATCH
|
||||||
? null
|
? null
|
||||||
: {
|
: {
|
||||||
@@ -19,8 +20,7 @@ export default defineConfig({
|
|||||||
format: 'es',
|
format: 'es',
|
||||||
entryFileNames: 'js/[name].js',
|
entryFileNames: 'js/[name].js',
|
||||||
chunkFileNames: 'js/[hash:6].js',
|
chunkFileNames: 'js/[hash:6].js',
|
||||||
assetFileNames: '[ext]/[name].[ext]',
|
assetFileNames: '[ext]/[name].[ext]'
|
||||||
compact: true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user