3 changed files with 386 additions and 303 deletions
@ -0,0 +1,198 @@ |
|||||||
|
/** |
||||||
|
* @fileoverview Low-level canvas utilities and sprite system. |
||||||
|
* Provides drawing primitives and sprite management. |
||||||
|
*/ |
||||||
|
|
||||||
|
/** @type {HTMLCanvasElement} */ |
||||||
|
export const canvas = document.getElementById('scene'); |
||||||
|
|
||||||
|
/** @type {CanvasRenderingContext2D} */ |
||||||
|
export const ctx = canvas.getContext('2d'); |
||||||
|
|
||||||
|
/** |
||||||
|
* @typedef {Object} SpriteOptions |
||||||
|
* @property {string} source - Path to the sprite image file |
||||||
|
* @property {number} [frameWidth] - Width of each frame for animated sprites |
||||||
|
* @property {number} [frameHeight] - Height of each frame for animated sprites |
||||||
|
* @property {number} [frameCount=1] - Number of frames in the sprite sheet |
||||||
|
* @property {number} [frameIndex] - Current frame index for animation |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* @typedef {Object} Sprite |
||||||
|
* @property {string} source - Path to the sprite image file |
||||||
|
* @property {HTMLImageElement} image - The loaded image element |
||||||
|
* @property {boolean} ready - Whether the image has finished loading |
||||||
|
* @property {number|null} frameWidth - Width of each frame (null for single-frame sprites) |
||||||
|
* @property {number|null} frameHeight - Height of each frame (null for single-frame sprites) |
||||||
|
* @property {number} frameCount - Total number of frames in the sprite |
||||||
|
* @property {number} [frameIndex] - Current frame index for animation |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* @typedef {Object} Cup |
||||||
|
* @property {Sprite} sprite - Reference to the cup sprite template |
||||||
|
* @property {number} x - X position on canvas |
||||||
|
* @property {number} y - Y position on canvas |
||||||
|
* @property {number} scale - Render scale |
||||||
|
* @property {number} frameIndex - Current frame (0 = empty, 1 = filled) |
||||||
|
* @property {boolean} filled - Whether the cup is filled |
||||||
|
* @property {Function} fill - Fills the cup |
||||||
|
* @property {Function} empty - Empties the cup |
||||||
|
* @property {Function} isFilled - Returns whether cup is filled |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates a new sprite object from the given options. |
||||||
|
* |
||||||
|
* @param {SpriteOptions} options - Configuration options for the sprite |
||||||
|
* @param {Function} [onLoad] - Optional callback when sprite loads |
||||||
|
* @returns {Sprite} The created sprite object |
||||||
|
*/ |
||||||
|
export function createSprite(options, onLoad) { |
||||||
|
const image = new Image(); |
||||||
|
|
||||||
|
const sprite = { |
||||||
|
source: options.source, |
||||||
|
image, |
||||||
|
ready: false, |
||||||
|
|
||||||
|
// optionals
|
||||||
|
frameWidth: options.frameWidth || null, |
||||||
|
frameHeight: options.frameHeight || null, |
||||||
|
frameCount: options.frameCount ?? 1, |
||||||
|
frameIndex: options.frameIndex || 0 |
||||||
|
}; |
||||||
|
|
||||||
|
image.onload = () => { |
||||||
|
sprite.ready = true; |
||||||
|
if (onLoad) onLoad(); |
||||||
|
}; |
||||||
|
|
||||||
|
image.src = options.source; |
||||||
|
return sprite; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Draws a sprite onto the canvas at the specified position. |
||||||
|
* |
||||||
|
* @param {Sprite} spriteObject - The sprite to draw |
||||||
|
* @param {number} x - The x-coordinate on the canvas |
||||||
|
* @param {number} y - The y-coordinate on the canvas |
||||||
|
* @param {number} scale - Scale factor for rendering |
||||||
|
* @returns {void} |
||||||
|
*/ |
||||||
|
export function drawSprite(spriteObject, x, y, scale) { |
||||||
|
if (!spriteObject.ready) return; |
||||||
|
|
||||||
|
const frameIndex = spriteObject.frameIndex; |
||||||
|
|
||||||
|
const isFramedSprite = spriteObject.frameWidth !== null |
||||||
|
|| spriteObject.frameHeight !== null; |
||||||
|
|
||||||
|
if (!isFramedSprite) { |
||||||
|
const drawWidth = spriteObject.image.naturalWidth * scale; |
||||||
|
const drawHeight = spriteObject.image.naturalHeight * scale; |
||||||
|
|
||||||
|
ctx.drawImage(spriteObject.image, x, y, drawWidth, drawHeight); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// framed horizontal sprite
|
||||||
|
const sourceX = frameIndex * spriteObject.frameWidth; |
||||||
|
const sourceY = 0; |
||||||
|
|
||||||
|
const drawWidth = spriteObject.frameWidth * scale; |
||||||
|
const drawHeight = spriteObject.frameHeight * scale; |
||||||
|
|
||||||
|
ctx.drawImage( |
||||||
|
spriteObject.image, |
||||||
|
sourceX, |
||||||
|
sourceY, |
||||||
|
spriteObject.frameWidth, |
||||||
|
spriteObject.frameHeight, |
||||||
|
x, |
||||||
|
y, |
||||||
|
drawWidth, |
||||||
|
drawHeight |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Draws a cup (or any object with sprite, x, y, scale, frameIndex). |
||||||
|
* |
||||||
|
* @param {Cup} instance - The cup to draw |
||||||
|
* @returns {void} |
||||||
|
*/ |
||||||
|
export function drawInstance(instance) { |
||||||
|
const { sprite, x, y, scale, frameIndex } = instance; |
||||||
|
|
||||||
|
if (!sprite.ready) return; |
||||||
|
|
||||||
|
const isFramedSprite = sprite.frameWidth !== null |
||||||
|
|| sprite.frameHeight !== null; |
||||||
|
|
||||||
|
if (!isFramedSprite) { |
||||||
|
const drawWidth = sprite.image.naturalWidth * scale; |
||||||
|
const drawHeight = sprite.image.naturalHeight * scale; |
||||||
|
ctx.drawImage(sprite.image, x, y, drawWidth, drawHeight); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// framed horizontal sprite
|
||||||
|
const sourceX = frameIndex * sprite.frameWidth; |
||||||
|
const drawWidth = sprite.frameWidth * scale; |
||||||
|
const drawHeight = sprite.frameHeight * scale; |
||||||
|
|
||||||
|
ctx.drawImage( |
||||||
|
sprite.image, |
||||||
|
sourceX, |
||||||
|
0, |
||||||
|
sprite.frameWidth, |
||||||
|
sprite.frameHeight, |
||||||
|
x, |
||||||
|
y, |
||||||
|
drawWidth, |
||||||
|
drawHeight |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Draws the ground using a grass sprite as a repeating pattern. |
||||||
|
* |
||||||
|
* @param {Sprite} grassSprite - The grass sprite to use as pattern |
||||||
|
* @param {number} [groundHeight=250] - Height of the ground area |
||||||
|
* @returns {void} |
||||||
|
*/ |
||||||
|
export function drawGround(grassSprite, groundHeight = 250) { |
||||||
|
if (!grassSprite.ready) return; |
||||||
|
|
||||||
|
const pattern = ctx.createPattern(grassSprite.image, 'repeat'); |
||||||
|
ctx.fillStyle = pattern; |
||||||
|
|
||||||
|
ctx.fillRect(0, canvas.height - groundHeight, canvas.width, groundHeight); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Draws an oval shadow. |
||||||
|
* |
||||||
|
* @param {number} x - Center x position |
||||||
|
* @param {number} y - Center y position |
||||||
|
* @param {number} radiusX - Horizontal radius |
||||||
|
* @param {number} radiusY - Vertical radius |
||||||
|
* @param {string} [color='rgba(0, 0, 0, 0.3)'] - Shadow color |
||||||
|
*/ |
||||||
|
export function drawShadow(x, y, radiusX, radiusY, color = 'rgba(0, 0, 0, 0.3)') { |
||||||
|
ctx.beginPath(); |
||||||
|
ctx.ellipse(x, y, radiusX, radiusY, 0, 0, Math.PI * 2); |
||||||
|
ctx.fillStyle = color; |
||||||
|
ctx.fill(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Clears the entire canvas. |
||||||
|
*/ |
||||||
|
export function clearCanvas() { |
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height); |
||||||
|
} |
||||||
|
|
||||||
@ -0,0 +1,161 @@ |
|||||||
|
/** |
||||||
|
* @fileoverview Canvas controller for scene management. |
||||||
|
* Handles sprites, scene objects, rendering, and game loop. |
||||||
|
*/ |
||||||
|
|
||||||
|
import { |
||||||
|
canvas, |
||||||
|
ctx, |
||||||
|
createSprite, |
||||||
|
drawSprite, |
||||||
|
drawInstance, |
||||||
|
drawGround, |
||||||
|
drawShadow, |
||||||
|
clearCanvas |
||||||
|
} from './canvas.js'; |
||||||
|
|
||||||
|
/** @type {Function|null} Callback to invoke on render */ |
||||||
|
let onRenderCallback = null; |
||||||
|
|
||||||
|
/** |
||||||
|
* Collection of sprite templates (shared image/frame data) |
||||||
|
* @type {Object.<string, import('./canvas.js').Sprite>} |
||||||
|
*/ |
||||||
|
export const sprites = { |
||||||
|
stand: createSprite({ source: 'stand.png' }), |
||||||
|
cup: createSprite({ |
||||||
|
source: 'cups.png', |
||||||
|
frameWidth: 251, |
||||||
|
frameHeight: 330, |
||||||
|
frameCount: 2 |
||||||
|
}), |
||||||
|
maker: createSprite({ |
||||||
|
source: 'maker.png', |
||||||
|
frameWidth: 562 / 2, |
||||||
|
frameHeight: 432, |
||||||
|
frameCount: 2 |
||||||
|
}), |
||||||
|
ice: createSprite({ |
||||||
|
source: 'ice.png' |
||||||
|
}), |
||||||
|
pitcher: createSprite({ |
||||||
|
source: 'pitcher_full.png' |
||||||
|
}), |
||||||
|
lemons: createSprite({ |
||||||
|
source: 'lemons.png' |
||||||
|
}), |
||||||
|
grass: createSprite({ |
||||||
|
source: 'grass.png' |
||||||
|
}), |
||||||
|
tree: createSprite({ |
||||||
|
source: 'tree.png' |
||||||
|
}), |
||||||
|
slide: createSprite({ |
||||||
|
source: 'slide.png' |
||||||
|
}), |
||||||
|
seesaw: createSprite({ |
||||||
|
source: 'seesaw.png' |
||||||
|
}) |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* Checks if all sprites are loaded. |
||||||
|
* @returns {boolean} |
||||||
|
*/ |
||||||
|
export function allSpritesReady() { |
||||||
|
return Object.values(sprites).every(sprite => sprite.ready); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Waits for all sprites to load, then calls the callback. |
||||||
|
* @param {Function} callback - Function to call when all sprites are ready |
||||||
|
*/ |
||||||
|
export function whenSpritesReady(callback) { |
||||||
|
function check() { |
||||||
|
if (allSpritesReady()) { |
||||||
|
callback(); |
||||||
|
} else { |
||||||
|
requestAnimationFrame(check); |
||||||
|
} |
||||||
|
} |
||||||
|
check(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates a cup instance. |
||||||
|
* |
||||||
|
* @param {number} x - X position |
||||||
|
* @param {number} y - Y position |
||||||
|
* @param {number} [scale=0.1] - Render scale |
||||||
|
* @returns {import('./canvas.js').Cup} |
||||||
|
*/ |
||||||
|
export function createCup(x, y, scale = 0.1) { |
||||||
|
return { |
||||||
|
sprite: sprites.cup, |
||||||
|
x, |
||||||
|
y, |
||||||
|
scale, |
||||||
|
frameIndex: 0, |
||||||
|
filled: false, |
||||||
|
|
||||||
|
/** Fills the cup */ |
||||||
|
fill() { |
||||||
|
this.filled = true; |
||||||
|
this.frameIndex = 1; |
||||||
|
}, |
||||||
|
|
||||||
|
/** Empties the cup */ |
||||||
|
empty() { |
||||||
|
this.filled = false; |
||||||
|
this.frameIndex = 0; |
||||||
|
}, |
||||||
|
|
||||||
|
/** @returns {boolean} Whether the cup is filled */ |
||||||
|
isFilled() { |
||||||
|
return this.filled; |
||||||
|
} |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Game objects - cups on the stand |
||||||
|
* @type {Array<import('./canvas.js').Cup>} |
||||||
|
*/ |
||||||
|
export const cups = [ |
||||||
|
createCup(455, 325, 0.15), |
||||||
|
createCup(495, 325, 0.15), |
||||||
|
createCup(535, 325, 0.15), |
||||||
|
createCup(575, 325, 0.15), |
||||||
|
]; |
||||||
|
|
||||||
|
/** |
||||||
|
* Renders the entire scene. |
||||||
|
*/ |
||||||
|
export function render() { |
||||||
|
clearCanvas(); |
||||||
|
|
||||||
|
// Background
|
||||||
|
drawGround(sprites.grass); |
||||||
|
|
||||||
|
// Background trees
|
||||||
|
drawSprite(sprites.tree, 600, 50, 0.25); |
||||||
|
drawSprite(sprites.tree, 200, 50, 0.2); |
||||||
|
|
||||||
|
// Playground equipment
|
||||||
|
drawSprite(sprites.slide, 880, 80, 0.4); |
||||||
|
drawSprite(sprites.seesaw, 200, 280, 0.30); |
||||||
|
|
||||||
|
// Shadow under the stand
|
||||||
|
drawShadow(500, 360, 180, 50); |
||||||
|
|
||||||
|
// Lemonade stand
|
||||||
|
drawSprite(sprites.maker, 430, 160, 0.6); |
||||||
|
drawSprite(sprites.stand, 350, 50, 0.7); |
||||||
|
drawSprite(sprites.pitcher, 620, 290, 0.3); |
||||||
|
|
||||||
|
// Draw all cup instances
|
||||||
|
cups.forEach(cup => drawInstance(cup)); |
||||||
|
|
||||||
|
// Invoke callback if set
|
||||||
|
if (onRenderCallback) onRenderCallback(); |
||||||
|
} |
||||||
@ -1,314 +1,38 @@ |
|||||||
/** |
/** |
||||||
* @fileoverview Lemonade stand game rendering module. |
* @fileoverview Main application orchestrator. |
||||||
* Handles sprite loading and canvas rendering. |
* Initializes game state, wires up UI events, and coordinates modules. |
||||||
*/ |
*/ |
||||||
|
|
||||||
import { init_game, make_lemonade, set_recipe, set_weather, Weather } from './game.js'; |
import { init_game } from './game.js'; |
||||||
|
import { sprites, cups, render, whenSpritesReady } from './canvasController.js'; |
||||||
/** @type {HTMLCanvasElement} */ |
|
||||||
const canvas = document.getElementById('scene'); |
|
||||||
|
|
||||||
// Initialize game state
|
// Initialize game state
|
||||||
let gameState = init_game(); |
let gameState = init_game(); |
||||||
|
|
||||||
/** @type {CanvasRenderingContext2D} */ |
// Wait for all sprites to load, then render once
|
||||||
const ctx = canvas.getContext('2d'); |
whenSpritesReady(() => { |
||||||
|
render(); |
||||||
/** @type {number} Global scale factor for rendering */ |
|
||||||
const scale = 0.4; |
|
||||||
|
|
||||||
/** |
|
||||||
* @typedef {Object} SpriteOptions |
|
||||||
* @property {string} source - Path to the sprite image file |
|
||||||
* @property {number} [frameWidth] - Width of each frame for animated sprites |
|
||||||
* @property {number} [frameHeight] - Height of each frame for animated sprites |
|
||||||
* @property {number} [frameCount=1] - Number of frames in the sprite sheet |
|
||||||
* @property {number} [frameIndex] - Current frame index for animation |
|
||||||
*/ |
|
||||||
|
|
||||||
/** |
|
||||||
* @typedef {Object} Sprite |
|
||||||
* @property {string} source - Path to the sprite image file |
|
||||||
* @property {HTMLImageElement} image - The loaded image element |
|
||||||
* @property {boolean} ready - Whether the image has finished loading |
|
||||||
* @property {number|null} frameWidth - Width of each frame (null for single-frame sprites) |
|
||||||
* @property {number|null} frameHeight - Height of each frame (null for single-frame sprites) |
|
||||||
* @property {number} frameCount - Total number of frames in the sprite |
|
||||||
* @property {number} [frameIndex] - Current frame index for animation |
|
||||||
*/ |
|
||||||
|
|
||||||
/** |
|
||||||
* @typedef {Object} Cup |
|
||||||
* @property {Sprite} sprite - Reference to the cup sprite template |
|
||||||
* @property {number} x - X position on canvas |
|
||||||
* @property {number} y - Y position on canvas |
|
||||||
* @property {number} scale - Render scale |
|
||||||
* @property {number} frameIndex - Current frame (0 = empty, 1 = filled) |
|
||||||
* @property {boolean} filled - Whether the cup is filled |
|
||||||
* @property {Function} fill - Fills the cup |
|
||||||
* @property {Function} empty - Empties the cup |
|
||||||
* @property {Function} isFilled - Returns whether cup is filled |
|
||||||
*/ |
|
||||||
|
|
||||||
/** |
|
||||||
* Collection of sprite templates (shared image/frame data) |
|
||||||
* @type {Object.<string, Sprite>} |
|
||||||
*/ |
|
||||||
const sprites = { |
|
||||||
stand: createSprite({ source: 'stand.png' }), |
|
||||||
cup: createSprite({ |
|
||||||
source: 'cups.png', |
|
||||||
frameWidth: 251, |
|
||||||
frameHeight: 330, |
|
||||||
frameCount: 2 |
|
||||||
}), |
|
||||||
maker: createSprite({ |
|
||||||
source: 'maker.png', |
|
||||||
frameWidth: 562 / 2, |
|
||||||
frameHeight: 432, |
|
||||||
frameCount: 2 |
|
||||||
}), |
|
||||||
ice: createSprite({ |
|
||||||
source: 'ice.png' |
|
||||||
}), |
|
||||||
pitcher: createSprite({ |
|
||||||
source: 'pitcher_full.png' |
|
||||||
}), |
|
||||||
lemons: createSprite({ |
|
||||||
source: 'lemons.png' |
|
||||||
}), |
|
||||||
grass: createSprite({ |
|
||||||
source: 'grass.png' |
|
||||||
}), |
|
||||||
tree: createSprite({ |
|
||||||
source: 'tree.png' |
|
||||||
}), |
|
||||||
slide: createSprite({ |
|
||||||
source: 'slide.png' |
|
||||||
}), |
|
||||||
seesaw: createSprite({ |
|
||||||
source: 'seesaw.png' |
|
||||||
}) |
|
||||||
}; |
|
||||||
|
|
||||||
/** |
|
||||||
* Creates a cup instance. |
|
||||||
* |
|
||||||
* @param {number} x - X position |
|
||||||
* @param {number} y - Y position |
|
||||||
* @param {number} [scale=0.1] - Render scale |
|
||||||
* @returns {Cup} |
|
||||||
*/ |
|
||||||
function createCup(x, y, scale = 0.1) { |
|
||||||
return { |
|
||||||
sprite: sprites.cup, |
|
||||||
x, |
|
||||||
y, |
|
||||||
scale, |
|
||||||
frameIndex: 0, |
|
||||||
filled: false, |
|
||||||
|
|
||||||
/** Fills the cup */ |
|
||||||
fill() { |
|
||||||
this.filled = true; |
|
||||||
this.frameIndex = 1; |
|
||||||
}, |
|
||||||
|
|
||||||
/** Empties the cup */ |
|
||||||
empty() { |
|
||||||
this.filled = false; |
|
||||||
this.frameIndex = 0; |
|
||||||
}, |
|
||||||
|
|
||||||
/** @returns {boolean} Whether the cup is filled */ |
|
||||||
isFilled() { |
|
||||||
return this.filled; |
|
||||||
} |
|
||||||
}; |
|
||||||
} |
|
||||||
|
|
||||||
// Game objects
|
|
||||||
const cups = [ |
|
||||||
createCup(455, 325, 0.15), |
|
||||||
createCup(495, 325, 0.15), |
|
||||||
createCup(535, 325, 0.15), |
|
||||||
createCup(575, 325, 0.15), |
|
||||||
|
|
||||||
] |
|
||||||
|
|
||||||
/** |
|
||||||
* Creates a new sprite object from the given options. |
|
||||||
* Automatically triggers a render when the image loads. |
|
||||||
* |
|
||||||
* @param {SpriteOptions} options - Configuration options for the sprite |
|
||||||
* @returns {Sprite} The created sprite object |
|
||||||
*/ |
|
||||||
function createSprite(options) { |
|
||||||
const image = new Image(); |
|
||||||
|
|
||||||
const sprite = { |
|
||||||
source: options.source, |
|
||||||
image, |
|
||||||
ready: false, |
|
||||||
|
|
||||||
// optionals
|
|
||||||
frameWidth: options.frameWidth || null, |
|
||||||
frameHeight: options.frameHeight || null, |
|
||||||
frameCount: options.frameCount ?? 1, |
|
||||||
frameIndex: options.frameIndex || 0 |
|
||||||
}; |
|
||||||
|
|
||||||
image.onload = () => { |
// Example: fill the first cup after 1 second
|
||||||
sprite.ready = true; |
setTimeout(() => { |
||||||
|
cups[0].fill(); |
||||||
|
sprites.maker.frameIndex = 1; |
||||||
render(); |
render(); |
||||||
} |
}, 1000); |
||||||
|
}); |
||||||
image.src = options.source; |
|
||||||
return sprite; |
// UI Elements
|
||||||
|
const goShoppingBtn = document.querySelector('.go-shopping-btn'); |
||||||
|
|
||||||
|
// Event handlers
|
||||||
|
if (goShoppingBtn) { |
||||||
|
goShoppingBtn.addEventListener('click', () => { |
||||||
|
console.log('Go shopping clicked!'); |
||||||
|
// TODO: Open shopping modal/UI
|
||||||
|
}); |
||||||
} |
} |
||||||
|
|
||||||
/** |
// Export for debugging in console
|
||||||
* Draws a sprite onto the canvas at the specified position. |
window.gameState = gameState; |
||||||
* |
window.sprites = sprites; |
||||||
* @param {Sprite} spriteObject - The sprite to draw |
window.cups = cups; |
||||||
* @param {number} x - The x-coordinate on the canvas |
|
||||||
* @param {number} y - The y-coordinate on the canvas |
|
||||||
* @param {number} scale - Scale factor for rendering |
|
||||||
* @returns {void} |
|
||||||
*/ |
|
||||||
function drawSprite(spriteObject, x, y, scale) { |
|
||||||
if (!spriteObject.ready) return; |
|
||||||
|
|
||||||
const frameIndex = spriteObject.frameIndex; |
|
||||||
|
|
||||||
const isFramedSprite = spriteObject.frameWidth !== null |
|
||||||
|| spriteObject.frameHeight !== null; |
|
||||||
|
|
||||||
if (!isFramedSprite) { |
|
||||||
const drawWidth = spriteObject.image.naturalWidth * scale; |
|
||||||
const drawHeight = spriteObject.image.naturalHeight * scale; |
|
||||||
|
|
||||||
ctx.drawImage(spriteObject.image, x, y, drawWidth, drawHeight); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
// framed horizontal sprite
|
|
||||||
const sourceX = frameIndex * spriteObject.frameWidth; |
|
||||||
const sourceY = 0; |
|
||||||
|
|
||||||
const drawWidth = spriteObject.frameWidth * scale; |
|
||||||
const drawHeight = spriteObject.frameHeight * scale; |
|
||||||
|
|
||||||
ctx.drawImage( |
|
||||||
spriteObject.image, |
|
||||||
sourceX, |
|
||||||
sourceY, |
|
||||||
spriteObject.frameWidth, |
|
||||||
spriteObject.frameHeight, |
|
||||||
x, |
|
||||||
y, |
|
||||||
drawWidth, |
|
||||||
drawHeight |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Draws a cup (or any object with sprite, x, y, scale, frameIndex). |
|
||||||
* |
|
||||||
* @param {Cup} instance - The cup to draw |
|
||||||
* @returns {void} |
|
||||||
*/ |
|
||||||
function drawInstance(instance) { |
|
||||||
const { sprite, x, y, scale, frameIndex } = instance; |
|
||||||
|
|
||||||
if (!sprite.ready) return; |
|
||||||
|
|
||||||
const isFramedSprite = sprite.frameWidth !== null |
|
||||||
|| sprite.frameHeight !== null; |
|
||||||
|
|
||||||
if (!isFramedSprite) { |
|
||||||
const drawWidth = sprite.image.naturalWidth * scale; |
|
||||||
const drawHeight = sprite.image.naturalHeight * scale; |
|
||||||
ctx.drawImage(sprite.image, x, y, drawWidth, drawHeight); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
// framed horizontal sprite
|
|
||||||
const sourceX = frameIndex * sprite.frameWidth; |
|
||||||
const drawWidth = sprite.frameWidth * scale; |
|
||||||
const drawHeight = sprite.frameHeight * scale; |
|
||||||
|
|
||||||
ctx.drawImage( |
|
||||||
sprite.image, |
|
||||||
sourceX, |
|
||||||
0, |
|
||||||
sprite.frameWidth, |
|
||||||
sprite.frameHeight, |
|
||||||
x, |
|
||||||
y, |
|
||||||
drawWidth, |
|
||||||
drawHeight |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
function drawGround() { |
|
||||||
if (!sprites.grass.ready) return; |
|
||||||
|
|
||||||
const pattern = ctx.createPattern(sprites.grass.image, 'repeat'); |
|
||||||
ctx.fillStyle = pattern; |
|
||||||
|
|
||||||
const groundHeight = 250; |
|
||||||
ctx.fillRect(0, canvas.height - groundHeight, canvas.width, groundHeight); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Draws an oval shadow. |
|
||||||
* |
|
||||||
* @param {number} x - Center x position |
|
||||||
* @param {number} y - Center y position |
|
||||||
* @param {number} radiusX - Horizontal radius |
|
||||||
* @param {number} radiusY - Vertical radius |
|
||||||
* @param {string} [color='rgba(0, 0, 0, 0.25)'] - Shadow color |
|
||||||
*/ |
|
||||||
function drawShadow(x, y, radiusX, radiusY, color = 'rgba(0, 0, 0, 0.3)') { |
|
||||||
ctx.beginPath(); |
|
||||||
ctx.ellipse(x, y, radiusX, radiusY, 0, 0, Math.PI * 2); |
|
||||||
ctx.fillStyle = color; |
|
||||||
ctx.fill(); |
|
||||||
} |
|
||||||
|
|
||||||
function render() { |
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height); |
|
||||||
|
|
||||||
drawGround(); |
|
||||||
drawSprite(sprites.tree, 600, 50, 0.25); |
|
||||||
drawSprite(sprites.tree, 200, 50, 0.2); |
|
||||||
|
|
||||||
// Slide on the right
|
|
||||||
drawSprite(sprites.slide, 880, 80, 0.4); |
|
||||||
|
|
||||||
// Seesaw
|
|
||||||
drawSprite(sprites.seesaw, 200, 280, 0.30); |
|
||||||
|
|
||||||
|
|
||||||
// Shadow under the stand
|
|
||||||
drawShadow(500, 360, 180, 50); |
|
||||||
|
|
||||||
drawSprite(sprites.maker, 430, 160, 0.6); |
|
||||||
drawSprite(sprites.stand, 350, 50, 0.7); |
|
||||||
drawSprite(sprites.pitcher, 620, 290, 0.3); |
|
||||||
|
|
||||||
|
|
||||||
// Draw all cup instances
|
|
||||||
cups.forEach(cup => drawInstance(cup)); |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
render(); |
|
||||||
|
|
||||||
// Example: fill the first cup after 1 second
|
|
||||||
setTimeout(() => { |
|
||||||
cups[0].fill(); |
|
||||||
sprites.maker.frameIndex = 1; |
|
||||||
render(); |
|
||||||
}, 1000); |
|
||||||
|
|||||||
Loading…
Reference in new issue