Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes

// Send the overlays message to the overlays message handler
self.overlays_message_handler
.process_message(message, responses, OverlaysMessageContext { visibility_settings, viewport });
.process_message(message, responses, OverlaysMessageContext { visibility_settings, viewport, animation_time: ipp.time as f64 });
}
DocumentMessage::PropertiesPanel(message) => {
let context = PropertiesPanelMessageContext {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,8 @@ fn apply_usvg_stroke(stroke: &usvg::Stroke, modify_inputs: &mut ModifyInputsCont
if let usvg::Paint::Color(color) = &stroke.paint() {
modify_inputs.stroke_set(Stroke {
color: Some(usvg_color(*color, stroke.opacity().get())),
//Added the gradient field to the Stroke struct
gradient: None,
weight: stroke.width().get() as f64,
dash_lengths: stroke.dasharray().as_ref().map(|lengths| lengths.iter().map(|&length| length as f64).collect()).unwrap_or_default(),
dash_offset: stroke.dashoffset() as f64,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use crate::messages::prelude::*;
pub struct OverlaysMessageContext<'a> {
pub visibility_settings: OverlaysVisibilitySettings,
pub viewport: &'a ViewportMessageHandler,
/// Current time in milliseconds passed from the input preprocessor, used to drive overlay animations (e.g. marching ants).
pub animation_time: f64,
}

#[derive(Debug, Clone, Default, ExtractField)]
Expand All @@ -19,7 +21,7 @@ pub struct OverlaysMessageHandler {
#[message_handler_data]
impl MessageHandler<OverlaysMessage, OverlaysMessageContext<'_>> for OverlaysMessageHandler {
fn process_message(&mut self, message: OverlaysMessage, responses: &mut VecDeque<Message>, context: OverlaysMessageContext) {
let OverlaysMessageContext { visibility_settings, viewport, .. } = context;
let OverlaysMessageContext { visibility_settings, viewport, animation_time } = context;

match message {
#[cfg(target_family = "wasm")]
Expand Down Expand Up @@ -55,13 +57,15 @@ impl MessageHandler<OverlaysMessage, OverlaysMessageContext<'_>> for OverlaysMes
render_context: canvas_context.clone(),
visibility_settings: visibility_settings.clone(),
viewport: *viewport,
animation_time,
},
});
for provider in &self.overlay_providers {
responses.add(provider(OverlayContext {
render_context: canvas_context.clone(),
visibility_settings: visibility_settings.clone(),
viewport: *viewport,
animation_time,
}));
}
}
Expand All @@ -70,7 +74,7 @@ impl MessageHandler<OverlaysMessage, OverlaysMessageContext<'_>> for OverlaysMes
OverlaysMessage::Draw => {
use super::utility_types::OverlayContext;

let overlay_context = OverlayContext::new(*viewport, visibility_settings);
let overlay_context = OverlayContext::new(*viewport, visibility_settings, animation_time);

if visibility_settings.all() {
responses.add(DocumentMessage::GridOverlays { context: overlay_context.clone() });
Expand All @@ -83,7 +87,7 @@ impl MessageHandler<OverlaysMessage, OverlaysMessageContext<'_>> for OverlaysMes
}
#[cfg(all(not(target_family = "wasm"), test))]
OverlaysMessage::Draw => {
let _ = (responses, visibility_settings, viewport);
let _ = (responses, visibility_settings, viewport, animation_time);
}
OverlaysMessage::AddProvider { provider: message } => {
self.overlay_providers.insert(message);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ pub struct OverlayContext {
internal: Arc<Mutex<OverlayContextInternal>>,
pub viewport: ViewportMessageHandler,
pub visibility_settings: OverlaysVisibilitySettings,
/// Current time in milliseconds, used to animate effects like marching ants.
pub animation_time: f64,
}

impl Clone for OverlayContext {
Expand All @@ -181,6 +183,7 @@ impl Clone for OverlayContext {
internal: self.internal.clone(),
viewport: self.viewport,
visibility_settings,
animation_time: self.animation_time,
}
}
}
Expand All @@ -198,6 +201,7 @@ impl std::fmt::Debug for OverlayContext {
.field("scene", &"Scene { ... }")
.field("viewport", &self.viewport)
.field("visibility_settings", &self.visibility_settings)
.field("animation_time", &self.animation_time)
.finish()
}
}
Expand All @@ -209,6 +213,7 @@ impl Default for OverlayContext {
internal: Mutex::new(OverlayContextInternal::default()).into(),
viewport: ViewportMessageHandler::default(),
visibility_settings: OverlaysVisibilitySettings::default(),
animation_time: 0.,
}
}
}
Expand All @@ -220,7 +225,7 @@ impl core::hash::Hash for OverlayContext {

impl OverlayContext {
#[allow(dead_code)]
pub(super) fn new(viewport: ViewportMessageHandler, visibility_settings: OverlaysVisibilitySettings) -> Self {
pub(super) fn new(viewport: ViewportMessageHandler, visibility_settings: OverlaysVisibilitySettings, animation_time: f64) -> Self {
Self {
internal: Arc::new(Mutex::new(OverlayContextInternal::new(viewport, visibility_settings))),
viewport,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ pub struct OverlayContext {
pub render_context: web_sys::CanvasRenderingContext2d,
pub viewport: ViewportMessageHandler,
pub visibility_settings: OverlaysVisibilitySettings,
/// Current time in milliseconds (e.g. from `js_sys::Date::now()`), used to animate effects like marching ants.
pub animation_time: f64,
}
// Message hashing isn't used but is required by the message system macros
impl core::hash::Hash for OverlayContext {
Expand Down
41 changes: 38 additions & 3 deletions editor/src/messages/tool/tool_messages/select_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,8 @@ struct SelectToolData {
snap_candidates: Vec<SnapCandidatePoint>,
auto_panning: AutoPanning,
drag_start_center: ViewportPosition,
/// Whether the tool is currently subscribed to animation frame events to drive the marching ants animation.
marching_ants_subscribed: bool,
}

impl SelectToolData {
Expand All @@ -421,6 +423,27 @@ impl SelectToolData {
}
}
}
/// Subscribe to per-frame animation ticks so the marching ants selection border animates continuously.
fn start_marching_ants(&mut self, responses: &mut VecDeque<Message>) {
if !self.marching_ants_subscribed {
self.marching_ants_subscribed = true;
responses.add(BroadcastMessage::SubscribeEvent {
on: EventMessage::AnimationFrame,
send: Box::new(OverlaysMessage::Draw.into()),
});
}
}

/// Unsubscribe from per-frame animation ticks when the selection box is no longer being drawn.
fn stop_marching_ants(&mut self, responses: &mut VecDeque<Message>) {
if self.marching_ants_subscribed {
self.marching_ants_subscribed = false;
responses.add(BroadcastMessage::UnsubscribeEvent {
on: EventMessage::AnimationFrame,
send: Box::new(OverlaysMessage::Draw.into()),
});
}
}

pub fn selection_quad(&self) -> Quad {
let bbox = self.selection_box();
Expand Down Expand Up @@ -965,10 +988,17 @@ impl Fsm for SelectToolFsmState {
let fill_color = Some(COLOR_OVERLAY_BLUE_05);

let polygon = &tool_data.lasso_polygon;
// Animate the dash offset to produce the "marching ants" effect. The dash pattern repeats every 8 px (4 px dash + 4 px gap),
// so wrapping the time to [0, 8) via the modulo gives a smooth, continuously looping animation.
// MARCHING_ANTS_PIXELS_PER_SECOND controls how fast the dashes march around the selection border.
const MARCHING_ANTS_PIXELS_PER_SECOND: f64 = 100.; // How many pixels the pattern advances per second
const MARCHING_ANTS_PERIOD: f64 = 8.; // One full cycle = dash length (4 px) + gap length (4 px)
let marching_ants_offset = (overlay_context.animation_time / 1000. * MARCHING_ANTS_PIXELS_PER_SECOND) % MARCHING_ANTS_PERIOD;


match (selection_shape, current_selection_mode) {
(SelectionShapeType::Box, SelectionMode::Enclosed) => overlay_context.dashed_quad(quad, None, fill_color, Some(4.), Some(4.), Some(0.5)),
(SelectionShapeType::Lasso, SelectionMode::Enclosed) => overlay_context.dashed_polygon(polygon, None, fill_color, Some(4.), Some(4.), Some(0.5)),
(SelectionShapeType::Box, SelectionMode::Enclosed) => overlay_context.dashed_quad(quad, None, fill_color, Some(4.), Some(4.), Some(marching_ants_offset)),
(SelectionShapeType::Lasso, SelectionMode::Enclosed) => overlay_context.dashed_polygon(polygon, None, fill_color, Some(4.), Some(4.), Some(marching_ants_offset)),
(SelectionShapeType::Box, _) => overlay_context.quad(quad, None, fill_color),
(SelectionShapeType::Lasso, _) => overlay_context.polygon(polygon, None, fill_color),
}
Expand Down Expand Up @@ -1125,6 +1155,8 @@ impl Fsm for SelectToolFsmState {
}
} else {
let selection_shape = if input.keyboard.key(lasso_select) { SelectionShapeType::Lasso } else { SelectionShapeType::Box };
// Subscribe to animation frames so the marching ants selection border animates continuously.
tool_data.start_marching_ants(responses);
SelectToolFsmState::Drawing { selection_shape, has_drawn: false }
}
};
Expand Down Expand Up @@ -1556,7 +1588,8 @@ impl Fsm for SelectToolFsmState {
}

tool_data.lasso_polygon.clear();

// Unsubscribe from animation frames now that the selection box is finalized.
tool_data.stop_marching_ants(responses);
responses.add(OverlaysMessage::Draw);

let selection = tool_data.nested_selection_behavior;
Expand Down Expand Up @@ -1603,6 +1636,8 @@ impl Fsm for SelectToolFsmState {
responses.add(DocumentMessage::AbortTransaction);
tool_data.snap_manager.cleanup(responses);
tool_data.lasso_polygon.clear();
// Unsubscribe from marching ants animation in case we were in Drawing state.
tool_data.stop_marching_ants(responses);
responses.add(OverlaysMessage::Draw);

let selection = tool_data.nested_selection_behavior;
Expand Down
20 changes: 15 additions & 5 deletions node-graph/libraries/rendering/src/render_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,23 @@ impl RenderExt for Stroke {
render_params: &RenderParams,
) -> Self::Output {
// Don't render a stroke at all if it would be invisible
let Some(color) = self.color else { return String::new() };
if !self.has_renderable_stroke() {
return String::new();
}
let paint = match (&self.gradient, self.color) {
(Some(gradient), _) => {
let gradient_id = gradient.render(_svg_defs, _element_transform, _stroke_transform, _bounds, _transformed_bounds, render_params);
format!(r##" stroke="url('#{gradient_id}')""##)
}
(_, Some(color)) => {
let mut result = format!(r##" stroke="#{}""##, color.to_rgb_hex_srgb_from_gamma());
if color.a() < 1. {
let _ = write!(result, r#" stroke-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
}
result
}
_ => return String::new(),
};

let default_weight = if self.align != StrokeAlign::Center && render_params.aligned_strokes { 1. / 2. } else { 1. };

Expand All @@ -125,10 +138,7 @@ impl RenderExt for Stroke {
let paint_order = (self.paint_order != PaintOrder::StrokeAbove || render_params.override_paint_order).then_some(PaintOrder::StrokeBelow);

// Render the needed stroke attributes
let mut attributes = format!(r##" stroke="#{}""##, color.to_rgb_hex_srgb_from_gamma());
if color.a() < 1. {
let _ = write!(&mut attributes, r#" stroke-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
}
let mut attributes = paint;
if let Some(mut weight) = weight {
if stroke_align.is_some() && render_params.aligned_strokes {
weight *= 2.;
Expand Down
84 changes: 70 additions & 14 deletions node-graph/libraries/rendering/src/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1115,34 +1115,90 @@ impl Render for Table<Vector> {
};

let do_stroke = |scene: &mut Scene, width_scale: f64| {
if let Some(stroke) = row.element.style.stroke() {
let color = match stroke.color {
Some(color) => peniko::Color::new([color.r(), color.g(), color.b(), color.a()]),
None => peniko::Color::TRANSPARENT,
};
let cap = match stroke.cap {
if let Some(stroke_style) = row.element.style.stroke() {
let cap = match stroke_style.cap {
StrokeCap::Butt => Cap::Butt,
StrokeCap::Round => Cap::Round,
StrokeCap::Square => Cap::Square,
};
let join = match stroke.join {
let join = match stroke_style.join {
StrokeJoin::Miter => Join::Miter,
StrokeJoin::Bevel => Join::Bevel,
StrokeJoin::Round => Join::Round,
};
let dash_pattern = stroke.dash_lengths.iter().map(|l| l.max(0.)).collect();
let stroke = kurbo::Stroke {
width: stroke.weight * width_scale,
miter_limit: stroke.join_miter_limit,
let dash_pattern = stroke_style.dash_lengths.iter().map(|l| l.max(0.)).collect();
let kurbo_stroke = kurbo::Stroke {
width: stroke_style.weight * width_scale,
miter_limit: stroke_style.join_miter_limit,
join,
start_cap: cap,
end_cap: cap,
dash_pattern,
dash_offset: stroke.dash_offset,
dash_offset: stroke_style.dash_offset,
};

if stroke.width > 0. {
scene.stroke(&stroke, kurbo::Affine::new(element_transform.to_cols_array()), color, None, &path);
if kurbo_stroke.width > 0. {
let (brush, brush_transform) = if let Some(gradient) = stroke_style.gradient.as_ref() {
let mut stops = peniko::ColorStops::new();
for (position, color, _) in gradient.stops.interpolated_samples() {
stops.push(peniko::ColorStop {
offset: position as f32,
color: peniko::color::DynamicColor::from_alpha_color(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])),
});
}

let bounds = row.element.nonzero_bounding_box();
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);

let inverse_parent_transform = if parent_transform.matrix2.determinant() != 0. {
parent_transform.inverse()
} else {
Default::default()
};
let mod_points = inverse_parent_transform * multiplied_transform * bound_transform;

let start = mod_points.transform_point2(gradient.start);
let end = mod_points.transform_point2(gradient.end);

let brush = peniko::Brush::Gradient(peniko::Gradient {
kind: match gradient.gradient_type {
GradientType::Linear => peniko::LinearGradientPosition {
start: to_point(start),
end: to_point(end),
}
.into(),
GradientType::Radial => {
let radius = start.distance(end);
peniko::RadialGradientPosition {
start_center: to_point(start),
start_radius: 0.,
end_center: to_point(start),
end_radius: radius as f32,
}
.into()
}
},
stops,
interpolation_alpha_space: peniko::InterpolationAlphaSpace::Premultiplied,
..Default::default()
});
let inverse_element_transform = if element_transform.matrix2.determinant() != 0. {
element_transform.inverse()
} else {
Default::default()
};
let brush_transform = kurbo::Affine::new((inverse_element_transform * parent_transform).to_cols_array());

(brush, Some(brush_transform))
} else {
let color = stroke_style
.color
.map(|color| peniko::Color::new([color.r(), color.g(), color.b(), color.a()]))
.unwrap_or(peniko::Color::TRANSPARENT);
(peniko::Brush::Solid(color), None)
};

scene.stroke(&kurbo_stroke, kurbo::Affine::new(element_transform.to_cols_array()), &brush, brush_transform, &path);
}
}
};
Expand Down
Loading