Better zoom/pan behavior, handle images of different aspects.
Nearest neighbor drawing, so zooming is pixalated.
This commit is contained in:
parent
a0aae68a97
commit
c4140a0618
@ -31,6 +31,7 @@ features = [
|
|||||||
"CanvasRenderingContext2d",
|
"CanvasRenderingContext2d",
|
||||||
"HtmlImageElement",
|
"HtmlImageElement",
|
||||||
"ImageData",
|
"ImageData",
|
||||||
|
"ContextAttributes2d",
|
||||||
]
|
]
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|||||||
235
src/lib.rs
235
src/lib.rs
@ -6,13 +6,13 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use chrono::{Local, NaiveDateTime, TimeZone};
|
use chrono::{Local, NaiveDateTime, TimeZone};
|
||||||
use log::Level;
|
use log::{info, Level};
|
||||||
use seed::{prelude::*, *};
|
use seed::{prelude::*, *};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use wasm_timer::{SystemTime, UNIX_EPOCH};
|
use wasm_timer::{SystemTime, UNIX_EPOCH};
|
||||||
use web_sys::{HtmlCanvasElement, HtmlImageElement};
|
use web_sys::{ContextAttributes2d, HtmlCanvasElement, HtmlImageElement};
|
||||||
|
|
||||||
const ROOT_URL: &'static str = "/www/tracer/";
|
const ROOT_URL: &'static str = "/www/tracer";
|
||||||
|
|
||||||
#[derive(Clone, Error, Debug)]
|
#[derive(Clone, Error, Debug)]
|
||||||
pub enum AppError {
|
pub enum AppError {
|
||||||
@ -27,7 +27,9 @@ pub enum AppError {
|
|||||||
// `init` describes what should happen when your app started.
|
// `init` describes what should happen when your app started.
|
||||||
fn init(_: Url, orders: &mut impl Orders<Msg>) -> Model {
|
fn init(_: Url, orders: &mut impl Orders<Msg>) -> Model {
|
||||||
let timer_handle = orders.stream_with_handle(streams::interval(1000, || Msg::OnCheck));
|
let timer_handle = orders.stream_with_handle(streams::interval(1000, || Msg::OnCheck));
|
||||||
orders.after_next_render(|_| Msg::Rendered);
|
orders
|
||||||
|
.subscribe(Msg::UrlChanged)
|
||||||
|
.after_next_render(|_| Msg::Rendered);
|
||||||
Model {
|
Model {
|
||||||
timer_handle,
|
timer_handle,
|
||||||
data: None,
|
data: None,
|
||||||
@ -66,8 +68,10 @@ enum Msg {
|
|||||||
Rendered,
|
Rendered,
|
||||||
Received(Data),
|
Received(Data),
|
||||||
Reset,
|
Reset,
|
||||||
|
SetZoom(f64, [i32; 2]),
|
||||||
Zoom(f64, [i32; 2]),
|
Zoom(f64, [i32; 2]),
|
||||||
Move([f64; 2]),
|
Move([f64; 2]),
|
||||||
|
UrlChanged(subs::UrlChanged),
|
||||||
}
|
}
|
||||||
|
|
||||||
// `update` describes how to handle each `Msg`.
|
// `update` describes how to handle each `Msg`.
|
||||||
@ -127,6 +131,14 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
model.zoom = 1.;
|
model.zoom = 1.;
|
||||||
model.offset = [0., 0.];
|
model.offset = [0., 0.];
|
||||||
}
|
}
|
||||||
|
Msg::SetZoom(scale, offset) => {
|
||||||
|
if scale < 0.1 {
|
||||||
|
model.zoom = 0.1;
|
||||||
|
} else {
|
||||||
|
model.zoom = scale;
|
||||||
|
}
|
||||||
|
model.offset = [offset[0] as f64, offset[1] as f64];
|
||||||
|
}
|
||||||
Msg::Zoom(scale, at) => {
|
Msg::Zoom(scale, at) => {
|
||||||
let old_zoom = model.zoom;
|
let old_zoom = model.zoom;
|
||||||
model.zoom += scale;
|
model.zoom += scale;
|
||||||
@ -135,12 +147,16 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
}
|
}
|
||||||
let z_rat = model.zoom / old_zoom;
|
let z_rat = model.zoom / old_zoom;
|
||||||
let at = [at[0] as f64, at[1] as f64];
|
let at = [at[0] as f64, at[1] as f64];
|
||||||
|
// https://stackoverflow.com/questions/38281745/calculating-view-offset-for-zooming-in-at-the-position-of-the-mouse-cursor
|
||||||
model.offset = [
|
model.offset = [
|
||||||
at[0] - (z_rat) * (at[0] - model.offset[0]),
|
at[0] - (z_rat) * (at[0] - model.offset[0]),
|
||||||
at[1] - (z_rat) * (at[1] - model.offset[1]),
|
at[1] - (z_rat) * (at[1] - model.offset[1]),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
Msg::Move(delta) => model.offset = [model.offset[0] + delta[0], model.offset[1] + delta[1]],
|
Msg::Move(delta) => model.offset = [model.offset[0] + delta[0], model.offset[1] + delta[1]],
|
||||||
|
Msg::UrlChanged(subs::UrlChanged(url)) => {
|
||||||
|
info!("url changed {}", url);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@ -206,8 +222,9 @@ fn view_metadata_table(data: &Data) -> Node<Msg> {
|
|||||||
|
|
||||||
let dt = NaiveDateTime::from_timestamp_opt(data.timestamp, 0).unwrap();
|
let dt = NaiveDateTime::from_timestamp_opt(data.timestamp, 0).unwrap();
|
||||||
let dt = Local.from_local_datetime(&dt).unwrap();
|
let dt = Local.from_local_datetime(&dt).unwrap();
|
||||||
|
let rendered_at = dt.format("%Y-%m-%d %H:%M:%S").to_string();
|
||||||
div![
|
div![
|
||||||
div!["Rendered @ ", dt.format("%Y-%m-%d %H:%M:%S").to_string()],
|
div!["Rendered @ ", &rendered_at],
|
||||||
div![
|
div![
|
||||||
C![IF!(fresh => "fresh")],
|
C![IF!(fresh => "fresh")],
|
||||||
humantime::format_duration(d).to_string(),
|
humantime::format_duration(d).to_string(),
|
||||||
@ -217,6 +234,7 @@ fn view_metadata_table(data: &Data) -> Node<Msg> {
|
|||||||
C!["table", "is-striped", "is-narrow"],
|
C!["table", "is-striped", "is-narrow"],
|
||||||
tbody![
|
tbody![
|
||||||
tr![th!["Name"], th!["Value"]],
|
tr![th!["Name"], th!["Value"]],
|
||||||
|
row("render_time_seconds", data.render_time_seconds),
|
||||||
row("subsamples", data.scene.subsamples),
|
row("subsamples", data.scene.subsamples),
|
||||||
row("adaptive_subsampling", data.scene.adaptive_subsampling),
|
row("adaptive_subsampling", data.scene.adaptive_subsampling),
|
||||||
row("num_threads", data.scene.num_threads),
|
row("num_threads", data.scene.num_threads),
|
||||||
@ -241,19 +259,14 @@ fn draw(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Serialize)]
|
let mut ca2 = ContextAttributes2d::new();
|
||||||
struct ContextOptions {
|
// Chrome warning:
|
||||||
#[serde(rename = "willReadFrequently")]
|
// Canvas2D: Multiple readback operations using getImageData are faster with the
|
||||||
will_read_frequently: bool,
|
// willReadFrequently attribute set to true. See:
|
||||||
}
|
// https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-will-read-frequently
|
||||||
|
ca2.will_read_frequently(true);
|
||||||
let ctx = canvas
|
let ctx = canvas
|
||||||
.get_context_with_context_options(
|
.get_context_with_context_options("2d", &ca2)
|
||||||
"2d",
|
|
||||||
&wasm_bindgen::JsValue::from_serde(&ContextOptions {
|
|
||||||
will_read_frequently: true,
|
|
||||||
})
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
.expect("Problem getting canvas context")
|
.expect("Problem getting canvas context")
|
||||||
.expect("The canvas context is empty")
|
.expect("The canvas context is empty")
|
||||||
.dyn_into::<web_sys::CanvasRenderingContext2d>()
|
.dyn_into::<web_sys::CanvasRenderingContext2d>()
|
||||||
@ -274,6 +287,8 @@ fn draw(
|
|||||||
let dy = offset[1];
|
let dy = offset[1];
|
||||||
let dw = dw * zoom;
|
let dw = dw * zoom;
|
||||||
let dh = dh * zoom;
|
let dh = dh * zoom;
|
||||||
|
// Use nearest neighbor resizing up.
|
||||||
|
ctx.set_image_smoothing_enabled(false);
|
||||||
ctx.draw_image_with_html_image_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
|
ctx.draw_image_with_html_image_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
|
||||||
&image, sx, sy, sw, sh, dx, dy, dw, dh,
|
&image, sx, sy, sw, sh, dx, dy, dw, dh,
|
||||||
)
|
)
|
||||||
@ -285,12 +300,21 @@ fn view_image(
|
|||||||
im: &ImageMetadata,
|
im: &ImageMetadata,
|
||||||
image_canvas: &ElRef<HtmlCanvasElement>,
|
image_canvas: &ElRef<HtmlCanvasElement>,
|
||||||
image_ref: &ElRef<HtmlImageElement>,
|
image_ref: &ElRef<HtmlImageElement>,
|
||||||
|
timestamp: i64,
|
||||||
) -> Node<Msg> {
|
) -> Node<Msg> {
|
||||||
let url = format!("{ROOT_URL}/{}", im.image);
|
let url = format!("{ROOT_URL}/{}?t={}", im.image, timestamp);
|
||||||
|
let href = url.clone();
|
||||||
|
let go = |_| Url::go_and_load_with_str(href);
|
||||||
let attrs = if let Some(canvas) = image_canvas.get() {
|
let attrs = if let Some(canvas) = image_canvas.get() {
|
||||||
|
// Width based on browser layout of canvas and it's parent (which would be set to
|
||||||
|
// 'width:100%' in CSS.
|
||||||
|
let width = canvas.client_width();
|
||||||
|
// But we need to compute an aspect correct height, because the browser can't figure this
|
||||||
|
// out for us (because I don't know CSS that well).
|
||||||
|
let height = width as f32 * im.ratio;
|
||||||
Some(attrs![
|
Some(attrs![
|
||||||
At::Width => px(canvas.client_width()),
|
At::Width => px(width),
|
||||||
At::Height => px(canvas.client_height()),
|
At::Height => px(height),
|
||||||
])
|
])
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@ -321,10 +345,11 @@ fn view_image(
|
|||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
button![C!["button"], "View image", ev(Ev::Click, go)],
|
||||||
img![
|
img![
|
||||||
el_ref(image_ref),
|
el_ref(image_ref),
|
||||||
attrs! {
|
attrs! {
|
||||||
At::Src => url
|
At::Src => &url
|
||||||
},
|
},
|
||||||
style! {
|
style! {
|
||||||
St::Display => "none",
|
St::Display => "none",
|
||||||
@ -333,6 +358,103 @@ fn view_image(
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn view_debug_buttons(
|
||||||
|
image_size: (usize, usize),
|
||||||
|
canvas_size: (i32, i32),
|
||||||
|
zoom: f64,
|
||||||
|
offset: [f64; 2],
|
||||||
|
) -> Node<Msg> {
|
||||||
|
let zoom_in = 0.1;
|
||||||
|
let zoom_out = -0.1;
|
||||||
|
let (image_width, image_height) = image_size;
|
||||||
|
let (canvas_width, canvas_height) = canvas_size;
|
||||||
|
let one2one = image_width as f64 / canvas_width as f64;
|
||||||
|
let one2one_offset = [
|
||||||
|
(canvas_width - image_width as i32) / 2,
|
||||||
|
(canvas_height - image_height as i32) / 2,
|
||||||
|
];
|
||||||
|
div![
|
||||||
|
p![format!(
|
||||||
|
"Z: {:.4} Off: {:.2}x{:.2}",
|
||||||
|
zoom, offset[0], offset[1]
|
||||||
|
)],
|
||||||
|
p![format!("Mid {}x{}", canvas_width / 2, canvas_height / 2)],
|
||||||
|
p![format!("Max {}x{}", canvas_width, canvas_height)],
|
||||||
|
div![
|
||||||
|
C!["buttons", "has-addons"],
|
||||||
|
button![
|
||||||
|
C!["button", "is-danger", "is-fullwidth"],
|
||||||
|
ev(Ev::Click, |_| Msg::Reset),
|
||||||
|
"Reset",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
div![
|
||||||
|
C!["buttons", "has-addons", "is-centered"],
|
||||||
|
button![
|
||||||
|
C!["button"],
|
||||||
|
ev(Ev::Click, move |_| Msg::SetZoom(one2one, one2one_offset)),
|
||||||
|
"1:1",
|
||||||
|
],
|
||||||
|
button![
|
||||||
|
C!["button"],
|
||||||
|
ev(Ev::Click, move |_| Msg::SetZoom(1., [0, 0])),
|
||||||
|
"Fit",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
div![
|
||||||
|
C!["buttons", "has-addons", "is-centered"],
|
||||||
|
button![
|
||||||
|
C!["button"],
|
||||||
|
ev(Ev::Click, move |_| Msg::Zoom(zoom_in, [0, 0])),
|
||||||
|
"+ @ 0x0",
|
||||||
|
],
|
||||||
|
button![
|
||||||
|
C!["button"],
|
||||||
|
ev(Ev::Click, move |_| Msg::Zoom(zoom_out, [0, 0])),
|
||||||
|
"- @ 0x0",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
div![
|
||||||
|
C!["buttons", "has-addons", "is-centered"],
|
||||||
|
button![
|
||||||
|
C!["button"],
|
||||||
|
ev(Ev::Click, move |_| Msg::Zoom(
|
||||||
|
zoom_in,
|
||||||
|
[canvas_width / 2, canvas_height / 2]
|
||||||
|
)),
|
||||||
|
"+ @ mid",
|
||||||
|
],
|
||||||
|
button![
|
||||||
|
C!["button"],
|
||||||
|
ev(Ev::Click, move |_| Msg::Zoom(
|
||||||
|
zoom_out,
|
||||||
|
[canvas_width / 2, canvas_height / 2]
|
||||||
|
)),
|
||||||
|
"- @ mid",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
div![
|
||||||
|
C!["buttons", "has-addons", "is-centered"],
|
||||||
|
button![
|
||||||
|
C!["button"],
|
||||||
|
ev(Ev::Click, move |_| Msg::Zoom(
|
||||||
|
zoom_in,
|
||||||
|
[canvas_width, canvas_height]
|
||||||
|
)),
|
||||||
|
"+ @ max",
|
||||||
|
],
|
||||||
|
button![
|
||||||
|
C!["button"],
|
||||||
|
ev(Ev::Click, move |_| Msg::Zoom(
|
||||||
|
zoom_out,
|
||||||
|
[canvas_width, canvas_height]
|
||||||
|
)),
|
||||||
|
"- @ max",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
fn view_data(
|
fn view_data(
|
||||||
data: &Data,
|
data: &Data,
|
||||||
image_canvas: &ElRef<HtmlCanvasElement>,
|
image_canvas: &ElRef<HtmlCanvasElement>,
|
||||||
@ -340,74 +462,29 @@ fn view_data(
|
|||||||
zoom: f64,
|
zoom: f64,
|
||||||
offset: [f64; 2],
|
offset: [f64; 2],
|
||||||
) -> Node<Msg> {
|
) -> Node<Msg> {
|
||||||
let mut width = 128;
|
let mut canvas_width = 128;
|
||||||
let mut height = 128;
|
let mut canvas_height = 128;
|
||||||
if let Some(c) = image_canvas.get() {
|
if let Some(c) = image_canvas.get() {
|
||||||
width = c.width() as i32;
|
canvas_width = c.width() as i32;
|
||||||
height = c.height() as i32;
|
canvas_height = c.height() as i32;
|
||||||
}
|
}
|
||||||
let zoom_in = 0.1;
|
let im = data.image_metadata.iter().nth(0);
|
||||||
let zoom_out = -0.1;
|
let image_size = im.map(|im| im.size).unwrap_or((512, 512));
|
||||||
div![
|
div![
|
||||||
C!["columns"],
|
C!["columns"],
|
||||||
div![
|
|
||||||
C!["column", "is-narrow"],
|
|
||||||
view_metadata_table(data),
|
|
||||||
p![format!(
|
|
||||||
"Z: {:.4} Off: {:.2}x{:.2}",
|
|
||||||
zoom, offset[0], offset[1]
|
|
||||||
)],
|
|
||||||
p![format!("Mid {}x{}", width / 2, height / 2)],
|
|
||||||
p![format!("Max {}x{}", width, height)],
|
|
||||||
button![ev(Ev::Click, |_| Msg::Reset), "Reset",],
|
|
||||||
div![
|
|
||||||
button![
|
|
||||||
ev(Ev::Click, move |_| Msg::Zoom(zoom_in, [0, 0])),
|
|
||||||
"+ @ 0x0",
|
|
||||||
],
|
|
||||||
button![
|
|
||||||
ev(Ev::Click, move |_| Msg::Zoom(zoom_out, [0, 0])),
|
|
||||||
"- @ 0x0",
|
|
||||||
],
|
|
||||||
],
|
|
||||||
div![
|
|
||||||
button![
|
|
||||||
ev(Ev::Click, move |_| Msg::Zoom(
|
|
||||||
zoom_in,
|
|
||||||
[width / 2, height / 2]
|
|
||||||
)),
|
|
||||||
"+ @ mid",
|
|
||||||
],
|
|
||||||
button![
|
|
||||||
ev(Ev::Click, move |_| Msg::Zoom(
|
|
||||||
zoom_out,
|
|
||||||
[width / 2, height / 2]
|
|
||||||
)),
|
|
||||||
"- @ mid",
|
|
||||||
],
|
|
||||||
],
|
|
||||||
div![
|
|
||||||
button![
|
|
||||||
ev(Ev::Click, move |_| Msg::Zoom(zoom_in, [width, height])),
|
|
||||||
"+ @ max",
|
|
||||||
],
|
|
||||||
button![
|
|
||||||
ev(Ev::Click, move |_| Msg::Zoom(zoom_out, [width, height])),
|
|
||||||
"- @ max",
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
div![
|
div![
|
||||||
C!["column"],
|
C!["column"],
|
||||||
div![
|
div![
|
||||||
C!["tabs"],
|
C!["tabs"],
|
||||||
ul![data.image_metadata.iter().map(|im| li![a![&im.name]])]
|
ul![data.image_metadata.iter().map(|im| li![a![&im.name]])]
|
||||||
],
|
],
|
||||||
data.image_metadata
|
im.map(|im| view_image(im, image_canvas, image_ref, data.timestamp))
|
||||||
.iter()
|
],
|
||||||
.nth(0)
|
div![
|
||||||
.map(|im| view_image(im, image_canvas, image_ref))
|
C!["column", "is-narrow"],
|
||||||
]
|
view_metadata_table(data),
|
||||||
|
view_debug_buttons(image_size, (canvas_width, canvas_height), zoom, offset),
|
||||||
|
],
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user