mirror of
				https://github.com/bspeice/speice.io
				synced 2025-11-04 02:20:36 -05:00 
			
		
		
		
	Start on animations so I don't kill the browser
This commit is contained in:
		@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  let x = randomBiUnit();
 | 
			
		||||
  let y = randomBiUnit();
 | 
			
		||||
    const index = histIndex(pixelX, pixelY, this.size);
 | 
			
		||||
    this.values[index] = 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Plot with quality 1
 | 
			
		||||
  const iterations = image.height * image.width;
 | 
			
		||||
  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],
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
  for (var i = 0; i < iterations; i++) {
 | 
			
		||||
    const Fi = randomInteger(0, F.length);
 | 
			
		||||
    [x, y] = F[Fi](x, y);
 | 
			
		||||
    let x = randomBiUnit();
 | 
			
		||||
    let y = randomBiUnit();
 | 
			
		||||
 | 
			
		||||
    if (i >= 20) {
 | 
			
		||||
      plot(x, y, image);
 | 
			
		||||
    const iterations = quality * this.size * this.size;
 | 
			
		||||
    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 { 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>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user