diff --git a/frontend/src/lib/components/ParallaxLandscape.svelte b/frontend/src/lib/components/ParallaxLandscape.svelte index f6b6ede..64b2dc9 100644 --- a/frontend/src/lib/components/ParallaxLandscape.svelte +++ b/frontend/src/lib/components/ParallaxLandscape.svelte @@ -17,12 +17,20 @@ const PATH_CONFIG: Record = { "/": { sceneType: "transition", staticScene: false }, "/chat": { sceneType: "oilRig", staticScene: true }, - "/goal": { sceneType: "pollutedCity", staticScene: true }, - "/report": { sceneType: "deforestation", staticScene: true }, + "/goal": { + sceneType: "transition", + staticScene: false, + scenes: ["city", "pollutedCity"], + }, + "/report": { + sceneType: "transition", + staticScene: false, + scenes: ["forest", "deforestation"], + }, "/catalogue": { sceneType: "transition", staticScene: false, - scenes: ["eco", "ocean"], + scenes: ["ocean", "pollutedOcean"], }, "/news": { sceneType: "ocean", staticScene: true }, }; diff --git a/frontend/src/lib/ts/parallax/index.ts b/frontend/src/lib/ts/parallax/index.ts index 3b05cdb..0ad3318 100644 --- a/frontend/src/lib/ts/parallax/index.ts +++ b/frontend/src/lib/ts/parallax/index.ts @@ -8,6 +8,7 @@ import { drawOceanScene, drawOceanWaves } from './scenes/ocean'; import { drawOilRigScene } from './scenes/oilRig'; import { drawCityScene } from './scenes/city'; import { drawPollutedCityScene } from './scenes/pollutedCity'; +import { drawPollutedOceanScene, drawPollutedOceanWaves } from './scenes/pollutedOcean'; export type { ParallaxState, SceneType }; @@ -20,11 +21,12 @@ const SCENE_ELEMENTS: Record, (dc: DrawContext) oilRig: drawOilRigScene, city: drawCityScene, pollutedCity: drawPollutedCityScene, + pollutedOcean: drawPollutedOceanScene, }; -const CUSTOM_WATER_SCENES: SceneType[] = ['ocean', 'oilRig', 'deforestation']; -const NO_MOUNTAINS_SCENES: SceneType[] = ['ocean', 'oilRig', 'deforestation']; -const NO_HILLS_SCENES: SceneType[] = ['ocean', 'oilRig']; +const CUSTOM_WATER_SCENES: SceneType[] = ['ocean', 'oilRig', 'deforestation', 'pollutedCity', 'pollutedOcean', 'city', 'forest']; +const NO_MOUNTAINS_SCENES: SceneType[] = ['ocean', 'oilRig', 'deforestation', 'pollutedCity', 'pollutedOcean', 'city', 'forest']; +const NO_HILLS_SCENES: SceneType[] = ['ocean', 'oilRig', 'pollutedCity', 'pollutedOcean', 'city']; export function drawLandscape( ctx: CanvasRenderingContext2D, @@ -37,9 +39,16 @@ export function drawLandscape( ctx.clearRect(0, 0, width, height); + const NO_CLOUDS_SCENES: SceneType[] = ['city', 'pollutedCity']; + const skipClouds = NO_CLOUDS_SCENES.includes(sceneType) || + (blendToScene && NO_CLOUDS_SCENES.includes(blendToScene) && (blendProgress ?? 0) > 0.5); + drawSky(dc); drawSun(dc); - drawClouds(dc); + + if (!skipClouds) { + drawClouds(dc); + } const skipMountains = NO_MOUNTAINS_SCENES.includes(sceneType) || (blendToScene && NO_MOUNTAINS_SCENES.includes(blendToScene) && (blendProgress ?? 0) > 0.5); @@ -119,9 +128,7 @@ export function drawLandscape( const useCustomWater = CUSTOM_WATER_SCENES.includes(sceneType) || (blendToScene && CUSTOM_WATER_SCENES.includes(blendToScene) && (blendProgress ?? 0) > 0.5); - if (sceneType === 'ocean' || (blendToScene === 'ocean' && (blendProgress ?? 0) > 0.5)) { - drawOceanWaves(dc); - } else if (!useCustomWater) { + if (!useCustomWater) { drawWater(dc); } } diff --git a/frontend/src/lib/ts/parallax/scenes/city.ts b/frontend/src/lib/ts/parallax/scenes/city.ts index f5b2af7..9fd6570 100644 --- a/frontend/src/lib/ts/parallax/scenes/city.ts +++ b/frontend/src/lib/ts/parallax/scenes/city.ts @@ -1,69 +1,151 @@ import type { DrawContext } from '../types'; -export function drawCityBuildings(dc: DrawContext): void { +export function drawCleanCityBuildings(dc: DrawContext): void { const { ctx, width, height, state } = dc; const parallax = state.mouseX * 0.35; - const drawBuilding = (x: number, bWidth: number, bHeight: number, color: string, hasSpire: boolean = false) => { - const baseY = height * 0.8; - + // Helper: Draw varied clean building shapes + const drawBuildingShape = ( + bx: number, by: number, bw: number, bh: number, + color: string, windowColor: string, type: number, + stableSeedX: number + ) => { ctx.fillStyle = color; - ctx.fillRect(x + parallax, baseY - bHeight, bWidth, bHeight); + // Base + ctx.fillRect(bx, by - bh, bw, bh + height * 0.5); - const gradient = ctx.createLinearGradient(x + parallax, 0, x + parallax + bWidth, 0); - gradient.addColorStop(0, 'rgba(255, 255, 255, 0.1)'); - gradient.addColorStop(0.5, 'rgba(255, 255, 255, 0.2)'); - gradient.addColorStop(1, 'rgba(255, 255, 255, 0.05)'); - ctx.fillStyle = gradient; - ctx.fillRect(x + parallax, baseY - bHeight, bWidth, bHeight); - - ctx.fillStyle = 'rgba(255, 255, 200, 0.8)'; - const windowCols = Math.floor(bWidth / 18); - const windowRows = Math.floor(bHeight / 25); - - for (let row = 0; row < windowRows; row++) { - for (let col = 0; col < windowCols; col++) { - if (Math.random() > 0.15) { - ctx.fillStyle = Math.random() > 0.7 ? 'rgba(255, 255, 200, 0.9)' : 'rgba(200, 220, 255, 0.6)'; - ctx.fillRect( - x + parallax + col * 18 + 5, - baseY - bHeight + row * 25 + 8, - 10, 12 - ); - } - } + // Roof variations + if (type === 1) { // Slanted + ctx.beginPath(); + ctx.moveTo(bx, by - bh); + ctx.lineTo(bx + bw, by - bh); + ctx.lineTo(bx + bw, by - bh - 20); + ctx.fill(); + } else if (type === 2) { // Spire + ctx.beginPath(); + ctx.moveTo(bx + bw/2 - 5, by - bh); + ctx.lineTo(bx + bw/2, by - bh - 40); + ctx.lineTo(bx + bw/2 + 5, by - bh); + ctx.fill(); + // Flag or light on tip? + ctx.fillStyle = '#CFD8DC'; + ctx.fillRect(bx + bw/2 - 1, by - bh - 40, 2, 40); + ctx.fillStyle = color; + } else if (type === 3) { // Tri-peak + ctx.beginPath(); + ctx.moveTo(bx, by - bh); + ctx.lineTo(bx + bw * 0.25, by - bh - 15); + ctx.lineTo(bx + bw * 0.5, by - bh); + ctx.lineTo(bx + bw * 0.75, by - bh - 15); + ctx.lineTo(bx + bw, by - bh); + ctx.fill(); } - if (hasSpire) { - ctx.fillStyle = '#90A4AE'; - ctx.beginPath(); - ctx.moveTo(x + parallax + bWidth / 2, baseY - bHeight - 40); - ctx.lineTo(x + parallax + bWidth / 2 - 8, baseY - bHeight); - ctx.lineTo(x + parallax + bWidth / 2 + 8, baseY - bHeight); - ctx.closePath(); - ctx.fill(); + // Windows (Glassy/Office style) + const cols = Math.floor(bw / 12); + const rows = Math.floor(bh / 18); + const padding = 4; + + ctx.fillStyle = windowColor; + for(let r=0; r 0.8) continue; // Some gaps + + // Reflection flicker + if (Math.abs(Math.cos(seed)) > 0.9) { + ctx.fillStyle = 'rgba(224, 247, 250, 0.6)'; // Bright reflection + ctx.fillRect(bx + c*12 + padding, by - bh + r*18 + padding, 8, 12); + ctx.fillStyle = windowColor; + } else { + ctx.fillRect(bx + c*12 + padding, by - bh + r*18 + padding, 8, 12); + } + } } }; - drawBuilding(width * 0.02, 55, height * 0.28, '#607D8B'); - drawBuilding(width * 0.08, 70, height * 0.42, '#78909C', true); - drawBuilding(width * 0.18, 50, height * 0.32, '#546E7A'); - drawBuilding(width * 0.25, 85, height * 0.55, '#455A64', true); - drawBuilding(width * 0.38, 60, height * 0.38, '#607D8B'); - drawBuilding(width * 0.48, 75, height * 0.48, '#78909C', true); - drawBuilding(width * 0.58, 55, height * 0.35, '#546E7A'); - drawBuilding(width * 0.68, 90, height * 0.52, '#455A64', true); - drawBuilding(width * 0.8, 65, height * 0.4, '#607D8B'); - drawBuilding(width * 0.9, 50, height * 0.3, '#78909C'); + // -- Background Layer -- (Far, light blue/grey) + const bgParallax = state.mouseX * 0.15; + const bgBaseY = height * 0.78; + let currentBgX = -100; + while(currentBgX < width + 100) { + const seed = currentBgX * 0.1; + const bW = 60 + (Math.abs(Math.sin(seed)) * 40); + const bH = height * 0.25 + (Math.abs(Math.cos(seed)) * height * 0.25); + const gap = 10 + Math.abs(Math.sin(seed * 2)) * 20; + + // Color: Cool Greys/Blues + const hue = 200 + Math.floor(Math.abs(Math.sin(seed)*20)); + const color = `hsl(${hue}, 15%, 75%)`; + + drawBuildingShape( + currentBgX + bgParallax, bgBaseY, bW, bH, + color, 'rgba(225, 245, 254, 0.3)', + Math.floor(Math.abs(Math.sin(seed * 5)) * 4), + currentBgX + ); + currentBgX += bW + gap; + } + + // -- Middle Layer -- (Mid distance, slightly darker/vibrant) + const midParallax = state.mouseX * 0.25; + const midBaseY = height * 0.8; + let currentMidX = -50; + while(currentMidX < width + 50) { + const seed = currentMidX * 0.17; + const bW = 70 + (Math.abs(Math.sin(seed)) * 50); + const bH = height * 0.2 + (Math.abs(Math.cos(seed)) * height * 0.2); + const gap = 30 + Math.abs(Math.sin(seed * 4)) * 40; + + const hue = 210 + Math.floor(Math.abs(Math.cos(seed)*20)); + const color = `hsl(${hue}, 25%, 65%)`; + + drawBuildingShape( + currentMidX + midParallax, midBaseY, bW, bH, + color, 'rgba(225, 245, 254, 0.5)', + Math.floor(Math.abs(Math.cos(seed * 7)) * 4), + currentMidX + ); + currentMidX += bW + gap; + } + + // -- Foreground Layer -- (Close, detailed) + const fgBaseY = height * 0.82; + let currentFgX = 20; + while(currentFgX < width - 20) { + const seed = currentFgX * 0.33; + const bW = 90 + (Math.abs(Math.sin(seed)) * 60); + const bH = height * 0.15 + (Math.abs(Math.cos(seed)) * height * 0.2); + const gap = 80 + Math.abs(Math.sin(seed * 1.5)) * 80; + + const hue = 220 + Math.floor(Math.abs(Math.sin(seed)*20)); + const color = `hsl(${hue}, 30%, 55%)`; + + ctx.save(); + drawBuildingShape( + currentFgX + parallax, fgBaseY, bW, bH, + color, 'rgba(255, 255, 255, 0.7)', + Math.floor(Math.abs(Math.sin(seed * 9)) * 4), + currentFgX + ); + ctx.restore(); + + currentFgX += bW + gap; + } } export function drawStreet(dc: DrawContext): void { const { ctx, width, height } = dc; const time = Date.now() * 0.001; - ctx.fillStyle = '#424242'; + // Road styling: Clean asphalt + ctx.fillStyle = '#455A64'; ctx.fillRect(0, height * 0.82, width, height * 0.08); + // Markings: Clean white/yellow ctx.strokeStyle = '#FFEB3B'; ctx.lineWidth = 3; ctx.setLineDash([30, 20]); @@ -73,117 +155,87 @@ export function drawStreet(dc: DrawContext): void { ctx.stroke(); ctx.setLineDash([]); + // Sidewalk (Clean) + ctx.fillStyle = '#90A4AE'; + ctx.fillRect(0, height * 0.90, width, height * 0.1); + // Curb + ctx.fillStyle = '#78909C'; + ctx.fillRect(0, height * 0.90, width, height * 0.01); + + // Cars - Clean, modern, electric? const drawCar = (baseX: number, y: number, color: string, direction: number) => { - const x = ((baseX + time * 50 * direction) % (width + 100)) - 50; + const x = ((baseX + time * 60 * direction) % (width + 200)) - 100; + // Body ctx.fillStyle = color; ctx.beginPath(); - ctx.roundRect(x, y, 40, 15, 3); - ctx.fill(); - - ctx.fillStyle = color; - ctx.beginPath(); - ctx.roundRect(x + 8, y - 10, 24, 12, 3); + ctx.roundRect(x, y, 40, 14, 4); ctx.fill(); + // Cabin/Window ctx.fillStyle = '#81D4FA'; - ctx.fillRect(x + 10, y - 8, 9, 8); - ctx.fillRect(x + 21, y - 8, 9, 8); - - ctx.fillStyle = '#212121'; ctx.beginPath(); - ctx.arc(x + 10, y + 15, 5, 0, Math.PI * 2); - ctx.arc(x + 30, y + 15, 5, 0, Math.PI * 2); + ctx.roundRect(x + 10, y - 8, 20, 10, 2); + ctx.fill(); + + // Wheels + ctx.fillStyle = '#263238'; + ctx.beginPath(); + ctx.arc(x + 10, y + 14, 5, 0, Math.PI * 2); + ctx.arc(x + 30, y + 14, 5, 0, Math.PI * 2); + ctx.fill(); + + // Headlights + if(direction > 0) { + ctx.fillStyle = '#FFF59D'; + ctx.beginPath(); + ctx.arc(x + 40, y + 5, 2, 0, Math.PI * 2); + ctx.fill(); + } else { + ctx.fillStyle = '#FFF59D'; + ctx.beginPath(); + ctx.arc(x, y + 5, 2, 0, Math.PI * 2); + ctx.fill(); + } + }; + + drawCar(width * 0.1, height * 0.83, '#4FC3F7', 1); + drawCar(width * 0.5, height * 0.83, '#E1BEE7', 1); + drawCar(width * 0.3, height * 0.87, '#81C784', -1); + drawCar(width * 0.8, height * 0.87, '#FFF176', -1); +} + +export function drawCityClouds(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + + // Copy logic from drawBase but customized + const drawCloud = (x: number, y: number, scale: number) => { + ctx.fillStyle = 'rgba(255, 255, 255, 0.9)'; // Clean white clouds + ctx.beginPath(); + ctx.ellipse(x, y, 60 * scale, 35 * scale, 0, 0, Math.PI * 2); + ctx.fill(); + ctx.beginPath(); + ctx.ellipse(x + 50 * scale, y + 10 * scale, 50 * scale, 30 * scale, 0, 0, Math.PI * 2); + ctx.fill(); + ctx.beginPath(); + ctx.ellipse(x - 40 * scale, y + 5 * scale, 45 * scale, 25 * scale, 0, 0, Math.PI * 2); + ctx.fill(); + ctx.beginPath(); + ctx.ellipse(x + 20 * scale, y - 15 * scale, 40 * scale, 25 * scale, 0, 0, Math.PI * 2); ctx.fill(); }; - drawCar(width * 0.1, height * 0.83, '#E53935', 1); - drawCar(width * 0.5, height * 0.83, '#1E88E5', 1); - drawCar(width * 0.3, height * 0.87, '#43A047', -1); - drawCar(width * 0.8, height * 0.87, '#FDD835', -1); -} + const cloudOffsetX = state.mouseX * 0.5 - state.scrollY * 0.03; + const cloudOffsetY = state.scrollY * 0.1; -export function drawAirplane(dc: DrawContext): void { - const { ctx, width, height, state } = dc; - const time = Date.now() * 0.0003; - const parallax = state.mouseX * 0.1; - - const x = (time * width) % (width + 200) - 100; - const y = height * 0.15 + Math.sin(time * 5) * 10; - - ctx.save(); - ctx.translate(x + parallax, y); - - ctx.fillStyle = '#ECEFF1'; - ctx.beginPath(); - ctx.ellipse(0, 0, 40, 8, 0, 0, Math.PI * 2); - ctx.fill(); - - ctx.beginPath(); - ctx.ellipse(45, 0, 12, 6, 0, 0, Math.PI * 2); - ctx.fill(); - - ctx.fillStyle = '#B0BEC5'; - ctx.beginPath(); - ctx.moveTo(-10, 0); - ctx.lineTo(-30, -35); - ctx.lineTo(10, -35); - ctx.lineTo(15, 0); - ctx.closePath(); - ctx.fill(); - ctx.beginPath(); - ctx.moveTo(-10, 0); - ctx.lineTo(-30, 35); - ctx.lineTo(10, 35); - ctx.lineTo(15, 0); - ctx.closePath(); - ctx.fill(); - - ctx.fillStyle = '#E53935'; - ctx.beginPath(); - ctx.moveTo(-35, 0); - ctx.lineTo(-50, -20); - ctx.lineTo(-30, 0); - ctx.closePath(); - ctx.fill(); - - ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)'; - ctx.lineWidth = 3; - ctx.beginPath(); - ctx.moveTo(-50, 0); - ctx.lineTo(-150, 3); - ctx.stroke(); - - ctx.restore(); -} - -export function drawParks(dc: DrawContext): void { - const { ctx, width, height } = dc; - - ctx.fillStyle = '#81C784'; - - const parks = [ - { x: width * 0.05, y: height * 0.81, rx: 25, ry: 8 }, - { x: width * 0.35, y: height * 0.81, rx: 30, ry: 10 }, - { x: width * 0.75, y: height * 0.81, rx: 35, ry: 9 }, - ]; - - parks.forEach(p => { - ctx.beginPath(); - ctx.ellipse(p.x, p.y, p.rx, p.ry, 0, 0, Math.PI * 2); - ctx.fill(); - - ctx.fillStyle = '#4CAF50'; - ctx.beginPath(); - ctx.arc(p.x - 10, p.y - 5, 8, 0, Math.PI * 2); - ctx.arc(p.x + 10, p.y - 3, 6, 0, Math.PI * 2); - ctx.fill(); - }); + // Placed higher to avoid intersecting with tall skyscrapers + drawCloud(width * 0.1 + cloudOffsetX, height * 0.1 + cloudOffsetY, 1.1); + drawCloud(width * 0.4 + cloudOffsetX * 0.8, height * 0.08 + cloudOffsetY * 0.8, 0.8); + drawCloud(width * 0.8 + cloudOffsetX * 0.6, height * 0.12 + cloudOffsetY * 0.7, 1.2); } export function drawCityScene(dc: DrawContext): void { - drawAirplane(dc); - drawCityBuildings(dc); - drawParks(dc); + drawCityClouds(dc); + drawCleanCityBuildings(dc); drawStreet(dc); } diff --git a/frontend/src/lib/ts/parallax/scenes/forest.ts b/frontend/src/lib/ts/parallax/scenes/forest.ts index 9c01ed3..c3f084b 100644 --- a/frontend/src/lib/ts/parallax/scenes/forest.ts +++ b/frontend/src/lib/ts/parallax/scenes/forest.ts @@ -1,168 +1,665 @@ import type { DrawContext } from '../types'; import { getSceneColors } from '../colors'; -export function drawForestTrees(dc: DrawContext): void { - const { ctx, width, height, state } = dc; - const colors = getSceneColors(state); - const parallax = state.mouseX * 0.8 + state.scrollY * 0.5; - - const drawPineTree = (x: number, y: number, scale: number, dark: boolean) => { - ctx.fillStyle = dark ? '#3E2723' : '#5D4037'; - ctx.fillRect(x - 6 * scale + parallax, y, 12 * scale, 35 * scale); - - const foliageColor = dark ? colors.treeDark : colors.treeLight; - ctx.fillStyle = foliageColor; - - for (let layer = 0; layer < 4; layer++) { - const layerY = y + 5 * scale - layer * 22 * scale; - const layerWidth = (35 - layer * 5) * scale; - ctx.beginPath(); - ctx.moveTo(x + parallax, layerY - 25 * scale); - ctx.lineTo(x - layerWidth + parallax, layerY); - ctx.lineTo(x + layerWidth + parallax, layerY); - ctx.closePath(); - ctx.fill(); - } - }; - - const treePositions = [ - { x: width * 0.05, y: height * 0.68, scale: 0.7, dark: true }, - { x: width * 0.15, y: height * 0.66, scale: 0.8, dark: true }, - { x: width * 0.28, y: height * 0.67, scale: 0.6, dark: true }, - { x: width * 0.42, y: height * 0.65, scale: 0.75, dark: true }, - { x: width * 0.58, y: height * 0.68, scale: 0.65, dark: true }, - { x: width * 0.72, y: height * 0.66, scale: 0.7, dark: true }, - { x: width * 0.88, y: height * 0.67, scale: 0.8, dark: true }, - { x: width * 0.95, y: height * 0.68, scale: 0.6, dark: true }, - { x: width * 0.08, y: height * 0.76, scale: 1.4, dark: false }, - { x: width * 0.22, y: height * 0.78, scale: 1.2, dark: false }, - { x: width * 0.38, y: height * 0.74, scale: 1.6, dark: false }, - { x: width * 0.52, y: height * 0.77, scale: 1.3, dark: false }, - { x: width * 0.68, y: height * 0.75, scale: 1.5, dark: false }, - { x: width * 0.82, y: height * 0.78, scale: 1.1, dark: false }, - { x: width * 0.94, y: height * 0.76, scale: 1.4, dark: false }, - ]; - - treePositions.forEach((t) => drawPineTree(t.x, t.y, t.scale, t.dark)); -} - -export function drawMushrooms(dc: DrawContext): void { - const { ctx, width, height, state } = dc; - const parallax = state.mouseX * 0.9 + state.scrollY * 0.6; - - const drawMushroom = (x: number, y: number, scale: number, isRed: boolean) => { - ctx.fillStyle = '#F5F5DC'; - ctx.beginPath(); - ctx.ellipse(x + parallax, y + 8 * scale, 6 * scale, 10 * scale, 0, 0, Math.PI * 2); - ctx.fill(); - - ctx.fillStyle = isRed ? '#D32F2F' : '#8D6E63'; - ctx.beginPath(); - ctx.ellipse(x + parallax, y - 2 * scale, 12 * scale, 8 * scale, 0, Math.PI, Math.PI * 2); - ctx.fill(); - - if (isRed) { - ctx.fillStyle = '#FFFFFF'; - ctx.beginPath(); - ctx.arc(x + parallax - 4, y - 4 * scale, 2 * scale, 0, Math.PI * 2); - ctx.arc(x + parallax + 5, y - 3 * scale, 1.5 * scale, 0, Math.PI * 2); - ctx.fill(); - } - }; - - drawMushroom(width * 0.15, height * 0.84, 1.0, true); - drawMushroom(width * 0.35, height * 0.86, 0.8, false); - drawMushroom(width * 0.52, height * 0.83, 1.2, true); - drawMushroom(width * 0.78, height * 0.85, 0.9, false); - drawMushroom(width * 0.88, height * 0.84, 1.1, true); -} - -export function drawDeer(dc: DrawContext): void { - const { ctx, width, height, state } = dc; - const time = Date.now() * 0.001; - const parallax = state.mouseX * 0.5; - - const x = width * 0.6 + parallax; - const y = height * 0.72; - const headBob = Math.sin(time * 2) * 3; - - ctx.fillStyle = '#8D6E63'; - ctx.beginPath(); - ctx.ellipse(x, y, 35, 25, 0, 0, Math.PI * 2); - ctx.fill(); - - ctx.fillStyle = '#6D4C41'; - ctx.fillRect(x - 20, y + 15, 8, 30); - ctx.fillRect(x - 5, y + 18, 8, 28); - ctx.fillRect(x + 10, y + 15, 8, 30); - ctx.fillRect(x + 25, y + 18, 8, 28); - - ctx.fillStyle = '#8D6E63'; - ctx.beginPath(); - ctx.ellipse(x + 40, y - 15 + headBob, 12, 18, 0.3, 0, Math.PI * 2); - ctx.fill(); - ctx.beginPath(); - ctx.ellipse(x + 55, y - 25 + headBob, 10, 12, 0.2, 0, Math.PI * 2); - ctx.fill(); - - ctx.strokeStyle = '#5D4037'; - ctx.lineWidth = 3; - ctx.lineCap = 'round'; - ctx.beginPath(); - ctx.moveTo(x + 50, y - 35 + headBob); - ctx.lineTo(x + 45, y - 50 + headBob); - ctx.lineTo(x + 40, y - 45 + headBob); - ctx.moveTo(x + 45, y - 50 + headBob); - ctx.lineTo(x + 50, y - 55 + headBob); - ctx.stroke(); - ctx.beginPath(); - ctx.moveTo(x + 60, y - 35 + headBob); - ctx.lineTo(x + 65, y - 50 + headBob); - ctx.lineTo(x + 70, y - 45 + headBob); - ctx.moveTo(x + 65, y - 50 + headBob); - ctx.lineTo(x + 60, y - 55 + headBob); - ctx.stroke(); - - ctx.fillStyle = '#1a1a1a'; - ctx.beginPath(); - ctx.arc(x + 60, y - 25 + headBob, 3, 0, Math.PI * 2); - ctx.fill(); -} - -export function drawLightRays(dc: DrawContext): void { - const { ctx, width, height, state } = dc; - const time = Date.now() * 0.0005; +// --- Assets / Drawing Helpers --- +function drawDetailedPine(ctx: CanvasRenderingContext2D, x: number, y: number, scale: number, color: string) { ctx.save(); - ctx.globalAlpha = 0.15 + Math.sin(time) * 0.05; + ctx.translate(x, y); + ctx.scale(scale, scale); - const gradient = ctx.createLinearGradient(width * 0.7, 0, width * 0.5, height * 0.7); - gradient.addColorStop(0, 'rgba(255, 255, 200, 0.8)'); - gradient.addColorStop(1, 'rgba(255, 255, 200, 0)'); - - ctx.fillStyle = gradient; + // Trunk + ctx.fillStyle = '#3E2723'; ctx.beginPath(); - ctx.moveTo(width * 0.75, 0); - ctx.lineTo(width * 0.85, 0); - ctx.lineTo(width * 0.55, height * 0.7); - ctx.lineTo(width * 0.45, height * 0.7); - ctx.closePath(); + ctx.moveTo(-4, 0); + ctx.lineTo(-1, -120); + ctx.lineTo(1, -120); + ctx.lineTo(4, 0); ctx.fill(); + // Foliage (Stylized swoops) + ctx.fillStyle = color; + const layers = 7; + for(let i=0; i { + ctx.moveTo(b.x + b.r, b.y); + ctx.arc(b.x, b.y, b.r, 0, Math.PI*2); + }); + ctx.fill(); + + // Highlight leaves (Top lit) + ctx.fillStyle = 'rgba(255,255,255,0.15)'; + ctx.beginPath(); + blobs.forEach(b => { + ctx.moveTo(b.x + b.r*0.8, b.y - b.r*0.5); + ctx.arc(b.x, b.y - b.r*0.2, b.r*0.6, 0, Math.PI*2); + }); ctx.fill(); ctx.restore(); } -export function drawForestScene(dc: DrawContext): void { - drawLightRays(dc); - drawForestTrees(dc); - drawMushrooms(dc); - drawDeer(dc); +function drawBirchTree(ctx: CanvasRenderingContext2D, x: number, y: number, scale: number, seed: number) { + ctx.save(); + ctx.translate(x, y); + ctx.scale(scale, scale); + + let s = seed * 999; + const rnd = () => { s = (s * 1664525 + 1013904223) % 4294967296; return s / 4294967296; }; + + // Trunk (Slender, slightly curved) + ctx.fillStyle = '#EEE'; + ctx.beginPath(); + ctx.moveTo(-3, 0); + const curve = (rnd() - 0.5) * 10; + ctx.quadraticCurveTo(curve, -60, 0, -130); + ctx.quadraticCurveTo(curve + 2, -60, 3, 0); + ctx.fill(); + + // Black stripes (Horizontal jagged) + ctx.fillStyle = '#263238'; + for(let i=0; i<12; i++) { + const yPos = -10 - rnd() * 110; + const width = 4 + rnd() * 3; + ctx.beginPath(); + const drift = (yPos / -130) * curve; + ctx.ellipse(drift, yPos, width, 1.5 + rnd(), 0, 0, Math.PI*2); + ctx.fill(); + } + + // Leaves (Drooping branches) + ctx.fillStyle = '#CDDC39'; // Fresh green + for(let i=0; i<5; i++) { + const branchY = -50 - rnd() * 70; + const side = rnd() > 0.5 ? 1 : -1; + const len = 20 + rnd() * 20; + + ctx.save(); + ctx.translate((branchY / -130) * curve, branchY); + ctx.rotate(side * 0.5); + + // Dangle leaves + for(let j=0; j<8; j++) { + const lx = side * j * 3; + const ly = j * (2 + rnd()*2); + ctx.beginPath(); + ctx.ellipse(lx, ly, 3, 5, 0, 0, Math.PI*2); + ctx.fill(); + } + ctx.restore(); + } + ctx.restore(); +} + +// --- Environment --- + +function getBezierY(t: number, y0: number, y1: number, y2: number, y3: number): number { + const oneMinusT = 1 - t; + return ( + Math.pow(oneMinusT, 3) * y0 + + 3 * Math.pow(oneMinusT, 2) * t * y1 + + 3 * oneMinusT * Math.pow(t, 2) * y2 + + Math.pow(t, 3) * y3 + ); +} + +const HILL_CONFIG = { + back: { y0: 0.70, cp1: 0.65, cp2: 0.75, y3: 0.68 }, + mid: { y0: 0.78, cp1: 0.82, cp2: 0.76, y3: 0.79 }, + front: { y0: 0.88, cp1: 0.84, cp2: 0.94, y3: 0.86 } // Lowered slightly to ensure screen coverage +}; + +export function drawForestGround(dc: DrawContext): void { + const { ctx, width, height } = dc; + + // Background Hill + ctx.fillStyle = '#1B5E20'; // Deepest green + ctx.beginPath(); + ctx.moveTo(0, height * HILL_CONFIG.back.y0); + ctx.bezierCurveTo( + width * 0.3, height * HILL_CONFIG.back.cp1, + width * 0.7, height * HILL_CONFIG.back.cp2, + width, height * HILL_CONFIG.back.y3 + ); + ctx.lineTo(width, height); + ctx.lineTo(0, height); + ctx.fill(); + + // Midground Hill + ctx.fillStyle = '#2E7D32'; // Mid green + ctx.beginPath(); + ctx.moveTo(0, height * HILL_CONFIG.mid.y0); + ctx.bezierCurveTo( + width * 0.25, height * HILL_CONFIG.mid.cp1, + width * 0.65, height * HILL_CONFIG.mid.cp2, + width, height * HILL_CONFIG.mid.y3 + ); + ctx.lineTo(width, height); + ctx.lineTo(0, height); + ctx.fill(); + + // Foreground Hill + ctx.fillStyle = '#388E3C'; // Lighter green + ctx.beginPath(); + ctx.moveTo(0, height * HILL_CONFIG.front.y0); + ctx.bezierCurveTo( + width * 0.2, height * HILL_CONFIG.front.cp1, + width * 0.6, height * HILL_CONFIG.front.cp2, + width, height * HILL_CONFIG.front.y3 + ); + ctx.lineTo(width, height); + ctx.lineTo(0, height); + ctx.fill(); +} + +export function drawVegetation(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + const parallax = state.mouseX * 1.2; + + // Place vegetation on the front hill primarily + const plants = [0.05, 0.25, 0.5, 0.75, 0.9]; + + plants.forEach(tx => { + // Calculate ground y at this x position on the front hill + // Approximate t as x for simplicity in this case (rendering purposes) + const ty = getBezierY(tx, HILL_CONFIG.front.y0, HILL_CONFIG.front.cp1, HILL_CONFIG.front.cp2, HILL_CONFIG.front.y3); + const y = height * ty; + const x = width * tx; + + ctx.save(); + ctx.translate(x + parallax, y); + ctx.fillStyle = '#1B5E20'; + for(let i=-2; i<=2; i++) { + ctx.save(); + ctx.rotate(i * 0.3); + ctx.beginPath(); + ctx.ellipse(0, -15, 4, 18, 0, 0, Math.PI*2); + ctx.fill(); + ctx.restore(); + } + ctx.restore(); + }); +} + +export function drawMushrooms(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + const parallax = state.mouseX * 1.0; + + const drawShroom = (tx: number, scale: number) => { + // Snap to front hill + const ty = getBezierY(tx, HILL_CONFIG.front.y0, HILL_CONFIG.front.cp1, HILL_CONFIG.front.cp2, HILL_CONFIG.front.y3); + const y = height * ty; + const x = width * tx; + + ctx.save(); + ctx.translate(x + parallax, y); + ctx.scale(scale, scale); + + ctx.fillStyle = '#F5F5DC'; + ctx.fillRect(-3, -15, 6, 15); // Stem growing UP + + ctx.fillStyle = '#D32F2F'; // Red Cap + ctx.beginPath(); + ctx.arc(0, -15, 10, Math.PI, 0); + ctx.fill(); + + ctx.fillStyle = 'white'; // Dots + ctx.beginPath(); ctx.arc(-4, -19, 2, 0, Math.PI*2); ctx.fill(); + ctx.beginPath(); ctx.arc(4, -20, 1.5, 0, Math.PI*2); ctx.fill(); + + ctx.restore(); + }; + + drawShroom(0.15, 1.0); + drawShroom(0.35, 0.8); + drawShroom(0.8, 1.1); +} + +export function drawForestTrees(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + const parallax = state.mouseX * 0.8; + + // Helper to draw a layer grounded on a specific hill config + const drawLayer = (trees: any[], config: any, parallaxFactor: number) => { + trees.forEach((t, i) => { + // Apply parallax to X + const px = (t.x * width + parallax * parallaxFactor + i * 20) % (width + 200) - 100; + + // Calculate normalized X (0 to 1) for curve sampling based on screen position + // This ensures the tree rides the curve even as it moves with parallax + // We clamp it 0-1 for the bezier calculation effectively + let normX = px / width; + if (normX < 0) normX = 0; + if (normX > 1) normX = 1; + + const ty = getBezierY(normX, config.y0, config.cp1, config.cp2, config.y3); + + // Plant the tree slightly deeper (+0.02) to ensure no hovering on steep sections + const py = height * (ty + 0.02); + + if (t.type === 'pine') drawDetailedPine(ctx, px, py, t.scale, t.color || '#1B5E20'); + else if (t.type === 'broad') drawDetailedBroadleaf(ctx, px, py, t.scale, t.color || '#2E7D32'); + else drawBirchTree(ctx, px, py, t.scale, i * 50); + }); + }; + + // Layer 1: Deep Background -> Back Hill + const layer1 = [ + {x: 0.02, type: 'pine', scale: 0.4, color: '#1B5E20'}, + {x: 0.08, type: 'broad', scale: 0.42, color: '#1B5E20'}, + {x: 0.15, type: 'pine', scale: 0.38, color: '#1B5E20'}, + {x: 0.22, type: 'broad', scale: 0.4, color: '#1B5E20'}, + {x: 0.30, type: 'pine', scale: 0.45, color: '#1B5E20'}, + {x: 0.38, type: 'pine', scale: 0.35, color: '#1B5E20'}, + {x: 0.45, type: 'broad', scale: 0.4, color: '#1B5E20'}, + {x: 0.52, type: 'pine', scale: 0.48, color: '#1B5E20'}, + {x: 0.60, type: 'broad', scale: 0.42, color: '#1B5E20'}, + {x: 0.68, type: 'pine', scale: 0.4, color: '#1B5E20'}, + {x: 0.75, type: 'pine', scale: 0.37, color: '#1B5E20'}, + {x: 0.82, type: 'broad', scale: 0.4, color: '#1B5E20'}, + {x: 0.90, type: 'pine', scale: 0.45, color: '#1B5E20'}, + {x: 0.98, type: 'broad', scale: 0.41, color: '#1B5E20'} + ]; + drawLayer(layer1, HILL_CONFIG.back, 0.3); + + // Layer 2: Midground -> Mid Hill + const layer2 = [ + {x: 0.05, type: 'pine', scale: 0.6, color: '#2E7D32'}, + {x: 0.15, type: 'broad', scale: 0.65, color: '#388E3C'}, + {x: 0.25, type: 'birch', scale: 0.55}, + {x: 0.35, type: 'pine', scale: 0.7, color: '#2E7D32'}, + {x: 0.45, type: 'broad', scale: 0.6, color: '#388E3C'}, + {x: 0.55, type: 'birch', scale: 0.6}, + {x: 0.65, type: 'pine', scale: 0.58, color: '#2E7D32'}, + {x: 0.75, type: 'broad', scale: 0.75, color: '#388E3C'}, + {x: 0.85, type: 'pine', scale: 0.65, color: '#2E7D32'}, + {x: 0.95, type: 'birch', scale: 0.58}, + ]; + drawLayer(layer2, HILL_CONFIG.mid, 0.5); + + // Layer 3: Foreground -> Front Hill + const layer3 = [ + {x: 0.02, type: 'broad', scale: 1.1, color: '#43A047'}, + {x: 0.12, type: 'pine', scale: 1.3, color: '#4CAF50'}, + {x: 0.22, type: 'birch', scale: 0.9}, + {x: 0.35, type: 'broad', scale: 1.0, color: '#43A047'}, + {x: 0.50, type: 'pine', scale: 1.2, color: '#4CAF50'}, + {x: 0.62, type: 'birch', scale: 1.0}, + {x: 0.75, type: 'pine', scale: 1.4, color: '#4CAF50'}, + {x: 0.88, type: 'broad', scale: 1.1, color: '#43A047'}, + {x: 0.98, type: 'birch', scale: 0.95}, + ]; + drawLayer(layer3, HILL_CONFIG.front, 1.0); +} + +// --- Wildlife --- + +export function drawDeer(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + const time = Date.now() * 0.001; + const parallax = state.mouseX * 0.7; + + const x = width * 0.6 + parallax; + const y = height * 0.86; + const scale = 0.9; + + // Grazing animation + const grazing = Math.sin(time * 0.5) > 0.5; + const headY = grazing ? 25 : -40; + const headRot = grazing ? 0.5 : 0; + + ctx.save(); + ctx.translate(x, y); + ctx.scale(scale, scale); + + const gradient = ctx.createLinearGradient(0, -20, 0, 20); + gradient.addColorStop(0, '#8D6E63'); + gradient.addColorStop(1, '#5D4037'); + + const legColor = '#4E342E'; + + // Rear Leg L (Behind) + ctx.fillStyle = legColor; + ctx.beginPath(); + ctx.moveTo(-18, 0); ctx.lineTo(-20, 20); ctx.lineTo(-22, 40); ctx.lineTo(-15, 40); ctx.lineTo(-12, 10); + ctx.fill(); + // Front Leg L (Behind) + ctx.beginPath(); + ctx.moveTo(12, 0); ctx.lineTo(10, 25); ctx.lineTo(8, 40); ctx.lineTo(14, 40); ctx.lineTo(18, 10); + ctx.fill(); + + // Body + ctx.fillStyle = gradient; + ctx.beginPath(); + ctx.moveTo(-25, 0); + ctx.bezierCurveTo(-30, -10, -30, -30, 0, -25); // Back hump + ctx.lineTo(20, -20); + ctx.bezierCurveTo(35, -20, 35, 0, 20, 10); // Chest + ctx.lineTo(0, 12); // Belly + ctx.bezierCurveTo(-20, 15, -25, 10, -25, 0); + ctx.fill(); + + // Rear Leg R (Front) + ctx.fillStyle = '#6D4C41'; + ctx.beginPath(); + ctx.moveTo(-12, 5); ctx.lineTo(-10, 25); ctx.lineTo(-12, 42); ctx.lineTo(-5, 42); ctx.lineTo(-2, 10); + ctx.fill(); + // Front Leg R (Front) + ctx.beginPath(); + ctx.moveTo(18, 5); ctx.lineTo(20, 25); ctx.lineTo(22, 42); ctx.lineTo(28, 42); ctx.lineTo(24, 10); + ctx.fill(); + + // White Tail + ctx.fillStyle = '#FFF'; + ctx.beginPath(); + ctx.moveTo(-28, -5); ctx.lineTo(-32, -15); ctx.lineTo(-25, -10); + ctx.fill(); + + // Neck + ctx.fillStyle = gradient; + ctx.beginPath(); + ctx.moveTo(20, -15); + ctx.lineTo(25 + (grazing ? 20 : 0), headY + 5); + ctx.lineTo(15 + (grazing ? 20 : 0), headY + 5); + ctx.lineTo(12, 5); + ctx.fill(); + + // Head Group + ctx.save(); + ctx.translate(22 + (grazing ? 20 : 0), headY); + ctx.rotate(headRot); + + // Head shape + ctx.fillStyle = '#8D6E63'; + ctx.beginPath(); + ctx.ellipse(0, 0, 10, 6, 0.2, 0, Math.PI*2); + ctx.fill(); + // Nose + ctx.fillStyle = '#3E2723'; + ctx.beginPath(); ctx.arc(9, 2, 1.5, 0, Math.PI*2); ctx.fill(); + + // Ears + ctx.fillStyle = '#6D4C41'; + ctx.beginPath(); + ctx.ellipse(-8, -5, 3, 7, -0.3, 0, Math.PI*2); + ctx.fill(); + + // Antlers (Detailed) + ctx.strokeStyle = '#3E2723'; + ctx.lineWidth = 1.5; + ctx.lineCap = 'round'; + ctx.beginPath(); + ctx.moveTo(-4, -6); + ctx.bezierCurveTo(-5, -20, 0, -25, 5, -35); // Main beam + ctx.moveTo(0, -18); ctx.lineTo(5, -25); // Tine 1 + ctx.moveTo(2, -28); ctx.lineTo(-2, -32); // Tine 2 + ctx.stroke(); + + // Eye + ctx.fillStyle = '#1a1a1a'; + ctx.beginPath(); + ctx.arc(0, -2, 1.2, 0, Math.PI * 2); + ctx.fill(); + + ctx.restore(); + ctx.restore(); +} + +export function drawFox(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + const parallax = state.mouseX * 0.9; + const time = Date.now() * 0.001; + + const x = width * 0.4 + parallax; + const y = height * 0.9; // Sitting on ground + + ctx.save(); + ctx.translate(x, y); + ctx.scale(0.85, 0.85); + + // Tail Wag (Smoother) + const tailAngle = Math.sin(time * 3) * 0.15; + + // Tail + ctx.save(); + ctx.rotate(tailAngle); + ctx.fillStyle = '#E65100'; // Orange + ctx.beginPath(); + ctx.moveTo(-15, -5); + ctx.bezierCurveTo(-35, -25, -50, -5, -45, 0); // Fluffy curve top + ctx.bezierCurveTo(-35, 5, -20, 5, -15, 0); // Fluffy bottom + ctx.fill(); + // White Tip + ctx.fillStyle = '#FFF'; + ctx.beginPath(); + ctx.moveTo(-45, 0); + ctx.bezierCurveTo(-48, -2, -50, -5, -46, -8); + ctx.fill(); + ctx.restore(); + + // Body Sitting + const furGradient = ctx.createLinearGradient(0, -20, 0, 0); + furGradient.addColorStop(0, '#EF6C00'); + furGradient.addColorStop(1, '#FFF3E0'); // White belly fade + + ctx.fillStyle = furGradient; + ctx.beginPath(); + ctx.ellipse(0, -12, 14, 18, 0, 0, Math.PI*2); + ctx.fill(); + + // White Chest Patch + ctx.fillStyle = '#FFF'; + ctx.beginPath(); + ctx.ellipse(2, -10, 6, 10, 0.2, 0, Math.PI*2); + ctx.fill(); + + // Head + ctx.save(); + ctx.translate(4, -32); + + // Face shape + ctx.fillStyle = '#EF6C00'; + ctx.beginPath(); + ctx.moveTo(-8, -5); // Top left + ctx.lineTo(8, -5); // Top right + ctx.lineTo(12, 2); // Cheek right + ctx.lineTo(0, 10); // Snout tip + ctx.lineTo(-12, 2); // Cheek left + ctx.fill(); + + // White cheeks + ctx.fillStyle = '#FEFBE9'; + ctx.beginPath(); + ctx.moveTo(-12, 2); ctx.lineTo(-5, 5); ctx.lineTo(0, 10); // Left cheek + ctx.moveTo(12, 2); ctx.lineTo(5, 5); ctx.lineTo(0, 10); // Right cheek + ctx.fill(); + + // Nose + ctx.fillStyle = '#111'; + ctx.beginPath(); ctx.arc(0, 9, 1.5, 0, Math.PI*2); ctx.fill(); + + // Ears + ctx.fillStyle = '#E65100'; + ctx.beginPath(); + ctx.moveTo(-8, -5); ctx.lineTo(-10, -15); ctx.lineTo(-3, -5); + ctx.moveTo(8, -5); ctx.lineTo(10, -15); ctx.lineTo(3, -5); + ctx.fill(); + // Inner Ear + ctx.fillStyle = '#3E2723'; + ctx.beginPath(); + ctx.moveTo(-7, -6); ctx.lineTo(-9, -12); ctx.lineTo(-4, -6); + ctx.moveTo(7, -6); ctx.lineTo(9, -12); ctx.lineTo(4, -6); + ctx.fill(); + + // Eyes + ctx.fillStyle = '#111'; + ctx.beginPath(); + ctx.ellipse(-4, 0, 1.5, 2, 0, 0, Math.PI*2); + ctx.ellipse(4, 0, 1.5, 2, 0, 0, Math.PI*2); + ctx.fill(); + + ctx.restore(); + + // Paws (Dark) + ctx.fillStyle = '#3E2723'; + ctx.beginPath(); ctx.ellipse(-8, 5, 3, 5, 0, 0, Math.PI*2); ctx.fill(); + ctx.beginPath(); ctx.ellipse(8, 5, 3, 5, 0, 0, Math.PI*2); ctx.fill(); + + ctx.restore(); +} + +export function drawRabbit(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + const time = Date.now() * 0.003; + const parallax = state.mouseX * 1.0; + + const x = width * 0.2 + parallax; + const y = height * 0.92; + // Small hop + const hop = Math.abs(Math.sin(time)) * 6; + + ctx.save(); + ctx.translate(x, y - hop); + ctx.scale(0.7, 0.7); + + // Body (Rounder) + ctx.fillStyle = '#D7CCC8'; + ctx.beginPath(); + ctx.ellipse(0, 0, 14, 10, 0, 0, Math.PI*2); + ctx.fill(); + + // Cotton Tail + ctx.fillStyle = '#FFF'; + ctx.beginPath(); + ctx.arc(-14, 0, 4, 0, Math.PI*2); + ctx.fill(); + + // Head + ctx.fillStyle = '#D7CCC8'; + ctx.beginPath(); + ctx.arc(10, -5, 8, 0, Math.PI*2); + ctx.fill(); + + // Ears (Longer) + ctx.fillStyle = '#A1887F'; + ctx.beginPath(); + ctx.ellipse(8, -15, 2.5, 10, -0.2, 0, Math.PI*2); + ctx.fill(); + ctx.beginPath(); + ctx.ellipse(12, -15, 2.5, 10, 0.2, 0, Math.PI*2); + ctx.fill(); + + // Inner Ear Pink + ctx.fillStyle = '#FFCCBC'; + ctx.beginPath(); + ctx.ellipse(8, -15, 1, 8, -0.2, 0, Math.PI*2); + ctx.fill(); + + // Eye + ctx.fillStyle = '#111'; + ctx.beginPath(); + ctx.arc(12, -7, 1.2, 0, Math.PI*2); + ctx.fill(); + + ctx.restore(); +} + +export function drawForestBirds(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + const time = Date.now() * 0.001; + const parallax = state.mouseX * 0.3; + + const birds = [ + {x: 0.2, y: 0.3, offset: 0, s: 1}, + {x: 0.25, y: 0.28, offset: 1, s: 0.8}, + {x: 0.18, y: 0.32, offset: 2, s: 0.9}, + ]; + + ctx.fillStyle = '#263238'; + + birds.forEach(b => { + const bx = (width * b.x + time * 30 + parallax) % (width + 50) - 25; + const by = height * b.y + Math.sin(time * 2 + b.offset) * 10; + const s = b.s; + + ctx.save(); + ctx.translate(bx, by); + ctx.scale(s, s); + + // Body + ctx.beginPath(); + ctx.ellipse(0, 0, 3, 1.5, 0, 0, Math.PI*2); + ctx.fill(); + + // Wings (Flapping triangles) + const flap = Math.sin(time * 15 + b.offset); + ctx.beginPath(); + // Left Wing + ctx.moveTo(0, 0); + ctx.lineTo(-8, flap * 5 - 5); + ctx.lineTo(-2, 0); + // Right Wing + ctx.moveTo(0, 0); + ctx.lineTo(8, flap * 5 - 5); + ctx.lineTo(2, 0); + ctx.fill(); + + ctx.restore(); + }); +} + +export function drawForestScene(dc: DrawContext): void { + drawForestGround(dc); + drawForestTrees(dc); + drawVegetation(dc); + drawMushrooms(dc); + drawForestBirds(dc); + drawDeer(dc); + drawFox(dc); + drawRabbit(dc); } diff --git a/frontend/src/lib/ts/parallax/scenes/ocean.ts b/frontend/src/lib/ts/parallax/scenes/ocean.ts index 5ed1856..e4c265f 100644 --- a/frontend/src/lib/ts/parallax/scenes/ocean.ts +++ b/frontend/src/lib/ts/parallax/scenes/ocean.ts @@ -79,12 +79,13 @@ export function drawBoats(dc: DrawContext): void { drawSailboat(width * 0.9, height * 0.56, 0.8, 3); } + export function drawDolphins(dc: DrawContext): void { const { ctx, width, height } = dc; const time = Date.now() * 0.001; const drawDolphin = (baseX: number, baseY: number, scale: number, phase: number) => { - const cycleLength = 3; + const cycleLength = 3.5; const cycleProgress = ((time + phase) % cycleLength) / cycleLength; let jumpProgress = 0; @@ -94,104 +95,166 @@ export function drawDolphins(dc: DrawContext): void { return; } - const jumpHeight = 80 * scale * Math.sin(jumpProgress * Math.PI); - const x = baseX + (jumpProgress - 0.5) * 150 * scale; + const jumpHeight = 90 * scale * Math.sin(jumpProgress * Math.PI); + const x = baseX + (jumpProgress - 0.5) * 180 * scale; const y = baseY - jumpHeight; - const rotation = Math.cos(jumpProgress * Math.PI) * -0.8; + const rotation = Math.cos(jumpProgress * Math.PI) * -0.9; ctx.save(); ctx.translate(x, y); ctx.rotate(rotation); - ctx.fillStyle = '#546E7A'; + // Body - Sleeker, curved + ctx.fillStyle = '#607D8B'; ctx.beginPath(); - ctx.ellipse(0, 0, 30 * scale, 12 * scale, 0, 0, Math.PI * 2); + ctx.moveTo(0, 0); + ctx.bezierCurveTo(20 * scale, -15 * scale, 40 * scale, -5 * scale, 45 * scale, 0); // Upper back + ctx.bezierCurveTo(40 * scale, 10 * scale, 10 * scale, 10 * scale, 0, 0); // Belly ctx.fill(); + // Fin ctx.beginPath(); - ctx.moveTo(-5 * scale, -10 * scale); - ctx.lineTo(5 * scale, -25 * scale); - ctx.lineTo(12 * scale, -8 * scale); + ctx.moveTo(25 * scale, -10 * scale); + ctx.quadraticCurveTo(30 * scale, -25 * scale, 20 * scale, -20 * scale); + ctx.lineTo(22 * scale, -10 * scale); + ctx.fill(); + + // Tail + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(-10 * scale, -8 * scale); + ctx.lineTo(-5 * scale, 0); + ctx.lineTo(-10 * scale, 8 * scale); ctx.closePath(); ctx.fill(); - ctx.beginPath(); - ctx.moveTo(-28 * scale, 0); - ctx.lineTo(-45 * scale, -12 * scale); - ctx.lineTo(-35 * scale, 0); - ctx.lineTo(-45 * scale, 12 * scale); - ctx.closePath(); - ctx.fill(); - - ctx.beginPath(); - ctx.ellipse(35 * scale, 0, 12 * scale, 6 * scale, 0, 0, Math.PI * 2); - ctx.fill(); - + // Eye ctx.fillStyle = '#1a1a1a'; ctx.beginPath(); - ctx.arc(28 * scale, -3 * scale, 2 * scale, 0, Math.PI * 2); + ctx.arc(38 * scale, -2 * scale, 1.5 * scale, 0, Math.PI * 2); + ctx.fill(); + + // Beak (Bottlenose) + ctx.fillStyle = '#607D8B'; + ctx.beginPath(); + ctx.moveTo(44 * scale, 0); + ctx.quadraticCurveTo(48 * scale, -2 * scale, 50 * scale, 2 * scale); + ctx.lineTo(44 * scale, 4 * scale); ctx.fill(); ctx.restore(); }; - drawDolphin(width * 0.25, height * 0.75, 1.0, 0); - drawDolphin(width * 0.5, height * 0.78, 0.8, 1); + drawDolphin(width * 0.2, height * 0.75, 1.0, 0); + drawDolphin(width * 0.4, height * 0.78, 0.9, 1); + drawDolphin(width * 0.65, height * 0.72, 0.85, 2.2); // New dolphin +} + +export function drawFish(dc: DrawContext): void { + const { ctx, width, height } = dc; + const time = Date.now() * 0.001; + const S = 0.6; // Scale + + const drawSingleFish = (seed: number, baseX: number, baseY: number, color: string, speedBase: number) => { + // "Completely Random" Path (Approximated via Perlin-like noise summation) + const t = time * speedBase; + + // Horizontal: Variable speed drift + const dx = baseX + t * 40 + Math.sin(t * 0.3 + seed) * 40; + const moveX = ((dx % (width + 100)) + (width + 100)) % (width + 100) - 50; + + // Vertical: Complex combination of waves to simulate randomness + const moveY = baseY + + Math.sin(t * 0.7 + seed * 3) * 12 + + Math.cos(t * 1.9 + seed * 7) * 8 + + Math.sin(t * 3.1 + seed) * 4; + + // Rotation: Follow the chaotic derivative + const dy = ( + Math.cos(t * 0.7 + seed * 3) * 8.4 - + Math.sin(t * 1.9 + seed * 7) * 15.2 + + Math.cos(t * 3.1 + seed) * 12.4 + ) * 0.01; + + ctx.save(); + ctx.translate(moveX, moveY); + ctx.rotate(dy); // Point in direction of chaotic curve + + ctx.fillStyle = color; + ctx.beginPath(); + // Body + ctx.ellipse(0, 0, 10 * S, 5 * S, 0, 0, Math.PI*2); + ctx.fill(); + // Tail + ctx.beginPath(); + // Flickering tail animation + const tailFlick = Math.sin(time * 15 + seed) * 2; + ctx.moveTo(-8 * S, 0); + ctx.lineTo(-14 * S, -5 * S + tailFlick); + ctx.lineTo(-14 * S, 5 * S + tailFlick); + ctx.fill(); + + ctx.restore(); + }; + + // Organized Schools + // School 1: Bright Orange (Mid-depth) + for(let i=0; i<10; i++) drawSingleFish(i*0.5, width * 0.1 - i*25, height * 0.82, '#FF7043', 0.8); + + // School 2: Yellow (Deeper) + for(let i=0; i<8; i++) drawSingleFish(i*0.8 + 10, width * 0.5 - i*30, height * 0.9, '#FFCA28', 0.6); + + // School 3: Pink (Upper) + for(let i=0; i<12; i++) drawSingleFish(i*0.3 + 20, width * 0.8 - i*20, height * 0.75, '#FF69B4', 1.0); + + // School 4: Purple (Scattered) + for(let i=0; i<7; i++) drawSingleFish(i*1.2 + 30, width * 0.3 - i*50, height * 0.88, '#AB47BC', 0.7); + + // School 5: Green (Fast) + for(let i=0; i<9; i++) drawSingleFish(i*0.4 + 40, width * 0.05 - i*35, height * 0.85, '#66BB6A', 1.2); } export function drawSeagulls(dc: DrawContext): void { const { ctx, width, height, state } = dc; - const time = Date.now() * 0.003; - const parallax = state.mouseX * 0.2; + const time = Date.now() * 0.002; + const parallax = state.mouseX * 0.15; const drawSeagull = (x: number, y: number, scale: number, phase: number) => { - const wingFlap = Math.sin(time * 3 + phase) * 0.4; - + const flap = Math.sin(time * 4 + phase) * 5; + ctx.save(); ctx.translate(x + parallax, y); - - ctx.fillStyle = '#FFFFFF'; - ctx.beginPath(); - ctx.ellipse(0, 0, 10 * scale, 5 * scale, 0, 0, Math.PI * 2); - ctx.fill(); - - ctx.strokeStyle = '#FFFFFF'; - ctx.lineWidth = 3 * scale; + ctx.scale(scale, scale); + + ctx.strokeStyle = '#ECEFF1'; // Off-white, not pure white for depth + ctx.lineWidth = 2.5; ctx.lineCap = 'round'; - ctx.beginPath(); - ctx.moveTo(-12 * scale, 0); - ctx.quadraticCurveTo(-18 * scale, -15 * scale * (1 + wingFlap), -25 * scale, -5 * scale); - ctx.stroke(); - ctx.beginPath(); - ctx.moveTo(12 * scale, 0); - ctx.quadraticCurveTo(18 * scale, -15 * scale * (1 + wingFlap), 25 * scale, -5 * scale); - ctx.stroke(); + ctx.lineJoin = 'round'; - ctx.fillStyle = '#FFFFFF'; ctx.beginPath(); - ctx.arc(12 * scale, -2 * scale, 4 * scale, 0, Math.PI * 2); - ctx.fill(); - - ctx.fillStyle = '#FF9800'; - ctx.beginPath(); - ctx.moveTo(16 * scale, -2 * scale); - ctx.lineTo(22 * scale, 0); - ctx.lineTo(16 * scale, 2 * scale); - ctx.closePath(); - ctx.fill(); + // Left wing + ctx.moveTo(-15, -5 + flap); + ctx.quadraticCurveTo(-8, -15 + flap, 0, 0); + // Right wing + ctx.quadraticCurveTo(8, -15 + flap, 15, -5 + flap); + ctx.stroke(); ctx.restore(); }; - drawSeagull(width * 0.15, height * 0.2, 1.0, 0); - drawSeagull(width * 0.35, height * 0.15, 0.8, 1); - drawSeagull(width * 0.55, height * 0.22, 1.2, 2); - drawSeagull(width * 0.8, height * 0.18, 0.9, 3); + // Repositioned seagulls + drawSeagull(width * 0.1, height * 0.1, 0.9, 0); + drawSeagull(width * 0.25, height * 0.18, 0.7, 1); + drawSeagull(width * 0.45, height * 0.08, 0.8, 2.5); + drawSeagull(width * 0.7, height * 0.15, 1.0, 1.2); + drawSeagull(width * 0.85, height * 0.22, 0.85, 3.5); } export function drawOceanScene(dc: DrawContext): void { + drawOceanWaves(dc); drawSeagulls(dc); drawBoats(dc); drawDolphins(dc); + drawFish(dc); } diff --git a/frontend/src/lib/ts/parallax/scenes/pollutedCity.ts b/frontend/src/lib/ts/parallax/scenes/pollutedCity.ts index 579474e..80d249e 100644 --- a/frontend/src/lib/ts/parallax/scenes/pollutedCity.ts +++ b/frontend/src/lib/ts/parallax/scenes/pollutedCity.ts @@ -4,46 +4,141 @@ export function drawSmoggyBuildings(dc: DrawContext): void { const { ctx, width, height, state } = dc; const parallax = state.mouseX * 0.35; - const drawBuilding = (x: number, bWidth: number, bHeight: number) => { - const baseY = height * 0.8; + // Helper to draw varied building shapes (Geometry matched to Clean City) + const drawBuildingShape = ( + bx: number, by: number, bw: number, bh: number, + color: string, windowColor: string, type: number, + stableSeedX: number + ) => { + ctx.fillStyle = color; + // Base + ctx.fillRect(bx, by - bh, bw, bh + height * 0.5); - ctx.fillStyle = '#37474F'; - ctx.fillRect(x + parallax, baseY - bHeight, bWidth, bHeight); + // Roof variations (Matched to Clean City) + if (type === 1) { // Slanted + ctx.beginPath(); + ctx.moveTo(bx, by - bh); + ctx.lineTo(bx + bw, by - bh); + ctx.lineTo(bx + bw, by - bh - 20); + ctx.fill(); + } else if (type === 2) { // Spire (Matched Clean City) + ctx.beginPath(); + ctx.moveTo(bx + bw/2 - 5, by - bh); + ctx.lineTo(bx + bw/2, by - bh - 40); + ctx.lineTo(bx + bw/2 + 5, by - bh); + ctx.fill(); + // Flag/Light (Red for pollution warning?) + ctx.fillStyle = '#B71C1C'; + ctx.fillRect(bx + bw/2 - 1, by - bh - 40, 2, 40); + ctx.fillStyle = color; + } else if (type === 3) { // Tri-peak (Matched Clean City) + ctx.beginPath(); + ctx.moveTo(bx, by - bh); + ctx.lineTo(bx + bw * 0.25, by - bh - 15); + ctx.lineTo(bx + bw * 0.5, by - bh); + ctx.lineTo(bx + bw * 0.75, by - bh - 15); + ctx.lineTo(bx + bw, by - bh); + ctx.fill(); + } - ctx.fillStyle = 'rgba(0, 0, 0, 0.2)'; - ctx.fillRect(x + parallax, baseY - bHeight * 0.8, bWidth * 0.5, bHeight * 0.3); - ctx.fillRect(x + parallax + bWidth * 0.6, baseY - bHeight * 0.5, bWidth * 0.3, bHeight * 0.2); - - const windowCols = Math.floor(bWidth / 18); - const windowRows = Math.floor(bHeight / 25); - - for (let row = 0; row < windowRows; row++) { - for (let col = 0; col < windowCols; col++) { - // Deterministic "randomness" based on position to stop flashing - const seed = x + col * 13 + row * 71; - const isBroken = Math.abs(Math.sin(seed)) > 0.85; - const isLit = Math.cos(seed) > 0.1; - - if (!isBroken) { - ctx.fillStyle = isLit ? 'rgba(255, 180, 100, 0.5)' : 'rgba(50, 50, 50, 0.8)'; - ctx.fillRect( - x + parallax + col * 18 + 5, - baseY - bHeight + row * 25 + 8, - 10, 12 - ); - } - } + // Windows (Matched Clean City: 8x12) + const cols = Math.floor(bw / 12); + const rows = Math.floor(bh / 18); + const padding = 4; + + ctx.fillStyle = windowColor; + for(let r=0; r 0.8) continue; // Some gaps + + // Flicker logic + if (Math.abs(Math.cos(seed)) > 0.92) { + ctx.fillStyle = 'rgba(255, 200, 100, 0.4)'; // Yellowish light + ctx.fillRect(bx + c*12 + padding, by - bh + r*18 + padding, 8, 12); + ctx.fillStyle = windowColor; + } else { + ctx.fillRect(bx + c*12 + padding, by - bh + r*18 + padding, 8, 12); + } + } } }; - drawBuilding(width * 0.02, 60, height * 0.35); - drawBuilding(width * 0.1, 75, height * 0.48); - drawBuilding(width * 0.22, 55, height * 0.38); - drawBuilding(width * 0.32, 90, height * 0.55); - drawBuilding(width * 0.48, 65, height * 0.42); - drawBuilding(width * 0.58, 80, height * 0.52); - drawBuilding(width * 0.72, 55, height * 0.36); - drawBuilding(width * 0.82, 70, height * 0.45); + // -- Background Layer -- (Darker, slower) + const bgParallax = state.mouseX * 0.15; + const bgBaseY = height * 0.78; + let currentBgX = -100; + while(currentBgX < width + 100) { + const seed = currentBgX * 0.1; + const bW = 60 + (Math.abs(Math.sin(seed)) * 40); + const bH = height * 0.25 + (Math.abs(Math.cos(seed)) * height * 0.25); + const gap = 10 + Math.abs(Math.sin(seed * 2)) * 20; + + // Exact same type distribution as Clean City BG + const type = Math.floor(Math.abs(Math.sin(seed * 5)) * 4); + + drawBuildingShape( + currentBgX + bgParallax, bgBaseY, bW, bH, + '#1f292e', 'rgba(255, 255, 255, 0.03)', + type, currentBgX + ); + currentBgX += bW + gap; + } + + // -- Middle Layer -- + const midParallax = state.mouseX * 0.25; + const midBaseY = height * 0.8; + let currentMidX = -50; + while(currentMidX < width + 50) { + const seed = currentMidX * 0.17; + const bW = 70 + (Math.abs(Math.sin(seed)) * 50); + const bH = height * 0.2 + (Math.abs(Math.cos(seed)) * height * 0.2); + const gap = 30 + Math.abs(Math.sin(seed * 4)) * 40; + + // Exact same type distribution as Clean City Mid + const type = Math.floor(Math.abs(Math.cos(seed * 7)) * 4); + + drawBuildingShape( + currentMidX + midParallax, midBaseY, bW, bH, + '#2d3b42', 'rgba(200, 200, 200, 0.08)', + type, currentMidX + ); + currentMidX += bW + gap; + } + + // -- Foreground Layer -- + const fgBaseY = height * 0.82; + let currentFgX = 20; + while(currentFgX < width - 20) { + const seed = currentFgX * 0.33; + const bW = 90 + (Math.abs(Math.sin(seed)) * 60); + const bH = height * 0.15 + (Math.abs(Math.cos(seed)) * height * 0.2); + const gap = 80 + Math.abs(Math.sin(seed * 1.5)) * 80; + + // Exact same type distribution as Clean City FG + const type = Math.floor(Math.abs(Math.sin(seed * 9)) * 4); + + ctx.save(); + drawBuildingShape( + currentFgX + parallax, fgBaseY, bW, bH, + '#37474F', 'rgba(50, 50, 50, 0.8)', + type, currentFgX + ); + + // Extra FG details (AC units) - Rare + if (Math.abs(Math.sin(seed)) > 0.7) { + const bx = currentFgX + parallax; + const by = fgBaseY - bH; + if (type === 0) { // Only on flat roofs + ctx.fillStyle = '#263238'; + ctx.fillRect(bx + 10, by - 12, 15, 12); + } + } + ctx.restore(); + + currentFgX += bW + gap; + } } export function drawSmokestacks(dc: DrawContext): void { @@ -55,7 +150,8 @@ export function drawSmokestacks(dc: DrawContext): void { const baseY = height * 0.8; ctx.fillStyle = '#263238'; - ctx.fillRect(x + parallax - 12, baseY - stackHeight, 24, stackHeight); + // Extend smokestack down as well + ctx.fillRect(x + parallax - 12, baseY - stackHeight, 24, stackHeight + height * 0.2); ctx.fillStyle = '#B71C1C'; ctx.fillRect(x + parallax - 12, baseY - stackHeight, 24, 15); @@ -106,6 +202,7 @@ export function drawSmog(dc: DrawContext): void { export function drawTrafficJam(dc: DrawContext): void { const { ctx, width, height } = dc; + const time = Date.now() * 0.0005; // Adjust speed ctx.fillStyle = '#2E2E2E'; ctx.fillRect(0, height * 0.82, width, height * 0.08); @@ -119,7 +216,7 @@ export function drawTrafficJam(dc: DrawContext): void { ctx.stroke(); ctx.setLineDash([]); - const drawCar = (x: number, y: number, color: string) => { + const drawCar = (x: number, y: number, color: string, facingRight: boolean) => { ctx.fillStyle = color; ctx.beginPath(); ctx.roundRect(x, y, 35, 14, 2); @@ -133,23 +230,63 @@ export function drawTrafficJam(dc: DrawContext): void { ctx.arc(x + 8, y + 14, 4, 0, Math.PI * 2); ctx.arc(x + 27, y + 14, 4, 0, Math.PI * 2); ctx.fill(); + + // Lights + if (facingRight) { + // Headlights (right) + ctx.fillStyle = 'rgba(255, 255, 200, 0.6)'; + ctx.beginPath(); + ctx.arc(x + 35, y + 5, 2, 0, Math.PI * 2); // Front right + ctx.arc(x + 35, y + 10, 2, 0, Math.PI * 2); // Front left (perspective approx) + ctx.fill(); + + // Taillights (left) - Red + ctx.fillStyle = 'rgba(255, 0, 0, 0.6)'; + ctx.beginPath(); + ctx.arc(x, y + 5, 2, 0, Math.PI * 2); + ctx.arc(x, y + 10, 2, 0, Math.PI * 2); + ctx.fill(); + + } else { + // Headlights (left) + ctx.fillStyle = 'rgba(255, 255, 200, 0.6)'; + ctx.beginPath(); + ctx.arc(x, y + 5, 2, 0, Math.PI * 2); + ctx.arc(x, y + 10, 2, 0, Math.PI * 2); + ctx.fill(); + + // Taillights (right) - Red + ctx.fillStyle = 'rgba(255, 0, 0, 0.6)'; + ctx.beginPath(); + ctx.arc(x + 35, y + 5, 2, 0, Math.PI * 2); + ctx.arc(x + 35, y + 10, 2, 0, Math.PI * 2); + ctx.fill(); + } }; const carColors = ['#616161', '#424242', '#757575', '#546E7A', '#455A64']; - for (let i = 0; i < 15; i++) { - const lane = i % 2; - const x = i * 55 + 20; - const y = height * 0.83 + lane * 35; - drawCar(x, y, carColors[i % carColors.length]); - } + + // Top lane (moving left) + const lane0Y = height * 0.82; + const spacing = 70; + const numCars = Math.ceil(width / spacing) + 2; - ctx.fillStyle = 'rgba(255, 0, 0, 0.4)'; - for (let i = 0; i < 15; i++) { - const x = i * 55 + 48; - const y = height * 0.835 + (i % 2) * 35 + 7; - ctx.beginPath(); - ctx.arc(x, y, 3, 0, Math.PI * 2); - ctx.fill(); + for (let i = 0; i < numCars; i++) { + // Lane 0: Move Left + const offset0 = (time * 60 + i * spacing) % (width + 100); + const x0 = (width + 50) - offset0 - 50; // Move from right to left + + drawCar(x0, lane0Y, carColors[i % carColors.length], false); + } + + // Bottom lane (moving right) + const lane1Y = height * 0.82 + 25; + for (let i = 0; i < numCars; i++) { + // Lane 1: Move Right + const offset1 = (time * 60 + i * spacing) % (width + 100); + const x1 = offset1 - 50; + + drawCar(x1, lane1Y, carColors[(i + 3) % carColors.length], true); } } @@ -159,9 +296,10 @@ export function drawDebris(dc: DrawContext): void { ctx.fillStyle = '#5D4037'; for (let i = 0; i < 12; i++) { - const x = Math.random() * width; - const y = height * 0.81 + Math.random() * 8; - const size = 3 + Math.random() * 5; + const seed = i * 13.7; + const x = (Math.abs(Math.sin(seed * 4.1)) * width); + const y = height * 0.81 + Math.abs(Math.cos(seed * 2.9)) * 8; + const size = 3 + Math.abs(Math.sin(seed * 7.3)) * 5; ctx.beginPath(); ctx.rect(x, y, size, size * 0.7); @@ -169,10 +307,136 @@ export function drawDebris(dc: DrawContext): void { } } +export function drawCityGround(dc: DrawContext): void { + const { ctx, width, height } = dc; + + // Draw Sidewalk at the bottom + const pavementY = height * 0.90; + + // Curb (side surface) + ctx.fillStyle = '#546E7A'; + ctx.fillRect(0, pavementY, width, height * 0.02); + + // Sidewalk pavement (top surface) - Plain + ctx.fillStyle = '#37474F'; + ctx.fillRect(0, pavementY + height * 0.02, width, height * 0.08); + + // Detailed Trash (retained for polluted theme, but kept sidewalk otherwise plain) + const drawTrash = (tx: number, ty: number, type: 'can' | 'paper' | 'bottle') => { + if (type === 'can') { + ctx.fillStyle = '#B71C1C'; // Red can + ctx.fillRect(tx, ty, 6, 8); + ctx.fillStyle = '#E57373'; // Label + ctx.fillRect(tx + 1, ty + 2, 4, 4); + } else if (type === 'paper') { + ctx.fillStyle = '#ECEFF1'; + ctx.beginPath(); + ctx.moveTo(tx, ty); + ctx.lineTo(tx + 8, ty + 2); + ctx.lineTo(tx + 6, ty + 6); + ctx.lineTo(tx - 2, ty + 4); + ctx.fill(); + } else { // bottle + ctx.fillStyle = 'rgba(76, 175, 80, 0.6)'; + ctx.beginPath(); + ctx.rect(tx, ty, 4, 10); + ctx.rect(tx + 1, ty - 3, 2, 3); + ctx.fill(); + } + }; + + for (let i = 0; i < 8; i++) { + const seed = i * 997; + const tx = (seed * 31) % width; + const ty = pavementY + height * 0.02 + (seed % (height * 0.06)); + const types: ('can' | 'paper' | 'bottle')[] = ['can', 'paper', 'bottle']; + drawTrash(tx, ty, types[i % 3]); + } +} + +export function drawStreetLamps(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + const parallax = state.mouseX * 0.8; + + const drawLamp = (x: number) => { + const lampBaseX = x + parallax; + // Anchor firmly on the sidewalk surface (sidewalk starts at 0.90 + 0.02) + const pavementY = height * 0.94; + + // Pole + ctx.fillStyle = '#263238'; + ctx.fillRect(lampBaseX - 3, pavementY - 150, 6, 150); + + // Lamp Head + ctx.beginPath(); + ctx.moveTo(lampBaseX, pavementY - 150); + ctx.lineTo(lampBaseX + 25, pavementY - 150); // Arm + ctx.stroke(); + + ctx.fillStyle = '#263238'; + ctx.beginPath(); + ctx.moveTo(lampBaseX + 20, pavementY - 155); + ctx.lineTo(lampBaseX + 45, pavementY - 145); + ctx.lineTo(lampBaseX + 20, pavementY - 140); + ctx.fill(); + + // Light Glow + const gradient = ctx.createRadialGradient( + lampBaseX + 35, pavementY - 142, 2, + lampBaseX + 35, pavementY - 142, 40 + ); + gradient.addColorStop(0, 'rgba(255, 235, 59, 0.9)'); + gradient.addColorStop(0.4, 'rgba(255, 193, 7, 0.3)'); + gradient.addColorStop(1, 'rgba(255, 193, 7, 0)'); + + ctx.fillStyle = gradient; + ctx.beginPath(); + ctx.arc(lampBaseX + 35, pavementY - 142, 40, 0, Math.PI * 2); + ctx.fill(); + }; + + // Draw a few lamps spaced out + drawLamp(width * 0.15); + drawLamp(width * 0.5); + drawLamp(width * 0.85); +} + +export function drawPollutedClouds(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + + // Darker, heavier clouds for pollution + const drawCloud = (x: number, y: number, scale: number) => { + ctx.fillStyle = 'rgba(90, 90, 90, 0.85)'; + ctx.beginPath(); + ctx.ellipse(x, y, 60 * scale, 35 * scale, 0, 0, Math.PI * 2); + ctx.fill(); + ctx.beginPath(); + ctx.ellipse(x + 50 * scale, y + 10 * scale, 50 * scale, 30 * scale, 0, 0, Math.PI * 2); + ctx.fill(); + ctx.beginPath(); + ctx.ellipse(x - 40 * scale, y + 5 * scale, 45 * scale, 25 * scale, 0, 0, Math.PI * 2); + ctx.fill(); + ctx.beginPath(); + ctx.ellipse(x + 20 * scale, y - 15 * scale, 40 * scale, 25 * scale, 0, 0, Math.PI * 2); + ctx.fill(); + }; + + const cloudOffsetX = state.mouseX * 0.5 - state.scrollY * 0.03; + const cloudOffsetY = state.scrollY * 0.1; + + // Lower, more oppressive positioning + drawCloud(width * 0.2 + cloudOffsetX, height * 0.18 + cloudOffsetY, 1.3); + drawCloud(width * 0.6 + cloudOffsetX * 0.7, height * 0.22 + cloudOffsetY * 0.8, 1.1); + drawCloud(width * 0.9 + cloudOffsetX * 0.5, height * 0.15 + cloudOffsetY * 0.6, 1.2); +} + export function drawPollutedCityScene(dc: DrawContext): void { + drawPollutedClouds(dc); drawSmog(dc); drawSmoggyBuildings(dc); drawSmokestacks(dc); - drawDebris(dc); + // Draw traffic BEFORE ground so it looks like it's behind the sidewalk "paved" area drawTrafficJam(dc); + drawCityGround(dc); + drawStreetLamps(dc); // Foreground, on top of sidewalk } diff --git a/frontend/src/lib/ts/parallax/scenes/pollutedOcean.ts b/frontend/src/lib/ts/parallax/scenes/pollutedOcean.ts new file mode 100644 index 0000000..3bf6fdc --- /dev/null +++ b/frontend/src/lib/ts/parallax/scenes/pollutedOcean.ts @@ -0,0 +1,225 @@ +import type { DrawContext } from '../types'; + +export function drawPollutedOceanWaves(dc: DrawContext): void { + const { ctx, width, height } = dc; + const time = Date.now() * 0.001; + + for (let layer = 0; layer < 3; layer++) { + const yBase = height * (0.55 + layer * 0.12); + const waveHeight = 20 - layer * 4; // Taller waves + const speed = 40 + layer * 20; + + // Base Water + ctx.beginPath(); + ctx.moveTo(-100, height); + + // Generate points for fill + for (let x = -100; x <= width + 100; x += 10) { + const y = yBase + Math.sin((x + time * speed) * 0.015) * waveHeight; // Slower frequency + ctx.lineTo(x, y); + } + + ctx.lineTo(width + 100, height); + ctx.closePath(); + + // Layer Colors - Murky greenish-grey styles (Polluted look but lighter for visibility) + if (layer === 0) ctx.fillStyle = '#62757f'; // Lighter muted blue-grey + else if (layer === 1) ctx.fillStyle = '#4b5d67'; // Medium muted + else ctx.fillStyle = '#37474f'; // Dark base, but not black + + ctx.fill(); + } +} + +export function drawFloatingTrash(dc: DrawContext): void { + const { ctx, width, height } = dc; + const time = Date.now() * 0.001; + + // Scale multiplier to make trash visible + const S = 1.8; + + const drawTrashItem = (x: number, y: number, type: 'bottle' | 'bag' | 'net' | 'tire') => { + const bob = Math.sin(time * 2 + x) * 5; + const ty = y + bob; + + if (type === 'bottle') { + // Bright Green Plastic Bottle + ctx.fillStyle = '#76FF03'; + ctx.strokeStyle = '#33691E'; + ctx.lineWidth = 1; + + ctx.beginPath(); + ctx.roundRect(x - 8 * S, ty - 3 * S, 16 * S, 6 * S, 2); + ctx.fill(); + ctx.stroke(); + + // Neck + ctx.fillStyle = '#CCFF90'; + ctx.fillRect(x + 8 * S, ty - 1 * S, 4 * S, 2 * S); + + // Label + ctx.fillStyle = '#FFFFFF'; + ctx.fillRect(x - 2 * S, ty - 3 * S, 4 * S, 6 * S); + + } else if (type === 'bag') { + // White Plastic Bag + ctx.fillStyle = '#F5F5F5'; + ctx.strokeStyle = '#BDBDBD'; + ctx.lineWidth = 1; + + ctx.beginPath(); + ctx.moveTo(x, ty); + ctx.bezierCurveTo(x + 12*S, ty - 12*S, x + 24*S, ty - 6*S, x + 30*S, ty); + ctx.bezierCurveTo(x + 18*S, ty + 12*S, x + 6*S, ty + 6*S, x, ty); + ctx.fill(); + ctx.stroke(); + + } else if (type === 'net') { + // Old Fishing Net + ctx.strokeStyle = '#5D4037'; + ctx.lineWidth = 2; // Thicker lines + ctx.beginPath(); + for(let i=0; i<6; i++) { + ctx.moveTo(x + i*5*S, ty); + ctx.lineTo(x + i*5*S + 15*S, ty + 20*S); + ctx.moveTo(x, ty + i*4*S); + ctx.lineTo(x + 30*S, ty + i*4*S + 8*S); + } + ctx.stroke(); + + } else if (type === 'tire') { + // Old Tire + ctx.fillStyle = '#212121'; + ctx.strokeStyle = '#000000'; + ctx.lineWidth = 2; + + ctx.beginPath(); + ctx.ellipse(x, ty + 5*S, 15*S, 8*S, 0, 0, Math.PI * 2); + ctx.fill(); + ctx.stroke(); + + ctx.fillStyle = '#546E7A'; // Inner water color match roughly + ctx.beginPath(); + ctx.ellipse(x, ty + 5*S, 8*S, 4*S, 0, 0, Math.PI * 2); + ctx.fill(); + } + }; + + // Draw mass amounts of trash + const trashItems = [ + { x: 0.1, y: 0.7, type: 'bag' }, + { x: 0.15, y: 0.85, type: 'bottle' }, + { x: 0.25, y: 0.65, type: 'tire' }, + { x: 0.3, y: 0.75, type: 'net' }, + { x: 0.4, y: 0.8, type: 'bottle' }, + { x: 0.45, y: 0.6, type: 'bag' }, + { x: 0.55, y: 0.72, type: 'tire' }, + { x: 0.65, y: 0.82, type: 'bottle' }, + { x: 0.7, y: 0.68, type: 'net' }, + { x: 0.8, y: 0.76, type: 'bag' }, + { x: 0.85, y: 0.62, type: 'bottle' }, + { x: 0.92, y: 0.88, type: 'bag' }, + { x: 0.05, y: 0.78, type: 'bottle' }, + + { x: 0.12, y: 0.62, type: 'bottle' }, + { x: 0.22, y: 0.78, type: 'bag' }, + { x: 0.35, y: 0.82, type: 'tire' }, + { x: 0.38, y: 0.65, type: 'bottle' }, + { x: 0.48, y: 0.74, type: 'net' }, + { x: 0.58, y: 0.86, type: 'bottle' }, + { x: 0.62, y: 0.64, type: 'bag' }, + { x: 0.72, y: 0.79, type: 'tire' }, + { x: 0.82, y: 0.67, type: 'bottle' }, + { x: 0.88, y: 0.76, type: 'net' }, + { x: 0.08, y: 0.84, type: 'tire' }, + { x: 0.95, y: 0.68, type: 'bag' }, + { x: 0.52, y: 0.68, type: 'bottle' } + ]; + + trashItems.forEach(item => { + const drift = Math.sin(time * 0.5 + item.x * 10) * 20; + drawTrashItem( + width * item.x + drift, + height * item.y, + item.type as any + ); + }); +} + +export function drawPollutedFish(dc: DrawContext): void { + const { ctx, width, height } = dc; + const time = Date.now() * 0.001; + const S = 0.6; // Scale + + const drawSickFish = (seed: number, baseX: number, baseY: number, color: string, speedBase: number, isSkeleton: boolean) => { + const t = time * speedBase; + + const dx = (baseX + t * 20 + Math.sin(t * 0.5 + seed) * 15); + const moveX = ((dx % (width + 50)) + (width + 50)) % (width + 50) - 25; + + const moveY = baseY + Math.sin(t * 1.0 + seed) * 8 + Math.cos(t * 2.5 + seed * 2) * 3; + + const dy = (Math.cos(t * 1.0 + seed) * 10 - Math.sin(t * 2.5 + seed * 2) * 10) * 0.02; + + ctx.save(); + ctx.translate(moveX, moveY); + ctx.rotate(dy); + + if (isSkeleton) { + ctx.strokeStyle = '#E0E0E0'; // Very bright grey for visible skeletons + ctx.lineWidth = 1.5; // Thicker lines + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(10*S, 0); + ctx.moveTo(3*S, -3*S); ctx.lineTo(3*S, 3*S); + ctx.moveTo(6*S, -3*S); ctx.lineTo(6*S, 3*S); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(10*S, 0); + ctx.lineTo(8*S, -4*S); + ctx.lineTo(12*S, 0); + ctx.lineTo(8*S, 4*S); + ctx.closePath(); + ctx.stroke(); + } else { + ctx.fillStyle = color; + ctx.beginPath(); + ctx.ellipse(0, 0, 10 * S, 4 * S, 0, 0, Math.PI*2); + ctx.fill(); + ctx.beginPath(); + ctx.moveTo(-8 * S, 0); + ctx.lineTo(-14 * S, -4 * S); + ctx.lineTo(-14 * S, 4 * S); + ctx.fill(); + + ctx.strokeStyle = '#000000'; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(4*S, -2*S); ctx.lineTo(6*S, 0); + ctx.moveTo(6*S, -2*S); ctx.lineTo(4*S, 0); + ctx.stroke(); + } + ctx.restore(); + }; + + // Schools of POLLUTED fish - Reduced count + // Sickly pale green + for(let i=0; i<5; i++) drawSickFish(i*9, width * 0.15 + i*25, height * 0.88, '#C5E1A5', 0.5, false); + + // Pale muddy brown + for(let i=0; i<4; i++) drawSickFish(i*11 + 50, width * 0.6 + i*30, height * 0.92, '#D7CCC8', 0.4, false); + + // Skeleton fish - Reduced count + for(let i=0; i<3; i++) drawSickFish(i*13 + 100, width * 0.3 + i*40, height * 0.85, '', 0.3, true); + for(let i=0; i<2; i++) drawSickFish(i*19 + 200, width * 0.8 + i*50, height * 0.9, '', 0.2, true); + + // Extra top layer fish + for(let i=0; i<3; i++) drawSickFish(i*7 + 300, width * 0.05 + i*60, height * 0.80, '#E6EE9C', 0.6, false); +} + +export function drawPollutedOceanScene(dc: DrawContext): void { + drawPollutedOceanWaves(dc); + drawFloatingTrash(dc); + drawPollutedFish(dc); +} diff --git a/frontend/src/lib/ts/parallax/types.ts b/frontend/src/lib/ts/parallax/types.ts index 0353f45..a87e072 100644 --- a/frontend/src/lib/ts/parallax/types.ts +++ b/frontend/src/lib/ts/parallax/types.ts @@ -18,6 +18,7 @@ export type SceneType = | 'oilRig' | 'city' | 'pollutedCity' + | 'pollutedOcean' | 'transition'; export interface DrawContext { diff --git a/frontend/src/routes/news/+page.svelte b/frontend/src/routes/news/+page.svelte deleted file mode 100644 index 54a46a9..0000000 --- a/frontend/src/routes/news/+page.svelte +++ /dev/null @@ -1,225 +0,0 @@ - - -
-
- -
- -
-
-

Eco News

-

Latest sustainability updates

-
- -
- {#each news as item (item.id)} -
-
-
- -
- {item.tag} -
-
{item.date}
-

{item.title}

-

{item.desc}

- - Read more - - -
- {/each} -
-
-
- -