Code for 2D camera system

This commit is contained in:
Bradlee Speice 2025-03-08 12:26:20 -05:00
parent 7fb5a22cdf
commit 2d810fe20f
7 changed files with 291 additions and 5 deletions

View File

@ -27,6 +27,8 @@ If the chaos game encounters the same pixel twice, nothing changes.
To demonstrate how much work is wasted, we'll count each time the chaos game To demonstrate how much work is wasted, we'll count each time the chaos game
visits a pixel while iterating. This gives us a kind of image "histogram": visits a pixel while iterating. This gives us a kind of image "histogram":
import CodeBlock from "@theme/CodeBlock";
import chaosGameHistogramSource from "!!raw-loader!./chaosGameHistogram" import chaosGameHistogramSource from "!!raw-loader!./chaosGameHistogram"
<CodeBlock language="typescript">{chaosGameHistogramSource}</CodeBlock> <CodeBlock language="typescript">{chaosGameHistogramSource}</CodeBlock>
@ -35,8 +37,6 @@ When the chaos game finishes, we find the pixel encountered most often.
Finally, we "paint" the image by setting each pixel's alpha (transparency) value Finally, we "paint" the image by setting each pixel's alpha (transparency) value
to the ratio of times visited divided by the maximum: to the ratio of times visited divided by the maximum:
import CodeBlock from "@theme/CodeBlock";
import paintLinearSource from "!!raw-loader!./paintLinear" import paintLinearSource from "!!raw-loader!./paintLinear"
<CodeBlock language="typescript">{paintLinearSource}</CodeBlock> <CodeBlock language="typescript">{paintLinearSource}</CodeBlock>

View File

@ -0,0 +1,80 @@
import React, { useContext, useEffect } from "react";
import * as params from "../src/params";
import { PainterContext } from "../src/Canvas";
import {chaosGameCamera, Props as ChaosGameColorProps} from "./chaosGameCamera";
import styles from "../src/css/styles.module.css";
type Props = {
children?: React.ReactElement;
}
export default function FlameCamera({ children }: Props) {
const { width, height, setPainter } = useContext(PainterContext);
// Scale chosen so the largest axis is `[-2, 2]` in IFS coordinates,
// the smaller axis will be a shorter range to maintain the aspect ratio.
const scale = Math.max(width, height) / 4;
const [zoom, setZoom] = React.useState(0);
const [rotate, setRotate] = React.useState(0);
const [offsetX, setOffsetX] = React.useState(0);
const [offsetY, setOffsetY] = React.useState(0);
const resetCamera = () => {
setZoom(0);
setRotate(0);
setOffsetX(0);
setOffsetY(0);
}
const resetButton = <button className={styles.inputReset} onClick={resetCamera}>Reset</button>;
useEffect(() => {
const gameParams: ChaosGameColorProps = {
width,
height,
transforms: params.xforms,
final: params.xformFinal,
palette: params.palette,
colors: [
{color: params.xform1Color, colorSpeed: 0.5},
{color: params.xform2Color, colorSpeed: 0.5},
{color: params.xform3Color, colorSpeed: 0.5}
],
finalColor: { color: params.xformFinalColor, colorSpeed: 0.5 },
scale,
zoom,
rotate: rotate / 180 * Math.PI,
offsetX,
offsetY
};
setPainter(chaosGameCamera(gameParams));
}, [scale, zoom, rotate, offsetX, offsetY]);
return (
<>
<div className={styles.inputGroup} style={{display: "grid", gridTemplateColumns: "1fr 1fr"}}>
<p className={styles.inputTitle} style={{gridColumn: "1/-1"}}>Camera {resetButton}</p>
<div className={styles.inputElement}>
<p>Zoom: {zoom}</p>
<input type={"range"} min={-0.5} max={2} step={0.01} value={zoom}
onInput={e => setZoom(Number(e.currentTarget.value))}/>
</div>
<div className={styles.inputElement}>
<p>Rotate (deg): {rotate}</p>
<input type={"range"} min={0} max={360} step={1} value={rotate}
onInput={e => setRotate(Number(e.currentTarget.value))}/>
</div>
<div className={styles.inputElement}>
<p>Offset X: {offsetX}</p>
<input type={"range"} min={-2} max={2} step={0.01} value={offsetX}
onInput={e => setOffsetX(Number(e.currentTarget.value))}/>
</div>
<div className={styles.inputElement}>
<p>Offset Y: {offsetY}</p>
<input type={"range"} min={-2} max={2} step={0.01} value={offsetY}
onInput={e => setOffsetY(Number(e.currentTarget.value))}/>
</div>
</div>
{children}
</>
);
}

View File

@ -0,0 +1,38 @@
export function camera(
x: number,
y: number,
width: number,
height: number,
scale: number,
zoom: number,
rotate: number,
offsetX: number,
offsetY: number,
): [number, number] {
const zoomFactor = Math.pow(2, zoom);
// Zoom, offset, and rotation are
// applied in IFS coordinates
[x, y] = [
(x - offsetX) * zoomFactor,
(y - offsetY) * zoomFactor,
];
[x, y] = [
x * Math.cos(rotate) -
y * Math.sin(rotate),
x * Math.sin(rotate) +
y * Math.cos(rotate),
]
// Scale is applied to pixel
// coordinates. Shift by half
// the image width and height
// to compensate for the
// IFS coordinates being symmetric
// around the origin
return [
Math.floor(x * scale + width / 2),
Math.floor(y * scale + height / 2)
];
}

View File

@ -0,0 +1,139 @@
// hidden-start
import { Props as ChaosGameColorProps } from "../3-log-density/chaosGameColor";
import { randomBiUnit } from "../src/randomBiUnit";
import { randomChoice } from "../src/randomChoice";
import { camera } from "./camera";
import { histIndex } from "../src/camera";
import {colorFromPalette} from "../3-log-density/colorFromPalette";
import {mixColor} from "../3-log-density/mixColor";
import {paintColor} from "../3-log-density/paintColor";
const quality = 10;
const step = 100_000;
// hidden-end
export type Props = ChaosGameColorProps & {
scale: number;
zoom: number,
rotate: number;
offsetX: number;
offsetY: number;
}
export function* chaosGameCamera(
{
width,
height,
transforms,
final,
palette,
colors,
finalColor,
scale,
zoom,
rotate,
offsetX,
offsetY,
}: Props
) {
const pixels = width * height;
// highlight-start
const imgRed = Array<number>(pixels)
.fill(0);
const imgGreen = Array<number>(pixels)
.fill(0);
const imgBlue = Array<number>(pixels)
.fill(0);
const imgAlpha = Array<number>(pixels)
.fill(0);
const plotColor = (
x: number,
y: number,
c: number
) => {
const [pixelX, pixelY] =
camera(x, y, width, height, scale, zoom, rotate, offsetX, offsetY);
if (
pixelX < 0 ||
pixelX >= width ||
pixelY < 0 ||
pixelY >= width
)
return;
const hIndex =
histIndex(pixelX, pixelY, width, 1);
const [r, g, b] =
colorFromPalette(palette, c);
imgRed[hIndex] += r;
imgGreen[hIndex] += g;
imgBlue[hIndex] += b;
imgAlpha[hIndex] += 1;
}
// highlight-end
let [x, y] = [
randomBiUnit(),
randomBiUnit()
];
let c = Math.random();
const iterations = quality * pixels;
for (let i = 0; i < iterations; i++) {
const [transformIndex, transform] =
randomChoice(transforms);
[x, y] = transform(x, y);
// highlight-start
const transformColor =
colors[transformIndex];
c = mixColor(
c,
transformColor.color,
transformColor.colorSpeed
);
// highlight-end
const [finalX, finalY] = final(x, y);
// highlight-start
const finalC = mixColor(
c,
finalColor.color,
finalColor.colorSpeed
);
// highlight-end
if (i > 20)
plotColor(
finalX,
finalY,
finalC
)
if (i % step === 0)
yield paintColor(
width,
height,
imgRed,
imgGreen,
imgBlue,
imgAlpha
);
}
yield paintColor(
width,
height,
imgRed,
imgGreen,
imgBlue,
imgAlpha
);
}

View File

@ -0,0 +1,20 @@
---
slug: 2025/03/playing-with-fire-camera
title: "Playing with fire: The camera"
date: 2025-03-07 12:00:00
authors: [bspeice]
tags: []
---
<!-- truncate -->
import CodeBlock from "@theme/CodeBlock";
import cameraSource from "!!raw-loader!./camera"
<CodeBlock language="typescript">{cameraSource}</CodeBlock>
import {SquareCanvas} from "../src/Canvas";
import FlameCamera from "./FlameCamera";
<SquareCanvas name={"flame_camera"} width={'95%'} aspectRatio={'4/3'}><FlameCamera /></SquareCanvas>

View File

@ -18,6 +18,8 @@ const downloadImage = (name: string) =>
type CanvasProps = { type CanvasProps = {
name: string; name: string;
width?: string;
aspectRatio?: string;
style?: any; style?: any;
children?: React.ReactElement children?: React.ReactElement
} }
@ -105,6 +107,6 @@ export const Canvas: React.FC<CanvasProps> = ({name, style, children}) => {
) )
} }
export const SquareCanvas: React.FC<CanvasProps> = ({name, style, children}) => { export const SquareCanvas: React.FC<CanvasProps> = ({name, width, aspectRatio, style, children}) => {
return <center><Canvas name={name} style={{width: '75%', aspectRatio: '1/1', ...style}} children={children}/></center> return <center><Canvas name={name} style={{width: width ?? '75%', aspectRatio: aspectRatio ?? '1/1', ...style}} children={children}/></center>
} }

9
package-lock.json generated
View File

@ -3035,6 +3035,7 @@
"version": "3.7.0", "version": "3.7.0",
"resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.7.0.tgz", "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.7.0.tgz",
"integrity": "sha512-b0fUmaL+JbzDIQaamzpAFpTviiaU4cX3Qz8cuo14+HGBCwa0evEK0UYCBFY3n4cLzL8Op1BueeroUD2LYAIHbQ==", "integrity": "sha512-b0fUmaL+JbzDIQaamzpAFpTviiaU4cX3Qz8cuo14+HGBCwa0evEK0UYCBFY3n4cLzL8Op1BueeroUD2LYAIHbQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"@docusaurus/babel": "3.7.0", "@docusaurus/babel": "3.7.0",
"@docusaurus/bundler": "3.7.0", "@docusaurus/bundler": "3.7.0",
@ -3109,6 +3110,7 @@
"version": "3.7.0", "version": "3.7.0",
"resolved": "https://registry.npmjs.org/@docusaurus/faster/-/faster-3.7.0.tgz", "resolved": "https://registry.npmjs.org/@docusaurus/faster/-/faster-3.7.0.tgz",
"integrity": "sha512-d+7uyOEs3SBk38i2TL79N6mFaP7J4knc5lPX/W9od+jplXZhnDdl5ZMh2u2Lg7JxGV/l33Bd7h/xwv4mr21zag==", "integrity": "sha512-d+7uyOEs3SBk38i2TL79N6mFaP7J4knc5lPX/W9od+jplXZhnDdl5ZMh2u2Lg7JxGV/l33Bd7h/xwv4mr21zag==",
"license": "MIT",
"dependencies": { "dependencies": {
"@docusaurus/types": "3.7.0", "@docusaurus/types": "3.7.0",
"@rspack/core": "1.2.0-alpha.0", "@rspack/core": "1.2.0-alpha.0",
@ -3362,6 +3364,7 @@
"version": "3.7.0", "version": "3.7.0",
"resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.7.0.tgz", "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.7.0.tgz",
"integrity": "sha512-g7WdPqDNaqA60CmBrr0cORTrsOit77hbsTj7xE2l71YhBn79sxdm7WMK7wfhcaafkbpIh7jv5ef5TOpf1Xv9Lg==", "integrity": "sha512-g7WdPqDNaqA60CmBrr0cORTrsOit77hbsTj7xE2l71YhBn79sxdm7WMK7wfhcaafkbpIh7jv5ef5TOpf1Xv9Lg==",
"license": "MIT",
"dependencies": { "dependencies": {
"@docusaurus/types": "3.7.0", "@docusaurus/types": "3.7.0",
"@types/history": "^4.7.11", "@types/history": "^4.7.11",
@ -3586,6 +3589,7 @@
"version": "3.7.0", "version": "3.7.0",
"resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.7.0.tgz", "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.7.0.tgz",
"integrity": "sha512-nPHj8AxDLAaQXs+O6+BwILFuhiWbjfQWrdw2tifOClQoNfuXDjfjogee6zfx6NGHWqshR23LrcN115DmkHC91Q==", "integrity": "sha512-nPHj8AxDLAaQXs+O6+BwILFuhiWbjfQWrdw2tifOClQoNfuXDjfjogee6zfx6NGHWqshR23LrcN115DmkHC91Q==",
"license": "MIT",
"dependencies": { "dependencies": {
"@docusaurus/core": "3.7.0", "@docusaurus/core": "3.7.0",
"@docusaurus/plugin-content-blog": "3.7.0", "@docusaurus/plugin-content-blog": "3.7.0",
@ -3681,6 +3685,7 @@
"version": "3.7.0", "version": "3.7.0",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-live-codeblock/-/theme-live-codeblock-3.7.0.tgz", "resolved": "https://registry.npmjs.org/@docusaurus/theme-live-codeblock/-/theme-live-codeblock-3.7.0.tgz",
"integrity": "sha512-peLs77sk+TuHjAnhyhT8IH3Qsr/zewpwHg5A4EOe/8K4Lj2T8fhro1/Dj66FS8784wwAoxhy5A9Ux9Rsp8h87w==", "integrity": "sha512-peLs77sk+TuHjAnhyhT8IH3Qsr/zewpwHg5A4EOe/8K4Lj2T8fhro1/Dj66FS8784wwAoxhy5A9Ux9Rsp8h87w==",
"license": "MIT",
"dependencies": { "dependencies": {
"@docusaurus/core": "3.7.0", "@docusaurus/core": "3.7.0",
"@docusaurus/theme-common": "3.7.0", "@docusaurus/theme-common": "3.7.0",
@ -3746,12 +3751,14 @@
"version": "3.7.0", "version": "3.7.0",
"resolved": "https://registry.npmjs.org/@docusaurus/tsconfig/-/tsconfig-3.7.0.tgz", "resolved": "https://registry.npmjs.org/@docusaurus/tsconfig/-/tsconfig-3.7.0.tgz",
"integrity": "sha512-vRsyj3yUZCjscgfgcFYjIsTcAru/4h4YH2/XAE8Rs7wWdnng98PgWKvP5ovVc4rmRpRg2WChVW0uOy2xHDvDBQ==", "integrity": "sha512-vRsyj3yUZCjscgfgcFYjIsTcAru/4h4YH2/XAE8Rs7wWdnng98PgWKvP5ovVc4rmRpRg2WChVW0uOy2xHDvDBQ==",
"dev": true "dev": true,
"license": "MIT"
}, },
"node_modules/@docusaurus/types": { "node_modules/@docusaurus/types": {
"version": "3.7.0", "version": "3.7.0",
"resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.7.0.tgz", "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.7.0.tgz",
"integrity": "sha512-kOmZg5RRqJfH31m+6ZpnwVbkqMJrPOG5t0IOl4i/+3ruXyNfWzZ0lVtVrD0u4ONc/0NOsS9sWYaxxWNkH1LdLQ==", "integrity": "sha512-kOmZg5RRqJfH31m+6ZpnwVbkqMJrPOG5t0IOl4i/+3ruXyNfWzZ0lVtVrD0u4ONc/0NOsS9sWYaxxWNkH1LdLQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"@mdx-js/mdx": "^3.0.0", "@mdx-js/mdx": "^3.0.0",
"@types/history": "^4.7.11", "@types/history": "^4.7.11",