Initial Commit
This commit is contained in:
1
packages/foundation/random/noise/src/worley/index.ts
Normal file
1
packages/foundation/random/noise/src/worley/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { type WorleyMode, WorleyNoise, worley2, worley3 } from "./worley";
|
||||
@@ -0,0 +1,10 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { WorleyNoise } from "./worley.js";
|
||||
|
||||
describe("WorleyNoise default seed", () => {
|
||||
it("constructs with default seed (no args)", { timeout: 5000 }, () => {
|
||||
const w = new WorleyNoise();
|
||||
expect(Number.isFinite(w.noise2(0.3, 0.4))).toBe(true);
|
||||
expect(Number.isFinite(w.noise3(0.3, 0.4, 0.5))).toBe(true);
|
||||
});
|
||||
});
|
||||
49
packages/foundation/random/noise/src/worley/worley.md
Normal file
49
packages/foundation/random/noise/src/worley/worley.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# worley/worley
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `worley` module implements Steven Worley's cellular noise in 2D and 3D. At every point in space it computes the distance to the nearest (F1) and second-nearest (F2) feature point, where feature points are scattered per-cell according to a seeded PRNG. The resulting fields (F1, F2, F2-F1) give procedural cell structures used for lava rock, cracked surfaces, Voronoi textures, stone patterns, and biological cell walls.
|
||||
|
||||
## Features
|
||||
|
||||
- `WorleyNoise` class and `worley2` / `worley3` functional APIs
|
||||
- `WorleyMode`: `"F1" | "F2" | "F2-F1"`
|
||||
- 3×3 (2D) / 3×3×3 (3D) neighbourhood search — the canonical compromise between quality and performance
|
||||
- 1..3 feature points per cell, seeded per-cell so different seeds give completely different fields
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { worley2 } from "@suaveplan/noise";
|
||||
|
||||
const distance = worley2("stones", 0.25, 0.75); // F1 default
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { WorleyNoise } from "@suaveplan/noise";
|
||||
|
||||
const w = new WorleyNoise("cracks");
|
||||
for (let y = 0; y < 256; y += 1) {
|
||||
for (let x = 0; x < 256; x += 1) {
|
||||
const edge = w.noise2(x * 0.05, y * 0.05, "F2-F1");
|
||||
// Paint edges where two cells meet equidistantly.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class WorleyNoise(seed: string | number = 0)` — one seeded field
|
||||
- `noise2(x, y, mode?): number` — `mode` defaults to `"F1"`
|
||||
- `noise3(x, y, z, mode?): number`
|
||||
- `worley2(seed, x, y, mode?): number`
|
||||
- `worley3(seed, x, y, z, mode?): number`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Feature points are _not_ cached between evaluations; we recreate the per-cell `Sfc32Rng` each time. This keeps the class stateless and thread-safe but makes repeated evaluation at nearby points slightly more expensive. For heavy workloads the simplest optimisation is to cache `featurePoints2/3` results by cell coordinate — feel free to subclass and override those protected methods.
|
||||
45
packages/foundation/random/noise/src/worley/worley.test.ts
Normal file
45
packages/foundation/random/noise/src/worley/worley.test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { WorleyNoise, worley2, worley3 } from "./worley.js";
|
||||
|
||||
describe("WorleyNoise", () => {
|
||||
it("is deterministic", { timeout: 5000 }, () => {
|
||||
const a = new WorleyNoise(1);
|
||||
const b = new WorleyNoise(1);
|
||||
expect(a.noise2(1.3, 2.5)).toBe(b.noise2(1.3, 2.5));
|
||||
expect(a.noise3(1.3, 2.5, 3.1)).toBe(b.noise3(1.3, 2.5, 3.1));
|
||||
});
|
||||
|
||||
it("F1 output in [0, 1]", { timeout: 5000 }, () => {
|
||||
const w = new WorleyNoise(2);
|
||||
for (let i = 0; i < 50; i += 1) {
|
||||
const v = w.noise2(i * 0.3, i * 0.7);
|
||||
expect(v).toBeGreaterThanOrEqual(0);
|
||||
expect(v).toBeLessThanOrEqual(1);
|
||||
}
|
||||
});
|
||||
|
||||
it("supports F2 and F2-F1 modes", { timeout: 5000 }, () => {
|
||||
const w = new WorleyNoise(3);
|
||||
const f1 = w.noise2(1.3, 2.5, "F1");
|
||||
const f2 = w.noise2(1.3, 2.5, "F2");
|
||||
const diff = w.noise2(1.3, 2.5, "F2-F1");
|
||||
expect(f2).toBeGreaterThanOrEqual(f1);
|
||||
expect(Math.abs(diff - (f2 - f1))).toBeLessThan(1e-9);
|
||||
});
|
||||
|
||||
it("3D variant works + supports modes", { timeout: 5000 }, () => {
|
||||
const w = new WorleyNoise(4);
|
||||
const f1 = w.noise3(1, 2, 3, "F1");
|
||||
const f2 = w.noise3(1, 2, 3, "F2");
|
||||
const diff = w.noise3(1, 2, 3, "F2-F1");
|
||||
expect(f1).toBeGreaterThanOrEqual(0);
|
||||
expect(f2).toBeGreaterThanOrEqual(f1);
|
||||
expect(diff).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it("functional wrappers match", { timeout: 5000 }, () => {
|
||||
const w = new WorleyNoise("z");
|
||||
expect(worley2("z", 0.4, 0.6)).toBe(w.noise2(0.4, 0.6));
|
||||
expect(worley3("z", 0.4, 0.6, 0.8)).toBe(w.noise3(0.4, 0.6, 0.8));
|
||||
});
|
||||
});
|
||||
120
packages/foundation/random/noise/src/worley/worley.ts
Normal file
120
packages/foundation/random/noise/src/worley/worley.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { Sfc32Rng } from "@suaveplan/pseudo-random";
|
||||
|
||||
export type WorleyMode = "F1" | "F2" | "F2-F1";
|
||||
|
||||
/**
|
||||
* Worley (cellular) noise. Output in `[0, 1]` for F1/F2; F2-F1 can exceed 1
|
||||
* when the two nearest feature points straddle more than a unit cell.
|
||||
*/
|
||||
export class WorleyNoise {
|
||||
protected readonly seed: string | number;
|
||||
|
||||
public constructor(seed: string | number = 0) {
|
||||
this.seed = seed;
|
||||
}
|
||||
|
||||
protected featurePoints2(cx: number, cy: number): Array<[number, number]> {
|
||||
const rng = new Sfc32Rng(`${this.seed}:${cx}:${cy}`);
|
||||
const n = 1 + (rng.nextUint32() % 3); // 1..3 feature points per cell
|
||||
const out: Array<[number, number]> = [];
|
||||
for (let i = 0; i < n; i += 1) {
|
||||
out.push([cx + rng.nextFloat(), cy + rng.nextFloat()]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
protected featurePoints3(
|
||||
cx: number,
|
||||
cy: number,
|
||||
cz: number
|
||||
): Array<[number, number, number]> {
|
||||
const rng = new Sfc32Rng(`${this.seed}:${cx}:${cy}:${cz}`);
|
||||
const n = 1 + (rng.nextUint32() % 3);
|
||||
const out: Array<[number, number, number]> = [];
|
||||
for (let i = 0; i < n; i += 1) {
|
||||
out.push([
|
||||
cx + rng.nextFloat(),
|
||||
cy + rng.nextFloat(),
|
||||
cz + rng.nextFloat(),
|
||||
]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
public noise2(x: number, y: number, mode: WorleyMode = "F1"): number {
|
||||
const cx = Math.floor(x);
|
||||
const cy = Math.floor(y);
|
||||
let f1 = Number.POSITIVE_INFINITY;
|
||||
let f2 = Number.POSITIVE_INFINITY;
|
||||
for (let dx = -1; dx <= 1; dx += 1) {
|
||||
for (let dy = -1; dy <= 1; dy += 1) {
|
||||
const pts = this.featurePoints2(cx + dx, cy + dy);
|
||||
for (const [px, py] of pts) {
|
||||
const d = Math.hypot(x - px, y - py);
|
||||
if (d < f1) {
|
||||
f2 = f1;
|
||||
f1 = d;
|
||||
} else if (d < f2) {
|
||||
f2 = d;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mode === "F1") return Math.min(1, f1);
|
||||
if (mode === "F2") return Math.min(1, f2);
|
||||
return Math.max(0, f2 - f1);
|
||||
}
|
||||
|
||||
public noise3(
|
||||
x: number,
|
||||
y: number,
|
||||
z: number,
|
||||
mode: WorleyMode = "F1"
|
||||
): number {
|
||||
const cx = Math.floor(x);
|
||||
const cy = Math.floor(y);
|
||||
const cz = Math.floor(z);
|
||||
let f1 = Number.POSITIVE_INFINITY;
|
||||
let f2 = Number.POSITIVE_INFINITY;
|
||||
for (let dx = -1; dx <= 1; dx += 1) {
|
||||
for (let dy = -1; dy <= 1; dy += 1) {
|
||||
for (let dz = -1; dz <= 1; dz += 1) {
|
||||
const pts = this.featurePoints3(cx + dx, cy + dy, cz + dz);
|
||||
for (const [px, py, pz] of pts) {
|
||||
const d = Math.sqrt(
|
||||
(x - px) ** 2 + (y - py) ** 2 + (z - pz) ** 2
|
||||
);
|
||||
if (d < f1) {
|
||||
f2 = f1;
|
||||
f1 = d;
|
||||
} else if (d < f2) {
|
||||
f2 = d;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mode === "F1") return Math.min(1, f1);
|
||||
if (mode === "F2") return Math.min(1, f2);
|
||||
return Math.max(0, f2 - f1);
|
||||
}
|
||||
}
|
||||
|
||||
export function worley2(
|
||||
seed: string | number,
|
||||
x: number,
|
||||
y: number,
|
||||
mode: WorleyMode = "F1"
|
||||
): number {
|
||||
return new WorleyNoise(seed).noise2(x, y, mode);
|
||||
}
|
||||
|
||||
export function worley3(
|
||||
seed: string | number,
|
||||
x: number,
|
||||
y: number,
|
||||
z: number,
|
||||
mode: WorleyMode = "F1"
|
||||
): number {
|
||||
return new WorleyNoise(seed).noise3(x, y, z, mode);
|
||||
}
|
||||
Reference in New Issue
Block a user