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(); |
||||||
|
} |
||||||
Loading…
Reference in new issue