Make some schnecken

This commit is contained in:
Schmop 2024-05-16 02:37:57 +02:00
parent f205d23179
commit 0eed2881ad
8 changed files with 74 additions and 38 deletions

BIN
src/assets/schnecke.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -164,13 +164,13 @@ export class Vec {
distance(x: number, y: number): number; distance(x: number, y: number): number;
distance(x: number|Vec, y?: number): number { distance(x: number|Vec, y?: number): number {
if (x instanceof Vec) { 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) { if (undefined === y) {
throw new Error("y-coordinate undefined on vector operation"); 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) { rotate(angle: number) {

1
src/env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module "*.png";

View File

@ -9,6 +9,7 @@ const gravity = 0.008;
const numParticles = 200; const numParticles = 200;
const pressureForce = 8000; const pressureForce = 8000;
export const particleSize = 10; export const particleSize = 10;
export const imageSize = 64;
export const desiredDensity = 6; export const desiredDensity = 6;
const densityRadius = 40; const densityRadius = 40;
const mass = 1; const mass = 1;

View File

@ -1,11 +1,13 @@
import { Canvas } from "@/canvas"; import { Canvas } from "@/canvas";
import { GameObject } from "@/common/gameobject"; import { GameObject } from "@/common/gameobject";
import { Vec } from "@/common/vec"; 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 { 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 NavigationType = 'stayInBounds' | 'doNotBumpIntoFlock' | 'keepFlockClose' | 'behaveLikeFlockDoes' | 'idling';
export type NavigatingDesire = { export type NavigatingDesire = {
pos: Vec; pos: Vec;
@ -117,14 +119,20 @@ export class Agent extends GameObject {
doNotBumpIntoFlock(nextPos: Vec): NavigatingDesire { doNotBumpIntoFlock(nextPos: Vec): NavigatingDesire {
const {agents} = this.swarm; const {agents} = this.swarm;
const visibleAgentsNotMe = agents const visibleAgentsInBounds = [];
.filter(a => a.pos.distance(this.pos) < this.swarm.config.flockPerceptionDistance && a !== this);
const visibleAgentsInBounds = visibleAgentsNotMe
.filter(a => this.swarm.rect.contains(a.pos));
let closestAgent: Agent|null = null; let closestAgent: Agent|null = null;
for (const a of visibleAgentsInBounds) { let closestDist: number = Infinity;
if (!closestAgent || a.pos.distance(this.pos) < closestAgent.pos.distance(this.pos)) { 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; closestAgent = a;
closestDist = dist;
} }
} }
if (!closestAgent) { if (!closestAgent) {
@ -135,7 +143,7 @@ export class Agent extends GameObject {
}; };
} }
const awayFromClosestAgentDir = this.pos.sub(closestAgent.pos).normalize(); 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 { return {
pos: this.pos.add(awayFromClosestAgentDir), pos: this.pos.add(awayFromClosestAgentDir),
urgency: urgency * urgency, urgency: urgency * urgency,
@ -145,10 +153,9 @@ export class Agent extends GameObject {
keepFlockClose(nextPos: Vec): NavigatingDesire { keepFlockClose(nextPos: Vec): NavigatingDesire {
const {agents} = this.swarm; 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); .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 const center = visibleAgentsInBounds
.reduce((sum, a) => sum.add(a.pos), new Vec) .reduce((sum, a) => sum.add(a.pos), new Vec)
.scale(1 / visibleAgentsInBounds.length); .scale(1 / visibleAgentsInBounds.length);
@ -203,18 +210,28 @@ export class Agent extends GameObject {
} }
render({ctx, input}: Canvas) { 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'); fillCircle(ctx, this.pos, particleSize, 'gray');
} else { } else if (this.swarm.config.renderStyle === 'satisfaction') {
if (this.swarm.config.renderStyle === 'satisfaction') {
const satisfaction = this.satisfaction(); const satisfaction = this.satisfaction();
const dissatisfaction = 255 - satisfaction; const dissatisfaction = 255 - satisfaction;
fillCircle(ctx, this.pos, particleSize, `rgb(${dissatisfaction}, 0, ${satisfaction})`); fillCircle(ctx, this.pos, particleSize, `rgb(${dissatisfaction}, 0, ${satisfaction})`);
} else { } else if (this.swarm.config.renderStyle === 'navigationMode') {
const color = navigationColorMap[this.lastNavigation.type]; const color = navigationColorMap[this.lastNavigation.type];
fillCircle(ctx, this.pos, particleSize, color); fillCircle(ctx, this.pos, particleSize, color);
} }
}
if (this.swarm.config.showDirections) { if (this.swarm.config.showDirections) {
strokeLine(ctx, this.pos, this.pos.add(this.dir.scale(30))); strokeLine(ctx, this.pos, this.pos.add(this.dir.scale(30)));
} }

View File

@ -6,7 +6,7 @@ import { Agent } from "@/game/swarm/agent";
import { strokeCircle } from "@/graphics"; import { strokeCircle } from "@/graphics";
import Button from "@/ui/button"; import Button from "@/ui/button";
export type RenderStyle = 'satisfaction'|'navigationMode'; export type RenderStyle = 'satisfaction'|'navigationMode'|'image';
export class Config { export class Config {
numAgents: number = 20; numAgents: number = 20;
@ -15,7 +15,7 @@ export class Config {
idlingUrgency: number = 0.001; idlingUrgency: number = 0.001;
idlingDistance: number = 20; idlingDistance: number = 20;
desiredDistanceToEveryone: number = 50; desiredDistanceToEveryone: number = 50;
renderStyle: RenderStyle = 'satisfaction'; renderStyle: RenderStyle = 'image';
showDirections: boolean = false; showDirections: boolean = false;
showFlockCenter: boolean = false; showFlockCenter: boolean = false;
flockPerceptionDistance: number = 100; flockPerceptionDistance: number = 100;
@ -64,15 +64,23 @@ export class Swarm extends GameObject {
const buttons = [ const buttons = [
new Button("Reset", Rect.bySize(new Vec(10, 10), buttonSize), () => this.init(canvas)), new Button("Reset", Rect.bySize(new Vec(10, 10), buttonSize), () => this.init(canvas)),
new Button("Toggle Colors", Rect.bySize(new Vec(110, 10), buttonSize), () => { 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), () => { new Button("Less Agents", Rect.bySize(new Vec(10, 60), buttonSize), () => {
this.config.numAgents -= Math.max(1, this.config.numAgents / 10); const agentsToRemove = Math.max(1, this.config.numAgents / 10);
this.init(canvas); 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), () => { new Button("More Agents", Rect.bySize(new Vec(110, 60), buttonSize), () => {
this.config.numAgents += Math.max(1, this.config.numAgents / 10); const agentsToAdd = Math.max(1, this.config.numAgents / 10);
this.init(canvas); 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), () => { new Button("toggle Directions", Rect.bySize(new Vec(10, 110), buttonSize), () => {
this.config.showDirections = !this.config.showDirections; this.config.showDirections = !this.config.showDirections;
@ -101,6 +109,11 @@ export class Swarm extends GameObject {
} }
update(canvas: Canvas, delta: DOMHighResTimeStamp) { update(canvas: Canvas, delta: DOMHighResTimeStamp) {
if (canvas.input.keypressed('Escape')) {
canvas.camera.reset();
canvas.camera.unlock();
this.selectedAgent = null;
}
} }
render(canvas: Canvas) { render(canvas: Canvas) {

View File

@ -9,12 +9,12 @@ export function drawImageRotated(
y: number, y: number,
w: number, w: number,
h: number, h: number,
x2: number, x2?: number,
y2: number, y2?: number,
w2: number, w2?: number,
h2: 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.translate(x2, y2);
ctx.rotate(angle); ctx.rotate(angle);
ctx.drawImage(img, x, y, w, h, -w2 / 2, -h2 / 2, w2, h2); ctx.drawImage(img, x, y, w, h, -w2 / 2, -h2 / 2, w2, h2);

View File

@ -36,9 +36,13 @@ const config = {
use: [stylesHandler, "css-loader"], 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", type: "asset",
}, },
{
test: /\.(png)$/i,
type: "asset/inline",
},
// Add your rules for custom modules here // Add your rules for custom modules here
// Learn more about loaders from https://webpack.js.org/loaders/ // Learn more about loaders from https://webpack.js.org/loaders/