UNPKG

@kya-os/cli

Version:

CLI for MCP-I setup and management

353 lines 11.2 kB
/** * Motion Path Engine for Terminal Effects * Provides sophisticated character movement with paths, waypoints, and bezier curves */ /** * Motion path for character movement */ export class Path { constructor(id, speed = 1, easingFn) { this.waypoints = []; this.segments = []; this._isLooping = false; this._speed = 1; /** Current position along the path (0-1) */ this.progress = 0; /** Current segment index */ this.currentSegmentIndex = 0; /** Time spent at current waypoint (for holds) */ this.holdTimeElapsed = 0; /** Total path distance */ this.totalDistance = 0; this.id = id; this._speed = speed; this._easingFn = easingFn; } /** * Add a waypoint to the path */ newWaypoint(coord, waypointId, bezierControl, holdTime) { const waypoint = { id: waypointId || `wp_${this.waypoints.length}`, coordinate: coord, bezierControl, holdTime, }; this.waypoints.push(waypoint); this.recalculateSegments(); return waypoint; } /** * Recalculate segments when waypoints change */ recalculateSegments() { this.segments = []; this.totalDistance = 0; for (let i = 0; i < this.waypoints.length - 1; i++) { const start = this.waypoints[i]; const end = this.waypoints[i + 1]; let distance; let curvePoints; if (end.bezierControl) { // Calculate bezier curve curvePoints = this.calculateBezierPoints(start.coordinate, end.bezierControl, end.coordinate, 20 // Resolution ); distance = this.calculatePathDistance(curvePoints); } else { // Simple linear distance distance = this.calculateDistance(start.coordinate, end.coordinate); } const segment = { start, end, distance, curvePoints, }; this.segments.push(segment); this.totalDistance += distance; } } /** * Calculate bezier curve points */ calculateBezierPoints(start, control, end, resolution) { const points = []; for (let i = 0; i <= resolution; i++) { const t = i / resolution; const x = Math.round((1 - t) * (1 - t) * start.x + 2 * (1 - t) * t * control.x + t * t * end.x); const y = Math.round((1 - t) * (1 - t) * start.y + 2 * (1 - t) * t * control.y + t * t * end.y); points.push({ x, y }); } return points; } /** * Calculate distance between two coordinates */ calculateDistance(a, b) { return Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2)); } /** * Calculate total distance along a path of points */ calculatePathDistance(points) { let distance = 0; for (let i = 0; i < points.length - 1; i++) { distance += this.calculateDistance(points[i], points[i + 1]); } return distance; } /** * Set looping behavior */ setLooping(isLooping) { this._isLooping = isLooping; } /** * Set speed */ setSpeed(speed) { this._speed = speed; } /** * Get position at current progress */ getCurrentPosition() { if (this.waypoints.length === 0) { return { x: 0, y: 0 }; } if (this.waypoints.length === 1) { return { ...this.waypoints[0].coordinate }; } if (this.currentSegmentIndex >= this.segments.length) { return { ...this.waypoints[this.waypoints.length - 1].coordinate }; } const segment = this.segments[this.currentSegmentIndex]; const segmentProgress = this.getSegmentProgress(); if (segment.curvePoints) { // Bezier curve interpolation const pointIndex = Math.floor(segmentProgress * (segment.curvePoints.length - 1)); const localProgress = (segmentProgress * (segment.curvePoints.length - 1)) % 1; if (pointIndex >= segment.curvePoints.length - 1) { return { ...segment.curvePoints[segment.curvePoints.length - 1] }; } const p1 = segment.curvePoints[pointIndex]; const p2 = segment.curvePoints[pointIndex + 1]; return { x: Math.round(p1.x + (p2.x - p1.x) * localProgress), y: Math.round(p1.y + (p2.y - p1.y) * localProgress), }; } else { // Linear interpolation const p1 = segment.start.coordinate; const p2 = segment.end.coordinate; return { x: Math.round(p1.x + (p2.x - p1.x) * segmentProgress), y: Math.round(p1.y + (p2.y - p1.y) * segmentProgress), }; } } /** * Get progress within current segment (0-1) */ getSegmentProgress() { if (this.segments.length === 0) return 0; let distanceCovered = this.progress * this.totalDistance; // Find which segment we're in for (let i = 0; i < this.segments.length; i++) { if (distanceCovered <= this.segments[i].distance) { return distanceCovered / this.segments[i].distance; } distanceCovered -= this.segments[i].distance; } return 1; } /** * Update motion progress */ update(deltaTime) { if (this.waypoints.length < 2) return; // Handle waypoint holds const currentWaypoint = this.waypoints[Math.min(Math.floor(this.progress * this.waypoints.length), this.waypoints.length - 1)]; if (currentWaypoint.holdTime && this.holdTimeElapsed < currentWaypoint.holdTime) { this.holdTimeElapsed += deltaTime; return; } // Reset hold time when moving to next waypoint if (this.holdTimeElapsed > 0) { this.holdTimeElapsed = 0; } // Calculate progress increment const distancePerMs = (this._speed * 50) / 1000; // 50 units per second at speed 1 const progressIncrement = distancePerMs * deltaTime / this.totalDistance; this.progress += progressIncrement; // Apply easing if configured if (this._easingFn) { this.progress = this._easingFn(Math.min(this.progress, 1)); } // Handle completion and looping if (this.progress >= 1) { if (this._isLooping) { this.progress = this.progress % 1; this.currentSegmentIndex = 0; } else { this.progress = 1; this.currentSegmentIndex = this.segments.length - 1; } } else { // Update current segment index let distanceCovered = this.progress * this.totalDistance; for (let i = 0; i < this.segments.length; i++) { if (distanceCovered <= this.segments[i].distance) { this.currentSegmentIndex = i; break; } distanceCovered -= this.segments[i].distance; } } } /** * Reset the path */ reset() { this.progress = 0; this.currentSegmentIndex = 0; this.holdTimeElapsed = 0; } /** * Check if path is complete */ isComplete() { return !this._isLooping && this.progress >= 1; } /** * Get waypoint count */ getWaypointCount() { return this.waypoints.length; } /** * Get total path distance */ getTotalDistance() { return this.totalDistance; } } /** * Motion event types */ export var MotionEvent; (function (MotionEvent) { MotionEvent["PATH_COMPLETE"] = "path_complete"; MotionEvent["PATH_ACTIVATED"] = "path_activated"; MotionEvent["WAYPOINT_REACHED"] = "waypoint_reached"; MotionEvent["SEGMENT_ENTERED"] = "segment_entered"; })(MotionEvent || (MotionEvent = {})); /** * Character motion manager */ export class CharacterMotion { // private _lastWaypointId: string = ""; // Reserved for future use constructor(initialPosition) { this.paths = new Map(); this.activePath = null; this.lastUpdateTime = 0; this.eventHandlers = []; this.currentPosition = { ...initialPosition }; } /** * Create a new path */ newPath(pathId, speed = 1, easingFn) { const path = new Path(pathId, speed, easingFn); this.paths.set(pathId, path); return path; } /** * Get a path by ID */ queryPath(pathId) { return this.paths.get(pathId); } /** * Activate a path */ activatePath(path) { if (this.activePath && this.activePath !== path) { this.triggerEvent(MotionEvent.PATH_COMPLETE, this.activePath.id); } this.activePath = path; this.lastUpdateTime = Date.now(); path.reset(); this.triggerEvent(MotionEvent.PATH_ACTIVATED, path.id); } /** * Set position directly */ setCoordinate(coord) { this.currentPosition = { ...coord }; } /** * Update motion and get current position */ update() { if (!this.activePath) { return this.currentPosition; } const currentTime = Date.now(); const deltaTime = currentTime - this.lastUpdateTime; this.lastUpdateTime = currentTime; // Update path progress this.activePath.update(deltaTime); // Get new position const newPosition = this.activePath.getCurrentPosition(); this.currentPosition = newPosition; // Check for path completion if (this.activePath.isComplete()) { this.triggerEvent(MotionEvent.PATH_COMPLETE, this.activePath.id); } return this.currentPosition; } /** * Register an event handler */ registerEvent(event, pathId, callback, waypointId) { this.eventHandlers.push({ event, pathId, waypointId, callback }); } /** * Trigger an event */ triggerEvent(event, pathId, waypointId) { for (const handler of this.eventHandlers) { if (handler.event === event && handler.pathId === pathId && (!waypointId || handler.waypointId === waypointId)) { handler.callback(); } } } /** * Reset all paths */ reset() { this.paths.forEach(path => path.reset()); this.activePath = null; this.lastUpdateTime = 0; } /** * Get current position */ getCurrentPosition() { return { ...this.currentPosition }; } } //# sourceMappingURL=motion-engine.js.map