fuzzyworld

herd / files / src / entities / Pen.ts

Pen.ts

3.5 KB · history · blame

1import { Vec2 } from '../math/Vec2.ts'
2
3export type GateSide = 'top' | 'right' | 'bottom' | 'left'
4
5export 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}