diff --git a/README.md b/README.md index 221bfa9d..92032646 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ Start with this html: - + diff --git a/package-lock.json b/package-lock.json index 7ff7503b..8e0846c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "pixelmanipulator", - "version": "5.0.0", + "version": "5.3.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1372,9 +1372,9 @@ "dev": true }, "async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", "dev": true, "requires": { "lodash": "^4.17.14" diff --git a/package.json b/package.json index 2c410c72..ba8ab744 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixelmanipulator", - "version": "5.2.0", + "version": "5.3.0", "description": "Run any cellular automata on an html5 canvas.", "main": "dist/main.js", "browser": "dist/browser.js", @@ -92,7 +92,7 @@ "build:bundle": "browserify dist/browser.js -o dist/bundle.js --standalone pixelmanipulator", "build": "npm run check && npm run build:docs && npm run build:parcel && npm run build:bundle", "updatedemo": "npm run build && gh-pages -d docs -m \"Update $npm_package_version\" -tf", - "node-demo": "ts-node src/node-demo", + "node-demo": "ts-node src/node-demo", "coverage": "npm run test" }, "dependencies": { diff --git a/src/lib/neighborhoods.ts b/src/lib/neighborhoods.ts index 85fef594..266a9585 100644 --- a/src/lib/neighborhoods.ts +++ b/src/lib/neighborhoods.ts @@ -19,6 +19,20 @@ import { Location } from './renderers' /** A list of locations, usually relative around a pixel. */ export type Hitbox=Location[] +/** A rect between two points +* @param topLeft - The top left corner +* @param bottomRight - The bottom right corner +* @returns A hitbox shaped like a rectangle between the corners. +*/ +export function rect (topLeft: Location, bottomRight: Location): Location[] { + const output: Hitbox = [] + for (let x = topLeft.x; x <= bottomRight.y; x++) { + for (let y = topLeft.y; y <= bottomRight.y; y++) { + output.push({ x, y }) + } + } + return output +} /** * Makes a wolfram neighborhood. * @@ -75,20 +89,15 @@ export function moore (radius?: number, includeSelf?: boolean): Hitbox { if (radius == null) { radius = 1 } - if (includeSelf == null) { - includeSelf = false - } - const output: Hitbox = [] // Note: no need to calculate the Chebyshev distance. All pixels in this // range are "magically" within. - for (let x = -1 * radius; x <= radius; x++) { - for (let y = -1 * radius; y <= radius; y++) { - if (includeSelf || !(x === 0 && y === 0)) { - output.push({ x, y }) - } - } - } - return output + return rect({ + x: -1 * radius, + y: -1 * radius + }, { + x: radius, + y: radius + }).filter(({ x, y }) => (includeSelf ?? false) || !(x === 0 && y === 0)) // And to think that this used to be hard... Perhaps they had a different // goal? Or just weren't using higher-order algorithims? } @@ -119,26 +128,21 @@ export function moore (radius?: number, includeSelf?: boolean): Hitbox { * ``` */ export function vonNeumann (radius?: number, includeSelf?: boolean): Hitbox { - if (typeof radius === 'undefined') { + if (radius == null) { radius = 1 } - if (typeof includeSelf === 'undefined') { - includeSelf = false - } - const output: Hitbox = [] // A Von Neumann neighborhood of a given distance always fits inside of a // Moore neighborhood of the same. (This is a bit brute-force) - for (let x = -1 * radius; x <= radius; x++) { - for (let y = -1 * radius; y <= radius; y++) { - if ( - (includeSelf || !(x === 0 && y === 0)) && - (Math.abs(x) + Math.abs(y) <= radius) // Taxicab distance - ) { - output.push({ x, y }) - } - } - } - return output + return rect({ + x: -1 * radius, + y: -1 * radius + }, { + x: radius, + y: radius + }).filter(({ x, y }) => + ((includeSelf ?? false) || !(x === 0 && y === 0)) && + (Math.abs(x) + Math.abs(y) <= (radius ?? 1)) // Taxicab distance + ) } /** * Makes a euclidean neighborhood. @@ -155,23 +159,18 @@ export function euclidean (radius?: number, includeSelf?: boolean): Hitbox { if (radius == null) { radius = 1 } - if (includeSelf == null) { - includeSelf = false - } - const output: Hitbox = [] // A circle of a given diameter always fits inside of a square of the same // side-length. (This is a bit brute-force) - for (let x = -1 * radius; x <= radius; x++) { - for (let y = -1 * radius; y <= radius; y++) { - if ( - (includeSelf || !(x === 0 && y === 0)) && - (Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)) <= radius) // Euclidean distance - ) { - output.push({ x, y }) - } - } - } - return output + return rect({ + x: -1 * radius, + y: -1 * radius + }, { + x: radius, + y: radius + }).filter(({ x, y }) => + ((includeSelf ?? false) || !(x === 0 && y === 0)) && + (Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)) <= (radius ?? 1)) // Euclidean distance + ) } // TODO https://www.npmjs.com/package/compute-minkowski-distance ? // TODO Non-Euclidean distance algorithim? diff --git a/src/lib/pixelmanipulator.ts b/src/lib/pixelmanipulator.ts index 14991666..3fc7f823 100644 --- a/src/lib/pixelmanipulator.ts +++ b/src/lib/pixelmanipulator.ts @@ -20,7 +20,7 @@ */ import { version as _version } from '../../package.json' import { Hitbox, moore, wolfram } from './neighborhoods' -import { Renderer, Location, location2Index } from './renderers' +import { Renderer, Location, location2Index, transposeLocations } from './renderers' // export * as neighborhoods from './neighborhoods' import * as _neighborhoods from './neighborhoods' export { _neighborhoods as neighborhoods } @@ -154,33 +154,20 @@ export const rules = { return { madeWithRule: true, hitbox: wolfram(1, 1), + // The current state is used as the index in the binstates, as binstates is a bit array of every state liveCell: function wlive ({ x, y, thisId }) { if (y === 0) return - // for every possible state - for (let binDex = 0; binDex < 8; binDex++) { - if ( - // if the state is "off". Use a bit mask and shift it - (binStates & 1 << binDex) === 0 && - // if there is a wolfram match (wolfram code goes from 111 to 000) - p.wolframNearbyCounter({ x, y, frame: 1, loop }, thisId, binDex) - ) { - p.setPixel({ x, y, loop }, p.defaultId) - return// No more logic needed, it is done. - } + const currentState = p.wolframNearby({ x, y, frame: 1, loop }, thisId) + // if the state is "off". Use a bit mask and shift it + if ((binStates & 1 << currentState) === 0) { + p.setPixel({ x, y, loop }, p.defaultId) } }, deadCell: function wdead ({ x, y, thisId }) { - // for every possible state - for (let binDex = 0; binDex < 8; binDex++) { - if ( - // if the state is "on". Use a bit mask and shift it - (binStates & 1 << binDex) > 0 && - // if there is a wolfram match (wolfram code goes from 111 to 000) - p.wolframNearbyCounter({ x, y, frame: 1, loop }, thisId, binDex) - ) { - p.setPixel({ x, y, loop }, thisId) - return// No more logic needed, it is done. - } + const currentState = p.wolframNearby({ x, y, frame: 1, loop }, thisId) + // if the state is "on". Use a bit mask and shift it + if ((binStates & 1 << currentState) > 0) { + p.setPixel({ x, y, loop }, thisId) } } } @@ -509,40 +496,53 @@ export class PixelManipulator { return this.getPixelId(loc) === tmp } + /** 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 { + return area + .filter(loc => this.confirmElm(loc, search)) + .length + } + + private static _moore = moore() /** @param name - element to look for - * @param x - x location of center of moore area - * @param y - y location of center of moore area - * @param frame - What frame to grab it from. (default 0, reccomended 1) - * @param loop - Should it loop around canvas edges while counting? + * @param center - location of the center of the moore area * @returns Number of matching elements in moore radius */ - mooreNearbyCounter ({ x, y, frame, loop }: Location, name: number|string): number { - return moore() - .map(rel => ({ x: x + rel.x, y: y + rel.y, frame, loop })) - .map(loc => this.confirmElm(loc, name)) - .map(boolToNumber) - .reduce((a, b) => a + b) + mooreNearbyCounter (center: Location, search: number|string): number { + return this.totalWithin(transposeLocations(PixelManipulator._moore, center), search) } + private static _wolfram = wolfram() /** - * @param x - "Current" pixel x location - * @param y - "Current" pixel y location - * @param frame - What frame to measure on? (default 0, reccomended 1) - * @param loop - Should this loop around edges when checking? + * @param current - "Current" pixel location + * @param search - element to look for + * encoded as a number. + * @returns Number used as bit area to indicate occupied cells */ + wolframNearby (current: Location, search: number|string): number { + // one-dimentional detectors by default don't loop around edges + current.loop = current.loop ?? false + return transposeLocations(PixelManipulator._wolfram, current) + .map((loc, i) => boolToNumber(this.confirmElm(loc, search)) << (2 - i)) + .reduce((a, b) => a | b) + } + + /** Counter tool used in slower wolfram algorithim. + * @deprecated Replaced with [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 moore radius */ - wolframNearbyCounter ({ x, y, frame, loop }: Location, name: number|string, binDex: number|string): boolean { + wolframNearbyCounter (current: Location, name: number|string, binDex: number|string): boolean { if (typeof binDex === 'string') { // Old format was a string of ones and zeros, three long. Use bitshifts to make it better. binDex = boolToNumber(binDex[0] === '1') << 2 | boolToNumber(binDex[1] === '1') << 1 | boolToNumber(binDex[2] === '1') << 0 } - loop = loop ?? false // one-dimentional detectors by default don't loop around edges - return wolfram() - .map(rel => ({ x: x + rel.x, y: y + rel.y, frame, loop })) - .map(loc => this.confirmElm(loc, name)) - .map((elm, i) => elm === (((binDex as number) & 1 << (2 - i)) > 0)) - .reduce((a, b) => a && b) + return this.wolframNearby(current, name) === binDex } /** Set a pixel in a given location. diff --git a/src/lib/renderers.ts b/src/lib/renderers.ts index cacb7ec0..ae85a590 100644 --- a/src/lib/renderers.ts +++ b/src/lib/renderers.ts @@ -38,6 +38,19 @@ export interface Location{ export function location2Index ({ x, y }: Location, width: number): number { return ((width * y) + x) } +/** 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]. +* @param offset - Amount to transpose the locations by, represented by a location. +*/ +export function transposeLocations (locs: Location[], offset: Location): Location[] { + const { x, y, frame, loop } = offset + return locs.map(loc => ({ + x: loc.x + x, + y: loc.y + y, + frame: loc.frame ?? frame, + loop: loc.loop ?? loop + })) +} /** Abstract rendering type. Used by [[PixelManipulator]] to enable rendering to * various targets. */ export abstract class Renderer {