Add simple tests & remove !
This commit is contained in:
parent
0735dd764f
commit
be0a49a11f
17 changed files with 1409 additions and 115 deletions
22
src/utils/format-number.test.ts
Normal file
22
src/utils/format-number.test.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { formatNumber } from './format-number';
|
||||
|
||||
describe('formatNumber', () => {
|
||||
it('renders integers without decimals', () => {
|
||||
expect(formatNumber(42)).toBe('42 ');
|
||||
});
|
||||
it('renders fractional values with two decimals', () => {
|
||||
expect(formatNumber(3.14159)).toBe('3.14 ');
|
||||
});
|
||||
it('renders thousands compactly', () => {
|
||||
expect(formatNumber(2500)).toBe('2.5 thousand ');
|
||||
});
|
||||
it('renders millions compactly', () => {
|
||||
expect(formatNumber(1_500_000)).toBe('1.5 million ');
|
||||
});
|
||||
it('appends the unit when provided', () => {
|
||||
expect(formatNumber(5, 'agents')).toBe('5 agents');
|
||||
expect(formatNumber(2_000_000, 'agents')).toBe('2.0 million agents');
|
||||
});
|
||||
});
|
||||
|
|
@ -13,75 +13,77 @@ export const generateNoise = ({
|
|||
height: number;
|
||||
}): GPUTextureView => {
|
||||
const cacheKey = `${width}x${height}`;
|
||||
if (!textureCache.has(cacheKey)) {
|
||||
const { buffer, vertex } = setUpFullScreenQuad(device);
|
||||
const vertexBuffer = buffer;
|
||||
|
||||
const pipeline = device.createRenderPipeline({
|
||||
layout: 'auto',
|
||||
vertex,
|
||||
fragment: {
|
||||
module: smartCompile(
|
||||
device,
|
||||
/* wgsl */ `
|
||||
fn random_with_seed(uv: vec2<f32>, seed: f32) -> f32 {
|
||||
return fract(sin(dot(uv, vec2(12.9898 + seed, 78.233 + seed)))* 43758.5453123 + seed);
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fragment(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
|
||||
return vec4(
|
||||
random_with_seed(uv, 0),
|
||||
random_with_seed(uv, 1),
|
||||
random_with_seed(uv, 2),
|
||||
random_with_seed(uv, 3),
|
||||
);
|
||||
}`
|
||||
),
|
||||
entryPoint: 'fragment',
|
||||
targets: [
|
||||
{
|
||||
format: 'rgba16float',
|
||||
},
|
||||
],
|
||||
},
|
||||
primitive: {
|
||||
topology: 'triangle-strip',
|
||||
},
|
||||
});
|
||||
|
||||
const colorTexture = device.createTexture({
|
||||
size: {
|
||||
width,
|
||||
height,
|
||||
depthOrArrayLayers: 1,
|
||||
},
|
||||
format: 'rgba16float',
|
||||
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT,
|
||||
});
|
||||
|
||||
const renderPassDescriptor: GPURenderPassDescriptor = {
|
||||
colorAttachments: [
|
||||
{
|
||||
view: colorTexture.createView(),
|
||||
clearValue: { r: 1.0, g: 1.0, b: 1.0, a: 1.0 },
|
||||
loadOp: 'clear',
|
||||
storeOp: 'store',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const commandEncoder = device.createCommandEncoder();
|
||||
|
||||
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
|
||||
passEncoder.setPipeline(pipeline);
|
||||
passEncoder.setVertexBuffer(0, vertexBuffer);
|
||||
passEncoder.draw(4, 1);
|
||||
passEncoder.end();
|
||||
|
||||
device.queue.submit([commandEncoder.finish()]);
|
||||
textureCache.set(cacheKey, colorTexture);
|
||||
const cached = textureCache.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached.createView();
|
||||
}
|
||||
|
||||
return textureCache.get(cacheKey)!.createView();
|
||||
const { buffer, vertex } = setUpFullScreenQuad(device);
|
||||
const vertexBuffer = buffer;
|
||||
|
||||
const pipeline = device.createRenderPipeline({
|
||||
layout: 'auto',
|
||||
vertex,
|
||||
fragment: {
|
||||
module: smartCompile(
|
||||
device,
|
||||
/* wgsl */ `
|
||||
fn random_with_seed(uv: vec2<f32>, seed: f32) -> f32 {
|
||||
return fract(sin(dot(uv, vec2(12.9898 + seed, 78.233 + seed)))* 43758.5453123 + seed);
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fragment(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
|
||||
return vec4(
|
||||
random_with_seed(uv, 0),
|
||||
random_with_seed(uv, 1),
|
||||
random_with_seed(uv, 2),
|
||||
random_with_seed(uv, 3),
|
||||
);
|
||||
}`
|
||||
),
|
||||
entryPoint: 'fragment',
|
||||
targets: [
|
||||
{
|
||||
format: 'rgba16float',
|
||||
},
|
||||
],
|
||||
},
|
||||
primitive: {
|
||||
topology: 'triangle-strip',
|
||||
},
|
||||
});
|
||||
|
||||
const colorTexture = device.createTexture({
|
||||
size: {
|
||||
width,
|
||||
height,
|
||||
depthOrArrayLayers: 1,
|
||||
},
|
||||
format: 'rgba16float',
|
||||
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT,
|
||||
});
|
||||
|
||||
const renderPassDescriptor: GPURenderPassDescriptor = {
|
||||
colorAttachments: [
|
||||
{
|
||||
view: colorTexture.createView(),
|
||||
clearValue: { r: 1.0, g: 1.0, b: 1.0, a: 1.0 },
|
||||
loadOp: 'clear',
|
||||
storeOp: 'store',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const commandEncoder = device.createCommandEncoder();
|
||||
|
||||
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
|
||||
passEncoder.setPipeline(pipeline);
|
||||
passEncoder.setVertexBuffer(0, vertexBuffer);
|
||||
passEncoder.draw(4, 1);
|
||||
passEncoder.end();
|
||||
|
||||
device.queue.submit([commandEncoder.finish()]);
|
||||
textureCache.set(cacheKey, colorTexture);
|
||||
return colorTexture.createView();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,49 +3,38 @@ import { vec2 } from 'gl-matrix';
|
|||
import { CopyPipeline } from '../../pipelines/copy/copy-pipeline';
|
||||
|
||||
export class ResizableTexture {
|
||||
private texture!: GPUTexture;
|
||||
private textureView!: GPUTextureView;
|
||||
private texture: GPUTexture;
|
||||
private textureView: GPUTextureView;
|
||||
private size: vec2;
|
||||
private readonly copyPipeline: CopyPipeline;
|
||||
private size: vec2 | null = null;
|
||||
|
||||
public constructor(
|
||||
private readonly device: GPUDevice,
|
||||
size: vec2
|
||||
) {
|
||||
this.copyPipeline = new CopyPipeline(this.device);
|
||||
this.resize(size);
|
||||
this.size = size;
|
||||
this.texture = this.createTexture(size);
|
||||
this.textureView = this.texture.createView();
|
||||
}
|
||||
|
||||
public resize(size: vec2): void {
|
||||
if (this.size !== null && vec2.equals(this.size, size)) {
|
||||
if (vec2.equals(this.size, size)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newTexture = this.device.createTexture({
|
||||
format: 'rgba16float',
|
||||
size: {
|
||||
width: size[0],
|
||||
height: size[1],
|
||||
},
|
||||
usage:
|
||||
GPUTextureUsage.STORAGE_BINDING |
|
||||
GPUTextureUsage.TEXTURE_BINDING |
|
||||
GPUTextureUsage.RENDER_ATTACHMENT,
|
||||
});
|
||||
|
||||
const newTexture = this.createTexture(size);
|
||||
const newTextureView = newTexture.createView();
|
||||
|
||||
if (this.size) {
|
||||
const commandEncoder = this.device.createCommandEncoder();
|
||||
this.copyPipeline.execute(
|
||||
commandEncoder,
|
||||
this.textureView,
|
||||
newTextureView,
|
||||
vec2.div(vec2.create(), this.size, size)
|
||||
);
|
||||
this.device.queue.submit([commandEncoder.finish()]);
|
||||
this.texture.destroy();
|
||||
}
|
||||
const commandEncoder = this.device.createCommandEncoder();
|
||||
this.copyPipeline.execute(
|
||||
commandEncoder,
|
||||
this.textureView,
|
||||
newTextureView,
|
||||
vec2.div(vec2.create(), this.size, size)
|
||||
);
|
||||
this.device.queue.submit([commandEncoder.finish()]);
|
||||
this.texture.destroy();
|
||||
|
||||
this.size = size;
|
||||
this.texture = newTexture;
|
||||
|
|
@ -60,4 +49,15 @@ export class ResizableTexture {
|
|||
this.texture.destroy();
|
||||
this.copyPipeline.destroy();
|
||||
}
|
||||
|
||||
private createTexture(size: vec2): GPUTexture {
|
||||
return this.device.createTexture({
|
||||
format: 'rgba16float',
|
||||
size: { width: size[0], height: size[1] },
|
||||
usage:
|
||||
GPUTextureUsage.STORAGE_BINDING |
|
||||
GPUTextureUsage.TEXTURE_BINDING |
|
||||
GPUTextureUsage.RENDER_ATTACHMENT,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
42
src/utils/hsl.test.ts
Normal file
42
src/utils/hsl.test.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { hsl } from './hsl';
|
||||
|
||||
describe('hsl', () => {
|
||||
it('produces pure red at hue 0', () => {
|
||||
const [r, g, b] = hsl(0, 100, 50);
|
||||
expect(r).toBeCloseTo(1);
|
||||
expect(g).toBeCloseTo(0);
|
||||
expect(b).toBeCloseTo(0);
|
||||
});
|
||||
it('produces pure green at hue 120', () => {
|
||||
const [r, g, b] = hsl(120, 100, 50);
|
||||
expect(r).toBeCloseTo(0);
|
||||
expect(g).toBeCloseTo(1);
|
||||
expect(b).toBeCloseTo(0);
|
||||
});
|
||||
it('produces pure blue at hue 240', () => {
|
||||
const [r, g, b] = hsl(240, 100, 50);
|
||||
expect(r).toBeCloseTo(0);
|
||||
expect(g).toBeCloseTo(0);
|
||||
expect(b).toBeCloseTo(1);
|
||||
});
|
||||
it('produces gray at saturation 0', () => {
|
||||
const [r, g, b] = hsl(180, 0, 50);
|
||||
expect(r).toBeCloseTo(0.5);
|
||||
expect(g).toBeCloseTo(0.5);
|
||||
expect(b).toBeCloseTo(0.5);
|
||||
});
|
||||
it('produces black at lightness 0', () => {
|
||||
const [r, g, b] = hsl(0, 100, 0);
|
||||
expect(r).toBe(0);
|
||||
expect(g).toBe(0);
|
||||
expect(b).toBe(0);
|
||||
});
|
||||
it('produces white at lightness 100', () => {
|
||||
const [r, g, b] = hsl(0, 100, 100);
|
||||
expect(r).toBeCloseTo(1);
|
||||
expect(g).toBeCloseTo(1);
|
||||
expect(b).toBeCloseTo(1);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
export function last<T>(a: Array<T>): T | null {
|
||||
return a.length > 0 ? a[a.length - 1] : null;
|
||||
}
|
||||
63
src/utils/math.test.ts
Normal file
63
src/utils/math.test.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { clamp, clamp01 } from './clamp';
|
||||
import { exponentialDecay } from './exponential-decay';
|
||||
import { mix } from './mix';
|
||||
|
||||
describe('clamp', () => {
|
||||
it('returns value when within bounds', () => {
|
||||
expect(clamp(5, 0, 10)).toBe(5);
|
||||
});
|
||||
it('clamps below to lower bound', () => {
|
||||
expect(clamp(-3, 0, 10)).toBe(0);
|
||||
});
|
||||
it('clamps above to upper bound', () => {
|
||||
expect(clamp(42, 0, 10)).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clamp01', () => {
|
||||
it('passes through values in [0, 1]', () => {
|
||||
expect(clamp01(0.25)).toBe(0.25);
|
||||
});
|
||||
it('clamps negatives to 0', () => {
|
||||
expect(clamp01(-1)).toBe(0);
|
||||
});
|
||||
it('clamps above 1 to 1', () => {
|
||||
expect(clamp01(2)).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mix', () => {
|
||||
it('returns from at q=0', () => {
|
||||
expect(mix(10, 20, 0)).toBe(10);
|
||||
});
|
||||
it('returns to at q=1', () => {
|
||||
expect(mix(10, 20, 1)).toBe(20);
|
||||
});
|
||||
it('interpolates at q=0.5', () => {
|
||||
expect(mix(10, 20, 0.5)).toBe(15);
|
||||
});
|
||||
it('extrapolates outside [0, 1]', () => {
|
||||
expect(mix(0, 10, 2)).toBe(20);
|
||||
expect(mix(0, 10, -1)).toBe(-10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('exponentialDecay', () => {
|
||||
it('returns nextValue when bias is 1', () => {
|
||||
expect(exponentialDecay({ accumulator: 0, nextValue: 10, biasOfNextValue: 1 })).toBe(
|
||||
10
|
||||
);
|
||||
});
|
||||
it('returns accumulator when bias is 0', () => {
|
||||
expect(exponentialDecay({ accumulator: 5, nextValue: 10, biasOfNextValue: 0 })).toBe(
|
||||
5
|
||||
);
|
||||
});
|
||||
it('blends with given bias', () => {
|
||||
expect(
|
||||
exponentialDecay({ accumulator: 0, nextValue: 10, biasOfNextValue: 0.25 })
|
||||
).toBe(2.5);
|
||||
});
|
||||
});
|
||||
41
src/utils/random.test.ts
Normal file
41
src/utils/random.test.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { Random } from './random';
|
||||
|
||||
describe('Random', () => {
|
||||
beforeEach(() => {
|
||||
Random.seed = 42;
|
||||
});
|
||||
|
||||
it('produces values in [0, 1)', () => {
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const v = Random.getRandom();
|
||||
expect(v).toBeGreaterThanOrEqual(0);
|
||||
expect(v).toBeLessThan(1);
|
||||
}
|
||||
});
|
||||
|
||||
it('is deterministic for the same seed', () => {
|
||||
Random.seed = 42;
|
||||
const a = Array.from({ length: 8 }, () => Random.getRandom());
|
||||
Random.seed = 42;
|
||||
const b = Array.from({ length: 8 }, () => Random.getRandom());
|
||||
expect(a).toEqual(b);
|
||||
});
|
||||
|
||||
it('produces different sequences for different seeds', () => {
|
||||
Random.seed = 1;
|
||||
const a = Array.from({ length: 4 }, () => Random.getRandom());
|
||||
Random.seed = 2;
|
||||
const b = Array.from({ length: 4 }, () => Random.getRandom());
|
||||
expect(a).not.toEqual(b);
|
||||
});
|
||||
|
||||
it('randomBetween stays within [from, to)', () => {
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const v = Random.randomBetween(-10, 10);
|
||||
expect(v).toBeGreaterThanOrEqual(-10);
|
||||
expect(v).toBeLessThan(10);
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue