Browse Source

reorganizing

master
Stephanie Gredell 3 weeks ago
parent
commit
176cb975ec
  1. 198
      canvas.js
  2. 161
      canvasController.js
  3. 330
      index.js

198
canvas.js

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

161
canvasController.js

@ -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();
}

330
index.js

@ -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…
Cancel
Save