mirror of
https://github.com/bspeice/speice.io
synced 2024-12-22 16:48:10 -05:00
Auto-sizing canvas, starting cleanup for display on mobile browsers
This commit is contained in:
parent
78d71cbc7b
commit
5ae6b82d26
@ -1,16 +1,21 @@
|
|||||||
import Canvas, {PainterContext} from "../src/Canvas";
|
import {SquareCanvas, PainterContext} from "../src/Canvas";
|
||||||
import {useContext} from "react";
|
import {useContext, useEffect} from "react";
|
||||||
|
|
||||||
export function Render({f}) {
|
export function Render({f}) {
|
||||||
const {setPainter} = useContext(PainterContext);
|
const {width, height, setPainter} = useContext(PainterContext);
|
||||||
setPainter(f);
|
useEffect(() => {
|
||||||
|
if (width && height) {
|
||||||
|
const painter = f({width, height});
|
||||||
|
setPainter(painter);
|
||||||
|
}
|
||||||
|
}, [width, height]);
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Gasket({f}) {
|
export default function Gasket({f}) {
|
||||||
return (
|
return (
|
||||||
<Canvas width={500} height={500}>
|
<SquareCanvas>
|
||||||
<Render f={f}/>
|
<Render f={f}/>
|
||||||
</Canvas>
|
</SquareCanvas>
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import {useEffect, useState, useContext} from "react";
|
import {useEffect, useState, useContext, useRef} from "react";
|
||||||
import {PainterContext} from "../src/Canvas";
|
import {PainterContext} from "../src/Canvas";
|
||||||
import {chaosGameWeighted} from "./chaosGameWeighted";
|
import {chaosGameWeighted} from "./chaosGameWeighted";
|
||||||
import TeX from '@matejmazur/react-katex';
|
import TeX from '@matejmazur/react-katex';
|
||||||
@ -30,7 +30,7 @@ export default function GasketWeighted() {
|
|||||||
const weightInput = (title, weight, setWeight) => (
|
const weightInput = (title, weight, setWeight) => (
|
||||||
<>
|
<>
|
||||||
<div className={styles.inputElement}>
|
<div className={styles.inputElement}>
|
||||||
<p><TeX>{title}</TeX> weight: {weight}</p>
|
<p><TeX>{title}</TeX>: {weight}</p>
|
||||||
<input type={'range'} min={0} max={1} step={.01} style={{width: '100%'}} value={weight}
|
<input type={'range'} min={0} max={1} step={.01} style={{width: '100%'}} value={weight}
|
||||||
onInput={e => setWeight(Number(e.currentTarget.value))}/>
|
onInput={e => setWeight(Number(e.currentTarget.value))}/>
|
||||||
</div>
|
</div>
|
||||||
@ -39,7 +39,7 @@ export default function GasketWeighted() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.inputGroup} style={{display: 'grid', gridTemplateColumns: 'auto auto auto'}}>
|
<div className={styles.inputGroup} style={{display: 'grid', gridTemplateColumns: '1fr 1fr 1fr'}}>
|
||||||
{weightInput("F_0", f0Weight, setF0Weight)}
|
{weightInput("F_0", f0Weight, setF0Weight)}
|
||||||
{weightInput("F_1", f1Weight, setF1Weight)}
|
{weightInput("F_1", f1Weight, setF1Weight)}
|
||||||
{weightInput("F_2", f2Weight, setF2Weight)}
|
{weightInput("F_2", f2Weight, setF2Weight)}
|
||||||
|
@ -1,30 +1,33 @@
|
|||||||
// Hint: try increasing the iteration count
|
// Hint: try changing the iteration count
|
||||||
const iterations = 10000;
|
const iterations = 100000;
|
||||||
|
|
||||||
// Hint: negating `x` and `y` creates some interesting images
|
// Hint: negating `x` and `y` creates some cool images
|
||||||
const transforms = [
|
const xforms = [
|
||||||
(x, y) => [x / 2, y / 2],
|
(x, y) => [x / 2, y / 2],
|
||||||
(x, y) => [(x + 1) / 2, y / 2],
|
(x, y) => [(x + 1) / 2, y / 2],
|
||||||
(x, y) => [x / 2, (y + 1) / 2]
|
(x, y) => [x / 2, (y + 1) / 2]
|
||||||
]
|
]
|
||||||
|
|
||||||
function* chaosGame() {
|
function* chaosGame({width, height}) {
|
||||||
let image = new ImageData(500, 500);
|
let img = new ImageData(width, height);
|
||||||
let [x, y] = [randomBiUnit(), randomBiUnit()];
|
let [x, y] = [
|
||||||
|
randomBiUnit(),
|
||||||
|
randomBiUnit()
|
||||||
|
];
|
||||||
|
|
||||||
for (var count = 0; count < iterations; count++) {
|
for (let c = 0; c < iterations; c++) {
|
||||||
const i = randomInteger(0, transforms.length);
|
const i = randomInteger(0, xforms.length);
|
||||||
[x, y] = transforms[i](x, y);
|
[x, y] = xforms[i](x, y);
|
||||||
|
|
||||||
if (count > 20)
|
if (c > 20)
|
||||||
plot(x, y, image);
|
plot(x, y, img);
|
||||||
|
|
||||||
if (count % 1000 === 0)
|
if (c % 1000 === 0)
|
||||||
yield image;
|
yield img;
|
||||||
}
|
}
|
||||||
|
|
||||||
yield image;
|
yield img;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wiring so the code above displays properly
|
// Wiring so the code above displays properly
|
||||||
render(<Gasket f={chaosGame()}/>)
|
render(<Gasket f={chaosGame}/>)
|
||||||
|
@ -6,27 +6,33 @@ import {Transform} from "../src/transform";
|
|||||||
const iterations = 50_000;
|
const iterations = 50_000;
|
||||||
const step = 1000;
|
const step = 1000;
|
||||||
// hidden-end
|
// hidden-end
|
||||||
export type ChaosGameWeightedProps = {
|
type Props = {
|
||||||
width: number,
|
width: number,
|
||||||
height: number,
|
height: number,
|
||||||
transforms: [number, Transform][]
|
transforms: [number, Transform][]
|
||||||
}
|
}
|
||||||
export function* chaosGameWeighted({width, height, transforms}: ChaosGameWeightedProps) {
|
export function* chaosGameWeighted(
|
||||||
let image = new ImageData(width, height);
|
{width, height, transforms}: Props
|
||||||
var [x, y] = [randomBiUnit(), randomBiUnit()];
|
) {
|
||||||
|
let img = new ImageData(width, height);
|
||||||
|
let [x, y] = [
|
||||||
|
randomBiUnit(),
|
||||||
|
randomBiUnit()
|
||||||
|
];
|
||||||
|
|
||||||
for (let i = 0; i < iterations; i++) {
|
const iterations = width * height * 0.5;
|
||||||
|
for (let c = 0; c < iterations; c++) {
|
||||||
// highlight-start
|
// highlight-start
|
||||||
const [_, transform] = randomChoice(transforms);
|
const [_, xform] = randomChoice(transforms);
|
||||||
// highlight-end
|
// highlight-end
|
||||||
[x, y] = transform(x, y);
|
[x, y] = xform(x, y);
|
||||||
|
|
||||||
if (i > 20)
|
if (c > 20)
|
||||||
plot(x, y, image);
|
plot(x, y, img);
|
||||||
|
|
||||||
if (i % step === 0)
|
if (c % step === 0)
|
||||||
yield image;
|
yield img;
|
||||||
}
|
}
|
||||||
|
|
||||||
yield image;
|
yield img;
|
||||||
}
|
}
|
@ -101,7 +101,7 @@ export const shiftData = simpleData.map(({x, y}) => { return {x: x + 1, y} })
|
|||||||
This is a simple example designed to illustrate the principle. In general, $F_i$ functions have the form:
|
This is a simple example designed to illustrate the principle. In general, $F_i$ functions have the form:
|
||||||
|
|
||||||
$$
|
$$
|
||||||
F_i(x,y) = (a_i \cdot x + b_i \cdot y + c_i, \hspace{0.2cm} d_i \cdot x + e_i \cdot y + f_i)
|
F_i(x,y) = (a_i \cdot x + b_i \cdot y + c_i, d_i \cdot x + e_i \cdot y + f_i)
|
||||||
$$
|
$$
|
||||||
|
|
||||||
The parameters ($a_i$, $b_i$, etc.) are values we get to choose. In the example above, we can represent our shift
|
The parameters ($a_i$, $b_i$, etc.) are values we get to choose. In the example above, we can represent our shift
|
||||||
@ -126,10 +126,10 @@ Fractal flames use more complex functions to produce a wide variety of images, b
|
|||||||
Using these definitions, we can build the first image. The paper defines a function system for us:
|
Using these definitions, we can build the first image. The paper defines a function system for us:
|
||||||
|
|
||||||
$$
|
$$
|
||||||
F_0(x, y) = \left({x \over 2}, {y \over 2} \right)
|
F_0(x, y) = \left({x \over 2}, {y \over 2} \right) \\
|
||||||
\hspace{0.8cm}
|
~\\
|
||||||
F_1(x, y) = \left({{x + 1} \over 2}, {y \over 2} \right)
|
F_1(x, y) = \left({{x + 1} \over 2}, {y \over 2} \right) \\
|
||||||
\hspace{0.8cm}
|
~\\
|
||||||
F_2(x, y) = \left({x \over 2}, {{y + 1} \over 2} \right)
|
F_2(x, y) = \left({x \over 2}, {{y + 1} \over 2} \right)
|
||||||
$$
|
$$
|
||||||
|
|
||||||
@ -141,11 +141,11 @@ Next, how do we find out all the points in $S$? The paper lays out an algorithm
|
|||||||
|
|
||||||
$$
|
$$
|
||||||
\begin{align*}
|
\begin{align*}
|
||||||
&(x, y) = \text{a random point in the bi-unit square} \\
|
&(x, y) = \text{random point in the bi-unit square} \\
|
||||||
&\text{iterate } \{ \\
|
&\text{iterate } \{ \\
|
||||||
&\hspace{1cm} i = \text{a random integer from 0 to } n - 1 \text{ inclusive} \\
|
&\hspace{1cm} i = \text{random integer from 0 to } n - 1 \\
|
||||||
&\hspace{1cm} (x,y) = F_i(x,y) \\
|
&\hspace{1cm} (x,y) = F_i(x,y) \\
|
||||||
&\hspace{1cm} \text{plot}(x,y) \text{ except during the first 20 iterations} \\
|
&\hspace{1cm} \text{plot}(x,y) \text{ if iterations} > 20 \\
|
||||||
\}
|
\}
|
||||||
\end{align*}
|
\end{align*}
|
||||||
$$
|
$$
|
||||||
@ -199,6 +199,6 @@ import chaosGameWeightedSource from "!!raw-loader!./chaosGameWeighted";
|
|||||||
<CodeBlock language={'typescript'}>{chaosGameWeightedSource}</CodeBlock>
|
<CodeBlock language={'typescript'}>{chaosGameWeightedSource}</CodeBlock>
|
||||||
|
|
||||||
import GasketWeighted from "./GasketWeighted";
|
import GasketWeighted from "./GasketWeighted";
|
||||||
import Canvas from "../src/Canvas";
|
import {SquareCanvas} from "../src/Canvas";
|
||||||
|
|
||||||
<Canvas><GasketWeighted/></Canvas>
|
<SquareCanvas><GasketWeighted/></SquareCanvas>
|
@ -1,34 +1,52 @@
|
|||||||
export function plot(x: number, y: number, image: ImageData) {
|
/**
|
||||||
// Translate (x,y) coordinates to pixel coordinates;
|
* ImageData is an array that contains
|
||||||
// also known as a "camera" function.
|
* four elements per pixel (one for each
|
||||||
//
|
* red, green, blue, and alpha value).
|
||||||
// The display range we care about is x=[0, 1], y=[0, 1],
|
* This maps from pixel coordinates
|
||||||
// so our pixelX and pixelY coordinates are easy to calculate:
|
* to the array index
|
||||||
const pixelX = Math.floor(x * image.width);
|
*/
|
||||||
const pixelY = Math.floor(y * image.height);
|
function imageIndex(
|
||||||
|
width: number,
|
||||||
|
x: number,
|
||||||
|
y: number
|
||||||
|
) {
|
||||||
|
return y * (width * 4) + x * 4;
|
||||||
|
}
|
||||||
|
|
||||||
// If we have an (x,y) coordinate outside the display range,
|
export function plot(
|
||||||
// skip it
|
x: number,
|
||||||
|
y: number,
|
||||||
|
img: ImageData
|
||||||
|
) {
|
||||||
|
// Translate (x,y) coordinates
|
||||||
|
// to pixel coordinates.
|
||||||
|
// Also known as a "camera" function.
|
||||||
|
//
|
||||||
|
// The display range is:
|
||||||
|
// x=[0, 1]
|
||||||
|
// y=[0, 1]
|
||||||
|
let pixelX = Math.floor(x * img.width);
|
||||||
|
let pixelY = Math.floor(y * img.height);
|
||||||
|
|
||||||
|
const index = imageIndex(
|
||||||
|
img.width,
|
||||||
|
pixelX,
|
||||||
|
pixelY
|
||||||
|
);
|
||||||
|
|
||||||
|
// Skip pixels outside the display range
|
||||||
if (
|
if (
|
||||||
pixelX < 0 ||
|
index < 0 ||
|
||||||
pixelX > image.width ||
|
index > img.data.length
|
||||||
pixelY < 0 ||
|
|
||||||
pixelY > image.height
|
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImageData is an array that contains four bytes per pixel
|
// Set the pixel to black by writing 0
|
||||||
// (one for each of the red, green, blue, and alpha values).
|
// to the first three elements,
|
||||||
// The (pixelX, pixelY) coordinates are used to find where
|
// and 255 to the last element
|
||||||
// in the image we need to write.
|
img.data[index] = 0;
|
||||||
const index = pixelY * (image.width * 4) + pixelX * 4;
|
img.data[index + 1] = 0;
|
||||||
|
img.data[index + 2] = 0;
|
||||||
// Set the pixel to black by writing a 0 to the first three
|
img.data[index + 3] = 0xff;
|
||||||
// bytes (red, green, blue), and 256 to the last byte (alpha),
|
|
||||||
// starting at our index:
|
|
||||||
image.data[index] = 0;
|
|
||||||
image.data[index + 1] = 0;
|
|
||||||
image.data[index + 2] = 0;
|
|
||||||
image.data[index + 3] = 0xff;
|
|
||||||
}
|
}
|
@ -145,7 +145,7 @@ each transform.
|
|||||||
import Canvas from "../src/Canvas";
|
import Canvas from "../src/Canvas";
|
||||||
import FlameBlend from "./FlameBlend";
|
import FlameBlend from "./FlameBlend";
|
||||||
|
|
||||||
<Canvas><FlameBlend/></Canvas>
|
<!-- <Canvas><FlameBlend/></Canvas> -->
|
||||||
|
|
||||||
## Post transforms
|
## Post transforms
|
||||||
|
|
||||||
@ -160,10 +160,10 @@ $$
|
|||||||
|
|
||||||
import FlamePost from "./FlamePost";
|
import FlamePost from "./FlamePost";
|
||||||
|
|
||||||
<Canvas><FlamePost/></Canvas>
|
<!-- <Canvas><FlamePost/></Canvas> -->
|
||||||
|
|
||||||
## Final transform
|
## Final transform
|
||||||
|
|
||||||
import FlameFinal from "./FlameFinal";
|
import FlameFinal from "./FlameFinal";
|
||||||
|
|
||||||
<Canvas><FlameFinal/></Canvas>
|
<!-- <Canvas><FlameFinal/></Canvas> -->
|
@ -32,7 +32,7 @@ import Canvas from "../src/Canvas";
|
|||||||
import FlameHistogram from "./FlameHistogram";
|
import FlameHistogram from "./FlameHistogram";
|
||||||
import {paintLinear} from "./paintLinear";
|
import {paintLinear} from "./paintLinear";
|
||||||
|
|
||||||
<Canvas><FlameHistogram quality={5} paint={paintLinear}/></Canvas>
|
<!-- <Canvas><FlameHistogram quality={5} paint={paintLinear}/></Canvas> -->
|
||||||
|
|
||||||
## Log display
|
## Log display
|
||||||
|
|
||||||
@ -42,7 +42,7 @@ import paintLogarithmicSource from "!!raw-loader!./paintLogarithmic"
|
|||||||
|
|
||||||
import {paintLogarithmic} from './paintLogarithmic'
|
import {paintLogarithmic} from './paintLogarithmic'
|
||||||
|
|
||||||
<Canvas><FlameHistogram quality={5} paint={paintLogarithmic}/></Canvas>
|
<!-- <Canvas><FlameHistogram quality={5} paint={paintLogarithmic}/></Canvas> -->
|
||||||
|
|
||||||
## Color
|
## Color
|
||||||
|
|
||||||
@ -52,4 +52,4 @@ import paintColorSource from "!!raw-loader!./paintColor"
|
|||||||
|
|
||||||
import FlameColor from "./FlameColor";
|
import FlameColor from "./FlameColor";
|
||||||
|
|
||||||
<Canvas><FlameColor quality={15}/></Canvas>
|
<!-- <Canvas><FlameColor quality={15}/></Canvas> -->
|
@ -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 {useColorMode} from "@docusaurus/theme-common";
|
||||||
import BrowserOnly from "@docusaurus/BrowserOnly";
|
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 = {
|
type PainterProps = {
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
setPainter: (painter: Iterator<ImageData>) => void;
|
setPainter: (painter: Iterator<ImageData>) => void;
|
||||||
}
|
}
|
||||||
export const PainterContext = createContext<PainterProps>(null);
|
export const PainterContext = createContext<PainterProps>(null)
|
||||||
|
|
||||||
interface CanvasProps {
|
type CanvasProps = {
|
||||||
width?: number;
|
style?: any;
|
||||||
height?: number;
|
children?: React.ReactElement
|
||||||
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);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!viewportDetectionRef) {
|
if (!canvasRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const observer = new IntersectionObserver(([entry]) => {
|
const observer = new IntersectionObserver((entries) => {
|
||||||
|
const [entry] = entries;
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
setIsVisible(true);
|
setIsVisible(true);
|
||||||
}
|
}
|
||||||
}, {root: null, threshold: .1});
|
});
|
||||||
observer.observe(viewportDetectionRef.current);
|
observer.observe(canvasRef.current);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (viewportDetectionRef.current) {
|
if (canvasRef.current) {
|
||||||
observer.unobserve(viewportDetectionRef.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);
|
const [painterHolder, setPainterHolder] = useState<[Iterator<ImageData>]>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isVisible || !painterHolder) {
|
if (!isVisible || !painterHolder) {
|
||||||
console.log("Skipping, not visible");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const painter = painterHolder[0];
|
const painter = painterHolder[0];
|
||||||
const nextImage = painter.next().value;
|
const nextImage = painter.next().value;
|
||||||
if (nextImage) {
|
if (nextImage) {
|
||||||
setImage([nextImage]);
|
setImageHolder([nextImage]);
|
||||||
setPainterHolder([painter]);
|
setPainterHolder([painter]);
|
||||||
} else {
|
} else {
|
||||||
setPainterHolder(null);
|
setPainterHolder(null);
|
||||||
@ -146,18 +76,25 @@ export default function Canvas({width, height, children}: CanvasProps) {
|
|||||||
}
|
}
|
||||||
}, [painter]);
|
}, [painter]);
|
||||||
|
|
||||||
width = width ?? 500;
|
const {colorMode} = useColorMode();
|
||||||
height = height ?? 500;
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<center>
|
<canvas
|
||||||
<div ref={viewportDetectionRef}>
|
ref={canvasRef}
|
||||||
<InvertibleCanvas width={width} height={height} image={image}/>
|
width={width}
|
||||||
</div>
|
height={height}
|
||||||
</center>
|
style={{
|
||||||
|
filter: colorMode === 'dark' ? 'invert(1)' : '',
|
||||||
|
...style
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<PainterContext.Provider value={{width, height, setPainter}}>
|
<PainterContext.Provider value={{width, height, setPainter}}>
|
||||||
<BrowserOnly>{() => children}</BrowserOnly>
|
<BrowserOnly>{() => children}</BrowserOnly>
|
||||||
</PainterContext.Provider>
|
</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 {
|
.inputGroup {
|
||||||
padding: .5em;
|
padding: .2em;
|
||||||
margin: .5em;
|
margin-top: .2em;
|
||||||
|
margin-bottom: .2em;
|
||||||
border: 1px solid;
|
border: 1px solid;
|
||||||
border-radius: var(--ifm-global-radius);
|
border-radius: var(--ifm-global-radius);
|
||||||
border-color: var(--ifm-color-emphasis-500);
|
border-color: var(--ifm-color-emphasis-500);
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
export function randomChoice<T>(choices: [number, T][]): [number, T] {
|
export function randomChoice<T>(
|
||||||
const weightSum = choices.reduce((sum, [weight, _]) => sum + weight, 0);
|
choices: [number, T][]
|
||||||
|
): [number, T] {
|
||||||
|
const weightSum = choices.reduce(
|
||||||
|
(sum, [weight, _]) => sum + weight,
|
||||||
|
0
|
||||||
|
);
|
||||||
let choice = Math.random() * weightSum;
|
let choice = Math.random() * weightSum;
|
||||||
|
|
||||||
for (const [index, element] of choices.entries()) {
|
for (const [idx, elem] of choices.entries()) {
|
||||||
const [weight, t] = element;
|
const [weight, t] = elem;
|
||||||
if (choice < weight) {
|
if (choice < weight) {
|
||||||
return [index, t];
|
return [idx, t];
|
||||||
}
|
}
|
||||||
choice -= weight;
|
choice -= weight;
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
export function randomInteger(min: number, max: number) {
|
export function randomInteger(
|
||||||
return Math.floor(Math.random() * (max - min)) + min;
|
min: number,
|
||||||
|
max: number
|
||||||
|
) {
|
||||||
|
let v = Math.random() * (max - min);
|
||||||
|
return Math.floor(v) + min;
|
||||||
}
|
}
|
@ -3,6 +3,19 @@
|
|||||||
--ifm-container-width-xl: 1440px;
|
--ifm-container-width-xl: 1440px;
|
||||||
--ifm-footer-padding-vertical: .5rem;
|
--ifm-footer-padding-vertical: .5rem;
|
||||||
--ifm-spacing-horizontal: .8rem;
|
--ifm-spacing-horizontal: .8rem;
|
||||||
|
|
||||||
|
/* Reduce padding on code blocks */
|
||||||
|
--ifm-pre-padding: .6rem;
|
||||||
|
|
||||||
|
/* More readable code highlight background */
|
||||||
|
--docusaurus-highlighted-code-line-bg: var(--ifm-color-emphasis-300);
|
||||||
|
|
||||||
|
/*--ifm-code-font-size: 85%;*/
|
||||||
|
}
|
||||||
|
|
||||||
|
.katex {
|
||||||
|
/* Default is 1.21, this helps with fitting on mobile screens */
|
||||||
|
font-size: 1.16em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-github-link:hover {
|
.header-github-link:hover {
|
||||||
|
Loading…
Reference in New Issue
Block a user