etch-canvas.tsx

a dynamic canvas which renders custom etches

'use client';
import type p5 from 'p5';
import { useRef, useEffect, useState } from 'react';
import { EtchModel, LignesEtchP5, EtchDimensions } from './etches/types';
import Etches, { EtchKey } from './etches';

interface Props {
	etchId: EtchKey;
	width?: number;
	height?: number;
}

export function EtchCanvas(props: Props) {
	return (
		<EtchCanvasBase
			etch={Etches[props.etchId]}
			width={props.width}
			height={props.height}
		/>
	);
}

function EtchCanvasBase({
	etch,
	width,
	height,
}: {
	etch: EtchModel;
	width?: number;
	height?: number;
}) {
	const containerRef = useRef<HTMLDivElement>(null);
	const [p5Instance, setP5Instance] = useState<LignesEtchP5>();
	const locals = useRef<{ [key: string]: any }>({
		lock: false,
	});

	useEffect(() => {
		// Ensure we don't create multiple instances
		// if rendering concurrently.
		const hasLock = locals.current.lock;

		const createP5Instance = async () => {
			if (!hasLock && !p5Instance && containerRef.current && typeof window) {
				const p5 = (await import('p5')).default;
				locals.current.lock = true;
				const $dimensions = { height, width };

				if (!$dimensions.height || !$dimensions.width) {
					const rect = containerRef.current.getBoundingClientRect();
					$dimensions.width = rect.width;
					$dimensions.height = rect.height;
				}

				const dimensions = $dimensions as { width: number; height: number };

				const instance = new p5(
					createEtch(dimensions, etch),
					containerRef.current
				);

				// On window resize, also resize the canvas
				// so it always fits within the container
				instance.windowResized = () => {
					if (containerRef.current) {
						const rect = containerRef.current.getBoundingClientRect();
						instance.resizeCanvas(rect.width, rect.height);
						// @ts-ignore - custom method
						instance.onResize && instance.onResize();
					}
				};

				setP5Instance(instance);
			}
		};

		createP5Instance();

		return () => {
			p5Instance?.exit && p5Instance.exit();
			p5Instance?.remove();
		};
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, [height, width, etch]);

	return <div style={{ width: '100%', height: '100%' }} ref={containerRef} />;
}

function createEtch(params: EtchDimensions, model: EtchModel) {
	return (p: p5) => model.render({ p, ...params });
}