Skip to content

Tiny, dependency free library to easily generate fractals

License

Notifications You must be signed in to change notification settings

4031651/fractals

Repository files navigation

Tiny, dependency free library to easily generate fractals

This library allows you to generate fractals in two different ways:

Live Demo

Installation

npm install fractals #npm
yarn add fractals #yarn

Usage

import { IFS, LSystem } from 'fractals';
import type { TBounds } from 'fractals';
// or separtly
import { IFS } from 'fractals/lib/ifs';
import { LSystem } from 'fractals/lib/l';
import type { TBounds } from 'fractals/lib/types';

API

Common types

Both classes implement the IFractal interface.

type TPoint = [x: number, y: number, meta: Record<string, unknown>];
type TBounds = [maxX: number, maxY: number, minX: number, minY: number];
type TPointCb = (p: TPoint, i: number) => unknown;

interface IFractal {
  readonly points: TPoint[]; // Array of generated points
  bounds: TBounds; // Bounding box of generated points
  run(fn?: TPointCb): void; // Function to start calculation process
}

IFS

An excellent material with many examples about IFS can be read here.

Types

interface IIFSMatrix {
  // The probability of choosing the current matrix
  p: number;
  // A set of constants for the equation for calculating the next point
  [$key: string]: number;
}
type TEPoint = { x: number; y: number };
// The equation for calculating the next point
type TEquation = (x: number, y: number, m: IIFSMatrix) => TEPoint;

// The type of the result point. matrixNum - the number of the matrix
// that was chosen to generate the point. It can be useful for
// debugging or coloring points depending on the matrix.
type TIFSPoint = [x: number, y: number, meta: { matrixNum: number }];

Predefined equation

There are two predefined equations:

  • affine
    affine formula
  • radial
    radial formula

Don't worry, as a code these formulas are not as scary as they seem:

export function affine(x: number, y: number, m: IIFSMatrix): TEPoint {
  const { a, b, c, d, e, f } = m;
  const newX = x * a + y * b + e;
  const newY = x * c + y * d + f;

  return { x: newX, y: newY };
}

export function radial(x: number, y: number, m: IIFSMatrix): TEPoint {
  const { a, b, t, e, f } = m;
  const newX = x * a * Math.cos(t) - y * b * Math.sin(t) + e;
  const newY = x * a * Math.sin(t) + y * b * Math.cos(t) + f;

  return { x: newX, y: newY };
}

Other classes of simple geometric transformations can also be used to construct the IFS. For example, projective:

X' = (ax*X + bx*Y + cx) / (dx*X + ex*Y + fx)
Y' = (ay*X + by*Y + cy) / (dy*X + ey*Y + fy)

or quadratic:

X' = ax*X*X + bx*X*Y + cx*Y*Y + dx*X + ex*Y + fx
Y' = ay*X*X + by*X*Y + cy*Y*Y + dy*X + ey*Y + fy

Matrices

The required matrix property is p. This is the probability of choosing a given matrix. All other fields depend on the equation. For example, Barnsley Fern is calculated using affine transformations with the following matrices:

Transformation Transition Probability
m1tf m1ts 1%
m2tf m2ts 85%
m3tf m3ts 7%
m4tf m4ts 7%

The matrices' configuration will be as follows:

const fern = {
  matrices: [
    { a: 0,     b: 0,     c: 0,     d: 0.16, e: 0, f: 0,    p: 0.01 },
    { a: 0.85,  b: 0.04,  c: -0.04, d: 0.85, e: 0, f: 1.6,  p: 0.85 },
    { a: 0.2,   b: -0.26, c: 0.23,  d: 0.22, e: 0, f: 1.6,  p: 0.07 },
    { a: -0.15, b: 0.28,  c: 0.26,  d: 0.24, e: 0, f: 0.44, p: 0.07 },
  ],
};

Properties

  • matrices: IIFSMatrix[] - array of given matrices
  • points: TIFSPoint[] - Calculated points.
  • bounds: TBounds - Bounding box of the calculated points in format [maxX, maxY, minX, minY]. Will be filled in after calling the run() method.

Methods

  • constructor(params: IIFSParams)
interface IIFSParams {
  // Array of matrices
  matrices: IIFSMatrix[];
  // Think of this parameter as the scale of the plot.
  // The larger the number, the fewer points will be per area unit.
  density?: number;
  // Number of iterations (points)
  iterations?: number;
  // Formula for calculating points.
  // You can use one of the predefined ones or write your own.
  equation?: TEquation;
}
  • run(callback?)
    Starts the calculation process. You can pass a callback function that will be called after each point is calculated. This will help to achieve higher performance by removing the extra cycle for drawing the fractal. Be careful - the bounds of points may be incorrect until the end of the calculation of the entire fractal.

Render function example

function canvasRenderer(canvas: HTMLCanvasElement, fractal: IFS) {
  if (!canvas) {
    console.warn('canvas is null');
    return;
  }

  const offsetX = fractal.bounds[2];
  const offsetY = fractal.bounds[3];
  // margins is 10 px
  canvas.height = fractal.bounds[1] + Math.abs(fractal.bounds[3]) + 20;
  canvas.width = fractal.bounds[0] + Math.abs(fractal.bounds[2]) + 20;

  const ctx = canvas.getContext('2d');

  ctx.save();
  ctx.fillStyle = '#000';
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  // margins
  ctx.translate(-offsetX + 10, offsetY + canvas.height - 10);
  ctx.scale(1, -1);

  const color = 255 / fractal.matrices.length - 1;

  for (let i = 0; i < fractal.points.length; i++) {
    const [x, y, { matrixNum }] = fractal.points[i];
    ctx.fillStyle = `hsl(${color * m}, 100%, 50%)`;
    ctx.fillRect(x, y, 1, 1);
  }
  ctx.restore();
}

document.addEventListener('DOMContentLoaded', () => {
  const fractal = new IFS(config);
  fractal.run();

  const canvas = document.getElementById('canvas') as HTMLCanvasElement;
  canvasRenderer(canvas, fractal);
});

L-system

An excellent material with many examples about L-system can be read here.

Commands

The following commands are supported:

Character Meaning
F Move forward by line length drawing a line
B Move backward by line length drawing a line
+ Turn left by turning angle
Turn right by turning angle
[ Push current drawing state onto stack
] Pop current drawing state from the stack
< Multiply the line length by the line length scale factor
> Divide the line length by the line length scale factor

Types

// The type of the result point.
// paintable - currently used to work with the stack. 
// When a point was added as a result of the ']' (pop) command.
type TLPoint = [x: number, y: number, meta: { paintable: boolean }];

Properties

  • points: TLPoint[] - Calculated points.
  • bounds: TBounds - Bounding box of the calculated points in format [maxX, maxY, minX, minY]. Will be filled in after calling the run() method.

Methods

  • constructor(params: ILParams)
interface ILParams {
  // Initial string string with commands, e.g. 'X'
  axiom: string;
  // Hash with replacement rules
  // {
  //    F: 'FF', - All occurrences of the character "F" will be replaced with the sequence "FF"
  //    X: 'F-[[X]+X]+F[+FX]-X', - and all occurrences of the character "X" will be replaced with the sequence "F-[[X]+X]+F[+FX]-X"
  // }
  rules: Record<string, string>;
  // The number of replacement iterations
  iterations: number;
  // The initial length of the line.
  distance: number;
  // Angle of rotation.
  angle: number;
  // Length scaling factor. See commands `&lt;` and `&gt;`
  lengthScale?: number;
}
  • run(callback?)
    Starts the calculation process. You can pass a callback function that will be called after each point is calculated. This will help to achieve higher performance by removing the extra cycle for drawing the fractal. Be careful - the bounds of points may be incorrect until the end of the calculation of the entire fractal.

Render function example

function canvasRenderer(canvas: HTMLCanvasElement, fractal: LSystem) {
  if (!canvas) {
    console.warn('canvas is null');
    return;
  }

  const offsetX = -fractal.bounds[2];
  const offsetY = -fractal.bounds[3];
  // margins is 10 px
  canvas.height = fractal.bounds[1] + Math.abs(fractal.bounds[3]) + 20;
  canvas.width = fractal.bounds[0] + Math.abs(fractal.bounds[2]) + 20;

  const ctx = canvas.getContext('2d');

  ctx.fillStyle = '#000';
  ctx.save();
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  ctx.translate(10, 10); // margins

  const color = 255 / fractal.points.length - 1;

  for (let i = 1; i < fractal.points.length; i++) {
    const [x, y, { paintable }] = fractal.points[i];
    if (!paintable) {
      continue;
    }

    ctx.beginPath();
    const [startX, startY] = fractal.points[i - 1];
    ctx.strokeStyle = `hsl(${color * i}, 100%, 50%)`;
    ctx.moveTo(startX + offsetX, startY + offsetY);
    ctx.lineTo(x + offsetX, y + offsetY);
    ctx.stroke();
    ctx.closePath();
  }
  ctx.restore();
}

document.addEventListener('DOMContentLoaded', () => {
  const fractal = new LSystem(config);
  fractal.run();

  const canvas = document.getElementById('canv') as HTMLCanvasElement;
  canvasRenderer(canvas, fractal);
});

Links

About

Tiny, dependency free library to easily generate fractals

Resources

License

Stars

Watchers

Forks

Packages

No packages published