UNPKG

@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
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; } }