From 797b59a38aceaac686f39ae9f582c95a6a0f468c Mon Sep 17 00:00:00 2001 From: Sped0n Date: Sun, 22 Mar 2026 17:29:11 +0800 Subject: [PATCH 1/3] fix: improve mobile device detection reliability Signed-off-by: Sped0n --- assets/ts/main.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/assets/ts/main.tsx b/assets/ts/main.tsx index cf49e01..207bc4e 100644 --- a/assets/ts/main.tsx +++ b/assets/ts/main.tsx @@ -38,9 +38,14 @@ const Mobile = lazy(async () => await import('./mobile/layout')) function Main(): JSX.Element { // variables const [ijs] = createResource(getImageJSON) - const isMobile = - window.matchMedia('(hover: none)').matches && - !window.navigator.userAgent.includes('Win') + 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) // states const [scrollable, setScollable] = createSignal(true) From 1c386386f395feb6de12b240faefa830d1b7d4eb Mon Sep 17 00:00:00 2001 From: Sped0n Date: Sun, 22 Mar 2026 17:30:41 +0800 Subject: [PATCH 2/3] refactor: extract image reveal logic and improve GSAP loading reliability Signed-off-by: Sped0n --- assets/ts/mobile/galleryImage.tsx | 78 ++++++++++++++++++++++++------- 1 file changed, 61 insertions(+), 17 deletions(-) diff --git a/assets/ts/mobile/galleryImage.tsx b/assets/ts/mobile/galleryImage.tsx index 7b7f6a6..07a5c9d 100644 --- a/assets/ts/mobile/galleryImage.tsx +++ b/assets/ts/mobile/galleryImage.tsx @@ -1,4 +1,5 @@ -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' @@ -14,40 +15,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 | undefined + let revealed = false const [state] = useState() + const revealImage = async (): Promise => { + 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 (state().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 ( <>
From f25b71a85852e0c3c9e752e167b94f45fff5ceb8 Mon Sep 17 00:00:00 2001 From: Sped0n Date: Sun, 22 Mar 2026 19:45:05 +0800 Subject: [PATCH 3/3] refactor: split monolithic state into context-based modules Extract image, desktop, mobile, and config state into separate context providers to improve modularity and reduce unnecessary re-renders. Signed-off-by: Sped0n --- assets/ts/configState.tsx | 91 +++++ assets/ts/desktop/customCursor.tsx | 1 - assets/ts/desktop/layout.tsx | 55 +-- assets/ts/desktop/nav.tsx | 112 +++--- assets/ts/desktop/stage.tsx | 480 +++++++------------------ assets/ts/desktop/stageAnimations.ts | 263 ++++++++++++++ assets/ts/desktop/stageNav.tsx | 75 ++-- assets/ts/desktop/stageUtils.ts | 67 ++++ assets/ts/desktop/state.ts | 96 +++++ assets/ts/imageState.tsx | 41 +++ assets/ts/main.tsx | 87 ++--- assets/ts/mobile/collection.tsx | 45 +-- assets/ts/mobile/gallery.tsx | 259 ++++++------- assets/ts/mobile/galleryImage.tsx | 7 +- assets/ts/mobile/galleryNav.tsx | 28 +- assets/ts/mobile/galleryTransitions.ts | 64 ++++ assets/ts/mobile/galleryUtils.ts | 26 ++ assets/ts/mobile/layout.tsx | 48 +-- assets/ts/mobile/state.ts | 78 ++++ assets/ts/state.tsx | 136 ------- 20 files changed, 1165 insertions(+), 894 deletions(-) create mode 100644 assets/ts/configState.tsx create mode 100644 assets/ts/desktop/stageAnimations.ts create mode 100644 assets/ts/desktop/stageUtils.ts create mode 100644 assets/ts/desktop/state.ts create mode 100644 assets/ts/imageState.tsx create mode 100644 assets/ts/mobile/galleryTransitions.ts create mode 100644 assets/ts/mobile/galleryUtils.ts create mode 100644 assets/ts/mobile/state.ts delete mode 100644 assets/ts/state.tsx diff --git a/assets/ts/configState.tsx b/assets/ts/configState.tsx new file mode 100644 index 0000000..f7fdbbe --- /dev/null +++ b/assets/ts/configState.tsx @@ -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, + { + 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() + +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(() => { + 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 ( + { + updateThreshold(1) + }, + decThreshold: () => { + updateThreshold(-1) + } + } + ]} + > + {props.children} + + ) +} + +export function useConfigState(): ConfigStateContextType { + const context = useContext(ConfigStateContext) + invariant(context, 'undefined config context') + return context +} diff --git a/assets/ts/desktop/customCursor.tsx b/assets/ts/desktop/customCursor.tsx index 2894bcb..16f1e74 100644 --- a/assets/ts/desktop/customCursor.tsx +++ b/assets/ts/desktop/customCursor.tsx @@ -4,7 +4,6 @@ export default function CustomCursor(props: { children?: JSX.Element active: Accessor cursorText: Accessor - isOpen: Accessor }): JSX.Element { // types interface XY { diff --git a/assets/ts/desktop/layout.tsx b/assets/ts/desktop/layout.tsx index 0b56683..abf92e3 100644 --- a/assets/ts/desktop/layout.tsx +++ b/assets/ts/desktop/layout.tsx @@ -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([]) - const [isLoading, setIsLoading] = createSignal(false) - const [isOpen, setIsOpen] = createSignal(false) - const [isAnimating, setIsAnimating] = createSignal(false) - const [hoverText, setHoverText] = createSignal('') - const [navVector, setNavVector] = createSignal('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 ( <>