Skip to content

Commit

Permalink
Add support for custom totalistic neighborhoods
Browse files Browse the repository at this point in the history
See #45
  • Loading branch information
Lazerbeak12345 committed Apr 20, 2022
1 parent f16b71f commit f8ac5ca
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 94 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Start with this html:
<html>
<head>
<!-- If you'd like you can replace this with a different URL for the library -->
<script src="https://unpkg.com/pixelmanipulator@^5.2.0"></script>
<script src="https://unpkg.com/pixelmanipulator@^5.3.0"></script>
</head>
<body>
<!-- The canvas element to render to -->
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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": {
Expand Down
85 changes: 42 additions & 43 deletions src/lib/neighborhoods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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?
}
Expand Down Expand Up @@ -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.
Expand All @@ -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?
Expand Down
88 changes: 44 additions & 44 deletions src/lib/pixelmanipulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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)
}
}
}
Expand Down Expand Up @@ -509,40 +496,53 @@ export class PixelManipulator<T> {
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.
Expand Down
13 changes: 13 additions & 0 deletions src/lib/renderers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> {
Expand Down

0 comments on commit f8ac5ca

Please sign in to comment.