Skip to content

Commit ed099ae

Browse files
MavisMavis
authored andcommitted
fix(compositor): make the zoom-frame growth continuous instead of binary
Follow-up to the original issue #179 fix and the f7c4317 bandaid. The two earlier commits each made the wrong call: the original swap to [0,0,1,1] in one step (abrupt padding disappearance), and f7c4317 kept the same switch but neutralized the shadow and radius to mask the regression (still abrupt on the padding ring, and now also abruptly loses the frame on zoom engage). The right behavior: the "zoom frame" (s_dst) grows continuously with the zoom, from the padded size up to the full frame. At zoom = 1 it's the padded area. As the zoom ramps up, the frame expands until it hits the frame edge at zoom = 1 / padding_scale (= frame / padded_size). Past that the frame stays put and only the source rect keeps shrinking — the GPU upscales further. The source texture itself is never touched, so full resolution is preserved all the way; only the mapping changes. s_radius and the shadow follow s_dst naturally now (s_min_px grows with the zoom frame), so the f7c4317 gates on those are removed. Result: no more binary switch anywhere. The padding smoothly fades as the zoom ramps up, the frame "follows" the zoom, and by the time the content hits the edges the frame is already at full size — visually consistent. Single TODO kept, pointing at the still-pending "frame rect" separation: if you want the shadow and corners to stay anchored at the padded box while the content overflows (the "window" effect), split the frame rect from the content rect and apply shadow+corners to the frame alone.
1 parent f7c4317 commit ed099ae

1 file changed

Lines changed: 59 additions & 36 deletions

File tree

‎crates/compositor/src/compositor.rs‎

Lines changed: 59 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1765,26 +1765,46 @@ impl Compositor {
17651765
fit_dst_to_aspect(scale_frame(dst, padding_scale), crop_aspect)
17661766
}
17671767
};
1768-
// ponytail: quand on zoome (p.zoom > 1.0), le contenu écran était
1769-
// contraint au rect paddé (s_dst), ce qui rendait le zoom plus faible
1770-
// que prévu — il s'arrêtait à la frontière paddée au lieu de déborder
1771-
// jusqu'aux bords du cadre (rapport issue #179). On étend s_dst au
1772-
// cadre complet quand un zoom est actif, pour que le contenu zoomé
1773-
// remplisse toute la frame, comme attendu. Le rayon et l'ombre sont
1774-
// neutralisés en parallèle (cf. plus bas) : sinon ils suivent
1775-
// l'expansion et arrondissent / ombrent tout l'output, pas l'écran.
1776-
// TODO : à la place, séparer "rect contenu" de "rect cadre" pour garder
1777-
// le cadre (ombre, coins) au rect paddé pendant que le contenu déborde.
1778-
let s_dst = if p.zoom > 1.0 {
1779-
[0.0, 0.0, 1.0, 1.0]
1780-
} else {
1781-
fit_screen(p.screen.dst)
1782-
};
1783-
let s_dst_prev = if pp.zoom > 1.0 {
1784-
[0.0, 0.0, 1.0, 1.0]
1785-
} else {
1786-
fit_screen(pp.screen.dst)
1768+
// ponytail (issue #179) : le rect destination de l'écran ("zoom frame")
1769+
// grandit avec le zoom, du rect paddé jusqu'au cadre complet — plus
1770+
// jamais le switch binaire d'avant (padded → [0,0,1,1] en une frame,
1771+
// qui faisait disparaître le padding instantanément et envoyait l'ombre
1772+
// et les coins sur tout l'output). Le source rect (computé par
1773+
// `screen_source_rect` plus bas) et le zoom frame sont liés : le GPU
1774+
// mappe le source rect sur le zoom frame, et le padding est le delta
1775+
// entre le zoom frame et le cadre. Le padding s'efface smoothly à
1776+
// mesure que le zoom monte, et disparaît pile quand le zoom atteint
1777+
// `1 / padding_scale` (= cadre / padded_size) — au-delà, le zoom
1778+
// frame reste au cadre et seul le source rect continue de rétrécir
1779+
// (le GPU upscale davantage). Le media reste à pleine résolution
1780+
// toute la durée : c'est le *mapping* qui s'ajuste, pas la texture.
1781+
//
1782+
// TODO : si on veut que l'ombre et les coins restent ancrés au rect
1783+
// paddé pendant que le contenu déborde (effet "fenêtre"), il faudra
1784+
// séparer le rect "cadre" du rect "contenu" et appliquer l'ombre et
1785+
// les coins au cadre seul. Pour l'instant ils suivent le zoom frame,
1786+
// ce qui donne un rendu cohérent (le cadre "suit" le zoom) sans
1787+
// l'abrupt switch.
1788+
let zoom_grow = |zoom: f32, base: [f32; 4]| -> [f32; 4] {
1789+
// growth = min(zoom, 1/padding_scale) : on sature pile quand le
1790+
// zoom frame atteint le cadre. padding_scale est dans (0, 1] ;
1791+
// `max(0.0001)` évite une division par 0 sur des fixtures
1792+
// pathologiques (padding = 100% clampé à 0.6 → 1.667, jamais
1793+
// infini en pratique).
1794+
let growth = zoom.min(1.0 / padding_scale.max(0.0001));
1795+
let cx = base[0] + base[2] * 0.5;
1796+
let cy = base[1] + base[3] * 0.5;
1797+
let nw = (base[2] * growth).min(1.0);
1798+
let nh = (base[3] * growth).min(1.0);
1799+
[
1800+
(cx - nw * 0.5).max(0.0).min(1.0 - nw),
1801+
(cy - nh * 0.5).max(0.0).min(1.0 - nh),
1802+
nw,
1803+
nh,
1804+
]
17871805
};
1806+
let s_dst = zoom_grow(p.zoom, fit_screen(p.screen.dst));
1807+
let s_dst_prev = zoom_grow(pp.zoom, fit_screen(pp.screen.dst));
17881808
// le padding n'affecte QUE l'écran (la quantité de fond révélée). La webcam reste ancrée
17891809
// en bas-droite à sa marge fixe, quelle que soit la valeur de padding (pas de scale_frame)
17901810
// — SAUF quand l'app a résolu un placement explicite (`app_webcam_rect`, drag-to-reposition
@@ -1863,18 +1883,19 @@ impl Compositor {
18631883
// (les deux quantités coïncident quand s_dst = [0,0,1,1]). TODO : à la place,
18641884
// séparer "rect contenu" de "rect cadre" pour garder les coins au rect paddé
18651885
// pendant que le contenu déborde (cf. le TODO sur `s_dst` plus haut).
1866-
let s_radius = if p.zoom > 1.0 {
1867-
0.0
1868-
} else {
1869-
match (cfg.rounded, app_screen_radius_frac, scene_roundness_frac) {
1870-
(false, _, _) => 0.0,
1871-
// Preset en bloc : le rayon appartient à la boîte écran (parité exacte avec la caméra).
1872-
(true, Some(f), _) => f * s_min_px,
1873-
// Scène sans rayon imposé : slider Roundness, relatif au cadre.
1874-
(true, None, Some(f)) => f * frame_min_px,
1875-
// Fixture/bench (pas de scène) : chemin inspector historique, inchangé.
1876-
(true, None, None) => p.screen.radius * lp.radius_scale,
1877-
}
1886+
//
1887+
// (révision : la croissance de `s_dst` est *continue* via `zoom_grow` au-dessus,
1888+
// donc `s_min_px` suit le zoom frame et le rayon aussi — pas de switch
1889+
// binaire. Le `if p.zoom > 1.0 { 0.0 }` n'est plus nécessaire ; on le
1890+
// retire pour laisser le rayon suivre naturellement le zoom frame.)
1891+
let s_radius = match (cfg.rounded, app_screen_radius_frac, scene_roundness_frac) {
1892+
(false, _, _) => 0.0,
1893+
// Preset en bloc : le rayon appartient à la boîte écran (parité exacte avec la caméra).
1894+
(true, Some(f), _) => f * s_min_px,
1895+
// Scène sans rayon imposé : slider Roundness, relatif au cadre.
1896+
(true, None, Some(f)) => f * frame_min_px,
1897+
// Fixture/bench (pas de scène) : chemin inspector historique, inchangé.
1898+
(true, None, None) => p.screen.radius * lp.radius_scale,
18781899
};
18791900
let w_px = [w_dst[2] * self.rw(), w_dst[3] * self.rh()];
18801901
// Rayon caméra. Le slider Roundness ne s'y applique jamais (il ne vaut que pour l'ÉCRAN).
@@ -2033,11 +2054,13 @@ impl Compositor {
20332054
// L'ombre suit la silhouette réellement affichée : le rect arrondi quand l'écran est
20342055
// droit, le quadrilatère projeté quand il est incliné. Un rect droit derrière un écran
20352056
// penché ne se lisait pas comme son ombre mais comme une seconde surface.
2036-
// Pendant un zoom, le rect contenu déborde jusqu'aux bords du cadre (issue #179) —
2037-
// pas d'ombre : dessinée autour de [0,0,1,1] avec son `spread`, elle se lirait comme
2038-
// un masque noir contre les bords, pas comme une ombre. TODO : la garder au rect
2039-
// paddé du cadre (cf. le TODO sur `s_dst` plus haut).
2040-
if cfg.shadow && p.zoom <= 1.0 {
2057+
//
2058+
// (révision : `s_dst` croît continuously avec le zoom via `zoom_grow`, donc
2059+
// l'ombre suit naturellement le zoom frame. Le `p.zoom <= 1.0` gate d'avant
2060+
// n'est plus nécessaire et on le retire — sinon on aurait une ombre
2061+
// qui disparaît d'un coup quand le zoom démarre, aussi abrupt que le
2062+
// switch binaire de `s_dst`.)
2063+
if cfg.shadow {
20412064
let spread = SCREEN_SHADOW_SPREAD_FRAC * frame_min_px;
20422065
let offset = [0.0, SCREEN_SHADOW_OFFSET_FRAC * frame_min_px];
20432066
let opacity = 0.45 * lp.shadow_scale;

0 commit comments

Comments
 (0)