@kya-os/cli
Version:
CLI for KYA-OS MCP-I setup and management
353 lines • 11.2 kB
JavaScript
/**
* 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