mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-02 21:48:12 +08:00
Move gradient picking into the color picker (#1778)
* Gradient picker * Fix up color picker layout CSS problems * Begin hooking up SpectrumInput for gradient in the ColorPicker * Working gradient picking on the frontend only * Plumb FillColorChoice into the backend * Hook everything else up, just with a weird bug remaining * Fix some svelty reactivity issues * Add and remove stops * Cleanup * Rename type * Fill node document format upgrading * Fix lint * Polish the color picker UX and fix a bug --------- Co-authored-by: 0hypercube <0hypercube@gmail.com>
This commit is contained in:
co-authored by
0hypercube
parent
449729f1e1
commit
a9a4b5cd19
Generated
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
@@ -4,6 +4,7 @@ use crate::messages::prelude::*;
|
|||||||
|
|
||||||
use graphene_core::raster::color::Color;
|
use graphene_core::raster::color::Color;
|
||||||
use graphene_core::text::Font;
|
use graphene_core::text::Font;
|
||||||
|
use graphene_std::vector::style::{FillChoice, GradientStops};
|
||||||
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
@@ -83,24 +84,63 @@ impl LayoutMessageHandler {
|
|||||||
let callback_message = match action {
|
let callback_message = match action {
|
||||||
WidgetValueAction::Commit => (color_button.on_commit.callback)(&()),
|
WidgetValueAction::Commit => (color_button.on_commit.callback)(&()),
|
||||||
WidgetValueAction::Update => {
|
WidgetValueAction::Update => {
|
||||||
let update_value = value.as_object().expect("ColorButton update was not of type: object");
|
let decode_color = |color: &serde_json::map::Map<String, serde_json::value::Value>| -> Option<Color> {
|
||||||
let parsed_color = (|| {
|
let red = color.get("red").and_then(|x| x.as_f64()).map(|x| x as f32);
|
||||||
let is_none = update_value.get("none")?.as_bool()?;
|
let green = color.get("green").and_then(|x| x.as_f64()).map(|x| x as f32);
|
||||||
|
let blue = color.get("blue").and_then(|x| x.as_f64()).map(|x| x as f32);
|
||||||
|
let alpha = color.get("alpha").and_then(|x| x.as_f64()).map(|x| x as f32);
|
||||||
|
|
||||||
if !is_none {
|
if let (Some(red), Some(green), Some(blue), Some(alpha)) = (red, green, blue, alpha) {
|
||||||
Some(Some(Color::from_rgbaf32(
|
if let Some(color) = Color::from_rgbaf32(red, green, blue, alpha) {
|
||||||
update_value.get("red")?.as_f64()? as f32,
|
return Some(color);
|
||||||
update_value.get("green")?.as_f64()? as f32,
|
}
|
||||||
update_value.get("blue")?.as_f64()? as f32,
|
|
||||||
update_value.get("alpha")?.as_f64()? as f32,
|
|
||||||
)?))
|
|
||||||
} else {
|
|
||||||
Some(None)
|
|
||||||
}
|
}
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
(|| {
|
||||||
|
let update_value = value.as_object().expect("ColorButton update was not of type: object");
|
||||||
|
|
||||||
|
// None
|
||||||
|
let is_none = update_value.get("none").and_then(|x| x.as_bool());
|
||||||
|
if is_none == Some(true) {
|
||||||
|
color_button.value = FillChoice::None;
|
||||||
|
return (color_button.on_update.callback)(color_button);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Solid
|
||||||
|
if let Some(color) = decode_color(update_value) {
|
||||||
|
color_button.value = FillChoice::Solid(color);
|
||||||
|
return (color_button.on_update.callback)(color_button);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gradient
|
||||||
|
let gradient = update_value.get("stops").and_then(|x| x.as_array());
|
||||||
|
if let Some(stops) = gradient {
|
||||||
|
let gradient_stops = stops
|
||||||
|
.iter()
|
||||||
|
.filter_map(|stop| {
|
||||||
|
stop.as_object().and_then(|stop| {
|
||||||
|
let position = stop.get("position").and_then(|x| x.as_f64());
|
||||||
|
let color = stop.get("color").and_then(|x| x.as_object());
|
||||||
|
|
||||||
|
if let (Some(position), Some(color_object)) = (position, color) {
|
||||||
|
if let Some(color) = decode_color(color_object) {
|
||||||
|
return Some((position, color));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
color_button.value = FillChoice::Gradient(GradientStops(gradient_stops));
|
||||||
|
return (color_button.on_update.callback)(color_button);
|
||||||
|
}
|
||||||
|
|
||||||
|
panic!("ColorButton update was not able to be parsed with color data: {color_button:?}");
|
||||||
})()
|
})()
|
||||||
.unwrap_or_else(|| panic!("ColorButton update was not able to be parsed with color data: {color_button:?}"));
|
|
||||||
color_button.value = parsed_color;
|
|
||||||
(color_button.on_update.callback)(color_button)
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::messages::input_mapper::utility_types::misc::ActionKeys;
|
|||||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||||
use crate::messages::portfolio::document::node_graph::utility_types::FrontendGraphDataType;
|
use crate::messages::portfolio::document::node_graph::utility_types::FrontendGraphDataType;
|
||||||
|
|
||||||
use graphene_core::raster::color::Color;
|
use graphene_std::vector::style::FillChoice;
|
||||||
use graphite_proc_macros::WidgetBuilder;
|
use graphite_proc_macros::WidgetBuilder;
|
||||||
|
|
||||||
use derivative::*;
|
use derivative::*;
|
||||||
@@ -124,7 +124,7 @@ pub struct TextButton {
|
|||||||
#[derivative(Debug, PartialEq, Default)]
|
#[derivative(Debug, PartialEq, Default)]
|
||||||
pub struct ColorButton {
|
pub struct ColorButton {
|
||||||
#[widget_builder(constructor)]
|
#[widget_builder(constructor)]
|
||||||
pub value: Option<Color>,
|
pub value: FillChoice,
|
||||||
|
|
||||||
pub disabled: bool,
|
pub disabled: bool,
|
||||||
|
|
||||||
@@ -137,7 +137,9 @@ pub struct ColorButton {
|
|||||||
#[derivative(Default(value = "true"))]
|
#[derivative(Default(value = "true"))]
|
||||||
pub allow_none: bool,
|
pub allow_none: bool,
|
||||||
|
|
||||||
|
// TODO: Implement
|
||||||
// pub disabled: bool,
|
// pub disabled: bool,
|
||||||
|
//
|
||||||
pub tooltip: String,
|
pub tooltip: String,
|
||||||
|
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ use graphene_core::raster::BlendMode;
|
|||||||
use graphene_core::raster::ImageFrame;
|
use graphene_core::raster::ImageFrame;
|
||||||
use graphene_core::renderer::ClickTarget;
|
use graphene_core::renderer::ClickTarget;
|
||||||
use graphene_core::vector::style::ViewMode;
|
use graphene_core::vector::style::ViewMode;
|
||||||
|
use graphene_std::vector::style::{Fill, FillType, Gradient};
|
||||||
|
|
||||||
use glam::{DAffine2, DVec2, IVec2};
|
use glam::{DAffine2, DVec2, IVec2};
|
||||||
|
|
||||||
@@ -1222,9 +1223,68 @@ impl DocumentMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn deserialize_document(serialized_content: &str) -> Result<Self, EditorError> {
|
pub fn deserialize_document(serialized_content: &str) -> Result<Self, EditorError> {
|
||||||
serde_json::from_str(serialized_content).map_err(|e| EditorError::DocumentDeserialization(e.to_string()))
|
// serde_json::from_str(serialized_content).map_err(|e| EditorError::DocumentDeserialization(e.to_string()))
|
||||||
|
|
||||||
// TODO: Use this to upgrade demo artwork with outdated document node internals from their definitions. Delete when it's no longer needed.
|
match serde_json::from_str::<Self>(serialized_content).map_err(|e| EditorError::DocumentDeserialization(e.to_string())) {
|
||||||
|
Ok(mut document) => {
|
||||||
|
for (_, node) in &mut document.network.nodes {
|
||||||
|
// Upgrade Fill nodes to the format change in #1778
|
||||||
|
// TODO: Eventually remove this (probably starting late 2024)
|
||||||
|
if node.name == "Fill" && node.inputs.len() == 8 {
|
||||||
|
let node_definition = crate::messages::portfolio::document::node_graph::document_node_types::resolve_document_node_type(&node.name).unwrap();
|
||||||
|
let default_definition_node = node_definition.default_document_node();
|
||||||
|
|
||||||
|
node.implementation = default_definition_node.implementation.clone();
|
||||||
|
let old_inputs = std::mem::replace(&mut node.inputs, default_definition_node.inputs.clone());
|
||||||
|
|
||||||
|
node.inputs[0] = old_inputs[0].clone();
|
||||||
|
|
||||||
|
let Some(fill_type) = old_inputs[1].as_value().cloned() else { continue };
|
||||||
|
let TaggedValue::FillType(fill_type) = fill_type else { continue };
|
||||||
|
let Some(solid_color) = old_inputs[2].as_value().cloned() else { continue };
|
||||||
|
let TaggedValue::OptionalColor(solid_color) = solid_color else { continue };
|
||||||
|
let Some(gradient_type) = old_inputs[3].as_value().cloned() else { continue };
|
||||||
|
let TaggedValue::GradientType(gradient_type) = gradient_type else { continue };
|
||||||
|
let Some(start) = old_inputs[4].as_value().cloned() else { continue };
|
||||||
|
let TaggedValue::DVec2(start) = start else { continue };
|
||||||
|
let Some(end) = old_inputs[5].as_value().cloned() else { continue };
|
||||||
|
let TaggedValue::DVec2(end) = end else { continue };
|
||||||
|
let Some(transform) = old_inputs[6].as_value().cloned() else { continue };
|
||||||
|
let TaggedValue::DAffine2(transform) = transform else { continue };
|
||||||
|
let Some(positions) = old_inputs[7].as_value().cloned() else { continue };
|
||||||
|
let TaggedValue::GradientStops(positions) = positions else { continue };
|
||||||
|
|
||||||
|
let fill = match (fill_type, solid_color) {
|
||||||
|
(FillType::Solid, None) => Fill::None,
|
||||||
|
(FillType::Solid, Some(color)) => Fill::Solid(color),
|
||||||
|
(FillType::Gradient, _) => Fill::Gradient(Gradient {
|
||||||
|
stops: positions,
|
||||||
|
gradient_type,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
transform,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
node.inputs[1] = NodeInput::value(TaggedValue::Fill(fill.clone()), false);
|
||||||
|
match fill {
|
||||||
|
Fill::None => {
|
||||||
|
node.inputs[2] = NodeInput::value(TaggedValue::OptionalColor(None), false);
|
||||||
|
}
|
||||||
|
Fill::Solid(color) => {
|
||||||
|
node.inputs[2] = NodeInput::value(TaggedValue::OptionalColor(Some(color)), false);
|
||||||
|
}
|
||||||
|
Fill::Gradient(gradient) => {
|
||||||
|
node.inputs[3] = NodeInput::value(TaggedValue::Gradient(gradient), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(document)
|
||||||
|
}
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: This can be used, if uncommented, to upgrade demo artwork with outdated document node internals from their definitions. Delete when it's no longer needed.
|
||||||
// Used for upgrading old internal networks for demo artwork nodes. Will reset all node internals for any opened file
|
// Used for upgrading old internal networks for demo artwork nodes. Will reset all node internals for any opened file
|
||||||
// match serde_json::from_str::<Self>(serialized_content).map_err(|e| EditorError::DocumentDeserialization(e.to_string())) {
|
// match serde_json::from_str::<Self>(serialized_content).map_err(|e| EditorError::DocumentDeserialization(e.to_string())) {
|
||||||
// Ok(mut document) => {
|
// Ok(mut document) => {
|
||||||
|
|||||||
+7
-2
@@ -12,6 +12,7 @@ use graphene_core::text::Font;
|
|||||||
use graphene_core::vector::style::{Fill, Gradient, GradientType, LineCap, LineJoin, Stroke};
|
use graphene_core::vector::style::{Fill, Gradient, GradientType, LineCap, LineJoin, Stroke};
|
||||||
use graphene_core::Color;
|
use graphene_core::Color;
|
||||||
use graphene_std::vector::convert_usvg_path;
|
use graphene_std::vector::convert_usvg_path;
|
||||||
|
use graphene_std::vector::style::GradientStops;
|
||||||
|
|
||||||
use glam::{DAffine2, DVec2, IVec2};
|
use glam::{DAffine2, DVec2, IVec2};
|
||||||
|
|
||||||
@@ -888,13 +889,15 @@ fn apply_usvg_fill(fill: &Option<usvg::Fill>, modify_inputs: &mut ModifyInputsCo
|
|||||||
let layer = [transform.inverse().transform_point2(document[0]), transform.inverse().transform_point2(document[1])];
|
let layer = [transform.inverse().transform_point2(document[0]), transform.inverse().transform_point2(document[1])];
|
||||||
|
|
||||||
let [start, end] = [bounds_transform.inverse().transform_point2(layer[0]), bounds_transform.inverse().transform_point2(layer[1])];
|
let [start, end] = [bounds_transform.inverse().transform_point2(layer[0]), bounds_transform.inverse().transform_point2(layer[1])];
|
||||||
|
let stops = linear.stops.iter().map(|stop| (stop.offset.get() as f64, usvg_color(stop.color, stop.opacity.get()))).collect();
|
||||||
|
let stops = GradientStops(stops);
|
||||||
|
|
||||||
Fill::Gradient(Gradient {
|
Fill::Gradient(Gradient {
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
transform: DAffine2::IDENTITY,
|
transform: DAffine2::IDENTITY,
|
||||||
gradient_type: GradientType::Linear,
|
gradient_type: GradientType::Linear,
|
||||||
positions: linear.stops.iter().map(|stop| (stop.offset.get() as f64, usvg_color(stop.color, stop.opacity.get()))).collect(),
|
stops,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
usvg::Paint::RadialGradient(radial) => {
|
usvg::Paint::RadialGradient(radial) => {
|
||||||
@@ -911,13 +914,15 @@ fn apply_usvg_fill(fill: &Option<usvg::Fill>, modify_inputs: &mut ModifyInputsCo
|
|||||||
let layer = [transform.inverse().transform_point2(document[0]), transform.inverse().transform_point2(document[1])];
|
let layer = [transform.inverse().transform_point2(document[0]), transform.inverse().transform_point2(document[1])];
|
||||||
|
|
||||||
let [start, end] = [bounds_transform.inverse().transform_point2(layer[0]), bounds_transform.inverse().transform_point2(layer[1])];
|
let [start, end] = [bounds_transform.inverse().transform_point2(layer[0]), bounds_transform.inverse().transform_point2(layer[1])];
|
||||||
|
let stops = radial.stops.iter().map(|stop| (stop.offset.get() as f64, usvg_color(stop.color, stop.opacity.get()))).collect();
|
||||||
|
let stops = GradientStops(stops);
|
||||||
|
|
||||||
Fill::Gradient(Gradient {
|
Fill::Gradient(Gradient {
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
transform: DAffine2::IDENTITY,
|
transform: DAffine2::IDENTITY,
|
||||||
gradient_type: GradientType::Radial,
|
gradient_type: GradientType::Radial,
|
||||||
positions: radial.stops.iter().map(|stop| (stop.offset.get() as f64, usvg_color(stop.color, stop.opacity.get()))).collect(),
|
stops,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
usvg::Paint::Pattern(_) => {
|
usvg::Paint::Pattern(_) => {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use graphene_core::raster::{BlendMode, ImageFrame};
|
|||||||
use graphene_core::text::Font;
|
use graphene_core::text::Font;
|
||||||
use graphene_core::uuid::ManipulatorGroupId;
|
use graphene_core::uuid::ManipulatorGroupId;
|
||||||
use graphene_core::vector::brush_stroke::BrushStroke;
|
use graphene_core::vector::brush_stroke::BrushStroke;
|
||||||
use graphene_core::vector::style::{Fill, FillType, Stroke};
|
use graphene_core::vector::style::{Fill, Stroke};
|
||||||
use graphene_core::Type;
|
use graphene_core::Type;
|
||||||
use graphene_core::{Artboard, Color};
|
use graphene_core::{Artboard, Color};
|
||||||
use graphene_std::vector::ManipulatorPointId;
|
use graphene_std::vector::ManipulatorPointId;
|
||||||
@@ -538,23 +538,24 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn fill_set(&mut self, fill: Fill) {
|
pub fn fill_set(&mut self, fill: Fill) {
|
||||||
|
let fill_index = 1;
|
||||||
|
let backup_color_index = 2;
|
||||||
|
let backup_gradient_index = 3;
|
||||||
|
|
||||||
self.modify_inputs("Fill", false, |inputs, _node_id, _metadata| {
|
self.modify_inputs("Fill", false, |inputs, _node_id, _metadata| {
|
||||||
let fill_type = match fill {
|
match &fill {
|
||||||
Fill::None | Fill::Solid(_) => FillType::Solid,
|
Fill::None => {
|
||||||
Fill::Gradient(_) => FillType::Gradient,
|
inputs[backup_color_index] = NodeInput::value(TaggedValue::OptionalColor(None), false);
|
||||||
};
|
}
|
||||||
inputs[1] = NodeInput::value(TaggedValue::FillType(fill_type), false);
|
Fill::Solid(color) => {
|
||||||
if Fill::None == fill {
|
inputs[backup_color_index] = NodeInput::value(TaggedValue::OptionalColor(Some(*color)), false);
|
||||||
inputs[2] = NodeInput::value(TaggedValue::OptionalColor(None), false);
|
}
|
||||||
} else if let Fill::Solid(color) = fill {
|
Fill::Gradient(gradient) => {
|
||||||
inputs[2] = NodeInput::value(TaggedValue::OptionalColor(Some(color)), false);
|
inputs[backup_gradient_index] = NodeInput::value(TaggedValue::Gradient(gradient.clone()), false);
|
||||||
} else if let Fill::Gradient(gradient) = fill {
|
}
|
||||||
inputs[3] = NodeInput::value(TaggedValue::GradientType(gradient.gradient_type), false);
|
|
||||||
inputs[4] = NodeInput::value(TaggedValue::DVec2(gradient.start), false);
|
|
||||||
inputs[5] = NodeInput::value(TaggedValue::DVec2(gradient.end), false);
|
|
||||||
inputs[6] = NodeInput::value(TaggedValue::DAffine2(gradient.transform), false);
|
|
||||||
inputs[7] = NodeInput::value(TaggedValue::GradientPositions(gradient.positions), false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inputs[fill_index] = NodeInput::value(TaggedValue::Fill(fill), false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2389,16 +2389,26 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
|||||||
DocumentNodeDefinition {
|
DocumentNodeDefinition {
|
||||||
name: "Fill",
|
name: "Fill",
|
||||||
category: "Vector",
|
category: "Vector",
|
||||||
implementation: DocumentNodeImplementation::proto("graphene_core::vector::SetFillNode<_, _, _, _, _, _, _>"),
|
implementation: DocumentNodeImplementation::Network(NodeNetwork {
|
||||||
|
exports: vec![NodeInput::node(NodeId(0), 0)],
|
||||||
|
nodes: vec![DocumentNode {
|
||||||
|
name: "Set Fill".to_string(),
|
||||||
|
inputs: vec![NodeInput::network(concrete!(VectorData), 0), NodeInput::network(concrete!(vector::style::Fill), 1)],
|
||||||
|
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::vector::SetFillNode<_>")),
|
||||||
|
..Default::default()
|
||||||
|
}]
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(id, node)| (NodeId(id as u64), node))
|
||||||
|
.collect(),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
inputs: vec![
|
inputs: vec![
|
||||||
DocumentInputType::value("Vector Data", TaggedValue::VectorData(graphene_core::vector::VectorData::empty()), true),
|
DocumentInputType::value("Vector Data", TaggedValue::VectorData(graphene_core::vector::VectorData::empty()), true),
|
||||||
DocumentInputType::value("Fill Type", TaggedValue::FillType(vector::style::FillType::Solid), false),
|
DocumentInputType::value("Fill", TaggedValue::Fill(vector::style::Fill::Solid(Color::BLACK)), false),
|
||||||
DocumentInputType::value("Solid Color", TaggedValue::OptionalColor(None), false),
|
// These backup values aren't exposed to the user, but are used to store the previous fill choices so the user can flip back from Solid to Gradient (or vice versa) without losing their settings
|
||||||
DocumentInputType::value("Gradient Type", TaggedValue::GradientType(vector::style::GradientType::Linear), false),
|
DocumentInputType::value("Backup Color", TaggedValue::OptionalColor(Some(Color::BLACK)), false),
|
||||||
DocumentInputType::value("Start", TaggedValue::DVec2(DVec2::new(0., 0.5)), false),
|
DocumentInputType::value("Backup Gradient", TaggedValue::Gradient(Default::default()), false),
|
||||||
DocumentInputType::value("End", TaggedValue::DVec2(DVec2::new(1., 0.5)), false),
|
|
||||||
DocumentInputType::value("Transform", TaggedValue::DAffine2(DAffine2::IDENTITY), false),
|
|
||||||
DocumentInputType::value("Positions", TaggedValue::GradientPositions(vec![(0., Color::BLACK), (1., Color::WHITE)]), false),
|
|
||||||
],
|
],
|
||||||
outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::VectorData)],
|
outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::VectorData)],
|
||||||
properties: node_properties::fill_properties,
|
properties: node_properties::fill_properties,
|
||||||
@@ -2492,7 +2502,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
|||||||
DocumentInputType::value("Instance", TaggedValue::VectorData(graphene_core::vector::VectorData::empty()), true),
|
DocumentInputType::value("Instance", TaggedValue::VectorData(graphene_core::vector::VectorData::empty()), true),
|
||||||
DocumentInputType::value("Random Scale Min", TaggedValue::F64(1.), false),
|
DocumentInputType::value("Random Scale Min", TaggedValue::F64(1.), false),
|
||||||
DocumentInputType::value("Random Scale Max", TaggedValue::F64(1.), false),
|
DocumentInputType::value("Random Scale Max", TaggedValue::F64(1.), false),
|
||||||
DocumentInputType::value("Random Scale Bias", TaggedValue::F64(1.), false),
|
DocumentInputType::value("Random Scale Bias", TaggedValue::F64(0.), false),
|
||||||
DocumentInputType::value("Random Rotation", TaggedValue::F64(0.), false),
|
DocumentInputType::value("Random Rotation", TaggedValue::F64(0.), false),
|
||||||
],
|
],
|
||||||
outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::VectorData)],
|
outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::VectorData)],
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ use graphene_core::raster::{
|
|||||||
};
|
};
|
||||||
use graphene_core::text::Font;
|
use graphene_core::text::Font;
|
||||||
use graphene_core::vector::misc::CentroidType;
|
use graphene_core::vector::misc::CentroidType;
|
||||||
use graphene_core::vector::style::{FillType, GradientType, LineCap, LineJoin};
|
use graphene_core::vector::style::{GradientType, LineCap, LineJoin};
|
||||||
|
use graphene_std::vector::style::{Fill, FillChoice};
|
||||||
|
|
||||||
use glam::{DVec2, IVec2, UVec2};
|
use glam::{DVec2, IVec2, UVec2};
|
||||||
use graphene_std::vector::misc::BooleanOperation;
|
use graphene_std::vector::misc::BooleanOperation;
|
||||||
@@ -692,206 +693,48 @@ fn line_join_widget(document_node: &DocumentNode, node_id: NodeId, index: usize,
|
|||||||
LayoutGroup::Row { widgets }
|
LayoutGroup::Row { widgets }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fill_type_widget(document_node: &DocumentNode, node_id: NodeId, index: usize) -> LayoutGroup {
|
|
||||||
let mut widgets = start_widgets(document_node, node_id, index, "Fill Type", FrontendGraphDataType::General, true);
|
|
||||||
if let &NodeInput::Value {
|
|
||||||
tagged_value: TaggedValue::FillType(fill_type),
|
|
||||||
exposed: false,
|
|
||||||
} = &document_node.inputs[index]
|
|
||||||
{
|
|
||||||
let entries = vec![
|
|
||||||
RadioEntryData::new("solid")
|
|
||||||
.label("Solid")
|
|
||||||
.on_update(update_value(move |_| TaggedValue::FillType(FillType::Solid), node_id, index))
|
|
||||||
.on_commit(commit_value),
|
|
||||||
RadioEntryData::new("gradient")
|
|
||||||
.label("Gradient")
|
|
||||||
.on_update(update_value(move |_| TaggedValue::FillType(FillType::Gradient), node_id, index))
|
|
||||||
.on_commit(commit_value),
|
|
||||||
];
|
|
||||||
|
|
||||||
widgets.extend_from_slice(&[
|
|
||||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
|
||||||
RadioInput::new(entries)
|
|
||||||
.selected_index(match fill_type {
|
|
||||||
FillType::Solid => Some(0),
|
|
||||||
FillType::Gradient => Some(1),
|
|
||||||
})
|
|
||||||
.widget_holder(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
LayoutGroup::Row { widgets }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn gradient_type_widget(document_node: &DocumentNode, node_id: NodeId, index: usize) -> LayoutGroup {
|
|
||||||
let mut widgets = start_widgets(document_node, node_id, index, "Gradient Type", FrontendGraphDataType::General, true);
|
|
||||||
if let &NodeInput::Value {
|
|
||||||
tagged_value: TaggedValue::GradientType(gradient_type),
|
|
||||||
exposed: false,
|
|
||||||
} = &document_node.inputs[index]
|
|
||||||
{
|
|
||||||
let entries = vec![
|
|
||||||
RadioEntryData::new("linear")
|
|
||||||
.label("Linear")
|
|
||||||
.on_update(update_value(move |_| TaggedValue::GradientType(GradientType::Linear), node_id, index))
|
|
||||||
.on_commit(commit_value),
|
|
||||||
RadioEntryData::new("radial")
|
|
||||||
.label("Radial")
|
|
||||||
.on_update(update_value(move |_| TaggedValue::GradientType(GradientType::Radial), node_id, index))
|
|
||||||
.on_commit(commit_value),
|
|
||||||
];
|
|
||||||
|
|
||||||
widgets.extend_from_slice(&[
|
|
||||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
|
||||||
RadioInput::new(entries).selected_index(Some(gradient_type as u32)).widget_holder(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
LayoutGroup::Row { widgets }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn gradient_row(row: &mut Vec<WidgetHolder>, positions: &Vec<(f64, Color)>, index: usize, node_id: NodeId, input_index: usize) {
|
|
||||||
let label = TextLabel::new(format!("Gradient: {:.0}%", positions[index].0 * 100.)).tooltip("Adjustable by dragging the gradient stops in the viewport with the Gradient tool active");
|
|
||||||
row.push(label.widget_holder());
|
|
||||||
let on_update = {
|
|
||||||
let positions = positions.clone();
|
|
||||||
move |color_button: &ColorButton| {
|
|
||||||
let mut new_positions = positions.clone();
|
|
||||||
new_positions[index].1 = color_button.value.unwrap();
|
|
||||||
TaggedValue::GradientPositions(new_positions)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let color = ColorButton::new(Some(positions[index].1))
|
|
||||||
.on_update(update_value(on_update, node_id, input_index))
|
|
||||||
.on_commit(commit_value)
|
|
||||||
.allow_none(false);
|
|
||||||
add_blank_assist(row);
|
|
||||||
row.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
|
||||||
row.push(color.widget_holder());
|
|
||||||
|
|
||||||
let mut skip_separator = false;
|
|
||||||
// Remove button
|
|
||||||
if positions.len() != index + 1 && index != 0 {
|
|
||||||
let on_update = {
|
|
||||||
let in_positions = positions.clone();
|
|
||||||
move |_: &IconButton| {
|
|
||||||
let mut new_positions = in_positions.clone();
|
|
||||||
new_positions.remove(index);
|
|
||||||
TaggedValue::GradientPositions(new_positions)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
skip_separator = true;
|
|
||||||
row.push(Separator::new(SeparatorType::Related).widget_holder());
|
|
||||||
row.push(
|
|
||||||
IconButton::new("Remove", 16)
|
|
||||||
.tooltip("Remove this gradient stop")
|
|
||||||
.on_update(update_value(on_update, node_id, input_index))
|
|
||||||
.on_commit(commit_value)
|
|
||||||
.widget_holder(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// Add button
|
|
||||||
if positions.len() != index + 1 {
|
|
||||||
let on_update = {
|
|
||||||
let positions = positions.clone();
|
|
||||||
move |_: &IconButton| {
|
|
||||||
let mut new_positions = positions.clone();
|
|
||||||
|
|
||||||
// Blend linearly between the two colors.
|
|
||||||
let get_color = |index: usize| match (new_positions[index].1, new_positions.get(index + 1).map(|x| x.1)) {
|
|
||||||
(a, Some(b)) => Color::from_rgbaf32_unchecked((a.r() + b.r()) / 2., (a.g() + b.g()) / 2., (a.b() + b.b()) / 2., ((a.a() + b.a()) / 2.).clamp(0., 1.)),
|
|
||||||
(_, None) => Color::WHITE,
|
|
||||||
};
|
|
||||||
let get_pos = |index: usize| (new_positions[index].0 + new_positions.get(index + 1).map(|v| v.0).unwrap_or(1.)) / 2.;
|
|
||||||
|
|
||||||
new_positions.push((get_pos(index), get_color(index)));
|
|
||||||
|
|
||||||
new_positions.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
|
|
||||||
|
|
||||||
TaggedValue::GradientPositions(new_positions)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if !skip_separator {
|
|
||||||
row.push(Separator::new(SeparatorType::Related).widget_holder());
|
|
||||||
}
|
|
||||||
row.push(
|
|
||||||
IconButton::new("Add", 16)
|
|
||||||
.tooltip("Add a gradient stop after this")
|
|
||||||
.on_update(update_value(on_update, node_id, input_index))
|
|
||||||
.on_commit(commit_value)
|
|
||||||
.widget_holder(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn gradient_positions(rows: &mut Vec<LayoutGroup>, document_node: &DocumentNode, name: &str, node_id: NodeId, input_index: usize) {
|
|
||||||
let mut widgets = vec![expose_widget(node_id, input_index, FrontendGraphDataType::General, document_node.inputs[input_index].is_exposed())];
|
|
||||||
if let NodeInput::Value {
|
|
||||||
tagged_value: TaggedValue::GradientPositions(gradient_positions),
|
|
||||||
exposed: false,
|
|
||||||
} = &document_node.inputs[input_index]
|
|
||||||
{
|
|
||||||
for index in 0..gradient_positions.len() {
|
|
||||||
gradient_row(&mut widgets, gradient_positions, index, node_id, input_index);
|
|
||||||
|
|
||||||
let widgets = std::mem::take(&mut widgets);
|
|
||||||
rows.push(LayoutGroup::Row { widgets });
|
|
||||||
}
|
|
||||||
let on_update = {
|
|
||||||
let gradient_positions = gradient_positions.clone();
|
|
||||||
move |_: &TextButton| {
|
|
||||||
let mut new_positions = gradient_positions.clone();
|
|
||||||
new_positions = new_positions.iter().map(|(distance, color)| (1. - distance, *color)).collect();
|
|
||||||
new_positions.reverse();
|
|
||||||
TaggedValue::GradientPositions(new_positions)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let invert = TextButton::new("Invert")
|
|
||||||
.icon(Some("Swap".into()))
|
|
||||||
.tooltip("Reverse the order of each color stop")
|
|
||||||
.on_update(update_value(on_update, node_id, input_index))
|
|
||||||
.on_commit(commit_value)
|
|
||||||
.widget_holder();
|
|
||||||
|
|
||||||
if widgets.is_empty() {
|
|
||||||
widgets.push(TextLabel::new("").widget_holder());
|
|
||||||
add_blank_assist(&mut widgets);
|
|
||||||
}
|
|
||||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
|
||||||
widgets.push(invert);
|
|
||||||
|
|
||||||
rows.push(LayoutGroup::Row { widgets });
|
|
||||||
} else {
|
|
||||||
widgets.push(TextLabel::new(name).widget_holder());
|
|
||||||
rows.push(LayoutGroup::Row { widgets })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn color_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, color_props: ColorButton, blank_assist: bool) -> LayoutGroup {
|
fn color_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, color_props: ColorButton, blank_assist: bool) -> LayoutGroup {
|
||||||
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
|
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
|
||||||
|
|
||||||
if let NodeInput::Value { tagged_value, exposed: false } = &document_node.inputs[index] {
|
// Return early with just the label if the input is exposed to the graph, meaning we don't want to show the color picker widget in the Properties panel
|
||||||
if let &TaggedValue::Color(x) = tagged_value {
|
let NodeInput::Value { tagged_value, exposed: false } = &document_node.inputs[index] else {
|
||||||
widgets.extend_from_slice(&[
|
return LayoutGroup::Row { widgets };
|
||||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
};
|
||||||
color_props
|
|
||||||
.value(Some(x as Color))
|
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||||
.on_update(update_value(|x: &ColorButton| TaggedValue::Color(x.value.unwrap()), node_id, index))
|
|
||||||
.on_commit(commit_value)
|
match tagged_value {
|
||||||
.widget_holder(),
|
TaggedValue::Color(color) => widgets.push(
|
||||||
])
|
color_props
|
||||||
} else if let &TaggedValue::OptionalColor(x) = tagged_value {
|
.value(FillChoice::Solid(*color))
|
||||||
widgets.extend_from_slice(&[
|
.on_update(update_value(|x: &ColorButton| TaggedValue::Color(x.value.as_solid().unwrap_or_default()), node_id, index))
|
||||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
.on_commit(commit_value)
|
||||||
color_props
|
.widget_holder(),
|
||||||
.value(x)
|
),
|
||||||
.on_update(update_value(|x: &ColorButton| TaggedValue::OptionalColor(x.value), node_id, index))
|
TaggedValue::OptionalColor(color) => widgets.push(
|
||||||
.on_commit(commit_value)
|
color_props
|
||||||
.widget_holder(),
|
.value(match color {
|
||||||
])
|
Some(color) => FillChoice::Solid(*color),
|
||||||
}
|
None => FillChoice::None,
|
||||||
|
})
|
||||||
|
.on_update(update_value(|x: &ColorButton| TaggedValue::OptionalColor(x.value.as_solid()), node_id, index))
|
||||||
|
.on_commit(commit_value)
|
||||||
|
.widget_holder(),
|
||||||
|
),
|
||||||
|
TaggedValue::GradientStops(ref x) => widgets.push(
|
||||||
|
color_props
|
||||||
|
.value(FillChoice::Gradient(x.clone()))
|
||||||
|
.on_update(update_value(
|
||||||
|
|x: &ColorButton| TaggedValue::GradientStops(x.value.as_gradient().cloned().unwrap_or_default()),
|
||||||
|
node_id,
|
||||||
|
index,
|
||||||
|
))
|
||||||
|
.on_commit(commit_value)
|
||||||
|
.widget_holder(),
|
||||||
|
),
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
LayoutGroup::Row { widgets }
|
LayoutGroup::Row { widgets }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2460,37 +2303,141 @@ pub fn morph_properties(document_node: &DocumentNode, node_id: NodeId, _context:
|
|||||||
|
|
||||||
/// Fill Node Widgets LayoutGroup
|
/// Fill Node Widgets LayoutGroup
|
||||||
pub fn fill_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
pub fn fill_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||||
let fill_type_index = 1;
|
let fill_index = 1;
|
||||||
let solid_color_index = 2;
|
let backup_color_index = 2;
|
||||||
let gradient_type_index = 3;
|
let backup_gradient_index = 3;
|
||||||
let positions_index = 7;
|
|
||||||
|
|
||||||
let fill_type = if let &NodeInput::Value {
|
let mut widgets_first_row = start_widgets(document_node, node_id, fill_index, "Fill", FrontendGraphDataType::General, true);
|
||||||
tagged_value: TaggedValue::FillType(fill_type),
|
|
||||||
..
|
let (fill, backup_color, backup_gradient) = if let (
|
||||||
} = &document_node.inputs[fill_type_index]
|
NodeInput::Value {
|
||||||
{
|
tagged_value: TaggedValue::Fill(fill),
|
||||||
Some(fill_type)
|
..
|
||||||
|
},
|
||||||
|
NodeInput::Value {
|
||||||
|
tagged_value: TaggedValue::OptionalColor(backup_color),
|
||||||
|
..
|
||||||
|
},
|
||||||
|
NodeInput::Value {
|
||||||
|
tagged_value: TaggedValue::Gradient(backup_gradient),
|
||||||
|
..
|
||||||
|
},
|
||||||
|
) = (
|
||||||
|
&document_node.inputs[fill_index],
|
||||||
|
&document_node.inputs[backup_color_index],
|
||||||
|
&document_node.inputs[backup_gradient_index],
|
||||||
|
) {
|
||||||
|
(fill, backup_color, backup_gradient)
|
||||||
} else {
|
} else {
|
||||||
None
|
return vec![LayoutGroup::Row { widgets: widgets_first_row }];
|
||||||
};
|
};
|
||||||
|
let fill2 = fill.clone();
|
||||||
|
let backup_color_fill: Fill = backup_color.clone().into();
|
||||||
|
let backup_gradient_fill: Fill = backup_gradient.clone().into();
|
||||||
|
|
||||||
let mut widgets = Vec::new();
|
widgets_first_row.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||||
let gradient = fill_type == Some(graphene_core::vector::style::FillType::Gradient);
|
widgets_first_row.push(
|
||||||
let solid = fill_type == Some(graphene_core::vector::style::FillType::Solid);
|
ColorButton::default()
|
||||||
|
.value(fill.clone().into())
|
||||||
|
.on_update(move |x: &ColorButton| {
|
||||||
|
Message::Batched(Box::new([
|
||||||
|
match &fill2 {
|
||||||
|
Fill::None => NodeGraphMessage::SetInputValue {
|
||||||
|
node_id,
|
||||||
|
input_index: backup_color_index,
|
||||||
|
value: TaggedValue::OptionalColor(None),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
Fill::Solid(color) => NodeGraphMessage::SetInputValue {
|
||||||
|
node_id,
|
||||||
|
input_index: backup_color_index,
|
||||||
|
value: TaggedValue::OptionalColor(Some(color.clone())),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
Fill::Gradient(gradient) => NodeGraphMessage::SetInputValue {
|
||||||
|
node_id,
|
||||||
|
input_index: backup_gradient_index,
|
||||||
|
value: TaggedValue::Gradient(gradient.clone()),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
},
|
||||||
|
NodeGraphMessage::SetInputValue {
|
||||||
|
node_id,
|
||||||
|
input_index: fill_index,
|
||||||
|
value: TaggedValue::Fill(x.value.to_fill(fill2.as_gradient())),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
]))
|
||||||
|
})
|
||||||
|
.on_commit(commit_value)
|
||||||
|
.widget_holder(),
|
||||||
|
);
|
||||||
|
let mut widgets = vec![LayoutGroup::Row { widgets: widgets_first_row }];
|
||||||
|
|
||||||
let fill_type_switch = fill_type_widget(document_node, node_id, fill_type_index);
|
let fill_type_switch = {
|
||||||
|
let mut row = vec![TextLabel::new("").widget_holder()];
|
||||||
|
add_blank_assist(&mut row);
|
||||||
|
|
||||||
|
let entries = vec![
|
||||||
|
RadioEntryData::new("solid")
|
||||||
|
.label("Solid")
|
||||||
|
.on_update(update_value(move |_| TaggedValue::Fill(backup_color_fill.clone()), node_id, fill_index))
|
||||||
|
.on_commit(commit_value),
|
||||||
|
RadioEntryData::new("gradient")
|
||||||
|
.label("Gradient")
|
||||||
|
.on_update(update_value(move |_| TaggedValue::Fill(backup_gradient_fill.clone()), node_id, fill_index))
|
||||||
|
.on_commit(commit_value),
|
||||||
|
];
|
||||||
|
|
||||||
|
row.extend_from_slice(&[
|
||||||
|
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||||
|
RadioInput::new(entries).selected_index(Some(if fill.as_gradient().is_some() { 1 } else { 0 })).widget_holder(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
LayoutGroup::Row { widgets: row }
|
||||||
|
};
|
||||||
widgets.push(fill_type_switch);
|
widgets.push(fill_type_switch);
|
||||||
|
|
||||||
if fill_type.is_none() || solid {
|
if let Fill::Gradient(gradient) = fill {
|
||||||
let solid_color = color_widget(document_node, node_id, solid_color_index, "Color", ColorButton::default(), true);
|
let mut row = vec![TextLabel::new("").widget_holder()];
|
||||||
widgets.push(solid_color);
|
add_blank_assist(&mut row);
|
||||||
}
|
|
||||||
|
|
||||||
if fill_type.is_none() || gradient {
|
let new_gradient1 = gradient.clone();
|
||||||
let gradient_type_switch = gradient_type_widget(document_node, node_id, gradient_type_index);
|
let new_gradient2 = gradient.clone();
|
||||||
widgets.push(gradient_type_switch);
|
|
||||||
gradient_positions(&mut widgets, document_node, "Gradient Positions", node_id, positions_index);
|
let entries = vec![
|
||||||
|
RadioEntryData::new("linear")
|
||||||
|
.label("Linear")
|
||||||
|
.on_update(update_value(
|
||||||
|
move |_| {
|
||||||
|
let mut new_gradient = new_gradient1.clone();
|
||||||
|
new_gradient.gradient_type = GradientType::Linear;
|
||||||
|
TaggedValue::Fill(Fill::Gradient(new_gradient))
|
||||||
|
},
|
||||||
|
node_id,
|
||||||
|
fill_index,
|
||||||
|
))
|
||||||
|
.on_commit(commit_value),
|
||||||
|
RadioEntryData::new("radial")
|
||||||
|
.label("Radial")
|
||||||
|
.on_update(update_value(
|
||||||
|
move |_| {
|
||||||
|
let mut new_gradient = new_gradient2.clone();
|
||||||
|
new_gradient.gradient_type = GradientType::Radial;
|
||||||
|
TaggedValue::Fill(Fill::Gradient(new_gradient))
|
||||||
|
},
|
||||||
|
node_id,
|
||||||
|
fill_index,
|
||||||
|
))
|
||||||
|
.on_commit(commit_value),
|
||||||
|
];
|
||||||
|
|
||||||
|
row.extend_from_slice(&[
|
||||||
|
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||||
|
RadioInput::new(entries).selected_index(Some(gradient.gradient_type as u32)).widget_holder(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
widgets.push(LayoutGroup::Row { widgets: row });
|
||||||
}
|
}
|
||||||
|
|
||||||
widgets
|
widgets
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use graphene_core::raster::color::Color;
|
|||||||
use graphene_core::renderer::Quad;
|
use graphene_core::renderer::Quad;
|
||||||
|
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
|
use graphene_std::vector::style::FillChoice;
|
||||||
|
|
||||||
fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, spacing: DVec2) {
|
fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, spacing: DVec2) {
|
||||||
let origin = document.snapping_state.grid.origin;
|
let origin = document.snapping_state.grid.origin;
|
||||||
@@ -226,7 +227,7 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
|
|||||||
};
|
};
|
||||||
let update_color = |grid, update: fn(&mut GridSnapping) -> Option<&mut Color>| {
|
let update_color = |grid, update: fn(&mut GridSnapping) -> Option<&mut Color>| {
|
||||||
update_val::<ColorButton>(grid, move |grid, color| {
|
update_val::<ColorButton>(grid, move |grid, color| {
|
||||||
if let Some(color) = color.value {
|
if let FillChoice::Solid(color) = color.value {
|
||||||
if let Some(update_color) = update(grid) {
|
if let Some(update_color) = update(grid) {
|
||||||
*update_color = color;
|
*update_color = color;
|
||||||
}
|
}
|
||||||
@@ -276,7 +277,7 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
|
|||||||
Separator::new(SeparatorType::Related).widget_holder(),
|
Separator::new(SeparatorType::Related).widget_holder(),
|
||||||
]);
|
]);
|
||||||
color_widgets.push(
|
color_widgets.push(
|
||||||
ColorButton::new(Some(grid.grid_color))
|
ColorButton::new(FillChoice::Solid(grid.grid_color))
|
||||||
.tooltip("Grid display color")
|
.tooltip("Grid display color")
|
||||||
.on_update(update_color(grid, |grid| Some(&mut grid.grid_color)))
|
.on_update(update_color(grid, |grid| Some(&mut grid.grid_color)))
|
||||||
.widget_holder(),
|
.widget_holder(),
|
||||||
|
|||||||
@@ -375,7 +375,6 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
|
|||||||
self.load_document(document, document_id, responses);
|
self.load_document(document, document_id, responses);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("Failed to open document: {e}");
|
|
||||||
if !document_is_auto_saved {
|
if !document_is_auto_saved {
|
||||||
responses.add(DialogMessage::DisplayDialogError {
|
responses.add(DialogMessage::DisplayDialogError {
|
||||||
title: "Failed to open document".to_string(),
|
title: "Failed to open document".to_string(),
|
||||||
@@ -612,7 +611,7 @@ impl PortfolioMessageHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Fix how this doesn't preserve tab order upon loading new document from *File > Load*
|
// TODO: Fix how this doesn't preserve tab order upon loading new document from *File > Open*
|
||||||
fn load_document(&mut self, new_document: DocumentMessageHandler, document_id: DocumentId, responses: &mut VecDeque<Message>) {
|
fn load_document(&mut self, new_document: DocumentMessageHandler, document_id: DocumentId, responses: &mut VecDeque<Message>) {
|
||||||
self.document_ids.push(document_id);
|
self.document_ids.push(document_id);
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use crate::messages::layout::utility_types::widget_prelude::*;
|
|||||||
use crate::messages::prelude::Message;
|
use crate::messages::prelude::Message;
|
||||||
|
|
||||||
use graphene_core::Color;
|
use graphene_core::Color;
|
||||||
|
use graphene_std::vector::style::FillChoice;
|
||||||
|
|
||||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
|
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||||
pub enum ToolColorType {
|
pub enum ToolColorType {
|
||||||
@@ -96,7 +97,9 @@ impl ToolColorOptions {
|
|||||||
widgets.push(radio);
|
widgets.push(radio);
|
||||||
widgets.push(Separator::new(SeparatorType::Related).widget_holder());
|
widgets.push(Separator::new(SeparatorType::Related).widget_holder());
|
||||||
|
|
||||||
let color_button = ColorButton::new(self.active_color()).allow_none(color_allow_none).on_update(color_callback);
|
let color_button = ColorButton::new(FillChoice::from_optional_color(self.active_color()))
|
||||||
|
.allow_none(color_allow_none)
|
||||||
|
.on_update(color_callback);
|
||||||
widgets.push(color_button.widget_holder());
|
widgets.push(color_button.widget_holder());
|
||||||
|
|
||||||
widgets
|
widgets
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use graph_craft::document::{value::TaggedValue, DocumentNode, NodeId, NodeInput,
|
|||||||
use graphene_core::raster::{BlendMode, ImageFrame};
|
use graphene_core::raster::{BlendMode, ImageFrame};
|
||||||
use graphene_core::text::Font;
|
use graphene_core::text::Font;
|
||||||
use graphene_core::uuid::ManipulatorGroupId;
|
use graphene_core::uuid::ManipulatorGroupId;
|
||||||
use graphene_core::vector::style::{FillType, Gradient};
|
use graphene_core::vector::style::Gradient;
|
||||||
use graphene_core::Color;
|
use graphene_core::Color;
|
||||||
|
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
@@ -95,38 +95,21 @@ pub fn get_colinear_manipulators(layer: LayerNodeIdentifier, document_network: &
|
|||||||
|
|
||||||
/// Get the current gradient of a layer from the closest Fill node
|
/// Get the current gradient of a layer from the closest Fill node
|
||||||
pub fn get_gradient(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option<Gradient> {
|
pub fn get_gradient(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option<Gradient> {
|
||||||
|
let fill_index = 1;
|
||||||
|
|
||||||
let inputs = NodeGraphLayer::new(layer, document_network).find_node_inputs("Fill")?;
|
let inputs = NodeGraphLayer::new(layer, document_network).find_node_inputs("Fill")?;
|
||||||
let TaggedValue::FillType(FillType::Gradient) = inputs.get(1)?.as_value()? else {
|
let TaggedValue::Fill(graphene_std::vector::style::Fill::Gradient(gradient)) = inputs.get(fill_index)?.as_value()? else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
let TaggedValue::GradientType(gradient_type) = inputs.get(3)?.as_value()? else {
|
Some(gradient.clone())
|
||||||
return None;
|
|
||||||
};
|
|
||||||
let TaggedValue::DVec2(start) = inputs.get(4)?.as_value()? else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
let TaggedValue::DVec2(end) = inputs.get(5)?.as_value()? else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
let TaggedValue::DAffine2(transform) = inputs.get(6)?.as_value()? else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
let TaggedValue::GradientPositions(positions) = inputs.get(7)?.as_value()? else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
Some(Gradient {
|
|
||||||
start: *start,
|
|
||||||
end: *end,
|
|
||||||
transform: *transform,
|
|
||||||
positions: positions.clone(),
|
|
||||||
gradient_type: *gradient_type,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current fill of a layer from the closest Fill node
|
/// Get the current fill of a layer from the closest Fill node
|
||||||
pub fn get_fill_color(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option<Color> {
|
pub fn get_fill_color(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option<Color> {
|
||||||
|
let fill_index = 1;
|
||||||
|
|
||||||
let inputs = NodeGraphLayer::new(layer, document_network).find_node_inputs("Fill")?;
|
let inputs = NodeGraphLayer::new(layer, document_network).find_node_inputs("Fill")?;
|
||||||
let TaggedValue::Color(color) = inputs.get(2)?.as_value()? else {
|
let TaggedValue::Fill(graphene_std::vector::style::Fill::Solid(color)) = inputs.get(fill_index)?.as_value()? else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
Some(*color)
|
Some(*color)
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ impl LayoutHolder for BrushTool {
|
|||||||
false,
|
false,
|
||||||
|_| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Color(None)).into(),
|
|_| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Color(None)).into(),
|
||||||
|color_type: ToolColorType| WidgetCallback::new(move |_| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::ColorType(color_type.clone())).into()),
|
|color_type: ToolColorType| WidgetCallback::new(move |_| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::ColorType(color_type.clone())).into()),
|
||||||
|color: &ColorButton| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Color(color.value)).into(),
|
|color: &ColorButton| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Color(color.value.as_solid())).into(),
|
||||||
));
|
));
|
||||||
|
|
||||||
widgets.push(Separator::new(SeparatorType::Related).widget_holder());
|
widgets.push(Separator::new(SeparatorType::Related).widget_holder());
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ impl LayoutHolder for EllipseTool {
|
|||||||
true,
|
true,
|
||||||
|_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColor(None)).into(),
|
|_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColor(None)).into(),
|
||||||
|color_type: ToolColorType| WidgetCallback::new(move |_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColorType(color_type.clone())).into()),
|
|color_type: ToolColorType| WidgetCallback::new(move |_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColorType(color_type.clone())).into()),
|
||||||
|color: &ColorButton| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColor(color.value)).into(),
|
|color: &ColorButton| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColor(color.value.as_solid())).into(),
|
||||||
);
|
);
|
||||||
|
|
||||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||||
@@ -100,7 +100,7 @@ impl LayoutHolder for EllipseTool {
|
|||||||
true,
|
true,
|
||||||
|_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColor(None)).into(),
|
|_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColor(None)).into(),
|
||||||
|color_type: ToolColorType| WidgetCallback::new(move |_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|
|color_type: ToolColorType| WidgetCallback::new(move |_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|
||||||
|color: &ColorButton| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColor(color.value)).into(),
|
|color: &ColorButton| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColor(color.value.as_solid())).into(),
|
||||||
));
|
));
|
||||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||||
widgets.push(create_weight_widget(self.options.line_weight));
|
widgets.push(create_weight_widget(self.options.line_weight));
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ impl LayoutHolder for FreehandTool {
|
|||||||
true,
|
true,
|
||||||
|_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColor(None)).into(),
|
|_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColor(None)).into(),
|
||||||
|color_type: ToolColorType| WidgetCallback::new(move |_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColorType(color_type.clone())).into()),
|
|color_type: ToolColorType| WidgetCallback::new(move |_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColorType(color_type.clone())).into()),
|
||||||
|color: &ColorButton| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColor(color.value)).into(),
|
|color: &ColorButton| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColor(color.value.as_solid())).into(),
|
||||||
);
|
);
|
||||||
|
|
||||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||||
@@ -109,7 +109,7 @@ impl LayoutHolder for FreehandTool {
|
|||||||
true,
|
true,
|
||||||
|_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColor(None)).into(),
|
|_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColor(None)).into(),
|
||||||
|color_type: ToolColorType| WidgetCallback::new(move |_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|
|color_type: ToolColorType| WidgetCallback::new(move |_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|
||||||
|color: &ColorButton| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColor(color.value)).into(),
|
|color: &ColorButton| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColor(color.value.as_solid())).into(),
|
||||||
));
|
));
|
||||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||||
widgets.push(create_weight_widget(self.options.line_weight));
|
widgets.push(create_weight_widget(self.options.line_weight));
|
||||||
|
|||||||
@@ -185,11 +185,11 @@ impl SelectedGradient {
|
|||||||
|
|
||||||
// Should not go off end but can swap
|
// Should not go off end but can swap
|
||||||
let clamped = new_pos.clamp(0., 1.);
|
let clamped = new_pos.clamp(0., 1.);
|
||||||
self.gradient.positions[s].0 = clamped;
|
self.gradient.stops.0[s].0 = clamped;
|
||||||
let new_pos = self.gradient.positions[s];
|
let new_pos = self.gradient.stops.0[s];
|
||||||
|
|
||||||
self.gradient.positions.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
|
self.gradient.stops.0.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.dragging = GradientDragTarget::Step(self.gradient.stops.0.iter().position(|x| *x == new_pos).unwrap());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.render_gradient(responses);
|
self.render_gradient(responses);
|
||||||
@@ -256,14 +256,14 @@ impl Fsm for GradientToolFsmState {
|
|||||||
.filter(|selected| selected.layer.map_or(false, |selected_layer| selected_layer == layer))
|
.filter(|selected| selected.layer.map_or(false, |selected_layer| selected_layer == layer))
|
||||||
.map(|selected| selected.dragging);
|
.map(|selected| selected.dragging);
|
||||||
|
|
||||||
let Gradient { start, end, positions, .. } = gradient;
|
let Gradient { start, end, stops, .. } = gradient;
|
||||||
let (start, end) = (transform.transform_point2(start), transform.transform_point2(end));
|
let (start, end) = (transform.transform_point2(start), transform.transform_point2(end));
|
||||||
|
|
||||||
overlay_context.line(start, end, None, None);
|
overlay_context.line(start, end, None, None);
|
||||||
overlay_context.manipulator_handle(start, dragging == Some(GradientDragTarget::Start));
|
overlay_context.manipulator_handle(start, dragging == Some(GradientDragTarget::Start));
|
||||||
overlay_context.manipulator_handle(end, dragging == Some(GradientDragTarget::End));
|
overlay_context.manipulator_handle(end, dragging == Some(GradientDragTarget::End));
|
||||||
|
|
||||||
for (index, (position, _)) in positions.into_iter().enumerate() {
|
for (index, (position, _)) in stops.0.into_iter().enumerate() {
|
||||||
if position.abs() < f64::EPSILON * 1000. || (1. - position).abs() < f64::EPSILON * 1000. {
|
if position.abs() < f64::EPSILON * 1000. || (1. - position).abs() < f64::EPSILON * 1000. {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -280,32 +280,31 @@ impl Fsm for GradientToolFsmState {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Skip if invalid gradient
|
// Skip if invalid gradient
|
||||||
if selected_gradient.gradient.positions.len() < 2 {
|
if selected_gradient.gradient.stops.0.len() < 2 {
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the selected point
|
// Remove the selected point
|
||||||
match selected_gradient.dragging {
|
match selected_gradient.dragging {
|
||||||
GradientDragTarget::Start => selected_gradient.gradient.positions.remove(0),
|
GradientDragTarget::Start => selected_gradient.gradient.stops.0.remove(0),
|
||||||
GradientDragTarget::End => selected_gradient.gradient.positions.pop().unwrap(),
|
GradientDragTarget::End => selected_gradient.gradient.stops.0.pop().unwrap(),
|
||||||
GradientDragTarget::Step(index) => selected_gradient.gradient.positions.remove(index),
|
GradientDragTarget::Step(index) => selected_gradient.gradient.stops.0.remove(index),
|
||||||
};
|
};
|
||||||
|
|
||||||
// The gradient has only one point and so should become a fill
|
// The gradient has only one point and so should become a fill
|
||||||
if selected_gradient.gradient.positions.len() == 1 {
|
if selected_gradient.gradient.stops.0.len() == 1 {
|
||||||
if let Some(layer) = selected_gradient.layer {
|
if let Some(layer) = selected_gradient.layer {
|
||||||
responses.add(GraphOperationMessage::FillSet {
|
responses.add(GraphOperationMessage::FillSet {
|
||||||
layer,
|
layer,
|
||||||
fill: Fill::Solid(selected_gradient.gradient.positions[0].1),
|
fill: Fill::Solid(selected_gradient.gradient.stops.0[0].1),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the minimum and maximum positions
|
// Find the minimum and maximum positions
|
||||||
let min_position = selected_gradient.gradient.positions.iter().map(|(pos, _)| *pos).reduce(f64::min).expect("No min");
|
let min_position = selected_gradient.gradient.stops.0.iter().map(|(pos, _)| *pos).reduce(f64::min).expect("No min");
|
||||||
let max_position = selected_gradient.gradient.positions.iter().map(|(pos, _)| *pos).reduce(f64::max).expect("No max");
|
let max_position = selected_gradient.gradient.stops.0.iter().map(|(pos, _)| *pos).reduce(f64::max).expect("No max");
|
||||||
|
|
||||||
// Recompute the start and end position of the gradient (in viewport transform)
|
// Recompute the start and end position of the gradient (in viewport transform)
|
||||||
let transform = selected_gradient.transform;
|
let transform = selected_gradient.transform;
|
||||||
@@ -315,7 +314,7 @@ impl Fsm for GradientToolFsmState {
|
|||||||
selected_gradient.gradient.end = transform.inverse().transform_point2(new_end);
|
selected_gradient.gradient.end = transform.inverse().transform_point2(new_end);
|
||||||
|
|
||||||
// Remap the positions
|
// Remap the positions
|
||||||
for (position, _) in selected_gradient.gradient.positions.iter_mut() {
|
for (position, _) in selected_gradient.gradient.stops.0.iter_mut() {
|
||||||
*position = (*position - min_position) / (max_position - min_position);
|
*position = (*position - min_position) / (max_position - min_position);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,7 +368,7 @@ impl Fsm for GradientToolFsmState {
|
|||||||
let transform = gradient_space_transform(layer, document);
|
let transform = gradient_space_transform(layer, document);
|
||||||
|
|
||||||
// Check for dragging step
|
// Check for dragging step
|
||||||
for (index, (pos, _)) in gradient.positions.iter().enumerate() {
|
for (index, (pos, _)) in gradient.stops.0.iter().enumerate() {
|
||||||
let pos = transform.transform_point2(gradient.start.lerp(gradient.end, *pos));
|
let pos = transform.transform_point2(gradient.start.lerp(gradient.end, *pos));
|
||||||
if pos.distance_squared(mouse) < tolerance {
|
if pos.distance_squared(mouse) < tolerance {
|
||||||
dragging = true;
|
dragging = true;
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ impl LayoutHolder for LineTool {
|
|||||||
true,
|
true,
|
||||||
|_| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColor(None)).into(),
|
|_| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColor(None)).into(),
|
||||||
|color_type: ToolColorType| WidgetCallback::new(move |_| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|
|color_type: ToolColorType| WidgetCallback::new(move |_| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|
||||||
|color: &ColorButton| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColor(color.value)).into(),
|
|color: &ColorButton| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColor(color.value.as_solid())).into(),
|
||||||
);
|
);
|
||||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||||
widgets.push(create_weight_widget(self.options.line_weight));
|
widgets.push(create_weight_widget(self.options.line_weight));
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ impl LayoutHolder for PenTool {
|
|||||||
true,
|
true,
|
||||||
|_| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(None)).into(),
|
|_| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(None)).into(),
|
||||||
|color_type: ToolColorType| WidgetCallback::new(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColorType(color_type.clone())).into()),
|
|color_type: ToolColorType| WidgetCallback::new(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColorType(color_type.clone())).into()),
|
||||||
|color: &ColorButton| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(color.value)).into(),
|
|color: &ColorButton| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(color.value.as_solid())).into(),
|
||||||
);
|
);
|
||||||
|
|
||||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||||
@@ -117,7 +117,7 @@ impl LayoutHolder for PenTool {
|
|||||||
true,
|
true,
|
||||||
|_| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(None)).into(),
|
|_| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(None)).into(),
|
||||||
|color_type: ToolColorType| WidgetCallback::new(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|
|color_type: ToolColorType| WidgetCallback::new(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|
||||||
|color: &ColorButton| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(color.value)).into(),
|
|color: &ColorButton| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(color.value.as_solid())).into(),
|
||||||
));
|
));
|
||||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||||
widgets.push(create_weight_widget(self.options.line_weight));
|
widgets.push(create_weight_widget(self.options.line_weight));
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ impl LayoutHolder for PolygonTool {
|
|||||||
true,
|
true,
|
||||||
|_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::FillColor(None)).into(),
|
|_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::FillColor(None)).into(),
|
||||||
|color_type: ToolColorType| WidgetCallback::new(move |_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::FillColorType(color_type.clone())).into()),
|
|color_type: ToolColorType| WidgetCallback::new(move |_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::FillColorType(color_type.clone())).into()),
|
||||||
|color: &ColorButton| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::FillColor(color.value)).into(),
|
|color: &ColorButton| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::FillColor(color.value.as_solid())).into(),
|
||||||
));
|
));
|
||||||
|
|
||||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||||
@@ -143,7 +143,7 @@ impl LayoutHolder for PolygonTool {
|
|||||||
true,
|
true,
|
||||||
|_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::StrokeColor(None)).into(),
|
|_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::StrokeColor(None)).into(),
|
||||||
|color_type: ToolColorType| WidgetCallback::new(move |_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|
|color_type: ToolColorType| WidgetCallback::new(move |_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|
||||||
|color: &ColorButton| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::StrokeColor(color.value)).into(),
|
|color: &ColorButton| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::StrokeColor(color.value.as_solid())).into(),
|
||||||
));
|
));
|
||||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||||
widgets.push(create_weight_widget(self.options.line_weight));
|
widgets.push(create_weight_widget(self.options.line_weight));
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ impl LayoutHolder for RectangleTool {
|
|||||||
true,
|
true,
|
||||||
|_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColor(None)).into(),
|
|_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColor(None)).into(),
|
||||||
|color_type: ToolColorType| WidgetCallback::new(move |_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColorType(color_type.clone())).into()),
|
|color_type: ToolColorType| WidgetCallback::new(move |_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColorType(color_type.clone())).into()),
|
||||||
|color: &ColorButton| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColor(color.value)).into(),
|
|color: &ColorButton| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColor(color.value.as_solid())).into(),
|
||||||
);
|
);
|
||||||
|
|
||||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||||
@@ -87,7 +87,7 @@ impl LayoutHolder for RectangleTool {
|
|||||||
true,
|
true,
|
||||||
|_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColor(None)).into(),
|
|_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColor(None)).into(),
|
||||||
|color_type: ToolColorType| WidgetCallback::new(move |_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|
|color_type: ToolColorType| WidgetCallback::new(move |_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|
||||||
|color: &ColorButton| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColor(color.value)).into(),
|
|color: &ColorButton| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColor(color.value.as_solid())).into(),
|
||||||
));
|
));
|
||||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||||
widgets.push(create_weight_widget(self.options.line_weight));
|
widgets.push(create_weight_widget(self.options.line_weight));
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ impl LayoutHolder for SplineTool {
|
|||||||
true,
|
true,
|
||||||
|_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColor(None)).into(),
|
|_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColor(None)).into(),
|
||||||
|color_type: ToolColorType| WidgetCallback::new(move |_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColorType(color_type.clone())).into()),
|
|color_type: ToolColorType| WidgetCallback::new(move |_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColorType(color_type.clone())).into()),
|
||||||
|color: &ColorButton| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColor(color.value)).into(),
|
|color: &ColorButton| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColor(color.value.as_solid())).into(),
|
||||||
);
|
);
|
||||||
|
|
||||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||||
@@ -109,7 +109,7 @@ impl LayoutHolder for SplineTool {
|
|||||||
true,
|
true,
|
||||||
|_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColor(None)).into(),
|
|_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColor(None)).into(),
|
||||||
|color_type: ToolColorType| WidgetCallback::new(move |_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|
|color_type: ToolColorType| WidgetCallback::new(move |_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|
||||||
|color: &ColorButton| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColor(color.value)).into(),
|
|color: &ColorButton| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColor(color.value.as_solid())).into(),
|
||||||
));
|
));
|
||||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||||
widgets.push(create_weight_widget(self.options.line_weight));
|
widgets.push(create_weight_widget(self.options.line_weight));
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ impl LayoutHolder for TextTool {
|
|||||||
true,
|
true,
|
||||||
|_| TextToolMessage::UpdateOptions(TextOptionsUpdate::FillColor(None)).into(),
|
|_| TextToolMessage::UpdateOptions(TextOptionsUpdate::FillColor(None)).into(),
|
||||||
|color_type: ToolColorType| WidgetCallback::new(move |_| TextToolMessage::UpdateOptions(TextOptionsUpdate::FillColorType(color_type.clone())).into()),
|
|color_type: ToolColorType| WidgetCallback::new(move |_| TextToolMessage::UpdateOptions(TextOptionsUpdate::FillColorType(color_type.clone())).into()),
|
||||||
|color: &ColorButton| TextToolMessage::UpdateOptions(TextOptionsUpdate::FillColor(color.value)).into(),
|
|color: &ColorButton| TextToolMessage::UpdateOptions(TextOptionsUpdate::FillColor(color.value.as_solid())).into(),
|
||||||
));
|
));
|
||||||
|
|
||||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
|
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ impl DocumentToolData {
|
|||||||
},
|
},
|
||||||
LayoutGroup::Row {
|
LayoutGroup::Row {
|
||||||
widgets: vec![
|
widgets: vec![
|
||||||
IconButton::new("Swap", 16)
|
IconButton::new("SwapVertical", 16)
|
||||||
.tooltip("Swap")
|
.tooltip("Swap")
|
||||||
.tooltip_shortcut(action_keys!(ToolMessageDiscriminant::SwapColors))
|
.tooltip_shortcut(action_keys!(ToolMessageDiscriminant::SwapColors))
|
||||||
.on_update(|_| ToolMessage::SwapColors.into())
|
.on_update(|_| ToolMessage::SwapColors.into())
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
|
||||||
|
<path d="M9,7l3-3L9,1v2H5v2h4V7z M0,8l3,3V9h4V7H3V5L0,8z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 130 B |
|
Before Width: | Height: | Size: 132 B After Width: | Height: | Size: 132 B |
@@ -136,10 +136,11 @@
|
|||||||
|
|
||||||
--color-transparent-checkered-background: linear-gradient(45deg, #cccccc 25%, transparent 25%, transparent 75%, #cccccc 75%),
|
--color-transparent-checkered-background: linear-gradient(45deg, #cccccc 25%, transparent 25%, transparent 75%, #cccccc 75%),
|
||||||
linear-gradient(45deg, #cccccc 25%, transparent 25%, transparent 75%, #cccccc 75%), linear-gradient(#ffffff, #ffffff);
|
linear-gradient(45deg, #cccccc 25%, transparent 25%, transparent 75%, #cccccc 75%), linear-gradient(#ffffff, #ffffff);
|
||||||
--color-transparent-checkered-background-size: 16px 16px;
|
--color-transparent-checkered-background-size: 16px 16px, 16px 16px, 16px 16px;
|
||||||
--color-transparent-checkered-background-position: 0 0, 8px 8px;
|
--color-transparent-checkered-background-position: 0 0, 8px 8px, 8px 8px;
|
||||||
--color-transparent-checkered-background-size-mini: 8px 8px;
|
--color-transparent-checkered-background-size-mini: 8px 8px, 8px 8px, 8px 8px;
|
||||||
--color-transparent-checkered-background-position-mini: 0 0, 4px 4px;
|
--color-transparent-checkered-background-position-mini: 0 0, 4px 4px, 4px 4px;
|
||||||
|
--color-transparent-checkered-background-repeat: repeat, repeat, repeat;
|
||||||
|
|
||||||
--background-inactive-stripes: repeating-linear-gradient(
|
--background-inactive-stripes: repeating-linear-gradient(
|
||||||
-45deg,
|
-45deg,
|
||||||
|
|||||||
@@ -3,14 +3,15 @@
|
|||||||
|
|
||||||
import { clamp } from "@graphite/utility-functions/math";
|
import { clamp } from "@graphite/utility-functions/math";
|
||||||
import type { Editor } from "@graphite/wasm-communication/editor";
|
import type { Editor } from "@graphite/wasm-communication/editor";
|
||||||
import { type HSV, type RGB } from "@graphite/wasm-communication/messages";
|
import { type HSV, type RGB, type FillChoice } from "@graphite/wasm-communication/messages";
|
||||||
import { Color } from "@graphite/wasm-communication/messages";
|
import { Color, Gradient } from "@graphite/wasm-communication/messages";
|
||||||
|
|
||||||
import FloatingMenu, { type MenuDirection } from "@graphite/components/layout/FloatingMenu.svelte";
|
import FloatingMenu, { type MenuDirection } from "@graphite/components/layout/FloatingMenu.svelte";
|
||||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||||
import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte";
|
import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte";
|
||||||
import NumberInput from "@graphite/components/widgets/inputs/NumberInput.svelte";
|
import NumberInput from "@graphite/components/widgets/inputs/NumberInput.svelte";
|
||||||
|
import SpectrumInput from "@graphite/components/widgets/inputs/SpectrumInput.svelte";
|
||||||
import TextInput from "@graphite/components/widgets/inputs/TextInput.svelte";
|
import TextInput from "@graphite/components/widgets/inputs/TextInput.svelte";
|
||||||
import Separator from "@graphite/components/widgets/labels/Separator.svelte";
|
import Separator from "@graphite/components/widgets/labels/Separator.svelte";
|
||||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||||
@@ -31,40 +32,47 @@
|
|||||||
|
|
||||||
const editor = getContext<Editor>("editor");
|
const editor = getContext<Editor>("editor");
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{ color: Color; startHistoryTransaction: undefined }>();
|
const dispatch = createEventDispatcher<{ colorOrGradient: FillChoice; startHistoryTransaction: undefined }>();
|
||||||
|
|
||||||
export let color: Color;
|
export let colorOrGradient: FillChoice;
|
||||||
export let allowNone = false;
|
export let allowNone = false;
|
||||||
// export let allowTransparency = false; // TODO: Implement
|
// export let allowTransparency = false; // TODO: Implement
|
||||||
export let direction: MenuDirection = "Bottom";
|
export let direction: MenuDirection = "Bottom";
|
||||||
// TODO: See if this should be made to follow the pattern of DropdownInput.svelte so this could be removed
|
// TODO: See if this should be made to follow the pattern of DropdownInput.svelte so this could be removed
|
||||||
export let open: boolean;
|
export let open: boolean;
|
||||||
|
|
||||||
const hsvaOrNone = color.toHSVA();
|
const hsvaOrNone = colorOrGradient instanceof Color ? colorOrGradient.toHSVA() : colorOrGradient.firstColor()?.toHSVA();
|
||||||
const hsva = hsvaOrNone || { h: 0, s: 0, v: 0, a: 1 };
|
const hsva = hsvaOrNone || { h: 0, s: 0, v: 0, a: 1 };
|
||||||
|
|
||||||
|
// Gradient color stops
|
||||||
|
$: gradient = colorOrGradient instanceof Gradient ? colorOrGradient : undefined;
|
||||||
|
let activeIndex = 0 as number | undefined;
|
||||||
|
$: selectedGradientColour = (activeIndex !== undefined && gradient?.atIndex(activeIndex)?.color) || (Color.fromCSS("black") as Color);
|
||||||
|
// Currently viewed color
|
||||||
|
$: color = colorOrGradient instanceof Color ? colorOrGradient : selectedGradientColour;
|
||||||
// New color components
|
// New color components
|
||||||
let hue = hsva.h;
|
let hue = hsva.h;
|
||||||
let saturation = hsva.s;
|
let saturation = hsva.s;
|
||||||
let value = hsva.v;
|
let value = hsva.v;
|
||||||
let alpha = hsva.a;
|
let alpha = hsva.a;
|
||||||
let isNone = hsvaOrNone === undefined;
|
let isNone = hsvaOrNone === undefined;
|
||||||
// Initial color components
|
// Old color components
|
||||||
let initialHue = hsva.h;
|
let oldHue = hsva.h;
|
||||||
let initialSaturation = hsva.s;
|
let oldSaturation = hsva.s;
|
||||||
let initialValue = hsva.v;
|
let oldValue = hsva.v;
|
||||||
let initialAlpha = hsva.a;
|
let oldAlpha = hsva.a;
|
||||||
let initialIsNone = hsvaOrNone === undefined;
|
let oldIsNone = hsvaOrNone === undefined;
|
||||||
// Transient state
|
// Transient state
|
||||||
let draggingPickerTrack: HTMLDivElement | undefined = undefined;
|
let draggingPickerTrack: HTMLDivElement | undefined = undefined;
|
||||||
let strayCloses = true;
|
let strayCloses = true;
|
||||||
|
|
||||||
let hexCodeInputWidget: TextInput | undefined;
|
let hexCodeInputWidget: TextInput | undefined;
|
||||||
|
let gradientSpectrumInputWidget: SpectrumInput | undefined;
|
||||||
|
|
||||||
$: watchOpen(open);
|
$: watchOpen(open);
|
||||||
$: watchColor(color);
|
$: watchColor(color);
|
||||||
|
|
||||||
$: initialColor = generateColor(initialHue, initialSaturation, initialValue, initialAlpha, initialIsNone);
|
$: oldColor = generateColor(oldHue, oldSaturation, oldValue, oldAlpha, oldIsNone);
|
||||||
$: newColor = generateColor(hue, saturation, value, alpha, isNone);
|
$: newColor = generateColor(hue, saturation, value, alpha, isNone);
|
||||||
$: rgbChannels = Object.entries(newColor.toRgb255() || { r: undefined, g: undefined, b: undefined }) as [keyof RGB, number | undefined][];
|
$: rgbChannels = Object.entries(newColor.toRgb255() || { r: undefined, g: undefined, b: undefined }) as [keyof RGB, number | undefined][];
|
||||||
$: hsvChannels = Object.entries(!isNone ? { h: hue * 360, s: saturation * 100, v: value * 100 } : { h: undefined, s: undefined, v: undefined }) as [keyof HSV, number | undefined][];
|
$: hsvChannels = Object.entries(!isNone ? { h: hue * 360, s: saturation * 100, v: value * 100 } : { h: undefined, s: undefined, v: undefined }) as [keyof HSV, number | undefined][];
|
||||||
@@ -79,7 +87,7 @@
|
|||||||
if (open) {
|
if (open) {
|
||||||
setTimeout(() => hexCodeInputWidget?.focus(), 0);
|
setTimeout(() => hexCodeInputWidget?.focus(), 0);
|
||||||
} else {
|
} else {
|
||||||
setInitialHSVA(hue, saturation, value, alpha, isNone);
|
setOldHSVA(hue, saturation, value, alpha, isNone);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,11 +171,18 @@
|
|||||||
|
|
||||||
function setColor(color?: Color) {
|
function setColor(color?: Color) {
|
||||||
const colorToEmit = color || new Color({ h: hue, s: saturation, v: value, a: alpha });
|
const colorToEmit = color || new Color({ h: hue, s: saturation, v: value, a: alpha });
|
||||||
dispatch("color", colorToEmit);
|
|
||||||
|
const stop = gradientSpectrumInputWidget && activeIndex !== undefined && gradient?.atIndex(activeIndex);
|
||||||
|
if (stop && gradientSpectrumInputWidget instanceof SpectrumInput) {
|
||||||
|
stop.color = colorToEmit;
|
||||||
|
gradient = gradient;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch("colorOrGradient", gradient || colorToEmit);
|
||||||
}
|
}
|
||||||
|
|
||||||
function swapNewWithInitial() {
|
function swapNewWithOld() {
|
||||||
const initial = initialColor;
|
const old = oldColor;
|
||||||
|
|
||||||
const tempHue = hue;
|
const tempHue = hue;
|
||||||
const tempSaturation = saturation;
|
const tempSaturation = saturation;
|
||||||
@@ -175,10 +190,10 @@
|
|||||||
const tempAlpha = alpha;
|
const tempAlpha = alpha;
|
||||||
const tempIsNone = isNone;
|
const tempIsNone = isNone;
|
||||||
|
|
||||||
setNewHSVA(initialHue, initialSaturation, initialValue, initialAlpha, initialIsNone);
|
setNewHSVA(oldHue, oldSaturation, oldValue, oldAlpha, oldIsNone);
|
||||||
setInitialHSVA(tempHue, tempSaturation, tempValue, tempAlpha, tempIsNone);
|
setOldHSVA(tempHue, tempSaturation, tempValue, tempAlpha, tempIsNone);
|
||||||
|
|
||||||
setColor(initial);
|
setColor(old);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setColorCode(colorCode: string) {
|
function setColorCode(colorCode: string) {
|
||||||
@@ -241,12 +256,12 @@
|
|||||||
isNone = none;
|
isNone = none;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setInitialHSVA(h: number, s: number, v: number, a: number, none: boolean) {
|
function setOldHSVA(h: number, s: number, v: number, a: number, none: boolean) {
|
||||||
initialHue = h;
|
oldHue = h;
|
||||||
initialSaturation = s;
|
oldSaturation = s;
|
||||||
initialValue = v;
|
oldValue = v;
|
||||||
initialAlpha = a;
|
oldAlpha = a;
|
||||||
initialIsNone = none;
|
oldIsNone = none;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function activateEyedropperSample() {
|
async function activateEyedropperSample() {
|
||||||
@@ -267,6 +282,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function gradientActiveMarkerIndexChange({ detail: index }: CustomEvent<number | undefined>) {
|
||||||
|
activeIndex = index;
|
||||||
|
const color = index === undefined ? undefined : gradient?.colorAtIndex(index);
|
||||||
|
const hsva = color?.toHSVA();
|
||||||
|
if (!color || !hsva) return;
|
||||||
|
|
||||||
|
setColor(color);
|
||||||
|
|
||||||
|
setNewHSVA(hsva.h, hsva.s, hsva.v, hsva.a, color.none);
|
||||||
|
setOldHSVA(hsva.h, hsva.s, hsva.v, hsva.a, color.none);
|
||||||
|
}
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
removeEvents();
|
removeEvents();
|
||||||
});
|
});
|
||||||
@@ -277,42 +304,83 @@
|
|||||||
styles={{
|
styles={{
|
||||||
"--new-color": newColor.toHexOptionalAlpha(),
|
"--new-color": newColor.toHexOptionalAlpha(),
|
||||||
"--new-color-contrasting": newColor.contrastingColor(),
|
"--new-color-contrasting": newColor.contrastingColor(),
|
||||||
"--initial-color": initialColor.toHexOptionalAlpha(),
|
"--old-color": oldColor.toHexOptionalAlpha(),
|
||||||
"--initial-color-contrasting": initialColor.contrastingColor(),
|
"--old-color-contrasting": oldColor.contrastingColor(),
|
||||||
"--hue-color": opaqueHueColor.toRgbCSS(),
|
"--hue-color": opaqueHueColor.toRgbCSS(),
|
||||||
"--hue-color-contrasting": opaqueHueColor.contrastingColor(),
|
"--hue-color-contrasting": opaqueHueColor.contrastingColor(),
|
||||||
"--opaque-color": (newColor.opaque() || new Color(0, 0, 0, 1)).toHexNoAlpha(),
|
"--opaque-color": (newColor.opaque() || new Color(0, 0, 0, 1)).toHexNoAlpha(),
|
||||||
"--opaque-color-contrasting": (newColor.opaque() || new Color(0, 0, 0, 1)).contrastingColor(),
|
"--opaque-color-contrasting": (newColor.opaque() || new Color(0, 0, 0, 1)).contrastingColor(),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LayoutCol class="saturation-value-picker" on:pointerdown={onPointerDown} data-saturation-value-picker>
|
<LayoutCol class="pickers-and-gradient">
|
||||||
{#if !isNone}
|
<LayoutRow class="pickers">
|
||||||
<div class="selection-circle" style:top={`${(1 - value) * 100}%`} style:left={`${saturation * 100}%`} />
|
<LayoutCol class="saturation-value-picker" on:pointerdown={onPointerDown} data-saturation-value-picker>
|
||||||
{/if}
|
{#if !isNone}
|
||||||
</LayoutCol>
|
<div class="selection-circle" style:top={`${(1 - value) * 100}%`} style:left={`${saturation * 100}%`} />
|
||||||
<LayoutCol class="hue-picker" on:pointerdown={onPointerDown} data-hue-picker>
|
{/if}
|
||||||
{#if !isNone}
|
</LayoutCol>
|
||||||
<div class="selection-needle" style:top={`${(1 - hue) * 100}%`} />
|
<LayoutCol class="hue-picker" on:pointerdown={onPointerDown} data-hue-picker>
|
||||||
{/if}
|
{#if !isNone}
|
||||||
</LayoutCol>
|
<div class="selection-needle" style:top={`${(1 - hue) * 100}%`} />
|
||||||
<LayoutCol class="alpha-picker" on:pointerdown={onPointerDown} data-alpha-picker>
|
{/if}
|
||||||
{#if !isNone}
|
</LayoutCol>
|
||||||
<div class="selection-needle" style:top={`${(1 - alpha) * 100}%`} />
|
<LayoutCol class="alpha-picker" on:pointerdown={onPointerDown} data-alpha-picker>
|
||||||
|
{#if !isNone}
|
||||||
|
<div class="selection-needle" style:top={`${(1 - alpha) * 100}%`} />
|
||||||
|
{/if}
|
||||||
|
</LayoutCol>
|
||||||
|
</LayoutRow>
|
||||||
|
{#if gradient}
|
||||||
|
<LayoutRow class="gradient">
|
||||||
|
<SpectrumInput
|
||||||
|
{gradient}
|
||||||
|
on:gradient={() => {
|
||||||
|
gradient = gradient;
|
||||||
|
if (gradient) dispatch("colorOrGradient", gradient);
|
||||||
|
}}
|
||||||
|
on:activeMarkerIndexChange={gradientActiveMarkerIndexChange}
|
||||||
|
activeMarkerIndex={activeIndex}
|
||||||
|
bind:this={gradientSpectrumInputWidget}
|
||||||
|
/>
|
||||||
|
{#if gradientSpectrumInputWidget && activeIndex !== undefined}
|
||||||
|
<NumberInput
|
||||||
|
value={(gradient.positionAtIndex(activeIndex) || 0) * 100}
|
||||||
|
on:value={({ detail }) => {
|
||||||
|
if (gradientSpectrumInputWidget && activeIndex !== undefined && detail !== undefined) gradientSpectrumInputWidget.setPosition(activeIndex, detail / 100);
|
||||||
|
}}
|
||||||
|
displayDecimalPlaces={0}
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
unit="%"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</LayoutRow>
|
||||||
{/if}
|
{/if}
|
||||||
</LayoutCol>
|
</LayoutCol>
|
||||||
<LayoutCol class="details">
|
<LayoutCol class="details">
|
||||||
<LayoutRow class="choice-preview" on:click={swapNewWithInitial} tooltip="Comparison views of the present color choice (left) and the color before any change (right). Click to swap sides.">
|
<LayoutRow
|
||||||
|
class="choice-preview"
|
||||||
|
tooltip={!newColor.equals(oldColor) ? "Comparison between the present color choice (left) and the color before any change was made (right)" : "The present color choice"}
|
||||||
|
>
|
||||||
|
{#if !newColor.equals(oldColor)}
|
||||||
|
<div class="swap-button-background"></div>
|
||||||
|
<IconButton class="swap-button" icon="SwapHorizontal" size={16} action={swapNewWithOld} tooltip="Swap" />
|
||||||
|
{/if}
|
||||||
<LayoutCol class="new-color" classes={{ none: isNone }}>
|
<LayoutCol class="new-color" classes={{ none: isNone }}>
|
||||||
<TextLabel>New</TextLabel>
|
{#if !newColor.equals(oldColor)}
|
||||||
</LayoutCol>
|
<TextLabel>New</TextLabel>
|
||||||
<LayoutCol class="initial-color" classes={{ none: initialIsNone }}>
|
{/if}
|
||||||
<TextLabel>Initial</TextLabel>
|
|
||||||
</LayoutCol>
|
</LayoutCol>
|
||||||
|
{#if !newColor.equals(oldColor)}
|
||||||
|
<LayoutCol class="old-color" classes={{ none: oldIsNone }}>
|
||||||
|
<TextLabel>Old</TextLabel>
|
||||||
|
</LayoutCol>
|
||||||
|
{/if}
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
<!-- <DropdownInput entries={[[{ label: "sRGB" }]]} selectedIndex={0} disabled={true} tooltip="Color model, color space, and HDR (coming soon)" /> -->
|
<!-- <DropdownInput entries={[[{ label: "sRGB" }]]} selectedIndex={0} disabled={true} tooltip="Color model, color space, and HDR (coming soon)" /> -->
|
||||||
<LayoutRow>
|
<LayoutRow>
|
||||||
<TextLabel tooltip={"Color code in hexadecimal format. 6 digits if opaque, 8 with alpha.\nAccepts input of CSS color values including named colors."}>Hex</TextLabel>
|
<TextLabel tooltip={"Color code in hexadecimal format. 6 digits if opaque, 8 with alpha.\nAccepts input of CSS color values including named colors."}>Hex</TextLabel>
|
||||||
<Separator />
|
<Separator type="Related" />
|
||||||
<LayoutRow>
|
<LayoutRow>
|
||||||
<TextInput
|
<TextInput
|
||||||
value={newColor.toHexOptionalAlpha() || "-"}
|
value={newColor.toHexOptionalAlpha() || "-"}
|
||||||
@@ -328,7 +396,7 @@
|
|||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
<LayoutRow>
|
<LayoutRow>
|
||||||
<TextLabel tooltip="Red/Green/Blue channels of the color, integers 0–255">RGB</TextLabel>
|
<TextLabel tooltip="Red/Green/Blue channels of the color, integers 0–255">RGB</TextLabel>
|
||||||
<Separator />
|
<Separator type="Related" />
|
||||||
<LayoutRow>
|
<LayoutRow>
|
||||||
{#each rgbChannels as [channel, strength], index}
|
{#each rgbChannels as [channel, strength], index}
|
||||||
{#if index > 0}
|
{#if index > 0}
|
||||||
@@ -345,7 +413,7 @@
|
|||||||
}}
|
}}
|
||||||
min={0}
|
min={0}
|
||||||
max={255}
|
max={255}
|
||||||
minWidth={56}
|
minWidth={1}
|
||||||
tooltip={`${{ r: "Red", g: "Green", b: "Blue" }[channel]} channel, integers 0–255`}
|
tooltip={`${{ r: "Red", g: "Green", b: "Blue" }[channel]} channel, integers 0–255`}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -355,7 +423,7 @@
|
|||||||
<TextLabel tooltip={"Hue/Saturation/Value, also known as Hue/Saturation/Brightness (HSB).\nNot to be confused with Hue/Saturation/Lightness (HSL), a different color model."}>
|
<TextLabel tooltip={"Hue/Saturation/Value, also known as Hue/Saturation/Brightness (HSB).\nNot to be confused with Hue/Saturation/Lightness (HSL), a different color model."}>
|
||||||
HSV
|
HSV
|
||||||
</TextLabel>
|
</TextLabel>
|
||||||
<Separator />
|
<Separator type="Related" />
|
||||||
<LayoutRow>
|
<LayoutRow>
|
||||||
{#each hsvChannels as [channel, strength], index}
|
{#each hsvChannels as [channel, strength], index}
|
||||||
{#if index > 0}
|
{#if index > 0}
|
||||||
@@ -373,7 +441,8 @@
|
|||||||
min={0}
|
min={0}
|
||||||
max={channel === "h" ? 360 : 100}
|
max={channel === "h" ? 360 : 100}
|
||||||
unit={channel === "h" ? "°" : "%"}
|
unit={channel === "h" ? "°" : "%"}
|
||||||
minWidth={56}
|
minWidth={1}
|
||||||
|
displayDecimalPlaces={1}
|
||||||
tooltip={{
|
tooltip={{
|
||||||
h: `Hue component, the shade along the spectrum of the rainbow`,
|
h: `Hue component, the shade along the spectrum of the rainbow`,
|
||||||
s: `Saturation component, the vividness from grayscale to full color`,
|
s: `Saturation component, the vividness from grayscale to full color`,
|
||||||
@@ -383,42 +452,45 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
<NumberInput
|
<LayoutRow>
|
||||||
label="Alpha"
|
<TextLabel tooltip="Scale from transparent (0%) to opaque (100%) for the color's alpha channel">Alpha</TextLabel>
|
||||||
value={!isNone ? alpha * 100 : undefined}
|
<Separator type="Related" />
|
||||||
on:value={({ detail }) => {
|
<NumberInput
|
||||||
if (detail !== undefined) alpha = detail / 100;
|
value={!isNone ? alpha * 100 : undefined}
|
||||||
setColorAlphaPercent(detail);
|
on:value={({ detail }) => {
|
||||||
}}
|
if (detail !== undefined) alpha = detail / 100;
|
||||||
on:startHistoryTransaction={() => {
|
setColorAlphaPercent(detail);
|
||||||
dispatch("startHistoryTransaction");
|
}}
|
||||||
}}
|
on:startHistoryTransaction={() => {
|
||||||
min={0}
|
dispatch("startHistoryTransaction");
|
||||||
max={100}
|
}}
|
||||||
rangeMin={0}
|
min={0}
|
||||||
rangeMax={100}
|
max={100}
|
||||||
unit="%"
|
rangeMin={0}
|
||||||
mode="Range"
|
rangeMax={100}
|
||||||
displayDecimalPlaces={1}
|
unit="%"
|
||||||
tooltip={`Scale from transparent (0%) to opaque (100%) for the color's alpha channel`}
|
mode="Range"
|
||||||
/>
|
displayDecimalPlaces={1}
|
||||||
|
tooltip={`Scale from transparent (0%) to opaque (100%) for the color's alpha channel`}
|
||||||
|
/>
|
||||||
|
</LayoutRow>
|
||||||
<LayoutRow class="leftover-space" />
|
<LayoutRow class="leftover-space" />
|
||||||
<LayoutRow>
|
<LayoutRow>
|
||||||
{#if allowNone}
|
{#if allowNone && !gradient}
|
||||||
<button class="preset-color none" on:click={() => setColorPreset("none")} title="Set No Color" tabindex="0" />
|
<button class="preset-color none" on:click={() => setColorPreset("none")} title="Set to no color" tabindex="0" />
|
||||||
<Separator type="Related" />
|
<Separator type="Related" />
|
||||||
{/if}
|
{/if}
|
||||||
<button class="preset-color black" on:click={() => setColorPreset("black")} title="Set Black" tabindex="0" />
|
<button class="preset-color black" on:click={() => setColorPreset("black")} title="Set to black" tabindex="0" />
|
||||||
<Separator type="Related" />
|
<Separator type="Related" />
|
||||||
<button class="preset-color white" on:click={() => setColorPreset("white")} title="Set White" tabindex="0" />
|
<button class="preset-color white" on:click={() => setColorPreset("white")} title="Set to white" tabindex="0" />
|
||||||
<Separator type="Related" />
|
<Separator type="Related" />
|
||||||
<button class="preset-color pure" on:click={setColorPresetSubtile} tabindex="-1">
|
<button class="preset-color pure" on:click={setColorPresetSubtile} tabindex="-1">
|
||||||
<div data-pure-tile="red" style="--pure-color: #ff0000; --pure-color-gray: #4c4c4c" title="Set Red" />
|
<div data-pure-tile="red" style="--pure-color: #ff0000; --pure-color-gray: #4c4c4c" title="Set to red" />
|
||||||
<div data-pure-tile="yellow" style="--pure-color: #ffff00; --pure-color-gray: #e3e3e3" title="Set Yellow" />
|
<div data-pure-tile="yellow" style="--pure-color: #ffff00; --pure-color-gray: #e3e3e3" title="Set to yellow" />
|
||||||
<div data-pure-tile="green" style="--pure-color: #00ff00; --pure-color-gray: #969696" title="Set Green" />
|
<div data-pure-tile="green" style="--pure-color: #00ff00; --pure-color-gray: #969696" title="Set to green" />
|
||||||
<div data-pure-tile="cyan" style="--pure-color: #00ffff; --pure-color-gray: #b2b2b2" title="Set Cyan" />
|
<div data-pure-tile="cyan" style="--pure-color: #00ffff; --pure-color-gray: #b2b2b2" title="Set to cyan" />
|
||||||
<div data-pure-tile="blue" style="--pure-color: #0000ff; --pure-color-gray: #1c1c1c" title="Set Blue" />
|
<div data-pure-tile="blue" style="--pure-color: #0000ff; --pure-color-gray: #1c1c1c" title="Set to blue" />
|
||||||
<div data-pure-tile="magenta" style="--pure-color: #ff00ff; --pure-color-gray: #696969" title="Set Magenta" />
|
<div data-pure-tile="magenta" style="--pure-color: #ff00ff; --pure-color-gray: #696969" title="Set to magenta" />
|
||||||
</button>
|
</button>
|
||||||
<Separator type="Related" />
|
<Separator type="Related" />
|
||||||
<IconButton icon="Eyedropper" size={24} action={activateEyedropperSample} tooltip="Sample a pixel color from the document" />
|
<IconButton icon="Eyedropper" size={24} action={activateEyedropperSample} tooltip="Sample a pixel color from the document" />
|
||||||
@@ -429,109 +501,125 @@
|
|||||||
|
|
||||||
<style lang="scss" global>
|
<style lang="scss" global>
|
||||||
.color-picker {
|
.color-picker {
|
||||||
.saturation-value-picker {
|
.pickers-and-gradient {
|
||||||
width: 256px;
|
.pickers {
|
||||||
background-blend-mode: multiply;
|
.saturation-value-picker {
|
||||||
background: linear-gradient(to bottom, #ffffff, #000000), linear-gradient(to right, #ffffff, var(--hue-color));
|
width: 256px;
|
||||||
position: relative;
|
background-blend-mode: multiply;
|
||||||
}
|
background: linear-gradient(to bottom, #ffffff, #000000), linear-gradient(to right, #ffffff, var(--hue-color));
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.saturation-value-picker,
|
.saturation-value-picker,
|
||||||
.hue-picker,
|
.hue-picker,
|
||||||
.alpha-picker {
|
.alpha-picker {
|
||||||
height: 256px;
|
height: 256px;
|
||||||
position: relative;
|
border-radius: 2px;
|
||||||
overflow: hidden;
|
position: relative;
|
||||||
}
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.hue-picker,
|
.hue-picker,
|
||||||
.alpha-picker {
|
.alpha-picker {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hue-picker {
|
.hue-picker {
|
||||||
background-blend-mode: screen;
|
--selection-needle-color: var(--hue-color-contrasting);
|
||||||
background:
|
background-blend-mode: screen;
|
||||||
// Reds
|
background:
|
||||||
linear-gradient(to top, #ff0000ff 16.666%, #ff000000 33.333%, #ff000000 66.666%, #ff0000ff 83.333%),
|
// Reds
|
||||||
// Greens
|
linear-gradient(to top, #ff0000ff calc(100% / 6), #ff000000 calc(200% / 6), #ff000000 calc(400% / 6), #ff0000ff calc(500% / 6)),
|
||||||
linear-gradient(to top, #00ff0000 0%, #00ff00ff 16.666%, #00ff00ff 50%, #00ff0000 66.666%),
|
// Greens
|
||||||
// Blues
|
linear-gradient(to top, #00ff0000 0%, #00ff00ff calc(100% / 6), #00ff00ff 50%, #00ff0000 calc(400% / 6)),
|
||||||
linear-gradient(to top, #0000ff00 33.333%, #0000ffff 50%, #0000ffff 83.333%, #0000ff00 100%);
|
// Blues
|
||||||
--selection-needle-color: var(--hue-color-contrasting);
|
linear-gradient(to top, #0000ff00 calc(200% / 6), #0000ffff 50%, #0000ffff calc(500% / 6), #0000ff00 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.alpha-picker {
|
.alpha-picker {
|
||||||
background: linear-gradient(to bottom, var(--opaque-color), transparent);
|
--selection-needle-color: var(--new-color-contrasting);
|
||||||
--selection-needle-color: var(--new-color-contrasting);
|
background-image: linear-gradient(to bottom, var(--opaque-color), transparent), var(--color-transparent-checkered-background);
|
||||||
|
background-size:
|
||||||
|
100% 100%,
|
||||||
|
var(--color-transparent-checkered-background-size);
|
||||||
|
background-position:
|
||||||
|
0 0,
|
||||||
|
var(--color-transparent-checkered-background-position);
|
||||||
|
background-repeat: no-repeat, var(--color-transparent-checkered-background-repeat);
|
||||||
|
}
|
||||||
|
|
||||||
&::before {
|
.selection-circle {
|
||||||
content: "";
|
position: absolute;
|
||||||
width: 100%;
|
left: 0%;
|
||||||
height: 100%;
|
top: 0%;
|
||||||
z-index: -1;
|
width: 0;
|
||||||
position: relative;
|
height: 0;
|
||||||
background: var(--color-transparent-checkered-background);
|
pointer-events: none;
|
||||||
background-size: var(--color-transparent-checkered-background-size);
|
|
||||||
background-position: var(--color-transparent-checkered-background-position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.selection-circle {
|
&::after {
|
||||||
position: absolute;
|
content: "";
|
||||||
left: 0%;
|
display: block;
|
||||||
top: 0%;
|
position: relative;
|
||||||
width: 0;
|
left: -6px;
|
||||||
height: 0;
|
top: -6px;
|
||||||
pointer-events: none;
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid var(--opaque-color-contrasting);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&::after {
|
.selection-needle {
|
||||||
content: "";
|
position: absolute;
|
||||||
display: block;
|
top: 0%;
|
||||||
position: relative;
|
width: 100%;
|
||||||
left: -6px;
|
height: 0;
|
||||||
top: -6px;
|
pointer-events: none;
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 2px solid var(--opaque-color-contrasting);
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.selection-needle {
|
&::before {
|
||||||
position: absolute;
|
content: "";
|
||||||
top: 0%;
|
position: absolute;
|
||||||
width: 100%;
|
top: -4px;
|
||||||
height: 0;
|
left: 0;
|
||||||
pointer-events: none;
|
border-style: solid;
|
||||||
|
border-width: 4px 0 4px 4px;
|
||||||
|
border-color: transparent transparent transparent var(--selection-needle-color);
|
||||||
|
}
|
||||||
|
|
||||||
&::before {
|
&::after {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -4px;
|
top: -4px;
|
||||||
left: 0;
|
right: 0;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-width: 4px 0 4px 4px;
|
border-width: 4px 4px 4px 0;
|
||||||
border-color: transparent transparent transparent var(--selection-needle-color);
|
border-color: transparent var(--selection-needle-color) transparent transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
.gradient {
|
||||||
content: "";
|
margin-top: 16px;
|
||||||
position: absolute;
|
|
||||||
top: -4px;
|
.spectrum-input {
|
||||||
right: 0;
|
flex: 1 1 100%;
|
||||||
border-style: solid;
|
}
|
||||||
border-width: 4px 4px 4px 0;
|
|
||||||
border-color: transparent var(--selection-needle-color) transparent transparent;
|
.number-input {
|
||||||
|
margin-left: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
width: calc(24px + 8px + 24px);
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.details {
|
.details {
|
||||||
margin-left: 16px;
|
margin-left: 16px;
|
||||||
width: 208px;
|
width: 200px;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
||||||
> .layout-row {
|
> .layout-row {
|
||||||
@@ -539,8 +627,9 @@
|
|||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
|
||||||
> .text-label {
|
> .text-label {
|
||||||
width: 24px;
|
// TODO: Use a table or grid layout for this width to match the widest label. Hard-coding it won't work when we add translation/localization.
|
||||||
flex: 0 0 auto;
|
flex: 0 0 34px;
|
||||||
|
line-height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.leftover-space {
|
&.leftover-space {
|
||||||
@@ -550,15 +639,58 @@
|
|||||||
|
|
||||||
.choice-preview {
|
.choice-preview {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
width: 208px;
|
width: 100%;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
border: 1px solid var(--color-1-nearblack);
|
border: 1px solid var(--color-1-nearblack);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
background-image: var(--color-transparent-checkered-background);
|
||||||
|
background-size: var(--color-transparent-checkered-background-size);
|
||||||
|
background-position: var(--color-transparent-checkered-background-position);
|
||||||
|
background-repeat: var(--color-transparent-checkered-background-repeat);
|
||||||
|
|
||||||
|
.swap-button-background {
|
||||||
|
overflow: hidden;
|
||||||
|
position: absolute;
|
||||||
|
mix-blend-mode: multiply;
|
||||||
|
opacity: 0.25;
|
||||||
|
border-radius: 2px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
width: 50%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
left: 0;
|
||||||
|
background: var(--new-color-contrasting);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
right: 0;
|
||||||
|
background: var(--old-color-contrasting);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.swap-button {
|
||||||
|
position: absolute;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
.new-color {
|
.new-color {
|
||||||
background: linear-gradient(var(--new-color), var(--new-color)), var(--color-transparent-checkered-background);
|
background: var(--new-color);
|
||||||
|
|
||||||
.text-label {
|
.text-label {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@@ -567,22 +699,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.initial-color {
|
.old-color {
|
||||||
background: linear-gradient(var(--initial-color), var(--initial-color)), var(--color-transparent-checkered-background);
|
background: var(--old-color);
|
||||||
|
|
||||||
.text-label {
|
.text-label {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
margin: 2px 8px;
|
margin: 2px 8px;
|
||||||
color: var(--initial-color-contrasting);
|
color: var(--old-color-contrasting);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.new-color,
|
.new-color,
|
||||||
.initial-color {
|
.old-color {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-size: var(--color-transparent-checkered-background-size);
|
|
||||||
background-position: var(--color-transparent-checkered-background-position);
|
|
||||||
|
|
||||||
&.none {
|
&.none {
|
||||||
background: var(--color-none);
|
background: var(--color-none);
|
||||||
@@ -614,8 +744,8 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
width: calc(48px + (48px + 4px) / 2);
|
|
||||||
height: 24px;
|
height: 24px;
|
||||||
|
flex: 1 1 100%;
|
||||||
|
|
||||||
&.none {
|
&.none {
|
||||||
background: var(--color-none);
|
background: var(--color-none);
|
||||||
@@ -643,6 +773,7 @@
|
|||||||
width: 24px;
|
width: 24px;
|
||||||
font-size: 0;
|
font-size: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
|
||||||
div {
|
div {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -654,8 +785,7 @@
|
|||||||
transition: background-color 0.2s ease;
|
transition: background-color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover div,
|
&:hover div {
|
||||||
&:focus div {
|
|
||||||
background: var(--pure-color);
|
background: var(--pure-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -523,7 +523,7 @@
|
|||||||
|
|
||||||
.entry-icon,
|
.entry-icon,
|
||||||
.no-icon {
|
.no-icon {
|
||||||
margin-left: 4px;
|
margin: 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-input-label {
|
.user-input-label {
|
||||||
|
|||||||
@@ -551,9 +551,10 @@
|
|||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
background: var(--color-transparent-checkered-background);
|
background-image: var(--color-transparent-checkered-background);
|
||||||
background-size: var(--color-transparent-checkered-background-size-mini);
|
background-size: var(--color-transparent-checkered-background-size-mini);
|
||||||
background-position: var(--color-transparent-checkered-background-position-mini);
|
background-position: var(--color-transparent-checkered-background-position-mini);
|
||||||
|
background-repeat: var(--color-transparent-checkered-background-repeat);
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
|
|||||||
@@ -1428,9 +1428,10 @@
|
|||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
background: var(--color-transparent-checkered-background);
|
background-image: var(--color-transparent-checkered-background);
|
||||||
background-size: var(--color-transparent-checkered-background-size);
|
background-size: var(--color-transparent-checkered-background-size);
|
||||||
background-position: var(--color-transparent-checkered-background-position);
|
background-position: var(--color-transparent-checkered-background-position);
|
||||||
|
background-repeat: var(--color-transparent-checkered-background-repeat);
|
||||||
}
|
}
|
||||||
|
|
||||||
&::before,
|
&::before,
|
||||||
|
|||||||
@@ -1,33 +1,36 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
import type { Color } from "@graphite/wasm-communication/messages";
|
import type { FillChoice } from "@graphite/wasm-communication/messages";
|
||||||
|
import { Color, Gradient } from "@graphite/wasm-communication/messages";
|
||||||
|
|
||||||
import ColorPicker from "@graphite/components/floating-menus/ColorPicker.svelte";
|
import ColorPicker from "@graphite/components/floating-menus/ColorPicker.svelte";
|
||||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{ value: Color; startHistoryTransaction: undefined }>();
|
const dispatch = createEventDispatcher<{ value: FillChoice; startHistoryTransaction: undefined }>();
|
||||||
|
|
||||||
let open = false;
|
let open = false;
|
||||||
|
|
||||||
export let value: Color;
|
export let value: FillChoice;
|
||||||
export let disabled = false;
|
export let disabled = false;
|
||||||
export let allowNone = false;
|
export let allowNone = false;
|
||||||
// export let allowTransparency = false; // TODO: Implement
|
// export let allowTransparency = false; // TODO: Implement
|
||||||
export let tooltip: string | undefined = undefined;
|
export let tooltip: string | undefined = undefined;
|
||||||
|
|
||||||
|
$: chosenGradient = value instanceof Gradient ? value.toLinearGradientCSS() : `linear-gradient(${value.toHexOptionalAlpha()}, ${value.toHexOptionalAlpha()})`;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<LayoutCol class="color-button" classes={{ disabled, none: value.none, open }} {tooltip}>
|
<LayoutCol class="color-button" classes={{ disabled, none: value instanceof Color ? value.none : false, open }} {tooltip}>
|
||||||
<button {disabled} style:--chosen-color={value.toHexOptionalAlpha()} on:click={() => (open = true)} tabindex="0" data-floating-menu-spawner></button>
|
<button {disabled} style:--chosen-gradient={chosenGradient} on:click={() => (open = true)} tabindex="0" data-floating-menu-spawner></button>
|
||||||
{#if disabled && !value.none}
|
{#if disabled && value instanceof Color && !value.none}
|
||||||
<TextLabel>sRGB</TextLabel>
|
<TextLabel>sRGB</TextLabel>
|
||||||
{/if}
|
{/if}
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
{open}
|
{open}
|
||||||
on:open={({ detail }) => (open = detail)}
|
on:open={({ detail }) => (open = detail)}
|
||||||
color={value}
|
colorOrGradient={value}
|
||||||
on:color={({ detail }) => {
|
on:colorOrGradient={({ detail }) => {
|
||||||
value = detail;
|
value = detail;
|
||||||
dispatch("value", detail);
|
dispatch("value", detail);
|
||||||
}}
|
}}
|
||||||
@@ -71,9 +74,14 @@
|
|||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
width: calc(100% - 4px);
|
width: calc(100% - 4px);
|
||||||
height: calc(100% - 4px);
|
height: calc(100% - 4px);
|
||||||
background: linear-gradient(var(--chosen-color), var(--chosen-color)), var(--color-transparent-checkered-background);
|
background-image: var(--chosen-gradient), var(--color-transparent-checkered-background);
|
||||||
background-size: var(--color-transparent-checkered-background-size);
|
background-size:
|
||||||
background-position: var(--color-transparent-checkered-background-position);
|
100% 100%,
|
||||||
|
var(--color-transparent-checkered-background-size);
|
||||||
|
background-position:
|
||||||
|
0 0,
|
||||||
|
var(--color-transparent-checkered-background-position);
|
||||||
|
background-repeat: no-repeat, var(--color-transparent-checkered-background-repeat);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.none {
|
&.none {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
import MenuList from "@graphite/components/floating-menus/MenuList.svelte";
|
import MenuList from "@graphite/components/floating-menus/MenuList.svelte";
|
||||||
import ConditionalWrapper from "@graphite/components/layout/ConditionalWrapper.svelte";
|
import ConditionalWrapper from "@graphite/components/layout/ConditionalWrapper.svelte";
|
||||||
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
|
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
|
||||||
import Separator from "@graphite/components/widgets/labels/Separator.svelte";
|
|
||||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||||
|
|
||||||
let self: MenuList;
|
let self: MenuList;
|
||||||
@@ -74,9 +73,6 @@
|
|||||||
<IconLabel icon={hoverIcon} />
|
<IconLabel icon={hoverIcon} />
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{#if icon && label}
|
|
||||||
<Separator type={flush ? "Unrelated" : "Related"} />
|
|
||||||
{/if}
|
|
||||||
{#if label}
|
{#if label}
|
||||||
<TextLabel>{label}</TextLabel>
|
<TextLabel>{label}</TextLabel>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -161,22 +157,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget-span.row > & + .text-button,
|
|
||||||
.layout-row > & + .text-button {
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.widget-span.column > & + .text-button,
|
|
||||||
.layout-column > & + .text-button {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-label {
|
.icon-label {
|
||||||
fill: var(--button-text-color);
|
fill: var(--button-text-color);
|
||||||
|
|
||||||
|
+ .text-label {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-label {
|
.text-label {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Custom styling for when multiple TextButton widgets are used next to one another in a row or column
|
||||||
|
.widget-span.row > & + .text-button,
|
||||||
|
.layout-row > & + .text-button {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
.widget-span.column > & + .text-button,
|
||||||
|
.layout-column > & + .text-button {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -128,8 +128,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-icon {
|
.dropdown-icon {
|
||||||
margin: 4px;
|
margin: 4px 8px;
|
||||||
margin-left: 8px;
|
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
|
||||||
& + .dropdown-label {
|
& + .dropdown-label {
|
||||||
|
|||||||
@@ -266,6 +266,9 @@
|
|||||||
// Only drag the number with left click (and when it's valid to do so)
|
// Only drag the number with left click (and when it's valid to do so)
|
||||||
if (e.button !== BUTTON_LEFT || mode !== "Increment" || value === undefined || disabled || editing) return;
|
if (e.button !== BUTTON_LEFT || mode !== "Increment" || value === undefined || disabled || editing) return;
|
||||||
|
|
||||||
|
// Remove the text entry cursor from any other selected text field
|
||||||
|
if (document.activeElement instanceof HTMLElement) document.activeElement.blur();
|
||||||
|
|
||||||
// Don't drag the text value from is input element
|
// Don't drag the text value from is input element
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,254 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher, onDestroy } from "svelte";
|
||||||
|
|
||||||
|
import { Color, type Gradient } from "@graphite/wasm-communication/messages";
|
||||||
|
|
||||||
|
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||||
|
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher<{ activeMarkerIndexChange: number | undefined; gradient: Gradient }>();
|
||||||
|
|
||||||
|
export let gradient: Gradient;
|
||||||
|
export let activeMarkerIndex = 0 as number | undefined;
|
||||||
|
// export let disabled = false;
|
||||||
|
// export let tooltip: string | undefined = undefined;
|
||||||
|
|
||||||
|
let markerTrack: LayoutRow | undefined;
|
||||||
|
|
||||||
|
function markerPointerDown(e: PointerEvent, index: number) {
|
||||||
|
// Left-click to select and begin potentially dragging
|
||||||
|
if (e.button === 0) {
|
||||||
|
activeMarkerIndex = index;
|
||||||
|
dispatch("activeMarkerIndexChange", index);
|
||||||
|
addEvents();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right-click to delete
|
||||||
|
if (e.button === 2) {
|
||||||
|
deleteStopByIndex(index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function markerPosition(e: MouseEvent): number | undefined {
|
||||||
|
const markerTrackRect = markerTrack?.div()?.getBoundingClientRect();
|
||||||
|
if (!markerTrackRect) return;
|
||||||
|
|
||||||
|
const ratio = (e.clientX - markerTrackRect.left) / markerTrackRect.width;
|
||||||
|
|
||||||
|
return Math.max(0, Math.min(1, ratio));
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertStop(e: MouseEvent) {
|
||||||
|
let position = markerPosition(e);
|
||||||
|
if (position === undefined) return;
|
||||||
|
|
||||||
|
let before = gradient.stops.findLast((value) => value.position < position);
|
||||||
|
let after = gradient.stops.find((value) => value.position > position);
|
||||||
|
|
||||||
|
let color = Color.fromCSS("black") as Color;
|
||||||
|
if (before && after) {
|
||||||
|
let t = (position - before.position) / (after.position - before.position);
|
||||||
|
color = before.color.lerp(after.color, t);
|
||||||
|
} else if (before) {
|
||||||
|
color = before.color;
|
||||||
|
} else if (after) {
|
||||||
|
color = after.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
let index = gradient.stops.findIndex((value) => value.position > position);
|
||||||
|
if (index === -1) index = gradient.stops.length;
|
||||||
|
|
||||||
|
gradient.stops.splice(index, 0, { position, color });
|
||||||
|
activeMarkerIndex = index;
|
||||||
|
|
||||||
|
dispatch("activeMarkerIndexChange", index);
|
||||||
|
dispatch("gradient", gradient);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteStop(e: KeyboardEvent) {
|
||||||
|
if (e.key.toLowerCase() !== "delete" && e.key.toLowerCase() !== "backspace") return;
|
||||||
|
if (activeMarkerIndex === undefined) return;
|
||||||
|
deleteStopByIndex(activeMarkerIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteStopByIndex(index: number) {
|
||||||
|
if (gradient.stops.length <= 2) return;
|
||||||
|
|
||||||
|
gradient.stops.splice(index, 1);
|
||||||
|
if (gradient.stops.length === 0) {
|
||||||
|
activeMarkerIndex = undefined;
|
||||||
|
} else {
|
||||||
|
activeMarkerIndex = Math.max(0, Math.min(gradient.stops.length - 1, index));
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch("activeMarkerIndexChange", activeMarkerIndex);
|
||||||
|
dispatch("gradient", gradient);
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveMarker(e: PointerEvent, index: number) {
|
||||||
|
// Just in case the mouseup event is lost
|
||||||
|
if (e.buttons === 0) removeEvents();
|
||||||
|
|
||||||
|
let position = markerPosition(e);
|
||||||
|
if (position === undefined) return;
|
||||||
|
setPosition(index, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setPosition(index: number, position: number) {
|
||||||
|
const active = gradient.stops[index];
|
||||||
|
active.position = position;
|
||||||
|
gradient.stops.sort((a, b) => a.position - b.position);
|
||||||
|
if (gradient.stops.indexOf(active) !== activeMarkerIndex) {
|
||||||
|
activeMarkerIndex = gradient.stops.indexOf(active);
|
||||||
|
dispatch("activeMarkerIndexChange", gradient.stops.indexOf(active));
|
||||||
|
}
|
||||||
|
dispatch("gradient", gradient);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPointerMove(e: PointerEvent) {
|
||||||
|
if (activeMarkerIndex !== undefined) {
|
||||||
|
moveMarker(e, activeMarkerIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPointerUp() {
|
||||||
|
removeEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addEvents() {
|
||||||
|
document.addEventListener("pointermove", onPointerMove);
|
||||||
|
document.addEventListener("pointerup", onPointerUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeEvents() {
|
||||||
|
document.removeEventListener("pointermove", onPointerMove);
|
||||||
|
document.removeEventListener("pointerup", onPointerUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("keydown", deleteStop);
|
||||||
|
onDestroy(() => {
|
||||||
|
removeEvents();
|
||||||
|
document.removeEventListener("keydown", deleteStop);
|
||||||
|
});
|
||||||
|
|
||||||
|
// # Backend -> Frontend
|
||||||
|
// Populate(gradient, { position, color }[], active) // The only way indexes get changed. Frontend drops marker if it's being dragged.
|
||||||
|
// UpdateGradient(gradient)
|
||||||
|
// UpdateMarkers({ index, position, color }[])
|
||||||
|
|
||||||
|
// # Frontend -> Backend
|
||||||
|
// SendNewActive(index)
|
||||||
|
// SendPositions({ index, position }[])
|
||||||
|
// AddMarker(position)
|
||||||
|
// RemoveMarkers(index[])
|
||||||
|
// ResetMarkerToDefault(index)
|
||||||
|
|
||||||
|
// // We need a way to encode constraints on some markers, like locking them in place or preventing reordering
|
||||||
|
// // We need a way to encode the allowability of adding new markers between certain markers, or preventing the deletion of certain markers
|
||||||
|
// // We need the ability to multi-select markers and move them all at once
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<LayoutCol
|
||||||
|
class="spectrum-input"
|
||||||
|
styles={{
|
||||||
|
"--gradient-start": gradient.firstColor()?.toHexOptionalAlpha() || "black",
|
||||||
|
"--gradient-end": gradient.lastColor()?.toHexOptionalAlpha() || "black",
|
||||||
|
"--gradient-stops": gradient.toLinearGradientCSS(),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LayoutRow class="gradient-strip" on:click={insertStop}></LayoutRow>
|
||||||
|
<LayoutRow class="marker-track" bind:this={markerTrack}>
|
||||||
|
{#each gradient.stops as marker, index}
|
||||||
|
<svg
|
||||||
|
style:--marker-position={marker.position}
|
||||||
|
style:--marker-color={marker.color.toRgbCSS()}
|
||||||
|
class="marker"
|
||||||
|
class:active={index === activeMarkerIndex}
|
||||||
|
data-gradient-marker
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 12 12"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
on:pointerdown={(e) => markerPointerDown(e, index)}
|
||||||
|
d="M10,11.5H2c-0.8,0-1.5-0.7-1.5-1.5V6.8c0-0.4,0.2-0.8,0.4-1.1L6,0.7l5.1,5.1c0.3,0.3,0.4,0.7,0.4,1.1V10C11.5,10.8,10.8,11.5,10,11.5z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
on:pointerdown={(e) => markerPointerDown(e, index)}
|
||||||
|
d="M6,1.4L1.3,6.1C1.1,6.3,1,6.6,1,6.8V10c0,0.6,0.4,1,1,1h8c0.6,0,1-0.4,1-1V6.8c0-0.3-0.1-0.5-0.3-0.7L6,1.4M6,0l5.4,5.4C11.8,5.8,12,6.3,12,6.8V10c0,1.1-0.9,2-2,2H2c-1.1,0-2-0.9-2-2V6.8c0-0.5,0.2-1,0.6-1.4L6,0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{/each}
|
||||||
|
</LayoutRow>
|
||||||
|
</LayoutCol>
|
||||||
|
|
||||||
|
<style lang="scss" global>
|
||||||
|
.spectrum-input {
|
||||||
|
--marker-half-width: 6px;
|
||||||
|
|
||||||
|
.gradient-strip {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
height: 16px;
|
||||||
|
background-image:
|
||||||
|
var(--gradient-stops),
|
||||||
|
// Solid start/end colors on either side so the gradient begins at the center of a marker
|
||||||
|
linear-gradient(var(--gradient-start), var(--gradient-start)),
|
||||||
|
linear-gradient(var(--gradient-end), var(--gradient-end)),
|
||||||
|
var(--color-transparent-checkered-background);
|
||||||
|
background-size:
|
||||||
|
calc(100% - 2 * var(--marker-half-width)) 100%,
|
||||||
|
// TODO: Find a solution that avoids visual artifacts where these end colors meet the gradient that appear when viewing with a non-integer zoom or display scaling factor
|
||||||
|
var(--marker-half-width) 100%,
|
||||||
|
var(--marker-half-width) 100%,
|
||||||
|
var(--color-transparent-checkered-background-size);
|
||||||
|
background-position:
|
||||||
|
var(--marker-half-width) 0,
|
||||||
|
left 0,
|
||||||
|
right 0,
|
||||||
|
var(--color-transparent-checkered-background-position);
|
||||||
|
background-repeat: no-repeat, no-repeat, no-repeat, var(--color-transparent-checkered-background-repeat);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker-track {
|
||||||
|
margin-top: calc(24px - 16px - 12px);
|
||||||
|
margin-left: var(--marker-half-width);
|
||||||
|
width: calc(100% - 2 * var(--marker-half-width));
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.marker {
|
||||||
|
position: absolute;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
left: calc(var(--marker-position) * 100%);
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
|
||||||
|
// Inner fill
|
||||||
|
path:first-child {
|
||||||
|
fill: var(--marker-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Outer border
|
||||||
|
path:last-child {
|
||||||
|
fill: var(--color-5-dullgray);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.active) path:first-child:hover + path:last-child,
|
||||||
|
&:not(.active) path:last-child:hover {
|
||||||
|
fill: var(--color-6-lowergray);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Outer border when active
|
||||||
|
&.active path:last-child {
|
||||||
|
fill: var(--color-e-nearwhite);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active path:first-child:hover + path:last-child,
|
||||||
|
&.active path:last-child:hover {
|
||||||
|
fill: var(--color-f-white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
import { getContext } from "svelte";
|
import { getContext } from "svelte";
|
||||||
|
|
||||||
import type { Editor } from "@graphite/wasm-communication/editor";
|
import type { Editor } from "@graphite/wasm-communication/editor";
|
||||||
import type { Color } from "@graphite/wasm-communication/messages";
|
import { Color } from "@graphite/wasm-communication/messages";
|
||||||
|
|
||||||
import ColorPicker from "@graphite/components/floating-menus/ColorPicker.svelte";
|
import ColorPicker from "@graphite/components/floating-menus/ColorPicker.svelte";
|
||||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||||
@@ -38,11 +38,23 @@
|
|||||||
<LayoutCol class="working-colors-button">
|
<LayoutCol class="working-colors-button">
|
||||||
<LayoutRow class="primary swatch">
|
<LayoutRow class="primary swatch">
|
||||||
<button on:click={clickPrimarySwatch} class:open={primaryOpen} style:--swatch-color={primary.toRgbaCSS()} data-floating-menu-spawner="no-hover-transfer" tabindex="0" />
|
<button on:click={clickPrimarySwatch} class:open={primaryOpen} style:--swatch-color={primary.toRgbaCSS()} data-floating-menu-spawner="no-hover-transfer" tabindex="0" />
|
||||||
<ColorPicker open={primaryOpen} on:open={({ detail }) => (primaryOpen = detail)} color={primary} on:color={({ detail }) => primaryColorChanged(detail)} direction="Right" />
|
<ColorPicker
|
||||||
|
open={primaryOpen}
|
||||||
|
on:open={({ detail }) => (primaryOpen = detail)}
|
||||||
|
colorOrGradient={primary}
|
||||||
|
on:colorOrGradient={({ detail }) => detail instanceof Color && primaryColorChanged(detail)}
|
||||||
|
direction="Right"
|
||||||
|
/>
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
<LayoutRow class="secondary swatch">
|
<LayoutRow class="secondary swatch">
|
||||||
<button on:click={clickSecondarySwatch} class:open={secondaryOpen} style:--swatch-color={secondary.toRgbaCSS()} data-floating-menu-spawner="no-hover-transfer" tabindex="0" />
|
<button on:click={clickSecondarySwatch} class:open={secondaryOpen} style:--swatch-color={secondary.toRgbaCSS()} data-floating-menu-spawner="no-hover-transfer" tabindex="0" />
|
||||||
<ColorPicker open={secondaryOpen} on:open={({ detail }) => (secondaryOpen = detail)} color={secondary} on:color={({ detail }) => secondaryColorChanged(detail)} direction="Right" />
|
<ColorPicker
|
||||||
|
open={secondaryOpen}
|
||||||
|
on:open={({ detail }) => (secondaryOpen = detail)}
|
||||||
|
colorOrGradient={secondary}
|
||||||
|
on:colorOrGradient={({ detail }) => detail instanceof Color && secondaryColorChanged(detail)}
|
||||||
|
direction="Right"
|
||||||
|
/>
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
</LayoutCol>
|
</LayoutCol>
|
||||||
|
|
||||||
@@ -67,8 +79,13 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: linear-gradient(var(--swatch-color), var(--swatch-color)), var(--color-transparent-checkered-background);
|
background: linear-gradient(var(--swatch-color), var(--swatch-color)), var(--color-transparent-checkered-background);
|
||||||
background-size: var(--color-transparent-checkered-background-size);
|
background-size:
|
||||||
background-position: var(--color-transparent-checkered-background-position);
|
100% 100%,
|
||||||
|
var(--color-transparent-checkered-background-size);
|
||||||
|
background-position:
|
||||||
|
0 0,
|
||||||
|
var(--color-transparent-checkered-background-position);
|
||||||
|
background-repeat: no-repeat, var(--color-transparent-checkered-background-repeat);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ import Link from "@graphite-frontend/assets/icon-12px-solid/link.svg";
|
|||||||
import Overlays from "@graphite-frontend/assets/icon-12px-solid/overlays.svg";
|
import Overlays from "@graphite-frontend/assets/icon-12px-solid/overlays.svg";
|
||||||
import Remove from "@graphite-frontend/assets/icon-12px-solid/remove.svg";
|
import Remove from "@graphite-frontend/assets/icon-12px-solid/remove.svg";
|
||||||
import Snapping from "@graphite-frontend/assets/icon-12px-solid/snapping.svg";
|
import Snapping from "@graphite-frontend/assets/icon-12px-solid/snapping.svg";
|
||||||
import Swap from "@graphite-frontend/assets/icon-12px-solid/swap.svg";
|
import SwapHorizontal from "@graphite-frontend/assets/icon-12px-solid/swap-horizontal.svg";
|
||||||
|
import SwapVertical from "@graphite-frontend/assets/icon-12px-solid/swap-vertical.svg";
|
||||||
import VerticalEllipsis from "@graphite-frontend/assets/icon-12px-solid/vertical-ellipsis.svg";
|
import VerticalEllipsis from "@graphite-frontend/assets/icon-12px-solid/vertical-ellipsis.svg";
|
||||||
import Warning from "@graphite-frontend/assets/icon-12px-solid/warning.svg";
|
import Warning from "@graphite-frontend/assets/icon-12px-solid/warning.svg";
|
||||||
import WindowButtonWinClose from "@graphite-frontend/assets/icon-12px-solid/window-button-win-close.svg";
|
import WindowButtonWinClose from "@graphite-frontend/assets/icon-12px-solid/window-button-win-close.svg";
|
||||||
@@ -78,7 +79,8 @@ const SOLID_12PX = {
|
|||||||
Overlays: { svg: Overlays, size: 12 },
|
Overlays: { svg: Overlays, size: 12 },
|
||||||
Remove: { svg: Remove, size: 12 },
|
Remove: { svg: Remove, size: 12 },
|
||||||
Snapping: { svg: Snapping, size: 12 },
|
Snapping: { svg: Snapping, size: 12 },
|
||||||
Swap: { svg: Swap, size: 12 },
|
SwapHorizontal: { svg: SwapHorizontal, size: 12 },
|
||||||
|
SwapVertical: { svg: SwapVertical, size: 12 },
|
||||||
VerticalEllipsis: { svg: VerticalEllipsis, size: 12 },
|
VerticalEllipsis: { svg: VerticalEllipsis, size: 12 },
|
||||||
Warning: { svg: Warning, size: 12 },
|
Warning: { svg: Warning, size: 12 },
|
||||||
WindowButtonWinClose: { svg: WindowButtonWinClose, size: 12 },
|
WindowButtonWinClose: { svg: WindowButtonWinClose, size: 12 },
|
||||||
|
|||||||
@@ -217,8 +217,62 @@ export type HSV = { h: number; s: number; v: number };
|
|||||||
export type RGBA = { r: number; g: number; b: number; a: number };
|
export type RGBA = { r: number; g: number; b: number; a: number };
|
||||||
export type RGB = { r: number; g: number; b: number };
|
export type RGB = { r: number; g: number; b: number };
|
||||||
|
|
||||||
|
export class Gradient {
|
||||||
|
readonly stops!: { position: number; color: Color }[];
|
||||||
|
|
||||||
|
constructor(stops: { position: number; color: Color }[]) {
|
||||||
|
this.stops = stops;
|
||||||
|
}
|
||||||
|
|
||||||
|
toLinearGradientCSS(): string {
|
||||||
|
if (this.stops.length === 1) {
|
||||||
|
return `linear-gradient(to right, ${this.stops[0].color.toHexOptionalAlpha()} 0%, ${this.stops[0].color.toHexOptionalAlpha()} 100%)`;
|
||||||
|
}
|
||||||
|
const pieces = this.stops.map((stop) => `${stop.color.toHexOptionalAlpha()} ${stop.position * 100}%`);
|
||||||
|
return `linear-gradient(to right, ${pieces.join(", ")})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
toLinearGradientCSSNoAlpha(): string {
|
||||||
|
if (this.stops.length === 1) {
|
||||||
|
return `linear-gradient(to right, ${this.stops[0].color.toHexNoAlpha()} 0%, ${this.stops[0].color.toHexNoAlpha()} 100%)`;
|
||||||
|
}
|
||||||
|
const pieces = this.stops.map((stop) => `${stop.color.toHexNoAlpha()} ${stop.position * 100}%`);
|
||||||
|
return `linear-gradient(to right, ${pieces.join(", ")})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
firstColor(): Color | undefined {
|
||||||
|
return this.stops[0]?.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastColor(): Color | undefined {
|
||||||
|
return this.stops[this.stops.length - 1]?.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
atIndex(index: number): { position: number; color: Color } | undefined {
|
||||||
|
return this.stops[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
colorAtIndex(index: number): Color | undefined {
|
||||||
|
return this.stops[index]?.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
positionAtIndex(index: number): number | undefined {
|
||||||
|
return this.stops[index]?.position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// All channels range from 0 to 1
|
// All channels range from 0 to 1
|
||||||
export class Color {
|
export class Color {
|
||||||
|
readonly red!: number;
|
||||||
|
|
||||||
|
readonly green!: number;
|
||||||
|
|
||||||
|
readonly blue!: number;
|
||||||
|
|
||||||
|
readonly alpha!: number;
|
||||||
|
|
||||||
|
readonly none!: boolean;
|
||||||
|
|
||||||
constructor();
|
constructor();
|
||||||
|
|
||||||
constructor(none: "none");
|
constructor(none: "none");
|
||||||
@@ -266,16 +320,6 @@ export class Color {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly red!: number;
|
|
||||||
|
|
||||||
readonly green!: number;
|
|
||||||
|
|
||||||
readonly blue!: number;
|
|
||||||
|
|
||||||
readonly alpha!: number;
|
|
||||||
|
|
||||||
readonly none!: boolean;
|
|
||||||
|
|
||||||
static fromCSS(colorCode: string): Color | undefined {
|
static fromCSS(colorCode: string): Color | undefined {
|
||||||
// Allow single-digit hex value inputs
|
// Allow single-digit hex value inputs
|
||||||
let colorValue = colorCode.trim();
|
let colorValue = colorCode.trim();
|
||||||
@@ -313,6 +357,15 @@ export class Color {
|
|||||||
return new Color(r / 255, g / 255, b / 255, a / 255);
|
return new Color(r / 255, g / 255, b / 255, a / 255);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
equals(other: Color): boolean {
|
||||||
|
if (this.none && other.none) return true;
|
||||||
|
return Math.abs(this.red - other.red) < 1e-6 && Math.abs(this.green - other.green) < 1e-6 && Math.abs(this.blue - other.blue) < 1e-6 && Math.abs(this.alpha - other.alpha) < 1e-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
lerp(other: Color, t: number): Color {
|
||||||
|
return new Color(this.red * (1 - t) + other.red * t, this.green * (1 - t) + other.green * t, this.blue * (1 - t) + other.blue * t, this.alpha * (1 - t) + other.alpha * t);
|
||||||
|
}
|
||||||
|
|
||||||
toHexNoAlpha(): string | undefined {
|
toHexNoAlpha(): string | undefined {
|
||||||
if (this.none) return undefined;
|
if (this.none) return undefined;
|
||||||
|
|
||||||
@@ -350,29 +403,18 @@ export class Color {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
toRgba255(): RGBA | undefined {
|
|
||||||
if (this.none) return undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
r: Math.round(this.red * 255),
|
|
||||||
g: Math.round(this.green * 255),
|
|
||||||
b: Math.round(this.blue * 255),
|
|
||||||
a: Math.round(this.alpha * 255),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
toRgbCSS(): string | undefined {
|
toRgbCSS(): string | undefined {
|
||||||
const rgba = this.toRgba255();
|
const rgb = this.toRgb255();
|
||||||
if (!rgba) return undefined;
|
if (!rgb) return undefined;
|
||||||
|
|
||||||
return `rgb(${rgba.r}, ${rgba.g}, ${rgba.b})`;
|
return `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
toRgbaCSS(): string | undefined {
|
toRgbaCSS(): string | undefined {
|
||||||
const rgba = this.toRgba255();
|
const rgb = this.toRgb255();
|
||||||
if (!rgba) return undefined;
|
if (!rgb) return undefined;
|
||||||
|
|
||||||
return `rgba(${rgba.r}, ${rgba.g}, ${rgba.b}, ${rgba.a})`;
|
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${this.alpha})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
toHSV(): HSV | undefined {
|
toHSV(): HSV | undefined {
|
||||||
@@ -700,10 +742,24 @@ export class CheckboxInput extends WidgetProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ColorButton extends WidgetProps {
|
export class ColorButton extends WidgetProps {
|
||||||
@Transform(({ value }: { value: { red: number; green: number; blue: number; alpha: number } | undefined }) =>
|
@Transform(({ value }) => {
|
||||||
value === undefined ? new Color("none") : new Color(value.red, value.green, value.blue, value.alpha),
|
const gradient = value["Gradient"];
|
||||||
)
|
if (gradient) {
|
||||||
value!: Color;
|
const stops = gradient.map(([position, color]: [number, color: { red: number; green: number; blue: number; alpha: number }]) => ({
|
||||||
|
position,
|
||||||
|
color: new Color(color.red, color.green, color.blue, color.alpha),
|
||||||
|
}));
|
||||||
|
return new Gradient(stops);
|
||||||
|
}
|
||||||
|
|
||||||
|
const solid = value["Solid"];
|
||||||
|
if (solid) {
|
||||||
|
return new Color(solid.red, solid.green, solid.blue, solid.alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Color("none");
|
||||||
|
})
|
||||||
|
value!: FillChoice;
|
||||||
|
|
||||||
disabled!: boolean;
|
disabled!: boolean;
|
||||||
|
|
||||||
@@ -715,6 +771,8 @@ export class ColorButton extends WidgetProps {
|
|||||||
tooltip!: string | undefined;
|
tooltip!: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FillChoice = Color | Gradient;
|
||||||
|
|
||||||
type MenuEntryCommon = {
|
type MenuEntryCommon = {
|
||||||
label: string;
|
label: string;
|
||||||
icon?: IconName;
|
icon?: IconName;
|
||||||
|
|||||||
@@ -265,6 +265,9 @@ impl Color {
|
|||||||
pub const RED: Color = Color::from_rgbf32_unchecked(1., 0., 0.);
|
pub const RED: Color = Color::from_rgbf32_unchecked(1., 0., 0.);
|
||||||
pub const GREEN: Color = Color::from_rgbf32_unchecked(0., 1., 0.);
|
pub const GREEN: Color = Color::from_rgbf32_unchecked(0., 1., 0.);
|
||||||
pub const BLUE: Color = Color::from_rgbf32_unchecked(0., 0., 1.);
|
pub const BLUE: Color = Color::from_rgbf32_unchecked(0., 0., 1.);
|
||||||
|
pub const YELLOW: Color = Color::from_rgbf32_unchecked(1., 1., 0.);
|
||||||
|
pub const CYAN: Color = Color::from_rgbf32_unchecked(0., 1., 1.);
|
||||||
|
pub const MAGENTA: Color = Color::from_rgbf32_unchecked(1., 0., 1.);
|
||||||
pub const TRANSPARENT: Color = Self {
|
pub const TRANSPARENT: Color = Self {
|
||||||
red: 0.,
|
red: 0.,
|
||||||
green: 0.,
|
green: 0.,
|
||||||
|
|||||||
@@ -27,29 +27,62 @@ pub enum GradientType {
|
|||||||
Radial,
|
Radial,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Someday we could switch this to a Box[T] to avoid over-allocation
|
||||||
|
/// A list of colors associated with positions (in the range 0 to 1) along a gradient.
|
||||||
|
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, specta::Type)]
|
||||||
|
pub struct GradientStops(pub Vec<(f64, Color)>);
|
||||||
|
|
||||||
|
impl std::hash::Hash for GradientStops {
|
||||||
|
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||||
|
self.0.len().hash(state);
|
||||||
|
self.0.iter().for_each(|(position, color)| {
|
||||||
|
position.to_bits().hash(state);
|
||||||
|
color.hash(state);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for GradientStops {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(vec![(0., Color::BLACK), (1., Color::WHITE)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A gradient fill.
|
/// A gradient fill.
|
||||||
///
|
///
|
||||||
/// Contains the start and end points, along with the colors at varying points along the length.
|
/// Contains the start and end points, along with the colors at varying points along the length.
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize, DynAny, specta::Type)]
|
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, specta::Type)]
|
||||||
pub struct Gradient {
|
pub struct Gradient {
|
||||||
|
pub stops: GradientStops,
|
||||||
|
pub gradient_type: GradientType,
|
||||||
pub start: DVec2,
|
pub start: DVec2,
|
||||||
pub end: DVec2,
|
pub end: DVec2,
|
||||||
pub transform: DAffine2,
|
pub transform: DAffine2,
|
||||||
pub positions: Vec<(f64, Color)>,
|
}
|
||||||
pub gradient_type: GradientType,
|
|
||||||
|
impl Default for Gradient {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
stops: GradientStops::default(),
|
||||||
|
gradient_type: GradientType::Linear,
|
||||||
|
start: DVec2::new(0., 0.5),
|
||||||
|
end: DVec2::new(1., 0.5),
|
||||||
|
transform: DAffine2::IDENTITY,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl core::hash::Hash for Gradient {
|
impl core::hash::Hash for Gradient {
|
||||||
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||||
self.positions.len().hash(state);
|
self.stops.0.len().hash(state);
|
||||||
[].iter()
|
[].iter()
|
||||||
.chain(self.start.to_array().iter())
|
.chain(self.start.to_array().iter())
|
||||||
.chain(self.end.to_array().iter())
|
.chain(self.end.to_array().iter())
|
||||||
.chain(self.transform.to_cols_array().iter())
|
.chain(self.transform.to_cols_array().iter())
|
||||||
.chain(self.positions.iter().map(|(position, _)| position))
|
.chain(self.stops.0.iter().map(|(position, _)| position))
|
||||||
.for_each(|x| x.to_bits().hash(state));
|
.for_each(|x| x.to_bits().hash(state));
|
||||||
self.positions.iter().for_each(|(_, color)| color.hash(state));
|
self.stops.0.iter().for_each(|(_, color)| color.hash(state));
|
||||||
self.gradient_type.hash(state);
|
self.gradient_type.hash(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,7 +93,7 @@ impl Gradient {
|
|||||||
Gradient {
|
Gradient {
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
positions: vec![(0., start_color), (1., end_color)],
|
stops: GradientStops(vec![(0., start_color), (1., end_color)]),
|
||||||
transform,
|
transform,
|
||||||
gradient_type,
|
gradient_type,
|
||||||
}
|
}
|
||||||
@@ -70,23 +103,25 @@ impl Gradient {
|
|||||||
let start = self.start + (other.start - self.start) * time;
|
let start = self.start + (other.start - self.start) * time;
|
||||||
let end = self.end + (other.end - self.end) * time;
|
let end = self.end + (other.end - self.end) * time;
|
||||||
let transform = self.transform;
|
let transform = self.transform;
|
||||||
let positions = self
|
let stops = self
|
||||||
.positions
|
.stops
|
||||||
|
.0
|
||||||
.iter()
|
.iter()
|
||||||
.zip(other.positions.iter())
|
.zip(other.stops.0.iter())
|
||||||
.map(|((a_pos, a_color), (b_pos, b_color))| {
|
.map(|((a_pos, a_color), (b_pos, b_color))| {
|
||||||
let position = a_pos + (b_pos - a_pos) * time;
|
let position = a_pos + (b_pos - a_pos) * time;
|
||||||
let color = a_color.lerp(b_color, time as f32);
|
let color = a_color.lerp(b_color, time as f32);
|
||||||
(position, color)
|
(position, color)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
let stops = GradientStops(stops);
|
||||||
let gradient_type = if time < 0.5 { self.gradient_type } else { other.gradient_type };
|
let gradient_type = if time < 0.5 { self.gradient_type } else { other.gradient_type };
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
transform,
|
transform,
|
||||||
positions,
|
stops,
|
||||||
gradient_type,
|
gradient_type,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,9 +132,9 @@ impl Gradient {
|
|||||||
let transformed_bound_transform = DAffine2::from_scale_angle_translation(transformed_bounds[1] - transformed_bounds[0], 0., transformed_bounds[0]);
|
let transformed_bound_transform = DAffine2::from_scale_angle_translation(transformed_bounds[1] - transformed_bounds[0], 0., transformed_bounds[0]);
|
||||||
let updated_transform = multiplied_transform * bound_transform;
|
let updated_transform = multiplied_transform * bound_transform;
|
||||||
|
|
||||||
let mut positions = String::new();
|
let mut stop = String::new();
|
||||||
for (position, color) in self.positions.iter() {
|
for (position, color) in self.stops.0.iter() {
|
||||||
let _ = write!(positions, r##"<stop offset="{}" stop-color="#{}" />"##, position, color.with_alpha(color.a()).rgba_hex());
|
let _ = write!(stop, r##"<stop offset="{}" stop-color="#{}" />"##, position, color.with_alpha(color.a()).rgba_hex());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mod_gradient = transformed_bound_transform.inverse();
|
let mod_gradient = transformed_bound_transform.inverse();
|
||||||
@@ -121,7 +156,7 @@ impl Gradient {
|
|||||||
let _ = write!(
|
let _ = write!(
|
||||||
svg_defs,
|
svg_defs,
|
||||||
r#"<linearGradient id="{}" x1="{}" x2="{}" y1="{}" y2="{}" gradientTransform="matrix({})">{}</linearGradient>"#,
|
r#"<linearGradient id="{}" x1="{}" x2="{}" y1="{}" y2="{}" gradientTransform="matrix({})">{}</linearGradient>"#,
|
||||||
gradient_id, start.x, end.x, start.y, end.y, transform, positions
|
gradient_id, start.x, end.x, start.y, end.y, transform, stop
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
GradientType::Radial => {
|
GradientType::Radial => {
|
||||||
@@ -129,7 +164,7 @@ impl Gradient {
|
|||||||
let _ = write!(
|
let _ = write!(
|
||||||
svg_defs,
|
svg_defs,
|
||||||
r#"<radialGradient id="{}" cx="{}" cy="{}" r="{}" gradientTransform="matrix({})">{}</radialGradient>"#,
|
r#"<radialGradient id="{}" cx="{}" cy="{}" r="{}" gradientTransform="matrix({})">{}</radialGradient>"#,
|
||||||
gradient_id, start.x, start.y, radius, transform, positions
|
gradient_id, start.x, start.y, radius, transform, stop
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,11 +186,11 @@ impl Gradient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Compute the color of the inserted stop
|
// Compute the color of the inserted stop
|
||||||
let get_color = |index: usize, time: f64| match (self.positions[index].1, self.positions.get(index + 1).map(|(_, c)| *c)) {
|
let get_color = |index: usize, time: f64| match (self.stops.0[index].1, self.stops.0.get(index + 1).map(|(_, c)| *c)) {
|
||||||
// Lerp between the nearest colors if applicable
|
// Lerp between the nearest colors if applicable
|
||||||
(a, Some(b)) => a.lerp(
|
(a, Some(b)) => a.lerp(
|
||||||
&b,
|
&b,
|
||||||
((time - self.positions[index].0) / self.positions.get(index + 1).map(|end| end.0 - self.positions[index].0).unwrap_or_default()) as f32,
|
((time - self.stops.0[index].0) / self.stops.0.get(index + 1).map(|end| end.0 - self.stops.0[index].0).unwrap_or_default()) as f32,
|
||||||
),
|
),
|
||||||
// Use the start or the end color if applicable
|
// Use the start or the end color if applicable
|
||||||
(v, _) => v,
|
(v, _) => v,
|
||||||
@@ -163,14 +198,14 @@ impl Gradient {
|
|||||||
|
|
||||||
// Compute the correct index to keep the positions in order
|
// Compute the correct index to keep the positions in order
|
||||||
let mut index = 0;
|
let mut index = 0;
|
||||||
while self.positions.len() > index && self.positions[index].0 <= new_position {
|
while self.stops.0.len() > index && self.stops.0[index].0 <= new_position {
|
||||||
index += 1;
|
index += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let new_color = get_color(index - 1, new_position);
|
let new_color = get_color(index - 1, new_position);
|
||||||
|
|
||||||
// Insert the new stop
|
// Insert the new stop
|
||||||
self.positions.insert(index, (new_position, new_color));
|
self.stops.0.insert(index, (new_position, new_color));
|
||||||
|
|
||||||
Some(index)
|
Some(index)
|
||||||
}
|
}
|
||||||
@@ -178,7 +213,9 @@ impl Gradient {
|
|||||||
|
|
||||||
/// Describes the fill of a layer.
|
/// Describes the fill of a layer.
|
||||||
///
|
///
|
||||||
/// Can be None, a solid [Color], a linear [Gradient], a radial [Gradient] or potentially some sort of image or pattern in the future
|
/// Can be None, a solid [Color], or a linear/radial [Gradient].
|
||||||
|
///
|
||||||
|
/// In the future we'll probably also add a pattern fill.
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, specta::Type)]
|
#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, specta::Type)]
|
||||||
pub enum Fill {
|
pub enum Fill {
|
||||||
@@ -207,8 +244,8 @@ impl Fill {
|
|||||||
match self {
|
match self {
|
||||||
Self::None => Color::BLACK,
|
Self::None => Color::BLACK,
|
||||||
Self::Solid(color) => *color,
|
Self::Solid(color) => *color,
|
||||||
// TODO: Should correctly sample the gradient
|
// TODO: Should correctly sample the gradient the equation here: https://svgwg.org/svg2-draft/pservers.html#Gradients
|
||||||
Self::Gradient(Gradient { positions, .. }) => positions[0].1,
|
Self::Gradient(Gradient { stops, .. }) => stops.0[0].1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,13 +258,13 @@ impl Fill {
|
|||||||
(Self::Solid(a), Self::Solid(b)) => Self::Solid(a.lerp(b, time as f32)),
|
(Self::Solid(a), Self::Solid(b)) => Self::Solid(a.lerp(b, time as f32)),
|
||||||
(Self::Solid(a), Self::Gradient(b)) => {
|
(Self::Solid(a), Self::Gradient(b)) => {
|
||||||
let mut solid_to_gradient = b.clone();
|
let mut solid_to_gradient = b.clone();
|
||||||
solid_to_gradient.positions.iter_mut().for_each(|(_, color)| *color = *a);
|
solid_to_gradient.stops.0.iter_mut().for_each(|(_, color)| *color = *a);
|
||||||
let a = &solid_to_gradient;
|
let a = &solid_to_gradient;
|
||||||
Self::Gradient(a.lerp(b, time))
|
Self::Gradient(a.lerp(b, time))
|
||||||
}
|
}
|
||||||
(Self::Gradient(a), Self::Solid(b)) => {
|
(Self::Gradient(a), Self::Solid(b)) => {
|
||||||
let mut gradient_to_solid = a.clone();
|
let mut gradient_to_solid = a.clone();
|
||||||
gradient_to_solid.positions.iter_mut().for_each(|(_, color)| *color = *b);
|
gradient_to_solid.stops.0.iter_mut().for_each(|(_, color)| *color = *b);
|
||||||
let b = &gradient_to_solid;
|
let b = &gradient_to_solid;
|
||||||
Self::Gradient(a.lerp(b, time))
|
Self::Gradient(a.lerp(b, time))
|
||||||
}
|
}
|
||||||
@@ -248,22 +285,91 @@ impl Fill {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the fill is not none
|
|
||||||
pub fn is_some(&self) -> bool {
|
|
||||||
*self != Self::None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extract a gradient from the fill
|
/// Extract a gradient from the fill
|
||||||
pub fn as_gradient(&self) -> Option<&Gradient> {
|
pub fn as_gradient(&self) -> Option<&Gradient> {
|
||||||
if let Self::Gradient(gradient) = self {
|
match self {
|
||||||
Some(gradient)
|
Self::Gradient(gradient) => Some(gradient),
|
||||||
} else {
|
_ => None,
|
||||||
None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enum describing the type of [Fill]
|
impl From<Color> for Fill {
|
||||||
|
fn from(color: Color) -> Fill {
|
||||||
|
Fill::Solid(color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Option<Color>> for Fill {
|
||||||
|
fn from(color: Option<Color>) -> Fill {
|
||||||
|
Fill::solid_or_none(color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Gradient> for Fill {
|
||||||
|
fn from(gradient: Gradient) -> Fill {
|
||||||
|
Fill::Gradient(gradient)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes the fill of a layer, but unlike [`Fill`], this doesn't store a [`Gradient`] directly but just its [`GradientStops`].
|
||||||
|
///
|
||||||
|
/// Can be None, a solid [Color], or a linear/radial [Gradient].
|
||||||
|
///
|
||||||
|
/// In the future we'll probably also add a pattern fill.
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, specta::Type)]
|
||||||
|
pub enum FillChoice {
|
||||||
|
#[default]
|
||||||
|
None,
|
||||||
|
Solid(Color),
|
||||||
|
Gradient(GradientStops),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FillChoice {
|
||||||
|
pub fn from_optional_color(color: Option<Color>) -> Self {
|
||||||
|
match color {
|
||||||
|
Some(color) => Self::Solid(color),
|
||||||
|
None => Self::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_solid(&self) -> Option<Color> {
|
||||||
|
let Self::Solid(color) = self else { return None };
|
||||||
|
Some(*color)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_gradient(&self) -> Option<&GradientStops> {
|
||||||
|
let Self::Gradient(gradient) = self else { return None };
|
||||||
|
Some(gradient)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert this [`FillChoice`] to a [`Fill`] using the provided [`Gradient`] as a base for the positional information of the gradient.
|
||||||
|
/// If a gradient isn't provided, default gradient positional information is used in cases where the [`FillChoice`] is a [`Gradient`].
|
||||||
|
pub fn to_fill(&self, existing_gradient: Option<&Gradient>) -> Fill {
|
||||||
|
match self {
|
||||||
|
Self::None => Fill::None,
|
||||||
|
Self::Solid(color) => Fill::Solid(*color),
|
||||||
|
Self::Gradient(stops) => {
|
||||||
|
let mut fill = existing_gradient.cloned().unwrap_or_default();
|
||||||
|
fill.stops = stops.clone();
|
||||||
|
Fill::Gradient(fill)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Fill> for FillChoice {
|
||||||
|
fn from(fill: Fill) -> Self {
|
||||||
|
match fill {
|
||||||
|
Fill::None => FillChoice::None,
|
||||||
|
Fill::Solid(color) => FillChoice::Solid(color),
|
||||||
|
Fill::Gradient(gradient) => FillChoice::Gradient(gradient.stops),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enum describing the type of [Fill].
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, specta::Type)]
|
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, specta::Type)]
|
||||||
pub enum FillType {
|
pub enum FillType {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use super::misc::CentroidType;
|
use super::misc::CentroidType;
|
||||||
use super::style::{Fill, FillType, Gradient, GradientType, Stroke};
|
use super::style::{Fill, Stroke};
|
||||||
use super::{PointId, SegmentId, StrokeId, VectorData};
|
use super::{PointId, SegmentId, StrokeId, VectorData};
|
||||||
use crate::renderer::GraphicElementRendered;
|
use crate::renderer::GraphicElementRendered;
|
||||||
use crate::transform::{Footprint, Transform, TransformMut};
|
use crate::transform::{Footprint, Transform, TransformMut};
|
||||||
@@ -11,37 +11,14 @@ use glam::{DAffine2, DVec2};
|
|||||||
use rand::{Rng, SeedableRng};
|
use rand::{Rng, SeedableRng};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct SetFillNode<FillType, SolidColor, GradientType, Start, End, Transform, Positions> {
|
pub struct SetFillNode<Fill> {
|
||||||
fill_type: FillType,
|
fill: Fill,
|
||||||
solid_color: SolidColor,
|
|
||||||
gradient_type: GradientType,
|
|
||||||
start: Start,
|
|
||||||
end: End,
|
|
||||||
transform: Transform,
|
|
||||||
positions: Positions,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[node_macro::node_fn(SetFillNode)]
|
#[node_macro::node_fn(SetFillNode)]
|
||||||
fn set_vector_data_fill(
|
fn set_vector_data_fill<T: Into<Fill>>(mut vector_data: VectorData, fill: T) -> VectorData {
|
||||||
mut vector_data: VectorData,
|
vector_data.style.set_fill(fill.into());
|
||||||
fill_type: FillType,
|
|
||||||
solid_color: Option<Color>,
|
|
||||||
gradient_type: GradientType,
|
|
||||||
start: DVec2,
|
|
||||||
end: DVec2,
|
|
||||||
transform: DAffine2,
|
|
||||||
positions: Vec<(f64, Color)>,
|
|
||||||
) -> VectorData {
|
|
||||||
vector_data.style.set_fill(match fill_type {
|
|
||||||
FillType::Solid => solid_color.map_or(Fill::None, Fill::Solid),
|
|
||||||
FillType::Gradient => Fill::Gradient(Gradient {
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
transform,
|
|
||||||
positions,
|
|
||||||
gradient_type,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
vector_data
|
vector_data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,8 +59,11 @@ pub enum TaggedValue {
|
|||||||
LineCap(graphene_core::vector::style::LineCap),
|
LineCap(graphene_core::vector::style::LineCap),
|
||||||
LineJoin(graphene_core::vector::style::LineJoin),
|
LineJoin(graphene_core::vector::style::LineJoin),
|
||||||
FillType(graphene_core::vector::style::FillType),
|
FillType(graphene_core::vector::style::FillType),
|
||||||
|
FillChoice(graphene_core::vector::style::FillChoice),
|
||||||
|
Gradient(graphene_core::vector::style::Gradient),
|
||||||
GradientType(graphene_core::vector::style::GradientType),
|
GradientType(graphene_core::vector::style::GradientType),
|
||||||
GradientPositions(Vec<(f64, graphene_core::Color)>),
|
#[serde(alias = "GradientPositions")] // TODO: Eventually remove this alias (probably starting late 2024)
|
||||||
|
GradientStops(graphene_core::vector::style::GradientStops),
|
||||||
Quantization(graphene_core::quantization::QuantizationChannels),
|
Quantization(graphene_core::quantization::QuantizationChannels),
|
||||||
OptionalColor(Option<graphene_core::raster::color::Color>),
|
OptionalColor(Option<graphene_core::raster::color::Color>),
|
||||||
ManipulatorGroupIds(Vec<graphene_core::uuid::ManipulatorGroupId>),
|
ManipulatorGroupIds(Vec<graphene_core::uuid::ManipulatorGroupId>),
|
||||||
@@ -130,10 +133,12 @@ impl Hash for TaggedValue {
|
|||||||
Self::LineCap(x) => x.hash(state),
|
Self::LineCap(x) => x.hash(state),
|
||||||
Self::LineJoin(x) => x.hash(state),
|
Self::LineJoin(x) => x.hash(state),
|
||||||
Self::FillType(x) => x.hash(state),
|
Self::FillType(x) => x.hash(state),
|
||||||
|
Self::FillChoice(x) => x.hash(state),
|
||||||
|
Self::Gradient(x) => x.hash(state),
|
||||||
Self::GradientType(x) => x.hash(state),
|
Self::GradientType(x) => x.hash(state),
|
||||||
Self::GradientPositions(x) => {
|
Self::GradientStops(x) => {
|
||||||
x.len().hash(state);
|
x.0.len().hash(state);
|
||||||
for (position, color) in x {
|
for (position, color) in &x.0 {
|
||||||
position.to_bits().hash(state);
|
position.to_bits().hash(state);
|
||||||
color.hash(state);
|
color.hash(state);
|
||||||
}
|
}
|
||||||
@@ -208,8 +213,10 @@ impl<'a> TaggedValue {
|
|||||||
TaggedValue::LineCap(x) => Box::new(x),
|
TaggedValue::LineCap(x) => Box::new(x),
|
||||||
TaggedValue::LineJoin(x) => Box::new(x),
|
TaggedValue::LineJoin(x) => Box::new(x),
|
||||||
TaggedValue::FillType(x) => Box::new(x),
|
TaggedValue::FillType(x) => Box::new(x),
|
||||||
|
TaggedValue::FillChoice(x) => Box::new(x),
|
||||||
|
TaggedValue::Gradient(x) => Box::new(x),
|
||||||
TaggedValue::GradientType(x) => Box::new(x),
|
TaggedValue::GradientType(x) => Box::new(x),
|
||||||
TaggedValue::GradientPositions(x) => Box::new(x),
|
TaggedValue::GradientStops(x) => Box::new(x),
|
||||||
TaggedValue::Quantization(x) => Box::new(x),
|
TaggedValue::Quantization(x) => Box::new(x),
|
||||||
TaggedValue::OptionalColor(x) => Box::new(x),
|
TaggedValue::OptionalColor(x) => Box::new(x),
|
||||||
TaggedValue::ManipulatorGroupIds(x) => Box::new(x),
|
TaggedValue::ManipulatorGroupIds(x) => Box::new(x),
|
||||||
@@ -287,8 +294,10 @@ impl<'a> TaggedValue {
|
|||||||
TaggedValue::LineCap(_) => concrete!(graphene_core::vector::style::LineCap),
|
TaggedValue::LineCap(_) => concrete!(graphene_core::vector::style::LineCap),
|
||||||
TaggedValue::LineJoin(_) => concrete!(graphene_core::vector::style::LineJoin),
|
TaggedValue::LineJoin(_) => concrete!(graphene_core::vector::style::LineJoin),
|
||||||
TaggedValue::FillType(_) => concrete!(graphene_core::vector::style::FillType),
|
TaggedValue::FillType(_) => concrete!(graphene_core::vector::style::FillType),
|
||||||
|
TaggedValue::FillChoice(_) => concrete!(graphene_core::vector::style::FillChoice),
|
||||||
|
TaggedValue::Gradient(_) => concrete!(graphene_core::vector::style::Gradient),
|
||||||
TaggedValue::GradientType(_) => concrete!(graphene_core::vector::style::GradientType),
|
TaggedValue::GradientType(_) => concrete!(graphene_core::vector::style::GradientType),
|
||||||
TaggedValue::GradientPositions(_) => concrete!(Vec<(f64, graphene_core::Color)>),
|
TaggedValue::GradientStops(_) => concrete!(graphene_core::vector::style::GradientStops),
|
||||||
TaggedValue::Quantization(_) => concrete!(graphene_core::quantization::QuantizationChannels),
|
TaggedValue::Quantization(_) => concrete!(graphene_core::quantization::QuantizationChannels),
|
||||||
TaggedValue::OptionalColor(_) => concrete!(Option<graphene_core::Color>),
|
TaggedValue::OptionalColor(_) => concrete!(Option<graphene_core::Color>),
|
||||||
TaggedValue::ManipulatorGroupIds(_) => concrete!(Vec<graphene_core::uuid::ManipulatorGroupId>),
|
TaggedValue::ManipulatorGroupIds(_) => concrete!(Vec<graphene_core::uuid::ManipulatorGroupId>),
|
||||||
@@ -354,8 +363,10 @@ impl<'a> TaggedValue {
|
|||||||
x if x == TypeId::of::<graphene_core::vector::style::LineCap>() => Ok(TaggedValue::LineCap(*downcast(input).unwrap())),
|
x if x == TypeId::of::<graphene_core::vector::style::LineCap>() => Ok(TaggedValue::LineCap(*downcast(input).unwrap())),
|
||||||
x if x == TypeId::of::<graphene_core::vector::style::LineJoin>() => Ok(TaggedValue::LineJoin(*downcast(input).unwrap())),
|
x if x == TypeId::of::<graphene_core::vector::style::LineJoin>() => Ok(TaggedValue::LineJoin(*downcast(input).unwrap())),
|
||||||
x if x == TypeId::of::<graphene_core::vector::style::FillType>() => Ok(TaggedValue::FillType(*downcast(input).unwrap())),
|
x if x == TypeId::of::<graphene_core::vector::style::FillType>() => Ok(TaggedValue::FillType(*downcast(input).unwrap())),
|
||||||
|
x if x == TypeId::of::<graphene_core::vector::style::FillChoice>() => Ok(TaggedValue::FillChoice(*downcast(input).unwrap())),
|
||||||
|
x if x == TypeId::of::<graphene_core::vector::style::Gradient>() => Ok(TaggedValue::Gradient(*downcast(input).unwrap())),
|
||||||
x if x == TypeId::of::<graphene_core::vector::style::GradientType>() => Ok(TaggedValue::GradientType(*downcast(input).unwrap())),
|
x if x == TypeId::of::<graphene_core::vector::style::GradientType>() => Ok(TaggedValue::GradientType(*downcast(input).unwrap())),
|
||||||
x if x == TypeId::of::<Vec<(f64, graphene_core::Color)>>() => Ok(TaggedValue::GradientPositions(*downcast(input).unwrap())),
|
x if x == TypeId::of::<graphene_core::vector::style::GradientStops>() => Ok(TaggedValue::GradientStops(*downcast(input).unwrap())),
|
||||||
x if x == TypeId::of::<graphene_core::quantization::QuantizationChannels>() => Ok(TaggedValue::Quantization(*downcast(input).unwrap())),
|
x if x == TypeId::of::<graphene_core::quantization::QuantizationChannels>() => Ok(TaggedValue::Quantization(*downcast(input).unwrap())),
|
||||||
x if x == TypeId::of::<Option<graphene_core::Color>>() => Ok(TaggedValue::OptionalColor(*downcast(input).unwrap())),
|
x if x == TypeId::of::<Option<graphene_core::Color>>() => Ok(TaggedValue::OptionalColor(*downcast(input).unwrap())),
|
||||||
x if x == TypeId::of::<Vec<graphene_core::uuid::ManipulatorGroupId>>() => Ok(TaggedValue::ManipulatorGroupIds(*downcast(input).unwrap())),
|
x if x == TypeId::of::<Vec<graphene_core::uuid::ManipulatorGroupId>>() => Ok(TaggedValue::ManipulatorGroupIds(*downcast(input).unwrap())),
|
||||||
@@ -434,7 +445,7 @@ impl<'a> TaggedValue {
|
|||||||
x if x == TypeId::of::<graphene_core::vector::style::LineJoin>() => TaggedValue::LineJoin(graphene_core::vector::style::LineJoin::Miter),
|
x if x == TypeId::of::<graphene_core::vector::style::LineJoin>() => TaggedValue::LineJoin(graphene_core::vector::style::LineJoin::Miter),
|
||||||
x if x == TypeId::of::<graphene_core::vector::style::FillType>() => TaggedValue::FillType(graphene_core::vector::style::FillType::Solid),
|
x if x == TypeId::of::<graphene_core::vector::style::FillType>() => TaggedValue::FillType(graphene_core::vector::style::FillType::Solid),
|
||||||
x if x == TypeId::of::<graphene_core::vector::style::GradientType>() => TaggedValue::GradientType(Default::default()),
|
x if x == TypeId::of::<graphene_core::vector::style::GradientType>() => TaggedValue::GradientType(Default::default()),
|
||||||
x if x == TypeId::of::<Vec<(f64, graphene_core::Color)>>() => TaggedValue::GradientPositions(Default::default()),
|
x if x == TypeId::of::<graphene_core::vector::style::GradientStops>() => TaggedValue::GradientStops(Default::default()),
|
||||||
x if x == TypeId::of::<graphene_core::quantization::QuantizationChannels>() => TaggedValue::Quantization(Default::default()),
|
x if x == TypeId::of::<graphene_core::quantization::QuantizationChannels>() => TaggedValue::Quantization(Default::default()),
|
||||||
x if x == TypeId::of::<Option<graphene_core::Color>>() => TaggedValue::OptionalColor(Default::default()),
|
x if x == TypeId::of::<Option<graphene_core::Color>>() => TaggedValue::OptionalColor(Default::default()),
|
||||||
x if x == TypeId::of::<Vec<graphene_core::uuid::ManipulatorGroupId>>() => TaggedValue::ManipulatorGroupIds(Default::default()),
|
x if x == TypeId::of::<Vec<graphene_core::uuid::ManipulatorGroupId>>() => TaggedValue::ManipulatorGroupIds(Default::default()),
|
||||||
|
|||||||
@@ -706,7 +706,10 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
|||||||
register_node!(graphene_core::transform::SetTransformNode<_>, input: ImageFrame<Color>, params: [ImageFrame<Color>]),
|
register_node!(graphene_core::transform::SetTransformNode<_>, input: ImageFrame<Color>, params: [ImageFrame<Color>]),
|
||||||
register_node!(graphene_core::transform::SetTransformNode<_>, input: VectorData, params: [DAffine2]),
|
register_node!(graphene_core::transform::SetTransformNode<_>, input: VectorData, params: [DAffine2]),
|
||||||
register_node!(graphene_core::transform::SetTransformNode<_>, input: ImageFrame<Color>, params: [DAffine2]),
|
register_node!(graphene_core::transform::SetTransformNode<_>, input: ImageFrame<Color>, params: [DAffine2]),
|
||||||
register_node!(graphene_core::vector::SetFillNode<_, _, _, _, _, _, _>, input: VectorData, params: [graphene_core::vector::style::FillType, Option<graphene_core::Color>, graphene_core::vector::style::GradientType, DVec2, DVec2, DAffine2, Vec<(f64, graphene_core::Color)>]),
|
register_node!(graphene_core::vector::SetFillNode<_>, input: VectorData, params: [graphene_std::vector::style::Fill]),
|
||||||
|
register_node!(graphene_core::vector::SetFillNode<_>, input: VectorData, params: [Color]),
|
||||||
|
register_node!(graphene_core::vector::SetFillNode<_>, input: VectorData, params: [Option<Color>]),
|
||||||
|
register_node!(graphene_core::vector::SetFillNode<_>, input: VectorData, params: [graphene_std::vector::style::Gradient]),
|
||||||
register_node!(graphene_core::vector::SetStrokeNode<_, _, _, _, _, _, _>, input: VectorData, params: [Option<graphene_core::Color>, f64, Vec<f64>, f64, graphene_core::vector::style::LineCap, graphene_core::vector::style::LineJoin, f64]),
|
register_node!(graphene_core::vector::SetStrokeNode<_, _, _, _, _, _, _>, input: VectorData, params: [Option<graphene_core::Color>, f64, Vec<f64>, f64, graphene_core::vector::style::LineCap, graphene_core::vector::style::LineJoin, f64]),
|
||||||
register_node!(graphene_core::vector::RepeatNode<_, _, _>, input: VectorData, params: [DVec2, f64, u32]),
|
register_node!(graphene_core::vector::RepeatNode<_, _, _>, input: VectorData, params: [DVec2, f64, u32]),
|
||||||
register_node!(graphene_core::vector::BoundingBoxNode, input: VectorData, params: []),
|
register_node!(graphene_core::vector::BoundingBoxNode, input: VectorData, params: []),
|
||||||
|
|||||||
@@ -6,18 +6,106 @@ use syn::{
|
|||||||
PredicateType, ReturnType, Token, TraitBound, Type, TypeImplTrait, TypeParam, TypeParamBound, TypeTuple, WhereClause, WherePredicate,
|
PredicateType, ReturnType, Token, TraitBound, Type, TypeImplTrait, TypeParam, TypeParamBound, TypeTuple, WhereClause, WherePredicate,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// A macro used to construct a proto node implementation from the given struct and the decorated function.
|
||||||
|
///
|
||||||
|
/// This works by generating two `impl` blocks for the given struct:
|
||||||
|
///
|
||||||
|
/// - `impl TheGivenStruct`:
|
||||||
|
/// Attaches a `new` constructor method to the struct.
|
||||||
|
/// - `impl Node for TheGivenStruct`:
|
||||||
|
/// Implements the [`Node`] trait for the struct, with the `eval` method inside which is a modified version of the decorated function. See below for how the function is modified.
|
||||||
|
///
|
||||||
|
/// # Usage of this and similar macros
|
||||||
|
///
|
||||||
|
/// You'll use this macro most commonly when writing proto nodes. It's a convenient combination of the [`node_new`] and [`node_impl`] proc macros, which handles both of the bullet points above, respectively. There can only be one constructor method, but additional functions decorated by the [`node_impl`] macro can be added to implement different functionality across multiple type signatures.
|
||||||
|
///
|
||||||
|
/// # Useful hint
|
||||||
|
///
|
||||||
|
/// It can be helpful to run the "rust-analyzer: Expand macro recursively at carat" command from the VS Code command palette (or your editor's equivalent) to see the generated code of the macro to understand how the translation magic works.
|
||||||
|
///
|
||||||
|
/// # How generics and type signatures are handled
|
||||||
|
///
|
||||||
|
/// The given struct has various fields, each of them generic. These correspond with the node's parameters (the secondary inputs, but not the primary input). We can implement multiple functions with different type signatures, each each of these are converted by the [`node_impl`] macro into separate `impl` blocks for different `Node` traits.
|
||||||
|
///
|
||||||
|
/// ## Type signature translation
|
||||||
|
///
|
||||||
|
/// The conversion into an `impl Node` corresponding with the decorated function's type signature involves:
|
||||||
|
///
|
||||||
|
/// - Mapping the type of the function's first argument (the node's primary input) to the impl'd `Node`'s generic type, e.g.:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// Node<'input, Color>
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// for a `Color` primary input type.
|
||||||
|
/// - Mapping the type of the function's remaining arguments (the node's secondary inputs) to the given struct fields' generic types, e.g.:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// TheGivenStruct<S0, S1>
|
||||||
|
/// where S0: Node<'input, (), Output = f64>,
|
||||||
|
/// where S1: Node<'input, (), Output = f64>,
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// for two `f64` parameter (secondary input) types. Since Graphene works by having each function evaluate its upstream node as a lambda that returns output data, these secondary inputs are not directly `f64` values but rather `Node`s that output `f64` values when evaluated (in this case, with an empty input of `()`).
|
||||||
|
/// - Mapping the function's return type to the impl'd `Node` trait's associated type, e.g.:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// Output = Color
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// for a `Color` return (secondary output) type.
|
||||||
|
///
|
||||||
|
/// ## `eval()` method generation
|
||||||
|
///
|
||||||
|
/// The conversion of the decorated function's body into the `eval` method within the `impl Node` block involves the following steps:
|
||||||
|
///
|
||||||
|
/// - The function's body gets copied over to the interior of the `eval` method.
|
||||||
|
/// - The function's argument list only has its first argument (the node's primary input) copied over to the `eval` function signature. The remaining arguments (the node's secondary inputs) are not copied over as `eval` function arguments.
|
||||||
|
/// - A series of `let` declarations are added before the copied-over function body, one for each secondary input. They look like `let secondaryA: SomeOutputType = self.secondaryA.eval(someInput);`. Each one is calling the `eval()` method on its corresponding struct field, obtaining the evaluated value of that secondary input node that gets used in the function body in the lines below these `let` declarations.
|
||||||
|
///
|
||||||
|
/// This process is necessary because the arguments in the original decorated function don't really exist with the actual values. Instead, they live as fields in the struct and they are `Node`s that output the actual values only once evaluated. So with the magic performed by this macro, the function body can written pretending to be working with the actual secondary input values, but the real types are `impl Node<SomeInputType, Output = SomeOutputType>` and they live in `self` as struct fields.
|
||||||
|
///
|
||||||
|
/// The function body runs with the actual primary input value from the `eval` method's argument and the secondary input values from the `eval` method's `let` declarations. The result looks like this:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// fn eval(&'input self, color: Color) -> Self::Output {
|
||||||
|
/// let secondaryA = self.secondaryA.eval(());
|
||||||
|
/// let secondaryB = self.secondaryB.eval(());
|
||||||
|
/// {
|
||||||
|
/// Color::from_rgbaf32_unchecked(
|
||||||
|
/// color.r() / secondaryA,
|
||||||
|
/// color.g() / secondaryA,
|
||||||
|
/// color.b() / secondaryA,
|
||||||
|
/// color.a() * secondaryB,
|
||||||
|
/// )
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// There is one exception where a `let` declaration is not added if an opt-out is desired. Any argument given to the decorated function may be of type `impl Node<SomeInputType, Output = SomeOutputType>` which will tell the macro not to add a `let` declaration for that argument. This allows for manually calling `eval` on the struct field in the function body, like `self.secondaryA.eval(())`.
|
||||||
|
///
|
||||||
|
/// When a `let` declaration is generated automatically, this is called **automatic composition**. When opting out, this is called **manual composition**.
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
pub fn node_fn(attr: TokenStream, item: TokenStream) -> TokenStream {
|
pub fn node_fn(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
let mut imp = node_impl_proxy(attr.clone(), item.clone());
|
// Performs the `node_impl` macro's functionality of attaching an `impl Node for TheGivenStruct` block to the node struct
|
||||||
let new = node_new_impl(attr, item);
|
let node_impl = node_impl_proxy(attr.clone(), item.clone());
|
||||||
imp.extend(new);
|
|
||||||
imp
|
// Performs the `node_new` macro's functionality of attaching a `new` constructor method to the node struct
|
||||||
|
let mut new_constructor = node_new_impl(attr, item);
|
||||||
|
|
||||||
|
// Combines the two pieces of Rust source code
|
||||||
|
new_constructor.extend(node_impl);
|
||||||
|
|
||||||
|
new_constructor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Attaches an `impl TheGivenStruct` block to the node struct, containing a `new` constructor method. This is almost always called by the combined [`node_fn`] macro instead of using this one, however it can be used separately if needed. See that macro's documentation for more information.
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
pub fn node_new(attr: TokenStream, item: TokenStream) -> TokenStream {
|
pub fn node_new(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
node_new_impl(attr, item)
|
node_new_impl(attr, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Attaches an `impl Node for TheGivenStruct` block to the node struct, containing an implementation of the node's `eval` method for a certain type signature. This can be called with multiple separate functions each having different type signatures. The [`node_fn`] macro calls this macro as well as defining a `new` constructor method on the node struct, which is a necessary part of defining a proto node; therefore you will most likely call that macro on the first decorated function and this macro on any additional decorated functions to provide additional type signatures for the proto node. See that macro's documentation for more information.
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
pub fn node_impl(attr: TokenStream, item: TokenStream) -> TokenStream {
|
pub fn node_impl(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
node_impl_proxy(attr, item)
|
node_impl_proxy(attr, item)
|
||||||
@@ -59,7 +147,7 @@ fn node_new_impl(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||||||
impl <#(#args),*> #node_name<#(#args),*>
|
impl <#(#args),*> #node_name<#(#args),*>
|
||||||
{
|
{
|
||||||
pub const fn new(#(#parameter_idents: #struct_generics_iter),*) -> Self{
|
pub const fn new(#(#parameter_idents: #struct_generics_iter),*) -> Self{
|
||||||
Self{
|
Self {
|
||||||
#(#parameter_idents,)*
|
#(#parameter_idents,)*
|
||||||
#(#arg_idents: core::marker::PhantomData,)*
|
#(#arg_idents: core::marker::PhantomData,)*
|
||||||
}
|
}
|
||||||
@@ -92,6 +180,7 @@ fn node_impl_proxy(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||||||
node_impl_impl(attr, item, Asyncness::Sync)
|
node_impl_impl(attr, item, Asyncness::Sync)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Asyncness {
|
enum Asyncness {
|
||||||
Sync,
|
Sync,
|
||||||
AllAsync,
|
AllAsync,
|
||||||
@@ -203,7 +292,7 @@ fn node_impl_impl(attr: TokenStream, item: TokenStream, asyncness: Asyncness) ->
|
|||||||
};
|
};
|
||||||
let mut body_with_inputs = quote::quote!(
|
let mut body_with_inputs = quote::quote!(
|
||||||
#parameters
|
#parameters
|
||||||
{#body}
|
#body
|
||||||
);
|
);
|
||||||
if async_out {
|
if async_out {
|
||||||
body_with_inputs = quote::quote!(Box::pin(async move { #body_with_inputs }));
|
body_with_inputs = quote::quote!(Box::pin(async move { #body_with_inputs }));
|
||||||
|
|||||||
Reference in New Issue
Block a user