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 React, { useEffect, useRef } from "react";
|
||||||
import { renderFn } from "./0-utility";
|
import { Renderer, renderFn } from "./0-utility";
|
||||||
|
|
||||||
export const Canvas: React.FC<{ f: renderFn }> = ({ f }) => {
|
export const Canvas: React.FC<{ f: renderFn }> = ({ f }) => {
|
||||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
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} />;
|
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;
|
export type renderFn = (image: ImageData) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns random number in the bi-unit square (-1, 1)
|
||||||
|
*/
|
||||||
export function randomBiUnit() {
|
export function randomBiUnit() {
|
||||||
// Math.random() produces a number in the range [0, 1),
|
// Math.random() produces a number in the range [0, 1),
|
||||||
// scale to (-1, 1)
|
// scale to (-1, 1)
|
||||||
return Math.random() * 2 - 1;
|
return Math.random() * 2 - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns random integer (with equal weight) in the range [min, max)
|
||||||
|
*/
|
||||||
export function randomInteger(min: number, max: number) {
|
export function randomInteger(min: number, max: number) {
|
||||||
return Math.floor(Math.random() * (max - min)) + min;
|
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] {
|
export function weightedChoice<T>(choices: [number, T][]): [number, T] {
|
||||||
const weightSum = choices.reduce(
|
const weightSum = choices.reduce(
|
||||||
(current, [weight, _t]) => current + weight,
|
(current, [weight, _t]) => current + weight,
|
||||||
@ -29,11 +69,23 @@ export function weightedChoice<T>(choices: [number, T][]): [number, T] {
|
|||||||
throw "unreachable";
|
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) {
|
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
|
// Taken from: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Pixel_manipulation_with_canvas
|
||||||
return y * (width * 4) + x * 4;
|
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) {
|
export function histIndex(x: number, y: number, width: number) {
|
||||||
return y * width + x;
|
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) {
|
type Transform = (x: number, y: number) => [number, number];
|
||||||
// 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);
|
|
||||||
|
|
||||||
// Set the pixel black:
|
export class RendererGasket extends Renderer {
|
||||||
const index = imageIndex(pixelX, pixelY, image.width);
|
private values = new Uint8Array(this.size * this.size);
|
||||||
image.data[index + 0] = 0;
|
|
||||||
image.data[index + 1] = 0;
|
|
||||||
image.data[index + 2] = 0;
|
|
||||||
image.data[index + 3] = 0xff;
|
|
||||||
}
|
|
||||||
|
|
||||||
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) => {
|
if (
|
||||||
const F: Xform[] = [
|
pixelX < 0 ||
|
||||||
(x, y) => {
|
pixelX >= this.size ||
|
||||||
return [x / 2, y / 2];
|
pixelY < 0 ||
|
||||||
},
|
pixelY >= this.size
|
||||||
(x, y) => {
|
) {
|
||||||
return [(x + 1) / 2, y / 2];
|
return;
|
||||||
},
|
}
|
||||||
(x, y) => {
|
|
||||||
return [x / 2, (y + 1) / 2];
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
let x = randomBiUnit();
|
const index = histIndex(pixelX, pixelY, this.size);
|
||||||
let y = randomBiUnit();
|
this.values[index] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
// Plot with quality 1
|
run(quality: number): void {
|
||||||
const iterations = image.height * image.width;
|
const transforms: Transform[] = [
|
||||||
|
(x, y) => [x / 2, y / 2],
|
||||||
|
(x, y) => [(x + 1) / 2, y / 2],
|
||||||
|
(x, y) => [x / 2, (y + 1) / 2],
|
||||||
|
];
|
||||||
|
|
||||||
for (var i = 0; i < iterations; i++) {
|
let x = randomBiUnit();
|
||||||
const Fi = randomInteger(0, F.length);
|
let y = randomBiUnit();
|
||||||
[x, y] = F[Fi](x, y);
|
|
||||||
|
|
||||||
if (i >= 20) {
|
const iterations = quality * this.size * this.size;
|
||||||
plot(x, y, image);
|
for (var i = 0; i < iterations; i++) {
|
||||||
|
const transformIndex = randomInteger(0, transforms.length);
|
||||||
|
[x, y] = transforms[transformIndex](x, y);
|
||||||
|
|
||||||
|
if (i >= 20) {
|
||||||
|
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 Blog from "../../../LayoutBlog";
|
||||||
|
|
||||||
import { Canvas } from "./0-canvas";
|
import { Canvas, CanvasRenderer } from "./0-canvas";
|
||||||
import { gasket } from "./1-gasket";
|
import { RendererGasket } from "./1-gasket";
|
||||||
import { renderBaseline } from "./2a-variations";
|
import { renderBaseline } from "./2a-variations";
|
||||||
import { renderPost } from "./2b-post";
|
import { renderPost } from "./2b-post";
|
||||||
import { renderFinal } from "./2c-final";
|
import { renderFinal } from "./2c-final";
|
||||||
@ -23,7 +23,16 @@ export default function () {
|
|||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<div>
|
<CanvasRenderer
|
||||||
|
params={{
|
||||||
|
defaultUrl: "",
|
||||||
|
size: 400,
|
||||||
|
renderer: new RendererGasket(400),
|
||||||
|
qualityMax: 10,
|
||||||
|
qualityStep: 0.25,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* <div>
|
||||||
<Canvas f={gasket} />
|
<Canvas f={gasket} />
|
||||||
<Canvas f={renderBaseline} />
|
<Canvas f={renderBaseline} />
|
||||||
</div>
|
</div>
|
||||||
@ -34,7 +43,7 @@ export default function () {
|
|||||||
<div>
|
<div>
|
||||||
<Canvas f={renderLogarithmic} />
|
<Canvas f={renderLogarithmic} />
|
||||||
<Canvas f={renderColor} />
|
<Canvas f={renderColor} />
|
||||||
</div>
|
</div> */}
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user