Create slime_dish.js

pull/2/head
Andreas Gysin 2021-04-12 09:50:54 +02:00
rodzic 97f81eed11
commit ae467b2b45
1 zmienionych plików z 290 dodań i 0 usunięć

Wyświetl plik

@ -0,0 +1,290 @@
/**
[header]
@author zspotter
(IG @zzz_desu, TW @zspotter)
@title Slime Dish
@desc Low-res physarum slime mold simulation
🔍 Tap and hold to magnify.
With inspiration from:
- https://sagejenson.com/physarum
- https://uwe-repository.worktribe.com/output/980579
- http://www.tech-algorithm.com/articles/nearest-neighbor-image-scaling
*/
import * as v2 from '/src/modules/vec2.js'
import { map } from '/src/modules/num.js'
// Environment
const WIDTH = 400;
const HEIGHT = 400;
const NUM_AGENTS = 1500;
const DECAY = 0.9;
const MIN_CHEM = 0.0001;
// Agents
const SENS_ANGLE = 45 * Math.PI / 180;
const SENS_DIST = 9;
const AGT_SPEED = 1;
const AGT_ANGLE = 45 * Math.PI / 180;
const DEPOSIT = 1;
// Rendering
const TEXTURE = [
" ``^@",
" ..„v0",
];
const OOB = ' ';
// 0@0@0@0@0@0^v^v^v^v^v^„`„`„`„`„`„`.`.`.`.`.`.`. . . . . .
// @0@0@0@0@0@v^v^v^v^v^v`„`„`„`„`„`„`.`.`.`.`.`.`. . . . . .
// 0@0@0@0@0@0^v^v^v^v^v^„`„`„`„`„`„`.`.`.`.`.`.`. . . . . .
// @0@0@0@0@0@v^v^v^v^v^v`„`„`„`„`„`„`.`.`.`.`.`.`. . . . . .
export const settings = {
backgroundColor : 'black',
color : 'white',
fontSize: '12px',
}
export function boot(context, buffer, data) {
document.body.style.cursor = 'crosshair';
data.chem = new Float32Array(HEIGHT*WIDTH);
data.wip = new Float32Array(HEIGHT*WIDTH);
data.agents = [];
for (let agent = 0; agent < NUM_AGENTS; agent++) {
data.agents.push(new Agent(
// Random position
v2.mulN(v2.addN(v2.mulN(randCircle(), 0.5), 1), 0.5 * WIDTH),
// Random direction
v2.rot(v2.vec2(1, 0), Math.random()*2*Math.PI),
));
}
data.viewScale = { y: 100/context.metrics.aspect, x: 100 };
data.viewFocus = { y: 0.5, x: 0.5 };
}
export function pre(context, cursor, buffer, data) {
// Diffuse & decay
for (let row = 0; row < HEIGHT; row++) {
for (let col = 0; col < WIDTH; col++) {
let val = DECAY * blur(row, col, data.chem);
if (val < MIN_CHEM)
val = 0;
data.wip[row*HEIGHT+col] = val;
}
}
const swap = data.chem;
data.chem = data.wip;
data.wip = swap;
const { chem, agents, view } = data;
// Sense, rotate, and move
const isScattering = Math.sin(context.frame/150) > 0.8;
for (const agent of agents) {
agent.scatter = isScattering;
agent.react(chem);
}
// Deposit
for (const agent of agents) {
agent.deposit(chem);
}
// Update view params
updateView(cursor, context, data);
}
export function main(coord, context, cursor, buffer, data) {
const { viewFocus, viewScale } = data;
// A down and upscaling algorithm based on nearest-neighbor image scaling.
const offset = {
y: Math.floor(viewFocus.y * (HEIGHT - viewScale.y * context.rows)),
x: Math.floor(viewFocus.x * (WIDTH - viewScale.x * context.cols)),
};
// The "nearest neighbor"
const sampleFrom = {
y: offset.y + Math.floor(coord.y * viewScale.y),
x: offset.x + Math.floor(coord.x * viewScale.x),
};
// The next nearest-neighbor cell, which we look up to the border of
const sampleTo = {
y: offset.y + Math.floor((coord.y+1) * viewScale.y),
x: offset.x + Math.floor((coord.x+1) * viewScale.x),
};
if (!bounded(sampleFrom) || !bounded(sampleTo))
return OOB;
// When upscaling, sample W/H may be 0
const sampleH = Math.max(1, sampleTo.y - sampleFrom.y);
const sampleW = Math.max(1, sampleTo.x - sampleFrom.x);
// Combine all cells in [sampleFrom, sampleTo) into a single value.
// For this case, the value half way between the average and max works well.
let max = 0;
let sum = 0;
for (let x = sampleFrom.x; x < sampleFrom.x + sampleW; x++) {
for (let y = sampleFrom.y; y < sampleFrom.y + sampleH; y++) {
const v = data.chem[y*HEIGHT+x];
max = Math.max(max, v);
sum += v;
}
}
let val = sum / (sampleW * sampleH);
val = (val + max) / 2;
// Weight val so we get better distribution of textures
val = Math.pow(val, 1/3);
// Convert the cell value into a character from the texture map
const texRow = (coord.x+coord.y) % TEXTURE.length;
const texCol = Math.ceil(val * (TEXTURE[0].length-1));
const char = TEXTURE[texRow][texCol];
if (!char)
throw new Error(`Invalid char for ${val}`);
return char;
}
// import { drawInfo } from '/src/modules/drawbox.js'
// export function post(context, cursor, buffer) {
// drawInfo(context, cursor, buffer)
// }
function updateView(cursor, context, data) {
let targetScale;
if (cursor.pressed) {
// 1 display char = 1 grid cell
targetScale = {
y: 1 / context.metrics.aspect,
x: 1,
};
}
else if (context.rows / context.metrics.aspect < context.cols) {
// Fit whole grid on wide window
targetScale = {
y: 1.1*WIDTH / context.rows,
x: 1.1*WIDTH / context.rows * context.metrics.aspect,
};
}
else {
// Fit whole grid on tall window
targetScale = {
y: 1.1*WIDTH / context.cols / context.metrics.aspect,
x: 1.1*WIDTH / context.cols,
};
}
if (data.viewScale.y !== targetScale.y || data.viewScale.x !== targetScale.x) {
data.viewScale.y += 0.1 * (targetScale.y - data.viewScale.y);
data.viewScale.x += 0.1 * (targetScale.x - data.viewScale.x);
}
let targetFocus = !cursor.pressed
? { y: 0.5, x: 0.5 }
: { y: cursor.y / context.rows, x: cursor.x / context.cols };
if (data.viewFocus.y !== targetFocus.y || data.viewFocus.x !== targetFocus.x) {
data.viewFocus.y += 0.1 * (targetFocus.y - data.viewFocus.y);
data.viewFocus.x += 0.1 * (targetFocus.x - data.viewFocus.x);
}
}
// 0@0@0@0@0@0^v^v^v^v^v^„`„`„`„`„`„`.`.`.`.`.`.`. . . . . .
// @0@0@0@0@0@v^v^v^v^v^v`„`„`„`„`„`„`.`.`.`.`.`.`. . . . . .
// 0@0@0@0@0@0^v^v^v^v^v^„`„`„`„`„`„`.`.`.`.`.`.`. . . . . .
// @0@0@0@0@0@v^v^v^v^v^v`„`„`„`„`„`„`.`.`.`.`.`.`. . . . . .
class Agent {
constructor(pos, dir) {
this.pos = pos;
this.dir = dir;
this.scatter = false;
}
sense(m, chem) {
const senseVec = v2.mulN(v2.rot(this.dir, m * SENS_ANGLE), SENS_DIST);
const pos = v2.floor(v2.add(this.pos, senseVec));
if (!bounded(pos))
return -1;
const sensed = chem[pos.y*HEIGHT+pos.x];
if (this.scatter)
return 1 - sensed;
return sensed;
}
react(chem) {
// Sense
let forwardChem = this.sense(0, chem);
let leftChem = this.sense(-1, chem);
let rightChem = this.sense(1, chem);
// Rotate
let rotate = 0;
if (forwardChem > leftChem && forwardChem > rightChem) {
rotate = 0;
}
else if (forwardChem < leftChem && forwardChem < rightChem) {
if (Math.random() < 0.5) {
rotate = -AGT_ANGLE;
}
else {
rotate = AGT_ANGLE;
}
}
else if (leftChem < rightChem) {
rotate = AGT_ANGLE;
}
else if (rightChem < leftChem) {
rotate = -AGT_ANGLE;
}
else if (forwardChem < 0) {
// Turn around at edge
rotate = Math.PI/2;
}
this.dir = v2.rot(this.dir, rotate);
// Move
this.pos = v2.add(this.pos, v2.mulN(this.dir, AGT_SPEED));
}
deposit(chem) {
const {y, x} = v2.floor(this.pos);
const i = y*HEIGHT+x;
chem[i] = Math.min(1, chem[i] + DEPOSIT);
}
}
const R = Math.min(WIDTH, HEIGHT)/2;
function bounded(vec) {
return ((vec.x-R)**2 + (vec.y-R)**2 <= R**2);
}
function blur(row, col, data) {
let sum = 0;
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
sum += data[(row+dy)*HEIGHT + col + dx] ?? 0;
}
}
return sum / 9;
}
function randCircle() {
const r = Math.sqrt(Math.random());
const theta = Math.random() * 2 * Math.PI;
return {
x: r * Math.cos(theta),
y: r * Math.sin(theta)
};
}