use std::fmt::Write as _;
use acorde_core::NoteHead;
pub(crate) fn f(v: f32) -> String {
format!("{v:.2}")
}
fn arc_points(cx: f32, cy: f32, rx: f32, ry: f32, start_deg: f32, end_deg: f32, steps: u32) -> Vec<(f32, f32)> {
let mut pts = Vec::with_capacity(steps as usize + 1);
for i in 0..=steps {
let t = i as f32 / steps as f32;
let deg = start_deg + (end_deg - start_deg) * t;
let rad = deg.to_radians();
pts.push((cx + rad.cos() * rx, cy + rad.sin() * ry));
}
pts
}
fn path_from_segments(segments: &[Vec<(f32, f32)>], ox: f32, oy: f32, space: f32) -> String {
let mut d = String::new();
let mut first = true;
for seg in segments {
for &(x, y) in seg {
let px = ox + x * space;
let py = oy + y * space;
let _ = write!(d, "{}{},{} ", if first { "M " } else { "L " }, f(px), f(py));
first = false;
}
}
d.trim_end().to_string()
}
pub(crate) fn clef_treble(ox: f32, oy: f32, space: f32) -> String {
let cx = 0.7;
let foot = arc_points(cx, 0.85, 0.42, 0.42, 200.0, 560.0, 24);
let belly = arc_points(cx + 0.05, -1.55, 0.62, 1.05, 55.0, -305.0, 40);
let top_curl = arc_points(cx - 0.5, -4.55, 0.42, 0.42, -10.0, 250.0, 24);
let d = path_from_segments(&[foot, belly, top_curl], ox, oy, space);
format!(
r#"<path class="acorde-clef acorde-clef-treble" d="{d}" fill="none" stroke="black" stroke-width="{sw}" stroke-linecap="round" stroke-linejoin="round"/>"#,
sw = f(0.15 * space)
)
}
pub(crate) fn clef_bass(ox: f32, oy: f32, space: f32) -> String {
let cx = 0.15;
let cy = -2.9;
let hook = arc_points(cx, cy, 0.62, 1.55, -100.0, 95.0, 50);
let tail_end = *hook.last().unwrap();
let tail_flick = arc_points(tail_end.0 - 0.35, tail_end.1 - 0.35, 0.35, 0.35, 30.0, -180.0, 16);
let d = path_from_segments(&[hook, tail_flick], ox, oy, space);
let dot1 = dot_at(cx + 0.95, -3.5, ox, oy, space);
let dot2 = dot_at(cx + 0.95, -2.5, ox, oy, space);
let sw = f(0.15 * space);
format!(
r#"<g class="acorde-clef acorde-clef-bass"><path d="{d}" fill="none" stroke="black" stroke-width="{sw}" stroke-linecap="round" stroke-linejoin="round"/>{dot1}{dot2}</g>"#
)
}
pub(crate) fn clef_c(ox: f32, oy: f32, space: f32, mid_position_u: f32) -> String {
let mid = -mid_position_u;
let lens_top = arc_points(0.35, mid - 0.6, 0.35, 0.6, 90.0, 270.0, 20);
let lens_bottom = arc_points(0.35, mid + 0.6, 0.35, 0.6, -90.0, 90.0, 20);
let d = path_from_segments(&[lens_top, lens_bottom], ox, oy, space);
let bar1_x = ox + 0.85 * space;
let bar2_x = ox + 1.1 * space;
let top_y = oy - 4.0 * space;
let bottom_y = oy;
let sw = f(0.15 * space);
format!(
r#"<g class="acorde-clef acorde-clef-c"><path d="{d}" fill="none" stroke="black" stroke-width="{sw}" stroke-linecap="round" stroke-linejoin="round"/><line x1="{bx1}" y1="{ty}" x2="{bx1}" y2="{by}" stroke="black" stroke-width="{sw}"/><line x1="{bx2}" y1="{ty}" x2="{bx2}" y2="{by}" stroke="black" stroke-width="{sw2}"/></g>"#,
bx1 = f(bar1_x), bx2 = f(bar2_x), ty = f(top_y), by = f(bottom_y),
sw2 = f(0.28 * space),
)
}
fn dot_at(cx: f32, cy: f32, ox: f32, oy: f32, space: f32) -> String {
format!(
r#"<circle cx="{x}" cy="{y}" r="{r}" fill="black"/>"#,
x = f(ox + cx * space), y = f(oy + cy * space), r = f(0.13 * space)
)
}
pub(crate) fn notehead(cx: f32, cy: f32, space: f32, filled: bool) -> String {
let rx = f(0.62 * space * 0.5);
let ry = f(0.48 * space * 0.5);
if filled {
format!(r#"<ellipse class="acorde-notehead" cx="{x}" cy="{y}" rx="{rx}" ry="{ry}" fill="black"/>"#, x = f(cx), y = f(cy))
} else {
let sw = f(0.16 * space * 0.5);
format!(
r#"<ellipse class="acorde-notehead" cx="{x}" cy="{y}" rx="{rx}" ry="{ry}" fill="none" stroke="black" stroke-width="{sw}"/>"#,
x = f(cx), y = f(cy)
)
}
}
pub(crate) fn notehead_shape(head: &NoteHead, cx: f32, cy: f32, space: f32, filled: bool) -> String {
match head {
NoteHead::Normal => notehead(cx, cy, space, filled),
NoteHead::Diamond => {
let w = 0.38 * space;
let h = 0.52 * space;
let points = format!("{},{} {},{} {},{} {},{}", f(cx), f(cy - h), f(cx + w), f(cy), f(cx), f(cy + h), f(cx - w), f(cy));
let fill = if filled { "black" } else { "none" };
format!(r#"<polygon class="acorde-notehead acorde-notehead-diamond" points="{points}" fill="{fill}" stroke="black" stroke-width="{}"/>"#, f(0.12 * space))
}
NoteHead::Triangle => {
let points = format!("{},{} {},{} {},{}", f(cx), f(cy - 0.55 * space), f(cx + 0.42 * space), f(cy + 0.35 * space), f(cx - 0.42 * space), f(cy + 0.35 * space));
let fill = if filled { "black" } else { "none" };
format!(r#"<polygon class="acorde-notehead acorde-notehead-triangle" points="{points}" fill="{fill}" stroke="black" stroke-width="{}"/>"#, f(0.12 * space))
}
NoteHead::X | NoteHead::Cross => {
let r = if matches!(head, NoteHead::X) { 0.34 } else { 0.42 } * space;
let sw = f(0.16 * space);
format!(r#"<g class="acorde-notehead acorde-notehead-{}" stroke="black" stroke-width="{sw}" stroke-linecap="round"><line x1="{}" y1="{}" x2="{}" y2="{}"/><line x1="{}" y1="{}" x2="{}" y2="{}"/></g>"#, if matches!(head, NoteHead::X) { "x" } else { "cross" }, f(cx-r), f(cy-r), f(cx+r), f(cy+r), f(cx-r), f(cy+r), f(cx+r), f(cy-r))
}
NoteHead::Slash => {
format!(r#"<line class="acorde-notehead acorde-notehead-slash" x1="{}" y1="{}" x2="{}" y2="{}" stroke="black" stroke-width="{}" stroke-linecap="round"/>"#, f(cx - 0.42 * space), f(cy + 0.42 * space), f(cx + 0.42 * space), f(cy - 0.42 * space), f(0.22 * space))
}
}
}
pub(crate) const NOTEHEAD_RX_U: f32 = 0.31; pub(crate) const DEFAULT_STEM_LEN_U: f32 = 3.0;
pub(crate) fn stem(cx: f32, cy: f32, space: f32, up: bool) -> (String, f32) {
let tip_y = if up { cy - DEFAULT_STEM_LEN_U * space } else { cy + DEFAULT_STEM_LEN_U * space };
(stem_to(cx, cy, tip_y, space, up), tip_y)
}
pub(crate) fn stem_to(cx: f32, cy: f32, tip_y: f32, space: f32, up: bool) -> String {
let x_off = NOTEHEAD_RX_U * space * 0.92;
let x = if up { cx + x_off } else { cx - x_off };
let sw = f(0.11 * space);
format!(
r#"<line class="acorde-stem" x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" stroke="black" stroke-width="{sw}"/>"#,
x1 = f(x), y1 = f(cy), x2 = f(x), y2 = f(tip_y)
)
}
pub(crate) fn flag(stem_x: f32, tip_y: f32, space: f32, up: bool) -> String {
let s = if up { 1.0 } else { -1.0 };
let d = format!(
"M {ox},{oy} Q {cx1},{cy1} {ex},{ey} Q {cx2},{cy2} {ox},{oy} Z",
ox = f(stem_x), oy = f(tip_y),
cx1 = f(stem_x + 0.65 * space), cy1 = f(tip_y + 0.35 * space * s),
ex = f(stem_x + 0.12 * space), ey = f(tip_y + 1.05 * space * s),
cx2 = f(stem_x - 0.05 * space), cy2 = f(tip_y + 0.55 * space * s),
);
format!(r#"<path d="{d}" fill="black" stroke="none"/>"#)
}
pub(crate) fn ledger_line(cx: f32, y: f32, space: f32) -> String {
let half_w = 0.5 * space;
let sw = f(0.1 * space);
format!(
r#"<line class="acorde-ledger" x1="{x1}" y1="{y}" x2="{x2}" y2="{y}" stroke="black" stroke-width="{sw}"/>"#,
x1 = f(cx - half_w), x2 = f(cx + half_w), y = f(y)
)
}
pub(crate) fn barline(x: f32, top_y: f32, bottom_y: f32, space: f32, thick: bool) -> String {
let sw = f(if thick { 0.3 * space } else { 0.09 * space });
format!(
r#"<line class="acorde-barline" x1="{x}" y1="{y1}" x2="{x}" y2="{y2}" stroke="black" stroke-width="{sw}"/>"#,
x = f(x), y1 = f(top_y), y2 = f(bottom_y)
)
}
pub(crate) fn beam_segment(x1: f32, y1: f32, x2: f32, y2: f32, thickness: f32) -> String {
let ht = thickness / 2.0;
format!(
r#"<polygon class="acorde-beam" points="{x1},{y1a} {x2},{y2a} {x2},{y2b} {x1},{y1b}" fill="black"/>"#,
x1 = f(x1), x2 = f(x2),
y1a = f(y1 - ht), y2a = f(y2 - ht), y2b = f(y2 + ht), y1b = f(y1 + ht),
)
}
pub(crate) fn accidental(alter: i8, cx: f32, cy: f32, space: f32) -> String {
match alter {
1 => sharp(cx, cy, space),
-1 => flat(cx, cy, space),
0 => natural(cx, cy, space),
2 => double_sharp(cx, cy, space),
-2 => double_flat(cx, cy, space),
_ => String::new(), }
}
pub(crate) fn accidental_width_u(alter: i8) -> f32 {
match alter {
-2 => 1.1,
_ => 0.65,
}
}
fn sharp(cx: f32, cy: f32, space: f32) -> String {
let sw_v = f(0.09 * space);
let sw_h = f(0.22 * space);
let x1 = cx - 0.18 * space;
let x2 = cx + 0.18 * space;
let y_top = cy - 0.75 * space;
let y_bot = cy + 0.75 * space;
format!(
r#"<g class="acorde-accidental acorde-sharp"><line x1="{x1}" y1="{yt}" x2="{x1}" y2="{yb}" stroke="black" stroke-width="{swv}"/><line x1="{x2}" y1="{yt}" x2="{x2}" y2="{yb}" stroke="black" stroke-width="{swv}"/><line x1="{hx1}" y1="{hy1}" x2="{hx2}" y2="{hy1a}" stroke="black" stroke-width="{swh}"/><line x1="{hx1}" y1="{hy2}" x2="{hx2}" y2="{hy2a}" stroke="black" stroke-width="{swh}"/></g>"#,
x1 = f(x1), x2 = f(x2), yt = f(y_top), yb = f(y_bot), swv = sw_v, swh = sw_h,
hx1 = f(cx - 0.3 * space), hx2 = f(cx + 0.3 * space),
hy1 = f(cy - 0.32 * space), hy1a = f(cy - 0.42 * space),
hy2 = f(cy + 0.42 * space), hy2a = f(cy + 0.32 * space),
)
}
fn flat(cx: f32, cy: f32, space: f32) -> String {
let sw = f(0.1 * space);
let x = cx - 0.22 * space;
let y_top = cy - 0.85 * space;
let y_bot = cy + 0.35 * space;
let bowl = arc_points(0.0, 0.15, 0.32, 0.35, -90.0, 110.0, 16);
let bowl_d = path_from_segments(&[bowl], x, cy, space);
format!(
r#"<g class="acorde-accidental acorde-flat"><line x1="{x}" y1="{yt}" x2="{x}" y2="{yb}" stroke="black" stroke-width="{sw}"/><path d="{bowl_d}" fill="none" stroke="black" stroke-width="{sw}" stroke-linecap="round"/></g>"#,
x = f(x), yt = f(y_top), yb = f(y_bot), sw = sw,
)
}
fn natural(cx: f32, cy: f32, space: f32) -> String {
let sw_v = f(0.08 * space);
let sw_h = f(0.16 * space);
let x1 = cx - 0.18 * space;
let x2 = cx + 0.18 * space;
format!(
r#"<g class="acorde-accidental acorde-natural"><line x1="{x1}" y1="{y1t}" x2="{x1}" y2="{y1b}" stroke="black" stroke-width="{swv}"/><line x1="{x2}" y1="{y2t}" x2="{x2}" y2="{y2b}" stroke="black" stroke-width="{swv}"/><line x1="{x1}" y1="{hy1}" x2="{x2}" y2="{hy1b}" stroke="black" stroke-width="{swh}"/><line x1="{x1}" y1="{hy2}" x2="{x2}" y2="{hy2b}" stroke="black" stroke-width="{swh}"/></g>"#,
x1 = f(x1), x2 = f(x2),
y1t = f(cy - 0.3 * space), y1b = f(cy + 0.85 * space),
y2t = f(cy - 0.85 * space), y2b = f(cy + 0.3 * space),
hy1 = f(cy - 0.34 * space), hy1b = f(cy - 0.5 * space),
hy2 = f(cy + 0.5 * space), hy2b = f(cy + 0.34 * space),
swv = sw_v, swh = sw_h,
)
}
fn double_sharp(cx: f32, cy: f32, space: f32) -> String {
let sw = f(0.16 * space);
let r = 0.3 * space;
format!(
r#"<g class="acorde-accidental acorde-double-sharp"><line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" stroke="black" stroke-width="{sw}" stroke-linecap="round"/><line x1="{x2}" y1="{y1}" x2="{x1}" y2="{y2}" stroke="black" stroke-width="{sw}" stroke-linecap="round"/></g>"#,
x1 = f(cx - r), x2 = f(cx + r), y1 = f(cy - r), y2 = f(cy + r), sw = sw,
)
}
fn double_flat(cx: f32, cy: f32, space: f32) -> String {
let left = flat(cx - 0.35 * space, cy, space);
let right = flat(cx + 0.35 * space, cy, space);
format!(r#"<g class="acorde-accidental acorde-double-flat">{left}{right}</g>"#)
}
pub(crate) fn rest_whole(cx: f32, staff_mid_y: f32, space: f32) -> String {
let y = staff_mid_y - 2.0 * space;
rest_block(cx, y, space, true)
}
pub(crate) fn rest_half(cx: f32, staff_mid_y: f32, space: f32) -> String {
rest_block(cx, staff_mid_y, space, false)
}
fn rest_block(cx: f32, line_y: f32, space: f32, hangs_below: bool) -> String {
let w = 0.6 * space;
let h = 0.22 * space;
let y = if hangs_below { line_y } else { line_y - h };
format!(
r#"<rect x="{x}" y="{y}" width="{w}" height="{h}" fill="black"/>"#,
x = f(cx - w / 2.0), y = f(y), w = f(w), h = f(h)
)
}
pub(crate) fn rest_quarter(cx: f32, staff_mid_y: f32, space: f32) -> String {
let pts: Vec<(f32, f32)> = vec![
(0.15, -1.1), (-0.15, -0.55), (0.2, -0.05), (-0.2, 0.55), (0.05, 0.75), (-0.15, 1.1),
];
let d = path_from_segments(&[pts], cx, staff_mid_y, space);
let sw = f(0.16 * space);
format!(
r#"<path d="{d}" fill="none" stroke="black" stroke-width="{sw}" stroke-linecap="round" stroke-linejoin="round"/>"#
)
}
pub(crate) fn rest_eighth(cx: f32, staff_mid_y: f32, space: f32) -> String {
let stroke = format!(
r#"<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" stroke="black" stroke-width="{sw}" stroke-linecap="round"/>"#,
x1 = f(cx + 0.32 * space), y1 = f(staff_mid_y - 0.75 * space),
x2 = f(cx - 0.28 * space), y2 = f(staff_mid_y + 0.75 * space),
sw = f(0.13 * space),
);
let head = format!(
r#"<circle cx="{x}" cy="{y}" r="{r}" fill="black"/>"#,
x = f(cx + 0.32 * space), y = f(staff_mid_y - 0.6 * space), r = f(0.2 * space)
);
format!("{stroke}{head}")
}
pub(crate) fn rest_short(cx: f32, staff_mid_y: f32, space: f32, flags: usize) -> String {
let mut out = rest_eighth(cx, staff_mid_y, space);
for i in 1..flags {
let y = staff_mid_y - (0.6 - i as f32 * 0.34) * space;
let _ = write!(out, r#"<path class="acorde-rest-flag" d="M {},{} Q {},{} {},{}" fill="none" stroke="black" stroke-width="{}" stroke-linecap="round"/>"#, f(cx + 0.32 * space), f(y), f(cx + 0.72 * space), f(y + 0.18 * space), f(cx + 0.16 * space), f(y + 0.55 * space), f(0.13 * space));
}
out
}
pub(crate) fn augmentation_dot(cx: f32, cy: f32, space: f32) -> String {
format!(r#"<circle cx="{x}" cy="{y}" r="{r}" fill="black"/>"#, x = f(cx), y = f(cy), r = f(0.11 * space))
}
const DIGIT_SEGMENTS: [[bool; 7]; 10] = [
[true, true, true, true, true, true, false], [false, true, true, false, false, false, false], [true, true, false, true, true, false, true], [true, true, true, true, false, false, true], [false, true, true, false, false, true, true], [true, false, true, true, false, true, true], [true, false, true, true, true, true, true], [true, true, true, false, false, false, false], [true, true, true, true, true, true, true], [true, true, true, true, false, true, true], ];
pub(crate) fn digit(d: u8, ox: f32, oy: f32, space: f32) -> String {
let segs = DIGIT_SEGMENTS[(d.min(9)) as usize];
let w = 0.55 * space;
let h = 1.5 * space;
let mid = h / 2.0;
let sw = f(0.14 * space);
let mut out = String::new();
let mut seg_line = |on: bool, x1: f32, y1: f32, x2: f32, y2: f32| {
if on {
let _ = write!(
out,
r#"<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" stroke="black" stroke-width="{sw}" stroke-linecap="square"/>"#,
x1 = f(ox + x1), y1 = f(oy + y1), x2 = f(ox + x2), y2 = f(oy + y2)
);
}
};
seg_line(segs[0], 0.0, 0.0, w, 0.0); seg_line(segs[1], w, 0.0, w, mid); seg_line(segs[2], w, mid, w, h); seg_line(segs[3], 0.0, h, w, h); seg_line(segs[4], 0.0, mid, 0.0, h); seg_line(segs[5], 0.0, 0.0, 0.0, mid); seg_line(segs[6], 0.0, mid, w, mid); out
}
pub(crate) const DIGIT_WIDTH_U: f32 = 0.75;
pub(crate) fn tuplet_line(x1: f32, y1: f32, x2: f32, y2: f32, space: f32) -> String {
let sw = f(0.09 * space);
format!(
r#"<line class="acorde-tuplet-bracket" x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" stroke="black" stroke-width="{sw}"/>"#,
x1 = f(x1), y1 = f(y1), x2 = f(x2), y2 = f(y2)
)
}
pub(crate) fn tuplet_number(n: u8, cx: f32, cy: f32, space: f32) -> String {
let digit_space = 0.65 * space;
let digits: Vec<u8> = if n == 0 {
vec![0]
} else {
let mut d = Vec::new();
let mut v = n;
while v > 0 {
d.push(v % 10);
v /= 10;
}
d.reverse();
d
};
let total_w = digits.len() as f32 * DIGIT_WIDTH_U * digit_space;
let mut ox = cx - total_w / 2.0;
let oy = cy - 0.75 * digit_space;
let mut out = String::from(r#"<g class="acorde-tuplet-number">"#);
for d in digits {
out.push_str(&digit(d, ox, oy, digit_space));
ox += DIGIT_WIDTH_U * digit_space;
}
out.push_str("</g>");
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn f_formats_two_decimals() {
assert_eq!(f(1.0), "1.00");
assert_eq!(f(1.005), "1.00"); }
#[test]
fn clef_treble_is_stable_across_calls() {
assert_eq!(clef_treble(0.0, 0.0, 24.0), clef_treble(0.0, 0.0, 24.0));
}
#[test]
fn digit_glyphs_nonempty_for_all_digits() {
for d in 0..=9u8 {
assert!(!digit(d, 0.0, 0.0, 20.0).is_empty(), "digit {d} produced no segments");
}
}
#[test]
fn accidental_covers_supported_range() {
for alter in [-2, -1, 0, 1, 2] {
assert!(!accidental(alter, 0.0, 0.0, 20.0).is_empty());
}
}
}