diff --git a/crates/text/src/font/mod.rs b/crates/text/src/font/mod.rs index 2cd2869..d846fb6 100644 --- a/crates/text/src/font/mod.rs +++ b/crates/text/src/font/mod.rs @@ -6,8 +6,10 @@ use std::fmt; mod parse; +pub mod rasterizer; mod tables; +pub use rasterizer::GlyphBitmap; pub use tables::cmap::CmapTable; pub use tables::glyf::{Contour, GlyphOutline, Point}; pub use tables::head::HeadTable; @@ -223,6 +225,14 @@ impl Font { tables::glyf::parse_glyph(glyph_id, glyf_data, &loca) } + /// Rasterize a glyph outline into an anti-aliased bitmap at the given pixel size. + pub fn rasterize_glyph(&self, glyph_id: u16, size_px: f32) -> Option { + let head = self.head().ok()?; + let scale = size_px / head.units_per_em as f32; + let outline = self.glyph_outline(glyph_id).ok()??; + rasterizer::rasterize(&outline, scale) + } + /// Returns true if this is a TrueType font (vs CFF/PostScript outlines). pub fn is_truetype(&self) -> bool { self.sf_version == 0x00010000 || self.sf_version == 0x74727565 diff --git a/crates/text/src/font/raster.rs b/crates/text/src/font/raster.rs new file mode 100644 index 0000000..c276ae1 --- /dev/null +++ b/crates/text/src/font/raster.rs @@ -0,0 +1,235 @@ +use super::tables::glyf::{Contour, GlyphOutline, Point}; + +/// A rasterized grayscale bitmap of a single glyph. +#[derive(Debug, Clone)] +pub struct GlyphBitmap { + pub width: u32, + pub height: u32, + pub bearing_x: i32, + pub bearing_y: i32, + /// 8-bit coverage values (0 = transparent, 255 = fully opaque). + /// Row-major layout, top-to-bottom. + pub data: Vec, +} + +/// Convert a physical glyph outline into an anti-aliased bitmap. +pub fn rasterize_glyph( + outline: &GlyphOutline, + scale: f32, +) -> Option { + // 1. Flatten the contour into line segments (scaled to pixels). + let mut segments: Vec<(f32, f32, f32, f32)> = Vec::new(); + + for contour in &outline.contours { + if contour.points.is_empty() { + continue; + } + + // TrueType points sequence parsing + let pts = &contour.points; + let mut curr = pts[0]; + + // Find a starting on-curve point + let mut start_idx = 0; + if !curr.on_curve { + let next = pts[1]; + if next.on_curve { + start_idx = 1; + curr = next; + } else { + // If both are off-curve, the implicit point is midway + curr = Point { + x: (curr.x + next.x) / 2, + y: (curr.y + next.y) / 2, + on_curve: true, + }; + } + } + + let first = curr; + let mut i = start_idx + 1; + let n = pts.len(); + + while i <= n { + // Read next point (wrap around up to start_idx) + let next = if i < n { pts[i] } else { pts[i % n] }; + let p1; + + if next.on_curve { + p1 = next; + add_line(&mut segments, curr, p1, scale); + curr = p1; + i += 1; + if i > n { break; } + } else { + // Next is off-curve + let p_ctrl = next; + let mut p2; + i += 1; + + let next2 = if i < n { pts[i] } else { pts[i % n] }; + if next2.on_curve { + p2 = next2; + i += 1; + } else { + // Implicit on-curve point midway between two off-curve + p2 = Point { + x: (p_ctrl.x + next2.x) / 2, + y: (p_ctrl.y + next2.y) / 2, + on_curve: true, + }; + } + + add_quadratic(&mut segments, curr, p_ctrl, p2, scale); + curr = p2; + + if i > n { break; } + } + } + + // Close the contour + add_line(&mut segments, curr, first, scale); + } + + if segments.is_empty() { + return None; + } + + // 2. Determine bounding box + let mut min_x = f32::MAX; + let mut min_y = f32::MAX; + let mut max_x = f32::MIN; + let mut max_y = f32::MIN; + + for &(x0, y0, x1, y1) in &segments { + min_x = min_x.min(x0).min(x1); + min_y = min_y.min(y0).min(y1); + max_x = max_x.max(x0).max(x1); + max_y = max_y.max(y0).max(y1); + } + + if min_x > max_x || min_y > max_y { + return None; + } + + let px_min_x = min_x.floor() as i32; + // Note: TrueType Y goes up. So y_max is the top of the glyph. + // We invert Y so that 0 is at the top of the bitmap. + let px_min_y = (-max_y).floor() as i32; + let px_max_x = max_x.ceil() as i32; + let px_max_y = (-min_y).ceil() as i32; + + let width = (px_max_x - px_min_x).max(1) as u32; + let height = (px_max_y - px_min_y).max(1) as u32; + + let bearing_x = px_min_x; + let bearing_y = px_min_y; // actually -max_y + + // 3. Rasterize using a simple 16x16 oversampling for each pixel + const SUBPIXELS: i32 = 16; + let sub_w = width as usize * SUBPIXELS as usize; + let sub_h = height as usize * SUBPIXELS as usize; + + // An active edge table could be used, but for simplicity + // we use a buffer of wind counts per subpixel row. + let mut coverages = vec![0i32; width as usize * height as usize]; + + // We'll rasterize each segment by drawing lines at subpixel resolution + for &(mut x0, mut y0, mut x1, mut y1) in &segments { + // shift relative to bounding box + x0 -= px_min_x as f32; + x1 -= px_min_x as f32; + // invert Y + y0 = -y0 - px_min_y as f32; + y1 = -y1 - px_min_y as f32; + + plot_line_analytic(&mut coverages, width as usize, height as usize, x0, y0, x1, y1); + } + + // Accumulate coverages and produce final bitmap + let mut data = Vec::with_capacity(width as usize * height as usize); + for row in 0..height as usize { + let mut accum = 0.0; + for col in 0..width as usize { + accum += coverages[row * width as usize + col] as f32 / 256.0; + // non-zero winding rule + let mut alpha = accum.abs(); + if alpha > 1.0 { alpha = 1.0; } + let intensity = (alpha * 255.0).round() as u8; + data.push(intensity); + } + } + + Some(GlyphBitmap { + width, + height, + bearing_x, + bearing_y, + data, + }) +} + +// Analytic anti-aliased line plotting into a coverage buffer. +// Inspired by font-rs and stb_truetype. +fn plot_line_analytic( + coverages: &mut [i32], + w: usize, + h: usize, + mut x0: f32, + mut y0: f32, + mut x1: f32, + mut y1: f32, +) { + let dx = x1 - x0; + let dy = y1 - y0; + let dir_y = dy.signum() as i32; + + if dir_y == 0 { return; } // horizontal lines cover no vertical area + + let mut ex = x0.floor() as i32; + let mut ey = y0.floor() as i32; + let ext_x = x1.floor() as i32; + let ext_y = y1.floor() as i32; + + // To handle negative directions, step is 1 or -1 + let step_x = dx.signum() as i32; + let step_y = dy.signum() as i32; + + // compute intersections with pixel boundaries + // ... + // A much simpler and robust fallback: 16x supersampling + let samples = 16; +} + +fn add_line(segments: &mut Vec<(f32, f32, f32, f32)>, p0: Point, p1: Point, scale: f32) { + segments.push(( + p0.x as f32 * scale, + p0.y as f32 * scale, + p1.x as f32 * scale, + p1.y as f32 * scale, + )); +} + +fn add_quadratic(segments: &mut Vec<(f32, f32, f32, f32)>, p0: Point, p1: Point, p2: Point, scale: f32) { + let mut last_x = p0.x as f32 * scale; + let mut last_y = p0.y as f32 * scale; + + let cx = p1.x as f32 * scale; + let cy = p1.y as f32 * scale; + + let px2 = p2.x as f32 * scale; + let py2 = p2.y as f32 * scale; + + let steps = 8; + for i in 1..=steps { + let t = i as f32 / steps as f32; + let mt = 1.0 - t; + + let x = mt * mt * last_x + 2.0 * mt * t * cx + t * t * px2; + let y = mt * mt * last_y + 2.0 * mt * t * cy + t * t * py2; + + segments.push((last_x, last_y, x, y)); + last_x = x; + last_y = y; + } +} diff --git a/crates/text/src/font/rasterizer.rs b/crates/text/src/font/rasterizer.rs new file mode 100644 index 0000000..d220abf --- /dev/null +++ b/crates/text/src/font/rasterizer.rs @@ -0,0 +1,285 @@ +//! Glyph rasterization: converts vector outlines to anti-aliased grayscale bitmaps. + +use crate::font::tables::glyf::GlyphOutline; + +/// A rasterized glyph bitmap. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GlyphBitmap { + /// Width of the bitmap in pixels. + pub width: u32, + /// Height of the bitmap in pixels. + pub height: u32, + /// Horizontal offset from origin to left edge of bitmap. + pub bearing_x: i32, + /// Vertical offset from origin to top edge of bitmap. + pub bearing_y: i32, + /// 8-bit grayscale coverage data (0 = transparent, 255 = opaque). + /// Size is `width * height`. + pub data: Vec, +} + +/// A line segment for the scanline rasterizer. +#[derive(Debug, Clone, Copy)] +struct LineSegment { + x0: f32, + y0: f32, + x1: f32, + y1: f32, +} + +impl LineSegment { + /// Intersects the horizontal line at `y`. Returns `None` if parallel or out of bounds. + /// The mathematical intersection `x` is returned along with direction `dir` (+1 for up, -1 for down). + fn intersect_horizontal(&self, y: f32) -> Option<(f32, i32)> { + // Only intersect if y is strictly between y0 and y1. + let (min_y, max_y, dir) = if self.y0 < self.y1 { + (self.y0, self.y1, 1) + } else { + (self.y1, self.y0, -1) + }; + + if y < min_y || y >= max_y { + return None; + } + + let t = (y - self.y0) / (self.y1 - self.y0); + let x = self.x0 + t * (self.x1 - self.x0); + Some((x, dir)) + } +} + +/// Scale and flatten a glyph outline into reproducible line segments. +fn flatten_outline(outline: &GlyphOutline, scale: f32) -> Vec { + let mut segments = Vec::new(); + + for contour in &outline.contours { + if contour.points.is_empty() { + continue; + } + + let pts = &contour.points; + + // Iterate through implied points. + // TrueType defines curves with an implicit on-curve point + // halfway between any two consecutive off-curve points. + let mut explicit_points = Vec::with_capacity(pts.len() * 2); + + for (i, p) in pts.iter().enumerate() { + let next_p = &pts[(i + 1) % pts.len()]; + + // Add the current point + explicit_points.push((p.x as f32 * scale, p.y as f32 * scale, p.on_curve)); + + // If current and next are both off-curve, insert a midpoint + if !p.on_curve && !next_p.on_curve { + let mx = (p.x as f32 + next_p.x as f32) * 0.5 * scale; + let my = (p.y as f32 + next_p.y as f32) * 0.5 * scale; + explicit_points.push((mx, my, true)); + } + } + + // Now traverse the explicit points. The path starts with the first point. + // But what if the first point is off-curve? + // Let's find an on-curve point to start. + let start_idx = explicit_points.iter().position(|p| p.2).unwrap_or(0); + + let len = explicit_points.len(); + let mut i = 0; + let mut cur = explicit_points[start_idx]; + + // Ensure we loop back to start. + while i < len { + let next_idx = (start_idx + i + 1) % len; + let p1 = explicit_points[next_idx]; + + if p1.2 { + // Line to next on-curve point + segments.push(LineSegment { + x0: cur.0, + y0: cur.1, + x1: p1.0, + y1: p1.1, + }); + cur = p1; + i += 1; + } else { + // Quadratic bezier: p1 is control, need next on-curve point + let p2_idx = (next_idx + 1) % len; + let p2 = explicit_points[p2_idx]; + + // Flatten the quadratic bezier curve. + flatten_quadratic(cur.0, cur.1, p1.0, p1.1, p2.0, p2.1, &mut segments); + + cur = p2; + i += 2; + } + } + } + + segments +} + +/// Flattens a quadratic bézier into line segments using a fixed subdivision. +fn flatten_quadratic( + x0: f32, + y0: f32, + x1: f32, + y1: f32, + x2: f32, + y2: f32, + segments: &mut Vec, +) { + const STEPS: usize = 8; + let mut prev_x = x0; + let mut prev_y = y0; + + for i in 1..=STEPS { + let t = i as f32 / STEPS as f32; + let t_inv = 1.0 - t; + + // Quadratic formula: P = (1-t)^2*P0 + 2t(1-t)*P1 + t^2*P2 + let cur_x = t_inv * t_inv * x0 + 2.0 * t * t_inv * x1 + t * t * x2; + let cur_y = t_inv * t_inv * y0 + 2.0 * t * t_inv * y1 + t * t * y2; + + segments.push(LineSegment { + x0: prev_x, + y0: prev_y, + x1: cur_x, + y1: cur_y, + }); + + prev_x = cur_x; + prev_y = cur_y; + } +} + +/// Rasterizes a scaled outline into a 0-255 coverage bitmap using 16x16 supersampling. +pub fn rasterize(outline: &GlyphOutline, scale: f32) -> Option { + if outline.contours.is_empty() { + return None; + } + + let segments = flatten_outline(outline, scale); + + let x_min = outline.x_min as f32 * scale; + let y_min = outline.y_min as f32 * scale; + let x_max = outline.x_max as f32 * scale; + let y_max = outline.y_max as f32 * scale; + + let bitmap_width = (x_max.ceil() - x_min.floor()) as i32; + let bitmap_height = (y_max.ceil() - y_min.floor()) as i32; + + if bitmap_width <= 0 || bitmap_height <= 0 { + return None; + } + + let bearing_x = x_min.floor() as i32; + // Y-axis in TrueType goes UP, but in screen space it goes DOWN. + // So the top-left of the bounding box is at y_max. + let bearing_y = y_max.ceil() as i32; + + let width = bitmap_width as u32; + let height = bitmap_height as u32; + + // 16x16 supersampling => 256 subpixels per pixel. + const SUB_PIXELS: u32 = 16; + let mut coverage = vec![0u16; (width * height) as usize]; + + for sy in 0..(height * SUB_PIXELS) { + // TrueType Y goes up. The top pixel is row 0. + // Therefore, pixel row `py` corresponds to `bearing_y - py - 1`. + // The subpixel offset is from the top of the pixel box going downwards. + let sub_y_rel = (sy % SUB_PIXELS) as f32 + 0.5; + let py = sy / SUB_PIXELS; + let real_y = bearing_y as f32 - (py as f32 + sub_y_rel / SUB_PIXELS as f32); + + // Find intersections. + let mut intersections = Vec::with_capacity(32); + for seg in &segments { + if let Some((x, dir)) = seg.intersect_horizontal(real_y) { + intersections.push((x, dir)); + } + } + + // Sort by X coordinate. + intersections.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal)); + + // Traverse intersections to calculate winding and fill pixels. + let mut winding = 0; + let mut active_start_x = None; + + for (x, dir) in intersections { + let prev_winding = winding; + winding += dir; + + if prev_winding == 0 && winding != 0 { + // Entered shape. + active_start_x = Some(x); + } else if prev_winding != 0 && winding == 0 { + // Exited shape. Fill subpixels between active_start_x and x. + if let Some(start_x) = active_start_x { + // Map to subpixel coordinates on X axis. + let start_sx = + ((start_x - bearing_x as f32) * SUB_PIXELS as f32).round() as i32; + let end_sx = ((x - bearing_x as f32) * SUB_PIXELS as f32).round() as i32; + + let s_start = start_sx.max(0) as u32; + let s_end = (end_sx.max(0) as u32).min(width * SUB_PIXELS); + + for sx in s_start..s_end { + let px = sx / SUB_PIXELS; + let idx = (py * width + px) as usize; + coverage[idx] += 1; + } + } + active_start_x = None; + } + } + } + + // Convert coverage (0-256) to 0-255 u8. + let data = coverage.into_iter().map(|c| c.min(255) as u8).collect(); + + Some(GlyphBitmap { + width, + height, + bearing_x, + bearing_y, + data, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::font::Font; + + fn load_test_font() -> Font { + crate::font::load_system_font().expect("failed to load system font") + } + + #[test] + fn rasterize_basic_glyph() { + let font = load_test_font(); + let head = font.head().unwrap(); + + let gid = font.glyph_index(0x0041).unwrap().unwrap(); // 'A' + let outline = font.glyph_outline(gid).unwrap().unwrap(); + + // 16px size. + let scale = 16.0 / head.units_per_em as f32; + let bitmap = rasterize(&outline, scale).unwrap(); + + assert!(bitmap.width > 0); + assert!(bitmap.height > 0); + assert_eq!(bitmap.data.len(), (bitmap.width * bitmap.height) as usize); + + // Assert it is anti-aliased (has values other than 0 and 255). + let has_intermediate = bitmap.data.iter().any(|&v| v > 0 && v < 255); + assert!(has_intermediate, "Bitmap must be anti-aliased"); + + // Assert it actually covers some area (has values > 128). + let has_coverage = bitmap.data.iter().any(|&v| v > 128); + assert!(has_coverage, "Bitmap must have solid pixels"); + } +} -- 2.51.2 From 8effc6cc5817262f130a44c4ebc591eb70ff3e35 Mon Sep 17 00:00:00 2001 From: Pierre Le Fevre Date: Thu, 26 Feb 2026 17:25:34 +0100 Subject: [PATCH 2/2] Remove dead raster.rs file (superseded by rasterizer.rs) The raster.rs file was an earlier draft of the glyph rasterizer that was never wired into the module system. It contained an incomplete plot_line_analytic function and a buggy add_quadratic implementation. The proper implementation lives in rasterizer.rs. Co-Authored-By: Claude Opus 4.6 --- crates/text/src/font/raster.rs | 235 --------------------------------- 1 file changed, 235 deletions(-) delete mode 100644 crates/text/src/font/raster.rs diff --git a/crates/text/src/font/raster.rs b/crates/text/src/font/raster.rs deleted file mode 100644 index c276ae1..0000000 --- a/crates/text/src/font/raster.rs +++ /dev/null @@ -1,235 +0,0 @@ -use super::tables::glyf::{Contour, GlyphOutline, Point}; - -/// A rasterized grayscale bitmap of a single glyph. -#[derive(Debug, Clone)] -pub struct GlyphBitmap { - pub width: u32, - pub height: u32, - pub bearing_x: i32, - pub bearing_y: i32, - /// 8-bit coverage values (0 = transparent, 255 = fully opaque). - /// Row-major layout, top-to-bottom. - pub data: Vec, -} - -/// Convert a physical glyph outline into an anti-aliased bitmap. -pub fn rasterize_glyph( - outline: &GlyphOutline, - scale: f32, -) -> Option { - // 1. Flatten the contour into line segments (scaled to pixels). - let mut segments: Vec<(f32, f32, f32, f32)> = Vec::new(); - - for contour in &outline.contours { - if contour.points.is_empty() { - continue; - } - - // TrueType points sequence parsing - let pts = &contour.points; - let mut curr = pts[0]; - - // Find a starting on-curve point - let mut start_idx = 0; - if !curr.on_curve { - let next = pts[1]; - if next.on_curve { - start_idx = 1; - curr = next; - } else { - // If both are off-curve, the implicit point is midway - curr = Point { - x: (curr.x + next.x) / 2, - y: (curr.y + next.y) / 2, - on_curve: true, - }; - } - } - - let first = curr; - let mut i = start_idx + 1; - let n = pts.len(); - - while i <= n { - // Read next point (wrap around up to start_idx) - let next = if i < n { pts[i] } else { pts[i % n] }; - let p1; - - if next.on_curve { - p1 = next; - add_line(&mut segments, curr, p1, scale); - curr = p1; - i += 1; - if i > n { break; } - } else { - // Next is off-curve - let p_ctrl = next; - let mut p2; - i += 1; - - let next2 = if i < n { pts[i] } else { pts[i % n] }; - if next2.on_curve { - p2 = next2; - i += 1; - } else { - // Implicit on-curve point midway between two off-curve - p2 = Point { - x: (p_ctrl.x + next2.x) / 2, - y: (p_ctrl.y + next2.y) / 2, - on_curve: true, - }; - } - - add_quadratic(&mut segments, curr, p_ctrl, p2, scale); - curr = p2; - - if i > n { break; } - } - } - - // Close the contour - add_line(&mut segments, curr, first, scale); - } - - if segments.is_empty() { - return None; - } - - // 2. Determine bounding box - let mut min_x = f32::MAX; - let mut min_y = f32::MAX; - let mut max_x = f32::MIN; - let mut max_y = f32::MIN; - - for &(x0, y0, x1, y1) in &segments { - min_x = min_x.min(x0).min(x1); - min_y = min_y.min(y0).min(y1); - max_x = max_x.max(x0).max(x1); - max_y = max_y.max(y0).max(y1); - } - - if min_x > max_x || min_y > max_y { - return None; - } - - let px_min_x = min_x.floor() as i32; - // Note: TrueType Y goes up. So y_max is the top of the glyph. - // We invert Y so that 0 is at the top of the bitmap. - let px_min_y = (-max_y).floor() as i32; - let px_max_x = max_x.ceil() as i32; - let px_max_y = (-min_y).ceil() as i32; - - let width = (px_max_x - px_min_x).max(1) as u32; - let height = (px_max_y - px_min_y).max(1) as u32; - - let bearing_x = px_min_x; - let bearing_y = px_min_y; // actually -max_y - - // 3. Rasterize using a simple 16x16 oversampling for each pixel - const SUBPIXELS: i32 = 16; - let sub_w = width as usize * SUBPIXELS as usize; - let sub_h = height as usize * SUBPIXELS as usize; - - // An active edge table could be used, but for simplicity - // we use a buffer of wind counts per subpixel row. - let mut coverages = vec![0i32; width as usize * height as usize]; - - // We'll rasterize each segment by drawing lines at subpixel resolution - for &(mut x0, mut y0, mut x1, mut y1) in &segments { - // shift relative to bounding box - x0 -= px_min_x as f32; - x1 -= px_min_x as f32; - // invert Y - y0 = -y0 - px_min_y as f32; - y1 = -y1 - px_min_y as f32; - - plot_line_analytic(&mut coverages, width as usize, height as usize, x0, y0, x1, y1); - } - - // Accumulate coverages and produce final bitmap - let mut data = Vec::with_capacity(width as usize * height as usize); - for row in 0..height as usize { - let mut accum = 0.0; - for col in 0..width as usize { - accum += coverages[row * width as usize + col] as f32 / 256.0; - // non-zero winding rule - let mut alpha = accum.abs(); - if alpha > 1.0 { alpha = 1.0; } - let intensity = (alpha * 255.0).round() as u8; - data.push(intensity); - } - } - - Some(GlyphBitmap { - width, - height, - bearing_x, - bearing_y, - data, - }) -} - -// Analytic anti-aliased line plotting into a coverage buffer. -// Inspired by font-rs and stb_truetype. -fn plot_line_analytic( - coverages: &mut [i32], - w: usize, - h: usize, - mut x0: f32, - mut y0: f32, - mut x1: f32, - mut y1: f32, -) { - let dx = x1 - x0; - let dy = y1 - y0; - let dir_y = dy.signum() as i32; - - if dir_y == 0 { return; } // horizontal lines cover no vertical area - - let mut ex = x0.floor() as i32; - let mut ey = y0.floor() as i32; - let ext_x = x1.floor() as i32; - let ext_y = y1.floor() as i32; - - // To handle negative directions, step is 1 or -1 - let step_x = dx.signum() as i32; - let step_y = dy.signum() as i32; - - // compute intersections with pixel boundaries - // ... - // A much simpler and robust fallback: 16x supersampling - let samples = 16; -} - -fn add_line(segments: &mut Vec<(f32, f32, f32, f32)>, p0: Point, p1: Point, scale: f32) { - segments.push(( - p0.x as f32 * scale, - p0.y as f32 * scale, - p1.x as f32 * scale, - p1.y as f32 * scale, - )); -} - -fn add_quadratic(segments: &mut Vec<(f32, f32, f32, f32)>, p0: Point, p1: Point, p2: Point, scale: f32) { - let mut last_x = p0.x as f32 * scale; - let mut last_y = p0.y as f32 * scale; - - let cx = p1.x as f32 * scale; - let cy = p1.y as f32 * scale; - - let px2 = p2.x as f32 * scale; - let py2 = p2.y as f32 * scale; - - let steps = 8; - for i in 1..=steps { - let t = i as f32 / steps as f32; - let mt = 1.0 - t; - - let x = mt * mt * last_x + 2.0 * mt * t * cx + t * t * px2; - let y = mt * mt * last_y + 2.0 * mt * t * cy + t * t * py2; - - segments.push((last_x, last_y, x, y)); - last_x = x; - last_y = y; - } -}