circles-sketch 0.4.0

Generate interactive Fourier epicycle animations from contours, text, or SVG files
Documentation
use font_kit::source::SystemSource;
use ttf_parser::OutlineBuilder;

struct SvgPathBuilder {
    path: String,
    x_offset: f64,
}

impl SvgPathBuilder {
    fn new(x_offset: f64) -> Self {
        Self {
            path: String::new(),
            x_offset,
        }
    }
}

impl OutlineBuilder for SvgPathBuilder {
    fn move_to(&mut self, x: f32, y: f32) {
        let x = x as f64 + self.x_offset;
        let y = -(y as f64);
        self.path.push_str(&format!("M {x} {y} "));
    }

    fn line_to(&mut self, x: f32, y: f32) {
        let x = x as f64 + self.x_offset;
        let y = -(y as f64);
        self.path.push_str(&format!("L {x} {y} "));
    }

    fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
        let x1 = x1 as f64 + self.x_offset;
        let y1 = -(y1 as f64);
        let x = x as f64 + self.x_offset;
        let y = -(y as f64);
        self.path.push_str(&format!("Q {x1} {y1} {x} {y} "));
    }

    fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
        let x1 = x1 as f64 + self.x_offset;
        let y1 = -(y1 as f64);
        let x2 = x2 as f64 + self.x_offset;
        let y2 = -(y2 as f64);
        let x = x as f64 + self.x_offset;
        let y = -(y as f64);
        self.path
            .push_str(&format!("C {x1} {y1} {x2} {y2} {x} {y} "));
    }

    fn close(&mut self) {
        self.path.push_str("Z ");
    }
}

pub fn svg_path_of_text(text: &str, font_name: &str) -> String {
    let font = SystemSource::new()
        .select_by_postscript_name(font_name)
        .unwrap_or_else(|_| panic!("font not found: {font_name}"))
        .load()
        .unwrap_or_else(|_| panic!("failed to load font: {font_name}"));

    let font_data = font.copy_font_data().expect("failed to copy font data");
    let face = ttf_parser::Face::parse(&font_data, 0).expect("failed to parse font");

    let mut path = String::new();
    let mut x: f64 = 0.0;

    for ch in text.chars() {
        let glyph_id = match face.glyph_index(ch) {
            Some(id) => id,
            None => continue,
        };

        let mut builder = SvgPathBuilder::new(x);
        face.outline_glyph(glyph_id, &mut builder);
        path.push_str(&builder.path);

        if let Some(advance) = face.glyph_hor_advance(glyph_id) {
            x += advance as f64;
        }
    }

    path.trim_end().to_string()
}