Add support for gradients with midpoints and add draggable diamonds to the color picker dialog (#3813)
* Refactor GradientStops to use struct-of-arrays and include midpoint * Implement interaction and rendering * Make color picker saturation-value color picking snap to original position and show both axis lines Make color picker saturation-value color picking snap to original position and show both axis lines * Add graphite:midpoint attribute to SVG exports * Add graphite:midpoint parsing to SVG importer
This commit is contained in:
parent
a1c1039ea1
commit
691d965bcf
|
|
@ -1,2 +1,2 @@
|
|||
https://github.com/Keavon/graphite-branded-assets/archive/f44aa2f362ae4fed8d634878b817a1d3948a7dcb.tar.gz
|
||||
dffe2b483e491979ef57c320d61446ada5400ef73ff26582976631d9c36efefc
|
||||
https://github.com/Keavon/graphite-branded-assets/archive/8ae15dc9c51a3855475d8cab1d0f29d9d9bc622c.tar.gz
|
||||
c19abe4ac848f3c835e43dc065c59e20e60233ae023ea0a064c5fed442be2d3d
|
||||
|
|
|
|||
|
|
@ -55,7 +55,8 @@
|
|||
"a11y-click-events-have-key-events": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
|
||||
"a11y_consider_explicit_label": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
|
||||
"a11y_click_events_have_key_events": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
|
||||
"a11y_no_noninteractive_element_interactions": "ignore" // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
|
||||
"a11y_no_noninteractive_element_interactions": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
|
||||
"a11y_no_static_element_interactions": "ignore" // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
|
||||
},
|
||||
// Git Graph config
|
||||
"git-graph.repository.fetchAndPrune": true,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use crate::messages::input_mapper::utility_types::input_keyboard::KeysGroup;
|
|||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::prelude::*;
|
||||
use graphene_std::raster::color::Color;
|
||||
use graphene_std::vector::style::{FillChoice, GradientStops};
|
||||
use graphene_std::vector::style::{FillChoice, GradientStop, GradientStops};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
|
@ -193,18 +193,17 @@ impl LayoutMessageHandler {
|
|||
}
|
||||
|
||||
// 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()).and_then(decode_color);
|
||||
if let (Some(position), Some(color)) = (position, color) { Some((position, color)) } else { None }
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let positions = update_value.get("position").and_then(|x| x.as_array());
|
||||
let midpoints = update_value.get("midpoint").and_then(|x| x.as_array());
|
||||
let colors = update_value.get("color").and_then(|x| x.as_array());
|
||||
|
||||
if let (Some(positions), Some(midpoints), Some(colors)) = (positions, midpoints, colors) {
|
||||
let gradient_stops = positions.iter().zip(midpoints.iter()).zip(colors.iter()).filter_map(|((pos, mid), col)| {
|
||||
let position = pos.as_f64()?;
|
||||
let midpoint = mid.as_f64()?;
|
||||
let color = col.as_object().and_then(decode_color)?;
|
||||
Some(GradientStop { position, midpoint, color })
|
||||
});
|
||||
|
||||
color_button.value = FillChoice::Gradient(GradientStops::new(gradient_stops));
|
||||
return (color_button.on_update.callback)(color_button);
|
||||
|
|
|
|||
|
|
@ -564,7 +564,7 @@ impl TableRowLayout for GradientStops {
|
|||
"Gradient"
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
format!("Gradient ({} stops)", self.0.len())
|
||||
format!("Gradient ({} stops)", self.len())
|
||||
}
|
||||
fn element_widget(&self, _index: usize) -> WidgetInstance {
|
||||
ColorInput::new(FillChoice::Gradient(self.clone()))
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ use graphene_std::renderer::Quad;
|
|||
use graphene_std::renderer::convert_usvg_path::convert_usvg_path;
|
||||
use graphene_std::table::Table;
|
||||
use graphene_std::text::{Font, TypesettingConfig};
|
||||
use graphene_std::vector::style::{Fill, Gradient, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
||||
use graphene_std::vector::style::{Fill, Gradient, GradientStop, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
||||
|
||||
#[derive(ExtractField)]
|
||||
pub struct GraphOperationMessageContext<'a> {
|
||||
|
|
@ -337,7 +337,17 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
|||
let offset_to_center = DVec2::new(size.width() as f64, size.height() as f64) / -2.;
|
||||
let transform = transform * DAffine2::from_translation(offset_to_center);
|
||||
|
||||
import_usvg_node(&mut modify_inputs, &usvg::Node::Group(Box::new(tree.root().clone())), transform, id, parent, insert_index);
|
||||
let graphite_gradient_stops = extract_graphite_gradient_stops(&svg);
|
||||
|
||||
import_usvg_node(
|
||||
&mut modify_inputs,
|
||||
&usvg::Node::Group(Box::new(tree.root().clone())),
|
||||
transform,
|
||||
id,
|
||||
parent,
|
||||
insert_index,
|
||||
&graphite_gradient_stops,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -362,7 +372,85 @@ fn usvg_transform(c: usvg::Transform) -> DAffine2 {
|
|||
DAffine2::from_cols_array(&[c.sx as f64, c.ky as f64, c.kx as f64, c.sy as f64, c.tx as f64, c.ty as f64])
|
||||
}
|
||||
|
||||
fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, transform: DAffine2, id: NodeId, parent: LayerNodeIdentifier, insert_index: usize) {
|
||||
const GRAPHITE_NAMESPACE: &str = "https://graphite.art";
|
||||
|
||||
/// Pre-parses the raw SVG XML to extract gradient stops that have `graphite:midpoint` attributes.
|
||||
/// Graphite exports gradients with midpoint curve data by writing interpolated approximation stops
|
||||
/// alongside the real stops. Real stops are tagged with `graphite:midpoint` attributes.
|
||||
/// Returns a map from gradient element `id` to `GradientStops` containing only the real stops.
|
||||
fn extract_graphite_gradient_stops(svg: &str) -> HashMap<String, GradientStops> {
|
||||
let mut result = HashMap::new();
|
||||
|
||||
// Quick check: if the SVG doesn't reference `graphite:midpoint` at all, skip parsing
|
||||
if !svg.contains("graphite:midpoint") {
|
||||
return result;
|
||||
}
|
||||
|
||||
let doc = match usvg::roxmltree::Document::parse(svg) {
|
||||
Ok(doc) => doc,
|
||||
Err(_) => return result,
|
||||
};
|
||||
|
||||
for node in doc.descendants() {
|
||||
match node.tag_name().name() {
|
||||
"linearGradient" | "radialGradient" => {}
|
||||
_ => continue,
|
||||
}
|
||||
|
||||
let gradient_id = match node.attribute("id") {
|
||||
Some(id) => id.to_string(),
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let mut real_stops = Vec::new();
|
||||
let mut has_any_midpoint = false;
|
||||
|
||||
for child in node.children() {
|
||||
if child.tag_name().name() != "stop" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let midpoint = child.attribute((GRAPHITE_NAMESPACE, "midpoint")).and_then(|v| v.parse::<f64>().ok());
|
||||
|
||||
if let Some(midpoint) = midpoint {
|
||||
has_any_midpoint = true;
|
||||
|
||||
let offset = child.attribute("offset").and_then(|v| v.parse::<f64>().ok()).unwrap_or(0.);
|
||||
let opacity = child.attribute("stop-opacity").and_then(|v| v.parse::<f32>().ok()).unwrap_or(1.);
|
||||
let color = child.attribute("stop-color").and_then(|hex| parse_hex_stop_color(hex, opacity)).unwrap_or(Color::BLACK);
|
||||
|
||||
real_stops.push(GradientStop { position: offset, midpoint, color });
|
||||
}
|
||||
}
|
||||
|
||||
if has_any_midpoint && !real_stops.is_empty() {
|
||||
result.insert(gradient_id, GradientStops::new(real_stops));
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn parse_hex_stop_color(hex: &str, opacity: f32) -> Option<Color> {
|
||||
let hex = hex.strip_prefix('#')?;
|
||||
if hex.len() != 6 {
|
||||
return None;
|
||||
}
|
||||
let r = u8::from_str_radix(&hex[0..2], 16).ok()? as f32 / 255.;
|
||||
let g = u8::from_str_radix(&hex[2..4], 16).ok()? as f32 / 255.;
|
||||
let b = u8::from_str_radix(&hex[4..6], 16).ok()? as f32 / 255.;
|
||||
Some(Color::from_rgbaf32_unchecked(r, g, b, opacity))
|
||||
}
|
||||
|
||||
fn import_usvg_node(
|
||||
modify_inputs: &mut ModifyInputsContext,
|
||||
node: &usvg::Node,
|
||||
transform: DAffine2,
|
||||
id: NodeId,
|
||||
parent: LayerNodeIdentifier,
|
||||
insert_index: usize,
|
||||
graphite_gradient_stops: &HashMap<String, GradientStops>,
|
||||
) {
|
||||
let layer = modify_inputs.create_layer(id);
|
||||
modify_inputs.network_interface.move_layer_to_stack(layer, parent, insert_index, &[]);
|
||||
modify_inputs.layer_node = Some(layer);
|
||||
|
|
@ -372,7 +460,7 @@ fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node,
|
|||
match node {
|
||||
usvg::Node::Group(group) => {
|
||||
for child in group.children() {
|
||||
import_usvg_node(modify_inputs, child, transform, NodeId::new(), layer, 0);
|
||||
import_usvg_node(modify_inputs, child, transform, NodeId::new(), layer, 0, graphite_gradient_stops);
|
||||
}
|
||||
modify_inputs.layer_node = Some(layer);
|
||||
}
|
||||
|
|
@ -388,7 +476,7 @@ fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node,
|
|||
|
||||
if let Some(fill) = path.fill() {
|
||||
let bounds_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
|
||||
apply_usvg_fill(fill, modify_inputs, bounds_transform);
|
||||
apply_usvg_fill(fill, modify_inputs, bounds_transform, graphite_gradient_stops);
|
||||
}
|
||||
if let Some(stroke) = path.stroke() {
|
||||
apply_usvg_stroke(stroke, modify_inputs, transform * usvg_transform(node.abs_transform()));
|
||||
|
|
@ -432,7 +520,7 @@ fn apply_usvg_stroke(stroke: &usvg::Stroke, modify_inputs: &mut ModifyInputsCont
|
|||
}
|
||||
}
|
||||
|
||||
fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, bounds_transform: DAffine2) {
|
||||
fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, bounds_transform: DAffine2, graphite_gradient_stops: &HashMap<String, GradientStops>) {
|
||||
modify_inputs.fill_set(match &fill.paint() {
|
||||
usvg::Paint::Color(color) => Fill::solid(usvg_color(*color, fill.opacity().get())),
|
||||
usvg::Paint::LinearGradient(linear) => {
|
||||
|
|
@ -443,8 +531,17 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, b
|
|||
|
||||
let gradient_type = GradientType::Linear;
|
||||
|
||||
let stops = linear.stops().iter().map(|stop| (stop.offset().get() as f64, usvg_color(stop.color(), stop.opacity().get()))).collect();
|
||||
let stops = GradientStops::new(stops);
|
||||
let stops = match graphite_gradient_stops.get(linear.id()) {
|
||||
Some(graphite_stops) => graphite_stops.clone(),
|
||||
None => {
|
||||
let stops = linear.stops().iter().map(|stop| GradientStop {
|
||||
position: stop.offset().get() as f64,
|
||||
midpoint: 0.5,
|
||||
color: usvg_color(stop.color(), stop.opacity().get()),
|
||||
});
|
||||
GradientStops::new(stops)
|
||||
}
|
||||
};
|
||||
|
||||
Fill::Gradient(Gradient { start, end, gradient_type, stops })
|
||||
}
|
||||
|
|
@ -457,8 +554,17 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, b
|
|||
|
||||
let gradient_type = GradientType::Radial;
|
||||
|
||||
let stops = radial.stops().iter().map(|stop| (stop.offset().get() as f64, usvg_color(stop.color(), stop.opacity().get()))).collect();
|
||||
let stops = GradientStops::new(stops);
|
||||
let stops = match graphite_gradient_stops.get(radial.id()) {
|
||||
Some(graphite_stops) => graphite_stops.clone(),
|
||||
None => {
|
||||
let stops = radial.stops().iter().map(|stop| GradientStop {
|
||||
position: stop.offset().get() as f64,
|
||||
midpoint: 0.5,
|
||||
color: usvg_color(stop.color(), stop.opacity().get()),
|
||||
});
|
||||
GradientStops::new(stops)
|
||||
}
|
||||
};
|
||||
|
||||
Fill::Gradient(Gradient { start, end, gradient_type, stops })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
|
|||
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::{NodeGraphLayer, get_gradient};
|
||||
use crate::messages::tool::common_functionality::snapping::SnapManager;
|
||||
use graphene_std::vector::style::{Fill, Gradient, GradientType};
|
||||
use graphene_std::vector::style::{Fill, Gradient, GradientStops, GradientType};
|
||||
|
||||
#[derive(Default, ExtractField)]
|
||||
pub struct GradientTool {
|
||||
|
|
@ -182,13 +182,13 @@ struct SelectedGradient {
|
|||
initial_gradient: Gradient,
|
||||
}
|
||||
|
||||
fn calculate_insertion(start: DVec2, end: DVec2, stops: &[(f64, graphene_std::Color)], mouse: DVec2) -> Option<f64> {
|
||||
fn calculate_insertion(start: DVec2, end: DVec2, stops: &GradientStops, mouse: DVec2) -> Option<f64> {
|
||||
let distance = (end - start).angle_to(mouse - start).sin() * (mouse - start).length();
|
||||
let projection = ((end - start).angle_to(mouse - start)).cos() * start.distance(mouse) / start.distance(end);
|
||||
|
||||
if distance.abs() < SEGMENT_INSERTION_DISTANCE && (0. ..=1.).contains(&projection) {
|
||||
for (position, _) in stops {
|
||||
let stop_pos = start.lerp(end, *position);
|
||||
for stop in stops {
|
||||
let stop_pos = start.lerp(end, stop.position);
|
||||
if stop_pos.distance_squared(mouse) < (MANIPULATOR_GROUP_MARKER_SIZE * 2.).powi(2) {
|
||||
return None;
|
||||
}
|
||||
|
|
@ -262,11 +262,12 @@ impl SelectedGradient {
|
|||
|
||||
// Should not go off end but can swap
|
||||
let clamped = new_pos.clamp(0., 1.);
|
||||
self.gradient.stops.get_mut(s).unwrap().0 = clamped;
|
||||
let new_pos = self.gradient.stops[s];
|
||||
self.gradient.stops.position[s] = clamped;
|
||||
let new_position = self.gradient.stops.position[s];
|
||||
let new_color = self.gradient.stops.color[s];
|
||||
|
||||
self.gradient.stops.sort();
|
||||
self.dragging = GradientDragTarget::Step(self.gradient.stops.iter().position(|x| *x == new_pos).unwrap());
|
||||
self.dragging = GradientDragTarget::Step(self.gradient.stops.iter().position(|s| s.position == new_position && s.color == new_color).unwrap());
|
||||
}
|
||||
}
|
||||
self.render_gradient(responses);
|
||||
|
|
@ -357,18 +358,18 @@ impl Fsm for GradientToolFsmState {
|
|||
format!("#{}", color.with_alpha(1.).to_rgba_hex_srgb())
|
||||
}
|
||||
|
||||
let start_hex = stops.first().map(|(_, c)| color_to_hex(*c)).unwrap_or(String::from(COLOR_OVERLAY_BLUE));
|
||||
let end_hex = stops.last().map(|(_, c)| color_to_hex(*c)).unwrap_or(String::from(COLOR_OVERLAY_BLUE));
|
||||
let start_hex = stops.color.first().map(|&c| color_to_hex(c)).unwrap_or(String::from(COLOR_OVERLAY_BLUE));
|
||||
let end_hex = stops.color.last().map(|&c| color_to_hex(c)).unwrap_or(String::from(COLOR_OVERLAY_BLUE));
|
||||
|
||||
overlay_context.line(start, end, None, None);
|
||||
overlay_context.gradient_color_stop(start, dragging == Some(GradientDragTarget::Start), &start_hex);
|
||||
overlay_context.gradient_color_stop(end, dragging == Some(GradientDragTarget::End), &end_hex);
|
||||
|
||||
for (index, (position, color)) in stops.clone().into_iter().enumerate() {
|
||||
if position.abs() < f64::EPSILON * 1000. || (1. - position).abs() < f64::EPSILON * 1000. {
|
||||
for (index, stop) in stops.iter().enumerate() {
|
||||
if stop.position.abs() < f64::EPSILON * 1000. || (1. - stop.position).abs() < f64::EPSILON * 1000. {
|
||||
continue;
|
||||
}
|
||||
overlay_context.gradient_color_stop(start.lerp(end, position), dragging == Some(GradientDragTarget::Step(index)), &color_to_hex(color));
|
||||
overlay_context.gradient_color_stop(start.lerp(end, stop.position), dragging == Some(GradientDragTarget::Step(index)), &color_to_hex(stop.color));
|
||||
}
|
||||
|
||||
if let (Some(projection), Some(dir)) = (calculate_insertion(start, end, stops, mouse), (end - start).try_normalize()) {
|
||||
|
|
@ -415,7 +416,7 @@ impl Fsm for GradientToolFsmState {
|
|||
if let Some(layer) = selected_gradient.layer {
|
||||
responses.add(GraphOperationMessage::FillSet {
|
||||
layer,
|
||||
fill: Fill::Solid(selected_gradient.gradient.stops[0].1),
|
||||
fill: Fill::Solid(selected_gradient.gradient.stops.color[0]),
|
||||
});
|
||||
}
|
||||
responses.add(DocumentMessage::CommitTransaction);
|
||||
|
|
@ -424,8 +425,8 @@ impl Fsm for GradientToolFsmState {
|
|||
}
|
||||
|
||||
// Find the minimum and maximum positions
|
||||
let min_position = selected_gradient.gradient.stops.iter().map(|(pos, _)| *pos).reduce(f64::min).expect("No min");
|
||||
let max_position = selected_gradient.gradient.stops.iter().map(|(pos, _)| *pos).reduce(f64::max).expect("No max");
|
||||
let min_position = selected_gradient.gradient.stops.position.iter().copied().reduce(f64::min).expect("No min");
|
||||
let max_position = selected_gradient.gradient.stops.position.iter().copied().reduce(f64::max).expect("No max");
|
||||
|
||||
// Recompute the start and end position of the gradient (in viewport transform)
|
||||
let transform = selected_gradient.transform;
|
||||
|
|
@ -435,7 +436,7 @@ impl Fsm for GradientToolFsmState {
|
|||
selected_gradient.gradient.end = transform.inverse().transform_point2(new_end);
|
||||
|
||||
// Remap the positions
|
||||
for (position, _) in selected_gradient.gradient.stops.iter_mut() {
|
||||
for position in selected_gradient.gradient.stops.position.iter_mut() {
|
||||
*position = (*position - min_position) / (max_position - min_position);
|
||||
}
|
||||
|
||||
|
|
@ -492,8 +493,8 @@ impl Fsm for GradientToolFsmState {
|
|||
let Some(gradient) = get_gradient(layer, &document.network_interface) else { continue };
|
||||
let transform = gradient_space_transform(layer, document);
|
||||
// Check for dragging step
|
||||
for (index, (pos, _)) in gradient.stops.iter().enumerate() {
|
||||
let pos = transform.transform_point2(gradient.start.lerp(gradient.end, *pos));
|
||||
for (index, stop) in gradient.stops.iter().enumerate() {
|
||||
let pos = transform.transform_point2(gradient.start.lerp(gradient.end, stop.position));
|
||||
if pos.distance_squared(mouse) < tolerance {
|
||||
dragging = true;
|
||||
tool_data.selected_gradient = Some(SelectedGradient {
|
||||
|
|
@ -787,7 +788,7 @@ mod test_gradient {
|
|||
let (gradient, transform) = get_gradient(&mut editor).await;
|
||||
|
||||
// Gradient goes from secondary color to primary color
|
||||
let stops = gradient.stops.iter().map(|stop| (stop.0, stop.1.to_rgba8_srgb())).collect::<Vec<_>>();
|
||||
let stops = gradient.stops.iter().map(|stop| (stop.position, stop.color.to_rgba8_srgb())).collect::<Vec<_>>();
|
||||
assert_eq!(stops, vec![(0., Color::BLUE.to_rgba8_srgb()), (1., Color::GREEN.to_rgba8_srgb())]);
|
||||
assert!(transform.transform_point2(gradient.start).abs_diff_eq(DVec2::new(2., 3.), 1e-10));
|
||||
assert!(transform.transform_point2(gradient.end).abs_diff_eq(DVec2::new(24., 4.), 1e-10));
|
||||
|
|
@ -879,7 +880,7 @@ mod test_gradient {
|
|||
let (updated_gradient, _) = get_gradient(&mut editor).await;
|
||||
assert_eq!(updated_gradient.stops.len(), 3, "Expected 3 stops, found {}", updated_gradient.stops.len());
|
||||
|
||||
let positions: Vec<f64> = updated_gradient.stops.iter().map(|(pos, _)| *pos).collect();
|
||||
let positions: Vec<f64> = updated_gradient.stops.iter().map(|stop| stop.position).collect();
|
||||
assert!(
|
||||
positions.iter().any(|pos| (pos - 0.5).abs() < 0.1),
|
||||
"Expected to find a stop near position 0.5, but found: {positions:?}"
|
||||
|
|
@ -975,12 +976,12 @@ mod test_gradient {
|
|||
|
||||
// Verify initial stop positions and colors
|
||||
let mut stops = initial_gradient.stops.clone();
|
||||
stops.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
|
||||
stops.sort();
|
||||
|
||||
let positions: Vec<f64> = stops.iter().map(|(pos, _)| *pos).collect();
|
||||
let positions: Vec<f64> = stops.iter().map(|stop| stop.position).collect();
|
||||
assert_stops_at_positions(&positions, &[0., 0.5, 1.], 0.1);
|
||||
|
||||
let middle_color = stops[1].1.to_rgba8_srgb();
|
||||
let middle_color = stops.color[1].to_rgba8_srgb();
|
||||
|
||||
// Simulate dragging the middle stop to position 0.8
|
||||
let click_position = DVec2::new(50., 0.);
|
||||
|
|
@ -1014,16 +1015,16 @@ mod test_gradient {
|
|||
|
||||
// Verify updated stop positions and colors
|
||||
let mut updated_stops = updated_gradient.stops.clone();
|
||||
updated_stops.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
|
||||
updated_stops.sort();
|
||||
|
||||
// Check positions are now correctly ordered
|
||||
let updated_positions: Vec<f64> = updated_stops.iter().map(|(pos, _)| *pos).collect();
|
||||
let updated_positions: Vec<f64> = updated_stops.iter().map(|stop| stop.position).collect();
|
||||
assert_stops_at_positions(&updated_positions, &[0., 0.8, 1.], 0.1);
|
||||
|
||||
// Colors should maintain their associations with the stop points
|
||||
assert_eq!(updated_stops[0].1.to_rgba8_srgb(), Color::BLUE.to_rgba8_srgb());
|
||||
assert_eq!(updated_stops[1].1.to_rgba8_srgb(), middle_color);
|
||||
assert_eq!(updated_stops[2].1.to_rgba8_srgb(), Color::GREEN.to_rgba8_srgb());
|
||||
assert_eq!(updated_stops.color[0].to_rgba8_srgb(), Color::BLUE.to_rgba8_srgb());
|
||||
assert_eq!(updated_stops.color[1].to_rgba8_srgb(), middle_color);
|
||||
assert_eq!(updated_stops.color[2].to_rgba8_srgb(), Color::GREEN.to_rgba8_srgb());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -1054,7 +1055,7 @@ mod test_gradient {
|
|||
let (updated_gradient, _) = get_gradient(&mut editor).await;
|
||||
assert_eq!(updated_gradient.stops.len(), 4, "Expected 4 stops, found {}", updated_gradient.stops.len());
|
||||
|
||||
let positions: Vec<f64> = updated_gradient.stops.iter().map(|(pos, _)| *pos).collect();
|
||||
let positions: Vec<f64> = updated_gradient.stops.iter().map(|stop| stop.position).collect();
|
||||
|
||||
// Use helper function to verify positions
|
||||
assert_stops_at_positions(&positions, &[0., 0.25, 0.75, 1.], 0.05);
|
||||
|
|
@ -1080,7 +1081,7 @@ mod test_gradient {
|
|||
let (final_gradient, _) = get_gradient(&mut editor).await;
|
||||
assert_eq!(final_gradient.stops.len(), 3, "Expected 3 stops after deletion, found {}", final_gradient.stops.len());
|
||||
|
||||
let final_positions: Vec<f64> = final_gradient.stops.iter().map(|(pos, _)| *pos).collect();
|
||||
let final_positions: Vec<f64> = final_gradient.stops.iter().map(|stop| stop.position).collect();
|
||||
|
||||
// Verify final positions with helper function
|
||||
assert_stops_at_positions(&final_positions, &[0., 0.25, 1.], 0.05);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte";
|
||||
import NumberInput from "@graphite/components/widgets/inputs/NumberInput.svelte";
|
||||
import SpectrumInput from "@graphite/components/widgets/inputs/SpectrumInput.svelte";
|
||||
import SpectrumInput, { MAX_MIDPOINT, MIN_MIDPOINT } from "@graphite/components/widgets/inputs/SpectrumInput.svelte";
|
||||
import TextInput from "@graphite/components/widgets/inputs/TextInput.svelte";
|
||||
import Separator from "@graphite/components/widgets/labels/Separator.svelte";
|
||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
|
|
@ -57,7 +57,8 @@
|
|||
// Gradient color stops
|
||||
$: gradient = colorOrGradient instanceof Gradient ? colorOrGradient : undefined;
|
||||
let activeIndex = 0 as number | undefined;
|
||||
$: selectedGradientColor = (activeIndex !== undefined && gradient?.atIndex(activeIndex)?.color) || (Color.fromCSS("black") as Color);
|
||||
let activeIndexIsMidpoint = false;
|
||||
$: selectedGradientColor = (activeIndex !== undefined && gradient?.color[activeIndex]) || (Color.fromCSS("black") as Color);
|
||||
// Currently viewed color
|
||||
$: color = colorOrGradient instanceof Color ? colorOrGradient : selectedGradientColor;
|
||||
// New color components
|
||||
|
|
@ -191,8 +192,8 @@
|
|||
alignedAxis = undefined;
|
||||
} else if (!shiftPressed && draggingPickerTrack) {
|
||||
shiftPressed = true;
|
||||
saturationStartOfAxisAlign = saturation;
|
||||
valueStartOfAxisAlign = value;
|
||||
saturationStartOfAxisAlign = saturationBeforeDrag;
|
||||
valueStartOfAxisAlign = valueBeforeDrag;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -286,8 +287,9 @@
|
|||
function setColor(color?: Color) {
|
||||
const colorToEmit = color || new Color({ h: hue, s: saturation, v: value, a: alpha });
|
||||
|
||||
const stop = gradientSpectrumInputWidget && activeIndex !== undefined && gradient?.atIndex(activeIndex);
|
||||
if (stop) stop.color = colorToEmit;
|
||||
if (gradientSpectrumInputWidget && activeIndex !== undefined && gradient?.position[activeIndex] !== undefined && colorOrGradient instanceof Gradient) {
|
||||
colorOrGradient.color[activeIndex] = colorToEmit;
|
||||
}
|
||||
|
||||
dispatch("colorOrGradient", gradient || colorToEmit);
|
||||
}
|
||||
|
|
@ -397,9 +399,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
function gradientActiveMarkerIndexChange({ detail: index }: CustomEvent<number | undefined>) {
|
||||
activeIndex = index;
|
||||
const color = index === undefined ? undefined : gradient?.colorAtIndex(index);
|
||||
function gradientActiveMarkerIndexChange({ detail: { activeMarkerIndex, activeMarkerIsMidpoint } }: CustomEvent<{ activeMarkerIndex: number | undefined; activeMarkerIsMidpoint: boolean }>) {
|
||||
activeIndex = activeMarkerIndex;
|
||||
activeIndexIsMidpoint = activeMarkerIsMidpoint;
|
||||
|
||||
const color = activeMarkerIndex === undefined ? undefined : gradient?.color[activeMarkerIndex];
|
||||
const hsva = color?.toHSVA();
|
||||
if (!color || !hsva) return;
|
||||
|
||||
|
|
@ -440,17 +444,24 @@
|
|||
on:pointerdown={onPointerDown}
|
||||
data-saturation-value-picker
|
||||
>
|
||||
{#if !isNone}
|
||||
<div class="selection-circle" style:top={`${(1 - value) * 100}%`} style:left={`${saturation * 100}%`}></div>
|
||||
{/if}
|
||||
{#if alignedAxis}
|
||||
<div
|
||||
class="selection-circle-alignment"
|
||||
class:saturation={alignedAxis === "saturation"}
|
||||
class:value={alignedAxis === "value"}
|
||||
style:top={`${(1 - value) * 100}%`}
|
||||
style:left={`${saturation * 100}%`}
|
||||
class="selection-circle-axis-snap-line"
|
||||
style:width={alignedAxis === "value" ? "100%" : undefined}
|
||||
style:height={alignedAxis === "saturation" ? "100%" : undefined}
|
||||
style:top={alignedAxis === "value" ? `${(1 - value) * 100}%` : undefined}
|
||||
style:left={alignedAxis === "saturation" ? `${saturation * 100}%` : undefined}
|
||||
></div>
|
||||
<div
|
||||
class="selection-circle-axis-snap-line"
|
||||
style:width={alignedAxis === "saturation" ? "100%" : undefined}
|
||||
style:height={alignedAxis === "value" ? "100%" : undefined}
|
||||
style:top={alignedAxis === "saturation" ? `${(1 - valueBeforeDrag) * 100}%` : undefined}
|
||||
style:left={alignedAxis === "value" ? `${saturationBeforeDrag * 100}%` : undefined}
|
||||
></div>
|
||||
{/if}
|
||||
{#if !isNone}
|
||||
<div class="selection-circle" style:top={`${(1 - value) * 100}%`} style:left={`${saturation * 100}%`}></div>
|
||||
{/if}
|
||||
</LayoutCol>
|
||||
<LayoutCol
|
||||
|
|
@ -484,19 +495,22 @@
|
|||
on:gradient={() => dispatch("colorOrGradient", gradient)}
|
||||
on:activeMarkerIndexChange={gradientActiveMarkerIndexChange}
|
||||
activeMarkerIndex={activeIndex}
|
||||
activeMarkerIsMidpoint={activeIndexIsMidpoint}
|
||||
on:dragging={({ detail }) => (gradientSpectrumDragging = detail)}
|
||||
bind:this={gradientSpectrumInputWidget}
|
||||
/>
|
||||
{#if gradientSpectrumInputWidget && activeIndex !== undefined}
|
||||
<NumberInput
|
||||
value={(gradient.positionAtIndex(activeIndex) || 0) * 100}
|
||||
value={(activeIndexIsMidpoint ? gradient.midpoint[activeIndex] : gradient.position[activeIndex] || 0) * 100}
|
||||
{disabled}
|
||||
on:value={({ detail }) => {
|
||||
if (gradientSpectrumInputWidget && activeIndex !== undefined && detail !== undefined) gradientSpectrumInputWidget.setPosition(activeIndex, detail / 100);
|
||||
on:value={({ detail: position }) => {
|
||||
if (gradientSpectrumInputWidget && activeIndex !== undefined && position !== undefined) {
|
||||
gradientSpectrumInputWidget.setPosition(activeIndex, position / 100, activeIndexIsMidpoint);
|
||||
}
|
||||
}}
|
||||
displayDecimalPlaces={0}
|
||||
min={0}
|
||||
max={100}
|
||||
min={activeIndexIsMidpoint ? MIN_MIDPOINT * 100 : 0}
|
||||
max={activeIndexIsMidpoint ? MAX_MIDPOINT * 100 : 100}
|
||||
unit="%"
|
||||
/>
|
||||
{/if}
|
||||
|
|
@ -744,12 +758,12 @@
|
|||
}
|
||||
|
||||
.selection-circle {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
|
|
@ -761,56 +775,31 @@
|
|||
height: calc(var(--picker-circle-radius) * 2 + 1px);
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--opaque-color-contrasting);
|
||||
background: var(--opaque-color);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.selection-circle-alignment {
|
||||
position: absolute;
|
||||
.selection-circle-axis-snap-line {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: var(--opaque-color-contrasting);
|
||||
|
||||
&.saturation::before,
|
||||
&.saturation::after,
|
||||
&.value::before,
|
||||
&.value::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
background: var(--opaque-color-contrasting);
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
&.saturation {
|
||||
&::before {
|
||||
height: var(--picker-size);
|
||||
margin-top: calc(-1 * var(--picker-size) - var(--picker-circle-radius));
|
||||
}
|
||||
|
||||
&::after {
|
||||
height: var(--picker-size);
|
||||
margin-top: var(--picker-circle-radius);
|
||||
}
|
||||
}
|
||||
|
||||
&.value {
|
||||
&::before {
|
||||
width: var(--picker-size);
|
||||
margin-left: var(--picker-circle-radius);
|
||||
}
|
||||
|
||||
&::after {
|
||||
width: var(--picker-size);
|
||||
margin-left: calc(-1 * var(--picker-size) - var(--picker-circle-radius));
|
||||
}
|
||||
+ .selection-circle-axis-snap-line {
|
||||
opacity: 0.25;
|
||||
}
|
||||
}
|
||||
|
||||
.selection-needle {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
|
|
@ -881,13 +870,13 @@
|
|||
|
||||
&.outlined::after {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
box-shadow: inset 0 0 0 1px rgba(var(--color-0-black-rgb), var(--outline-amount));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.transparency {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
$: outlined = outlineFactor > 0.0001;
|
||||
$: chosenGradient = value instanceof Gradient ? value.toLinearGradientCSS() : `linear-gradient(${value.toHexOptionalAlpha()}, ${value.toHexOptionalAlpha()})`;
|
||||
$: none = value instanceof Color ? value.none : false;
|
||||
$: transparency = value instanceof Gradient ? value.stops.some((stop) => stop.color.alpha < 1) : value.alpha < 1;
|
||||
$: transparency = value instanceof Gradient ? value.color.some((color) => color.alpha < 1) : value.alpha < 1;
|
||||
</script>
|
||||
|
||||
<LayoutCol class="color-button" classes={{ open, disabled, narrow, none, transparency, outlined, "direction-top": menuDirection === "Top" }} {tooltipLabel} {tooltipDescription} {tooltipShortcut}>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
<script lang="ts" context="module">
|
||||
export const MIN_MIDPOINT = 0.01;
|
||||
export const MAX_MIDPOINT = 0.99;
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onDestroy } from "svelte";
|
||||
|
||||
import { evaluateGradientAtPosition } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import { Color, type Gradient } from "@graphite/messages";
|
||||
|
||||
import { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
|
|
@ -10,27 +16,44 @@
|
|||
const BUTTON_LEFT = 0;
|
||||
const BUTTON_RIGHT = 2;
|
||||
|
||||
const dispatch = createEventDispatcher<{ activeMarkerIndexChange: number | undefined; gradient: Gradient; dragging: boolean }>();
|
||||
const dispatch = createEventDispatcher<{ activeMarkerIndexChange: { activeMarkerIndex: number | undefined; activeMarkerIsMidpoint: boolean }; gradient: Gradient; dragging: boolean }>();
|
||||
|
||||
export let gradient: Gradient;
|
||||
export let disabled = false;
|
||||
export let activeMarkerIndex = 0 as number | undefined;
|
||||
export let activeMarkerIsMidpoint = false;
|
||||
// export let disabled = false;
|
||||
// export let tooltipLabel: string | undefined = undefined;
|
||||
// export let tooltipDescription: string | undefined = undefined;
|
||||
// export let tooltipShortcut: ActionShortcut | undefined = undefined;
|
||||
|
||||
/// Reference to the marker track element so we can access its div.
|
||||
let markerTrack: LayoutRow | undefined = undefined;
|
||||
let positionRestore: number | undefined = undefined;
|
||||
/// When dragging, stores the original value of the marker or midpoint being dragged, so we can restore it if the drag is cancelled.
|
||||
let dragRestore: number | undefined = undefined;
|
||||
/// When dragging, indicates whether this maker was inserted during the drag, so we know whether to remove it again if the drag is cancelled.
|
||||
let deletionRestore: boolean | undefined = undefined;
|
||||
/// When dragging, stores the previous active marker (or its midpoint) index, so we can restore active selection to that one if the drag is cancelled on a different marker.
|
||||
let activeMarkerIndexRestore: number | undefined = undefined;
|
||||
/// When dragging, stores whether the previously active drag item was a midpoint (matching the index kept in `activeMarkerIndexRestore`), so we can restore its active selection if cancelled.
|
||||
let activeMarkerIsMidpointRestore = false;
|
||||
/// When dragging a midpoint, tracks whether the midpoint has actually moved by at least a pixel, to tell between a click-then-click-and-drag (just a drag) or a double-click (reset the midpoint).
|
||||
let midpointDragged = false;
|
||||
|
||||
function markerPointerDown(e: PointerEvent, index: number) {
|
||||
if (disabled) return;
|
||||
|
||||
// Left-click to select and begin potentially dragging
|
||||
if (e.button === BUTTON_LEFT) {
|
||||
// Set restore values at this time, so later the user can cancel the drag and restore to these values
|
||||
activeMarkerIndexRestore = activeMarkerIndex;
|
||||
activeMarkerIsMidpointRestore = activeMarkerIsMidpoint;
|
||||
|
||||
// Update the parent component with the newly activated marker or midpoint drag item
|
||||
activeMarkerIndex = index;
|
||||
dispatch("activeMarkerIndexChange", index);
|
||||
activeMarkerIsMidpoint = false;
|
||||
dispatch("activeMarkerIndexChange", { activeMarkerIndex, activeMarkerIsMidpoint });
|
||||
|
||||
addEvents();
|
||||
return;
|
||||
}
|
||||
|
|
@ -51,37 +74,68 @@
|
|||
return Math.max(0, Math.min(1, ratio));
|
||||
}
|
||||
|
||||
function insertStop(e: MouseEvent) {
|
||||
function midpointPointerDown(e: PointerEvent, index: number) {
|
||||
if (disabled) return;
|
||||
|
||||
if (e.button !== BUTTON_LEFT) return;
|
||||
|
||||
// Since we just pressed the mouse button down, the midpoint has not been dragged by any distance
|
||||
midpointDragged = false;
|
||||
|
||||
// Set restore values at this time, so later the user can cancel the drag and restore to these values
|
||||
activeMarkerIndexRestore = activeMarkerIndex;
|
||||
activeMarkerIsMidpointRestore = activeMarkerIsMidpoint;
|
||||
|
||||
// Update the parent component with the newly activated marker or midpoint drag item
|
||||
activeMarkerIndex = index;
|
||||
activeMarkerIsMidpoint = true;
|
||||
dispatch("activeMarkerIndexChange", { activeMarkerIndex, activeMarkerIsMidpoint });
|
||||
|
||||
addEvents();
|
||||
}
|
||||
|
||||
function resetMidpoint(index: number) {
|
||||
if (disabled || midpointDragged) return;
|
||||
|
||||
gradient.midpoint[index] = 0.5;
|
||||
dispatch("gradient", gradient);
|
||||
}
|
||||
|
||||
function insertStop(e: MouseEvent) {
|
||||
if (disabled) return;
|
||||
if (e.button !== BUTTON_LEFT) return;
|
||||
|
||||
// Determine the position along the gradient (0-1) based on the click position in the marker track
|
||||
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);
|
||||
// Determine which index the new stop should be inserted at based on its position
|
||||
let index = gradient.position.findIndex((item) => item > position);
|
||||
if (index === -1) index = gradient.position.length;
|
||||
|
||||
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;
|
||||
}
|
||||
// Determine the color of the new stop by evaluating the gradient at the position of the new stop
|
||||
type ReturnedColor = { red: number; green: number; blue: number; alpha: number };
|
||||
const evaluated = evaluateGradientAtPosition(position, new Float64Array(gradient.position), new Float64Array(gradient.midpoint), gradient.color) as ReturnedColor;
|
||||
const color = new Color(evaluated.red, evaluated.green, evaluated.blue, evaluated.alpha);
|
||||
|
||||
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;
|
||||
deletionRestore = true;
|
||||
|
||||
dispatch("activeMarkerIndexChange", index);
|
||||
// Insert the new stop into the gradient
|
||||
gradient.position.splice(index, 0, position);
|
||||
// Duplicate the midpoint ratio position of the interval we're inserting into, so both new intervals have the same midpoint position ratio
|
||||
gradient.midpoint.splice(index, 0, gradient.midpoint[index - 1] || 0.5);
|
||||
gradient.color.splice(index, 0, color);
|
||||
dispatch("gradient", gradient);
|
||||
|
||||
// Set restore values at this time, so later the user can cancel the drag and restore to these values
|
||||
activeMarkerIndexRestore = activeMarkerIndex;
|
||||
activeMarkerIsMidpointRestore = activeMarkerIsMidpoint;
|
||||
|
||||
// Update the parent component with the newly activated marker or midpoint drag item
|
||||
activeMarkerIndex = index;
|
||||
activeMarkerIsMidpoint = false;
|
||||
dispatch("activeMarkerIndexChange", { activeMarkerIndex, activeMarkerIsMidpoint });
|
||||
|
||||
// Since this stop insertion can happen as part of the beginning of a drag, we set this to indicate that it should be removed again if the drag is cancelled
|
||||
deletionRestore = true;
|
||||
|
||||
addEvents();
|
||||
}
|
||||
|
||||
|
|
@ -90,27 +144,35 @@
|
|||
|
||||
if (e.key !== "Delete" && e.key !== "Backspace") return;
|
||||
if (activeMarkerIndex === undefined) return;
|
||||
if (gradient.position.length <= 2 && !activeMarkerIsMidpoint) return;
|
||||
|
||||
if (positionRestore !== undefined) stopDrag();
|
||||
// Stop dragging the marker or midpoint
|
||||
stopDrag();
|
||||
|
||||
deleteStopByIndex(activeMarkerIndex);
|
||||
// Either reset the midpoint to 50% or delete the marker, based on which type is currently active
|
||||
if (activeMarkerIsMidpoint) resetMidpoint(activeMarkerIndex);
|
||||
else deleteStopByIndex(activeMarkerIndex);
|
||||
}
|
||||
|
||||
function deleteStopByIndex(index: number) {
|
||||
if (disabled) return;
|
||||
|
||||
if (gradient.stops.length <= 2) return;
|
||||
if (gradient.position.length <= 2) return;
|
||||
|
||||
gradient.position.splice(index, 1);
|
||||
gradient.midpoint.splice(index, 1);
|
||||
gradient.color.splice(index, 1);
|
||||
dispatch("gradient", gradient);
|
||||
|
||||
gradient.stops.splice(index, 1);
|
||||
if (gradient.stops.length === 0) {
|
||||
activeMarkerIndex = undefined;
|
||||
} else {
|
||||
activeMarkerIndex = Math.max(0, Math.min(gradient.stops.length - 1, index));
|
||||
}
|
||||
deletionRestore = undefined;
|
||||
|
||||
dispatch("activeMarkerIndexChange", activeMarkerIndex);
|
||||
dispatch("gradient", gradient);
|
||||
if (gradient.position.length === 0) {
|
||||
activeMarkerIndex = undefined;
|
||||
} else {
|
||||
activeMarkerIndex = Math.max(0, Math.min(gradient.position.length - 1, index));
|
||||
}
|
||||
activeMarkerIsMidpoint = false;
|
||||
dispatch("activeMarkerIndexChange", { activeMarkerIndex, activeMarkerIsMidpoint });
|
||||
}
|
||||
|
||||
function moveMarker(e: PointerEvent, index: number) {
|
||||
|
|
@ -122,40 +184,99 @@
|
|||
let position = markerPosition(e);
|
||||
if (position === undefined) return;
|
||||
|
||||
if (positionRestore === undefined) positionRestore = position;
|
||||
if (dragRestore === undefined) dragRestore = position;
|
||||
if (deletionRestore === undefined) {
|
||||
deletionRestore = false;
|
||||
|
||||
dispatch("dragging", true);
|
||||
}
|
||||
|
||||
setPosition(index, position);
|
||||
setPosition(index, position, false);
|
||||
}
|
||||
|
||||
export function setPosition(index: number, position: number) {
|
||||
function moveMidpoint(e: PointerEvent, index: number) {
|
||||
if (disabled) return;
|
||||
|
||||
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));
|
||||
// Guard in case the mouseup event is lost
|
||||
if (e.buttons === 0) {
|
||||
stopDrag();
|
||||
return;
|
||||
}
|
||||
|
||||
let position = markerPosition(e);
|
||||
if (position === undefined) return;
|
||||
|
||||
if (dragRestore === undefined) {
|
||||
dragRestore = gradient.midpoint[index];
|
||||
midpointDragged = true;
|
||||
dispatch("dragging", true);
|
||||
}
|
||||
|
||||
const leftStop = gradient.position[index];
|
||||
const rightStop = gradient.position[index + 1];
|
||||
const range = rightStop - leftStop;
|
||||
if (range <= 0) return;
|
||||
|
||||
gradient.midpoint[index] = Math.max(MIN_MIDPOINT, Math.min(MAX_MIDPOINT, (position - leftStop) / range));
|
||||
dispatch("gradient", gradient);
|
||||
}
|
||||
|
||||
export function setPosition(index: number, position: number, isMidpoint: boolean) {
|
||||
if (disabled) return;
|
||||
|
||||
const markers = toMarkers(gradient);
|
||||
const active = markers[index];
|
||||
|
||||
if (isMidpoint) active.midpoint = position;
|
||||
else active.position = position;
|
||||
|
||||
markers.sort((a, b) => a.position - b.position);
|
||||
if (markers.indexOf(active) !== activeMarkerIndex) {
|
||||
activeMarkerIndex = markers.indexOf(active);
|
||||
dispatch("activeMarkerIndexChange", { activeMarkerIndex, activeMarkerIsMidpoint });
|
||||
}
|
||||
|
||||
gradient.position = markers.map((stop) => stop.position);
|
||||
gradient.midpoint = markers.map((stop) => stop.midpoint);
|
||||
gradient.color = markers.map((stop) => stop.color);
|
||||
dispatch("gradient", gradient);
|
||||
}
|
||||
|
||||
function toMarkers(gradient: Gradient): { position: number; midpoint: number; color: Color }[] {
|
||||
return gradient.position.map((position, i) => ({
|
||||
position,
|
||||
midpoint: gradient.midpoint[i],
|
||||
color: gradient.color[i],
|
||||
}));
|
||||
}
|
||||
|
||||
function toMidpoints(gradient: Gradient): number[] {
|
||||
if (gradient.position.length < 2) return [];
|
||||
|
||||
return gradient.midpoint.slice(0, -1).map((midpoint, i) => {
|
||||
const leftMarker = gradient.position[i];
|
||||
const rightMarker = gradient.position[i + 1];
|
||||
return leftMarker + midpoint * (rightMarker - leftMarker);
|
||||
});
|
||||
}
|
||||
|
||||
function abortDrag() {
|
||||
if (disabled) return;
|
||||
|
||||
if (activeMarkerIndex === undefined) return;
|
||||
|
||||
if (deletionRestore) {
|
||||
deleteStopByIndex(activeMarkerIndex);
|
||||
} else if (positionRestore !== undefined) {
|
||||
setPosition(activeMarkerIndex, positionRestore);
|
||||
if (activeMarkerIndex !== undefined) {
|
||||
if (activeMarkerIsMidpoint && dragRestore !== undefined) {
|
||||
gradient.midpoint[activeMarkerIndex] = dragRestore;
|
||||
dispatch("gradient", gradient);
|
||||
} else {
|
||||
if (deletionRestore) deleteStopByIndex(activeMarkerIndex);
|
||||
else if (dragRestore !== undefined) setPosition(activeMarkerIndex, dragRestore, false);
|
||||
}
|
||||
}
|
||||
|
||||
activeMarkerIndex = activeMarkerIndexRestore;
|
||||
activeMarkerIsMidpoint = activeMarkerIsMidpointRestore;
|
||||
dispatch("activeMarkerIndexChange", { activeMarkerIndex, activeMarkerIsMidpoint });
|
||||
|
||||
stopDrag();
|
||||
}
|
||||
|
||||
|
|
@ -164,8 +285,11 @@
|
|||
|
||||
removeEvents();
|
||||
|
||||
positionRestore = undefined;
|
||||
dragRestore = undefined;
|
||||
deletionRestore = undefined;
|
||||
activeMarkerIndexRestore = undefined;
|
||||
activeMarkerIsMidpointRestore = false;
|
||||
midpointDragged = false;
|
||||
|
||||
dispatch("dragging", false);
|
||||
}
|
||||
|
|
@ -173,7 +297,8 @@
|
|||
function onPointerMove(e: PointerEvent) {
|
||||
if (disabled) return;
|
||||
|
||||
if (activeMarkerIndex !== undefined) moveMarker(e, activeMarkerIndex);
|
||||
if (activeMarkerIsMidpoint && activeMarkerIndex !== undefined) moveMidpoint(e, activeMarkerIndex);
|
||||
else if (activeMarkerIndex !== undefined) moveMarker(e, activeMarkerIndex);
|
||||
}
|
||||
|
||||
function onPointerUp() {
|
||||
|
|
@ -249,11 +374,27 @@
|
|||
}}
|
||||
>
|
||||
<LayoutRow class="gradient-strip" on:pointerdown={insertStop}></LayoutRow>
|
||||
<LayoutRow class="midpoint-track">
|
||||
{#each toMidpoints(gradient) as midpoint, index}
|
||||
<svg
|
||||
class="midpoint"
|
||||
class:active={index === activeMarkerIndex && activeMarkerIsMidpoint}
|
||||
style:--midpoint-position={midpoint}
|
||||
on:pointerdown={(e) => midpointPointerDown(e, index)}
|
||||
on:dblclick={() => resetMidpoint(index)}
|
||||
data-gradient-midpoint
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 8 8"
|
||||
>
|
||||
<polygon points="0,4 4,0 8,4 4,8" />
|
||||
</svg>
|
||||
{/each}
|
||||
</LayoutRow>
|
||||
<LayoutRow class="marker-track" bind:this={markerTrack}>
|
||||
{#each gradient.stops as marker, index}
|
||||
{#each toMarkers(gradient) as marker, index}
|
||||
<svg
|
||||
class="marker"
|
||||
class:active={index === activeMarkerIndex}
|
||||
class:active={index === activeMarkerIndex && !activeMarkerIsMidpoint}
|
||||
style:--marker-position={marker.position}
|
||||
style:--marker-color={marker.color.toRgbCSS()}
|
||||
on:pointerdown={(e) => markerPointerDown(e, index)}
|
||||
|
|
@ -276,6 +417,7 @@
|
|||
|
||||
<style lang="scss" global>
|
||||
.spectrum-input {
|
||||
position: relative;
|
||||
--marker-half-width: 6px;
|
||||
|
||||
.gradient-strip {
|
||||
|
|
@ -310,6 +452,39 @@
|
|||
}
|
||||
}
|
||||
|
||||
.midpoint-track {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: var(--marker-half-width);
|
||||
right: var(--marker-half-width);
|
||||
|
||||
.midpoint {
|
||||
position: absolute;
|
||||
margin-left: -4px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
bottom: 0;
|
||||
left: calc(var(--midpoint-position) * 100%);
|
||||
|
||||
polygon {
|
||||
stroke: var(--color-e-nearwhite);
|
||||
fill: var(--color-2-mildblack);
|
||||
}
|
||||
|
||||
&.active {
|
||||
z-index: 1;
|
||||
|
||||
polygon {
|
||||
fill: var(--color-e-nearwhite);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled .midpoint-track .midpoint polygon {
|
||||
stroke: var(--color-4-dimgray);
|
||||
}
|
||||
|
||||
.marker-track {
|
||||
margin-top: calc(24px - 16px - 12px);
|
||||
margin-left: var(--marker-half-width);
|
||||
|
|
@ -357,6 +532,8 @@
|
|||
}
|
||||
|
||||
&.active {
|
||||
z-index: 1;
|
||||
|
||||
.inner-fill {
|
||||
filter: drop-shadow(0 0 1px var(--color-2-mildblack)) drop-shadow(0 0 1px var(--color-2-mildblack));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { Transform, Type, plainToClass } from "class-transformer";
|
||||
|
||||
import { type EditorHandle } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import { sampleInterpolatedGradient, type EditorHandle } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import { type PopoverButtonStyle, type IconName, type IconSize } from "@graphite/icons";
|
||||
|
||||
export class JsMessage {
|
||||
|
|
@ -355,46 +355,40 @@ export type RGBA = { r: number; g: number; b: number; a: number };
|
|||
export type RGB = { r: number; g: number; b: number };
|
||||
|
||||
export class Gradient {
|
||||
readonly stops!: { position: number; color: Color }[];
|
||||
position!: number[];
|
||||
midpoint!: number[];
|
||||
color!: Color[];
|
||||
|
||||
constructor(stops: { position: number; color: Color }[]) {
|
||||
this.stops = stops;
|
||||
constructor(position: number[], midpoint: number[], color: Color[]) {
|
||||
this.position = position;
|
||||
this.midpoint = midpoint;
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
toLinearGradientCSS(): string {
|
||||
if (this.stops.length === 1) {
|
||||
return `linear-gradient(to right, ${this.stops[0].color.toHexOptionalAlpha()} 0%, ${this.stops[0].color.toHexOptionalAlpha()} 100%)`;
|
||||
if (this.position.length === 1) {
|
||||
return `linear-gradient(to right, ${this.color[0].toHexOptionalAlpha()} 0%, ${this.color[0].toHexOptionalAlpha()} 100%)`;
|
||||
}
|
||||
const pieces = this.stops.map((stop) => `${stop.color.toHexOptionalAlpha()} ${stop.position * 100}%`);
|
||||
return `linear-gradient(to right, ${pieces.join(", ")})`;
|
||||
|
||||
const pieces = sampleInterpolatedGradient(new Float64Array(this.position), new Float64Array(this.midpoint), this.color, false);
|
||||
return `linear-gradient(to right, ${pieces})`;
|
||||
}
|
||||
|
||||
toLinearGradientCSSNoAlpha(): string {
|
||||
if (this.stops.length === 1) {
|
||||
return `linear-gradient(to right, ${this.stops[0].color.toHexNoAlpha()} 0%, ${this.stops[0].color.toHexNoAlpha()} 100%)`;
|
||||
if (this.position.length === 1) {
|
||||
return `linear-gradient(to right, ${this.color[0].toHexNoAlpha()} 0%, ${this.color[0].toHexNoAlpha()} 100%)`;
|
||||
}
|
||||
const pieces = this.stops.map((stop) => `${stop.color.toHexNoAlpha()} ${stop.position * 100}%`);
|
||||
return `linear-gradient(to right, ${pieces.join(", ")})`;
|
||||
|
||||
const pieces = sampleInterpolatedGradient(new Float64Array(this.position), new Float64Array(this.midpoint), this.color, true);
|
||||
return `linear-gradient(to right, ${pieces})`;
|
||||
}
|
||||
|
||||
firstColor(): Color | undefined {
|
||||
return this.stops[0]?.color;
|
||||
return this.color[0];
|
||||
}
|
||||
|
||||
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;
|
||||
return this.color[this.color.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -499,10 +493,6 @@ export class Color {
|
|||
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 {
|
||||
if (this.none) return undefined;
|
||||
|
||||
|
|
@ -951,20 +941,19 @@ export class ColorInput extends WidgetProps {
|
|||
// Content
|
||||
@Transform(({ value }) => {
|
||||
if (value instanceof Gradient) return value;
|
||||
const gradient = value["Gradient"];
|
||||
const gradient: Gradient | undefined = value["Gradient"];
|
||||
if (gradient) {
|
||||
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);
|
||||
return new Gradient(
|
||||
gradient.position,
|
||||
gradient.midpoint,
|
||||
gradient.color.map((color: any) => new Color(color.red, color.green, color.blue, color.alpha)),
|
||||
);
|
||||
}
|
||||
|
||||
if (value instanceof Color) return value;
|
||||
|
||||
const solid = value["Solid"];
|
||||
if (solid) {
|
||||
return new Color(solid.red, solid.green, solid.blue, solid.alpha);
|
||||
}
|
||||
if (solid) return new Color(solid.red, solid.green, solid.blue, solid.alpha);
|
||||
|
||||
return new Color("none");
|
||||
})
|
||||
|
|
@ -1008,10 +997,10 @@ export function contrastingOutlineFactor(value: FillChoice, proximityColor: stri
|
|||
};
|
||||
|
||||
if (value instanceof Gradient) {
|
||||
if (value.stops.length === 0) return 0;
|
||||
if (value.color.length === 0) return 0;
|
||||
|
||||
const first = contrast(value.stops[0].color);
|
||||
const last = contrast(value.stops[value.stops.length - 1].color);
|
||||
const first = contrast(value.color[0]);
|
||||
const last = contrast(value.color[value.color.length - 1]);
|
||||
|
||||
return Math.min(first, last);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export default defineConfig(({ mode }) => {
|
|||
"a11y_consider_explicit_label", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json`
|
||||
"a11y_click_events_have_key_events", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json`
|
||||
"a11y_no_noninteractive_element_interactions", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json`
|
||||
"a11y_no_static_element_interactions", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json`
|
||||
];
|
||||
if (suppressed.includes(warning.code)) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ use editor::messages::tool::tool_messages::tool_prelude::WidgetId;
|
|||
use graph_craft::document::NodeId;
|
||||
use graphene_std::raster::Image;
|
||||
use graphene_std::raster::color::Color;
|
||||
use graphene_std::vector::GradientStops;
|
||||
use js_sys::{Object, Reflect};
|
||||
use serde::Serialize;
|
||||
use serde_wasm_bindgen::{self, from_value};
|
||||
|
|
@ -903,6 +904,34 @@ pub fn evaluate_math_expression(expression: &str) -> Option<f64> {
|
|||
Some(real)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = sampleInterpolatedGradient)]
|
||||
pub fn sample_interpolated_gradient(position: Vec<f64>, midpoint: Vec<f64>, color: Vec<JsValue>, omit_alpha: bool) -> String {
|
||||
let color = color.into_iter().filter_map(|c| serde_wasm_bindgen::from_value(c).ok()).collect();
|
||||
GradientStops { position, midpoint, color }
|
||||
.interpolated_samples()
|
||||
.into_iter()
|
||||
.map(|(position, color, _)| {
|
||||
let hex = if omit_alpha { color.to_rgb_hex_srgb_from_gamma() } else { color.to_rgba_hex_srgb_from_gamma() };
|
||||
let percent = ((position * 100.) * 1e2).round() / 1e2;
|
||||
format!("#{hex} {percent}%")
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = evaluateGradientAtPosition)]
|
||||
pub fn evaluate_gradient_at_position(t: f64, position: Vec<f64>, midpoint: Vec<f64>, color: Vec<JsValue>) -> Object {
|
||||
let color = color.into_iter().filter_map(|c| serde_wasm_bindgen::from_value(c).ok()).collect();
|
||||
let color = GradientStops { position, midpoint, color }.evaluate(t);
|
||||
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &JsValue::from_str("red"), &JsValue::from_f64(color.r() as f64)).unwrap();
|
||||
Reflect::set(&obj, &JsValue::from_str("green"), &JsValue::from_f64(color.g() as f64)).unwrap();
|
||||
Reflect::set(&obj, &JsValue::from_str("blue"), &JsValue::from_f64(color.b() as f64)).unwrap();
|
||||
Reflect::set(&obj, &JsValue::from_str("alpha"), &JsValue::from_f64(color.a() as f64)).unwrap();
|
||||
obj
|
||||
}
|
||||
|
||||
/// Helper function for calling JS's `requestAnimationFrame` with the given closure
|
||||
fn request_animation_frame(f: &Closure<dyn FnMut(f64)>) {
|
||||
web_sys::window()
|
||||
|
|
|
|||
|
|
@ -847,6 +847,18 @@ impl Color {
|
|||
format!("{:02x?}{:02x?}{:02x?}", (self.r() * 255.) as u8, (self.g() * 255.) as u8, (self.b() * 255.) as u8)
|
||||
}
|
||||
|
||||
/// Return an 8-character RGBA hex string (without a # prefix). Use this if the [`Color`] is in gamma space.
|
||||
#[cfg(feature = "std")]
|
||||
pub fn to_rgba_hex_srgb_from_gamma(&self) -> String {
|
||||
format!(
|
||||
"{:02x?}{:02x?}{:02x?}{:02x?}",
|
||||
(self.r() * 255.) as u8,
|
||||
(self.g() * 255.) as u8,
|
||||
(self.b() * 255.) as u8,
|
||||
(self.a() * 255.) as u8,
|
||||
)
|
||||
}
|
||||
|
||||
/// Return the all components as a u8 slice, first component is red, followed by green, followed by blue, followed by alpha. Use this if the [`Color`] is in linear space.
|
||||
///
|
||||
/// # Examples
|
||||
|
|
|
|||
|
|
@ -16,15 +16,18 @@ impl RenderExt for Gradient {
|
|||
/// Adds the gradient def through mutating the first argument, returning the gradient ID.
|
||||
fn render(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: DAffine2, transformed_bounds: DAffine2, _render_params: &RenderParams) -> Self::Output {
|
||||
let mut stop = String::new();
|
||||
for (position, color) in self.stops.0.iter() {
|
||||
for (position, color, original_midpoint) in self.stops.interpolated_samples() {
|
||||
stop.push_str("<stop");
|
||||
if *position != 0. {
|
||||
if position != 0. {
|
||||
let _ = write!(stop, r#" offset="{}""#, (position * 1_000_000.).round() / 1_000_000.);
|
||||
}
|
||||
let _ = write!(stop, r##" stop-color="#{}""##, color.to_rgb_hex_srgb_from_gamma());
|
||||
if color.a() < 1. {
|
||||
let _ = write!(stop, r#" stop-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
|
||||
}
|
||||
if let Some(midpoint) = original_midpoint {
|
||||
let _ = write!(stop, r#" graphite:midpoint="{}""#, (midpoint * 1000.).round() / 1000.);
|
||||
}
|
||||
stop.push_str(" />")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ impl SvgRender {
|
|||
let (x, y) = bounds_min.into();
|
||||
let (size_x, size_y) = (bounds_max - bounds_min).into();
|
||||
let defs = &self.svg_defs;
|
||||
let svg_header = format!(r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{x} {y} {size_x} {size_y}"><defs>{defs}</defs>"#,);
|
||||
let svg_header = format!(r#"<svg xmlns="http://www.w3.org/2000/svg" xmlns:graphite="https://graphite.art" viewBox="{x} {y} {size_x} {size_y}"><defs>{defs}</defs>"#,);
|
||||
self.svg.insert(0, svg_header.into());
|
||||
self.svg.push("</svg>".into());
|
||||
}
|
||||
|
|
@ -99,7 +99,7 @@ impl SvgRender {
|
|||
let matrix = format_transform_matrix(transform);
|
||||
let transform = if matrix.is_empty() { String::new() } else { format!(r#" transform="{matrix}""#) };
|
||||
|
||||
let svg_header = format!(r#"<svg xmlns="http://www.w3.org/2000/svg" {view_box}><defs>{defs}</defs><g{transform}>"#);
|
||||
let svg_header = format!(r#"<svg xmlns="http://www.w3.org/2000/svg" xmlns:graphite="https://graphite.art" {view_box}><defs>{defs}</defs><g{transform}>"#);
|
||||
self.svg.insert(0, svg_header.into());
|
||||
self.svg.push("</g></svg>".into());
|
||||
}
|
||||
|
|
@ -997,9 +997,9 @@ impl Render for Table<Vector> {
|
|||
}
|
||||
Fill::Gradient(gradient) => {
|
||||
let mut stops = peniko::ColorStops::new();
|
||||
for &(offset, color) in &gradient.stops {
|
||||
for (position, color, _) in gradient.stops.interpolated_samples() {
|
||||
stops.push(peniko::ColorStop {
|
||||
offset: offset as f32,
|
||||
offset: position as f32,
|
||||
color: peniko::color::DynamicColor::from_alpha_color(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])),
|
||||
});
|
||||
}
|
||||
|
|
@ -1557,11 +1557,14 @@ impl Render for Table<GradientStops> {
|
|||
attributes.push("points", format!("{max},{max} -{max},{max} -{max},-{max} {max},-{max}"));
|
||||
|
||||
let mut stop_string = String::new();
|
||||
for (position, color) in row.element.0.iter() {
|
||||
for (position, color, original_midpoint) in row.element.interpolated_samples() {
|
||||
let _ = write!(stop_string, r##"<stop offset="{}" stop-color="#{}""##, position, color.to_rgb_hex_srgb_from_gamma());
|
||||
if color.a() < 1. {
|
||||
let _ = write!(stop_string, r#" stop-opacity="{}""#, color.a());
|
||||
}
|
||||
if let Some(midpoint) = original_midpoint {
|
||||
let _ = write!(stop_string, r#" graphite:midpoint="{}""#, (midpoint * 1000.).round() / 1000.);
|
||||
}
|
||||
stop_string.push_str(" />");
|
||||
}
|
||||
|
||||
|
|
@ -1619,7 +1622,7 @@ impl Render for Table<GradientStops> {
|
|||
let blend_mode = alpha_blending.blend_mode.to_peniko();
|
||||
let opacity = alpha_blending.opacity(render_params.for_mask);
|
||||
|
||||
let color = row.element.0.first().map(|stop| stop.1).unwrap_or(Color::MAGENTA);
|
||||
let color = row.element.color.first().copied().unwrap_or(Color::MAGENTA);
|
||||
let vello_color = peniko::Color::new([color.r(), color.g(), color.b(), color.a()]);
|
||||
|
||||
let rect = kurbo::Rect::from_origin_size(kurbo::Point::ZERO, kurbo::Size::new(1., 1.));
|
||||
|
|
|
|||
|
|
@ -13,22 +13,69 @@ pub enum GradientType {
|
|||
// TODO: Someday we could switch this to a Box[T] to avoid over-allocation
|
||||
// TODO: Use linear not gamma colors
|
||||
/// 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)>);
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, DynAny, specta::Type)]
|
||||
pub struct GradientStops {
|
||||
/// The position of this stop, a factor from 0-1 along the length of the full gradient.
|
||||
pub position: Vec<f64>,
|
||||
/// The midpoint to the right of this stop, a factor from 0-1 along the distance to the next stop. The final stop's midpoint is ignored.
|
||||
pub midpoint: Vec<f64>,
|
||||
/// The color at this stop.
|
||||
pub color: Vec<Color>,
|
||||
}
|
||||
|
||||
// TODO: Eventually remove this migration document upgrade code
|
||||
impl<'de> serde::Deserialize<'de> for GradientStops {
|
||||
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
#[derive(serde::Deserialize)]
|
||||
struct NewFormat {
|
||||
position: Vec<f64>,
|
||||
midpoint: Vec<f64>,
|
||||
color: Vec<Color>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum GradientStopsFormat {
|
||||
New(NewFormat),
|
||||
Old(Vec<(f64, Color)>),
|
||||
}
|
||||
|
||||
Ok(match GradientStopsFormat::deserialize(deserializer)? {
|
||||
GradientStopsFormat::New(new) => Self {
|
||||
position: new.position,
|
||||
midpoint: new.midpoint,
|
||||
color: new.color,
|
||||
},
|
||||
GradientStopsFormat::Old(stops) => {
|
||||
let count = stops.len();
|
||||
Self {
|
||||
position: stops.iter().map(|(p, _)| *p).collect(),
|
||||
midpoint: vec![0.5; count],
|
||||
color: stops.into_iter().map(|(_, c)| c).collect(),
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::hash::Hash for GradientStops {
|
||||
fn hash<H: std::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);
|
||||
});
|
||||
self.position.len().hash(state);
|
||||
for i in 0..self.position.len() {
|
||||
self.position[i].to_bits().hash(state);
|
||||
self.midpoint[i].to_bits().hash(state);
|
||||
self.color[i].hash(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GradientStops {
|
||||
fn default() -> Self {
|
||||
Self(vec![(0., Color::BLACK), (1., Color::WHITE)])
|
||||
Self {
|
||||
position: vec![0., 1.],
|
||||
midpoint: vec![0.5, 0.5],
|
||||
color: vec![Color::BLACK, Color::WHITE],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -38,71 +85,145 @@ impl RenderComplexity for GradientStops {
|
|||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for GradientStops {
|
||||
type Item = (f64, Color);
|
||||
type IntoIter = std::vec::IntoIter<(f64, Color)>;
|
||||
/// Apply the midpoint curve to a normalized parameter `t` (0 to 1) given a `midpoint` (0 to 1, where 0.5 is linear).
|
||||
fn apply_midpoint(t: f64, midpoint: f64) -> f64 {
|
||||
if (midpoint - 0.5).abs() < 1e-6 {
|
||||
return t;
|
||||
}
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
let midpoint = midpoint.clamp(f64::EPSILON, 1. - f64::EPSILON);
|
||||
|
||||
if midpoint < 0.5 {
|
||||
let q = -1. / (1. - midpoint).log2();
|
||||
1. - (1. - t).powf(q)
|
||||
} else {
|
||||
let p = -1. / midpoint.log2();
|
||||
t.powf(p)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct GradientStop {
|
||||
pub position: f64,
|
||||
pub midpoint: f64,
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
pub struct GradientStopsIter<'a> {
|
||||
stops: &'a GradientStops,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for GradientStopsIter<'a> {
|
||||
type Item = GradientStop;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.index >= self.stops.position.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let stop = GradientStop {
|
||||
position: self.stops.position[self.index],
|
||||
midpoint: self.stops.midpoint[self.index],
|
||||
color: self.stops.color[self.index],
|
||||
};
|
||||
self.index += 1;
|
||||
Some(stop)
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
let remaining = self.stops.position.len() - self.index;
|
||||
(remaining, Some(remaining))
|
||||
}
|
||||
}
|
||||
|
||||
impl ExactSizeIterator for GradientStopsIter<'_> {}
|
||||
|
||||
impl<'a> IntoIterator for &'a GradientStops {
|
||||
type Item = &'a (f64, Color);
|
||||
type IntoIter = std::slice::Iter<'a, (f64, Color)>;
|
||||
type Item = GradientStop;
|
||||
type IntoIter = GradientStopsIter<'a>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.iter()
|
||||
GradientStopsIter { stops: self, index: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Index<usize> for GradientStops {
|
||||
type Output = (f64, Color);
|
||||
impl IntoIterator for GradientStops {
|
||||
type Item = GradientStop;
|
||||
type IntoIter = std::vec::IntoIter<GradientStop>;
|
||||
|
||||
fn index(&self, index: usize) -> &Self::Output {
|
||||
&self.0[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for GradientStops {
|
||||
type Target = Vec<(f64, Color)>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for GradientStops {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.position
|
||||
.into_iter()
|
||||
.zip(self.midpoint)
|
||||
.zip(self.color)
|
||||
.map(|((position, midpoint), color)| GradientStop { position, midpoint, color })
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl GradientStops {
|
||||
pub fn new(stops: Vec<(f64, Color)>) -> Self {
|
||||
let mut stops = Self(stops);
|
||||
stops.sort();
|
||||
stops
|
||||
pub fn new(stops: impl IntoIterator<Item = GradientStop>) -> Self {
|
||||
let mut position = Vec::new();
|
||||
let mut midpoint = Vec::new();
|
||||
let mut color = Vec::new();
|
||||
|
||||
for stop in stops {
|
||||
position.push(stop.position);
|
||||
midpoint.push(stop.midpoint);
|
||||
color.push(stop.color);
|
||||
}
|
||||
|
||||
Self { position, midpoint, color }
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.position.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.position.is_empty()
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> GradientStopsIter<'_> {
|
||||
self.into_iter()
|
||||
}
|
||||
|
||||
/// Remove a stop at the given index.
|
||||
pub fn remove(&mut self, index: usize) {
|
||||
self.position.remove(index);
|
||||
self.midpoint.remove(index);
|
||||
self.color.remove(index);
|
||||
}
|
||||
|
||||
/// Remove and return the last stop's color, or `None` if empty.
|
||||
pub fn pop(&mut self) -> Option<Color> {
|
||||
self.position.pop();
|
||||
self.midpoint.pop();
|
||||
self.color.pop()
|
||||
}
|
||||
|
||||
pub fn evaluate(&self, t: f64) -> Color {
|
||||
if self.0.is_empty() {
|
||||
if self.position.is_empty() {
|
||||
return Color::BLACK;
|
||||
}
|
||||
|
||||
if t <= self.0[0].0 {
|
||||
return self.0[0].1;
|
||||
if t <= self.position[0] {
|
||||
return self.color[0];
|
||||
}
|
||||
if t >= self.0[self.0.len() - 1].0 {
|
||||
return self.0[self.0.len() - 1].1;
|
||||
let last = self.position.len() - 1;
|
||||
if t >= self.position[last] {
|
||||
return self.color[last];
|
||||
}
|
||||
|
||||
for i in 0..self.0.len() - 1 {
|
||||
let (t1, c1) = self.0[i];
|
||||
let (t2, c2) = self.0[i + 1];
|
||||
for i in 0..self.position.len() - 1 {
|
||||
let (t1, c1) = (self.position[i], self.color[i]);
|
||||
let (t2, c2) = (self.position[i + 1], self.color[i + 1]);
|
||||
if t >= t1 && t <= t2 {
|
||||
let normalized_t = (t - t1) / (t2 - t1);
|
||||
return c1.lerp(&c2, normalized_t as f32);
|
||||
let adjusted_t = apply_midpoint(normalized_t, self.midpoint[i]);
|
||||
return c1.lerp(&c2, adjusted_t as f32);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -110,15 +231,104 @@ impl GradientStops {
|
|||
}
|
||||
|
||||
pub fn sort(&mut self) {
|
||||
self.0.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
|
||||
let mut indices: Vec<usize> = (0..self.position.len()).collect();
|
||||
indices.sort_unstable_by(|&a, &b| self.position[a].partial_cmp(&self.position[b]).unwrap());
|
||||
self.position = indices.iter().map(|&i| self.position[i]).collect();
|
||||
self.midpoint = indices.iter().map(|&i| self.midpoint[i]).collect();
|
||||
self.color = indices.iter().map(|&i| self.color[i]).collect();
|
||||
}
|
||||
|
||||
pub fn reversed(&self) -> Self {
|
||||
Self(self.0.iter().rev().map(|(position, color)| (1. - position, *color)).collect())
|
||||
let position: Vec<f64> = self.position.iter().rev().map(|&p| 1. - p).collect();
|
||||
|
||||
let count = self.midpoint.len();
|
||||
let midpoint = (0..count).map(|i| if i < count - 1 { 1. - self.midpoint[count - 2 - i] } else { 0.5 }).collect::<Vec<_>>();
|
||||
|
||||
let color: Vec<Color> = self.color.iter().rev().cloned().collect();
|
||||
|
||||
Self { position, midpoint, color }
|
||||
}
|
||||
|
||||
pub fn map_colors<F: Fn(&Color) -> Color>(&self, f: F) -> Self {
|
||||
Self(self.0.iter().map(|(position, color)| (*position, f(color))).collect())
|
||||
Self {
|
||||
position: self.position.clone(),
|
||||
midpoint: self.midpoint.clone(),
|
||||
color: self.color.iter().map(f).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Produce a set of linearly-interpolated color samples that approximate the gradient's midpoint curves.
|
||||
///
|
||||
/// Each sample is `(position, color, original_midpoint)` where `original_midpoint` is `Some(f64)` with the corresponding
|
||||
/// midpoint for actual gradient stops, and `None` for interpolated samples added to approximate midpoint curves.
|
||||
pub fn interpolated_samples(&self) -> Vec<(f64, Color, Option<f64>)> {
|
||||
/// Controls accuracy vs. number of samples tradeoff.
|
||||
/// 2/255 means the linear approximation will deviate by no more than 2 gradations of 8-bit color from the theoretically perfect curve with this midpoint bias.
|
||||
const THRESHOLD: f64 = 2. / 255.;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn subdivide(left: f64, right: f64, midpoint: f64, pos_a: f64, pos_b: f64, color_a: Color, color_b: Color, result: &mut Vec<(f64, Color, Option<f64>)>, depth: u32) {
|
||||
const MAX_DEPTH: u32 = 20;
|
||||
if depth >= MAX_DEPTH {
|
||||
return;
|
||||
}
|
||||
|
||||
let mid = (left + right) / 2.;
|
||||
|
||||
let y_actual = apply_midpoint(mid, midpoint);
|
||||
let y_left = apply_midpoint(left, midpoint);
|
||||
let y_right = apply_midpoint(right, midpoint);
|
||||
let y_linear = (y_left + y_right) / 2.;
|
||||
|
||||
if (y_actual - y_linear).abs() > THRESHOLD {
|
||||
subdivide(left, mid, midpoint, pos_a, pos_b, color_a, color_b, result, depth + 1);
|
||||
|
||||
let global_pos = pos_a + mid * (pos_b - pos_a);
|
||||
let color = color_a.lerp(&color_b, y_actual as f32);
|
||||
result.push((global_pos, color, None));
|
||||
|
||||
subdivide(mid, right, midpoint, pos_a, pos_b, color_a, color_b, result, depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
if self.position.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
if self.position.len() == 1 {
|
||||
return vec![(self.position[0], self.color[0], Some(self.midpoint[0]))];
|
||||
}
|
||||
|
||||
let mut result = Vec::new();
|
||||
|
||||
for i in 0..self.position.len() - 1 {
|
||||
let pos_a = self.position[i];
|
||||
let pos_b = self.position[i + 1];
|
||||
let color_a = self.color[i];
|
||||
let color_b = self.color[i + 1];
|
||||
let midpoint = self.midpoint[i].clamp(0.01, 0.99);
|
||||
let next_midpoint = self.midpoint[i + 1].clamp(0.01, 0.99);
|
||||
|
||||
// Add the start stop (subsequent segments share the previous end stop)
|
||||
if i == 0 {
|
||||
result.push((pos_a, color_a, Some(midpoint)));
|
||||
}
|
||||
|
||||
// Only subdivide if midpoint deviates from linear (0.5)
|
||||
if (midpoint - 0.5).abs() >= 1e-6 {
|
||||
subdivide(0., 1., midpoint, pos_a, pos_b, color_a, color_b, &mut result, 0);
|
||||
}
|
||||
|
||||
// Add the end stop
|
||||
result.push((pos_b, color_b, Some(next_midpoint)));
|
||||
}
|
||||
|
||||
// If every midpoint is 0.5 (or within epsilon), turn all midpoints to None
|
||||
if result.iter().all(|(_, _, midpoint)| matches!(midpoint, Some(m) if (m - 0.5).abs() < 1e-6)) {
|
||||
result.iter_mut().for_each(|(_, _, midpoint)| *midpoint = None);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -147,13 +357,14 @@ impl Default for Gradient {
|
|||
|
||||
impl std::hash::Hash for Gradient {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.stops.0.len().hash(state);
|
||||
self.stops.len().hash(state);
|
||||
[].iter()
|
||||
.chain(self.start.to_array().iter())
|
||||
.chain(self.end.to_array().iter())
|
||||
.chain(self.stops.0.iter().map(|(position, _)| position))
|
||||
.chain(self.stops.position.iter())
|
||||
.chain(self.stops.midpoint.iter())
|
||||
.for_each(|x| x.to_bits().hash(state));
|
||||
self.stops.0.iter().for_each(|(_, color)| color.hash(state));
|
||||
self.stops.color.iter().for_each(|color| color.hash(state));
|
||||
self.gradient_type.hash(state);
|
||||
}
|
||||
}
|
||||
|
|
@ -163,9 +374,8 @@ impl std::fmt::Display for Gradient {
|
|||
let round = |x: f64| (x * 1e3).round() / 1e3;
|
||||
let stops = self
|
||||
.stops
|
||||
.0
|
||||
.iter()
|
||||
.map(|(position, color)| format!("[{}%: #{}]", round(position * 100.), color.to_rgba_hex_srgb()))
|
||||
.map(|stop| format!("[{}%: #{}]", round(stop.position * 100.), stop.color.to_rgba_hex_srgb()))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
write!(f, "{} Gradient: {stops}", self.gradient_type)
|
||||
|
|
@ -175,7 +385,18 @@ impl std::fmt::Display for Gradient {
|
|||
impl Gradient {
|
||||
/// Constructs a new gradient with the colors at 0 and 1 specified.
|
||||
pub fn new(start: DVec2, start_color: Color, end: DVec2, end_color: Color, gradient_type: GradientType) -> Self {
|
||||
let stops = GradientStops::new(vec![(0., start_color.to_gamma_srgb()), (1., end_color.to_gamma_srgb())]);
|
||||
let stops = GradientStops::new([
|
||||
GradientStop {
|
||||
position: 0.,
|
||||
midpoint: 0.5,
|
||||
color: start_color.to_gamma_srgb(),
|
||||
},
|
||||
GradientStop {
|
||||
position: 1.,
|
||||
midpoint: 0.5,
|
||||
color: end_color.to_gamma_srgb(),
|
||||
},
|
||||
]);
|
||||
|
||||
Self { start, end, stops, gradient_type }
|
||||
}
|
||||
|
|
@ -183,17 +404,11 @@ impl Gradient {
|
|||
pub fn lerp(&self, other: &Self, time: f64) -> Self {
|
||||
let start = self.start + (other.start - self.start) * time;
|
||||
let end = self.end + (other.end - self.end) * time;
|
||||
let stops = self
|
||||
.stops
|
||||
.0
|
||||
.iter()
|
||||
.zip(other.stops.0.iter())
|
||||
.map(|((a_pos, a_color), (b_pos, b_color))| {
|
||||
let position = a_pos + (b_pos - a_pos) * time;
|
||||
let color = a_color.lerp(b_color, time as f32);
|
||||
(position, color)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let stops = self.stops.iter().zip(other.stops.iter()).map(|(a, b)| {
|
||||
let position = a.position + (b.position - a.position) * time;
|
||||
let color = a.color.lerp(&b.color, time as f32);
|
||||
GradientStop { position, midpoint: 0.5, color }
|
||||
});
|
||||
let stops = GradientStops::new(stops);
|
||||
let gradient_type = if time < 0.5 { self.gradient_type } else { other.gradient_type };
|
||||
|
||||
|
|
@ -213,27 +428,19 @@ impl Gradient {
|
|||
return None;
|
||||
}
|
||||
|
||||
// Compute the color of the inserted stop
|
||||
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
|
||||
(a, Some(b)) => a.lerp(
|
||||
&b,
|
||||
((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
|
||||
(v, _) => v,
|
||||
};
|
||||
// Compute the color of the inserted stop using evaluate (which respects midpoints)
|
||||
let new_color = self.stops.evaluate(new_position);
|
||||
|
||||
// Compute the correct index to keep the positions in order
|
||||
let mut index = 0;
|
||||
while self.stops.0.len() > index && self.stops.0[index].0 <= new_position {
|
||||
while self.stops.len() > index && self.stops.position[index] <= new_position {
|
||||
index += 1;
|
||||
}
|
||||
|
||||
let new_color = get_color(index - 1, new_position);
|
||||
|
||||
// Insert the new stop
|
||||
self.stops.0.insert(index, (new_position, new_color));
|
||||
self.stops.position.insert(index, new_position);
|
||||
self.stops.midpoint.insert(index, 0.5);
|
||||
self.stops.color.insert(index, new_color);
|
||||
|
||||
Some(index)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ pub mod vector;
|
|||
|
||||
// Re-export commonly used types at the crate root
|
||||
pub use core_types as gcore;
|
||||
pub use gradient::{GradientStops, GradientType};
|
||||
pub use gradient::{GradientStop, GradientStops, GradientType};
|
||||
pub use math::{QuadExt, RectExt};
|
||||
pub use subpath::Subpath;
|
||||
pub use vector::Vector;
|
||||
|
|
|
|||
|
|
@ -51,7 +51,13 @@ impl Fill {
|
|||
Self::None => Color::BLACK,
|
||||
Self::Solid(color) => *color,
|
||||
// TODO: Should correctly sample the gradient the equation here: https://svgwg.org/svg2-draft/pservers.html#Gradients
|
||||
Self::Gradient(Gradient { stops, .. }) => stops.0[0].1,
|
||||
Self::Gradient(Gradient { stops, .. }) => {
|
||||
if stops.is_empty() {
|
||||
Color::BLACK
|
||||
} else {
|
||||
stops.color[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -64,13 +70,13 @@ impl Fill {
|
|||
(Self::Solid(a), Self::Solid(b)) => Self::Solid(a.lerp(b, time as f32)),
|
||||
(Self::Solid(a), Self::Gradient(b)) => {
|
||||
let mut solid_to_gradient = b.clone();
|
||||
solid_to_gradient.stops.0.iter_mut().for_each(|(_, color)| *color = *a);
|
||||
solid_to_gradient.stops.color.iter_mut().for_each(|color| *color = *a);
|
||||
let a = &solid_to_gradient;
|
||||
Self::Gradient(a.lerp(b, time))
|
||||
}
|
||||
(Self::Gradient(a), Self::Solid(b)) => {
|
||||
let mut gradient_to_solid = a.clone();
|
||||
gradient_to_solid.stops.0.iter_mut().for_each(|(_, color)| *color = *b);
|
||||
gradient_to_solid.stops.color.iter_mut().for_each(|color| *color = *b);
|
||||
let b = &gradient_to_solid;
|
||||
Self::Gradient(a.lerp(b, time))
|
||||
}
|
||||
|
|
@ -99,7 +105,7 @@ impl Fill {
|
|||
pub fn is_opaque(&self) -> bool {
|
||||
match self {
|
||||
Fill::Solid(color) => color.is_opaque(),
|
||||
Fill::Gradient(gradient) => gradient.stops.iter().all(|(_, color)| color.is_opaque()),
|
||||
Fill::Gradient(gradient) => gradient.stops.color.iter().all(|color| color.is_opaque()),
|
||||
Fill::None => true,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ pub mod subpath {
|
|||
}
|
||||
|
||||
pub mod gradient {
|
||||
pub use vector_types::GradientStops;
|
||||
pub use vector_types::{GradientStop, GradientStops};
|
||||
}
|
||||
|
||||
pub mod transform {
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ mod adjust_std {
|
|||
}
|
||||
impl Adjust<Color> for GradientStops {
|
||||
fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) {
|
||||
for (_, color) in self.iter_mut() {
|
||||
for color in self.color.iter_mut() {
|
||||
*color = map_fn(color);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use no_std_types::registry::types::PercentageF32;
|
|||
#[cfg(feature = "std")]
|
||||
use raster_types::{CPU, Raster};
|
||||
#[cfg(feature = "std")]
|
||||
use vector_types::GradientStops;
|
||||
use vector_types::{GradientStop, GradientStops};
|
||||
|
||||
pub trait Blend<P: Pixel> {
|
||||
fn blend(&self, under: &Self, blend_fn: impl Fn(P, P) -> P) -> Self;
|
||||
|
|
@ -63,18 +63,15 @@ mod blend_std {
|
|||
}
|
||||
impl Blend<Color> for GradientStops {
|
||||
fn blend(&self, under: &Self, blend_fn: impl Fn(Color, Color) -> Color) -> Self {
|
||||
let mut combined_stops = self.iter().map(|(position, _)| position).chain(under.iter().map(|(position, _)| position)).collect::<Vec<_>>();
|
||||
combined_stops.dedup_by(|&mut a, &mut b| (a - b).abs() < 1e-6);
|
||||
let mut combined_stops = self.position.iter().chain(under.position.iter()).copied().collect::<Vec<_>>();
|
||||
combined_stops.dedup_by(|a, b| (*a - *b).abs() < 1e-6);
|
||||
combined_stops.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
|
||||
let stops = combined_stops
|
||||
.into_iter()
|
||||
.map(|&position| {
|
||||
let over_color = self.evaluate(position);
|
||||
let under_color = under.evaluate(position);
|
||||
let color = blend_fn(over_color, under_color);
|
||||
(position, color)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let stops = combined_stops.into_iter().map(|position| {
|
||||
let over_color = self.evaluate(position);
|
||||
let under_color = under.evaluate(position);
|
||||
let color = blend_fn(over_color, under_color);
|
||||
GradientStop { position, midpoint: 0.5, color }
|
||||
});
|
||||
GradientStops::new(stops)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue