mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-22 18:22:00 +00:00
As in Photoshop, with the Layer Style dialog on a Drop Shadow or Inner Shadow page, dragging on the image moves the shadow: its offset follows the pointer and is stored as the effect's angle and distance (whole degrees and pixels), moving the shared global light angle when the effect uses it. Space and the middle button still pan; colour pickers still sample. Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
56d3b7f983
commit
6729fa1e50
2 files changed
+78
-1
No files matched your search
@@ -3312,7 +3312,10 @@ pub fn canvas_view(app: &mut PhotocraftApp, ui: &mut egui::Ui, idx: usize, rect:
|
||||
primary && app.ui.dialogs.last().is_some_and(|d| crate::select_mask_view::owns(&d.fields) && crate::select_mask_view::brush_on(&d.fields));
|
||||
// Field Blur's pins are placed and dragged on the image (`blur_pins`).
|
||||
let pinning = primary && app.ui.dialogs.last().is_some_and(|d| crate::blur_pins::owns(&d.fields));
|
||||
let hand = app.ui.tool == Tool::Hand && !picking && !range_picking && !refine_brushing && !pinning;
|
||||
// Layer Style on a Drop or Inner Shadow page: a drag on the image moves the shadow (#5701).
|
||||
let shadow_drag =
|
||||
!picking && app.ui.dialogs.last().is_some_and(|d| d.kind == crate::state::DialogKind::LayerStyle && crate::layer_style::drags_shadow(&d.fields));
|
||||
let hand = app.ui.tool == Tool::Hand && !picking && !range_picking && !refine_brushing && !pinning && !shadow_drag;
|
||||
if let Some(d) = crate::dialogs::pan_delta(&ctx, rect, hand) {
|
||||
let d = xf.unmap_vec(d) / (view.zoom / ppp);
|
||||
view.center[0] -= d.x;
|
||||
@@ -3320,6 +3323,15 @@ pub fn canvas_view(app: &mut PhotocraftApp, ui: &mut egui::Ui, idx: usize, rect:
|
||||
ctx.set_cursor_icon(crate::tool_cursor::hand(&ctx, true));
|
||||
} else if free_hover && (space_pan || hand) {
|
||||
ctx.set_cursor_icon(crate::tool_cursor::hand(&ctx, false));
|
||||
} else if shadow_drag && !middle && crate::dialogs::free_pointer_over(&ctx, rect).is_some() {
|
||||
ctx.set_cursor_icon(egui::CursorIcon::Move);
|
||||
if let Some(p) = crate::dialogs::free_press(&ctx, rect) {
|
||||
let (start, origin) = ctx.input(|i| (i.pointer.primary_pressed(), i.pointer.press_origin().unwrap_or(p)));
|
||||
let total = xf.unmap_vec(p - origin) / (view.zoom / ppp);
|
||||
if let Some(d) = app.ui.dialogs.last_mut() {
|
||||
crate::layer_style::drag_shadow(&mut d.fields, start, [f64::from(total.x), f64::from(total.y)]);
|
||||
}
|
||||
}
|
||||
} else if picking && let Some(p) = crate::dialogs::free_pointer_over(&ctx, rect) {
|
||||
if app.session.prefs().cursors.other == photocraft_engine::prefs::OtherCursor::Precise {
|
||||
ctx.set_cursor_icon(egui::CursorIcon::Crosshair);
|
||||
|
||||
@@ -461,6 +461,39 @@ fn remove_instance(f: &mut Map<String, Value>, id: &str) {
|
||||
/// the layer's order), the blending options and the selected page. `select`
|
||||
/// names a kind to open: its first instance is selected (and enabled), or one
|
||||
/// is created. `light` is the document's global light angle.
|
||||
/// Whether the selected page is a Drop Shadow or Inner Shadow, which a drag on the image moves
|
||||
/// while the dialog is open, as in Photoshop (#5701; [`drag_shadow`]).
|
||||
pub fn drags_shadow(f: &Map<String, Value>) -> bool {
|
||||
let selected = f.get("selected").and_then(Value::as_str).unwrap_or_default();
|
||||
entry(f, selected).and_then(|e| e.get("kind")).and_then(Value::as_str).is_some_and(|k| matches!(k, "dropShadow" | "innerShadow"))
|
||||
}
|
||||
|
||||
/// A drag on the image with a shadow page selected: `start` is true on the press (it records the
|
||||
/// shadow's angle and distance then), `total` the pointer's movement since in document pixels.
|
||||
/// The shadow's offset follows the pointer, kept as Photoshop's whole-degree angle and whole-pixel
|
||||
/// distance (offset = (−cos angle, sin angle) × distance, the light opposite the shadow); with
|
||||
/// Use Global Light the shared angle moves too ([`set_param`]).
|
||||
pub fn drag_shadow(f: &mut Map<String, Value>, start: bool, total: [f64; 2]) {
|
||||
const KEY: &str = "__shadowDrag";
|
||||
let Some(id) = f.get("selected").and_then(Value::as_str).map(str::to_string).filter(|_| drags_shadow(f)) else { return };
|
||||
let Some(p) = entry(f, &id).and_then(|e| e.get("params")) else { return };
|
||||
if start || f.get(KEY).is_none() {
|
||||
let angle = p.get("angle").and_then(Value::as_f64).unwrap_or(120.0);
|
||||
let distance = p.get("distance").and_then(Value::as_f64).unwrap_or(0.0);
|
||||
f.insert(KEY.into(), json!([angle, distance]));
|
||||
}
|
||||
let Some([angle, distance]) = f.get(KEY).and_then(Value::as_array).and_then(|a| Some([a.first()?.as_f64()?, a.get(1)?.as_f64()?])) else { return };
|
||||
let a = angle.to_radians();
|
||||
let (ox, oy) = (-a.cos() * distance + total[0], a.sin() * distance + total[1]);
|
||||
let d = ox.hypot(oy);
|
||||
if !d.is_finite() {
|
||||
return;
|
||||
}
|
||||
let new_angle = if d >= 0.5 { oy.atan2(-ox).to_degrees().round() } else { angle };
|
||||
set_param(f, &id, "angle", json!(new_angle));
|
||||
set_param(f, &id, "distance", json!(d.round().clamp(0.0, 30000.0)));
|
||||
}
|
||||
|
||||
pub fn initial_fields(layer: &Layer, select: Option<&str>, light: f32) -> Map<String, Value> {
|
||||
let mut f = Map::new();
|
||||
f.insert("layer".into(), json!(layer.id.0));
|
||||
@@ -1346,6 +1379,38 @@ mod tests {
|
||||
use super::*;
|
||||
use photocraft_doc::StrokeFx;
|
||||
|
||||
/// #5701: with a shadow page selected, a drag on the image moves the shadow: its offset
|
||||
/// follows the pointer, as whole degrees and pixels, the global light angle with it.
|
||||
#[test]
|
||||
fn dragging_on_the_image_moves_the_selected_shadow() {
|
||||
let shadow = |id: &str, kind: &str, global: bool| json!({"id": id, "kind": kind, "on": true, "params": {"angle": 120.0, "distance": 10.0, "useGlobalLight": global}});
|
||||
let mut f = Map::new();
|
||||
f.insert("selected".into(), json!("fx1"));
|
||||
f.insert("globalLight".into(), json!(120.0));
|
||||
f.insert("effects".into(), json!([shadow("fx1", "dropShadow", true), shadow("fx2", "innerShadow", true), shadow("fx3", "dropShadow", false)]));
|
||||
assert!(drags_shadow(&f));
|
||||
let params = |f: &Map<String, Value>, id: &str| {
|
||||
let p = &entry(f, id).unwrap()["params"];
|
||||
(p["angle"].as_f64().unwrap(), p["distance"].as_f64().unwrap())
|
||||
};
|
||||
// The press keeps the shadow where it is: (5, 8.66) px.
|
||||
drag_shadow(&mut f, true, [0.0, 0.0]);
|
||||
assert_eq!(params(&f, "fx1"), (120.0, 10.0));
|
||||
// Dragged to (15, 0): straight right of the layer, the light from the left (180°).
|
||||
drag_shadow(&mut f, false, [10.0, -8.660_254]);
|
||||
assert_eq!(params(&f, "fx1"), (180.0, 15.0));
|
||||
assert_eq!(params(&f, "fx2").0, 180.0, "the global light angle is shared");
|
||||
assert_eq!(params(&f, "fx3"), (120.0, 10.0), "an instance with its own angle is left alone");
|
||||
// Back onto the layer: no offset, and the angle the drag started with.
|
||||
drag_shadow(&mut f, false, [-5.0, -8.660_254]);
|
||||
assert_eq!(params(&f, "fx1"), (120.0, 0.0));
|
||||
// Not on a shadow page: nothing moves.
|
||||
f.insert("selected".into(), json!(BLENDING));
|
||||
assert!(!drags_shadow(&f));
|
||||
drag_shadow(&mut f, true, [50.0, 50.0]);
|
||||
assert_eq!(params(&f, "fx1"), (120.0, 0.0));
|
||||
}
|
||||
|
||||
fn session() -> photocraft_engine::Session {
|
||||
let mut s = photocraft_engine::Session::new();
|
||||
s.execute("file.new", json!({"width": 64, "height": 64})).unwrap();
|
||||
|
||||
Reference in new issue
Block a user