/** * Frame evaluation and interpolation logic */ import { deepCopy } from "../utils/helpers.js"; /** * Get interpolated pointTypes array from two curves. * Since pointTypes should be the same across all keyframes, we prefer curveA's types. * @param {Object} curveA - First curve (earlier keyframe) * @param {Object} curveB - Second curve (later keyframe) * @param {number} totalPoints - Total point count in the interpolated result * @returns {Array} Array of point types */ function getInterpolatedPointTypes(curveA, curveB, totalPoints) { // Since pointTypes should be consistent across keyframes, prefer curveA // but fall back to curveB if curveA doesn't have valid types const typesA = Array.isArray(curveA.pointTypes) ? curveA.pointTypes : []; const typesB = Array.isArray(curveB.pointTypes) ? curveB.pointTypes : []; const result = []; for (let i = 0; i < totalPoints; i++) { if (i < typesA.length && (typesA[i] === "smooth" || typesA[i] === "hard")) { result.push(typesA[i]); } else if (i < typesB.length && (typesB[i] === "smooth" || typesB[i] === "hard")) { result.push(typesB[i]); } else { result.push("hard"); // Default for any missing entries } } return result; } /** * Evaluate curves for a given frame. * Each curve is interpolated independently based on its own keyframes. * * @param {Object} shape - Shape object with frames * @param {number} frameIndex - Frame number to evaluate * @param {Object} options - Evaluation options * @param {boolean} options.hold - If true, hold curves from nearest keyframe when no interpolation possible * @returns {Array} List of curve objects for this frame */ export function evaluateFrame(shape, frameIndex, options = {}) { const frames = shape.frames || {}; const frameKey = String(frameIndex); const hold = Boolean(options.hold); // Collect all unique curve IDs across all keyframes const allCurveIds = new Set(); const curveKeyframes = new Map(); // curveId -> sorted array of frame numbers where it exists Object.entries(frames).forEach(([frame, curves]) => { const frameNum = parseInt(frame); if (isNaN(frameNum)) return; (curves || []).forEach(c => { if (c && c.id) { allCurveIds.add(c.id); if (!curveKeyframes.has(c.id)) { curveKeyframes.set(c.id, []); } curveKeyframes.get(c.id).push(frameNum); } }); }); // Sort keyframes for each curve curveKeyframes.forEach((frames, id) => { frames.sort((a, b) => a - b); }); if (allCurveIds.size === 0) { return []; } const result = []; // Evaluate each curve independently based on its own keyframes for (const curveId of allCurveIds) { const keyframes = curveKeyframes.get(curveId) || []; if (keyframes.length === 0) continue; // Check for exact keyframe for this curve if (keyframes.includes(frameIndex)) { const frameCurves = frames[frameKey] || []; const curve = frameCurves.find(c => c.id === curveId); if (curve) { result.push(deepCopy(curve)); } continue; } // Find prev and next keyframes FOR THIS CURVE let prevFrame = null; let nextFrame = null; for (const fn of keyframes) { if (fn < frameIndex) { prevFrame = fn; } else if (fn > frameIndex) { nextFrame = fn; break; } } // Interpolate or hold based on this curve's keyframes if (prevFrame !== null && nextFrame !== null) { // Interpolate between this curve's keyframes const curveA = (frames[String(prevFrame)] || []).find(c => c.id === curveId); const curveB = (frames[String(nextFrame)] || []).find(c => c.id === curveId); if (curveA && curveB) { const t = (frameIndex - prevFrame) / (nextFrame - prevFrame); const interpolatedPoints = []; // Handle different point counts - use the smaller count const minPoints = Math.min(curveA.points.length, curveB.points.length); for (let i = 0; i < minPoints; i++) { const pa = curveA.points[i]; const pb = curveB.points[i]; const x = pa[0] + (pb[0] - pa[0]) * t; const y = pa[1] + (pb[1] - pa[1]) * t; interpolatedPoints.push([x, y]); } // If one curve has more points, add them at the end (hold from that curve) if (curveA.points.length > minPoints) { for (let i = minPoints; i < curveA.points.length; i++) { interpolatedPoints.push([...curveA.points[i]]); } } else if (curveB.points.length > minPoints) { for (let i = minPoints; i < curveB.points.length; i++) { interpolatedPoints.push([...curveB.points[i]]); } } result.push({ id: curveA.id, closed: curveA.closed, points: interpolatedPoints, pointTypes: getInterpolatedPointTypes(curveA, curveB, interpolatedPoints.length) }); } } else if (hold) { // Hold from nearest keyframe for this curve const holdFrame = prevFrame !== null ? prevFrame : nextFrame; if (holdFrame !== null) { const curve = (frames[String(holdFrame)] || []).find(c => c.id === curveId); if (curve) { result.push(deepCopy(curve)); } } } } return result; } /** * Interpolate curves between two keyframes. * Curves that only exist in one keyframe are held (included as-is). * * @param {Array} curvesA - Curves at earlier keyframe * @param {Array} curvesB - Curves at later keyframe * @param {number} frameA - Earlier frame number * @param {number} frameB - Later frame number * @param {number} targetFrame - Frame to interpolate to * @returns {Array} List of interpolated curves */ export function interpolateCurves(curvesA, curvesB, frameA, frameB, targetFrame) { const t = (targetFrame - frameA) / (frameB - frameA); const result = []; const processedIds = new Set(); // Process curves from A - interpolate if in both, hold if only in A for (const curveA of curvesA) { processedIds.add(curveA.id); const curveB = curvesB.find(cb => cb.id === curveA.id); if (curveB) { // Curve exists in both keyframes - interpolate const interpolatedPoints = []; // Handle different point counts - use the smaller count const minPoints = Math.min(curveA.points.length, curveB.points.length); for (let i = 0; i < minPoints; i++) { const pa = curveA.points[i]; const pb = curveB.points[i]; const x = pa[0] + (pb[0] - pa[0]) * t; const y = pa[1] + (pb[1] - pa[1]) * t; interpolatedPoints.push([x, y]); } // If one curve has more points, add them at the end (hold from that curve) if (curveA.points.length > minPoints) { for (let i = minPoints; i < curveA.points.length; i++) { interpolatedPoints.push([...curveA.points[i]]); } } else if (curveB.points.length > minPoints) { for (let i = minPoints; i < curveB.points.length; i++) { interpolatedPoints.push([...curveB.points[i]]); } } result.push({ id: curveA.id, closed: curveA.closed, points: interpolatedPoints, pointTypes: getInterpolatedPointTypes(curveA, curveB, interpolatedPoints.length) }); } else { // Curve only in A - hold it (use A's position) result.push(deepCopy(curveA)); } } // Process curves only in B (not in A) - hold them for (const curveB of curvesB) { if (!processedIds.has(curveB.id)) { result.push(deepCopy(curveB)); } } return result; }