mirror of
https://github.com/bspeice/speice.io
synced 2025-07-01 13:56:11 -04:00
Code for 2D camera system
This commit is contained in:
@ -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
|
||||
visits a pixel while iterating. This gives us a kind of image "histogram":
|
||||
|
||||
import CodeBlock from "@theme/CodeBlock";
|
||||
|
||||
import chaosGameHistogramSource from "!!raw-loader!./chaosGameHistogram"
|
||||
|
||||
<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
|
||||
to the ratio of times visited divided by the maximum:
|
||||
|
||||
import CodeBlock from "@theme/CodeBlock";
|
||||
|
||||
import paintLinearSource from "!!raw-loader!./paintLinear"
|
||||
|
||||
<CodeBlock language="typescript">{paintLinearSource}</CodeBlock>
|
||||
|
80
blog/2024-11-15-playing-with-fire/4-camera/FlameCamera.tsx
Normal file
80
blog/2024-11-15-playing-with-fire/4-camera/FlameCamera.tsx
Normal 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}
|
||||
</>
|
||||
);
|
||||
}
|
38
blog/2024-11-15-playing-with-fire/4-camera/camera.ts
Normal file
38
blog/2024-11-15-playing-with-fire/4-camera/camera.ts
Normal 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)
|
||||
];
|
||||
}
|
139
blog/2024-11-15-playing-with-fire/4-camera/chaosGameCamera.ts
Normal file
139
blog/2024-11-15-playing-with-fire/4-camera/chaosGameCamera.ts
Normal 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
|
||||
);
|
||||
}
|
20
blog/2024-11-15-playing-with-fire/4-camera/index.mdx
Normal file
20
blog/2024-11-15-playing-with-fire/4-camera/index.mdx
Normal 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>
|
@ -18,6 +18,8 @@ const downloadImage = (name: string) =>
|
||||
|
||||
type CanvasProps = {
|
||||
name: string;
|
||||
width?: string;
|
||||
aspectRatio?: string;
|
||||
style?: any;
|
||||
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}) => {
|
||||
return <center><Canvas name={name} style={{width: '75%', aspectRatio: '1/1', ...style}} children={children}/></center>
|
||||
export const SquareCanvas: React.FC<CanvasProps> = ({name, width, aspectRatio, style, children}) => {
|
||||
return <center><Canvas name={name} style={{width: width ?? '75%', aspectRatio: aspectRatio ?? '1/1', ...style}} children={children}/></center>
|
||||
}
|
Reference in New Issue
Block a user