15 changed files with 155 additions and 50 deletions
@ -1,35 +1,92 @@ |
|||||||
import { Skeleton } from '@/components/ui/skeleton' |
import { Skeleton } from '@/components/ui/skeleton' |
||||||
import { cn } from '@/lib/utils' |
import { cn } from '@/lib/utils' |
||||||
import { HTMLAttributes, useState } from 'react' |
import { TImageInfo } from '@/types' |
||||||
|
import { decode } from 'blurhash' |
||||||
|
import { HTMLAttributes, useEffect, useMemo, useState } from 'react' |
||||||
|
|
||||||
export default function Image({ |
export default function Image({ |
||||||
src, |
image: { url, blurHash, dim }, |
||||||
alt, |
alt, |
||||||
className = '', |
className = '', |
||||||
classNames = {}, |
classNames = {}, |
||||||
...props |
...props |
||||||
}: HTMLAttributes<HTMLDivElement> & { |
}: HTMLAttributes<HTMLDivElement> & { |
||||||
src: string |
image: TImageInfo |
||||||
alt?: string |
alt?: string |
||||||
classNames?: { |
classNames?: { |
||||||
wrapper?: string |
wrapper?: string |
||||||
} |
} |
||||||
}) { |
}) { |
||||||
const [isLoading, setIsLoading] = useState(true) |
const [isLoading, setIsLoading] = useState(true) |
||||||
|
const [displayBlurHash, setDisplayBlurHash] = useState(true) |
||||||
|
const [blurDataUrl, setBlurDataUrl] = useState<string | null>(null) |
||||||
|
const { width, height } = useMemo<{ width?: number; height?: number }>(() => { |
||||||
|
if (dim) { |
||||||
|
return dim |
||||||
|
} |
||||||
|
if (blurHash) { |
||||||
|
const { numX, numY } = decodeBlurHashSize(blurHash) |
||||||
|
return { width: numX * 10, height: numY * 10 } |
||||||
|
} |
||||||
|
return {} |
||||||
|
}, [dim]) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (blurHash) { |
||||||
|
const pixels = decode(blurHash, 32, 32) |
||||||
|
const canvas = document.createElement('canvas') |
||||||
|
canvas.width = 32 |
||||||
|
canvas.height = 32 |
||||||
|
const ctx = canvas.getContext('2d') |
||||||
|
if (ctx) { |
||||||
|
const imageData = ctx.createImageData(32, 32) |
||||||
|
imageData.data.set(pixels) |
||||||
|
ctx.putImageData(imageData, 0, 0) |
||||||
|
setBlurDataUrl(canvas.toDataURL()) |
||||||
|
} |
||||||
|
} |
||||||
|
}, [blurHash]) |
||||||
|
|
||||||
return ( |
return ( |
||||||
<div className={cn('relative', classNames.wrapper ?? '')} {...props}> |
<div className={cn('relative', classNames.wrapper ?? '')} {...props}> |
||||||
{isLoading && <Skeleton className={cn('absolute inset-0', className)} />} |
{isLoading && <Skeleton className={cn('absolute inset-0', className)} />} |
||||||
<img |
<img |
||||||
src={src} |
src={url} |
||||||
alt={alt} |
alt={alt} |
||||||
className={cn( |
className={cn( |
||||||
'object-cover transition-opacity duration-700', |
'object-cover transition-opacity duration-700', |
||||||
isLoading ? 'opacity-0' : 'opacity-100', |
isLoading ? 'opacity-0' : 'opacity-100', |
||||||
className |
className |
||||||
)} |
)} |
||||||
onLoad={() => setIsLoading(false)} |
onLoad={() => { |
||||||
|
setIsLoading(false) |
||||||
|
setTimeout(() => setDisplayBlurHash(false), 1000) |
||||||
|
}} |
||||||
/> |
/> |
||||||
|
{displayBlurHash && blurDataUrl && ( |
||||||
|
<img |
||||||
|
src={blurDataUrl} |
||||||
|
className={cn('absolute inset-0 object-cover -z-10', className)} |
||||||
|
alt={alt} |
||||||
|
width={width} |
||||||
|
height={height} |
||||||
|
/> |
||||||
|
)} |
||||||
</div> |
</div> |
||||||
) |
) |
||||||
} |
} |
||||||
|
|
||||||
|
const DIGITS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~' |
||||||
|
function decodeBlurHashSize(blurHash: string) { |
||||||
|
const sizeFlag = blurHash.charAt(0) |
||||||
|
|
||||||
|
const sizeValue = DIGITS.indexOf(sizeFlag) |
||||||
|
|
||||||
|
const numY = Math.floor(sizeValue / 9) + 1 |
||||||
|
const numX = (sizeValue % 9) + 1 |
||||||
|
|
||||||
|
return { |
||||||
|
numX, |
||||||
|
numY |
||||||
|
} |
||||||
|
} |
||||||
|
|||||||
Loading…
Reference in new issue