fuzzyworld

herd / commit

Add Pen entity with gate, fill indicator, and center()

e410805ce5183722b66fd5332f085f5abae464be · Pavel Safronov · 2026-05-16 22:03:53 -0700 · parents 1001e9efc31a

Add Pen entity with gate, fill indicator, and center()

Three-sided fence with a configurable gate side. Fill colour shifts from
transparent to green as sheep fraction increases. center() returns a proper
Vec2 instance so Vec2 methods are available on the result.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
 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()
+  }
+}