Timeline: property names have the property menu (#407)
Right-clicking a property's name in the Timeline did nothing, so
Position's dimensions could be separated only from the Graph Editor.
As in After Effects, it now opens the property's menu: Reset, Edit
Value…, Separate Dimensions (Position and, once separated, X / Y / Z
Position, checked), Add / Remove Expression and Add Property to
Essential Graphics. Only Position separates, as in After Effects
(Scale stays one property).
`prop.reset` without `default` used to leave the value as it was: it
now gives the property its own default (an effect parameter's, or a
Transform property's, shared with Layer ▸ Transform ▸ Reset), which
also makes the Properties panel's Reset reset the values. `prop.set
{layer, prop}` without `value` opens the value dialog, like
`keys.set`.
Tests: timeline_property_names_have_the_property_menu
(ui_blank_context_menus), reset_gives_a_property_its_default (engine).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
9c9e954ee2
commit
79bf5a6b59
|
|
@ -1141,6 +1141,22 @@ pub(crate) fn update_position(tr: &mut PropGroup, lt: Tick, f: impl Fn([f64; 3])
|
|||
}
|
||||
}
|
||||
|
||||
/// A Transform property's default (Reset) by match id: the anchor at the centre of the source
|
||||
/// (`w`×`h`), the position at the comp's (`cw`×`ch`), 100% scale and opacity, no rotation.
|
||||
pub(crate) fn transform_default(m: &str, (w, h): (f64, f64), (cw, ch): (f64, f64)) -> Option<KV> {
|
||||
Some(match m {
|
||||
"anchor" => KV::Vec3([w / 2.0, h / 2.0, 0.0]),
|
||||
"position" => KV::Vec3([cw / 2.0, ch / 2.0, 0.0]),
|
||||
"positionX" => KV::Scalar(cw / 2.0),
|
||||
"positionY" => KV::Scalar(ch / 2.0),
|
||||
"positionZ" | "rotation" | "rotationX" | "rotationY" => KV::Scalar(0.0),
|
||||
"scale" => KV::Vec3([100.0; 3]),
|
||||
"orientation" => KV::Vec3([0.0; 3]),
|
||||
"opacity" => KV::Scalar(100.0),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn transform_op(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (cid, ids) = layers_p(s, p)?;
|
||||
let ids = unlocked(s, cid, ids, "layer.transform")?;
|
||||
|
|
@ -1163,14 +1179,12 @@ fn transform_op(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
};
|
||||
match op.as_str() {
|
||||
"reset" => {
|
||||
set(tr, "anchor", KV::Vec3([w / 2.0, h / 2.0, 0.0]));
|
||||
for m in ["anchor", "scale", "rotation", "rotationX", "rotationY", "orientation", "opacity"] {
|
||||
if let Some(v) = transform_default(m, (w, h), (cw, ch)) {
|
||||
set(tr, m, v);
|
||||
}
|
||||
}
|
||||
update_position(tr, lt, |_| [cw / 2.0, ch / 2.0, 0.0]);
|
||||
set(tr, "scale", KV::Vec3([100.0; 3]));
|
||||
set(tr, "rotation", KV::Scalar(0.0));
|
||||
set(tr, "rotationX", KV::Scalar(0.0));
|
||||
set(tr, "rotationY", KV::Scalar(0.0));
|
||||
set(tr, "orientation", KV::Vec3([0.0; 3]));
|
||||
set(tr, "opacity", KV::Scalar(100.0));
|
||||
}
|
||||
// Centred in the frame; a 3D layer keeps its depth.
|
||||
"center" => update_position(tr, lt, |p| [cw / 2.0, ch / 2.0, p[2]]),
|
||||
|
|
|
|||
|
|
@ -271,13 +271,40 @@ fn set_expr(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
Ok(r)
|
||||
}
|
||||
|
||||
/// A property's default value, when it has a known one: an effect parameter's, or a Transform
|
||||
/// property's (not a camera's or light's).
|
||||
fn default_value(s: &Session, cid: ItemId, lid: LayerId, uid: Uid) -> Option<effectcraft_keyframe::Value> {
|
||||
let comp = s.project.comp(cid)?;
|
||||
let layer = comp.layer(lid)?;
|
||||
let pr = layer.props.find(uid)?;
|
||||
let effect = layer.props.node_chain(uid)?.into_iter().find_map(|n| match n {
|
||||
effectcraft_project::Node::Group(g) => match &g.kind {
|
||||
GroupKind::Effect { effect } => Some(effect.clone()),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
});
|
||||
if let Some(effect) = effect {
|
||||
let ps = effectcraft_effects::find(&effect)?.params.iter().find(|ps| ps.id == pr.match_id)?;
|
||||
return Some(effectcraft_effects::default_value(ps, effectcraft_render::effect_bounds(&s.project, comp, layer).0));
|
||||
}
|
||||
if matches!(layer.source, effectcraft_project::LayerSource::Camera | effectcraft_project::LayerSource::Light { .. })
|
||||
|| !layer.transform()?.props().any(|t| t.uid == uid)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let (w, h) = effectcraft_render::source_size(&s.project, layer);
|
||||
super::layer::transform_default(&pr.match_id, (w as f64, h as f64), (comp.width as f64, comp.height as f64))
|
||||
}
|
||||
|
||||
fn reset(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (cid, lid, uid) = prop_ref(s, p, "prop.reset")?;
|
||||
let def = p.get("default").cloned();
|
||||
let own = default_value(s, cid, lid, uid);
|
||||
with_prop(s, "Reset Property", None, cid, lid, uid, |pr, _| {
|
||||
pr.keys.clear();
|
||||
pr.expr = None;
|
||||
if let Some(d) = def.as_ref().and_then(|d| pr.value.coerce_json(d)) {
|
||||
if let Some(d) = def.as_ref().and_then(|d| pr.value.coerce_json(d)).or(own) {
|
||||
pr.value = d;
|
||||
}
|
||||
Ok(Value::Null)
|
||||
|
|
@ -783,7 +810,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
toggle_transform_key
|
||||
),
|
||||
cmd!("prop.setExpression", "Add Expression", ["Animation"], Some("Alt+Shift+="), "{layer?, path|prop, expression?, enabled?}", has_layers, set_expr),
|
||||
cmd!("prop.reset", "Reset Property", [], None, "{layer?, path|prop, default?}", has_layers, reset),
|
||||
cmd!(
|
||||
"prop.reset",
|
||||
"Reset Property",
|
||||
[],
|
||||
None,
|
||||
"{layer?, path|prop, default?} → removes keyframes and expression; the value becomes `default`, else the property's own (an effect parameter's, a Transform property's)",
|
||||
has_layers,
|
||||
reset
|
||||
),
|
||||
cmd!("prop.select", "Select Property", [], None, "{layer?, path|prop, add?, selectKeys?}", has_layers, select_prop),
|
||||
cmd!(
|
||||
"prop.convertExpressionToKeyframes",
|
||||
|
|
|
|||
|
|
@ -482,6 +482,43 @@ fn pick_whip_generates_ae_reference_expressions() {
|
|||
assert!(s.execute("prop.pickWhip", json!({"layer": l, "path": "transform/opacity", "target": {"layer": l, "path": "transform/opacity"}})).is_err());
|
||||
}
|
||||
|
||||
/// #407: Reset without a value gives a property its own default (a Transform property's, an
|
||||
/// effect parameter's) besides removing its keyframes and expression.
|
||||
#[test]
|
||||
fn reset_gives_a_property_its_default() {
|
||||
let (mut s, l) = setup();
|
||||
s.execute("effect.apply", json!({"layer": l, "effect": "Gaussian Blur"})).unwrap();
|
||||
let blur = prop(&s, l, "effects/#1/blurriness").value;
|
||||
let set = [
|
||||
("transform/anchor", json!([10, 20, 0])),
|
||||
("transform/position", json!([10, 20, 0])),
|
||||
("transform/scale", json!([50, 70, 100])),
|
||||
("transform/rotation", json!(30)),
|
||||
("transform/opacity", json!(40)),
|
||||
("effects/#1/blurriness", json!(25)),
|
||||
];
|
||||
for (path, v) in &set {
|
||||
s.execute("prop.set", json!({"layer": l, "path": path, "value": v})).unwrap();
|
||||
}
|
||||
animate(&mut s, l, "transform/rotation", &[(0.0, json!(10)), (1.0, json!(20))]);
|
||||
s.execute("prop.setExpression", json!({"layer": l, "path": "transform/opacity", "expression": "50"})).unwrap();
|
||||
for (path, _) in &set {
|
||||
s.execute("prop.reset", json!({"layer": l, "path": path})).unwrap();
|
||||
}
|
||||
let v = |s: &Session, path: &str| prop(s, l, path).value;
|
||||
assert_eq!(v(&s, "transform/anchor"), KV::Vec3([50.0, 50.0, 0.0]), "the solid's centre");
|
||||
assert_eq!(v(&s, "transform/position"), KV::Vec3([200.0, 150.0, 0.0]), "the comp's centre");
|
||||
assert_eq!(v(&s, "transform/scale"), KV::Vec3([100.0; 3]));
|
||||
assert_eq!(v(&s, "transform/rotation"), KV::Scalar(0.0));
|
||||
assert!(!prop(&s, l, "transform/rotation").is_animated());
|
||||
assert_eq!(v(&s, "transform/opacity"), KV::Scalar(100.0));
|
||||
assert!(prop(&s, l, "transform/opacity").expr.is_none());
|
||||
assert_eq!(v(&s, "effects/#1/blurriness"), blur);
|
||||
// A given default still wins.
|
||||
s.execute("prop.reset", json!({"layer": l, "path": "transform/opacity", "default": 30})).unwrap();
|
||||
assert_eq!(v(&s, "transform/opacity"), KV::Scalar(30.0));
|
||||
}
|
||||
|
||||
/// #363: dropped on one of the target's values the pick whip picks that dimension
|
||||
/// (`position[0]`), as in After Effects; a multi-dimension property follows it on every axis.
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -194,6 +194,17 @@ fn sequence_fields(app: &EffectcraftApp, runs: &[Vec<String>]) -> Vec<Field> {
|
|||
]
|
||||
}
|
||||
|
||||
/// Fields to type a numeric property value in (`value`, or `value[0]`… per dimension).
|
||||
fn value_fields(name: &str, v: &effectcraft_engine::keyframe::Value) -> Option<Vec<Field>> {
|
||||
use effectcraft_engine::keyframe::Value as KV;
|
||||
Some(match v {
|
||||
KV::Scalar(v) => vec![Field::num("value", name, *v)],
|
||||
KV::Vec2(v) => vec![Field::num("value[0]", "X", v[0]), Field::num("value[1]", "Y", v[1])],
|
||||
KV::Vec3(v) => vec![Field::num("value[0]", "X", v[0]), Field::num("value[1]", "Y", v[1]), Field::num("value[2]", "Z", v[2])],
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// If `id` is a dialog command invoked without its parameters, open its form and return true.
|
||||
pub fn open_form(app: &mut EffectcraftApp, id: &str, p: &Value) -> bool {
|
||||
let s = &app.session;
|
||||
|
|
@ -251,13 +262,14 @@ pub fn open_form(app: &mut EffectcraftApp, id: &str, p: &Value) -> bool {
|
|||
let t = p.get("time").and_then(Value::as_f64).map(effectcraft_engine::time::Tick::from_seconds_f64);
|
||||
let Some((pr, t)) = pr.zip(t) else { return false };
|
||||
let Some(k) = pr.keys.iter().min_by_key(|k| (k.time.0 - t.0).abs()) else { return false };
|
||||
use effectcraft_engine::keyframe::Value as KV;
|
||||
let fields = match &k.value {
|
||||
KV::Scalar(v) => vec![Field::num("value", &pr.name, *v)],
|
||||
KV::Vec2(v) => vec![Field::num("value[0]", "X", v[0]), Field::num("value[1]", "Y", v[1])],
|
||||
KV::Vec3(v) => vec![Field::num("value[0]", "X", v[0]), Field::num("value[1]", "Y", v[1]), Field::num("value[2]", "Z", v[2])],
|
||||
_ => return false,
|
||||
};
|
||||
let Some(fields) = value_fields(&pr.name, &k.value) else { return false };
|
||||
(pr.name.clone(), fields)
|
||||
}
|
||||
// A property's context menu ▸ Edit Value…: its value at the current time.
|
||||
"prop.set" if !has(p, &["value"]) => {
|
||||
let l = p.get("layer").and_then(Value::as_u64).and_then(|l| comp?.layer(effectcraft_engine::project::LayerId(l)));
|
||||
let Some((l, pr)) = l.and_then(|l| Some((l, l.props.find(p.get("prop").and_then(Value::as_u64)?)?))) else { return false };
|
||||
let Some(fields) = value_fields(&pr.name, &pr.value_at(l.layer_time(t))) else { return false };
|
||||
(pr.name.clone(), fields)
|
||||
}
|
||||
"layer.setTransform" if !has(p, &["value"]) => {
|
||||
|
|
|
|||
|
|
@ -495,12 +495,7 @@ pub(crate) fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painte
|
|||
app.ui.timeline.graph_transform_box = !tbox;
|
||||
}
|
||||
x += 6.0;
|
||||
let pos_sel = app.session.state.selected_props.iter().find_map(|(l, u)| {
|
||||
let layer = comp.layer(*l)?;
|
||||
let tr = layer.transform()?;
|
||||
(tr.get("position").is_some_and(|p| p.uid == *u) || ["positionX", "positionY", "positionZ"].iter().any(|m| tr.get(m).is_some_and(|p| p.uid == *u)))
|
||||
.then_some(l.0)
|
||||
});
|
||||
let pos_sel = app.session.state.selected_props.iter().find_map(|(l, u)| super::timeline::is_position(comp.layer(*l)?, *u).then_some(l.0));
|
||||
if text_button(app, ui, p, &mut x, y, "Separate Dimensions", false, "separateDimensions", "Separate Dimensions (Position)")
|
||||
&& let Some(l) = pos_sel.or(app.session.state.selected_layers.first().map(|l| l.0))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2356,6 +2356,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
if name_resp.clicked() {
|
||||
actions.push(("prop.select".into(), json!({"layer": layer.id.0, "prop": uid, "add": ui.input(|i| i.modifiers.shift)})));
|
||||
}
|
||||
name_resp.context_menu(|ui| prop_menu(ui, &mut app.auto, cid, layer, prop, &mut actions));
|
||||
// Value editors.
|
||||
let value = ectx.value(layer, prop);
|
||||
let vx = (cw.switches + 4.0).max(name_x + 120.0);
|
||||
|
|
@ -3243,6 +3244,43 @@ fn kind_name(k: MatteKind) -> &'static str {
|
|||
}
|
||||
}
|
||||
|
||||
/// Is property `uid` the layer's Position (or, separated, its X, Y or Z Position)?
|
||||
pub(crate) fn is_position(layer: &Layer, uid: u64) -> bool {
|
||||
layer.transform().is_some_and(|tr| ["position", "positionX", "positionY", "positionZ"].iter().any(|m| tr.get(m).is_some_and(|p| p.uid == uid)))
|
||||
}
|
||||
|
||||
/// A property's context menu (right-click its name in the Timeline), as in After Effects.
|
||||
fn prop_menu(
|
||||
ui: &mut egui::Ui,
|
||||
auto: &mut crate::automation::Registry,
|
||||
comp: ItemId,
|
||||
layer: &Layer,
|
||||
prop: &Property,
|
||||
actions: &mut Vec<(String, serde_json::Value)>,
|
||||
) {
|
||||
let (l, uid) = (layer.id.0, prop.uid);
|
||||
let mut item = |ui: &mut egui::Ui, key: &str, label: &str, selected: bool, cmd: &str, params: serde_json::Value| {
|
||||
let resp = ui.add(egui::Button::new(label).selected(selected));
|
||||
auto.add(&format!("timeline.prop.{uid}.menu.{key}"), resp.rect, label);
|
||||
if resp.clicked() {
|
||||
actions.push((cmd.into(), params));
|
||||
ui.close();
|
||||
}
|
||||
};
|
||||
item(ui, "reset", "Reset", false, "prop.reset", json!({"layer": l, "prop": uid}));
|
||||
if matches!(prop.value, Value::Scalar(_) | Value::Vec2(_) | Value::Vec3(_)) {
|
||||
item(ui, "editValue", "Edit Value…", false, "prop.set", json!({"layer": l, "prop": uid}));
|
||||
}
|
||||
if is_position(layer, uid) {
|
||||
let separated = layer.transform().is_some_and(|tr| tr.get("positionX").is_some());
|
||||
item(ui, "separateDimensions", "Separate Dimensions", separated, "prop.separateDimensions", json!({"layer": l, "value": !separated}));
|
||||
}
|
||||
ui.separator();
|
||||
let (key, label) = if prop.expr.is_some() { ("removeExpression", "Remove Expression") } else { ("addExpression", "Add Expression") };
|
||||
item(ui, key, label, false, "prop.setExpression", json!({"layer": l, "prop": uid}));
|
||||
item(ui, "essentialGraphics", "Add Property to Essential Graphics", false, "essential.addProperty", json!({"comp": comp.0, "layer": l, "prop": uid}));
|
||||
}
|
||||
|
||||
fn layer_context_menu(resp: &egui::Response, layer: &Layer, actions: &mut Vec<(String, serde_json::Value)>) {
|
||||
resp.context_menu(|ui| layer_menu(ui, &[layer.id.0], actions));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//! Right-click actions in the blank Project and Timeline areas.
|
||||
//! Right-click actions in the blank Project and Timeline areas, and on Timeline property names.
|
||||
use effectcraft_engine::Session;
|
||||
use effectcraft_ui_egui::{Dialog, EffectcraftApp};
|
||||
use egui::{Event, Modifiers, PointerButton, Pos2, Rect, pos2, vec2};
|
||||
|
|
@ -131,3 +131,67 @@ fn timeline_blank_menu_opens_composition_settings() {
|
|||
h.run_steps(3);
|
||||
assert_eq!(h.state().dialog, Some(Dialog::CompSettings));
|
||||
}
|
||||
|
||||
/// #407: right-clicking a property's name in the Timeline opens After Effects' property menu:
|
||||
/// Reset, Edit Value…, Separate Dimensions (Position only, checked once separated), Add or
|
||||
/// Remove Expression and Add Property to Essential Graphics.
|
||||
#[test]
|
||||
fn timeline_property_names_have_the_property_menu() {
|
||||
let mut h = harness();
|
||||
let layer = h.state_mut().session.execute("layer.newSolid", json!({"name": "Plate", "color": "#406080"})).unwrap()["layer"].as_u64().unwrap();
|
||||
h.state_mut().session.execute("prop.set", json!({"layer": layer, "path": "transform/rotation", "value": 30})).unwrap();
|
||||
let props =
|
||||
|h: &Harness<'_, EffectcraftApp>| h.state().session.active_comp().unwrap().layer(effectcraft_engine::project::LayerId(layer)).unwrap().props.clone();
|
||||
let uid = |h: &Harness<'_, EffectcraftApp>, path: &str| props(h).prop(path).unwrap().uid;
|
||||
h.state_mut().ui.timeline.open_layers.insert(layer);
|
||||
let transform = props(&h).sub("transform").unwrap().uid;
|
||||
h.state_mut().ui.timeline.open_groups.insert(transform);
|
||||
h.run_steps(3);
|
||||
let menu = |h: &mut Harness<'_, EffectcraftApp>, uid: u64| {
|
||||
let p = rect(h, &format!("timeline.prop.{uid}.name")).center();
|
||||
right_click(h, p);
|
||||
};
|
||||
let choose = |h: &mut Harness<'_, EffectcraftApp>, uid: u64, entry: &str| {
|
||||
let p = rect(h, &format!("timeline.prop.{uid}.menu.{entry}")).center();
|
||||
for pressed in [true, false] {
|
||||
h.input_mut().events.push(Event::PointerMoved(p));
|
||||
h.input_mut().events.push(Event::PointerButton { pos: p, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE });
|
||||
h.step();
|
||||
}
|
||||
h.run_steps(3);
|
||||
};
|
||||
// Reset: Rotation's default.
|
||||
let rotation = uid(&h, "transform/rotation");
|
||||
menu(&mut h, rotation);
|
||||
assert!(h.query_by_label("Edit Value…").is_some());
|
||||
assert!(h.query_by_label("Separate Dimensions").is_none(), "only Position separates");
|
||||
choose(&mut h, rotation, "reset");
|
||||
assert_eq!(props(&h).prop("transform/rotation").unwrap().value.as_f64(), 0.0);
|
||||
// Separate Dimensions on Position; X Position's menu has it too, and joins them again.
|
||||
let position = uid(&h, "transform/position");
|
||||
menu(&mut h, position);
|
||||
choose(&mut h, position, "separateDimensions");
|
||||
assert!(props(&h).prop("transform/positionX").is_some(), "separated");
|
||||
let x = uid(&h, "transform/positionX");
|
||||
menu(&mut h, x);
|
||||
choose(&mut h, x, "separateDimensions");
|
||||
assert!(props(&h).prop("transform/positionX").is_none(), "joined");
|
||||
// Add Expression, then Remove Expression.
|
||||
let opacity = uid(&h, "transform/opacity");
|
||||
menu(&mut h, opacity);
|
||||
choose(&mut h, opacity, "addExpression");
|
||||
assert_eq!(props(&h).prop("transform/opacity").unwrap().expr.as_ref().map(|e| e.text.clone()).as_deref(), Some("transform.opacity"));
|
||||
menu(&mut h, opacity);
|
||||
choose(&mut h, opacity, "removeExpression");
|
||||
assert!(props(&h).prop("transform/opacity").unwrap().expr.is_none());
|
||||
// Add Property to Essential Graphics.
|
||||
menu(&mut h, opacity);
|
||||
choose(&mut h, opacity, "essentialGraphics");
|
||||
let comp = h.state().session.active_comp_id().unwrap();
|
||||
assert_eq!(h.state().session.project.comp(comp).unwrap().essential.as_ref().map_or(0, |e| e.controls.len()), 1);
|
||||
// Edit Value… opens the value dialog.
|
||||
let scale = uid(&h, "transform/scale");
|
||||
menu(&mut h, scale);
|
||||
choose(&mut h, scale, "editValue");
|
||||
assert_eq!(h.state().dialog, Some(Dialog::Form));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -487,6 +487,12 @@ Extension points and the bundled extensions: [plugins.md](plugins.md).
|
|||
* `prop.pickWhip {layer, prop, target: {layer, prop, dimension?}}` links a property as its
|
||||
expression pick whip does; `dimension` picks one of the target's values (`position[0]`), as a
|
||||
drop on that value field does.
|
||||
* A property's menu (right-click its name; entries `timeline.prop.<uid>.menu.<entry>`): Reset
|
||||
(`prop.reset`: no keyframes or expression, and the property's default value, an effect
|
||||
parameter's or a Transform property's), Edit Value… (`prop.set {layer, prop}` without `value`
|
||||
opens the value dialog), Separate Dimensions (Position: `prop.separateDimensions {layer,
|
||||
value}`), Add / Remove Expression (`prop.setExpression {layer, prop}`) and Add Property to
|
||||
Essential Graphics (`essential.addProperty`).
|
||||
|
||||
### History, puppet recording, plug-ins
|
||||
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ the disk cache in the formats wave; the 3D Camera Tracker in M12.6.)
|
|||
| Web | 98% | 0.1 | No shared-memory threads inside one engine instance (the decided design is one engine instance per worker; a threaded build needs nightly `build-std`) (M13.30: Render Queue, analyses and Content-Aware Fill render on each job worker's own WebGPU device in passes, particles and Advanced 3D read back under keys, layer buffers in the browser's disk cache; browser storage, Web Audio, Web Worker renders/analyses, WebGPU viewer and offline install landed in M15.2; viewer frames in frame workers fed by project diffs, Roto Brush propagation in a worker, non-blocking `wait: true` jobs and a browser Media Browser (File System Access folders, browser storage) in M13.10; WEB-1 in M13.24: GPU effects and the GPU compositor in the frame workers on their own WebGPU devices with deferred readbacks (frames render in passes; Backend Auto per comp), the disk cache in the Origin Private File System (written by the workers with sync access handles, LRU under the settings' limit, served after a reload), the storage manager (Settings ▸ Disk ▸ Browser Storage, `storage.info` / `storage.persist` / `storage.clear`), and a WGSL constant Chrome rejected (it disabled every GPU kernel in Chrome) fixed) |
|
||||
| 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 9 October: the viewer's resize cursors point the way each handle scales and turn with the layer, #393; Ctrl/Cmd-click on the Timeline's or the Composition panel's current-time display toggles Timecode / Frames, #370; Tab / Shift+Tab in a value being typed commits it and types in the next / previous value of the panel (Timeline, Effect Controls), #361; the in-window menu bar works from the keyboard (Alt, arrows, Enter, Escape) and the app's shortcuts wait while a menu is open, #279; the Timeline's composition marker bin and numpad * add comp markers, #275; Settings ▸ Appearance ▸ UI Scale (75–200 %, beyond After Effects, which follows the system's scaling), #284; 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) |
|
||||
| Interface | 75% | 5.0 | more Learn tutorials and pixel-level fidelity of dialogs (G2 on 9 October: the viewer's resize cursors point the way each handle scales and turn with the layer, #393; Ctrl/Cmd-click on the Timeline's or the Composition panel's current-time display toggles Timecode / Frames, #370; Tab / Shift+Tab in a value being typed commits it and types in the next / previous value of the panel (Timeline, Effect Controls), #361; right-clicking a property's name in the Timeline opens its menu (Reset, Edit Value…, Separate Dimensions, Add / Remove Expression, Add Property to Essential Graphics), #407; the in-window menu bar works from the keyboard (Alt, arrows, Enter, Escape) and the app's shortcuts wait while a menu is open, #279; the Timeline's composition marker bin and numpad * add comp markers, #275; Settings ▸ Appearance ▸ UI Scale (75–200 %, beyond After Effects, which follows the system's scaling), #284; 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), missing-frame placeholders and Interpret Footage Start Timecode 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) |
|
||||
|
|
|
|||
Loading…
Reference in New Issue