diff --git a/README.md b/README.md index 2242fad..132fee0 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ ## todo - add info page -- generate starting shape on the gpu -- add cancer -- graceful error messages when no support - settings page +- shareable settings +- query max agent count + +- graceful error messages when no support diff --git a/src/game-loop/game-loop-settings.ts b/src/game-loop/game-loop-settings.ts index b40b172..6df1e95 100644 --- a/src/game-loop/game-loop-settings.ts +++ b/src/game-loop/game-loop-settings.ts @@ -6,4 +6,6 @@ export interface GameLoopSettings { aggressionFactor: number; nextGenerationSpawnRadius: number; nextGenerationSpawnInterval: number; + + startColorHue: number; } diff --git a/src/game-loop/game-loop.ts b/src/game-loop/game-loop.ts index 10aae19..5127616 100644 --- a/src/game-loop/game-loop.ts +++ b/src/game-loop/game-loop.ts @@ -1,5 +1,4 @@ import { AgentGenerationPipeline } from '../pipelines/agents/agent-generation/agent-generation-pipeline'; -import { GenerationCounts } from '../pipelines/agents/agent-generation/generation-counts'; import { AgentPipeline } from '../pipelines/agents/agent-pipeline'; import { BrushPipeline } from '../pipelines/brush/brush-pipeline'; import { CommonState } from '../pipelines/common-state/common-state'; @@ -11,6 +10,7 @@ import { DeltaTimeCalculator } from '../utils/delta-time-calculator'; import { initializeContext } from '../utils/graphics/initialize-context'; import { ResizableTexture } from '../utils/graphics/resizable-texture'; import { sleep } from '../utils/sleep'; +import { GamePresentation } from './game-presentation'; import { GameRules } from './game-rules'; import { vec2 } from 'gl-matrix'; @@ -27,8 +27,6 @@ export default class GameLoop { private readonly brushPipeline: BrushPipeline; private readonly diffusionPipeline: DiffusionPipeline; - private readonly gameRules = new GameRules(performance.now() / 1000); - private hasFinished = false; private readonly hasFinishedPromise: Promise = new Promise( (resolve) => (this.resolveHasFinished = resolve) @@ -40,7 +38,8 @@ export default class GameLoop { public constructor( private readonly canvas: HTMLCanvasElement, private readonly device: GPUDevice, - private readonly deltaTimeCalculator: DeltaTimeCalculator + private readonly deltaTimeCalculator: DeltaTimeCalculator, + private readonly gameRules: GameRules ) { const context = initializeContext({ device, canvas }); @@ -131,10 +130,19 @@ export default class GameLoop { return; } + const accentColor = GamePresentation.getGenerationColor( + this.gameRules.nextGenerationId - 1 + ); + document.documentElement.style.setProperty( + '--accent-color', + `rgb(${accentColor.map((v) => v * 255).join(',')})` + ); + const deltaTime = this.deltaTimeCalculator.calculateDeltaTimeInSeconds(time); time *= settings.renderSpeed; const timeInSeconds = time / 1000; + const spawnAction = this.gameRules.getSpawnAction(timeInSeconds, this.canvasSize); [ this.commonState, @@ -156,20 +164,28 @@ export default class GameLoop { nextGenerationId: this.gameRules.nextGenerationId, deltaTime, canvasSize: this.canvasSize, + brushColor: GamePresentation.getGenerationColor( + this.gameRules.nextGenerationId - 1 + ), + evenGenerationColor: GamePresentation.getGenerationColor( + this.gameRules.nextGenerationId % 2 == 0 + ? this.gameRules.nextGenerationId + : this.gameRules.nextGenerationId - 1 + ), + oddGenerationColor: GamePresentation.getGenerationColor( + this.gameRules.nextGenerationId % 2 == 1 + ? this.gameRules.nextGenerationId + : this.gameRules.nextGenerationId - 1 + ), ...settings, + center: spawnAction.position, + radius: spawnAction.radius, }) ); for (let i = 0; i < settings.renderSpeed; i++) { const commandEncoder = this.device.createCommandEncoder(); - const spawnAction = this.gameRules.getSpawnAction(timeInSeconds, this.canvasSize); - this.agentGenerationPipeline.spawnNextGeneration( - spawnAction.position, - spawnAction.radius, - spawnAction.generation - ); - this.copyPipeline.execute( commandEncoder, this.trailMapA.getTextureView(), diff --git a/src/game-loop/game-presentation.ts b/src/game-loop/game-presentation.ts index 8aade9c..5683b2a 100644 --- a/src/game-loop/game-presentation.ts +++ b/src/game-loop/game-presentation.ts @@ -1,11 +1,22 @@ +import { settings } from '../settings'; import { hsl } from '../utils/colors/hsl'; -import { hash } from '../utils/hash'; +import { last } from '../utils/last'; +import { Random } from '../utils/random'; import { vec3 } from 'gl-matrix'; +const hues = [settings.startColorHue]; + +for (let i = 0; i < 100; i++) { + hues.push((last(hues) + Random.randomBetween(90, 240)) % 360); +} + +const colors = hues.map((hue) => + hsl(hue, Random.randomBetween(80, 90), Random.randomBetween(20, 30)) +); + export class GamePresentation { - public getGenerationColor(generation: number): vec3 { - const hue = Math.round(hash(generation) * 360); - return hsl(hue, 100, 50); + public static getGenerationColor(generation: number): vec3 { + return colors[generation % colors.length]; } } diff --git a/src/index.scss b/src/index.scss index 3c3aae4..1eb74b0 100644 --- a/src/index.scss +++ b/src/index.scss @@ -8,7 +8,6 @@ margin: 0; padding: 0; box-sizing: border-box; - color: var(--very-light-text-color); @media (prefers-reduced-motion) { transition: none !important; @@ -78,7 +77,7 @@ html { } > aside { - @include blurred-background(#777); + @include blurred-background(#fff); position: absolute; top: 50%; @@ -97,19 +96,19 @@ html { margin: var(--small-margin); > button.info { - @include image-button(url('../assets/icons/info.svg')); + @include image-button(url('../assets/icons/info.svg'), var(--accent-color)); } > button.maximize-full-screen { - @include image-button(url('../assets/icons/maximize.svg')); + @include image-button(url('../assets/icons/maximize.svg'), var(--accent-color)); } > button.minimize-full-screen { - @include image-button(url('../assets/icons/minimize.svg')); + @include image-button(url('../assets/icons/minimize.svg'), var(--accent-color)); } > button.restart { - @include image-button(url('../assets/icons/restart.svg')); + @include image-button(url('../assets/icons/restart.svg'), var(--accent-color)); } } diff --git a/src/index.ts b/src/index.ts index bb3c970..3a24d06 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ import '../assets/icons/info.svg'; import GameLoop from './game-loop/game-loop'; +import { GameRules } from './game-loop/game-rules'; import './index.scss'; import { FullScreenHandler } from './page/full-screen-handler'; import { InfoPageHandler } from './page/info-page-handler'; @@ -49,7 +50,7 @@ const main = async () => { let shouldStop = false; let game: GameLoop | null = null; - ErrorHandler.addOnErrorListener((error, metadata) => { + ErrorHandler.addOnErrorListener((error, _metadata) => { elements.errorContainer.innerHTML += `
${error.message}
     `;
@@ -73,7 +74,9 @@ const main = async () => {
     elements.restartButton.addEventListener('click', () => game?.destroy());
 
     const deltaTimeCalculator = new DeltaTimeCalculator();
+    const gameRules = new GameRules(performance.now() / 1000);
 
+    console.log(gameRules.nextGenerationId);
     const updateCounters = () => {
       elements.counters.innerHTML = `FPS: ${deltaTimeCalculator.fps.toFixed(2)}
 current gen: ${game?.aliveAgentCounts.currentGenerationCount ?? 0}
@@ -83,7 +86,7 @@ next gen: ${game?.aliveAgentCounts.nextGenerationCount ?? 0}`;
     updateCounters();
 
     while (!shouldStop) {
-      game = new GameLoop(elements.canvas, gpu, deltaTimeCalculator);
+      game = new GameLoop(elements.canvas, gpu, deltaTimeCalculator, gameRules);
       await game.start();
     }
   } catch (e) {
diff --git a/src/pipelines/agents/agent-generation/agent-generation-pipeline.ts b/src/pipelines/agents/agent-generation/agent-generation-pipeline.ts
index 4d193cf..ca2d0ad 100644
--- a/src/pipelines/agents/agent-generation/agent-generation-pipeline.ts
+++ b/src/pipelines/agents/agent-generation/agent-generation-pipeline.ts
@@ -1,15 +1,12 @@
 import random from '../../../utils/graphics/random.wgsl';
 import { smartCompile } from '../../../utils/graphics/smart-compile';
 import { CommonState } from '../../common-state/common-state';
-import { AGENT_SIZE_IN_BYTES, Agent } from './agent';
+import { AGENT_SIZE_IN_BYTES } from './agent';
 import countingShader from './agent-counting.wgsl';
 import firstGenerationShader from './agent-first-generation.wgsl';
-import agentGenerationShader from './agent-generation.wgsl';
 import agentSchema from './agent-schema.wgsl';
 import { GenerationCounts } from './generation-counts';
 
-import { vec2 } from 'gl-matrix';
-
 export class AgentGenerationPipeline {
   private static readonly WORKGROUP_SIZE = 64;
   private static readonly UNIFORM_COUNT = 4;
@@ -20,7 +17,6 @@ export class AgentGenerationPipeline {
   private readonly bindGroup: GPUBindGroup;
 
   private readonly firstGenerationPipeline: GPUComputePipeline;
-  private readonly nextGenerationPipeline: GPUComputePipeline;
   private readonly countingPipeline: GPUComputePipeline;
 
   public readonly agentsBuffer: GPUBuffer;
@@ -122,22 +118,6 @@ export class AgentGenerationPipeline {
       },
     });
 
-    this.nextGenerationPipeline = device.createComputePipeline({
-      layout: device.createPipelineLayout({
-        bindGroupLayouts: [commonState.bindGroupLayout, this.bindGroupLayout],
-      }),
-      compute: {
-        module: smartCompile(
-          device,
-          CommonState.shaderCode,
-          random,
-          agentSchema,
-          agentGenerationShader
-        ),
-        entryPoint: 'main',
-      },
-    });
-
     this.countingPipeline = device.createComputePipeline({
       layout: device.createPipelineLayout({
         bindGroupLayouts: [commonState.bindGroupLayout, this.bindGroupLayout],
@@ -170,27 +150,6 @@ export class AgentGenerationPipeline {
     this.device.queue.submit([commandEncoder.finish()]);
   }
 
-  public spawnNextGeneration(center: vec2, radius: number, generationId: number): void {
-    this.device.queue.writeBuffer(
-      this.uniforms,
-      0,
-      new Float32Array([...center, radius, generationId])
-    );
-
-    const commandEncoder = this.device.createCommandEncoder();
-
-    const passEncoder = commandEncoder.beginComputePass();
-    this.commonState.execute(passEncoder);
-    passEncoder.setPipeline(this.nextGenerationPipeline);
-    passEncoder.setBindGroup(1, this.bindGroup);
-    passEncoder.dispatchWorkgroups(
-      Math.ceil(this.agentCount / AgentGenerationPipeline.WORKGROUP_SIZE)
-    );
-    passEncoder.end();
-
-    this.device.queue.submit([commandEncoder.finish()]);
-  }
-
   public async countAgents(): Promise {
     this.device.queue.writeBuffer(this.countersBuffer, 0, new Int32Array([0, 0]));
 
diff --git a/src/pipelines/agents/agent-generation/agent-generation.wgsl b/src/pipelines/agents/agent-generation/agent-generation.wgsl
deleted file mode 100644
index e3c6069..0000000
--- a/src/pipelines/agents/agent-generation/agent-generation.wgsl
+++ /dev/null
@@ -1,20 +0,0 @@
-struct Settings {
-  center: vec2,
-  radius: f32,
-  nextGenerationId: f32,
-};
-
-@group(1) @binding(0) var settings: Settings;
-
-@compute @workgroup_size(64)
-fn main(@builtin(global_invocation_id) global_id: vec3) {
-  let id = global_id.x;
-
-  if id >= arrayLength(&agents) {
-    return;
-  }
-
-  if length(settings.center - agents[id].position) < settings.radius {
-    agents[id].generation = settings.nextGenerationId;
-  }
-}
diff --git a/src/pipelines/agents/agent-pipeline.ts b/src/pipelines/agents/agent-pipeline.ts
index 82e7675..b74f720 100644
--- a/src/pipelines/agents/agent-pipeline.ts
+++ b/src/pipelines/agents/agent-pipeline.ts
@@ -1,19 +1,20 @@
 import random from '../../utils/graphics/random.wgsl';
 import { smartCompile } from '../../utils/graphics/smart-compile';
 import { CommonState } from '../common-state/common-state';
-import { AGENT_SIZE_IN_BYTES, Agent } from './agent-generation/agent';
+import { AGENT_SIZE_IN_BYTES } from './agent-generation/agent';
 import agentSchme from './agent-generation/agent-schema.wgsl';
 import { AgentSettings } from './agent-settings';
 import shader from './agent.wgsl';
 
+import { vec2 } from 'gl-matrix';
+
 export class AgentPipeline {
   private static readonly WORKGROUP_SIZE = 64;
-  private static readonly UNIFORM_COUNT = 8;
+  private static readonly UNIFORM_COUNT = 16;
 
   private readonly bindGroupLayout: GPUBindGroupLayout;
   private readonly pipeline: GPUComputePipeline;
   private readonly uniforms: GPUBuffer;
-
   private bindGroup?: GPUBindGroup;
   private previousTrailMapIn?: GPUTextureView;
   private previousTrailMapOut?: GPUTextureView;
@@ -50,10 +51,18 @@ export class AgentPipeline {
     evenGenerationAggression,
     oddGenerationAggression,
     nextGenerationId,
+    center,
+    radius,
+    turnWhenGoingInTheRightDirection,
+    turnWhenLost,
+    individualTrailWeight,
+    deinfectionProbability,
   }: AgentSettings & {
     evenGenerationAggression: number;
     oddGenerationAggression: number;
     nextGenerationId: number;
+    center: vec2;
+    radius: number;
   }) {
     this.device.queue.writeBuffer(
       this.uniforms,
@@ -67,6 +76,12 @@ export class AgentPipeline {
         evenGenerationAggression,
         oddGenerationAggression,
         nextGenerationId,
+        ...center,
+        radius,
+        turnWhenGoingInTheRightDirection,
+        turnWhenLost,
+        individualTrailWeight,
+        deinfectionProbability,
       ])
     );
   }
diff --git a/src/pipelines/agents/agent-settings.ts b/src/pipelines/agents/agent-settings.ts
index aa309fe..d72fa43 100644
--- a/src/pipelines/agents/agent-settings.ts
+++ b/src/pipelines/agents/agent-settings.ts
@@ -4,4 +4,8 @@ export interface AgentSettings {
   turnSpeed: number;
   sensorOffsetAngle: number;
   sensorOffsetDistance: number;
+  turnWhenGoingInTheRightDirection: number;
+  turnWhenLost: number;
+  individualTrailWeight: number;
+  deinfectionProbability: number;
 }
diff --git a/src/pipelines/agents/agent.wgsl b/src/pipelines/agents/agent.wgsl
index bf7291e..21d7c9e 100644
--- a/src/pipelines/agents/agent.wgsl
+++ b/src/pipelines/agents/agent.wgsl
@@ -1,15 +1,32 @@
 struct Settings {
   brushTrailWeight: f32,
   moveRate: f32,
+
   turnRate: f32,
   sensorAngle: f32,
+
   sensorOffset: f32,
   evenGenerationAggression: f32,
+
   oddGenerationAggression: f32,
-  nextGenerationId: f32
+  nextGenerationId: f32,
+
+  center: vec2,
+  radius: f32,
+
+  turnWhenGoingInTheRightDirection: f32,
+  turnWhenLost: f32,
+  individualTrailWeight: f32,
+  deinfectionProbability: f32
 };
 
+
 @group(1) @binding(0) var settings: Settings;
+
+// even generation's trail -> red channel
+// odd generation's trail -> green channel
+// unused -> blue channel
+// brush -> alpha channel
 @group(1) @binding(2) var trailMapIn: texture_2d;
 @group(1) @binding(3) var trailMapOut: texture_storage_2d;
 
@@ -22,24 +39,38 @@ fn main(@builtin(global_invocation_id) global_id: vec3) {
   }
 
   var agent = agents[id];
+  var trailBelow = textureLoad(trailMapIn, vec2(agent.position), 0);
+
+  if settings.radius > 0 && length(settings.center - agent.position) < settings.radius {
+    agents[id].generation = settings.nextGenerationId;
+    
+    // clear trail map below so the agent won't die immediately
+    if (settings.nextGenerationId % 2 == 0) {
+      trailBelow.r += trailBelow.g;
+      trailBelow.g = 0;
+    } else {
+      trailBelow.g += trailBelow.r;
+      trailBelow.r = 0;
+    }
+
+    textureStore(trailMapOut, vec2(agent.position), trailBelow);
+    return;
+  }
 
   let random = hash(id + u32(state.time % 107 * 1673.7));
-  let trailCurrent = textureLoad(trailMapIn, vec2(agent.position), 0);
-
-  var weight: f32;
-
-  // even generation id -> red channel
-  // odd generation id -> green channel
 
   let isFromEvenGeneration = agent.generation % 2 == 0;
-  
-  let trailForward = sense(agent.position, agent.angle, settings.sensorOffset, 0);
+  let isFromNextGeneration = agent.generation == settings.nextGenerationId;
+  let isFromCurrentGeneration = !isFromNextGeneration;
+
+  let trailForward = sense(agent.position, agent.angle, settings.sensorOffset , 0);
   let trailLeft = sense(agent.position, agent.angle, settings.sensorOffset, settings.sensorAngle);
   let trailRight = sense(agent.position, agent.angle, settings.sensorOffset, -settings.sensorAngle);
 
-  var weightForward: f32 = trailForward.a * settings.brushTrailWeight;
-  var weightLeft: f32 = trailLeft.a * settings.brushTrailWeight;
-  var weightRight: f32 = trailRight.a * settings.brushTrailWeight;
+  var weightForward: f32 = f32(isFromCurrentGeneration) * trailForward.a * settings.brushTrailWeight;
+  var weightLeft: f32 = f32(isFromCurrentGeneration) * trailLeft.a * settings.brushTrailWeight;
+  var weightRight: f32 = f32(isFromCurrentGeneration) * trailRight.a * settings.brushTrailWeight;
+
   if (isFromEvenGeneration) {
     weightForward += trailForward.r + settings.evenGenerationAggression * trailForward.g;
     weightLeft += trailLeft.r + settings.evenGenerationAggression * trailLeft.g;
@@ -51,38 +82,37 @@ fn main(@builtin(global_invocation_id) global_id: vec3) {
   }
 
   var rotation: f32 = 0;
-  if (weightForward < weightLeft && weightForward < weightRight) {
-    rotation = (random - 0.5) * 2. * settings.turnRate * state.deltaTime;
-  } else if (weightLeft < weightRight) {
-    rotation = random * -settings.turnRate * state.deltaTime;
-  } else if (weightRight < weightLeft) {
-    rotation = random * settings.turnRate * state.deltaTime;
+  if weightForward > weightLeft && weightForward > weightRight {
+    rotation = (random - 0.5) * settings.turnWhenGoingInTheRightDirection * settings.turnRate * state.deltaTime;
+  } else if weightLeft < weightRight {
+    rotation = -min(settings.sensorAngle, settings.turnRate * state.deltaTime);
+  } else if weightRight < weightLeft {
+    rotation = min(settings.sensorAngle, settings.turnRate * state.deltaTime);
+  } else {
+    rotation = (random - 0.5) * settings.turnWhenLost * settings.turnRate * state.deltaTime;
   }
 
-  var nextAngle = agent.angle + rotation;
-
   let direction = vec2(cos(agent.angle), sin(agent.angle));
   var nextPosition = agent.position + direction * settings.moveRate * state.deltaTime;
   nextPosition = clamp(nextPosition, vec2(0, 0), state.size);
   if nextPosition.x == 0 || nextPosition.x == state.size.x || nextPosition.y == 0 || nextPosition.y == state.size.y {
     rotation = 3.14159265359 + random - 0.5;
-    nextAngle = agent.angle + rotation;
   }
 
-  var trail = vec4(0, 0.1, 0, 0);
-  if (isFromEvenGeneration) {
-    trail = vec4(0.1, 0, 0, 0);
+  var trail = vec4(0, settings.individualTrailWeight, 0, 0);
+  if isFromEvenGeneration {
+    trail = vec4(settings.individualTrailWeight, 0, 0, 0);
   }
 
-  let current = textureLoad(trailMapIn, vec2(nextPosition), 0);
-  let next = vec4(trail.rgb + current.rgb, current.a);
+  let next = vec4(trail.rgb + trailBelow.rgb, trailBelow.a);
   textureStore(trailMapOut, vec2(nextPosition), next);
-
   
-  if(isFromEvenGeneration) {
+  if isFromEvenGeneration {
     if next.r < next.g {
       if agent.generation == settings.nextGenerationId {
-        // agent.generation -= 1;
+        if random < settings.deinfectionProbability {
+          agent.generation -= 1;
+        }
       } else {
         agent.generation += 1;
       }
@@ -90,7 +120,9 @@ fn main(@builtin(global_invocation_id) global_id: vec3) {
   } else {
     if next.g < next.r {
        if agent.generation == settings.nextGenerationId {
-        // agent.generation -= 1;
+        if random < settings.deinfectionProbability {
+          agent.generation -= 1;
+        }
       } else {
         agent.generation += 1;
       }
@@ -98,7 +130,7 @@ fn main(@builtin(global_invocation_id) global_id: vec3) {
   }
 
   agent.position = nextPosition;
-  agent.angle = nextAngle;
+  agent.angle += rotation;
   agents[id] = agent;
 }
 
diff --git a/src/pipelines/copy/copy-pipeline.ts b/src/pipelines/copy/copy-pipeline.ts
index 021125d..97d1417 100644
--- a/src/pipelines/copy/copy-pipeline.ts
+++ b/src/pipelines/copy/copy-pipeline.ts
@@ -1,4 +1,3 @@
-import { setUpFullScreenQuad } from '../../utils/graphics/full-screen-quad/full-screen-quad';
 import { smartCompile } from '../../utils/graphics/smart-compile';
 import shader from './copy.wgsl';
 
diff --git a/src/pipelines/diffusion/diffuse.wgsl b/src/pipelines/diffusion/diffuse.wgsl
index 447b92a..94734b9 100644
--- a/src/pipelines/diffusion/diffuse.wgsl
+++ b/src/pipelines/diffusion/diffuse.wgsl
@@ -33,7 +33,7 @@ fn fragment(@location(0) uv: vec2) -> @location(0) vec4 {
 
   let decayed = clamp(vec4(
     current.rgb * settings.decayRateTrails,
-    current.a * settings.decayRateBrush
+    max(0, current.a - settings.decayRateBrush)
   ), vec4(0), vec4(1));
  
   return decayed;
diff --git a/src/pipelines/render/render-pipeline.ts b/src/pipelines/render/render-pipeline.ts
index e086771..376c5ef 100644
--- a/src/pipelines/render/render-pipeline.ts
+++ b/src/pipelines/render/render-pipeline.ts
@@ -1,10 +1,11 @@
-import { generateFbmNoise } from '../../utils/graphics/fbm-noise/fbm-noise';
 import { setUpFullScreenQuad } from '../../utils/graphics/full-screen-quad/full-screen-quad';
 import { smartCompile } from '../../utils/graphics/smart-compile';
 import { CommonState } from '../common-state/common-state';
 import { RenderSettings } from './render-settings';
 import shader from './render.wgsl';
 
+import { vec3 } from 'gl-matrix';
+
 export class RenderPipeline {
   private static readonly UNIFORM_COUNT = 13;
 
@@ -56,7 +57,11 @@ export class RenderPipeline {
     evenGenerationColor,
     oddGenerationColor,
     clarity,
-  }: RenderSettings) {
+  }: RenderSettings & {
+    brushColor: vec3;
+    evenGenerationColor: vec3;
+    oddGenerationColor: vec3;
+  }) {
     this.device.queue.writeBuffer(
       this.uniforms,
       0,
diff --git a/src/pipelines/render/render-settings.ts b/src/pipelines/render/render-settings.ts
index 34b418c..c329309 100644
--- a/src/pipelines/render/render-settings.ts
+++ b/src/pipelines/render/render-settings.ts
@@ -1,8 +1,3 @@
-import { vec3 } from 'gl-matrix';
-
 export interface RenderSettings {
-  brushColor: vec3;
-  evenGenerationColor: vec3;
-  oddGenerationColor: vec3;
   clarity: number;
 }
diff --git a/src/pipelines/render/render.wgsl b/src/pipelines/render/render.wgsl
index a5c3321..28c5105 100644
--- a/src/pipelines/render/render.wgsl
+++ b/src/pipelines/render/render.wgsl
@@ -16,15 +16,19 @@ fn fragment(@location(0) uv: vec2) -> @location(0) vec4 {
 
   let backgroundColor = vec3(0.9) + 0.075 * random.r;
 
-  let evenGenerationStrength = clamp(pow(traces.r, settings.clarity), 0, 1);
-  let oddGenerationStrength = clamp(pow(traces.g, settings.clarity), 0, 1);
+  let evenGenerationStrength = pow(traces.r, settings.clarity);
+  let oddGenerationStrength = pow(traces.g, settings.clarity);
   let brushStrength = traces.a;
 
-  let agentColor = step(evenGenerationStrength, oddGenerationStrength) * settings.oddGenerationColor * oddGenerationStrength + step(oddGenerationStrength, evenGenerationStrength) * settings.evenGenerationColor * evenGenerationStrength;
-  let agentStrength = evenGenerationStrength + oddGenerationStrength;
+  let color = max(
+    mix(
+      evenGenerationStrength * settings.evenGenerationColor,
+      oddGenerationStrength * settings.oddGenerationColor,
+      oddGenerationStrength / (evenGenerationStrength + oddGenerationStrength + 0.000001)
+    ),
+    brushStrength * settings.brushColor);
 
-  let rgbColor = sqrt(
-    mix(agentColor, settings.brushColor * brushStrength, clamp(brushStrength - agentStrength, 0, 1))
-  );
-  return vec4(backgroundColor - rgbColor, 1);
+  let strength = max(evenGenerationStrength, max(oddGenerationStrength, brushStrength));
+
+  return vec4(mix(backgroundColor, color, strength), 1);
 }
diff --git a/src/settings.ts b/src/settings.ts
index 96c14cb..84759d1 100644
--- a/src/settings.ts
+++ b/src/settings.ts
@@ -3,16 +3,6 @@ import { AgentSettings } from './pipelines/agents/agent-settings';
 import { BrushSettings } from './pipelines/brush/brush-settings';
 import { DiffusionSettings } from './pipelines/diffusion/diffusion-settings';
 import { RenderSettings } from './pipelines/render/render-settings';
-import { rgb255 } from './utils/colors/rgb255';
-
-const palette = {
-  blue: rgb255(0, 110, 202),
-  red: rgb255(232, 141, 122),
-  green: rgb255(90, 188, 94),
-  purple: rgb255(161, 90, 188),
-  yellow: rgb255(255, 204, 0),
-  beige: rgb255(229, 204, 175),
-};
 
 export const settings: GameLoopSettings &
   AgentSettings &
@@ -21,29 +11,31 @@ export const settings: GameLoopSettings &
   RenderSettings = {
   agentCount: 4_000_000, // requires restart
 
-  aggressionFactor: 0.5, // requires restart
-  nextGenerationSpawnRadius: 1,
-  nextGenerationSpawnInterval: 2,
+  aggressionFactor: 3,
+  nextGenerationSpawnRadius: 5,
+  nextGenerationSpawnInterval: 1,
 
-  renderSpeed: 10,
+  renderSpeed: 2,
   simulatedDelayMs: 0,
 
-  brushWidth: 20,
-  brushWidthRandomness: 8,
+  brushWidth: 12,
+  brushWidthRandomness: 5.5,
 
   brushTrailWeight: 5,
-  moveSpeed: 40,
-  turnSpeed: 20,
+  moveSpeed: 80,
+  turnSpeed: 550,
   sensorOffsetAngle: 30,
-  sensorOffsetDistance: 60,
+  sensorOffsetDistance: 30,
+  turnWhenGoingInTheRightDirection: 0.05,
+  turnWhenLost: 0.2,
+  individualTrailWeight: 0.5,
+  deinfectionProbability: 0.001,
 
-  diffusionRateTrails: 0.4, // inverse
+  diffusionRateTrails: 2, // inverse
   decayRateTrails: 0.9, // inverse
   diffusionRateBrush: 4, // inverse
-  decayRateBrush: 0.995, // inverse
+  decayRateBrush: 0.003,
 
-  brushColor: palette.blue,
-  evenGenerationColor: palette.yellow,
-  oddGenerationColor: palette.purple,
-  clarity: 1,
+  clarity: 2,
+  startColorHue: 200,
 };
diff --git a/src/style/mixins.scss b/src/style/mixins.scss
index de4cce5..9dc02cb 100644
--- a/src/style/mixins.scss
+++ b/src/style/mixins.scss
@@ -33,18 +33,17 @@ $breakpoint-width: 700px !default;
   }
 }
 
-@mixin image-button($background-image) {
+@mixin image-button($background-image, $background-color) {
   @include square(var(--icon-size));
   border: none;
   cursor: pointer;
 
-  background-color: transparent;
-  background-image: $background-image;
-  background-size: contain;
-  background-repeat: no-repeat;
-  background-position: center;
+  background-color: $background-color;
+  mask-image: $background-image;
+  -webkit-mask-image: $background-image;
+  mask-repeat: no-repeat;
 
-  transition: transform var(--transition-time);
+  transition: transform var(--transition-time), background-color var(--transition-time);
   &:hover {
     transform: scale(1.15);
   }
diff --git a/src/style/vars.scss b/src/style/vars.scss
index 104eb33..5e5d4e7 100644
--- a/src/style/vars.scss
+++ b/src/style/vars.scss
@@ -1,13 +1,11 @@
 @use 'mixins' as *;
 
-$accent-color: #b7455e;
-
 :root {
   --transition-time: 200ms;
   --transition-time-long: 350ms;
   --line-width: 4px;
   --line-height: 1.125rem;
-  --accent-color: $accent-color;
+  --accent-color: #ff6b6b;
   --sun-color: #f7f78c;
   --very-light-text-color: #ffffff;
   --background: #ffffff;
diff --git a/src/utils/colors/hsl.ts b/src/utils/colors/hsl.ts
index 06367a1..322b8b8 100644
--- a/src/utils/colors/hsl.ts
+++ b/src/utils/colors/hsl.ts
@@ -6,7 +6,7 @@ export const hsl = (hue: number, saturation: number, lightness: number): vec3 =>
   hue /= 360;
   saturation /= 100;
   lightness /= 100;
-  let r, g, b;
+  let r: number, g: number, b: number;
 
   if (saturation == 0) {
     r = g = b = lightness;
diff --git a/src/utils/hash.ts b/src/utils/hash.ts
deleted file mode 100644
index 9be98b1..0000000
--- a/src/utils/hash.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-export const hash = (state: number): number => {
-  state ^= 2747636419;
-  state *= 2654435769;
-  state ^= state >> 16;
-  state *= 2654435769;
-  state ^= state >> 16;
-  state *= 2654435769;
-  return state / 4294967295.0;
-};