mirror of
https://github.com/SirBlobby/Hoya26.git
synced 2026-02-04 03:34:34 -05:00
Landscape updates
This commit is contained in:
@@ -17,12 +17,20 @@
|
||||
const PATH_CONFIG: Record<string, SceneConfig> = {
|
||||
"/": { 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 },
|
||||
};
|
||||
|
||||
@@ -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<Exclude<SceneType, 'transition'>, (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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<rows; r++) {
|
||||
for(let c=0; c<cols; c++) {
|
||||
// Deterministic pattern
|
||||
const seed = stableSeedX + c * 23 + r * 17;
|
||||
// Office buildings usually have uniform glass, maybe some variation
|
||||
// Checkboard-ish or full glass lines
|
||||
if (Math.abs(Math.sin(seed)) > 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);
|
||||
}
|
||||
|
||||
@@ -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<layers; i++) {
|
||||
const ly = -30 - i * 14;
|
||||
const lw = 38 - i * 5;
|
||||
ctx.beginPath();
|
||||
// Left side curve
|
||||
ctx.moveTo(0, ly - 20);
|
||||
ctx.bezierCurveTo(-lw/2, ly - 10, -lw, ly + 10, -lw, ly + 15);
|
||||
ctx.lineTo(0, ly + 5);
|
||||
// Right side curve
|
||||
ctx.lineTo(lw, ly + 15);
|
||||
ctx.bezierCurveTo(lw, ly + 10, lw/2, ly - 10, 0, ly - 20);
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawDetailedBroadleaf(ctx: CanvasRenderingContext2D, x: number, y: number, scale: number, color: string) {
|
||||
ctx.save();
|
||||
ctx.translate(x, y);
|
||||
ctx.scale(scale, scale);
|
||||
|
||||
// Trunk with bark texture hints
|
||||
ctx.fillStyle = '#5D4037';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(width * 0.55, 0);
|
||||
ctx.lineTo(width * 0.62, 0);
|
||||
ctx.lineTo(width * 0.35, height * 0.65);
|
||||
ctx.lineTo(width * 0.28, height * 0.65);
|
||||
ctx.closePath();
|
||||
ctx.moveTo(-8, 0);
|
||||
ctx.bezierCurveTo(-5, -20, -12, -50, -6, -70); // Left main branch
|
||||
ctx.lineTo(-4, -72);
|
||||
ctx.lineTo(0, -40); // Crotch
|
||||
ctx.lineTo(4, -72);
|
||||
ctx.lineTo(6, -70);
|
||||
ctx.bezierCurveTo(12, -50, 5, -20, 8, 0); // Right side
|
||||
ctx.fill();
|
||||
|
||||
// Canopy (Dense cloud clusters)
|
||||
ctx.fillStyle = color;
|
||||
const blobs = [
|
||||
{x: 0, y: -90, r: 35},
|
||||
{x: -25, y: -75, r: 28},
|
||||
{x: 25, y: -75, r: 28},
|
||||
{x: -40, y: -60, r: 22},
|
||||
{x: 40, y: -60, r: 22},
|
||||
{x: -15, y: -50, r: 25},
|
||||
{x: 15, y: -50, r: 25},
|
||||
];
|
||||
|
||||
ctx.beginPath();
|
||||
blobs.forEach(b => {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<rows; r++) {
|
||||
for(let c=0; c<cols; c++) {
|
||||
// Deterministic pattern matching clean city
|
||||
const seed = stableSeedX + c * 23 + r * 17;
|
||||
if (Math.abs(Math.sin(seed)) > 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
|
||||
}
|
||||
|
||||
225
frontend/src/lib/ts/parallax/scenes/pollutedOcean.ts
Normal file
225
frontend/src/lib/ts/parallax/scenes/pollutedOcean.ts
Normal file
@@ -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);
|
||||
}
|
||||
@@ -18,6 +18,7 @@ export type SceneType =
|
||||
| 'oilRig'
|
||||
| 'city'
|
||||
| 'pollutedCity'
|
||||
| 'pollutedOcean'
|
||||
| 'transition';
|
||||
|
||||
export interface DrawContext {
|
||||
|
||||
@@ -1,225 +0,0 @@
|
||||
<script lang="ts">
|
||||
import ParallaxLandscape from "$lib/components/ParallaxLandscape.svelte";
|
||||
|
||||
const news = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Ocean Cleanup hits milestone",
|
||||
desc: "500 tons of plastic have been successfully removed from the Pacific garbage patch, marking a major breakthrough in ocean restoration efforts.",
|
||||
date: "2h ago",
|
||||
icon: "ri:ship-line",
|
||||
tag: "Trending",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "New Plastic Ban in effect",
|
||||
desc: "Major cities across the globe adopt strict single-use plastic policies, signaling a shift toward sustainable alternatives.",
|
||||
date: "5h ago",
|
||||
icon: "ri:prohibited-line",
|
||||
tag: "Policy",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Ethix App launches globally",
|
||||
desc: "Empowering users worldwide to make better choices with AI-powered sustainability scanning and verification.",
|
||||
date: "1d ago",
|
||||
icon: "ri:global-line",
|
||||
tag: "Launch",
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="page-wrapper">
|
||||
<div class="desktop-bg">
|
||||
<ParallaxLandscape />
|
||||
</div>
|
||||
|
||||
<div class="content-container">
|
||||
<div class="header">
|
||||
<h1 class="page-title">Eco News</h1>
|
||||
<p class="subtitle">Latest sustainability updates</p>
|
||||
</div>
|
||||
|
||||
<div class="news-grid">
|
||||
{#each news as item (item.id)}
|
||||
<article class="news-card">
|
||||
<div class="card-header">
|
||||
<div class="news-icon">
|
||||
<iconify-icon icon={item.icon} width="28"
|
||||
></iconify-icon>
|
||||
</div>
|
||||
<span class="news-tag">{item.tag}</span>
|
||||
</div>
|
||||
<div class="news-meta">{item.date}</div>
|
||||
<h2 class="news-title">{item.title}</h2>
|
||||
<p class="news-desc">{item.desc}</p>
|
||||
<a href="/news/{item.id}" class="news-link">
|
||||
Read more
|
||||
<iconify-icon icon="ri:arrow-right-line" width="16"
|
||||
></iconify-icon>
|
||||
</a>
|
||||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.page-wrapper {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.desktop-bg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
padding: 100px 24px 120px;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
color: white;
|
||||
font-size: 42px;
|
||||
font-weight: 900;
|
||||
letter-spacing: -2px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-size: 16px;
|
||||
margin: 10px 0 0 0;
|
||||
}
|
||||
|
||||
.news-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.news-card {
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 24px;
|
||||
padding: 28px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.news-card:hover {
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.news-icon {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
.news-tag {
|
||||
background: rgba(34, 197, 94, 0.2);
|
||||
color: #4ade80;
|
||||
padding: 6px 14px;
|
||||
border-radius: 50px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.news-meta {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
font-size: 13px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.news-title {
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
font-size: 22px;
|
||||
margin: 0 0 12px 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.news-desc {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
.news-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #4ade80;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.news-link:hover {
|
||||
gap: 12px;
|
||||
color: #86efac;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.desktop-bg {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.news-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.news-card:first-child {
|
||||
grid-column: span 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.content-container {
|
||||
padding: 60px 16px 100px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 32px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user