export const MIN_FRAME_DURATION_MS = 50; export const MAX_FRAME_DURATION_MS = 604800000; export function effectiveFrameName(animationName, frame, position = frame?.position) { const customName = typeof frame?.name === "string" ? frame.name.trim() : ""; if (customName) return customName; return `${String(animationName || "动图").trim() || "动图"}-第${position}帧`; } export function animationEditContextName(animationName, frame, position = frame?.position) { const normalizedAnimationName = String(animationName || "动图").trim() || "动图"; const customName = typeof frame?.name === "string" ? frame.name.trim() : ""; return `${normalizedAnimationName}-${customName || `第${position}帧`}`; } export function animationPreviewFrameAtElapsed(frames, elapsedMs) { if (!Array.isArray(frames) || !frames.length) return null; const durations = frames.map((frame) => Number(frame?.duration_ms)); if (durations.some((duration) => !Number.isFinite(duration) || duration <= 0)) return null; const totalDurationMs = durations.reduce((total, duration) => total + duration, 0); const normalizedElapsed = Number.isFinite(Number(elapsedMs)) ? Number(elapsedMs) : 0; const cycleElapsedMs = ((normalizedElapsed % totalDurationMs) + totalDurationMs) % totalDurationMs; let boundary = 0; for (let index = 0; index < frames.length; index += 1) { boundary += durations[index]; if (cycleElapsedMs < boundary) { return { frame: frames[index], index, remainingMs: boundary - cycleElapsedMs, totalDurationMs, }; } } return null; } export function formatFrameDuration(durationMs, unit = "ms") { const duration = Number(durationMs); if (!Number.isInteger(duration)) return ""; if (unit === "ms") return String(duration); if (unit !== "s") throw new Error("unknown animation duration unit"); return (duration / 1000).toFixed(3).replace(/\.?0+$/, ""); } export function validateFrameDuration(value, unit = "ms") { const normalized = typeof value === "string" ? value.trim() : String(value ?? "").trim(); const millisecondsPattern = /^\d+$/; const secondsPattern = /^\d+(?:\.\d{1,3})?$/; if (!normalized || (unit === "ms" ? !millisecondsPattern.test(normalized) : !secondsPattern.test(normalized))) { return { valid: false, duration: null, error: unit === "ms" ? `请输入 ${MIN_FRAME_DURATION_MS}..${MAX_FRAME_DURATION_MS} 之间的整数毫秒值。` : "请输入 0.050..604800 之间、最多三位小数的秒数。", }; } const numeric = Number(normalized); const duration = unit === "s" ? numeric * 1000 : numeric; if (!Number.isInteger(duration) || duration < MIN_FRAME_DURATION_MS || duration > MAX_FRAME_DURATION_MS) { return { valid: false, duration: null, error: unit === "ms" ? `请输入 ${MIN_FRAME_DURATION_MS}..${MAX_FRAME_DURATION_MS} 之间的整数毫秒值。` : "请输入 0.050..604800 之间、最多三位小数的秒数。", }; } return { valid: true, duration, error: "" }; } export function reorderSelectedFrameIds(frameIds, selectedFrameIds, insertAfterFrameId = null) { if (!Array.isArray(frameIds) || !Array.isArray(selectedFrameIds) || !selectedFrameIds.length) return null; const selected = new Set(selectedFrameIds); if (selected.size !== selectedFrameIds.length || selectedFrameIds.some((id) => !frameIds.includes(id))) return null; if (insertAfterFrameId !== null && (!frameIds.includes(insertAfterFrameId) || selected.has(insertAfterFrameId))) return null; const moving = frameIds.filter((id) => selected.has(id)); const remaining = frameIds.filter((id) => !selected.has(id)); const insertAt = insertAfterFrameId === null ? 0 : remaining.indexOf(insertAfterFrameId) + 1; return [...remaining.slice(0, insertAt), ...moving, ...remaining.slice(insertAt)]; }