heygen-com/hyperframes · error · Error

motionToGsap: empty track "${track.property}"

Error message

motionToGsap: empty track "${track.property}"

What it means

Thrown by buildTween as a defensive guard after the length check passes: track.values[0] is undefined despite values.length >= 2. With a dense array and values.length >= 2 this is unreachable, so in practice it fires only for a sparse/holey array (e.g. [undefined, 1]) — a malformed MotionTrack where the first keyframe slot exists but holds no value. It prevents GSAP from receiving an undefined initial value and emitting a broken tween deep in the pipeline.

Source

Thrown at packages/core/src/figma/motionToGsap.ts:78

    const rawEase = track.ease[i - 1] ?? "linear";
    const ease = resolveStepEase(rawEase, customEases, counter);
    steps.push({ value, duration: (tCur - tPrev) * track.duration, ease });
  }

  return steps;
}

function buildTween(
  track: MotionTrack,
  selector: string,
  customEases: CustomEaseRef[],
  counter: CustomEaseCounter,
): GsapTween {
  if (track.values.length < 2 || track.times.length !== track.values.length) {
    throw new Error(`motionToGsap: invalid track "${track.property}" (values/times mismatch)`);
  }
  const initial = track.values[0];
  if (initial === undefined) throw new Error(`motionToGsap: empty track "${track.property}"`);

  return {
    selector,
    property: track.property,
    initial,
    steps: buildSteps(track, customEases, counter),
    repeat: clampRepeat(track.repeat),
  };
}

export function motionToGsap(doc: MotionDoc): TimelineSpec {
  const customEases: CustomEaseRef[] = [];
  const counter: CustomEaseCounter = { value: 0 };
  const tweens = doc.tracks.map((track) => buildTween(track, doc.selector, customEases, counter));
  return { timelineId: deriveId(doc.selector), tweens, customEases };
}

View on GitHub (pinned to c2996c8626)

Solutions

  1. Build the values array with push()/literal form so no holes appear.
  2. Validate the track before motionToGsap: values.every(v => v !== undefined).
  3. Inspect the source of the track (figma motion snippet parser) for an off-by-one in array assignment.

Example fix

// before — off-by-one leaves index 0 undefined
const values = new Array(2);
for (let i = 1; i <= 2; i++) values[i] = i * 0.5; // values[0] is a hole

// after — assign from 0, or build with a literal
const values = [0, 0.5, 1];
Defensive patterns

Strategy: validation

Validate before calling

import type { MotionTrack } from '.../figma/types';
export function isDenseTrack(t: MotionTrack): boolean {
  return t.values.length >= 2 && t.values.every((v) => v !== undefined);
}
if (!isDenseTrack(track)) throw new Error(`track ${track.property} has holes`);

Type guard

import type { MotionTrack } from '.../figma/types';
export function isDenseAnimatableTrack(t: MotionTrack): t is MotionTrack {
  return t.values.length >= 2
    && t.times.length === t.values.length
    && t.values.every((v) => v !== undefined);
}

Prevention

When it happens

Trigger: A MotionTrack whose values array is sparse ([undefined, 0.5] or values[0] deleted); a builder that allocated array length then failed to populate index 0; JSON deserialisation producing holes after filtering.

Common situations: A motion-snippet parser that does `values[i] = ...` starting at i=1; an array built with `new Array(n)` whose slots were never all assigned; an object spread that dropped the first value.

Related errors


AI-assisted analysis of heygen-com/hyperframes@c2996c8626 (2026-08-12). Data as JSON: /api/errors/6310aeececbaf4e3. Report an issue: GitHub.