mirror of
https://github.com/bspeice/speice.io
synced 2024-12-22 16:48:10 -05:00
Rewrite canvas to use React state management properly
This commit is contained in:
parent
ce5a28b7bd
commit
112470ce5a
@ -1,6 +1,6 @@
|
|||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState, useContext} from "react";
|
||||||
import Canvas from "../src/Canvas";
|
import {PainterContext} from "../src/Canvas";
|
||||||
import { Params, chaosGameWeighted } from "./chaosGameWeighted";
|
import {chaosGameWeighted } from "./chaosGameWeighted";
|
||||||
import TeX from '@matejmazur/react-katex';
|
import TeX from '@matejmazur/react-katex';
|
||||||
|
|
||||||
import styles from "../src/css/styles.module.css"
|
import styles from "../src/css/styles.module.css"
|
||||||
@ -8,10 +8,6 @@ import styles from "../src/css/styles.module.css"
|
|||||||
type Transform = (x: number, y: number) => [number, number];
|
type Transform = (x: number, y: number) => [number, number];
|
||||||
|
|
||||||
export default function GasketWeighted() {
|
export default function GasketWeighted() {
|
||||||
const image = new ImageData(600, 600);
|
|
||||||
const iterations = 100_000;
|
|
||||||
const step = 1000;
|
|
||||||
|
|
||||||
const [f0Weight, setF0Weight] = useState<number>(1);
|
const [f0Weight, setF0Weight] = useState<number>(1);
|
||||||
const [f1Weight, setF1Weight] = useState<number>(1);
|
const [f1Weight, setF1Weight] = useState<number>(1);
|
||||||
const [f2Weight, setF2Weight] = useState<number>(1);
|
const [f2Weight, setF2Weight] = useState<number>(1);
|
||||||
@ -20,19 +16,14 @@ export default function GasketWeighted() {
|
|||||||
const f1: Transform = (x, y) => [(x + 1) / 2, y / 2];
|
const f1: Transform = (x, y) => [(x + 1) / 2, y / 2];
|
||||||
const f2: Transform = (x, y) => [x / 2, (y + 1) / 2];
|
const f2: Transform = (x, y) => [x / 2, (y + 1) / 2];
|
||||||
|
|
||||||
const [game, setGame] = useState<Generator<ImageData>>(null);
|
const {setPainter} = useContext(PainterContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const params: Params = {
|
setPainter(chaosGameWeighted([
|
||||||
transforms: [
|
|
||||||
[f0Weight, f0],
|
[f0Weight, f0],
|
||||||
[f1Weight, f1],
|
[f1Weight, f1],
|
||||||
[f2Weight, f2]
|
[f2Weight, f2]
|
||||||
],
|
]));
|
||||||
image,
|
|
||||||
iterations,
|
|
||||||
step
|
|
||||||
}
|
|
||||||
setGame(chaosGameWeighted(params))
|
|
||||||
}, [f0Weight, f1Weight, f2Weight]);
|
}, [f0Weight, f1Weight, f2Weight]);
|
||||||
|
|
||||||
const weightInput = (title, weight, setWeight) => (
|
const weightInput = (title, weight, setWeight) => (
|
||||||
@ -47,7 +38,6 @@ export default function GasketWeighted() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Canvas width={image.width} height={image.height} painter={game}/>
|
|
||||||
<div style={{paddingTop: '1em', display: 'grid', gridTemplateColumns: 'auto auto auto'}}>
|
<div style={{paddingTop: '1em', display: 'grid', gridTemplateColumns: 'auto auto auto'}}>
|
||||||
{weightInput("F_0", f0Weight, setF0Weight)}
|
{weightInput("F_0", f0Weight, setF0Weight)}
|
||||||
{weightInput("F_1", f1Weight, setF1Weight)}
|
{weightInput("F_1", f1Weight, setF1Weight)}
|
||||||
|
@ -1,18 +1,16 @@
|
|||||||
function Gasket() {
|
// Hint: try increasing the iteration count
|
||||||
// Hint: try increasing the iteration count
|
const iterations = 10000;
|
||||||
const iterations = 10000;
|
|
||||||
|
|
||||||
// Hint: negating `x` and `y` creates some interesting images
|
// Hint: negating `x` and `y` creates some interesting images
|
||||||
const transforms = [
|
const transforms = [
|
||||||
(x, y) => [x / 2, y / 2],
|
(x, y) => [x / 2, y / 2],
|
||||||
(x, y) => [(x + 1) / 2, y / 2],
|
(x, y) => [(x + 1) / 2, y / 2],
|
||||||
(x, y) => [x / 2, (y + 1) / 2]
|
(x, y) => [x / 2, (y + 1) / 2]
|
||||||
]
|
]
|
||||||
|
|
||||||
const image = new ImageData(600, 600);
|
function* chaosGame() {
|
||||||
|
let image = new ImageData(500, 500);
|
||||||
function* chaosGame() {
|
let [x, y] = [randomBiUnit(), randomBiUnit()];
|
||||||
var [x, y] = [randomBiUnit(), randomBiUnit()];
|
|
||||||
|
|
||||||
for (var count = 0; count < iterations; count++) {
|
for (var count = 0; count < iterations; count++) {
|
||||||
const i = randomInteger(0, transforms.length);
|
const i = randomInteger(0, transforms.length);
|
||||||
@ -24,13 +22,15 @@ function Gasket() {
|
|||||||
if (count % 1000 === 0)
|
if (count % 1000 === 0)
|
||||||
yield image;
|
yield image;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
yield image;
|
||||||
<Canvas
|
}
|
||||||
width={image.width}
|
|
||||||
height={image.height}
|
// Wiring so the code above displays properly
|
||||||
painter={chaosGame()}/>
|
function Gasket() {
|
||||||
)
|
const {setPainter} = useContext(PainterContext);
|
||||||
|
setPainter(chaosGame());
|
||||||
|
|
||||||
|
return (<></>)
|
||||||
}
|
}
|
||||||
render(<Gasket/>)
|
render(<Gasket/>)
|
||||||
|
@ -2,15 +2,12 @@
|
|||||||
import { randomBiUnit } from "../src/randomBiUnit";
|
import { randomBiUnit } from "../src/randomBiUnit";
|
||||||
import { randomChoice } from "../src/randomChoice";
|
import { randomChoice } from "../src/randomChoice";
|
||||||
import { plot } from "./plot"
|
import { plot } from "./plot"
|
||||||
export type Transform = (x: number, y: number) => [number, number];
|
import {Transform} from "../src/transform";
|
||||||
export type Params = {
|
const iterations = 50_000;
|
||||||
transforms: [number, Transform][],
|
const step = 1000;
|
||||||
image: ImageData,
|
|
||||||
iterations: number,
|
|
||||||
step: number
|
|
||||||
}
|
|
||||||
// hidden-end
|
// hidden-end
|
||||||
export function* chaosGameWeighted({transforms, image, iterations, step}: Params) {
|
export function* chaosGameWeighted(transforms: [number, Transform][]) {
|
||||||
|
let image = new ImageData(500, 500);
|
||||||
var [x, y] = [randomBiUnit(), randomBiUnit()];
|
var [x, y] = [randomBiUnit(), randomBiUnit()];
|
||||||
|
|
||||||
for (let i = 0; i < iterations; i++) {
|
for (let i = 0; i < iterations; i++) {
|
||||||
|
@ -177,7 +177,9 @@ import Scope from './scope'
|
|||||||
|
|
||||||
import chaosGameSource from '!!raw-loader!./chaosGame'
|
import chaosGameSource from '!!raw-loader!./chaosGame'
|
||||||
|
|
||||||
|
<!--
|
||||||
<Playground scope={Scope} noInline={true}>{chaosGameSource}</Playground>
|
<Playground scope={Scope} noInline={true}>{chaosGameSource}</Playground>
|
||||||
|
-->
|
||||||
|
|
||||||
<hr/>
|
<hr/>
|
||||||
|
|
||||||
@ -198,7 +200,9 @@ import chaosGameWeightedSource from "!!raw-loader!./chaosGameWeighted";
|
|||||||
|
|
||||||
<CodeBlock language={'typescript'}>{chaosGameWeightedSource}</CodeBlock>
|
<CodeBlock language={'typescript'}>{chaosGameWeightedSource}</CodeBlock>
|
||||||
|
|
||||||
import BrowserOnly from "@docusaurus/BrowserOnly";
|
|
||||||
import GasketWeighted from "./GasketWeighted"
|
import GasketWeighted from "./GasketWeighted"
|
||||||
|
import Canvas from "../src/Canvas"
|
||||||
|
|
||||||
<BrowserOnly>{() => <GasketWeighted/>}</BrowserOnly>
|
<Canvas width={500} height={500}>
|
||||||
|
<GasketWeighted/>
|
||||||
|
</Canvas>
|
@ -1,12 +1,15 @@
|
|||||||
|
import {useContext} from "react";
|
||||||
import { plot } from './plot';
|
import { plot } from './plot';
|
||||||
import { randomBiUnit } from '../src/randomBiUnit';
|
import { randomBiUnit } from '../src/randomBiUnit';
|
||||||
import { randomInteger } from '../src/randomInteger';
|
import { randomInteger } from '../src/randomInteger';
|
||||||
import Canvas from "../src/Canvas";
|
import Canvas, {PainterContext} from "../src/Canvas";
|
||||||
|
|
||||||
const Scope = {
|
const Scope = {
|
||||||
|
Canvas,
|
||||||
|
PainterContext,
|
||||||
plot,
|
plot,
|
||||||
randomBiUnit,
|
randomBiUnit,
|
||||||
randomInteger,
|
randomInteger,
|
||||||
Canvas
|
useContext,
|
||||||
}
|
}
|
||||||
export default Scope;
|
export default Scope;
|
@ -1,4 +1,4 @@
|
|||||||
import {useState} from "react";
|
import {useContext, useEffect, useState} from "react";
|
||||||
import { blend } from "./blend";
|
import { blend } from "./blend";
|
||||||
import { applyCoefs, Coefs } from "../src/coefs"
|
import { applyCoefs, Coefs } from "../src/coefs"
|
||||||
import {randomBiUnit} from "../src/randomBiUnit";
|
import {randomBiUnit} from "../src/randomBiUnit";
|
||||||
@ -19,7 +19,7 @@ import {
|
|||||||
} from "../src/params";
|
} from "../src/params";
|
||||||
import {randomChoice} from "../src/randomChoice";
|
import {randomChoice} from "../src/randomChoice";
|
||||||
import {plotBinary} from "../src/plotBinary"
|
import {plotBinary} from "../src/plotBinary"
|
||||||
import Canvas from "../src/Canvas"
|
import {PainterContext} from "../src/Canvas"
|
||||||
|
|
||||||
import styles from "../src/css/styles.module.css"
|
import styles from "../src/css/styles.module.css"
|
||||||
|
|
||||||
@ -31,10 +31,11 @@ type VariationBlend = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function FlameBlend() {
|
export default function FlameBlend() {
|
||||||
const image = new ImageData(400, 400);
|
|
||||||
const quality = 2;
|
const quality = 2;
|
||||||
const step = 5000;
|
const step = 5000;
|
||||||
|
|
||||||
|
const {width, height, setPainter} = useContext(PainterContext);
|
||||||
|
|
||||||
const xform1Default: VariationBlend = {
|
const xform1Default: VariationBlend = {
|
||||||
linear: 0,
|
linear: 0,
|
||||||
julia: 1,
|
julia: 1,
|
||||||
@ -73,6 +74,7 @@ export default function FlameBlend() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const image = new ImageData(width, height);
|
||||||
function* chaosGame() {
|
function* chaosGame() {
|
||||||
let [x, y] = [randomBiUnit(), randomBiUnit()];
|
let [x, y] = [randomBiUnit(), randomBiUnit()];
|
||||||
const transforms: [number, Transform][] = [
|
const transforms: [number, Transform][] = [
|
||||||
@ -97,6 +99,7 @@ export default function FlameBlend() {
|
|||||||
|
|
||||||
yield image;
|
yield image;
|
||||||
}
|
}
|
||||||
|
useEffect(() => setPainter(chaosGame()), [xform1Variations, xform2Variations, xform3Variations]);
|
||||||
|
|
||||||
const variationEditor = (title, variations, setVariations) => {
|
const variationEditor = (title, variations, setVariations) => {
|
||||||
return (
|
return (
|
||||||
@ -127,16 +130,10 @@ export default function FlameBlend() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<Canvas
|
|
||||||
width={image.width}
|
|
||||||
height={image.height}
|
|
||||||
painter={chaosGame()}/>
|
|
||||||
<div style={{paddingTop: '1em', display: 'grid', gridTemplateColumns: 'auto auto auto auto'}}>
|
<div style={{paddingTop: '1em', display: 'grid', gridTemplateColumns: 'auto auto auto auto'}}>
|
||||||
{variationEditor("Transform 1", xform1Variations, setXform1Variations)}
|
{variationEditor("Transform 1", xform1Variations, setXform1Variations)}
|
||||||
{variationEditor("Transform 2", xform2Variations, setXform2Variations)}
|
{variationEditor("Transform 2", xform2Variations, setXform2Variations)}
|
||||||
{variationEditor("Transform 3", xform3Variations, setXform3Variations)}
|
{variationEditor("Transform 3", xform3Variations, setXform3Variations)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -119,7 +119,7 @@ import pdjSrc from '!!raw-loader!../src/pdj'
|
|||||||
|
|
||||||
<CodeBlock language={'typescript'}>{pdjSrc}</CodeBlock>
|
<CodeBlock language={'typescript'}>{pdjSrc}</CodeBlock>
|
||||||
|
|
||||||
### Blending
|
## Blending
|
||||||
|
|
||||||
Now, one variation is fun, but we can also combine variations in a single transform by "blending."
|
Now, one variation is fun, but we can also combine variations in a single transform by "blending."
|
||||||
Each variation receives the same $x$ and $y$ inputs, and we add together each variation's $x$ and $y$ outputs.
|
Each variation receives the same $x$ and $y$ inputs, and we add together each variation's $x$ and $y$ outputs.
|
||||||
@ -132,10 +132,15 @@ $$
|
|||||||
|
|
||||||
The formula looks intimidating, but it's not hard to implement:
|
The formula looks intimidating, but it's not hard to implement:
|
||||||
|
|
||||||
TODO: Blending implementation?
|
import blendSource from "!!raw-loader!./blend";
|
||||||
|
|
||||||
|
<CodeBlock language={'typescript'}>{blendSource}</CodeBlock>
|
||||||
|
|
||||||
And with that in place, we have enough to render a first full fractal flame:
|
And with that in place, we have enough to render a first full fractal flame:
|
||||||
|
|
||||||
|
import Canvas from "../src/Canvas";
|
||||||
import FlameBlend from "./FlameBlend";
|
import FlameBlend from "./FlameBlend";
|
||||||
|
|
||||||
<FlameBlend/>
|
<Canvas width={500} height={500}>
|
||||||
|
<FlameBlend/>
|
||||||
|
</Canvas>
|
@ -1,16 +1,53 @@
|
|||||||
import React, {useCallback, useEffect, useState} from "react";
|
import React, {useCallback, useEffect, useState, createContext} from "react";
|
||||||
import {useColorMode} from "@docusaurus/theme-common";
|
import {useColorMode} from "@docusaurus/theme-common";
|
||||||
|
|
||||||
interface Props {
|
export interface PainterProps {
|
||||||
|
readonly width: number;
|
||||||
|
readonly height: number;
|
||||||
|
readonly setPainter: (painter: Iterator<ImageData>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Context provider for child elements to submit image iterator functions
|
||||||
|
* (painters) for rendering
|
||||||
|
*/
|
||||||
|
export const PainterContext = createContext<PainterProps>(null);
|
||||||
|
|
||||||
|
interface CanvasProps {
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
painter: Iterator<ImageData>;
|
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
export default function Canvas({width, height, painter, children}: Props) {
|
|
||||||
const {colorMode} = useColorMode();
|
|
||||||
const [image, setImage] = useState<[ImageData]>(null);
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw fractal flames to a canvas.
|
||||||
|
*
|
||||||
|
* This component is a bit involved because it attempts to solve
|
||||||
|
* a couple problems at the same time:
|
||||||
|
* - Incrementally drawing an image to the canvas
|
||||||
|
* - Interrupting drawing with new parameters on demand
|
||||||
|
* - Dark mode
|
||||||
|
*
|
||||||
|
* Image iterators provide a means to draw incremental images;
|
||||||
|
* iterators can easily checkpoint state, and this component will
|
||||||
|
* request the next image on the next animation frame. As a result,
|
||||||
|
* the browser should be responsive even though we run CPU-heavy
|
||||||
|
* code on the main thread.
|
||||||
|
*
|
||||||
|
* Swapping a new iterator allows interrupting a render in progress,
|
||||||
|
* as the canvas completely repaints on each provided image.
|
||||||
|
*
|
||||||
|
* Finally, check whether dark mode is active, and invert the most
|
||||||
|
* recent image prior to painting if so.
|
||||||
|
*
|
||||||
|
* PainterContext is used to allow child elements to swap in
|
||||||
|
* new iterators.
|
||||||
|
*
|
||||||
|
* @param width Canvas draw width
|
||||||
|
* @param height Canvas draw height
|
||||||
|
* @param children Child elements
|
||||||
|
*/
|
||||||
|
export default function Canvas({width, height, children}: CanvasProps) {
|
||||||
const [canvasCtx, setCanvasCtx] = useState<CanvasRenderingContext2D>(null);
|
const [canvasCtx, setCanvasCtx] = useState<CanvasRenderingContext2D>(null);
|
||||||
const canvasRef = useCallback(node => {
|
const canvasRef = useCallback(node => {
|
||||||
if (node !== null) {
|
if (node !== null) {
|
||||||
@ -18,52 +55,93 @@ export default function Canvas({width, height, painter, children}: Props) {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const paintImage = new ImageData(width, height);
|
const {colorMode} = useColorMode();
|
||||||
const paint = () => {
|
|
||||||
if (!canvasCtx || !image) {
|
// Holder objects are used to force re-painting even if the iterator
|
||||||
|
// returns a modified image with the same reference
|
||||||
|
type ImageHolder = { image?: ImageData };
|
||||||
|
const [imageHolder, setImageHolder] = useState<ImageHolder>({ image: null });
|
||||||
|
useEffect(() => {
|
||||||
|
const image = imageHolder.image;
|
||||||
|
if (!image) {
|
||||||
|
// No image is available, leave the canvas as-is
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [index, value] of image[0].data.entries()) {
|
if (!canvasCtx) {
|
||||||
if (index % 4 === 3) {
|
// Canvas is not ready for the image we have,
|
||||||
// Alpha values are copied as-is
|
// re-submit the image and wait for the ref to populate
|
||||||
paintImage.data[index] = value;
|
setImageHolder({ image });
|
||||||
} else {
|
return;
|
||||||
// If dark mode is active, invert the color
|
|
||||||
paintImage.data[index] = colorMode === 'light' ? value : 255 - value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If light mode is active, paint the image as-is
|
||||||
|
if (colorMode === 'light') {
|
||||||
|
canvasCtx.putImageData(image, 0, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If dark mode is active, copy the image into a new buffer
|
||||||
|
// and invert colors prior to painting.
|
||||||
|
// Copy alpha values as-is.
|
||||||
|
const paintImage = new ImageData(image.width, image.height);
|
||||||
|
image.data.forEach((value, index) => {
|
||||||
|
const isAlpha = index % 4 === 3;
|
||||||
|
paintImage.data[index] = isAlpha ? value : 255 - value;
|
||||||
|
})
|
||||||
canvasCtx.putImageData(paintImage, 0, 0);
|
canvasCtx.putImageData(paintImage, 0, 0);
|
||||||
}
|
}, [colorMode, imageHolder]);
|
||||||
useEffect(paint, [colorMode, image]);
|
|
||||||
|
|
||||||
const animate = () => {
|
// Image iterators (painters) are also in a holder; this allows
|
||||||
|
// re-submitting the existing iterator to draw the next frame,
|
||||||
|
// and also allows child components to over-write the iterator
|
||||||
|
// if a new set of parameters becomes available
|
||||||
|
// TODO(bspeice): Potential race condition?
|
||||||
|
// Not sure if it's possible for painters submitted by children
|
||||||
|
// to be over-ridden as a result re-submitting the
|
||||||
|
// existing iterator
|
||||||
|
type PainterHolder = { painter?: Iterator<ImageData> };
|
||||||
|
const [animHolder, setAnimHolder] = useState<PainterHolder>({ painter: null });
|
||||||
|
useEffect(() => {
|
||||||
|
const painter = animHolder.painter;
|
||||||
if (!painter) {
|
if (!painter) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Animating");
|
if (!canvasCtx) {
|
||||||
const nextImage = painter.next().value;
|
setAnimHolder({ painter });
|
||||||
if (nextImage) {
|
return;
|
||||||
setImage([nextImage])
|
|
||||||
requestAnimationFrame(animate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const image = painter.next().value;
|
||||||
|
if (image) {
|
||||||
|
setImageHolder({ image });
|
||||||
|
setAnimHolder({ painter });
|
||||||
|
} else {
|
||||||
|
setAnimHolder({ painter: null });
|
||||||
}
|
}
|
||||||
useEffect(animate, [painter, canvasCtx]);
|
}, [animHolder, canvasCtx]);
|
||||||
|
|
||||||
|
// Finally, child elements submit painters through a context provider
|
||||||
|
const [painter, setPainter] = useState<Iterator<ImageData>>(null);
|
||||||
|
useEffect(() => setAnimHolder({ painter }), [painter]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<center>
|
||||||
<canvas
|
<canvas
|
||||||
ref={canvasRef}
|
ref={canvasRef}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
style={{
|
style={{
|
||||||
aspectRatio: width / height,
|
aspectRatio: width / height,
|
||||||
width: '100%'
|
width: '80%'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</center>
|
||||||
|
<PainterContext.Provider value={{width, height, setPainter}}>
|
||||||
{children}
|
{children}
|
||||||
|
</PainterContext.Provider>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -4,9 +4,7 @@
|
|||||||
* The way `flam3` actually calculates the "camera" for mapping a point
|
* The way `flam3` actually calculates the "camera" for mapping a point
|
||||||
* to its pixel coordinate is fairly involved - it also needs to calculate
|
* to its pixel coordinate is fairly involved - it also needs to calculate
|
||||||
* zoom and rotation (see the bucket accumulator code in rect.c).
|
* zoom and rotation (see the bucket accumulator code in rect.c).
|
||||||
* We'll make some simplifying assumptions:
|
* We simplify things here by assuming a square image
|
||||||
* - The final image is square
|
|
||||||
* - We want to plot the range [-2, 2]
|
|
||||||
*
|
*
|
||||||
* The reference parameters were designed in Apophysis, which uses the
|
* The reference parameters were designed in Apophysis, which uses the
|
||||||
* range [-2, 2] by default (the `scale` parameter in XML defines the
|
* range [-2, 2] by default (the `scale` parameter in XML defines the
|
||||||
@ -15,7 +13,7 @@
|
|||||||
*
|
*
|
||||||
* @param x point in the range [-2, 2]
|
* @param x point in the range [-2, 2]
|
||||||
* @param y point in the range [-2, 2]
|
* @param y point in the range [-2, 2]
|
||||||
* @param size image size
|
* @param size image width/height in pixels
|
||||||
* @returns pair of pixel coordinates
|
* @returns pair of pixel coordinates
|
||||||
*/
|
*/
|
||||||
export function camera(x: number, y: number, size: number): [number, number] {
|
export function camera(x: number, y: number, size: number): [number, number] {
|
||||||
|
Loading…
Reference in New Issue
Block a user