// Rendering constants const RENDER_CONSTANTS = { VIEWPORT_BUFFER: 100, SKY_COLORS: { TOP: '#1a1a2e', UPPER_MID: '#2d1b3d', LOWER_MID: '#462639', BOTTOM: '#1f1f2f' }, GROUND_COLOR: '#4a4', DEBUG_FONT: '14px monospace', DEBUG_COLOR: '#ffffff' }; // Helper functions for rendering const createSkyGradient = (ctx, groundY) => { const gradient = ctx.createLinearGradient(0, 0, 0, groundY); gradient.addColorStop(0, RENDER_CONSTANTS.SKY_COLORS.TOP); gradient.addColorStop(0.4, RENDER_CONSTANTS.SKY_COLORS.UPPER_MID); gradient.addColorStop(0.7, RENDER_CONSTANTS.SKY_COLORS.LOWER_MID); gradient.addColorStop(1, RENDER_CONSTANTS.SKY_COLORS.BOTTOM); return gradient; }; const getViewBounds = (camera) => ({ left: camera.x - RENDER_CONSTANTS.VIEWPORT_BUFFER, right: camera.x + camera.width + RENDER_CONSTANTS.VIEWPORT_BUFFER, top: camera.y - RENDER_CONSTANTS.VIEWPORT_BUFFER, bottom: camera.y + camera.height + RENDER_CONSTANTS.VIEWPORT_BUFFER }); const isInView = (x, viewBounds) => x > viewBounds.left && x < viewBounds.right; // Layer rendering functions const renderBackground = (ctx, state, groundY, viewBounds) => { // Save the current transform ctx.save(); // Reset transform for viewport-fixed sky and initial black fill ctx.setTransform(1, 0, 0, 1, 0, 0); // Sky (fixed to viewport) ctx.fillStyle = state.cachedGradient; ctx.fillRect(0, 0, ctx.canvas.width, groundY); // Initial black fill for the bottom of the viewport ctx.fillStyle = '#000000'; ctx.fillRect(0, groundY, ctx.canvas.width, ctx.canvas.height - groundY + 1); // Restore transform for world-space rendering ctx.restore(); // Additional black fill that follows the camera (extends far to left and right) ctx.fillStyle = '#000000'; ctx.fillRect( viewBounds.left - 2000, groundY, viewBounds.right - viewBounds.left + 4000, ctx.canvas.height ); }; const renderBackgroundObjects = (ctx, state, groundY, viewBounds) => { state.world.backgroundTrees .filter(tree => isInView(tree.x, viewBounds)) .forEach(tree => renderTree(ctx, tree, groundY)); state.world.backgroundRocks .filter(rock => isInView(rock.x, viewBounds)) .forEach(rock => renderRock(ctx, rock, groundY)); state.world.backgroundGrass .filter(grass => isInView(grass.x, viewBounds)) .forEach(grass => renderGrass(ctx, grass, groundY, state.time)); }; const renderForegroundObjects = (ctx, state, groundY, viewBounds) => { state.world.foregroundTrees .filter(tree => isInView(tree.x, viewBounds)) .forEach(tree => renderTree(ctx, tree, groundY)); state.world.foregroundRocks .filter(rock => isInView(rock.x, viewBounds)) .forEach(rock => renderRock(ctx, rock, groundY)); state.world.foregroundGrass .filter(grass => isInView(grass.x, viewBounds)) .forEach(grass => renderGrass(ctx, grass, groundY, state.time)); }; const renderDebugInfo = (ctx, state) => { ctx.fillStyle = RENDER_CONSTANTS.DEBUG_COLOR; ctx.font = RENDER_CONSTANTS.DEBUG_FONT; const text = `x: ${Math.round(state.debug.mouseX)}, y: ${Math.round(state.debug.mouseY)}`; ctx.fillText(text, 10, ctx.canvas.height - 20); };