Image sequences: a gap in the numbering holds the previous frame, as After Effects

After Effects 2026 shows the last existing file across a gap in a sequence's
numbering (case 860: files 1, 2, 4, 5 show frame 2 at frame 3; VERIFY
read-back). EffectCraft drew colour bars there. The media pool now holds the
previous file (Footage::sequence_file_held); colour bars remain only where no
file precedes the frame. Missing-frame reports on import are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
vangel76
2026-10-19 16:47:38 +02:00
co-authored by Claude Opus 5.5
parent 4c65271c72
commit 7850cbf45b
5 changed files with 24 additions and 14 deletions
+2 -2
View File
@@ -682,9 +682,9 @@ impl Inner {
match footage.kind {
FootageKind::Sequence if !footage.sequence.is_empty() => {
let i = Self::frame_index(footage.frame_rate, t, footage.sequence_frames(), footage.loop_count);
match footage.sequence_file(i) {
match footage.sequence_file_held(i) {
Some(path) => Loc { key: key(path, 0), media_t: None, float },
// A gap in the numbering: colour bars, as in After Effects.
// No file at or before the frame: colour bars.
None => Loc { key: key(PLACEHOLDER, ((footage.width as i64) << 32) | footage.height as i64), media_t: None, float },
}
}
+6 -8
View File
@@ -546,10 +546,11 @@ fn png_sequence_import() {
assert_eq!(*img, load_png(&first.with_file_name("seq_0004.png")));
}
/// A gap in a sequence's numbering shows colour bars, as in After Effects; in Force
/// Alphabetical Order the files play back to back (#297).
/// A gap in a sequence's numbering holds the previous frame, as After Effects 2026 does (files
/// 1, 2, 4, 5 show frame 2 at frame 3); in Force Alphabetical Order the files play back to back
/// (#297).
#[test]
fn sequence_gaps_show_placeholders() {
fn sequence_gaps_hold_the_previous_frame() {
let dir = fixtures().join(format!("gap-seq-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("mkdir");
// Frames 1, 2 and 4: frame 3 is missing. Each frame is a flat grey of its number.
@@ -567,11 +568,8 @@ fn sequence_gaps_show_placeholders() {
let at = |f: &Footage, n: i64| pool.frame_at(f, t_of(f.frame_rate, n)).expect("frame");
let level = |img: &Image| (img.get(0, 0)[0] * 255.0).round() as u32;
assert_eq!(level(&at(&f, 1)), 100);
// The placeholder: colour bars across the frame, starting with 75 % white.
let bars = at(&f, 2);
assert_eq!((bars.width, bars.height), (8, 4));
assert_eq!(bars.get(0, 0), [0.75, 0.75, 0.75, 1.0]);
assert_eq!(bars.get(7, 3), [0.0, 0.0, 0.75, 1.0], "blue on the right");
// The gap holds frame 2.
assert_eq!(level(&at(&f, 2)), 100);
assert_eq!(level(&at(&f, 3)), 200);
f.alphabetical = true;
f.sync_sequence_duration();
+12
View File
@@ -107,6 +107,18 @@ impl Footage {
self.sequence.get(k).filter(|p| frame_number(p) == Some(n)).map(String::as_str)
}
/// The file an image sequence shows at its frame `i`, holding the last existing file across a
/// gap in the numbering (After Effects 2026: files 1, 2, 4, 5 show frame 2 at frame 3).
/// `None` only when no file precedes the frame (or there are no files).
pub fn sequence_file_held(&self, i: i64) -> Option<&str> {
if let Some(p) = self.sequence_file(i) {
return Some(p);
}
let first = self.numbered()?;
let k = self.first_file_from(first.saturating_add(i));
self.sequence.get(k.checked_sub(1)?).map(String::as_str)
}
/// Set an image sequence's duration to its frames at its frame rate (after its files or
/// frame rate changed).
pub fn sync_sequence_duration(&mut self) {
+2 -2
View File
@@ -275,8 +275,8 @@ build real projects through these interfaces; they are worked examples of everyt
numbered run as one item (several picked frames: that range; a folder: each run in it;
`"sequence": false`: one still; dropped files, `"drag": true`, import as stills unless
`"sequence": true`, as in After Effects; `"alphabetical": true`: Force Alphabetical Order, the files
play one after another) at `frameRate` or 30 fps (Settings ▸ Import). Missing frames show
colour bars, as in After Effects. `file.interpretFootage {"items":[id], "frameRate":12,
play one after another) at `frameRate` or 30 fps (Settings ▸ Import). A gap in the numbering
holds the previous frame, as in After Effects. `file.interpretFootage {"items":[id], "frameRate":12,
"startTimecode":1001}` conforms them and sets Override Start (the item and layers that ran to its end get the new
length). See [footage.md](footage.md).
* **Colour of footage**: `file.interpretFootage {"items":[id], "preserveRgb":true}` is Interpret
+2 -2
View File
@@ -47,8 +47,8 @@ Stills of any format EffectCraft reads can form a sequence: PNG, JPEG, TIFF, Ope
WebP. Layered and vector documents (PSD, SVG, PDF) import one by one.
**Missing frames.** Where the numbering skips a number (`shot_0003.png` is missing between
`0002` and `0004`), the sequence shows a placeholder of colour bars for that frame, as After
Effects does, so every other frame stays at its number. Render or copy the missing files into the
`0002` and `0004`), the sequence holds the previous frame (`0002`) there, as After Effects 2026
does, so every other frame stays at its number. Render or copy the missing files into the
folder and choose File ▸ Reload Footage to fill the gaps.
Agents: `file.import {"paths": ["/shots/shot_0001.png"], "sequence": true, "frameRate": 24}`.