mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
Import: missing sequence frames show a placeholder, as in After Effects (#297)
After Effects shows a missing frame of an image sequence as a placeholder (colour bars) and has no Hold Previous Frame or Skip choice in Interpret Footage, so EffectCraft doesn't either. Interpret Footage loses its Missing Frames field, file.interpretFootage its missingFrames parameter, and Footage its missing_frames setting. A gap in a numbered sequence's numbering always shows colour bars. A sequence imported with Force Alphabetical Order still plays its files one after another (its numbering doesn't count), as in After Effects; that is now an internal Footage.alphabetical flag, set on import and kept by Reload. The Interpret Footage note lists the missing frames. Projects saved before this have no such field and open with the After Effects behaviour: a sequence with gaps shows placeholders where it used to close them up. Test: project sequence unit tests (placeholders, alphabetical order, projects saved without the flag), tests_sequence (Start Frame instead of Hold / Skip), sequence_gaps_show_placeholders (media) and the Interpret Footage fields in ui_drag_drop. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
50c9549529
commit
efd348335f
14 files changed
+108
-169
No files matched your search
@@ -260,11 +260,11 @@ impl Prober {
|
||||
let mut warning = None;
|
||||
// Settings ▸ Import ▸ Report Missing Frames (numbered sequences).
|
||||
if f.kind == FootageKind::Sequence
|
||||
&& f.missing_frames != effectcraft_project::MissingFrames::Skip
|
||||
&& !f.alphabetical
|
||||
&& self.report_missing_frames
|
||||
&& let Some(gaps) = crate::sequence::missing_report(&f.sequence)
|
||||
{
|
||||
warning = Some(format!("{name}: {gaps} (shown as placeholders; File ▸ Interpret Footage ▸ Missing Frames)"));
|
||||
warning = Some(format!("{name}: {gaps} (shown as placeholders)"));
|
||||
}
|
||||
// Settings ▸ Import ▸ Interpret Unlabeled Alpha As: images only (a 3D model renders its
|
||||
// own alpha, and audio and data have none).
|
||||
|
||||
@@ -553,8 +553,6 @@ pub(crate) struct Interpretation {
|
||||
fields: Option<effectcraft_project::FieldOrder>,
|
||||
profile: Option<Option<effectcraft_project::ColorSpace>>,
|
||||
linear: Option<bool>,
|
||||
/// Image sequences: Missing Frames.
|
||||
missing: Option<effectcraft_project::MissingFrames>,
|
||||
/// Image sequences: Start Frame (`Some(None)` = the first file's number).
|
||||
start: Option<Option<i64>>,
|
||||
}
|
||||
@@ -603,10 +601,6 @@ impl Interpretation {
|
||||
None => None,
|
||||
};
|
||||
it.linear = super::b_p(p, "linearLight");
|
||||
it.missing = match str_p(p, "missingFrames") {
|
||||
Some(m) => Some(effectcraft_project::MissingFrames::parse(m).ok_or_else(|| bad(cmd, format!("missingFrames: placeholder|hold|skip, not `{m}`")))?),
|
||||
None => None,
|
||||
};
|
||||
// Start Frame: a frame number, or "file" for the first file's.
|
||||
it.start = match p.get("startFrame") {
|
||||
None | Some(Value::Null) => None,
|
||||
@@ -624,7 +618,7 @@ impl Interpretation {
|
||||
pub(crate) fn apply(&self, f: &mut Footage, guessed: Option<(AlphaMode, [f32; 3])>) {
|
||||
let before = f.frame_rate;
|
||||
self.apply_settings(f, guessed);
|
||||
// A sequence's length is its frames (Missing Frames and Start Frame decide how many).
|
||||
// A sequence's length is its frames (Start Frame decides how many).
|
||||
if f.kind == FootageKind::Sequence && !f.sequence.is_empty() {
|
||||
f.sync_sequence_duration();
|
||||
return;
|
||||
@@ -677,15 +671,12 @@ impl Interpretation {
|
||||
if let Some(v) = self.linear {
|
||||
f.linear_light = v;
|
||||
}
|
||||
if f.kind == FootageKind::Sequence {
|
||||
if let Some(m) = self.missing {
|
||||
f.missing_frames = m;
|
||||
}
|
||||
if let Some(start) = self.start {
|
||||
// The first file's own number needn't be stored (it follows the files).
|
||||
let first = f.sequence.first().and_then(|p| effectcraft_project::sequence::frame_number(p));
|
||||
f.start_frame = start.filter(|n| Some(*n) != first);
|
||||
}
|
||||
if f.kind == FootageKind::Sequence
|
||||
&& let Some(start) = self.start
|
||||
{
|
||||
// The first file's own number needn't be stored (it follows the files).
|
||||
let first = f.sequence.first().and_then(|p| effectcraft_project::sequence::frame_number(p));
|
||||
f.start_frame = start.filter(|n| Some(*n) != first);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -763,7 +754,7 @@ fn interpret(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Some(ItemKind::Footage(f)) => Some(json!({
|
||||
"item": i.0, "alpha": format!("{:?}", f.alpha), "matteColor": f.premul_color, "invertAlpha": f.invert_alpha,
|
||||
"fields": f.fields.label(), "pixelAspect": f.pixel_aspect, "loop": f.loop_count, "frameRate": f.frame_rate.as_f64(),
|
||||
"linearLight": f.linear_light, "missingFrames": f.missing_frames.id(), "startFrame": f.first_frame_number(),
|
||||
"linearLight": f.linear_light, "startFrame": f.first_frame_number(),
|
||||
"frames": f.sequence_frames(),
|
||||
})),
|
||||
_ => None,
|
||||
@@ -900,7 +891,7 @@ fn reload(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if nf.kind == FootageKind::Sequence && f.kind == FootageKind::Sequence {
|
||||
nf.frame_rate = f.frame_rate;
|
||||
nf.native_rate = f.native_rate;
|
||||
nf.missing_frames = f.missing_frames;
|
||||
nf.alphabetical = f.alphabetical;
|
||||
nf.start_frame = f.start_frame;
|
||||
nf.sync_sequence_duration();
|
||||
}
|
||||
@@ -1003,7 +994,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Main...",
|
||||
["File", "Interpret Footage"],
|
||||
Some("Cmd+Alt+G"),
|
||||
"{items?, frameRate?: fps|\"file\", alpha?: straight|premultiplied|ignore|guess, guessAlpha?, matteColor?, invertAlpha?, loop?, pixelAspect?, fields?: off|upper|lower, colorProfile?: srgb|rec709|rec2020|p3|auto, linearLight?, missingFrames?: placeholder|hold|skip (image sequences: what a gap in the numbering shows), startFrame?: number|\"file\" (image sequences: the frame number at the footage's first frame)}",
|
||||
"{items?, frameRate?: fps|\"file\", alpha?: straight|premultiplied|ignore|guess, guessAlpha?, matteColor?, invertAlpha?, loop?, pixelAspect?, fields?: off|upper|lower, colorProfile?: srgb|rec709|rec2020|p3|auto, linearLight?, startFrame?: number|\"file\" (image sequences: the frame number at the footage's first frame)}",
|
||||
has_footage_selection,
|
||||
interpret
|
||||
),
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
//! picking every frame of a sequence imports it there too.
|
||||
|
||||
use effectcraft_project::sequence::{frame_number, missing_frame_ranges, sequence_name, split_frame_number};
|
||||
use effectcraft_project::{Footage, FootageKind, MissingFrames};
|
||||
use effectcraft_project::{Footage, FootageKind};
|
||||
use effectcraft_time::FrameRate;
|
||||
|
||||
use crate::{Importer, Services};
|
||||
@@ -186,8 +186,8 @@ pub fn sequences(services: &dyn Services, paths: &[String], alphabetical: bool)
|
||||
}
|
||||
|
||||
/// Probe what a [`Source`] makes. A sequence is its first file's footage with every file, at
|
||||
/// `rate`, showing gaps in its numbering as placeholders (or closing them up when
|
||||
/// alphabetical).
|
||||
/// `rate`, showing gaps in its numbering as placeholders, as in After Effects (in Force
|
||||
/// Alphabetical Order the files play one after another).
|
||||
pub fn probe(importer: &dyn Importer, src: &Source, rate: FrameRate) -> Result<Footage, String> {
|
||||
let Source::Sequence { files, alphabetical } = src else { return importer.probe(src.path()) };
|
||||
let mut f = importer.probe(src.path())?;
|
||||
@@ -196,7 +196,7 @@ pub fn probe(importer: &dyn Importer, src: &Source, rate: FrameRate) -> Result<F
|
||||
}
|
||||
f.kind = FootageKind::Sequence;
|
||||
f.sequence = files.clone();
|
||||
f.missing_frames = if *alphabetical { MissingFrames::Skip } else { MissingFrames::Placeholder };
|
||||
f.alphabetical = *alphabetical;
|
||||
f.start_frame = None;
|
||||
f.frame_rate = rate;
|
||||
f.native_rate = None;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Image sequences (#297): File ▸ Import with the "<format> Sequence" option, picked ranges,
|
||||
//! folders, Force Alphabetical Order, the import frame rate, Replace Footage, and Interpret
|
||||
//! Footage ▸ Missing Frames / Start Frame.
|
||||
//! folders, Force Alphabetical Order, the import frame rate, Replace Footage, missing frames
|
||||
//! (placeholders, as in After Effects) and Interpret Footage ▸ Start Frame.
|
||||
|
||||
use effectcraft_project::{Footage, FootageKind, ItemId, ItemKind, MissingFrames};
|
||||
use effectcraft_project::{Footage, FootageKind, ItemId, ItemKind};
|
||||
use effectcraft_time::{FrameRate, Tick};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -58,7 +58,7 @@ fn one_picked_numbered_file_imports_its_run_as_one_sequence() {
|
||||
assert_eq!(r["items"].as_array().unwrap().len(), 1, "{r}");
|
||||
let (name, f) = footage(&s, &r["items"][0]);
|
||||
assert_eq!(name, "shot_[0001-0005].png");
|
||||
assert_eq!((f.kind, f.sequence.len(), f.missing_frames), (FootageKind::Sequence, 4, MissingFrames::Placeholder));
|
||||
assert_eq!((f.kind, f.sequence.len(), f.alphabetical), (FootageKind::Sequence, 4, false));
|
||||
assert_eq!(f.path, at(&dir, "shot_0001.png"));
|
||||
// Frames 1–5 at Settings ▸ Import ▸ Sequence Footage (30 fps); frame 3 is a placeholder.
|
||||
assert_eq!(f.duration, FrameRate::FPS_30.tick_of(5));
|
||||
@@ -118,11 +118,13 @@ fn force_alphabetical_order_takes_every_image_of_the_type() {
|
||||
let (name, f) = footage(&s, &r["items"][0]);
|
||||
let files: Vec<String> = f.sequence.iter().map(|p| crate::sequence::file_name(p)).collect();
|
||||
assert_eq!(files, ["other_7.png", "take_a.png", "take_b.png"]);
|
||||
assert_eq!((name.as_str(), f.missing_frames), ("other_7.png", MissingFrames::Skip));
|
||||
// Its numbering doesn't count: the files play one after another.
|
||||
assert_eq!((name.as_str(), f.alphabetical, f.sequence_frames()), ("other_7.png", true, 3));
|
||||
assert!(r["warnings"].as_array().is_none_or(Vec::is_empty), "{r}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interpret_footage_sets_missing_frames_and_start_frame() {
|
||||
fn interpret_footage_sets_start_frame() {
|
||||
let dir = folder(&RUN);
|
||||
let mut s = session();
|
||||
let item = s.execute("file.import", json!({"paths": [at(&dir, "shot_0001.png")]})).unwrap()["items"][0].clone();
|
||||
@@ -130,15 +132,11 @@ fn interpret_footage_sets_missing_frames_and_start_frame() {
|
||||
let layer = s.execute("layer.addItem", json!({"item": item})).unwrap()["layer"].as_u64().unwrap();
|
||||
let out = |s: &Session| s.active_comp().unwrap().layer(effectcraft_project::LayerId(layer)).unwrap().out_point;
|
||||
assert_eq!(out(&s), FrameRate::FPS_30.tick_of(5));
|
||||
// Hold: the gap shows frame 2; the length stays 5 frames.
|
||||
let r = s.execute("file.interpretFootage", json!({"items": [item], "missingFrames": "hold"})).unwrap();
|
||||
assert_eq!((r["items"][0]["missingFrames"].as_str(), r["items"][0]["frames"].as_i64()), (Some("hold"), Some(5)));
|
||||
assert_eq!(footage(&s, &item).1.sequence_file(2).map(crate::sequence::file_name).as_deref(), Some("shot_0002.png"));
|
||||
// Skip: 4 frames, and the layer that ran to the end still does.
|
||||
s.execute("file.interpretFootage", json!({"items": [item], "missingFrames": "skip"})).unwrap();
|
||||
// Start Frame 2 trims frame 1: 4 frames, and the layer that ran to the end still does.
|
||||
s.execute("file.interpretFootage", json!({"items": [item], "startFrame": 2})).unwrap();
|
||||
assert_eq!((footage(&s, &item).1.duration, out(&s)), (FrameRate::FPS_30.tick_of(4), FrameRate::FPS_30.tick_of(4)));
|
||||
// Start Frame 0 with placeholders: frame 0 is missing, 6 frames in all.
|
||||
let r = s.execute("file.interpretFootage", json!({"items": [item], "missingFrames": "placeholder", "startFrame": 0})).unwrap();
|
||||
// Start Frame 0: frame 0 is missing (a placeholder), 6 frames in all.
|
||||
let r = s.execute("file.interpretFootage", json!({"items": [item], "startFrame": 0})).unwrap();
|
||||
assert_eq!((r["items"][0]["startFrame"].as_i64(), r["items"][0]["frames"].as_i64()), (Some(0), Some(6)));
|
||||
assert_eq!(footage(&s, &item).1.sequence_file(0), None);
|
||||
// The first file's own number isn't stored; "file" goes back to it.
|
||||
@@ -150,7 +148,7 @@ fn interpret_footage_sets_missing_frames_and_start_frame() {
|
||||
// The frame rate re-times the frames.
|
||||
s.execute("file.interpretFootage", json!({"items": [item], "frameRate": 10})).unwrap();
|
||||
assert_eq!(footage(&s, &item).1.duration, Tick::from_seconds_f64(0.5));
|
||||
for bad in [json!({"missingFrames": "blank"}), json!({"startFrame": "x"}), json!({"startFrame": 1e300})] {
|
||||
for bad in [json!({"startFrame": "x"}), json!({"startFrame": 1e300})] {
|
||||
let mut p = bad.clone();
|
||||
p["items"] = json!([item]);
|
||||
assert!(s.execute("file.interpretFootage", p).is_err(), "{bad}");
|
||||
@@ -170,10 +168,10 @@ fn replace_footage_and_reload_keep_sequences() {
|
||||
let (name, f) = footage(&s, &item);
|
||||
assert_eq!((name.as_str(), f.sequence.len()), ("shot_[0001-0005].png", 4));
|
||||
// Reload finds a new frame and keeps the interpretation.
|
||||
s.execute("file.interpretFootage", json!({"items": [item], "frameRate": 12, "missingFrames": "hold"})).unwrap();
|
||||
s.execute("file.interpretFootage", json!({"items": [item], "frameRate": 12, "startFrame": 2})).unwrap();
|
||||
std::fs::write(at(&dir, "shot_0003.png"), b"").unwrap();
|
||||
s.execute("file.reloadFootage", json!({"items": [item]})).unwrap();
|
||||
let (_, f) = footage(&s, &item);
|
||||
assert_eq!((f.sequence.len(), f.frame_rate, f.missing_frames), (5, FrameRate::from_f64(12.0), MissingFrames::Hold));
|
||||
assert_eq!(f.duration, FrameRate::from_f64(12.0).tick_of(5));
|
||||
assert_eq!((f.sequence.len(), f.frame_rate, f.start_frame), (5, FrameRate::from_f64(12.0), Some(2)));
|
||||
assert_eq!(f.duration, FrameRate::from_f64(12.0).tick_of(4));
|
||||
}
|
||||
@@ -523,7 +523,7 @@ impl Inner {
|
||||
let i = Self::frame_index(footage.frame_rate, t, footage.sequence_frames(), footage.loop_count);
|
||||
match footage.sequence_file(i) {
|
||||
Some(path) => Loc { key: key(path, 0), media_t: None },
|
||||
// A gap in the numbering (Missing Frames ▸ Show Placeholder).
|
||||
// A gap in the numbering: colour bars, as in After Effects.
|
||||
None => Loc { key: key(PLACEHOLDER, ((footage.width as i64) << 32) | footage.height as i64), media_t: None },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use effectcraft_media::{MediaPool, probe};
|
||||
use effectcraft_project::{AlphaMode, Footage, FootageKind, ItemId, MissingFrames};
|
||||
use effectcraft_project::{AlphaMode, Footage, FootageKind, ItemId};
|
||||
use effectcraft_raster::Image;
|
||||
use effectcraft_render::FootageSource;
|
||||
use effectcraft_time::{FrameRate, TICKS_PER_SECOND, Tick};
|
||||
@@ -379,10 +379,10 @@ 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 (Missing Frames ▸ Show Placeholder), the
|
||||
/// frame before it (Hold) or nothing at all (Skip: the files play back to back) (#297).
|
||||
/// 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).
|
||||
#[test]
|
||||
fn sequence_gaps_show_placeholders_hold_or_close_up() {
|
||||
fn sequence_gaps_show_placeholders() {
|
||||
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.
|
||||
@@ -393,8 +393,7 @@ fn sequence_gaps_show_placeholders_hold_or_close_up() {
|
||||
grey(n).save(&p).expect("write png");
|
||||
files.push(p.to_string_lossy().to_string());
|
||||
}
|
||||
let mut f =
|
||||
Footage { kind: FootageKind::Sequence, sequence: files, missing_frames: MissingFrames::Placeholder, ..probe(dir.join("gap_001.png")).expect("probe") };
|
||||
let mut f = Footage { kind: FootageKind::Sequence, sequence: files, ..probe(dir.join("gap_001.png")).expect("probe") };
|
||||
f.sync_sequence_duration();
|
||||
assert_eq!(f.duration, f.frame_rate.tick_of(4), "frames 1-4");
|
||||
let pool = MediaPool::new();
|
||||
@@ -407,9 +406,7 @@ fn sequence_gaps_show_placeholders_hold_or_close_up() {
|
||||
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");
|
||||
assert_eq!(level(&at(&f, 3)), 200);
|
||||
f.missing_frames = MissingFrames::Hold;
|
||||
assert_eq!(level(&at(&f, 2)), 100, "holds frame 2");
|
||||
f.missing_frames = MissingFrames::Skip;
|
||||
f.alphabetical = true;
|
||||
f.sync_sequence_duration();
|
||||
assert_eq!(f.duration, f.frame_rate.tick_of(3));
|
||||
assert_eq!(level(&at(&f, 2)), 200, "frame 4 follows frame 2");
|
||||
|
||||
@@ -22,7 +22,6 @@ pub use effectcraft_color::ColorSpace;
|
||||
use effectcraft_color::{BlendMode, Label};
|
||||
use effectcraft_time::{FrameRate, Tick};
|
||||
pub use props::{Expression, FeatherFalloff, GroupKind, MaskMode, MaskMotionBlur, Node, ParamUi, PropGroup, Property, Uid, parse_path};
|
||||
pub use sequence::MissingFrames;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub use effectcraft_keyframe as keyframe;
|
||||
@@ -965,9 +964,11 @@ pub struct Footage {
|
||||
/// PDF / Illustrator footage: the page shown (0-based; File ▸ Import ▸ Page).
|
||||
#[serde(default, skip_serializing_if = "is_default")]
|
||||
pub page: u32,
|
||||
/// Image sequences: Interpret Footage ▸ Missing Frames ([`sequence`]).
|
||||
#[serde(default, skip_serializing_if = "is_default")]
|
||||
pub missing_frames: MissingFrames,
|
||||
/// Image sequences imported with Force Alphabetical Order: the files play one after another
|
||||
/// and their numbering doesn't count (otherwise a gap in it shows a placeholder, as in After
|
||||
/// Effects; [`sequence`]).
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub alphabetical: bool,
|
||||
/// Image sequences: Interpret Footage ▸ Start Frame, the frame number at the footage's first
|
||||
/// frame (`None` = the first file's).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
@@ -1004,7 +1005,7 @@ impl Default for Footage {
|
||||
data: None,
|
||||
layer: None,
|
||||
page: 0,
|
||||
missing_frames: MissingFrames::Skip,
|
||||
alphabetical: false,
|
||||
start_frame: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,9 @@
|
||||
//!
|
||||
//! A sequence's files are kept in frame-number order ([`Footage::sequence`]). How its frames map
|
||||
//! to the files follows Interpret Footage: **Start Frame** ([`Footage::start_frame`]) is the
|
||||
//! frame number shown at the footage's first frame, and **Missing Frames**
|
||||
//! ([`Footage::missing_frames`]) says what a gap in the numbering shows.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
//! frame number shown at the footage's first frame. A gap in the numbering shows a placeholder
|
||||
//! (colour bars), as in After Effects, except in a sequence imported with Force Alphabetical
|
||||
//! Order ([`Footage::alphabetical`]), whose files play one after another.
|
||||
|
||||
use crate::{Footage, FootageKind};
|
||||
|
||||
@@ -14,45 +13,6 @@ use crate::{Footage, FootageKind};
|
||||
/// naming accident, such as dates in file names, not footage).
|
||||
pub const MAX_SEQUENCE_FRAMES: i64 = 10_000_000;
|
||||
|
||||
/// Interpret Footage ▸ Missing Frames: what an image sequence shows where its numbering skips a
|
||||
/// number.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum MissingFrames {
|
||||
/// The files play one after another and the gaps close up (and a sequence imported with
|
||||
/// Force Alphabetical Order, whose numbering doesn't count).
|
||||
#[default]
|
||||
Skip,
|
||||
/// A missing frame shows colour bars, as in After Effects.
|
||||
Placeholder,
|
||||
/// A missing frame shows the frame before it.
|
||||
Hold,
|
||||
}
|
||||
|
||||
impl MissingFrames {
|
||||
pub const ALL: [MissingFrames; 3] = [MissingFrames::Placeholder, MissingFrames::Hold, MissingFrames::Skip];
|
||||
|
||||
/// The command parameter value.
|
||||
pub fn id(self) -> &'static str {
|
||||
match self {
|
||||
MissingFrames::Skip => "skip",
|
||||
MissingFrames::Placeholder => "placeholder",
|
||||
MissingFrames::Hold => "hold",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
MissingFrames::Skip => "Skip (close the gaps)",
|
||||
MissingFrames::Placeholder => "Show Placeholder (color bars)",
|
||||
MissingFrames::Hold => "Hold Previous Frame",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(s: &str) -> Option<MissingFrames> {
|
||||
MissingFrames::ALL.into_iter().find(|m| m.id().eq_ignore_ascii_case(s.trim()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Split a file stem into (prefix, frame number digits): `shot_0012` → (`shot_`, `0012`).
|
||||
/// `None` when it doesn't end in digits.
|
||||
pub fn split_frame_number(stem: &str) -> Option<(&str, &str)> {
|
||||
@@ -108,9 +68,9 @@ impl Footage {
|
||||
self.sequence.get(i).and_then(|p| frame_number(p))
|
||||
}
|
||||
|
||||
/// The numbering is used: Placeholder / Hold and a numbered first file.
|
||||
/// The numbering is used: a numbered first file, not in Force Alphabetical Order.
|
||||
fn numbered(&self) -> Option<i64> {
|
||||
(self.missing_frames != MissingFrames::Skip).then(|| self.file_number(0)).flatten()
|
||||
(!self.alphabetical).then(|| self.file_number(0)).flatten()
|
||||
}
|
||||
|
||||
/// Index of the first file numbered `n` or later.
|
||||
@@ -124,9 +84,9 @@ impl Footage {
|
||||
self.start_frame.or_else(|| self.file_number(0)).unwrap_or(0)
|
||||
}
|
||||
|
||||
/// How many frames an image sequence has: the span of its numbering from Start Frame (with
|
||||
/// Placeholder / Hold), or its files from Start Frame on (Skip). At least 1; 0 for other
|
||||
/// footage.
|
||||
/// How many frames an image sequence has: the span of its numbering from Start Frame, or its
|
||||
/// files from Start Frame on (Force Alphabetical Order, or unnumbered files). At least 1; 0
|
||||
/// for other footage.
|
||||
pub fn sequence_frames(&self) -> i64 {
|
||||
if !self.is_file_sequence() {
|
||||
return 0;
|
||||
@@ -158,15 +118,11 @@ impl Footage {
|
||||
};
|
||||
let n = self.start_frame.unwrap_or(first).saturating_add(i);
|
||||
let k = self.first_file_from(n);
|
||||
match self.sequence.get(k) {
|
||||
Some(p) if frame_number(p) == Some(n) => Some(p),
|
||||
_ if self.missing_frames == MissingFrames::Hold => self.sequence.get(k.saturating_sub(1)).map(String::as_str),
|
||||
_ => None,
|
||||
}
|
||||
self.sequence.get(k).filter(|p| frame_number(p) == Some(n)).map(String::as_str)
|
||||
}
|
||||
|
||||
/// Set an image sequence's duration to its frames at its frame rate (after its files,
|
||||
/// frame rate, Missing Frames or Start Frame changed).
|
||||
/// frame rate or Start Frame changed).
|
||||
pub fn sync_sequence_duration(&mut self) {
|
||||
if self.is_file_sequence() {
|
||||
self.duration = self.frame_rate.tick_of(self.sequence_frames());
|
||||
@@ -178,13 +134,8 @@ impl Footage {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn seq(numbers: &[i64], missing: MissingFrames) -> Footage {
|
||||
Footage {
|
||||
kind: FootageKind::Sequence,
|
||||
sequence: numbers.iter().map(|n| format!("C:\\shots\\a_{n:04}.png")).collect(),
|
||||
missing_frames: missing,
|
||||
..Default::default()
|
||||
}
|
||||
fn seq(numbers: &[i64]) -> Footage {
|
||||
Footage { kind: FootageKind::Sequence, sequence: numbers.iter().map(|n| format!("C:\\shots\\a_{n:04}.png")).collect(), ..Default::default() }
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -198,53 +149,62 @@ mod tests {
|
||||
let files: Vec<String> = ["/s/fx_0998.png", "/s/fx_1002.png"].map(String::from).to_vec();
|
||||
assert_eq!(sequence_name(&files).as_deref(), Some("fx_[0998-1002].png"));
|
||||
assert_eq!(sequence_name(&["/s/a.png".to_string()]), None);
|
||||
assert_eq!(MissingFrames::parse("Hold"), Some(MissingFrames::Hold));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_frames_map_frames_to_files() {
|
||||
// Files 1, 2, 5: frames 3 and 4 are missing.
|
||||
let f = seq(&[1, 2, 5], MissingFrames::Placeholder);
|
||||
// Files 1, 2, 5: frames 3 and 4 are missing and show placeholders.
|
||||
let f = seq(&[1, 2, 5]);
|
||||
assert_eq!(f.sequence_frames(), 5);
|
||||
let at = |f: &Footage, i| f.sequence_file(i).map(|p| frame_number(p).unwrap_or(-1));
|
||||
assert_eq!((0..5).map(|i| at(&f, i)).collect::<Vec<_>>(), [Some(1), Some(2), None, None, Some(5)]);
|
||||
let h = seq(&[1, 2, 5], MissingFrames::Hold);
|
||||
assert_eq!((0..5).map(|i| at(&h, i)).collect::<Vec<_>>(), [Some(1), Some(2), Some(2), Some(2), Some(5)]);
|
||||
let s = seq(&[1, 2, 5], MissingFrames::Skip);
|
||||
// Force Alphabetical Order: the files play one after another.
|
||||
let s = Footage { alphabetical: true, ..seq(&[1, 2, 5]) };
|
||||
assert_eq!(s.sequence_frames(), 3);
|
||||
assert_eq!((0..3).map(|i| at(&s, i)).collect::<Vec<_>>(), [Some(1), Some(2), Some(5)]);
|
||||
assert_eq!(missing_frame_ranges(&f.sequence), [(3, 4)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn projects_saved_without_the_flag_show_placeholders() {
|
||||
// Projects saved before #297 have no `alphabetical`: their gaps show placeholders, as in
|
||||
// After Effects; only Force Alphabetical Order is saved.
|
||||
let f = seq(&[1, 2, 5]);
|
||||
let json = serde_json::to_value(&f).unwrap();
|
||||
assert!(json.get("alphabetical").is_none(), "{json}");
|
||||
let back: Footage = serde_json::from_value(json).unwrap();
|
||||
assert_eq!((back.alphabetical, back.sequence_frames()), (false, 5));
|
||||
let alpha = serde_json::to_value(Footage { alphabetical: true, ..seq(&[1, 2, 5]) }).unwrap();
|
||||
assert_eq!(serde_json::from_value::<Footage>(alpha).unwrap().sequence_frames(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_frame_trims_or_pads_the_head() {
|
||||
let at = |f: &Footage, i| f.sequence_file(i).map(|p| frame_number(p).unwrap_or(-1));
|
||||
let mut f = seq(&[1001, 1002, 1003], MissingFrames::Placeholder);
|
||||
let mut f = seq(&[1001, 1002, 1003]);
|
||||
assert_eq!(f.first_frame_number(), 1001);
|
||||
f.start_frame = Some(1002);
|
||||
assert_eq!((f.sequence_frames(), at(&f, 0), at(&f, 1)), (2, Some(1002), Some(1003)));
|
||||
f.start_frame = Some(999);
|
||||
assert_eq!((f.sequence_frames(), at(&f, 0), at(&f, 2)), (5, None, Some(1001)));
|
||||
f.missing_frames = MissingFrames::Hold;
|
||||
assert_eq!(at(&f, 0), Some(1001), "nothing before the first file: hold it");
|
||||
f.missing_frames = MissingFrames::Skip;
|
||||
f.alphabetical = true;
|
||||
f.start_frame = Some(1003);
|
||||
assert_eq!((f.sequence_frames(), at(&f, 0)), (1, Some(1003)));
|
||||
// Hostile values never panic and stay bounded.
|
||||
f.missing_frames = MissingFrames::Placeholder;
|
||||
f.alphabetical = false;
|
||||
f.start_frame = Some(i64::MIN);
|
||||
assert_eq!(f.sequence_frames(), MAX_SEQUENCE_FRAMES);
|
||||
assert_eq!(f.sequence_file(i64::MAX), None);
|
||||
f.start_frame = Some(i64::MAX);
|
||||
assert_eq!(f.sequence_frames(), 1);
|
||||
f.missing_frames = MissingFrames::Skip;
|
||||
f.alphabetical = true;
|
||||
assert_eq!(f.sequence_file(-1), None);
|
||||
assert!(f.sequence_file(5).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_follows_frames_and_rate() {
|
||||
let mut f = seq(&[1, 2, 5], MissingFrames::Placeholder);
|
||||
let mut f = seq(&[1, 2, 5]);
|
||||
f.frame_rate = effectcraft_time::FrameRate::from_f64(10.0);
|
||||
f.sync_sequence_duration();
|
||||
assert!((f.duration.seconds() - 0.5).abs() < 1e-9);
|
||||
|
||||
@@ -189,7 +189,7 @@ fn sequence_fields(app: &EffectcraftApp, runs: &[Vec<String>]) -> Vec<Field> {
|
||||
Field::bool("sequence", &format!("{ext} Sequence"), true),
|
||||
Field::bool("alphabetical", "Force alphabetical order", false),
|
||||
Field::num("frameRate", "Frame rate (fps)", app.session.prefs.import.sequence_fps),
|
||||
Field::note("after", "Change the frame rate, alpha, missing frames and start frame later with File ▸ Interpret Footage."),
|
||||
Field::note("after", "Change the frame rate, alpha and start frame later with File ▸ Interpret Footage."),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -393,22 +393,7 @@ pub fn open_form(app: &mut EffectcraftApp, id: &str, p: &Value) -> bool {
|
||||
],
|
||||
),
|
||||
"file.interpretFootage" | "file.interpretProxy"
|
||||
if !has(
|
||||
p,
|
||||
&[
|
||||
"frameRate",
|
||||
"alpha",
|
||||
"loop",
|
||||
"pixelAspect",
|
||||
"colorProfile",
|
||||
"fields",
|
||||
"invertAlpha",
|
||||
"matteColor",
|
||||
"linearLight",
|
||||
"missingFrames",
|
||||
"startFrame",
|
||||
],
|
||||
) =>
|
||||
if !has(p, &["frameRate", "alpha", "loop", "pixelAspect", "colorProfile", "fields", "invertAlpha", "matteColor", "linearLight", "startFrame"]) =>
|
||||
{
|
||||
let proxy = id == "file.interpretProxy";
|
||||
let f = s.state.project_selection.first().and_then(|i| s.project.item(*i)).and_then(|it| match (&it.kind, &it.proxy) {
|
||||
@@ -462,18 +447,19 @@ pub fn open_form(app: &mut EffectcraftApp, id: &str, p: &Value) -> bool {
|
||||
),
|
||||
Field::bool("linearLight", "Interpret As Linear Light", f.linear_light),
|
||||
];
|
||||
// Image sequences: what a gap in the numbering shows, and the first frame's number.
|
||||
// Image sequences: their frames (missing ones show colour bars, as in After Effects)
|
||||
// and the first frame's number.
|
||||
if f.kind == effectcraft_engine::project::FootageKind::Sequence {
|
||||
use effectcraft_engine::project::MissingFrames;
|
||||
let at = fields.iter().position(|x| x.key == "frameRate").map_or(fields.len(), |i| i + 1);
|
||||
let gaps = effectcraft_engine::sequence::missing_report(&f.sequence).unwrap_or_else(|| "no missing frames".into());
|
||||
let options: Vec<(&str, Value)> = MissingFrames::ALL.iter().map(|m| (m.label(), json!(m.id()))).collect();
|
||||
let sel = MissingFrames::ALL.iter().position(|m| *m == f.missing_frames).unwrap_or(0);
|
||||
let gaps = match effectcraft_engine::sequence::missing_report(&f.sequence) {
|
||||
_ if f.alphabetical => "files in alphabetical order".into(),
|
||||
Some(gaps) => format!("{gaps} shown as color bars"),
|
||||
None => "no missing frames".into(),
|
||||
};
|
||||
fields.splice(
|
||||
at..at,
|
||||
[
|
||||
Field::note("sequenceInfo", &format!("Image sequence: {} files, {} frames; {gaps}.", f.sequence.len(), f.sequence_frames())),
|
||||
Field::choice("missingFrames", "Missing Frames", &options, sel),
|
||||
Field::num("startFrame", "Start Frame", f.first_frame_number() as f64),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -340,7 +340,8 @@ fn import_dialog_offers_the_image_sequence_option() {
|
||||
}
|
||||
|
||||
/// #297: every frame of a sequence dropped on the window arrives as one image sequence (it used
|
||||
/// to make one item per frame), and Interpret Footage shows its Missing Frames and Start Frame.
|
||||
/// to make one item per frame), and Interpret Footage shows its Start Frame (missing frames
|
||||
/// always show colour bars, as in After Effects: there is no Missing Frames choice).
|
||||
#[test]
|
||||
fn dropped_frames_import_as_one_sequence_with_sequence_interpretation() {
|
||||
let (mut h, _) = harness();
|
||||
@@ -358,8 +359,9 @@ fn dropped_frames_import_as_one_sequence_with_sequence_interpretation() {
|
||||
let ctx = h.ctx.clone();
|
||||
effectcraft_ui_egui::menus::invoke(h.state_mut(), &ctx, "file.interpretFootage", json!({})).unwrap();
|
||||
h.run_steps(2);
|
||||
for id in ["form.field.missingFrames", "form.field.startFrame", "form.field.frameRate", "form.field.alpha"] {
|
||||
for id in ["form.field.sequenceInfo", "form.field.startFrame", "form.field.frameRate", "form.field.alpha"] {
|
||||
assert!(h.state().auto.find(id).is_some(), "{id}");
|
||||
}
|
||||
assert!(h.state().auto.find("form.field.missingFrames").is_none());
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
+5
-4
@@ -207,10 +207,11 @@ build real projects through these interfaces; they are worked examples of everyt
|
||||
`{"colors":[[0,"#0080ff"],[1,[1,1,0]]],"opacities":[[0,1],[1,0.5]]}`.
|
||||
* **Image sequences**: `file.import {"paths":["/r/shot_0001.png"]}` imports the file's whole
|
||||
numbered run as one item (several picked frames: that range; a folder: each run in it;
|
||||
`"sequence": false`: one still; `"alphabetical": true`: Force Alphabetical Order) at
|
||||
`frameRate` or 30 fps (Settings ▸ Import). `file.interpretFootage {"items":[id],
|
||||
"frameRate":12, "startFrame":1001}` conforms them (the
|
||||
item and layers that ran to its end get the new length). See [footage.md](footage.md).
|
||||
`"sequence": false`: one still; `"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,
|
||||
"startFrame":1001}` conforms them (the item and layers that ran to its end get the new
|
||||
length). See [footage.md](footage.md).
|
||||
* **Multi-layer OpenEXR**: `layer.channels {layer}` lists a footage layer's EXR layers and
|
||||
channels (`{"layers":[{"name":"diffuse","rgba":["diffuse.R","diffuse.G","diffuse.B",""]}]}`);
|
||||
EXtractoR shows them when its `red` / `green` / `blue` / `alpha` are set to those names
|
||||
|
||||
+9
-6
@@ -27,7 +27,7 @@ dialog shows the run it found (and any missing frames) with these options:
|
||||
| Option | What it does |
|
||||
|---|---|
|
||||
| **PNG Sequence** (EXR, TIFF, JPG… after the file type) | On: the whole run imports as one sequence. Off: just the picked file imports, as a still. |
|
||||
| **Force alphabetical order** | Imports every image of that type in the folder in name order, whatever the numbering. Use it for files that aren't numbered consistently. |
|
||||
| **Force alphabetical order** | Imports every image of that type in the folder in name order, whatever the numbering, and plays the files one after another (gaps in the numbering don't count). Use it for files that aren't numbered consistently. |
|
||||
| **Frame rate (fps)** | The sequence's frame rate. The default comes from Settings ▸ Import ▸ Sequence Footage (30 fps). |
|
||||
|
||||
Other ways in:
|
||||
@@ -42,6 +42,11 @@ Other ways in:
|
||||
Stills of any format EffectCraft reads can form a sequence: PNG, JPEG, TIFF, OpenEXR, BMP and
|
||||
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
|
||||
folder and choose File ▸ Reload Footage to fill the gaps.
|
||||
|
||||
Agents: `file.import {"paths": ["/shots/shot_0001.png"], "sequence": true, "frameRate": 24}`.
|
||||
`sequence` is on by default; `"sequence": false` imports the file as a still, and
|
||||
`"alphabetical": true` is Force Alphabetical Order.
|
||||
@@ -56,19 +61,17 @@ used, and layers that ran to the end of the footage keep doing so.
|
||||
|---|---|
|
||||
| **Alpha** | *Straight - Unmatted*, *Premultiplied - Matted With Color* (with the matte colour), *Ignore* (opaque), or *Guess* (EffectCraft looks at the first frame). *Invert Alpha* flips it. |
|
||||
| **Assume this frame rate** | The rate the frames play at. A 250-frame sequence at 25 fps lasts 10 seconds; at 24 fps, 10.4 seconds. |
|
||||
| **Missing Frames** (sequences) | What a gap in the numbering shows: *Show Placeholder* (color bars, as After Effects shows), *Hold Previous Frame*, or *Skip* (the files play back to back and the gap closes up). |
|
||||
| **Start Frame** (sequences) | The frame number at the footage's first frame. It defaults to the first file's number. A higher number trims the head (start a 1001–1250 render at 1010); a lower one adds missing frames before the first file. |
|
||||
| **Start Frame** (sequences) | The frame number at the footage's first frame. It defaults to the first file's number. A higher number trims the head (start a 1001–1250 render at 1010); a lower one adds placeholder frames before the first file. |
|
||||
| Fields, pixel aspect, loop, colour profile, linear light | As in After Effects. |
|
||||
|
||||
Settings ▸ Import ▸ **Report Missing Frames** (on by default) lists the missing frame numbers
|
||||
when a sequence imports.
|
||||
when a sequence imports; Interpret Footage lists them too.
|
||||
|
||||
**File ▸ Interpret Footage ▸ Remember Interpretation / Apply Interpretation** copy the alpha,
|
||||
fields, pixel aspect, loop and colour settings to other footage.
|
||||
|
||||
Agents: `file.interpretFootage {"items": [id], "frameRate": 24, "alpha": "premultiplied",
|
||||
"missingFrames": "hold", "startFrame": 1001}` (`"startFrame": "file"` goes back to the first
|
||||
file's number).
|
||||
"startFrame": 1001}` (`"startFrame": "file"` goes back to the first file's number).
|
||||
|
||||
## Replacing and reloading
|
||||
|
||||
|
||||
+1
-1
@@ -172,7 +172,7 @@ the disk cache in the formats wave; the 3D Camera Tracker in M12.6.)
|
||||
| 3D | 88% | 5.5 | reflections of other layers (Advanced 3D surfaces reflect only the Environment light; Material Options ▸ Appears in Reflections is stored but has no visible effect yet, #261), multi-view layouts, the Extended Viewer for Advanced 3D comps (Classic 3D Extended Viewer landed in M13.5 UI completion); collapsed precomps of another size seen through the parent's camera render (fixed in M13.2); stereo rigs, orbit nulls, lights controlled by the camera, cameras/lights from glTF models, environment backgrounds, Advanced 3D motion blur, blend modes and track mattes landed in M7.7; Classic 3D iris-shaped bokeh with highlights, progressive depth of field on tilted layers and the focus-link commands landed in M13.5; Advanced 3D depth of field with the iris and highlight options, collapsed precomps as real Advanced 3D geometry and extruded text/shape strokes landed in M13.8; Advanced 3D (glTF/OBJ models, primitives, extruded text and shapes, PBR, image-based light, shadow maps, GPU rasteriser) in M7.4–M7.6; Advanced 3D end to end on the GPU (motion blur, iris depth of field, compositing) in M13.11 |
|
||||
| Effects | ≈ 85% | 3.0 | GPU versions of the remaining effects (280 run on the GPU since M13.29, EFF-5: M13.28 added the remaining pixel effects and the geometry and audio generators (Lightning, Advanced Lightning, Beam, Lens Flare, Radio Waves, Vegas, Stroke, Scribble, Write-on, Paint Bucket, Eyedropper Fill, CC Glue Gun, CC Threads, Audio Spectrum / Waveform, Basic / Path Text); M13.29 added the particle effects (CC Particle World / Systems II, Particle Playground, CC Ball Action, CC Pixel Polly, CC Scatterize, Curl Noise) and removed the fallbacks of Shatter's wireframe views, Foam's extras, Timewarp's Matte Layer, strong Warp Fisheye / Twist bends and custom `.ocio` configs; M13.23 added the 3D Channel and Immersive Video families, Apply Color LUT, the OCIO effects, Color Profile Converter and the simulations' render passes (Card Wipe included); M13.22 added the CC light family, the CC transitions, Block Dissolve, Radial Shadow, CC Bender / Blobbylize / Cylinder / Sphere / Spotlight / Environment, 3D Glasses, Numbers, Timecode and the time effects joined the 166 of M13.13; Fractal and the analysis / tool effects still render on the CPU) and the missing controls listed as partial in [effects.md](effects.md) (every After Effects effect exists since M9.11, M12.5 and M12.6; parameter names, order, twirl-downs, popups, units and defaults were aligned in M9.12) |
|
||||
| Interface | 75% | 5.0 | more Learn tutorials and pixel-level fidelity of dialogs (G2 on 7 October: Project items, files and effects dropped on the Composition viewer, Project items dropped in the Timeline between layers and at a time, #85, #88, #89; popup menus moved to fit the window take clicks, so Render Queue templates apply, #117; View ▸ New Viewer landed in M13.32; an automation id on every control of Composition and Solid Settings, Group / Ungroup Shapes in the Layer menu, Window ▸ Learn, the flowchart shortcuts and Reveal in Explorer landed in M3.10 and M3.13; the Home ▸ Templates gallery (eight original built-in templates, user templates from File ▸ Save as Template…) and View ▸ Simulate Output ▸ My Custom RGB… landed in M13.25; Timeline layer reordering by drag, a non-snapping viewer pan, a working rename field and twirl arrows, and the full set of property reveal shortcuts — double presses, Alt+Shift keyframes, Ctrl+` — landed in M13.17–M13.20; the Home ▸ Learn tab with interactive tutorials and a UI fidelity pass landed in M13.5 UI completion; visual editors for Lumetri RGB / hue-saturation curves, Colorama's output cycle wheel, Glow's colour map and Reshape's correspondence points (viewer handles), a scrolling Preview panel and AE-style Composition / Timeline tabs (close, label swatch, viewer lock) landed in M13.10; Timeline outline and Project panel columns scroll horizontally, the Layer Style dialog, ROI resize handles, Pan Behind snapping and 3D Reference Axes landed in M13.5; native macOS menu bar, Timeline columns/search/reveal-add, Home screen with recent projects and all AE workspaces landed; viewer rulers/snapping/channels/snapshots landed in M0.13) |
|
||||
| Project | ≈ 68% | 5.5 | OCIO displays beyond the built-in tone map; relative footage paths and relinking when a project moves (unsaved-changes prompts, a modified mark that follows undo and project files that name their version and always reopen landed in M3.9 and M3.14; auto-save and crash recovery in M3.7; Color Engine with OCIO/ACES working spaces, HDR compand/tone mapping, Rec. 2100 PQ/HLG output, Feet + Frames, display colour management, Simulate Output and the locked viewer landed in M7.7; proxies and Interpret Footage fields / pixel aspect / alpha guess landed: PRJ-8, PRJ-3; image sequence import options (Sequence, Force Alphabetical Order, frame rate, picked ranges, folders, the browser) and Interpret Footage Missing Frames / Start Frame landed: #297) |
|
||||
| Project | ≈ 68% | 5.5 | OCIO displays beyond the built-in tone map; relative footage paths and relinking when a project moves (unsaved-changes prompts, a modified mark that follows undo and project files that name their version and always reopen landed in M3.9 and M3.14; auto-save and crash recovery in M3.7; Color Engine with OCIO/ACES working spaces, HDR compand/tone mapping, Rec. 2100 PQ/HLG output, Feet + Frames, display colour management, Simulate Output and the locked viewer landed in M7.7; proxies and Interpret Footage fields / pixel aspect / alpha guess landed: PRJ-8, PRJ-3; image sequence import options (Sequence, Force Alphabetical Order, frame rate, picked ranges, folders, the browser), missing-frame placeholders and Interpret Footage Start Frame landed: #297) |
|
||||
| Masks & roto | 74% | 5.0 | (audit estimates; Roto Brush 2.0 / 3.0's trained model, MobileSAM in the swappable `segment` module, has since landed in M13.35; variable-width mask feather points with the Mask Feather tool landed in M13.5; mask tracking and Mask Interpolation landed in M6.6; Roto Brush & Refine Edge with graph-cut segmentation, flow propagation, edge matting, decontamination and Freeze in M6.7) |
|
||||
| Preview | 77% | 2.8 | GPU effects for the audio-driven effects and the stepped particle systems; content-keyed RAM preview frames (an edit drops every comp's), audio scrubbing, more snapshot slots (G2 on 7 October: Audio, Lock and Shy switches keep the cached frames and playback with audio waits for frames instead of skipping them, #103; running out of video memory renders the frame on the CPU and keeps fewer GPU frames instead of crashing, #106; RAM preview keys with the render options, least-recently-shown eviction, Auto resolution while playing, Cache Before Playback that fits, an achieved-fps readout, Cache Frames When Idle and purges that clear what they name landed in M4.5–M4.8; 3D channel, VR, OCIO, Card Wipe and the simulations' render passes run on the GPU since M13.23; Advanced 3D layers with blend modes / track mattes / Preserve Transparency and environment backgrounds composite on the GPU since M13.22; pooled GPU textures and fused quantisation since M13.13: Lower Third GPU warm 21 → 4 ms/frame, and Auto now picks the CPU or the GPU per comp from measured frame times; Advanced 3D runs — raster, motion blur, iris depth of field, compositing — and wireframes run on the GPU since M13.11: CPU warm 3394 → GPU warm 290 ms/frame at 1920×1080 (11.7×; Half 968 → 85 ms), GPU ≠ CPU on 0.005 % of pixels, on an M4 Pro under load; Classic 3D bokeh depth of field — iris shapes, highlights, fringe, progressive blur on tilted planes — runs in WGSL since M13.6: 3D Showcase GPU warm 205 → 54 ms/frame at full size on an M4 Pro; the Preview panel's five shortcuts with their own Include / Loop / Cache Before Playback / Range / Play From / Frame Rate / Skip / Resolution / Full Screen / stop options and `playback.settings.get/set` landed in M13.8; Classic 3D runs and adjustment layers composite on the GPU since M12.7; persistent disk cache with the blue cache bar landed; region of interest, snapshots, exposure and Fast Previews landed in M0.13) |
|
||||
| Tracking | ≈ 96% | 0.3 | face tracking's trained model (MediaPipe Face Landmarker, M13.36) is an optional download; without it the classical (skin model + feature components + shape model) fitter is weak on profile views and occluded faces; Rolling Shutter Ripple is approximated by Subspace Warp's mesh density (face tracking (Outline Only / Detailed Features with Face Track Points and Extract & Copy Face Measurements), Subspace Warp's content-preserving mesh warp on subspace-smoothed trajectories, and radial lens distortion (k1, k2) in the camera bundle adjustment with Undistort Footage landed in M13.3; Rolling Shutter Repair in M9.11; point tracker, mask tracking, Warp Stabilizer and the 3D Camera Tracker in M6.x / M12.5 / M12.6) |
|
||||
|
||||
+1
-1
@@ -106,7 +106,7 @@ Commands (CLI, MCP, control channel):
|
||||
- `type.recentFonts`: how many recently used fonts head the font menu
|
||||
- `type.fontNamesInEnglish`: off, fonts with a native-language family name show it
|
||||
- `import.reportMissingFrames`: gaps in an image sequence's numbering are reported on import
|
||||
(they show as placeholders; Interpret Footage ▸ Missing Frames, see [footage.md](footage.md))
|
||||
(they show as colour-bar placeholders, as in After Effects; see [footage.md](footage.md))
|
||||
- `import.unlabeledAlpha`: alpha of TGA / TIFF / movie footage (Ask opens Interpret Footage;
|
||||
Guess takes premultiplied for movies, straight for stills)
|
||||
- `import.dragImportAs`: layered files dropped on the window import as footage or a comp
|
||||
|
||||
Reference in new issue
Block a user