mirror of
https://github.com/bspeice/speice.io
synced 2025-03-10 00:31:30 -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
|
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>
|
||||||
|
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 = {
|
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
9
package-lock.json
generated
@ -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",
|
||||||
|
Loading…
Reference in New Issue
Block a user