lots of rendering work & the font pain begins
This commit is contained in:
parent
95f27171f8
commit
7df3f8b3a8
|
@ -26,6 +26,7 @@ winit = { version = "0.29", features = ["rwh_05"], default-features = false }
|
|||
wgpu = { version = "0.18", features = ["webgl"] }
|
||||
raw-window-handle = "0.5"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
glyphon = { git = "https://github.com/grovesNL/glyphon" }
|
||||
|
||||
[dependencies.js-sys]
|
||||
version = "0.3"
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -1,5 +1,5 @@
|
|||
use nexrad2::message31::MOMENT_DATA_FOLDED;
|
||||
use crate::mode::Mode;
|
||||
use nexrad2::message31::MOMENT_DATA_FOLDED;
|
||||
|
||||
pub fn correlation_coefficient(val: f32) -> &'static str {
|
||||
let gradient = [
|
||||
|
@ -24,7 +24,7 @@ pub fn correlation_coefficient(val: f32) -> &'static str {
|
|||
(1.02, "purple"),
|
||||
(1.03, "mediumvioletred"),
|
||||
(1.045, "pink"),
|
||||
(1.05, "lavenderblush")
|
||||
(1.05, "lavenderblush"),
|
||||
];
|
||||
|
||||
for (threshold, color) in gradient {
|
||||
|
@ -61,7 +61,7 @@ pub fn spectrum_width(val: f32) -> &'static str {
|
|||
(32.0, "white"),
|
||||
(35.0, "yellow"),
|
||||
(60.0, "lime"),
|
||||
(1000.0, "purple")
|
||||
(1000.0, "purple"),
|
||||
];
|
||||
|
||||
for (threshold, color) in gradient {
|
||||
|
@ -96,7 +96,7 @@ pub fn differential_reflectivity(val: f32) -> &'static str {
|
|||
(6.0, "maroon"),
|
||||
(7.0, "hotpink"),
|
||||
(10.0, "pink"),
|
||||
(999.0, "white")
|
||||
(999.0, "white"),
|
||||
];
|
||||
|
||||
for (threshold, color) in gradient {
|
||||
|
@ -183,7 +183,13 @@ pub fn velocity(vel: f32) -> &'static str {
|
|||
"#520106", // 140
|
||||
];
|
||||
|
||||
let i = scale_int((vel.floor()) as i32, 140, -140, (colors.len() - 1) as i32, 0);
|
||||
let i = scale_int(
|
||||
(vel.floor()) as i32,
|
||||
140,
|
||||
-140,
|
||||
(colors.len() - 1) as i32,
|
||||
0,
|
||||
);
|
||||
|
||||
colors[i as usize]
|
||||
}
|
||||
|
@ -201,6 +207,6 @@ pub fn color_scheme(product: Mode, value: f32) -> &'static str {
|
|||
Mode::DifferentialPhase => correlation_coefficient(value),
|
||||
Mode::CorrelationCoefficient => correlation_coefficient(value),
|
||||
Mode::ClutterFilterPowerRemoved => dbz_noaa(value),
|
||||
Mode::RadarInoperative => "#ff0000"
|
||||
Mode::RadarInoperative => "#ff0000",
|
||||
}
|
||||
}
|
|
@ -1,11 +1,10 @@
|
|||
use web_sys::FileReader;
|
||||
use crate::loadar2;
|
||||
use crate::mode::Mode;
|
||||
use crate::scope::ScopeState;
|
||||
use web_sys::FileReader;
|
||||
|
||||
pub fn should_newline(state: &mut ScopeState) -> bool {
|
||||
state.command_buf.starts_with("MODE SET")
|
||||
|| state.command_buf.starts_with("ELEVATION SET")
|
||||
state.command_buf.starts_with("MODE SET") || state.command_buf.starts_with("ELEVATION SET")
|
||||
}
|
||||
|
||||
pub fn exec(state: &mut ScopeState, command: String) {
|
||||
|
@ -20,7 +19,7 @@ pub fn exec(state: &mut ScopeState, command: String) {
|
|||
if tokens.len() < 3 {
|
||||
state.command_buf = "ARGUMENT INVALID".to_string();
|
||||
state.command_buf_response_mode = true;
|
||||
return
|
||||
return;
|
||||
} else {
|
||||
let mode = tokens[2];
|
||||
if let Some(ar2) = &state.ar2 {
|
||||
|
@ -30,7 +29,7 @@ pub fn exec(state: &mut ScopeState, command: String) {
|
|||
if !valid_modes.contains(&&mode.to_string()) {
|
||||
state.command_buf = "ARGUMENT INVALID".to_string();
|
||||
state.command_buf_response_mode = true;
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
state.scope_mode = match mode {
|
||||
|
@ -41,19 +40,19 @@ pub fn exec(state: &mut ScopeState, command: String) {
|
|||
"PHI" => Mode::DifferentialPhase,
|
||||
"RHO" => Mode::CorrelationCoefficient,
|
||||
"CFP" => Mode::ClutterFilterPowerRemoved,
|
||||
_ => unreachable!()
|
||||
_ => unreachable!(),
|
||||
};
|
||||
} else {
|
||||
state.command_buf = "CANNOT SET MODE\nRADAR INOPERATIVE".to_string();
|
||||
state.command_buf_response_mode = true;
|
||||
return
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if command.starts_with("ELEVATION SET") {
|
||||
if tokens.len() < 3 {
|
||||
state.command_buf = "ARGUMENT INVALID".to_string();
|
||||
state.command_buf_response_mode = true;
|
||||
return
|
||||
return;
|
||||
} else {
|
||||
let elev = tokens[2].to_string();
|
||||
if let Some(ar2) = &state.ar2 {
|
||||
|
@ -64,7 +63,7 @@ pub fn exec(state: &mut ScopeState, command: String) {
|
|||
if !valid_elevs.contains(&elev) {
|
||||
state.command_buf = "ARGUMENT INVALID".to_string();
|
||||
state.command_buf_response_mode = true;
|
||||
return
|
||||
return;
|
||||
}
|
||||
let number: usize = elev.parse().unwrap();
|
||||
|
||||
|
@ -74,7 +73,7 @@ pub fn exec(state: &mut ScopeState, command: String) {
|
|||
} else {
|
||||
state.command_buf = "CANNOT SET MODE\nRADAR INOPERATIVE".to_string();
|
||||
state.command_buf_response_mode = true;
|
||||
return
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if command == "`T" {
|
||||
|
|
|
@ -3,5 +3,8 @@ pub const EARTH_RADIUS_MILES: f64 = 3958.8;
|
|||
pub fn latlong_to_xy(lat: f64, long: f64) -> (f64, f64) {
|
||||
// x = r λ cos(φ0)
|
||||
// y = r φ
|
||||
(EARTH_RADIUS_MILES * long * lat.cos(), EARTH_RADIUS_MILES * lat)
|
||||
(
|
||||
EARTH_RADIUS_MILES * long * lat.cos(),
|
||||
EARTH_RADIUS_MILES * lat,
|
||||
)
|
||||
}
|
|
@ -1,31 +1,32 @@
|
|||
pub mod utils;
|
||||
pub mod colors;
|
||||
pub mod command;
|
||||
pub mod equirectangular;
|
||||
pub mod mode;
|
||||
pub mod scope;
|
||||
pub mod equirectangular;
|
||||
pub mod sites;
|
||||
pub mod command;
|
||||
pub mod text;
|
||||
pub mod utils;
|
||||
pub mod vcp;
|
||||
pub mod colors;
|
||||
|
||||
use std::io::Cursor;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use js_sys::Uint8Array;
|
||||
use log::{debug, error, info};
|
||||
use wasm_bindgen::prelude::*;
|
||||
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement};
|
||||
use wgpu::SurfaceError;
|
||||
use winit::dpi::PhysicalSize;
|
||||
use winit::event::{ElementState, Event, WindowEvent};
|
||||
use winit::event::WindowEvent::KeyboardInput;
|
||||
use winit::event_loop::{EventLoop, EventLoopWindowTarget};
|
||||
use winit::keyboard::KeyCode;
|
||||
use winit::window::WindowBuilder;
|
||||
use winit::platform::web::{WindowExtWebSys, WindowBuilderExtWebSys, EventLoopExtWebSys};
|
||||
use crate::command::{exec, should_newline};
|
||||
use crate::mode::Mode;
|
||||
use crate::mode::Mode::Reflectivity;
|
||||
use crate::scope::{Preferences, ScopeState, WgpuState};
|
||||
|
||||
use glyphon::{Metrics, TextBounds};
|
||||
use js_sys::Uint8Array;
|
||||
use log::{debug, error, info};
|
||||
use std::io::Cursor;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use wasm_bindgen::prelude::*;
|
||||
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement};
|
||||
use wgpu::SurfaceError;
|
||||
use winit::dpi::PhysicalSize;
|
||||
use winit::event::WindowEvent::KeyboardInput;
|
||||
use winit::event::{ElementState, Event, WindowEvent};
|
||||
use winit::event_loop::{EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget};
|
||||
use winit::keyboard::KeyCode;
|
||||
use winit::platform::web::{EventLoopExtWebSys, WindowBuilderExtWebSys, WindowExtWebSys};
|
||||
use winit::window::WindowBuilder;
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
|
@ -33,22 +34,31 @@ extern "C" {
|
|||
fn loadar2(c: &str);
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct AbiScopeState(ScopeState);
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum RenderMessage {
|
||||
Resize { new_w: u32, new_h: u32 },
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub async fn __nxrd_browser_init(w: u32, h: u32) -> AbiScopeState {
|
||||
pub struct AbiProxy(EventLoopProxy<RenderMessage>);
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub async fn __nxrd_browser_init(w: u32, h: u32) -> AbiProxy {
|
||||
wasm_logger::init(wasm_logger::Config::new(log::Level::Debug));
|
||||
utils::set_panic_hook();
|
||||
|
||||
info!("wgpu setup");
|
||||
|
||||
let event_loop = EventLoop::new().expect("event loop creation failed");
|
||||
let window = WindowBuilder::new().with_prevent_default(false).build(&event_loop).unwrap();
|
||||
let event_loop = EventLoopBuilder::<RenderMessage>::with_user_event()
|
||||
.build()
|
||||
.expect("event loop creation failed");
|
||||
let window = WindowBuilder::new()
|
||||
.with_prevent_default(false)
|
||||
.build(&event_loop)
|
||||
.unwrap();
|
||||
|
||||
window.set_min_inner_size(Some(PhysicalSize::new(w, h)));
|
||||
|
||||
|
||||
web_sys::window()
|
||||
.and_then(|win| win.document())
|
||||
.and_then(|doc| {
|
||||
|
@ -62,70 +72,115 @@ pub async fn __nxrd_browser_init(w: u32, h: u32) -> AbiScopeState {
|
|||
|
||||
let mut render_state = WgpuState::new(window, PhysicalSize::new(w, h)).await;
|
||||
|
||||
|
||||
// If you see an error here, your IDE is not compiling for webassembly
|
||||
event_loop.spawn(move |event: Event<_>, window: &EventLoopWindowTarget<_>| {
|
||||
match event {
|
||||
Event::WindowEvent { ref event, window_id} => {
|
||||
match event {
|
||||
KeyboardInput { event, .. } => {
|
||||
debug!("{:?}", event.physical_key);
|
||||
},
|
||||
WindowEvent::CloseRequested => { window.exit(); },
|
||||
WindowEvent::Resized(physical_size) => {
|
||||
render_state.reconfigure(*physical_size);
|
||||
},
|
||||
WindowEvent::RedrawRequested => {
|
||||
render_state.update();
|
||||
match render_state.render() {
|
||||
Ok(_) => {},
|
||||
Err(SurfaceError::Lost) => render_state.reconfigure(render_state.size),
|
||||
Err(SurfaceError::OutOfMemory) => {
|
||||
error!("out of memory!");
|
||||
window.exit();
|
||||
},
|
||||
Err(e) => error!("transient rendering error: {}", e),
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
Event::AboutToWait => {
|
||||
render_state.window().request_redraw();
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
|
||||
info!("initializing the scope");
|
||||
|
||||
let document = web_sys::window().expect("window should exist").document().expect("document should exist");
|
||||
let document = web_sys::window()
|
||||
.expect("window should exist")
|
||||
.document()
|
||||
.expect("document should exist");
|
||||
|
||||
info!("finding form input element");
|
||||
|
||||
let file = document.get_element_by_id("file").expect("file element should exist");
|
||||
let file: web_sys::HtmlInputElement = file.dyn_into::<web_sys::HtmlInputElement>().map_err(|_| ()).expect("file input is not an input");
|
||||
let file = document
|
||||
.get_element_by_id("file")
|
||||
.expect("file element should exist");
|
||||
let file: web_sys::HtmlInputElement = file
|
||||
.dyn_into::<web_sys::HtmlInputElement>()
|
||||
.map_err(|_| ())
|
||||
.expect("file input is not an input");
|
||||
|
||||
let scope_state = ScopeState {
|
||||
let mut scope_state = ScopeState {
|
||||
ar2: None,
|
||||
scope_mode: Mode::RadarInoperative,
|
||||
file_input: file,
|
||||
lat: 30.48500, // jacksonville
|
||||
long: -81.70200, // jacksonville
|
||||
prefs: Preferences {
|
||||
fcs: 20
|
||||
},
|
||||
prefs: Preferences { fcs: 20 },
|
||||
command_buf: String::new(),
|
||||
command_buf_response_mode: false,
|
||||
new_data_available: false,
|
||||
selected_elevation: 1,
|
||||
show_ui: false
|
||||
show_ui: false,
|
||||
wgpu: render_state,
|
||||
};
|
||||
|
||||
scope_state.wgpu.add_textarea(
|
||||
"status_bar".to_string(),
|
||||
Metrics::new(30.0, 42.0),
|
||||
10.0,
|
||||
10.0,
|
||||
1.0,
|
||||
TextBounds {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 600,
|
||||
bottom: 160,
|
||||
},
|
||||
glyphon::Color::rgb(0x4a, 0xf6, 0x26),
|
||||
"NEXRAD INOP 00:00:00Z",
|
||||
);
|
||||
|
||||
let event_proxy: EventLoopProxy<RenderMessage> = event_loop.create_proxy();
|
||||
|
||||
// If you see an error here, your IDE is not compiling for webassembly
|
||||
event_loop.spawn(
|
||||
move |event: Event<_>, window: &EventLoopWindowTarget<_>| match event {
|
||||
Event::UserEvent(e) => match e {
|
||||
RenderMessage::Resize { new_w, new_h } => {
|
||||
scope_state
|
||||
.wgpu
|
||||
.reconfigure(PhysicalSize::new(new_w, new_h));
|
||||
}
|
||||
},
|
||||
Event::WindowEvent {
|
||||
ref event,
|
||||
window_id,
|
||||
} => match event {
|
||||
KeyboardInput { event, .. } => {
|
||||
debug!("{:?}", event.physical_key);
|
||||
}
|
||||
WindowEvent::CloseRequested => {
|
||||
window.exit();
|
||||
}
|
||||
WindowEvent::Resized(physical_size) => {
|
||||
scope_state.wgpu.reconfigure(*physical_size);
|
||||
}
|
||||
WindowEvent::RedrawRequested => {
|
||||
scope_state.wgpu.update();
|
||||
match scope_state.wgpu.render() {
|
||||
Ok(_) => {}
|
||||
Err(SurfaceError::Lost) => {
|
||||
scope_state.wgpu.reconfigure(scope_state.wgpu.size)
|
||||
}
|
||||
Err(SurfaceError::OutOfMemory) => {
|
||||
error!("out of memory!");
|
||||
window.exit();
|
||||
}
|
||||
Err(e) => error!("transient rendering error: {}", e),
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Event::AboutToWait => {
|
||||
scope_state.wgpu.window().request_redraw();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
);
|
||||
|
||||
info!("nexrad-browser initialized successfully");
|
||||
|
||||
AbiScopeState(scope_state)
|
||||
AbiProxy(event_proxy)
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn resize_abi(w: u32, h: u32, proxy: &mut AbiProxy) {
|
||||
proxy
|
||||
.0
|
||||
.send_event(RenderMessage::Resize { new_w: w, new_h: h })
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/*
|
||||
#[wasm_bindgen]
|
||||
pub fn render_abi(state: &mut AbiScopeState) {
|
||||
|
@ -133,7 +188,7 @@ pub fn render_abi(state: &mut AbiScopeState) {
|
|||
}
|
||||
|
||||
*/
|
||||
|
||||
/*
|
||||
#[wasm_bindgen]
|
||||
pub fn load_ar2(buf: &JsValue, scope: &mut AbiScopeState) {
|
||||
let array = Uint8Array::new(buf);
|
||||
|
@ -175,3 +230,5 @@ pub fn keydown(state: &mut AbiScopeState, key: String) {
|
|||
state.0.command_buf += &key.to_uppercase();
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
|
|
|
@ -15,7 +15,7 @@ pub enum Mode {
|
|||
/// CFP
|
||||
ClutterFilterPowerRemoved,
|
||||
/// RADR INOP
|
||||
RadarInoperative
|
||||
RadarInoperative,
|
||||
}
|
||||
impl Mode {
|
||||
pub fn unselected(&self) -> &str {
|
||||
|
@ -27,7 +27,7 @@ impl Mode {
|
|||
Self::DifferentialPhase => " PHI ",
|
||||
Self::CorrelationCoefficient => " RHO ",
|
||||
Self::ClutterFilterPowerRemoved => " CFP ",
|
||||
Self::RadarInoperative => " RADR INOP"
|
||||
Self::RadarInoperative => " RADR INOP",
|
||||
}
|
||||
}
|
||||
pub fn selected(&self) -> &str {
|
||||
|
@ -39,7 +39,7 @@ impl Mode {
|
|||
Self::DifferentialPhase => ">PHI<",
|
||||
Self::CorrelationCoefficient => ">RHO<",
|
||||
Self::ClutterFilterPowerRemoved => ">CFP<",
|
||||
Self::RadarInoperative => ">RADR INOP<"
|
||||
Self::RadarInoperative => ">RADR INOP<",
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -52,7 +52,7 @@ impl Mode {
|
|||
Self::DifferentialPhase => "PHI",
|
||||
Self::CorrelationCoefficient => "RHO",
|
||||
Self::ClutterFilterPowerRemoved => "CFP",
|
||||
Self::RadarInoperative => "NOP"
|
||||
Self::RadarInoperative => "NOP",
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,16 +1,19 @@
|
|||
use std::f64::consts::PI;
|
||||
use chrono::{DateTime, Timelike, Utc};
|
||||
use itertools::Itertools;
|
||||
use log::{debug, info};
|
||||
use wasm_bindgen::JsValue;
|
||||
use nexrad2::message31::MOMENT_DATA_BELOW_THRESHOLD;
|
||||
use crate::colors::color_scheme;
|
||||
use crate::mode::Mode;
|
||||
use crate::mode::Mode::{ClutterFilterPowerRemoved, CorrelationCoefficient, DifferentialPhase, DifferentialReflectivity, RadarInoperative, Reflectivity, SpectrumWidth, Velocity};
|
||||
use crate::mode::Mode::{
|
||||
ClutterFilterPowerRemoved, CorrelationCoefficient, DifferentialPhase, DifferentialReflectivity,
|
||||
RadarInoperative, Reflectivity, SpectrumWidth, Velocity,
|
||||
};
|
||||
use crate::rescaleCanvas;
|
||||
use crate::scope::ScopeState;
|
||||
use crate::utils::parse_date;
|
||||
use crate::vcp::vcp_string;
|
||||
use chrono::{DateTime, Timelike, Utc};
|
||||
use itertools::Itertools;
|
||||
use log::{debug, info};
|
||||
use nexrad2::message31::MOMENT_DATA_BELOW_THRESHOLD;
|
||||
use std::f64::consts::PI;
|
||||
use wasm_bindgen::JsValue;
|
||||
|
||||
pub const TEXT_COLOR_RED: &str = "#ef0000";
|
||||
pub const TEXT_COLOR_GREEN: &str = "#4af626";
|
||||
|
@ -52,24 +55,48 @@ pub fn render(state: &mut ScopeState) -> Result<(), JsValue> {
|
|||
|
||||
// render out the scope id line
|
||||
if state.show_ui {
|
||||
ctx.fill_text(&format!("NEXRAD {} {}", state.ar2.as_ref().map(|u| u.volume_header_record.icao.as_str()).unwrap_or("INOP"), zulu(time)), 50.0, 50.0)?;
|
||||
ctx.fill_text(
|
||||
&format!(
|
||||
"NEXRAD {} {}",
|
||||
state
|
||||
.ar2
|
||||
.as_ref()
|
||||
.map(|u| u.volume_header_record.icao.as_str())
|
||||
.unwrap_or("INOP"),
|
||||
zulu(time)
|
||||
),
|
||||
50.0,
|
||||
50.0,
|
||||
)?;
|
||||
if state.ar2.is_none() {
|
||||
// inop alert
|
||||
ctx.set_fill_style(&JsValue::from_str(TEXT_COLOR_RED));
|
||||
ctx.fill_text("RADAR INOPERATIVE NO DATA LOADED", 50.0, 50.0 + (state.prefs.fcs as f64))?;
|
||||
ctx.fill_text(
|
||||
"RADAR INOPERATIVE NO DATA LOADED",
|
||||
50.0,
|
||||
50.0 + (state.prefs.fcs as f64),
|
||||
)?;
|
||||
ctx.set_fill_style(&JsValue::from_str(TEXT_COLOR_GREEN));
|
||||
}
|
||||
if state.new_data_available {
|
||||
// inop alert
|
||||
ctx.set_fill_style(&JsValue::from_str(TEXT_COLOR_RED));
|
||||
ctx.fill_text("NEW DATA AVAIL RLD RQD", 50.0, 50.0 + (state.prefs.fcs as f64) * 2.0)?;
|
||||
ctx.fill_text(
|
||||
"NEW DATA AVAIL RLD RQD",
|
||||
50.0,
|
||||
50.0 + (state.prefs.fcs as f64) * 2.0,
|
||||
)?;
|
||||
ctx.set_fill_style(&JsValue::from_str(TEXT_COLOR_GREEN));
|
||||
}
|
||||
}
|
||||
|
||||
// render the command buffer
|
||||
for (line_no, line) in state.command_buf.split('\n').enumerate() {
|
||||
ctx.fill_text(line, 50.0, (canvas.height() / 3) as f64 + (state.prefs.fcs * line_no) as f64)?;
|
||||
ctx.fill_text(
|
||||
line,
|
||||
50.0,
|
||||
(canvas.height() / 3) as f64 + (state.prefs.fcs * line_no) as f64,
|
||||
)?;
|
||||
}
|
||||
|
||||
// render the radar site info
|
||||
|
@ -77,7 +104,16 @@ pub fn render(state: &mut ScopeState) -> Result<(), JsValue> {
|
|||
ctx.set_text_align("right");
|
||||
ctx.fill_text("RADAR SITE", (canvas.width() - 50) as f64, 50.0)?;
|
||||
if let Some(ar2) = &state.ar2 {
|
||||
ctx.fill_text(&format!("{} VCP {} {}", ar2.volume_header_record.icao, ar2.meta_rda_status_data.vcp, vcp_string(ar2.meta_rda_status_data.vcp)), (canvas.width() - 50) as f64, 50.0 + (state.prefs.fcs as f64))?;
|
||||
ctx.fill_text(
|
||||
&format!(
|
||||
"{} VCP {} {}",
|
||||
ar2.volume_header_record.icao,
|
||||
ar2.meta_rda_status_data.vcp,
|
||||
vcp_string(ar2.meta_rda_status_data.vcp)
|
||||
),
|
||||
(canvas.width() - 50) as f64,
|
||||
50.0 + (state.prefs.fcs as f64),
|
||||
)?;
|
||||
let recorded = parse_date(ar2.volume_header_record.date, ar2.volume_header_record.time);
|
||||
let delay = time - recorded;
|
||||
|
||||
|
@ -85,18 +121,39 @@ pub fn render(state: &mut ScopeState) -> Result<(), JsValue> {
|
|||
let minutes = (delay.num_seconds() / 60) % 60;
|
||||
let hours = delay.num_seconds() / 3600;
|
||||
|
||||
ctx.fill_text(&format!("SITE DELAY {:0>2}:{:0>2}:{:0>2}", hours, minutes, seconds), (canvas.width() - 50) as f64, 50.0 + (state.prefs.fcs as f64 * 2.0))?;
|
||||
ctx.fill_text(
|
||||
&format!("SITE DELAY {:0>2}:{:0>2}:{:0>2}", hours, minutes, seconds),
|
||||
(canvas.width() - 50) as f64,
|
||||
50.0 + (state.prefs.fcs as f64 * 2.0),
|
||||
)?;
|
||||
} else {
|
||||
ctx.fill_text("SITE INFORMATION UNAVAILABLE", (canvas.width() - 50) as f64, 50.0 + state.prefs.fcs as f64)?;
|
||||
ctx.fill_text("DELAY INFORMATION UNAVAILABLE", (canvas.width() - 50) as f64, 50.0 + (state.prefs.fcs * 2) as f64)?;
|
||||
ctx.fill_text(
|
||||
"SITE INFORMATION UNAVAILABLE",
|
||||
(canvas.width() - 50) as f64,
|
||||
50.0 + state.prefs.fcs as f64,
|
||||
)?;
|
||||
ctx.fill_text(
|
||||
"DELAY INFORMATION UNAVAILABLE",
|
||||
(canvas.width() - 50) as f64,
|
||||
50.0 + (state.prefs.fcs * 2) as f64,
|
||||
)?;
|
||||
}
|
||||
|
||||
// mode info
|
||||
ctx.fill_text("MODE", (canvas.width() - 50) as f64, (canvas.height() / 3) as f64)?;
|
||||
ctx.fill_text(
|
||||
"MODE",
|
||||
(canvas.width() - 50) as f64,
|
||||
(canvas.height() / 3) as f64,
|
||||
)?;
|
||||
let modelines = [
|
||||
[Reflectivity, Velocity, SpectrumWidth].as_slice(),
|
||||
[DifferentialReflectivity, DifferentialPhase, CorrelationCoefficient].as_slice(),
|
||||
[ClutterFilterPowerRemoved, RadarInoperative].as_slice()
|
||||
[
|
||||
DifferentialReflectivity,
|
||||
DifferentialPhase,
|
||||
CorrelationCoefficient,
|
||||
]
|
||||
.as_slice(),
|
||||
[ClutterFilterPowerRemoved, RadarInoperative].as_slice(),
|
||||
];
|
||||
|
||||
let mut available_modes = vec![];
|
||||
|
@ -114,7 +171,11 @@ pub fn render(state: &mut ScopeState) -> Result<(), JsValue> {
|
|||
let pad_end = 2 - item_no;
|
||||
let pad_start = " ".repeat(pad_start * 6);
|
||||
let pad_end = " ".repeat(pad_end * 6);
|
||||
let middle = if state.scope_mode == *item { item.selected() } else { item.unselected() };
|
||||
let middle = if state.scope_mode == *item {
|
||||
item.selected()
|
||||
} else {
|
||||
item.unselected()
|
||||
};
|
||||
|
||||
if state.scope_mode == *item {
|
||||
// display selected color
|
||||
|
@ -130,7 +191,6 @@ pub fn render(state: &mut ScopeState) -> Result<(), JsValue> {
|
|||
continue;
|
||||
}
|
||||
|
||||
|
||||
if available_modes.contains(&&item.rname().to_string()) {
|
||||
ctx.fill_text(&format!(" {}{}{}", pad_start, middle, pad_end), x, y)?;
|
||||
}
|
||||
|
@ -142,26 +202,56 @@ pub fn render(state: &mut ScopeState) -> Result<(), JsValue> {
|
|||
}
|
||||
}
|
||||
|
||||
ctx.fill_text("ELEVATIONS", (canvas.width() - 50) as f64, ((canvas.height() / 7) * 4) as f64)?;
|
||||
ctx.fill_text(
|
||||
"ELEVATIONS",
|
||||
(canvas.width() - 50) as f64,
|
||||
((canvas.height() / 7) * 4) as f64,
|
||||
)?;
|
||||
if let Some(ar2) = &state.ar2 {
|
||||
let mut line_no = 0;
|
||||
|
||||
for chunk in &ar2.elevations.iter().sorted_by(|a, b| {
|
||||
Ord::cmp(&a.0, &b.0)
|
||||
}).enumerate().chunks(4) {
|
||||
for chunk in &ar2
|
||||
.elevations
|
||||
.iter()
|
||||
.sorted_by(|a, b| Ord::cmp(&a.0, &b.0))
|
||||
.enumerate()
|
||||
.chunks(4)
|
||||
{
|
||||
for (no_in_line, (elevation_no, scans)) in chunk {
|
||||
if state.selected_elevation != *elevation_no {
|
||||
ctx.fill_text(&format!(" {:0>3} {}", elevation_no, " ".repeat(5 * (3 - (no_in_line % 4)))), (canvas.width() - 50) as f64, ((canvas.height() / 7) * 4) as f64 + (state.prefs.fcs * (line_no + 1)) as f64)?;
|
||||
ctx.fill_text(
|
||||
&format!(
|
||||
" {:0>3} {}",
|
||||
elevation_no,
|
||||
" ".repeat(5 * (3 - (no_in_line % 4)))
|
||||
),
|
||||
(canvas.width() - 50) as f64,
|
||||
((canvas.height() / 7) * 4) as f64
|
||||
+ (state.prefs.fcs * (line_no + 1)) as f64,
|
||||
)?;
|
||||
} else {
|
||||
ctx.set_fill_style(&JsValue::from_str(TEXT_COLOR_WHITE));
|
||||
ctx.fill_text(&format!(">{:0>3}<{}", elevation_no, " ".repeat(5 * (3 - (no_in_line % 4)))), (canvas.width() - 50) as f64, ((canvas.height() / 7) * 4) as f64 + (state.prefs.fcs * (line_no + 1)) as f64)?;
|
||||
ctx.fill_text(
|
||||
&format!(
|
||||
">{:0>3}<{}",
|
||||
elevation_no,
|
||||
" ".repeat(5 * (3 - (no_in_line % 4)))
|
||||
),
|
||||
(canvas.width() - 50) as f64,
|
||||
((canvas.height() / 7) * 4) as f64
|
||||
+ (state.prefs.fcs * (line_no + 1)) as f64,
|
||||
)?;
|
||||
ctx.set_fill_style(&JsValue::from_str(TEXT_COLOR_GREEN));
|
||||
}
|
||||
}
|
||||
line_no += 1;
|
||||
}
|
||||
} else {
|
||||
ctx.fill_text("ELEVATION INFORMATION UNAVAILABLE", (canvas.width() - 50) as f64, ((canvas.height() / 7) * 4) as f64 + state.prefs.fcs as f64)?;
|
||||
ctx.fill_text(
|
||||
"ELEVATION INFORMATION UNAVAILABLE",
|
||||
(canvas.width() - 50) as f64,
|
||||
((canvas.height() / 7) * 4) as f64 + state.prefs.fcs as f64,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -173,8 +263,21 @@ pub fn render(state: &mut ScopeState) -> Result<(), JsValue> {
|
|||
|
||||
let radials = ar2.elevations.get(&state.selected_elevation).unwrap();
|
||||
|
||||
let first_gate_px = (radials[0].available_data.get("REF").expect("reflectivity is missing!").gdm.data_moment_range) as f32 / 1000.0 * px_per_km;
|
||||
let gate_interval_km = (radials[0].available_data.get("REF").expect("reflectivity is missing!").gdm.data_moment_range_sample_interval) as f32 / 1000.0;
|
||||
let first_gate_px = (radials[0]
|
||||
.available_data
|
||||
.get("REF")
|
||||
.expect("reflectivity is missing!")
|
||||
.gdm
|
||||
.data_moment_range) as f32
|
||||
/ 1000.0
|
||||
* px_per_km;
|
||||
let gate_interval_km = (radials[0]
|
||||
.available_data
|
||||
.get("REF")
|
||||
.expect("reflectivity is missing!")
|
||||
.gdm
|
||||
.data_moment_range_sample_interval) as f32
|
||||
/ 1000.0;
|
||||
let gate_width_px = gate_interval_km * px_per_km;
|
||||
|
||||
for radial in radials {
|
||||
|
@ -188,7 +291,7 @@ pub fn render(state: &mut ScopeState) -> Result<(), JsValue> {
|
|||
|
||||
let azimuth_spacing = match radial.header.azimuth_resolution_spacing {
|
||||
1 => 0.5,
|
||||
_ => 1.0
|
||||
_ => 1.0,
|
||||
};
|
||||
if (azimuth_angle + azimuth_spacing).floor() > azimuth {
|
||||
azimuth += azimuth_spacing;
|
||||
|
@ -205,26 +308,60 @@ pub fn render(state: &mut ScopeState) -> Result<(), JsValue> {
|
|||
// line width
|
||||
// line cap
|
||||
|
||||
let gates = radial.available_data.get(state.scope_mode.rname()).expect("selected unavailable product").scaled_data();
|
||||
let gates = radial
|
||||
.available_data
|
||||
.get(state.scope_mode.rname())
|
||||
.expect("selected unavailable product")
|
||||
.scaled_data();
|
||||
|
||||
let num_gates = gates.len();
|
||||
|
||||
for (num, value) in gates.iter().enumerate() {
|
||||
if *value != MOMENT_DATA_BELOW_THRESHOLD {
|
||||
|
||||
ctx.move_to(xc as f64 + start_angle.cos() * distance_x as f64, yc as f64 + start_angle.sin() * distance_y as f64);
|
||||
ctx.move_to(
|
||||
xc as f64 + start_angle.cos() * distance_x as f64,
|
||||
yc as f64 + start_angle.sin() * distance_y as f64,
|
||||
);
|
||||
|
||||
ctx.begin_path();
|
||||
|
||||
if num == 0 {
|
||||
ctx.ellipse(xc as f64, yc as f64, distance_x as f64, distance_y as f64, 0.0, start_angle - 0.001, end_angle + 0.001)?;
|
||||
ctx.ellipse(
|
||||
xc as f64,
|
||||
yc as f64,
|
||||
distance_x as f64,
|
||||
distance_y as f64,
|
||||
0.0,
|
||||
start_angle - 0.001,
|
||||
end_angle + 0.001,
|
||||
)?;
|
||||
} else if num == num_gates - 1 {
|
||||
ctx.ellipse(xc as f64, yc as f64, distance_x as f64, distance_y as f64, 0.0, start_angle, end_angle)?;
|
||||
ctx.ellipse(
|
||||
xc as f64,
|
||||
yc as f64,
|
||||
distance_x as f64,
|
||||
distance_y as f64,
|
||||
0.0,
|
||||
start_angle,
|
||||
end_angle,
|
||||
)?;
|
||||
} else {
|
||||
ctx.ellipse(xc as f64, yc as f64, distance_x as f64, distance_y as f64, 0.0, start_angle, end_angle + 0.001)?;
|
||||
ctx.ellipse(
|
||||
xc as f64,
|
||||
yc as f64,
|
||||
distance_x as f64,
|
||||
distance_y as f64,
|
||||
0.0,
|
||||
start_angle,
|
||||
end_angle + 0.001,
|
||||
)?;
|
||||
}
|
||||
|
||||
ctx.set_stroke_style(&JsValue::from_str(&format!("{}px {}", gate_width_px + 1.0, color_scheme(state.scope_mode, *value))));
|
||||
ctx.set_stroke_style(&JsValue::from_str(&format!(
|
||||
"{}px {}",
|
||||
gate_width_px + 1.0,
|
||||
color_scheme(state.scope_mode, *value)
|
||||
)));
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,26 @@
|
|||
use crate::mode::Mode;
|
||||
use crate::text::OTextArea;
|
||||
use glyphon::{
|
||||
Attrs, Buffer, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache, TextArea,
|
||||
TextAtlas, TextBounds, TextRenderer,
|
||||
};
|
||||
use nexrad2::Nexrad2Chunk;
|
||||
use raw_window_handle::HasRawDisplayHandle;
|
||||
use std::collections::HashMap;
|
||||
use std::iter;
|
||||
use std::sync::Arc;
|
||||
use glyphon::fontdb::Source;
|
||||
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, HtmlInputElement};
|
||||
use wgpu::{Backends, Color, CommandEncoderDescriptor, Device, Features, Instance, InstanceDescriptor, Limits, LoadOp, Operations, Queue, RenderPassColorAttachment, RenderPassDescriptor, StoreOp, Surface, SurfaceConfiguration, SurfaceError, TextureUsages, TextureViewDescriptor};
|
||||
use wgpu::DeviceDescriptor;
|
||||
use wgpu::{
|
||||
Backends, Color, CommandEncoderDescriptor, Device, Features, Instance, InstanceDescriptor,
|
||||
Limits, LoadOp, MultisampleState, Operations, Queue, RenderPassColorAttachment,
|
||||
RenderPassDescriptor, StoreOp, Surface, SurfaceConfiguration, SurfaceError, TextureUsages,
|
||||
TextureViewDescriptor,
|
||||
};
|
||||
use winit::dpi::PhysicalSize;
|
||||
use winit::event::WindowEvent;
|
||||
use raw_window_handle::HasRawDisplayHandle;
|
||||
use winit::window::Window;
|
||||
use nexrad2::Nexrad2Chunk;
|
||||
use crate::mode::Mode;
|
||||
use wgpu::DeviceDescriptor;
|
||||
|
||||
pub struct ScopeState {
|
||||
pub ar2: Option<Nexrad2Chunk>,
|
||||
|
@ -20,7 +33,8 @@ pub struct ScopeState {
|
|||
pub command_buf_response_mode: bool,
|
||||
pub new_data_available: bool,
|
||||
pub selected_elevation: usize,
|
||||
pub show_ui: bool
|
||||
pub show_ui: bool,
|
||||
pub wgpu: WgpuState,
|
||||
}
|
||||
|
||||
pub struct WgpuState {
|
||||
|
@ -29,7 +43,12 @@ pub struct WgpuState {
|
|||
pub queue: Queue,
|
||||
pub surface_config: SurfaceConfiguration,
|
||||
pub size: PhysicalSize<u32>,
|
||||
pub window: Window
|
||||
pub window: Window,
|
||||
pub font_system: FontSystem,
|
||||
pub font_cache: SwashCache,
|
||||
pub atlas: TextAtlas,
|
||||
pub text_renderer: TextRenderer,
|
||||
pub text_buffers: HashMap<String, OTextArea>,
|
||||
}
|
||||
|
||||
impl WgpuState {
|
||||
|
@ -40,30 +59,34 @@ impl WgpuState {
|
|||
dx12_shader_compiler: Default::default(),
|
||||
gles_minor_version: Default::default(),
|
||||
});
|
||||
let surface = unsafe {
|
||||
instance.create_surface(&window).unwrap()
|
||||
};
|
||||
let surface = unsafe { instance.create_surface(&window).unwrap() };
|
||||
|
||||
let adapter = instance.request_adapter(
|
||||
&wgpu::RequestAdapterOptions {
|
||||
let adapter = instance
|
||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
power_preference: wgpu::PowerPreference::default(),
|
||||
compatible_surface: Some(&surface),
|
||||
force_fallback_adapter: false,
|
||||
},
|
||||
).await.unwrap();
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (device, queue) = adapter.request_device(
|
||||
let (device, queue) = adapter
|
||||
.request_device(
|
||||
&DeviceDescriptor {
|
||||
features: Features::empty(),
|
||||
limits: Limits::downlevel_webgl2_defaults(),
|
||||
label: None,
|
||||
},
|
||||
None, // Trace path
|
||||
).await.unwrap();
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let surface_capabilities = surface.get_capabilities(&adapter);
|
||||
|
||||
let surface_format = surface_capabilities.formats.iter()
|
||||
let surface_format = surface_capabilities
|
||||
.formats
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|f| f.is_srgb())
|
||||
.unwrap_or(surface_capabilities.formats[0]);
|
||||
|
@ -80,13 +103,27 @@ impl WgpuState {
|
|||
|
||||
surface.configure(&device, &surface_config);
|
||||
|
||||
let mut fonts = vec![];
|
||||
fonts.push(Source::Binary(Arc::new(include_bytes!("./VT323-Regular.ttf"))));
|
||||
|
||||
let mut font_system = FontSystem::new_with_fonts(fonts.into_iter());
|
||||
let mut font_cache = SwashCache::new();
|
||||
let mut atlas = TextAtlas::new(&device, &queue, surface_format);
|
||||
let mut text_renderer =
|
||||
TextRenderer::new(&mut atlas, &device, MultisampleState::default(), None);
|
||||
|
||||
Self {
|
||||
window,
|
||||
surface,
|
||||
device,
|
||||
queue,
|
||||
surface_config,
|
||||
size
|
||||
size,
|
||||
font_system,
|
||||
font_cache,
|
||||
atlas,
|
||||
text_renderer,
|
||||
text_buffers: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -107,29 +144,70 @@ impl WgpuState {
|
|||
false
|
||||
}
|
||||
|
||||
pub fn update(&mut self) {
|
||||
|
||||
}
|
||||
pub fn update(&mut self) {}
|
||||
|
||||
pub fn render(&mut self) -> Result<(), SurfaceError> {
|
||||
let output = self.surface.get_current_texture()?;
|
||||
let view = output.texture.create_view(&TextureViewDescriptor::default());
|
||||
let mut encoder = self.device.create_command_encoder(&CommandEncoderDescriptor {
|
||||
let view = output
|
||||
.texture
|
||||
.create_view(&TextureViewDescriptor::default());
|
||||
let mut encoder = self
|
||||
.device
|
||||
.create_command_encoder(&CommandEncoderDescriptor {
|
||||
label: Some("Render Encoder"),
|
||||
});
|
||||
|
||||
let physical_width = (self.surface_config.width as f64 * self.window.scale_factor()) as f32;
|
||||
let physical_height =
|
||||
(self.surface_config.height as f64 * self.window.scale_factor()) as f32;
|
||||
|
||||
for textarea in self.text_buffers.values_mut() {
|
||||
// resize the buf
|
||||
textarea
|
||||
.buffer
|
||||
.set_size(&mut self.font_system, physical_width, physical_height);
|
||||
}
|
||||
|
||||
let textareas = self
|
||||
.text_buffers
|
||||
.values()
|
||||
.map(|u| TextArea {
|
||||
buffer: &u.buffer,
|
||||
left: u.left.clone() as f32,
|
||||
top: u.top.clone() as f32,
|
||||
scale: u.scale.clone() as f32,
|
||||
bounds: u.bounds.clone(),
|
||||
default_color: u.default_color.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
self.text_renderer
|
||||
.prepare(
|
||||
&self.device,
|
||||
&self.queue,
|
||||
&mut self.font_system,
|
||||
&mut self.atlas,
|
||||
Resolution {
|
||||
width: self.surface_config.width,
|
||||
height: self.surface_config.height,
|
||||
},
|
||||
textareas,
|
||||
&mut self.font_cache,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// LOCK BLOCK
|
||||
{
|
||||
let render_pass = encoder.begin_render_pass(&RenderPassDescriptor {
|
||||
let mut render_pass = encoder.begin_render_pass(&RenderPassDescriptor {
|
||||
label: Some("Render Pass"),
|
||||
color_attachments: &[Some(RenderPassColorAttachment {
|
||||
view: &view,
|
||||
resolve_target: None,
|
||||
ops: Operations {
|
||||
load: LoadOp::Clear(Color {
|
||||
r: 0.1,
|
||||
g: 0.2,
|
||||
b: 0.3,
|
||||
r: 0.0,
|
||||
g: 0.0,
|
||||
b: 0.0,
|
||||
a: 1.0,
|
||||
}),
|
||||
store: StoreOp::Store,
|
||||
|
@ -139,16 +217,61 @@ impl WgpuState {
|
|||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
});
|
||||
|
||||
self.text_renderer
|
||||
.render(&self.atlas, &mut render_pass)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
self.queue.submit(iter::once(encoder.finish()));
|
||||
output.present();
|
||||
|
||||
self.atlas.trim();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_textarea(
|
||||
&mut self,
|
||||
id: String,
|
||||
metrics: Metrics,
|
||||
left: f64,
|
||||
top: f64,
|
||||
scale: f64,
|
||||
bounds: TextBounds,
|
||||
default_color: glyphon::Color,
|
||||
start_text: &str,
|
||||
) -> &mut OTextArea {
|
||||
let mut buffer = Buffer::new(&mut self.font_system, metrics);
|
||||
|
||||
let physical_width = (self.surface_config.width as f64 * self.window.scale_factor()) as f32;
|
||||
let physical_height =
|
||||
(self.surface_config.height as f64 * self.window.scale_factor()) as f32;
|
||||
|
||||
buffer.set_size(&mut self.font_system, physical_width, physical_height);
|
||||
buffer.set_text(
|
||||
&mut self.font_system,
|
||||
start_text,
|
||||
Attrs::new().family(Family::Monospace),
|
||||
Shaping::Basic,
|
||||
);
|
||||
buffer.shape_until_scroll(&mut self.font_system);
|
||||
|
||||
let textarea = OTextArea {
|
||||
left,
|
||||
top,
|
||||
scale,
|
||||
bounds,
|
||||
default_color,
|
||||
buffer,
|
||||
};
|
||||
|
||||
self.text_buffers.insert(id.clone(), textarea);
|
||||
|
||||
self.text_buffers.get_mut(&id).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Preferences {
|
||||
pub fcs: usize
|
||||
pub fcs: usize,
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,10 @@
|
|||
use glyphon::{Buffer, Color, TextArea, TextBounds};
|
||||
|
||||
pub struct OTextArea {
|
||||
pub left: f64,
|
||||
pub top: f64,
|
||||
pub scale: f64,
|
||||
pub bounds: TextBounds,
|
||||
pub default_color: Color,
|
||||
pub buffer: Buffer,
|
||||
}
|
|
@ -1,6 +1,12 @@
|
|||
import * as wasm from "./wasm/nexrad_browser.js";
|
||||
await wasm.default();
|
||||
let global_context = wasm.__nxrd_browser_init(window.innerWidth, window.innerHeight);
|
||||
let abi_proxy = await wasm.__nxrd_browser_init(window.innerWidth, window.innerHeight);
|
||||
|
||||
window.onresize = async () => {
|
||||
console.log("window resized!");
|
||||
await wasm.resize_abi(window.innerWidth, window.innerHeight, abi_proxy);
|
||||
}
|
||||
|
||||
/*
|
||||
function rescaleCanvas(canvas, ctx) {
|
||||
var dpr = window.devicePixelRatio || 1;
|
||||
|
|
Loading…
Reference in New Issue