@meganetaaan/mouse-follower
Version:
TypeScript library for creating animated sprites that smoothly follow mouse cursor or other targets using physics-based movement
108 lines (107 loc) • 4.29 kB
JavaScript
function calculateDistance(from, to) {
const dx = to.x - from.x;
const dy = to.y - from.y;
return Math.sqrt(dx * dx + dy * dy);
}
export function calculateAcceleration(position, target, maxAccel) {
const dx = target.x - position.x;
const dy = target.y - position.y;
const distance = calculateDistance(position, target);
if (distance === 0) {
return { x: 0, y: 0 };
}
const directionX = dx / distance;
const directionY = dy / distance;
return {
x: directionX * maxAccel,
y: directionY * maxAccel,
};
}
function calculateBrakingForce(distance, stopWithin, brakingStartDistance, brakingStrength) {
// Phase 1: No braking when far from target
if (distance >= brakingStartDistance) {
return 0; // Full speed ahead!
}
// Phase 2: Start braking when within brakingStartDistance
if (distance > stopWithin) {
// Gradual braking: increases from 0 to brakingStrength as we approach stopWithin
const brakingRange = brakingStartDistance - stopWithin;
const distanceInBrakingZone = distance - stopWithin;
const brakingRatio = 1 - distanceInBrakingZone / brakingRange;
return brakingStrength * brakingRatio;
}
// Phase 3: Maximum braking when very close to target
return brakingStrength;
}
export function updatePhysics(state, config, deltaTime) {
const distance = calculateDistance(state.position, state.target);
const currentSpeedSquared = state.velocity.x * state.velocity.x + state.velocity.y * state.velocity.y;
const minStopVelocitySquared = config.minStopVelocity * config.minStopVelocity;
// Check for complete stop condition using squared values (avoid sqrt)
if (distance <= config.stopWithin &&
currentSpeedSquared <= minStopVelocitySquared) {
return {
position: state.position,
velocity: { x: 0, y: 0 },
target: state.target,
};
}
// Always calculate acceleration toward target (driving force)
const accel = calculateAcceleration(state.position, state.target, config.maxAccel);
// Calculate braking force based on distance to target
const brakingForce = calculateBrakingForce(distance, config.stopWithin, config.brakingStartDistance, config.brakingStrength);
// Apply driving force and braking force
// Braking force opposes current velocity (like friction)
let newVelocityX = state.velocity.x +
accel.x * deltaTime -
state.velocity.x * brakingForce * deltaTime;
let newVelocityY = state.velocity.y +
accel.y * deltaTime -
state.velocity.y * brakingForce * deltaTime;
// Apply velocity limit using squared comparison when possible
const newSpeedSquared = newVelocityX * newVelocityX + newVelocityY * newVelocityY;
const maxVelocitySquared = config.maxVelocity * config.maxVelocity;
if (newSpeedSquared > maxVelocitySquared) {
// Only calculate sqrt when velocity limiting is needed
const newSpeed = Math.sqrt(newSpeedSquared);
const scale = config.maxVelocity / newSpeed;
newVelocityX *= scale;
newVelocityY *= scale;
}
const newPositionX = state.position.x + newVelocityX * deltaTime;
const newPositionY = state.position.y + newVelocityY * deltaTime;
return {
position: { x: newPositionX, y: newPositionY },
velocity: { x: newVelocityX, y: newVelocityY },
target: state.target,
};
}
export class Physics {
state;
config;
constructor(config, initialPosition) {
this.config = config;
this.state = {
position: { ...initialPosition },
velocity: { x: 0, y: 0 },
target: { ...initialPosition },
};
}
update(deltaTime) {
this.state = updatePhysics(this.state, this.config, deltaTime);
}
setTarget(target) {
this.state.target = { ...target };
}
getPosition() {
return { ...this.state.position };
}
getVelocity() {
return { ...this.state.velocity };
}
isMoving(threshold = 10.0) {
const speed = Math.sqrt(this.state.velocity.x * this.state.velocity.x +
this.state.velocity.y * this.state.velocity.y);
return speed > threshold;
}
}