mirror of
https://github.com/bspeice/speice.io
synced 2024-12-22 16:48:10 -05:00
Start on animations so I don't kill the browser
This commit is contained in:
parent
458920b1ae
commit
01af290363
@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { renderFn } from "./0-utility";
|
||||
import { Renderer, renderFn } from "./0-utility";
|
||||
|
||||
export const Canvas: React.FC<{ f: renderFn }> = ({ f }) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
@ -29,3 +29,45 @@ export const Canvas: React.FC<{ f: renderFn }> = ({ f }) => {
|
||||
|
||||
return <canvas ref={canvasRef} width={400} height={400} />;
|
||||
};
|
||||
|
||||
export type CanvasParams = {
|
||||
defaultUrl: string;
|
||||
size: number;
|
||||
qualityMax: number;
|
||||
qualityStep: number;
|
||||
renderer: Renderer;
|
||||
};
|
||||
|
||||
export const CanvasRenderer: React.FC<{ params: CanvasParams }> = ({
|
||||
params,
|
||||
}) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
var qualityCurrent: number = 0;
|
||||
|
||||
const animate = () => {
|
||||
const ctx = canvasRef.current?.getContext("2d");
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Animating");
|
||||
|
||||
const image = ctx.createImageData(params.size, params.size);
|
||||
params.renderer.run(params.qualityStep);
|
||||
params.renderer.render(image);
|
||||
ctx.putImageData(image, 0, 0);
|
||||
|
||||
qualityCurrent += params.qualityStep;
|
||||
if (qualityCurrent < params.qualityMax) {
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (canvasRef.current) {
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return <canvas ref={canvasRef} width={params.size} height={params.size} />;
|
||||
};
|
||||
|
@ -1,15 +1,55 @@
|
||||
/**
|
||||
* Image render manager
|
||||
*
|
||||
* This class tracks the chaos game state so we can periodically
|
||||
* get an image.
|
||||
*/
|
||||
export abstract class Renderer {
|
||||
/**
|
||||
* Build a render manager. For simplicity, this class assumes
|
||||
* we're working with a square image.
|
||||
*
|
||||
* @param size Image width and height
|
||||
*/
|
||||
constructor(public readonly size: number) {}
|
||||
|
||||
/**
|
||||
* Run the chaos game
|
||||
*
|
||||
* @param quality iteration count
|
||||
*/
|
||||
abstract run(quality: number): void;
|
||||
|
||||
/**
|
||||
* Output the current chaos game state to image
|
||||
*
|
||||
* @param image output pixel buffer
|
||||
*/
|
||||
abstract render(image: ImageData): void;
|
||||
}
|
||||
|
||||
export type renderFn = (image: ImageData) => void;
|
||||
|
||||
/**
|
||||
* @returns random number in the bi-unit square (-1, 1)
|
||||
*/
|
||||
export function randomBiUnit() {
|
||||
// Math.random() produces a number in the range [0, 1),
|
||||
// scale to (-1, 1)
|
||||
return Math.random() * 2 - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns random integer (with equal weight) in the range [min, max)
|
||||
*/
|
||||
export function randomInteger(min: number, max: number) {
|
||||
return Math.floor(Math.random() * (max - min)) + min;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param choices array of [weight, value] pairs
|
||||
* @returns pair of [index, value]
|
||||
*/
|
||||
export function weightedChoice<T>(choices: [number, T][]): [number, T] {
|
||||
const weightSum = choices.reduce(
|
||||
(current, [weight, _t]) => current + weight,
|
||||
@ -29,11 +69,23 @@ export function weightedChoice<T>(choices: [number, T][]): [number, T] {
|
||||
throw "unreachable";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param x pixel coordinate
|
||||
* @param y pixel coordinate
|
||||
* @param width image width
|
||||
* @returns index into ImageData buffer for a specific pixel
|
||||
*/
|
||||
export function imageIndex(x: number, y: number, width: number) {
|
||||
// Taken from: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Pixel_manipulation_with_canvas
|
||||
return y * (width * 4) + x * 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param x pixel coordinate
|
||||
* @param y pixel coordinate
|
||||
* @param width image width
|
||||
* @returns index into a histogram for a specific pixel
|
||||
*/
|
||||
export function histIndex(x: number, y: number, width: number) {
|
||||
return y * width + x;
|
||||
}
|
||||
|
@ -1,57 +1,86 @@
|
||||
import { randomBiUnit, randomInteger, renderFn, imageIndex } from "./0-utility";
|
||||
import {
|
||||
randomBiUnit,
|
||||
randomInteger,
|
||||
imageIndex,
|
||||
Renderer,
|
||||
histIndex,
|
||||
} from "./0-utility";
|
||||
|
||||
function plot(x: number, y: number, image: ImageData) {
|
||||
// A trivial `plot` implementation would take the range [-1, 1],
|
||||
// shift it to [0, 2], then scale by the width or height
|
||||
// as appropriate:
|
||||
// pixelX = Math.floor((x + 1) * image.width / 2)
|
||||
// pixelY = Math.floor((y + 1) * image.height / 2)
|
||||
//
|
||||
// However, that produces a mirror image (across both X and Y)
|
||||
// from the paper. We'll negate X and Y to compensate.
|
||||
// Second, because the gasket solution only contains points in
|
||||
// the range [0, 1), the naive plot above would waste 75% of
|
||||
// the pixels available. We'll keep the shift by 1 (to compensate
|
||||
// for mirroring X and Y), but scale by the full image width or
|
||||
// height so we'll plot the specific quadrant we care about.
|
||||
var pixelX = Math.floor((-x + 1) * image.width);
|
||||
var pixelY = Math.floor((-y + 1) * image.height);
|
||||
type Transform = (x: number, y: number) => [number, number];
|
||||
|
||||
// Set the pixel black:
|
||||
const index = imageIndex(pixelX, pixelY, image.width);
|
||||
image.data[index + 0] = 0;
|
||||
image.data[index + 1] = 0;
|
||||
image.data[index + 2] = 0;
|
||||
image.data[index + 3] = 0xff;
|
||||
}
|
||||
export class RendererGasket extends Renderer {
|
||||
private values = new Uint8Array(this.size * this.size);
|
||||
|
||||
type Xform = (x: number, y: number) => [number, number];
|
||||
/**
|
||||
* Translate values in the flame coordinate system to pixel coordinates
|
||||
*
|
||||
* A trivial implementation would take the range [-1, 1], shift it to [0, 2],
|
||||
* then scale by the image size:
|
||||
* pixelX = Math.floor((x + 1) * this.size / 2)
|
||||
* pixelY = Math.floor((y + 1) * this.size / 2)
|
||||
*
|
||||
* However, because the gasket solution set only has values in the range [0, 1],
|
||||
* that would lead to wasting 3/4 of the pixels. We'll instead plot just the range
|
||||
* we care about:
|
||||
* pixelX = Math.floor(x * this.size)
|
||||
* pixelY = Math.floor(x * this.size)
|
||||
*
|
||||
* @param x point in the range [-1, 1]
|
||||
* @param y point in the range [-1, 1]
|
||||
*/
|
||||
plot(x: number, y: number): void {
|
||||
var pixelX = Math.floor(x * this.size);
|
||||
var pixelY = Math.floor(y * this.size);
|
||||
|
||||
export const gasket: renderFn = (image) => {
|
||||
const F: Xform[] = [
|
||||
(x, y) => {
|
||||
return [x / 2, y / 2];
|
||||
},
|
||||
(x, y) => {
|
||||
return [(x + 1) / 2, y / 2];
|
||||
},
|
||||
(x, y) => {
|
||||
return [x / 2, (y + 1) / 2];
|
||||
},
|
||||
if (
|
||||
pixelX < 0 ||
|
||||
pixelX >= this.size ||
|
||||
pixelY < 0 ||
|
||||
pixelY >= this.size
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = histIndex(pixelX, pixelY, this.size);
|
||||
this.values[index] = 1;
|
||||
}
|
||||
|
||||
run(quality: number): void {
|
||||
const transforms: Transform[] = [
|
||||
(x, y) => [x / 2, y / 2],
|
||||
(x, y) => [(x + 1) / 2, y / 2],
|
||||
(x, y) => [x / 2, (y + 1) / 2],
|
||||
];
|
||||
|
||||
let x = randomBiUnit();
|
||||
let y = randomBiUnit();
|
||||
|
||||
// Plot with quality 1
|
||||
const iterations = image.height * image.width;
|
||||
|
||||
const iterations = quality * this.size * this.size;
|
||||
for (var i = 0; i < iterations; i++) {
|
||||
const Fi = randomInteger(0, F.length);
|
||||
[x, y] = F[Fi](x, y);
|
||||
const transformIndex = randomInteger(0, transforms.length);
|
||||
[x, y] = transforms[transformIndex](x, y);
|
||||
|
||||
if (i >= 20) {
|
||||
plot(x, y, image);
|
||||
this.plot(x, y);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
render(image: ImageData): void {
|
||||
for (var pixelX = 0; pixelX < image.width; pixelX++) {
|
||||
for (var pixelY = 0; pixelY < image.height; pixelY++) {
|
||||
const hIndex = histIndex(pixelX, pixelY, this.size);
|
||||
if (!this.values[hIndex]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Set the pixel black
|
||||
const iIndex = imageIndex(pixelX, pixelY, this.size);
|
||||
image.data[iIndex + 0] = 0;
|
||||
image.data[iIndex + 1] = 0;
|
||||
image.data[iIndex + 2] = 0;
|
||||
image.data[iIndex + 3] = 0xff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import Blog from "../../../LayoutBlog";
|
||||
|
||||
import { Canvas } from "./0-canvas";
|
||||
import { gasket } from "./1-gasket";
|
||||
import { Canvas, CanvasRenderer } from "./0-canvas";
|
||||
import { RendererGasket } from "./1-gasket";
|
||||
import { renderBaseline } from "./2a-variations";
|
||||
import { renderPost } from "./2b-post";
|
||||
import { renderFinal } from "./2c-final";
|
||||
@ -23,7 +23,16 @@ export default function () {
|
||||
});
|
||||
return (
|
||||
<Layout>
|
||||
<div>
|
||||
<CanvasRenderer
|
||||
params={{
|
||||
defaultUrl: "",
|
||||
size: 400,
|
||||
renderer: new RendererGasket(400),
|
||||
qualityMax: 10,
|
||||
qualityStep: 0.25,
|
||||
}}
|
||||
/>
|
||||
{/* <div>
|
||||
<Canvas f={gasket} />
|
||||
<Canvas f={renderBaseline} />
|
||||
</div>
|
||||
@ -34,7 +43,7 @@ export default function () {
|
||||
<div>
|
||||
<Canvas f={renderLogarithmic} />
|
||||
<Canvas f={renderColor} />
|
||||
</div>
|
||||
</div> */}
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user