diff --git a/pyproject.toml b/pyproject.toml
index 84eb4e3..369a432 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -36,7 +36,7 @@ select = [
]
# 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 = []
diff --git a/static/index.js b/static/index.js
index e1c4d35..088f304 100644
--- a/static/index.js
+++ b/static/index.js
@@ -1,6 +1,8 @@
+// === ===
+
const CREATURE_HEIGHT = 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 DEFAULT_SPRITE_SHEET_COUNT = 1; // number of default sprite sheets
@@ -103,6 +105,10 @@ const CREATURE_STATE_TO_SPRITE_FRAME_OFFSET_INDICES = Object.freeze({
],
});
+// === ===
+
+// === ===
+
/**
* Properties for creatures running around the screen
* @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)
*/
+// === ===
+
+// === ===
+
/**
* Returns random number between min (inclusive) and max (exclusive)
* 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
- * @param {Creature} creature
- * @return Creature the next frame of the creature
+ * Returns value if value is in [min, max]. Otherwise, bound it to those limits
+ * @param {number} value
+ * @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) => {
- // TODO
- return { ...creature, stateDuration: creature.stateDuration + 1 };
+const constrain = (value, min = -Number.MAX_VALUE, max = Number.MAX_VALUE) => {
+ return Math.max(Math.min(value, max), min);
+}
+
+/**
+ * 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)
+}
+
+// === ===
+
+// === ===
+
+/**
+ * Return a new updateCreature function that implicitly accepts the context
+ * of all other creatures being rendered.
+ * @param {(creature: Creature, allCreatures?: Array) => Creature} updateCreature
+ * updateCreature function that uses the context of all creatures
+ * @param {Array} 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
* @param {Creature} creature
- * @return Creature the next frame of the creature
+ * @return {Creature} the creature being rendered
*/
const renderCreature = (creature) => {
+ creature.element.style.setProperty('display', 'block');
+
// 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`);
+ creature.element.style.setProperty('left', `${creature.positionX}px`);
+ creature.element.style.setProperty('top', `${creature.positionY}px`);
// set sprite
const spriteFrames = CREATURE_STATE_TO_SPRITE_FRAME_OFFSET_INDICES[creature.state]
@@ -157,9 +204,7 @@ const renderCreature = (creature) => {
'background-position',
`${currentSpriteFrameOffset[0] * CREATURE_WIDTH}px ${currentSpriteFrameOffset[1] * CREATURE_HEIGHT}px`
)
-
- const nextCreatureFrame = getNextCreatureFrame(creature);
- setTimeout(renderCreature, FRAME_DELAY, nextCreatureFrame)
+ return creature;
}
/**
@@ -171,6 +216,7 @@ const renderCreature = (creature) => {
* @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)
+ * @return Creature
*/
const createCreature = (
container,
@@ -180,38 +226,91 @@ const createCreature = (
initialPositionX = 0,
initialPositionY = 0
) => {
- const creatureEl = document.createElement('div');
+ const creatureEle = 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);
+ creatureEle.setAttribute('id', name);
+ creatureEle.style.setProperty('width', `${CREATURE_WIDTH}px`);
+ creatureEle.style.setProperty('height', `${CREATURE_HEIGHT}px`);
+ creatureEle.style.setProperty('position', 'fixed');
+ creatureEle.style.setProperty('image-rendering', 'pixelated');
+ creatureEle.style.setProperty('background-image', spriteSheetUrl);
+ creatureEle.style.setProperty('display', 'hidden');
- container.appendChild(creatureEl);
+ container.appendChild(creatureEle);
- renderCreature({
+ return {
name,
spriteSheet,
state: initialState,
stateDuration: 0,
- positionX: Math.max(0, initialPositionX),
- positionY: Math.max(0, initialPositionY),
- element: creatureEl,
+ positionX: constrain(initialPositionX, 0, container.clientWidth),
+ positionY: constrain(initialPositionY, 0, container.clientHeight),
+ element: creatureEle,
container
- });
+ }
}
-window.onload = () => {
- const kennelWindowEle = document.querySelector('#kennel-window');
- createCreature(
- kennelWindowEle,
- 'test-creature',
- undefined,
- CreatureState.SCRATCH_SELF
- )
+/**
+ * Start creature animation
+ * @param {Creature} creature
+ * @param {(creature: Creature) => Creature} [updateCreature] if undefined, uses identity
+ * (via {@link renderAndUpdateCreature})
+ */
+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)));
+}
+
+// === ===
+
+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))
}
diff --git a/static/style.css b/static/style.css
index f77e958..73c2486 100644
--- a/static/style.css
+++ b/static/style.css
@@ -3,5 +3,5 @@
min-width: 8rem;
height: 80vw;
min-height: 8rem;
- margin: auto inherit;
+ margin: auto;
}
\ No newline at end of file