/** * SIM-12: Flussmanagement — Canvas Renderer * * Zeichnet eine Landschaft mit Fluss, Vegetation, Siedlung, Auen. * Alle visuellen Elemente reagieren auf den Simulationszustand. */ import type { State, Controls } from './logic' export class FlussRenderer { private ctx: CanvasRenderingContext2D private w: number private h: number private time = 0 constructor(private canvas: HTMLCanvasElement) { this.ctx = canvas.getContext('2d')! this.w = canvas.width this.h = canvas.height } resize(): void { const rect = this.canvas.getBoundingClientRect() const dpr = window.devicePixelRatio || 1 this.canvas.width = rect.width * dpr this.canvas.height = rect.height * dpr this.ctx.scale(dpr, dpr) this.w = rect.width this.h = rect.height } render(state: State, controls: Controls): void { this.time += 0.02 const ctx = this.ctx const w = this.w const h = this.h // Hintergrund — Himmel const skyGrad = ctx.createLinearGradient(0, 0, 0, h * 0.4) skyGrad.addColorStop(0, '#87CEEB') skyGrad.addColorStop(1, '#B0E0E6') ctx.fillStyle = skyGrad ctx.fillRect(0, 0, w, h) // Grund — Erde/Gras const groundY = h * 0.35 const groundGrad = ctx.createLinearGradient(0, groundY, 0, h) const greenIntensity = Math.round(80 + state.biodiversity * 0.8) groundGrad.addColorStop(0, `rgb(${120 - state.soilFertility * 0.3}, ${greenIntensity}, ${60 - state.erosion * 0.3})`) groundGrad.addColorStop(1, `rgb(${140 - state.soilFertility * 0.2}, ${100 + state.biodiversity * 0.4}, ${70})`) ctx.fillStyle = groundGrad ctx.fillRect(0, groundY, w, h - groundY) // Berge im Hintergrund this.drawMountains(ctx, w, h) // Auen-Bereich (wenn freigegeben) if (controls.floodplainRelease > 10) { this.drawFloodplains(ctx, w, h, controls.floodplainRelease, state) } // Fluss zeichnen this.drawRiver(ctx, w, h, state, controls) // Deiche if (controls.levees > 10) { this.drawLevees(ctx, w, h, controls.levees) } // Hochwasser-Overlay if (state.floodLocal > 50) { this.drawFlood(ctx, w, h, state.floodLocal) } // Vegetation / Baeume (Biodiversitaet) this.drawVegetation(ctx, w, h, state.biodiversity, state.soilFertility) // Siedlung this.drawSettlement(ctx, w, h, state.economy, state.usableLand) // Felder (Landwirtschaft) this.drawFarms(ctx, w, h, state.soilFertility, controls.irrigation) // Erosionsspuren if (state.erosion > 40) { this.drawErosion(ctx, w, h, state.erosion) } // Wolken this.drawClouds(ctx, w, h) } private drawMountains(ctx: CanvasRenderingContext2D, w: number, h: number): void { const baseY = h * 0.35 ctx.fillStyle = '#8BA89A' ctx.beginPath() ctx.moveTo(0, baseY) ctx.lineTo(w * 0.1, baseY - h * 0.15) ctx.lineTo(w * 0.2, baseY - h * 0.08) ctx.lineTo(w * 0.35, baseY - h * 0.2) ctx.lineTo(w * 0.5, baseY - h * 0.05) ctx.lineTo(w * 0.65, baseY - h * 0.18) ctx.lineTo(w * 0.8, baseY - h * 0.1) ctx.lineTo(w * 0.9, baseY - h * 0.14) ctx.lineTo(w, baseY) ctx.closePath() ctx.fill() // Schneedecke ctx.fillStyle = 'rgba(255,255,255,0.5)' ctx.beginPath() ctx.moveTo(w * 0.33, baseY - h * 0.2) ctx.lineTo(w * 0.35, baseY - h * 0.2) ctx.lineTo(w * 0.37, baseY - h * 0.17) ctx.lineTo(w * 0.31, baseY - h * 0.17) ctx.closePath() ctx.fill() } private drawRiver(ctx: CanvasRenderingContext2D, w: number, h: number, state: State, controls: Controls): void { const riverWidth = 20 + (100 - controls.straightening) * 0.15 const meander = (100 - controls.straightening) * 0.4 // Maeander-Amplitude const riverY = h * 0.55 // Flussverlauf (von links nach rechts) ctx.beginPath() ctx.moveTo(0, riverY) const steps = 50 for (let i = 0; i <= steps; i++) { const x = (i / steps) * w const progress = i / steps const wave = Math.sin(progress * Math.PI * 3 + this.time) * meander const y = riverY + wave ctx.lineTo(x, y) } // Untere Kante (Flussbreite) for (let i = steps; i >= 0; i--) { const x = (i / steps) * w const progress = i / steps const wave = Math.sin(progress * Math.PI * 3 + this.time) * meander const y = riverY + wave + riverWidth ctx.lineTo(x, y) } ctx.closePath() // Flussfarbe: klar (biodiversitaet hoch) bis trueb (erosion hoch) const clarity = Math.max(0, Math.min(1, (state.biodiversity - state.erosion * 0.5) / 80)) const r = Math.round(30 + (1 - clarity) * 60) const g = Math.round(100 + clarity * 50) const b = Math.round(160 + clarity * 40) ctx.fillStyle = `rgba(${r}, ${g}, ${b}, 0.85)` ctx.fill() // Wasseroberflaeche Glanz ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)' ctx.lineWidth = 1 for (let i = 0; i < 5; i++) { const sx = Math.random() * w const sy = riverY + Math.sin(sx / w * Math.PI * 3 + this.time) * meander + riverWidth * 0.3 ctx.beginPath() ctx.moveTo(sx, sy) ctx.lineTo(sx + 15 + Math.random() * 20, sy - 1) ctx.stroke() } } private drawLevees(ctx: CanvasRenderingContext2D, w: number, h: number, intensity: number): void { const leveeH = 3 + intensity * 0.08 const riverY = h * 0.55 ctx.fillStyle = '#8B7355' // Oberer Deich ctx.fillRect(0, riverY - leveeH - 5, w, leveeH) // Unterer Deich ctx.fillRect(0, riverY + 30, w, leveeH) } private drawFloodplains(ctx: CanvasRenderingContext2D, w: number, h: number, intensity: number, state: State): void { const riverY = h * 0.55 const extent = intensity * 0.3 const alpha = 0.15 + intensity * 0.002 ctx.fillStyle = `rgba(100, 180, 140, ${alpha})` // Aue oben ctx.fillRect(0, riverY - extent - 20, w, extent) // Aue unten ctx.fillRect(0, riverY + 35, w, extent) // Schilf in den Auen if (intensity > 30) { ctx.fillStyle = '#5A8A3E' for (let i = 0; i < intensity * 0.3; i++) { const x = (i * 47 + 20) % w const y = riverY - 25 - Math.random() * extent * 0.5 this.drawReeds(ctx, x, y) } } } private drawReeds(ctx: CanvasRenderingContext2D, x: number, y: number): void { ctx.save() ctx.strokeStyle = '#4A7A2E' ctx.lineWidth = 1.5 for (let i = 0; i < 3; i++) { const lean = Math.sin(this.time * 2 + x * 0.1) * 3 ctx.beginPath() ctx.moveTo(x + i * 3, y) ctx.quadraticCurveTo(x + i * 3 + lean, y - 8, x + i * 3 + lean * 1.5, y - 15) ctx.stroke() } ctx.restore() } private drawFlood(ctx: CanvasRenderingContext2D, w: number, h: number, intensity: number): void { const alpha = Math.min(0.4, (intensity - 50) / 100) const extent = (intensity - 50) * 0.8 const riverY = h * 0.55 ctx.fillStyle = `rgba(70, 130, 180, ${alpha})` // Ueberflutung breitet sich vom Fluss aus ctx.fillRect(0, riverY - extent, w, extent * 2 + 30) // Wellenlinien ctx.strokeStyle = `rgba(255, 255, 255, ${alpha * 0.5})` ctx.lineWidth = 1 for (let row = 0; row < 3; row++) { ctx.beginPath() const baseY = riverY - extent + row * extent * 0.6 for (let x = 0; x < w; x += 5) { const y = baseY + Math.sin(x * 0.05 + this.time * 3 + row) * 3 x === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y) } ctx.stroke() } } private drawVegetation(ctx: CanvasRenderingContext2D, w: number, h: number, biodiversity: number, fertility: number): void { const treeCount = Math.floor(biodiversity * 0.15) const groundY = h * 0.35 for (let i = 0; i < treeCount; i++) { const seed = i * 137.5 // goldener Winkel fuer Verteilung const x = (seed % w) const yBase = groundY + 10 + (seed * 7 % (h * 0.15)) // Nur Baeume die nicht im Flussbereich sind if (yBase > h * 0.5 && yBase < h * 0.65) continue const treeH = 12 + (fertility * 0.1) + (i % 5) * 2 const green = Math.round(60 + biodiversity * 0.8 + (i % 3) * 20) // Stamm ctx.fillStyle = '#6B4423' ctx.fillRect(x - 1.5, yBase - treeH * 0.4, 3, treeH * 0.4) // Krone ctx.fillStyle = `rgb(${40 + (i % 20)}, ${green}, ${30 + (i % 15)})` ctx.beginPath() ctx.arc(x, yBase - treeH * 0.5, treeH * 0.35, 0, Math.PI * 2) ctx.fill() } } private drawSettlement(ctx: CanvasRenderingContext2D, w: number, h: number, economy: number, usableLand: number): void { const houseCount = Math.floor(3 + economy * 0.08) const startX = w * 0.6 const baseY = h * 0.42 for (let i = 0; i < houseCount; i++) { const x = startX + (i % 4) * 30 + Math.floor(i / 4) * 15 const y = baseY + Math.floor(i / 4) * 20 const houseH = 12 + (economy * 0.05) if (x > w - 20) continue // Haus ctx.fillStyle = i < 3 ? '#D4A574' : '#C4956A' ctx.fillRect(x, y - houseH, 16, houseH) // Dach ctx.fillStyle = '#8B4513' ctx.beginPath() ctx.moveTo(x - 3, y - houseH) ctx.lineTo(x + 8, y - houseH - 8) ctx.lineTo(x + 19, y - houseH) ctx.closePath() ctx.fill() // Fenster ctx.fillStyle = '#FFF8DC' ctx.fillRect(x + 3, y - houseH + 3, 4, 4) ctx.fillRect(x + 9, y - houseH + 3, 4, 4) } } private drawFarms(ctx: CanvasRenderingContext2D, w: number, h: number, fertility: number, irrigation: number): void { const farmArea = h * 0.75 const rows = Math.floor(3 + fertility * 0.04) for (let r = 0; r < rows; r++) { const y = farmArea + r * 12 const x = w * 0.05 + r * 20 // Feldstreifen const green = Math.round(100 + fertility * 0.8) const brown = Math.round(180 - fertility * 0.5) ctx.fillStyle = r % 2 === 0 ? `rgb(${brown}, ${green}, 60)` : `rgb(${brown - 20}, ${Math.min(180, green + 20)}, 40)` ctx.fillRect(x, y, w * 0.25, 8) // Bewaesserungskanaele if (irrigation > 20) { ctx.strokeStyle = 'rgba(70, 130, 200, 0.4)' ctx.lineWidth = 1 ctx.setLineDash([3, 3]) ctx.beginPath() ctx.moveTo(x, y + 4) ctx.lineTo(x + w * 0.25, y + 4) ctx.stroke() ctx.setLineDash([]) } } } private drawErosion(ctx: CanvasRenderingContext2D, w: number, h: number, erosion: number): void { const count = Math.floor((erosion - 40) * 0.2) ctx.fillStyle = 'rgba(139, 115, 85, 0.3)' for (let i = 0; i < count; i++) { const x = (i * 89 + 30) % w const y = h * 0.6 + (i * 43 % (h * 0.2)) ctx.beginPath() ctx.ellipse(x, y, 8 + erosion * 0.05, 3, 0.3, 0, Math.PI * 2) ctx.fill() } } private drawClouds(ctx: CanvasRenderingContext2D, w: number, h: number): void { ctx.fillStyle = 'rgba(255, 255, 255, 0.7)' for (let i = 0; i < 3; i++) { const cx = (i * 250 + this.time * 15) % (w + 100) - 50 const cy = 30 + i * 25 ctx.beginPath() ctx.arc(cx, cy, 20, 0, Math.PI * 2) ctx.arc(cx + 15, cy - 5, 15, 0, Math.PI * 2) ctx.arc(cx + 30, cy, 18, 0, Math.PI * 2) ctx.arc(cx - 12, cy + 2, 14, 0, Math.PI * 2) ctx.fill() } } }