Skip to content

Commit

Permalink
enhance landscape rendering; add snow ground rendering with parallax …
Browse files Browse the repository at this point in the history
…effects and state management
  • Loading branch information
gtanczyk committed Dec 1, 2024
1 parent 10b44ec commit 66b0af0
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { LandscapeState } from './landscape-state';
import { renderStars } from './star/star-renderer';
import { renderMountains } from './mountain/mountain-renderer';
import { renderTrees } from './tree/tree-renderer';
import { renderSnowGrounds } from './snow-ground/snow-ground-renderer';
import { ViewportState } from '../render-state';

export function renderLandscape(ctx: CanvasRenderingContext2D, state: LandscapeState, viewport: ViewportState): void {
Expand All @@ -11,15 +12,18 @@ export function renderLandscape(ctx: CanvasRenderingContext2D, state: LandscapeS
// Enable crisp pixel art rendering
ctx.imageSmoothingEnabled = false;

// Render stars
// Render stars (background)
renderStars(ctx, state.stars.stars);

// Render mountains (back to front)
renderMountains(ctx, state.mountains.mountains, viewport);

// Render snow ground (between mountains and trees)
renderSnowGrounds(ctx, state.snowGround.grounds, viewport);

// Render trees (back to front)
renderTrees(ctx, state.trees.trees, viewport);

// Restore the context state
ctx.restore();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ import { createTreeState, updateTreeState } from './tree/tree-state';
import { createStarState, updateStarState } from './star/star-state';
import { TreeState } from './tree/tree-types';
import { StarState } from './star/star-types';
import { SnowGroundState } from './snow-ground/snow-ground-types';
import { createSnowGroundState, updateSnowGroundState } from './snow-ground/snow-ground-state';

// Combined landscape state type
export type LandscapeState = {
mountains: MountainState;
trees: TreeState;
stars: StarState;
snowGround: SnowGroundState;
};

/**
Expand All @@ -22,6 +25,7 @@ export function createLandscapeState(): LandscapeState {
mountains: createMountainState(),
trees: createTreeState(),
stars: createStarState(),
snowGround: createSnowGroundState(),
};
}

Expand All @@ -34,5 +38,6 @@ export function updateLandscapeState(world: GameWorldState, state: LandscapeStat
mountains: updateMountainState(world, state.mountains),
trees: updateTreeState(world, state.trees),
stars: updateStarState(world, state.stars, deltaTime),
snowGround: updateSnowGroundState(world, state.snowGround),
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { GAME_WORLD_HEIGHT } from '../../../game-world/game-world-consts';
import { SnowGround, SNOW_GROUND } from './snow-ground-types';
import { ViewportState } from '../../render-state';

/**
* Apply parallax translation to a point based on viewport position and parallax factor
* Returns pixel-perfect coordinates for smooth rendering
*/
function applyParallaxTranslation(
x: number,
y: number,
viewport: ViewportState,
parallaxFactor: number,
): { x: number; y: number } {
return {
x: Math.round(x + viewport.x * parallaxFactor),
y: Math.round(y + viewport.y * parallaxFactor),
};
}

/**
* Get color for specific snow ground layer
*/
function getLayerColor(layer: number): string {
return layer === 0 ? SNOW_GROUND.COLORS.DISTANT : layer === 1 ? SNOW_GROUND.COLORS.MIDDLE : SNOW_GROUND.COLORS.NEAR;
}

/**
* Render a single snow ground piece using pixel art style with parallax effect
*/
function renderSnowGround(ctx: CanvasRenderingContext2D, ground: SnowGround, viewport: ViewportState): void {
// Calculate base position with parallax
const basePosition = applyParallaxTranslation(ground.x, GAME_WORLD_HEIGHT, viewport, ground.parallaxFactor);

// Calculate dimensions
const width = Math.round(ground.width);
const height = Math.round(GAME_WORLD_HEIGHT * 0.025); // 10% of world height for snow ground

// Draw rectangular snow ground piece
ctx.beginPath();
ctx.rect(basePosition.x, basePosition.y - height, width, height);
ctx.fill();
}

/**
* Render snow grounds by layer with parallax effect
*/
export function renderSnowGrounds(ctx: CanvasRenderingContext2D, grounds: SnowGround[], viewport: ViewportState): void {
// Sort grounds by layer (back to front)
const sortedGrounds = [...grounds].sort((a, b) => a.layer - b.layer);

// Group grounds by layer for efficient rendering
const groundsByLayer = sortedGrounds.reduce((acc, ground) => {
acc[ground.layer] = acc[ground.layer] || [];
acc[ground.layer].push(ground);
return acc;
}, {} as Record<number, SnowGround[]>);

// Save current context state
ctx.save();

// Render each layer with its color
Object.entries(groundsByLayer).forEach(([layer, layerGrounds]) => {
// Set layer-specific color
ctx.fillStyle = getLayerColor(Number(layer));

// Render all grounds in this layer
layerGrounds.forEach((ground) => renderSnowGround(ctx, ground, viewport));
});

// Restore context state
ctx.restore();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { GAME_WORLD_WIDTH } from '../../../game-world/game-world-consts';
import { GameWorldState } from '../../../game-world/game-world-types';
import { SnowGround, SNOW_GROUND, SnowGroundState } from './snow-ground-types';

/**
* Get parallax factor for a specific layer with slight randomization
*/
function getParallaxFactor(layer: number): number {
// Get base parallax factor for the layer
const baseParallax =
layer === 0 ? SNOW_GROUND.PARALLAX.DISTANT : layer === 1 ? SNOW_GROUND.PARALLAX.MIDDLE : SNOW_GROUND.PARALLAX.NEAR;

// Add random variation within the specified range
const variation = (Math.random() * 2 - 1) * SNOW_GROUND.PARALLAX.VARIATION;
return Math.max(0, Math.min(1, baseParallax + variation));
}

/**
* Get number of snow ground pieces for a specific layer
*/
function getGroundDensityForLayer(layer: number): number {
return layer === 0
? SNOW_GROUND.DENSITY.DISTANT
: layer === 1
? SNOW_GROUND.DENSITY.MIDDLE
: SNOW_GROUND.DENSITY.NEAR;
}

/**
* Generate snow ground pieces for a specific layer
*/
function generateGrounds(layer: number): SnowGround[] {
const grounds: SnowGround[] = [];
const count = getGroundDensityForLayer(layer);
const baseSpacing = GAME_WORLD_WIDTH / count;

for (let i = 0; i < count; i++) {
// Calculate width with random variation
const width = SNOW_GROUND.MIN_WIDTH + Math.random() * (SNOW_GROUND.MAX_WIDTH - SNOW_GROUND.MIN_WIDTH);

// Calculate position with spacing variation
const baseX = (i * GAME_WORLD_WIDTH) / count;
const spacingVariation = baseSpacing * SNOW_GROUND.SPACING_VARIATION;
const x = baseX + (Math.random() - 0.5) * spacingVariation;

grounds.push({
x,
width,
layer,
parallaxFactor: getParallaxFactor(layer),
});
}

return grounds;
}

/**
* Create initial snow ground state
*/
export function createSnowGroundState(): SnowGroundState {
const grounds: SnowGround[] = [];

// Generate snow ground pieces for each layer
for (let layer = 0; layer < SNOW_GROUND.LAYERS; layer++) {
grounds.push(...generateGrounds(layer));
}

return { grounds };
}

/**
* Update snow ground state
* Currently snow grounds are static, but this function is included for consistency
* and potential future animations
*/
export function updateSnowGroundState(_world: GameWorldState, state: SnowGroundState): SnowGroundState {
return state;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { GAME_WORLD_WIDTH } from '../../../game-world/game-world-consts';

/**
* Snow ground piece interface
* Represents a single piece of snow ground with position, width, and layer information
*/
export interface SnowGround {
x: number; // X position of the snow ground piece
width: number; // Width of the snow ground piece
layer: number; // Layer number (0: distant, 1: middle, 2: near)
parallaxFactor: number; // Factor for parallax movement
}

/**
* Snow ground state interface
* Contains all snow ground pieces
*/
export interface SnowGroundState {
grounds: SnowGround[];
}

/**
* Snow ground constants
*/
export const SNOW_GROUND = {
// Number of layers for depth perception
LAYERS: 3,

// Width configuration
MIN_WIDTH: GAME_WORLD_WIDTH / 8,
MAX_WIDTH: GAME_WORLD_WIDTH / 4,

// Spacing between snow ground pieces
SPACING_VARIATION: 0.2, // 20% variation in spacing

// Density (number of pieces) per layer
DENSITY: {
DISTANT: 8, // Fewer pieces in the distance
MIDDLE: 10, // Medium density in the middle
NEAR: 12, // More pieces in the foreground
},

// Parallax movement factors
PARALLAX: {
DISTANT: 0.2, // Slowest movement in the background
MIDDLE: 0.5, // Medium movement in the middle
NEAR: 0.8, // Fastest movement in the foreground
VARIATION: 0.05, // Small random variation in parallax
},

// Colors for each layer
COLORS: {
DISTANT: '#a3b5c7', // Light grayish blue for distant snow
MIDDLE: '#cad5e0', // Lighter grayish blue for middle ground
NEAR: '#ffffff', // Pure white for nearest snow
},
} as const;
Original file line number Diff line number Diff line change
Expand Up @@ -3,49 +3,49 @@ import { GAME_WORLD_HEIGHT, GAME_WORLD_WIDTH } from '../../../game-world/game-wo
// Tree silhouette colors from back to front
export const TREE_COLORS = {
DISTANT: '#000033', // Most distant trees
MIDDLE: '#000022', // Middle layer trees
NEAR: '#000011', // Nearest trees
MIDDLE: '#000022', // Middle layer trees
NEAR: '#000011', // Nearest trees
} as const;

// Tree configuration
export const TREES = {
LAYERS: 3, // Number of tree layers for depth
MAX_HEIGHT: GAME_WORLD_HEIGHT * 0.15, // Maximum tree height
MIN_HEIGHT: GAME_WORLD_HEIGHT * 0.1, // Minimum tree height
MIN_HEIGHT: GAME_WORLD_HEIGHT * 0.1, // Minimum tree height
WIDTH_RATIO: 0.6, // Tree width as ratio of height
DENSITY: {
// Trees per layer (more trees in closer layers)
DISTANT: Math.floor(GAME_WORLD_WIDTH / 150), // Fewest trees, most distant
MIDDLE: Math.floor(GAME_WORLD_WIDTH / 100), // Medium number of trees
NEAR: Math.floor(GAME_WORLD_WIDTH / 50), // Most trees, nearest layer
MIDDLE: Math.floor(GAME_WORLD_WIDTH / 100), // Medium number of trees
NEAR: Math.floor(GAME_WORLD_WIDTH / 50), // Most trees, nearest layer
},
PARALLAX: {
// Parallax factors for each layer (0 = no movement, 1 = full movement)
DISTANT: 0.65, // Continuing from mountain parallax (mountains end at 0.6)
MIDDLE: 0.8, // Medium movement
NEAR: 0.95, // Fastest movement, nearest layer
DISTANT: 0.75, // Continuing from mountain parallax (mountains end at 0.6)
MIDDLE: 0.8, // Medium movement
NEAR: 0.95, // Fastest movement, nearest layer
VARIATION: 0.05, // Random variation in parallax factor
},
// Size multipliers for each layer to enhance depth perception
SIZE_MULTIPLIER: {
DISTANT: 0.7, // Smallest trees in distance
MIDDLE: 0.85, // Medium sized trees
NEAR: 1.0, // Full size trees in front
DISTANT: 0.7, // Smallest trees in distance
MIDDLE: 0.85, // Medium sized trees
NEAR: 1.0, // Full size trees in front
},
// Spacing variation to avoid uniform appearance
SPACING_VARIATION: 0.3, // How much random variation in tree spacing
} as const;

// Tree type definition
export type Tree = {
x: number; // Base x position
height: number; // Height of the tree
width: number; // Width of the tree
layer: number; // Which layer (0 = distant, 1 = middle, 2 = near)
x: number; // Base x position
height: number; // Height of the tree
width: number; // Width of the tree
layer: number; // Which layer (0 = distant, 1 = middle, 2 = near)
parallaxFactor: number; // Factor affecting parallax movement
};

// Tree state type
export type TreeState = {
trees: Tree[];
};
};

0 comments on commit 66b0af0

Please sign in to comment.