ellipse.rs (6061B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 use api::units::*; 6 use euclid::Size2D; 7 use std::f32::consts::FRAC_PI_2; 8 9 10 /// Number of steps to integrate arc length over. 11 const STEP_COUNT: usize = 20; 12 13 /// Represents an ellipse centred at a local space origin. 14 #[derive(Debug, Clone)] 15 pub struct Ellipse<U> { 16 pub radius: Size2D<f32, U>, 17 pub total_arc_length: f32, 18 } 19 20 impl<U> Ellipse<U> { 21 pub fn new(radius: Size2D<f32, U>) -> Ellipse<U> { 22 // Approximate the total length of the first quadrant of this ellipse. 23 let total_arc_length = get_simpson_length(FRAC_PI_2, radius.width, radius.height); 24 25 Ellipse { 26 radius, 27 total_arc_length, 28 } 29 } 30 31 /// Binary search to estimate the angle of an ellipse 32 /// for a given arc length. This only searches over the 33 /// first quadrant of an ellipse. 34 pub fn find_angle_for_arc_length(&self, arc_length: f32) -> f32 { 35 // Clamp arc length to [0, pi]. 36 let arc_length = arc_length.max(0.0).min(self.total_arc_length); 37 38 let epsilon = 0.01; 39 let mut low = 0.0; 40 let mut high = FRAC_PI_2; 41 let mut theta = 0.0; 42 let mut new_low = 0.0; 43 let mut new_high = FRAC_PI_2; 44 45 while low <= high { 46 theta = 0.5 * (low + high); 47 let length = get_simpson_length(theta, self.radius.width, self.radius.height); 48 49 if (length - arc_length).abs() < epsilon { 50 break; 51 } else if length < arc_length { 52 new_low = theta; 53 } else { 54 new_high = theta; 55 } 56 57 // If we have stopped moving down the arc, the answer that we have is as good as 58 // it is going to get. We break to avoid going into an infinite loop. 59 if new_low == low && new_high == high { 60 break; 61 } 62 63 high = new_high; 64 low = new_low; 65 } 66 67 theta 68 } 69 70 /// Get a point and tangent on this ellipse from a given angle. 71 /// This only works for the first quadrant of the ellipse. 72 pub fn get_point_and_tangent(&self, theta: f32) -> (LayoutPoint, LayoutPoint) { 73 let (sin_theta, cos_theta) = theta.sin_cos(); 74 let point = LayoutPoint::new( 75 self.radius.width * cos_theta, 76 self.radius.height * sin_theta, 77 ); 78 let tangent = LayoutPoint::new( 79 -self.radius.width * sin_theta, 80 self.radius.height * cos_theta, 81 ); 82 (point, tangent) 83 } 84 85 pub fn contains(&self, point: LayoutPoint) -> bool { 86 self.signed_distance(point.to_vector()) <= 0.0 87 } 88 89 /// Find the signed distance from this ellipse given a point. 90 /// Taken from http://www.iquilezles.org/www/articles/ellipsedist/ellipsedist.htm 91 fn signed_distance(&self, point: LayoutVector2D) -> f32 { 92 // This algorithm fails for circles, so we handle them here. 93 if self.radius.width == self.radius.height { 94 return point.length() - self.radius.width; 95 } 96 97 let mut p = LayoutVector2D::new(point.x.abs(), point.y.abs()); 98 let mut ab = self.radius.to_vector(); 99 if p.x > p.y { 100 p = p.yx(); 101 ab = ab.yx(); 102 } 103 104 let l = ab.y * ab.y - ab.x * ab.x; 105 106 let m = ab.x * p.x / l; 107 let n = ab.y * p.y / l; 108 let m2 = m * m; 109 let n2 = n * n; 110 111 let c = (m2 + n2 - 1.0) / 3.0; 112 let c3 = c * c * c; 113 114 let q = c3 + m2 * n2 * 2.0; 115 let d = c3 + m2 * n2; 116 let g = m + m * n2; 117 118 let co = if d < 0.0 { 119 let p = (q / c3).acos() / 3.0; 120 let s = p.cos(); 121 let t = p.sin() * (3.0_f32).sqrt(); 122 let rx = (-c * (s + t + 2.0) + m2).sqrt(); 123 let ry = (-c * (s - t + 2.0) + m2).sqrt(); 124 (ry + l.signum() * rx + g.abs() / (rx * ry) - m) / 2.0 125 } else { 126 let h = 2.0 * m * n * d.sqrt(); 127 let s = (q + h).signum() * (q + h).abs().powf(1.0 / 3.0); 128 let u = (q - h).signum() * (q - h).abs().powf(1.0 / 3.0); 129 let rx = -s - u - c * 4.0 + 2.0 * m2; 130 let ry = (s - u) * (3.0_f32).sqrt(); 131 let rm = (rx * rx + ry * ry).sqrt(); 132 let p = ry / (rm - rx).sqrt(); 133 (p + 2.0 * g / rm - m) / 2.0 134 }; 135 136 let si = (1.0 - co * co).sqrt(); 137 let r = LayoutVector2D::new(ab.x * co, ab.y * si); 138 (r - p).length() * (p.y - r.y).signum() 139 } 140 } 141 142 /// Use Simpsons rule to approximate the arc length of 143 /// part of an ellipse. Note that this only works over 144 /// the range of [0, pi/2]. 145 // TODO(gw): This is a simplistic way to estimate the 146 // arc length of an ellipse segment. We can probably use 147 // a faster / more accurate method! 148 fn get_simpson_length(theta: f32, rx: f32, ry: f32) -> f32 { 149 let df = theta / STEP_COUNT as f32; 150 let mut sum = 0.0; 151 152 for i in 0 .. (STEP_COUNT + 1) { 153 let (sin_theta, cos_theta) = (i as f32 * df).sin_cos(); 154 let a = rx * sin_theta; 155 let b = ry * cos_theta; 156 let y = (a * a + b * b).sqrt(); 157 let q = if i == 0 || i == STEP_COUNT { 158 1.0 159 } else if i % 2 == 0 { 160 2.0 161 } else { 162 4.0 163 }; 164 165 sum += q * y; 166 } 167 168 (df / 3.0) * sum 169 } 170 171 #[cfg(test)] 172 pub mod test { 173 use super::*; 174 175 #[test] 176 fn find_angle_for_arc_length_for_long_eclipse() { 177 // Ensure that finding the angle on giant ellipses produces and answer and 178 // doesn't send us into an infinite loop. 179 let ellipse = Ellipse::new(LayoutSize::new(57500.0, 25.0)); 180 let _ = ellipse.find_angle_for_arc_length(55674.53); 181 assert!(true); 182 183 let ellipse = Ellipse::new(LayoutSize::new(25.0, 57500.0)); 184 let _ = ellipse.find_angle_for_arc_length(55674.53); 185 assert!(true); 186 } 187 }