Skip to content
Merged
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
200 changes: 189 additions & 11 deletions crates/rustmotion-components/src/arrow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,19 +175,28 @@ impl Arrow {
}
}

if props.draw_progress >= 0.0 && props.draw_progress < 1.0 {
let mut measure = PathMeasure::new(&path, false, None);
let total_len = measure.length();
if total_len > 0.0 {
let draw_len = total_len * props.draw_progress.clamp(0.0, 1.0);
let intervals = [draw_len, total_len - draw_len + 0.01];
if let Some(dash) = skia_safe::PathEffect::dash(&intervals, 0.0) {
paint.set_path_effect(dash);
}
}
let drawing = props.draw_progress >= 0.0 && props.draw_progress < 1.0;
let draw_start = props.draw_start.max(0.0);
let draw_offset = props.draw_offset;
let trimming = draw_start > 0.0 || draw_offset.abs() > 0.0005;
let trim_start = (draw_start + draw_offset).clamp(0.0, 1.0);
let trim_end = if drawing {
(props.draw_progress.clamp(0.0, 1.0) + draw_offset).clamp(0.0, 1.0)
} else {
(1.0 + draw_offset).clamp(0.0, 1.0)
};

if trim_end <= trim_start && (drawing || trimming) {
return;
}

canvas.draw_path(&path, &paint);
if drawing || trimming {
let trimmed =
rustmotion_core::engine::renderer::trim_path_between(&path, trim_start, trim_end);
canvas.draw_path(&trimmed, &paint);
} else {
canvas.draw_path(&path, &paint);
}

let show_arrows = props.draw_progress < 0.0 || props.draw_progress >= 0.95;
if show_arrows {
Expand All @@ -212,3 +221,172 @@ impl Painter for Arrow {
self.paint(canvas, props);
}
}

#[cfg(test)]
mod tests {
use super::*;
use rustmotion_core::engine::layout_pass::Insets;

const W: i32 = 100;
const H: i32 = 100;

fn test_layout() -> BoxLayout {
BoxLayout {
x: 0.0,
y: 0.0,
width: W as f32,
height: H as f32,
border: Insets::default(),
padding: Insets::default(),
}
}

fn test_ctx() -> PaintCtx {
PaintCtx {
time: 0.0,
scenario_time: 0.0,
scene_duration: 1.0,
frame_index: 0,
fps: 30,
video_width: W as u32,
video_height: H as u32,
stagger_offset: 0.0,
}
}

fn straight_arrow() -> Arrow {
Arrow {
x1: 10.0,
y1: 50.0,
x2: 90.0,
y2: 50.0,
cp: None,
cp1: None,
cp2: None,
curve: None,
width: 12.0,
color: "#FFFFFF".to_string(),
arrow_end: true,
arrow_start: false,
arrow_size: default_arrow_size(),
dashed: None,
timing: Default::default(),
style: Default::default(),
timeline: Vec::new(),
stagger: None,
}
}

fn render_alpha(arrow: &Arrow, props: &AnimatedProperties) -> Vec<u8> {
let mut surface = skia_safe::surfaces::raster_n32_premul((W, H)).expect("raster surface");
arrow.paint_content(surface.canvas(), &test_layout(), props, &test_ctx());
let snapshot = surface.image_snapshot();
let info = skia_safe::ImageInfo::new(
(W, H),
skia_safe::ColorType::RGBA8888,
skia_safe::AlphaType::Unpremul,
None,
);
let mut buf = vec![0u8; (W * H * 4) as usize];
let ok = snapshot.read_pixels(
&info,
&mut buf,
(W * 4) as usize,
skia_safe::IPoint::new(0, 0),
skia_safe::image::CachingHint::Disallow,
);
assert!(ok, "pixel read should succeed");
buf
}

fn lit_pixel_count(buf: &[u8]) -> usize {
(0..(W * H) as usize)
.filter(|&i| buf[i * 4 + 3] > 10)
.count()
}

fn alpha_at(buf: &[u8], x: i32, y: i32) -> u8 {
buf[((y * W + x) * 4 + 3) as usize]
}

#[test]
fn draw_progress_zero_paints_nothing_not_even_a_dot() {
let arrow = straight_arrow();
let props = AnimatedProperties {
draw_progress: 0.0,
..AnimatedProperties::default()
};
let lit = lit_pixel_count(&render_alpha(&arrow, &props));
assert_eq!(
lit, 0,
"at draw_progress=0 a round-capped zero-length dash must not paint a solid dot, \
got {lit} lit pixels"
);
}

#[test]
fn draw_progress_partial_paints_a_partial_stroke() {
let arrow = straight_arrow();
let props = AnimatedProperties {
draw_progress: 0.5,
..AnimatedProperties::default()
};
let lit = lit_pixel_count(&render_alpha(&arrow, &props));
assert!(
lit > 0,
"at draw_progress=0.5 the arrow must paint a partial stroke, got {lit} lit pixels"
);
}

#[test]
fn draw_start_erases_the_head_of_an_already_finished_arrow() {
let arrow = straight_arrow();
let props = AnimatedProperties {
draw_start: 0.5,
..AnimatedProperties::default()
};
let buf = render_alpha(&arrow, &props);
assert_eq!(
alpha_at(&buf, 20, 50),
0,
"before a keyframe-driven draw_start=0.5 on a finished arrow must be erased"
);
assert!(
alpha_at(&buf, 60, 50) > 40,
"after draw_start=0.5 must stay painted"
);
}

#[test]
fn draw_offset_marches_the_drawn_window_along_the_path() {
let arrow = straight_arrow();
let no_offset = render_alpha(
&arrow,
&AnimatedProperties {
draw_progress: 0.3,
..AnimatedProperties::default()
},
);
let offset = render_alpha(
&arrow,
&AnimatedProperties {
draw_progress: 0.3,
draw_offset: 0.5,
..AnimatedProperties::default()
},
);
assert!(
alpha_at(&no_offset, 20, 50) > 40,
"with no offset the window starts at the path's own beginning"
);
assert_eq!(
alpha_at(&offset, 20, 50),
0,
"draw_offset=0.5 must march the window forward, leaving the path's start empty"
);
assert!(
alpha_at(&offset, 70, 50) > 40,
"and paint further along the path instead"
);
}
}
Loading
Loading