324 lines
8.0 KiB
TypeScript

/**
* 视频编辑功能工具函数
*/
import { EditPoint, EditPointPosition } from './types';
/**
* 防抖函数
*/
export function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number
): (...args: Parameters<T>) => void {
let timeout: NodeJS.Timeout;
return (...args: Parameters<T>) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
}
/**
* 节流函数
*/
export function throttle<T extends (...args: any[]) => any>(
func: T,
limit: number
): (...args: Parameters<T>) => void {
let inThrottle: boolean;
return (...args: Parameters<T>) => {
if (!inThrottle) {
func(...args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
/**
* 计算两点之间的距离
*/
export function calculateDistance(
point1: { x: number; y: number },
point2: { x: number; y: number }
): number {
const dx = point2.x - point1.x;
const dy = point2.y - point1.y;
return Math.sqrt(dx * dx + dy * dy);
}
/**
* 检查点是否在矩形区域内
*/
export function isPointInRect(
point: { x: number; y: number },
rect: { x: number; y: number; width: number; height: number }
): boolean {
return (
point.x >= rect.x &&
point.x <= rect.x + rect.width &&
point.y >= rect.y &&
point.y <= rect.y + rect.height
);
}
/**
* 将屏幕坐标转换为百分比坐标
*/
export function screenToPercentage(
screenPoint: { x: number; y: number },
containerSize: { width: number; height: number }
): EditPointPosition {
return {
x: Math.max(0, Math.min(100, (screenPoint.x / containerSize.width) * 100)),
y: Math.max(0, Math.min(100, (screenPoint.y / containerSize.height) * 100))
};
}
/**
* 将百分比坐标转换为屏幕坐标
*/
export function percentageToScreen(
percentagePoint: EditPointPosition,
containerSize: { width: number; height: number }
): { x: number; y: number } {
return {
x: (percentagePoint.x / 100) * containerSize.width,
y: (percentagePoint.y / 100) * containerSize.height
};
}
/**
* 生成唯一ID
*/
export function generateId(): string {
return `edit-point-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
/**
* 格式化时间戳为可读格式
*/
export function formatTimestamp(seconds: number): string {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = Math.floor(seconds % 60);
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
}
/**
* 验证编辑点数据
*/
export function validateEditPoint(editPoint: Partial<EditPoint>): string[] {
const errors: string[] = [];
if (!editPoint.videoId) {
errors.push('Video ID is required');
}
if (!editPoint.projectId) {
errors.push('Project ID is required');
}
if (!editPoint.position) {
errors.push('Position is required');
} else {
if (editPoint.position.x < 0 || editPoint.position.x > 100) {
errors.push('Position X must be between 0 and 100');
}
if (editPoint.position.y < 0 || editPoint.position.y > 100) {
errors.push('Position Y must be between 0 and 100');
}
}
if (editPoint.timestamp !== undefined && editPoint.timestamp < 0) {
errors.push('Timestamp must be non-negative');
}
if (editPoint.description && editPoint.description.length > 500) {
errors.push('Description must be less than 500 characters');
}
return errors;
}
/**
* 计算最佳输入框位置(避免重叠)
*/
export function calculateOptimalInputPosition(
editPoint: EditPointPosition,
containerSize: { width: number; height: number },
existingPositions: { x: number; y: number }[],
inputSize: { width: number; height: number } = { width: 280, height: 120 }
): { x: number; y: number; connectionEnd: { x: number; y: number } } {
const pointScreen = percentageToScreen(editPoint, containerSize);
// 候选位置(相对于编辑点)
const candidates = [
{ x: 60, y: -60, direction: 'top-right' },
{ x: -60 - inputSize.width, y: -60, direction: 'top-left' },
{ x: 60, y: 60, direction: 'bottom-right' },
{ x: -60 - inputSize.width, y: 60, direction: 'bottom-left' },
{ x: 80, y: -30, direction: 'right' },
{ x: -80 - inputSize.width, y: -30, direction: 'left' }
];
// 为每个候选位置计算得分
const scoredCandidates = candidates.map(candidate => {
const absolutePos = {
x: pointScreen.x + candidate.x,
y: pointScreen.y + candidate.y
};
let score = 0;
// 检查是否在容器内
if (
absolutePos.x >= 10 &&
absolutePos.x + inputSize.width <= containerSize.width - 10 &&
absolutePos.y >= 10 &&
absolutePos.y + inputSize.height <= containerSize.height - 10
) {
score += 100;
}
// 检查与现有位置的距离
const minDistance = Math.min(
...existingPositions.map(pos => calculateDistance(absolutePos, pos)),
Infinity
);
score += Math.min(minDistance / 10, 50);
// 偏好右上角位置
if (candidate.direction === 'top-right') {
score += 20;
}
return {
...candidate,
position: absolutePos,
score
};
});
// 选择得分最高的位置
const bestCandidate = scoredCandidates.reduce((best, current) =>
current.score > best.score ? current : best
);
// 计算连接线终点
const connectionEnd = {
x: bestCandidate.position.x + (bestCandidate.x > 0 ? 0 : inputSize.width),
y: bestCandidate.position.y + inputSize.height / 2
};
return {
x: Math.max(10, Math.min(containerSize.width - inputSize.width - 10, bestCandidate.position.x)),
y: Math.max(10, Math.min(containerSize.height - inputSize.height - 10, bestCandidate.position.y)),
connectionEnd
};
}
/**
* 深度比较两个对象是否相等
*/
export function deepEqual(obj1: any, obj2: any): boolean {
if (obj1 === obj2) return true;
if (obj1 == null || obj2 == null) return false;
if (typeof obj1 !== typeof obj2) return false;
if (typeof obj1 !== 'object') return obj1 === obj2;
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) return false;
for (const key of keys1) {
if (!keys2.includes(key)) return false;
if (!deepEqual(obj1[key], obj2[key])) return false;
}
return true;
}
/**
* 创建错误处理装饰器
*/
export function withErrorHandling<T extends (...args: any[]) => any>(
fn: T,
errorHandler?: (error: Error) => void
): T {
return ((...args: Parameters<T>) => {
try {
const result = fn(...args);
if (result instanceof Promise) {
return result.catch((error: Error) => {
console.error('Async function error:', error);
errorHandler?.(error);
throw error;
});
}
return result;
} catch (error) {
console.error('Function error:', error);
errorHandler?.(error as Error);
throw error;
}
}) as T;
}
/**
* 性能监控装饰器
*/
export function withPerformanceMonitoring<T extends (...args: any[]) => any>(
fn: T,
name: string
): T {
return ((...args: Parameters<T>) => {
const start = performance.now();
const result = fn(...args);
if (result instanceof Promise) {
return result.finally(() => {
const end = performance.now();
console.log(`${name} took ${end - start} milliseconds`);
});
} else {
const end = performance.now();
console.log(`${name} took ${end - start} milliseconds`);
return result;
}
}) as T;
}
/**
* 本地存储工具
*/
export const storage = {
get<T>(key: string, defaultValue: T): T {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : defaultValue;
} catch {
return defaultValue;
}
},
set<T>(key: string, value: T): void {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error('Failed to save to localStorage:', error);
}
},
remove(key: string): void {
try {
localStorage.removeItem(key);
} catch (error) {
console.error('Failed to remove from localStorage:', error);
}
}
};