( script: string, animationId: string, percentage: number, properties: Record<string, number | string>, ease?: string, )
| 2424 | * Replace the properties (and optionally ease) at an existing keyframe percentage. |
| 2425 | */ |
| 2426 | export function updateKeyframeInScript( |
| 2427 | script: string, |
| 2428 | animationId: string, |
| 2429 | percentage: number, |
| 2430 | properties: Record<string, number | string>, |
| 2431 | ease?: string, |
| 2432 | ): string { |
| 2433 | // Array-form keyframes (`keyframes: [{x,y}, …]`) have no explicit percentages — |
| 2434 | // GSAP distributes them evenly. The percentage-keyed object path below can't |
| 2435 | // match them (findKeyframesObjectNode only matches ObjectExpression), so dragging |
| 2436 | // a motion-path node on an array-authored tween silently no-op'd. Resolve the |
| 2437 | // element by its implicit percentage and replace it in place. Mirrors the array |
| 2438 | // branch in removeKeyframeFromScript. |
| 2439 | const arrLoc = locateAnimationWithFallback(script, animationId); |
| 2440 | const arrVal = arrLoc && findPropertyNode(arrLoc.target.call.varsArg, "keyframes"); |
| 2441 | if (arrLoc && arrVal?.type === "ArrayExpression") { |
| 2442 | const elements: AstNode[] = (arrVal.elements ?? []).filter( |
| 2443 | (e: AstNode | null): e is AstNode => !!e && e.type === "ObjectExpression", |
| 2444 | ); |
| 2445 | const n = elements.length; |
| 2446 | if (n === 0) return script; |
| 2447 | let matchIdx = -1; |
| 2448 | let bestDist = Number.POSITIVE_INFINITY; |
| 2449 | for (let i = 0; i < n; i++) { |
| 2450 | const pct = n > 1 ? (i / (n - 1)) * 100 : 0; |
| 2451 | const dist = Math.abs(pct - percentage); |
| 2452 | if (dist <= PCT_TOLERANCE && dist < bestDist) { |
| 2453 | matchIdx = i; |
| 2454 | bestDist = dist; |
| 2455 | } |
| 2456 | } |
| 2457 | if (matchIdx === -1) return script; |
| 2458 | const realIdx = arrVal.elements.indexOf(elements[matchIdx]); |
| 2459 | arrVal.elements[realIdx] = buildKeyframeValueNode(properties, ease); |
| 2460 | return recast.print(arrLoc.parsed.ast).code; |
| 2461 | } |
| 2462 | |
| 2463 | const ctx = locateKeyframeCtx(script, animationId, percentage); |
| 2464 | if (!ctx) return script; |
| 2465 | const { loc, kfNode } = ctx; |
| 2466 | |
| 2467 | const match = findKeyframePropByPct(kfNode, percentage); |
| 2468 | if (!match) return script; |
| 2469 | |
| 2470 | if (Object.keys(properties).length === 0 && ease) { |
| 2471 | // Ease-only update: preserve existing properties, just add/replace ease |
| 2472 | const existing = match.prop.value; |
| 2473 | if (existing?.type === "ObjectExpression") { |
| 2474 | const props = (existing.properties ?? []) as AstNode[]; |
| 2475 | const easeIdx = props.findIndex( |
| 2476 | (p: AstNode) => isObjectProperty(p) && propKeyName(p) === "ease", |
| 2477 | ); |
| 2478 | const easeNode = parseExpr(`({ ease: ${JSON.stringify(ease)} })`).properties[0]; |
| 2479 | if (easeIdx >= 0) { |
| 2480 | props[easeIdx] = easeNode; |
| 2481 | } else { |
| 2482 | props.push(easeNode); |
| 2483 | } |
no test coverage detected