Compare commits
No commits in common. "b005f4b83a786d50a694bd7303925e641c2933bf" and "514b0338844334aa4aaf332c6e29efb6a1aadaa6" have entirely different histories.
b005f4b83a
...
514b033884
|
@ -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 = ["kennel", "tests"]
|
src = ["fastapi_poetry_starter", "tests"]
|
||||||
|
|
||||||
ignore = []
|
ignore = []
|
||||||
|
|
||||||
|
|
315
static/index.js
315
static/index.js
|
@ -1,316 +1,5 @@
|
||||||
// === <CONSTANTS> ===
|
|
||||||
|
|
||||||
const CREATURE_HEIGHT = 32; // in px
|
|
||||||
const CREATURE_WIDTH = 32; // in px
|
|
||||||
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
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.<number, Array<SpriteFrameOffset>>}
|
|
||||||
*/
|
|
||||||
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],
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
// === </CONSTANTS> ===
|
|
||||||
|
|
||||||
// === <TYPES> ===
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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)
|
|
||||||
*/
|
|
||||||
|
|
||||||
// === <TYPES> ===
|
|
||||||
|
|
||||||
// === <MATH_UTILS> ===
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// === </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
|
|
||||||
* @param {Creature} creature
|
|
||||||
* @return {Creature} the creature being rendered
|
|
||||||
*/
|
|
||||||
const renderCreature = (creature) => {
|
|
||||||
creature.element.style.setProperty('display', 'block');
|
|
||||||
|
|
||||||
// set position
|
|
||||||
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]
|
|
||||||
const currentSpriteFrameOffset = spriteFrames?.[creature.stateDuration % spriteFrames.length]
|
|
||||||
creature.element.style.setProperty(
|
|
||||||
'background-position',
|
|
||||||
`${currentSpriteFrameOffset[0] * CREATURE_WIDTH}px ${currentSpriteFrameOffset[1] * CREATURE_HEIGHT}px`
|
|
||||||
)
|
|
||||||
return creature;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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)
|
|
||||||
* @return Creature
|
|
||||||
*/
|
|
||||||
const createCreature = (
|
|
||||||
container,
|
|
||||||
name,
|
|
||||||
spriteSheet,
|
|
||||||
initialState = CreatureState.IDLE,
|
|
||||||
initialPositionX = 0,
|
|
||||||
initialPositionY = 0
|
|
||||||
) => {
|
|
||||||
const creatureEle = document.createElement('div');
|
|
||||||
const spriteSheetUrl = spriteSheet
|
|
||||||
? `url('/static/sprites/${spriteSheet}')`
|
|
||||||
: `url('/static/sprites/defaults/${randomInt(1, DEFAULT_SPRITE_SHEET_COUNT)}.gif')`;
|
|
||||||
|
|
||||||
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(creatureEle);
|
|
||||||
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
spriteSheet,
|
|
||||||
state: initialState,
|
|
||||||
stateDuration: 0,
|
|
||||||
positionX: constrain(initialPositionX, 0, container.clientWidth),
|
|
||||||
positionY: constrain(initialPositionY, 0, container.clientHeight),
|
|
||||||
element: creatureEle,
|
|
||||||
container
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// === </CREATURE_UTILS> ===
|
|
||||||
|
|
||||||
window.onload = () => {
|
window.onload = () => {
|
||||||
// EXAMPLE SCRIPT
|
console.log('from js');
|
||||||
const kennelWindowEle = document.querySelector('#kennel-window');
|
const kennelWindowEle = document.querySelector('#kennel-window');
|
||||||
const creatures = ['test-creature-1', 'test-creature-2', 'test-creature-3']
|
kennelWindowEle.innerHTML = 'rendered from static/index.js';
|
||||||
.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))
|
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 6.4 KiB |
|
@ -1,7 +0,0 @@
|
||||||
#kennel-window {
|
|
||||||
width: 80vw;
|
|
||||||
min-width: 8rem;
|
|
||||||
height: 80vw;
|
|
||||||
min-height: 8rem;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
|
@ -4,7 +4,6 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', path='style.css') }}">
|
|
||||||
<title>Kennel Club</title>
|
<title>Kennel Club</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
Loading…
Reference in New Issue