// Color palettes and configurations const NATURE_COLORS = { GREENS: [ '#11aa33', // Darker forest green '#22bb44', // Medium forest green '#33cc55', // Bright forest green '#118844', // Deep sea green '#22aa66', // Sage green '#11bb55', // Deep pine '#229955', // Ocean green '#33bb66', // Fresh spring green '#117744', // Dark emerald ], TRUNK_COLORS: { LIGHT: '#664422', MEDIUM: '#553311', DARK: '#442200' } }; const GRASS_CONFIG = { MIN_BLADES: 4, MAX_BLADES: 8, SPREAD_FACTOR: 0.7, RUSTLE_SPEED: 300, WAVE_SPEED: 1000, GLOW_SPEED: 500, BASE_BLADE_WIDTH: 5 }; const FIR_CONFIG = { ROW_COUNT: 40, FEATHERS_PER_ROW: 24, EDGE_FEATHER_COUNT: 40, FEATHER_WIDTH: 2, TAPER_POWER: 0.8, CENTER_NEEDLES: 5, // Number of needles in center column NEEDLE_SPACING: 1.5, // Spacing between center needles UPWARD_TILT: 0.1, // Amount of upward tilt (in radians) ANGLE_VARIATION: 0.05, // Variation in needle angles LENGTH_MULTIPLIER: 0.8 // Base length multiplier for needles }; // Define world object types const WORLD_OBJECTS = { PLATFORM: 'platform', FIR_BACKGROUND: 'fir_background', FIR_FOREGROUND: 'fir_foreground', MAPLE_BACKGROUND: 'maple_background', MAPLE_FOREGROUND: 'maple_foreground', ROCK_BACKGROUND: 'rock_background', ROCK_FOREGROUND: 'rock_foreground', GRASS_BACKGROUND: 'grass_background', GRASS_FOREGROUND: 'grass_foreground' }; // Separate configurations for different tree types const FIR_TYPES = { SMALL: { type: 'fir', width: 100, height: 230, canopyOffset: 45, canopyColor: '#22aa44', trunkColor: '#553311' }, LARGE: { type: 'fir', width: 150, height: 320, canopyOffset: 65, canopyColor: '#11aa44', trunkColor: '#442200' } }; const MAPLE_TYPES = { SMALL: { type: 'maple', width: 100, height: 330, trunkHeight: 60, canopyRadius: 35, leafClusters: 5, canopyColor: '#33aa22', // Bright green trunkColor: '#664422' }, MEDIUM: { type: 'maple', width: 130, height: 360, trunkHeight: 70, canopyRadius: 45, leafClusters: 6, canopyColor: '#228833', // Deep forest green trunkColor: '#553311' }, LARGE: { type: 'maple', width: 160, height: 400, trunkHeight: 85, canopyRadius: 55, leafClusters: 7, canopyColor: '#115522', // Dark green trunkColor: '#553311' }, BRIGHT: { type: 'maple', width: 140, height: 380, trunkHeight: 75, canopyRadius: 50, leafClusters: 6, canopyColor: '#44bb33', // Vibrant green trunkColor: '#664422' }, SAGE: { type: 'maple', width: 150, height: 490, trunkHeight: 80, canopyRadius: 52, leafClusters: 6, canopyColor: '#225544', // Sage green trunkColor: '#553311' } }; // Rock configurations const ROCK_TYPES = { SMALL: { width: 40, height: 30, color: '#666', highlights: '#888' }, MEDIUM: { width: 70, height: 45, color: '#555', highlights: '#777' }, LARGE: { width: 100, height: 60, color: '#444', highlights: '#666' } }; // Add grass configurations const GRASS_TYPES = { TALL: { type: 'grass', width: 30, height: 40, color: '#33aa55', shadowColor: '#229944' }, SHORT: { type: 'grass', width: 20, height: 25, color: '#33bb66', shadowColor: '#22aa55' }, GOLDEN_TALL: { type: 'grass', width: 30, height: 40, color: '#eebb33', shadowColor: '#cc9922' }, GOLDEN_SHORT: { type: 'grass', width: 20, height: 25, color: '#ffcc44', shadowColor: '#ddaa33' }, BLUE_TALL: { type: 'grass', width: 30, height: 40, color: '#44aaff', shadowColor: '#2299ff', glowing: true }, BLUE_SHORT: { type: 'grass', width: 20, height: 25, color: '#55bbff', shadowColor: '#33aaff', glowing: true } }; // Utility functions const utils = { getRandomColorFromPalette: (palette, seed1, seed2) => { const colorIndex = Math.floor(seededRandom(seed1, seed2) * palette.length); return palette[colorIndex]; }, getBladeCount: (x, height) => { const randomValue = Math.abs(seededRandom(x, height)); return GRASS_CONFIG.MIN_BLADES + Math.round(randomValue * (GRASS_CONFIG.MAX_BLADES - GRASS_CONFIG.MIN_BLADES)); }, calculateTaper: (t) => Math.pow(1 - t, FIR_CONFIG.TAPER_POWER) }; // Create a world with platforms, trees, and rocks const createWorld = () => { const world = { groundHeight: 12, // Separate arrays for different layers backgroundTrees: [ // Far left trees { type: WORLD_OBJECTS.FIR_BACKGROUND, x: -1500, config: FIR_TYPES.LARGE }, { type: WORLD_OBJECTS.MAPLE_BACKGROUND, x: -1200, config: MAPLE_TYPES.SAGE }, { type: WORLD_OBJECTS.FIR_BACKGROUND, x: -900, config: FIR_TYPES.SMALL }, // Existing trees { type: WORLD_OBJECTS.FIR_BACKGROUND, x: -400, config: FIR_TYPES.LARGE }, { type: WORLD_OBJECTS.MAPLE_BACKGROUND, x: -250, config: MAPLE_TYPES.BRIGHT }, { type: WORLD_OBJECTS.FIR_BACKGROUND, x: 50, config: FIR_TYPES.LARGE }, { type: WORLD_OBJECTS.MAPLE_BACKGROUND, x: 250, config: MAPLE_TYPES.MEDIUM }, { type: WORLD_OBJECTS.FIR_BACKGROUND, x: 500, config: FIR_TYPES.SMALL }, { type: WORLD_OBJECTS.MAPLE_BACKGROUND, x: 650, config: MAPLE_TYPES.SMALL }, { type: WORLD_OBJECTS.FIR_BACKGROUND, x: 900, config: FIR_TYPES.LARGE }, { type: WORLD_OBJECTS.MAPLE_BACKGROUND, x: 1100, config: MAPLE_TYPES.LARGE }, // Far right trees { type: WORLD_OBJECTS.MAPLE_BACKGROUND, x: 1400, config: MAPLE_TYPES.LARGE }, { type: WORLD_OBJECTS.FIR_BACKGROUND, x: 1700, config: FIR_TYPES.SMALL }, { type: WORLD_OBJECTS.MAPLE_BACKGROUND, x: 2000, config: MAPLE_TYPES.LARGE } ], backgroundRocks: [ // Far left rocks { type: WORLD_OBJECTS.ROCK_BACKGROUND, x: -1300, config: ROCK_TYPES.LARGE }, { type: WORLD_OBJECTS.ROCK_BACKGROUND, x: -1000, config: ROCK_TYPES.SMALL }, { type: WORLD_OBJECTS.ROCK_BACKGROUND, x: -300, config: ROCK_TYPES.MEDIUM }, { type: WORLD_OBJECTS.ROCK_BACKGROUND, x: -100, config: ROCK_TYPES.SMALL }, { type: WORLD_OBJECTS.ROCK_BACKGROUND, x: 150, config: ROCK_TYPES.LARGE }, { type: WORLD_OBJECTS.ROCK_BACKGROUND, x: 400, config: ROCK_TYPES.SMALL }, { type: WORLD_OBJECTS.ROCK_BACKGROUND, x: 750, config: ROCK_TYPES.MEDIUM }, { type: WORLD_OBJECTS.ROCK_BACKGROUND, x: 1000, config: ROCK_TYPES.SMALL }, // Far right rocks { type: WORLD_OBJECTS.ROCK_BACKGROUND, x: 1600, config: ROCK_TYPES.MEDIUM }, { type: WORLD_OBJECTS.ROCK_BACKGROUND, x: 1900, config: ROCK_TYPES.SMALL } ], platforms: [ // { // type: WORLD_OBJECTS.PLATFORM, // x: 300, // y: 300, // width: 200, // height: 20, // color: '#484' // }, // { // type: WORLD_OBJECTS.PLATFORM, // x: 600, // y: 200, // width: 200, // height: 20, // color: '#484' // }, // { // type: WORLD_OBJECTS.PLATFORM, // x: -200, // y: 250, // width: 200, // height: 20, // color: '#484' // } ], foregroundTrees: [ // Far left trees { type: WORLD_OBJECTS.FIR_FOREGROUND, x: -1400, config: FIR_TYPES.LARGE }, { type: WORLD_OBJECTS.MAPLE_FOREGROUND, x: -1100, config: MAPLE_TYPES.SMALL }, // Existing trees { type: WORLD_OBJECTS.MAPLE_FOREGROUND, x: -150, config: MAPLE_TYPES.BRIGHT }, { type: WORLD_OBJECTS.FIR_FOREGROUND, x: 200, config: FIR_TYPES.SMALL }, { type: WORLD_OBJECTS.MAPLE_FOREGROUND, x: 450, config: MAPLE_TYPES.SAGE }, { type: WORLD_OBJECTS.FIR_FOREGROUND, x: 800, config: FIR_TYPES.LARGE }, { type: WORLD_OBJECTS.MAPLE_FOREGROUND, x: 1200, config: MAPLE_TYPES.SMALL }, // Far right trees { type: WORLD_OBJECTS.FIR_FOREGROUND, x: 1500, config: FIR_TYPES.LARGE }, { type: WORLD_OBJECTS.MAPLE_FOREGROUND, x: 1800, config: MAPLE_TYPES.SMALL } ], foregroundRocks: [ // Far left rocks { type: WORLD_OBJECTS.ROCK_FOREGROUND, x: -1000, config: ROCK_TYPES.MEDIUM }, { type: WORLD_OBJECTS.ROCK_FOREGROUND, x: 0, config: ROCK_TYPES.SMALL }, { type: WORLD_OBJECTS.ROCK_FOREGROUND, x: 300, config: ROCK_TYPES.MEDIUM }, { type: WORLD_OBJECTS.ROCK_FOREGROUND, x: 700, config: ROCK_TYPES.SMALL }, { type: WORLD_OBJECTS.ROCK_FOREGROUND, x: 950, config: ROCK_TYPES.LARGE }, // Far right rocks { type: WORLD_OBJECTS.ROCK_FOREGROUND, x: 1500, config: ROCK_TYPES.LARGE } ], backgroundGrass: [ // Far left grass { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: -1400, config: GRASS_TYPES.BLUE_TALL }, { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: -1100, config: GRASS_TYPES.GOLDEN_SHORT }, { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: -950, config: GRASS_TYPES.TALL }, { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: -350, config: GRASS_TYPES.SHORT }, { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: -180, config: GRASS_TYPES.GOLDEN_TALL }, { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: 100, config: GRASS_TYPES.TALL }, { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: 320, config: GRASS_TYPES.BLUE_SHORT }, { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: 580, config: GRASS_TYPES.GOLDEN_SHORT }, { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: 750, config: GRASS_TYPES.BLUE_TALL }, { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: 950, config: GRASS_TYPES.TALL }, { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: 1050, config: GRASS_TYPES.GOLDEN_TALL }, // Far right grass { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: 1500, config: GRASS_TYPES.BLUE_SHORT }, { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: 1750, config: GRASS_TYPES.GOLDEN_TALL }, { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: 1850, config: GRASS_TYPES.SHORT } ], foregroundGrass: [ // Far left grass { type: WORLD_OBJECTS.GRASS_FOREGROUND, x: -1250, config: GRASS_TYPES.TALL }, { type: WORLD_OBJECTS.GRASS_FOREGROUND, x: -1050, config: GRASS_TYPES.BLUE_SHORT }, { type: WORLD_OBJECTS.GRASS_FOREGROUND, x: -280, config: GRASS_TYPES.BLUE_TALL }, { type: WORLD_OBJECTS.GRASS_FOREGROUND, x: -50, config: GRASS_TYPES.GOLDEN_SHORT }, { type: WORLD_OBJECTS.GRASS_FOREGROUND, x: 150, config: GRASS_TYPES.TALL }, { type: WORLD_OBJECTS.GRASS_FOREGROUND, x: 420, config: GRASS_TYPES.BLUE_SHORT }, { type: WORLD_OBJECTS.GRASS_FOREGROUND, x: 680, config: GRASS_TYPES.GOLDEN_TALL }, { type: WORLD_OBJECTS.GRASS_FOREGROUND, x: 880, config: GRASS_TYPES.SHORT }, { type: WORLD_OBJECTS.GRASS_FOREGROUND, x: 1150, config: GRASS_TYPES.BLUE_TALL }, // Far right grass { type: WORLD_OBJECTS.GRASS_FOREGROUND, x: 1650, config: GRASS_TYPES.GOLDEN_TALL }, { type: WORLD_OBJECTS.GRASS_FOREGROUND, x: 1850, config: GRASS_TYPES.BLUE_SHORT } ], // Track grass animation states grassStates: {}, // Add methods for quick spatial lookups getObjectsInView: function(bounds) { return { backgroundTrees: this.backgroundTrees.filter(tree => tree.x > bounds.left && tree.x < bounds.right ), // ... similar for other object types }; } }; return world; }; // Add seededRandom function const seededRandom = (x, y) => { const dot = x * 12.9898 + y * 78.233; return (Math.sin(dot) * 43758.5453123) % 1; }; // Add hatching helper function const addHatchingPattern = (ctx, x, y, width, height, color) => { const spacing = 4; // Space between hatch lines const length = 10; // Length of each hatch line const margin = 4; // Increased margin from edges const baseAngle = Math.PI * 1.5; // Vertical angle (pointing down) const angleVariation = Math.PI / 12; // Reduced angle variation // Define darker brown shades const brownShades = [ '#442211', // Dark brown '#553322', // Medium dark brown '#443311', // Reddish dark brown '#332211', // Very dark brown '#554422', // Warm dark brown ]; // Create clipping path for trunk ctx.save(); ctx.beginPath(); ctx.rect( x - width/2, y - height, width, height ); ctx.clip(); // Calculate bounds with increased safety margin const bounds = { minX: x - width/2 + margin, maxX: x + width/2 - margin, minY: y - height + margin, maxY: y - margin }; // Draw hatching ctx.lineWidth = 1; for (let hatchX = bounds.minX; hatchX < bounds.maxX; hatchX += spacing) { for (let hatchY = bounds.minY; hatchY < bounds.maxY; hatchY += spacing) { // Use position for random variation const seed1 = hatchX * 13.37; const seed2 = hatchY * 7.89; // Random variations with reduced range const variation = { x: (seededRandom(seed1, seed2) - 0.5) * 1.5, // Reduced variation y: (seededRandom(seed2, seed1) - 0.5) * 1.5, // Reduced variation angle: baseAngle + (seededRandom(seed1 + seed2, seed2 - seed1) - 0.5) * angleVariation }; // Pick a random brown shade const colorIndex = Math.floor(seededRandom(seed1 * seed2, seed2 * seed1) * brownShades.length); ctx.strokeStyle = brownShades[colorIndex]; // Draw hatch line ctx.beginPath(); ctx.moveTo( hatchX + variation.x, hatchY + variation.y ); ctx.lineTo( hatchX + Math.cos(variation.angle) * length + variation.x, hatchY + Math.sin(variation.angle) * length + variation.y ); ctx.stroke(); } } ctx.restore(); }; class FirTreeRenderer { static renderTrunk(ctx, x, width, height, groundY, trunkColor) { const trunkWidth = width/4; const trunkHeight = height; ctx.fillStyle = trunkColor; ctx.fillRect( x - trunkWidth/2, groundY - trunkHeight, trunkWidth, trunkHeight ); addHatchingPattern(ctx, x, groundY, trunkWidth, trunkHeight, trunkColor); } static renderCenterNeedles(ctx, x, rowY, taper, featherLength, greenColors) { for (let i = -FIR_CONFIG.CENTER_NEEDLES; i <= FIR_CONFIG.CENTER_NEEDLES; i++) { const offset = i * (FIR_CONFIG.NEEDLE_SPACING * 1.5); // Left needle FirTreeRenderer.renderSingleNeedle( ctx, x + offset, rowY, Math.PI, taper, featherLength, greenColors ); // Right needle FirTreeRenderer.renderSingleNeedle( ctx, x + offset, rowY, 0, taper, featherLength, greenColors ); } } static renderSingleNeedle(ctx, x, y, baseAngle, taper, length, colors) { const angleVar = Math.PI * 0.02; const angle = baseAngle + (angleVar * seededRandom(x, y) - angleVar/2); ctx.strokeStyle = utils.getRandomColorFromPalette(colors, x, y); ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo( x + Math.cos(angle) * length * taper, y + Math.sin(angle) * length * taper ); ctx.stroke(); } static renderRowNeedles(ctx, x, rowY, rowWidth, featherCount, t, taper, featherLength, greenColors) { for (let i = 0; i < featherCount; i++) { const featherT = i / (featherCount - 1); const startX = x - rowWidth/2 + rowWidth * featherT; if (Math.abs(startX - x) < 1) continue; const relativeX = (startX - x) / (rowWidth/2); const baseAngle = relativeX < 0 ? Math.PI : 0; const upwardTilt = Math.PI * FIR_CONFIG.UPWARD_TILT * t; const angle = baseAngle + (FIR_CONFIG.ANGLE_VARIATION * seededRandom(startX, rowY) - FIR_CONFIG.ANGLE_VARIATION/2) - upwardTilt; const lengthMultiplier = FIR_CONFIG.LENGTH_MULTIPLIER + (1 - t) * 0.3; const finalLength = featherLength * lengthMultiplier * taper * (0.9 + seededRandom(startX * rowY, rowY) * 0.2); ctx.strokeStyle = utils.getRandomColorFromPalette(greenColors, startX, rowY); ctx.beginPath(); ctx.moveTo(startX, rowY); ctx.lineTo( startX + Math.cos(angle) * finalLength, rowY + Math.sin(angle) * finalLength ); ctx.stroke(); } } static renderEdgeNeedles(ctx, x, baseWidth, baseY, tipY, featherLength, greenColors) { for (let i = 0; i < FIR_CONFIG.EDGE_FEATHER_COUNT; i++) { const t = i / (FIR_CONFIG.EDGE_FEATHER_COUNT - 1); const taper = utils.calculateTaper(t); if (taper <= 0.1) continue; // Left edge FirTreeRenderer.renderEdgeNeedle( ctx, x, baseWidth, baseY, tipY, t, taper, featherLength, Math.PI, greenColors, true ); // Right edge FirTreeRenderer.renderEdgeNeedle( ctx, x, baseWidth, baseY, tipY, t, taper, featherLength, 0, greenColors, false ); } } static renderEdgeNeedle(ctx, x, baseWidth, baseY, tipY, t, taper, length, baseAngle, colors, isLeft) { const sign = isLeft ? -1 : 1; const needleX = x + sign * (baseWidth/2 * taper - (baseWidth/2 * taper) * t); const needleY = baseY - (baseY - tipY) * t; const upwardTilt = Math.PI * 0.1 * t; const angle = baseAngle + (Math.PI * 0.05 * seededRandom(needleX, needleY) - Math.PI * 0.025) - upwardTilt; ctx.strokeStyle = utils.getRandomColorFromPalette(colors, needleX, needleY); ctx.beginPath(); ctx.moveTo(needleX, needleY); ctx.lineTo( needleX + Math.cos(angle) * length * taper * 1.2, needleY + Math.sin(angle) * length * taper * 1.2 ); ctx.stroke(); } } // Helper function to darken/lighten colors const shadeColor = (color, percent) => { const num = parseInt(color.replace('#', ''), 16); const amt = Math.round(2.55 * percent); const R = (num >> 16) + amt; const G = (num >> 8 & 0x00FF) + amt; const B = (num & 0x0000FF) + amt; return '#' + (0x1000000 + (R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 + (G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 + (B < 255 ? (B < 1 ? 0 : B) : 255) ).toString(16).slice(1); }; // Add new maple tree render function const renderMapleTree = (ctx, tree, groundY) => { const { x, config } = tree; const { width, height, trunkHeight, canopyRadius, leafClusters, trunkColor, canopyColor } = config; // Draw trunk base with narrower width (changed from width/4 to width/5) const trunkWidth = width/5.5; ctx.fillStyle = trunkColor; ctx.fillRect( x - trunkWidth/2, groundY - trunkHeight, trunkWidth, trunkHeight ); // Add trunk hatching with updated width addHatchingPattern( ctx, x, groundY, trunkWidth, trunkHeight, trunkColor ); // Function to create an irregular polygon const createIrregularPolygon = (centerX, centerY, radius, sides, seed1, seed2) => { ctx.beginPath(); // Create points array first to allow smoothing const points = []; for (let i = 0; i < sides; i++) { const angle = (i / sides) * Math.PI * 2; // Even more subtle variation range (0.95-1.05) const radiusVariation = 0.95 + seededRandom(seed1 * i, seed2 * i) * 0.10; const pointRadius = radius * radiusVariation; points.push({ x: centerX + Math.cos(angle) * pointRadius, y: centerY + Math.sin(angle) * pointRadius }); } // Start the path ctx.moveTo(points[0].x, points[0].y); // Draw smooth curves through all points for (let i = 0; i < points.length; i++) { const current = points[i]; const next = points[(i + 1) % points.length]; const nextNext = points[(i + 2) % points.length]; // Calculate control points for bezier curve with more smoothing const cp1x = current.x + (next.x - points[(i - 1 + points.length) % points.length].x) / 4; const cp1y = current.y + (next.y - points[(i - 1 + points.length) % points.length].y) / 4; const cp2x = next.x - (nextNext.x - current.x) / 4; const cp2y = next.y - (nextNext.y - current.y) / 4; // Draw bezier curve ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, next.x, next.y); } ctx.closePath(); }; // Draw single canopy as irregular polygon const sides = Math.floor(48 + seededRandom(x, groundY) * 16); // 48-64 sides const mainY = groundY - trunkHeight - canopyRadius; ctx.fillStyle = canopyColor; createIrregularPolygon( x, mainY, canopyRadius * 1.4, // Increased size sides, x, groundY ); ctx.fill(); }; // Main tree render function that handles both types const renderTree = (ctx, tree, groundY) => { if (tree.config.type === 'fir') { renderFirTree(ctx, tree, groundY); } else if (tree.config.type === 'maple') { renderMapleTree(ctx, tree, groundY); } }; const renderRock = (ctx, rock, groundY) => { const { x, config } = rock; const { width, height, color, highlights } = config; // Draw main rock shape (slightly irregular pentagon) ctx.fillStyle = color; ctx.beginPath(); ctx.moveTo(x - width/2, groundY); ctx.lineTo(x - width/2 + width/6, groundY - height); ctx.lineTo(x + width/3, groundY - height); ctx.lineTo(x + width/2, groundY - height/2); ctx.lineTo(x + width/2, groundY); ctx.closePath(); ctx.fill(); // Add highlights ctx.fillStyle = highlights; ctx.beginPath(); ctx.moveTo(x - width/4, groundY - height); ctx.lineTo(x, groundY - height); ctx.lineTo(x + width/6, groundY - height/2); ctx.lineTo(x - width/6, groundY - height/2); ctx.closePath(); ctx.fill(); }; // Add grass rendering function const renderGrass = (ctx, grass, groundY, time) => { const { x, config } = grass; const { width, height, color, shadowColor, glowing } = config; // Initialize or get grass state const stateKey = `grass_${x}`; if (!window.gameState.world.grassStates[stateKey]) { window.gameState.world.grassStates[stateKey] = GrassState.create(x, height); } const state = GrassState.validate(window.gameState.world.grassStates[stateKey]); // Update interaction const player = window.gameState.player; const distanceToPlayer = Math.abs(player.x + player.width / 2 - x); GrassState.update(state, distanceToPlayer, width); // Handle rendering setupGlowEffect(ctx, glowing, color, time); renderGrassBlades(ctx, state, x, width, height, groundY, time, color, shadowColor); resetGlowEffect(ctx, glowing); }; const setupGlowEffect = (ctx, glowing, color, time) => { if (glowing) { ctx.save(); ctx.shadowColor = color; ctx.shadowBlur = 10 + Math.sin(time/GRASS_CONFIG.GLOW_SPEED) * 3; } }; const resetGlowEffect = (ctx, glowing) => { if (glowing) { ctx.restore(); } }; const renderGrassBlades = (ctx, state, x, width, height, groundY, time, color, shadowColor) => { const bladeWidth = width / GRASS_CONFIG.BASE_BLADE_WIDTH * 0.7; for (let i = 0; i < state.bladeCount; i++) { renderSingleBlade( ctx, i, state, x, width, height, groundY, time, bladeWidth, color, shadowColor ); } }; const renderSingleBlade = ( ctx, index, state, x, width, height, groundY, time, bladeWidth, color, shadowColor ) => { const centerOffset = (index - (state.bladeCount - 1) / 2) * (width * GRASS_CONFIG.SPREAD_FACTOR / state.bladeCount); const bladeX = x + centerOffset; const rustleOffset = Math.sin(time / GRASS_CONFIG.RUSTLE_SPEED + index) * 5 * state.rustleAmount; const baseWave = Math.sin(time / GRASS_CONFIG.WAVE_SPEED + index) * 2; drawBladeCurve( ctx, bladeX, groundY, height, rustleOffset, baseWave, bladeWidth, index % 2 === 0 ? color : shadowColor ); }; const drawBladeCurve = ( ctx, bladeX, groundY, height, rustleOffset, baseWave, bladeWidth, color ) => { const cp1x = bladeX + rustleOffset + baseWave; const cp1y = groundY - height / 2; const cp2x = bladeX + rustleOffset * 1.5 + baseWave; const cp2y = groundY - height; ctx.fillStyle = color; ctx.beginPath(); ctx.moveTo(bladeX, groundY); ctx.quadraticCurveTo(cp1x, cp1y, cp2x, cp2y); ctx.quadraticCurveTo(cp1x + bladeWidth, cp1y, bladeX + bladeWidth, groundY); ctx.fill(); }; // Collision detection helper const checkCollision = (player, platform) => { return player.x < platform.x + platform.width && player.x + player.width > platform.x && player.y < platform.y + platform.height && player.y + player.height > platform.y; }; // World physics helper const handleWorldCollisions = (player, world) => { let onGround = false; // Check ground collision first const groundY = window.innerHeight - world.groundHeight; if (player.y + player.height > groundY) { player.y = groundY - player.height; player.velocityY = 0; onGround = true; } // Then check platform collisions for (const platform of world.platforms) { if (checkCollision(player, platform)) { // Calculate overlap on each axis const overlapX = Math.min( player.x + player.width - platform.x, platform.x + platform.width - player.x ); const overlapY = Math.min( player.y + player.height - platform.y, platform.y + platform.height - player.y ); // Resolve collision on the axis with smallest overlap if (overlapX < overlapY) { // Horizontal collision if (player.x < platform.x) { player.x = platform.x - player.width; } else { player.x = platform.x + platform.width; } player.velocityX = 0; } else { // Vertical collision if (player.y < platform.y) { player.y = platform.y - player.height; player.velocityY = 0; onGround = true; } else { player.y = platform.y + platform.height; player.velocityY = 0; } } } } return { ...player, jumping: !onGround }; }; class GrassState { static create(x, height) { return { rustleAmount: 0, rustleDecay: 0.95, bladeCount: utils.getBladeCount(x, height) }; } static validate(state) { if (!state.bladeCount || state.bladeCount < GRASS_CONFIG.MIN_BLADES) { state.bladeCount = GRASS_CONFIG.MIN_BLADES; } return state; } static update(state, distanceToPlayer, interactionDistance) { if (distanceToPlayer < interactionDistance) { state.rustleAmount = Math.min(1, state.rustleAmount + 0.3); } else { state.rustleAmount *= state.rustleDecay; } } } const renderFirTree = (ctx, tree, groundY) => { const { x, config } = tree; const { width, height, canopyOffset, trunkColor, canopyColor } = config; // Setup const greenColors = NATURE_COLORS.GREENS.filter(color => color !== canopyColor); ctx.lineWidth = FIR_CONFIG.FEATHER_WIDTH; // Render trunk FirTreeRenderer.renderTrunk( ctx, x, width, height - (height - canopyOffset)/2, groundY, trunkColor ); const drawFeatheredTree = (baseWidth, baseY, tipY) => { const featherLength = baseWidth * 0.3; for (let row = 0; row < FIR_CONFIG.ROW_COUNT; row++) { const t = row / (FIR_CONFIG.ROW_COUNT - 1); const rowY = baseY - (baseY - tipY) * t; const taper = utils.calculateTaper(t); const rowWidth = baseWidth * taper; if (rowWidth < 2) continue; const featherCount = Math.max(2, Math.floor(FIR_CONFIG.FEATHERS_PER_ROW * taper)); // Render center column of needles FirTreeRenderer.renderCenterNeedles(ctx, x, rowY, taper, featherLength, greenColors); // Render row needles FirTreeRenderer.renderRowNeedles( ctx, x, rowY, rowWidth, featherCount, t, taper, featherLength, greenColors ); } // Render edge needles FirTreeRenderer.renderEdgeNeedles( ctx, x, baseWidth, baseY, tipY, featherLength, greenColors ); }; // Draw complete tree drawFeatheredTree( width * 1.2, groundY - canopyOffset, groundY - height * 1.1 ); };