feat: implement blue noise generation for initial creature positions
This commit is contained in:
parent
7e98ec63ac
commit
c37d8d13c7
161
static/index.js
161
static/index.js
|
@ -6,6 +6,7 @@ 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
|
||||
const WALK_SIZE = 8; // magnitude of position change, in px
|
||||
const MAX_RETRIES = 10;
|
||||
|
||||
/**
|
||||
* Enum of directions
|
||||
|
@ -274,6 +275,120 @@ const isBetween = (val, min, max) => {
|
|||
return val >= min && val < max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an array of n vec2's such that all vectors lie in
|
||||
* [left, left + width) x [top, top + height) and are at least radius
|
||||
* distance away from all other points.
|
||||
* This function uses almost (but not quite)
|
||||
* [the poisson disk sampling method]{@link https://a5huynh.github.io/posts/2019/poisson-disk-sampling/}
|
||||
* insofar that instead of generating points in an annulus around a seed,
|
||||
* it generates points anywhere else in the bounds.
|
||||
* @param {number} n the number of points to generate
|
||||
* @param {number} radius the minimum distance between points
|
||||
* @param {number} left x lower bound of points
|
||||
* @param {number} top y lower bound of points
|
||||
* @param {number} width x width of bounds
|
||||
* @param {number} height y width of bounds
|
||||
* @param {number=} maxRetries maximum number of generation attemps before giving up
|
||||
* @return {Array<vec2>}
|
||||
*/
|
||||
const generateBlueNoise = (n, radius, left, top, width, height, maxRetries = MAX_RETRIES) => {
|
||||
const gridCellSize = radius / Math.sqrt(2);
|
||||
const gridWidth = Math.ceil(width / gridCellSize);
|
||||
const gridHeight = Math.ceil(height / gridCellSize);
|
||||
const grid = /** @type {Array<Array<vec2|undefined>>} */ [...Array(gridHeight)].map(() => Array(gridWidth));
|
||||
|
||||
/**
|
||||
* @param {vec2} point
|
||||
* @return {vec2}
|
||||
*/
|
||||
const getSamplePoint = (point) => {
|
||||
const randomX = Math.random() * (width - 2 * radius);
|
||||
const randomY = Math.random() * (height - 2 * radius);
|
||||
|
||||
const samplePointX = left + (
|
||||
randomX < (point.at(0) - radius) ? randomX : randomX + 2 * radius
|
||||
)
|
||||
const samplePointY = top + (
|
||||
randomY < (point.at(1) - radius) ? randomY : randomY + 2 * radius
|
||||
)
|
||||
|
||||
return [samplePointX, samplePointY];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {vec2} point
|
||||
* @return {void}
|
||||
*/
|
||||
const insertPointIntoGrid = (point) => {
|
||||
const indexX = Math.floor((point.at(0) - left) / gridCellSize);
|
||||
const indexY = Math.floor((point.at(1) - top) / gridCellSize);
|
||||
|
||||
grid[indexY][indexX] = point;
|
||||
}
|
||||
|
||||
const areValid = (point1, point2) => {
|
||||
const deltaX = point1.at(0) - point2.at(0);
|
||||
const deltaY = point1.at(1) - point2.at(1);
|
||||
return (deltaX * deltaX + deltaY * deltaY) > (radius * radius)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {vec2} point
|
||||
* @return {boolean}
|
||||
*/
|
||||
const isValidPoint = (point) => {
|
||||
const indexX = Math.floor((point.at(0) - left) / gridCellSize);
|
||||
const indexY = Math.floor((point.at(1) - top) / gridCellSize);
|
||||
|
||||
const testPoints = [
|
||||
[indexX - 1, indexY - 1], [indexX, indexY - 1], [indexX + 1, indexY - 1],
|
||||
[indexX - 1, indexY], [indexX, indexY], [indexX + 1, indexY],
|
||||
[indexX - 1, indexY + 1], [indexX, indexY + 1], [indexX + 1, indexY + 1],
|
||||
]
|
||||
.filter((indices) => (
|
||||
indices.at(0) >= 0 && indices.at(1) >= 0 && indices.at(0) < gridWidth && indices.at(1) < gridHeight
|
||||
))
|
||||
.map((indices) => grid[indices.at(1)][indices.at(0)])
|
||||
.filter((point) => point !== undefined);
|
||||
|
||||
return testPoints.every((testPoint) => areValid(point, testPoint))
|
||||
}
|
||||
|
||||
// initialize
|
||||
const initialPoint = [
|
||||
left + width / 2, top + height / 2,
|
||||
]
|
||||
insertPointIntoGrid(initialPoint);
|
||||
const blueNoiseSamples = [initialPoint];
|
||||
const activeList = [initialPoint];
|
||||
|
||||
const findAndInsertPoint = () => {
|
||||
if (activeList.length === 0) return;
|
||||
|
||||
const activePoint = /** @type {vec2} */ getRandomChoice(undefined, activeList);
|
||||
const samplePoints = [...Array(MAX_RETRIES)].map(_ => getSamplePoint(activePoint));
|
||||
const samplePoint = samplePoints.find(isValidPoint);
|
||||
|
||||
if (samplePoint === undefined) {
|
||||
// remove active point from active list
|
||||
const indexOfActivePoint = activeList.indexOf(activePoint);
|
||||
activeList.splice(indexOfActivePoint, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
activeList.push(samplePoint);
|
||||
blueNoiseSamples.push(samplePoint);
|
||||
insertPointIntoGrid(samplePoint);
|
||||
}
|
||||
|
||||
while (activeList.length > 0 && blueNoiseSamples.length < n) {
|
||||
findAndInsertPoint();
|
||||
}
|
||||
|
||||
return blueNoiseSamples.length < n ? [] : blueNoiseSamples;
|
||||
}
|
||||
|
||||
// === </MATH_UTILS> ===
|
||||
|
||||
// === <CREATURE_UTILS> ===
|
||||
|
@ -416,7 +531,6 @@ const createCreature = (
|
|||
* Start creature animation
|
||||
* @param {Creature} creature
|
||||
* @param {(creature: Creature) => Creature} [updateCreature] if undefined, uses identity
|
||||
* (via {@link renderAndUpdateCreature})
|
||||
*/
|
||||
const beginCreatureAnimation = (
|
||||
creature,
|
||||
|
@ -441,8 +555,8 @@ const beginCreatureAnimation = (
|
|||
*/
|
||||
const isCreaturesColliding = (creature1, creature2) => {
|
||||
return isIntersectingCircles(
|
||||
creature1.positionX, creature1.positionY, CREATURE_WIDTH * 1/2,
|
||||
creature2.positionX, creature2.positionY, CREATURE_WIDTH * 1/2,
|
||||
creature1.positionX, creature1.positionY, CREATURE_WIDTH / 2,
|
||||
creature2.positionX, creature2.positionY, CREATURE_WIDTH / 2,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -771,35 +885,34 @@ const resolveCollision = (updatedCreature, allCreatures, creature) => {
|
|||
}
|
||||
|
||||
window.onload = () => {
|
||||
if (
|
||||
window.matchMedia(`(prefers-reduced-motion: reduce)`).matches
|
||||
|| window.matchMedia(`(prefers-reduced-motion: reduce)`).matches
|
||||
) {
|
||||
// user preference for low motion. do not render creatures
|
||||
// user preference for low motion. do not render creatures
|
||||
if (window.matchMedia(`(prefers-reduced-motion: reduce)`).matches) {
|
||||
return;
|
||||
}
|
||||
|
||||
const TEST_CREATURE_COUNT = 32;
|
||||
const kennelWindowEle = document.querySelector('#kennel-window');
|
||||
const creatures = Array(32).fill('test-creature')
|
||||
.map((name, index) => createCreature(
|
||||
kennelWindowEle,
|
||||
name + index.toString(),
|
||||
undefined, // default sprite sheet
|
||||
getRandomChoice(undefined, [
|
||||
|
||||
const startingPoints = generateBlueNoise(
|
||||
TEST_CREATURE_COUNT, CREATURE_WIDTH,
|
||||
kennelWindowEle.clientLeft, kennelWindowEle.clientTop,
|
||||
kennelWindowEle.clientWidth, kennelWindowEle.clientHeight
|
||||
)
|
||||
|
||||
const creatures = /** @type {Array<Creature>} */ startingPoints
|
||||
.map((startingPoint, index) => createCreature(
|
||||
kennelWindowEle,
|
||||
`test-creature-${index}`,
|
||||
undefined, // use a default sprite sheet
|
||||
CreatureState.IDLE,
|
||||
CreatureState.TIRED,
|
||||
CreatureState.SCRATCH_SELF,
|
||||
]),
|
||||
getRandomInRange(kennelWindowEle.clientLeft, kennelWindowEle.clientLeft + kennelWindowEle.clientWidth),
|
||||
getRandomInRange(kennelWindowEle.clientTop, kennelWindowEle.clientTop + kennelWindowEle.clientHeight),
|
||||
startingPoint.at(0),
|
||||
startingPoint.at(1),
|
||||
))
|
||||
|
||||
const updateCreature = updateCreatureWithContext((creature, allCreatures) => {
|
||||
return resolveCollision(
|
||||
CREATURE_STATE_TO_UPDATE_CREATURE_FUNCTION[creature.state](creature, allCreatures),
|
||||
allCreatures,
|
||||
creature,
|
||||
);
|
||||
const updateCreatureFunction = CREATURE_STATE_TO_UPDATE_CREATURE_FUNCTION[creature.state];
|
||||
const updatedCreature = updateCreatureFunction(creature, allCreatures);
|
||||
return resolveCollision(updatedCreature, allCreatures, creature);
|
||||
}, creatures);
|
||||
|
||||
creatures.forEach((creature) => beginCreatureAnimation(creature, updateCreature))
|
||||
|
|
Loading…
Reference in New Issue