mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-26 19:18:14 -06:00
Viewer: a layer drag moves the picture while the button is down
Every pointer move replaces the comp's content identity, so the frame on screen takes a new cache key at every step while its render is still in flight; the viewer only ever showed an exact key, so the picture froze on the last completed frame until the drag ended. While a drag or a property scrub is running, show the newest frame of the same comp, frame, scale, view and render options that is ready — a render behind, as After Effects does — and go back to exact frames when it stops.
This commit is contained in:
@@ -541,6 +541,25 @@ impl Frames {
|
||||
Some(img)
|
||||
}
|
||||
|
||||
/// The newest cached frame of `base`'s viewer series — its comp, frame, scale, view and render
|
||||
/// options, whatever the comp content or revision: the last completed render. A drag (or a
|
||||
/// property scrub) gives the comp a new content identity with every pointer move, so the
|
||||
/// viewer falls back to this while the exact key's render is still in flight, and the picture
|
||||
/// follows the pointer instead of freezing until it stops. `None` if the series has nothing
|
||||
/// cached.
|
||||
pub fn get_newest(&self, base: &FrameKey) -> Option<(FrameKey, FrameImage)> {
|
||||
let mut c = self.cache.lock().ok()?;
|
||||
let newest = c
|
||||
.map
|
||||
.keys()
|
||||
.filter(|k| k.comp == base.comp && k.frame == base.frame && k.scale == base.scale && k.view == base.view && k.opts == base.opts)
|
||||
.max_by_key(|k| k.revision)
|
||||
.copied()?;
|
||||
let img = c.map.get(&newest).cloned()?;
|
||||
c.touch(&newest);
|
||||
Some((newest, img))
|
||||
}
|
||||
|
||||
/// The [`comp_content`] identity of `comp` in `project` at `revision` (taken once per
|
||||
/// revision). The project is kept while frames of that identity are cached or rendering, so
|
||||
/// the comp addresses it hashes can't be reused by other comps meanwhile.
|
||||
|
||||
@@ -668,16 +668,22 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let frame = comp.frame_rate.frame_at(time);
|
||||
let key = app.frame_key(cid, frame, scale);
|
||||
app.request_frame_urgent(cid, frame, scale);
|
||||
if let Some(img) = app.frames.get(&key) {
|
||||
let stale = app.viewer_shown.as_ref().is_none_or(|(_, k)| *k != key);
|
||||
// The frame on screen. While a drag or a property scrub is rendering, the exact key changes
|
||||
// with every edit and its render is still in flight; showing the newest frame that is ready
|
||||
// keeps the picture following the pointer (a render behind), where waiting for the exact key
|
||||
// would freeze it until the pointer stops.
|
||||
let interacting = app.ui.viewer.interacting || app.ui.viewer.property_interacting;
|
||||
let shown = app.frames.get(&key).map(|img| (key, img)).or_else(|| interacting.then(|| app.frames.get_newest(&key)).flatten());
|
||||
if let Some((k, img)) = shown {
|
||||
let stale = app.viewer_shown.as_ref().is_none_or(|(_, s)| *s != k);
|
||||
// A CPU frame goes up again when the magnification calls for another factor.
|
||||
let minify = minify_factor(app.session.prefs.viewer_zoom_smooth(), scale, (zoom * ppp) as f64);
|
||||
let refit = !app.viewer_on_gpu() && app.viewer_tex.as_ref().is_some_and(|(_, _, m)| *m != minify);
|
||||
if stale || refit {
|
||||
show_frame(app, &ctx, key, img, minify);
|
||||
} else if let Some((_, k)) = app.viewer_shown.as_mut() {
|
||||
show_frame(app, &ctx, k, img, minify);
|
||||
} else if let Some((_, s)) = app.viewer_shown.as_mut() {
|
||||
// The same frame at a later revision (an edit elsewhere, an undo): it shows that one.
|
||||
k.revision = key.revision;
|
||||
s.revision = k.revision;
|
||||
}
|
||||
}
|
||||
if app.ui.viewer.transparency_grid {
|
||||
|
||||
@@ -526,6 +526,43 @@ fn shape_drag_ghost_shows_where_the_shape_will_land() {
|
||||
assert_eq!(comp.layer(plate).unwrap().props.sub("masks").unwrap().groups().count(), 1, "the drag drew a mask");
|
||||
}
|
||||
|
||||
/// Dragging a layer moves its picture while the button is still down. Every pointer move replaces
|
||||
/// the comp's content identity, so the exact frame on screen is re-rendered at every step: waiting
|
||||
/// for it would freeze the viewer on the last completed frame for the whole drag.
|
||||
#[test]
|
||||
fn a_layer_drag_moves_the_picture_while_the_button_is_down() {
|
||||
let mut h = harness();
|
||||
let box_ = h.state().session.active_comp().unwrap().layers.iter().find(|l| l.name == "Box").unwrap().id;
|
||||
h.state_mut().session.execute("layer.select", json!({"layers": [box_.0]})).unwrap();
|
||||
settle_viewer(&mut h);
|
||||
let from = screen(&h, [320.0, 180.0]);
|
||||
let to = screen(&h, [520.0, 180.0]);
|
||||
h.input_mut().events.push(Event::PointerMoved(from));
|
||||
h.input_mut().events.push(Event::PointerButton { pos: from, button: egui::PointerButton::Primary, pressed: true, modifiers: Default::default() });
|
||||
h.step();
|
||||
// Where the layer is not (yet): the plate behind it.
|
||||
let plate = px_at(&h.render().unwrap(), &h, [440.0, 200.0]);
|
||||
|
||||
// Drag in small steps, still holding the button, and give the renders a chance to land.
|
||||
let mut moved = false;
|
||||
for i in 1..=40 {
|
||||
let p = from + (to - from) * (i as f32 / 40.0);
|
||||
h.input_mut().events.push(Event::PointerMoved(p));
|
||||
h.step();
|
||||
std::thread::sleep(std::time::Duration::from_millis(15));
|
||||
// 30 comp px behind the pointer: inside the layer, well away from its outline, its anchor
|
||||
// icon and where it started.
|
||||
let at = px_at(&h.render().expect("mid-drag frame"), &h, [320.0 + 200.0 * (i as f32 / 40.0) - 30.0, 200.0]);
|
||||
if i >= 30 && delta(at, plate) > 30 {
|
||||
moved = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(moved, "the layer's pixels did not follow the pointer while the button was down");
|
||||
h.input_mut().events.push(Event::PointerButton { pos: to, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() });
|
||||
h.run_steps(2);
|
||||
}
|
||||
|
||||
/// Clicking the word "Fill" opens Fill Options: a radial gradient in Multiply at 40% paints the
|
||||
/// next shape drawn (#227).
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user