@sg-pattern-engine/algorithms 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +19 -0
- package/dist/index.d.mts +44 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +181 -0
- package/dist/index.mjs +150 -0
- package/package.json +27 -0
- package/src/automata/reactionDiffusion.ts +111 -0
- package/src/geometry/voronoi.ts +19 -0
- package/src/index.ts +16 -0
- package/src/noise/perlin.ts +29 -0
- package/src/particles/flowField.ts +18 -0
- package/tsconfig.json +11 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
|
|
2
|
+
> @sg-pattern-engine/algorithms@1.0.0 build /workspace/packages/algorithms
|
|
3
|
+
> tsup src/index.ts --format cjs,esm --dts --clean
|
|
4
|
+
|
|
5
|
+
[34mCLI[39m Building entry: src/index.ts
|
|
6
|
+
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
7
|
+
[34mCLI[39m tsup v8.5.1
|
|
8
|
+
[34mCLI[39m Target: es2020
|
|
9
|
+
[34mCLI[39m Cleaning output folder
|
|
10
|
+
[34mCJS[39m Build start
|
|
11
|
+
[34mESM[39m Build start
|
|
12
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m4.55 KB[39m
|
|
13
|
+
[32mESM[39m ⚡️ Build success in 69ms
|
|
14
|
+
[32mCJS[39m [1mdist/index.js [22m[32m5.73 KB[39m
|
|
15
|
+
[32mCJS[39m ⚡️ Build success in 73ms
|
|
16
|
+
DTS Build start
|
|
17
|
+
DTS ⚡️ Build success in 2012ms
|
|
18
|
+
DTS dist/index.d.ts 1.34 KB
|
|
19
|
+
DTS dist/index.d.mts 1.34 KB
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { AlgorithmPlugin, AlgorithmContext, AlgorithmOutput } from '@sg-pattern-engine/core';
|
|
2
|
+
|
|
3
|
+
declare class PerlinNoisePlugin implements AlgorithmPlugin {
|
|
4
|
+
name: string;
|
|
5
|
+
generate(ctx: AlgorithmContext): AlgorithmOutput;
|
|
6
|
+
private noise;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
declare class FlowFieldPlugin implements AlgorithmPlugin {
|
|
10
|
+
name: string;
|
|
11
|
+
generate(ctx: AlgorithmContext): AlgorithmOutput;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
declare class VoronoiPlugin implements AlgorithmPlugin {
|
|
15
|
+
name: string;
|
|
16
|
+
generate(ctx: AlgorithmContext): AlgorithmOutput;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Gray-Scott reaction-diffusion.
|
|
21
|
+
* Two chemical species A and B diffuse and react:
|
|
22
|
+
* A + 2B → 3B (A is consumed, B is produced)
|
|
23
|
+
* B → P (B decays at kill rate k)
|
|
24
|
+
*
|
|
25
|
+
* Classic parameter sets:
|
|
26
|
+
* Coral: f=0.0545, k=0.062
|
|
27
|
+
* Mitosis: f=0.0367, k=0.0649
|
|
28
|
+
* Solitons: f=0.030, k=0.060
|
|
29
|
+
* Worms: f=0.058, k=0.065
|
|
30
|
+
* Spots: f=0.035, k=0.060
|
|
31
|
+
*/
|
|
32
|
+
declare class ReactionDiffusionPlugin implements AlgorithmPlugin {
|
|
33
|
+
name: string;
|
|
34
|
+
private readonly DA;
|
|
35
|
+
private readonly DB;
|
|
36
|
+
private readonly F;
|
|
37
|
+
private readonly K;
|
|
38
|
+
private readonly DT;
|
|
39
|
+
generate(ctx: AlgorithmContext): AlgorithmOutput;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
declare const algorithms: (PerlinNoisePlugin | FlowFieldPlugin | VoronoiPlugin | ReactionDiffusionPlugin)[];
|
|
43
|
+
|
|
44
|
+
export { FlowFieldPlugin, PerlinNoisePlugin, ReactionDiffusionPlugin, VoronoiPlugin, algorithms };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { AlgorithmPlugin, AlgorithmContext, AlgorithmOutput } from '@sg-pattern-engine/core';
|
|
2
|
+
|
|
3
|
+
declare class PerlinNoisePlugin implements AlgorithmPlugin {
|
|
4
|
+
name: string;
|
|
5
|
+
generate(ctx: AlgorithmContext): AlgorithmOutput;
|
|
6
|
+
private noise;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
declare class FlowFieldPlugin implements AlgorithmPlugin {
|
|
10
|
+
name: string;
|
|
11
|
+
generate(ctx: AlgorithmContext): AlgorithmOutput;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
declare class VoronoiPlugin implements AlgorithmPlugin {
|
|
15
|
+
name: string;
|
|
16
|
+
generate(ctx: AlgorithmContext): AlgorithmOutput;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Gray-Scott reaction-diffusion.
|
|
21
|
+
* Two chemical species A and B diffuse and react:
|
|
22
|
+
* A + 2B → 3B (A is consumed, B is produced)
|
|
23
|
+
* B → P (B decays at kill rate k)
|
|
24
|
+
*
|
|
25
|
+
* Classic parameter sets:
|
|
26
|
+
* Coral: f=0.0545, k=0.062
|
|
27
|
+
* Mitosis: f=0.0367, k=0.0649
|
|
28
|
+
* Solitons: f=0.030, k=0.060
|
|
29
|
+
* Worms: f=0.058, k=0.065
|
|
30
|
+
* Spots: f=0.035, k=0.060
|
|
31
|
+
*/
|
|
32
|
+
declare class ReactionDiffusionPlugin implements AlgorithmPlugin {
|
|
33
|
+
name: string;
|
|
34
|
+
private readonly DA;
|
|
35
|
+
private readonly DB;
|
|
36
|
+
private readonly F;
|
|
37
|
+
private readonly K;
|
|
38
|
+
private readonly DT;
|
|
39
|
+
generate(ctx: AlgorithmContext): AlgorithmOutput;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
declare const algorithms: (PerlinNoisePlugin | FlowFieldPlugin | VoronoiPlugin | ReactionDiffusionPlugin)[];
|
|
43
|
+
|
|
44
|
+
export { FlowFieldPlugin, PerlinNoisePlugin, ReactionDiffusionPlugin, VoronoiPlugin, algorithms };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
FlowFieldPlugin: () => FlowFieldPlugin,
|
|
24
|
+
PerlinNoisePlugin: () => PerlinNoisePlugin,
|
|
25
|
+
ReactionDiffusionPlugin: () => ReactionDiffusionPlugin,
|
|
26
|
+
VoronoiPlugin: () => VoronoiPlugin,
|
|
27
|
+
algorithms: () => algorithms
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(index_exports);
|
|
30
|
+
|
|
31
|
+
// src/noise/perlin.ts
|
|
32
|
+
var PerlinNoisePlugin = class {
|
|
33
|
+
constructor() {
|
|
34
|
+
this.name = "perlin";
|
|
35
|
+
}
|
|
36
|
+
generate(ctx) {
|
|
37
|
+
const { width, height } = ctx.tile;
|
|
38
|
+
const field = ctx.fieldManager.createScalarField("perlin", width, height);
|
|
39
|
+
const scale = ctx.config.scale || 0.01;
|
|
40
|
+
for (let y = 0; y < height; y++) {
|
|
41
|
+
for (let x = 0; x < ctx.tile.width; x++) {
|
|
42
|
+
const val = this.noise(
|
|
43
|
+
(ctx.tile.offsetX + x) * scale,
|
|
44
|
+
(ctx.tile.offsetY + y) * scale,
|
|
45
|
+
ctx.rng.random()
|
|
46
|
+
);
|
|
47
|
+
field.set(x, y, val);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { type: "scalarField", data: field.data, width, height };
|
|
51
|
+
}
|
|
52
|
+
noise(x, y, seed) {
|
|
53
|
+
return Math.sin(x * 12.9898 + y * 78.233 + seed * 43758.5453) * 0.5 + 0.5;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// src/particles/flowField.ts
|
|
58
|
+
var FlowFieldPlugin = class {
|
|
59
|
+
constructor() {
|
|
60
|
+
this.name = "flowField";
|
|
61
|
+
}
|
|
62
|
+
generate(ctx) {
|
|
63
|
+
const { width, height } = ctx.tile;
|
|
64
|
+
const field = ctx.fieldManager.createVectorField("flow", width, height);
|
|
65
|
+
for (let y = 0; y < height; y++) {
|
|
66
|
+
for (let x = 0; x < width; x++) {
|
|
67
|
+
const angle = ctx.rng.random() * Math.PI * 2;
|
|
68
|
+
field.set(x, y, Math.cos(angle), Math.sin(angle));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { type: "vectorField", data: field.data, width, height };
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// src/geometry/voronoi.ts
|
|
76
|
+
var VoronoiPlugin = class {
|
|
77
|
+
constructor() {
|
|
78
|
+
this.name = "voronoi";
|
|
79
|
+
}
|
|
80
|
+
generate(ctx) {
|
|
81
|
+
const pointsCount = ctx.config.density || 50;
|
|
82
|
+
const points = [];
|
|
83
|
+
for (let i = 0; i < pointsCount; i++) {
|
|
84
|
+
points.push({
|
|
85
|
+
x: ctx.rng.random() * ctx.tile.totalWidth,
|
|
86
|
+
y: ctx.rng.random() * ctx.tile.totalHeight
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return { type: "points", data: points };
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// src/automata/reactionDiffusion.ts
|
|
94
|
+
var ReactionDiffusionPlugin = class {
|
|
95
|
+
constructor() {
|
|
96
|
+
this.name = "reactionDiffusion";
|
|
97
|
+
// Default Gray-Scott parameters — coral growth preset
|
|
98
|
+
this.DA = 1;
|
|
99
|
+
// diffusion rate of A
|
|
100
|
+
this.DB = 0.5;
|
|
101
|
+
// diffusion rate of B
|
|
102
|
+
this.F = 0.0545;
|
|
103
|
+
// feed rate
|
|
104
|
+
this.K = 0.062;
|
|
105
|
+
// kill rate
|
|
106
|
+
this.DT = 1;
|
|
107
|
+
}
|
|
108
|
+
// time step
|
|
109
|
+
generate(ctx) {
|
|
110
|
+
const { width, height } = ctx.tile;
|
|
111
|
+
const iterations = ctx.config.iterations ?? 2e3;
|
|
112
|
+
let A = new Float32Array(width * height);
|
|
113
|
+
let B = new Float32Array(width * height);
|
|
114
|
+
let nextA = new Float32Array(width * height);
|
|
115
|
+
let nextB = new Float32Array(width * height);
|
|
116
|
+
const f = ctx.config.feedRate ?? this.F;
|
|
117
|
+
const k = ctx.config.killRate ?? this.K;
|
|
118
|
+
const da = this.DA;
|
|
119
|
+
const db = this.DB;
|
|
120
|
+
const dt = this.DT;
|
|
121
|
+
A.fill(1);
|
|
122
|
+
B.fill(0);
|
|
123
|
+
const patchCount = Math.max(1, Math.floor(width * height * 1e-3));
|
|
124
|
+
const patchRadius = Math.max(3, Math.floor(Math.min(width, height) * 0.04));
|
|
125
|
+
for (let p = 0; p < patchCount; p++) {
|
|
126
|
+
const cx = Math.floor(ctx.rng.random() * width);
|
|
127
|
+
const cy = Math.floor(ctx.rng.random() * height);
|
|
128
|
+
for (let dy = -patchRadius; dy <= patchRadius; dy++) {
|
|
129
|
+
for (let dx = -patchRadius; dx <= patchRadius; dx++) {
|
|
130
|
+
if (dx * dx + dy * dy > patchRadius * patchRadius) continue;
|
|
131
|
+
const nx = (cx + dx + width) % width;
|
|
132
|
+
const ny = (cy + dy + height) % height;
|
|
133
|
+
const i = ny * width + nx;
|
|
134
|
+
A[i] = 0.5 + ctx.rng.random() * 0.1;
|
|
135
|
+
B[i] = 0.25 + ctx.rng.random() * 0.1;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (let iter = 0; iter < iterations; iter++) {
|
|
140
|
+
for (let y = 0; y < height; y++) {
|
|
141
|
+
for (let x = 0; x < width; x++) {
|
|
142
|
+
const i = y * width + x;
|
|
143
|
+
const xL = (x - 1 + width) % width;
|
|
144
|
+
const xR = (x + 1) % width;
|
|
145
|
+
const yU = (y - 1 + height) % height;
|
|
146
|
+
const yD = (y + 1) % height;
|
|
147
|
+
const lapA = A[yU * width + x] + A[yD * width + x] + A[y * width + xL] + A[y * width + xR] - 4 * A[i];
|
|
148
|
+
const lapB = B[yU * width + x] + B[yD * width + x] + B[y * width + xL] + B[y * width + xR] - 4 * B[i];
|
|
149
|
+
const a = A[i];
|
|
150
|
+
const b = B[i];
|
|
151
|
+
const reaction = a * b * b;
|
|
152
|
+
nextA[i] = Math.max(0, Math.min(1, a + dt * (da * lapA - reaction + f * (1 - a))));
|
|
153
|
+
nextB[i] = Math.max(0, Math.min(1, b + dt * (db * lapB + reaction - (k + f) * b)));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const tmpA = A;
|
|
157
|
+
A = nextA;
|
|
158
|
+
nextA = tmpA;
|
|
159
|
+
const tmpB = B;
|
|
160
|
+
B = nextB;
|
|
161
|
+
nextB = tmpB;
|
|
162
|
+
}
|
|
163
|
+
return { type: "scalarField", data: B, width, height };
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// src/index.ts
|
|
168
|
+
var algorithms = [
|
|
169
|
+
new PerlinNoisePlugin(),
|
|
170
|
+
new FlowFieldPlugin(),
|
|
171
|
+
new VoronoiPlugin(),
|
|
172
|
+
new ReactionDiffusionPlugin()
|
|
173
|
+
];
|
|
174
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
175
|
+
0 && (module.exports = {
|
|
176
|
+
FlowFieldPlugin,
|
|
177
|
+
PerlinNoisePlugin,
|
|
178
|
+
ReactionDiffusionPlugin,
|
|
179
|
+
VoronoiPlugin,
|
|
180
|
+
algorithms
|
|
181
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// src/noise/perlin.ts
|
|
2
|
+
var PerlinNoisePlugin = class {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.name = "perlin";
|
|
5
|
+
}
|
|
6
|
+
generate(ctx) {
|
|
7
|
+
const { width, height } = ctx.tile;
|
|
8
|
+
const field = ctx.fieldManager.createScalarField("perlin", width, height);
|
|
9
|
+
const scale = ctx.config.scale || 0.01;
|
|
10
|
+
for (let y = 0; y < height; y++) {
|
|
11
|
+
for (let x = 0; x < ctx.tile.width; x++) {
|
|
12
|
+
const val = this.noise(
|
|
13
|
+
(ctx.tile.offsetX + x) * scale,
|
|
14
|
+
(ctx.tile.offsetY + y) * scale,
|
|
15
|
+
ctx.rng.random()
|
|
16
|
+
);
|
|
17
|
+
field.set(x, y, val);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return { type: "scalarField", data: field.data, width, height };
|
|
21
|
+
}
|
|
22
|
+
noise(x, y, seed) {
|
|
23
|
+
return Math.sin(x * 12.9898 + y * 78.233 + seed * 43758.5453) * 0.5 + 0.5;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// src/particles/flowField.ts
|
|
28
|
+
var FlowFieldPlugin = class {
|
|
29
|
+
constructor() {
|
|
30
|
+
this.name = "flowField";
|
|
31
|
+
}
|
|
32
|
+
generate(ctx) {
|
|
33
|
+
const { width, height } = ctx.tile;
|
|
34
|
+
const field = ctx.fieldManager.createVectorField("flow", width, height);
|
|
35
|
+
for (let y = 0; y < height; y++) {
|
|
36
|
+
for (let x = 0; x < width; x++) {
|
|
37
|
+
const angle = ctx.rng.random() * Math.PI * 2;
|
|
38
|
+
field.set(x, y, Math.cos(angle), Math.sin(angle));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return { type: "vectorField", data: field.data, width, height };
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// src/geometry/voronoi.ts
|
|
46
|
+
var VoronoiPlugin = class {
|
|
47
|
+
constructor() {
|
|
48
|
+
this.name = "voronoi";
|
|
49
|
+
}
|
|
50
|
+
generate(ctx) {
|
|
51
|
+
const pointsCount = ctx.config.density || 50;
|
|
52
|
+
const points = [];
|
|
53
|
+
for (let i = 0; i < pointsCount; i++) {
|
|
54
|
+
points.push({
|
|
55
|
+
x: ctx.rng.random() * ctx.tile.totalWidth,
|
|
56
|
+
y: ctx.rng.random() * ctx.tile.totalHeight
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return { type: "points", data: points };
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// src/automata/reactionDiffusion.ts
|
|
64
|
+
var ReactionDiffusionPlugin = class {
|
|
65
|
+
constructor() {
|
|
66
|
+
this.name = "reactionDiffusion";
|
|
67
|
+
// Default Gray-Scott parameters — coral growth preset
|
|
68
|
+
this.DA = 1;
|
|
69
|
+
// diffusion rate of A
|
|
70
|
+
this.DB = 0.5;
|
|
71
|
+
// diffusion rate of B
|
|
72
|
+
this.F = 0.0545;
|
|
73
|
+
// feed rate
|
|
74
|
+
this.K = 0.062;
|
|
75
|
+
// kill rate
|
|
76
|
+
this.DT = 1;
|
|
77
|
+
}
|
|
78
|
+
// time step
|
|
79
|
+
generate(ctx) {
|
|
80
|
+
const { width, height } = ctx.tile;
|
|
81
|
+
const iterations = ctx.config.iterations ?? 2e3;
|
|
82
|
+
let A = new Float32Array(width * height);
|
|
83
|
+
let B = new Float32Array(width * height);
|
|
84
|
+
let nextA = new Float32Array(width * height);
|
|
85
|
+
let nextB = new Float32Array(width * height);
|
|
86
|
+
const f = ctx.config.feedRate ?? this.F;
|
|
87
|
+
const k = ctx.config.killRate ?? this.K;
|
|
88
|
+
const da = this.DA;
|
|
89
|
+
const db = this.DB;
|
|
90
|
+
const dt = this.DT;
|
|
91
|
+
A.fill(1);
|
|
92
|
+
B.fill(0);
|
|
93
|
+
const patchCount = Math.max(1, Math.floor(width * height * 1e-3));
|
|
94
|
+
const patchRadius = Math.max(3, Math.floor(Math.min(width, height) * 0.04));
|
|
95
|
+
for (let p = 0; p < patchCount; p++) {
|
|
96
|
+
const cx = Math.floor(ctx.rng.random() * width);
|
|
97
|
+
const cy = Math.floor(ctx.rng.random() * height);
|
|
98
|
+
for (let dy = -patchRadius; dy <= patchRadius; dy++) {
|
|
99
|
+
for (let dx = -patchRadius; dx <= patchRadius; dx++) {
|
|
100
|
+
if (dx * dx + dy * dy > patchRadius * patchRadius) continue;
|
|
101
|
+
const nx = (cx + dx + width) % width;
|
|
102
|
+
const ny = (cy + dy + height) % height;
|
|
103
|
+
const i = ny * width + nx;
|
|
104
|
+
A[i] = 0.5 + ctx.rng.random() * 0.1;
|
|
105
|
+
B[i] = 0.25 + ctx.rng.random() * 0.1;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
for (let iter = 0; iter < iterations; iter++) {
|
|
110
|
+
for (let y = 0; y < height; y++) {
|
|
111
|
+
for (let x = 0; x < width; x++) {
|
|
112
|
+
const i = y * width + x;
|
|
113
|
+
const xL = (x - 1 + width) % width;
|
|
114
|
+
const xR = (x + 1) % width;
|
|
115
|
+
const yU = (y - 1 + height) % height;
|
|
116
|
+
const yD = (y + 1) % height;
|
|
117
|
+
const lapA = A[yU * width + x] + A[yD * width + x] + A[y * width + xL] + A[y * width + xR] - 4 * A[i];
|
|
118
|
+
const lapB = B[yU * width + x] + B[yD * width + x] + B[y * width + xL] + B[y * width + xR] - 4 * B[i];
|
|
119
|
+
const a = A[i];
|
|
120
|
+
const b = B[i];
|
|
121
|
+
const reaction = a * b * b;
|
|
122
|
+
nextA[i] = Math.max(0, Math.min(1, a + dt * (da * lapA - reaction + f * (1 - a))));
|
|
123
|
+
nextB[i] = Math.max(0, Math.min(1, b + dt * (db * lapB + reaction - (k + f) * b)));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const tmpA = A;
|
|
127
|
+
A = nextA;
|
|
128
|
+
nextA = tmpA;
|
|
129
|
+
const tmpB = B;
|
|
130
|
+
B = nextB;
|
|
131
|
+
nextB = tmpB;
|
|
132
|
+
}
|
|
133
|
+
return { type: "scalarField", data: B, width, height };
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// src/index.ts
|
|
138
|
+
var algorithms = [
|
|
139
|
+
new PerlinNoisePlugin(),
|
|
140
|
+
new FlowFieldPlugin(),
|
|
141
|
+
new VoronoiPlugin(),
|
|
142
|
+
new ReactionDiffusionPlugin()
|
|
143
|
+
];
|
|
144
|
+
export {
|
|
145
|
+
FlowFieldPlugin,
|
|
146
|
+
PerlinNoisePlugin,
|
|
147
|
+
ReactionDiffusionPlugin,
|
|
148
|
+
VoronoiPlugin,
|
|
149
|
+
algorithms
|
|
150
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sg-pattern-engine/algorithms",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "./dist/index.js",
|
|
5
|
+
"module": "./dist/index.mjs",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.mjs",
|
|
11
|
+
"require": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@sg-pattern-engine/core": "1.0.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"tsup": "^8.0.0",
|
|
19
|
+
"typescript": "^5.4.0"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { AlgorithmPlugin, AlgorithmContext, AlgorithmOutput } from '@sg-pattern-engine/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Gray-Scott reaction-diffusion.
|
|
5
|
+
* Two chemical species A and B diffuse and react:
|
|
6
|
+
* A + 2B → 3B (A is consumed, B is produced)
|
|
7
|
+
* B → P (B decays at kill rate k)
|
|
8
|
+
*
|
|
9
|
+
* Classic parameter sets:
|
|
10
|
+
* Coral: f=0.0545, k=0.062
|
|
11
|
+
* Mitosis: f=0.0367, k=0.0649
|
|
12
|
+
* Solitons: f=0.030, k=0.060
|
|
13
|
+
* Worms: f=0.058, k=0.065
|
|
14
|
+
* Spots: f=0.035, k=0.060
|
|
15
|
+
*/
|
|
16
|
+
export class ReactionDiffusionPlugin implements AlgorithmPlugin {
|
|
17
|
+
name = 'reactionDiffusion';
|
|
18
|
+
|
|
19
|
+
// Default Gray-Scott parameters — coral growth preset
|
|
20
|
+
private readonly DA = 1.0; // diffusion rate of A
|
|
21
|
+
private readonly DB = 0.5; // diffusion rate of B
|
|
22
|
+
private readonly F = 0.0545; // feed rate
|
|
23
|
+
private readonly K = 0.062; // kill rate
|
|
24
|
+
private readonly DT = 1.0; // time step
|
|
25
|
+
|
|
26
|
+
generate(ctx: AlgorithmContext): AlgorithmOutput {
|
|
27
|
+
const { width, height } = ctx.tile;
|
|
28
|
+
const iterations = ctx.config.iterations ?? 2000;
|
|
29
|
+
|
|
30
|
+
// Allocate two channels: A (activator) and B (inhibitor)
|
|
31
|
+
// Flat arrays: index = y * width + x
|
|
32
|
+
let A = new Float32Array(width * height);
|
|
33
|
+
let B = new Float32Array(width * height);
|
|
34
|
+
let nextA = new Float32Array(width * height);
|
|
35
|
+
let nextB = new Float32Array(width * height);
|
|
36
|
+
|
|
37
|
+
// Allow parameter overrides via config
|
|
38
|
+
const f = ctx.config.feedRate ?? this.F;
|
|
39
|
+
const k = ctx.config.killRate ?? this.K;
|
|
40
|
+
const da = this.DA;
|
|
41
|
+
const db = this.DB;
|
|
42
|
+
const dt = this.DT;
|
|
43
|
+
|
|
44
|
+
// ── Initialise ─────────────────────────────────────────────────────────
|
|
45
|
+
// Fill A=1 everywhere, seed random B=1 patches
|
|
46
|
+
A.fill(1.0);
|
|
47
|
+
B.fill(0.0);
|
|
48
|
+
|
|
49
|
+
const patchCount = Math.max(1, Math.floor(width * height * 0.001));
|
|
50
|
+
const patchRadius = Math.max(3, Math.floor(Math.min(width, height) * 0.04));
|
|
51
|
+
|
|
52
|
+
for (let p = 0; p < patchCount; p++) {
|
|
53
|
+
const cx = Math.floor(ctx.rng.random() * width);
|
|
54
|
+
const cy = Math.floor(ctx.rng.random() * height);
|
|
55
|
+
for (let dy = -patchRadius; dy <= patchRadius; dy++) {
|
|
56
|
+
for (let dx = -patchRadius; dx <= patchRadius; dx++) {
|
|
57
|
+
if (dx * dx + dy * dy > patchRadius * patchRadius) continue;
|
|
58
|
+
const nx = ((cx + dx) + width) % width;
|
|
59
|
+
const ny = ((cy + dy) + height) % height;
|
|
60
|
+
const i = ny * width + nx;
|
|
61
|
+
A[i] = 0.5 + ctx.rng.random() * 0.1;
|
|
62
|
+
B[i] = 0.25 + ctx.rng.random() * 0.1;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── Iterate Gray-Scott ──────────────────────────────────────────────────
|
|
68
|
+
for (let iter = 0; iter < iterations; iter++) {
|
|
69
|
+
for (let y = 0; y < height; y++) {
|
|
70
|
+
for (let x = 0; x < width; x++) {
|
|
71
|
+
const i = y * width + x;
|
|
72
|
+
|
|
73
|
+
// Wrap-around neighbour indices
|
|
74
|
+
const xL = (x - 1 + width) % width;
|
|
75
|
+
const xR = (x + 1) % width;
|
|
76
|
+
const yU = (y - 1 + height) % height;
|
|
77
|
+
const yD = (y + 1) % height;
|
|
78
|
+
|
|
79
|
+
// Discrete Laplacian (5-point stencil)
|
|
80
|
+
const lapA =
|
|
81
|
+
A[yU * width + x ] +
|
|
82
|
+
A[yD * width + x ] +
|
|
83
|
+
A[y * width + xL] +
|
|
84
|
+
A[y * width + xR] -
|
|
85
|
+
4.0 * A[i];
|
|
86
|
+
|
|
87
|
+
const lapB =
|
|
88
|
+
B[yU * width + x ] +
|
|
89
|
+
B[yD * width + x ] +
|
|
90
|
+
B[y * width + xL] +
|
|
91
|
+
B[y * width + xR] -
|
|
92
|
+
4.0 * B[i];
|
|
93
|
+
|
|
94
|
+
const a = A[i];
|
|
95
|
+
const b = B[i];
|
|
96
|
+
const reaction = a * b * b;
|
|
97
|
+
|
|
98
|
+
nextA[i] = Math.max(0, Math.min(1, a + dt * (da * lapA - reaction + f * (1 - a))));
|
|
99
|
+
nextB[i] = Math.max(0, Math.min(1, b + dt * (db * lapB + reaction - (k + f) * b)));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Swap buffers without allocation
|
|
104
|
+
const tmpA = A; A = nextA; nextA = tmpA;
|
|
105
|
+
const tmpB = B; B = nextB; nextB = tmpB;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Return B channel — this is where the patterns form
|
|
109
|
+
return { type: 'scalarField', data: B, width, height };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { AlgorithmPlugin, AlgorithmContext, AlgorithmOutput } from '@sg-pattern-engine/core';
|
|
2
|
+
|
|
3
|
+
export class VoronoiPlugin implements AlgorithmPlugin {
|
|
4
|
+
name = 'voronoi';
|
|
5
|
+
|
|
6
|
+
generate(ctx: AlgorithmContext): AlgorithmOutput {
|
|
7
|
+
const pointsCount = ctx.config.density || 50;
|
|
8
|
+
const points = [];
|
|
9
|
+
|
|
10
|
+
for (let i = 0; i < pointsCount; i++) {
|
|
11
|
+
points.push({
|
|
12
|
+
x: ctx.rng.random() * ctx.tile.totalWidth,
|
|
13
|
+
y: ctx.rng.random() * ctx.tile.totalHeight
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return { type: 'points', data: points };
|
|
18
|
+
}
|
|
19
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { PerlinNoisePlugin } from './noise/perlin';
|
|
2
|
+
import { FlowFieldPlugin } from './particles/flowField';
|
|
3
|
+
import { VoronoiPlugin } from './geometry/voronoi';
|
|
4
|
+
import { ReactionDiffusionPlugin } from './automata/reactionDiffusion';
|
|
5
|
+
|
|
6
|
+
export const algorithms = [
|
|
7
|
+
new PerlinNoisePlugin(),
|
|
8
|
+
new FlowFieldPlugin(),
|
|
9
|
+
new VoronoiPlugin(),
|
|
10
|
+
new ReactionDiffusionPlugin()
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export * from './noise/perlin';
|
|
14
|
+
export * from './particles/flowField';
|
|
15
|
+
export * from './geometry/voronoi';
|
|
16
|
+
export * from './automata/reactionDiffusion';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { AlgorithmPlugin, AlgorithmContext, AlgorithmOutput } from '@sg-pattern-engine/core';
|
|
2
|
+
|
|
3
|
+
// Simplified deterministic Perlin implementation
|
|
4
|
+
export class PerlinNoisePlugin implements AlgorithmPlugin {
|
|
5
|
+
name = 'perlin';
|
|
6
|
+
|
|
7
|
+
generate(ctx: AlgorithmContext): AlgorithmOutput {
|
|
8
|
+
const { width, height } = ctx.tile;
|
|
9
|
+
const field = ctx.fieldManager.createScalarField('perlin', width, height);
|
|
10
|
+
const scale = ctx.config.scale || 0.01;
|
|
11
|
+
|
|
12
|
+
for (let y = 0; y < height; y++) {
|
|
13
|
+
for (let x = 0; x < ctx.tile.width; x++) {
|
|
14
|
+
const val = this.noise(
|
|
15
|
+
(ctx.tile.offsetX + x) * scale,
|
|
16
|
+
(ctx.tile.offsetY + y) * scale,
|
|
17
|
+
ctx.rng.random()
|
|
18
|
+
);
|
|
19
|
+
field.set(x, y, val);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return { type: 'scalarField', data: field.data, width, height };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private noise(x: number, y: number, seed: number): number {
|
|
26
|
+
// Deterministic hash-based noise approximation
|
|
27
|
+
return Math.sin(x * 12.9898 + y * 78.233 + seed * 43758.5453) * 0.5 + 0.5;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { AlgorithmPlugin, AlgorithmContext, AlgorithmOutput } from '@sg-pattern-engine/core';
|
|
2
|
+
|
|
3
|
+
export class FlowFieldPlugin implements AlgorithmPlugin {
|
|
4
|
+
name = 'flowField';
|
|
5
|
+
|
|
6
|
+
generate(ctx: AlgorithmContext): AlgorithmOutput {
|
|
7
|
+
const { width, height } = ctx.tile;
|
|
8
|
+
const field = ctx.fieldManager.createVectorField('flow', width, height);
|
|
9
|
+
|
|
10
|
+
for (let y = 0; y < height; y++) {
|
|
11
|
+
for (let x = 0; x < width; x++) {
|
|
12
|
+
const angle = ctx.rng.random() * Math.PI * 2;
|
|
13
|
+
field.set(x, y, Math.cos(angle), Math.sin(angle));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return { type: 'vectorField', data: field.data, width, height };
|
|
17
|
+
}
|
|
18
|
+
}
|