Skip to content

Studio: add-keyframe merging into an already-eased keyframe serializes a duplicate ease key (0.7.21) #1801

Description

@nicolasfilippini

Version: 0.7.21

Summary
When a canvas move/resize lands an add-keyframe mutation on top of an existing keyframe that already has an ease, the serializer writes the ease key twice in the same keyframe object. It's bounded to 2 (never 3+), functionally benign (duplicate JS object key → last one wins, same value), and the runtime reader de-dupes — but hyperframes lint does not flag it, so it silently dirties design sources.

Repro

  1. Composition with a non-grouped element and a keyframed tween containing an intermediate eased keyframe, e.g.:
    tl.to("#box", { keyframes: {
      "0%":   { scale: 1 },
      "60%":  { scale: 1.18, ease: "power2.out" },
      "100%": { scale: 1 }
    }, duration: 4 });
  2. Move the playhead onto the eased 60% keyframe.
  3. Move or resize #box on the canvas (this fires an add-keyframe that merges into the existing 60% record).
  4. Inspect the source.

Expected: "60%": { scale: <new>, x: <new>, y: <new>, ease: "power2.out" } (single ease).
Actual: "60%": { scale: <new>, ease: "power2.out", x: <new>, y: <new>, ease: "power2.out" } (duplicate ease).

Scope (tested matrix)

Mutation Starting keyframe Result
add-keyframe {x,y} {scale, ease} (eased) DUP {scale, ease, x, y, ease}
add-keyframe {scale} {scale, ease} (eased) DUP {scale, ease, ease}
add-keyframe {x,y} {scale} (non-eased) clean
update-keyframe {x,y} {scale, ease} (eased) clean (upsertProp is dedup-safe)

So it triggers only on add-keyframe merging into an already-eased keyframe (the canvas move/resize gesture while the playhead sits on an eased keyframe). Inspector field edits (update-keyframe) are unaffected.

Mechanism (cli.js)
addKeyframeToScript2, "existing ObjectExpression" branch:

const merged = { ...existingRecord };          // existingRecord already contains `ease`
for (...properties...) merged[k] = v;
existing.prop.value = buildKeyframeValueNode(
  merged,                                       // merged carries the `ease` key
  ease ?? (typeof existingRecord.ease === "string" ? existingRecord.ease : void 0) // and `ease` is passed again as arg
);

buildKeyframeValueNode(properties, ease) emits ease twice: once from Object.entries(properties) (the merged.ease) and once from the trailing if (ease) entries.push("ease: …"). → duplicate key.

Suggested fix: in buildKeyframeValueNode, skip the trailing ease push if properties already carries an ease key (or drop ease from merged before passing it). Either makes the two paths mutually exclusive.

Severity: low — bounded to 2, benign at render/runtime. Reported mainly because lint is blind to it, so it accumulates invisibly in design sources. Same serialization family as #1800.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions