303 lines
8.8 KiB
TypeScript
303 lines
8.8 KiB
TypeScript
/**
|
||
* Утилиты для анимации
|
||
* Основано на логике анимации из HTML файла (a.html)
|
||
*/
|
||
|
||
/**
|
||
* Линейная интерполяция между двумя значениями
|
||
* @param from - начальное значение
|
||
* @param to - конечное значение
|
||
* @param t - коэффициент интерполяции (0-1)
|
||
* @returns интерполированное значение
|
||
*/
|
||
export const lerp = (from: number, to: number, t: number): number => {
|
||
return from + (to - from) * t;
|
||
};
|
||
|
||
/**
|
||
* Интерполяция угла с учетом кратчайшего пути
|
||
* @param from - начальный угол в радианах
|
||
* @param to - конечный угол в радианах
|
||
* @param t - коэффициент интерполяции (0-1)
|
||
* @returns интерполированный угол в радианах
|
||
*/
|
||
export const lerpAngle = (from: number, to: number, t: number): number => {
|
||
// Нормализуем углы к диапазону 0-2π
|
||
from = ((from % (Math.PI * 2)) + Math.PI * 2) % (Math.PI * 2);
|
||
to = ((to % (Math.PI * 2)) + Math.PI * 2) % (Math.PI * 2);
|
||
|
||
// Найдем кратчайший путь между углами
|
||
let diff = to - from;
|
||
if (diff > Math.PI) {
|
||
diff -= Math.PI * 2;
|
||
} else if (diff < -Math.PI) {
|
||
diff += Math.PI * 2;
|
||
}
|
||
|
||
return from + diff * t;
|
||
};
|
||
|
||
/**
|
||
* Класс для анимации позиции
|
||
* Основано на логике анимации из HTML файла (a.html)
|
||
*/
|
||
export class PositionAnimator {
|
||
private currentX: number;
|
||
private currentY: number;
|
||
private targetX: number;
|
||
private targetY: number;
|
||
private isAnimating: boolean = false;
|
||
private animationId: number | null = null;
|
||
private onUpdate: (position: { x: number; y: number }) => void;
|
||
private onComplete: () => void;
|
||
private duration: number;
|
||
private startTime: number | null = null;
|
||
|
||
constructor(
|
||
onUpdate: (position: { x: number; y: number }) => void,
|
||
onComplete: () => void = () => {},
|
||
duration: number = 500
|
||
) {
|
||
this.currentX = 0;
|
||
this.currentY = 0;
|
||
this.targetX = 0;
|
||
this.targetY = 0;
|
||
this.onUpdate = onUpdate;
|
||
this.onComplete = onComplete;
|
||
this.duration = duration;
|
||
}
|
||
|
||
/**
|
||
* Анимировать к новой позиции
|
||
*/
|
||
animateTo(x: number, y: number, duration?: number): void {
|
||
this.targetX = x;
|
||
this.targetY = y;
|
||
this.duration = duration || this.duration;
|
||
this.startTime = null;
|
||
this.isAnimating = true;
|
||
|
||
if (this.animationId) {
|
||
cancelAnimationFrame(this.animationId);
|
||
}
|
||
|
||
this.animationId = requestAnimationFrame(this.animate);
|
||
}
|
||
|
||
/**
|
||
* Установить позицию мгновенно
|
||
*/
|
||
setPosition(x: number, y: number): void {
|
||
this.currentX = x;
|
||
this.currentY = y;
|
||
this.targetX = x;
|
||
this.targetY = y;
|
||
this.isAnimating = false;
|
||
this.onUpdate({ x, y });
|
||
}
|
||
|
||
/**
|
||
* Остановить анимацию
|
||
*/
|
||
stop(): void {
|
||
this.isAnimating = false;
|
||
if (this.animationId) {
|
||
cancelAnimationFrame(this.animationId);
|
||
this.animationId = null;
|
||
}
|
||
}
|
||
|
||
private animate = (timestamp: number): void => {
|
||
if (!this.isAnimating) return;
|
||
|
||
if (!this.startTime) {
|
||
this.startTime = timestamp;
|
||
}
|
||
|
||
const elapsed = timestamp - this.startTime;
|
||
const progress = Math.min(elapsed / this.duration, 1);
|
||
|
||
// Используем easeOutCubic для плавной анимации
|
||
const easedProgress = 1 - Math.pow(1 - progress, 3);
|
||
|
||
this.currentX = lerp(this.currentX, this.targetX, easedProgress);
|
||
this.currentY = lerp(this.currentY, this.targetY, easedProgress);
|
||
|
||
this.onUpdate({ x: this.currentX, y: this.currentY });
|
||
|
||
if (progress < 1) {
|
||
this.animationId = requestAnimationFrame(this.animate);
|
||
} else {
|
||
this.isAnimating = false;
|
||
this.onComplete();
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Передискретизация пути для обеспечения равномерного расстояния между точками
|
||
* @param path - массив [lat, lon] или [x, y]
|
||
* @param segmentLength - желаемое расстояние между точками (в единицах координат)
|
||
* @returns новый массив точек
|
||
*/
|
||
export const resamplePath = <T extends number[]>(path: T[], segmentLength: number): T[] => {
|
||
if (path.length < 2) return path;
|
||
|
||
const newPath: T[] = [path[0]];
|
||
let leftover = 0;
|
||
|
||
for (let i = 0; i < path.length - 1; i++) {
|
||
const p1 = path[i];
|
||
const p2 = path[i + 1];
|
||
const dx = p2[0] - p1[0];
|
||
const dy = p2[1] - p1[1];
|
||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||
|
||
if (dist === 0) continue;
|
||
|
||
let currentDist = segmentLength - leftover;
|
||
while (currentDist <= dist) {
|
||
const t = currentDist / dist;
|
||
const point = new Array(p1.length) as T;
|
||
for (let j = 0; j < p1.length; j++) {
|
||
point[j] = p1[j] + (p2[j] - p1[j]) * t;
|
||
}
|
||
newPath.push(point);
|
||
currentDist += segmentLength;
|
||
}
|
||
leftover = dist - (currentDist - segmentLength);
|
||
}
|
||
|
||
// Добавляем последнюю точку, если она существенно отличается от последней добавленной
|
||
const lastP = path[path.length - 1];
|
||
const lastNewP = newPath[newPath.length - 1];
|
||
let isDifferent = false;
|
||
for (let j = 0; j < lastP.length; j++) {
|
||
if (Math.abs(lastP[j] - lastNewP[j]) > 0.0000001) {
|
||
isDifferent = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (isDifferent) {
|
||
newPath.push(lastP);
|
||
}
|
||
|
||
return newPath;
|
||
};
|
||
|
||
/**
|
||
* Класс для анимации по полярным координатам
|
||
* Основано на логике анимации из HTML файла (a.html)
|
||
*/
|
||
export class PolarAnimator {
|
||
private currentAngle: number;
|
||
private currentDistance: number;
|
||
private targetAngle: number;
|
||
private targetDistance: number;
|
||
private centerX: number;
|
||
private centerY: number;
|
||
private isAnimating: boolean = false;
|
||
private animationId: number | null = null;
|
||
private onUpdate: (position: { x: number; y: number }) => void;
|
||
private onComplete: () => void;
|
||
private duration: number;
|
||
private startTime: number | null = null;
|
||
|
||
constructor(
|
||
onUpdate: (position: { x: number; y: number }) => void,
|
||
onComplete: () => void = () => {},
|
||
duration: number = 500
|
||
) {
|
||
this.currentAngle = 0;
|
||
this.currentDistance = 0;
|
||
this.targetAngle = 0;
|
||
this.targetDistance = 0;
|
||
this.centerX = 0;
|
||
this.centerY = 0;
|
||
this.onUpdate = onUpdate;
|
||
this.onComplete = onComplete;
|
||
this.duration = duration;
|
||
}
|
||
|
||
/**
|
||
* Анимировать к новой позиции по полярным координатам
|
||
*/
|
||
animateToPolar(
|
||
centerX: number,
|
||
centerY: number,
|
||
targetAngle: number,
|
||
targetDistance: number,
|
||
duration?: number
|
||
): void {
|
||
this.centerX = centerX;
|
||
this.centerY = centerY;
|
||
this.targetAngle = targetAngle;
|
||
this.targetDistance = targetDistance;
|
||
this.duration = duration || this.duration;
|
||
this.startTime = null;
|
||
this.isAnimating = true;
|
||
|
||
if (this.animationId) {
|
||
cancelAnimationFrame(this.animationId);
|
||
}
|
||
|
||
this.animationId = requestAnimationFrame(this.animate);
|
||
}
|
||
|
||
/**
|
||
* Установить позицию мгновенно
|
||
*/
|
||
setPosition(angle: number, distance: number): void {
|
||
this.currentAngle = angle;
|
||
this.currentDistance = distance;
|
||
this.targetAngle = angle;
|
||
this.targetDistance = distance;
|
||
this.isAnimating = false;
|
||
|
||
const x = this.centerX + distance * Math.cos(angle);
|
||
const y = this.centerY - distance * Math.sin(angle);
|
||
this.onUpdate({ x, y });
|
||
}
|
||
|
||
/**
|
||
* Остановить анимацию
|
||
*/
|
||
stop(): void {
|
||
this.isAnimating = false;
|
||
if (this.animationId) {
|
||
cancelAnimationFrame(this.animationId);
|
||
this.animationId = null;
|
||
}
|
||
}
|
||
|
||
private animate = (timestamp: number): void => {
|
||
if (!this.isAnimating) return;
|
||
|
||
if (!this.startTime) {
|
||
this.startTime = timestamp;
|
||
}
|
||
|
||
const elapsed = timestamp - this.startTime;
|
||
const progress = Math.min(elapsed / this.duration, 1);
|
||
|
||
// Используем easeOutCubic для плавной анимации
|
||
const easedProgress = 1 - Math.pow(1 - progress, 3);
|
||
|
||
this.currentAngle = lerpAngle(this.currentAngle, this.targetAngle, easedProgress);
|
||
this.currentDistance = lerp(this.currentDistance, this.targetDistance, easedProgress);
|
||
|
||
const x = this.centerX + this.currentDistance * Math.cos(this.currentAngle);
|
||
const y = this.centerY - this.currentDistance * Math.sin(this.currentAngle);
|
||
|
||
this.onUpdate({ x, y });
|
||
|
||
if (progress < 1) {
|
||
this.animationId = requestAnimationFrame(this.animate);
|
||
} else {
|
||
this.isAnimating = false;
|
||
this.onComplete();
|
||
}
|
||
};
|
||
}
|