From 9b31cb88d4283ea04d80f68491677b368c573c18 Mon Sep 17 00:00:00 2001 From: Bill Thiede Date: Sun, 19 Feb 2023 21:49:10 -0800 Subject: [PATCH] Implement showing color on mouse hover. --- index.html | 3 + src/lib.rs | 299 ++++++++++++++++++++++++++++++----------------------- 2 files changed, 174 insertions(+), 128 deletions(-) diff --git a/index.html b/index.html index 665c1ce..6178d73 100644 --- a/index.html +++ b/index.html @@ -8,6 +8,9 @@ + + diff --git a/src/lib.rs b/src/lib.rs index bd84712..3c35a73 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,14 +3,17 @@ // but some rules are too "annoying" or are not applicable for your case.) #![allow(clippy::wildcard_imports)] -use std::time::Duration; +use std::{ + any::{Any, TypeId}, + time::Duration, +}; use chrono::{Local, NaiveDateTime, TimeZone}; use log::{info, Level}; use seed::{prelude::*, *}; use thiserror::Error; use wasm_timer::{SystemTime, UNIX_EPOCH}; -use web_sys::{ContextAttributes2d, HtmlCanvasElement, HtmlImageElement}; +use web_sys::{ContextAttributes2d, Event, HtmlCanvasElement, HtmlImageElement}; const ROOT_URL: &'static str = "/www/tracer"; @@ -38,6 +41,8 @@ fn init(_: Url, orders: &mut impl Orders) -> Model { image_ref: ElRef::::default(), zoom: 1.0, offset: [0., 0.], + current_image_idx: None, + mouse_position: None, } } @@ -54,6 +59,8 @@ struct Model { image_ref: ElRef, zoom: f64, offset: [f64; 2], + current_image_idx: Option, + mouse_position: Option<(i32, i32)>, } // ------ ------ @@ -67,11 +74,11 @@ enum Msg { OnError(AppError), Rendered, Received(Data), - Reset, SetZoom(f64, [i32; 2]), Zoom(f64, [i32; 2]), Move([f64; 2]), UrlChanged(subs::UrlChanged), + SetMousePosition(i32, i32), } // `update` describes how to handle each `Msg`. @@ -127,10 +134,6 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { // TODO(wathiede): can this be done less often? orders.after_next_render(|_| Msg::Rendered).skip(); } - Msg::Reset => { - model.zoom = 1.; - model.offset = [0., 0.]; - } Msg::SetZoom(scale, offset) => { if scale < 0.1 { model.zoom = 0.1; @@ -157,6 +160,7 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { Msg::UrlChanged(subs::UrlChanged(url)) => { info!("url changed {}", url); } + Msg::SetMousePosition(x, y) => model.mouse_position = Some((x, y)), } } use serde::{Deserialize, Serialize}; @@ -199,17 +203,30 @@ pub enum ImageType { // ------ ------ // View // ------ ------ -fn view_metadata_table(data: &Data) -> Node { - fn row(name: &str, value: T) -> Node - where - T: std::fmt::Debug, - { - let mut v = format!("{:?}", value); - if &v == "None" { - v = "❌".to_string(); - } - tr![td![name], td![v]] +fn row(name: &str, value: T) -> Node +where + T: Any + std::fmt::Debug, +{ + let value_any = &value as &dyn Any; + let t_string = TypeId::of::(); + let t_f32 = TypeId::of::(); + let t_f64 = TypeId::of::(); + let t_t = TypeId::of::(); + let mut v = if t_t == t_string { + format!("{}", value_any.downcast_ref::().unwrap()) + } else if t_t == t_f32 { + format!("{:.2}", value_any.downcast_ref::().unwrap()) + } else if t_t == t_f64 { + format!("{:.2}", value_any.downcast_ref::().unwrap()) + } else { + format!("{value:?}") + }; + if &v == "None" { + v = "❌".to_string(); } + tr![td![name], td![C!["has-text-right"], v]] +} +fn view_metadata_table(data: &Data) -> Vec> { //❌ //✔️ let d = SystemTime::now() @@ -223,26 +240,32 @@ fn view_metadata_table(data: &Data) -> Node { let dt = NaiveDateTime::from_timestamp_opt(data.timestamp, 0).unwrap(); let dt = Local.from_local_datetime(&dt).unwrap(); let rendered_at = dt.format("%Y-%m-%d %H:%M:%S").to_string(); - div![ - div!["Rendered @ ", &rendered_at], - div![ - C![IF!(fresh => "fresh")], - humantime::format_duration(d).to_string(), - " ago" + /* + a![ + span![C!["icon"], i![C!["fa-solid", "fa-up-right-from-square"]]], + ev( + Ev::Click, + make_external_link(format!("{ROOT_URL}/{}?t={}", im.image, data.timestamp)) + ) + ], + */ + nodes![ + tr![th![attrs! {At::ColSpan=>2}, "Render settings"]], + tr![ + td![rendered_at,], + td![ + C![IF!(fresh => "fresh")], + humantime::format_duration(d).to_string(), + " ago" + ] ], - table![ - C!["table", "is-striped", "is-narrow"], - tbody![ - tr![th!["Name"], th!["Value"]], - row("render_time_seconds", data.render_time_seconds), - row("subsamples", data.scene.subsamples), - row("adaptive_subsampling", data.scene.adaptive_subsampling), - row("num_threads", data.scene.num_threads), - row("width", data.scene.width), - row("height", data.scene.height), - row("global_illumination", data.scene.global_illumination), - ], - ] + row("render_time_seconds", data.render_time_seconds), + row("subsamples", data.scene.subsamples), + row("adaptive_subsampling", data.scene.adaptive_subsampling), + row("num_threads", data.scene.num_threads), + row("width", data.scene.width), + row("height", data.scene.height), + row("global_illumination", data.scene.global_illumination), ] } @@ -293,8 +316,10 @@ fn draw( &image, sx, sy, sw, sh, dx, dy, dw, dh, ) .unwrap(); +} - //let color = ctx.get_image_data(10.0, 10.0, 11.0, 11.0).unwrap().data(); +fn make_external_link(href: String) -> impl FnOnce(Event) + Clone { + |_| Url::go_and_load_with_str(href) } fn view_image( im: &ImageMetadata, @@ -303,15 +328,13 @@ fn view_image( timestamp: i64, ) -> Node { 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() { // 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; + let height = width as f32 / im.ratio; Some(attrs![ At::Width => px(width), At::Height => px(height), @@ -332,7 +355,7 @@ fn view_image( if e.buttons() != 0 { Msg::Move([e.movement_x() as f64, e.movement_y() as f64]) } else { - Msg::Move([0., 0.]) + Msg::SetMousePosition(e.offset_x(), e.offset_y()) } }), wheel_ev(Ev::Wheel, |event| { @@ -345,7 +368,6 @@ fn view_image( }) }), ], - button![C!["button"], "View image", ev(Ev::Click, go)], img![ el_ref(image_ref), attrs! { @@ -363,9 +385,9 @@ fn view_debug_buttons( canvas_size: (i32, i32), zoom: f64, offset: [f64; 2], -) -> Node { - let zoom_in = 0.1; - let zoom_out = -0.1; + image_canvas: &ElRef, + mouse_position: Option<(i32, i32)>, +) -> Vec> { let (image_width, image_height) = image_size; let (canvas_width, canvas_height) = canvas_size; let one2one = image_width as f64 / canvas_width as f64; @@ -373,85 +395,30 @@ fn view_debug_buttons( (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![ + let (r, g, b) = get_hover_color(image_canvas, mouse_position); + nodes![ + tr![th![attrs! {At::ColSpan=>2}, "View settings"]], + row("Zoom", format!("{:.2}", zoom)), + row("X Offset", format!("{:.2}", offset[0])), + row("Y Offset", format!("{:.2}", offset[1])), + tr![td!["Hover"],td![ + style! { + St::BackgroundColor => format!("rgb({},{},{})",r,g,b), + }, + ]] + tr![th![attrs! {At::ColSpan=>2}, "Set Zoom"]], + tr![ + td![button![ C!["button"], ev(Ev::Click, move |_| Msg::SetZoom(one2one, one2one_offset)), "1:1", - ], - button![ + ],], + td![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", - ], - ], + ]] + ] ] } @@ -461,30 +428,59 @@ fn view_data( image_ref: &ElRef, zoom: f64, offset: [f64; 2], + mouse_position: Option<(i32, i32)>, ) -> Node { - let mut canvas_width = 128; - let mut canvas_height = 128; + let mut canvas_width = 1024; + let mut canvas_height = 1024; if let Some(c) = image_canvas.get() { canvas_width = c.width() as i32; canvas_height = c.height() as i32; } let im = data.image_metadata.iter().nth(0); - let image_size = im.map(|im| im.size).unwrap_or((512, 512)); + let image_size = im.map(|im| im.size).unwrap_or((1024, 1024)); div![ C!["columns"], div![ C!["column"], div![ C!["tabs"], - ul![data.image_metadata.iter().map(|im| li![a![&im.name]])] + ul![data + .image_metadata + .iter() + .map(|im| li![a![span![&im.name],]])] ], - im.map(|im| view_image(im, image_canvas, image_ref, data.timestamp)) + im.map(|im| view_image(im, image_canvas, image_ref, data.timestamp)), ], div![ C!["column", "is-narrow"], - view_metadata_table(data), - view_debug_buttons(image_size, (canvas_width, canvas_height), zoom, offset), - ], + table![ + C!["table", "is-striped", "is-size-7"], + tbody![ + view_metadata_table(data), + view_debug_buttons( + image_size, + (canvas_width, canvas_height), + zoom, + offset, + image_canvas, + mouse_position + ), + ] + ], + /* + im.map(|im| { + let url = format!("{ROOT_URL}/{}?t={}", im.image, data.timestamp); + div![ + C!["buttons", "has-addons", "is-centered"], + button![ + C!["button"], + "View image", + ev(Ev::Click, make_external_link(url.clone())) + ] + ] + }), + */ + ] ] } @@ -502,9 +498,13 @@ fn view(model: &Model) -> Node { &model.image_canvas, &model.image_ref, model.zoom, - model.offset + model.offset, + model.mouse_position, ), - None => h1!["Loading...."], + None => section![ + C!["content", "hero", "is-large", "is-fullheight"], + div![C!["hero-body"], p![C!["title"], "Loading...."]] + ], }, error, div![ @@ -517,6 +517,49 @@ fn view(model: &Model) -> Node { ] } +fn get_hover_color( + image_canvas: &ElRef, + mouse_position: Option<(i32, i32)>, +) -> (u8, u8, u8) { + let mouse_position = if let Some(mouse_position) = mouse_position { + mouse_position + } else { + return (0, 0, 0); + }; + let canvas = if let Some(canvas) = image_canvas.get() { + canvas + } else { + return (0, 0, 0); + }; + let mut ca2 = ContextAttributes2d::new(); + // Chrome warning: + // Canvas2D: Multiple readback operations using getImageData are faster with the + // 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 + .get_context_with_context_options("2d", &ca2) + .expect("Problem getting canvas context") + .expect("The canvas context is empty") + .dyn_into::() + .expect("Problem casting as web_sys::CanvasRenderingContext2d"); + + let x = mouse_position.0 as f64; + let y = mouse_position.1 as f64; + let c = ctx.get_image_data(x, y, x + 1., y + 1.).unwrap().data(); + (c[0], c[1], c[2]) + /* + let c_str = format!("{},{},{}", c[0], c[1], c[2]); + button![ + C!["button", "is-fullwidth"], + style! { + St::BackgroundColor => format!("rgb({c_str})"), + }, + c_str + ] + */ +} + // ------ ------ // Start // ------ ------