Skip to content

Commit 79a54f2

Browse files
committed
feat(camera): desk view for Full Camera sections, with a covered tilt
A Full Camera section can now show a webcam tilted down onto the desk: "Desk view" in the inspector turns the camera 180°, switches the mirror off for that section (otherwise text on the page reads back to front) and shows the whole camera frame instead of the face crop. The timeline marks such sections with a rotate icon. The moment the camera is tilted is covered at both ends of the section: the whole camera picture is blurred and dimmed for the Full Camera grow (and the shrink), fading in and out around it, with a translated "Desk mode" label over it that can be switched off per section. Preview and export render it identically through the compositor's frame plan. - Rust: per-frame orientation (u/v bound swaps, crop bypass) and cover strength, a `LayerCB.cover` lane (HLSL, WGSL, MSL), the blur clamped to the camera's valid area so aligned decoder padding never bleeds in; the label is drawn at the cover strength of the same frame, so it fades exactly with the cover. - App: `rotation` / `mirror` / `deskLabel` on Full Camera regions (defaults are not stored), store and persistence, inspector controls, one generated label annotation per projected piece, translations in every locale. - Tests for each layer; WGSL is validated with naga on every host.
1 parent 9c4417e commit 79a54f2

44 files changed

Lines changed: 1715 additions & 77 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎crates/compositor/src/compositor_linux.rs‎

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,12 @@ fn layer_source(models: bool) -> String {
6767
/// en parcourant les 18 wallpapers livres) en laissant le jeu actif resident.
6868
const IMG_CACHE_BUDGET_BYTES: u64 = 512 * 1024 * 1024;
6969

70-
/// Taille du buffer uniforme d'un calque : `LayerCB` entier (176 octets), le `struct Layer` de
70+
/// Taille du buffer uniforme d'un calque : `LayerCB` entier (192 octets), le `struct Layer` de
7171
/// `layer.wgsl`. `blur.wgsl` n'en lit que les 128 premiers.
7272
const LAYER_BYTES: u64 = std::mem::size_of::<LayerCB>() as u64;
7373

7474
/// `&LayerCB` -> ses octets. `LayerCB` est `#[repr(C, align(16))]`, son layout EST le buffer
75-
/// uniforme WGSL (dix vec4 et un vec2 + 2 f32 = 176 octets).
75+
/// uniforme WGSL (douze vec4 = 192 octets).
7676
fn layer_bytes(cb: &LayerCB) -> &[u8] {
7777
unsafe { std::slice::from_raw_parts(cb as *const LayerCB as *const u8, LAYER_BYTES as usize) }
7878
}
@@ -2485,25 +2485,24 @@ impl Compositor {
24852485
let [cu0, cv0, cu1, cv1] = crate::frame_geometry::webcam_source_rect(
24862486
[wcw, wch],
24872487
[wtw as f32, wth as f32],
2488-
scene_ref
2489-
.as_ref()
2490-
.and_then(|scene| scene.layout.webcam_crop),
2488+
if g.webcam.full_frame {
2489+
None
2490+
} else {
2491+
scene_ref.as_ref().and_then(|scene| scene.layout.webcam_crop)
2492+
},
24912493
g.w_px[0] / g.w_px[1].max(0.0001),
24922494
);
2493-
// MIROIR : on inverse l'intervalle u. Le VS interpole `src`
2494-
// lineairement et `fs_main` ne re-clampe pas `i.uv`, donc un
2495-
// intervalle a l'envers suffit -- aucune retouche du WGSL. Apres le
2496-
// cover-crop les deux bornes sont strictement a l'interieur de la
2497-
// texture, donc le sampler ClampToEdge ne bave pas sur les bords.
2498-
let (u0, u1) = if lp.webcam_mirror { (cu1, cu0) } else { (cu0, cu1) };
2495+
// Mirror and the desk-shot turn are both bound swaps: u for horizontal, v for vertical.
2496+
let (u0, u1) = if g.webcam.flip_u { (cu1, cu0) } else { (cu0, cu1) };
2497+
let (v0, v1) = if g.webcam.flip_v { (cv1, cv0) } else { (cv0, cv1) };
24992498
// `src_prev` doit valoir EXACTEMENT le `src` de ce draw, miroir
25002499
// compris : le shader s'en sert pour reconstruire l'UV de la frame
25012500
// precedente, et un rect source qui ne correspond pas au calque
25022501
// dessine ferait diverger la trainee vers une zone de la texture qui
25032502
// n'a jamais ete affichee. Seul `dst_prev` porte le mouvement.
25042503
let cb = LayerCB {
25052504
dst: g.w_dst,
2506-
src: [u0, cv0, u1, cv1],
2505+
src: [u0, v0, u1, v1],
25072506
quad_px: g.w_px,
25082507
radius_px: g.w_radius,
25092508
mode: 0.0,
@@ -2515,9 +2514,10 @@ impl Compositor {
25152514
// `fx.z` = mode, `fx.w` = intensite du flou. Contrat commun aux
25162515
// trois back-ends, cf. `layer.wgsl` et `webcam-segmentation.md`.
25172516
fx: [w_valid[0], w_valid[1], effect_code, blur_intensity],
2518-
src_prev: [u0, cv0, u1, cv1],
2517+
src_prev: [u0, v0, u1, v1],
25192518
dst_prev: g.w_dst_prev,
25202519
mb: [g.mb_taps, g.mb_amount, 1.0, 0.0],
2520+
cover: [g.webcam_cover, 0.04 * g.w_px[0].min(g.w_px[1]) * g.webcam_cover, 0.35, 0.0],
25212521
..Default::default()
25222522
};
25232523
// Le masque est lie par `make_bind` sur tous les draws, pas seulement
@@ -2753,6 +2753,13 @@ impl Compositor {
27532753
if text.content.trim().is_empty() {
27542754
continue;
27552755
}
2756+
// The desk label shows only while the camera is covered: skip the rest of
2757+
// its section before rasterizing anything.
2758+
if crate::text_anim::is_desk_cover(text.animation.as_deref())
2759+
&& g.webcam_cover <= 0.0
2760+
{
2761+
continue;
2762+
}
27562763
let color = parse_hex(&text.color).unwrap_or([1.0, 1.0, 1.0, 1.0]);
27572764
let background =
27582765
parse_hex(&text.background_color).unwrap_or([0.0, 0.0, 0.0, 0.0]);
@@ -2787,10 +2794,11 @@ impl Compositor {
27872794
// et remis a l'echelle de la sortie, comme la taille de
27882795
// police : en px absolus la meme animation sauterait deux
27892796
// fois plus haut dans un rendu 4K que dans l'apercu.
2790-
let anim = crate::text_anim::text_animation_state(
2797+
let anim = crate::text_anim::annotation_text_state(
27912798
text.animation.as_deref(),
27922799
(g.source_t - a.start_sec as f32) * 1000.0,
27932800
((a.end_sec - a.start_sec) * 1000.0) as f32,
2801+
g.webcam_cover,
27942802
);
27952803
let anim_px = rh / crate::text_anim::ANIMATION_REFERENCE_HEIGHT;
27962804
let (mut ax, mut ay, mut aw, mut ah) = (
@@ -4888,6 +4896,9 @@ mod tests {
48884896
clip_index: None,
48894897
start_sec: 2.0,
48904898
end_sec: 8.0,
4899+
rotation: 0,
4900+
mirror: None,
4901+
full_frame: false,
48914902
});
48924903
// 5 s : la montee (~1 s) est finie, le retour (~1,5 s) n'a pas commence.
48934904
comp.set_timeline_time(Some(5.0));

‎crates/compositor/src/compositor_macos.rs‎

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1673,6 +1673,13 @@ impl Compositor {
16731673
if text.content.trim().is_empty() {
16741674
continue;
16751675
}
1676+
// The desk label shows only while the camera is covered: skip the rest of
1677+
// its section before rasterizing anything.
1678+
if crate::text_anim::is_desk_cover(text.animation.as_deref())
1679+
&& g.webcam_cover <= 0.0
1680+
{
1681+
continue;
1682+
}
16761683
let spec = crate::text::TextSpec {
16771684
content: text.content.clone(),
16781685
color: parse_hex(&text.color).unwrap_or([1.0, 1.0, 1.0, 1.0]),
@@ -1706,10 +1713,11 @@ impl Compositor {
17061713
}) else {
17071714
continue;
17081715
};
1709-
let anim = crate::text_anim::text_animation_state(
1716+
let anim = crate::text_anim::annotation_text_state(
17101717
text.animation.as_deref(),
17111718
(t - a.start_sec as f32) * 1000.0,
17121719
((a.end_sec - a.start_sec) * 1000.0) as f32,
1720+
g.webcam_cover,
17131721
);
17141722
let anim_px = rh / crate::text_anim::ANIMATION_REFERENCE_HEIGHT;
17151723
let (mut ax, mut ay, mut aw, mut ah) = (
@@ -2663,10 +2671,16 @@ impl Compositor {
26632671
let [cu0, cv0, cu1, cv1] = crate::frame_geometry::webcam_source_rect(
26642672
[wcw, wch],
26652673
[wtw as f32, wth as f32],
2666-
scene_ref.as_ref().and_then(|scene| scene.layout.webcam_crop),
2674+
if g.webcam.full_frame {
2675+
None
2676+
} else {
2677+
scene_ref.as_ref().and_then(|scene| scene.layout.webcam_crop)
2678+
},
26672679
g.w_px[0] / g.w_px[1].max(0.0001),
26682680
);
2669-
let (u0, u1) = if lp.webcam_mirror { (cu1, cu0) } else { (cu0, cu1) };
2681+
// Mirror and the desk-shot turn are both bound swaps: u for horizontal, v for vertical.
2682+
let (u0, u1) = if g.webcam.flip_u { (cu1, cu0) } else { (cu0, cu1) };
2683+
let (v0, v1) = if g.webcam.flip_v { (cv1, cv0) } else { (cv0, cv1) };
26702684
let webcam_is_block = matches!(
26712685
g.scene_preset.as_deref(),
26722686
Some("dual-frame") | Some("vertical-stack")
@@ -2733,17 +2747,18 @@ impl Compositor {
27332747
enc,
27342748
&LayerCB {
27352749
dst: g.w_dst,
2736-
src: [u0, cv0, u1, cv1],
2750+
src: [u0, v0, u1, v1],
27372751
quad_px: g.w_px,
27382752
radius_px: g.w_radius,
27392753
mode: 0.0,
27402754
// `color.a` porte l'alpha du découpage (`color.a * personne`) ; le RGB n'est
27412755
// plus lu, le fond ayant déjà été peint sous la caméra.
27422756
color: [0.0, 0.0, 0.0, 1.0],
27432757
fx: [w_valid[0], w_valid[1], effect_code, blur_intensity],
2744-
src_prev: [u0, cv0, u1, cv1],
2758+
src_prev: [u0, v0, u1, v1],
27452759
dst_prev: g.w_dst_prev,
27462760
mb: [g.mb_taps, g.mb_amount, 1.0, 0.0],
2761+
cover: [g.webcam_cover, 0.04 * g.w_px[0].min(g.w_px[1]) * g.webcam_cover, 0.35, 0.0],
27472762
..Default::default()
27482763
},
27492764
wy,

‎crates/compositor/src/compositor_windows.rs‎

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2438,13 +2438,16 @@ impl Compositor {
24382438
let [su0, sv0, su1, sv1] = crate::frame_geometry::webcam_source_rect(
24392439
[wcw, wch],
24402440
[wtw as f32, wth as f32],
2441-
scene_ref
2442-
.as_ref()
2443-
.and_then(|scene| scene.layout.webcam_crop),
2441+
if g.webcam.full_frame {
2442+
None
2443+
} else {
2444+
scene_ref.as_ref().and_then(|scene| scene.layout.webcam_crop)
2445+
},
24442446
w_px[0] / w_px[1].max(0.0001),
24452447
);
2446-
// miroir = échanger les bornes u du rect source (flip horizontal).
2447-
let (u0, u1) = if lp.webcam_mirror { (su1, su0) } else { (su0, su1) };
2448+
// Mirror and the desk-shot turn are both bound swaps: u for horizontal, v for vertical.
2449+
let (u0, u1) = if g.webcam.flip_u { (su1, su0) } else { (su0, su1) };
2450+
let (v0, v1) = if g.webcam.flip_v { (sv1, sv0) } else { (sv0, sv1) };
24482451
if lp.has_webcam {
24492452
// L'ombre portée appartient à la bulle flottante PiP : elle se retire avec elle
24502453
// (`shape_fade`), pour qu'au plein écran plus rien n'encadre la caméra. C'est une
@@ -2508,17 +2511,18 @@ impl Compositor {
25082511
self.draw_video(
25092512
&LayerCB {
25102513
dst: w_dst,
2511-
src: [u0, sv0, u1, sv1],
2514+
src: [u0, v0, u1, v1],
25122515
quad_px: w_px,
25132516
radius_px: w_radius,
25142517
mode: 0.0,
25152518
// `color.a` porte l'alpha du découpage (`color.a * personne`) ; le RGB n'est
25162519
// plus lu, le fond ayant déjà été peint sous la caméra.
25172520
color: [0.0, 0.0, 0.0, 1.0],
25182521
fx: [w_valid[0], w_valid[1], effect_code, blur_intensity],
2519-
src_prev: [u0, sv0, u1, sv1], // src fixe (pas de zoom webcam)
2522+
src_prev: [u0, v0, u1, v1], // src fixe (pas de zoom webcam)
25202523
dst_prev: w_dst_prev,
25212524
mb: [mb_taps, mb_amount, 1.0, 0.0],
2525+
cover: [g.webcam_cover, 0.04 * w_px[0].min(w_px[1]) * g.webcam_cover, 0.35, 0.0],
25222526
..Default::default()
25232527
},
25242528
&wy,
@@ -2741,6 +2745,13 @@ impl Compositor {
27412745
if text.content.trim().is_empty() {
27422746
continue;
27432747
}
2748+
// The desk label shows only while the camera is covered: skip the rest of
2749+
// its section before rasterizing anything.
2750+
if crate::text_anim::is_desk_cover(text.animation.as_deref())
2751+
&& g.webcam_cover <= 0.0
2752+
{
2753+
continue;
2754+
}
27442755
// `font_size_rel` est une fraction de la HAUTEUR DE LA BOÎTE D'ANCRAGE — rect
27452756
// écran, ou cadre de sortie pour un sous-titre (cf. le contrat et
27462757
// `annotationScale.ts`) : on la ramène en pixels de sortie ici, avec le même
@@ -2786,10 +2797,13 @@ impl Compositor {
27862797
// dispose ici) : dans une région accélérée, elle défile donc au rythme du
27872798
// clip. À vitesse 1 — le cas de toutes les annotations existantes — c'est
27882799
// exactement le timing de l'aperçu DOM.
2789-
let anim = crate::text_anim::text_animation_state(
2800+
// The desk label is the exception: its opacity is the camera cover, which
2801+
// runs on the screen clock (`annotation_text_state`).
2802+
let anim = crate::text_anim::annotation_text_state(
27902803
text.animation.as_deref(),
27912804
(t - annotation.start_sec as f32) * 1000.0,
27922805
((annotation.end_sec - annotation.start_sec) * 1000.0) as f32,
2806+
g.webcam_cover,
27932807
);
27942808
// Les décalages sont donnés à la hauteur de référence : on les ramène à la
27952809
// sortie, comme la taille de police, pour que l'animation ait la même

0 commit comments

Comments
 (0)