UNPKG

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