mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-06-23 15:30:34 -04:00
Use a canvas on the front to draw blurhash
This commit is contained in:
parent
1a92094eaf
commit
9409197766
@ -19,7 +19,17 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { decode } from "blurhash";
|
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 { useYoshiki } from "yoshiki";
|
||||||
import { Stylable, nativeStyleToCss } from "yoshiki/native";
|
import { Stylable, nativeStyleToCss } from "yoshiki/native";
|
||||||
import { StyleList, processStyleList } from "yoshiki/src/type";
|
import { StyleList, processStyleList } from "yoshiki/src/type";
|
||||||
@ -191,28 +201,102 @@ function generatePng(width: number, height: number, rgbaString: string) {
|
|||||||
return pngString;
|
return pngString;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BlurhashContainer = ({
|
const BlurhashCanvas = forwardRef<
|
||||||
blurhash,
|
HTMLCanvasElement,
|
||||||
blurhashUrl,
|
{
|
||||||
children,
|
blurhash: string;
|
||||||
...props
|
} & HTMLAttributes<HTMLCanvasElement>
|
||||||
}: {
|
>(function BlurhashCanvas({ blurhash, ...props }, forwardedRef) {
|
||||||
blurhash?: string;
|
const ref = useRef<HTMLCanvasElement>(null);
|
||||||
blurhashUrl?: string;
|
const { css } = useYoshiki();
|
||||||
children?: ReactElement | ReactElement[];
|
|
||||||
}) => {
|
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();
|
const { css } = useYoshiki();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={ref}
|
||||||
style={{
|
style={{
|
||||||
// Use a blurhash here to nicely fade the NextImage when it is loaded completly
|
// 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)
|
// (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",
|
backgroundSize: "cover",
|
||||||
backgroundRepeat: "no-repeat",
|
backgroundRepeat: "no-repeat",
|
||||||
backgroundPosition: "50% 50%",
|
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(
|
{...css(
|
||||||
{
|
{
|
||||||
// To reproduce view's behavior
|
// To reproduce view's behavior
|
||||||
@ -223,6 +307,11 @@ export const BlurhashContainer = ({
|
|||||||
nativeStyleToCss(props),
|
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}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -37,13 +37,9 @@ export const Image = ({
|
|||||||
}: Props & { style?: ImageStyle } & { layout: ImageLayout }) => {
|
}: Props & { style?: ImageStyle } & { layout: ImageLayout }) => {
|
||||||
const { css } = useYoshiki();
|
const { css } = useYoshiki();
|
||||||
const [state, setState] = useState<"loading" | "errored" | "finished">(
|
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;
|
const border = { borderRadius: 6 } satisfies ViewStyle;
|
||||||
|
|
||||||
if (forcedLoading) return <Skeleton variant="custom" {...css([layout, border], props)} />;
|
if (forcedLoading) return <Skeleton variant="custom" {...css([layout, border], props)} />;
|
||||||
@ -55,9 +51,8 @@ export const Image = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const blurhashUrl = blurHashToDataURL(src.blurhash);
|
|
||||||
return (
|
return (
|
||||||
<BlurhashContainer blurhashUrl={blurhashUrl} {...css([layout, border], props)}>
|
<BlurhashContainer blurhash={src.blurhash} {...css([layout, border], props)}>
|
||||||
<NextImage
|
<NextImage
|
||||||
src={src[quality ?? "high"]}
|
src={src[quality ?? "high"]}
|
||||||
priority={quality === "high"}
|
priority={quality === "high"}
|
||||||
@ -68,12 +63,11 @@ export const Image = ({
|
|||||||
opacity: state === "loading" ? 0 : 1,
|
opacity: state === "loading" ? 0 : 1,
|
||||||
transition: "opacity .2s ease-out",
|
transition: "opacity .2s ease-out",
|
||||||
}}
|
}}
|
||||||
blurDataURL={blurhashUrl}
|
|
||||||
placeholder="blur"
|
|
||||||
// Don't use next's server to reprocess images, they are already optimized by kyoo.
|
// Don't use next's server to reprocess images, they are already optimized by kyoo.
|
||||||
unoptimized={true}
|
unoptimized={true}
|
||||||
onLoad={() => setState("finished")}
|
onLoad={() => setState("finished")}
|
||||||
onError={() => setState("errored")}
|
onError={() => setState("errored")}
|
||||||
|
suppressHydrationWarning
|
||||||
/>
|
/>
|
||||||
</BlurhashContainer>
|
</BlurhashContainer>
|
||||||
);
|
);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user