(
parsed: ParsedDocument,
ids: HfId[],
timing: { start?: number; duration?: number; trackIndex?: number },
)
| 399 | |
| 400 | // fallow-ignore-next-line complexity |
| 401 | function handleSetTiming( |
| 402 | parsed: ParsedDocument, |
| 403 | ids: HfId[], |
| 404 | timing: { start?: number; duration?: number; trackIndex?: number }, |
| 405 | ): MutationResult { |
| 406 | const result: MutationResult = { forward: [], inverse: [] }; |
| 407 | |
| 408 | // Parse GSAP script once; updateAnimationInScript re-parses internally per call but |
| 409 | // we avoid re-fetching the script element on every iteration. |
| 410 | const origScript = getGsapScript(parsed.document); |
| 411 | const parsedGsap = origScript ? parseGsapScriptAcornForWrite(origScript) : null; |
| 412 | let currentScript = origScript; |
| 413 | |
| 414 | for (const id of ids) { |
| 415 | const el = resolveScoped(parsed.document, id); |
| 416 | if (!el) continue; |
| 417 | |
| 418 | const oldStartStr = el.getAttribute("data-start"); |
| 419 | const oldEndStr = el.getAttribute("data-end"); |
| 420 | const oldDurationStr = el.getAttribute("data-duration"); |
| 421 | const oldTrackStr = el.getAttribute("data-track-index"); |
| 422 | |
| 423 | const oldStart = oldStartStr !== null ? parseFloat(oldStartStr) : null; |
| 424 | const oldEnd = oldEndStr !== null ? parseFloat(oldEndStr) : null; |
| 425 | const oldDurationAttr = oldDurationStr !== null ? parseFloat(oldDurationStr) : null; |
| 426 | // Prefer an explicit data-duration — the attribute clips are authored with and |
| 427 | // the runtime reads — falling back to data-end − data-start. Reading only |
| 428 | // data-end left oldDuration null for duration-authored clips, collapsing the |
| 429 | // GSAP duration-scale ratio to 1 and scaling nothing. |
| 430 | const oldDuration = |
| 431 | oldDurationAttr !== null |
| 432 | ? oldDurationAttr |
| 433 | : oldStart !== null && oldEnd !== null |
| 434 | ? oldEnd - oldStart |
| 435 | : null; |
| 436 | const oldTrack = oldTrackStr !== null ? parseInt(oldTrackStr, 10) : null; |
| 437 | |
| 438 | const newStart = timing.start ?? oldStart; |
| 439 | const newDuration = timing.duration ?? oldDuration; |
| 440 | |
| 441 | if (timing.start !== undefined && newStart !== null) { |
| 442 | const path = timingPath(id, "start"); |
| 443 | const p = scalarChange(path, oldStart, newStart); |
| 444 | result.forward.push(p.forward); |
| 445 | result.inverse.push(p.inverse); |
| 446 | el.setAttribute("data-start", String(newStart)); |
| 447 | } |
| 448 | |
| 449 | // Write to whichever timing attribute the clip actually uses. A data-duration |
| 450 | // clip updates data-duration only on a real resize (duration is invariant |
| 451 | // under a move); a data-end clip updates data-end whenever start or duration |
| 452 | // changes (end = start + duration). Writing a fresh data-end beside a stale |
| 453 | // data-duration had no playback effect. |
| 454 | if (oldDurationStr !== null) { |
| 455 | if (timing.duration !== undefined && newDuration !== null) { |
| 456 | const path = timingPath(id, "duration"); |
| 457 | const p = scalarChange(path, oldDurationAttr, newDuration); |
| 458 | result.forward.push(p.forward); |
no test coverage detected