pixelmanipulator
Version:
A super powerful Typescript library for cellular automation.
482 lines (480 loc) • 21.3 kB
TypeScript
/** Various rendering targets
*
* Copyright (C) 2018-2024 Nathan Fritzler
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/
*/
/** The location of a pixel */
export interface Location {
/** x position */
x: number;
/** y position */
y: number;
/** Should this location loop around screen borders? */
loop?: boolean;
/** Should this location be treated to be on the current frame, previous, or older?
*
* Current frame is zero. Higher is older - but not guarenteed to be present
*/
frame?: number;
}
/** Convert a loction to a index to reduce need for 2D arrays
* @param x - x location
* @param y - y location
* @param width - width of the canvas
*/
export function location2Index({ x, y }: Location, width: number): number;
/** Transpose a list of locations, using a location.
* @param locs - Locations to be transposed. If the frame or loop values are
* absent, they are set to the value in [offset]. If absent from [offset] they
* are not set.
* @param offset - Amount to transpose the locations by, represented by a
* location.
*/
export function transposeLocations(locs: Location[], offset: Location): Location[];
/** Abstract rendering type. Used by {@link pixelmanipulator.PixelManipulator} to enable rendering to
* various targets. */
export abstract class Renderer<T> {
/** Renders a pixel on a given location on the next call to {@link Renderer.update}
* @param location - Where to render the pixel.
* @param id - the pixel to render.
*/
abstract renderPixel(location: Location, id: number): void;
/** Reset the render target. */
abstract reset(): void;
/** Update the render target. Draws all changes queued up by {@link Renderer.renderPixel}. */
abstract update(): void;
/** The {@link pixelmanipulator.ElementData.renderAs} value for the default element */
abstract defaultRenderAs: T;
/** Ordered by ID, the {@link pixelmanipulator.ElementData.renderAs} info for each element. */
renderInfo: T[];
/** Intentionally overridable, called when an element is modified.
* @param id - The id of the element to modify.
* @param newRenderAs - The new {@link pixelmanipulator.ElementData.renderAs} info.
* @returns The value passed upstream to be stored as the actual renderAs info,
* allowing for sanitation in this function, or one overriding it.
*/
modifyElement(id: number, newRenderAs: T): T;
/** @param value - The new width of the canvas */
set_width(value: number): void;
/** @returns the width of the canvas */
get_width(): number;
/** @param value - The new height of the canvas */
set_height(value: number): void;
/** @returns the height of the canvas */
get_height(): number;
}
/** The color of an element */
export type Color = [number, number, number, number] | [number, number, number] | [number, number] | [number] | [];
/** Render onto an {@link HTMLCanvasElement} using a {@link CanvasRenderingContext2D} */
export class Ctx2dRenderer extends Renderer<Color> {
/** @param canvas - The canvas to render on, and to adjust the size of */
constructor(canvas: HTMLCanvasElement);
/** The last known image data from {@link Ctx2dRenderer.ctx} */
imageData: ImageData;
/** The rendering context for the canvas */
ctx: CanvasRenderingContext2D;
/** The canvas */
canvas: HTMLCanvasElement;
/** Default color is solid black */
defaultRenderAs: Color;
/** In addition to calling {@link Renderer.modifyElement}, this leftpads colors
* with `255` and checks for dupicates.
* @param id - Id of element
* @param newRenderAs - The proposed color of the element.
* @returns the actual color of the element. Always 4 long.
*/
modifyElement(id: number, newRenderAs: Color): Color;
/** @param loc - location of the pixel to render. Ignores {@link Location.frame} and {@link Location.loop}
* @param id - The id of the pixel to render.
*/
renderPixel(loc: Location, id: number): void;
reset(): void;
update(): void;
set_width(value: number): void;
set_height(value: number): void;
}
/** Render to a string */
export class StringRenderer extends Renderer<string> {
defaultRenderAs: string;
/** The callback function passed to the constructor. Called on {@link StringRenderer.update} */
readonly _callback: (string: string) => void;
/** @param callback - A function called on {@link StringRenderer.update}. Passed a
* string with the renderable state of the {@link pixelmanipulator.PixelManipulator} */
constructor(callback: (string: string) => void);
/** @param newRenderAs - The proposed character to use. Must be 1 char long and unique */
modifyElement(id: number, newRenderAs: string): string;
reset(): void;
/** @param x - X location of pixel
* @param y - y location of pixel
* @param id - The id of the pixel
*/
renderPixel({ x, y }: Location, id: number): void;
update(): void;
}
/** render on two different targets (which may also be {@link SplitRenderer}) */
export class SplitRenderer<A, B> extends Renderer<{
a: A;
b: B;
}> {
defaultRenderAs: {
a: A;
b: B;
};
a: Renderer<A>;
b: Renderer<B>;
constructor(a: Renderer<A>, b: Renderer<B>);
renderPixel(loc: Location, id: number): void;
reset(): void;
update(): void;
modifyElement(id: number, { a, b }: {
a: A;
b: B;
}): {
a: A;
b: B;
};
}
/** A list of locations, usually relative around a pixel. */
type Hitbox = Location[];
declare function startAnimation(callback: () => void): number | ReturnType<typeof setInterval>;
/** The argument to {@link ElementDataUnknown.liveCell} and
* {@link ElementDataUnknown.deadCell}
*/
export interface Rel {
/** The X location of this pixel. */
x: number;
/** The Y location of this pixel. */
y: number;
/** The ID number of the current pixel. Reccommended if performance profiling
* shows string comparision is a bottleneck.
*/
oldId: number;
/** The ID of the element for which this is being called. (in a
* {@link ElementDataUnknown.liveCell} that's the same as {@link Rel.oldId}, but in a
* {@link ElementDataUnknown.deadCell} it's the id that the deadCell belongs to.
*/
thisId: number;
}
/** Much like {@link ElementDataUnknown} but all fields except {@link ElementData.madeWithRule},
* {@link ElementData.liveCell} and {@link ElementData.deadCell} are mandatory. */
export interface ElementData<T> extends ElementDataUnknownNameMandatory<T> {
renderAs: T;
hitbox: Hitbox;
}
/** Information about an element. */
export interface ElementDataUnknown<T> {
/** The name of the element. */
name?: string;
/** Information on how to render this element */
renderAs?: T;
/** {@link ElementDataUnknown.deadCell} will only be called on empty
* pixels within the hitbox of a live cell. Array of relative coordinate pairs.
* Optional, defaults to the result of {@link neighborhoods.moore}
* called with no arguments.
*/
hitbox?: Hitbox;
/** Every frame of animation, pixelmanipulator iterates through each and every pixel on the screen. If this element is found, it calls this function.
*/
liveCell?: (rel: Rel) => void;
/** Every frame of animation, pixelmanipulator iterates through each and every
* pixel on the screen. If this element is found, it calls this function on
* each of the locations defined in {@link ElementDataUnknown.hitbox} so long as
* the pixel matches the value in {@link PixelManipulator.defaultId}, without
* calling the same dead pixel twice.
*/
deadCell?: (rel: Rel) => void;
/** If present, indicates that this element was auto-generated */
madeWithRule?: true;
}
/** Much like {@link ElementDataUnknown} but the name is mandatory. */
export interface ElementDataUnknownNameMandatory<T> extends ElementDataUnknown<T> {
name: string;
}
/** Template generators for your elements. */
export const rules: {
/** Generates elements like conway's game of life.
* @param p - `lifelike` needs to be able to call {@link PixelManipulator.mooreNearbyCounter}
* @param pattern - The B/S syntax indicator of on how many cells of the same
* type in the moore radius around each pixel should survive, and on how many
* should be born.
* @param loop - Should this loop around screen edges? (Passed to {@link renderers.Location.loop})
*/
lifelike: <T>(p: PixelManipulator<T>, pattern: string, loop?: boolean) => ElementDataUnknown<T>;
/** Generates fundamental cellular automata
* @param p - `wolfram` needs to be able to call {@link PixelManipulator.wolframNearbyCounter}
* @param pattern - The Rule num syntax, where the 8-bit number is translated
* into a binary list, each where the inverted 3-binary-digit index represents
* the state of cells in the row above. On a match, the cell becomes the state
* specified in the initial 8-bit number.
* @param loop - Should this loop around screen edges? (Passed to {@link PixelManipulator.wolframNearby})
*/
wolfram: <T>(p: PixelManipulator<T>, pattern: string, loop?: boolean) => ElementDataUnknown<T>;
};
/** Sizes to set the canvases to. If a value below is absent, old value is used.
*/
export interface CanvasSizes {
/** width of the canvas */
canvasW?: number;
/** height of the canvas */
canvasH?: number;
}
/** A cellular automata engine */
export class PixelManipulator<T> {
/**
* @param renderer - The target to render things to.
* @param width - How wide should the initial target be?
* @param height - How tall should the initial target be?
*/
constructor(renderer: Renderer<T>, width: number, height: number);
/** An instanace of the object that shows the state to the user. */
renderer: Renderer<T>;
/**
* This is the number that indicates what animation frame the iterate function
* is being called with.
*
* You can use this to mannually stop the iterations like so:
* `cancelAnimationFrame(this.loopint)` (not reccommended)
*/
loopint: ReturnType<typeof startAnimation>;
/**
* A low-level listing of the availiable elements.
*
* Format is much like the argument to
* {@link PixelManipulator.addMultipleElements}, but is not sanitized.
*/
readonly elements: Array<ElementData<T>>;
/**
* A mapping from old names for elements to new names for elements.
*
* Allows a user to modify the name of an element at runtime.
*/
nameAliases: Map<string, string>;
/**
* A string indicating weather it is currently animating or not.
*
* It is `"playing"` if it is currently animating, or `"paused"` if not
* currently animating.
*
* This has been around since early version 0, and once was the `innerText`
* value of a pause/play button!
*/
mode: 'playing' | 'paused';
/**
* The elm that pixelmanipulator will fill the screen with upon initialization,
* and what elements should return to when they are "dead". Default value is
* 0, an element with the color `#000F`
*
* If you update this, be sure to update {@link renderers.Renderer.defaultRenderAs} in {@link PixelManipulator.renderer}
*/
defaultId: number;
/** Called before {@link PixelManipulator.iterate} does its work.
* @returns false to postposne iteration.
*/
onIterate: () => (boolean | undefined);
/** Called after {@link PixelManipulator.iterate} does its work. */
onAfterIterate: () => undefined;
/** Gets called after a call to {@link PixelManipulator.modifyElement}. The ID is
* passed as the only argument.
* @param id - The element that was modified.
*/
onElementModified: (id: number) => void;
/** @returns the width of the canvas */
get_width(): number;
/** @param value - The new width of the canvas */
set_width(value: number): void;
/** @returns the height of the canvas */
get_height(): number;
/** @param value - The new height of the canvas */
set_height(value: number): void;
/** fills the screen with value, at an optional given percent
* @param value - The element to put on the screen
* @param pr - The percent as a number from 1 to 100, defaulting at 15
*/
randomlyFill(value: string | number, pr?: number): void;
/** Adds multiple elements.
*
* @param elements - Index is the element name, value is the element data (and
* does not require the name). Value is passed to
* {@link PixelManipulator.addElement}
*/
addMultipleElements(elements: Record<string, ElementDataUnknown<T>>): void;
/** Add an element with the given element data
* @param data - The details about the element.
* @returns The generated {@link PixelManipulator.elements} index
*/
addElement(data: ElementDataUnknownNameMandatory<T>): number;
/**
* @param id - How to identify what element to modify.
* @param data - Values to apply to the pre-existing element.
*
* Automatically calls {@link PixelManipulator.aliasElements} if
* {@link ElementDataUnknown.name} is present in `data`
*/
modifyElement(id: number, data: ElementDataUnknown<T>): void;
/**
* @param oldName - The old {@link ElementData.name}
* @param newName - The new {@link ElementData.name}
*
* Adds the name to {@link PixelManipulator.nameAliases}, and ensures no alias
* loops are present.
*/
aliasElements(oldName: string, newName: string): void;
/** Respecting aliases, convert an element name into its number.
* @param name - name of element
* @returns The number of the element
*/
nameToId(name: string): number;
/**
* @param name - Name of the (possibly aliased) element.
* @returns The element from {@link PixelManipulator.elements}, respecting
* aliases in {@link PixelManipulator.nameAliases}, or {@link undefined} if not found.
*/
getElementByName(name: string): ElementData<T> | undefined;
/**
* @param loc - Location of the element.
* @returns Name of element at passed-in location. See {@link ElementData.name}
*/
whatIs(loc: Location): string;
/** Start iterations on all of the elements on the canvas.
* Sets {@link PixelManipulator.mode} to `"playing"`, and requests a new animation
* frame, saving it in {@link PixelManipulator.loopint}.
*
* @param canvasSizes - If {@link PixelManipulator.mode} is already `"playing"` then
* canvasSizes is passed to {@link PixelManipulator.reset}. Otherwise reset is not
* called.
*/
play(canvasSizes?: CanvasSizes): void;
/** Reset, resize and initialize the canvas(es).
* Calls {@link PixelManipulator.pause} then
* {@link PixelManipulator.update}. Resets all internal state, excluding the
* element definitions.
*
* @param canvasSizes - Allows one to change the size of the canvases during
* the reset.
*/
reset(canvasSizes?: CanvasSizes): void;
/** pause canvas iterations
* Sets {@link PixelManipulator.mode} to `"paused"` and cancels the animation frame
* referenced in {@link PixelManipulator.loopint}
*/
pause(): void;
/**
* @param loc - Location of the pixel (could be out of bounds).
* @returns null if out-of-bounds when loop setting is false, or the location (loop set to false).
*/
locationBoundsCheck(loc: Location): null | Location;
/**
* @param loc - Location of the pixel
* @returns the element id at a given location
*/
getPixelId(loc: Location): number;
/**
* Applies any changes made with {@link renderers.Renderer.renderPixel} on {@link PixelManipulator.renderer} to the canvas
*/
update(): void;
/**
* @param loc - Where to confirm the element
* @param id - The elm you expect it to be
* @returns Does the cell at `loc` match `ident`?
*/
confirmElm(loc: Location, id: number | string): boolean;
/** Calculate the total number of elements within an area
* @param area - The locations to total up.
* @param search - The element to look for
* @returns The total
*/
totalWithin(area: Location[], search: number | string): number;
/** @param name - element to look for
* @param center - location of the center of the moore area
* @returns Number of matching elements in moore radius */
mooreNearbyCounter(center: Location, search: number | string): number;
/** @param area - The Area to search within
* @param ruleNum - A bitfield of what states a pixel should live or die on.
* @param search - The element to search for
* @see {@link PixelManipulator.wolframNewState} for higher-level tool
* @see {@link PixelManipulator.fundamentalStatesWithin} for lower-level tool
* @returns The state that the bitfied says this pixel should be in the next frame.
*/
fundamentalNewState(area: Location[], ruleNum: number, search: number | string): boolean;
/** @param area - Locations to look at.
* @param search - Locations to mark as a true bit.
* @see {@link PixelManipulator.fundamentalNewState} for higher-level tool
* @returns number as a bitfied array, in order of the items in area, from left to right.
*
* That means that `(fundamentalStatesWithin([loc], search) & 1) === boolToNumber(confirmElm(loc, search))`
*
* You may want to see [this page](https://www.wolframscience.com/nks/notes-5-2--general-rules-for-multidimensional-cellular-automata/)
* for more details on how this might be used.
*/
fundamentalStatesWithin(area: Location[], search: number | string): number;
/** @param loc - The pixel to change. (Defaults {@link renderers.Location.loop} to false)
* @param ruleNum - A bitfield of what states a pixel should live or die on.
* @param search - The element to search for
* @see {@link PixelManipulator.fundamentalNewState} for more general tool.
* @returns The state that the bitfied says this pixel should be in the next frame.
*/
wolframNewState(loc: Location, ruleNum: number, search: number | string): boolean;
/**
* @param current - "Current" pixel location. (Defaults {@link renderers.Location.loop} to false)
* @param search - element to look for
* @see {@link PixelManipulator.fundamentalStatesWithin} for lower-level tool
* @returns Number used as bit area to indicate occupied cells
*/
wolframNearby(current: Location, search: number | string): number;
/** Counter tool used in slower wolfram algorithim.
* @deprecated Replaced with {@link PixelManipulator.wolframNearby} for use in faster
* algorithms
* @param current - "Current" pixel location
* @param name - element to look for
* @param bindex - Either a string like `"001"` to match to, or the same
* encoded as a number.
* @returns Number of elements in wolfram radius */
wolframNearbyCounter(current: Location, name: number | string, binDex: number | string): boolean;
/** Set a pixel in a given location.
*
* @param x - X position.
* @param y - Y position.
* @param ident - Value to identify the element.
*
* - If a string, it assumes it's an element name.
* - If a number, it assumes it's an element ID
*
* @param loop - Defaults to {@link true}. Wraps `x` and `y` around canvas borders.
*/
setPixel(loc: Location, ident: string | number): void;
/** Number of pixels per element in the last frame */
pixelCounts: Record<number, number>;
/** A single frame of animation. Media functions pass this into
* {@link requestAnimationFrame}.
*
* Be careful! Calling this while {@link PixelManipulator.mode} is `"playing"`
* might cause two concurrent calls to this function. If any of your automata
* have "hidden state" - that is they don't represent every detail about
* themselves as data within the pixels - it might cause conflicts.
*/
iterate(): void;
/**
* A List of {@link Uint32Array}s each the length of width times height of the
* canvas. Frame 0 is the new frame, frame one is the prior, etc. Each item
* holds the element id of each element on screen, from left to right, top to
* bottom.
*/
frames: Uint32Array[];
}
/** Version of library **for logging purposes only**. Uses semver. */
export const version: string;
/** Licence disclaimer for PixelManipulator */
export const licence: string;
//# sourceMappingURL=types.d.ts.map