herd / files / src / game / Canvas.ts
Canvas.ts
1001e9ef 2026-05-17 Pavel Safronov | 1 | const GRASS_STEP = 40 |
| 2 | ||
| 3 | export class Canvas { | |
| 4 | readonly el: HTMLCanvasElement | |
| 5 | readonly ctx: CanvasRenderingContext2D | |
| 6 | private _width = 0 | |
| 7 | private _height = 0 | |
| 8 | private _dpr = 1 | |
| 9 | ||
| 10 | // offscreen canvas for the static grass texture — regenerated on resize | |
| 11 | private grassCanvas: HTMLCanvasElement | null = null | |
| 12 | ||
| 13 | constructor(id: string) { | |
| 14 | const el = document.getElementById(id) | |
| 15 | if (!(el instanceof HTMLCanvasElement)) throw new Error(`#${id} is not a canvas`) | |
| 16 | this.el = el | |
| 17 | this.ctx = el.getContext('2d')! | |
| 18 | this.resize() | |
| 19 | window.addEventListener('resize', () => this.resize()) | |
| 20 | } | |
| 21 | ||
| 22 | get width() { return this._width } | |
| 23 | get height() { return this._height } | |
| 24 | get dpr() { return this._dpr } | |
| 25 | ||
| 26 | private resize() { | |
| 27 | this._dpr = window.devicePixelRatio || 1 | |
| 28 | this._width = window.innerWidth | |
| 29 | this._height = window.innerHeight | |
| 30 | this.el.width = this._width * this._dpr | |
| 31 | this.el.height = this._height * this._dpr | |
| 32 | this.el.style.width = `${this._width}px` | |
| 33 | this.el.style.height = `${this._height}px` | |
| 34 | this.ctx.setTransform(this._dpr, 0, 0, this._dpr, 0, 0) | |
| 35 | this.buildGrassTexture() | |
| 36 | } | |
| 37 | ||
| 38 | private buildGrassTexture() { | |
| 39 | const oc = document.createElement('canvas') | |
| 40 | oc.width = this._width | |
| 41 | oc.height = this._height | |
| 42 | const ctx = oc.getContext('2d')! | |
| 43 | ||
| 44 | ctx.strokeStyle = 'rgba(60,110,30,0.3)' | |
| 45 | ctx.lineWidth = 1 | |
| 46 | for (let x = 0; x < this._width; x += GRASS_STEP) { | |
| 47 | for (let y = 0; y < this._height; y += GRASS_STEP) { | |
| 48 | // two blades per cell, randomized once at build time | |
| 49 | for (let b = 0; b < 2; b++) { | |
| 50 | const bx = x + Math.random() * GRASS_STEP | |
| 51 | const by = y + Math.random() * GRASS_STEP | |
| 52 | ctx.beginPath() | |
| 53 | ctx.moveTo(bx, by) | |
| 54 | ctx.lineTo(bx + (Math.random() - 0.5) * 6, by - 4 - Math.random() * 5) | |
| 55 | ctx.stroke() | |
| 56 | } | |
| 57 | } | |
| 58 | } | |
| 59 | this.grassCanvas = oc | |
| 60 | } | |
| 61 | ||
| 62 | drawField() { | |
| 63 | const { ctx } = this | |
| 64 | ctx.fillStyle = '#4a8c2a' | |
| 65 | ctx.fillRect(0, 0, this._width, this._height) | |
| 66 | if (this.grassCanvas) { | |
| 67 | ctx.drawImage(this.grassCanvas, 0, 0) | |
| 68 | } | |
| 69 | } | |
| 70 | ||
| 71 | clear() { | |
| 72 | this.ctx.clearRect(0, 0, this._width, this._height) | |
| 73 | } | |
| 74 | } |