feat: add update context with all creatures

This commit is contained in:
B Wu 2024-08-10 21:20:06 -07:00
parent 73e1530a1d
commit b005f4b83a
3 changed files with 137 additions and 38 deletions

View File

@ -36,7 +36,7 @@ select = [
] ]
# Note: Ruff supports a top-level `src` option in lieu of isort's `src_paths` setting. # Note: Ruff supports a top-level `src` option in lieu of isort's `src_paths` setting.
src = ["fastapi_poetry_starter", "tests"] src = ["kennel", "tests"]
ignore = [] ignore = []

View File

@ -1,6 +1,8 @@
// === <CONSTANTS> ===
const CREATURE_HEIGHT = 32; // in px const CREATURE_HEIGHT = 32; // in px
const CREATURE_WIDTH = 32; // in px const CREATURE_WIDTH = 32; // in px
const FRAME_RATE = 12; // in FPS const FRAME_RATE = 6; // in FPS
const FRAME_DELAY = 1 / FRAME_RATE * 1000; // in ms const FRAME_DELAY = 1 / FRAME_RATE * 1000; // in ms
const DEFAULT_SPRITE_SHEET_COUNT = 1; // number of default sprite sheets const DEFAULT_SPRITE_SHEET_COUNT = 1; // number of default sprite sheets
@ -103,6 +105,10 @@ const CREATURE_STATE_TO_SPRITE_FRAME_OFFSET_INDICES = Object.freeze({
], ],
}); });
// === </CONSTANTS> ===
// === <TYPES> ===
/** /**
* Properties for creatures running around the screen * Properties for creatures running around the screen
* @typedef {Object} Creature * @typedef {Object} Creature
@ -116,6 +122,10 @@ const CREATURE_STATE_TO_SPRITE_FRAME_OFFSET_INDICES = Object.freeze({
* @property {HTMLElement} container the HTML element containing the creature (the kennel, if you will) * @property {HTMLElement} container the HTML element containing the creature (the kennel, if you will)
*/ */
// === <TYPES> ===
// === <MATH_UTILS> ===
/** /**
* Returns random number between min (inclusive) and max (exclusive) * Returns random number between min (inclusive) and max (exclusive)
* If max is less than or equal to min, return min * If max is less than or equal to min, return min
@ -129,26 +139,63 @@ const randomInt = (min, max) => {
} }
/** /**
* Return a creature's next frame based on the current creature frame * Returns value if value is in [min, max]. Otherwise, bound it to those limits
* @param {Creature} creature * @param {number} value
* @return Creature the next frame of the creature * @param {number} [min] lower bound. if not provided, do not bound the value from beneath
* @param {number} [max] upper bound. if not provided, do not bound the value from above
* @return {number}
*/ */
const getNextCreatureFrame = (creature) => { const constrain = (value, min = -Number.MAX_VALUE, max = Number.MAX_VALUE) => {
// TODO return Math.max(Math.min(value, max), min);
return { ...creature, stateDuration: creature.stateDuration + 1 }; }
/**
* Normalize a vector (ie an array) to be of length 1
* @param {...number} components components of the vector
* @return {number[]} normalized vector
*/
const normalize = (...components) => {
const magnitude = Math.sqrt(
components.reduce((squaredSum, component) => squaredSum + (component * component), 0)
)
return components.map(component => component / magnitude)
}
// === </MATH_UTILS> ===
// === <CREATURE_UTILS> ===
/**
* Return a new updateCreature function that implicitly accepts the context
* of all other creatures being rendered.
* @param {(creature: Creature, allCreatures?: Array<Creature>) => Creature} updateCreature
* updateCreature function that uses the context of all creatures
* @param {Array<Creature>} allCreatures all creatures rendered in the scene
* @return {(creature: Creature) => Creature} an updateCreature function
*/
const updateCreatureWithAllCreaturesContext = (updateCreature, allCreatures) => {
const creaturesMap = Object.fromEntries(allCreatures.map(
creature => [ creature.name, creature ]
));
return (creature) => {
const updatedCreature = updateCreature(creature, Array.from(Object.values(creaturesMap)));
creaturesMap[updatedCreature.name] = { ...updatedCreature };
return updatedCreature;
}
} }
/** /**
* Render a frame of the creature and loads the next frame for render * Render a frame of the creature and loads the next frame for render
* @param {Creature} creature * @param {Creature} creature
* @return Creature the next frame of the creature * @return {Creature} the creature being rendered
*/ */
const renderCreature = (creature) => { const renderCreature = (creature) => {
creature.element.style.setProperty('display', 'block');
// set position // set position
const positionX = Math.min(creature.positionX, creature.container.clientWidth); creature.element.style.setProperty('left', `${creature.positionX}px`);
const positionY = Math.min(creature.positionY, creature.container.clientHeight); creature.element.style.setProperty('top', `${creature.positionY}px`);
creature.element.style.setProperty('left', `${positionX}px`);
creature.element.style.setProperty('top', `${positionY}px`);
// set sprite // set sprite
const spriteFrames = CREATURE_STATE_TO_SPRITE_FRAME_OFFSET_INDICES[creature.state] const spriteFrames = CREATURE_STATE_TO_SPRITE_FRAME_OFFSET_INDICES[creature.state]
@ -157,9 +204,7 @@ const renderCreature = (creature) => {
'background-position', 'background-position',
`${currentSpriteFrameOffset[0] * CREATURE_WIDTH}px ${currentSpriteFrameOffset[1] * CREATURE_HEIGHT}px` `${currentSpriteFrameOffset[0] * CREATURE_WIDTH}px ${currentSpriteFrameOffset[1] * CREATURE_HEIGHT}px`
) )
return creature;
const nextCreatureFrame = getNextCreatureFrame(creature);
setTimeout(renderCreature, FRAME_DELAY, nextCreatureFrame)
} }
/** /**
@ -171,6 +216,7 @@ const renderCreature = (creature) => {
* @param {number} [initialState] starting state of the creature * @param {number} [initialState] starting state of the creature
* @param {number} [initialPositionX] initial x position in pixels (from the left side) * @param {number} [initialPositionX] initial x position in pixels (from the left side)
* @param {number} [initialPositionY] initial y position in pixels (from the top) * @param {number} [initialPositionY] initial y position in pixels (from the top)
* @return Creature
*/ */
const createCreature = ( const createCreature = (
container, container,
@ -180,38 +226,91 @@ const createCreature = (
initialPositionX = 0, initialPositionX = 0,
initialPositionY = 0 initialPositionY = 0
) => { ) => {
const creatureEl = document.createElement('div'); const creatureEle = document.createElement('div');
const spriteSheetUrl = spriteSheet const spriteSheetUrl = spriteSheet
? `url('/static/sprites/${spriteSheet}')` ? `url('/static/sprites/${spriteSheet}')`
: `url('/static/sprites/defaults/${randomInt(1, DEFAULT_SPRITE_SHEET_COUNT)}.gif')`; : `url('/static/sprites/defaults/${randomInt(1, DEFAULT_SPRITE_SHEET_COUNT)}.gif')`;
creatureEl.setAttribute('id', name); creatureEle.setAttribute('id', name);
creatureEl.style.setProperty('width', `${CREATURE_WIDTH}px`); creatureEle.style.setProperty('width', `${CREATURE_WIDTH}px`);
creatureEl.style.setProperty('height', `${CREATURE_HEIGHT}px`); creatureEle.style.setProperty('height', `${CREATURE_HEIGHT}px`);
creatureEl.style.setProperty('position', 'fixed'); creatureEle.style.setProperty('position', 'fixed');
creatureEl.style.setProperty('image-rendering', 'pixelated'); creatureEle.style.setProperty('image-rendering', 'pixelated');
creatureEl.style.setProperty('background-image', spriteSheetUrl); creatureEle.style.setProperty('background-image', spriteSheetUrl);
creatureEle.style.setProperty('display', 'hidden');
container.appendChild(creatureEl); container.appendChild(creatureEle);
renderCreature({ return {
name, name,
spriteSheet, spriteSheet,
state: initialState, state: initialState,
stateDuration: 0, stateDuration: 0,
positionX: Math.max(0, initialPositionX), positionX: constrain(initialPositionX, 0, container.clientWidth),
positionY: Math.max(0, initialPositionY), positionY: constrain(initialPositionY, 0, container.clientHeight),
element: creatureEl, element: creatureEle,
container container
}); }
} }
window.onload = () => { /**
const kennelWindowEle = document.querySelector('#kennel-window'); * Start creature animation
createCreature( * @param {Creature} creature
kennelWindowEle, * @param {(creature: Creature) => Creature} [updateCreature] if undefined, uses identity
'test-creature', * (via {@link renderAndUpdateCreature})
undefined, */
CreatureState.SCRATCH_SELF const beginCreatureAnimation = (
) creature,
updateCreature = (creature) => ({ ... creature })
) => {
const timeoutCallback = (callbackCreature) => {
const newCreatureFrame = updateCreature(renderCreature(callbackCreature));
setTimeout(timeoutCallback, FRAME_DELAY, newCreatureFrame);
}
// render/update initial frame and start animation
setTimeout(timeoutCallback, FRAME_DELAY, updateCreature(renderCreature(creature)));
}
// === </CREATURE_UTILS> ===
window.onload = () => {
// EXAMPLE SCRIPT
const kennelWindowEle = document.querySelector('#kennel-window');
const creatures = ['test-creature-1', 'test-creature-2', 'test-creature-3']
.map((name) => createCreature(
kennelWindowEle,
name,
undefined, // default sprite sheet
randomInt(0, 17), // random state
randomInt(0, 2) * kennelWindowEle.clientWidth, // randomly left or right side
randomInt(0, 2) * kennelWindowEle.clientHeight, // randomly top or bottom
))
const updateCreature = updateCreatureWithAllCreaturesContext((creature, allCreatures) => {
// example update creature script to bring creatures to the centroid
const centroidX = allCreatures.reduce(
(sum, creature) => sum + creature.positionX,
0,
) / allCreatures.length;
const centroidY = allCreatures.reduce(
(sum, creature) => sum + creature.positionY,
0,
) / allCreatures.length;
const [deltaX, deltaY] = normalize(
centroidX - creature.positionX,
centroidY - creature.positionY,
);
return {
...creature,
positionX: constrain(creature.positionX + deltaX, 0, creature.container.clientWidth),
positionY: constrain(creature.positionY + deltaY, 0, creature.container.clientHeight),
stateDuration: creature.stateDuration + 1,
}
}, creatures);
creatures.forEach((creature) => beginCreatureAnimation(creature, updateCreature))
} }

View File

@ -3,5 +3,5 @@
min-width: 8rem; min-width: 8rem;
height: 80vw; height: 80vw;
min-height: 8rem; min-height: 8rem;
margin: auto inherit; margin: auto;
} }