mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-21 19:22:52 -05:00
Engine (all commands with undo/redo + tests in engine/src/tests_timeline.rs): - keys.copy/paste (Edit Copy/Cut/Paste route to keys when keys are selected; relative timing kept, paste at CTI, cross-property paste of same type), keys.selectAll (Cmd+Alt+A), keys.nudge* (Alt+Arrow, Alt+Shift+Arrow), keys.set / keys.setEase (graph editor drags), keys.info (dialog data), keys.timeReverse; keys.interpolation gains continuous/auto Bezier, spatial interpolation and roving; keys.velocity takes per-dimension values and Continuous. - Roving keys (keyframe::retime_roving): constant speed along the motion path. - Layer > Time: layer.timeStretch (percent|duration, hold in/current/out), layer.timeReverse (Cmd+Alt+R), layer.enableTimeRemap (Cmd+Alt+T), layer.freezeFrame, layer.freezeOnLastFrame; render uses Time Remap as the source time for precomp/footage layers. - prop.separateDimensions (X/Y/Z Position; prop.set on Position writes them). - layer.setParent compensates so the layer stays in place (compensate:false to opt out). - prop.pickWhip writes AE-style reference expressions (thisComp.layer(..), effect(..)(..), mask(..).maskFeather, temp=…;[temp,temp], …[0]). - prop.setExpression: syntax errors keep the text but disable the expression. - expr: arrays of properties returned by an expression are resolved. UI: Keyframe Velocity, Keyframe Interpolation and Time Stretch dialogs; Graph Editor (value + speed graphs, draggable keys and Bezier/influence handles, auto-zoom, fit, show selected, separate dimensions, hold/linear/ auto Bezier/Easy Ease buttons); key + layer context menus. Clears the workspace clippy -D warnings failures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
66 lines
3.3 KiB
Rust
66 lines
3.3 KiB
Rust
//! Time remapping and separated Position dimensions.
|
|
|
|
use effectcraft_color::Label;
|
|
use effectcraft_keyframe::{Keyframe, Value};
|
|
use effectcraft_project::build::{self, Ids};
|
|
use effectcraft_project::{Comp, ItemKind, LayerSource, Node, Project, Solid};
|
|
use effectcraft_time::{FrameRate, Tick};
|
|
|
|
use crate::render_frame;
|
|
|
|
fn s(x: f64) -> Tick {
|
|
Tick::from_seconds_f64(x)
|
|
}
|
|
|
|
#[test]
|
|
fn separated_position_drives_the_transform() {
|
|
let mut p = Project::default();
|
|
let comp = Comp::new(200, 100, FrameRate::FPS_30, s(2.0));
|
|
let cid = p.add_item("Comp", Label::Sandstone, None, ItemKind::Comp(comp.clone().into()));
|
|
let sid = p.add_item("S", Label::Red, None, ItemKind::Solid(Solid { color: [1.0, 1.0, 1.0], width: 20, height: 20, pixel_aspect: 1.0 }));
|
|
let mut l = build::layer(&mut p, &comp, "S", LayerSource::Solid { item: sid }, (20, 20), None);
|
|
let mut next = p.next_id;
|
|
let mut ids = Ids(&mut next);
|
|
let mut px = ids.prop("positionX", "X Position", Value::Scalar(20.0));
|
|
px.keys = vec![Keyframe::new(Tick::ZERO, Value::Scalar(20.0)), Keyframe::new(s(1.0), Value::Scalar(180.0))];
|
|
let py = ids.prop("positionY", "Y Position", Value::Scalar(50.0));
|
|
let pz = ids.prop("positionZ", "Z Position", Value::Scalar(0.0));
|
|
p.next_id = next;
|
|
let tr = l.props.sub_mut("transform").unwrap();
|
|
// The combined property is ignored while separated.
|
|
tr.get_mut("position").unwrap().value = Value::Vec3([100.0, 10.0, 0.0]);
|
|
tr.children.extend([Node::Prop(px), Node::Prop(py), Node::Prop(pz)]);
|
|
p.comp_mut(cid).unwrap().layers.push(l);
|
|
let a = render_frame(&p, cid, Tick::ZERO, 1.0);
|
|
assert!(a.get(20, 50)[3] > 0.99 && a.get(100, 10)[3] < 0.01);
|
|
let b = render_frame(&p, cid, s(0.5), 1.0);
|
|
assert!(b.get(100, 50)[3] > 0.99 && b.get(20, 50)[3] < 0.01);
|
|
}
|
|
|
|
#[test]
|
|
fn time_remap_picks_the_source_frame() {
|
|
let mut p = Project::default();
|
|
let outer = Comp::new(100, 100, FrameRate::FPS_30, s(4.0));
|
|
let mut inner = Comp::new(100, 100, FrameRate::FPS_30, s(4.0));
|
|
// Inner comp: a solid that moves from x=10 (t=0) to x=90 (t=2).
|
|
let sid = p.add_item("S", Label::Red, None, ItemKind::Solid(Solid { color: [1.0, 1.0, 1.0], width: 10, height: 10, pixel_aspect: 1.0 }));
|
|
let mut l = build::layer(&mut p, &inner, "S", LayerSource::Solid { item: sid }, (10, 10), None);
|
|
l.props.prop_mut("transform/position").unwrap().keys =
|
|
vec![Keyframe::new(Tick::ZERO, Value::Vec3([10.0, 50.0, 0.0])), Keyframe::new(s(2.0), Value::Vec3([90.0, 50.0, 0.0]))];
|
|
inner.layers.push(l);
|
|
let iid = p.add_item("Inner", Label::Sandstone, None, ItemKind::Comp(inner.into()));
|
|
let cid = p.add_item("Outer", Label::Sandstone, None, ItemKind::Comp(outer.clone().into()));
|
|
let mut pre = build::layer(&mut p, &outer, "Inner", LayerSource::Comp { item: iid }, (100, 100), None);
|
|
let mut next = p.next_id;
|
|
// Freeze on source time 2 s.
|
|
let tr = Ids(&mut next).prop("timeRemap", "Time Remap", Value::Scalar(2.0));
|
|
p.next_id = next;
|
|
let mut tr = tr;
|
|
tr.keys = vec![Keyframe::new(Tick::ZERO, Value::Scalar(2.0)).hold()];
|
|
pre.props.children.push(Node::Prop(tr));
|
|
p.comp_mut(cid).unwrap().layers.push(pre);
|
|
let img = render_frame(&p, cid, Tick::ZERO, 1.0);
|
|
assert!(img.get(90, 50)[3] > 0.99, "frozen at the end position");
|
|
assert!(img.get(10, 50)[3] < 0.01);
|
|
}
|