Files
WhiteNightsAdminPanel/src/client/src/utils/animationUtils.ts

303 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Утилиты для анимации
* Основано на логике анимации из 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();
}
};
}