Gray scott algorithm for reaction diffusion patterns, then using marching squares to trace blobs to paths.
More info on kill ,feed parameters mrob.com/pub/comp/xmorphia/index.html
See sarah.skyon.be/conte…actiondiffusion.html for the iterative process
Log in to post a comment.
// Global code will be evaluated once.
const scale = 50; //min = 0, max = 100, step = 10
const tileWidth = 3;
const tileHeight = 3;
const tileOutsideArea = true; // set to true to make sure the outside area is still drawn on the edges
const randomSeed = 12345; //min = 1, max = 999999, step = 1
const killRatePercentage = 6.3; //min = 1, max = 10, step = 0.1
const feedRatePercentage = 5; //min = 1, max = 10, step = 0.1
const f = feedRatePercentage / 100;
const k = killRatePercentage / 100;
// the number of starting points to introduce the other gas to diffuse
const numberOfInitialSeeds = 150; //min = 1, max = 1000, step = 1
// the area of the simulation, bigger means slower.
const grayScottSize = 200; //min = 100, max = 500, step = 10
// the more steps, the longer the simulation will run
const steps = 2000; //min = 100, max = 10000, step = 100
const originOffset = 256/4;
// You can find the Turtle API reference here: https://turtletoy.net/syntax
Canvas.setpenopacity(1);
class LineSegment {
constructor(x1, y1, x2, y2) {
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
}
}
class Random {
constructor(seed) {
if (typeof seed === "undefined")
seed = ~~(Math.random() * 10000000);
this.seed = seed;
this.val = seed;
}
next() {
// this is in no way uniformly distributed, so it's really a bad rng, but it's fast enough
// and random enough
let x = Math.sin(this.val++) * 10000;
return x - Math.floor(x);
}
nextBetween(min, max) {
return min + this.next() * (max - min);
}
}
class Cell {
constructor() {
this.a = 0;
this.b = 0;
}
}
class GrayScott {
constructor(width, height, k, f, DA = 1, DB = 0.5) {
this.width = width;
this.height = height;
this.k = k;
this.f = f;
this.DA = DA;
this.DB = DB;
this.dt = 1;
this._t = 0;
this.minB = Number.POSITIVE_INFINITY;
this.maxB = Number.NEGATIVE_INFINITY;
this.minA = Number.POSITIVE_INFINITY;
this.maxA = Number.NEGATIVE_INFINITY;
this.buffer = this.createArray();
this.nextFrameBuffer = this.createArray();
}
get t() {
return this._t;
}
createArray() {
let arr = new Array(this.width);
for (let i = 0; i < this.width; i++) {
arr[i] = new Array(this.height);
for (let j = 0; j < this.height; j++) {
let c = new Cell();
c.a = 1;
c.b = 0;
arr[i][j] = c;
}
}
return arr;
}
addInitialSeed(x, y, rng) {
for (let i = 0; i < 2; i++) {
for (let j = 0; j < 2; j++) {
this.buffer[Math.floor(x + i)][Math.floor(y + j)].a = 1;
this.buffer[Math.floor(x + i)][Math.floor(y + j)].b = rng.next();
}
}
}
step() {
this.minA = Number.POSITIVE_INFINITY;
this.minB = Number.POSITIVE_INFINITY;
this.maxA = Number.NEGATIVE_INFINITY;
this.maxB = Number.NEGATIVE_INFINITY;
for (let x = 0; x < this.width; x++) {
for (let y = 0; y < this.height; y++) {
let oldA = this.buffer[x][y].a;
let oldB = this.buffer[x][y].b;
let middle = 0;
let adjacent = 0;
let diag = 0;
middle = oldA * -1;
let left = x - 1;
let right = x + 1;
let top = y - 1;
let bottom = y + 1;
if (left < 0)
left = this.width - 1;
if (right >= this.width)
right %= this.width;
if (top < 0)
top = this.height - 1;
if (bottom >= this.height)
bottom %= this.height;
adjacent += this.buffer[x][bottom].a * 0.2;
adjacent += this.buffer[x][top].a * 0.2;
adjacent += this.buffer[left][y].a * 0.2;
adjacent += this.buffer[right][y].a * 0.2;
diag += this.buffer[right][bottom].a * 0.05;
diag += this.buffer[right][top].a * 0.05;
diag += this.buffer[left][top].a * 0.05;
diag += this.buffer[left][bottom].a * 0.05;
let factor2 = oldA * oldB * oldB;
let laplaceA = middle + adjacent + diag;
let newA = (oldA + (this.DA * laplaceA - factor2) + this.f * (1 - oldA)) * this.dt;
middle = oldB * -1;
adjacent = 0;
diag = 0;
adjacent += this.buffer[x][bottom].b * 0.2;
adjacent += this.buffer[x][top].b * 0.2;
adjacent += this.buffer[left][y].b * 0.2;
adjacent += this.buffer[right][y].b * 0.2;
diag += this.buffer[right][bottom].b * 0.05;
diag += this.buffer[right][top].b * 0.05;
diag += this.buffer[left][top].b * 0.05;
diag += this.buffer[left][bottom].b * 0.05;
let laplaceB = middle + adjacent + diag;
let newB = (oldB + (this.DB * laplaceB + factor2) - (this.k + this.f) * oldB) * this.dt;
if (newB < this.minB)
this.minB = newB;
if (newA < this.minA)
this.minA = newA;
if (newB > this.maxB)
this.maxB = newB;
if (newA > this.maxA)
this.maxA = newA;
this.nextFrameBuffer[x][y].a = newA;
this.nextFrameBuffer[x][y].b = newB;
// A' = A + (lapl - A*B² + F * (1-A))* dt
// B' = B + (lapl + A*B² - (k+F) * B)* dt
}
}
this.swapBuffers();
this._t++;
}
swapBuffers() {
let tmp = this.buffer;
this.buffer = this.nextFrameBuffer;
this.nextFrameBuffer = tmp;
}
foreachCell(func) {
for (let x = 0; x < this.width; x++) {
for (let y = 0; y < this.height; y++) {
let c = this.buffer[x][y];
let val = 0;
// normalize
//val += Math.floor((c.b - this.minB) / (this.maxB - this.minB) * 255);
val += this.maxA - this.minA > 0 ? Math.floor(((c.a - this.minA) / (this.maxA - this.minA)) * 255) : 0;
func(x, y, val);
}
}
}
}
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class MarchingSquareCell {
constructor(nr, p1, p2) {
this.nr = nr;
this.p1 = p1;
this.p2 = p2;
}
}
/**
*
* https://en.wikipedia.org/wiki/Marching_squares
*/
function marchingSquares(values, threshold, width, height, wrapAround = true) {
let mask;
let cells = [];
for (let j = 0; j < height; j++)
cells.push([]);
for (let j = 0; j < height; j++) {
for (let i = 0; i < width; i++) {
if (!wrapAround) {
mask = [
j + 1 >= height ? values[j][i] > threshold : values[j + 1][i] > threshold,
i + 1 >= width || j + 1 >= height ? values[j][i] > threshold : values[j + 1][i + 1] > threshold,
i + 1 >= width ? values[j][i] > threshold : values[j][i + 1] > threshold,
values[j][i] > threshold,
];
}
else {
let nextX = i + 1 >= width ? i + 1 - width : i + 1;
let nextY = j + 1 >= height ? j + 1 - height : j + 1;
mask = [
values[nextY][i] > threshold,
values[nextY][nextX] > threshold,
values[j][nextX] > threshold,
values[j][i] > threshold,
];
}
let nr = ((mask[0] ? 1 : 0) << 3) + ((mask[1] ? 1 : 0) << 2) + ((mask[2] ? 1 : 0) << 1) + ((mask[3] ? 1 : 0) << 0);
if (nr != 0 && nr != 15) {
let uv;
if (!wrapAround) {
uv = getUV(nr, threshold, values[j][i], j + 1 >= height ? threshold : values[j + 1][i], i + 1 >= width ? threshold : values[j][i + 1], i + 1 >= width || j + 1 >= height ? threshold : values[j + 1][i + 1]);
}
else {
let nextX = i + 1 >= width ? i + 1 - width : i + 1;
let nextY = j + 1 >= height ? j + 1 - height : j + 1;
uv = getUV(nr, threshold, values[j][i], values[nextY][i], values[j][nextX], values[nextY][nextX]);
}
if (uv.length > 0) {
cells[j].push(new MarchingSquareCell(nr, uv[0], uv[1]));
}
}
else
cells[j].push(new MarchingSquareCell(nr));
}
}
return cells;
}
function getUV(nr, threshold, vLT = 0, vLB = 2, vRT = 0, vRB = 2) {
// (1 - min) / (max-min)
let lInterpol = (threshold - vLT) / (vLB - vLT);
let rInterpol = (threshold - vRT) / (vRB - vRT);
let tInterpol = (threshold - vLT) / (vRT - vLT);
let bInterpol = (threshold - vLB) / (vRB - vLB);
let l = new Point(0, lInterpol);
let t = new Point(tInterpol, 0);
let r = new Point(1, rInterpol);
let b = new Point(bInterpol, 1);
switch (nr) {
case 0:
return [];
case 1:
return [l, t];
case 2:
return [t, r];
case 3:
return [l, r];
case 4:
return [r, b];
case 5:
return [b, l];
case 6:
return [b, t];
case 7:
return [b, l];
case 8:
return [b, l];
case 9:
return [b, t];
case 10:
return [b, r];
case 11:
return [b, r];
case 12:
return [l, r];
case 13:
return [t, r];
case 14:
return [t, l];
case 15:
return [];
default:
return [];
}
}
function getMarchingSquaresResultFromReactionDiffusion(randomSeed, nrOfSeeds, grayScottSize, k, f, steps) {
const gs = new GrayScott(grayScottSize, grayScottSize, k, f);
const rng = new Random(randomSeed);
for (let k = 0; k < nrOfSeeds; k++) {
let x = Math.floor(rng.next() * (grayScottSize - 2));
let y = Math.floor(rng.next() * (grayScottSize - 2));
gs.addInitialSeed(x, y, rng);
}
console.log("running gs");
for (let i = 0; i < steps; i++) {
gs.step();
}
const cells = [];
for (let i = 0; i < grayScottSize; i++) {
cells.push([]);
for (let j = 0; j < grayScottSize; j++) {
cells[i].push(0);
}
}
gs.foreachCell((i, j, val) => {
cells[i][j] = 255 - val;
});
console.log("done running gs");
console.log("running marching squares");
let result = marchingSquares(cells, 128, grayScottSize, grayScottSize);
return result;
}
function getReactionDiffusionSegments(scale, randomSeed = 12345, numberOfSeeds = 150, grayScottSize = 250, k = 0.063, f = 0.05, steps = 2000) {
const cellSize = scale / grayScottSize;
const result = getMarchingSquaresResultFromReactionDiffusion(randomSeed, numberOfSeeds, grayScottSize, k, f, steps);
let segments = [];
for (let j = 0; j < grayScottSize; j++) {
for (let i = 0; i < grayScottSize; i++) {
const cell = result[j][i];
if (cell.nr != 0 && cell.nr != 15) {
let xOffset = i * cellSize;
let yOffset = j * cellSize;
let x1 = xOffset + cellSize * cell.p1.x + cellSize / 2;
let y1 = yOffset + cellSize * cell.p1.y + cellSize / 2;
let x2 = xOffset + cellSize * cell.p2.x + cellSize / 2;
let y2 = yOffset + cellSize * cell.p2.y + cellSize / 2;
segments.push(new LineSegment(x1, y1, x2, y2));
}
}
}
return segments;
}
function getReactionDiffusionPath(scale, randomSeed = 12345, numberOfSeeds = 150, grayScottSize = 250, k = 0.060, f = 0.06, steps = 2000) {
const cellSize = scale / grayScottSize;
const result = getMarchingSquaresResultFromReactionDiffusion(randomSeed, numberOfSeeds, grayScottSize, k, f, steps);
console.log("rendering marching squares");
const polygons = getPathsFromMarchingSquaresResult(result, grayScottSize, grayScottSize, cellSize);
return polygons;
}
function getPathsFromMarchingSquaresResult(result, width, height, cellSize) {
let polygons = [];
const visited = [];
for (let j = 0; j < height; j++) {
visited.push([]);
for (let i = 0; i < width; i++) {
visited[j].push(false);
}
}
// let debugCanvas: HTMLCanvasElement = document.createElement("canvas");
// debugCanvas.width = width;
// debugCanvas.height = height;
// document.getElementById("container").parentNode.appendChild(debugCanvas);
// let debugCtx = debugCanvas.getContext("2d");
const log = false;
for (let j = 0; j < height; j++) {
for (let i = 0; i < width; i++) {
if (!visited[j][i]) {
let nr = result[j][i].nr;
if (nr != 0 && nr != 15) {
let stepX = 0;
let stepY = 0;
let subSegment = [];
if (log)
console.log("new subsegment at " + (i + "," + j));
let curY = j + stepY;
let curX = i + stepX;
while (!visited[curY][curX]) {
let oldNr = nr;
nr = result[curY][curX].nr;
if (nr != 0 && nr != 15) {
let cellP1 = result[curY][curX].p1;
let cellP2 = result[curY][curX].p2;
let xOffset = (i + stepX) * cellSize;
let yOffset = (j + stepY) * cellSize;
let x1 = xOffset + cellSize * cellP1.x + cellSize / 2;
let y1 = yOffset + cellSize * cellP1.y + cellSize / 2;
let x2 = xOffset + cellSize * cellP2.x + cellSize / 2;
let y2 = yOffset + cellSize * cellP2.y + cellSize / 2;
if (oldNr == nr || (oldNr != nr && nr != 5 && nr != 10)) {
// if we wrapped around don't flag it as visited
visited[curY][curX] = true;
}
// debugCtx.fillStyle = `rgb(255, ${nr}, 255)`;
// debugCtx.fillRect(i + stepX, j + stepY, 1, 1);
if (subSegment.length == 0) {
subSegment.push({ x: x1, y: y1 });
}
switch (nr) {
case 4:
case 12:
case 13:
//right
// console.log(nr + " going right");
//if (i + stepX + 1 < width)
stepX++;
if (x1 < x2)
subSegment.push({ x: x2, y: y2 });
else
subSegment.push({ x: x1, y: y1 });
break;
case 5:
if (log)
console.log(nr + " saddle point at " + (i + stepX) + ", " + (j + stepY));
if (oldNr == 2 || oldNr == 6 || oldNr == 14) {
// came from the bottom -> to right
if (log)
console.log("came from bottom, go to right");
//if (i + stepX + 1 < width)
stepX++;
if (x1 < x2)
subSegment.push({ x: x2, y: y2 });
else
subSegment.push({ x: x1, y: y1 });
}
else {
// came from the top -> to left
if (log)
console.log("came from top, go to left");
//if (i + stepX - 1 >= 0)
stepX--;
if (x2 < x1)
subSegment.push({ x: x2, y: y2 });
else
subSegment.push({ x: x1, y: y1 });
}
break;
case 10:
if (log)
console.log(nr + " saddle point at " + (i + stepX) + ", " + (j + stepY));
if (oldNr == 1 || oldNr == 3 || oldNr == 7) {
// came from the right -> to right
if (log)
console.log("came from bottom, go up");
//if (j + stepY - 1 >= 0)
stepY--;
if (y2 < y1)
subSegment.push({ x: x2, y: y2 });
else
subSegment.push({ x: x1, y: y1 });
}
else {
// came from the left -> to bottom
if (log)
console.log("came from left, go down");
//if (j + stepY + 1 < height)
stepY++;
if (y1 < y2)
subSegment.push({ x: x2, y: y2 });
else
subSegment.push({ x: x1, y: y1 });
break;
}
break;
case 2:
case 6:
case 14:
//up
if (log)
console.log(nr + " going up");
//if (j + stepY - 1 >= 0)
stepY--;
if (y2 < y1)
subSegment.push({ x: x2, y: y2 });
else
subSegment.push({ x: x1, y: y1 });
break;
case 9:
case 11:
case 8:
//down
if (log)
console.log(nr + " going down");
//if (j + stepY + 1 < height)
stepY++;
if (y1 < y2)
subSegment.push({ x: x2, y: y2 });
else
subSegment.push({ x: x1, y: y1 });
break;
case 1:
case 3:
case 7:
// left
if (log)
console.log(nr + " going left");
//if (i + stepX - 1 >= 0)
stepX--;
if (x2 < x1)
subSegment.push({ x: x2, y: y2 });
else
subSegment.push({ x: x1, y: y1 });
break;
default:
}
}
else {
break;
}
curY = j + stepY;
curX = i + stepX;
if (curX < 0)
curX += width; // wrap around
if (curY < 0)
curY += height;
if (curX >= width)
curX -= width;
if (curY >= height)
curY -= height;
}
if (subSegment.length > 0) {
if (polygons.length > 0) {
if (subSegment.every((p) => inside(p, polygons[polygons.length - 1].segments[0]))) {
polygons[polygons.length - 1].segments.push(subSegment);
}
else {
polygons.push({
segments: [subSegment],
});
}
}
else
polygons.push({
segments: [subSegment],
});
}
}
}
}
}
return polygons;
}
function inside(point, vs) {
// ray-casting algorithm based on
// https://wrf.ecse.rpi.edu/Research/Short_Notes/pnpoly.html/pnpoly.html
var x = point.x, y = point.y;
var inside = false;
for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) {
var xi = vs[i].x, yi = vs[i].y;
var xj = vs[j].x, yj = vs[j].y;
var intersect = yi > y != yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;
if (intersect)
inside = !inside;
}
return inside;
}
const reactionDiffusionPaths = getReactionDiffusionPath(scale, randomSeed, numberOfInitialSeeds, grayScottSize, k, f, steps);
const turtle = new Turtle();
// The walk function will be called until it returns false.
const iterator = draw();
function walk(i) {
const val = iterator.next();
return !val.done;
}
function* draw() {
for (let j = tileOutsideArea ? -1 : 0; j < tileWidth + (tileOutsideArea ? 1 : 0); j++) {
for (let i = tileOutsideArea ? -1 : 0; i < tileHeight + (tileOutsideArea ? 1 : 0); i++) {
let offsetX = -originOffset + i * scale;
let offsetY = -originOffset + j * scale;
for (let polygon of reactionDiffusionPaths) {
turtle.penup();
for (let segment of polygon.segments) {
turtle.goto(offsetX + segment[0].x, offsetY + segment[0].y);
turtle.pendown();
for (let i = 1; i < segment.length; i++) {
//ctx.lineTo(offsetX + segment[i].x, offsetY + segment[i].y);
turtle.goto(offsetX + segment[i].x, offsetY + segment[i].y);
}
// close path
turtle.goto(offsetX + segment[0].x, offsetY + segment[0].y);
}
if (i >= 0 && j >= 0) {
yield;
}
}
}
}
}