mirror of
https://github.com/bspeice/speice.io
synced 2025-03-09 16:21:29 -04:00
Code for 2D camera system
This commit is contained in:
parent
7fb5a22cdf
commit
2d810fe20f
@ -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>
|
||||
}
|
9
package-lock.json
generated
9
package-lock.json
generated
@ -3035,6 +3035,7 @@
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.7.0.tgz",
|
||||
"integrity": "sha512-b0fUmaL+JbzDIQaamzpAFpTviiaU4cX3Qz8cuo14+HGBCwa0evEK0UYCBFY3n4cLzL8Op1BueeroUD2LYAIHbQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@docusaurus/babel": "3.7.0",
|
||||
"@docusaurus/bundler": "3.7.0",
|
||||
@ -3109,6 +3110,7 @@
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/faster/-/faster-3.7.0.tgz",
|
||||
"integrity": "sha512-d+7uyOEs3SBk38i2TL79N6mFaP7J4knc5lPX/W9od+jplXZhnDdl5ZMh2u2Lg7JxGV/l33Bd7h/xwv4mr21zag==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@docusaurus/types": "3.7.0",
|
||||
"@rspack/core": "1.2.0-alpha.0",
|
||||
@ -3362,6 +3364,7 @@
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.7.0.tgz",
|
||||
"integrity": "sha512-g7WdPqDNaqA60CmBrr0cORTrsOit77hbsTj7xE2l71YhBn79sxdm7WMK7wfhcaafkbpIh7jv5ef5TOpf1Xv9Lg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@docusaurus/types": "3.7.0",
|
||||
"@types/history": "^4.7.11",
|
||||
@ -3586,6 +3589,7 @@
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.7.0.tgz",
|
||||
"integrity": "sha512-nPHj8AxDLAaQXs+O6+BwILFuhiWbjfQWrdw2tifOClQoNfuXDjfjogee6zfx6NGHWqshR23LrcN115DmkHC91Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "3.7.0",
|
||||
"@docusaurus/plugin-content-blog": "3.7.0",
|
||||
@ -3681,6 +3685,7 @@
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/theme-live-codeblock/-/theme-live-codeblock-3.7.0.tgz",
|
||||
"integrity": "sha512-peLs77sk+TuHjAnhyhT8IH3Qsr/zewpwHg5A4EOe/8K4Lj2T8fhro1/Dj66FS8784wwAoxhy5A9Ux9Rsp8h87w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "3.7.0",
|
||||
"@docusaurus/theme-common": "3.7.0",
|
||||
@ -3746,12 +3751,14 @@
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/tsconfig/-/tsconfig-3.7.0.tgz",
|
||||
"integrity": "sha512-vRsyj3yUZCjscgfgcFYjIsTcAru/4h4YH2/XAE8Rs7wWdnng98PgWKvP5ovVc4rmRpRg2WChVW0uOy2xHDvDBQ==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@docusaurus/types": {
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.7.0.tgz",
|
||||
"integrity": "sha512-kOmZg5RRqJfH31m+6ZpnwVbkqMJrPOG5t0IOl4i/+3ruXyNfWzZ0lVtVrD0u4ONc/0NOsS9sWYaxxWNkH1LdLQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mdx-js/mdx": "^3.0.0",
|
||||
"@types/history": "^4.7.11",
|
||||
|
Loading…
Reference in New Issue
Block a user