mirror of
https://github.com/bspeice/speice.io
synced 2024-12-22 16:48:10 -05:00
Checkpoint for histogram
It takes a lot of render time to get a usable result, and it's not that interesting. Committing so I can save the work if I want to revisit it, but abandoning the idea for now.
This commit is contained in:
parent
06069fdcea
commit
79b66337e8
@ -198,8 +198,7 @@ import chaosGameWeightedSource from "!!raw-loader!./chaosGameWeighted";
|
|||||||
|
|
||||||
<CodeBlock language={'typescript'}>{chaosGameWeightedSource}</CodeBlock>
|
<CodeBlock language={'typescript'}>{chaosGameWeightedSource}</CodeBlock>
|
||||||
|
|
||||||
import BrowserOnly from "@docusaurus/BrowserOnly";
|
|
||||||
import GasketWeighted from "./GasketWeighted";
|
import GasketWeighted from "./GasketWeighted";
|
||||||
import Canvas from "../src/Canvas";
|
import Canvas from "../src/Canvas";
|
||||||
|
|
||||||
<Canvas><BrowserOnly>{() => <GasketWeighted/>}</BrowserOnly></Canvas>
|
<Canvas><GasketWeighted/></Canvas>
|
@ -24,6 +24,12 @@ import {VariationEditor, VariationProps} from "./VariationEditor";
|
|||||||
import {CoefEditor} from "./CoefEditor";
|
import {CoefEditor} from "./CoefEditor";
|
||||||
import {Transform} from "../src/transform";
|
import {Transform} from "../src/transform";
|
||||||
|
|
||||||
|
export const transforms: [number, Transform][] = [
|
||||||
|
[xform1Weight, transformPost(buildTransform(xform1Coefs, xform1Variations), xform1CoefsPost)],
|
||||||
|
[xform2Weight, transformPost(buildTransform(xform2Coefs, xform2Variations), xform2CoefsPost)],
|
||||||
|
[xform3Weight, transformPost(buildTransform(xform3Coefs, xform3Variations), xform3CoefsPost)]
|
||||||
|
];
|
||||||
|
|
||||||
export default function FlameFinal() {
|
export default function FlameFinal() {
|
||||||
const {width, height, setPainter} = useContext(PainterContext);
|
const {width, height, setPainter} = useContext(PainterContext);
|
||||||
|
|
||||||
@ -43,12 +49,6 @@ export default function FlameFinal() {
|
|||||||
const resetXformFinalCoefsPost = () => setXformFinalCoefsPost(xformFinalCoefsPostDefault);
|
const resetXformFinalCoefsPost = () => setXformFinalCoefsPost(xformFinalCoefsPostDefault);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const transforms: [number, Transform][] = [
|
|
||||||
[xform1Weight, transformPost(buildTransform(xform1Coefs, xform1Variations), xform1CoefsPost)],
|
|
||||||
[xform2Weight, transformPost(buildTransform(xform2Coefs, xform2Variations), xform2CoefsPost)],
|
|
||||||
[xform3Weight, transformPost(buildTransform(xform3Coefs, xform3Variations), xform3CoefsPost)]
|
|
||||||
];
|
|
||||||
|
|
||||||
const finalBlend = buildBlend(xformFinalCoefs, xformFinalVariations);
|
const finalBlend = buildBlend(xformFinalCoefs, xformFinalVariations);
|
||||||
const finalTransform = buildTransform(xformFinalCoefs, finalBlend);
|
const finalTransform = buildTransform(xformFinalCoefs, finalBlend);
|
||||||
const finalPost = transformPost(finalTransform, xformFinalCoefsPost);
|
const finalPost = transformPost(finalTransform, xformFinalCoefsPost);
|
||||||
|
@ -142,11 +142,10 @@ The sliders below change the variation weights for each transform (the $v_{ij}$
|
|||||||
try changing them around to see which parts of the image are controlled by
|
try changing them around to see which parts of the image are controlled by
|
||||||
each transform.
|
each transform.
|
||||||
|
|
||||||
import BrowserOnly from "@docusaurus/BrowserOnly";
|
|
||||||
import Canvas from "../src/Canvas";
|
import Canvas from "../src/Canvas";
|
||||||
import FlameBlend from "./FlameBlend";
|
import FlameBlend from "./FlameBlend";
|
||||||
|
|
||||||
<Canvas><BrowserOnly>{() => <FlameBlend/>}</BrowserOnly></Canvas>
|
<Canvas><FlameBlend/></Canvas>
|
||||||
|
|
||||||
## Post transforms
|
## Post transforms
|
||||||
|
|
||||||
@ -161,10 +160,10 @@ $$
|
|||||||
|
|
||||||
import FlamePost from "./FlamePost";
|
import FlamePost from "./FlamePost";
|
||||||
|
|
||||||
<Canvas><BrowserOnly>{() => <FlamePost/>}</BrowserOnly></Canvas>
|
<Canvas><FlamePost/></Canvas>
|
||||||
|
|
||||||
## Final transform
|
## Final transform
|
||||||
|
|
||||||
import FlameFinal from "./FlameFinal";
|
import FlameFinal from "./FlameFinal";
|
||||||
|
|
||||||
<Canvas><BrowserOnly>{() => <FlameFinal/>}</BrowserOnly></Canvas>
|
<Canvas><FlameFinal/></Canvas>
|
@ -1,5 +1,30 @@
|
|||||||
import {VictoryArea} from "victory";
|
import {VictoryChart, VictoryLine, VictoryScatter, VictoryTheme} from "victory";
|
||||||
|
import {useContext, useEffect, useState} from "react";
|
||||||
|
import {PainterContext} from "../src/Canvas";
|
||||||
|
import {chaosGameHistogram} from "./chaosGameHistogram";
|
||||||
|
import {PlotData, plotHistogram} from "./plotHistogram";
|
||||||
|
|
||||||
function F() {
|
function* plotChaosGame(width: number, height: number, setPdf: (data: PlotData) => void, setCdf: (data: PlotData) => void) {
|
||||||
return <VictoryArea data={}
|
const emptyImage = new ImageData(width, height);
|
||||||
|
for (let histogram of chaosGameHistogram(width, height)) {
|
||||||
|
const plotData = plotHistogram(histogram);
|
||||||
|
setPdf(plotData);
|
||||||
|
yield emptyImage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FlameHistogram() {
|
||||||
|
const {width, height, setPainter} = useContext(PainterContext);
|
||||||
|
const [pdfData, setPdfData] = useState<{ x: number, y: number }[]>(null);
|
||||||
|
|
||||||
|
useEffect(() => setPainter(plotChaosGame(width, height, setPdfData, null)), []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VictoryChart theme={VictoryTheme.clean}>
|
||||||
|
<VictoryLine
|
||||||
|
data={pdfData}
|
||||||
|
interpolation='natural'
|
||||||
|
/>
|
||||||
|
</VictoryChart>
|
||||||
|
)
|
||||||
}
|
}
|
@ -1,35 +0,0 @@
|
|||||||
// hidden-start
|
|
||||||
import {VictoryChart} from "victory";
|
|
||||||
import {camera, histIndex} from "../src/camera";
|
|
||||||
// hidden-end
|
|
||||||
export class PlotHistogram {
|
|
||||||
public readonly pixels: Uint32Array;
|
|
||||||
|
|
||||||
public constructor(private readonly width: number, height: number) {
|
|
||||||
this.pixels = new Uint32Array(width * height);
|
|
||||||
}
|
|
||||||
|
|
||||||
public plot(x: number, y: number) {
|
|
||||||
const [pixelX, pixelY] = camera(x, y, this.width);
|
|
||||||
const pixelIndex = histIndex(pixelX, pixelY, this.width, 1);
|
|
||||||
this.pixels[pixelIndex] += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getHistogram() {
|
|
||||||
const data = new Map<number, number>();
|
|
||||||
this.pixels.forEach(value => {
|
|
||||||
const bucket = 32 - Math.clz32(value);
|
|
||||||
|
|
||||||
if (bucket in data) {
|
|
||||||
data[bucket] += 1;
|
|
||||||
} else {
|
|
||||||
data[bucket] = 1;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const output: {x: number, y: number}[] = [];
|
|
||||||
data.forEach((bucket, value) =>
|
|
||||||
output.push({x: Math.pow(bucket, 2), y: value}));
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,34 @@
|
|||||||
|
import {plot} from "./plotHistogram";
|
||||||
|
import {randomBiUnit} from "../src/randomBiUnit";
|
||||||
|
import {randomChoice} from "../src/randomChoice";
|
||||||
|
import {buildTransform} from "../2-transforms/buildTransform";
|
||||||
|
import {transformPost} from "../2-transforms/post";
|
||||||
|
import {transforms} from "../2-transforms/FlameFinal";
|
||||||
|
import * as params from "../src/params";
|
||||||
|
|
||||||
|
const finalTransform = buildTransform(params.xformFinalCoefs, params.xformFinalVariations);
|
||||||
|
const finalTransformPost = transformPost(finalTransform, params.xformFinalCoefsPost);
|
||||||
|
|
||||||
|
const step = 1000;
|
||||||
|
const quality = 1;
|
||||||
|
|
||||||
|
export function* chaosGameHistogram(width: number, height: number) {
|
||||||
|
let iterations = quality * width * height;
|
||||||
|
let histogram = new Uint32Array(width * height);
|
||||||
|
|
||||||
|
let [x, y] = [randomBiUnit(), randomBiUnit()];
|
||||||
|
|
||||||
|
for (let i = 0; i < iterations; i++) {
|
||||||
|
const [_, transform] = randomChoice(transforms);
|
||||||
|
[x, y] = transform(x, y);
|
||||||
|
[x, y] = finalTransformPost(x, y);
|
||||||
|
|
||||||
|
if (i > 20)
|
||||||
|
plot(x, y, width, histogram);
|
||||||
|
|
||||||
|
if (i % step === 0)
|
||||||
|
yield histogram;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield histogram;
|
||||||
|
}
|
@ -23,6 +23,13 @@ We'll render the reference image again, but this time, counting the times
|
|||||||
we tried to turn on a pixel.
|
we tried to turn on a pixel.
|
||||||
|
|
||||||
import CodeBlock from "@theme/CodeBlock";
|
import CodeBlock from "@theme/CodeBlock";
|
||||||
import plotHistogramSource from "!!raw-loader!./PlotHistogram";
|
import plotHistogramSource from "!!raw-loader!./plotHistogram";
|
||||||
|
|
||||||
<CodeBlock language="typescript">{plotHistogramSource}</CodeBlock>
|
<CodeBlock language="typescript">{plotHistogramSource}</CodeBlock>
|
||||||
|
|
||||||
|
import Canvas from "../src/Canvas";
|
||||||
|
import FlameHistogram from "./FlameHistogram";
|
||||||
|
|
||||||
|
<Canvas width={400} height={400} hidden={true}>
|
||||||
|
<FlameHistogram/>
|
||||||
|
</Canvas>
|
@ -0,0 +1,23 @@
|
|||||||
|
// hidden-start
|
||||||
|
import {camera, histIndex} from "../src/camera";
|
||||||
|
// hidden-end
|
||||||
|
export function plot(x: number, y: number, width: number, hitCount: Uint32Array) {
|
||||||
|
const [pixelX, pixelY] = camera(x, y, width);
|
||||||
|
const pixelIndex = histIndex(pixelX, pixelY, width, 1);
|
||||||
|
hitCount[pixelIndex] += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PlotData = {x: number, y: number}[];
|
||||||
|
export function plotHistogram(hitCount: Uint32Array) {
|
||||||
|
const data = new Map<number, number>();
|
||||||
|
hitCount.forEach(value => {
|
||||||
|
const bucket = 32 - Math.clz32(value);
|
||||||
|
const currentCount = data.get(bucket) ?? 0;
|
||||||
|
data.set(bucket, currentCount + 1);
|
||||||
|
})
|
||||||
|
|
||||||
|
const output: PlotData = [];
|
||||||
|
data.forEach((value, bucket) =>
|
||||||
|
output.push({x: Math.pow(2, bucket), y: value}));
|
||||||
|
return output;
|
||||||
|
}
|
@ -1,23 +1,80 @@
|
|||||||
import React, {useCallback, useEffect, useState, createContext} from "react";
|
import React, {useCallback, useEffect, useState, createContext} from "react";
|
||||||
import {useColorMode} from "@docusaurus/theme-common";
|
import {useColorMode} from "@docusaurus/theme-common";
|
||||||
|
import BrowserOnly from "@docusaurus/BrowserOnly";
|
||||||
|
|
||||||
export interface PainterProps {
|
function invertImage(sourceImage: ImageData): ImageData {
|
||||||
readonly width: number;
|
const image = new ImageData(sourceImage.width, sourceImage.height);
|
||||||
readonly height: number;
|
image.data.forEach((value, index) =>
|
||||||
readonly setPainter: (painter: Iterator<ImageData>) => void;
|
image.data[index] = index % 4 === 3 ? value : 0xff - value)
|
||||||
|
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
type InvertibleCanvasProps = {
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
hidden?: boolean,
|
||||||
|
// NOTE: Images are provided as a single-element array
|
||||||
|
//so we can allow re-painting with the same (modified) ImageData reference.
|
||||||
|
image?: [ImageData],
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Context provider for child elements to submit image iterator functions
|
* Draw images to a canvas, automatically inverting colors as needed.
|
||||||
* (painters) for rendering
|
*
|
||||||
|
* @param width Canvas width
|
||||||
|
* @param height Canvas height
|
||||||
|
* @param hidden Hide the canvas element
|
||||||
|
* @param image Image data to draw on the canvas
|
||||||
*/
|
*/
|
||||||
|
const InvertibleCanvas: React.FC<InvertibleCanvasProps> = ({width, height, hidden, 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}
|
||||||
|
hidden={hidden ?? false}
|
||||||
|
style={{
|
||||||
|
aspectRatio: width / height,
|
||||||
|
width: '75%'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
interface CanvasProps {
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -27,131 +84,61 @@ interface CanvasProps {
|
|||||||
* a couple problems at once:
|
* a couple problems at once:
|
||||||
* - Incrementally drawing an image to the canvas
|
* - Incrementally drawing an image to the canvas
|
||||||
* - Interrupting drawing with new parameters
|
* - Interrupting drawing with new parameters
|
||||||
* - Dark mode
|
|
||||||
*
|
*
|
||||||
* Running a full render is labor-intensive, so we model it
|
* Running a full render is labor-intensive, so we model it
|
||||||
* as an iterator that yields an image of the current system.
|
* as an iterator that yields an image of the current system.
|
||||||
* Internally, that iterator is re-queued on each new image;
|
* Internally, that iterator is re-queued on each new image;
|
||||||
* so long as that image is returned quickly, we keep
|
* so long as retrieving each image happens quickly,
|
||||||
* the main loop running even with CPU-heavy code.
|
* 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
|
* To interrupt drawing, children set the active iterator
|
||||||
* through the context provider. This component doesn't care
|
* through the context provider. This component doesn't care
|
||||||
* about which iterator is in progress, it exists only
|
* about which iterator is in progress, it exists only
|
||||||
* to fetch the next image and paint it to our canvas.
|
* to fetch the next image and paint it to our canvas.
|
||||||
*
|
*
|
||||||
* Finally, we make a distinction between "render" and "paint" buffers.
|
|
||||||
* The render image is provided by the iterator, and then:
|
|
||||||
* - If light mode is active, draw it to the canvas as-is
|
|
||||||
* - If dark mode is active, copy the "render" buffer to the "paint" buffer,
|
|
||||||
* invert colors, and then draw the image
|
|
||||||
*
|
|
||||||
* TODO(bspeice): Can we make this "re-queueing iterator" pattern generic?
|
* TODO(bspeice): Can we make this "re-queueing iterator" pattern generic?
|
||||||
* It would be nice to have iterators returning arbitrary objects,
|
* It would be nice to have iterators returning arbitrary objects,
|
||||||
* but we rely on contexts to manage the iterator, and there's
|
* but we rely on contexts to manage the iterator, and I can't find
|
||||||
* no good way to make those generic.
|
* a good way to make those generic.
|
||||||
*
|
|
||||||
* @param width Canvas draw width
|
|
||||||
* @param height Canvas draw height
|
|
||||||
* @param hidden Hide the canvas
|
|
||||||
* @param children Child elements
|
|
||||||
*/
|
*/
|
||||||
export default function Canvas({width, height, hidden, children}: CanvasProps) {
|
export default function Canvas({width, height, hidden, children}: CanvasProps) {
|
||||||
const [canvasCtx, setCanvasCtx] = useState<CanvasRenderingContext2D>(null);
|
const [image, setImage] = useState<[ImageData]>(null);
|
||||||
const canvasRef = useCallback(node => {
|
const [painterHolder, setPainterHolder] = useState<[Iterator<ImageData>]>(null);
|
||||||
if (node !== null) {
|
|
||||||
setCanvasCtx(node.getContext("2d"));
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Holder objects are used to force re-painting even if the iterator
|
|
||||||
// returns a modified image with the same reference
|
|
||||||
type ImageHolder = { image?: ImageData };
|
|
||||||
|
|
||||||
const [paintImage, setPaintImage] = useState<ImageHolder>({ image: null });
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (paintImage.image && canvasCtx) {
|
if (!painterHolder) {
|
||||||
canvasCtx.putImageData(paintImage.image, 0, 0);
|
|
||||||
}
|
|
||||||
}, [paintImage, canvasCtx]);
|
|
||||||
|
|
||||||
const {colorMode} = useColorMode();
|
|
||||||
const [renderImage, setRenderImage] = useState<ImageHolder>({ image: null });
|
|
||||||
useEffect(() => {
|
|
||||||
const image = renderImage.image;
|
|
||||||
if (!image) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If light mode is active, paint the image as-is
|
const painter = painterHolder[0];
|
||||||
if (colorMode === 'light') {
|
const nextImage = painter.next().value;
|
||||||
setPaintImage({ image });
|
if (nextImage) {
|
||||||
return;
|
setImage([nextImage]);
|
||||||
}
|
setPainterHolder([painter]);
|
||||||
|
|
||||||
// If dark mode is active, copy the image into a new buffer
|
|
||||||
// and invert colors prior to painting.
|
|
||||||
// Copy alpha values as-is.
|
|
||||||
const paintImage = new ImageData(image.width, image.height);
|
|
||||||
image.data.forEach((value, index) => {
|
|
||||||
const isAlpha = index % 4 === 3;
|
|
||||||
paintImage.data[index] = isAlpha ? value : 255 - value;
|
|
||||||
})
|
|
||||||
setPaintImage({ image: paintImage });
|
|
||||||
}, [colorMode, renderImage]);
|
|
||||||
|
|
||||||
// Image iterators (painters) are also in a holder; this allows
|
|
||||||
// re-submitting the existing iterator to draw the next frame,
|
|
||||||
// and also allows child components to over-write the iterator
|
|
||||||
// if a new set of parameters becomes available
|
|
||||||
// TODO(bspeice): Potential race condition?
|
|
||||||
// Not sure if it's possible for painters submitted by children
|
|
||||||
// to be over-ridden as a result re-submitting the
|
|
||||||
// existing iterator
|
|
||||||
type PainterHolder = { painter?: Iterator<ImageData> };
|
|
||||||
const [animHolder, setAnimHolder] = useState<PainterHolder>({ painter: null });
|
|
||||||
useEffect(() => {
|
|
||||||
const painter = animHolder.painter;
|
|
||||||
if (!painter) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!canvasCtx) {
|
|
||||||
setAnimHolder({ painter });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const image = painter.next().value;
|
|
||||||
if (image) {
|
|
||||||
setRenderImage({ image });
|
|
||||||
setAnimHolder({ painter });
|
|
||||||
} else {
|
} else {
|
||||||
setAnimHolder({ painter: null });
|
setPainterHolder(null);
|
||||||
}
|
}
|
||||||
}, [animHolder, canvasCtx]);
|
}, [painterHolder]);
|
||||||
|
|
||||||
// Finally, child elements submit painters through a context provider
|
|
||||||
const [painter, setPainter] = useState<Iterator<ImageData>>(null);
|
const [painter, setPainter] = useState<Iterator<ImageData>>(null);
|
||||||
useEffect(() => setAnimHolder({ painter }), [painter]);
|
useEffect(() => {
|
||||||
|
if (painter) {
|
||||||
|
setPainterHolder([painter]);
|
||||||
|
}
|
||||||
|
}, [painter]);
|
||||||
|
|
||||||
width = width ?? 500;
|
width = width ?? 500;
|
||||||
height = height ?? 500;
|
height = height ?? 500;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<center>
|
<center>
|
||||||
<canvas
|
<InvertibleCanvas width={width} height={height} hidden={hidden} image={image}/>
|
||||||
ref={canvasRef}
|
|
||||||
width={width}
|
|
||||||
height={height}
|
|
||||||
hidden={hidden ?? false}
|
|
||||||
style={{
|
|
||||||
aspectRatio: width / height,
|
|
||||||
width: '80%'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</center>
|
</center>
|
||||||
<PainterContext.Provider value={{width, height, setPainter}}>
|
<PainterContext.Provider value={{width, height, setPainter}}>
|
||||||
{children}
|
<BrowserOnly>{() => children}</BrowserOnly>
|
||||||
</PainterContext.Provider>
|
</PainterContext.Provider>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user