mirror of
https://github.com/bspeice/speice.io
synced 2025-07-01 22:06:26 -04:00
Auto-sizing canvas, starting cleanup for display on mobile browsers
This commit is contained in:
@ -1,16 +1,21 @@
|
||||
import Canvas, {PainterContext} from "../src/Canvas";
|
||||
import {useContext} from "react";
|
||||
import {SquareCanvas, PainterContext} from "../src/Canvas";
|
||||
import {useContext, useEffect} from "react";
|
||||
|
||||
export function Render({f}) {
|
||||
const {setPainter} = useContext(PainterContext);
|
||||
setPainter(f);
|
||||
const {width, height, setPainter} = useContext(PainterContext);
|
||||
useEffect(() => {
|
||||
if (width && height) {
|
||||
const painter = f({width, height});
|
||||
setPainter(painter);
|
||||
}
|
||||
}, [width, height]);
|
||||
return <></>;
|
||||
}
|
||||
|
||||
export default function Gasket({f}) {
|
||||
return (
|
||||
<Canvas width={500} height={500}>
|
||||
<SquareCanvas>
|
||||
<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 {chaosGameWeighted} from "./chaosGameWeighted";
|
||||
import TeX from '@matejmazur/react-katex';
|
||||
@ -30,7 +30,7 @@ export default function GasketWeighted() {
|
||||
const weightInput = (title, weight, setWeight) => (
|
||||
<>
|
||||
<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}
|
||||
onInput={e => setWeight(Number(e.currentTarget.value))}/>
|
||||
</div>
|
||||
@ -39,7 +39,7 @@ export default function GasketWeighted() {
|
||||
|
||||
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_1", f1Weight, setF1Weight)}
|
||||
{weightInput("F_2", f2Weight, setF2Weight)}
|
||||
|
@ -1,30 +1,33 @@
|
||||
// Hint: try increasing the iteration count
|
||||
const iterations = 10000;
|
||||
// Hint: try changing the iteration count
|
||||
const iterations = 100000;
|
||||
|
||||
// Hint: negating `x` and `y` creates some interesting images
|
||||
const transforms = [
|
||||
(x, y) => [x / 2, y / 2],
|
||||
(x, y) => [(x + 1) / 2, y / 2],
|
||||
(x, y) => [x / 2, (y + 1) / 2]
|
||||
// Hint: negating `x` and `y` creates some cool images
|
||||
const xforms = [
|
||||
(x, y) => [x / 2, y / 2],
|
||||
(x, y) => [(x + 1) / 2, y / 2],
|
||||
(x, y) => [x / 2, (y + 1) / 2]
|
||||
]
|
||||
|
||||
function* chaosGame() {
|
||||
let image = new ImageData(500, 500);
|
||||
let [x, y] = [randomBiUnit(), randomBiUnit()];
|
||||
function* chaosGame({width, height}) {
|
||||
let img = new ImageData(width, height);
|
||||
let [x, y] = [
|
||||
randomBiUnit(),
|
||||
randomBiUnit()
|
||||
];
|
||||
|
||||
for (var count = 0; count < iterations; count++) {
|
||||
const i = randomInteger(0, transforms.length);
|
||||
[x, y] = transforms[i](x, y);
|
||||
for (let c = 0; c < iterations; c++) {
|
||||
const i = randomInteger(0, xforms.length);
|
||||
[x, y] = xforms[i](x, y);
|
||||
|
||||
if (count > 20)
|
||||
plot(x, y, image);
|
||||
if (c > 20)
|
||||
plot(x, y, img);
|
||||
|
||||
if (count % 1000 === 0)
|
||||
yield image;
|
||||
}
|
||||
if (c % 1000 === 0)
|
||||
yield img;
|
||||
}
|
||||
|
||||
yield image;
|
||||
yield img;
|
||||
}
|
||||
|
||||
// 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 step = 1000;
|
||||
// hidden-end
|
||||
export type ChaosGameWeightedProps = {
|
||||
type Props = {
|
||||
width: number,
|
||||
height: number,
|
||||
transforms: [number, Transform][]
|
||||
}
|
||||
export function* chaosGameWeighted({width, height, transforms}: ChaosGameWeightedProps) {
|
||||
let image = new ImageData(width, height);
|
||||
var [x, y] = [randomBiUnit(), randomBiUnit()];
|
||||
export function* chaosGameWeighted(
|
||||
{width, height, transforms}: Props
|
||||
) {
|
||||
let img = new ImageData(width, height);
|
||||
let [x, y] = [
|
||||
randomBiUnit(),
|
||||
randomBiUnit()
|
||||
];
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
// highlight-start
|
||||
const [_, transform] = randomChoice(transforms);
|
||||
// highlight-end
|
||||
[x, y] = transform(x, y);
|
||||
const iterations = width * height * 0.5;
|
||||
for (let c = 0; c < iterations; c++) {
|
||||
// highlight-start
|
||||
const [_, xform] = randomChoice(transforms);
|
||||
// highlight-end
|
||||
[x, y] = xform(x, y);
|
||||
|
||||
if (i > 20)
|
||||
plot(x, y, image);
|
||||
if (c > 20)
|
||||
plot(x, y, img);
|
||||
|
||||
if (i % step === 0)
|
||||
yield image;
|
||||
}
|
||||
if (c % step === 0)
|
||||
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:
|
||||
|
||||
$$
|
||||
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
|
||||
@ -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:
|
||||
|
||||
$$
|
||||
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)
|
||||
\hspace{0.8cm}
|
||||
F_0(x, y) = \left({x \over 2}, {y \over 2} \right) \\
|
||||
~\\
|
||||
F_1(x, y) = \left({{x + 1} \over 2}, {y \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*}
|
||||
&(x, y) = \text{a random point in the bi-unit square} \\
|
||||
&(x, y) = \text{random point in the bi-unit square} \\
|
||||
&\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} \text{plot}(x,y) \text{ except during the first 20 iterations} \\
|
||||
&\hspace{1cm} \text{plot}(x,y) \text{ if iterations} > 20 \\
|
||||
\}
|
||||
\end{align*}
|
||||
$$
|
||||
@ -199,6 +199,6 @@ import chaosGameWeightedSource from "!!raw-loader!./chaosGameWeighted";
|
||||
<CodeBlock language={'typescript'}>{chaosGameWeightedSource}</CodeBlock>
|
||||
|
||||
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;
|
||||
// also known as a "camera" function.
|
||||
//
|
||||
// The display range we care about is x=[0, 1], y=[0, 1],
|
||||
// so our pixelX and pixelY coordinates are easy to calculate:
|
||||
const pixelX = Math.floor(x * image.width);
|
||||
const pixelY = Math.floor(y * image.height);
|
||||
/**
|
||||
* ImageData is an array that contains
|
||||
* four elements per pixel (one for each
|
||||
* red, green, blue, and alpha value).
|
||||
* This maps from pixel coordinates
|
||||
* to the array index
|
||||
*/
|
||||
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,
|
||||
// skip it
|
||||
if (
|
||||
pixelX < 0 ||
|
||||
pixelX > image.width ||
|
||||
pixelY < 0 ||
|
||||
pixelY > image.height
|
||||
) {
|
||||
return;
|
||||
}
|
||||
export function plot(
|
||||
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);
|
||||
|
||||
// ImageData is an array that contains four bytes per pixel
|
||||
// (one for each of the red, green, blue, and alpha values).
|
||||
// The (pixelX, pixelY) coordinates are used to find where
|
||||
// in the image we need to write.
|
||||
const index = pixelY * (image.width * 4) + pixelX * 4;
|
||||
const index = imageIndex(
|
||||
img.width,
|
||||
pixelX,
|
||||
pixelY
|
||||
);
|
||||
|
||||
// Set the pixel to black by writing a 0 to the first three
|
||||
// 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;
|
||||
// Skip pixels outside the display range
|
||||
if (
|
||||
index < 0 ||
|
||||
index > img.data.length
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the pixel to black by writing 0
|
||||
// to the first three elements,
|
||||
// and 255 to the last element
|
||||
img.data[index] = 0;
|
||||
img.data[index + 1] = 0;
|
||||
img.data[index + 2] = 0;
|
||||
img.data[index + 3] = 0xff;
|
||||
}
|
Reference in New Issue
Block a user