diff --git a/src/assets/schnecke.png b/src/assets/schnecke.png new file mode 100644 index 0000000..593a892 Binary files /dev/null and b/src/assets/schnecke.png differ diff --git a/src/common/vec.ts b/src/common/vec.ts index c75ded6..64c8aba 100644 --- a/src/common/vec.ts +++ b/src/common/vec.ts @@ -164,13 +164,13 @@ export class Vec { distance(x: number, y: number): number; distance(x: number|Vec, y?: number): number { if (x instanceof Vec) { - return this.sub(x).length(); + return Math.sqrt((this.x - x.x) * (this.x - x.x) + (this.y - x.y) * (this.y - x.y)); } if (undefined === y) { throw new Error("y-coordinate undefined on vector operation"); } - return this.sub(x, y).length(); + return Math.sqrt((this.x - x) * (this.x - x) + (this.y - y) * (this.y - y)); } rotate(angle: number) { diff --git a/src/env.d.ts b/src/env.d.ts new file mode 100644 index 0000000..4997750 --- /dev/null +++ b/src/env.d.ts @@ -0,0 +1 @@ +declare module "*.png"; diff --git a/src/game/fluids/fluids.ts b/src/game/fluids/fluids.ts index 7f393d3..5046403 100644 --- a/src/game/fluids/fluids.ts +++ b/src/game/fluids/fluids.ts @@ -9,6 +9,7 @@ const gravity = 0.008; const numParticles = 200; const pressureForce = 8000; export const particleSize = 10; +export const imageSize = 64; export const desiredDensity = 6; const densityRadius = 40; const mass = 1; diff --git a/src/game/swarm/agent.ts b/src/game/swarm/agent.ts index 5888aeb..9dec799 100644 --- a/src/game/swarm/agent.ts +++ b/src/game/swarm/agent.ts @@ -1,11 +1,13 @@ import { Canvas } from "@/canvas"; import { GameObject } from "@/common/gameobject"; import { Vec } from "@/common/vec"; -import { particleSize } from "@/game/fluids/fluids"; +import { imageSize, particleSize } from "@/game/fluids/fluids"; import { Swarm } from "@/game/swarm/swarm"; -import { fillCircle, strokeCircle, strokeLine } from "@/graphics"; - +import { drawImageRotated, fillCircle, strokeCircle, strokeLine } from "@/graphics"; +import Schnecke from "@/assets/schnecke.png"; +const schneckenImage = new Image(); +schneckenImage.src = Schnecke; export type NavigationType = 'stayInBounds' | 'doNotBumpIntoFlock' | 'keepFlockClose' | 'behaveLikeFlockDoes' | 'idling'; export type NavigatingDesire = { pos: Vec; @@ -117,14 +119,20 @@ export class Agent extends GameObject { doNotBumpIntoFlock(nextPos: Vec): NavigatingDesire { const {agents} = this.swarm; - const visibleAgentsNotMe = agents - .filter(a => a.pos.distance(this.pos) < this.swarm.config.flockPerceptionDistance && a !== this); - const visibleAgentsInBounds = visibleAgentsNotMe - .filter(a => this.swarm.rect.contains(a.pos)); + const visibleAgentsInBounds = []; let closestAgent: Agent|null = null; - for (const a of visibleAgentsInBounds) { - if (!closestAgent || a.pos.distance(this.pos) < closestAgent.pos.distance(this.pos)) { + let closestDist: number = Infinity; + for (const a of agents) { + const dist = a.pos.distance(this.pos); + if (!this.swarm.rect.contains(a.pos) + || dist >= this.swarm.config.flockPerceptionDistance + || a === this) { + continue; + } + visibleAgentsInBounds.push(a); + if (!closestAgent || dist < closestDist) { closestAgent = a; + closestDist = dist; } } if (!closestAgent) { @@ -135,7 +143,7 @@ export class Agent extends GameObject { }; } const awayFromClosestAgentDir = this.pos.sub(closestAgent.pos).normalize(); - const urgency = Math.max(0, (this.swarm.config.desiredDistanceToEveryone - closestAgent.pos.distance(this.pos)) / this.swarm.config.desiredDistanceToEveryone); + const urgency = Math.max(0, (this.swarm.config.desiredDistanceToEveryone - closestDist) / this.swarm.config.desiredDistanceToEveryone); return { pos: this.pos.add(awayFromClosestAgentDir), urgency: urgency * urgency, @@ -145,10 +153,9 @@ export class Agent extends GameObject { keepFlockClose(nextPos: Vec): NavigatingDesire { const {agents} = this.swarm; - const visibleAgents = agents + const visibleAgentsInBounds = agents + .filter(a => this.swarm.rect.contains(a.pos)) .filter(a => a.pos.distance(this.pos) < this.swarm.config.flockPerceptionDistance); - const visibleAgentsInBounds = visibleAgents - .filter(a => this.swarm.rect.contains(a.pos)); const center = visibleAgentsInBounds .reduce((sum, a) => sum.add(a.pos), new Vec) .scale(1 / visibleAgentsInBounds.length); @@ -203,17 +210,27 @@ export class Agent extends GameObject { } render({ctx, input}: Canvas) { - if (input.mouseDown) { + if (this === this.swarm.selectedAgent) { + console.log(this.dir.clockwiseAngleBetween(1, 0)); + } + if (this.swarm.config.renderStyle === 'image') { + const satisfaction = Math.floor(this.satisfaction()); + const dissatisfaction = 255 - satisfaction; + // draw color + ctx.filter = `hue-rotate(${satisfaction}deg) saturate(1000%)`; + + // set composite mode + drawImageRotated(ctx, schneckenImage, -this.dir.clockwiseAngleBetween(1, 0), this.pos.x, this.pos.y, imageSize, imageSize); + ctx.filter = `none`; + } else if (input.mouseDown) { fillCircle(ctx, this.pos, particleSize, 'gray'); - } else { - if (this.swarm.config.renderStyle === 'satisfaction') { - const satisfaction = this.satisfaction(); - const dissatisfaction = 255 - satisfaction; - fillCircle(ctx, this.pos, particleSize, `rgb(${dissatisfaction}, 0, ${satisfaction})`); - } else { - const color = navigationColorMap[this.lastNavigation.type]; - fillCircle(ctx, this.pos, particleSize, color); - } + } else if (this.swarm.config.renderStyle === 'satisfaction') { + const satisfaction = this.satisfaction(); + const dissatisfaction = 255 - satisfaction; + fillCircle(ctx, this.pos, particleSize, `rgb(${dissatisfaction}, 0, ${satisfaction})`); + } else if (this.swarm.config.renderStyle === 'navigationMode') { + const color = navigationColorMap[this.lastNavigation.type]; + fillCircle(ctx, this.pos, particleSize, color); } if (this.swarm.config.showDirections) { strokeLine(ctx, this.pos, this.pos.add(this.dir.scale(30))); diff --git a/src/game/swarm/swarm.ts b/src/game/swarm/swarm.ts index 685ce1e..8de4fe6 100644 --- a/src/game/swarm/swarm.ts +++ b/src/game/swarm/swarm.ts @@ -6,7 +6,7 @@ import { Agent } from "@/game/swarm/agent"; import { strokeCircle } from "@/graphics"; import Button from "@/ui/button"; -export type RenderStyle = 'satisfaction'|'navigationMode'; +export type RenderStyle = 'satisfaction'|'navigationMode'|'image'; export class Config { numAgents: number = 20; @@ -15,7 +15,7 @@ export class Config { idlingUrgency: number = 0.001; idlingDistance: number = 20; desiredDistanceToEveryone: number = 50; - renderStyle: RenderStyle = 'satisfaction'; + renderStyle: RenderStyle = 'image'; showDirections: boolean = false; showFlockCenter: boolean = false; flockPerceptionDistance: number = 100; @@ -64,15 +64,23 @@ export class Swarm extends GameObject { const buttons = [ new Button("Reset", Rect.bySize(new Vec(10, 10), buttonSize), () => this.init(canvas)), new Button("Toggle Colors", Rect.bySize(new Vec(110, 10), buttonSize), () => { - this.config.renderStyle = this.config.renderStyle === 'satisfaction' ? 'navigationMode' : 'satisfaction'; + const styles: RenderStyle[] = ['satisfaction', 'navigationMode', 'image']; + this.config.renderStyle = styles[(styles.indexOf(this.config.renderStyle) + 1) % styles.length]; }), new Button("Less Agents", Rect.bySize(new Vec(10, 60), buttonSize), () => { - this.config.numAgents -= Math.max(1, this.config.numAgents / 10); - this.init(canvas); + const agentsToRemove = Math.max(1, this.config.numAgents / 10); + this.agents.splice(-agentsToRemove, agentsToRemove).forEach(a => canvas.remove(a)); + this.config.numAgents -= agentsToRemove; }), new Button("More Agents", Rect.bySize(new Vec(110, 60), buttonSize), () => { - this.config.numAgents += Math.max(1, this.config.numAgents / 10); - this.init(canvas); + const agentsToAdd = Math.max(1, this.config.numAgents / 10); + for (let i = 0; i < agentsToAdd; i++) { + const pos = Vec.random(this.rect); + const agent = new Agent(pos, this); + this.agents.push(agent); + canvas.add(agent); + } + this.config.numAgents += agentsToAdd; }), new Button("toggle Directions", Rect.bySize(new Vec(10, 110), buttonSize), () => { this.config.showDirections = !this.config.showDirections; @@ -101,6 +109,11 @@ export class Swarm extends GameObject { } update(canvas: Canvas, delta: DOMHighResTimeStamp) { + if (canvas.input.keypressed('Escape')) { + canvas.camera.reset(); + canvas.camera.unlock(); + this.selectedAgent = null; + } } render(canvas: Canvas) { diff --git a/src/graphics.ts b/src/graphics.ts index 8d55b9c..dda887e 100644 --- a/src/graphics.ts +++ b/src/graphics.ts @@ -9,12 +9,12 @@ export function drawImageRotated( y: number, w: number, h: number, - x2: number, - y2: number, - w2: number, - h2: number, + x2?: number, + y2?: number, + w2?: number, + h2?: number, ) { - if (typeof h2 !== "undefined") { + if (typeof x2 !== "undefined" && typeof h2 !== "undefined" && typeof w2 !== "undefined" && typeof y2 !== "undefined") { ctx.translate(x2, y2); ctx.rotate(angle); ctx.drawImage(img, x, y, w, h, -w2 / 2, -h2 / 2, w2, h2); diff --git a/webpack.config.js b/webpack.config.js index 7f5b051..51d1fc7 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -36,9 +36,13 @@ const config = { use: [stylesHandler, "css-loader"], }, { - test: /\.(eot|svg|ttf|woff|woff2|png|jpg|gif)$/i, + test: /\.(eot|svg|ttf|woff|woff2|jpg|gif)$/i, type: "asset", }, + { + test: /\.(png)$/i, + type: "asset/inline", + }, // Add your rules for custom modules here // Learn more about loaders from https://webpack.js.org/loaders/