Files
2026-01-28 19:33:35 +13:00

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;
}