2024-11-29 23:08:47 -05:00
|
|
|
import React, {useCallback, useEffect, useState, createContext} from "react";
|
2024-11-24 00:06:22 -05:00
|
|
|
import {useColorMode} from "@docusaurus/theme-common";
|
2024-12-01 18:17:36 -05:00
|
|
|
import BrowserOnly from "@docusaurus/BrowserOnly";
|
2024-11-24 00:06:22 -05:00
|
|
|
|
2024-12-01 18:17:36 -05:00
|
|
|
function invertImage(sourceImage: ImageData): ImageData {
|
|
|
|
const image = new ImageData(sourceImage.width, sourceImage.height);
|
2024-12-01 21:57:10 -05:00
|
|
|
sourceImage.data.forEach((value, index) =>
|
2024-12-01 18:17:36 -05:00
|
|
|
image.data[index] = index % 4 === 3 ? value : 0xff - value)
|
|
|
|
|
|
|
|
return image;
|
|
|
|
}
|
|
|
|
|
2024-12-02 20:01:29 -05:00
|
|
|
class RenderManager {
|
|
|
|
private isFinished: boolean = false;
|
|
|
|
private painter: Iterator<ImageData> = null;
|
|
|
|
constructor(private readonly setImage: (image: [ImageData]) => void) {
|
|
|
|
requestAnimationFrame(() => this.animate());
|
|
|
|
}
|
|
|
|
|
|
|
|
setPainter(painter: Iterator<ImageData>) {
|
|
|
|
console.log("Received next painter");
|
|
|
|
if (!this) {
|
|
|
|
console.log(this);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.painter = painter;
|
|
|
|
}
|
|
|
|
|
|
|
|
setFinished() {
|
|
|
|
console.log("Received finished");
|
|
|
|
if (!this) {
|
|
|
|
console.log(this);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.isFinished = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
animate() {
|
|
|
|
console.log("Received next frame");
|
|
|
|
if (!this) {
|
|
|
|
console.log(this);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.isFinished) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!this.painter) {
|
|
|
|
requestAnimationFrame(() => this.animate());
|
|
|
|
}
|
|
|
|
|
|
|
|
const nextImage = this.painter.next().value;
|
|
|
|
if (nextImage) {
|
|
|
|
console.log("Received next image");
|
|
|
|
this.setImage([nextImage]);
|
|
|
|
}
|
|
|
|
|
|
|
|
requestAnimationFrame(() => this.animate());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-01 18:17:36 -05:00
|
|
|
type InvertibleCanvasProps = {
|
|
|
|
width: number,
|
|
|
|
height: number,
|
|
|
|
// NOTE: Images are provided as a single-element array
|
|
|
|
//so we can allow re-painting with the same (modified) ImageData reference.
|
|
|
|
image?: [ImageData],
|
2024-11-29 23:08:47 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2024-12-01 18:17:36 -05:00
|
|
|
* Draw images to a canvas, automatically inverting colors as needed.
|
|
|
|
*
|
|
|
|
* @param width Canvas width
|
|
|
|
* @param height Canvas height
|
|
|
|
* @param hidden Hide the canvas element
|
|
|
|
* @param image Image data to draw on the canvas
|
2024-11-29 23:08:47 -05:00
|
|
|
*/
|
2024-12-01 21:57:10 -05:00
|
|
|
const InvertibleCanvas: React.FC<InvertibleCanvasProps> = ({width, height, image}) => {
|
2024-12-01 18:17:36 -05:00
|
|
|
const [canvasCtx, setCanvasCtx] = useState<CanvasRenderingContext2D>(null);
|
|
|
|
const canvasRef = useCallback(node => {
|
|
|
|
if (node !== null) {
|
|
|
|
setCanvasCtx(node.getContext("2d"));
|
|
|
|
}
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
const [paintImage, setPaintImage] = useState<[ImageData]>(null);
|
|
|
|
useEffect(() => {
|
|
|
|
if (canvasCtx && paintImage) {
|
|
|
|
canvasCtx.putImageData(paintImage[0], 0, 0);
|
|
|
|
}
|
|
|
|
}, [canvasCtx, paintImage]);
|
|
|
|
|
|
|
|
const {colorMode} = useColorMode();
|
|
|
|
useEffect(() => {
|
|
|
|
if (image) {
|
|
|
|
setPaintImage(colorMode === 'light' ? image : [invertImage(image[0])]);
|
|
|
|
}
|
|
|
|
}, [image, colorMode]);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<canvas
|
|
|
|
ref={canvasRef}
|
|
|
|
width={width}
|
|
|
|
height={height}
|
|
|
|
style={{
|
|
|
|
aspectRatio: width / height,
|
|
|
|
width: '75%'
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
type PainterProps = {
|
|
|
|
width: number;
|
|
|
|
height: number;
|
|
|
|
setPainter: (painter: Iterator<ImageData>) => void;
|
|
|
|
}
|
2024-11-29 23:08:47 -05:00
|
|
|
export const PainterContext = createContext<PainterProps>(null);
|
|
|
|
|
|
|
|
interface CanvasProps {
|
2024-11-30 18:01:29 -05:00
|
|
|
width?: number;
|
|
|
|
height?: number;
|
2024-12-01 18:17:36 -05:00
|
|
|
children?: React.ReactElement;
|
2024-11-24 00:06:22 -05:00
|
|
|
}
|
2024-11-24 18:59:11 -05:00
|
|
|
|
2024-11-29 23:08:47 -05:00
|
|
|
/**
|
|
|
|
* Draw fractal flames to a canvas.
|
|
|
|
*
|
|
|
|
* This component is a bit involved because it attempts to solve
|
2024-12-01 15:16:30 -05:00
|
|
|
* a couple problems at once:
|
2024-11-29 23:08:47 -05:00
|
|
|
* - Incrementally drawing an image to the canvas
|
2024-12-01 15:16:30 -05:00
|
|
|
* - Interrupting drawing with new parameters
|
2024-11-29 23:08:47 -05:00
|
|
|
*
|
2024-12-01 15:16:30 -05:00
|
|
|
* Running a full render is labor-intensive, so we model it
|
|
|
|
* as an iterator that yields an image of the current system.
|
|
|
|
* Internally, that iterator is re-queued on each new image;
|
2024-12-01 18:17:36 -05:00
|
|
|
* so long as retrieving each image happens quickly,
|
|
|
|
* we keep the main loop running even with CPU-heavy code.
|
|
|
|
* As a side benefit, this also animates the chaos game nicely.
|
|
|
|
* TODO(bspeice): This also causes React to complain about maximum update depth
|
|
|
|
* Would this be better off spawning a `useEffect` animator
|
|
|
|
* that has access to a `setState` queue?
|
2024-11-29 23:08:47 -05:00
|
|
|
*
|
2024-12-01 15:16:30 -05:00
|
|
|
* To interrupt drawing, children set the active iterator
|
|
|
|
* through the context provider. This component doesn't care
|
|
|
|
* about which iterator is in progress, it exists only
|
|
|
|
* to fetch the next image and paint it to our canvas.
|
2024-11-29 23:08:47 -05:00
|
|
|
*
|
2024-12-01 15:16:30 -05:00
|
|
|
* TODO(bspeice): Can we make this "re-queueing iterator" pattern generic?
|
|
|
|
* It would be nice to have iterators returning arbitrary objects,
|
2024-12-01 18:17:36 -05:00
|
|
|
* but we rely on contexts to manage the iterator, and I can't find
|
|
|
|
* a good way to make those generic.
|
2024-11-29 23:08:47 -05:00
|
|
|
*/
|
2024-12-01 21:57:10 -05:00
|
|
|
export default function Canvas({width, height, children}: CanvasProps) {
|
2024-12-01 18:17:36 -05:00
|
|
|
const [image, setImage] = useState<[ImageData]>(null);
|
2024-12-02 20:01:29 -05:00
|
|
|
const [renderManager, _setRenderManager] = useState<RenderManager>(new RenderManager(setImage));
|
|
|
|
const setPainter = (painter: Iterator<ImageData>) => renderManager.setPainter.bind(renderManager)(painter);
|
2024-11-24 22:37:53 -05:00
|
|
|
|
2024-12-02 20:01:29 -05:00
|
|
|
useEffect(() => () => { renderManager.setFinished.bind(renderManager)(); }, []);
|
2024-11-24 00:06:22 -05:00
|
|
|
|
2024-11-30 18:01:29 -05:00
|
|
|
width = width ?? 500;
|
|
|
|
height = height ?? 500;
|
2024-11-24 00:06:22 -05:00
|
|
|
return (
|
2024-11-24 18:59:11 -05:00
|
|
|
<>
|
2024-11-29 23:08:47 -05:00
|
|
|
<center>
|
2024-12-01 21:57:10 -05:00
|
|
|
<InvertibleCanvas width={width} height={height} image={image}/>
|
2024-11-29 23:08:47 -05:00
|
|
|
</center>
|
|
|
|
<PainterContext.Provider value={{width, height, setPainter}}>
|
2024-12-01 18:17:36 -05:00
|
|
|
<BrowserOnly>{() => children}</BrowserOnly>
|
2024-11-29 23:08:47 -05:00
|
|
|
</PainterContext.Provider>
|
2024-11-24 18:59:11 -05:00
|
|
|
</>
|
2024-11-24 00:06:22 -05:00
|
|
|
)
|
|
|
|
}
|