herd / files / src / entities / Pen.ts
Pen.ts
e410805c 2026-05-17 Pavel Safronov | 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 | ||
21437a01 2026-05-18 Pavel Safronov | 20 | draw(ctx: CanvasRenderingContext2D, filledFraction: number, scale = 1) { |
e410805c 2026-05-17 Pavel Safronov | 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' | |
21437a01 2026-05-18 Pavel Safronov | 30 | ctx.lineWidth = 3 * scale |
e410805c 2026-05-17 Pavel Safronov | 31 | |
21437a01 2026-05-18 Pavel Safronov | 32 | const postR = 5 * scale |
e410805c 2026-05-17 Pavel Safronov | 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 | |
21437a01 2026-05-18 Pavel Safronov | 76 | this.drawGateArrow(ctx, scale) |
e410805c 2026-05-17 Pavel Safronov | 77 | } |
| 78 | ||
21437a01 2026-05-18 Pavel Safronov | 79 | private drawGateArrow(ctx: CanvasRenderingContext2D, scale = 1) { |
e410805c 2026-05-17 Pavel Safronov | 80 | const { x, y, w, h, gateSide } = this |
| 81 | const cx = x + w / 2 | |
| 82 | const cy = y + h / 2 | |
21437a01 2026-05-18 Pavel Safronov | 83 | const arrowLen = 14 * scale |
e410805c 2026-05-17 Pavel Safronov | 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)' | |
21437a01 2026-05-18 Pavel Safronov | 94 | ctx.lineWidth = 2 * scale |
e410805c 2026-05-17 Pavel Safronov | 95 | ctx.beginPath() |
| 96 | ctx.moveTo(ax, ay) | |
| 97 | ctx.lineTo(ax + dx * arrowLen, ay + dy * arrowLen) | |
| 98 | ctx.stroke() | |
| 99 | ||
| 100 | // arrowhead | |
21437a01 2026-05-18 Pavel Safronov | 101 | const perpX = -dy * 5 * scale, perpY = dx * 5 * scale |
e410805c 2026-05-17 Pavel Safronov | 102 | const tipX = ax + dx * arrowLen, tipY = ay + dy * arrowLen |
| 103 | ctx.beginPath() | |
| 104 | ctx.moveTo(tipX, tipY) | |
21437a01 2026-05-18 Pavel Safronov | 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) | |
e410805c 2026-05-17 Pavel Safronov | 107 | ctx.closePath() |
| 108 | ctx.fillStyle = 'rgba(255,255,255,0.6)' | |
| 109 | ctx.fill() | |
| 110 | } | |
| 111 | } |