@meganetaaan/mouse-follower
Version:
TypeScript library for creating animated sprites that smoothly follow mouse cursor or other targets using physics-based movement
174 lines (173 loc) • 6.26 kB
JavaScript
import { Physics } from "./follower/physics.js";
import { Sprite } from "./follower/sprite.js";
import { DEFAULT_ANIMATIONS, DEFAULTS, MouseTarget, OffsetTarget, } from "./follower/types.js";
class FollowerImpl {
options;
physics;
sprite;
animationId;
lastTime = 0;
isRunning = false;
// Event system
eventTarget = new EventTarget();
wasMoving = false;
get x() {
return this.physics.getPosition().x;
}
set x(value) {
const currentPos = this.physics.getPosition();
this.physics.setTarget({ x: value, y: currentPos.y });
}
get y() {
return this.physics.getPosition().y;
}
set y(value) {
const currentPos = this.physics.getPosition();
this.physics.setTarget({ x: currentPos.x, y: value });
}
constructor(options = {}) {
// Process options
const followTarget = options.target || mouseTarget();
const bindTo = options.bindTo || document.body;
const physicsConfig = {
maxVelocity: options.physics?.velocity ?? DEFAULTS.physics.velocity,
maxAccel: options.physics?.accel ?? DEFAULTS.physics.accel,
stopWithin: options.physics?.braking?.stopDistance ??
DEFAULTS.physics.braking.stopDistance,
brakingStartDistance: options.physics?.braking?.distance ?? DEFAULTS.physics.braking.distance,
brakingStrength: options.physics?.braking?.strength ?? DEFAULTS.physics.braking.strength,
minStopVelocity: options.physics?.braking?.minVelocity ??
DEFAULTS.physics.braking.minVelocity,
};
const spriteConfig = {
spriteUrl: options.sprite?.url ?? DEFAULTS.sprite.url ?? "/ugo-mini.png",
spriteFrames: options.sprite?.frames ?? DEFAULTS.sprite.frames ?? 2,
spriteWidth: options.sprite?.width ?? DEFAULTS.sprite.width ?? 32,
spriteHeight: options.sprite?.height ?? DEFAULTS.sprite.height ?? 64,
transparentColor: options.sprite?.transparentColor ??
DEFAULTS.sprite.transparentColor ??
"rgb(0, 255, 0)",
animationInterval: options.sprite?.animation?.interval ??
DEFAULTS.sprite.animation?.interval ??
125,
animations: options.sprite?.animations ??
DEFAULTS.sprite.animations ??
DEFAULT_ANIMATIONS,
};
this.options = {
followTarget,
bindTo,
physicsConfig,
spriteConfig,
};
// Initialize position to current target position
const initialPosition = { x: followTarget.x, y: followTarget.y };
// Create Physics and Sprite instances
this.physics = new Physics(physicsConfig, initialPosition);
this.sprite = new Sprite(spriteConfig, bindTo);
}
async start() {
if (this.isRunning)
return;
try {
// Initialize the sprite
await this.sprite.initialize();
this.lastTime = performance.now();
this.isRunning = true;
// Start walking animation
this.sprite.playAnimation("walk");
this.animate();
}
catch (error) {
console.error("Failed to initialize sprite:", error);
throw error;
}
}
stop() {
this.isRunning = false;
if (this.animationId) {
cancelAnimationFrame(this.animationId);
this.animationId = undefined;
}
this.sprite.pauseAnimation();
}
setTarget(target) {
this.options.followTarget = target;
}
destroy() {
this.stop();
this.sprite.destroy();
}
playAnimation(name) {
this.sprite.playAnimation(name);
}
pauseAnimation() {
this.sprite.pauseAnimation();
}
addEventListener(type, listener) {
this.eventTarget.addEventListener(type, listener);
}
removeEventListener(type, listener) {
this.eventTarget.removeEventListener(type, listener);
}
animate = () => {
if (!this.isRunning)
return;
const currentTime = performance.now();
const deltaTime = (currentTime - this.lastTime) / 1000;
this.lastTime = currentTime;
// Update physics
const target = this.getTargetPosition();
this.physics.setTarget(target);
this.physics.update(deltaTime);
// Get current position and velocity
const position = this.physics.getPosition();
const velocity = this.physics.getVelocity();
// Determine sprite direction based on velocity
let direction = "right";
if (Math.abs(velocity.x) > 0.1) {
direction = velocity.x < 0 ? "left" : "right";
}
// Render sprite at current position
this.sprite.render(position, direction);
// Detect movement state changes and emit events
const isMoving = this.physics.isMoving(10.0);
if (isMoving !== this.wasMoving) {
if (isMoving) {
this.eventTarget.dispatchEvent(new CustomEvent("start", {
detail: { follower: this },
}));
}
else {
this.eventTarget.dispatchEvent(new CustomEvent("stop", {
detail: { follower: this },
}));
}
this.wasMoving = isMoving;
}
this.animationId = requestAnimationFrame(this.animate);
};
getTargetPosition() {
const target = this.options.followTarget;
return {
x: target.x,
y: target.y,
};
}
}
// Singleton mouse target instance
let mouseTargetInstance = null;
export function follower(options) {
return new FollowerImpl(options);
}
export function mouseTarget() {
if (!mouseTargetInstance) {
mouseTargetInstance = new MouseTarget();
}
return mouseTargetInstance;
}
export function offsetTarget(target, offsetX = 0, offsetY = 0) {
return new OffsetTarget(target, offsetX, offsetY);
}
// Export all necessary types and presets
export { SPRITE_PRESET_STACK_CHAN, stackChanPreset, } from "./follower/types.js";