Files
HKSingleParty/99_references/beacon-main/src/components/blurhash.tsx
2025-05-28 09:55:51 +08:00

162 lines
3.3 KiB
TypeScript

/**
* @file Blurhash component
*/
import {decode} from "blurhash";
import {FC, HTMLProps, useEffect, useRef, useState} from "react";
import {
BLURHASH_COMPONENT_X,
BLURHASH_COMPONENT_Y,
BLURHASH_PIXELS_PER_COMPONENT,
} from "~/lib/media";
import {usePersistentStore} from "~/lib/stores/persistent";
import {MediaDimensions, Theme} from "~/lib/types";
/**
* Blurhash component props
*/
interface BlurhashProps extends HTMLProps<HTMLCanvasElement> {
/**
* Ambient effect
*/
ambient: boolean;
/**
* Blurhash string
*/
hash: string;
/**
* Height of the blurhash
*/
height: number;
/**
* Width of the blurhash
*/
width: number;
}
/**
* Blurhash component
* @param props Props
* @returns JSX
*/
export const Blurhash: FC<BlurhashProps> = ({
hash,
height,
width,
ambient,
...props
}) => {
// Hooks
const canvas = useRef<HTMLCanvasElement>(null);
const [pixels, setPixels] = useState<Uint8ClampedArray | undefined>();
const [avg, setAvg] = useState<string | undefined>();
const theme = usePersistentStore(state => state.theme);
// Constants
const scaledDimensions = {
height: BLURHASH_COMPONENT_X * BLURHASH_PIXELS_PER_COMPONENT,
width: BLURHASH_COMPONENT_Y * BLURHASH_PIXELS_PER_COMPONENT,
} as MediaDimensions;
// Effects
useEffect(() => {
// Decode the blurhash
setPixels(decode(hash, scaledDimensions.width, scaledDimensions.height));
}, [hash]);
useEffect(() => {
if (ambient && pixels !== undefined) {
// Compute the average color of the bottom row
let r = 0;
let g = 0;
let b = 0;
for (let col = 0; col < scaledDimensions.width; col++) {
const index =
4 * (scaledDimensions.height - 1) * scaledDimensions.width;
r += pixels[index]!;
g += pixels[index + 1]!;
b += pixels[index + 2]!;
}
r /= scaledDimensions.width;
g /= scaledDimensions.width;
b /= scaledDimensions.width;
// Convert to hex
const hex = `#${[
r,
g,
b,
]
.map(component => component.toString(16).padStart(2, "0"))
.join("")}`;
setAvg(hex);
} else {
setAvg(undefined);
}
}, [ambient, pixels]);
useEffect(() => {
(async () => {
if (
canvas.current === null ||
height === 0 ||
width === 0 ||
pixels === undefined
) {
return;
}
// Get the context
const context = canvas.current.getContext("2d")!;
// Get the image data
const imageData = context.createImageData(
scaledDimensions.width,
scaledDimensions.height,
);
// Set the pixels
imageData.data.set(pixels);
context.putImageData(imageData, 0, 0);
// Scale the canvas
context.scale(
width / scaledDimensions.width,
height / scaledDimensions.height,
);
context.drawImage(canvas.current, 0, 0);
})();
}, [
hash,
width,
height,
]);
return (
<canvas
{...props}
style={{
...props.style,
...(avg === undefined
? {}
: {
boxShadow: `0 0 300px ${theme === Theme.DARK ? 5 : 50}px ${avg}`,
}),
}}
height={height}
width={width}
ref={canvas}
/>
);
};