( script: string, animationId: string, fromPercentage: number, toPercentage: number, )
| 2331 | * the move resolves onto the same keyframe. Acorn twin: moveKeyframeInScript. |
| 2332 | */ |
| 2333 | export function moveKeyframeInScript( |
| 2334 | script: string, |
| 2335 | animationId: string, |
| 2336 | fromPercentage: number, |
| 2337 | toPercentage: number, |
| 2338 | ): string { |
| 2339 | const loc = locateAnimationWithFallback(script, animationId); |
| 2340 | if (!loc) return script; |
| 2341 | // Array-form keyframes can't host an arbitrary destination percentage — |
| 2342 | // normalize to object form in place first (mirrors addKeyframeToScript). |
| 2343 | const kfNode = |
| 2344 | findKeyframesObjectNode(loc.target.call.varsArg) ?? |
| 2345 | convertArrayKeyframesToObjectNode(loc.target.call.varsArg); |
| 2346 | if (!kfNode) return script; |
| 2347 | |
| 2348 | const match = findKeyframePropByPct(kfNode, fromPercentage); |
| 2349 | if (!match) return script; |
| 2350 | // No-op ONLY for a negligible move (matches the drag's NOOP_EPSILON). The old |
| 2351 | // `collision.prop === match.prop` guard dropped EVERY sub-PCT_TOLERANCE (2%) |
| 2352 | // retime, because findKeyframePropByPct resolves the destination back onto the |
| 2353 | // from-keyframe — so a deliberate 1% drag committed nothing. Acorn twin too. |
| 2354 | if (Math.abs(fromPercentage - toPercentage) < MOVE_NOOP_EPSILON_PCT) return script; |
| 2355 | // A destination keyframe is only a real collision (overwrite) when it's a |
| 2356 | // DIFFERENT keyframe; resolving back onto the from-keyframe is not. |
| 2357 | const dest = findKeyframePropByPct(kfNode, toPercentage); |
| 2358 | const collision = dest && dest.prop !== match.prop ? dest : null; |
| 2359 | |
| 2360 | // Reuse each keyframe's value node verbatim (preserves properties + |
| 2361 | // per-keyframe ease + _auto). Drop the moved keyframe (and any destination |
| 2362 | // keyframe it overwrites), re-key the moved value to toPercentage, then re-sort. |
| 2363 | const movedValue = match.prop.value; |
| 2364 | const entries: Array<{ pct: number; value: AstNode }> = []; |
| 2365 | for (const prop of filterPercentageProps(kfNode)) { |
| 2366 | if (prop === match.prop) continue; |
| 2367 | if (collision && prop === collision.prop) continue; |
| 2368 | const pct = percentageFromKey(propKeyName(prop) ?? ""); |
| 2369 | if (Number.isNaN(pct)) continue; |
| 2370 | entries.push({ pct, value: prop.value }); |
| 2371 | } |
| 2372 | entries.push({ pct: toPercentage, value: movedValue }); |
| 2373 | entries.sort((a, b) => a.pct - b.pct); |
| 2374 | |
| 2375 | kfNode.properties = entries.map((e) => { |
| 2376 | const p = parseExpr(`{ ${JSON.stringify(`${e.pct}%`)}: {} }`).properties[0]; |
| 2377 | p.value = e.value; |
| 2378 | return p; |
| 2379 | }); |
| 2380 | return recast.print(loc.parsed.ast).code; |
| 2381 | } |
| 2382 | |
| 2383 | /** |
| 2384 | * Resize a keyframed tween's window (boundary drag-to-retime): set the tween's |
no test coverage detected