diff --git a/static/index.js b/static/index.js index 2c8ed38..e1c4d35 100644 --- a/static/index.js +++ b/static/index.js @@ -1,5 +1,217 @@ -window.onload = () => { - console.log('from js'); - const kennelWindowEle = document.querySelector('#kennel-window'); - kennelWindowEle.innerHTML = 'rendered from static/index.js'; +const CREATURE_HEIGHT = 32; // in px +const CREATURE_WIDTH = 32; // in px +const FRAME_RATE = 12; // in FPS +const FRAME_DELAY = 1 / FRAME_RATE * 1000; // in ms +const DEFAULT_SPRITE_SHEET_COUNT = 1; // number of default sprite sheets + +/** + * Enum of creature states + * @readonly + * @enum {number} + */ +const CreatureState = Object.freeze({ + IDLE: 0, + ALERT: 1, + SCRATCH_SELF: 2, + SCRATCH_NORTH: 3, + SCRATCH_SOUTH: 4, + SCRATCH_EAST: 5, + SCRATCH_WEST: 6, + TIRED: 7, + SLEEPING: 8, + WALK_NORTH: 9, + WALK_NORTHEAST: 10, + WALK_EAST: 11, + WALK_SOUTHEAST: 12, + WALK_SOUTH: 13, + WALK_SOUTHWEST: 14, + WALK_WEST: 15, + WALK_NORTHWEST: 16, +}) + +/** + * @typedef {[number, number]} SpriteFrameOffset the offset of the sprite with respect to + * the left/top background position offset (off by factor of sprite size) + * @type {Object.>} + */ +const CREATURE_STATE_TO_SPRITE_FRAME_OFFSET_INDICES = Object.freeze({ + [CreatureState.IDLE]: [ + [-3, -3] + ], + [CreatureState.ALERT]: [ + [-7, -3] + ], + [CreatureState.SCRATCH_SELF]: [ + [-5, 0], + [-6, 0], + [-7, 0], + ], + [CreatureState.SCRATCH_NORTH]: [ + [0, 0], + [0, -1], + ], + [CreatureState.SCRATCH_SOUTH]: [ + [-7, -1], + [-6, -2], + ], + [CreatureState.SCRATCH_EAST]: [ + [-2, -2], + [-2, -3], + ], + [CreatureState.SCRATCH_WEST]: [ + [-4, 0], + [-4, -1], + ], + [CreatureState.TIRED]: [ + [-3, -2] + ], + [CreatureState.SLEEPING]: [ + [-2, 0], + [-2, -1], + ], + [CreatureState.WALK_NORTH]: [ + [-1, -2], + [-1, -3], + ], + [CreatureState.WALK_NORTHEAST]: [ + [0, -2], + [0, -3], + ], + [CreatureState.WALK_EAST]: [ + [-3, 0], + [-3, -1], + ], + [CreatureState.WALK_SOUTHEAST]: [ + [-5, -1], + [-5, -2], + ], + [CreatureState.WALK_SOUTH]: [ + [-6, -3], + [-7, -2], + ], + [CreatureState.WALK_SOUTHWEST]: [ + [-5, -3], + [-6, -1], + ], + [CreatureState.WALK_WEST]: [ + [-4, -2], + [-4, -3], + ], + [CreatureState.WALK_NORTHWEST]: [ + [-1, 0], + [-1, -1], + ], +}); + +/** + * Properties for creatures running around the screen + * @typedef {Object} Creature + * @property {string} name the name of the creature, used as the HTML element's id + * @property {string} spriteSheet the file name of the sprite sheet. should exist in {@link /static/sprites} + * @property {number} state the current state of the creature (should be member of {@link CreatureState} enum) + * @property {number} stateDuration the number of frames the creature has been in its current state + * @property {number} positionX the number of pixels away from the left side of the container element + * @property {number} positionY the number of pixels away from the top of the container element + * @property {HTMLElement} element the HTML element rendering the creature in the DOM + * @property {HTMLElement} container the HTML element containing the creature (the kennel, if you will) + */ + +/** + * Returns random number between min (inclusive) and max (exclusive) + * If max is less than or equal to min, return min + * @param {number} min inclusive lower bound + * @param {number} max exclusive upper bound + * @return {number} number in [min, max) + */ +const randomInt = (min, max) => { + if (max <= min) return min; + return Math.floor(Math.random() * (max - min) + min); +} + +/** + * Return a creature's next frame based on the current creature frame + * @param {Creature} creature + * @return Creature the next frame of the creature + */ +const getNextCreatureFrame = (creature) => { + // TODO + return { ...creature, stateDuration: creature.stateDuration + 1 }; +} + +/** + * Render a frame of the creature and loads the next frame for render + * @param {Creature} creature + * @return Creature the next frame of the creature + */ +const renderCreature = (creature) => { + // set position + const positionX = Math.min(creature.positionX, creature.container.clientWidth); + const positionY = Math.min(creature.positionY, creature.container.clientHeight); + creature.element.style.setProperty('left', `${positionX}px`); + creature.element.style.setProperty('top', `${positionY}px`); + + // set sprite + const spriteFrames = CREATURE_STATE_TO_SPRITE_FRAME_OFFSET_INDICES[creature.state] + const currentSpriteFrameOffset = spriteFrames?.[creature.stateDuration % spriteFrames.length] + creature.element.style.setProperty( + 'background-position', + `${currentSpriteFrameOffset[0] * CREATURE_WIDTH}px ${currentSpriteFrameOffset[1] * CREATURE_HEIGHT}px` + ) + + const nextCreatureFrame = getNextCreatureFrame(creature); + setTimeout(renderCreature, FRAME_DELAY, nextCreatureFrame) +} + +/** + * Create the creature and start its rendering + * @param {HTMLElement} container container element for creatures. the kennel if you will + * @param {string} name name of the creature + * @param {string} [spriteSheet] name of the sprite sheet. must be in /static/sprites + * uses default sprite sheet if undefined + * @param {number} [initialState] starting state of the creature + * @param {number} [initialPositionX] initial x position in pixels (from the left side) + * @param {number} [initialPositionY] initial y position in pixels (from the top) + */ +const createCreature = ( + container, + name, + spriteSheet, + initialState = CreatureState.IDLE, + initialPositionX = 0, + initialPositionY = 0 +) => { + const creatureEl = document.createElement('div'); + const spriteSheetUrl = spriteSheet + ? `url('/static/sprites/${spriteSheet}')` + : `url('/static/sprites/defaults/${randomInt(1, DEFAULT_SPRITE_SHEET_COUNT)}.gif')`; + + creatureEl.setAttribute('id', name); + creatureEl.style.setProperty('width', `${CREATURE_WIDTH}px`); + creatureEl.style.setProperty('height', `${CREATURE_HEIGHT}px`); + creatureEl.style.setProperty('position', 'fixed'); + creatureEl.style.setProperty('image-rendering', 'pixelated'); + creatureEl.style.setProperty('background-image', spriteSheetUrl); + + container.appendChild(creatureEl); + + renderCreature({ + name, + spriteSheet, + state: initialState, + stateDuration: 0, + positionX: Math.max(0, initialPositionX), + positionY: Math.max(0, initialPositionY), + element: creatureEl, + container + }); +} + +window.onload = () => { + const kennelWindowEle = document.querySelector('#kennel-window'); + createCreature( + kennelWindowEle, + 'test-creature', + undefined, + CreatureState.SCRATCH_SELF + ) } diff --git a/static/sprites/defaults/1.gif b/static/sprites/defaults/1.gif new file mode 100644 index 0000000..0d264d8 Binary files /dev/null and b/static/sprites/defaults/1.gif differ diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..f77e958 --- /dev/null +++ b/static/style.css @@ -0,0 +1,7 @@ +#kennel-window { + width: 80vw; + min-width: 8rem; + height: 80vw; + min-height: 8rem; + margin: auto inherit; +} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 1bb53e8..e36ba58 100644 --- a/templates/index.html +++ b/templates/index.html @@ -4,6 +4,7 @@ + Kennel Club