162 lines
3.3 KiB
TypeScript
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}
|
|
/>
|
|
);
|
|
};
|