use crate::pointer::{PointerGesture, PointerTracker};
use crate::render::RenderEngine;
use crate::ui::Ui;
use crate::waterfall::Waterfall;
use std::cell::RefCell;
use std::rc::Rc;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{HtmlCanvasElement, PointerEvent, WheelEvent, Window};
#[derive(Clone)]
pub struct WaterfallInteraction {
window: Rc<Window>,
canvas: Rc<HtmlCanvasElement>,
render_engine: Rc<RefCell<RenderEngine>>,
waterfall: Rc<RefCell<Waterfall>>,
pointer_tracker: Rc<RefCell<PointerTracker>>,
ui: Rc<RefCell<Option<Ui>>>,
center_freq_overflow: Rc<RefCell<f32>>,
}
impl WaterfallInteraction {
pub fn new(
window: Rc<Window>,
canvas: Rc<HtmlCanvasElement>,
render_engine: Rc<RefCell<RenderEngine>>,
waterfall: Rc<RefCell<Waterfall>>,
) -> Result<WaterfallInteraction, JsValue> {
canvas.style().set_property("cursor", "crosshair")?;
let interaction = WaterfallInteraction {
window,
canvas,
render_engine,
waterfall,
pointer_tracker: Rc::new(RefCell::new(PointerTracker::new())),
ui: Rc::new(RefCell::new(None)),
center_freq_overflow: Rc::new(RefCell::new(0.0)),
};
interaction.set_callbacks();
Ok(interaction)
}
pub fn set_ui(&mut self, ui: Ui) {
self.ui.borrow_mut().replace(ui);
}
fn set_callbacks(&self) {
self.resize_canvas()();
self.window
.set_onresize(Some(self.onresize().into_js_value().unchecked_ref()));
self.canvas
.set_onwheel(Some(self.onwheel().into_js_value().unchecked_ref()));
self.canvas
.set_onpointerdown(Some(self.onpointerdown().into_js_value().unchecked_ref()));
let onpointerup = self.onpointerup();
self.canvas
.set_onpointercancel(Some(onpointerup.as_ref().unchecked_ref()));
self.canvas
.set_onpointerout(Some(onpointerup.as_ref().unchecked_ref()));
self.canvas
.set_onpointerleave(Some(onpointerup.as_ref().unchecked_ref()));
self.canvas
.set_onpointerup(Some(onpointerup.into_js_value().unchecked_ref()));
self.canvas
.set_onpointermove(Some(self.onpointermove().into_js_value().unchecked_ref()));
}
fn resize_canvas(&self) -> impl Fn() {
let render_engine = Rc::clone(&self.render_engine);
let waterfall = Rc::clone(&self.waterfall);
move || {
let mut engine = render_engine.borrow_mut();
engine.resize_canvas().unwrap();
waterfall.borrow_mut().resize_canvas(&mut engine).unwrap();
}
}
fn onresize(&self) -> Closure<dyn Fn()> {
Closure::new(self.resize_canvas())
}
fn clamp_zoom(zoom: f32) -> f32 {
let min_zoom = 1.0;
let max_zoom = 128.0;
zoom.clamp(min_zoom, max_zoom)
}
fn clamp_center_frequency(frequency: f32, zoom: f32) -> f32 {
let max_freq = 1.0 - 1.0 / zoom;
frequency.clamp(-max_freq, max_freq)
}
fn units_per_px(render_engine: &RenderEngine, waterfall: &Waterfall) -> f32 {
let canvas_width = render_engine.canvas_dims().css_pixels().0;
let width_units = 2.0 / waterfall.get_zoom();
width_units / canvas_width as f32
}
fn apply_dilation(
canvas: &HtmlCanvasElement,
render_engine: &RenderEngine,
waterfall: &mut Waterfall,
dilation: f32,
center: i32,
) {
let center = center - canvas.get_bounding_client_rect().x().round() as i32;
let zoom = waterfall.get_zoom();
let new_zoom = Self::clamp_zoom(dilation * zoom);
if new_zoom == zoom {
return;
}
let units_per_px = Self::units_per_px(render_engine, waterfall);
let freq = waterfall.get_center_frequency();
let center = freq + center as f32 * units_per_px - 1.0 / zoom;
let freq = ((dilation - 1.0) * center + freq) / dilation;
let freq = Self::clamp_center_frequency(freq, new_zoom);
waterfall.set_zoom(new_zoom);
waterfall.set_center_frequency(freq);
}
fn onwheel(&self) -> Closure<dyn Fn(WheelEvent)> {
let render_engine = Rc::clone(&self.render_engine);
let waterfall = Rc::clone(&self.waterfall);
let canvas = Rc::clone(&self.canvas);
Closure::new(move |event: WheelEvent| {
event.prevent_default();
let dilation = (-1e-3 * event.delta_y() as f32).exp();
let center = event.client_x();
Self::apply_dilation(
&canvas,
&render_engine.borrow(),
&mut waterfall.borrow_mut(),
dilation,
center,
);
})
}
fn onpointerdown(&self) -> Closure<dyn Fn(PointerEvent)> {
let canvas = Rc::clone(&self.canvas);
let pointer_tracker = Rc::clone(&self.pointer_tracker);
Closure::new(move |event: PointerEvent| {
canvas.style().set_property("cursor", "col-resize").unwrap();
pointer_tracker.borrow_mut().on_pointer_down(event);
})
}
fn onpointerup(&self) -> Closure<dyn Fn(PointerEvent)> {
let interaction = self.clone();
Closure::new(move |event: PointerEvent| {
let mut pointer_tracker = interaction.pointer_tracker.borrow_mut();
pointer_tracker.on_pointer_up(event);
if !pointer_tracker.has_active_pointers() {
interaction
.canvas
.style()
.set_property("cursor", "crosshair")
.unwrap();
*interaction.center_freq_overflow.borrow_mut() = 0.0;
}
})
}
fn onpointermove(&self) -> Closure<dyn Fn(PointerEvent)> {
let interaction = self.clone();
Closure::new(move |event: PointerEvent| {
if let Some(gesture) = interaction
.pointer_tracker
.borrow_mut()
.on_pointer_move(event)
{
interaction.process_gesture(gesture).unwrap();
}
})
}
fn process_gesture(&self, gesture: PointerGesture) -> Result<(), JsValue> {
match gesture {
PointerGesture::Drag { dx, .. } => {
let mut waterfall = self.waterfall.borrow_mut();
let units_per_px = Self::units_per_px(&self.render_engine.borrow(), &waterfall);
let freq = waterfall.get_center_frequency() - (dx as f32 * units_per_px);
let clamped = Self::clamp_center_frequency(freq, waterfall.get_zoom());
let mut overflow = self.center_freq_overflow.borrow_mut();
*overflow += freq - clamped;
let shift_threshold = 0.25;
match self.ui.borrow().as_ref() {
Some(ui) if overflow.abs() >= shift_threshold => {
let shift = shift_threshold.copysign(*overflow);
*overflow -= shift;
let (fc, fs) = waterfall.get_freq_samprate();
let new_fc = fc + 0.5 * f64::from(shift) * fs;
ui.set_rx_lo_frequency(new_fc as u64)?;
}
_ => waterfall.set_center_frequency(clamped),
}
}
PointerGesture::Pinch { center, dilation } => Self::apply_dilation(
&self.canvas,
&self.render_engine.borrow(),
&mut self.waterfall.borrow_mut(),
dilation.0,
center.0,
),
}
Ok(())
}
}