mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-07 02:51:24 +00:00
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:
1 parent
4c65271c72
commit
7850cbf45b
5 files changed
+24
-14
No files matched your search
@@ -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 },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
@@ -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}`.
|
||||
|
||||
Reference in new issue
Block a user