Files
8-cut/docs/plans/2026-04-06-portrait-crop-design.md
2026-04-06 13:44:24 +02:00

56 lines
3.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Portrait Crop Design
## Overview
Export the 8s clip as portrait video by cropping a vertical window from the landscape source. The user picks a portrait aspect ratio from a dropdown and positions the crop window by clicking on the video or a crop bar below the timeline. Settings persist between relaunches.
## UI
- **Ratio dropdown (`QComboBox`)** — options: `Off`, `9:16`, `4:5`, `1:1`. Added to the export row between the Short side field and the next-file label. Persisted via `QSettings` key `portrait_ratio`.
- **`CropBarWidget`** — a thin (~16px) horizontal bar placed between the timeline and the playback controls. Hidden when ratio is "Off". Shows the full frame width as a dark bar with the selected crop window highlighted in a lighter color. Clicking repositions the crop center. Shown/hidden based on ratio selection.
## CropBarWidget
- `set_source_ratio(w: int, h: int)` — called after file load; stores `w/h` to compute crop window width as fraction of bar.
- `set_portrait_ratio(ratio: str | None)` — updates the crop window proportion and repaints.
- `set_crop_center(frac: float)` — updates highlighted position and repaints.
- `mousePressEvent` — emits `crop_changed = pyqtSignal(float)` with clamped x fraction.
- Crop window fraction = `portrait_w / source_w` where `portrait_w = source_h * (num/den)`. Clamped so window never exceeds bar bounds.
## MpvWidget click
`MpvWidget` gains `crop_clicked = pyqtSignal(float)` emitted on `mousePressEvent` with `event.position().x() / self.width()`.
## MainWindow wiring
- `_crop_center: float = 0.5` — current crop center, persisted via `QSettings` key `crop_center`.
- `_on_crop_click(frac: float)` — shared slot for both `MpvWidget.crop_clicked` and `CropBarWidget.crop_changed`. Clamps, stores, saves to QSettings, updates bar.
- `_after_load` — calls `self._crop_bar.set_source_ratio(w, h)` using mpv dimensions after file load.
- `_on_portrait_ratio_changed` — shows/hides `CropBarWidget`, updates bar ratio, saves to QSettings.
- `_on_export` — reads ratio and crop center, passes to `build_ffmpeg_command`.
## ffmpeg filter chain
`build_ffmpeg_command` gains `portrait_ratio: str | None = None` and `crop_center: float = 0.5`.
Crop filter expressions (ffmpeg evaluates `iw`/`ih` at runtime — no need to know source dimensions ahead of time):
| Ratio | Crop width expression |
|-------|----------------------|
| 9:16 | `ih*9/16` |
| 4:5 | `ih*4/5` |
| 1:1 | `ih` |
X offset: `max(0\\,min((iw-CW)*C\\,iw-CW))` where `CW` is the crop width expression and `C` is crop center (01).
Full crop filter: `crop=CW:ih:X:0`
When both portrait crop and short-side resize are active, filters chain as a single `-vf` value: `crop=...,scale=...` (crop first, then scale).
## Persistence
| QSettings key | Value |
|------------------|--------------------|
| `portrait_ratio` | `"Off"` / `"9:16"` / `"4:5"` / `"1:1"` |
| `crop_center` | float string, default `"0.5"` |