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
6 changes: 4 additions & 2 deletions crates/compositor/src/compositor_macos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1637,8 +1637,10 @@ impl Compositor {

enc.end_encoding();

// --- annotations : calque le plus haut, ancré sur le rect ÉCRAN ---
self.draw_annotations(cmd_buf, scene_ref.as_ref(), g.source_t, g.s_dst)?;
// --- annotations : calque le plus haut, ancré sur le rect ÉCRAN SANS ZOOM ---
// `s_ann`, pas `s_dst` : le zoom vit dans la boîte depuis l'issue #179, donc `s_dst`
// grandit avec lui et emmenait annotations et sous-titres dans le mouvement.
self.draw_annotations(cmd_buf, scene_ref.as_ref(), g.source_t, g.s_ann)?;

// Ni miroir RGBA ni attente ici : le miroir ne sert qu'à `readback_direct` (la
// preview), et l'export ne lit jamais le RGBA — le blit pleine résolution était payé
Expand Down
15 changes: 9 additions & 6 deletions crates/compositor/src/compositor_windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1100,7 +1100,8 @@ impl Compositor {
// inchangé, à l'octet près.
let crate::frame_geometry::FrameGeometry {
scene_preset, mb_taps, source_t, zoom_rotation, padding_scale, cut, s_dst,
s_dst_prev, s_radius, frame_min_px, w_dst, w_dst_prev, w_px, w_radius, shape_fade,
s_dst_prev, s_ann, s_radius, frame_min_px, w_dst, w_dst_prev, w_px, w_radius,
shape_fade,
} = crate::frame_geometry::plan_frame(&crate::frame_geometry::FrameGeometryInput {
render_px: [self.rw(), self.rh()],
screen_tex_px: [stw as f32, sth as f32],
Expand Down Expand Up @@ -1556,14 +1557,16 @@ impl Compositor {
}

// --- annotations : calque le plus haut, comme dans le DOM de la preview (le calque y est
// monté après la vidéo). Ancrées sur `s_dst`, le rect ÉCRAN — c'est le conteneur que reçoit
// l'overlay web (`layout.screenRect`) — et volontairement pas sur le rect de sortie, ni
// sujettes au crop de zoom : dans la preview l'overlay est frère de l'élément qui porte la
// transform, donc les annotations restent en place pendant que le contenu zoome dessous.
// monté après la vidéo). Ancrées sur `s_ann`, le rect ÉCRAN SANS ZOOM — c'est le conteneur
// que reçoit l'overlay web (`layout.screenRect`) — et volontairement pas sur le rect de
// sortie, ni sujettes au zoom : dans la preview l'overlay est frère de l'élément qui porte
// la transform, donc les annotations restent en place pendant que le contenu zoome dessous.
// Ce fut `s_dst` tant que le zoom vivait dans la coupe source ; depuis l'issue #179 il vit
// dans la BOÎTE, et `s_dst` emmenait annotations et sous-titres avec lui.
// `source_t`, la même base de temps que les zoom/speed regions : le temps SOURCE du clip,
// pas le compteur de frames. C'est ce qui garde une annotation alignée sur l'image quand
// une speed region répète ou saute des frames.
self.draw_annotations(scene_ref.as_ref(), source_t, s_dst);
self.draw_annotations(scene_ref.as_ref(), source_t, s_ann);
Ok(())
}

Expand Down
62 changes: 62 additions & 0 deletions crates/compositor/src/frame_geometry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,18 @@ pub struct FrameGeometry {
pub cut: [f32; 4],
pub s_dst: [f32; 4],
pub s_dst_prev: [f32; 4],
/// Boîte écran **sans le zoom** : le conteneur auquel les annotations et les
/// sous-titres sont ancrés.
///
/// C'est `s_dst` avant le `remap_box` du zoom, donc le rect que l'app a résolu
/// (`layout.screenRect`) et que l'overlay web reçoit comme conteneur. Le contrat de
/// `SceneAnnotation` est explicite : « deliberately NOT affected by the zoom crop — the
/// overlay is a sibling of the element carrying the zoom transform, so annotations hold
/// still while the content zooms underneath them ». Tant que le zoom vivait dans la
/// coupe source, `s_dst` tenait ce rôle ; depuis l'issue #179 il vit dans la BOÎTE, donc
/// `s_dst` grandit et se déplace avec lui — et les annotations le suivaient, sous-titres
/// compris, qui se mettaient à zoomer avec l'écran.
pub s_ann: [f32; 4],
pub s_radius: f32,
pub frame_min_px: f32,
pub w_dst: [f32; 4],
Expand Down Expand Up @@ -1079,6 +1091,8 @@ pub fn plan_frame(input: &FrameGeometryInput) -> FrameGeometry {
cut,
s_dst,
s_dst_prev,
// La boîte écran telle qu'elle serait sans zoom : `remap_box` n'est PAS appliqué.
s_ann: s_base,
s_radius,
frame_min_px,
w_dst,
Expand Down Expand Up @@ -1252,6 +1266,54 @@ mod tests {
}
}

/// La même scène, avec une région de zoom active à `t = 1.5 s`.
fn zoomed_golden_scene() -> Scene {
Scene::from_json(
r##"{
"clips":[{"screenPath":"/s.mp4","webcamPath":"/w.mp4","sourceStartSec":0,"sourceEndSec":10,"webcamOffsetSec":0,"hasAudio":true}],
"layout":{"preset":"picture-in-picture","webcamSize":0.44,"webcamShape":"circle","webcamMirror":false,
"webcamPosition":{"cx":0.8577,"cy":0.8159},"webcamReactiveZoom":false},
"effects":{"padding":0.51,"blur":false,"shadow":0.35,"roundnessFrac":0.0255,"motionBlur":0.35},
"background":{"kind":"color","color":"#1e1e2e"},
"zoomRegions":[{"clipIndex":0,"startSec":0.0,"endSec":5.0,"scale":2.0,"focusX":0.5,"focusY":0.3,"rotation":"none"}],
"cursor":{"show":true,"size":7.76,"smoothing":0,"motionBlur":0.35,"clickBounce":1,"clipToBounds":false,"theme":"default"},
"cropByClip":[{"x":0,"y":0,"width":0.61,"height":0.61}],
"output":{"width":1170,"height":658,"fps":60}
}"##,
)
.expect("zoomed golden scene")
}

/// L'ancre des annotations ne bouge PAS avec le zoom, alors que la boîte écran, si.
///
/// C'est tout le contrat de `SceneAnnotation` : l'overlay web est frère de l'élément qui
/// porte la transform de zoom, donc annotations et sous-titres tiennent en place pendant
/// que le contenu grossit dessous. Tant que le zoom vivait dans la coupe source, `s_dst`
/// jouait ce rôle sans effort ; depuis l'issue #179 il vit dans la BOÎTE, et le natif
/// zoomait les sous-titres avec l'écran. Ce test échoue si `s_ann` se remet à suivre.
#[test]
fn the_annotation_anchor_ignores_the_zoom() {
let cfg = crate::config::all().pop().expect("au moins une config");
let plain = golden_scene();
let zoomed = zoomed_golden_scene();
let a = plan_frame(&golden_input(&plain, &cfg));
let b = plan_frame(&golden_input(&zoomed, &cfg));

assert_ne!(
a.s_dst, b.s_dst,
"le zoom doit bel et bien agir sur la boîte écran (issue #179) — \
sinon ce test ne prouve rien"
);
assert_eq!(
a.s_ann, b.s_ann,
"l'ancre des annotations a suivi le zoom : sans zoom {:?}, avec zoom {:?}",
a.s_ann, b.s_ann
);
// Et sans zoom, l'ancre EST la boîte écran : `s_ann` ne doit pas devenir un rect
// parallèle qui dériverait de `s_dst` pour d'autres raisons (padding, cover, crop).
assert_eq!(a.s_ann, a.s_dst, "sans zoom, ancre et boîte écran coïncident");
}

/// **Le golden iso-render.**
///
/// Les deux backends ne peuvent pas tourner sur la même machine, donc « iso avec
Expand Down
1 change: 1 addition & 0 deletions crates/compositor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ pub mod gif_export;
pub mod regions;
pub mod scene;
pub mod text_anim;
pub mod text_plate;
pub(crate) mod timeline_walk;

// GPU backend : Windows → d3d_windows, macOS → d3d_macos. Ré-exporté sous le nom `d3d`
Expand Down
Loading
Loading