UNPKG

agentscape

Version:

Agentscape is a library for creating agent-based simulations. It provides a simple API for defining agents and their behavior, and for defining the environment in which the agents interact. Agentscape is designed to be flexible and extensible, allowing

485 lines (401 loc) 13.2 kB
import { Angle, Color } from '../../numbers' import { AgentSet, CellGrid } from '../../structures' import { Agent, Cell } from '../../entities' import { AgentStyle } from '../../entities/Agent' export interface Render2DConstructor { root: HTMLElement renderWidth: number renderHeight: number worldWidth: number id?: string title?: string autoPlay?: boolean frameRate?: number onCellClick?: (position: [number, number], renderer: Render2D) => void } export type ColorFunction<U extends Agent|Cell> = (entity: U) => { fill: Color, stroke?: Color } export type StyleFunction<T extends Agent> = (entity: T) => AgentStyle export class Render2D { renderWidth: number // in pixels renderHeight: number // in pixels ctx: CanvasRenderingContext2D frameRate: number cellWidth: number cellHeight: number constructor(opts: Render2DConstructor) { const { root, worldWidth, renderHeight, renderWidth, title, id, autoPlay, frameRate } = opts // create a draggable canvas element const draggable = document.createElement('drag-pane') draggable.style.zIndex = '1' draggable.id = 'canvas_main' draggable.setAttribute('heading', title ) draggable.setAttribute('key', id) this.renderHeight = renderHeight this.renderWidth = renderWidth this.cellWidth = renderWidth / worldWidth this.cellHeight = renderHeight / worldWidth const canvas = document.createElement('canvas') this.ctx = canvas.getContext('2d')! canvas.width = renderWidth canvas.height = renderHeight // create a click event handler for the canvas canvas.addEventListener('click', (e) => { const rect = canvas.getBoundingClientRect() const x = e.clientX - rect.left const y = e.clientY - rect.top // dispatch a custom event with the clicked cell coordinates const event = new CustomEvent('inspectClick', { detail: [Math.floor(x / this.cellWidth),Math.floor(y / this.cellHeight)] }) if (opts.onCellClick) { opts.onCellClick([Math.floor(x / this.cellWidth), Math.floor(y / this.cellHeight)], this) } window.dispatchEvent(event) }) draggable.appendChild(canvas) const controlBar = document.createElement('animation-toolbar') controlBar.setAttribute('autoPlay', autoPlay.toString()) controlBar.setAttribute('fps', frameRate.toString()) draggable.appendChild(controlBar) root.appendChild(draggable) } clear() { this.ctx.clearRect(0, 0, this.renderWidth, this.renderHeight) } drawCellGrid<T extends Cell>(world: CellGrid<T>, opts:{ colorFunction?: ColorFunction<T> } = {} ) { const { colorFunction } = opts for (let x = 0; x < world.width; x++) { for (let y = 0; y < world.height; y++) { const cell = world.getCell([x, y]) const color = colorFunction ? colorFunction(cell) : { fill: cell.color, stroke: cell.strokeColor } this.drawRectangle( x, y, { width: this.cellWidth, height: this.cellHeight, fill: color.fill, stroke: color.stroke } ) } } } /** * Draws agents on the canvas * @param agents */ drawAgentSet<T extends Agent>( agents: AgentSet<T>, opts: { colorFunction?: ColorFunction<T>, styleFunction?: StyleFunction<T> } = {} ) { const { colorFunction, styleFunction } = opts agents.forEach(agent => { this.drawAgent(agent, { colorFunction, styleFunction }) }) } drawAgent<T extends Agent>( agent: T, opts: { colorFunction?: ColorFunction<T>, styleFunction?: StyleFunction<T> } = {} ) { const { colorFunction, styleFunction } = opts const color = colorFunction ? colorFunction(agent) : { fill: agent.color, stroke: agent.strokeColor } const style = styleFunction ? styleFunction(agent) : agent.style const [x, y] = agent.position.components const options = { width: agent.radius, height: agent.radius, radius: agent.radius, rotation: agent.rotation, fill: color.fill, stroke: color.stroke } if (style === AgentStyle.CIRCLE) { this.drawCircle( x, y, options ) } if (style === AgentStyle.TRIANGLE) { this.drawTriangle( x, y, options ) } if (style === AgentStyle.SQUARE) { this.drawRectangle( x, y, options ) } } public drawCircle( x: number, y: number, options?: { radius?: number, rotation?: Angle, fill?: Color, stroke?: Color lineWidth?: number showRotation?: boolean } ) { const { radius = 1, rotation = new Angle(0, 'rad'), fill = Color.fromName('blue'), stroke = undefined, showRotation = true, lineWidth = 1 } = options ?? {} const _x = x * this.cellWidth const _y = y * this.cellHeight const _radius = radius * (this.cellWidth / 2) this.ctx.fillStyle = fill.toRGB() this.ctx.beginPath() this.ctx.arc(_x, _y, _radius, 0, 2 * Math.PI) this.ctx.fill() if (stroke) { this.ctx.strokeStyle = stroke.toRGB() this.ctx.stroke() } // draw a line from the center of the circle to the edge to show rotation if (rotation && showRotation) { this.ctx.beginPath() this.ctx.moveTo(_x, _y) this.ctx.lineTo(_x + _radius * Math.cos(rotation.asRadians()), _y + _radius * Math.sin(rotation.asRadians())) } if (stroke) { this.ctx.strokeStyle = stroke.toRGB() this.ctx.lineWidth = lineWidth this.ctx.stroke() } } /** * Draws a unit rectangle centered at x,y */ public drawRectangle( x: number, y: number, options?: { width?: number, height?: number, rotation?: Angle, fill?: Color, stroke?: Color lineWidth?: number } ) { const { width = 1, height = 1, rotation = new Angle(0, 'rad'), fill = Color.fromName('blue'), stroke = undefined, lineWidth = 1 } = options ?? {} const _x = x * this.cellWidth const _y = y * this.cellHeight const _width = width * this.cellWidth const _height = height * this.cellHeight this.ctx.fillStyle = fill.toRGB() this.ctx.fillRect(_x, _y, _width, _height) if (stroke) { this.ctx.strokeStyle = stroke.toRGB() this.ctx.strokeRect(_x, _y, _width, _height) } // draw a line from the center of the rectangle to the edge to show rotation if (rotation) { this.ctx.beginPath() this.ctx.moveTo(_x + _width / 2, _y + _height / 2) this.ctx.lineTo(_x + _width / 2 + _width / 2 * Math.cos(rotation.asRadians()), _y + _height / 2 + _height / 2 * Math.sin(rotation.asRadians())) if (stroke) { this.ctx.strokeStyle = stroke.toRGB() this.ctx.lineWidth = lineWidth this.ctx.stroke() } } } public drawLine( x1: number, y1: number, x2: number, y2: number, stroke: Color, options?: { lineWidth?: number } ) { this.ctx.beginPath() this.ctx.moveTo(x1 * this.cellWidth, y1 * this.cellHeight) this.ctx.lineTo(x2 * this.cellWidth, y2 * this.cellHeight) this.ctx.strokeStyle = stroke.toRGB() this.ctx.lineWidth = options?.lineWidth ?? 1 this.ctx.stroke() } public drawTriangle( x: number, y: number, options?: { width?: number, rotation?: Angle, fill?: Color stroke?: Color, lineWidth?: number } ) { const { width = 1, rotation = new Angle(0, 'rad'), fill = Color.fromName('blue'), stroke = undefined, } = options const _width = width * this.cellWidth const _x = x * this.cellWidth const _y = y * this.cellHeight // Define the triangle's vertices relative to the origin const halfWidth = _width / 2 const height = (Math.sqrt(3) / 2) * _width + _width / 2 // Height of an equilateral triangle const vertices = [ { x: 0, y: -height / 2 }, // Top vertex { x: -halfWidth, y: height / 2 }, // Bottom-left vertex { x: halfWidth, y: height / 2 } // Bottom-right vertex ] // Save the current state of the canvas this.ctx.save() // Translate to the origin this.ctx.translate(_x, _y) // Rotate by R radians this.ctx.rotate(rotation.asDegrees()) // Begin drawing the triangle this.ctx.beginPath() this.ctx.moveTo(vertices[0].x, vertices[0].y) // Move to the first vertex for (let i = 1; i < vertices.length; i++) { this.ctx.lineTo(vertices[i].x, vertices[i].y) } this.ctx.closePath() // Fill and stroke the triangle this.ctx.fillStyle = fill.toRGB() this.ctx.fill() this.ctx.strokeStyle = stroke.toRGB() this.ctx.lineWidth = options?.lineWidth ?? 1 this.ctx.stroke() // Restore the canvas to its original state this.ctx.restore() } public drawPolygon( points: [number, number][], options?: { fill?: Color stroke?: Color, lineWidth?: number } ): void { const { fill = Color.fromName('blue'), stroke = undefined } = options ?? {} if (points.length < 3) { throw new Error('A polygon must have at least 3 vertices') } // Begin the path this.ctx.beginPath() // Move to the first point const firstPoint = points[0] const x0 = firstPoint[0] * this.cellWidth const y0 = firstPoint[1] * this.cellHeight this.ctx.moveTo(x0, y0) // Draw lines to the remaining points for (let i = 1; i < points.length; i++) { const point = points[i] const x = point[0] * this.cellWidth const y = point[1] * this.cellHeight this.ctx.lineTo(x, y) } // Close the path this.ctx.closePath() // Fill the polygon if (fill) { this.ctx.fillStyle = fill.toRGB() this.ctx.fill() } // Stroke the polygon if (stroke) { this.ctx.strokeStyle = stroke.toRGB() this.ctx.lineWidth = options?.lineWidth ?? 1 this.ctx.stroke() } // Restore the context this.ctx.restore() } public drawPolyline( points: [number, number][], stroke: Color, options?: { lineWidth?: number } ): void { if (points.length < 2) { throw new Error('A polyline must have at least 2 vertices') } // Begin the path this.ctx.beginPath() // Move to the first point const firstPoint = points[0] const x0 = firstPoint[0] * this.cellWidth const y0 = firstPoint[1] * this.cellHeight this.ctx.moveTo(x0, y0) // Draw lines to the remaining points for (let i = 1; i < points.length; i++) { const point = points[i] const x = point[0] * this.cellWidth const y = point[1] * this.cellHeight this.ctx.lineTo(x, y) } // Stroke the polyline this.ctx.lineWidth = options?.lineWidth ?? 1 this.ctx.strokeStyle = stroke.toRGB() this.ctx.stroke() // Restore the context this.ctx.restore() } } export default Render2D