u
This commit is contained in:
94
src/components/settings/SkeleHashImage.jsx
Normal file
94
src/components/settings/SkeleHashImage.jsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import { PureComponent, createRef } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import { decodeBase62 } from 'skelehash-wasm';
|
||||
|
||||
// Module-level cache so re-renders / re-mounts don't re-run the WASM decoder
|
||||
// for the same SkeleHash. Stores a Promise resolving to the decoded payload.
|
||||
const decodeCache = new Map(); // base62 -> Promise<{ rgba, metadata } | null>
|
||||
|
||||
function decodeSkeleHash(base62) {
|
||||
if (!base62) return Promise.resolve(null);
|
||||
let cached = decodeCache.get(base62);
|
||||
if (!cached) {
|
||||
cached = decodeBase62(base62).catch(() => null);
|
||||
decodeCache.set(base62, cached);
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a SkeleHash (Base62) as an RGBA canvas placeholder.
|
||||
* Mirrors the synchronous-read / fine-grained styling approach of ThumbHashImage.
|
||||
*/
|
||||
export default class SkeleHashImage extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { ready: false };
|
||||
this.canvasRef = createRef();
|
||||
this.requestId = 0;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.draw();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.hash !== this.props.hash) {
|
||||
this.draw();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// Invalidate any in-flight decode so it doesn't touch the unmounted canvas.
|
||||
this.requestId += 1;
|
||||
}
|
||||
|
||||
async draw() {
|
||||
const { hash } = this.props;
|
||||
const requestId = ++this.requestId;
|
||||
|
||||
this.setState({ ready: false });
|
||||
const decoded = await decodeSkeleHash(hash);
|
||||
|
||||
// Stale request (hash changed or component unmounted) — bail out.
|
||||
if (requestId !== this.requestId) return;
|
||||
const canvas = this.canvasRef.current;
|
||||
if (!canvas || !decoded?.rgba || !decoded?.metadata) return;
|
||||
|
||||
const { rgba, metadata } = decoded;
|
||||
const width = metadata.outputWidth;
|
||||
const height = metadata.outputHeight;
|
||||
if (!width || !height) return;
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const clamped = new Uint8ClampedArray(rgba.buffer, rgba.byteOffset, rgba.byteLength);
|
||||
ctx.putImageData(new ImageData(clamped, width, height), 0, 0);
|
||||
|
||||
this.setState({ ready: true });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { sx, hash } = this.props;
|
||||
const { ready } = this.state;
|
||||
|
||||
return (
|
||||
<Box
|
||||
component="canvas"
|
||||
ref={this.canvasRef}
|
||||
aria-hidden
|
||||
data-skelehash={hash || undefined}
|
||||
sx={{
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
opacity: ready ? 1 : 0,
|
||||
transition: 'opacity 200ms ease',
|
||||
...sx,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user