Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions crates/compositor/src/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,19 +368,30 @@ impl CursorTrack {
/// Pour chaque clic `tc`, un poids vers son point : il monte en smoothstep sur
/// `PIN_APPROACH_S`, vaut 1 sur `[tc, tc + PIN_HOLD_S]` (le contact), puis retombe en
/// smoothstep sur `PIN_RELEASE_S`. Les clics s'appliquent dans l'ordre, le plus récent
/// l'emporte. Continue et de pente continue en `t` ; hors de ces fenêtres, `at(t)` à l'identique.
/// l'emporte. L'approche d'un clic ne commence qu'à la fin de la tenue du précédent : deux clics
/// rapprochés posent chacun la pointe sur leur point. Sous `2 * PIN_HOLD_S` d'écart, la tenue et
/// l'approche suivante se partagent l'écart à parts égales. Continue en `t` ; hors de ces
/// fenêtres, `at(t)` à l'identique.
pub fn pinned_at(&self, t: f32) -> Option<(f32, f32)> {
let (mut x, mut y) = self.at(t)?;
let lo = self
.clicks
.partition_point(|&tc| tc <= t - PIN_HOLD_S - PIN_RELEASE_S);
let hi = self.clicks.partition_point(|&tc| tc < t + PIN_APPROACH_S);
let hold = |i: usize| match self.clicks.get(i + 1) {
Some(&next) => ((next - self.clicks[i]) * 0.5).min(PIN_HOLD_S),
None => PIN_HOLD_S,
};
for i in lo..hi.max(lo) {
let d = t - self.clicks[i];
let w = if d < 0.0 {
smoothstep01(1.0 + d / PIN_APPROACH_S)
let approach = match i.checked_sub(1) {
Some(p) => (self.clicks[i] - self.clicks[p] - hold(p)).clamp(1e-3, PIN_APPROACH_S),
None => PIN_APPROACH_S,
};
smoothstep01(1.0 + d / approach)
} else {
1.0 - smoothstep01((d - PIN_HOLD_S) / PIN_RELEASE_S)
1.0 - smoothstep01((d - hold(i)) / PIN_RELEASE_S)
};
let (cx, cy) = self.click_points[i];
(x, y) = if w >= 1.0 {
Expand Down Expand Up @@ -948,6 +959,24 @@ mod tests {
);
}

/// Deux clics à 60 ms, sur deux points distincts : la pointe se pose sur chacun à son instant, et
/// passe de l'un à l'autre sans sauter d'une image à l'autre.
#[test]
fn close_clicks_on_distinct_points_glide_between_them() {
let samples = vec![(0.0, 0.2, 0.2), (1.0, 0.2, 0.2), (1.06, 0.6, 0.5), (2.0, 0.6, 0.5)];
let track = CursorTrack::new(samples, vec![1.0, 1.06], vec![]).smoothed(0.67);
let (a, b): ((f32, f32), (f32, f32)) = ((0.2, 0.2), (0.6, 0.5));
assert_eq!(track.pinned_at(1.0), Some(a));
assert_eq!(track.pinned_at(1.06), Some(b));
let gap = (b.0 - a.0).hypot(b.1 - a.1);
for k in 1000..1060 {
let p = track.pinned_at(k as f32 * 0.001).unwrap();
let q = track.pinned_at((k + 1) as f32 * 0.001).unwrap();
let step = (q.0 - p.0).hypot(q.1 - p.1);
assert!(step < 0.06 * gap, "{k} ms : saut de {step}");
}
}

/// `pinned_at` pose le curseur sur le point BRUT du clic pendant le contact, même quand la
/// piste lissée traîne loin derrière, reste continue, et rend `at()` loin des clics.
#[test]
Expand Down
32 changes: 28 additions & 4 deletions crates/compositor/src/frame_geometry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3834,11 +3834,11 @@ impl<'a> CursorPlacer<'a> {
}
}

/// La pointe à `t`. Le curseur modélisé touche le plan à chaque clic : il se pose sur le point
/// cliqué brut (`pinned_at`), pas sur la piste lissée qui traîne derrière la souris. Le sprite
/// plat garde la piste telle quelle, son rendu est celui d'avant.
/// La pointe à `t`, posée sur le point cliqué brut à chaque clic (`pinned_at`), pas sur la piste
/// lissée qui traîne derrière la souris. Sprite plat comme modèle 3D : le rebond, l'anneau
/// d'impact et `ctx.clicks` se centrent sur ce point brut, la pointe doit s'y trouver.
fn at(&self, t: f32) -> Option<(f32, f32)> {
if self.model3d { self.track.pinned_at(t) } else { self.track.at(t) }
self.track.pinned_at(t)
}

/// `None` hors de la coupe courante (zoom serré, hors écran).
Expand Down Expand Up @@ -9279,6 +9279,30 @@ mod tests {
assert_eq!(fx(2.5).cursor, Some(drawn.placement.upright_center()), "là où le curseur se dessine");
}

/// Le sprite plat aussi se pose sur le clic brut : à fort lissage, le ressort traîne loin derrière
/// une souris qui clique dès l'arrivée, mais la pointe dessinée, `ctx.cursor` et `ctx.clicks`
/// tombent sur le même pixel pendant le contact.
#[test]
fn the_flat_cursor_lands_on_the_click_under_smoothing() {
const TC: f32 = 1.5;
let scene = model_scene();
let cfg = crate::config::all().pop().expect("cfg");
let fg = full_frame_geometry();
let raw = approach_track([0.08, 0.05], [0.7, 0.6], [0.2, 0.9], 2.0, 0.0, TC);
for smoothing in [0.0, 0.3, 0.67, 1.0] {
let track = raw.smoothed(smoothing);
for ms in [0, 40, 100] {
let input = cursor_input(&scene, &cfg, &track, TC + ms as f32 / 1000.0);
let fx = effect_pointer(&fg, &input);
let (cursor, click) = (fx.cursor.expect("pointe"), fx.clicks[0].0);
let drawn = plan_cursor(&fg, &input).expect("visible").placement.upright_center();
let off = |a: [f32; 2], b: [f32; 2]| ((a[0] - b[0]) * 1920.0).hypot((a[1] - b[1]) * 1080.0);
assert!(off(cursor, click) < 0.5, "lissage {smoothing}, {ms} ms : {} px", off(cursor, click));
assert!(off(drawn, click) < 0.5, "lissage {smoothing}, {ms} ms : {} px", off(drawn, click));
}
}
}

/// Masqué par l'utilisateur, le curseur n'est plus dessiné, mais les effets gardent la pointe
/// et les clics : un halo suit toujours la souris, une onde part toujours du clic. Quand c'est
/// l'enregistrement qui cache la pointe, les effets la perdent, et gardent les clics d'avant.
Expand Down
3 changes: 2 additions & 1 deletion src/lib/projectDefaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,8 @@ export const DEFAULT_PROJECT_APPEARANCE: ProjectAppearanceDefaults = {
// 3.6 draws a 98 px arrow in a 1080p export, about five times the system one: the cursor
// effects are part of the product, and a smaller arrow plays them down. 6 reaches 164 px.
size: 3.6,
smoothing: 0.67,
// About 95 ms of spring lag: 0.67 trailed the pointer by 190 ms, 150 px on a quick click.
smoothing: 0.3,
motionBlur: 0.35,
// A light tap: 2.5 squashed the arrow to 40% and threw it to 140% in 260 ms on every click.
clickBounce: 1,
Expand Down
Loading