Temporal Animation Diff
Finds animation discontinuities by rendering a dense deterministic frame
sequence, diffing each adjacent frame, and inspecting the highest-delta
transitions. This is for motion debugging, not for committed goldens.
Use this before tuning by eye when a clip feels like it snaps, teleports, pops,
or has rubbery wobble. The output should name exact frame pairs and phases
(p=...) so the code fix can target the real transition.
Loop
stateDiagram-v2
[*] --> RenderSequence
RenderSequence --> RankDiffs: adjacent frame pixel deltas
RankDiffs --> InspectWorst: before/after/diff PNGs
InspectWorst --> Diagnose: map frame phase to channels, camera, pinning, layout
Diagnose --> Patch
Patch --> RerenderSameSequence
RerenderSameSequence --> RankDiffs
RankDiffs --> Regression: convert proven bug into a focused test
Regression --> Cleanup
Cleanup --> [*]
Workflow
Render densely enough to catch snaps. Use at least 120 frames per loop;
use 240 when the user reports occasional jumps. Keep viewport, scale,
camera, expression, backdrop, and timing identical across runs.
Diff adjacent frames. For each pair, compute changed pixels, mean changed
delta, score (changedPixels * meanDelta), changed bounding box, and changed
centroid. Sort descending by score.
Write artifacts for the worst transitions. Save:
fNNN.png
fNNN+1.png
diff_NNN_NNN+1.png
Put them under build/character_frame_diffs/<label>/ or another ignored
build directory. Do not commit them.
Inspect before patching. Read the worst before/after/diff images. Decide
whether the delta is a true discontinuity or just a large legitimate pose
change. True discontinuities usually show whole-body translation, camera
jumps, support-foot re-anchors, expression swaps, z-order pops, or limb
teleporting.
Map phase to code. Convert frame pair to normalized phase:
p0 = from / frames, p1 = to / frames. Check channels/keyframes/contact
spans/camera curves that cross that phase. For character clips, also compare
scene-level transforms against painter-level output; a bug can live after
frameAt.
Rerender the exact same sequence after each fix. Report the before/after
scores for the same frame pair. Do not say a snap is fixed unless the same
transition has been rerendered and inspected.
Keep only durable tests. Delete scratch diff tests/scripts before commit.
If the bug was real, add a small regression test that asserts the measured
failure mode directly, such as max visible center delta, no support re-anchor,
monotonic camera movement, or bounded joint displacement.
Minimal Dart Diff Core
Use this core inside a throwaway Flutter test after rendering each frame to
rawRgba bytes:
_VisualDiff _diff(
Uint8List a,
Uint8List b,
int width,
int height,
int from,
int to,
) {
var changedPixels = 0;
var totalDelta = 0;
var minX = width;
var minY = height;
var maxX = 0;
var maxY = 0;
var sumX = 0.0;
var sumY = 0.0;
for (var y = 0; y < height; y++) {
for (var x = 0; x < width; x++) {
final offset = (y * width + x) * 4;
final delta =
(a[offset] - b[offset]).abs() +
(a[offset + 1] - b[offset + 1]).abs() +
(a[offset + 2] - b[offset + 2]).abs() +
(a[offset + 3] - b[offset + 3]).abs();
if (delta < 36) continue;
changedPixels++;
totalDelta += delta;
minX = math.min(minX, x);
minY = math.min(minY, y);
maxX = math.max(maxX, x);
maxY = math.max(maxY, y);
sumX += x;
sumY += y;
}
}
final mean = changedPixels == 0 ? 0.0 : totalDelta / changedPixels;
return _VisualDiff(
from: from,
to: to,
changedPixels: changedPixels,
meanChangedDelta: mean,
score: changedPixels * mean,
minX: changedPixels == 0 ? 0 : minX,
minY: changedPixels == 0 ? 0 : minY,
width: changedPixels == 0 ? 0 : maxX - minX + 1,
height: changedPixels == 0 ? 0 : maxY - minY + 1,
cx: changedPixels == 0 ? 0 : sumX / changedPixels,
cy: changedPixels == 0 ? 0 : sumY / changedPixels,
);
}
Reporting Format
Keep reports factual and phase-addressable:
Top temporal diffs, 240-frame dance ensemble:
1. 59->60 p=0.2458-0.2500 score=36.9M box=... centroid=...
Read: f059.png, f060.png, diff_059_060.png
Diagnosis: whole dancer re-anchors horizontally at support handoff.
2. ...
After patch:
59->60 score 36.9M -> 24.3M; inspected diff shows no whole-body teleport,
remaining delta is pose/silhouette. New worst: ...
Never collapse this to "looks fixed" without the numbers and image inspection.
Cleanup
- Scratch tests should use disposable names such as
test/features/character/zzz_temporal_diff_test.dart.
- Scratch scripts can live under
tool/, but remove them before commit unless
the user explicitly asks to keep a maintained diagnostic.
- Diff PNGs belong in ignored build output.
- Commit only the production fix and focused regression tests.
See Also
character-motion-review-panel for expert scoring after the discontinuities
are measured and localized.
choreo-phrase-authoring when the fix should become labelled move data rather
than another anonymous key tweak.
1---2name: temporal-animation-diff3description: Analyze an animation frame-by-frame with golden-test-style temporal diffs. Use when motion looks jumpy, snappy, discontinuous, robotic, wobbly, or when a user asks to inspect animation "frame by frame", find "big diffs", "temporal diffs", "snaps", "jumps", or "where the animation changes too much between frames".4---56# Temporal Animation Diff78Finds animation discontinuities by rendering a dense deterministic frame9sequence, diffing each adjacent frame, and inspecting the highest-delta10transitions. This is for motion debugging, not for committed goldens.1112Use this before tuning by eye when a clip feels like it snaps, teleports, pops,13or has rubbery wobble. The output should name exact frame pairs and phases14(`p=...`) so the code fix can target the real transition.1516## Loop1718```mermaid19stateDiagram-v220 [*] --> RenderSequence21 RenderSequence --> RankDiffs: adjacent frame pixel deltas22 RankDiffs --> InspectWorst: before/after/diff PNGs23 InspectWorst --> Diagnose: map frame phase to channels, camera, pinning, layout24 Diagnose --> Patch25 Patch --> RerenderSameSequence26 RerenderSameSequence --> RankDiffs27 RankDiffs --> Regression: convert proven bug into a focused test28 Regression --> Cleanup29 Cleanup --> [*]30```3132## Workflow33341. **Render densely enough to catch snaps.** Use at least 120 frames per loop;35 use 240 when the user reports occasional jumps. Keep viewport, scale,36 camera, expression, backdrop, and timing identical across runs.37382. **Diff adjacent frames.** For each pair, compute changed pixels, mean changed39 delta, score (`changedPixels * meanDelta`), changed bounding box, and changed40 centroid. Sort descending by score.41423. **Write artifacts for the worst transitions.** Save:43 - `fNNN.png`44 - `fNNN+1.png`45 - `diff_NNN_NNN+1.png`4647 Put them under `build/character_frame_diffs/<label>/` or another ignored48 build directory. Do not commit them.49504. **Inspect before patching.** Read the worst before/after/diff images. Decide51 whether the delta is a true discontinuity or just a large legitimate pose52 change. True discontinuities usually show whole-body translation, camera53 jumps, support-foot re-anchors, expression swaps, z-order pops, or limb54 teleporting.55565. **Map phase to code.** Convert frame pair to normalized phase:57 `p0 = from / frames`, `p1 = to / frames`. Check channels/keyframes/contact58 spans/camera curves that cross that phase. For character clips, also compare59 scene-level transforms against painter-level output; a bug can live after60 `frameAt`.61626. **Rerender the exact same sequence after each fix.** Report the before/after63 scores for the same frame pair. Do not say a snap is fixed unless the same64 transition has been rerendered and inspected.65667. **Keep only durable tests.** Delete scratch diff tests/scripts before commit.67 If the bug was real, add a small regression test that asserts the measured68 failure mode directly, such as max visible center delta, no support re-anchor,69 monotonic camera movement, or bounded joint displacement.7071## Minimal Dart Diff Core7273Use this core inside a throwaway Flutter test after rendering each frame to74`rawRgba` bytes:7576```dart77_VisualDiff _diff(78 Uint8List a,79 Uint8List b,80 int width,81 int height,82 int from,83 int to,84) {85 var changedPixels = 0;86 var totalDelta = 0;87 var minX = width;88 var minY = height;89 var maxX = 0;90 var maxY = 0;91 var sumX = 0.0;92 var sumY = 0.0;9394 for (var y = 0; y < height; y++) {95 for (var x = 0; x < width; x++) {96 final offset = (y * width + x) * 4;97 final delta =98 (a[offset] - b[offset]).abs() +99 (a[offset + 1] - b[offset + 1]).abs() +100 (a[offset + 2] - b[offset + 2]).abs() +101 (a[offset + 3] - b[offset + 3]).abs();102 if (delta < 36) continue;103 changedPixels++;104 totalDelta += delta;105 minX = math.min(minX, x);106 minY = math.min(minY, y);107 maxX = math.max(maxX, x);108 maxY = math.max(maxY, y);109 sumX += x;110 sumY += y;111 }112 }113114 final mean = changedPixels == 0 ? 0.0 : totalDelta / changedPixels;115 return _VisualDiff(116 from: from,117 to: to,118 changedPixels: changedPixels,119 meanChangedDelta: mean,120 score: changedPixels * mean,121 minX: changedPixels == 0 ? 0 : minX,122 minY: changedPixels == 0 ? 0 : minY,123 width: changedPixels == 0 ? 0 : maxX - minX + 1,124 height: changedPixels == 0 ? 0 : maxY - minY + 1,125 cx: changedPixels == 0 ? 0 : sumX / changedPixels,126 cy: changedPixels == 0 ? 0 : sumY / changedPixels,127 );128}129```130131## Reporting Format132133Keep reports factual and phase-addressable:134135```text136Top temporal diffs, 240-frame dance ensemble:1371. 59->60 p=0.2458-0.2500 score=36.9M box=... centroid=...138 Read: f059.png, f060.png, diff_059_060.png139 Diagnosis: whole dancer re-anchors horizontally at support handoff.1402. ...141142After patch:14359->60 score 36.9M -> 24.3M; inspected diff shows no whole-body teleport,144remaining delta is pose/silhouette. New worst: ...145```146147Never collapse this to "looks fixed" without the numbers and image inspection.148149## Cleanup150151- Scratch tests should use disposable names such as152 `test/features/character/zzz_temporal_diff_test.dart`.153- Scratch scripts can live under `tool/`, but remove them before commit unless154 the user explicitly asks to keep a maintained diagnostic.155- Diff PNGs belong in ignored build output.156- Commit only the production fix and focused regression tests.157158## See Also159160- `character-motion-review-panel` for expert scoring after the discontinuities161 are measured and localized.162- `choreo-phrase-authoring` when the fix should become labelled move data rather163 than another anonymous key tweak.