mirror of
https://github.com/bspeice/speice.io
synced 2025-07-29 03:25:03 -04:00
Auto-sizing canvas, starting cleanup for display on mobile browsers
This commit is contained in:
@ -1,138 +1,68 @@
|
||||
import React, {useCallback, useEffect, useState, createContext, useRef} from "react";
|
||||
import React, {useEffect, useState, createContext, useRef} from "react";
|
||||
import {useColorMode} from "@docusaurus/theme-common";
|
||||
import BrowserOnly from "@docusaurus/BrowserOnly";
|
||||
|
||||
function invertImage(sourceImage: ImageData): ImageData {
|
||||
const image = new ImageData(sourceImage.width, sourceImage.height);
|
||||
sourceImage.data.forEach((value, index) =>
|
||||
image.data[index] = index % 4 === 3 ? value : 0xff - value)
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
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],
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export const InvertibleCanvas: React.FC<InvertibleCanvasProps> = ({width, height, image}) => {
|
||||
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}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
type PainterProps = {
|
||||
width: number;
|
||||
height: number;
|
||||
setPainter: (painter: Iterator<ImageData>) => void;
|
||||
}
|
||||
export const PainterContext = createContext<PainterProps>(null);
|
||||
export const PainterContext = createContext<PainterProps>(null)
|
||||
|
||||
interface CanvasProps {
|
||||
width?: number;
|
||||
height?: number;
|
||||
children?: React.ReactElement;
|
||||
type CanvasProps = {
|
||||
style?: any;
|
||||
children?: React.ReactElement
|
||||
}
|
||||
export const Canvas: React.FC<CanvasProps> = ({style, children}) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
/**
|
||||
* Draw fractal flames to a canvas.
|
||||
*
|
||||
* This component is a bit involved because it attempts to solve
|
||||
* a couple problems at once:
|
||||
* - Incrementally drawing an image to the canvas
|
||||
* - Interrupting drawing with new parameters
|
||||
*
|
||||
* 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;
|
||||
* 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?
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* TODO(bspeice): Can we make this "re-queueing iterator" pattern generic?
|
||||
* It would be nice to have iterators returning arbitrary objects,
|
||||
* but we rely on contexts to manage the iterator, and I can't find
|
||||
* a good way to make those generic.
|
||||
*/
|
||||
export default function Canvas({width, height, children}: CanvasProps) {
|
||||
const viewportDetectionRef = useRef<HTMLDivElement>(null);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
useEffect(() => {
|
||||
if (!viewportDetectionRef) {
|
||||
if (!canvasRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(([entry]) => {
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
const [entry] = entries;
|
||||
if (entry.isIntersecting) {
|
||||
setIsVisible(true);
|
||||
}
|
||||
}, {root: null, threshold: .1});
|
||||
observer.observe(viewportDetectionRef.current);
|
||||
});
|
||||
observer.observe(canvasRef.current);
|
||||
|
||||
return () => {
|
||||
if (viewportDetectionRef.current) {
|
||||
observer.unobserve(viewportDetectionRef.current);
|
||||
if (canvasRef.current) {
|
||||
observer.unobserve(canvasRef.current);
|
||||
}
|
||||
}
|
||||
}, [viewportDetectionRef]);
|
||||
}, [canvasRef]);
|
||||
|
||||
const [width, setWidth] = useState(0);
|
||||
const [height, setHeight] = useState(0);
|
||||
useEffect(() => {
|
||||
if (canvasRef.current) {
|
||||
setWidth(canvasRef.current.offsetWidth);
|
||||
setHeight(canvasRef.current.offsetHeight);
|
||||
}
|
||||
}, [canvasRef]);
|
||||
|
||||
const [imageHolder, setImageHolder] = useState<[ImageData]>(null);
|
||||
useEffect(() => {
|
||||
if (canvasRef.current && imageHolder) {
|
||||
canvasRef.current.getContext("2d").putImageData(imageHolder[0], 0, 0);
|
||||
}
|
||||
}, [canvasRef, imageHolder]);
|
||||
|
||||
const [image, setImage] = useState<[ImageData]>(null);
|
||||
const [painterHolder, setPainterHolder] = useState<[Iterator<ImageData>]>(null);
|
||||
useEffect(() => {
|
||||
if (!isVisible || !painterHolder) {
|
||||
console.log("Skipping, not visible");
|
||||
return;
|
||||
}
|
||||
|
||||
const painter = painterHolder[0];
|
||||
const nextImage = painter.next().value;
|
||||
if (nextImage) {
|
||||
setImage([nextImage]);
|
||||
setImageHolder([nextImage]);
|
||||
setPainterHolder([painter]);
|
||||
} else {
|
||||
setPainterHolder(null);
|
||||
@ -146,18 +76,25 @@ export default function Canvas({width, height, children}: CanvasProps) {
|
||||
}
|
||||
}, [painter]);
|
||||
|
||||
width = width ?? 500;
|
||||
height = height ?? 500;
|
||||
const {colorMode} = useColorMode();
|
||||
return (
|
||||
<>
|
||||
<center>
|
||||
<div ref={viewportDetectionRef}>
|
||||
<InvertibleCanvas width={width} height={height} image={image}/>
|
||||
</div>
|
||||
</center>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
width={width}
|
||||
height={height}
|
||||
style={{
|
||||
filter: colorMode === 'dark' ? 'invert(1)' : '',
|
||||
...style
|
||||
}}
|
||||
/>
|
||||
<PainterContext.Provider value={{width, height, setPainter}}>
|
||||
<BrowserOnly>{() => children}</BrowserOnly>
|
||||
</PainterContext.Provider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const SquareCanvas: React.FC<CanvasProps> = ({style, children}) => {
|
||||
return <Canvas style={{width: '100%', aspectRatio: '1/1', ...style}} children={children}/>
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
.inputGroup {
|
||||
padding: .5em;
|
||||
margin: .5em;
|
||||
padding: .2em;
|
||||
margin-top: .2em;
|
||||
margin-bottom: .2em;
|
||||
border: 1px solid;
|
||||
border-radius: var(--ifm-global-radius);
|
||||
border-color: var(--ifm-color-emphasis-500);
|
||||
|
@ -1,15 +1,20 @@
|
||||
export function randomChoice<T>(choices: [number, T][]): [number, T] {
|
||||
const weightSum = choices.reduce((sum, [weight, _]) => sum + weight, 0);
|
||||
let choice = Math.random() * weightSum;
|
||||
export function randomChoice<T>(
|
||||
choices: [number, T][]
|
||||
): [number, T] {
|
||||
const weightSum = choices.reduce(
|
||||
(sum, [weight, _]) => sum + weight,
|
||||
0
|
||||
);
|
||||
let choice = Math.random() * weightSum;
|
||||
|
||||
for (const [index, element] of choices.entries()) {
|
||||
const [weight, t] = element;
|
||||
if (choice < weight) {
|
||||
return [index, t];
|
||||
}
|
||||
choice -= weight;
|
||||
for (const [idx, elem] of choices.entries()) {
|
||||
const [weight, t] = elem;
|
||||
if (choice < weight) {
|
||||
return [idx, t];
|
||||
}
|
||||
choice -= weight;
|
||||
}
|
||||
|
||||
const index = choices.length - 1;
|
||||
return [index, choices[index][1]];
|
||||
const index = choices.length - 1;
|
||||
return [index, choices[index][1]];
|
||||
}
|
@ -1,3 +1,7 @@
|
||||
export function randomInteger(min: number, max: number) {
|
||||
return Math.floor(Math.random() * (max - min)) + min;
|
||||
export function randomInteger(
|
||||
min: number,
|
||||
max: number
|
||||
) {
|
||||
let v = Math.random() * (max - min);
|
||||
return Math.floor(v) + min;
|
||||
}
|
Reference in New Issue
Block a user