// 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
);
};