Timeline: colour properties get an eyedropper after their swatch, as in After Effects (#633)
A shape's Fill and Stroke Color (and every other colour property) in the Timeline only had a swatch that opens the colour picker; After Effects puts an eyedropper next to it to sample a colour from the Composition panel. The Timeline now shows that eyedropper (`timeline.prop.<uid>.eyedropper`), reusing Effect Controls' viewer pick: click it, then click in the Composition panel, and the property is set to the sampled colour. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
This commit is contained in:
parent
d3b26f2a65
commit
1bcfe6912d
|
|
@ -186,8 +186,9 @@ fn key_navigator(
|
|||
}
|
||||
}
|
||||
|
||||
/// Toggle a viewer pick (crosshair / eyedropper) for a parameter.
|
||||
fn toggle_pick(app: &mut EffectcraftApp, kind: &str, layer: &Layer, prop: &Property) {
|
||||
/// Toggle a viewer pick (crosshair / eyedropper) for a parameter (also the Timeline's colour
|
||||
/// eyedroppers).
|
||||
pub(crate) fn toggle_pick(app: &mut EffectcraftApp, kind: &str, layer: &Layer, prop: &Property) {
|
||||
let pick = FxPick { kind: kind.into(), layer: layer.id.0, prop: prop.uid, name: prop.name.clone() };
|
||||
if app.ui.fx_pick.as_ref() == Some(&pick) {
|
||||
app.ui.fx_pick = None;
|
||||
|
|
|
|||
|
|
@ -3591,6 +3591,17 @@ fn value_editor(
|
|||
if resp.clicked() {
|
||||
widgets::open_popup(ui, pop);
|
||||
}
|
||||
// The eyedropper after the swatch samples the Composition panel (shape Fill and
|
||||
// Stroke colours, text and effect colours), as in After Effects' Timeline (#633).
|
||||
let er = Rect::from_center_size(pos2(r.max.x + 12.0, at.y), vec2(16.0, 16.0));
|
||||
let active = app.ui.fx_pick.as_ref().is_some_and(|k| k.prop == uid && k.kind == "color");
|
||||
if super::fx_widgets::eyedropper_button(ui, er, active, egui::Id::new(("tl-eye", uid)), &t)
|
||||
.on_hover_text(crate::i18n::tr("Eyedropper: click, then click in the Composition panel"))
|
||||
.clicked()
|
||||
{
|
||||
super::effect_controls::toggle_pick(app, "color", layer, prop);
|
||||
}
|
||||
app.auto.add(&format!("timeline.prop.{uid}.eyedropper"), er, &prop.name);
|
||||
let mut rgb = [c[0] as f32, c[1] as f32, c[2] as f32];
|
||||
if crate::widgets::color_popup(ui, pop, r.left_bottom(), &mut rgb) {
|
||||
set(actions, json!([rgb[0], rgb[1], rgb[2], 1.0]));
|
||||
|
|
|
|||
|
|
@ -197,3 +197,49 @@ fn toolbar_stroke_edits_the_selected_shape_layer() {
|
|||
assert_eq!(width, Some(4.0), "a stroke was added with the toolbar's width");
|
||||
assert_eq!(h.state().session.state.shape_tool.stroke_width, 4.0);
|
||||
}
|
||||
|
||||
/// Issue #633: a colour property in the Timeline (a shape's Fill Color) has an eyedropper after
|
||||
/// its swatch, as in After Effects: click it, then click in the Composition panel to sample.
|
||||
#[test]
|
||||
fn timeline_colour_eyedropper_samples_the_comp() {
|
||||
let mut s = session();
|
||||
s.execute("layer.newSolid", json!({"name": "Back", "color": "#20c040"})).unwrap();
|
||||
let l = s.execute("layer.newShape", json!({"kind": "rect", "fill": [1.0, 0.0, 0.0], "size": [100.0, 60.0]})).unwrap()["layer"].as_u64().unwrap();
|
||||
let layer = s.active_comp().unwrap().layer(LayerId(l)).unwrap().clone();
|
||||
let mut fill = None;
|
||||
layer.props.walk("", &mut |_, p| {
|
||||
if fill.is_none() && matches!(p.value, effectcraft_engine::keyframe::Value::Color(_)) {
|
||||
fill = Some(p.uid);
|
||||
}
|
||||
});
|
||||
let fill = fill.expect("a fill colour");
|
||||
fn groups(g: &effectcraft_engine::project::PropGroup, out: &mut Vec<u64>) {
|
||||
for c in g.groups() {
|
||||
out.push(c.uid);
|
||||
groups(c, out);
|
||||
}
|
||||
}
|
||||
let mut open = vec![];
|
||||
groups(&layer.props, &mut open);
|
||||
let mut h = harness(s);
|
||||
h.state_mut().ui.timeline.open_groups.extend(open);
|
||||
open_layer(&mut h, l);
|
||||
h.run_steps(2);
|
||||
click(&mut h, &format!("timeline.prop.{fill}.eyedropper"));
|
||||
assert_eq!(h.state().ui.fx_pick.as_ref().map(|p| (p.prop, p.kind.as_str())), Some((fill, "color")), "the eyedropper waits for a viewer click");
|
||||
let pos = effectcraft_ui_egui::panels::viewer::comp_to_screen(&h.ctx, [6.0, 6.0]).unwrap();
|
||||
click_at(&mut h, pos);
|
||||
assert!(h.state().ui.fx_pick.is_none(), "the pick is used up");
|
||||
let layer = h.state().session.active_comp().unwrap().layer(LayerId(l)).unwrap().clone();
|
||||
let mut got = None;
|
||||
layer.props.walk("", &mut |_, p| {
|
||||
if p.uid == fill {
|
||||
got = Some(p.value.clone());
|
||||
}
|
||||
});
|
||||
let Some(effectcraft_engine::keyframe::Value::Color(c)) = got else { panic!("{got:?}") };
|
||||
let want = [0x20 as f64 / 255.0, 0xc0 as f64 / 255.0, 0x40 as f64 / 255.0];
|
||||
for k in 0..3 {
|
||||
assert!((c[k] - want[k]).abs() < 0.02, "{c:?} vs {want:?}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue