herd / files / src / entities / Pen.ts
Pen.ts
| 1 | import { Vec2 } from '../math/Vec2.ts' |
| 2 | |
| 3 | export type GateSide = 'top' | 'right' | 'bottom' | 'left' |
| 4 | |
| 5 | export class Pen { |
| 6 | x: number; y: number; w: number; h: number; gateSide: GateSide |
| 7 | constructor(x: number, y: number, w: number, h: number, gateSide: GateSide = 'bottom') { |
| 8 | this.x = x; this.y = y; this.w = w; this.h = h; this.gateSide = gateSide |
| 9 | } |
| 10 | |
| 11 | contains(pos: Vec2): boolean { |
| 12 | return pos.x >= this.x && pos.x <= this.x + this.w && |
| 13 | pos.y >= this.y && pos.y <= this.y + this.h |
| 14 | } |
| 15 | |
| 16 | center(): Vec2 { |
| 17 | return new Vec2(this.x + this.w / 2, this.y + this.h / 2) |
| 18 | } |
| 19 | |
| 20 | draw(ctx: CanvasRenderingContext2D, filledFraction: number, scale = 1) { |
| 21 | const { x, y, w, h, gateSide } = this |
| 22 | |
| 23 | // filled interior — shifts from transparent to green as sheep enter |
| 24 | const alpha = 0.08 + filledFraction * 0.25 |
| 25 | ctx.fillStyle = `rgba(80, 200, 80, ${alpha})` |
| 26 | ctx.fillRect(x, y, w, h) |
| 27 | |
| 28 | // fence posts + rails on three sides |
| 29 | ctx.strokeStyle = '#7a5c2e' |
| 30 | ctx.lineWidth = 3 * scale |
| 31 | |
| 32 | const postR = 5 * scale |
| 33 | const sides: Array<[number, number, number, number]> = [ |
| 34 | [x, y, x + w, y ], // top |
| 35 | [x + w, y, x + w, y + h], // right |
| 36 | [x, y + h, x + w, y + h], // bottom |
| 37 | [x, y, x, y + h], // left |
| 38 | ] |
| 39 | const sideNames: GateSide[] = ['top', 'right', 'bottom', 'left'] |
| 40 | |
| 41 | sides.forEach(([x1, y1, x2, y2], i) => { |
| 42 | if (sideNames[i] === gateSide) { |
| 43 | // draw two short segments leaving a gap in the center for the gate |
| 44 | const midX = (x1 + x2) / 2 |
| 45 | const midY = (y1 + y2) / 2 |
| 46 | const gapHalf = Math.max(w, h) * 0.15 |
| 47 | |
| 48 | ctx.beginPath() |
| 49 | ctx.moveTo(x1, y1) |
| 50 | if (Math.abs(x2 - x1) > Math.abs(y2 - y1)) { |
| 51 | ctx.lineTo(midX - gapHalf, midY) // horizontal side |
| 52 | ctx.moveTo(midX + gapHalf, midY) |
| 53 | } else { |
| 54 | ctx.lineTo(midX, midY - gapHalf) // vertical side |
| 55 | ctx.moveTo(midX, midY + gapHalf) |
| 56 | } |
| 57 | ctx.lineTo(x2, y2) |
| 58 | ctx.stroke() |
| 59 | return |
| 60 | } |
| 61 | ctx.beginPath() |
| 62 | ctx.moveTo(x1, y1) |
| 63 | ctx.lineTo(x2, y2) |
| 64 | ctx.stroke() |
| 65 | }) |
| 66 | |
| 67 | // fence posts at corners |
| 68 | ;[[x, y], [x + w, y], [x + w, y + h], [x, y + h]].forEach(([px, py]) => { |
| 69 | ctx.fillStyle = '#7a5c2e' |
| 70 | ctx.beginPath() |
| 71 | ctx.arc(px, py, postR, 0, Math.PI * 2) |
| 72 | ctx.fill() |
| 73 | }) |
| 74 | |
| 75 | // gate indicator arrow pointing inward |
| 76 | this.drawGateArrow(ctx, scale) |
| 77 | } |
| 78 | |
| 79 | private drawGateArrow(ctx: CanvasRenderingContext2D, scale = 1) { |
| 80 | const { x, y, w, h, gateSide } = this |
| 81 | const cx = x + w / 2 |
| 82 | const cy = y + h / 2 |
| 83 | const arrowLen = 14 * scale |
| 84 | let ax: number, ay: number, dx: number, dy: number |
| 85 | |
| 86 | switch (gateSide) { |
| 87 | case 'top': ax = cx; ay = y; dx = 0; dy = 1; break |
| 88 | case 'right': ax = x + w; ay = cy; dx = -1; dy = 0; break |
| 89 | case 'bottom': ax = cx; ay = y + h; dx = 0; dy = -1; break |
| 90 | case 'left': ax = x; ay = cy; dx = 1; dy = 0; break |
| 91 | } |
| 92 | |
| 93 | ctx.strokeStyle = 'rgba(255,255,255,0.6)' |
| 94 | ctx.lineWidth = 2 * scale |
| 95 | ctx.beginPath() |
| 96 | ctx.moveTo(ax, ay) |
| 97 | ctx.lineTo(ax + dx * arrowLen, ay + dy * arrowLen) |
| 98 | ctx.stroke() |
| 99 | |
| 100 | // arrowhead |
| 101 | const perpX = -dy * 5 * scale, perpY = dx * 5 * scale |
| 102 | const tipX = ax + dx * arrowLen, tipY = ay + dy * arrowLen |
| 103 | ctx.beginPath() |
| 104 | ctx.moveTo(tipX, tipY) |
| 105 | ctx.lineTo(tipX - dx * 8 * scale + perpX, tipY - dy * 8 * scale + perpY) |
| 106 | ctx.lineTo(tipX - dx * 8 * scale - perpX, tipY - dy * 8 * scale - perpY) |
| 107 | ctx.closePath() |
| 108 | ctx.fillStyle = 'rgba(255,255,255,0.6)' |
| 109 | ctx.fill() |
| 110 | } |
| 111 | } |