- {customUrls.map((url) => (
+
+
+
+ {/* role="tab" + aria-selected are what make the tablist above mean
+ anything: without them a screen reader announces three plain
+ buttons and never says which one is current. */}
+
+
+
+
+ {tab === "image" ? (
+ <>
- >
- ) : tab === "color" ? (
- void set({ wallpaper: color })}
- />
- ) : (
- <>
-
- {GRAD_PRESETS.map((bg, i) => (
-
+
+ {customUrls.map((url) => (
+
+ >
+ ) : tab === "color" ? (
+
void set({ wallpaper: color })}
/>
- ))}
+ ) : (
+ <>
+
+ {GRAD_PRESETS.map((bg, i) => (
+ void set({ wallpaper: bg })}
+ />
+ ))}
+
+ {hasDocument ? : null}
+ >
+ )}
- {hasDocument ? : null}
- >
- )}
-
+
+
+ {/* Stays mounted OUTSIDE the popover: opening the OS file dialog takes focus,
+ which closes the popover and would unmount the input mid-pick, dropping the
+ file. It has no layout to cost us here. */}
+
+ {/* Reads in the order it acts: pick a background, then blur it. Lived under
+ "Effects" while that was a separate facet, which is how a control named
+ "Blur BG" ended up in the tab that doesn't say background. */}
+
+ {ts("effects.blurBg")}
+ {
+ void set({ showBlur: v });
+ if (isNativeCompositorActive()) {
+ setNativeParam("backgroundBlur", v);
+ }
+ }}
+ />
+
+ >
);
}
+/**
+ * The CSS `background` shorthand that paints a wallpaper value as a swatch — the same
+ * painting the grid thumbs do, hoisted out so the collapsed trigger shows exactly what the
+ * grid would show as selected. Bundled wallpapers resolve to their small pre-generated
+ * thumbnail; colours and gradients are their own literal; a custom `data:` URL passes
+ * through `resolveImageWallpaperUrl` untouched.
+ */
+function backgroundSwatchStyle(value: string): CSSProperties {
+ const classified = classifyWallpaper(value);
+ if (classified.kind !== "image") return { background: classified.value };
+ const bundled = WALLPAPER_PATHS.indexOf(classified.path);
+ try {
+ const url = resolveImageWallpaperUrl(
+ bundled >= 0 ? WALLPAPER_THUMB_PATHS[bundled] : classified.path,
+ );
+ return { background: `center/cover no-repeat url(${url})` };
+ } catch {
+ // resolveImageWallpaperUrl THROWS for an image path outside /wallpapers/ — a guard
+ // that exists to stop the app loading arbitrary files. The swatch grid only ever
+ // feeds it constants, but this call site feeds it whatever the document holds, and a
+ // throw here happens during render: one project saved by an older build with a path
+ // we no longer allow would take the whole pane down instead of drawing a dull square.
+ return { background: "var(--surface-2)" };
+ }
+}
+
// keep the user's last data: URL after they switch tabs so the Image
// tab can keep showing it without immediately pushing it back through `set`.
function useMemoCustomWallpapers(current: string): string[] {
@@ -1359,57 +1469,252 @@ function restoreCaretBeforeWord(editor: HTMLElement | null, wordId: string): voi
// pulling the schema into the helpers block.
export type { AxcutWord };
+// ─── Fit a clip ────────────────────────────────────────────────────
+
+/**
+ * The patch behind the action.
+ *
+ * There is no inverse. It was a toggle once, and the OFF branch restored the shipped defaults
+ * — which was already a guess dressed as a memory, since nothing stored what the user had
+ * before. Undo does that job properly, and the three sliders it writes sit directly below the
+ * button, so "put it back" was never missing; it was being modelled twice.
+ */
+export function fitClipPatch(nativeToken: AspectRatio): EditorSettingsPatch {
+ return { padding: 0, borderRadius: 0, shadowIntensity: 0, aspectRatio: nativeToken };
+}
+
+/**
+ * The catalog key for a count, by CLDR plural category.
+ *
+ * `translate` interpolates and nothing else, so each form is its own key. Selecting by
+ * category rather than by `count === 1` is what makes French say "0 clip" — and, more to the
+ * point, what lets a locale carry more than two forms at all: Russian needs "клипа" for 2–4
+ * and "клипов" for 5+, so mapping everything that is not `one` onto a single plural produced
+ * "2 клипов", which is simply wrong rather than merely coarse.
+ *
+ * Falls back to `fitClipMany` for any category a locale has not authored, so adding a form is
+ * a catalog change and never a code change. Arabic still needs its `two`, `few` and `many`
+ * forms — it has six categories and I could not verify the grammar, so it is deliberately
+ * left on the fallback rather than filled in with a guess.
+ */
+function pluralKey(locale: string, count: number): string {
+ const category = new Intl.PluralRules(locale).select(count);
+ return category === "one"
+ ? "effects.fitClipOne"
+ : `effects.fitClip${category === "few" ? "Few" : "Many"}`;
+}
+
// ─── Video Effects ─────────────────────────────────────────────────
+/**
+ * One pane for everything that shapes the composition.
+ *
+ * Background and Effects used to be two facets, and four of Effects' five controls were
+ * background controls in disguise: the blur blurs the background, the shadow falls ON the
+ * background, and roundness and padding exist only to let it show through. So a user who
+ * wanted no background at all opened "Background", found nothing but wallpapers, and filed
+ * #84. The split had no seam to sit on — it just hid the answer in the tab that doesn't say
+ * "background".
+ *
+ * Merged, the sections read as what they are: pick a background, decide how the recording
+ * sits on it, then the one control that is about neither.
+ */
export function VideoEffectsPane() {
const ts = useScopedT("settings");
const { settings, set, setLive, commit, hasDocument } = useEditorSettings();
+ const document = useProjectStore((s) => s.document);
+
+ // Same source the ratio picker reads, so "fill frame" and the ORIGINAL section of that menu
+ // can never disagree about what shape the footage is. Already sorted by clip count then by
+ // pixel area, so [0] is "the shape most of this timeline is in" with no heuristic of ours.
+ const nativeFormats = useMemo(() => (document ? collectNativeFormats(document) : []), [document]);
+ const [fitMenuOpen, setFitMenuOpen] = useState(false);
+ const [ratioMenuOpen, setRatioMenuOpen] = useState(false);
+ const { locale } = useI18n();
+ const clipCountLabel = (count: number) => ts(pluralKey(locale, count), { count });
- // Push the current frame-styling settings into the native D3D compositor
- // view whenever it becomes active (or the settings change while it's up).
- // The onChange handlers above already push per-control diffs; this effect
- // also covers the "user tweaked a setting before the native view was
- // mounted" case so the view doesn't render with stale defaults.
// Le rayon natif = rayon de base de la fixture (~24px @1920) × cette échelle. Diviser la
// valeur px de l'UI par ce même rayon de base fait que le coin natif ≈ les px affichés
// (au lieu de plafonner à ~24px comme avec /64).
const NATIVE_SCREEN_BASE_RADIUS_PX = 24;
- // La synchro initiale de ces params vit desormais dans NativeCompositorOverlay
+ // La synchro initiale de ces params vit dans NativeCompositorOverlay
// (`pushAllNativeParams`) : l'inspecteur n'affiche qu'un panneau a la fois, donc
// un effet de montage ici ne poussait rien tant que ce panneau precis n'avait pas
// ete ouvert. Les handlers par controle ci-dessous poussent toujours leurs diffs.
+ const applyFitClip = (token: AspectRatio) => {
+ const patch = fitClipPatch(token);
+ void set(patch);
+ if (isNativeCompositorActive()) {
+ setNativeParam("padding", 0);
+ setNativeParam("roundness", 0);
+ setNativeParam("shadow", 0);
+ }
+ };
+
return (
-
} helpText={ts("effects.help")}>
+
}
+ // Two complete sentences, one per merged half, rather than a third string to
+ // translate 13 times — both already exist in every locale and neither is a
+ // fragment of the other, so joining them survives translation and RTL alike.
+ helpText={`${ts("background.help")} ${ts("effects.help")}`}
+ >
+
+
+
{ts("effects.frame")}
+ {/* #84: "how do I turn the background off". The honest answer was four settings
+ in three places, so nobody found it. This is that answer as one control.
+
+ An ACTION, not a state, and not one setting among the four below either — it
+ overwrites all of them at once, which is why it rides the section header
+ instead of joining the list. The nearest thing it has to a peer is a reset
+ button, except it resets to a TARGET state rather than to the initial one.
+
+ It was a switch first, and a switch has room for one outcome while a timeline
+ with several shapes has one per shape — so it took the majority silently.
+ Making the choice explicit as a row of chips then failed on its own terms:
+ the chips read `683:384` and `64:27`, and ten of them do not fit. So: a
+ button that does the thing, and a list to pick from when there is more than
+ one thing it could do. Rows lead with the RESOLUTION, which is what a user
+ recognises about their own footage. */}
+
+
+ {
+ // One shape means no decision to delegate: act, do not ask.
+ if (nativeFormats.length <= 1) {
+ e.preventDefault();
+ applyFitClip(nativeFormats[0].token);
+ }
+ }}
+ >
+ {ts("effects.fitClip")}
+
+
+
+
+ {nativeFormats.map((format) => (
+ {
+ setFitMenuOpen(false);
+ applyFitClip(format.token);
+ }}
+ >
+
+ {format.width} × {format.height}
+
+ {format.token}
+ {clipCountLabel(format.clipCount)}
+
+ ))}
+
+
+
+
+ {/* The output shape moved here from the timeline toolbar. It is the one setting the
+ other three depend on — padding, roundness and shadow only mean anything against
+ a known frame — and among Trim / Speed / Zoom / transport it read as a playback
+ control rather than as the shape of what gets exported. Its old placement was
+ incidental: it arrived inside 1f25410b, a commit about per-clip crop export and
+ a HUD redesign, and no decision record ever argued for it. */}
-
{ts("effects.blurBg")}
-
{
- void set({ showBlur: v });
- if (isNativeCompositorActive()) {
- setNativeParam("backgroundBlur", v);
- }
- }}
- />
+ {ts("effects.format")}
+
+
+
+ {/* `getAspectRatioLabel` hardcodes English "Original" for the legacy
+ `"native"` value, which is still reachable: the v5→v6 migration only
+ bakes it into a concrete token once clip dimensions are known, and
+ leaves it alone until then. The group header below is localized, so
+ without this the two would disagree in twelve locales. */}
+ {settings.aspectRatio === "native"
+ ? ts("effects.formatOriginal")
+ : getAspectRatioLabel(settings.aspectRatio)}
+
+
+
+
+
+ {ASPECT_RATIO_PRESETS.map((ratio) => (
+
{
+ setRatioMenuOpen(false);
+ void set({ aspectRatio: ratio });
+ }}
+ >
+ {ratio}
+
+ ))}
+ {/* The timeline's own shapes stay listed here, and NOT only behind "fit":
+ that action also zeroes the frame styling, so without these rows there
+ would be no way to export at the footage's native shape while keeping a
+ padded, rounded look. */}
+ {nativeFormats.length > 0 ? (
+ <>
+
{ts("effects.formatOriginal")}
+ {nativeFormats.map((format) => (
+
{
+ setRatioMenuOpen(false);
+ void set({ aspectRatio: format.token });
+ }}
+ >
+ {/* Token leads and the pixel size rides on the right, exactly as this
+ menu read in the timeline toolbar — here the row names an output
+ FORMAT, so the ratio is the identity. (The "fit" menu leads with
+ the resolution instead, because there a row names a clip.) */}
+ {format.token}
+
+ {`${format.width}×${format.height}`}
+ {nativeFormats.length > 1 ? ` · ${format.clipCount}` : ""}
+
+
+ ))}
+ >
+ ) : null}
+
+
+
- {
- setLive({ motionBlurAmount: v / 100 });
- if (isNativeCompositorActive()) {
- setNativeParam("motionBlur", v / 100);
- }
- }}
- onCommit={() => void commit()}
- />
void commit()}
/>
+ {/* Alone in its section, and correctly so: this blurs the RECORDING as it moves
+ (zooms, layout changes) — see `effects.motion_blur` driving the tap count in
+ frame_geometry.rs. It is the one control here that never touches the
+ background, so it does not belong under "Frame" either. */}
+
{ts("effects.motion")}
+
+ {
+ setLive({ motionBlurAmount: v / 100 });
+ if (isNativeCompositorActive()) {
+ setNativeParam("motionBlur", v / 100);
+ }
+ }}
+ onCommit={() => void commit()}
+ />
+
);
}
@@ -1571,7 +1898,7 @@ export function LayoutPane() {
} helpText={helpText}>
{ts("layout.preset")}
-
+
) : (
// Media is an ARRANGING surface: add, remove, reorder. Nothing here
diff --git a/src/i18n/locales/ar/settings.json b/src/i18n/locales/ar/settings.json
index 664131e1..b0beb268 100644
--- a/src/i18n/locales/ar/settings.json
+++ b/src/i18n/locales/ar/settings.json
@@ -40,7 +40,7 @@
"deleteRegion": "حذف منطقة القص"
},
"layout": {
- "title": "التخطيط",
+ "title": "تخطيط الكاميرا",
"preset": "الإعداد المسبق",
"selectPreset": "حدد إعدادًا مسبقًا",
"pictureInPicture": "صورة داخل صورة",
@@ -66,7 +66,7 @@
}
},
"effects": {
- "title": "تأثيرات الفيديو",
+ "title": "التركيب",
"blurBg": "تمويه الخلفية",
"motionBlur": "ضبابية الحركة",
"off": "إيقاف",
@@ -74,6 +74,14 @@
"shadow": "ظل",
"roundness": "الاستدارة",
"padding": "المسافة البادئة",
+ "frame": "الإطار",
+ "format": "التنسيق",
+ "formatOriginal": "الأصلي",
+ "fitClip": "ملاءمة",
+ "fitClipOne": "مقطع واحد",
+ "fitClipFew": "{{count}} مقاطع",
+ "fitClipMany": "{{count}} مقاطع",
+ "motion": "الحركة",
"help": "تنسيق إطار التسجيل: ضبابية الخلفية، والظل، وضبابية الحركة، واستدارة الزوايا، والحشو حول الفيديو."
},
"background": {
diff --git a/src/i18n/locales/ar/timeline.json b/src/i18n/locales/ar/timeline.json
index 011483f8..4412a0a0 100644
--- a/src/i18n/locales/ar/timeline.json
+++ b/src/i18n/locales/ar/timeline.json
@@ -66,8 +66,6 @@
"smartZoomsAndCuts": "قصات ذكية",
"smartZoomsAndCutsHint": "باستخدام الذكاء الاصطناعي",
"comment": "تعليق",
- "aspectRatio": "نسبة العرض إلى الارتفاع",
- "original": "الأصلي",
"timelineTools": "أدوات المخطط الزمني",
"arrangeClips": "ترتيب المقاطع",
"arrangeClipsHint": "اسحب المقاطع أدناه لإعادة ترتيبها أو إسقاط مقاطع جديدة",
diff --git a/src/i18n/locales/en/settings.json b/src/i18n/locales/en/settings.json
index 4569adbb..071fd485 100644
--- a/src/i18n/locales/en/settings.json
+++ b/src/i18n/locales/en/settings.json
@@ -40,7 +40,7 @@
"deleteRegion": "Delete Trim Region"
},
"layout": {
- "title": "Layout",
+ "title": "Camera layout",
"preset": "Preset",
"selectPreset": "Select preset",
"pictureInPicture": "Picture in Picture",
@@ -72,7 +72,7 @@
"reset": "Reset audio"
},
"effects": {
- "title": "Video Effects",
+ "title": "Composition",
"blurBg": "Blur BG",
"motionBlur": "Motion Blur",
"off": "off",
@@ -80,6 +80,14 @@
"shadow": "Shadow",
"roundness": "Roundness",
"padding": "Padding",
+ "frame": "Frame",
+ "format": "Format",
+ "formatOriginal": "Original",
+ "fitClip": "Fit",
+ "fitClipOne": "{{count}} clip",
+ "fitClipFew": "{{count}} clips",
+ "fitClipMany": "{{count}} clips",
+ "motion": "Motion",
"help": "Frame styling for the recording: background blur, drop shadow, motion blur, corner radius, and padding around the video."
},
"background": {
diff --git a/src/i18n/locales/en/timeline.json b/src/i18n/locales/en/timeline.json
index 517024fc..c4196611 100644
--- a/src/i18n/locales/en/timeline.json
+++ b/src/i18n/locales/en/timeline.json
@@ -66,8 +66,6 @@
"smartZoomsAndCuts": "Smart cuts",
"smartZoomsAndCutsHint": "With AI",
"comment": "Comment",
- "aspectRatio": "Aspect ratio",
- "original": "Original",
"timelineTools": "Timeline tools",
"arrangeClips": "Arrange clips",
"arrangeClipsHint": "Drag clips below to reorder or drop new ones in",
diff --git a/src/i18n/locales/es/settings.json b/src/i18n/locales/es/settings.json
index 1386e1e1..099928b5 100644
--- a/src/i18n/locales/es/settings.json
+++ b/src/i18n/locales/es/settings.json
@@ -40,7 +40,7 @@
"deleteRegion": "Eliminar región de recorte"
},
"layout": {
- "title": "Diseño",
+ "title": "Disposición de cámara",
"preset": "Predefinido",
"selectPreset": "Seleccionar predefinido",
"pictureInPicture": "Imagen en imagen",
@@ -66,13 +66,21 @@
}
},
"effects": {
- "title": "Efectos de video",
+ "title": "Composición",
"blurBg": "Desenfocar fondo",
"motionBlur": "Desenfoque de movimiento",
"off": "desactivado",
"shadow": "Sombra",
"roundness": "Redondez",
"padding": "Relleno",
+ "frame": "Marco",
+ "format": "Formato",
+ "formatOriginal": "Original",
+ "fitClip": "Ajustar",
+ "fitClipOne": "{{count}} clip",
+ "fitClipFew": "{{count}} clips",
+ "fitClipMany": "{{count}} clips",
+ "motion": "Movimiento",
"on": "activado",
"help": "Estilo del marco de la grabación: desenfoque de fondo, sombra, desenfoque de movimiento, radio de esquinas y margen alrededor del vídeo."
},
diff --git a/src/i18n/locales/es/timeline.json b/src/i18n/locales/es/timeline.json
index a4b4124b..989289e0 100644
--- a/src/i18n/locales/es/timeline.json
+++ b/src/i18n/locales/es/timeline.json
@@ -66,8 +66,6 @@
"smartZoomsAndCuts": "Cortes inteligentes",
"smartZoomsAndCutsHint": "Con IA",
"comment": "Comentario",
- "aspectRatio": "Relación de aspecto",
- "original": "Original",
"timelineTools": "Herramientas de la línea de tiempo",
"arrangeClips": "Organizar clips",
"arrangeClipsHint": "Arrastra los clips de abajo para reordenarlos o suelta otros nuevos",
diff --git a/src/i18n/locales/fr/settings.json b/src/i18n/locales/fr/settings.json
index ad3dc0df..1b7f5fa2 100644
--- a/src/i18n/locales/fr/settings.json
+++ b/src/i18n/locales/fr/settings.json
@@ -40,7 +40,7 @@
"deleteRegion": "Supprimer la région de coupe"
},
"layout": {
- "title": "Mise en page",
+ "title": "Disposition caméra",
"preset": "Préréglage",
"selectPreset": "Choisir un préréglage",
"pictureInPicture": "Incrustation d'image",
@@ -66,13 +66,21 @@
}
},
"effects": {
- "title": "Effets vidéo",
+ "title": "Composition",
"blurBg": "Flou arrière-plan",
"motionBlur": "Flou de mouvement",
"off": "désactivé",
"shadow": "Ombre",
"roundness": "Arrondi",
"padding": "Marge",
+ "frame": "Cadre",
+ "format": "Format",
+ "formatOriginal": "Original",
+ "fitClip": "Ajuster",
+ "fitClipOne": "{{count}} clip",
+ "fitClipFew": "{{count}} clips",
+ "fitClipMany": "{{count}} clips",
+ "motion": "Mouvement",
"on": "activé",
"help": "Mise en forme du cadre de l'enregistrement : flou d'arrière-plan, ombre portée, flou de mouvement, arrondi des coins et marge autour de la vidéo."
},
diff --git a/src/i18n/locales/fr/timeline.json b/src/i18n/locales/fr/timeline.json
index 8e871599..a35b8858 100644
--- a/src/i18n/locales/fr/timeline.json
+++ b/src/i18n/locales/fr/timeline.json
@@ -66,8 +66,6 @@
"smartZoomsAndCuts": "Coupes intelligentes",
"smartZoomsAndCutsHint": "Avec l'IA",
"comment": "Commentaire",
- "aspectRatio": "Format",
- "original": "Original",
"timelineTools": "Outils de la timeline",
"arrangeClips": "Organiser les clips",
"arrangeClipsHint": "Glissez les clips ci-dessous pour les réorganiser ou en déposer de nouveaux",
diff --git a/src/i18n/locales/it/settings.json b/src/i18n/locales/it/settings.json
index 206d40bb..587aae96 100644
--- a/src/i18n/locales/it/settings.json
+++ b/src/i18n/locales/it/settings.json
@@ -40,7 +40,7 @@
"deleteRegion": "Elimina regione taglio"
},
"layout": {
- "title": "Layout",
+ "title": "Disposizione camera",
"preset": "Predefinito",
"selectPreset": "Seleziona predefinito",
"pictureInPicture": "Immagine nell'immagine",
@@ -66,7 +66,7 @@
}
},
"effects": {
- "title": "Effetti video",
+ "title": "Composizione",
"blurBg": "Sfuma sfondo",
"motionBlur": "Sfocatura movimento",
"off": "spento",
@@ -74,6 +74,14 @@
"shadow": "Ombra",
"roundness": "Arrotondamento",
"padding": "Spaziatura",
+ "frame": "Cornice",
+ "format": "Formato",
+ "formatOriginal": "Originale",
+ "fitClip": "Adatta",
+ "fitClipOne": "{{count}} clip",
+ "fitClipFew": "{{count}} clip",
+ "fitClipMany": "{{count}} clip",
+ "motion": "Movimento",
"help": "Stile della cornice della registrazione: sfocatura dello sfondo, ombra, sfocatura di movimento, raggio degli angoli e margine attorno al video."
},
"background": {
diff --git a/src/i18n/locales/it/timeline.json b/src/i18n/locales/it/timeline.json
index 8390254c..09bb116e 100644
--- a/src/i18n/locales/it/timeline.json
+++ b/src/i18n/locales/it/timeline.json
@@ -66,8 +66,6 @@
"smartZoomsAndCuts": "Tagli intelligenti",
"smartZoomsAndCutsHint": "Con l'IA",
"comment": "Commento",
- "aspectRatio": "Proporzioni",
- "original": "Originale",
"timelineTools": "Strumenti della timeline",
"arrangeClips": "Organizza clip",
"arrangeClipsHint": "Trascina le clip qui sotto per riordinarle o rilasciane di nuove",
diff --git a/src/i18n/locales/ja-JP/settings.json b/src/i18n/locales/ja-JP/settings.json
index 8d999dc7..f856cde4 100644
--- a/src/i18n/locales/ja-JP/settings.json
+++ b/src/i18n/locales/ja-JP/settings.json
@@ -40,7 +40,7 @@
"deleteRegion": "トリム範囲を削除"
},
"layout": {
- "title": "レイアウト",
+ "title": "カメラレイアウト",
"preset": "プリセット",
"selectPreset": "プリセットを選択",
"pictureInPicture": "ピクチャーインピクチャ",
@@ -66,13 +66,21 @@
}
},
"effects": {
- "title": "動画効果",
+ "title": "コンポジション",
"blurBg": "背景をぼかす",
"motionBlur": "モーションブラー",
"off": "オフ",
"shadow": "影",
"roundness": "丸み",
"padding": "余白",
+ "frame": "フレーム",
+ "format": "フォーマット",
+ "formatOriginal": "元のサイズ",
+ "fitClip": "合わせる",
+ "fitClipOne": "{{count}} クリップ",
+ "fitClipFew": "{{count}} クリップ",
+ "fitClipMany": "{{count}} クリップ",
+ "motion": "モーション",
"on": "オン",
"help": "録画フレームのスタイル設定: 背景ぼかし、ドロップシャドウ、モーションブラー、角丸、動画まわりの余白。"
},
diff --git a/src/i18n/locales/ja-JP/timeline.json b/src/i18n/locales/ja-JP/timeline.json
index a2c9a88e..68911ba9 100644
--- a/src/i18n/locales/ja-JP/timeline.json
+++ b/src/i18n/locales/ja-JP/timeline.json
@@ -66,8 +66,6 @@
"smartZoomsAndCuts": "スマートカット",
"smartZoomsAndCutsHint": "AIを使用",
"comment": "コメント",
- "aspectRatio": "アスペクト比",
- "original": "オリジナル",
"timelineTools": "タイムラインツール",
"arrangeClips": "クリップを配置",
"arrangeClipsHint": "下のクリップをドラッグして並べ替えるか、新しいクリップをドロップします",
diff --git a/src/i18n/locales/ko-KR/settings.json b/src/i18n/locales/ko-KR/settings.json
index cbbc77ad..ee083c9b 100644
--- a/src/i18n/locales/ko-KR/settings.json
+++ b/src/i18n/locales/ko-KR/settings.json
@@ -40,7 +40,7 @@
"deleteRegion": "트림 구간 삭제"
},
"layout": {
- "title": "레이아웃",
+ "title": "카메라 레이아웃",
"preset": "프리셋",
"selectPreset": "프리셋 선택",
"pictureInPicture": "화면 속 화면",
@@ -66,7 +66,7 @@
}
},
"effects": {
- "title": "비디오 효과",
+ "title": "컴포지션",
"blurBg": "배경 흐림",
"motionBlur": "모션 블러",
"off": "끄기",
@@ -74,6 +74,14 @@
"shadow": "그림자",
"roundness": "모서리 둥글기",
"padding": "여백",
+ "frame": "프레임",
+ "format": "형식",
+ "formatOriginal": "원본",
+ "fitClip": "맞추기",
+ "fitClipOne": "{{count}}개 클립",
+ "fitClipFew": "{{count}}개 클립",
+ "fitClipMany": "{{count}}개 클립",
+ "motion": "모션",
"help": "녹화 프레임 스타일: 배경 흐림, 그림자, 모션 블러, 모서리 둥글기, 영상 주변 여백."
},
"background": {
diff --git a/src/i18n/locales/ko-KR/timeline.json b/src/i18n/locales/ko-KR/timeline.json
index 75417443..8a100ee6 100644
--- a/src/i18n/locales/ko-KR/timeline.json
+++ b/src/i18n/locales/ko-KR/timeline.json
@@ -66,8 +66,6 @@
"smartZoomsAndCuts": "스마트 컷",
"smartZoomsAndCutsHint": "AI 사용",
"comment": "코멘트",
- "aspectRatio": "화면 비율",
- "original": "원본",
"timelineTools": "타임라인 도구",
"arrangeClips": "클립 정리",
"arrangeClipsHint": "아래 클립을 드래그하여 순서를 바꾸거나 새 클립을 놓으세요",
diff --git a/src/i18n/locales/pt-BR/settings.json b/src/i18n/locales/pt-BR/settings.json
index 68a4112c..6512686b 100644
--- a/src/i18n/locales/pt-BR/settings.json
+++ b/src/i18n/locales/pt-BR/settings.json
@@ -40,7 +40,7 @@
"deleteRegion": "Excluir Região de Recorte"
},
"layout": {
- "title": "Layout",
+ "title": "Layout da câmera",
"preset": "Predefinição",
"selectPreset": "Selecionar predefinição",
"pictureInPicture": "Picture in Picture",
@@ -66,7 +66,7 @@
}
},
"effects": {
- "title": "Efeitos de Vídeo",
+ "title": "Composição",
"blurBg": "Desfocar Fundo",
"motionBlur": "Desfoque de Movimento",
"off": "desativado",
@@ -74,6 +74,14 @@
"shadow": "Sombra",
"roundness": "Arredondamento",
"padding": "Espaçamento",
+ "frame": "Moldura",
+ "format": "Formato",
+ "formatOriginal": "Original",
+ "fitClip": "Ajustar",
+ "fitClipOne": "{{count}} clipe",
+ "fitClipFew": "{{count}} clipes",
+ "fitClipMany": "{{count}} clipes",
+ "motion": "Movimento",
"help": "Estilo do quadro da gravação: desfoque de fundo, sombra, desfoque de movimento, raio dos cantos e margem em volta do vídeo."
},
"background": {
diff --git a/src/i18n/locales/pt-BR/timeline.json b/src/i18n/locales/pt-BR/timeline.json
index 4cb43bc0..5359feba 100644
--- a/src/i18n/locales/pt-BR/timeline.json
+++ b/src/i18n/locales/pt-BR/timeline.json
@@ -66,8 +66,6 @@
"smartZoomsAndCuts": "Cortes inteligentes",
"smartZoomsAndCutsHint": "Com IA",
"comment": "Comentário",
- "aspectRatio": "Proporção",
- "original": "Original",
"timelineTools": "Ferramentas da linha do tempo",
"arrangeClips": "Organizar clipes",
"arrangeClipsHint": "Arraste os clipes abaixo para reordená-los ou solte novos",
diff --git a/src/i18n/locales/ru/settings.json b/src/i18n/locales/ru/settings.json
index a6798b66..dcd82de3 100644
--- a/src/i18n/locales/ru/settings.json
+++ b/src/i18n/locales/ru/settings.json
@@ -40,7 +40,7 @@
"deleteRegion": "Удалить область обрезки"
},
"layout": {
- "title": "Макет",
+ "title": "Расположение камеры",
"preset": "Пресет",
"selectPreset": "Выбрать пресет",
"pictureInPicture": "Картинка в картинке",
@@ -66,7 +66,7 @@
}
},
"effects": {
- "title": "Видеоэффекты",
+ "title": "Композиция",
"blurBg": "Размытие фона",
"motionBlur": "Размытие движения",
"off": "выкл",
@@ -74,6 +74,14 @@
"shadow": "Тень",
"roundness": "Скругление",
"padding": "Отступ",
+ "frame": "Рамка",
+ "format": "Формат",
+ "formatOriginal": "Исходный",
+ "fitClip": "Подогнать",
+ "fitClipOne": "{{count}} клип",
+ "fitClipFew": "{{count}} клипа",
+ "fitClipMany": "{{count}} клипов",
+ "motion": "Движение",
"help": "Оформление кадра записи: размытие фона, тень, размытие движения, скругление углов и отступ вокруг видео."
},
"background": {
diff --git a/src/i18n/locales/ru/timeline.json b/src/i18n/locales/ru/timeline.json
index eb679791..38708695 100644
--- a/src/i18n/locales/ru/timeline.json
+++ b/src/i18n/locales/ru/timeline.json
@@ -66,8 +66,6 @@
"smartZoomsAndCuts": "Умные вырезки",
"smartZoomsAndCutsHint": "С помощью ИИ",
"comment": "Комментарий",
- "aspectRatio": "Соотношение сторон",
- "original": "Исходный",
"timelineTools": "Инструменты таймлайна",
"arrangeClips": "Упорядочить клипы",
"arrangeClipsHint": "Перетащите клипы ниже, чтобы изменить порядок, или добавьте новые",
diff --git a/src/i18n/locales/tr/settings.json b/src/i18n/locales/tr/settings.json
index c2ab5477..b3ccf10d 100644
--- a/src/i18n/locales/tr/settings.json
+++ b/src/i18n/locales/tr/settings.json
@@ -40,7 +40,7 @@
"deleteRegion": "Kırpma Bölgesini Sil"
},
"layout": {
- "title": "Düzen",
+ "title": "Kamera düzeni",
"preset": "Ön Ayar",
"selectPreset": "Ön ayar seçin",
"pictureInPicture": "Resim İçinde Resim",
@@ -66,13 +66,21 @@
}
},
"effects": {
- "title": "Video Efektleri",
+ "title": "Kompozisyon",
"blurBg": "Arka Planı Bulanıklaştır",
"motionBlur": "Hareket Bulanıklığı",
"off": "kapalı",
"shadow": "Gölge",
"roundness": "Yuvarlaklık",
"padding": "Dolgu",
+ "frame": "Çerçeve",
+ "format": "Biçim",
+ "formatOriginal": "Orijinal",
+ "fitClip": "Sığdır",
+ "fitClipOne": "{{count}} klip",
+ "fitClipFew": "{{count}} klip",
+ "fitClipMany": "{{count}} klip",
+ "motion": "Hareket",
"on": "açık",
"help": "Kayıt çerçevesinin biçimi: arka plan bulanıklığı, gölge, hareket bulanıklığı, köşe yuvarlaklığı ve videonun çevresindeki boşluk."
},
diff --git a/src/i18n/locales/tr/timeline.json b/src/i18n/locales/tr/timeline.json
index 1b36c8e4..d5a531f3 100644
--- a/src/i18n/locales/tr/timeline.json
+++ b/src/i18n/locales/tr/timeline.json
@@ -66,8 +66,6 @@
"smartZoomsAndCuts": "Akıllı kırpma",
"smartZoomsAndCutsHint": "Yapay zeka ile",
"comment": "Yorum",
- "aspectRatio": "En-boy oranı",
- "original": "Orijinal",
"timelineTools": "Zaman çizelgesi araçları",
"arrangeClips": "Klipleri düzenle",
"arrangeClipsHint": "Yeniden sıralamak için aşağıdaki klipleri sürükleyin veya yenilerini bırakın",
diff --git a/src/i18n/locales/vi/settings.json b/src/i18n/locales/vi/settings.json
index d65c801f..2274559b 100644
--- a/src/i18n/locales/vi/settings.json
+++ b/src/i18n/locales/vi/settings.json
@@ -40,7 +40,7 @@
"deleteRegion": "Xóa vùng cắt"
},
"layout": {
- "title": "Bố cục",
+ "title": "Bố cục camera",
"preset": "Cài đặt sẵn",
"selectPreset": "Chọn cài đặt sẵn",
"pictureInPicture": "Hình trong hình",
@@ -66,13 +66,21 @@
}
},
"effects": {
- "title": "Hiệu ứng video",
+ "title": "Bố cục hình ảnh",
"blurBg": "Làm mờ nền",
"motionBlur": "Làm mờ chuyển động",
"off": "tắt",
"shadow": "Bóng đổ",
"roundness": "Độ bo tròn",
"padding": "Phần đệm",
+ "frame": "Khung",
+ "format": "Định dạng",
+ "formatOriginal": "Gốc",
+ "fitClip": "Vừa khít",
+ "fitClipOne": "{{count}} clip",
+ "fitClipFew": "{{count}} clip",
+ "fitClipMany": "{{count}} clip",
+ "motion": "Chuyển động",
"on": "bật",
"help": "Kiểu khung của bản ghi: làm mờ nền, đổ bóng, mờ chuyển động, bo góc và khoảng đệm quanh video."
},
diff --git a/src/i18n/locales/vi/timeline.json b/src/i18n/locales/vi/timeline.json
index 9162539f..1d963e58 100644
--- a/src/i18n/locales/vi/timeline.json
+++ b/src/i18n/locales/vi/timeline.json
@@ -66,8 +66,6 @@
"smartZoomsAndCuts": "Cắt thông minh",
"smartZoomsAndCutsHint": "Với AI",
"comment": "Bình luận",
- "aspectRatio": "Tỷ lệ khung hình",
- "original": "Gốc",
"timelineTools": "Công cụ dòng thời gian",
"arrangeClips": "Sắp xếp clip",
"arrangeClipsHint": "Kéo các clip bên dưới để sắp xếp lại hoặc thả clip mới vào",
diff --git a/src/i18n/locales/zh-CN/settings.json b/src/i18n/locales/zh-CN/settings.json
index 3fd503d2..cd2856f7 100644
--- a/src/i18n/locales/zh-CN/settings.json
+++ b/src/i18n/locales/zh-CN/settings.json
@@ -40,7 +40,7 @@
"deleteRegion": "删除剪辑区域"
},
"layout": {
- "title": "布局",
+ "title": "摄像头布局",
"preset": "预设",
"selectPreset": "选择预设",
"pictureInPicture": "画中画",
@@ -66,13 +66,21 @@
}
},
"effects": {
- "title": "视频效果",
+ "title": "画面合成",
"blurBg": "模糊背景",
"motionBlur": "运动模糊",
"off": "关",
"shadow": "阴影",
"roundness": "圆角",
"padding": "内边距",
+ "frame": "画框",
+ "format": "格式",
+ "formatOriginal": "原始",
+ "fitClip": "适配",
+ "fitClipOne": "{{count}} 个片段",
+ "fitClipFew": "{{count}} 个片段",
+ "fitClipMany": "{{count}} 个片段",
+ "motion": "运动",
"on": "开",
"help": "录制画面的边框样式:背景模糊、投影、运动模糊、圆角半径以及视频四周的内边距。"
},
diff --git a/src/i18n/locales/zh-CN/timeline.json b/src/i18n/locales/zh-CN/timeline.json
index 9a278d0f..1451e6d3 100644
--- a/src/i18n/locales/zh-CN/timeline.json
+++ b/src/i18n/locales/zh-CN/timeline.json
@@ -66,8 +66,6 @@
"smartZoomsAndCuts": "智能剪切",
"smartZoomsAndCutsHint": "使用 AI",
"comment": "评论",
- "aspectRatio": "宽高比",
- "original": "原始",
"timelineTools": "时间轴工具",
"arrangeClips": "排列片段",
"arrangeClipsHint": "拖动下方片段以重新排序,或拖入新片段",
diff --git a/src/i18n/locales/zh-TW/settings.json b/src/i18n/locales/zh-TW/settings.json
index 4a124525..b8606a2e 100644
--- a/src/i18n/locales/zh-TW/settings.json
+++ b/src/i18n/locales/zh-TW/settings.json
@@ -41,7 +41,7 @@
"deleteRegion": "刪除剪輯區域"
},
"layout": {
- "title": "版面配置",
+ "title": "攝影機版面",
"preset": "預設",
"selectPreset": "選擇預設",
"pictureInPicture": "子母畫面",
@@ -67,13 +67,21 @@
}
},
"effects": {
- "title": "影片效果",
+ "title": "畫面合成",
"blurBg": "模糊背景",
"motionBlur": "動態模糊",
"off": "關",
"shadow": "陰影",
"roundness": "圓角",
"padding": "內邊距",
+ "frame": "外框",
+ "format": "格式",
+ "formatOriginal": "原始",
+ "fitClip": "符合",
+ "fitClipOne": "{{count}} 個片段",
+ "fitClipFew": "{{count}} 個片段",
+ "fitClipMany": "{{count}} 個片段",
+ "motion": "動態",
"on": "開",
"help": "錄製畫面的外框樣式:背景模糊、陰影、動態模糊、圓角半徑,以及影片四周的內距。"
},
diff --git a/src/i18n/locales/zh-TW/timeline.json b/src/i18n/locales/zh-TW/timeline.json
index 036e4658..7f4ba987 100644
--- a/src/i18n/locales/zh-TW/timeline.json
+++ b/src/i18n/locales/zh-TW/timeline.json
@@ -66,8 +66,6 @@
"smartZoomsAndCuts": "智慧剪輯",
"smartZoomsAndCutsHint": "使用 AI",
"comment": "留言",
- "aspectRatio": "長寬比",
- "original": "原始",
"timelineTools": "時間軸工具",
"arrangeClips": "排列片段",
"arrangeClipsHint": "拖曳下方片段以重新排序,或拖曳新片段至此",
diff --git a/technical-documentation/architecture/decisions.md b/technical-documentation/architecture/decisions.md
index e6cd0461..96d97bff 100644
--- a/technical-documentation/architecture/decisions.md
+++ b/technical-documentation/architecture/decisions.md
@@ -54,5 +54,6 @@ behaviour lives now.
| `Titlebar.tsx`, `Bottombar.tsx` | `src/components/ai-edition/v4/EditorTopBar.tsx` |
| `RightPanelStack.tsx` | `src/components/ai-edition/v4/FloatingInspector.tsx` |
| `TranscriptEditor.tsx` | `src/components/ai-edition/CaptionsPane.tsx` + `src/lib/ai-edition/captions/` |
+| `BackgroundPane`, and the `"background"` inspector facet | the **Background** section of `VideoEffectsPane` (`src/components/ai-edition/RightPanes.tsx`). Four of the five "effects" controls were background controls — the blur blurs it, the shadow falls on it, roundness and padding exist to let it show through — so the split put the answer to "how do I remove the background" in the tab that doesn't say background ([#84](https://github.com/getopenscreen/openscreen/issues/84)). |
| The browser-based exporter | the native compositor export path — see [export-pipeline.md](export-pipeline.md) |
| CTranslate2 speech-to-text | whisper.cpp — see [transcription-and-captions.md](transcription-and-captions.md) |
diff --git a/technical-documentation/architecture/editor-shell.md b/technical-documentation/architecture/editor-shell.md
index eed79f8f..c2efb429 100644
--- a/technical-documentation/architecture/editor-shell.md
+++ b/technical-documentation/architecture/editor-shell.md
@@ -38,7 +38,7 @@ flowchart TD
Stage -- "mode === 'media'" --> Media
Stage -- "mode === 'rec'" --> Rec
- Inspector -- "FacetBody" --> RightPanes["BackgroundPane / VideoEffectsPane /
LayoutPane / CursorPane /
TranscriptPane / CaptionsPane
(RightPanes.tsx · CaptionsPane.tsx)"]
+ Inspector -- "FacetBody" --> RightPanes["VideoEffectsPane / LayoutPane /
AudioPane / CursorPane /
TranscriptPane / CaptionsPane
(RightPanes.tsx · CaptionsPane.tsx)"]
```
The shell is the single owner of mode, transport (`playing`/`currentTimeSec` come
@@ -57,16 +57,16 @@ of those are local React state, with the document itself read through
| **Stage — Media mode** | `src/components/ai-edition/v4/MediaStage.tsx` | Searches, adds, regenerates transcripts for the assets in the project. The variant that the timeline shows in this mode is "media" (timeline height, no lanes). |
| **Stage — Rec mode** | `src/components/ai-edition/v4/RecStage.tsx` | Pre-flight config for a new recording — mic / camera / system audio / cursor capture mode — then hands off to the standalone recorder HUD window when the user hits record. |
| **Bottom timeline** | `src/components/ai-edition/v4/V4Timeline.tsx` | Renders the clips, the ruler, and the five lanes (`annPills`, `speedPills`, `trimPills`, `zoomPills`, `cameraFullscreenPills`, computed at `:321-363`). Owns transport (play / prev / next / loop), zoom/pan, scrub, drag-and-drop of asset cards, the "smart zooms + cuts" AI prompt, and resize/move/delete of every pill. Pills render through `coalesceRegionsForRuler` and `coalescedTrimGroups` so what the user sees is exactly what the rules in [timeline-model.md](timeline-model.md) describe. |
-| **Floating inspector** | `src/components/ai-edition/v4/FloatingInspector.tsx` | Floating facet rail over the stage; the open panel either shows the `FacetBody` for the current facet (`background` / `effects` / `layout` / `cursor` / `captions` / `transcript`) or, when a region is selected, a `SelectionPane` (`:444`) that edits the selected pill by id. The "pencil" rail button opens `EditClipModal` for crop + trim. |
+| **Floating inspector** | `src/components/ai-edition/v4/FloatingInspector.tsx` | Floating facet rail over the stage; the open panel either shows the `FacetBody` for the current facet (`effects` / `layout` / `audio` / `cursor` / `captions` / `transcript`) or, when a region is selected, a `SelectionPane` (`:434`) that edits the selected pill by id. The "pencil" rail button opens `EditClipModal` for crop + trim. |
| **Left chat column** | `src/components/ai-edition/LeftPanel.tsx` | Only mounted when `mode === "edit"` and `chatOpen` is true (`NewEditorShell.tsx` `:1133-1151`). Sends user messages to the LLM via IPC. Resize handle is `v4.chatResizeHandle`; width persists in `localStorage` as `os-editor-chat-width`. |
| **Modals** | `src/components/ai-edition/Modals.tsx` | `OpenProjectModal`, `NewProjectModal`, `EditClipModal` (per-clip crop + in/out), `UnsavedChangesModal`. Mounted at the shell level (`:1286-1332`) so every trigger site reuses the same instance. |
| **Export dialog** | `src/components/ai-edition/ExportDialog.tsx` | Format / quality / frame-rate / codec / size; calls `exportAxcutDocument` (GIF path, WebCodecs) or `exportMultiNative` (MP4 path, native D3D compositor). The MP4 path is the one that goes through `src/lib/ai-edition/exporter/documentExporter.ts`'s `projectRegionsToSourceTime` and the multi-clip native bridge. |
-| **Captions pane** | `src/components/ai-edition/CaptionsPane.tsx` | Mounted as a facet body from `FloatingInspector.tsx` (`:1077`). Controls caption appearance + translations; the cues themselves are a derived view over `document.transcripts` (see [`src/lib/ai-edition/captions/`](../../src/lib/ai-edition/captions/)). |
+| **Captions pane** | `src/components/ai-edition/CaptionsPane.tsx` | Mounted as a facet body from `FloatingInspector.tsx` (`:1062`). Controls caption appearance + translations; the cues themselves are a derived view over `document.transcripts` (see [`src/lib/ai-edition/captions/`](../../src/lib/ai-edition/captions/)). |
## Modes and facets
-`mode` is local React state in `NewEditorShell` (`:75`); `facet` is local React
-state at `:84`. Both are exported as string unions from the components that
+`mode` is local React state in `NewEditorShell` (`:112`); `facet` is local React
+state at `:121`. Both are exported as string unions from the components that
introduce them.
### EditorMode (`v4/EditorTopBar.tsx:20`)
@@ -84,17 +84,17 @@ export type EditorMode = "media" | "edit" | "rec";
### Facet (`v4/FloatingInspector.tsx:57`)
```ts
-export type Facet = "background" | "effects" | "layout" | "cursor" | "captions" | "transcript";
+export type Facet = "effects" | "layout" | "audio" | "cursor" | "captions" | "transcript";
```
| Facet | Body component | Purpose |
|---|---|---|
-| `"background"` | `BackgroundPane` (`src/components/ai-edition/RightPanes.tsx:178`) | Wallpaper, shadow intensity, blur, motion blur, corner radius, padding — all read out of `document.legacyEditor`. |
-| `"effects"` | `VideoEffectsPane` (`RightPanes.tsx:1218`) | Per-clip / per-document video effects that aren't zoom / speed / annotation (cursor zoom, etc.). |
-| `"layout"` | `LayoutPane` (`RightPanes.tsx:1376`) | Webcam layout (PiP / side / full / off), mask shape, mirroring — all also from `legacyEditor`. |
-| `"cursor"` | `CursorPane` (`RightPanes.tsx:1544`) | Cursor smoothing, theme, click ring, halo. |
+| `"effects"` | `VideoEffectsPane` (`src/components/ai-edition/RightPanes.tsx`) | Everything that shapes the composition, in three sections: **Background** (a swatch trigger opening the wallpaper / colour / gradient picker in a popover, plus the background blur), **Frame** (shadow, roundness, padding) and **Motion** (motion blur). All read out of `document.legacyEditor`. The picker floats so the frame sliders stay above the fold — inline, its 18-swatch grid alone was two thirds of the pane's height. |
+| `"layout"` | `LayoutPane` (`RightPanes.tsx`) | Webcam layout (PiP / side / full / off), mask shape, mirroring — all also from `legacyEditor`. |
+| `"audio"` | `AudioPane` (`RightPanes.tsx`) | Output gain. |
+| `"cursor"` | `CursorPane` (`RightPanes.tsx`) | Cursor smoothing, theme, click ring, halo. |
| `"captions"` | `CaptionsPane` (`CaptionsPane.tsx`) | Caption appearance (font, size, background, animation) and translations. The pane owns the `transcribe` action — it's the only place that runs it from the shell. |
-| `"transcript"` | `TranscriptPane` (`RightPanes.tsx:475`) | Editable view of the transcript words / segments. Writes back to the document. |
+| `"transcript"` | `TranscriptPane` (`RightPanes.tsx`) | Editable view of the transcript words / segments. Writes back to the document. |
Selecting a region on the timeline supersedes the current facet body: the
inspector opens (if it was closed) and renders the `SelectionPane` for that