herd / commit
Add Pen entity with gate, fill indicator, and center()
src/entities/Pen.ts | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/src/entities/Pen.ts b/src/entities/Pen.ts new file mode 100644 index 0000000..3b5f97f --- /dev/null +++ b/src/entities/Pen.ts @@ -0,0 +1,111 @@ +import { Vec2 } from '../math/Vec2.ts' + +export type GateSide = 'top' | 'right' | 'bottom' | 'left' + +export class Pen { + x: number; y: number; w: number; h: number; gateSide: GateSide + constructor(x: number, y: number, w: number, h: number, gateSide: GateSide = 'bottom') { + this.x = x; this.y = y; this.w = w; this.h = h; this.gateSide = gateSide + } + + contains(pos: Vec2): boolean { + return pos.x >= this.x && pos.x <= this.x + this.w && + pos.y >= this.y && pos.y <= this.y + this.h + } + + center(): Vec2 { + return new Vec2(this.x + this.w / 2, this.y + this.h / 2) + } + + draw(ctx: CanvasRenderingContext2D, filledFraction: number) { + const { x, y, w, h, gateSide } = this + + // filled interior — shifts from transparent to green as sheep enter + const alpha = 0.08 + filledFraction * 0.25 + ctx.fillStyle = `rgba(80, 200, 80, ${alpha})` + ctx.fillRect(x, y, w, h) + + // fence posts + rails on three sides + ctx.strokeStyle = '#7a5c2e' + ctx.lineWidth = 3 + + const postR = 5 + const sides: Array<[number, number, number, number]> = [ + [x, y, x + w, y ], // top + [x + w, y, x + w, y + h], // right + [x, y + h, x + w, y + h], // bottom + [x, y, x, y + h], // left + ] + const sideNames: GateSide[] = ['top', 'right', 'bottom', 'left'] + + sides.forEach(([x1, y1, x2, y2], i) => { + if (sideNames[i] === gateSide) { + // draw two short segments leaving a gap in the center for the gate + const midX = (x1 + x2) / 2 + const midY = (y1 + y2) / 2 + const gapHalf = Math.max(w, h) * 0.15 + + ctx.beginPath() + ctx.moveTo(x1, y1) + if (Math.abs(x2 - x1) > Math.abs(y2 - y1)) { + ctx.lineTo(midX - gapHalf, midY) // horizontal side + ctx.moveTo(midX + gapHalf, midY) + } else { + ctx.lineTo(midX, midY - gapHalf) // vertical side + ctx.moveTo(midX, midY + gapHalf) + } + ctx.lineTo(x2, y2) + ctx.stroke() + return + } + ctx.beginPath() + ctx.moveTo(x1, y1) + ctx.lineTo(x2, y2) + ctx.stroke() + }) + + // fence posts at corners + ;[[x, y], [x + w, y], [x + w, y + h], [x, y + h]].forEach(([px, py]) => { + ctx.fillStyle = '#7a5c2e' + ctx.beginPath() + ctx.arc(px, py, postR, 0, Math.PI * 2) + ctx.fill() + }) + + // gate indicator arrow pointing inward + this.drawGateArrow(ctx) + } + + private drawGateArrow(ctx: CanvasRenderingContext2D) { + const { x, y, w, h, gateSide } = this + const cx = x + w / 2 + const cy = y + h / 2 + const arrowLen = 14 + let ax: number, ay: number, dx: number, dy: number + + switch (gateSide) { + case 'top': ax = cx; ay = y; dx = 0; dy = 1; break + case 'right': ax = x + w; ay = cy; dx = -1; dy = 0; break + case 'bottom': ax = cx; ay = y + h; dx = 0; dy = -1; break + case 'left': ax = x; ay = cy; dx = 1; dy = 0; break + } + + ctx.strokeStyle = 'rgba(255,255,255,0.6)' + ctx.lineWidth = 2 + ctx.beginPath() + ctx.moveTo(ax, ay) + ctx.lineTo(ax + dx * arrowLen, ay + dy * arrowLen) + ctx.stroke() + + // arrowhead + const perpX = -dy * 5, perpY = dx * 5 + const tipX = ax + dx * arrowLen, tipY = ay + dy * arrowLen + ctx.beginPath() + ctx.moveTo(tipX, tipY) + ctx.lineTo(tipX - dx * 8 + perpX, tipY - dy * 8 + perpY) + ctx.lineTo(tipX - dx * 8 - perpX, tipY - dy * 8 - perpY) + ctx.closePath() + ctx.fillStyle = 'rgba(255,255,255,0.6)' + ctx.fill() + } +}