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 { type WorleyMode, WorleyNoise, worley2, worley3 } from "./worley";

View File

@@ -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);
});
});

View 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.

View 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));
});
});

View 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);
}