audio-widgets 0.1.0

A collection of audio related UI widgets for Rust.
Documentation
use crate::eq::common::*;
use scales::prelude::*;
use wasm_bindgen::JsCast;
use wasm_bindgen::JsValue;
use web_sys::CanvasRenderingContext2d;
use web_sys::CssStyleDeclaration;
use web_sys::HtmlCanvasElement;

pub struct CanvasEqRenderer {
    context: CanvasRenderingContext2d,
    major_gain_markers: Vec<f64>,
    minor_gain_markers: Vec<f64>,
    minor_grid: bool,
    band_curves: bool,
    style: Style,
    geometry: Geometry,
}

struct Style {
    background_fill: Option<String>,
    major_grid_stroke: Option<String>,
    minor_grid_stroke: Option<String>,
    band_stroke: Option<String>,
    band_strokes: Vec<Option<String>>,
    band_disabled_stroke: Option<String>,
    band_fill: Option<String>,
    band_fills: Vec<Option<String>>,
    band_disabled_fill: Option<String>,
    sum_stroke: Option<String>,
    sum_fill: Option<String>,
}

struct Geometry {
    width: f64,
    height: f64,
}

impl<'a> CanvasEqRenderer {
    pub fn new(
        canvas: HtmlCanvasElement,
        major_gain_markers: Vec<f64>,
        minor_gain_markers: Vec<f64>,
        minor_grid: bool,
        band_curves: bool,
    ) -> Option<CanvasEqRenderer> {
        let context = if let Ok(Some(ctx)) = canvas.get_context("2d") {
            if let Ok(ctx) = ctx.dyn_into::<web_sys::CanvasRenderingContext2d>() {
                ctx
            } else {
                return None;
            }
        } else {
            return None;
        };

        let window = web_sys::window();
        let style = window.map(|w| w.get_computed_style(&canvas));

        let width = canvas.width() as f64;
        let height = canvas.height() as f64;

        let geometry = Geometry { width, height };

        let background_fill = get_style("--background", &style, Some("black"));
        let major_grid_stroke = get_style("--major-grid", &style, Some("#333"));
        let minor_grid_stroke = get_style("--minor-grid", &style, Some("#222"));
        let band_stroke = get_style("--band-stroke", &style, Some("#88f6"));
        let band_strokes = (0..10)
            .map(|i| {
                let style_name = format!("--band-{}-stroke", (i + 1));
                get_style(style_name, &style, None)
            })
            .collect();
        let band_disabled_stroke = get_style("--band-disabled-stroke", &style, Some("#333"));
        let band_fill = get_style("--band-fill", &style, Some("#88f6"));
        let band_fills = (0..10)
            .map(|i| {
                let style_name = format!("--band-{}-fill", (i + 1));
                get_style(style_name, &style, None)
            })
            .collect();
        let band_disabled_fill = get_style("--band-disabled-fill", &style, Some("#88f6"));
        let sum_stroke = get_style("--sum-stroke", &style, Some("#88f"));
        let sum_fill = get_style("--sum-fill", &style, Some("#88f6"));

        let style = Style {
            background_fill,
            major_grid_stroke,
            minor_grid_stroke,
            band_stroke,
            band_strokes,
            band_disabled_stroke,
            band_fill,
            band_fills,
            band_disabled_fill,
            sum_stroke,
            sum_fill,
        };

        Some(CanvasEqRenderer {
            context,
            major_gain_markers,
            minor_gain_markers,
            minor_grid,
            band_curves,
            geometry,
            style,
        })
    }

    pub fn render_grid_to_canvas(&self, eq: &EQ) {
        let context = &self.context;

        let width = self.geometry.width;
        let height = self.geometry.height;

        set_fill(&context, self.style.background_fill.as_ref());
        context.fill_rect(0.0, 0.0, width, height);

        let y_conv = eq.y_to_gain_converter(height, true);

        if self.minor_grid {
            set_stroke(&context, self.style.minor_grid_stroke.as_ref());
            context.begin_path();

            for x in eq.calc_minor_frequency_grid_markers(width) {
                let x = x.floor() + 0.5;
                context.move_to(x, 0.0);
                context.line_to(x, height);
            }

            for g in &self.minor_gain_markers {
                let y = y_conv.convert_back(*g).floor() + 0.5;
                context.move_to(0.0, y);
                context.line_to(width, y);
            }

            context.stroke();
        }

        set_stroke(&context, self.style.major_grid_stroke.as_ref());
        context.begin_path();

        for x in eq.calc_major_frequency_grid_markers(width) {
            let x = x.floor() + 0.5;
            context.move_to(x, 0.0);
            context.line_to(x, height);
        }

        for g in &self.major_gain_markers {
            let y = y_conv.convert_back(*g).floor() + 0.5;
            context.move_to(0.0, y);
            context.line_to(width, y);
        }

        context.stroke();
    }

    pub fn render_to_canvas(&self, eq: &EQ) {
        let width = self.geometry.width;
        let height = self.geometry.height;

        let context = &self.context;

        let x_conv = eq.x_to_frequency_converter(width);
        let y_conv = eq.y_to_gain_converter(height, true);
        let q_conv = eq.q_to_radius_converter(width);

        let graph = eq.plot(width, height, true);

        if self.band_curves {
            for (i, (band, active)) in graph.band_curves.iter().enumerate() {
                context.begin_path();
                let style = if *active {
                    self.style.band_strokes[i]
                        .as_ref()
                        .or(self.style.band_stroke.as_ref())
                } else {
                    self.style.band_disabled_stroke.as_ref()
                };
                set_stroke(context, style);
                stroke_curve(&band, &context);
                context.stroke();
            }
        }

        context.begin_path();
        set_stroke(context, self.style.sum_stroke.as_ref());
        set_fill(context, self.style.sum_fill.as_ref());
        stroke_curve(&graph.sum, &context);
        context.stroke();
        context.line_to(width, y_conv.convert_back(0.0));
        context.line_to(0.0, y_conv.convert_back(0.0));
        context.fill();

        if self.band_curves {
            for (i, (band, active)) in eq.bands.iter().enumerate() {
                let style = if *active {
                    self.style.band_fills[i]
                        .as_ref()
                        .or(self.style.band_fill.as_ref())
                } else {
                    self.style.band_disabled_fill.as_ref()
                };
                set_fill(context, style);

                let x = x_conv.convert_back(band.frequency());
                let y = y_conv.convert_back(band.gain().unwrap_or(0.0));

                let radius = if let EqBand::Bell { q, .. } = band {
                    q_conv.convert(*q)
                } else {
                    q_conv.convert(1.0)
                };

                context.begin_path();
                context
                    .arc(x, y, radius, 0.0, 2.0 * std::f64::consts::PI)
                    .expect("arc failed");
                context.fill();
            }
        }
    }
}

fn stroke_curve(curve: &Vec<(X, Y)>, context: &web_sys::CanvasRenderingContext2d) {
    if curve.is_empty() {
        return;
    }

    let (x, y) = curve[0];
    context.move_to(x - 0.5, y + 0.5);

    for (x, y) in curve {
        context.line_to(*x + 0.5, *y + 0.5);
    }
}

fn get_style(
    style_name: impl AsRef<str>,
    style: &Option<Result<Option<CssStyleDeclaration>, JsValue>>,
    default: Option<&str>,
) -> Option<String> {
    if let Some(Ok(Some(style))) = style.as_ref() {
        if let Ok(style) = style.get_property_value(style_name.as_ref()) {
            if !style.is_empty() {
                Some(style)
            } else {
                default.map(|s| s.to_owned())
            }
        } else {
            default.map(|s| s.to_owned())
        }
    } else {
        default.map(|s| s.to_owned())
    }
}

fn set_fill(context: &CanvasRenderingContext2d, style: Option<impl AsRef<str>>) {
    if let Some(style) = style {
        context.set_fill_style(&style.as_ref().into());
    }
}

fn set_stroke(context: &CanvasRenderingContext2d, style: Option<impl AsRef<str>>) {
    if let Some(style) = style {
        context.set_stroke_style(&style.as_ref().into());
    }
}