Code for 2D camera system

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

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>