diff --git a/apps/vectorcraft-web/src/pencil.rs b/apps/vectorcraft-web/src/pencil.rs index 4416196cd..8651bbf60 100644 --- a/apps/vectorcraft-web/src/pencil.rs +++ b/apps/vectorcraft-web/src/pencil.rs @@ -73,7 +73,7 @@ impl Pointers { } /// Follow one pointer event; `true` if it was the pen's or a finger's (taken from eframe). - fn feed(&mut self, kind: &str, e: &web_sys::PointerEvent, pos: Pos2, on_canvas: bool) -> bool { + fn feed(&mut self, kind: &str, e: &web_sys::PointerEvent, pos: Pos2, coalesced: &[(Pos2, f32)], on_canvas: bool) -> bool { let id = e.pointer_id(); match e.pointer_type().as_str() { "pen" => { @@ -91,6 +91,12 @@ impl Pointers { self.press(id, pos); } "pointermove" if self.pen == Some(id) => { + // The positions the pen reported between two events (up to 240 a second + // on an iPad), each with its pressure, so fast strokes keep their curve. + for c in coalesced.iter().take(coalesced.len().saturating_sub(1)) { + self.touch(PEN_DEVICE, id, TouchPhase::Move, c.0, Some(c.1)); + self.events.push(Event::PointerMoved(c.0)); + } self.touch(PEN_DEVICE, id, TouchPhase::Move, pos, force); self.events.push(Event::PointerMoved(pos)); } @@ -195,7 +201,21 @@ pub fn install(runner: &eframe::WebRunner, canvas: &web_sys::HtmlCanvasElement, let rect = canvas.get_bounding_client_rect(); let z = runner.egui_ctx().zoom_factor(); let pos = egui::pos2((e.client_x() as f32 - rect.left() as f32) / z, (e.client_y() as f32 - rect.top() as f32) / z); - if !pointers.borrow_mut().feed(kind, &e, pos, on_canvas) { + let at = |x: i32, y: i32| egui::pos2((x as f32 - rect.left() as f32) / z, (y as f32 - rect.top() as f32) / z); + let coalesced: Vec<(Pos2, f32)> = if kind == "pointermove" + && e.pointer_type() == "pen" + // Safari before 17 hasn't it, and calling a missing method would abort the app. + && js_sys::Reflect::has(&e, &"getCoalescedEvents".into()).unwrap_or(false) + { + e.get_coalesced_events() + .iter() + .filter_map(|c| c.dyn_into::().ok()) + .map(|c| (at(c.client_x(), c.client_y()), c.pressure().clamp(0.0, 1.0))) + .collect() + } else { + vec![] + }; + if !pointers.borrow_mut().feed(kind, &e, pos, &coalesced, on_canvas) { return; } e.stop_immediate_propagation(); diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index fc1e11be2..7dcfae666 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -115,6 +115,34 @@ fn pen_pressure(ui: &Ui, pressed: bool) -> f32 { p } +/// The positions this frame's pointer moves passed through before its last one, each with the +/// pen force a touch event gave just before it (if any). The last move is the pointer's position. +/// Only for pens and fingers (frames with touch events): a mouse's moves are left as they were. +fn frame_moves(ui: &Ui) -> Vec<(Pos2, Option)> { + ui.input(|i| { + if !i.events.iter().any(|e| matches!(e, egui::Event::Touch { .. })) { + return vec![]; + } + let mut force = None; + let mut moves = vec![]; + for e in &i.events { + match e { + egui::Event::Touch { force: Some(f), .. } if f.is_finite() && *f > 0.0 => force = Some(f.clamp(0.0, 1.0)), + egui::Event::PointerMoved(p) => moves.push((*p, force)), + _ => {} + } + } + moves.pop(); + // A burst of events (a stalled frame) is thinned to a bounded count. + const MAX: usize = 64; + if moves.len() > MAX { + let step = moves.len().div_ceil(MAX); + moves = moves.into_iter().step_by(step).collect(); + } + moves + }) +} + pub fn mods(m: egui::Modifiers, space: bool) -> Mods { Mods { shift: m.shift, alt: m.alt, cmd: m.command, ctrl: m.ctrl, space } } @@ -691,6 +719,12 @@ fn handle_input(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Response, rect: } Drag::Tool => { if pointer.delta() != egui::Vec2::ZERO { + // Every position the pen or mouse passed this frame, not just the last: a + // fast stroke keeps its curve (a pen reports far more often than frames come). + for (at, force) in frame_moves(ui) { + let pressure = force.unwrap_or_else(|| pen_pressure(ui, false)); + dispatch(app, &PointerEvent { kind: PointerKind::Drag, pos: xf.to_doc(at), mods: mods(m, space), pressure }, view); + } let ev = PointerEvent { kind: PointerKind::Drag, pos: xf.to_doc(p), mods: mods(m, space), pressure: pen_pressure(ui, false) }; dispatch(app, &ev, view); }