From ce5be7f783ac7c6589b501433caafd717c6e119f Mon Sep 17 00:00:00 2001 From: Melody Horn Date: Fri, 3 Dec 2021 23:30:52 -0700 Subject: swag --- src/history.rs | 51 ++++++++++++ src/lib.rs | 255 ++++++++++++++++++++++++++++++++++++++++----------------- src/ocr.rs | 6 +- 3 files changed, 232 insertions(+), 80 deletions(-) create mode 100644 src/history.rs (limited to 'src') diff --git a/src/history.rs b/src/history.rs new file mode 100644 index 0000000..5216d10 --- /dev/null +++ b/src/history.rs @@ -0,0 +1,51 @@ +// god is dead and we have killed him and consequently std::time::Instant doesn't work on wasm + +use chrono::prelude::*; +use wasm_bindgen::prelude::*; + +struct Reading { + time: DateTime, + queue_size: u32, +} + +#[derive(Default)] +pub struct History { + data: Vec, +} + +impl History { + pub fn record(&mut self, queue_size: u32) { + self.data.push(Reading { + time: Local::now(), + queue_size, + }); + } + + pub fn completion_time(&self) -> Option { + // TODO make this not suck + let &Reading { + time: first_time, + queue_size: first_size, + .. + } = self.data.first()?; + let &Reading { + time: last_time, + queue_size: last_size, + } = self.data.last()?; + let overall_time_elapsed = last_time - first_time; + if overall_time_elapsed.is_zero() { + return None; + } + let overall_queue_motion = first_size - last_size; + if overall_queue_motion == 0 { + return None; + } + let duration_per_step = overall_time_elapsed / overall_queue_motion as i32; + let remaining_duration = duration_per_step * last_size as i32; + let completion_time = last_time + remaining_duration; + let completion_time_gmt = completion_time.timestamp_millis(); + Some(js_sys::Date::new(&JsValue::from( + completion_time_gmt as f64, + ))) + } +} diff --git a/src/lib.rs b/src/lib.rs index 8f28199..8c8bcc3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,13 @@ use image::{DynamicImage, ImageFormat}; +use std::cell::{Cell, RefCell}; use std::rc::Rc; -use std::sync::atomic::Ordering::Relaxed; -use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; -use js_sys::{ArrayBuffer, Uint8Array}; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; use wasm_bindgen_futures::*; use web_sys::*; +mod history; mod ocr; // When the `wee_alloc` feature is enabled, use `wee_alloc` as the global @@ -18,14 +17,9 @@ mod ocr; static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; async fn blob_to_image(blob: Blob) -> DynamicImage { - let array_buffer_promise = blob.array_buffer(); - let array_buffer: ArrayBuffer = JsFuture::from(array_buffer_promise) + let image_data = gloo::file::futures::read_as_bytes(&blob.into()) .await - .unwrap() - .dyn_into() .unwrap(); - let image_array = Uint8Array::new(&array_buffer); - let image_data = image_array.to_vec(); image::load_from_memory_with_format(&image_data, ImageFormat::Png).unwrap() } @@ -37,30 +31,58 @@ fn image_to_data_uri(image: &DynamicImage) -> String { result } -fn get_queue_size(image: DynamicImage) -> DynamicImage { - let useful_region = image.crop_imm(911, 514, 70, 20); +const QUEUE_SIZE_WIDTH: u32 = 70; +const QUEUE_SIZE_HEIGHT: u32 = 20; + +fn get_queue_size(image: DynamicImage, corner: &(u32, u32)) -> DynamicImage { + let &(x, y) = corner; + let useful_region = image.crop_imm(x, y, QUEUE_SIZE_WIDTH, QUEUE_SIZE_HEIGHT); useful_region } -#[cfg(test)] -#[test] -fn test_queue_size() { - let image = image::open("data/2021-12-03 18-22-11.png").unwrap(); - let queue_size = get_queue_size(image); - queue_size.save("data/test.png").unwrap(); - let result = ocr::ocr(queue_size); - assert_eq!(result, 4_480); +fn update_target_marker( + target_marker: &HtmlElement, + width: &Rc>, + height: &Rc>, + video: &HtmlVideoElement, + queue_position: &Rc>, +) { + let (real_x, real_y) = queue_position.get(); + let real_width = width.get() as f64; + let real_height = height.get() as f64; + let fake_width = video.offset_width() as f64; + let fake_height = video.offset_height() as f64; + let fake_x = real_x as f64 / real_width * fake_width; + let fake_y = real_y as f64 / real_height * fake_height; + + let target_width = QUEUE_SIZE_WIDTH as f64 / real_width * fake_width; + let target_height = QUEUE_SIZE_HEIGHT as f64 / real_height * fake_height; + + let style = target_marker.style(); + style + .set_property("left", &format!("{}px", fake_x)) + .unwrap(); + style.set_property("top", &format!("{}px", fake_y)).unwrap(); + style + .set_property("width", &format!("{}px", target_width)) + .unwrap(); + style + .set_property("height", &format!("{}px", target_height)) + .unwrap(); } async fn do_boot() { - let width = Rc::new(AtomicU32::new(0)); - let height = Rc::new(AtomicU32::new(0)); + let width = Rc::new(Cell::new(0)); + let height = Rc::new(Cell::new(0)); + + let streaming = Rc::new(Cell::new(false)); - let streaming = AtomicBool::new(false); + let queue_position = Rc::new(Cell::new((920u32, 520u32))); - let window = window().unwrap(); - let document = window.document().unwrap(); + let document = gloo::utils::document(); + + let history = Rc::new(RefCell::new(history::History::default())); let video: HtmlVideoElement = document .get_element_by_id("video") @@ -72,10 +94,37 @@ async fn do_boot() { .unwrap() .dyn_into() .unwrap(); - let photo = document.get_element_by_id("photo").unwrap(); - let start_button = document.get_element_by_id("startbutton").unwrap(); + let target_marker: HtmlElement = document + .get_element_by_id("target-marker") + .unwrap() + .dyn_into() + .unwrap(); + + let video_click_listener = { + let video = video.clone(); + let also_video = video.clone(); + let queue_position = queue_position.clone(); + let width = width.clone(); + let height = height.clone(); + let target_marker = target_marker.clone(); + gloo::events::EventListener::new(&video, "click", move |event| { + let video = also_video.clone(); + let mouse_event: &MouseEvent = event.dyn_ref().unwrap(); + let mouse_x = mouse_event.offset_x() as f64; + let mouse_y = mouse_event.offset_y() as f64; + let real_width = width.get() as f64; + let real_height = height.get() as f64; + let fake_width = video.offset_width() as f64; + let fake_height = video.offset_height() as f64; + let real_x = mouse_x / fake_width * real_width - QUEUE_SIZE_WIDTH as f64 / 2.0; + let real_y = mouse_y / fake_height * real_height - QUEUE_SIZE_HEIGHT as f64 / 2.0; + queue_position.replace((real_x.round() as u32, real_y.round() as u32)); + update_target_marker(&target_marker, &width, &height, &video, &queue_position); + }) + }; + video_click_listener.forget(); - let navigator = window.navigator(); + let navigator = gloo::utils::window().navigator(); let media_devices = navigator.media_devices().unwrap(); let constraints = { let mut constraints = DisplayMediaStreamConstraints::new(); @@ -89,65 +138,117 @@ async fn do_boot() { let capture_stream = JsFuture::from(capture_stream_promise).await.unwrap(); let capture_stream: MediaStream = capture_stream.dyn_into().unwrap(); video.set_src_object(Some(&capture_stream)); - let video_listener = { + let video_play_listener = { let width = width.clone(); let height = height.clone(); + let streaming = streaming.clone(); let video = video.clone(); let also_video = video.clone(); let canvas = canvas.clone(); + let queue_position = queue_position.clone(); gloo::events::EventListener::new(&video, "canplay", move |_event| { - let _ = also_video.play().unwrap(); - if !streaming.load(Relaxed) { - width.store(also_video.video_width(), Relaxed); - height.store(also_video.video_height(), Relaxed); - canvas.set_attribute("width", &format!("{}", width.load(Relaxed))); - canvas.set_attribute("height", &format!("{}", height.load(Relaxed))); - streaming.store(true, Relaxed); + let video = also_video.clone(); + let _ = video.play().unwrap(); + if !streaming.get() { + width.set(video.video_width()); + height.set(video.video_height()); + canvas.set_attribute("width", &format!("{}", width.get())); + canvas.set_attribute("height", &format!("{}", height.get())); + streaming.set(true); + update_target_marker(&target_marker, &width, &height, &video, &queue_position); } }) }; - video_listener.forget(); - - let start_button_event_listener = { - let photo = photo.clone(); - gloo::events::EventListener::new(&start_button, "click", move |_event| { - let context = canvas.get_context("2d").unwrap().unwrap(); - let context: CanvasRenderingContext2d = context.dyn_into().unwrap(); - canvas.set_width(width.load(Ordering::Relaxed)); - canvas.set_height(height.load(Ordering::Relaxed)); - context - .draw_image_with_html_video_element_and_dw_and_dh( - &video, - 0.0, - 0.0, - width.load(Relaxed).into(), - height.load(Relaxed).into(), - ) - .unwrap(); - - canvas - .to_blob( - Closure::once_into_js(|data: Blob| { - console::log_1(&JsValue::from("hewwo?")); - // lifetime bullshit here - let window = web_sys::window().unwrap(); - let document = window.document().unwrap(); - let photo = document.get_element_by_id("photo").unwrap(); - spawn_local(async move { - let image = blob_to_image(data).await; - let useful_region = get_queue_size(image); - let data_uri = image_to_data_uri(&useful_region); - photo.set_attribute("src", &data_uri).unwrap(); - console::log_1(&JsValue::from("uhhhh hi")); - }) - }) - .dyn_ref() - .unwrap(), - ) - .unwrap(); - }) - }; - start_button_event_listener.forget(); + video_play_listener.forget(); + + let update_interval = gloo::timers::callback::Interval::new(2_000, move || { + let history = history.clone(); + let queue_position = queue_position.clone(); + if !streaming.get() { + return; + } + let context = canvas.get_context("2d").unwrap().unwrap(); + let context: CanvasRenderingContext2d = context.dyn_into().unwrap(); + canvas.set_width(width.get()); + canvas.set_height(height.get()); + context + .draw_image_with_html_video_element_and_dw_and_dh( + &video, + 0.0, + 0.0, + width.get().into(), + height.get().into(), + ) + .unwrap(); + + canvas + .to_blob( + Closure::once_into_js(move |data: Blob| { + let history = history.clone(); + let queue_position = queue_position.clone(); + // lifetime bullshit here + let document = gloo::utils::document(); + let photo = document.get_element_by_id("photo").unwrap(); + let current_queue_position: HtmlElement = document + .get_element_by_id("current-queue-position") + .unwrap() + .dyn_into() + .unwrap(); + let eta: HtmlElement = document + .get_element_by_id("eta") + .unwrap() + .dyn_into() + .unwrap(); + spawn_local(async move { + let image = blob_to_image(data).await; + let useful_region = get_queue_size(image, &queue_position.get()); + let data_uri = image_to_data_uri(&useful_region); + photo.set_attribute("src", &data_uri).unwrap(); + let queue_size = ocr::ocr(useful_region); + match queue_size { + Some(queue_size) => { + current_queue_position.set_inner_text(&format!("{}", queue_size)); + if let Ok(mut history) = history.try_borrow_mut() { + history.record(queue_size); + let estimated_finish = history.completion_time(); + let eta_text = match estimated_finish { + Some(finish) => { + let empty_array = js_sys::Array::new(); + let options = { + let kv_pair = js_sys::Array::new(); + kv_pair.push(&JsValue::from("timeStyle")); + kv_pair.push(&JsValue::from("short")); + let kv_pairs = js_sys::Array::new(); + kv_pairs.push(&kv_pair); + js_sys::Object::from_entries(&kv_pairs).unwrap() + }; + let format = js_sys::Intl::DateTimeFormat::new( + &empty_array, + &options, + ); + let format = format.format(); + let locale_string = + format.call1(&JsValue::UNDEFINED, &finish).unwrap(); + locale_string.as_string().unwrap() + } + None => format!("pending"), + }; + let label = format!(" - ETA {}", eta_text); + eta.set_inner_text(&label); + } + } + None => { + current_queue_position.set_inner_text("something i can't read"); + } + } + }); + }) + .dyn_ref() + .unwrap(), + ) + .unwrap(); + }); + update_interval.forget(); } #[wasm_bindgen] diff --git a/src/ocr.rs b/src/ocr.rs index bc7e535..cde116f 100644 --- a/src/ocr.rs +++ b/src/ocr.rs @@ -22,12 +22,12 @@ fn x_matches(image: &GrayImage, template: &GrayImage) -> Vec { ); match_values .enumerate_pixels() - .filter(|(_x, _y, pix)| pix.0[0] > 0.9) + .filter(|(_x, _y, pix)| pix.0[0] > 0.95) .map(|(x, _y, _pix)| x) .collect() } -pub fn ocr(image: DynamicImage) -> u32 { +pub fn ocr(image: DynamicImage) -> Option { let grayscale_image = image::imageops::grayscale(&image); let mut digit_x_positions: Vec<(u8, u32)> = (0..10) .flat_map(|i| { @@ -46,5 +46,5 @@ pub fn ocr(image: DynamicImage) -> u32 { .map(|(i, _x)| format!("{}", i)) .collect(); dbg!(&digits); - digits.parse().unwrap() + digits.parse().ok() } -- cgit v1.2.3