feat(canvas): pen strokes keep every position reported between frames

A pen reports far more often than frames come (an Apple Pencil up to 240 times
a second): the canvas now hands the tool each move of the frame with its
pressure instead of the last one only, and the web shell feeds Safari's
coalesced pen events. Mouse input is unchanged.
This commit is contained in:
gniax
2026-10-16 01:36:24 +07:00
committed by bflatastic
parent 51673cd098
commit 9fc7fd7038
2 changed files with 56 additions and 2 deletions
+22 -2
View File
@@ -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::<web_sys::PointerEvent>().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();
+34
View File
@@ -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<f32>)> {
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);
}