Fishies 0.8.1
This commit is contained in:
commit
5821ce9f4d
76 changed files with 2198 additions and 0 deletions
71
src/FishiesImg.tsx
Normal file
71
src/FishiesImg.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useLayoutEffect, useRef, useState, type ImgHTMLAttributes, type Ref } from 'react'
|
||||
import { reveal } from './fx-image'
|
||||
import './loader.css'
|
||||
|
||||
const useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect
|
||||
|
||||
/** The one way Fishies puts a picture on screen: a drop-in `<img>` that shows the Fishies
|
||||
* spinner on a glass placeholder in its own box until the file is fully downloaded and
|
||||
* decoded, then fades it in. Same element, same attributes, same box as the bare `<img>` it
|
||||
* replaces, so no surface's selectors or layout move. See fx-image.ts for the states.
|
||||
*
|
||||
* Give it a size (width/height, or a CSS box) wherever you can: that box is the placeholder,
|
||||
* and it is what keeps the layout still while the picture is on its way. */
|
||||
export default function FishiesImg({ ref, decoding, ...props }: ImgHTMLAttributes<HTMLImageElement> & { ref?: Ref<HTMLImageElement> }) {
|
||||
const own = useRef<HTMLImageElement | null>(null)
|
||||
const mounted = useRef(false)
|
||||
const setRef = useCallback((node: HTMLImageElement | null) => {
|
||||
own.current = node
|
||||
if (typeof ref === 'function') ref(node)
|
||||
else if (ref) ref.current = node
|
||||
}, [ref])
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
const img = own.current
|
||||
if (!img) return
|
||||
// A new address waits again; the document's listener (FX_IMAGE_SCRIPT) or this effect
|
||||
// reveals it. A picture already complete at first mount was in memory: no fade.
|
||||
if (mounted.current) img.setAttribute('data-fx', 'wait')
|
||||
const first = !mounted.current
|
||||
mounted.current = true
|
||||
reveal(img, first && img.complete)
|
||||
const settle = () => reveal(img)
|
||||
img.addEventListener('load', settle)
|
||||
img.addEventListener('error', settle)
|
||||
return () => { img.removeEventListener('load', settle); img.removeEventListener('error', settle) }
|
||||
}, [props.src, props.srcSet])
|
||||
return <img {...props} ref={setRef} decoding={decoding ?? 'async'} data-fx="wait" suppressHydrationWarning/>
|
||||
}
|
||||
|
||||
/** A `<video>` with a poster: render it with `data-fx={poster ? 'wait' : undefined}` and
|
||||
* `suppressHydrationWarning`, and pass its ref here. The same placeholder as FishiesImg holds
|
||||
* its box until the poster is decoded (or the first frame arrives, whichever is first). */
|
||||
export function useFxPoster(video: { current: HTMLVideoElement | null }, poster: string | null | undefined) {
|
||||
const decoded = useDecodedUrl(poster)
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
const element = video.current
|
||||
if (!element || !poster) return
|
||||
const show = () => { if (element.getAttribute('data-fx') === 'wait') element.setAttribute('data-fx', 'fade') }
|
||||
if (decoded || element.readyState >= 2) { show(); return }
|
||||
element.addEventListener('loadeddata', show)
|
||||
return () => element.removeEventListener('loadeddata', show)
|
||||
}, [video, poster, decoded])
|
||||
}
|
||||
|
||||
/** For a picture drawn as a CSS background: the address once it is downloaded and decoded,
|
||||
* `null` until then (show a FishiesSpinner meanwhile). */
|
||||
export function useDecodedUrl(url: string | null | undefined): string | null {
|
||||
const [ready, setReady] = useState<string | null>(null)
|
||||
useEffect(() => {
|
||||
if (!url) return
|
||||
let live = true
|
||||
const probe = new Image()
|
||||
probe.decoding = 'async'
|
||||
probe.src = url
|
||||
const done = () => { if (live) setReady(url) }
|
||||
;(typeof probe.decode === 'function' ? probe.decode() : new Promise(resolve => { probe.onload = resolve })).then(done, done)
|
||||
return () => { live = false }
|
||||
}, [url])
|
||||
return url && ready === url ? url : null
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue