226 lines
8.5 KiB
JavaScript
226 lines
8.5 KiB
JavaScript
/**
|
|
* 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<string>} 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;
|
|
}
|