app_ui/
editor_preview_canvas.rs1use edit::EditProject;
20use leptos::prelude::*;
21
22use crate::editor_ipc::EditorStatus;
23
24#[cfg(target_arch = "wasm32")]
25use leptos::task::spawn_local;
26#[cfg(target_arch = "wasm32")]
27use wasm_bindgen::JsCast;
28#[cfg(target_arch = "wasm32")]
29use wasm_bindgen::prelude::*;
30
31pub const EDITOR_PREVIEW_CANVAS_ID: &str = "editor-preview-canvas";
34
35pub const EDITOR_PREVIEW_POLL_MS: i32 = 66;
37
38#[cfg(target_arch = "wasm32")]
42pub fn install_editor_preview(
43 project: Option<RwSignal<Option<EditProject>>>,
44 status: RwSignal<EditorStatus>,
45) {
46 Effect::new(move |_| {
50 if let Some(sig) = project
51 && let Some(p) = sig.get()
52 {
53 crate::editor_ipc::editor_preview_open(&p);
54 }
55 });
56
57 let closure = Closure::wrap(Box::new(move || {
58 spawn_local(async move {
59 paint_one_editor_frame(project, status).await;
60 });
61 }) as Box<dyn FnMut()>);
62 if let Some(window) = web_sys::window() {
63 let _ = window.set_interval_with_callback_and_timeout_and_arguments_0(
64 closure.as_ref().unchecked_ref(),
65 EDITOR_PREVIEW_POLL_MS,
66 );
67 }
68 closure.forget();
69}
70
71#[cfg(not(target_arch = "wasm32"))]
73pub fn install_editor_preview(
74 _project: Option<RwSignal<Option<EditProject>>>,
75 _status: RwSignal<EditorStatus>,
76) {
77}
78
79#[cfg(target_arch = "wasm32")]
84#[allow(
85 clippy::cast_precision_loss,
86 reason = "the playhead frame index is well under 2^53, so u64→f64 is exact"
87)]
88async fn paint_one_editor_frame(
89 project: Option<RwSignal<Option<EditProject>>>,
90 status: RwSignal<EditorStatus>,
91) {
92 use js_sys::{Reflect, Uint8ClampedArray};
93 use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, ImageData};
94
95 let Some((width, height)) = project
100 .and_then(|sig| sig.get_untracked())
101 .map(|p| p.canvas_dims())
102 else {
103 return;
104 };
105 let bytes_len = (width as usize) * (height as usize) * 4;
106 if bytes_len == 0 {
107 return;
108 }
109 let frame = status.get_untracked().current_frame;
110
111 let Some(window) = web_sys::window() else {
112 return;
113 };
114 let Ok(invoke_fn) = Reflect::get(&window, &JsValue::from_str("__screenEditorPreviewFrame"))
115 else {
116 return;
117 };
118 if !invoke_fn.is_function() {
119 return;
120 }
121 let invoke: js_sys::Function = invoke_fn.unchecked_into();
122 let Ok(promise) = invoke.call1(&JsValue::NULL, &JsValue::from_f64(frame as f64)) else {
123 return;
124 };
125 let promise: js_sys::Promise = match promise.dyn_into() {
126 Ok(p) => p,
127 Err(_) => return,
128 };
129 let Ok(buf) = wasm_bindgen_futures::JsFuture::from(promise).await else {
130 return;
131 };
132
133 let bytes = if let Ok(array_buffer) = buf.clone().dyn_into::<js_sys::ArrayBuffer>() {
134 Uint8ClampedArray::new(&array_buffer)
135 } else if let Ok(typed_array) = buf.dyn_into::<Uint8ClampedArray>() {
136 typed_array
137 } else {
138 return;
139 };
140 if (bytes.length() as usize) < bytes_len {
142 return;
143 }
144
145 let mut rgba = vec![0u8; bytes_len];
146 bytes.copy_to(&mut rgba[..bytes_len]);
147 for px in rgba.chunks_exact_mut(4) {
148 px.swap(0, 2);
150 }
151
152 let Some(document) = window.document() else {
153 return;
154 };
155 let Some(canvas_el) = document.get_element_by_id(EDITOR_PREVIEW_CANVAS_ID) else {
156 return;
157 };
158 let Ok(canvas) = canvas_el.dyn_into::<HtmlCanvasElement>() else {
159 return;
160 };
161 let Ok(Some(ctx)) = canvas.get_context("2d") else {
162 return;
163 };
164 let Ok(ctx) = ctx.dyn_into::<CanvasRenderingContext2d>() else {
165 return;
166 };
167 let Ok(image_data) = ImageData::new_with_u8_clamped_array_and_sh(
168 wasm_bindgen::Clamped(&rgba[..]),
169 width,
170 height,
171 ) else {
172 return;
173 };
174 let _ = ctx.put_image_data(&image_data, 0.0, 0.0);
175}