Skip to content

Commit

Permalink
fix: rect loop timing fix, bezpath panic beyond 1.0 border translation
Browse files Browse the repository at this point in the history
0.1 tolerance for path_elements
  • Loading branch information
nixon-voxell committed Aug 9, 2024
1 parent bb7c57a commit 8d1d893
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 84 deletions.
7 changes: 4 additions & 3 deletions examples/hello_world.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ fn render_shapes(mut commands: Commands) {
let mut triangle_path = kurbo::BezPath::new();
triangle_path.move_to(kurbo::Point::new(0.0, -10.0));
triangle_path.line_to(kurbo::Point::new(0.0, 10.0));
triangle_path.line_to(kurbo::Point::new(20.0, 0.0));
triangle_path.line_to(kurbo::Point::new(25.0, 0.0));
triangle_path.close_path();

let triangle = VelloBezPath::new().with_path(triangle_path);
Expand Down Expand Up @@ -55,7 +55,6 @@ fn render_shapes(mut commands: Commands) {
let mut bez_path = kurbo::BezPath::new();
bez_path.move_to((300.0, 100.0));
bez_path.curve_to((200.0, 50.0), (400.0, -50.0), (300.0, -100.0));
bez_path.close_path();

// Bézier Path
let bezier_path = (
Expand All @@ -73,7 +72,9 @@ fn render_shapes(mut commands: Commands) {
}

fn animation(mut q_heads: Query<&mut Head>, time: Res<Time>) {
let factor = time.elapsed_seconds_f64().sin() * 0.5 + 0.5;
// Overshoots for stability check
let mut factor = time.elapsed_seconds_f64() * 0.5;
factor = factor.sin().remap(-1.0, 1.0, -0.2, 1.2);

for mut head in q_heads.iter_mut() {
head.time = factor;
Expand Down
153 changes: 96 additions & 57 deletions src/bezpath.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ impl VelloBezPath {
/// NOTE: each [`kurbo::PathEl`] will be the same at `t == 1.0`
fn inbetween(&self, t: f64) -> ((PathEl, PathEl), f64) {
let elements = self.path.elements();
let index_f = t * (elements.len() - 1) as f64;
let index_f = t.clamp(0.0, 1.0) * (elements.len() - 1) as f64;
let index = index_f as usize;

(
Expand All @@ -61,7 +61,7 @@ impl Default for VelloBezPath {
impl Vector for VelloBezPath {
fn shape(&self) -> impl kurbo::Shape {
let pathels = self.path.elements();
// TODO(perf): Prevent from creating a new BezPath for each animation change.
// TODO(perf): Prevent from creating a new BezPath for every animation update.
let mut path = kurbo::BezPath::new();

let pathel_count = pathels.len();
Expand All @@ -73,9 +73,6 @@ impl Vector for VelloBezPath {
for (path_index, pathel) in pathels.iter().enumerate() {
let mut interp_value = trace_raw - path_index as f64;

// if interp_value <= 0.0 {
// pathels[path_index] = kurbo::PathEl::MoveTo(kurbo::Point::default());
// } else {
if interp_value > 0.0 {
// Clamp value within 1.0
interp_value = f64::min(interp_value, 1.0);
Expand Down Expand Up @@ -121,21 +118,43 @@ impl Vector for VelloBezPath {
}

fn border_translation(&self, time: f64) -> DVec2 {
let (path, t) = self.inbetween(time);
let pathels = self.path.elements();
let pathel_count = pathels.len();

let current = path.0.end_point().unwrap_or_default();
let point = interp_pathel(current, path.1, t)
.end_point()
.unwrap_or(current.lerp(
self.path.elements().first().unwrap().end_point().unwrap(),
t,
))
.to_vec2();

DVec2::new(point.x, point.y)
let fallback = pathels
.first()
.and_then(|path| path.end_point().map(|point| DVec2::new(point.x, point.y)))
.unwrap_or_default();

// Guarantee to have at least 2 path elements
if pathel_count < 2 {
return fallback;
}

let seg_count = pathel_count - 1;
let trace_raw = time * seg_count as f64;
let trace_index = f64::clamp(trace_raw, 0.0, (seg_count - 1) as f64) as usize;
let seg_index = trace_index + 1;

if let Some(segment) = self.path.get_seg(seg_index) {
let t = trace_raw - trace_index as f64;
return match segment {
kurbo::PathSeg::Line(line) => point_to_vec(Point::lerp(line.p0, line.p1, t)),
kurbo::PathSeg::Quad(quad) => {
point_to_vec(lerp_quad_point(quad.p0, quad.p1, quad.p2, t))
}
kurbo::PathSeg::Cubic(cubic) => {
point_to_vec(lerp_cubic_point(cubic.p0, cubic.p1, cubic.p2, cubic.p3, t))
}
};
}

// All else fails..
fallback
}

fn border_rotation(&self, time: f64) -> f64 {
return 0.0;
let (path, t) = self.inbetween(time);

let current = path.0.end_point().unwrap_or_default();
Expand Down Expand Up @@ -166,57 +185,77 @@ impl Vector for VelloBezPath {
let e = b.lerp(c, t);
(e.y - d.y).atan2(e.x - d.x)
}
_ => {
// cant do f64::EPSILON cause of precision issues
let before = interp_pathel(current, path.1, t - f32::EPSILON as f64)
.end_point()
.unwrap();
let next = interp_pathel(current, path.1, t)
.end_point()
.unwrap()
.to_vec2();

(next - before.to_vec2()).angle()
}
}
}
}

/// Interpolate [`kurbo::PathEl`].
fn interp_pathel(p0: kurbo::Point, pathel: kurbo::PathEl, t: f64) -> kurbo::PathEl {
fn interp_pathel(p0: Point, pathel: PathEl, t: f64) -> PathEl {
if t == 1.0 {
return pathel;
}

match pathel {
PathEl::MoveTo(p1) => kurbo::PathEl::MoveTo(kurbo::Point::lerp(p0, p1, t)),
PathEl::LineTo(p1) => kurbo::PathEl::LineTo(kurbo::Point::lerp(p0, p1, t)),
PathEl::QuadTo(p1, p2) => {
// Point between p0 and p1
let x0 = Point::lerp(p0, p1, t);
// Point between p1 and p2
let x1 = Point::lerp(p1, p2, t);
// Point on curve
let end_p = Point::lerp(x0, x1, t);

PathEl::QuadTo(x0, end_p)
}
PathEl::CurveTo(p1, p2, p3) => {
// Point between p0 and p1
let x0 = Point::lerp(p0, p1, t);
// Point between p1 and p2
let x1 = Point::lerp(p1, p2, t);
// Point between p2 and p3
let x2 = Point::lerp(p2, p3, t);
// Point between x0 and x1
let y0 = Point::lerp(x0, x1, t);
// Point between x1 and x2
let y1 = Point::lerp(x1, x2, t);
// Point on curve
let end_p = Point::lerp(y0, y1, t);

PathEl::CurveTo(x0, y0, end_p)
}
PathEl::MoveTo(p1) => PathEl::MoveTo(Point::lerp(p0, p1, t)),
PathEl::LineTo(p1) => PathEl::LineTo(Point::lerp(p0, p1, t)),
PathEl::QuadTo(p1, p2) => lerp_quad_pathel(p0, p1, p2, t),
PathEl::CurveTo(p1, p2, p3) => lerp_cubic_pathel(p0, p1, p2, p3, t),
PathEl::ClosePath => PathEl::ClosePath,
}
}

fn lerp_quad_pathel(p0: Point, p1: Point, p2: Point, t: f64) -> PathEl {
// Point between p0 and p1
let x0 = Point::lerp(p0, p1, t);
// Point between p1 and p2
let x1 = Point::lerp(p1, p2, t);
// Point on curve
let end_p = Point::lerp(x0, x1, t);

PathEl::QuadTo(x0, end_p)
}

fn lerp_quad_point(p0: Point, p1: Point, p2: Point, t: f64) -> Point {
// Point between p0 and p1
let x0 = Point::lerp(p0, p1, t);
// Point between p1 and p2
let x1 = Point::lerp(p1, p2, t);
// Point on curve
Point::lerp(x0, x1, t)
}

fn lerp_cubic_pathel(p0: Point, p1: Point, p2: Point, p3: Point, t: f64) -> PathEl {
// Point between p0 and p1
let x0 = Point::lerp(p0, p1, t);
// Point between p1 and p2
let x1 = Point::lerp(p1, p2, t);
// Point between p2 and p3
let x2 = Point::lerp(p2, p3, t);
// Point between x0 and x1
let y0 = Point::lerp(x0, x1, t);
// Point between x1 and x2
let y1 = Point::lerp(x1, x2, t);
// Point on curve
let end_p = Point::lerp(y0, y1, t);

PathEl::CurveTo(x0, y0, end_p)
}

fn lerp_cubic_point(p0: Point, p1: Point, p2: Point, p3: Point, t: f64) -> Point {
// Point between p0 and p1
let x0 = Point::lerp(p0, p1, t);
// Point between p1 and p2
let x1 = Point::lerp(p1, p2, t);
// Point between p2 and p3
let x2 = Point::lerp(p2, p3, t);
// Point between x0 and x1
let y0 = Point::lerp(x0, x1, t);
// Point between x1 and x2
let y1 = Point::lerp(x1, x2, t);
// Point on curve
Point::lerp(y0, y1, t)
}

fn point_to_vec(point: Point) -> DVec2 {
DVec2::new(point.x, point.y)
}
53 changes: 31 additions & 22 deletions src/rect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,33 +67,42 @@ impl Vector for VelloRect {
kurbo::RoundedRect::new(self.x0(), self.y0(), self.x1(), self.y1(), self.radius)
}

fn border_translation(&self, time: f64) -> DVec2 {
let t = time * 4.0;
let time = t.ceil() as u64;
let t = t % 1.0;

if time > 3 {
DVec2::new(self.x0(), self.y0().lerp(self.y1(), t))
} else if time > 2 {
DVec2::new(self.x1().lerp(self.x0(), t), self.y0())
} else if time > 1 {
DVec2::new(self.x1(), self.y1().lerp(self.y0(), t))
fn border_translation(&self, mut time: f64) -> DVec2 {
// Loop around the rect
if time > 0.0 {
time %= 1.0;
} else {
DVec2::new(self.x0().lerp(self.x1(), t), self.y1())
time = 1.0 - (time.abs() % 1.0);
}
}

fn border_rotation(&self, time: f64) -> f64 {
let time = (time * 4.0).ceil() as u64;
let scaled_time = time * 4.0;
let time_int = scaled_time as u64;
let t = scaled_time % 1.0;

match time_int {
0 => DVec2::new(f64::lerp(self.x0(), self.x1(), t), self.y1()),
1 => DVec2::new(self.x1(), f64::lerp(self.y1(), self.y0(), t)),
2 => DVec2::new(f64::lerp(self.x1(), self.x0(), t), self.y0()),
3.. => DVec2::new(self.x0(), self.y0().lerp(self.y1(), t)),
}
}

if time > 3 {
std::f64::consts::FRAC_PI_2
} else if time > 2 {
std::f64::consts::PI
} else if time > 1 {
3.0 * std::f64::consts::FRAC_PI_2
fn border_rotation(&self, mut time: f64) -> f64 {
// Loop around the rect
if time > 0.0 {
time %= 1.0;
} else {
0.0
time = 1.0 - (time.abs() % 1.0);
}

let scaled_time = time * 4.0;
let time_int = scaled_time as u64;

match time_int {
0 => 0.0,
1 => 3.0 * std::f64::consts::FRAC_PI_2,
2 => std::f64::consts::PI,
3.. => std::f64::consts::FRAC_PI_2,
}
}
}
5 changes: 3 additions & 2 deletions src/vector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,10 @@ pub struct VectorScene;
pub trait Vector {
/// Returns vector graphics that implements [`kurbo::Shape`].
fn shape(&self) -> impl kurbo::Shape;

/// Translation of the border at a specific `time` value.
fn border_translation(&self, time: f64) -> DVec2 {
let path = BezPath::from_iter(self.shape().path_elements(0.0));
let path = BezPath::from_iter(self.shape().path_elements(0.1));

VelloBezPath::default()
.with_path(path)
Expand All @@ -64,7 +65,7 @@ pub trait Vector {

/// The rotation at the tangent of the border at a specific `time` value.
fn border_rotation(&self, time: f64) -> f64 {
let path = BezPath::from_iter(self.shape().path_elements(0.0));
let path = BezPath::from_iter(self.shape().path_elements(0.1));

VelloBezPath::default()
.with_path(path)
Expand Down

0 comments on commit 8d1d893

Please sign in to comment.