Use a canvas on the front to draw blurhash

This commit is contained in:
Zoe Roux 2023-11-08 15:48:29 +01:00
parent 1a92094eaf
commit 9409197766
2 changed files with 104 additions and 21 deletions

View File

@ -19,7 +19,17 @@
*/
import { decode } from "blurhash";
import { ReactElement } from "react";
import {
HTMLAttributes,
ReactElement,
createElement,
forwardRef,
useEffect,
useImperativeHandle,
useLayoutEffect,
useRef,
useState,
} from "react";
import { useYoshiki } from "yoshiki";
import { Stylable, nativeStyleToCss } from "yoshiki/native";
import { StyleList, processStyleList } from "yoshiki/src/type";
@ -191,28 +201,102 @@ function generatePng(width: number, height: number, rgbaString: string) {
return pngString;
}
export const BlurhashContainer = ({
blurhash,
blurhashUrl,
children,
...props
}: {
blurhash?: string;
blurhashUrl?: string;
children?: ReactElement | ReactElement[];
}) => {
const BlurhashCanvas = forwardRef<
HTMLCanvasElement,
{
blurhash: string;
} & HTMLAttributes<HTMLCanvasElement>
>(function BlurhashCanvas({ blurhash, ...props }, forwardedRef) {
const ref = useRef<HTMLCanvasElement>(null);
const { css } = useYoshiki();
useImperativeHandle(forwardedRef, () => ref.current!, []);
useLayoutEffect(() => {
if (!ref.current) return;
const pixels = decode(blurhash, 32, 32);
const ctx = ref.current.getContext("2d");
if (!ctx) return;
const imageData = ctx.createImageData(32, 32);
imageData.data.set(pixels);
ctx.putImageData(imageData, 0, 0);
}, [blurhash]);
return (
<canvas
ref={ref}
width={32}
height={32}
{...css(
{
position: "absolute",
top: 0,
bottom: 0,
left: 0,
right: 0,
width: "100%",
height: "100%",
},
props,
)}
/>
);
});
const BlurhashDiv = forwardRef<
HTMLDivElement,
{ blurhash: string } & HTMLAttributes<HTMLDivElement>
>(function BlurhashDiv({ blurhash, ...props }, ref) {
const { css } = useYoshiki();
return (
<div
ref={ref}
style={{
// Use a blurhash here to nicely fade the NextImage when it is loaded completly
// (this prevents loading the image line by line which is ugly and buggy on firefox)
backgroundImage: `url(${blurhashUrl ?? blurHashToDataURL(blurhash)})`,
backgroundImage: `url(${blurHashToDataURL(blurhash)})`,
backgroundSize: "cover",
backgroundRepeat: "no-repeat",
backgroundPosition: "50% 50%",
}}
{...css(
{
position: "absolute",
top: 0,
bottom: 0,
left: 0,
right: 0,
width: "100%",
height: "100%",
},
props,
)}
/>
);
});
export const BlurhashContainer = ({
blurhash,
children,
...props
}: {
blurhash: string;
children?: ReactElement | ReactElement[];
}) => {
const { css } = useYoshiki();
const ref = useRef<HTMLCanvasElement & HTMLDivElement>(null);
const [renderType, setRenderType] = useState<"ssr" | "hydratation" | "client">(
typeof window === "undefined" ? "ssr" : "hydratation",
);
useLayoutEffect(() => {
// If the html is empty, it was not SSRed.
if (ref.current?.innerHTML === '') setRenderType("client");
}, []);
return (
<div
{...css(
{
// To reproduce view's behavior
@ -223,6 +307,11 @@ export const BlurhashContainer = ({
nativeStyleToCss(props),
)}
>
{renderType === "ssr" && <BlurhashDiv ref={ref} blurhash={blurhash} />}
{renderType === "client" && <BlurhashCanvas ref={ref} blurhash={blurhash} />}
{renderType === "hydratation" && (
<div ref={ref} dangerouslySetInnerHTML={{ __html: "" }} suppressHydrationWarning />
)}
{children}
</div>
);

View File

@ -37,13 +37,9 @@ export const Image = ({
}: Props & { style?: ImageStyle } & { layout: ImageLayout }) => {
const { css } = useYoshiki();
const [state, setState] = useState<"loading" | "errored" | "finished">(
src ? "finished" : "errored",
src ? (typeof window === "undefined" ? "finished" : "loading") : "errored",
);
useLayoutEffect(() => {
setState("loading");
}, []);
const border = { borderRadius: 6 } satisfies ViewStyle;
if (forcedLoading) return <Skeleton variant="custom" {...css([layout, border], props)} />;
@ -55,9 +51,8 @@ export const Image = ({
);
}
const blurhashUrl = blurHashToDataURL(src.blurhash);
return (
<BlurhashContainer blurhashUrl={blurhashUrl} {...css([layout, border], props)}>
<BlurhashContainer blurhash={src.blurhash} {...css([layout, border], props)}>
<NextImage
src={src[quality ?? "high"]}
priority={quality === "high"}
@ -68,12 +63,11 @@ export const Image = ({
opacity: state === "loading" ? 0 : 1,
transition: "opacity .2s ease-out",
}}
blurDataURL={blurhashUrl}
placeholder="blur"
// Don't use next's server to reprocess images, they are already optimized by kyoo.
unoptimized={true}
onLoad={() => setState("finished")}
onError={() => setState("errored")}
suppressHydrationWarning
/>
</BlurhashContainer>
);