Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1 @@
export { SimplexNoise, simplex2, simplex3, simplex4 } from "./simplex";

View File

@@ -0,0 +1,52 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { SimplexNoise } from "./simplex.js";
describe("SimplexNoise default seed", () => {
it("constructs with default seed (no args)", { timeout: 5000 }, () => {
const s = new SimplexNoise();
expect(Number.isFinite(s.noise2(0.3, 0.4))).toBe(true);
expect(Number.isFinite(s.noise3(0.3, 0.4, 0.5))).toBe(true);
expect(Number.isFinite(s.noise4(0.3, 0.4, 0.5, 0.6))).toBe(true);
});
});
/**
* Extra coverage for SimplexNoise.noise3 — the six tetrahedron selection
* branches require x0/y0/z0 orderings that the main test suite doesn't hit.
* For small positive inputs, i=j=k=0 so x0≈xin, y0≈yin, z0≈zin.
*/
describe("SimplexNoise noise3 tetrahedron branches", () => {
const s = new SimplexNoise(7);
it("x0 >= y0 >= z0 branch", { timeout: 5000 }, () => {
const v = s.noise3(0.6, 0.4, 0.1);
expect(Number.isFinite(v)).toBe(true);
});
it("x0 >= z0 > y0 branch", { timeout: 5000 }, () => {
const v = s.noise3(0.6, 0.1, 0.4);
expect(Number.isFinite(v)).toBe(true);
});
it("z0 > x0 >= y0 branch", { timeout: 5000 }, () => {
const v = s.noise3(0.4, 0.1, 0.6);
expect(Number.isFinite(v)).toBe(true);
});
it("y0 > z0 > x0 branch (the previously uncovered one)", {
timeout: 5000,
}, () => {
const v = s.noise3(0.1, 0.6, 0.4);
expect(Number.isFinite(v)).toBe(true);
});
it("z0 > y0 > x0 branch", { timeout: 5000 }, () => {
const v = s.noise3(0.1, 0.4, 0.6);
expect(Number.isFinite(v)).toBe(true);
});
it("y0 >= x0, y0 >= z0 else branch", { timeout: 5000 }, () => {
const v = s.noise3(0.3, 0.6, 0.1);
expect(Number.isFinite(v)).toBe(true);
});
});

View File

@@ -0,0 +1,54 @@
# simplex/simplex
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `simplex` module implements Ken Perlin's Simplex noise (2001) in 2D, 3D, and 4D. Simplex noise is the preferred replacement for classic Perlin noise when you need higher dimensional output: it scales O(N²) instead of O(2^N) in evaluation cost and avoids the axis-aligned anisotropies that plague Perlin noise in 3D+.
Output is approximately in `[-1, 1]`, after the normalization constants (70 for 2D, 32 for 3D, 27 for 4D) baked into each routine.
## Features
- `SimplexNoise` class — reusable seeded permutation table
- `simplex2` / `simplex3` / `simplex4` — one-shot functional APIs
- Uses the 12-gradient table for 2D/3D and a 32-gradient table for 4D
- Deterministic per seed
## Usage: Basic Example
```ts
import { simplex2 } from "@suaveplan/noise";
const v = simplex2("world", 1.5, 2.3);
```
## Usage: Advanced Example
```ts
import { SimplexNoise } from "@suaveplan/noise";
const s = new SimplexNoise("clouds");
// Animated clouds: use the 4th dimension as time.
for (let y = 0; y < 256; y += 1) {
for (let x = 0; x < 256; x += 1) {
const v = s.noise4(x * 0.01, y * 0.01, 0, performance.now() * 0.0001);
// ... render ...
}
}
```
## API Reference
- `new SimplexNoise(seed)` — constructs the permutation table once.
- `noise2(x, y) / noise3(x, y, z) / noise4(x, y, z, w): number`
- `simplex2/3/4(seed, ...coords): number` — functional convenience.
## Implementation Notes
The 4D simplex-corner selection uses rank-based selection from Gustavson's reference implementation. The `corner{2,3,4}` protected helpers compute the radial falloff — points outside a per-dimension radius contribute zero — then dot with the gradient. Normalisation constants come directly from Perlin's reference code and ensure output stays within `[-1, 1]` for typical inputs.
### Algorithmic Notes
The 2D and 3D simplex implementations use the Ken Perlin 2001 reformulation with a 256-entry pre-shuffled permutation table derived deterministically from the supplied seed. Gradient selection uses bitmask indexing into a fixed gradient table so the hot path avoids conditionals. Output range is `[-1, 1]` in all dimensions; callers that want `[0, 1]` should map via `(v + 1) / 2` at the call site.

View File

@@ -0,0 +1,40 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { SimplexNoise, simplex2, simplex3, simplex4 } from "./simplex.js";
describe("SimplexNoise", () => {
it("is deterministic", { timeout: 5000 }, () => {
const a = new SimplexNoise(1);
const b = new SimplexNoise(1);
expect(a.noise2(0.3, 0.7)).toBe(b.noise2(0.3, 0.7));
expect(a.noise3(0.1, 0.2, 0.3)).toBe(b.noise3(0.1, 0.2, 0.3));
expect(a.noise4(0.1, 0.2, 0.3, 0.4)).toBe(b.noise4(0.1, 0.2, 0.3, 0.4));
});
it("output roughly in [-1, 1]", { timeout: 5000 }, () => {
const s = new SimplexNoise(2);
for (let i = 0; i < 200; i += 1) {
const v2 = s.noise2(i * 0.15, i * 0.25);
const v3 = s.noise3(i * 0.15, i * 0.25, i * 0.35);
const v4 = s.noise4(i * 0.1, i * 0.2, i * 0.3, i * 0.4);
expect(v2).toBeGreaterThan(-1.05);
expect(v2).toBeLessThan(1.05);
expect(v3).toBeGreaterThan(-1.05);
expect(v3).toBeLessThan(1.05);
expect(v4).toBeGreaterThan(-1.05);
expect(v4).toBeLessThan(1.05);
}
});
it("varies across different seeds", { timeout: 5000 }, () => {
expect(simplex2(1, 0.3, 0.7)).not.toBe(simplex2(2, 0.3, 0.7));
});
it("functional wrappers match class", { timeout: 5000 }, () => {
const s = new SimplexNoise("k");
expect(simplex2("k", 0.2, 0.3)).toBe(s.noise2(0.2, 0.3));
expect(simplex3("k", 0.2, 0.3, 0.4)).toBe(s.noise3(0.2, 0.3, 0.4));
expect(simplex4("k", 0.2, 0.3, 0.4, 0.5)).toBe(
s.noise4(0.2, 0.3, 0.4, 0.5)
);
});
});

View File

@@ -0,0 +1,381 @@
import { buildPermutation } from "../utils/permutation.js";
const F2 = 0.5 * (Math.sqrt(3) - 1);
const G2 = (3 - Math.sqrt(3)) / 6;
const F3 = 1 / 3;
const G3 = 1 / 6;
const F4 = (Math.sqrt(5) - 1) / 4;
const G4 = (5 - Math.sqrt(5)) / 20;
const GRAD3 = [
[1, 1, 0],
[-1, 1, 0],
[1, -1, 0],
[-1, -1, 0],
[1, 0, 1],
[-1, 0, 1],
[1, 0, -1],
[-1, 0, -1],
[0, 1, 1],
[0, -1, 1],
[0, 1, -1],
[0, -1, -1],
] as const;
const GRAD4 = [
[0, 1, 1, 1],
[0, 1, 1, -1],
[0, 1, -1, 1],
[0, 1, -1, -1],
[0, -1, 1, 1],
[0, -1, 1, -1],
[0, -1, -1, 1],
[0, -1, -1, -1],
[1, 0, 1, 1],
[1, 0, 1, -1],
[1, 0, -1, 1],
[1, 0, -1, -1],
[-1, 0, 1, 1],
[-1, 0, 1, -1],
[-1, 0, -1, 1],
[-1, 0, -1, -1],
[1, 1, 0, 1],
[1, 1, 0, -1],
[1, -1, 0, 1],
[1, -1, 0, -1],
[-1, 1, 0, 1],
[-1, 1, 0, -1],
[-1, -1, 0, 1],
[-1, -1, 0, -1],
[1, 1, 1, 0],
[1, 1, -1, 0],
[1, -1, 1, 0],
[1, -1, -1, 0],
[-1, 1, 1, 0],
[-1, 1, -1, 0],
[-1, -1, 1, 0],
[-1, -1, -1, 0],
] as const;
function dot2(
g: readonly [number, number, number],
x: number,
y: number
): number {
return g[0] * x + g[1] * y;
}
function dot3(
g: readonly [number, number, number],
x: number,
y: number,
z: number
): number {
return g[0] * x + g[1] * y + g[2] * z;
}
function dot4(
g: readonly [number, number, number, number],
x: number,
y: number,
z: number,
w: number
): number {
return g[0] * x + g[1] * y + g[2] * z + g[3] * w;
}
/** Simplex noise (Ken Perlin, 2001) in 2D/3D/4D with seeded permutation. */
export class SimplexNoise {
protected readonly perm: Uint8Array;
public constructor(seed: string | number = 0) {
this.perm = buildPermutation(seed);
}
public noise2(xin: number, yin: number): number {
const s = (xin + yin) * F2;
const i = Math.floor(xin + s);
const j = Math.floor(yin + s);
const t = (i + j) * G2;
const X0 = i - t;
const Y0 = j - t;
const x0 = xin - X0;
const y0 = yin - Y0;
let i1: number;
let j1: number;
if (x0 > y0) {
i1 = 1;
j1 = 0;
} else {
i1 = 0;
j1 = 1;
}
const x1 = x0 - i1 + G2;
const y1 = y0 - j1 + G2;
const x2 = x0 - 1.0 + 2.0 * G2;
const y2 = y0 - 1.0 + 2.0 * G2;
const ii = i & 0xff;
const jj = j & 0xff;
const gi0 = (this.perm[ii + (this.perm[jj] as number)] as number) % 12;
const gi1 =
(this.perm[ii + i1 + (this.perm[jj + j1] as number)] as number) %
12;
const gi2 =
(this.perm[ii + 1 + (this.perm[jj + 1] as number)] as number) % 12;
const n0 = this.corner2(x0, y0, gi0);
const n1 = this.corner2(x1, y1, gi1);
const n2 = this.corner2(x2, y2, gi2);
return 70.0 * (n0 + n1 + n2);
}
protected corner2(x: number, y: number, gi: number): number {
let t = 0.5 - x * x - y * y;
if (t < 0) return 0;
t *= t;
return (
t * t * dot2(GRAD3[gi] as readonly [number, number, number], x, y)
);
}
public noise3(xin: number, yin: number, zin: number): number {
const s = (xin + yin + zin) * F3;
const i = Math.floor(xin + s);
const j = Math.floor(yin + s);
const k = Math.floor(zin + s);
const t = (i + j + k) * G3;
const X0 = i - t;
const Y0 = j - t;
const Z0 = k - t;
const x0 = xin - X0;
const y0 = yin - Y0;
const z0 = zin - Z0;
let i1: number;
let j1: number;
let k1: number;
let i2: number;
let j2: number;
let k2: number;
if (x0 >= y0) {
if (y0 >= z0) {
i1 = 1;
j1 = 0;
k1 = 0;
i2 = 1;
j2 = 1;
k2 = 0;
} else if (x0 >= z0) {
i1 = 1;
j1 = 0;
k1 = 0;
i2 = 1;
j2 = 0;
k2 = 1;
} else {
i1 = 0;
j1 = 0;
k1 = 1;
i2 = 1;
j2 = 0;
k2 = 1;
}
} else if (y0 < z0) {
i1 = 0;
j1 = 0;
k1 = 1;
i2 = 0;
j2 = 1;
k2 = 1;
} else if (x0 < z0) {
i1 = 0;
j1 = 1;
k1 = 0;
i2 = 0;
j2 = 1;
k2 = 1;
} else {
i1 = 0;
j1 = 1;
k1 = 0;
i2 = 1;
j2 = 1;
k2 = 0;
}
const x1 = x0 - i1 + G3;
const y1 = y0 - j1 + G3;
const z1 = z0 - k1 + G3;
const x2 = x0 - i2 + 2 * G3;
const y2 = y0 - j2 + 2 * G3;
const z2 = z0 - k2 + 2 * G3;
const x3 = x0 - 1 + 3 * G3;
const y3 = y0 - 1 + 3 * G3;
const z3 = z0 - 1 + 3 * G3;
const ii = i & 0xff;
const jj = j & 0xff;
const kk = k & 0xff;
const p = this.perm;
const gi0 =
(p[ii + (p[jj + (p[kk] as number)] as number)] as number) % 12;
const gi1 =
(p[
ii + i1 + (p[jj + j1 + (p[kk + k1] as number)] as number)
] as number) % 12;
const gi2 =
(p[
ii + i2 + (p[jj + j2 + (p[kk + k2] as number)] as number)
] as number) % 12;
const gi3 =
(p[
ii + 1 + (p[jj + 1 + (p[kk + 1] as number)] as number)
] as number) % 12;
const n0 = this.corner3(x0, y0, z0, gi0);
const n1 = this.corner3(x1, y1, z1, gi1);
const n2 = this.corner3(x2, y2, z2, gi2);
const n3 = this.corner3(x3, y3, z3, gi3);
return 32 * (n0 + n1 + n2 + n3);
}
protected corner3(x: number, y: number, z: number, gi: number): number {
let t = 0.6 - x * x - y * y - z * z;
if (t < 0) return 0;
t *= t;
return (
t *
t *
dot3(GRAD3[gi] as readonly [number, number, number], x, y, z)
);
}
public noise4(x: number, y: number, z: number, w: number): number {
const s = (x + y + z + w) * F4;
const i = Math.floor(x + s);
const j = Math.floor(y + s);
const k = Math.floor(z + s);
const l = Math.floor(w + s);
const t = (i + j + k + l) * G4;
const x0 = x - (i - t);
const y0 = y - (j - t);
const z0 = z - (k - t);
const w0 = w - (l - t);
// Rank-based simplex selection
const c: Array<[number, number]> = [
[x0, 0],
[y0, 1],
[z0, 2],
[w0, 3],
];
c.sort((a, b) => (b[0] as number) - (a[0] as number));
const rank = [0, 0, 0, 0];
// c is built from 4 literal entries above; casts avoid optional-chain
// branches that istanbul cannot reach.
rank[(c[0] as [number, number])[1]] = 3;
rank[(c[1] as [number, number])[1]] = 2;
rank[(c[2] as [number, number])[1]] = 1;
const off = (r: number): [number, number, number, number] => [
/* istanbul ignore next -- rank is 4-element literal, ?? 0 unreachable */
(rank[0] ?? 0) >= r ? 1 : 0,
/* istanbul ignore next -- rank is 4-element literal, ?? 0 unreachable */
(rank[1] ?? 0) >= r ? 1 : 0,
/* istanbul ignore next -- rank is 4-element literal, ?? 0 unreachable */
(rank[2] ?? 0) >= r ? 1 : 0,
/* istanbul ignore next -- rank is 4-element literal, ?? 0 unreachable */
(rank[3] ?? 0) >= r ? 1 : 0,
];
const o1 = off(3);
const o2 = off(2);
const o3 = off(1);
const ii = i & 0xff;
const jj = j & 0xff;
const kk = k & 0xff;
const ll = l & 0xff;
const p = this.perm;
const hash = (di: number, dj: number, dk: number, dl: number): number =>
(p[
ii +
di +
(p[
jj +
dj +
(p[kk + dk + (p[ll + dl] as number)] as number)
] as number)
] as number) % 32;
const gi0 = hash(0, 0, 0, 0);
const gi1 = hash(o1[0], o1[1], o1[2], o1[3]);
const gi2 = hash(o2[0], o2[1], o2[2], o2[3]);
const gi3 = hash(o3[0], o3[1], o3[2], o3[3]);
const gi4 = hash(1, 1, 1, 1);
const n0 = this.corner4(x0, y0, z0, w0, gi0);
const n1 = this.corner4(
x0 - o1[0] + G4,
y0 - o1[1] + G4,
z0 - o1[2] + G4,
w0 - o1[3] + G4,
gi1
);
const n2 = this.corner4(
x0 - o2[0] + 2 * G4,
y0 - o2[1] + 2 * G4,
z0 - o2[2] + 2 * G4,
w0 - o2[3] + 2 * G4,
gi2
);
const n3 = this.corner4(
x0 - o3[0] + 3 * G4,
y0 - o3[1] + 3 * G4,
z0 - o3[2] + 3 * G4,
w0 - o3[3] + 3 * G4,
gi3
);
const n4 = this.corner4(
x0 - 1 + 4 * G4,
y0 - 1 + 4 * G4,
z0 - 1 + 4 * G4,
w0 - 1 + 4 * G4,
gi4
);
return 27 * (n0 + n1 + n2 + n3 + n4);
}
protected corner4(
x: number,
y: number,
z: number,
w: number,
gi: number
): number {
let t = 0.6 - x * x - y * y - z * z - w * w;
if (t < 0) return 0;
t *= t;
return (
t *
t *
dot4(
GRAD4[gi] as readonly [number, number, number, number],
x,
y,
z,
w
)
);
}
}
export function simplex2(seed: string | number, x: number, y: number): number {
return new SimplexNoise(seed).noise2(x, y);
}
export function simplex3(
seed: string | number,
x: number,
y: number,
z: number
): number {
return new SimplexNoise(seed).noise3(x, y, z);
}
export function simplex4(
seed: string | number,
x: number,
y: number,
z: number,
w: number
): number {
return new SimplexNoise(seed).noise4(x, y, z, w);
}