Public
Edited
May 10, 2024
5 stars
Insert cell
Insert cell
Insert cell
Insert cell
config = ({
sensor_angle: Math.PI / 2,
sensor_distance: 5,
rotation_angle: Math.PI / 4,
step_distance: 1,
decay_rate: 0.05,
deposit_amount: 0.2,
blur_strength: 0.01,
})
Insert cell
renderable = {
const w = SIZE_CONST;
const h = SIZE_CONST;
const agentCount = 10_000;

const { device } = await gpu.device();

const sample = gpu.context(w, h, '2d');

sample.fillStyle = '#000';
sample.fillRect(0, 0, w, h);
sample.fillStyle = '#fff';
sample.beginPath();
sample.arc(w / 2, h / 2, w / 4, 0, util.TWO_PI);
sample.fill();

const uniforms = new Float32Array([0, ...Object.values(config)]);
const uniformBuffer = gpu.uniformBuffer(device, uniforms);

const agent = new Float32Array(
util.flatArr(agentCount, () => [
util.randi(w),
util.randi(h),
0, // padding
util.rand(util.TWO_PI),
]),
);

const agentBufferPing = gpu.storageBuffer(device, agent);
const agentBufferPong = gpu.storageBuffer(device, agent);

const trailCountsBuffer = gpu.storageBuffer(device, new Uint32Array(w * h));

const trailTexturePong = gpu.createTexture(device, {
width: sample.canvas.width,
height: sample.canvas.height,
usage: gpu.Usage.StorageTexture,
});

const trailTexturePing = gpu.canvasTexture(device, sample.canvas, {
usage: gpu.Usage.StorageTexture,
});

const bindGroupLayout = device.createBindGroupLayout({
label: 'bind group layout',
entries: [
// agents in
{
binding: 0,
visibility: GPUShaderStage.VERTEX | GPUShaderStage.COMPUTE,
buffer: { type: 'read-only-storage' },
},
// agents out
{
binding: 1,
visibility: GPUShaderStage.COMPUTE,
buffer: { type: 'storage' },
},
// trail counts
{
binding: 2,
visibility: GPUShaderStage.COMPUTE,
buffer: { type: 'storage' },
},
// trail texture blur
{
binding: 3,
visibility: GPUShaderStage.VERTEX | GPUShaderStage.COMPUTE,
texture: {},
},
// trail texture
{
binding: 4,
visibility: GPUShaderStage.COMPUTE,
storageTexture: {
format: 'rgba8unorm',
},
},
// uniforms!
{
binding: 5,
visibility: GPUShaderStage.FRAGMENT | GPUShaderStage.COMPUTE,
buffer: {},
},
],
});

const agentShader = `
const TWO_PI: f32 = 6.283185307179586;

struct Uniforms {
time: f32,
sensor_angle: f32,
sensor_distance: f32,
rotation_angle: f32,
step_distance: f32,
decay_rate: f32,
deposit_amount: f32,
blur_strength: f32,
}

struct Agent {
pos: vec2f,
pad: f32,
heading: f32
}

@group(0) @binding(0) var<storage> agents_in: array<Agent>;
@group(0) @binding(1) var<storage, read_write> agents_out: array<Agent>;
@group(0) @binding(2) var<storage, read_write> trail_counts: array<atomic<u32>>;
@group(0) @binding(3) var trails_in: texture_2d<f32>;
@group(0) @binding(5) var<uniform> uniforms: Uniforms;

fn grid_index_from_agent_pos(pos: vec2u) -> u32 {
return (pos.x % ${w}) + (pos.y % ${w}) * ${w};
}

// On generating random numbers, with help of y= [(a+x)sin(bx)] mod 1", W.J.J. Rey, 22nd European Meeting of Statisticians 1998
fn rand11(n: f32) -> f32 { return fract(sin(n) * 43758.5453123); }
fn rand22(n: vec2f) -> f32 { return fract(sin(dot(n, vec2f(12.9898, 4.1414))) * 43758.5453); }

fn shft(x: f32, size: f32) -> f32 {
return x + select(0.0, size, x < 0) + select(0.0, -size, x >= size);
}

fn next_agent_state(agent: Agent) -> Agent {
let p = agent.pos;
let a = agent.heading;

let sa = uniforms.sensor_angle;
let sd = uniforms.sensor_distance;
let ra = uniforms.rotation_angle;
let step_dist = uniforms.step_distance;

let xc = p.x + cos(a) * sd;
let yc = p.y + sin(a) * sd;
let xl = p.x + cos(a - sa) * sd;
let yl = p.y + sin(a - sa) * sd;
let xr = p.x + cos(a + sa) * sd;
let yr = p.y + sin(a + sa) * sd;

// good enough
let C = textureLoad(trails_in, vec2u(u32(xc % ${w}), u32(yc % ${h})), 0).r;
let L = textureLoad(trails_in, vec2u(u32(xl % ${w}), u32(yl % ${h})), 0).r;
let R = textureLoad(trails_in, vec2u(u32(xr % ${w}), u32(yr % ${h})), 0).r;

var d = 0.0;
if (C > L && C > R) {
d = 0.0;
} else if (C < L && C < R) {
d = select(-1.0, 1.0, rand11(uniforms.time) > 0.5);
} else if (L < R) {
d = 1.0;
} else if (R < L) {
d = -1.0;
}

let da = ra * d;
let next_angle = shft(a + da, TWO_PI);
let next_pos = vec2f(
shft(p.x + cos(next_angle) * step_dist, ${w}),
shft(p.y + sin(next_angle) * step_dist, ${h}),
);
return Agent(next_pos, 0, next_angle);
}

@compute
@workgroup_size(${w}, 1, 1)
fn cs(
@builtin(global_invocation_id) gid: vec3u,
@builtin(local_invocation_id) lid: vec3u,
) {
let agent_index = gid.x;
if (agent_index > arrayLength(&agents_in)) {
return;
}
let agent = agents_in[agent_index];

let next_agent = next_agent_state(agent);
agents_out[agent_index].pos = next_agent.pos;
agents_out[agent_index].heading = next_agent.heading;

// add to trail counts
let trail_index = grid_index_from_agent_pos(vec2u(u32(next_agent.pos.x), u32(next_agent.pos.y)));
atomicAdd(&trail_counts[trail_index], 1);
}
`;

// FIXME this is backwards - we should compose all assets first, then run agent compute

const trailCombineShader = `
struct Uniforms {
time: f32,
sensor_angle: f32,
sensor_distance: f32,
rotation_angle: f32,
step_distance: f32,
decay_rate: f32,
deposit_amount: f32,
blur_strength: f32,
}

struct Agent {
pos: vec2f,
pad: f32,
heading: f32
}

@group(0) @binding(2) var<storage, read_write> trail_counts: array<atomic<u32>>;
@group(0) @binding(3) var tex_in: texture_2d<f32>;
@group(0) @binding(4) var tex_out: texture_storage_2d<rgba8unorm, write>;
@group(0) @binding(5) var<uniform> uniforms: Uniforms;

// from https://threejs.org/examples/webgpu_compute_texture_pingpong.html
fn blur(image: texture_2d<f32>, uv: vec2i) -> vec4f {
var color = vec4f(0.0);
color += textureLoad(image, uv + vec2i(-1, 1), 0);
color += textureLoad(image, uv + vec2i(-1, -1), 0);
color += textureLoad(image, uv + vec2i( 0, 0), 0);
color += textureLoad(image, uv + vec2i( 1, -1), 0);
color += textureLoad(image, uv + vec2i( 1, 1), 0);
return color / 5.0;
}

@compute
@workgroup_size(${w}, 1, 1)
fn cs(
@builtin(global_invocation_id) gid: vec3u,
@builtin(local_invocation_id) lid: vec3u,
) {
let y = floor(f32(gid.x) / ${w}.0);
let x = f32(lid.x);
let index = gid.x;
let tex_pos = vec2u(u32(x), u32(y));
let prev = textureLoad(tex_in, tex_pos, 0);
let value = clamp(f32(atomicLoad(&trail_counts[index])) * uniforms.deposit_amount, 0.0, 1.0);
// should be doing "val * 1 - decay_rate" but this looks smoother
let next = vec3(clamp(prev.r + value - uniforms.decay_rate, 0.0, 1.0));

// blur for next pass (this prob isn't in the correct order)
var b = blur(tex_in, vec2i(i32(x), i32(y))) * uniforms.blur_strength;
textureStore(tex_out, tex_pos, vec4f(vec3f(b.r + next), 1.0));

// reset
atomicStore(&trail_counts[index], 0);
}
`;

const agentBindGroupPing = device.createBindGroup({
label: 'agent bind group ping',
layout: bindGroupLayout,
entries: [
{ binding: 0, resource: { buffer: agentBufferPing.buffer } },
{ binding: 1, resource: { buffer: agentBufferPong.buffer } },
{ binding: 2, resource: { buffer: trailCountsBuffer.buffer } },
{ binding: 3, resource: trailTexturePing.createView() },
{ binding: 4, resource: trailTexturePong.createView() },
{ binding: 5, resource: { buffer: uniformBuffer.buffer } },
],
});

const agentBindGroupPong = device.createBindGroup({
label: 'agent bind group pong',
layout: bindGroupLayout,
entries: [
{ binding: 0, resource: { buffer: agentBufferPong.buffer } },
{ binding: 1, resource: { buffer: agentBufferPing.buffer } },
{ binding: 2, resource: { buffer: trailCountsBuffer.buffer } },
{ binding: 3, resource: trailTexturePong.createView() },
{ binding: 4, resource: trailTexturePing.createView() },
{ binding: 5, resource: { buffer: uniformBuffer.buffer } },
],
});

const pipelineLayout = device.createPipelineLayout({
label: 'pipeline layout',
bindGroupLayouts: [bindGroupLayout],
});

const agentPipeline = device.createComputePipeline({
label: 'agent pipeline',
layout: pipelineLayout,
compute: {
module: device.createShaderModule({ code: agentShader }),
entryPoint: 'cs',
},
});

const trailCombinePipeline = device.createComputePipeline({
label: 'agent pipeline',
layout: pipelineLayout,
compute: {
module: device.createShaderModule({ code: trailCombineShader }),
entryPoint: 'cs',
},
});

const debugViewer = await renderTexture(device, trailTexturePing);

const debugAgents = await drawAgents(device, {
w,
h,
layout: pipelineLayout,
});

let step = 0;

function render() {
uniforms.set([performance.now()], 0);
uniformBuffer.write();

const encoder = device.createCommandEncoder();

const agentPass = encoder.beginComputePass();
agentPass.setPipeline(agentPipeline);
agentPass.setBindGroup(
0,
[agentBindGroupPing, agentBindGroupPong][step % 2],
);
agentPass.dispatchWorkgroups(h, 1, 1);
agentPass.end();

const trailCombinePass = encoder.beginComputePass();
trailCombinePass.setPipeline(trailCombinePipeline);
trailCombinePass.setBindGroup(
0,
[agentBindGroupPing, agentBindGroupPong][step % 2],
);
trailCombinePass.dispatchWorkgroups(h, 1, 1);
trailCombinePass.end();

step++;

debugViewer.render();
debugAgents.render([agentBindGroupPing, agentBindGroupPong][step % 2]);

device.queue.submit([encoder.finish()]);
}

render();
debugViewer.render();

const display = [debugViewer.context.canvas, debugAgents.context.canvas];
display.forEach(d => {
d.style.width = `${d.width * 2}px`
d.style.height = `${d.height * 2}px`
})
return { render, display };
}
Insert cell
async function renderTexture(
device /*: GPUDevice*/,
texture /*: GPUTexture*/,
{
bgColor,
layout,
} /*: { bgColor?: string; shader?: string; layout?: GPUPipelineLayout }*/ = {},
) {
const context = gpu.context(texture.width, texture.height);
// so we can "see" alpha and white-on-white
if (bgColor) {
context.canvas.style.backgroundColor = bgColor;
}

const format = gpu.format();
context.configure({ device, format, alphaMode: 'premultiplied' });

const module = device.createShaderModule({
code: `
struct VertexOut {
@builtin(position) position: vec4f,
@location(0) texcoord: vec2f,
}
@group(0) @binding(0) var tex_sampler: sampler;
@group(0) @binding(1) var tex: texture_2d<f32>;
@vertex
fn vs(
@builtin(vertex_index) vertexIndex : u32
) -> VertexOut {
let quad = array(
vec2f(0.0, 0.0),
vec2f(1.0, 0.0),
vec2f(0.0, 1.0),
vec2f(0.0, 1.0),
vec2f(1.0, 0.0),
vec2f(1.0, 1.0),
);
let texcoord = quad[vertexIndex];
return VertexOut(
vec4f((texcoord - 0.5) * 2, 0, 1),
texcoord
);
}
@fragment
fn fs(vs_out: VertexOut) -> @location(0) vec4f {
return textureSample(tex, tex_sampler, vs_out.texcoord);
}
`,
});

const sampler = gpu.sampler(device, {
minFilter: 'linear',
magFilter: 'linear',
mipmapFilter: 'linear',
});

const pipeline = device.createRenderPipeline({
layout: layout ?? 'auto',
vertex: {
module,
entryPoint: 'vs',
},
fragment: {
module,
entryPoint: 'fs',
targets: [{ format }],
},
});

const result = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: sampler },
{ binding: 1, resource: texture.createView() },
],
});

function render() {
const encoder = device.createCommandEncoder();

const pass = encoder.beginRenderPass({
colorAttachments: [
{
view: context.getCurrentTexture().createView(),
clearValue: [0, 0, 0, 1],
loadOp: 'clear',
storeOp: 'store',
},
],
});
pass.setPipeline(pipeline);
pass.setBindGroup(0, result);
pass.draw(6);
pass.end();

device.queue.submit([encoder.finish()]);
}

return { render, context: context };
}
Insert cell
function drawAgents(
device /*: GPUDevice*/,
{
w,
h,
layout = 'auto',
} /*: { w: number; h: number; layout?: GPUPipelineLayout | GPUAutoLayoutMode }*/,
) {
const context = gpu.context(SIZE_CONST, SIZE_CONST);

context.configure({
device,
format: gpu.format(),
alphaMode: 'premultiplied',
});

const module = device.createShaderModule({
code: `
const TWO_PI: f32 = 6.283185307179586;

struct Agent {
pos: vec2f,
pad: f32,
heading: f32,
}

struct VertexOut {
@builtin(position) position: vec4f,
@location(0) agent_pos: vec2f,
@location(1) agent_heading: f32,
@location(2) texcoord: vec2f,
@location(3) norm_index: f32,
@location(4) trail_color: vec4f,
}

@group(0) @binding(0) var<storage, read> agents: array<Agent>;
@group(0) @binding(3) var trails_tex: texture_2d<f32>;

fn grid_index_from_agent_pos(pos: vec2u) -> u32 {
return (pos.x % ${w}) + (pos.y % ${w}) * ${w};
}

@vertex
fn vs(
@builtin(instance_index) instance_index : u32,
@builtin(vertex_index) vertex_index : u32,
) -> VertexOut {
let q = array(
vec2f(0.0, 0.0),
vec2f(1.0, 0.0),
vec2f(0.0, 1.0),
vec2f(0.0, 1.0),
vec2f(1.0, 0.0),
vec2f(1.0, 1.0),
);
let agent = agents[instance_index];
let texcoord = agent.pos / vec2f(${w});
let agent_pos = (texcoord - 0.5) * 2.0;
let xy = (q[vertex_index] - 0.5) * 0.02 + agent_pos;

let c = textureLoad(trails_tex, vec2u(u32(agent.pos.x), u32(agent.pos.y)), 0);

return VertexOut(
vec4f(xy, 1, 1),
agent.pos,
agent.heading,
texcoord,
f32(instance_index) / f32(arrayLength(&agents)),
c
);
}

@fragment
fn fs(vs_out: VertexOut) -> @location(0) vec4f {
let h = vs_out.agent_heading / TWO_PI;
return vec4f(h, 1.0 - vs_out.trail_color.r, 1.0, 1.0) * 1.0;
}
`,
});

const pipeline = device.createRenderPipeline({
label: 'pipeline',
layout,
vertex: {
module,
entryPoint: 'vs',
},
fragment: {
module,
entryPoint: 'fs',
targets: [
{
format: gpu.format(),
// blend: gpu.Blending.Additive,
blend: gpu.Blending.SourceOver,
},
],
},
});

function render(bindGroup) {
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [
{
clearValue: [0, 0, 0, 1],
loadOp: 'clear',
storeOp: 'store',
// @ts-ignore
view: context.getCurrentTexture().createView(),
},
],
});
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup);
pass.draw(6, SIZE_CONST * SIZE_CONST);
pass.end();
device.queue.submit([encoder.finish()]);
}

return { render, context };
}
Insert cell
gpu = {
const context = (width = 512, height = 512, contextType = 'webgpu') => {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const context = canvas.getContext(contextType);
return context;
};
const format = () => navigator.gpu.getPreferredCanvasFormat();
const device = async () => {
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
const format = navigator.gpu.getPreferredCanvasFormat();
return { adapter, device, format };
};
const init = async (width = 512, height = 512) => {
// @ts-ignore
const ctx = context(width, height, 'webgpu') /* as GPUCanvasContext*/;
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
const format = navigator.gpu.getPreferredCanvasFormat();
ctx.configure({ device, format, alphaMode: 'premultiplied' });
return { context: ctx, adapter, device, format };
};
const sampler = (device /*: GPUDevice*/, options = {}) => {
return device.createSampler({
magFilter: 'linear',
minFilter: 'linear',
...options,
});
};

/*
interface BaseTextureOptions {
format?: GPUTextureFormat;
usage: number;
label?: string;
}

interface ImageTextureOptions {
width: number;
height: number;
flipY?: boolean;
}
*/
const createTexture = (
device /*: GPUDevice */,
{
width,
height,
format = 'rgba8unorm',
usage = GPUTextureUsage.COPY_DST |
GPUTextureUsage.STORAGE_BINDING |
GPUTextureUsage.TEXTURE_BINDING,
label,
} /*: BaseTextureOptions & ImageTextureOptions,*/
) /*: GPUTexture*/ => {
return device.createTexture({
...(label && { label }),
format,
size: [width, height],
usage,
});
};
const canvasTexture = (
device /*: GPUDevice*/,
canvas /*: HTMLCanvasElement*/,
{
format = 'rgba8unorm',
flipY = true,
usage = GPUTextureUsage.TEXTURE_BINDING |
GPUTextureUsage.COPY_DST |
GPUTextureUsage.RENDER_ATTACHMENT,
label,
} /*: Partial<BaseTextureOptions & ImageTextureOptions>*/ = {},
) /*: GPUTexture*/ => {
const texture = device.createTexture({
...(label && { label }),
format,
size: [canvas.width, canvas.height],
usage,
});
device.queue.copyExternalImageToTexture(
{ source: canvas, flipY },
{ texture },
[canvas.width, canvas.height],
);
return texture;
};
const DefaultUsage = {
STORAGE: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
UNIFORM: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
};

/*
interface WrappedBuffer {
write: (next?: any) => void;
buffer: GPUBuffer;
}
*/
const buffer = (
device /*: GPUDevice */,
values /*: any */,
{
usage,
offset = 0,
} /*: { usage: BaseTextureOptions['usage']; offset?: number } */,
) /*: WrappedBuffer*/ => {
const buffer = device.createBuffer({
size: values.byteLength,
usage,
});
device.queue.writeBuffer(buffer, offset, values);
return {
buffer,
write: (next = values) => device.queue.writeBuffer(buffer, offset, next),
};
};
const storageBuffer = (
device /*: GPUDevice*/,
values /*: any*/,
options /*: Partial<BaseTextureOptions>*/ = {},
) => {
return buffer(device, values, {
...options,
usage: options.usage ?? DefaultUsage.STORAGE,
});
};
const uniformBuffer = (
device /*: GPUDevice*/,
values /*: any*/,
options /*: Partial<BaseTextureOptions>*/ = {},
) => {
return buffer(device, values, {
...options,
usage: options.usage ?? DefaultUsage.UNIFORM,
});
};
// Shaders
// type ShaderInjections = { [key: string]: string | number };
// TODO compose shaders using a JS API
const injectShader = (shader /*: string*/, injections/*: ShaderInjections*/ = {}) => {
return Object.entries(injections).reduce((shader, [k, v]) => {
const regex = new RegExp('\\$\\{' + k + '\\}', 'g');
return shader.replace(regex, String(v));
}, shader);
};
const bindGroupEntry = (
binding /*: number*/,
resource /*: GPUSampler | GPUTexture | GPUBuffer | WrappedBuffer*/,
) => {
let r;
if (resource instanceof GPUSampler) r = resource;
else if (resource instanceof GPUTexture) r = resource.createView();
else if (resource instanceof GPUBuffer) r = { buffer: resource };
// specific to the util above
else if (resource.hasOwnProperty('buffer')) r = { buffer: resource.buffer };
else r = resource;
return { binding, resource: r };
};
// via https://webgpufundamentals.org/webgpu/lessons/webgpu-transparency.html
const Blending /*: { [name: string]: GPUBlendState }*/ = {
SourceOver: {
color: {
operation: 'add',
srcFactor: 'one',
dstFactor: 'one-minus-src-alpha',
},
alpha: {
operation: 'add',
srcFactor: 'one',
dstFactor: 'one-minus-src-alpha',
},
},
DestinationOver: {
color: {
operation: 'add',
srcFactor: 'one-minus-dst-alpha',
dstFactor: 'one',
},
alpha: {
operation: 'add',
srcFactor: 'one-minus-dst-alpha',
dstFactor: 'one',
},
},
Additive: {
color: {
operation: 'add',
srcFactor: 'one',
dstFactor: 'one',
},
alpha: {
operation: 'add',
srcFactor: 'one',
dstFactor: 'one',
},
},
SourceIn: {
color: {
operation: 'add',
srcFactor: 'dst-alpha',
dstFactor: 'zero',
},
alpha: {
operation: 'add',
srcFactor: 'dst-alpha',
dstFactor: 'zero',
},
},
DestinationIn: {
color: {
operation: 'add',
srcFactor: 'zero',
dstFactor: 'src-alpha',
},
alpha: {
operation: 'add',
srcFactor: 'zero',
dstFactor: 'src-alpha',
},
},
SourceOut: {
color: {
operation: 'add',
srcFactor: 'one-minus-dst-alpha',
dstFactor: 'zero',
},
alpha: {
operation: 'add',
srcFactor: 'one-minus-dst-alpha',
dstFactor: 'zero',
},
},
DestinationOut: {
color: {
operation: 'add',
srcFactor: 'zero',
dstFactor: 'one-minus-src-alpha',
},
alpha: {
operation: 'add',
srcFactor: 'zero',
dstFactor: 'one-minus-src-alpha',
},
},
SourceAtop: {
color: {
operation: 'add',
srcFactor: 'dst-alpha',
dstFactor: 'one-minus-src-alpha',
},
alpha: {
operation: 'add',
srcFactor: 'dst-alpha',
dstFactor: 'one-minus-src-alpha',
},
},
DestinationAtop: {
color: {
operation: 'add',
srcFactor: 'one-minus-dst-alpha',
dstFactor: 'src-alpha',
},
alpha: {
operation: 'add',
srcFactor: 'one-minus-dst-alpha',
dstFactor: 'src-alpha',
},
},
};
const gpu = {
device,
context,
init,
format,
sampler,
canvasTexture,
createTexture,
storageBuffer,
uniformBuffer,
bindGroupEntry,
shader: {
inject: injectShader,
},
Usage: {
StorageTexture:
GPUTextureUsage.COPY_SRC |
GPUTextureUsage.COPY_DST |
GPUTextureUsage.STORAGE_BINDING |
GPUTextureUsage.TEXTURE_BINDING |
GPUTextureUsage.RENDER_ATTACHMENT,
CopyTexture:
GPUTextureUsage.COPY_SRC |
GPUTextureUsage.COPY_DST |
GPUTextureUsage.TEXTURE_BINDING |
GPUTextureUsage.RENDER_ATTACHMENT,
},
Blending,
};
return gpu
}
Insert cell
util = {
const arr = (
size /*: number*/,
callback /*?: ((index: number) => any) | any*/,
) /*: any[]*/ => {
const arr = new Array(size);
if (typeof callback !== 'function') {
return arr.fill(callback);
}
return arr.fill(null).map((_, i) => callback(i));
};
const arr2d = (
m /*: number*/,
n /*: number*/,
callback /*: (i: number, j: number) => any*/,
) => {
return arr(m, (i /*: number*/) => arr(n, (j /*: number*/) => callback(i, j)));
};
const util = {
arr,
arr2d,
flatArr: (size /*: number*/, callback /*?: any*/) => arr(size, callback).flat(),
flip: (opt1 /*: any*/, opt2 /*: any*/, threshold = 0.5) => {
return Math.random() < threshold ? opt1 : opt2;
},
TWO_PI: 2 * Math.PI,
rand: (min = 1, max /*?: number*/) => {
if (max === undefined) {
return Math.random() * min;
}
return Math.random() * (max - min) + min;
},
// rand int
randi: (max /*: number*/) => Math.floor(Math.random() * max),
// random signed
rands: (width = 1) => (Math.random() - 0.5) * 2 * width,
// from karpathy:
randn: (mean /*: number*/, variance /*: number*/) => {
let V1, V2, S;
do {
const U1 = Math.random();
const U2 = Math.random();
V1 = 2 * U1 - 1;
V2 = 2 * U2 - 1;
S = V1 * V1 + V2 * V2;
} while (S > 1);
let X = Math.sqrt((-2 * Math.log(S)) / S) * V1;
X = mean + Math.sqrt(variance) * X;
return X;
},
zeros: (size /*: number*/) => arr(size, 0),
nearest: (value /*: number*/, step /*: number*/) => Math.floor(value / step) * step,
};

return util;
}
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more