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
4 changes: 2 additions & 2 deletions crates/compositor/src/compositor_windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2808,8 +2808,8 @@ impl Compositor {
if lp.has_webcam {
// L'ombre portée appartient à la bulle flottante PiP : elle se retire avec elle
// (`shape_fade`), pour qu'au plein écran plus rien n'encadre la caméra. C'est une
// ombre légère NON paramétrable — indépendante du slider Shadow, qui ne pilote plus
// que l'écran (`WEBCAM_SHADOW_OPACITY`, pas `shadow_scale`) — et propre au PiP : les
// ombre indépendante du slider Shadow de l'écran, réglable via `g.webcam_shadow`,
// et propre au PiP : les
// blocs side-by-side / top-bottom soudent la caméra à l'écran et n'en portent aucune
// (parité `preset.shadow` web, `null` hors PiP dans `compositeLayout.ts`).
// L'ombre appartient à la bulle PiP. En détourage il n'y a plus de bulle — une
Expand Down
16 changes: 15 additions & 1 deletion crates/compositor/src/frame_geometry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3677,7 +3677,8 @@ pub fn plan_frame(input: &FrameGeometryInput) -> FrameGeometry {

FrameGeometry {
webcam_shadow: sampled_layout.as_ref().map(|s| s.webcam_shadow)
.unwrap_or_else(|| if matches!(scene_preset.as_deref(), Some("dual-frame") | Some("vertical-stack") | Some("camera-only")) { 0.0 } else { 1.0 }),
.unwrap_or_else(|| if matches!(scene_preset.as_deref(), Some("dual-frame") | Some("vertical-stack") | Some("camera-only")) { 0.0 } else { 1.0 })
* input.scene.and_then(|scene| scene.effects.webcam_shadow).unwrap_or(0.6).clamp(0.0, 1.0) / 0.6,
scene_preset,
mb_taps,
mb_amount,
Expand Down Expand Up @@ -5187,6 +5188,19 @@ mod tests {
}
}

#[test]
fn webcam_shadow_preserves_old_scenes_and_scales_independently() {
let mut scene = golden_scene();
let cfg = crate::config::all().pop().expect("compositor config");
let baseline = plan_frame(&golden_input(&scene, &cfg));
for (strength, scale) in [(0.0, 0.0), (0.3, 0.5), (0.6, 1.0), (0.9, 1.5)] {
scene.effects.webcam_shadow = Some(strength);
let frame = plan_frame(&golden_input(&scene, &cfg));
assert!((frame.webcam_shadow - baseline.webcam_shadow * scale).abs() < 1e-5);
assert_eq!(frame.w_dst, baseline.w_dst);
}
}

/// Le JSON de la scène zoomée, brut : `tilted_golden_scene` n'en change QUE la
/// rotation, et le faire par substitution garantit que les deux scènes ne diffèrent
/// pas ailleurs sans qu'on s'en aperçoive.
Expand Down
2 changes: 2 additions & 0 deletions crates/compositor/src/scene.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,8 @@ pub struct SceneEffects {
pub blur: f32,
/// 0..1 force de l'ombre.
pub shadow: f32,
#[serde(default)]
pub webcam_shadow: Option<f32>,
/// Slider Roundness, en FRACTION du petit côté du cadre de sortie.
///
/// Toute longueur qui traverse ce contrat est une fraction, jamais un nombre de pixels, et
Expand Down
23 changes: 23 additions & 0 deletions src/components/ai-edition/RightPanes.layout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,29 @@ describe("LayoutPane picture-in-picture camera", () => {
expect(stored()).toMatchObject({ webcamAnchor: "top-left" });
});

it("changes the camera shadow independently of composition shadow", () => {
renderLayout(seedProject(true));
const shadow = screen.getByRole("group", { name: "Shadow" });
expect(within(shadow).getByRole("button", { name: "Medium" })).toHaveAttribute(
"aria-pressed",
"true",
);
fireEvent.click(within(shadow).getByRole("button", { name: "None" }));
expect(stored()).toMatchObject({ webcamShadowIntensity: 0 });
fireEvent.click(within(shadow).getByRole("button", { name: "Strong" }));
expect(stored()).toMatchObject({ webcamShadowIntensity: 0.9 });
});

// A cutout camera has no bubble to cast a shadow, so the setting is hidden, not disabled.
it("hides the camera shadow in cutout mode", () => {
const doc = seedProject(true);
renderLayout({
...doc,
legacyEditor: { ...doc.legacyEditor, webcamBackgroundMode: "transparent" },
});
expect(screen.queryByRole("group", { name: "Shadow" })).toBeNull();
});

// The middle of the frame is a hole in the grid, not a ninth place to land.
it("steps over the middle of the grid with the arrow keys", () => {
renderLayout(seedProject(true));
Expand Down
10 changes: 10 additions & 0 deletions src/components/ai-edition/RightPanes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2931,6 +2931,16 @@ export function LayoutPane() {
) : null}
{isPip ? (
<>
{/* A cutout camera has no bubble, so no shadow to tune. */}
{settings.webcamBackgroundMode !== "transparent"
? namedLevelRow(
ts("effects.shadow"),
SHADOW_LEVELS.map((level) => ({ value: level.value, label: ts(level.labelKey) })),
settings.webcamShadowIntensity,
layoutControlsDisabled,
(webcamShadowIntensity) => void set({ webcamShadowIntensity }),
)
: null}
<div className={styles.sectionLabel}>{ts("layout.webcamShape")}</div>
<div style={{ padding: "0 var(--sp-4) 12px" }}>
<ChoiceRow<WebcamMask>
Expand Down
23 changes: 23 additions & 0 deletions src/lib/ai-edition/store/editorSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,29 @@ const baseDoc: AxcutDocument = {
};

describe("getEditorSettings", () => {
it("preserves the historical camera shadow by default and round-trips an independent strength", () => {
expect(getEditorSettings(baseDoc).webcamShadowIntensity).toBe(0.6);
for (const strength of [0, 0.3, 0.6, 0.9]) {
const saved = patchEditorSettings(baseDoc, {
webcamShadowIntensity: strength,
shadowIntensity: 0.3,
});
const reopened = getEditorSettings(JSON.parse(JSON.stringify(saved)));
expect(reopened.webcamShadowIntensity).toBe(strength);
expect(reopened.shadowIntensity).toBe(0.3);
}
});
it.each([
[-1, 0],
[5, 1],
["invalid", 0.6],
[null, 0.6],
])("bounds camera shadow %s as %s", (value, expected) => {
expect(
getEditorSettings({ ...baseDoc, legacyEditor: { webcamShadowIntensity: value } })
.webcamShadowIntensity,
).toBe(expected);
});
it("returns the defaults when the document has no legacyEditor", () => {
const snap = getEditorSettings(baseDoc);
expect(snap.wallpaper).toBe(DEFAULT_EDITOR_SETTINGS.wallpaper);
Expand Down
8 changes: 8 additions & 0 deletions src/lib/ai-edition/store/editorSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export interface EditorSettingsSnapshot {
webcamMirrored: boolean;
webcamReactiveZoom: boolean;
webcamSizePreset: WebcamSizePreset;
webcamShadowIntensity: number;
/** Where the picture-in-picture camera sits, at a constant distance from the border. */
webcamAnchor: WebcamAnchor;
webcamCropRegion: CropRegion;
Expand Down Expand Up @@ -170,6 +171,7 @@ interface LegacyShape {
webcamMirrored?: boolean;
webcamReactiveZoom?: boolean;
webcamSizePreset?: WebcamSizePreset;
webcamShadowIntensity?: number;
webcamAnchor?: WebcamAnchor;
/** Written by builds that let the camera be dragged anywhere; read as the nearest anchor. */
webcamPosition?: WebcamPosition | null;
Expand Down Expand Up @@ -300,6 +302,11 @@ export function getEditorSettings(doc: AxcutDocument | null | undefined): Editor
webcamMaskShape: webcamMask.shape,
webcamRoundness: webcamMask.roundness,
webcamMirrored: bool(legacy?.webcamMirrored, DEFAULT_EDITOR_SETTINGS.webcamMirrored),
webcamShadowIntensity: readBounded(
legacy?.webcamShadowIntensity,
"webcamShadowIntensity",
defaults.webcamShadowIntensity,
),
webcamReactiveZoom: bool(
legacy?.webcamReactiveZoom,
DEFAULT_EDITOR_SETTINGS.webcamReactiveZoom,
Expand Down Expand Up @@ -360,6 +367,7 @@ export interface EditorSettingsPatch {
webcamMirrored?: boolean;
webcamReactiveZoom?: boolean;
webcamSizePreset?: WebcamSizePreset;
webcamShadowIntensity?: number;
webcamAnchor?: WebcamAnchor;
webcamCropRegion?: CropRegion;
webcamCropPan?: CropPan;
Expand Down
3 changes: 3 additions & 0 deletions src/lib/projectDefaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ export interface ProjectAppearanceDefaults {
webcamMirrored: boolean;
webcamReactiveZoom: boolean;
webcamSizePreset: number;
webcamShadowIntensity: number;
webcamAnchor: WebcamAnchor;
webcamBackgroundMode: "none" | "transparent" | "blur" | "custom";
webcamWallpaper: string;
Expand Down Expand Up @@ -256,6 +257,7 @@ export const DEFAULT_PROJECT_APPEARANCE: ProjectAppearanceDefaults = {
webcamMirrored: false,
webcamReactiveZoom: true,
webcamSizePreset: 40,
webcamShadowIntensity: 0.6,
webcamAnchor: "bottom-right",
webcamBackgroundMode: "none",
webcamWallpaper: "/wallpapers/wallpaper11.jpg",
Expand Down Expand Up @@ -295,6 +297,7 @@ export const DEFAULT_PROJECT_APPEARANCE: ProjectAppearanceDefaults = {
*/
export const SETTING_BOUNDS = {
shadowIntensity: [0, 1],
webcamShadowIntensity: [0, 1],
Comment thread
EtienneLescot marked this conversation as resolved.
Resolved
backgroundBlur: [0, 1],
motionBlurAmount: [0, 1],
// `ROUNDNESS_SLIDER_MAX_PX` (src/native/paramUnits.ts); a test holds the two together.
Expand Down
13 changes: 13 additions & 0 deletions src/native/sceneDescription.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,19 @@ function makeDoc(

// --- background ------------------------------------------------------------

describe("camera shadow scene contract", () => {
it("serializes independent camera shadow strength for preview and export", () => {
for (const strength of [0, 0.3, 0.6, 0.9]) {
const scene = buildSceneDescription(
makeDoc({ legacyEditor: { webcamShadowIntensity: strength, shadowIntensity: 0.3 } }),
);
expect(scene.effects.webcamShadow).toBe(strength);
expect(scene.effects.shadow).toBe(0.3);
}
expect(buildSceneDescription(makeDoc()).effects.webcamShadow).toBe(0.6);
});
});

describe("buildSceneDescription.background", () => {
it('"#123456" → color', () => {
const doc = makeDoc({ legacyEditor: { wallpaper: "#123456" } });
Expand Down
3 changes: 3 additions & 0 deletions src/native/sceneDescription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,8 @@ export interface SceneEffects {
blur: number;
/** 0..1 drop-shadow strength. */
shadow: number;
/** Camera shadow strength, 0..1; absent payloads retain the historical medium shadow. */
webcamShadow?: number;
/**
* Roundness slider, as a fraction of a fixed 1080 px reference (`ROUNDNESS_REFERENCE_PX`).
*
Expand Down Expand Up @@ -1487,6 +1489,7 @@ export function buildSceneDescription(
padding: settings.padding / 100,
blur: settings.backgroundBlur,
shadow: settings.shadowIntensity,
webcamShadow: settings.webcamShadowIntensity,
// The slider is in pixels of a 1080 reference, whatever the source resolution —
// see `roundnessFrac`. No padding and no frame: the footage meets the scene's edges,
// with no background to round against, so the corners go square and the stored value
Expand Down
Loading