mirror of
https://github.com/Sped0n/bridget.git
synced 2026-04-16 11:09:30 -07:00
Compare commits
135 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
268159e7d2 | ||
|
|
2e7131a5a5 | ||
|
|
1de3926c49 | ||
|
|
4b1f529589 | ||
|
|
8b3b5cd77a | ||
|
|
3a0025ebd1 | ||
|
|
283f386371 | ||
|
|
4c91cd269e | ||
|
|
304abf3b65 | ||
|
|
99a2866d4a |
97
.github/workflows/build.yml
vendored
97
.github/workflows/build.yml
vendored
@@ -12,93 +12,72 @@ permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
filter:
|
||||
runs-on: ubuntu-latest
|
||||
name: Filter
|
||||
outputs:
|
||||
any_changed: ${{ steps.changed-files-specific.outputs.any_changed }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get changed files in scope
|
||||
id: changed-files-specific
|
||||
uses: tj-actions/changed-files@v46
|
||||
with:
|
||||
files: |
|
||||
package.json
|
||||
assets/**
|
||||
|
||||
build:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
name: Build (Hugo ${{ matrix.hugo-version }})
|
||||
needs: [filter]
|
||||
if: |
|
||||
github.ref == 'refs/heads/main' &&
|
||||
github.event.repository.fork == false
|
||||
name: Build (Hugo ${{ matrix.hugo-label }})
|
||||
if: github.event.repository.fork == false
|
||||
strategy:
|
||||
matrix:
|
||||
hugo-version: ['latest', '0.114.0']
|
||||
include:
|
||||
- hugo-version: latest
|
||||
hugo-label: Latest
|
||||
- hugo-version: '0.114.0'
|
||||
hugo-label: 'v0.114.0'
|
||||
steps:
|
||||
- name: Set current date as env variable
|
||||
run: |
|
||||
echo "builddate=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
id: version
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
|
||||
- name: Setup Hugo
|
||||
uses: peaceiris/actions-hugo@v2.6.0
|
||||
- name: Setup Mise
|
||||
uses: jdx/mise-action@v3
|
||||
with:
|
||||
hugo-version: ${{ matrix.hugo-version }}
|
||||
extended: true
|
||||
install_args: node@latest pnpm@10 hugo-extended@${{ matrix.hugo-version }}
|
||||
tool_versions: |
|
||||
node latest
|
||||
pnpm 10
|
||||
hugo-extended ${{ matrix.hugo-version }}
|
||||
cache: true
|
||||
|
||||
- name: Setup Dart Sass
|
||||
run: sudo snap install dart-sass
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- name: Get pnpm store directory
|
||||
- name: Get pnpm store path
|
||||
id: pnpm-cache
|
||||
run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
run: 'echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT'
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('./pnpm-lock.yaml') }}
|
||||
key: pnpm-store-${{ hashFiles('./pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
pnpm-store-
|
||||
|
||||
- name: Setup hugo cache
|
||||
uses: actions/cache@v4
|
||||
- name: Setup Hugo cache
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ./exampleSite/resources
|
||||
key: ${{ runner.os }}-hugo-${{ matrix.hugo-version }}-${{ hashFiles('./exampleSite') }}
|
||||
path: exampleSite/resources/_gen
|
||||
key: hugo-${{ matrix.hugo-version }}-${{ hashFiles('./exampleSite/**/*.jpg') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-hugo-${{ matrix.hugo-version }}-
|
||||
hugo-${{ matrix.hugo-version }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
- name: Install project dependencies
|
||||
run: 'pnpm install'
|
||||
|
||||
- name: Pre-build cleanup
|
||||
if: >
|
||||
matrix.hugo-version == 'latest' &&
|
||||
(github.event_name == 'push' || github.event.pull_request.merged == true)
|
||||
run: 'rm -rf bundled'
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
pnpm run vite:build
|
||||
hugo --logLevel info --source=exampleSite --gc --minify
|
||||
run: 'pnpm run build'
|
||||
|
||||
- name: Push artifacts
|
||||
if: >
|
||||
matrix.hugo-version == 'latest' &&
|
||||
(github.event_name == 'push' || github.event.pull_request.merged == true) &&
|
||||
needs.filter.outputs.any_changed == 'true'
|
||||
uses: stefanzweifel/git-auto-commit-action@v5
|
||||
(github.event_name == 'push' || github.event.pull_request.merged == true)
|
||||
uses: stefanzweifel/git-auto-commit-action@v7
|
||||
with:
|
||||
file_pattern: 'bundled/**/*.js bundled/**/*.css'
|
||||
commit_message: 'ci: update bundled artifacts [skip ci]'
|
||||
|
||||
8
.github/workflows/codeql.yml
vendored
8
.github/workflows/codeql.yml
vendored
@@ -46,11 +46,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
@@ -77,6 +77,6 @@ jobs:
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: '/language:${{matrix.language}}'
|
||||
|
||||
57
.github/workflows/eslint.yml
vendored
57
.github/workflows/eslint.yml
vendored
@@ -1,57 +0,0 @@
|
||||
name: 'ESLint && Prettier'
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
name: Lint
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.PAT || github.token }}
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- 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@v3
|
||||
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]'
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,6 +1,6 @@
|
||||
# Hugo default output directory
|
||||
public/
|
||||
/exampleSite/resources/
|
||||
exampleSite/resources/
|
||||
|
||||
node_modules/
|
||||
build/
|
||||
@@ -25,3 +25,6 @@ jsconfig.json
|
||||
|
||||
# css map
|
||||
*.css.map
|
||||
|
||||
# dummmy file
|
||||
bundled/js/critical.js
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
node_modules
|
||||
static
|
||||
exmapleSite
|
||||
node_modules/
|
||||
static/
|
||||
exmapleSite/
|
||||
single.json
|
||||
pnpm-lock.yaml
|
||||
bundled/
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
 
|
||||
|
||||
Bridget is a minimal [Hugo](https://gohugo.io) theme for photographers/visual artists, powered by [Solid.js](https://www.solidjs.com). Based on the https://github.com/tylermcrobert/bridget-pictures-www.
|
||||
Bridget is a minimal [Hugo](https://gohugo.io) theme for photographers/visual artists, based on https://github.com/tylermcrobert/bridget-pictures-www.
|
||||
|
||||
Here is a [live demo](https://bridget-demo.sped0n.com).
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
@font-face {
|
||||
font-family: 'Geist';
|
||||
src:
|
||||
url('{{- "lib/fonts/GeistVF.woff2" | absURL -}}')
|
||||
url(/* @vite-ignore */'{{- "lib/fonts/GeistVF.woff2" | absURL -}}')
|
||||
format('woff2 supports variations'),
|
||||
url('{{- "lib/fonts/GeistVF.woff2" | absURL -}}') format('woff2-variations');
|
||||
url(/* @vite-ignore */'{{- "lib/fonts/GeistVF.woff2" | absURL -}}')
|
||||
format('woff2-variations');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -11,7 +12,7 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'FW';
|
||||
src: url('/lib/fonts/fw.woff2') format('woff2');
|
||||
src: url(/* @vite-ignore */'{{- "lib/fonts/fw.woff2" | absURL -}}') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
position: fixed;
|
||||
top: var(--nav-height);
|
||||
z-index: var(--z-nav-gallery);
|
||||
overflow: hidden;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -25,8 +26,9 @@
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: calc(var(--window-height) - 2 * var(--nav-height));
|
||||
max-width: 100%;
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
@@ -49,6 +51,21 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.navClose {
|
||||
cursor: pointer;
|
||||
z-index: calc(var(--z-nav-gallery) + 1);
|
||||
|
||||
min-width: 25%;
|
||||
height: calc(var(--nav-height) * 2.5);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
margin-right: calc(var(--space-standard) * -1);
|
||||
padding-right: var(--space-standard);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
91
assets/ts/configState.tsx
Normal file
91
assets/ts/configState.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import {
|
||||
createContext,
|
||||
createMemo,
|
||||
createSignal,
|
||||
useContext,
|
||||
type Accessor,
|
||||
type JSX
|
||||
} from 'solid-js'
|
||||
import invariant from 'tiny-invariant'
|
||||
|
||||
import { getThresholdSessionIndex } from './utils'
|
||||
|
||||
export interface ThresholdRelated {
|
||||
threshold: number
|
||||
trailLength: number
|
||||
}
|
||||
|
||||
export interface ConfigState {
|
||||
thresholdIndex: number
|
||||
threshold: number
|
||||
trailLength: number
|
||||
}
|
||||
|
||||
export type ConfigStateContextType = readonly [
|
||||
Accessor<ConfigState>,
|
||||
{
|
||||
readonly incThreshold: () => void
|
||||
readonly decThreshold: () => void
|
||||
}
|
||||
]
|
||||
|
||||
const thresholds: ThresholdRelated[] = [
|
||||
{ threshold: 20, trailLength: 20 },
|
||||
{ threshold: 40, trailLength: 10 },
|
||||
{ threshold: 80, trailLength: 5 },
|
||||
{ threshold: 140, trailLength: 5 },
|
||||
{ threshold: 200, trailLength: 5 }
|
||||
]
|
||||
|
||||
const ConfigStateContext = createContext<ConfigStateContextType>()
|
||||
|
||||
function getSafeThresholdIndex(): number {
|
||||
const index = getThresholdSessionIndex()
|
||||
if (index < 0 || index >= thresholds.length) return 2
|
||||
return index
|
||||
}
|
||||
|
||||
export function ConfigStateProvider(props: { children?: JSX.Element }): JSX.Element {
|
||||
const [thresholdIndex, setThresholdIndex] = createSignal(getSafeThresholdIndex())
|
||||
|
||||
const state = createMemo<ConfigState>(() => {
|
||||
const current = thresholds[thresholdIndex()]
|
||||
|
||||
return {
|
||||
thresholdIndex: thresholdIndex(),
|
||||
threshold: current.threshold,
|
||||
trailLength: current.trailLength
|
||||
}
|
||||
})
|
||||
|
||||
const updateThreshold = (stride: number): void => {
|
||||
const nextIndex = thresholdIndex() + stride
|
||||
if (nextIndex < 0 || nextIndex >= thresholds.length) return
|
||||
sessionStorage.setItem('thresholdsIndex', nextIndex.toString())
|
||||
setThresholdIndex(nextIndex)
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfigStateContext.Provider
|
||||
value={[
|
||||
state,
|
||||
{
|
||||
incThreshold: () => {
|
||||
updateThreshold(1)
|
||||
},
|
||||
decThreshold: () => {
|
||||
updateThreshold(-1)
|
||||
}
|
||||
}
|
||||
]}
|
||||
>
|
||||
{props.children}
|
||||
</ConfigStateContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useConfigState(): ConfigStateContextType {
|
||||
const context = useContext(ConfigStateContext)
|
||||
invariant(context, 'undefined config context')
|
||||
return context
|
||||
}
|
||||
2
assets/ts/critical.ts
Normal file
2
assets/ts/critical.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
// this is a dummy file to trick vite to generate a critical.css file
|
||||
import '../scss/critical.scss'
|
||||
@@ -4,7 +4,6 @@ export default function CustomCursor(props: {
|
||||
children?: JSX.Element
|
||||
active: Accessor<boolean>
|
||||
cursorText: Accessor<string>
|
||||
isOpen: Accessor<boolean>
|
||||
}): JSX.Element {
|
||||
// types
|
||||
interface XY {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Show, createMemo, createSignal, type JSX } from 'solid-js'
|
||||
import { Show, createMemo, type JSX } from 'solid-js'
|
||||
|
||||
import type { ImageJSON } from '../resources'
|
||||
import type { Vector } from '../utils'
|
||||
import { useImageState } from '../imageState'
|
||||
|
||||
import CustomCursor from './customCursor'
|
||||
import Nav from './nav'
|
||||
import Stage from './stage'
|
||||
import StageNav from './stageNav'
|
||||
import { useDesktopState } from './state'
|
||||
|
||||
/**
|
||||
* interfaces and types
|
||||
@@ -23,65 +23,36 @@ export interface DesktopImage extends HTMLImageElement {
|
||||
}
|
||||
}
|
||||
|
||||
export interface HistoryItem {
|
||||
i: number
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
/**
|
||||
* components
|
||||
*/
|
||||
|
||||
export default function Desktop(props: {
|
||||
children?: JSX.Element
|
||||
ijs: ImageJSON[]
|
||||
prevText: string
|
||||
closeText: string
|
||||
nextText: string
|
||||
loadingText: string
|
||||
}): JSX.Element {
|
||||
const [cordHist, setCordHist] = createSignal<HistoryItem[]>([])
|
||||
const [isLoading, setIsLoading] = createSignal(false)
|
||||
const [isOpen, setIsOpen] = createSignal(false)
|
||||
const [isAnimating, setIsAnimating] = createSignal(false)
|
||||
const [hoverText, setHoverText] = createSignal('')
|
||||
const [navVector, setNavVector] = createSignal<Vector>('none')
|
||||
const imageState = useImageState()
|
||||
const [desktop] = useDesktopState()
|
||||
|
||||
const active = createMemo(() => isOpen() && !isAnimating())
|
||||
const cursorText = createMemo(() => (isLoading() ? props.loadingText : hoverText()))
|
||||
const active = createMemo(() => desktop.isOpen() && !desktop.isAnimating())
|
||||
const cursorText = createMemo(() =>
|
||||
desktop.isLoading() ? props.loadingText : desktop.hoverText()
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Nav />
|
||||
<Show when={props.ijs.length > 0}>
|
||||
<Stage
|
||||
ijs={props.ijs}
|
||||
setIsLoading={setIsLoading}
|
||||
isOpen={isOpen}
|
||||
setIsOpen={setIsOpen}
|
||||
isAnimating={isAnimating}
|
||||
setIsAnimating={setIsAnimating}
|
||||
cordHist={cordHist}
|
||||
setCordHist={setCordHist}
|
||||
navVector={navVector}
|
||||
setNavVector={setNavVector}
|
||||
/>
|
||||
<Show when={isOpen()}>
|
||||
<CustomCursor cursorText={cursorText} active={active} isOpen={isOpen} />
|
||||
<Show when={imageState().length > 0}>
|
||||
<Stage />
|
||||
<Show when={desktop.isOpen()}>
|
||||
<CustomCursor cursorText={cursorText} active={active} />
|
||||
<StageNav
|
||||
prevText={props.prevText}
|
||||
closeText={props.closeText}
|
||||
nextText={props.nextText}
|
||||
loadingText={props.loadingText}
|
||||
active={active}
|
||||
isAnimating={isAnimating}
|
||||
setCordHist={setCordHist}
|
||||
isOpen={isOpen}
|
||||
setIsOpen={setIsOpen}
|
||||
setHoverText={setHoverText}
|
||||
navVector={navVector}
|
||||
setNavVector={setNavVector}
|
||||
/>
|
||||
</Show>
|
||||
</Show>
|
||||
|
||||
@@ -1,66 +1,68 @@
|
||||
import { createEffect } from 'solid-js'
|
||||
import { createEffect, onCleanup, onMount } from 'solid-js'
|
||||
|
||||
import { useState } from '../state'
|
||||
import { useConfigState } from '../configState'
|
||||
import { useImageState } from '../imageState'
|
||||
import { expand } from '../utils'
|
||||
|
||||
/**
|
||||
* constants
|
||||
*/
|
||||
|
||||
// threshold div
|
||||
const thresholdDiv = document.getElementsByClassName('threshold')[0] as HTMLDivElement
|
||||
// threshold nums span
|
||||
const thresholdDispNums = Array.from(
|
||||
thresholdDiv.getElementsByClassName('num')
|
||||
) as HTMLSpanElement[]
|
||||
// threshold buttons
|
||||
const decButton = thresholdDiv
|
||||
.getElementsByClassName('dec')
|
||||
.item(0) as HTMLButtonElement
|
||||
const incButton = thresholdDiv
|
||||
.getElementsByClassName('inc')
|
||||
.item(0) as HTMLButtonElement
|
||||
// index div
|
||||
const indexDiv = document.getElementsByClassName('index').item(0) as HTMLDivElement
|
||||
// index nums span
|
||||
const indexDispNums = Array.from(
|
||||
indexDiv.getElementsByClassName('num')
|
||||
) as HTMLSpanElement[]
|
||||
|
||||
/**
|
||||
* helper functions
|
||||
*/
|
||||
|
||||
function updateThresholdText(thresholdValue: string): void {
|
||||
thresholdDispNums.forEach((e: HTMLSpanElement, i: number) => {
|
||||
e.innerText = thresholdValue[i]
|
||||
})
|
||||
}
|
||||
|
||||
function updateIndexText(indexValue: string, indexLength: string): void {
|
||||
indexDispNums.forEach((e: HTMLSpanElement, i: number) => {
|
||||
if (i < 4) {
|
||||
e.innerText = indexValue[i]
|
||||
} else {
|
||||
e.innerText = indexLength[i - 4]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Nav component
|
||||
*/
|
||||
import { useDesktopState } from './state'
|
||||
|
||||
export default function Nav(): null {
|
||||
const [state, { incThreshold, decThreshold }] = useState()
|
||||
let thresholdNums: HTMLSpanElement[] = []
|
||||
let indexNums: HTMLSpanElement[] = []
|
||||
let decButton: HTMLButtonElement | undefined
|
||||
let incButton: HTMLButtonElement | undefined
|
||||
let controller: AbortController | undefined
|
||||
|
||||
createEffect(() => {
|
||||
updateIndexText(expand(state().index + 1), expand(state().length))
|
||||
updateThresholdText(expand(state().threshold))
|
||||
const imageState = useImageState()
|
||||
const [config, { incThreshold, decThreshold }] = useConfigState()
|
||||
const [desktop] = useDesktopState()
|
||||
|
||||
const updateThresholdText = (thresholdValue: string): void => {
|
||||
thresholdNums.forEach((element, i) => {
|
||||
element.innerText = thresholdValue[i]
|
||||
})
|
||||
}
|
||||
|
||||
const updateIndexText = (indexValue: string, indexLength: string): void => {
|
||||
indexNums.forEach((element, i) => {
|
||||
if (i < 4) {
|
||||
element.innerText = indexValue[i]
|
||||
} else {
|
||||
element.innerText = indexLength[i - 4]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const thresholdDiv = document.getElementsByClassName(
|
||||
'threshold'
|
||||
)[0] as HTMLDivElement
|
||||
const indexDiv = document.getElementsByClassName('index').item(0) as HTMLDivElement
|
||||
|
||||
thresholdNums = Array.from(
|
||||
thresholdDiv.getElementsByClassName('num')
|
||||
) as HTMLSpanElement[]
|
||||
indexNums = Array.from(indexDiv.getElementsByClassName('num')) as HTMLSpanElement[]
|
||||
decButton = thresholdDiv.getElementsByClassName('dec').item(0) as HTMLButtonElement
|
||||
incButton = thresholdDiv.getElementsByClassName('inc').item(0) as HTMLButtonElement
|
||||
|
||||
controller = new AbortController()
|
||||
const signal = controller.signal
|
||||
|
||||
decButton.addEventListener('click', decThreshold, { signal })
|
||||
incButton.addEventListener('click', incThreshold, { signal })
|
||||
})
|
||||
|
||||
decButton.onclick = decThreshold
|
||||
incButton.onclick = incThreshold
|
||||
createEffect(() => {
|
||||
if (thresholdNums.length === 0 || indexNums.length === 0) return
|
||||
|
||||
updateIndexText(expand(desktop.index() + 1), expand(imageState().length))
|
||||
updateThresholdText(expand(config().threshold))
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
controller?.abort()
|
||||
})
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,409 +1,167 @@
|
||||
import { type gsap } from 'gsap'
|
||||
import {
|
||||
For,
|
||||
createEffect,
|
||||
on,
|
||||
onMount,
|
||||
type Accessor,
|
||||
type JSX,
|
||||
type Setter
|
||||
} from 'solid-js'
|
||||
import { For, createEffect, on, onMount, type JSX } from 'solid-js'
|
||||
|
||||
import type { ImageJSON } from '../resources'
|
||||
import { useState, type State } from '../state'
|
||||
import { decrement, increment, loadGsap, type Vector } from '../utils'
|
||||
import { useConfigState } from '../configState'
|
||||
import { useImageState } from '../imageState'
|
||||
import { increment, loadGsap } from '../utils'
|
||||
|
||||
import type { DesktopImage, HistoryItem } from './layout'
|
||||
import type { DesktopImage } from './layout'
|
||||
import { expandStage, minimizeStage, syncStagePosition } from './stageAnimations'
|
||||
import { onMutation } from './stageUtils'
|
||||
import { useDesktopState } from './state'
|
||||
|
||||
/**
|
||||
* helper functions
|
||||
*/
|
||||
|
||||
function getTrailElsIndex(cordHistValue: HistoryItem[]): number[] {
|
||||
return cordHistValue.map((el) => el.i)
|
||||
}
|
||||
|
||||
function getTrailCurrentElsIndex(
|
||||
cordHistValue: HistoryItem[],
|
||||
stateValue: State
|
||||
): number[] {
|
||||
return getTrailElsIndex(cordHistValue).slice(-stateValue.trailLength)
|
||||
}
|
||||
|
||||
function getTrailInactiveElsIndex(
|
||||
cordHistValue: HistoryItem[],
|
||||
stateValue: State
|
||||
): number[] {
|
||||
return getTrailCurrentElsIndex(cordHistValue, stateValue).slice(0, -1)
|
||||
}
|
||||
|
||||
function getCurrentElIndex(cordHistValue: HistoryItem[]): number {
|
||||
return getTrailElsIndex(cordHistValue).slice(-1)[0]
|
||||
}
|
||||
|
||||
function getPrevElIndex(cordHistValue: HistoryItem[], stateValue: State): number {
|
||||
return decrement(cordHistValue.slice(-1)[0].i, stateValue.length)
|
||||
}
|
||||
|
||||
function getNextElIndex(cordHistValue: HistoryItem[], stateValue: State): number {
|
||||
return increment(cordHistValue.slice(-1)[0].i, stateValue.length)
|
||||
}
|
||||
|
||||
function getImagesFromIndexes(imgs: DesktopImage[], indexes: number[]): DesktopImage[] {
|
||||
return indexes.map((i) => imgs[i])
|
||||
}
|
||||
|
||||
function hires(imgs: DesktopImage[]): void {
|
||||
imgs.forEach((img) => {
|
||||
if (img.src === img.dataset.hiUrl) return
|
||||
img.src = img.dataset.hiUrl
|
||||
img.height = parseInt(img.dataset.hiImgH)
|
||||
img.width = parseInt(img.dataset.hiImgW)
|
||||
})
|
||||
}
|
||||
|
||||
function lores(imgs: DesktopImage[]): void {
|
||||
imgs.forEach((img) => {
|
||||
if (img.src === img.dataset.loUrl) return
|
||||
img.src = img.dataset.loUrl
|
||||
img.height = parseInt(img.dataset.loImgH)
|
||||
img.width = parseInt(img.dataset.loImgW)
|
||||
})
|
||||
}
|
||||
|
||||
function onMutation<T extends HTMLElement>(
|
||||
element: T,
|
||||
trigger: (arg0: MutationRecord) => boolean,
|
||||
observeOptions: MutationObserverInit = { attributes: true }
|
||||
): void {
|
||||
new MutationObserver((mutations, observer) => {
|
||||
for (const mutation of mutations) {
|
||||
if (trigger(mutation)) {
|
||||
observer.disconnect()
|
||||
break
|
||||
}
|
||||
}
|
||||
}).observe(element, observeOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Stage component
|
||||
*/
|
||||
|
||||
export default function Stage(props: {
|
||||
ijs: ImageJSON[]
|
||||
setIsLoading: Setter<boolean>
|
||||
isOpen: Accessor<boolean>
|
||||
setIsOpen: Setter<boolean>
|
||||
isAnimating: Accessor<boolean>
|
||||
setIsAnimating: Setter<boolean>
|
||||
cordHist: Accessor<HistoryItem[]>
|
||||
setCordHist: Setter<HistoryItem[]>
|
||||
navVector: Accessor<Vector>
|
||||
setNavVector: Setter<Vector>
|
||||
}): JSX.Element {
|
||||
// variables
|
||||
export default function Stage(): JSX.Element {
|
||||
let _gsap: typeof gsap
|
||||
let gsapPromise: Promise<void> | undefined
|
||||
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
const imgs: DesktopImage[] = Array<DesktopImage>(props.ijs.length)
|
||||
const imageState = useImageState()
|
||||
const [config] = useConfigState()
|
||||
const [
|
||||
desktop,
|
||||
{ setIndex, setCordHist, setIsOpen, setIsAnimating, setIsLoading, setNavVector }
|
||||
] = useDesktopState()
|
||||
|
||||
const imgs: DesktopImage[] = Array<DesktopImage>(imageState().length)
|
||||
let last = { x: 0, y: 0 }
|
||||
|
||||
let abortController: AbortController | undefined
|
||||
|
||||
// states
|
||||
let gsapLoaded = false
|
||||
|
||||
const [state, { incIndex }] = useState()
|
||||
const stateLength = state().length
|
||||
|
||||
let mounted = false
|
||||
|
||||
const ensureGsapReady: () => Promise<void> = async () => {
|
||||
if (gsapPromise !== undefined) return await gsapPromise
|
||||
|
||||
gsapPromise = loadGsap()
|
||||
.then((g) => {
|
||||
_gsap = g
|
||||
gsapLoaded = true
|
||||
})
|
||||
.catch((e) => {
|
||||
gsapPromise = undefined
|
||||
console.log(e)
|
||||
})
|
||||
|
||||
await gsapPromise
|
||||
}
|
||||
|
||||
const onMouse: (e: MouseEvent) => void = (e) => {
|
||||
if (props.isOpen() || props.isAnimating() || !gsapLoaded || !mounted) return
|
||||
if (desktop.isOpen() || desktop.isAnimating() || !gsapLoaded || !mounted) return
|
||||
|
||||
const length = imageState().length
|
||||
if (length <= 0) return
|
||||
|
||||
const cord = { x: e.clientX, y: e.clientY }
|
||||
const travelDist = Math.hypot(cord.x - last.x, cord.y - last.y)
|
||||
|
||||
if (travelDist > state().threshold) {
|
||||
last = cord
|
||||
incIndex()
|
||||
if (travelDist > config().threshold) {
|
||||
const nextIndex = increment(desktop.index(), length)
|
||||
|
||||
const _state = state()
|
||||
const newHist = { i: _state.index, ...cord }
|
||||
props.setCordHist((prev) => [...prev, newHist].slice(-stateLength))
|
||||
last = cord
|
||||
setIndex(nextIndex)
|
||||
setCordHist((prev) => [...prev, { i: nextIndex, ...cord }].slice(-length))
|
||||
}
|
||||
}
|
||||
|
||||
const onClick: () => void = () => {
|
||||
if (!props.isAnimating()) props.setIsOpen(true)
|
||||
const onClick: () => Promise<void> = async () => {
|
||||
if (!gsapLoaded) {
|
||||
await ensureGsapReady()
|
||||
}
|
||||
|
||||
if (desktop.isAnimating() || !gsapLoaded) return
|
||||
if (desktop.index() < 0 || desktop.cordHist().length === 0) return
|
||||
setIsOpen(true)
|
||||
}
|
||||
|
||||
const setPosition: () => void = () => {
|
||||
if (!mounted) return
|
||||
if (imgs.length === 0) return
|
||||
const _cordHist = props.cordHist()
|
||||
const trailElsIndex = getTrailElsIndex(_cordHist)
|
||||
if (trailElsIndex.length === 0) return
|
||||
|
||||
const elsTrail = getImagesFromIndexes(imgs, trailElsIndex)
|
||||
|
||||
const _isOpen = props.isOpen()
|
||||
const _state = state()
|
||||
|
||||
_gsap.set(elsTrail, {
|
||||
x: (i: number) => _cordHist[i].x - window.innerWidth / 2,
|
||||
y: (i: number) => _cordHist[i].y - window.innerHeight / 2,
|
||||
opacity: (i: number) =>
|
||||
Math.max(
|
||||
(i + 1 + _state.trailLength <= _cordHist.length ? 0 : 1) - (_isOpen ? 1 : 0),
|
||||
0
|
||||
),
|
||||
zIndex: (i: number) => i,
|
||||
scale: 0.6
|
||||
})
|
||||
|
||||
if (_isOpen) {
|
||||
const elc = getImagesFromIndexes(imgs, [getCurrentElIndex(_cordHist)])[0]
|
||||
const indexArrayToHires: number[] = []
|
||||
const indexArrayToCleanup: number[] = []
|
||||
switch (props.navVector()) {
|
||||
case 'prev':
|
||||
indexArrayToHires.push(getPrevElIndex(_cordHist, _state))
|
||||
indexArrayToCleanup.push(getNextElIndex(_cordHist, _state))
|
||||
break
|
||||
case 'next':
|
||||
indexArrayToHires.push(getNextElIndex(_cordHist, _state))
|
||||
indexArrayToCleanup.push(getPrevElIndex(_cordHist, _state))
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
hires(getImagesFromIndexes(imgs, indexArrayToHires)) // preload
|
||||
_gsap.set(getImagesFromIndexes(imgs, indexArrayToCleanup), { opacity: 0 })
|
||||
_gsap.set(elc, { x: 0, y: 0, scale: 1 }) // set current to center
|
||||
setLoaderForHiresImage(elc) // set loader, if loaded set current opacity to 1
|
||||
} else {
|
||||
lores(elsTrail)
|
||||
}
|
||||
}
|
||||
|
||||
const expandImage: () => Promise<
|
||||
gsap.core.Omit<gsap.core.Timeline, 'then'>
|
||||
> = async () => {
|
||||
// isAnimating is prechecked in isOpen effect
|
||||
if (!mounted || !gsapLoaded) throw new Error('not mounted or gsap not loaded')
|
||||
|
||||
props.setIsAnimating(true)
|
||||
|
||||
const _cordHist = props.cordHist()
|
||||
const _state = state()
|
||||
|
||||
const elcIndex = getCurrentElIndex(_cordHist)
|
||||
const elc = imgs[elcIndex]
|
||||
|
||||
// don't hide here because we want a better transition
|
||||
hires(
|
||||
getImagesFromIndexes(imgs, [
|
||||
elcIndex,
|
||||
getPrevElIndex(_cordHist, _state),
|
||||
getNextElIndex(_cordHist, _state)
|
||||
])
|
||||
)
|
||||
setLoaderForHiresImage(elc)
|
||||
|
||||
const tl = _gsap.timeline()
|
||||
const trailInactiveEls = getImagesFromIndexes(
|
||||
syncStagePosition({
|
||||
gsap: _gsap,
|
||||
imgs,
|
||||
getTrailInactiveElsIndex(_cordHist, _state)
|
||||
)
|
||||
// move down and hide trail inactive
|
||||
tl.to(trailInactiveEls, {
|
||||
y: '+=20',
|
||||
ease: 'power3.in',
|
||||
stagger: 0.075,
|
||||
duration: 0.3,
|
||||
delay: 0.1,
|
||||
opacity: 0
|
||||
})
|
||||
// current move to center
|
||||
tl.to(elc, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
ease: 'power3.inOut',
|
||||
duration: 0.7,
|
||||
delay: 0.3
|
||||
})
|
||||
// current expand
|
||||
tl.to(elc, {
|
||||
delay: 0.1,
|
||||
scale: 1,
|
||||
ease: 'power3.inOut'
|
||||
})
|
||||
// finished
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
return await tl.then(() => {
|
||||
props.setIsAnimating(false)
|
||||
cordHist: desktop.cordHist(),
|
||||
trailLength: config().trailLength,
|
||||
length: imageState().length,
|
||||
isOpen: desktop.isOpen(),
|
||||
navVector: desktop.navVector(),
|
||||
mounted,
|
||||
setIsLoading
|
||||
})
|
||||
}
|
||||
|
||||
const minimizeImage: () => Promise<
|
||||
gsap.core.Omit<gsap.core.Timeline, 'then'>
|
||||
> = async () => {
|
||||
const expandImage: () => Promise<void> = async () => {
|
||||
if (!mounted || !gsapLoaded) throw new Error('not mounted or gsap not loaded')
|
||||
|
||||
props.setIsAnimating(true)
|
||||
props.setNavVector('none') // cleanup
|
||||
|
||||
const _cordHist = props.cordHist()
|
||||
const _state = state()
|
||||
|
||||
const elcIndex = getCurrentElIndex(_cordHist)
|
||||
const elsTrailInactiveIndexes = getTrailInactiveElsIndex(_cordHist, _state)
|
||||
|
||||
lores(getImagesFromIndexes(imgs, [...elsTrailInactiveIndexes, elcIndex]))
|
||||
|
||||
const tl = _gsap.timeline()
|
||||
const elc = getImagesFromIndexes(imgs, [elcIndex])[0]
|
||||
const elsTrailInactive = getImagesFromIndexes(imgs, elsTrailInactiveIndexes)
|
||||
// shrink current
|
||||
tl.to(elc, {
|
||||
scale: 0.6,
|
||||
duration: 0.6,
|
||||
ease: 'power3.inOut'
|
||||
})
|
||||
// move current to original position
|
||||
tl.to(elc, {
|
||||
delay: 0.3,
|
||||
duration: 0.7,
|
||||
ease: 'power3.inOut',
|
||||
x: _cordHist.slice(-1)[0].x - window.innerWidth / 2,
|
||||
y: _cordHist.slice(-1)[0].y - window.innerHeight / 2
|
||||
})
|
||||
// show trail inactive
|
||||
tl.to(elsTrailInactive, {
|
||||
y: '-=20',
|
||||
ease: 'power3.out',
|
||||
stagger: -0.1,
|
||||
duration: 0.3,
|
||||
opacity: 1
|
||||
})
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
return await tl.then(() => {
|
||||
props.setIsAnimating(false)
|
||||
await expandStage({
|
||||
gsap: _gsap,
|
||||
imgs,
|
||||
cordHist: desktop.cordHist(),
|
||||
trailLength: config().trailLength,
|
||||
length: imageState().length,
|
||||
mounted,
|
||||
setIsLoading,
|
||||
setIsAnimating
|
||||
})
|
||||
}
|
||||
|
||||
function setLoaderForHiresImage(img: DesktopImage): void {
|
||||
if (!mounted || !gsapLoaded) return
|
||||
if (!img.complete) {
|
||||
props.setIsLoading(true)
|
||||
// abort controller for cleanup
|
||||
const controller = new AbortController()
|
||||
const abortSignal = controller.signal
|
||||
// event listeners
|
||||
img.addEventListener(
|
||||
'load',
|
||||
() => {
|
||||
_gsap
|
||||
.to(img, { opacity: 1, ease: 'power3.out', duration: 0.5 })
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
.then(() => {
|
||||
props.setIsLoading(false)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
.finally(() => {
|
||||
controller.abort()
|
||||
})
|
||||
},
|
||||
{ once: true, passive: true, signal: abortSignal }
|
||||
)
|
||||
img.addEventListener(
|
||||
'error',
|
||||
() => {
|
||||
_gsap
|
||||
.set(img, { opacity: 1 })
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
.then(() => {
|
||||
props.setIsLoading(false)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
.finally(() => {
|
||||
controller.abort()
|
||||
})
|
||||
},
|
||||
{ once: true, passive: true, signal: abortSignal }
|
||||
)
|
||||
} else {
|
||||
_gsap
|
||||
.set(img, { opacity: 1 })
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
.then(() => {
|
||||
props.setIsLoading(false)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
}
|
||||
const minimizeImage: () => Promise<void> = async () => {
|
||||
if (!mounted || !gsapLoaded) throw new Error('not mounted or gsap not loaded')
|
||||
|
||||
setNavVector('none')
|
||||
|
||||
await minimizeStage({
|
||||
gsap: _gsap,
|
||||
imgs,
|
||||
cordHist: desktop.cordHist(),
|
||||
trailLength: config().trailLength,
|
||||
mounted,
|
||||
setIsAnimating
|
||||
})
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// preload logic
|
||||
imgs.forEach((img, i) => {
|
||||
// preload first 5 images on page load
|
||||
if (i < 5) {
|
||||
img.src = img.dataset.loUrl
|
||||
}
|
||||
// lores preloader for rest of the images
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
|
||||
onMutation(img, (mutation) => {
|
||||
// if open or animating, hold
|
||||
if (props.isOpen() || props.isAnimating()) return false
|
||||
// if mutation is not about style attribute, hold
|
||||
if (desktop.isOpen() || desktop.isAnimating()) return false
|
||||
if (mutation.attributeName !== 'style') return false
|
||||
|
||||
const opacity = parseFloat(img.style.opacity)
|
||||
// if opacity is not 1, hold
|
||||
if (opacity !== 1) return false
|
||||
// preload the i + 5th image, if it exists
|
||||
|
||||
if (i + 5 < imgs.length) {
|
||||
imgs[i + 5].src = imgs[i + 5].dataset.loUrl
|
||||
}
|
||||
// triggered
|
||||
|
||||
return true
|
||||
})
|
||||
})
|
||||
// load gsap on mousemove
|
||||
window.addEventListener(
|
||||
'mousemove',
|
||||
() => {
|
||||
loadGsap()
|
||||
.then((g) => {
|
||||
_gsap = g
|
||||
gsapLoaded = true
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
},
|
||||
{ passive: true, once: true }
|
||||
)
|
||||
// event listeners
|
||||
|
||||
window.addEventListener('pointermove', () => void ensureGsapReady(), {
|
||||
passive: true,
|
||||
once: true
|
||||
})
|
||||
window.addEventListener('pointerdown', () => void ensureGsapReady(), {
|
||||
passive: true,
|
||||
once: true
|
||||
})
|
||||
window.addEventListener('click', () => void ensureGsapReady(), {
|
||||
passive: true,
|
||||
once: true
|
||||
})
|
||||
|
||||
abortController = new AbortController()
|
||||
const abortSignal = abortController.signal
|
||||
window.addEventListener('mousemove', onMouse, {
|
||||
passive: true,
|
||||
signal: abortSignal
|
||||
})
|
||||
// mounted
|
||||
|
||||
mounted = true
|
||||
})
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => props.cordHist(),
|
||||
() => desktop.cordHist(),
|
||||
() => {
|
||||
setPosition()
|
||||
},
|
||||
@@ -413,36 +171,38 @@ export default function Stage(props: {
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => props.isOpen(),
|
||||
async () => {
|
||||
if (props.isAnimating()) return
|
||||
if (props.isOpen()) {
|
||||
// expand image
|
||||
desktop.isOpen,
|
||||
async (isOpen) => {
|
||||
if (desktop.isAnimating()) return
|
||||
|
||||
if (isOpen) {
|
||||
if (desktop.index() < 0 || desktop.cordHist().length === 0) {
|
||||
setIsOpen(false)
|
||||
return
|
||||
}
|
||||
|
||||
await expandImage()
|
||||
.catch(() => {
|
||||
void 0
|
||||
setIsOpen(false)
|
||||
setIsAnimating(false)
|
||||
setIsLoading(false)
|
||||
})
|
||||
.then(() => {
|
||||
// abort controller for cleanup
|
||||
abortController?.abort()
|
||||
})
|
||||
} else {
|
||||
// minimize image
|
||||
await minimizeImage()
|
||||
.catch(() => {
|
||||
void 0
|
||||
})
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
.then(() => {
|
||||
// event listeners and its abort controller
|
||||
abortController = new AbortController()
|
||||
const abortSignal = abortController.signal
|
||||
window.addEventListener('mousemove', onMouse, {
|
||||
passive: true,
|
||||
signal: abortSignal
|
||||
})
|
||||
// cleanup isLoading
|
||||
props.setIsLoading(false)
|
||||
setIsLoading(false)
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -453,7 +213,7 @@ export default function Stage(props: {
|
||||
return (
|
||||
<>
|
||||
<div class="stage" onClick={onClick} onKeyDown={onClick}>
|
||||
<For each={props.ijs}>
|
||||
<For each={imageState().images}>
|
||||
{(ij, i) => (
|
||||
<img
|
||||
ref={imgs[i()]}
|
||||
|
||||
263
assets/ts/desktop/stageAnimations.ts
Normal file
263
assets/ts/desktop/stageAnimations.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
import { type gsap } from 'gsap'
|
||||
|
||||
import type { Vector } from '../utils'
|
||||
|
||||
import type { DesktopImage } from './layout'
|
||||
import {
|
||||
getCurrentElIndex,
|
||||
getImagesFromIndexes,
|
||||
getNextElIndex,
|
||||
getPrevElIndex,
|
||||
getTrailElsIndex,
|
||||
getTrailInactiveElsIndex,
|
||||
hires,
|
||||
lores
|
||||
} from './stageUtils'
|
||||
import type { HistoryItem } from './state'
|
||||
|
||||
type SetLoading = (value: boolean) => void
|
||||
|
||||
export function setLoaderForHiresImage(args: {
|
||||
gsap: typeof gsap
|
||||
img: DesktopImage
|
||||
mounted: boolean
|
||||
setIsLoading: SetLoading
|
||||
}): void {
|
||||
const { gsap, img, mounted, setIsLoading } = args
|
||||
if (!mounted) return
|
||||
|
||||
if (img.complete) {
|
||||
gsap
|
||||
.set(img, { opacity: 1 })
|
||||
.then(() => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
|
||||
const controller = new AbortController()
|
||||
const abortSignal = controller.signal
|
||||
|
||||
img.addEventListener(
|
||||
'load',
|
||||
() => {
|
||||
gsap
|
||||
.to(img, { opacity: 1, ease: 'power3.out', duration: 0.5 })
|
||||
.then(() => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
.finally(() => {
|
||||
controller.abort()
|
||||
})
|
||||
},
|
||||
{ once: true, passive: true, signal: abortSignal }
|
||||
)
|
||||
img.addEventListener(
|
||||
'error',
|
||||
() => {
|
||||
gsap
|
||||
.set(img, { opacity: 1 })
|
||||
.then(() => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
.finally(() => {
|
||||
controller.abort()
|
||||
})
|
||||
},
|
||||
{ once: true, passive: true, signal: abortSignal }
|
||||
)
|
||||
}
|
||||
|
||||
export function syncStagePosition(args: {
|
||||
gsap: typeof gsap
|
||||
imgs: DesktopImage[]
|
||||
cordHist: HistoryItem[]
|
||||
trailLength: number
|
||||
length: number
|
||||
isOpen: boolean
|
||||
navVector: Vector
|
||||
mounted: boolean
|
||||
setIsLoading: SetLoading
|
||||
}): void {
|
||||
const {
|
||||
gsap,
|
||||
imgs,
|
||||
cordHist,
|
||||
trailLength,
|
||||
length,
|
||||
isOpen,
|
||||
navVector,
|
||||
mounted,
|
||||
setIsLoading
|
||||
} = args
|
||||
|
||||
if (!mounted || imgs.length === 0) return
|
||||
|
||||
const trailElsIndex = getTrailElsIndex(cordHist)
|
||||
if (trailElsIndex.length === 0) return
|
||||
|
||||
const elsTrail = getImagesFromIndexes(imgs, trailElsIndex)
|
||||
|
||||
gsap.set(elsTrail, {
|
||||
x: (i: number) => cordHist[i].x - window.innerWidth / 2,
|
||||
y: (i: number) => cordHist[i].y - window.innerHeight / 2,
|
||||
opacity: (i: number) =>
|
||||
Math.max((i + 1 + trailLength <= cordHist.length ? 0 : 1) - (isOpen ? 1 : 0), 0),
|
||||
zIndex: (i: number) => i,
|
||||
scale: 0.6
|
||||
})
|
||||
|
||||
if (!isOpen) {
|
||||
lores(elsTrail)
|
||||
return
|
||||
}
|
||||
|
||||
const current = getImagesFromIndexes(imgs, [getCurrentElIndex(cordHist)])[0]
|
||||
const indexArrayToHires: number[] = []
|
||||
const indexArrayToCleanup: number[] = []
|
||||
|
||||
switch (navVector) {
|
||||
case 'prev':
|
||||
indexArrayToHires.push(getPrevElIndex(cordHist, length))
|
||||
indexArrayToCleanup.push(getNextElIndex(cordHist, length))
|
||||
break
|
||||
case 'next':
|
||||
indexArrayToHires.push(getNextElIndex(cordHist, length))
|
||||
indexArrayToCleanup.push(getPrevElIndex(cordHist, length))
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
hires(getImagesFromIndexes(imgs, indexArrayToHires))
|
||||
gsap.set(getImagesFromIndexes(imgs, indexArrayToCleanup), { opacity: 0 })
|
||||
gsap.set(current, { x: 0, y: 0, scale: 1 })
|
||||
setLoaderForHiresImage({ gsap, img: current, mounted, setIsLoading })
|
||||
}
|
||||
|
||||
export async function expandStage(args: {
|
||||
gsap: typeof gsap
|
||||
imgs: DesktopImage[]
|
||||
cordHist: HistoryItem[]
|
||||
trailLength: number
|
||||
length: number
|
||||
mounted: boolean
|
||||
setIsLoading: SetLoading
|
||||
setIsAnimating: (value: boolean) => void
|
||||
}): Promise<void> {
|
||||
const {
|
||||
gsap,
|
||||
imgs,
|
||||
cordHist,
|
||||
trailLength,
|
||||
length,
|
||||
mounted,
|
||||
setIsLoading,
|
||||
setIsAnimating
|
||||
} = args
|
||||
|
||||
if (!mounted) throw new Error('not mounted')
|
||||
|
||||
setIsAnimating(true)
|
||||
|
||||
const currentIndex = getCurrentElIndex(cordHist)
|
||||
const current = imgs[currentIndex]
|
||||
|
||||
hires(
|
||||
getImagesFromIndexes(imgs, [
|
||||
currentIndex,
|
||||
getPrevElIndex(cordHist, length),
|
||||
getNextElIndex(cordHist, length)
|
||||
])
|
||||
)
|
||||
setLoaderForHiresImage({ gsap, img: current, mounted, setIsLoading })
|
||||
|
||||
const tl = gsap.timeline()
|
||||
const trailInactiveEls = getImagesFromIndexes(
|
||||
imgs,
|
||||
getTrailInactiveElsIndex(cordHist, trailLength)
|
||||
)
|
||||
|
||||
tl.to(trailInactiveEls, {
|
||||
y: '+=20',
|
||||
ease: 'power3.in',
|
||||
stagger: 0.075,
|
||||
duration: 0.3,
|
||||
delay: 0.1,
|
||||
opacity: 0
|
||||
})
|
||||
tl.to(current, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
ease: 'power3.inOut',
|
||||
duration: 0.7,
|
||||
delay: 0.3
|
||||
})
|
||||
tl.to(current, {
|
||||
delay: 0.1,
|
||||
scale: 1,
|
||||
ease: 'power3.inOut'
|
||||
})
|
||||
|
||||
await tl.then(() => {
|
||||
setIsAnimating(false)
|
||||
})
|
||||
}
|
||||
|
||||
export async function minimizeStage(args: {
|
||||
gsap: typeof gsap
|
||||
imgs: DesktopImage[]
|
||||
cordHist: HistoryItem[]
|
||||
trailLength: number
|
||||
mounted: boolean
|
||||
setIsAnimating: (value: boolean) => void
|
||||
}): Promise<void> {
|
||||
const { gsap, imgs, cordHist, trailLength, mounted, setIsAnimating } = args
|
||||
if (!mounted) throw new Error('not mounted')
|
||||
|
||||
setIsAnimating(true)
|
||||
|
||||
const currentIndex = getCurrentElIndex(cordHist)
|
||||
const trailInactiveIndexes = getTrailInactiveElsIndex(cordHist, trailLength)
|
||||
|
||||
lores(getImagesFromIndexes(imgs, [...trailInactiveIndexes, currentIndex]))
|
||||
|
||||
const tl = gsap.timeline()
|
||||
const current = getImagesFromIndexes(imgs, [currentIndex])[0]
|
||||
const trailInactiveEls = getImagesFromIndexes(imgs, trailInactiveIndexes)
|
||||
|
||||
tl.to(current, {
|
||||
scale: 0.6,
|
||||
duration: 0.6,
|
||||
ease: 'power3.inOut'
|
||||
})
|
||||
tl.to(current, {
|
||||
delay: 0.3,
|
||||
duration: 0.7,
|
||||
ease: 'power3.inOut',
|
||||
x: cordHist.slice(-1)[0].x - window.innerWidth / 2,
|
||||
y: cordHist.slice(-1)[0].y - window.innerHeight / 2
|
||||
})
|
||||
tl.to(trailInactiveEls, {
|
||||
y: '-=20',
|
||||
ease: 'power3.out',
|
||||
stagger: -0.1,
|
||||
duration: 0.3,
|
||||
opacity: 1
|
||||
})
|
||||
|
||||
await tl.then(() => {
|
||||
setIsAnimating(false)
|
||||
})
|
||||
}
|
||||
@@ -1,24 +1,15 @@
|
||||
import { For, createEffect, type Accessor, type JSX, type Setter } from 'solid-js'
|
||||
import { For, createEffect, createMemo, on, onCleanup, type JSX } from 'solid-js'
|
||||
|
||||
import { useState } from '../state'
|
||||
import { decrement, increment, type Vector } from '../utils'
|
||||
import { useImageState } from '../imageState'
|
||||
import { decrement, increment } from '../utils'
|
||||
|
||||
import type { HistoryItem } from './layout'
|
||||
import { useDesktopState } from './state'
|
||||
|
||||
export default function StageNav(props: {
|
||||
children?: JSX.Element
|
||||
prevText: string
|
||||
closeText: string
|
||||
nextText: string
|
||||
loadingText: string
|
||||
active: Accessor<boolean>
|
||||
isAnimating: Accessor<boolean>
|
||||
setCordHist: Setter<HistoryItem[]>
|
||||
isOpen: Accessor<boolean>
|
||||
setIsOpen: Setter<boolean>
|
||||
setHoverText: Setter<string>
|
||||
navVector: Accessor<Vector>
|
||||
setNavVector: Setter<Vector>
|
||||
}): JSX.Element {
|
||||
// types
|
||||
type NavItem = (typeof navItems)[number]
|
||||
@@ -29,64 +20,74 @@ export default function StageNav(props: {
|
||||
const navItems = [props.prevText, props.closeText, props.nextText] as const
|
||||
|
||||
// states
|
||||
const [state, { incIndex, decIndex }] = useState()
|
||||
const imageState = useImageState()
|
||||
const [
|
||||
desktop,
|
||||
{ incIndex, decIndex, setCordHist, setHoverText, setIsOpen, setNavVector }
|
||||
] = useDesktopState()
|
||||
|
||||
const stateLength = state().length
|
||||
const active = createMemo(() => desktop.isOpen() && !desktop.isAnimating())
|
||||
|
||||
const prevImage: () => void = () => {
|
||||
props.setNavVector('prev')
|
||||
props.setCordHist((c) =>
|
||||
setNavVector('prev')
|
||||
setCordHist((c) =>
|
||||
c.map((item) => {
|
||||
return { ...item, i: decrement(item.i, stateLength) }
|
||||
return { ...item, i: decrement(item.i, imageState().length) }
|
||||
})
|
||||
)
|
||||
decIndex()
|
||||
}
|
||||
|
||||
const closeImage: () => void = () => {
|
||||
props.setIsOpen(false)
|
||||
setIsOpen(false)
|
||||
}
|
||||
|
||||
const nextImage: () => void = () => {
|
||||
props.setNavVector('next')
|
||||
props.setCordHist((c) =>
|
||||
setNavVector('next')
|
||||
setCordHist((c) =>
|
||||
c.map((item) => {
|
||||
return { ...item, i: increment(item.i, stateLength) }
|
||||
return { ...item, i: increment(item.i, imageState().length) }
|
||||
})
|
||||
)
|
||||
incIndex()
|
||||
}
|
||||
|
||||
const handleClick: (item: NavItem) => void = (item) => {
|
||||
if (!props.isOpen() || props.isAnimating()) return
|
||||
if (!desktop.isOpen() || desktop.isAnimating()) return
|
||||
if (item === navItems[0]) prevImage()
|
||||
else if (item === navItems[1]) closeImage()
|
||||
else nextImage()
|
||||
}
|
||||
|
||||
const handleKey: (e: KeyboardEvent) => void = (e) => {
|
||||
if (!props.isOpen() || props.isAnimating()) return
|
||||
if (!desktop.isOpen() || desktop.isAnimating()) return
|
||||
if (e.key === 'ArrowLeft') prevImage()
|
||||
else if (e.key === 'Escape') closeImage()
|
||||
else if (e.key === 'ArrowRight') nextImage()
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (props.isOpen()) {
|
||||
controller = new AbortController()
|
||||
const abortSignal = controller.signal
|
||||
window.addEventListener('keydown', handleKey, {
|
||||
passive: true,
|
||||
signal: abortSignal
|
||||
})
|
||||
} else {
|
||||
createEffect(
|
||||
on(desktop.isOpen, (isOpen) => {
|
||||
controller?.abort()
|
||||
}
|
||||
|
||||
if (isOpen) {
|
||||
controller = new AbortController()
|
||||
const abortSignal = controller.signal
|
||||
window.addEventListener('keydown', handleKey, {
|
||||
passive: true,
|
||||
signal: abortSignal
|
||||
})
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
onCleanup(() => {
|
||||
controller?.abort()
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="navOverlay" classList={{ active: props.active() }}>
|
||||
<div class="navOverlay" classList={{ active: active() }}>
|
||||
<For each={navItems}>
|
||||
{(item) => (
|
||||
<div
|
||||
@@ -94,8 +95,8 @@ export default function StageNav(props: {
|
||||
onClick={() => {
|
||||
handleClick(item)
|
||||
}}
|
||||
onFocus={() => props.setHoverText(item)}
|
||||
onMouseOver={() => props.setHoverText(item)}
|
||||
onFocus={() => setHoverText(item)}
|
||||
onMouseOver={() => setHoverText(item)}
|
||||
tabIndex="-1"
|
||||
/>
|
||||
)}
|
||||
|
||||
67
assets/ts/desktop/stageUtils.ts
Normal file
67
assets/ts/desktop/stageUtils.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { decrement, increment } from '../utils'
|
||||
|
||||
import type { DesktopImage } from './layout'
|
||||
import type { HistoryItem } from './state'
|
||||
|
||||
export function getTrailElsIndex(cordHistValue: HistoryItem[]): number[] {
|
||||
return cordHistValue.map((el) => el.i)
|
||||
}
|
||||
|
||||
export function getTrailInactiveElsIndex(
|
||||
cordHistValue: HistoryItem[],
|
||||
trailLength: number
|
||||
): number[] {
|
||||
return getTrailElsIndex(cordHistValue).slice(-trailLength).slice(0, -1)
|
||||
}
|
||||
|
||||
export function getCurrentElIndex(cordHistValue: HistoryItem[]): number {
|
||||
return getTrailElsIndex(cordHistValue).slice(-1)[0]
|
||||
}
|
||||
|
||||
export function getPrevElIndex(cordHistValue: HistoryItem[], length: number): number {
|
||||
return decrement(cordHistValue.slice(-1)[0].i, length)
|
||||
}
|
||||
|
||||
export function getNextElIndex(cordHistValue: HistoryItem[], length: number): number {
|
||||
return increment(cordHistValue.slice(-1)[0].i, length)
|
||||
}
|
||||
|
||||
export function getImagesFromIndexes(
|
||||
imgs: DesktopImage[],
|
||||
indexes: number[]
|
||||
): DesktopImage[] {
|
||||
return indexes.map((i) => imgs[i])
|
||||
}
|
||||
|
||||
export function hires(imgs: DesktopImage[]): void {
|
||||
imgs.forEach((img) => {
|
||||
if (img.src === img.dataset.hiUrl) return
|
||||
img.src = img.dataset.hiUrl
|
||||
img.height = parseInt(img.dataset.hiImgH)
|
||||
img.width = parseInt(img.dataset.hiImgW)
|
||||
})
|
||||
}
|
||||
|
||||
export function lores(imgs: DesktopImage[]): void {
|
||||
imgs.forEach((img) => {
|
||||
if (img.src === img.dataset.loUrl) return
|
||||
img.src = img.dataset.loUrl
|
||||
img.height = parseInt(img.dataset.loImgH)
|
||||
img.width = parseInt(img.dataset.loImgW)
|
||||
})
|
||||
}
|
||||
|
||||
export function onMutation<T extends HTMLElement>(
|
||||
element: T,
|
||||
trigger: (arg0: MutationRecord) => boolean,
|
||||
observeOptions: MutationObserverInit = { attributes: true }
|
||||
): void {
|
||||
new MutationObserver((mutations, observer) => {
|
||||
for (const mutation of mutations) {
|
||||
if (trigger(mutation)) {
|
||||
observer.disconnect()
|
||||
break
|
||||
}
|
||||
}
|
||||
}).observe(element, observeOptions)
|
||||
}
|
||||
96
assets/ts/desktop/state.ts
Normal file
96
assets/ts/desktop/state.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import {
|
||||
createComponent,
|
||||
createContext,
|
||||
createSignal,
|
||||
useContext,
|
||||
type Accessor,
|
||||
type JSX,
|
||||
type Setter
|
||||
} from 'solid-js'
|
||||
import invariant from 'tiny-invariant'
|
||||
|
||||
import { useImageState } from '../imageState'
|
||||
import { decrement, increment, type Vector } from '../utils'
|
||||
|
||||
export interface HistoryItem {
|
||||
i: number
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export interface DesktopState {
|
||||
index: Accessor<number>
|
||||
cordHist: Accessor<HistoryItem[]>
|
||||
hoverText: Accessor<string>
|
||||
isOpen: Accessor<boolean>
|
||||
isAnimating: Accessor<boolean>
|
||||
isLoading: Accessor<boolean>
|
||||
navVector: Accessor<Vector>
|
||||
}
|
||||
|
||||
export type DesktopStateContextType = readonly [
|
||||
DesktopState,
|
||||
{
|
||||
readonly setIndex: Setter<number>
|
||||
readonly incIndex: () => void
|
||||
readonly decIndex: () => void
|
||||
readonly setCordHist: Setter<HistoryItem[]>
|
||||
readonly setHoverText: Setter<string>
|
||||
readonly setIsOpen: Setter<boolean>
|
||||
readonly setIsAnimating: Setter<boolean>
|
||||
readonly setIsLoading: Setter<boolean>
|
||||
readonly setNavVector: Setter<Vector>
|
||||
}
|
||||
]
|
||||
|
||||
const DesktopStateContext = createContext<DesktopStateContextType>()
|
||||
|
||||
export function DesktopStateProvider(props: { children?: JSX.Element }): JSX.Element {
|
||||
const imageState = useImageState()
|
||||
|
||||
const [index, setIndex] = createSignal(-1)
|
||||
const [cordHist, setCordHist] = createSignal<HistoryItem[]>([])
|
||||
const [hoverText, setHoverText] = createSignal('')
|
||||
const [isOpen, setIsOpen] = createSignal(false)
|
||||
const [isAnimating, setIsAnimating] = createSignal(false)
|
||||
const [isLoading, setIsLoading] = createSignal(false)
|
||||
const [navVector, setNavVector] = createSignal<Vector>('none')
|
||||
|
||||
const updateIndex = (stride: 1 | -1): void => {
|
||||
const length = imageState().length
|
||||
if (length <= 0) return
|
||||
setIndex((current) =>
|
||||
stride === 1 ? increment(current, length) : decrement(current, length)
|
||||
)
|
||||
}
|
||||
|
||||
return createComponent(DesktopStateContext.Provider, {
|
||||
value: [
|
||||
{ index, cordHist, hoverText, isOpen, isAnimating, isLoading, navVector },
|
||||
{
|
||||
setIndex,
|
||||
incIndex: () => {
|
||||
updateIndex(1)
|
||||
},
|
||||
decIndex: () => {
|
||||
updateIndex(-1)
|
||||
},
|
||||
setCordHist,
|
||||
setHoverText,
|
||||
setIsOpen,
|
||||
setIsAnimating,
|
||||
setIsLoading,
|
||||
setNavVector
|
||||
}
|
||||
],
|
||||
get children() {
|
||||
return props.children
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function useDesktopState(): DesktopStateContextType {
|
||||
const context = useContext(DesktopStateContext)
|
||||
invariant(context, 'undefined desktop context')
|
||||
return context
|
||||
}
|
||||
41
assets/ts/imageState.tsx
Normal file
41
assets/ts/imageState.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
createContext,
|
||||
createMemo,
|
||||
useContext,
|
||||
type Accessor,
|
||||
type JSX
|
||||
} from 'solid-js'
|
||||
import invariant from 'tiny-invariant'
|
||||
|
||||
import type { ImageJSON } from './resources'
|
||||
|
||||
export interface ImageState {
|
||||
images: ImageJSON[]
|
||||
length: number
|
||||
}
|
||||
|
||||
type ImageStateContextType = Accessor<ImageState>
|
||||
|
||||
const ImageStateContext = createContext<ImageStateContextType>()
|
||||
|
||||
export function ImageStateProvider(props: {
|
||||
children?: JSX.Element
|
||||
images: ImageJSON[]
|
||||
}): JSX.Element {
|
||||
const state = createMemo<ImageState>(() => ({
|
||||
images: props.images,
|
||||
length: props.images.length
|
||||
}))
|
||||
|
||||
return (
|
||||
<ImageStateContext.Provider value={state}>
|
||||
{props.children}
|
||||
</ImageStateContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useImageState(): ImageStateContextType {
|
||||
const context = useContext(ImageStateContext)
|
||||
invariant(context, 'undefined image context')
|
||||
return context
|
||||
}
|
||||
@@ -1,17 +1,11 @@
|
||||
import {
|
||||
Match,
|
||||
Show,
|
||||
Switch,
|
||||
createEffect,
|
||||
createResource,
|
||||
createSignal,
|
||||
lazy,
|
||||
type JSX
|
||||
} from 'solid-js'
|
||||
import { Match, Show, Switch, createResource, lazy, type JSX } from 'solid-js'
|
||||
import { render } from 'solid-js/web'
|
||||
|
||||
import { ConfigStateProvider } from './configState'
|
||||
import { DesktopStateProvider } from './desktop/state'
|
||||
import { ImageStateProvider } from './imageState'
|
||||
import { MobileStateProvider } from './mobile/state'
|
||||
import { getImageJSON } from './resources'
|
||||
import { StateProvider } from './state'
|
||||
|
||||
import '../scss/style.scss'
|
||||
|
||||
@@ -35,48 +29,60 @@ const container = document.getElementsByClassName('container')[0] as Container
|
||||
const Desktop = lazy(async () => await import('./desktop/layout'))
|
||||
const Mobile = lazy(async () => await import('./mobile/layout'))
|
||||
|
||||
function AppContent(props: {
|
||||
isMobile: boolean
|
||||
prevText: string
|
||||
closeText: string
|
||||
nextText: string
|
||||
loadingText: string
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<Switch fallback={<div>Error</div>}>
|
||||
<Match when={props.isMobile}>
|
||||
<MobileStateProvider>
|
||||
<Mobile closeText={props.closeText} loadingText={props.loadingText} />
|
||||
</MobileStateProvider>
|
||||
</Match>
|
||||
<Match when={!props.isMobile}>
|
||||
<DesktopStateProvider>
|
||||
<Desktop
|
||||
prevText={props.prevText}
|
||||
closeText={props.closeText}
|
||||
nextText={props.nextText}
|
||||
loadingText={props.loadingText}
|
||||
/>
|
||||
</DesktopStateProvider>
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
function Main(): JSX.Element {
|
||||
// variables
|
||||
const [ijs] = createResource(getImageJSON)
|
||||
const isMobile =
|
||||
window.matchMedia('(hover: none)').matches &&
|
||||
!window.navigator.userAgent.includes('Win')
|
||||
|
||||
// states
|
||||
const [scrollable, setScollable] = createSignal(true)
|
||||
|
||||
createEffect(() => {
|
||||
if (scrollable()) {
|
||||
container.classList.remove('disableScroll')
|
||||
} else {
|
||||
container.classList.add('disableScroll')
|
||||
}
|
||||
})
|
||||
const ua = window.navigator.userAgent.toLowerCase()
|
||||
const hasTouchInput = 'ontouchstart' in window || window.navigator.maxTouchPoints > 0
|
||||
const hasTouchLayout =
|
||||
window.matchMedia('(pointer: coarse)').matches ||
|
||||
window.matchMedia('(hover: none)').matches
|
||||
const isMobileUA = /android|iphone|ipad|ipod|mobile/.test(ua)
|
||||
const isWindowsDesktop = /windows nt/.test(ua)
|
||||
const isMobile = isMobileUA || (hasTouchInput && hasTouchLayout && !isWindowsDesktop)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Show when={ijs.state === 'ready'}>
|
||||
<StateProvider length={ijs()?.length ?? 0}>
|
||||
<Switch fallback={<div>Error</div>}>
|
||||
<Match when={isMobile}>
|
||||
<Mobile
|
||||
ijs={ijs() ?? []}
|
||||
closeText={container.dataset.close}
|
||||
loadingText={container.dataset.loading}
|
||||
setScrollable={setScollable}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={!isMobile}>
|
||||
<Desktop
|
||||
ijs={ijs() ?? []}
|
||||
prevText={container.dataset.prev}
|
||||
closeText={container.dataset.close}
|
||||
nextText={container.dataset.next}
|
||||
loadingText={container.dataset.loading}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
</StateProvider>
|
||||
<ImageStateProvider images={ijs() ?? []}>
|
||||
<ConfigStateProvider>
|
||||
<AppContent
|
||||
isMobile={isMobile}
|
||||
prevText={container.dataset.prev}
|
||||
closeText={container.dataset.close}
|
||||
nextText={container.dataset.next}
|
||||
loadingText={container.dataset.loading}
|
||||
/>
|
||||
</ConfigStateProvider>
|
||||
</ImageStateProvider>
|
||||
</Show>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
import {
|
||||
For,
|
||||
createEffect,
|
||||
on,
|
||||
onMount,
|
||||
type Accessor,
|
||||
type JSX,
|
||||
type Setter
|
||||
} from 'solid-js'
|
||||
import { For, createEffect, on, onMount, type JSX } from 'solid-js'
|
||||
|
||||
import type { ImageJSON } from '../resources'
|
||||
import { useState } from '../state'
|
||||
import { useImageState } from '../imageState'
|
||||
|
||||
import type { MobileImage } from './layout'
|
||||
import { useMobileState } from './state'
|
||||
|
||||
function getRandom(min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min
|
||||
@@ -31,29 +23,26 @@ function onIntersection<T extends HTMLElement>(
|
||||
}).observe(element)
|
||||
}
|
||||
|
||||
export default function Collection(props: {
|
||||
children?: JSX.Element
|
||||
ijs: ImageJSON[]
|
||||
isAnimating: Accessor<boolean>
|
||||
isOpen: Accessor<boolean>
|
||||
setIsOpen: Setter<boolean>
|
||||
}): JSX.Element {
|
||||
export default function Collection(): JSX.Element {
|
||||
// variables
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
const imgs: MobileImage[] = Array<MobileImage>(props.ijs.length)
|
||||
const imageState = useImageState()
|
||||
const imgs: MobileImage[] = Array<MobileImage>(imageState().length)
|
||||
|
||||
// states
|
||||
const [state, { setIndex }] = useState()
|
||||
const [mobile, { setIndex, setIsOpen }] = useMobileState()
|
||||
|
||||
// helper functions
|
||||
const handleClick: (i: number) => void = (i) => {
|
||||
if (props.isAnimating()) return
|
||||
if (mobile.isAnimating()) return
|
||||
setIndex(i)
|
||||
props.setIsOpen(true)
|
||||
setIsOpen(true)
|
||||
}
|
||||
|
||||
const scrollToActive: () => void = () => {
|
||||
imgs[state().index].scrollIntoView({ behavior: 'auto', block: 'center' })
|
||||
const index = mobile.index()
|
||||
|
||||
if (index < 0) return
|
||||
imgs[index].scrollIntoView({ behavior: 'auto', block: 'center' })
|
||||
}
|
||||
|
||||
// effects
|
||||
@@ -94,11 +83,9 @@ export default function Collection(props: {
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
mobile.isOpen,
|
||||
() => {
|
||||
props.isOpen()
|
||||
},
|
||||
() => {
|
||||
if (!props.isOpen()) scrollToActive() // scroll to active when closed
|
||||
if (!mobile.isOpen()) scrollToActive() // scroll to active when closed
|
||||
},
|
||||
{ defer: true }
|
||||
)
|
||||
@@ -107,7 +94,7 @@ export default function Collection(props: {
|
||||
return (
|
||||
<>
|
||||
<div class="collection">
|
||||
<For each={props.ijs}>
|
||||
<For each={imageState().images}>
|
||||
{(ij, i) => (
|
||||
<img
|
||||
ref={imgs[i()]}
|
||||
|
||||
@@ -1,209 +1,170 @@
|
||||
import { type gsap } from 'gsap'
|
||||
import {
|
||||
createEffect,
|
||||
createMemo,
|
||||
createSignal,
|
||||
For,
|
||||
on,
|
||||
onMount,
|
||||
Show,
|
||||
type Accessor,
|
||||
type JSX,
|
||||
type Setter
|
||||
untrack,
|
||||
type JSX
|
||||
} from 'solid-js'
|
||||
import { createStore } from 'solid-js/store'
|
||||
import { type Swiper } from 'swiper'
|
||||
import invariant from 'tiny-invariant'
|
||||
|
||||
import { type ImageJSON } from '../resources'
|
||||
import { useState } from '../state'
|
||||
import { loadGsap, type Vector } from '../utils'
|
||||
import { useImageState } from '../imageState'
|
||||
import { loadGsap, removeDuplicates, type Vector } from '../utils'
|
||||
|
||||
import GalleryImage from './galleryImage'
|
||||
import GalleryNav, { capitalizeFirstLetter } from './galleryNav'
|
||||
|
||||
function removeDuplicates<T>(arr: T[]): T[] {
|
||||
if (arr.length < 2) return arr // optimization
|
||||
return [...new Set(arr)]
|
||||
}
|
||||
|
||||
async function loadSwiper(): Promise<typeof Swiper> {
|
||||
const s = await import('swiper')
|
||||
return s.Swiper
|
||||
}
|
||||
import { closeGallery, openGallery } from './galleryTransitions'
|
||||
import { getActiveImageIndexes, loadSwiper } from './galleryUtils'
|
||||
import { useMobileState } from './state'
|
||||
|
||||
export default function Gallery(props: {
|
||||
children?: JSX.Element
|
||||
ijs: ImageJSON[]
|
||||
closeText: string
|
||||
loadingText: string
|
||||
isAnimating: Accessor<boolean>
|
||||
setIsAnimating: Setter<boolean>
|
||||
isOpen: Accessor<boolean>
|
||||
setIsOpen: Setter<boolean>
|
||||
setScrollable: Setter<boolean>
|
||||
}): JSX.Element {
|
||||
// variables
|
||||
let _gsap: typeof gsap
|
||||
let _swiper: Swiper
|
||||
let _swiper: Swiper | undefined
|
||||
let initPromise: Promise<void> | undefined
|
||||
|
||||
let curtain: HTMLDivElement | undefined
|
||||
let gallery: HTMLDivElement | undefined
|
||||
let galleryInner: HTMLDivElement | undefined
|
||||
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
const _loadingText = capitalizeFirstLetter(props.loadingText)
|
||||
const imageState = useImageState()
|
||||
const [mobile, { setIndex, setIsAnimating, setIsScrollLocked }] = useMobileState()
|
||||
|
||||
const loadingText = createMemo(() => capitalizeFirstLetter(props.loadingText))
|
||||
|
||||
// states
|
||||
let lastIndex = -1
|
||||
let mounted = false
|
||||
let navigateVector: Vector = 'none'
|
||||
|
||||
const [state, { setIndex }] = useState()
|
||||
const [libLoaded, setLibLoaded] = createSignal(false)
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
const [loads, setLoads] = createStore(Array<boolean>(props.ijs.length).fill(false))
|
||||
const [swiperReady, setSwiperReady] = createSignal(false)
|
||||
const [loads, setLoads] = createStore(Array<boolean>(imageState().length).fill(false))
|
||||
|
||||
// helper functions
|
||||
const slideUp: () => void = () => {
|
||||
// isAnimating is prechecked in isOpen effect
|
||||
if (!libLoaded() || !mounted) return
|
||||
props.setIsAnimating(true)
|
||||
|
||||
invariant(curtain, 'curtain is not defined')
|
||||
invariant(gallery, 'gallery is not defined')
|
||||
|
||||
_gsap.to(curtain, {
|
||||
opacity: 1,
|
||||
duration: 1
|
||||
openGallery({
|
||||
gsap: _gsap,
|
||||
curtain,
|
||||
gallery,
|
||||
setIsAnimating,
|
||||
setIsScrollLocked
|
||||
})
|
||||
|
||||
_gsap.to(gallery, {
|
||||
y: 0,
|
||||
ease: 'power3.inOut',
|
||||
duration: 1,
|
||||
delay: 0.4
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
props.setScrollable(false)
|
||||
props.setIsAnimating(false)
|
||||
}, 1200)
|
||||
}
|
||||
|
||||
const slideDown: () => void = () => {
|
||||
// isAnimating is prechecked in isOpen effect
|
||||
props.setIsAnimating(true)
|
||||
|
||||
invariant(gallery, 'curtain is not defined')
|
||||
invariant(curtain, 'gallery is not defined')
|
||||
|
||||
_gsap.to(gallery, {
|
||||
y: '100%',
|
||||
ease: 'power3.inOut',
|
||||
duration: 1
|
||||
closeGallery({
|
||||
gsap: _gsap,
|
||||
curtain,
|
||||
gallery,
|
||||
setIsAnimating,
|
||||
setIsScrollLocked,
|
||||
onClosed: () => {
|
||||
lastIndex = -1
|
||||
}
|
||||
})
|
||||
|
||||
_gsap.to(curtain, {
|
||||
opacity: 0,
|
||||
duration: 1.2,
|
||||
delay: 0.4
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
// cleanup
|
||||
props.setScrollable(true)
|
||||
props.setIsAnimating(false)
|
||||
lastIndex = -1
|
||||
}, 1400)
|
||||
}
|
||||
|
||||
const galleryLoadImages: () => void = () => {
|
||||
let activeImagesIndex: number[] = []
|
||||
const _state = state()
|
||||
const currentIndex = _state.index
|
||||
const nextIndex = Math.min(currentIndex + 1, _state.length - 1)
|
||||
const prevIndex = Math.max(currentIndex - 1, 0)
|
||||
switch (navigateVector) {
|
||||
case 'next':
|
||||
activeImagesIndex = [nextIndex]
|
||||
break
|
||||
case 'prev':
|
||||
activeImagesIndex = [prevIndex]
|
||||
break
|
||||
case 'none':
|
||||
activeImagesIndex = [currentIndex, nextIndex, prevIndex]
|
||||
break
|
||||
}
|
||||
setLoads(removeDuplicates(activeImagesIndex), true)
|
||||
const currentIndex = mobile.index()
|
||||
|
||||
setLoads(
|
||||
removeDuplicates(
|
||||
getActiveImageIndexes(currentIndex, imageState().length, navigateVector)
|
||||
),
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
const changeSlide: (slide: number) => void = (slide) => {
|
||||
// we are already in the gallery, don't need to
|
||||
// check mounted or libLoaded
|
||||
if (!swiperReady() || _swiper === undefined) return
|
||||
galleryLoadImages()
|
||||
_swiper.slideTo(slide, 0)
|
||||
}
|
||||
|
||||
// effects
|
||||
onMount(() => {
|
||||
window.addEventListener(
|
||||
'touchstart',
|
||||
() => {
|
||||
loadGsap()
|
||||
.then((g) => {
|
||||
_gsap = g
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
loadSwiper()
|
||||
.then((S) => {
|
||||
invariant(galleryInner, 'galleryInner is not defined')
|
||||
_swiper = new S(galleryInner, { spaceBetween: 20 })
|
||||
_swiper.on('slideChange', ({ realIndex }) => {
|
||||
setIndex(realIndex)
|
||||
})
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
const ensureGalleryReady: () => Promise<void> = async () => {
|
||||
if (initPromise !== undefined) return await initPromise
|
||||
|
||||
initPromise = (async () => {
|
||||
try {
|
||||
const [g, S] = await Promise.all([loadGsap(), loadSwiper()])
|
||||
|
||||
_gsap = g
|
||||
|
||||
invariant(galleryInner, 'galleryInner is not defined')
|
||||
_swiper = new S(galleryInner, { spaceBetween: 20 })
|
||||
_swiper.on('slideChange', ({ realIndex }) => {
|
||||
setIndex(realIndex)
|
||||
})
|
||||
|
||||
setLibLoaded(true)
|
||||
},
|
||||
{ once: true, passive: true }
|
||||
)
|
||||
setSwiperReady(true)
|
||||
|
||||
const initialIndex = untrack(mobile.index)
|
||||
|
||||
if (initialIndex >= 0) {
|
||||
changeSlide(initialIndex)
|
||||
lastIndex = initialIndex
|
||||
}
|
||||
} catch (e) {
|
||||
initPromise = undefined
|
||||
setSwiperReady(false)
|
||||
console.log(e)
|
||||
}
|
||||
})()
|
||||
|
||||
await initPromise
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('touchstart', () => void ensureGalleryReady(), {
|
||||
once: true,
|
||||
passive: true
|
||||
})
|
||||
mounted = true
|
||||
})
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => {
|
||||
state()
|
||||
},
|
||||
() => {
|
||||
const i = state().index
|
||||
if (i === lastIndex)
|
||||
return // change slide only when index is changed
|
||||
else if (lastIndex === -1)
|
||||
navigateVector = 'none' // lastIndex before set
|
||||
else if (i < lastIndex)
|
||||
navigateVector = 'prev' // set navigate vector for galleryLoadImages
|
||||
else if (i > lastIndex)
|
||||
navigateVector = 'next' // set navigate vector for galleryLoadImages
|
||||
else navigateVector = 'none' // default
|
||||
changeSlide(i) // change slide to new index
|
||||
lastIndex = i // update last index
|
||||
() => [swiperReady(), mobile.index()] as const,
|
||||
([ready, index]) => {
|
||||
if (!ready || index < 0) return
|
||||
if (index === lastIndex) return
|
||||
if (lastIndex === -1) navigateVector = 'none'
|
||||
else if (index < lastIndex) navigateVector = 'prev'
|
||||
else if (index > lastIndex) navigateVector = 'next'
|
||||
else navigateVector = 'none'
|
||||
changeSlide(index)
|
||||
lastIndex = index
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => {
|
||||
props.isOpen()
|
||||
},
|
||||
() => {
|
||||
if (props.isAnimating()) return
|
||||
if (props.isOpen()) slideUp()
|
||||
() => mobile.isOpen(),
|
||||
async (isOpen) => {
|
||||
if (isOpen && !swiperReady()) {
|
||||
await ensureGalleryReady()
|
||||
}
|
||||
|
||||
if (!libLoaded() || !swiperReady()) return
|
||||
if (mobile.isAnimating()) return
|
||||
if (isOpen) slideUp()
|
||||
else slideDown()
|
||||
},
|
||||
{ defer: true }
|
||||
@@ -215,26 +176,16 @@ export default function Gallery(props: {
|
||||
<div ref={gallery} class="gallery">
|
||||
<div ref={galleryInner} class="galleryInner">
|
||||
<div class="swiper-wrapper">
|
||||
<Show when={libLoaded()}>
|
||||
<For each={props.ijs}>
|
||||
{(ij, i) => (
|
||||
<div class="swiper-slide">
|
||||
<GalleryImage
|
||||
load={loads[i()]}
|
||||
ij={ij}
|
||||
loadingText={_loadingText}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
<For each={imageState().images}>
|
||||
{(ij, i) => (
|
||||
<div class="swiper-slide">
|
||||
<GalleryImage load={loads[i()]} ij={ij} loadingText={loadingText()} />
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
<GalleryNav
|
||||
closeText={props.closeText}
|
||||
isAnimating={props.isAnimating}
|
||||
setIsOpen={props.setIsOpen}
|
||||
/>
|
||||
<GalleryNav closeText={props.closeText} />
|
||||
</div>
|
||||
<div ref={curtain} class="curtain" />
|
||||
</>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { onMount, type JSX } from 'solid-js'
|
||||
import { type gsap } from 'gsap'
|
||||
import { createEffect, on, onMount, type JSX } from 'solid-js'
|
||||
import invariant from 'tiny-invariant'
|
||||
|
||||
import type { ImageJSON } from '../resources'
|
||||
import { useState } from '../state'
|
||||
import { loadGsap } from '../utils'
|
||||
|
||||
import { useMobileState } from './state'
|
||||
|
||||
export default function GalleryImage(props: {
|
||||
children?: JSX.Element
|
||||
load: boolean
|
||||
@@ -14,40 +16,83 @@ export default function GalleryImage(props: {
|
||||
let img: HTMLImageElement | undefined
|
||||
let loadingDiv: HTMLDivElement | undefined
|
||||
|
||||
let _gsap: typeof gsap
|
||||
let _gsap: typeof gsap | undefined
|
||||
let gsapPromise: Promise<typeof gsap> | undefined
|
||||
let revealed = false
|
||||
|
||||
const [state] = useState()
|
||||
const [mobile] = useMobileState()
|
||||
|
||||
const revealImage = async (): Promise<void> => {
|
||||
if (revealed) return
|
||||
revealed = true
|
||||
|
||||
invariant(img, 'ref must be defined')
|
||||
invariant(loadingDiv, 'loadingDiv must be defined')
|
||||
|
||||
gsapPromise ??= loadGsap()
|
||||
|
||||
try {
|
||||
_gsap ??= await gsapPromise
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
|
||||
if (_gsap === undefined) {
|
||||
img.style.opacity = '1'
|
||||
loadingDiv.style.opacity = '0'
|
||||
return
|
||||
}
|
||||
|
||||
if (mobile.index() !== props.ij.index) {
|
||||
_gsap.set(img, { opacity: 1 })
|
||||
_gsap.set(loadingDiv, { opacity: 0 })
|
||||
return
|
||||
}
|
||||
|
||||
_gsap.to(img, {
|
||||
opacity: 1,
|
||||
delay: 0.5,
|
||||
duration: 0.5,
|
||||
ease: 'power3.out'
|
||||
})
|
||||
_gsap.to(loadingDiv, { opacity: 0, duration: 0.5, ease: 'power3.in' })
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
loadGsap()
|
||||
gsapPromise = loadGsap()
|
||||
.then((g) => {
|
||||
_gsap = g
|
||||
return g
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
throw e
|
||||
})
|
||||
|
||||
img?.addEventListener(
|
||||
'load',
|
||||
() => {
|
||||
invariant(img, 'ref must be defined')
|
||||
invariant(loadingDiv, 'loadingDiv must be defined')
|
||||
if (state().index !== props.ij.index) {
|
||||
_gsap.set(img, { opacity: 1 })
|
||||
_gsap.set(loadingDiv, { opacity: 0 })
|
||||
} else {
|
||||
_gsap.to(img, {
|
||||
opacity: 1,
|
||||
delay: 0.5,
|
||||
duration: 0.5,
|
||||
ease: 'power3.out'
|
||||
})
|
||||
_gsap.to(loadingDiv, { opacity: 0, duration: 0.5, ease: 'power3.in' })
|
||||
}
|
||||
void revealImage()
|
||||
},
|
||||
{ once: true, passive: true }
|
||||
)
|
||||
|
||||
if (props.load && img?.complete && img.currentSrc !== '') {
|
||||
void revealImage()
|
||||
}
|
||||
})
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => props.load,
|
||||
(load) => {
|
||||
if (!load || img === undefined || !img.complete || img.currentSrc === '') return
|
||||
void revealImage()
|
||||
},
|
||||
{ defer: true }
|
||||
)
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="slideContainer">
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { createMemo, type Accessor, type JSX, type Setter } from 'solid-js'
|
||||
import { createMemo, type JSX } from 'solid-js'
|
||||
|
||||
import { useState } from '../state'
|
||||
import { useImageState } from '../imageState'
|
||||
import { expand } from '../utils'
|
||||
|
||||
import { useMobileState } from './state'
|
||||
|
||||
export function capitalizeFirstLetter(str: string): string {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||
}
|
||||
@@ -10,17 +12,16 @@ export function capitalizeFirstLetter(str: string): string {
|
||||
export default function GalleryNav(props: {
|
||||
children?: JSX.Element
|
||||
closeText: string
|
||||
isAnimating: Accessor<boolean>
|
||||
setIsOpen: Setter<boolean>
|
||||
}): JSX.Element {
|
||||
// states
|
||||
const [state] = useState()
|
||||
const indexValue = createMemo(() => expand(state().index + 1))
|
||||
const indexLength = createMemo(() => expand(state().length))
|
||||
const imageState = useImageState()
|
||||
const [mobile, { setIsOpen }] = useMobileState()
|
||||
const indexValue = createMemo(() => expand(mobile.index() + 1))
|
||||
const indexLength = createMemo(() => expand(imageState().length))
|
||||
|
||||
const onClick: () => void = () => {
|
||||
if (props.isAnimating()) return
|
||||
props.setIsOpen(false)
|
||||
if (mobile.isAnimating()) return
|
||||
setIsOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -37,7 +38,14 @@ export default function GalleryNav(props: {
|
||||
<span class="num">{indexLength()[2]}</span>
|
||||
<span class="num">{indexLength()[3]}</span>
|
||||
</div>
|
||||
<div onClick={onClick} onKeyDown={onClick}>
|
||||
<div
|
||||
class="navClose"
|
||||
onClick={onClick}
|
||||
onTouchEnd={onClick}
|
||||
onKeyDown={onClick}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
>
|
||||
{capitalizeFirstLetter(props.closeText)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
64
assets/ts/mobile/galleryTransitions.ts
Normal file
64
assets/ts/mobile/galleryTransitions.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { type gsap } from 'gsap'
|
||||
|
||||
const OPEN_DELAY_MS = 1200
|
||||
const CLOSE_DELAY_MS = 1400
|
||||
|
||||
export function openGallery(args: {
|
||||
gsap: typeof gsap
|
||||
curtain: HTMLDivElement
|
||||
gallery: HTMLDivElement
|
||||
setIsAnimating: (value: boolean) => void
|
||||
setIsScrollLocked: (value: boolean) => void
|
||||
}): void {
|
||||
const { gsap, curtain, gallery, setIsAnimating, setIsScrollLocked } = args
|
||||
|
||||
setIsAnimating(true)
|
||||
|
||||
gsap.to(curtain, {
|
||||
opacity: 1,
|
||||
duration: 1
|
||||
})
|
||||
|
||||
gsap.to(gallery, {
|
||||
y: 0,
|
||||
ease: 'power3.inOut',
|
||||
duration: 1,
|
||||
delay: 0.4
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
setIsScrollLocked(true)
|
||||
setIsAnimating(false)
|
||||
}, OPEN_DELAY_MS)
|
||||
}
|
||||
|
||||
export function closeGallery(args: {
|
||||
gsap: typeof gsap
|
||||
curtain: HTMLDivElement
|
||||
gallery: HTMLDivElement
|
||||
setIsAnimating: (value: boolean) => void
|
||||
setIsScrollLocked: (value: boolean) => void
|
||||
onClosed: () => void
|
||||
}): void {
|
||||
const { gsap, curtain, gallery, setIsAnimating, setIsScrollLocked, onClosed } = args
|
||||
|
||||
setIsAnimating(true)
|
||||
|
||||
gsap.to(gallery, {
|
||||
y: '100%',
|
||||
ease: 'power3.inOut',
|
||||
duration: 1
|
||||
})
|
||||
|
||||
gsap.to(curtain, {
|
||||
opacity: 0,
|
||||
duration: 1.2,
|
||||
delay: 0.4
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
setIsScrollLocked(false)
|
||||
setIsAnimating(false)
|
||||
onClosed()
|
||||
}, CLOSE_DELAY_MS)
|
||||
}
|
||||
26
assets/ts/mobile/galleryUtils.ts
Normal file
26
assets/ts/mobile/galleryUtils.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { type Swiper } from 'swiper'
|
||||
|
||||
import type { Vector } from '../utils'
|
||||
|
||||
export async function loadSwiper(): Promise<typeof Swiper> {
|
||||
const swiper = await import('swiper')
|
||||
return swiper.Swiper
|
||||
}
|
||||
|
||||
export function getActiveImageIndexes(
|
||||
currentIndex: number,
|
||||
length: number,
|
||||
navigateVector: Vector
|
||||
): number[] {
|
||||
const nextIndex = Math.min(currentIndex + 1, length - 1)
|
||||
const prevIndex = Math.max(currentIndex - 1, 0)
|
||||
|
||||
switch (navigateVector) {
|
||||
case 'next':
|
||||
return [nextIndex]
|
||||
case 'prev':
|
||||
return [prevIndex]
|
||||
case 'none':
|
||||
return [currentIndex, nextIndex, prevIndex]
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Show, createSignal, type JSX, type Setter } from 'solid-js'
|
||||
import { Show, createEffect, onCleanup, type JSX } from 'solid-js'
|
||||
|
||||
import type { ImageJSON } from '../resources'
|
||||
import { useImageState } from '../imageState'
|
||||
|
||||
import Collection from './collection'
|
||||
import Gallery from './gallery'
|
||||
import { useMobileState } from './state'
|
||||
|
||||
/**
|
||||
* interfaces
|
||||
@@ -18,34 +19,33 @@ export interface MobileImage extends HTMLImageElement {
|
||||
|
||||
export default function Mobile(props: {
|
||||
children?: JSX.Element
|
||||
ijs: ImageJSON[]
|
||||
closeText: string
|
||||
loadingText: string
|
||||
setScrollable: Setter<boolean>
|
||||
}): JSX.Element {
|
||||
// states
|
||||
const [isOpen, setIsOpen] = createSignal(false)
|
||||
const [isAnimating, setIsAnimating] = createSignal(false)
|
||||
const imageState = useImageState()
|
||||
const [mobile] = useMobileState()
|
||||
|
||||
createEffect(() => {
|
||||
const container = document.getElementsByClassName('container').item(0)
|
||||
if (container === null) return
|
||||
|
||||
if (mobile.isScrollLocked()) {
|
||||
container.classList.add('disableScroll')
|
||||
} else {
|
||||
container.classList.remove('disableScroll')
|
||||
}
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
const container = document.getElementsByClassName('container').item(0)
|
||||
container?.classList.remove('disableScroll')
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<Show when={props.ijs.length > 0}>
|
||||
<Collection
|
||||
ijs={props.ijs}
|
||||
isAnimating={isAnimating}
|
||||
isOpen={isOpen}
|
||||
setIsOpen={setIsOpen}
|
||||
/>
|
||||
<Gallery
|
||||
ijs={props.ijs}
|
||||
closeText={props.closeText}
|
||||
loadingText={props.loadingText}
|
||||
isAnimating={isAnimating}
|
||||
setIsAnimating={setIsAnimating}
|
||||
isOpen={isOpen}
|
||||
setIsOpen={setIsOpen}
|
||||
setScrollable={props.setScrollable}
|
||||
/>
|
||||
<Show when={imageState().length > 0}>
|
||||
<Collection />
|
||||
<Gallery closeText={props.closeText} loadingText={props.loadingText} />
|
||||
</Show>
|
||||
</>
|
||||
)
|
||||
|
||||
78
assets/ts/mobile/state.ts
Normal file
78
assets/ts/mobile/state.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
createComponent,
|
||||
createContext,
|
||||
createSignal,
|
||||
useContext,
|
||||
type Accessor,
|
||||
type JSX,
|
||||
type Setter
|
||||
} from 'solid-js'
|
||||
import invariant from 'tiny-invariant'
|
||||
|
||||
import { useImageState } from '../imageState'
|
||||
import { decrement, increment } from '../utils'
|
||||
|
||||
export interface MobileState {
|
||||
index: Accessor<number>
|
||||
isOpen: Accessor<boolean>
|
||||
isAnimating: Accessor<boolean>
|
||||
isScrollLocked: Accessor<boolean>
|
||||
}
|
||||
|
||||
export type MobileStateContextType = readonly [
|
||||
MobileState,
|
||||
{
|
||||
readonly setIndex: Setter<number>
|
||||
readonly incIndex: () => void
|
||||
readonly decIndex: () => void
|
||||
readonly setIsOpen: Setter<boolean>
|
||||
readonly setIsAnimating: Setter<boolean>
|
||||
readonly setIsScrollLocked: Setter<boolean>
|
||||
}
|
||||
]
|
||||
|
||||
const MobileStateContext = createContext<MobileStateContextType>()
|
||||
|
||||
export function MobileStateProvider(props: { children?: JSX.Element }): JSX.Element {
|
||||
const imageState = useImageState()
|
||||
|
||||
const [index, setIndex] = createSignal(-1)
|
||||
const [isOpen, setIsOpen] = createSignal(false)
|
||||
const [isAnimating, setIsAnimating] = createSignal(false)
|
||||
const [isScrollLocked, setIsScrollLocked] = createSignal(false)
|
||||
|
||||
const updateIndex = (stride: 1 | -1): void => {
|
||||
const length = imageState().length
|
||||
if (length <= 0) return
|
||||
setIndex((current) =>
|
||||
stride === 1 ? increment(current, length) : decrement(current, length)
|
||||
)
|
||||
}
|
||||
|
||||
return createComponent(MobileStateContext.Provider, {
|
||||
value: [
|
||||
{ index, isOpen, isAnimating, isScrollLocked },
|
||||
{
|
||||
setIndex,
|
||||
incIndex: () => {
|
||||
updateIndex(1)
|
||||
},
|
||||
decIndex: () => {
|
||||
updateIndex(-1)
|
||||
},
|
||||
setIsOpen,
|
||||
setIsAnimating,
|
||||
setIsScrollLocked
|
||||
}
|
||||
],
|
||||
get children() {
|
||||
return props.children
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function useMobileState(): MobileStateContextType {
|
||||
const context = useContext(MobileStateContext)
|
||||
invariant(context, 'undefined mobile context')
|
||||
return context
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
import {
|
||||
createContext,
|
||||
createSignal,
|
||||
useContext,
|
||||
type Accessor,
|
||||
type JSX,
|
||||
type Setter
|
||||
} from 'solid-js'
|
||||
import invariant from 'tiny-invariant'
|
||||
|
||||
import { decrement, getThresholdSessionIndex, increment } from './utils'
|
||||
|
||||
/**
|
||||
* interfaces and types
|
||||
*/
|
||||
|
||||
export interface ThresholdRelated {
|
||||
threshold: number
|
||||
trailLength: number
|
||||
}
|
||||
|
||||
export interface State {
|
||||
index: number
|
||||
length: number
|
||||
threshold: number
|
||||
trailLength: number
|
||||
}
|
||||
|
||||
export type StateContextType = readonly [
|
||||
Accessor<State>,
|
||||
{
|
||||
readonly setIndex: (index: number) => void
|
||||
readonly incIndex: () => void
|
||||
readonly decIndex: () => void
|
||||
readonly incThreshold: () => void
|
||||
readonly decThreshold: () => void
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
* constants
|
||||
*/
|
||||
|
||||
const thresholds: ThresholdRelated[] = [
|
||||
{ threshold: 20, trailLength: 20 },
|
||||
{ threshold: 40, trailLength: 10 },
|
||||
{ threshold: 80, trailLength: 5 },
|
||||
{ threshold: 140, trailLength: 5 },
|
||||
{ threshold: 200, trailLength: 5 }
|
||||
]
|
||||
const makeStateContext: (
|
||||
state: Accessor<State>,
|
||||
setState: Setter<State>
|
||||
) => StateContextType = (state: Accessor<State>, setState: Setter<State>) => {
|
||||
return [
|
||||
state,
|
||||
{
|
||||
setIndex: (index: number) => {
|
||||
setState((s) => {
|
||||
return { ...s, index }
|
||||
})
|
||||
},
|
||||
incIndex: () => {
|
||||
setState((s) => {
|
||||
return { ...s, index: increment(s.index, s.length) }
|
||||
})
|
||||
},
|
||||
decIndex: () => {
|
||||
setState((s) => {
|
||||
return { ...s, index: decrement(s.index, s.length) }
|
||||
})
|
||||
},
|
||||
incThreshold: () => {
|
||||
setState((s) => {
|
||||
return { ...s, ...updateThreshold(s.threshold, thresholds, 1) }
|
||||
})
|
||||
},
|
||||
decThreshold: () => {
|
||||
setState((s) => {
|
||||
return { ...s, ...updateThreshold(s.threshold, thresholds, -1) }
|
||||
})
|
||||
}
|
||||
}
|
||||
] as const
|
||||
}
|
||||
const StateContext = createContext<StateContextType>()
|
||||
|
||||
/**
|
||||
* helper functions
|
||||
*/
|
||||
|
||||
function updateThreshold(
|
||||
currentThreshold: number,
|
||||
thresholds: ThresholdRelated[],
|
||||
stride: number
|
||||
): ThresholdRelated {
|
||||
const i = thresholds.findIndex((t) => t.threshold === currentThreshold) + stride
|
||||
if (i < 0 || i >= thresholds.length) return thresholds[i - stride]
|
||||
// storage the index so we can restore it even if we go to another page
|
||||
sessionStorage.setItem('thresholdsIndex', i.toString())
|
||||
return thresholds[i]
|
||||
}
|
||||
|
||||
/**
|
||||
* StateProvider
|
||||
*/
|
||||
|
||||
export function StateProvider(props: {
|
||||
children?: JSX.Element
|
||||
length: number
|
||||
}): JSX.Element {
|
||||
const defaultState: State = {
|
||||
index: -1,
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
length: props.length,
|
||||
threshold: thresholds[getThresholdSessionIndex()].threshold,
|
||||
trailLength: thresholds[getThresholdSessionIndex()].trailLength
|
||||
}
|
||||
|
||||
const [state, setState] = createSignal(defaultState)
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
const contextValue = makeStateContext(state, setState)
|
||||
return (
|
||||
<StateContext.Provider value={contextValue}>{props.children}</StateContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* use context
|
||||
*/
|
||||
|
||||
export function useState(): StateContextType {
|
||||
const uc = useContext(StateContext)
|
||||
invariant(uc, 'undefined context')
|
||||
return uc
|
||||
}
|
||||
1
bundled/css/critical.css
Normal file
1
bundled/css/critical.css
Normal file
@@ -0,0 +1 @@
|
||||
*:where(:not(html,iframe,canvas,img,svg,video,audio):not(svg *,symbol *)){all:unset;display:revert}*,*:before,*:after{box-sizing:border-box}html{-moz-text-size-adjust:none;-webkit-text-size-adjust:none;text-size-adjust:none}a,button{cursor:revert}ol,ul,menu,summary{list-style:none}img{max-inline-size:100%;max-block-size:100%}table{border-collapse:collapse}input,textarea{-webkit-user-select:auto}textarea{white-space:revert}meter{-webkit-appearance:revert;appearance:revert}:where(pre){all:revert;box-sizing:border-box}::placeholder{color:unset}:where([hidden]){display:none}:where([contenteditable]:not([contenteditable=false])){-moz-user-modify:read-write;-webkit-user-modify:read-write;overflow-wrap:break-word;-webkit-line-break:after-white-space;-webkit-user-select:auto}:where([draggable=true]){-webkit-user-drag:element}:where(dialog:modal){all:revert;box-sizing:border-box}@font-face{font-family:Geist;src:url('{{- "lib/fonts/GeistVF.woff2" | absURL -}}') format("woff2 supports variations"),url('{{- "lib/fonts/GeistVF.woff2" | absURL -}}') format("woff2-variations");font-weight:400;font-style:normal;font-display:swap}@font-face{font-family:FW;src:url('{{- "lib/fonts/fw.woff2" | absURL -}}') format("woff2");font-weight:400;font-style:normal;font-display:swap}body{line-height:1.2;font-size:16px;font-family:Geist,sans-serif}body button{font-family:FW,sans-serif}@media(min-width:768px){body{font-size:18px}}@media(min-width:1024px){body{font-size:19px}}:root{--window-height: 100vh;--nav-height: 2rem;--space-standard: .625rem;--z-curtain: 200;--z-nav-gallery: 500;--z-cursor: 600;--z-nav: 800}*{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{-webkit-user-select:none;user-select:none;background:#fff}html,body{overscroll-behavior-y:none}a,button{cursor:pointer}nav{display:flex;justify-content:space-between;align-items:center;width:100%;height:var(--nav-height);padding:0 var(--space-standard);position:fixed;bottom:0;background:#fff;z-index:var(--z-nav);pointer-events:all}.num{width:.625em;display:inline-block;text-align:center}.current{font-style:italic;text-decoration:underline}@media(max-width:767px),(hover:none){nav{top:0}.index,.threshold{display:none}}article{padding:var(--space-standard);max-width:25em}article p{margin-bottom:1em}article u{text-decoration:underline}article>h1{font-size:1.6em}article>h2{font-size:1.5em}article>h3{font-size:1.375em}article>h4{font-size:1.25em}article>h5{font-size:1.125em}article h1,article h2,article h3,article h4,article h5,article h6{font-weight:700;margin:1.2rem 0}@media(max-width:767px),(hover:none){article{margin-top:var(--nav-height)}}@media(max-width:767px),(hover:none){.container{position:fixed;top:0;z-index:0;width:100vw;height:var(--window-height);overflow-y:scroll;overflow-x:hidden;background:#fff;overscroll-behavior:none;-webkit-overflow-scrolling:none}.disableScroll{pointer-events:none}}
|
||||
1
bundled/css/main.css
Normal file
1
bundled/css/main.css
Normal file
File diff suppressed because one or more lines are too long
1
bundled/js/CO8Cxe.js
Normal file
1
bundled/js/CO8Cxe.js
Normal file
File diff suppressed because one or more lines are too long
1
bundled/js/DaqdZh.js
Normal file
1
bundled/js/DaqdZh.js
Normal file
File diff suppressed because one or more lines are too long
1
bundled/js/fZjYgW.js
Normal file
1
bundled/js/fZjYgW.js
Normal file
File diff suppressed because one or more lines are too long
1
bundled/js/h6I38a.js
Normal file
1
bundled/js/h6I38a.js
Normal file
File diff suppressed because one or more lines are too long
2
bundled/js/main.js
Normal file
2
bundled/js/main.js
Normal file
@@ -0,0 +1,2 @@
|
||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["js/fZjYgW.js","js/h6I38a.js","js/zA1TQP.js"])))=>i.map(i=>d[i]);
|
||||
import{B as e,C as t,D as n,E as r,M as i,P as a,S as o,_ as s,a as c,j as l,k as u,l as d,n as f,o as p,p as m,r as h,t as g,u as _,w as v}from"./h6I38a.js";var y=[{threshold:20,trailLength:20},{threshold:40,trailLength:10},{threshold:80,trailLength:5},{threshold:140,trailLength:5},{threshold:200,trailLength:5}],b=n();function x(){let e=c();return e<0||e>=y.length?2:e}function S(e){let[t,n]=i(x()),a=u(()=>{let e=y[t()];return{thresholdIndex:t(),threshold:e.threshold,trailLength:e.trailLength}}),o=e=>{let r=t()+e;r<0||r>=y.length||(sessionStorage.setItem(`thresholdsIndex`,r.toString()),n(r))};return r(b.Provider,{value:[a,{incThreshold:()=>{o(1)},decThreshold:()=>{o(-1)}}],get children(){return e.children}})}function C(){let t=e(b);return _(t,`undefined config context`),t}var w=n();function T(e){let t=f(),[n,a]=i(-1),[o,s]=i([]),[c,l]=i(``),[u,d]=i(!1),[m,g]=i(!1),[_,v]=i(!1),[y,b]=i(`none`),x=e=>{let n=t().length;n<=0||a(t=>e===1?p(t,n):h(t,n))};return r(w.Provider,{value:[{index:n,cordHist:o,hoverText:c,isOpen:u,isAnimating:m,isLoading:_,navVector:y},{setIndex:a,incIndex:()=>{x(1)},decIndex:()=>{x(-1)},setCordHist:s,setHoverText:l,setIsOpen:d,setIsAnimating:g,setIsLoading:v,setNavVector:b}],get children(){return e.children}})}function E(){let t=e(w);return _(t,`undefined desktop context`),t}var D=n();function O(e){let t=f(),[n,a]=i(-1),[o,s]=i(!1),[c,l]=i(!1),[u,d]=i(!1),m=e=>{let n=t().length;n<=0||a(t=>e===1?p(t,n):h(t,n))};return r(D.Provider,{value:[{index:n,isOpen:o,isAnimating:c,isScrollLocked:u},{setIndex:a,incIndex:()=>{m(1)},decIndex:()=>{m(-1)},setIsOpen:s,setIsAnimating:l,setIsScrollLocked:d}],get children(){return e.children}})}function k(){let t=e(D);return _(t,`undefined mobile context`),t}async function A(){if(document.title.split(` | `)[0]===`404`)return[];let e=document.querySelector(`meta[property="og:url"]`),t=e?.content?new URL(`index.json`,e.content).href:new URL(`index.json`,window.location.href).href;try{return(await(await fetch(t,{headers:{Accept:`application/json`}})).json()).sort((e,t)=>e.index<t.index?-1:1)}catch(e){return console.error(e),[]}}var j=s(`<div>Error`),M=document.getElementsByClassName(`container`)[0],N=a(async()=>await d(()=>import(`./fZjYgW.js`),__vite__mapDeps([0,1]))),P=a(async()=>await d(()=>import(`./zA1TQP.js`),__vite__mapDeps([2,1])));function F(e){return r(v,{get fallback(){return j()},get children(){return[r(o,{get when(){return e.isMobile},get children(){return r(O,{get children(){return r(P,{get closeText(){return e.closeText},get loadingText(){return e.loadingText}})}})}}),r(o,{get when(){return!e.isMobile},get children(){return r(T,{get children(){return r(N,{get prevText(){return e.prevText},get closeText(){return e.closeText},get nextText(){return e.nextText},get loadingText(){return e.loadingText}})}})}})]}})}function I(){let[e]=l(A),n=window.navigator.userAgent.toLowerCase(),i=`ontouchstart`in window||window.navigator.maxTouchPoints>0,a=window.matchMedia(`(pointer: coarse)`).matches||window.matchMedia(`(hover: none)`).matches,o=/android|iphone|ipad|ipod|mobile/.test(n),s=/windows nt/.test(n),c=o||i&&a&&!s;return r(t,{get when(){return e.state===`ready`},get children(){return r(g,{get images(){return e()??[]},get children(){return r(S,{get children(){return r(F,{isMobile:c,get prevText(){return M.dataset.prev},get closeText(){return M.dataset.close},get nextText(){return M.dataset.next},get loadingText(){return M.dataset.loading}})}})}})}})}m(()=>r(I,{}),M);export{E as n,C as r,k as t};
|
||||
1
bundled/js/zA1TQP.js
Normal file
1
bundled/js/zA1TQP.js
Normal file
File diff suppressed because one or more lines are too long
32
docs.md
32
docs.md
@@ -15,6 +15,7 @@
|
||||
- [`outputs.toml`](#outputstoml)
|
||||
- [`params.toml`](#paramstoml)
|
||||
- [`sitemap.toml`](#sitemaptoml)
|
||||
- [Usage](#usage)
|
||||
- [Customizations](#customizations)
|
||||
- [Change Font](#change-font)
|
||||
- [Add a Custom Analytic Script](#add-a-custom-analytic-script)
|
||||
@@ -25,24 +26,19 @@
|
||||
|
||||
_[Contents](#contents)_
|
||||
|
||||
- Hugo (extended), minimum required version can be seen in the [`theme.toml`](https://github.com/Sped0n/bridget/blob/main/theme.toml#L19)
|
||||
- [Hugo (extended)](https://gohugo.io/installation/), minimum required version can be seen in the [`theme.toml`](https://github.com/Sped0n/bridget/blob/main/theme.toml#L19)
|
||||
|
||||
```bash
|
||||
❯ hugo version
|
||||
hugo v0.152.2+extended+withdeploy darwin/arm64 BuildDate=unknown VendorInfo=nixpkgs
|
||||
```
|
||||
|
||||
- [Dart Sass](https://gohugo.io/functions/css/sass/#dart-sass) (**DO NOT INSTALL IT FROM NPM**, since it is doesn't support `--embedded`)
|
||||
- [pnpm](https://pnpm.io/installation) and [Node.js](https://nodejs.org/en/download), please note that these two are only needed for customizations or development.
|
||||
|
||||
```bash
|
||||
❯ sass --embedded --version
|
||||
{
|
||||
"protocolVersion": "2.4.0",
|
||||
"compilerVersion": "1.70.0",
|
||||
"implementationVersion": "1.70.0",
|
||||
"implementationName": "dart-sass",
|
||||
"id": 0
|
||||
}
|
||||
❯ pnpm --version && node --version
|
||||
10.20.0
|
||||
v22.20.0
|
||||
```
|
||||
|
||||
## Installation
|
||||
@@ -260,6 +256,14 @@ _[Contents](#contents)_
|
||||
|
||||
https://gohugo.io/templates/sitemap-template/#configuration
|
||||
|
||||
## Usage
|
||||
|
||||
_[Contents](#contents)_
|
||||
|
||||
Bridget will work as a normal Hugo theme (if you don't have needs to customize), https://gohugo.io/getting-started/usage/ is a great start.
|
||||
|
||||
For further reading, you can refer to the `scripts` field of `package.json`.
|
||||
|
||||
## Customizations
|
||||
|
||||
_[Contents](#contents)_
|
||||
@@ -267,9 +271,11 @@ _[Contents](#contents)_
|
||||
> [!IMPORTANT]
|
||||
> Please make sure you have [installation with Git](#git-repository-for-customizations).
|
||||
>
|
||||
> - Use `pnpm install` to install neceessary dependencies.
|
||||
> - Use `pnpm run dev` to start a dev server (`http://localhost:1313`).
|
||||
> - When you’re ready, run `pnpm run build` to update artifacts.
|
||||
> If you want to try some changes on the `exampleSite`, below are some commands you might need:
|
||||
>
|
||||
> - `pnpm install` to install dependencies.
|
||||
> - `pnpm run dev` to start a dev server (`http://localhost:1313`).
|
||||
> - `pnpm run build` to update artifacts.
|
||||
|
||||
### Change Font
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { default as eslint, default as js } from '@eslint/js'
|
||||
import js from '@eslint/js'
|
||||
import tsParser from '@typescript-eslint/parser'
|
||||
import love from 'eslint-config-love'
|
||||
import importPlugin from 'eslint-plugin-import'
|
||||
@@ -9,11 +9,10 @@ import tseslint from 'typescript-eslint'
|
||||
|
||||
export default defineConfig([
|
||||
js.configs.recommended,
|
||||
eslint.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
importPlugin.flatConfigs.recommended,
|
||||
solid,
|
||||
globalIgnores(['**/node_modules', '**/static', '**/exampleSite', '*.mjs']),
|
||||
globalIgnores(['node_modules/', 'static/', 'exampleSite/', '*.mjs', 'bundled/']),
|
||||
{
|
||||
...love,
|
||||
...prettier,
|
||||
|
||||
@@ -13,4 +13,4 @@ enableRobotsTXT = true
|
||||
[module]
|
||||
replacements = "github.com/Sped0n/bridget/v2 -> ../.." # deploy with local dir (relative to hugo site theme dir) WARN: delete this line if you want to deploy with git
|
||||
[[module.imports]]
|
||||
path = "github.com/Sped0n/bridget/v2" # deploy with git (recommended) WARN: you should also set `bundled` to true in params.toml !!!
|
||||
path = "github.com/Sped0n/bridget/v2" # deploy with git (recommended)
|
||||
|
||||
@@ -12,7 +12,7 @@ build:
|
||||
publishResources: false
|
||||
---
|
||||
|
||||
Bridget is a _minimal_ Hugo theme designed for photographers/visual artists, powered by <u>[Solid.js](https://www.solidjs.com)</u>.
|
||||
Bridget is a _minimal_ Hugo theme designed for photographers/visual artists, powered by <u>[SolidJS](https://www.solidjs.com)</u>.
|
||||
|
||||
The inspiration for this theme came from a video by <u>[Hyperlexed](https://www.youtube.com/@Hyperplexed)</u>, which can be found <u>[here](https://www.youtube.com/watch?v=Jt3A2lNN2aE)</u>. Initially, it was developed using no third-party dependencies. However, after website designer <u>[Tyler McRobert](https://tylermcrobert.com)</u> made the source code publicly available, I realized that I have invented many unnecessary wheels, and this project was modified to porting the original design to Hugo while focusing on _performance_.
|
||||
|
||||
|
||||
10
flake.lock
generated
10
flake.lock
generated
@@ -2,12 +2,12 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1762596750,
|
||||
"narHash": "sha256-rXXuz51Bq7DHBlfIjN7jO8Bu3du5TV+3DSADBX7/9YQ=",
|
||||
"rev": "b6a8526db03f735b89dd5ff348f53f752e7ddc8e",
|
||||
"revCount": 891611,
|
||||
"lastModified": 1769461804,
|
||||
"narHash": "sha256-msG8SU5WsBUfVVa/9RPLaymvi5bI8edTavbIq3vRlhI=",
|
||||
"rev": "bfc1b8a4574108ceef22f02bafcf6611380c100d",
|
||||
"revCount": 935279,
|
||||
"type": "tarball",
|
||||
"url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.891611%2Brev-b6a8526db03f735b89dd5ff348f53f752e7ddc8e/019a684c-ea63-75fd-99cc-3b869954e5f9/source.tar.gz"
|
||||
"url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.935279%2Brev-bfc1b8a4574108ceef22f02bafcf6611380c100d/019c02ef-f13d-717e-8527-f1603ec205db/source.tar.gz"
|
||||
},
|
||||
"original": {
|
||||
"type": "tarball",
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
packages = with pkgs; [
|
||||
nodejs
|
||||
nodePackages.pnpm
|
||||
dart-sass
|
||||
hugo
|
||||
go
|
||||
];
|
||||
|
||||
18
hugo.toml
Normal file
18
hugo.toml
Normal file
@@ -0,0 +1,18 @@
|
||||
[[module.mounts]]
|
||||
source = 'archetypes'
|
||||
target = 'archetypes'
|
||||
[[module.mounts]]
|
||||
source = 'assets'
|
||||
target = 'assets'
|
||||
[[module.mounts]]
|
||||
source = 'layouts'
|
||||
target = 'layouts'
|
||||
[[module.mounts]]
|
||||
source = 'static'
|
||||
target = 'static'
|
||||
[[module.mounts]]
|
||||
source = "bundled"
|
||||
target = "assets/bundled"
|
||||
[[module.mounts]]
|
||||
source = "bundled"
|
||||
target = "static/bundled"
|
||||
@@ -2,8 +2,8 @@
|
||||
{{- $fingerprint := .Scratch.Get "fingerprint" | default "" -}}
|
||||
|
||||
{{- /* critical style */ -}}
|
||||
{{- $style := dict "Source" "scss/critical.scss" "Fingerprint" $fingerprint -}}
|
||||
{{- $options := dict "enableSourceMap" true "includePaths" (slice "node_modules") "transpiler" "dartsass" -}}
|
||||
{{- $style := dict "Source" "bundled/css/critical.css" "Fingerprint" $fingerprint -}}
|
||||
{{- $options := dict "enableSourceMap" false -}}
|
||||
{{- $style = dict "Context" . "ToCSS" $options "Inline" true "Template" true | merge $style -}}
|
||||
{{- partial "plugin/style.html" $style -}}
|
||||
|
||||
|
||||
30
package.json
30
package.json
@@ -3,7 +3,7 @@
|
||||
"version": "v1.0.0",
|
||||
"type": "module",
|
||||
"description": "bridget theme source file",
|
||||
"packageManager": "pnpm@8.10.2",
|
||||
"packageManager": "pnpm@10.20.0",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
@@ -39,29 +39,31 @@
|
||||
},
|
||||
"homepage": "https://github.com/Sped0n/bridget#readme",
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.10.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.46.4",
|
||||
"@typescript-eslint/parser": "^8.46.4",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-love": "^133.0.0",
|
||||
"@eslint/js": "^9.39.4",
|
||||
"@types/node": "^25.5.0",
|
||||
"@typescript-eslint/parser": "^8.57.1",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-love": "^151.0.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"eslint-plugin-solid": "^0.14.5",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "3.6.2",
|
||||
"prettier": "3.8.1",
|
||||
"prettier-plugin-go-template": "^0.0.15",
|
||||
"prettier-plugin-organize-imports": "^4.3.0",
|
||||
"sass": "^1.94.0",
|
||||
"sass-embedded": "^1.98.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.2.2",
|
||||
"vite-plugin-solid": "^2.11.10"
|
||||
"typescript-eslint": "^8.53.1",
|
||||
"vite": "^8.0.1",
|
||||
"vite-plugin-solid": "^2.11.11",
|
||||
"vitefu": "^1.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"gsap": "^3.13.0",
|
||||
"solid-js": "^1.9.10",
|
||||
"swiper": "^12.0.3",
|
||||
"gsap": "^3.14.2",
|
||||
"solid-js": "^1.9.11",
|
||||
"swiper": "^12.1.2",
|
||||
"tiny-invariant": "^1.3.3"
|
||||
}
|
||||
}
|
||||
|
||||
5958
pnpm-lock.yaml
generated
5958
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
22
vercel.sh
22
vercel.sh
@@ -1,22 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
node_modules_generated_dir="./node_modules/exampleSite/resources/_gen"
|
||||
project_generated_dir="./exampleSite/resources/_gen"
|
||||
dart_sass_version="1.93.3"
|
||||
dart_sass_install_dir="${HOME}/.local/dart-sass"
|
||||
dart_sass_tarball="dart-sass-${dart_sass_version}-linux-x64.tar.gz"
|
||||
dart_sass_download_url="https://github.com/sass/dart-sass/releases/download/${dart_sass_version}/${dart_sass_tarball}"
|
||||
|
||||
install_dart_sass() {
|
||||
echo "Installing Dart Sass ${dart_sass_version}..."
|
||||
mkdir -p "${HOME}/.local"
|
||||
curl -sSLO "${dart_sass_download_url}"
|
||||
rm -rf "${dart_sass_install_dir}"
|
||||
tar -C "${HOME}/.local" -xf "${dart_sass_tarball}"
|
||||
rm -f "${dart_sass_tarball}"
|
||||
export PATH="${dart_sass_install_dir}:${PATH}"
|
||||
}
|
||||
node_modules_generated_dir="./node_modules/exampleSite/resources/_gen/images"
|
||||
project_generated_dir="./exampleSite/resources/_gen/images"
|
||||
|
||||
copy_generated_assets_to_project() {
|
||||
if [ -d "${node_modules_generated_dir}" ]; then
|
||||
@@ -30,7 +16,8 @@ copy_generated_assets_to_project() {
|
||||
}
|
||||
|
||||
run_site_build() {
|
||||
pnpm run vite:build && hugo --logLevel info --source=exampleSite --gc --minify
|
||||
rm -rf bundled
|
||||
pnpm run build
|
||||
}
|
||||
|
||||
copy_generated_assets_to_node_modules() {
|
||||
@@ -44,7 +31,6 @@ copy_generated_assets_to_node_modules() {
|
||||
fi
|
||||
}
|
||||
|
||||
install_dart_sass
|
||||
copy_generated_assets_to_project
|
||||
run_site_build
|
||||
copy_generated_assets_to_node_modules
|
||||
|
||||
@@ -4,20 +4,30 @@ import solidPlugin from 'vite-plugin-solid'
|
||||
export default defineConfig({
|
||||
plugins: [solidPlugin()],
|
||||
build: {
|
||||
outDir: './static/bundled',
|
||||
outDir: './bundled',
|
||||
cssMinify: 'esbuild',
|
||||
watch: process.env.DISABLE_WATCH
|
||||
? null
|
||||
: {
|
||||
include: 'assets/**'
|
||||
},
|
||||
rollupOptions: {
|
||||
input: './assets/ts/main.tsx',
|
||||
input: {
|
||||
main: './assets/ts/main.tsx',
|
||||
critical: './assets/ts/critical.ts'
|
||||
},
|
||||
output: {
|
||||
format: 'es',
|
||||
entryFileNames: 'js/[name].js',
|
||||
chunkFileNames: 'js/[hash:6].js',
|
||||
assetFileNames: '[ext]/[name].[ext]',
|
||||
compact: true
|
||||
assetFileNames: '[ext]/[name].[ext]'
|
||||
}
|
||||
}
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
loadPaths: ['./assets/scss']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user