mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-02 04:58:11 +08:00
Add control over gradient stops (#834)
* Add gradient stops * Better step adding * Steps can be dragged past each other * Swapping and switching gradient/fill * Fix convert to gradient * Skip non finite transforms for overlays Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
committed by
Keavon Chambers
co-authored by
Keavon Chambers
parent
a1e061fa14
commit
1462d2b662
@@ -1,4 +1,5 @@
|
|||||||
use super::utility_types::TransformOp;
|
use super::utility_types::TransformOp;
|
||||||
|
use crate::application::generate_uuid;
|
||||||
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
|
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
|
||||||
use crate::messages::layout::utility_types::misc::LayoutTarget;
|
use crate::messages::layout::utility_types::misc::LayoutTarget;
|
||||||
use crate::messages::layout::utility_types::widgets::assist_widgets::PivotAssist;
|
use crate::messages::layout::utility_types::widgets::assist_widgets::PivotAssist;
|
||||||
@@ -10,9 +11,10 @@ use crate::messages::layout::utility_types::widgets::label_widgets::{IconLabel,
|
|||||||
use crate::messages::portfolio::utility_types::{ImaginateServerStatus, PersistentData};
|
use crate::messages::portfolio::utility_types::{ImaginateServerStatus, PersistentData};
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
|
|
||||||
|
use graphene::color::Color;
|
||||||
use graphene::document::pick_layer_safe_imaginate_resolution;
|
use graphene::document::pick_layer_safe_imaginate_resolution;
|
||||||
use graphene::layers::imaginate_layer::{ImaginateLayer, ImaginateSamplingMethod, ImaginateStatus};
|
use graphene::layers::imaginate_layer::{ImaginateLayer, ImaginateSamplingMethod, ImaginateStatus};
|
||||||
use graphene::layers::layer_info::{Layer, LayerDataType, LayerDataTypeDiscriminant};
|
use graphene::layers::layer_info::{Layer, LayerData, LayerDataType, LayerDataTypeDiscriminant};
|
||||||
use graphene::layers::style::{Fill, Gradient, GradientType, LineCap, LineJoin, Stroke};
|
use graphene::layers::style::{Fill, Gradient, GradientType, LineCap, LineJoin, Stroke};
|
||||||
use graphene::layers::text_layer::{FontCache, TextLayer};
|
use graphene::layers::text_layer::{FontCache, TextLayer};
|
||||||
|
|
||||||
@@ -1137,7 +1139,7 @@ fn node_gradient_type(gradient: &Gradient) -> LayoutGroup {
|
|||||||
RadioEntryData {
|
RadioEntryData {
|
||||||
value: "linear".into(),
|
value: "linear".into(),
|
||||||
label: "Linear".into(),
|
label: "Linear".into(),
|
||||||
tooltip: "Linear Gradient".into(),
|
tooltip: "Linear gradient changes colors from one side to the other along a line".into(),
|
||||||
on_update: WidgetCallback::new(move |_| {
|
on_update: WidgetCallback::new(move |_| {
|
||||||
PropertiesPanelMessage::ModifyFill {
|
PropertiesPanelMessage::ModifyFill {
|
||||||
fill: Fill::Gradient(cloned_gradient_linear.clone()),
|
fill: Fill::Gradient(cloned_gradient_linear.clone()),
|
||||||
@@ -1149,7 +1151,7 @@ fn node_gradient_type(gradient: &Gradient) -> LayoutGroup {
|
|||||||
RadioEntryData {
|
RadioEntryData {
|
||||||
value: "radial".into(),
|
value: "radial".into(),
|
||||||
label: "Radial".into(),
|
label: "Radial".into(),
|
||||||
tooltip: "Radial Gradient".into(),
|
tooltip: "Radial gradient changes colors from the inside to the outside of a circular area".into(),
|
||||||
on_update: WidgetCallback::new(move |_| {
|
on_update: WidgetCallback::new(move |_| {
|
||||||
PropertiesPanelMessage::ModifyFill {
|
PropertiesPanelMessage::ModifyFill {
|
||||||
fill: Fill::Gradient(cloned_gradient_radial.clone()),
|
fill: Fill::Gradient(cloned_gradient_radial.clone()),
|
||||||
@@ -1165,60 +1167,210 @@ fn node_gradient_type(gradient: &Gradient) -> LayoutGroup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn node_gradient_color(gradient: &Gradient, percent_label: &'static str, position: usize) -> LayoutGroup {
|
fn node_gradient_color(gradient: &Gradient, position: usize) -> LayoutGroup {
|
||||||
let gradient_clone = Rc::new(gradient.clone());
|
let gradient_clone = Rc::new(gradient.clone());
|
||||||
|
let gradient_2 = gradient_clone.clone();
|
||||||
|
let gradient_3 = gradient_clone.clone();
|
||||||
let send_fill_message = move |new_gradient: Gradient| PropertiesPanelMessage::ModifyFill { fill: Fill::Gradient(new_gradient) }.into();
|
let send_fill_message = move |new_gradient: Gradient| PropertiesPanelMessage::ModifyFill { fill: Fill::Gradient(new_gradient) }.into();
|
||||||
LayoutGroup::Row {
|
|
||||||
widgets: vec![
|
let value = format!("Gradient: {:.0}%", gradient_clone.positions[position].0 * 100.);
|
||||||
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
let mut widgets = vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||||
value: format!("Gradient: {}", percent_label),
|
value,
|
||||||
..TextLabel::default()
|
tooltip: "Adjustable by dragging the gradient stops in the viewport with the Gradient tool active".into(),
|
||||||
})),
|
..TextLabel::default()
|
||||||
WidgetHolder::new(Widget::Separator(Separator {
|
}))];
|
||||||
separator_type: SeparatorType::Unrelated,
|
widgets.push(WidgetHolder::new(Widget::Separator(Separator {
|
||||||
direction: SeparatorDirection::Horizontal,
|
separator_type: SeparatorType::Unrelated,
|
||||||
})),
|
direction: SeparatorDirection::Horizontal,
|
||||||
WidgetHolder::new(Widget::ColorInput(ColorInput {
|
})));
|
||||||
value: gradient_clone.positions[position].1,
|
widgets.push(WidgetHolder::new(Widget::ColorInput(ColorInput {
|
||||||
on_update: WidgetCallback::new(move |text_input: &ColorInput| {
|
value: gradient_clone.positions[position].1,
|
||||||
let mut new_gradient = (*gradient_clone).clone();
|
on_update: WidgetCallback::new(move |text_input: &ColorInput| {
|
||||||
new_gradient.positions[position].1 = text_input.value;
|
let mut new_gradient = (*gradient_clone).clone();
|
||||||
send_fill_message(new_gradient)
|
new_gradient.positions[position].1 = text_input.value;
|
||||||
}),
|
send_fill_message(new_gradient)
|
||||||
..ColorInput::default()
|
}),
|
||||||
})),
|
..ColorInput::default()
|
||||||
],
|
})));
|
||||||
|
|
||||||
|
let mut skip_separator = false;
|
||||||
|
// Remove button
|
||||||
|
if gradient.positions.len() != position + 1 && position != 0 {
|
||||||
|
let on_update = WidgetCallback::new(move |_| {
|
||||||
|
let mut new_gradient = (*gradient_3).clone();
|
||||||
|
new_gradient.positions.remove(position);
|
||||||
|
send_fill_message(new_gradient)
|
||||||
|
});
|
||||||
|
|
||||||
|
skip_separator = true;
|
||||||
|
widgets.push(WidgetHolder::new(Widget::Separator(Separator {
|
||||||
|
separator_type: SeparatorType::Related,
|
||||||
|
direction: SeparatorDirection::Horizontal,
|
||||||
|
})));
|
||||||
|
widgets.push(WidgetHolder::new(Widget::IconButton(IconButton {
|
||||||
|
icon: "Remove".to_string(),
|
||||||
|
tooltip: "Remove this gradient stop".to_string(),
|
||||||
|
size: 16,
|
||||||
|
on_update,
|
||||||
|
..Default::default()
|
||||||
|
})));
|
||||||
}
|
}
|
||||||
|
// Add button
|
||||||
|
if gradient.positions.len() != position + 1 {
|
||||||
|
let on_update = WidgetCallback::new(move |_| {
|
||||||
|
let mut gradient = (*gradient_2).clone();
|
||||||
|
|
||||||
|
let get_color = |index: usize| match (gradient.positions[index].1, gradient.positions.get(index + 1).and_then(|x| x.1)) {
|
||||||
|
(Some(a), Some(b)) => Color::from_rgbaf32((a.r() + b.r()) / 2., (a.g() + b.g()) / 2., (a.b() + b.b()) / 2., ((a.a() + b.a()) / 2.).clamp(0., 1.)),
|
||||||
|
(Some(v), _) | (_, Some(v)) => Some(v),
|
||||||
|
_ => Some(Color::WHITE),
|
||||||
|
};
|
||||||
|
let get_pos = |index: usize| (gradient.positions[index].0 + gradient.positions.get(index + 1).map(|v| v.0).unwrap_or(1.)) / 2.;
|
||||||
|
|
||||||
|
gradient.positions.push((get_pos(position), get_color(position)));
|
||||||
|
|
||||||
|
gradient.positions.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
|
||||||
|
|
||||||
|
send_fill_message(gradient)
|
||||||
|
});
|
||||||
|
|
||||||
|
if !skip_separator {
|
||||||
|
widgets.push(WidgetHolder::new(Widget::Separator(Separator {
|
||||||
|
separator_type: SeparatorType::Related,
|
||||||
|
direction: SeparatorDirection::Horizontal,
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
widgets.push(WidgetHolder::new(Widget::IconButton(IconButton {
|
||||||
|
icon: "Add".to_string(),
|
||||||
|
tooltip: "Add a gradient stop after this".to_string(),
|
||||||
|
size: 16,
|
||||||
|
on_update,
|
||||||
|
..Default::default()
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
LayoutGroup::Row { widgets }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn node_section_fill(fill: &Fill) -> Option<LayoutGroup> {
|
fn node_section_fill(fill: &Fill) -> Option<LayoutGroup> {
|
||||||
|
let initial_color = if let Fill::Solid(color) = fill { *color } else { Color::BLACK };
|
||||||
|
|
||||||
match fill {
|
match fill {
|
||||||
Fill::Solid(_) | Fill::None => Some(LayoutGroup::Section {
|
Fill::Solid(_) | Fill::None => Some(LayoutGroup::Section {
|
||||||
name: "Fill".into(),
|
name: "Fill".into(),
|
||||||
layout: vec![LayoutGroup::Row {
|
layout: vec![
|
||||||
widgets: vec![
|
LayoutGroup::Row {
|
||||||
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
widgets: vec![
|
||||||
value: "Color".into(),
|
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||||
..TextLabel::default()
|
value: "Color".into(),
|
||||||
})),
|
..TextLabel::default()
|
||||||
WidgetHolder::new(Widget::Separator(Separator {
|
})),
|
||||||
separator_type: SeparatorType::Unrelated,
|
WidgetHolder::new(Widget::Separator(Separator {
|
||||||
direction: SeparatorDirection::Horizontal,
|
separator_type: SeparatorType::Unrelated,
|
||||||
})),
|
direction: SeparatorDirection::Horizontal,
|
||||||
WidgetHolder::new(Widget::ColorInput(ColorInput {
|
})),
|
||||||
value: if let Fill::Solid(color) = fill { Some(*color) } else { None },
|
WidgetHolder::new(Widget::ColorInput(ColorInput {
|
||||||
on_update: WidgetCallback::new(|text_input: &ColorInput| {
|
value: if let Fill::Solid(color) = fill { Some(*color) } else { None },
|
||||||
let fill = if let Some(value) = text_input.value { Fill::Solid(value) } else { Fill::None };
|
on_update: WidgetCallback::new(|text_input: &ColorInput| {
|
||||||
PropertiesPanelMessage::ModifyFill { fill }.into()
|
let fill = if let Some(value) = text_input.value { Fill::Solid(value) } else { Fill::None };
|
||||||
}),
|
PropertiesPanelMessage::ModifyFill { fill }.into()
|
||||||
..ColorInput::default()
|
}),
|
||||||
})),
|
..ColorInput::default()
|
||||||
],
|
})),
|
||||||
}],
|
],
|
||||||
|
},
|
||||||
|
LayoutGroup::Row {
|
||||||
|
widgets: vec![
|
||||||
|
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||||
|
value: "".into(),
|
||||||
|
..TextLabel::default()
|
||||||
|
})),
|
||||||
|
WidgetHolder::new(Widget::Separator(Separator {
|
||||||
|
separator_type: SeparatorType::Unrelated,
|
||||||
|
direction: SeparatorDirection::Horizontal,
|
||||||
|
})),
|
||||||
|
WidgetHolder::new(Widget::TextButton(TextButton {
|
||||||
|
label: "Use Gradient".into(),
|
||||||
|
tooltip: "Change this fill from a solid color to a gradient".into(),
|
||||||
|
on_update: WidgetCallback::new(move |_: &TextButton| {
|
||||||
|
let (r, g, b, _) = initial_color.components();
|
||||||
|
let opposite_color = Color::from_rgbaf32(1. - r, 1. - g, 1. - b, 1.).unwrap();
|
||||||
|
|
||||||
|
PropertiesPanelMessage::ModifyFill {
|
||||||
|
fill: Fill::Gradient(Gradient::new(
|
||||||
|
DVec2::new(0., 0.5),
|
||||||
|
initial_color,
|
||||||
|
DVec2::new(1., 0.5),
|
||||||
|
opposite_color,
|
||||||
|
DAffine2::IDENTITY,
|
||||||
|
generate_uuid(),
|
||||||
|
GradientType::Linear,
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}),
|
||||||
|
..TextButton::default()
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
Fill::Gradient(gradient) => Some(LayoutGroup::Section {
|
Fill::Gradient(gradient) => Some(LayoutGroup::Section {
|
||||||
name: "Fill".into(),
|
name: "Fill".into(),
|
||||||
layout: vec![node_gradient_type(gradient), node_gradient_color(gradient, "0%", 0), node_gradient_color(gradient, "100%", 1)],
|
layout: {
|
||||||
|
let cloned_gradient = gradient.clone();
|
||||||
|
let first_color = gradient.positions.get(0).unwrap_or(&(0., None)).1;
|
||||||
|
|
||||||
|
let mut layout = vec![node_gradient_type(gradient)];
|
||||||
|
layout.extend((0..gradient.positions.len()).map(|pos| node_gradient_color(gradient, pos)));
|
||||||
|
layout.push(LayoutGroup::Row {
|
||||||
|
widgets: vec![
|
||||||
|
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||||
|
value: "".into(),
|
||||||
|
..TextLabel::default()
|
||||||
|
})),
|
||||||
|
WidgetHolder::new(Widget::Separator(Separator {
|
||||||
|
separator_type: SeparatorType::Unrelated,
|
||||||
|
direction: SeparatorDirection::Horizontal,
|
||||||
|
})),
|
||||||
|
WidgetHolder::new(Widget::TextButton(TextButton {
|
||||||
|
label: "Invert".into(),
|
||||||
|
icon: Some("Swap".into()),
|
||||||
|
tooltip: "Reverse the order of each color stop".into(),
|
||||||
|
on_update: WidgetCallback::new(move |_: &TextButton| {
|
||||||
|
let mut new_gradient = cloned_gradient.clone();
|
||||||
|
new_gradient.positions = new_gradient.positions.iter().map(|(distance, color)| (1. - distance, *color)).collect();
|
||||||
|
new_gradient.positions.reverse();
|
||||||
|
PropertiesPanelMessage::ModifyFill { fill: Fill::Gradient(new_gradient) }.into()
|
||||||
|
}),
|
||||||
|
..TextButton::default()
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
layout.push(LayoutGroup::Row {
|
||||||
|
widgets: vec![
|
||||||
|
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||||
|
value: "".into(),
|
||||||
|
..TextLabel::default()
|
||||||
|
})),
|
||||||
|
WidgetHolder::new(Widget::Separator(Separator {
|
||||||
|
separator_type: SeparatorType::Unrelated,
|
||||||
|
direction: SeparatorDirection::Horizontal,
|
||||||
|
})),
|
||||||
|
WidgetHolder::new(Widget::TextButton(TextButton {
|
||||||
|
label: "Use Solid Color".into(),
|
||||||
|
tooltip: "Change this fill from a gradient to a solid color, keeping the 0% stop color".into(),
|
||||||
|
on_update: WidgetCallback::new(move |_: &TextButton| {
|
||||||
|
PropertiesPanelMessage::ModifyFill {
|
||||||
|
fill: Fill::Solid(first_color.unwrap_or_default()),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}),
|
||||||
|
..TextButton::default()
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
layout
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ fn gradient_space_transform(path: &[LayerId], layer: &Layer, document: &Document
|
|||||||
pub struct GradientOverlay {
|
pub struct GradientOverlay {
|
||||||
pub handles: [Vec<LayerId>; 2],
|
pub handles: [Vec<LayerId>; 2],
|
||||||
pub line: Vec<LayerId>,
|
pub line: Vec<LayerId>,
|
||||||
|
pub steps: Vec<Vec<LayerId>>,
|
||||||
path: Vec<LayerId>,
|
path: Vec<LayerId>,
|
||||||
transform: DAffine2,
|
transform: DAffine2,
|
||||||
gradient: Gradient,
|
gradient: Gradient,
|
||||||
@@ -205,22 +206,35 @@ impl GradientOverlay {
|
|||||||
path
|
path
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(fill: &Gradient, dragging_start: Option<bool>, path: &[LayerId], layer: &Layer, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>, font_cache: &FontCache) -> Self {
|
pub fn new(
|
||||||
|
fill: &Gradient,
|
||||||
|
dragging: Option<GradientDragTarget>,
|
||||||
|
path: &[LayerId],
|
||||||
|
layer: &Layer,
|
||||||
|
document: &DocumentMessageHandler,
|
||||||
|
responses: &mut VecDeque<Message>,
|
||||||
|
font_cache: &FontCache,
|
||||||
|
) -> Self {
|
||||||
let transform = gradient_space_transform(path, layer, document, font_cache);
|
let transform = gradient_space_transform(path, layer, document, font_cache);
|
||||||
let Gradient { start, end, .. } = fill;
|
let Gradient { start, end, positions, .. } = fill;
|
||||||
let [start, end] = [transform.transform_point2(*start), transform.transform_point2(*end)];
|
let [start, end] = [transform.transform_point2(*start), transform.transform_point2(*end)];
|
||||||
|
|
||||||
let line = Self::generate_overlay_line(start, end, responses);
|
let line = Self::generate_overlay_line(start, end, responses);
|
||||||
let handles = [
|
let handles = [
|
||||||
Self::generate_overlay_handle(start, responses, dragging_start == Some(true)),
|
Self::generate_overlay_handle(start, responses, dragging == Some(GradientDragTarget::Start)),
|
||||||
Self::generate_overlay_handle(end, responses, dragging_start == Some(false)),
|
Self::generate_overlay_handle(end, responses, dragging == Some(GradientDragTarget::End)),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
let not_at_end = |(_, x): &(_, f64)| x.abs() > f64::EPSILON * 1000. && (1. - x).abs() > f64::EPSILON * 1000.;
|
||||||
|
let create_step = |(index, pos)| Self::generate_overlay_handle(start.lerp(end, pos), responses, dragging == Some(GradientDragTarget::Step(index)));
|
||||||
|
let steps = positions.iter().map(|(pos, _)| *pos).enumerate().filter(not_at_end).map(create_step).collect();
|
||||||
|
|
||||||
let path = path.to_vec();
|
let path = path.to_vec();
|
||||||
let gradient = fill.clone();
|
let gradient = fill.clone();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
handles,
|
handles,
|
||||||
|
steps,
|
||||||
line,
|
line,
|
||||||
path,
|
path,
|
||||||
transform,
|
transform,
|
||||||
@@ -233,6 +247,9 @@ impl GradientOverlay {
|
|||||||
let [start, end] = self.handles;
|
let [start, end] = self.handles;
|
||||||
responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path: start }.into()).into());
|
responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path: start }.into()).into());
|
||||||
responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path: end }.into()).into());
|
responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path: end }.into()).into());
|
||||||
|
for step in self.steps {
|
||||||
|
responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path: step }.into()).into());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn evaluate_gradient_start(&self) -> DVec2 {
|
pub fn evaluate_gradient_start(&self) -> DVec2 {
|
||||||
@@ -244,13 +261,21 @@ impl GradientOverlay {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Clone, Copy, Debug, Default)]
|
||||||
|
pub enum GradientDragTarget {
|
||||||
|
Start,
|
||||||
|
#[default]
|
||||||
|
End,
|
||||||
|
Step(usize),
|
||||||
|
}
|
||||||
|
|
||||||
/// Contains information about the selected gradient handle
|
/// Contains information about the selected gradient handle
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
struct SelectedGradient {
|
struct SelectedGradient {
|
||||||
path: Vec<LayerId>,
|
path: Vec<LayerId>,
|
||||||
transform: DAffine2,
|
transform: DAffine2,
|
||||||
gradient: Gradient,
|
gradient: Gradient,
|
||||||
dragging_start: bool,
|
dragging: GradientDragTarget,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SelectedGradient {
|
impl SelectedGradient {
|
||||||
@@ -260,7 +285,7 @@ impl SelectedGradient {
|
|||||||
path: path.to_vec(),
|
path: path.to_vec(),
|
||||||
transform,
|
transform,
|
||||||
gradient,
|
gradient,
|
||||||
dragging_start: false,
|
dragging: GradientDragTarget::End,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,8 +297,8 @@ impl SelectedGradient {
|
|||||||
pub fn update_gradient(&mut self, mut mouse: DVec2, responses: &mut VecDeque<Message>, snap_rotate: bool, gradient_type: GradientType) {
|
pub fn update_gradient(&mut self, mut mouse: DVec2, responses: &mut VecDeque<Message>, snap_rotate: bool, gradient_type: GradientType) {
|
||||||
self.gradient.gradient_type = gradient_type;
|
self.gradient.gradient_type = gradient_type;
|
||||||
|
|
||||||
if snap_rotate {
|
if snap_rotate && matches!(self.dragging, GradientDragTarget::End | GradientDragTarget::Start) {
|
||||||
let point = if self.dragging_start {
|
let point = if self.dragging == GradientDragTarget::Start {
|
||||||
self.transform.transform_point2(self.gradient.end)
|
self.transform.transform_point2(self.gradient.end)
|
||||||
} else {
|
} else {
|
||||||
self.transform.transform_point2(self.gradient.start)
|
self.transform.transform_point2(self.gradient.start)
|
||||||
@@ -293,10 +318,22 @@ impl SelectedGradient {
|
|||||||
|
|
||||||
mouse = self.transform.inverse().transform_point2(mouse);
|
mouse = self.transform.inverse().transform_point2(mouse);
|
||||||
|
|
||||||
if self.dragging_start {
|
match self.dragging {
|
||||||
self.gradient.start = mouse;
|
GradientDragTarget::Start => self.gradient.start = mouse,
|
||||||
} else {
|
GradientDragTarget::End => self.gradient.end = mouse,
|
||||||
self.gradient.end = mouse;
|
GradientDragTarget::Step(s) => {
|
||||||
|
// Calculate the new position by finding the closest point on the line
|
||||||
|
let new_pos = ((self.gradient.end - self.gradient.start).angle_between(mouse - self.gradient.start)).cos() * self.gradient.start.distance(mouse)
|
||||||
|
/ self.gradient.start.distance(self.gradient.end);
|
||||||
|
|
||||||
|
// Should not go off end but can swap (like inscape)
|
||||||
|
let clamped = new_pos.clamp(0., 1.);
|
||||||
|
self.gradient.positions[s].0 = clamped;
|
||||||
|
let new_pos = self.gradient.positions[s];
|
||||||
|
|
||||||
|
self.gradient.positions.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
|
||||||
|
self.dragging = GradientDragTarget::Step(self.gradient.positions.iter().position(|x| *x == new_pos).unwrap());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.gradient.transform = self.transform;
|
self.gradient.transform = self.transform;
|
||||||
@@ -348,16 +385,17 @@ impl Fsm for GradientToolFsmState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for path in document.selected_visible_layers() {
|
for path in document.selected_visible_layers() {
|
||||||
|
if !document.graphene_document.multiply_transforms(path).unwrap().inverse().is_finite() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let layer = document.graphene_document.layer(path).unwrap();
|
let layer = document.graphene_document.layer(path).unwrap();
|
||||||
|
|
||||||
if let Ok(Fill::Gradient(gradient)) = layer.style().map(|style| style.fill()) {
|
if let Ok(Fill::Gradient(gradient)) = layer.style().map(|style| style.fill()) {
|
||||||
let dragging_start = tool_data
|
let dragging = tool_data
|
||||||
.selected_gradient
|
.selected_gradient
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|selected| if selected.path == path { Some(selected.dragging_start) } else { None });
|
.and_then(|selected| if selected.path == path { Some(selected.dragging) } else { None });
|
||||||
tool_data
|
tool_data.gradient_overlays.push(GradientOverlay::new(gradient, dragging, path, layer, document, responses, font_cache))
|
||||||
.gradient_overlays
|
|
||||||
.push(GradientOverlay::new(gradient, dragging_start, path, layer, document, responses, font_cache))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,25 +409,35 @@ impl Fsm for GradientToolFsmState {
|
|||||||
|
|
||||||
let mut dragging = false;
|
let mut dragging = false;
|
||||||
for overlay in &tool_data.gradient_overlays {
|
for overlay in &tool_data.gradient_overlays {
|
||||||
if overlay.evaluate_gradient_start().distance_squared(mouse) < tolerance {
|
// Check for dragging step
|
||||||
dragging = true;
|
for (index, (pos, _)) in overlay.gradient.positions.iter().enumerate() {
|
||||||
start_snap(&mut tool_data.snap_manager, document, font_cache);
|
let pos = overlay.transform.transform_point2(overlay.gradient.start.lerp(overlay.gradient.end, *pos));
|
||||||
tool_data.selected_gradient = Some(SelectedGradient {
|
if pos.distance_squared(mouse) < tolerance {
|
||||||
path: overlay.path.clone(),
|
dragging = true;
|
||||||
transform: overlay.transform,
|
tool_data.selected_gradient = Some(SelectedGradient {
|
||||||
gradient: overlay.gradient.clone(),
|
path: overlay.path.clone(),
|
||||||
dragging_start: true,
|
transform: overlay.transform,
|
||||||
})
|
gradient: overlay.gradient.clone(),
|
||||||
|
dragging: GradientDragTarget::Step(index),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if overlay.evaluate_gradient_end().distance_squared(mouse) < tolerance {
|
|
||||||
dragging = true;
|
// Check dragging start or end handle
|
||||||
start_snap(&mut tool_data.snap_manager, document, font_cache);
|
for (pos, dragging_target) in [
|
||||||
tool_data.selected_gradient = Some(SelectedGradient {
|
(overlay.evaluate_gradient_start(), GradientDragTarget::Start),
|
||||||
path: overlay.path.clone(),
|
(overlay.evaluate_gradient_end(), GradientDragTarget::End),
|
||||||
transform: overlay.transform,
|
] {
|
||||||
gradient: overlay.gradient.clone(),
|
if pos.distance_squared(mouse) < tolerance {
|
||||||
dragging_start: false,
|
dragging = true;
|
||||||
})
|
start_snap(&mut tool_data.snap_manager, document, font_cache);
|
||||||
|
tool_data.selected_gradient = Some(SelectedGradient {
|
||||||
|
path: overlay.path.clone(),
|
||||||
|
transform: overlay.transform,
|
||||||
|
gradient: overlay.gradient.clone(),
|
||||||
|
dragging: dragging_target,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if dragging {
|
if dragging {
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
|
||||||
|
<rect width="8" height="2" x="2" y="5" />
|
||||||
|
<rect width="2" height="8" x="5" y="2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 154 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
|
||||||
|
<rect width="8" height="2" x="2" y="5" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 111 B |
@@ -8,6 +8,7 @@ const GRAPHICS = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// 12px Solid
|
// 12px Solid
|
||||||
|
import Add from "@/../assets/icon-12px-solid/add.svg";
|
||||||
import Checkmark from "@/../assets/icon-12px-solid/checkmark.svg";
|
import Checkmark from "@/../assets/icon-12px-solid/checkmark.svg";
|
||||||
import CloseX from "@/../assets/icon-12px-solid/close-x.svg";
|
import CloseX from "@/../assets/icon-12px-solid/close-x.svg";
|
||||||
import DropdownArrow from "@/../assets/icon-12px-solid/dropdown-arrow.svg";
|
import DropdownArrow from "@/../assets/icon-12px-solid/dropdown-arrow.svg";
|
||||||
@@ -30,6 +31,7 @@ import KeyboardSpace from "@/../assets/icon-12px-solid/keyboard-space.svg";
|
|||||||
import KeyboardTab from "@/../assets/icon-12px-solid/keyboard-tab.svg";
|
import KeyboardTab from "@/../assets/icon-12px-solid/keyboard-tab.svg";
|
||||||
import Link from "@/../assets/icon-12px-solid/link.svg";
|
import Link from "@/../assets/icon-12px-solid/link.svg";
|
||||||
import Overlays from "@/../assets/icon-12px-solid/overlays.svg";
|
import Overlays from "@/../assets/icon-12px-solid/overlays.svg";
|
||||||
|
import Remove from "@/../assets/icon-12px-solid/remove.svg";
|
||||||
import ResetColors from "@/../assets/icon-12px-solid/reset-colors.svg";
|
import ResetColors from "@/../assets/icon-12px-solid/reset-colors.svg";
|
||||||
import Snapping from "@/../assets/icon-12px-solid/snapping.svg";
|
import Snapping from "@/../assets/icon-12px-solid/snapping.svg";
|
||||||
import Swap from "@/../assets/icon-12px-solid/swap.svg";
|
import Swap from "@/../assets/icon-12px-solid/swap.svg";
|
||||||
@@ -41,6 +43,7 @@ import WindowButtonWinMinimize from "@/../assets/icon-12px-solid/window-button-w
|
|||||||
import WindowButtonWinRestoreDown from "@/../assets/icon-12px-solid/window-button-win-restore-down.svg";
|
import WindowButtonWinRestoreDown from "@/../assets/icon-12px-solid/window-button-win-restore-down.svg";
|
||||||
|
|
||||||
const SOLID_12PX = {
|
const SOLID_12PX = {
|
||||||
|
Add: { component: Add, size: 12 },
|
||||||
Checkmark: { component: Checkmark, size: 12 },
|
Checkmark: { component: Checkmark, size: 12 },
|
||||||
CloseX: { component: CloseX, size: 12 },
|
CloseX: { component: CloseX, size: 12 },
|
||||||
DropdownArrow: { component: DropdownArrow, size: 12 },
|
DropdownArrow: { component: DropdownArrow, size: 12 },
|
||||||
@@ -63,6 +66,7 @@ const SOLID_12PX = {
|
|||||||
KeyboardTab: { component: KeyboardTab, size: 12 },
|
KeyboardTab: { component: KeyboardTab, size: 12 },
|
||||||
Link: { component: Link, size: 12 },
|
Link: { component: Link, size: 12 },
|
||||||
Overlays: { component: Overlays, size: 12 },
|
Overlays: { component: Overlays, size: 12 },
|
||||||
|
Remove: { component: Remove, size: 12 },
|
||||||
ResetColors: { component: ResetColors, size: 12 },
|
ResetColors: { component: ResetColors, size: 12 },
|
||||||
Snapping: { component: Snapping, size: 12 },
|
Snapping: { component: Snapping, size: 12 },
|
||||||
Swap: { component: Swap, size: 12 },
|
Swap: { component: Swap, size: 12 },
|
||||||
|
|||||||
Reference in New Issue
Block a user