forked from 77media/video-flow
324 lines
8.0 KiB
TypeScript
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);
|
|
}
|
|
}
|
|
};
|