From ea51773bed00691bb5758ec0ecd84a5a93a8d548 Mon Sep 17 00:00:00 2001 From: 4aiman Date: Mon, 31 Aug 2026 00:46:58 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9A=D0=BB=D1=8E=D1=87=20=D0=B8=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=B7=D1=80=D0=B0=D1=87=D0=BD=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D1=8C=20=D0=B2=D0=B8=D0=B4=D0=BD=D0=BE=20=D0=BF=D1=80=D1=8F?= =?UTF-8?q?=D0=BC=D0=BE=20=D0=B2=20=D0=BF=D1=80=D0=B5=D0=B4=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D1=81=D0=BC=D0=BE=D1=82=D1=80=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Оверлей показывался как есть, со своим фоном: чтобы увидеть, что выйдет после сборки, приходилось собирать. Теперь выбитый цвет и приглушение считаются в той же цепочке плеера, на лету. Строки фильтров стали общими со сборкой (ColorKeyFilter, OpacityFilter в uTransform): окно и готовый ролик больше не могут разойтись по тому, как записан цвет или порог. Переключатель в меню «Вид», состояние переживает перезапуск. Замер на 2688x1512, 600 кадров: основа 2.07 с, с наложением 2.43 с, с ключом и прозрачностью 2.66 с — вчетверо быстрее реального времени, но на слабой машине это заметнее, поэтому выключить можно. В бэклог записано окно настроек: переключателей, которые стоят машинного времени, набралось много, и живут они кто где — кнопками внизу, в меню, а выбор кодировщика вообще нигде. --- BACKLOG.md | 22 ++++++++++ README.md | 12 ++++++ .../2026-08-31_наложение-в-предпросмотре.md | 33 +++++++++++++-- src/uFfmpegCmd.pas | 10 ++--- src/uFlatten.pas | 22 +++++++++- src/uMainForm.pas | 42 +++++++++++++++++-- src/uSettings.pas | 6 +++ src/uTransform.pas | 36 +++++++++++++++- 8 files changed, 165 insertions(+), 18 deletions(-) diff --git a/BACKLOG.md b/BACKLOG.md index ef3652d..f507c80 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -76,6 +76,28 @@ ffmpeg проблемой не является: он запускается о MP4/H.264 нельзя — контейнер и кодек альфу не хранят. Для этого нужен отдельный вывод в VP9 или ProRes 4444. +## Окно настроек программы + +Переключателей, которые стоят машинного времени, становится больше, а +живут они кто где: прилипание и схлопывание — кнопками внизу, ключ в +предпросмотре — в меню «Вид», выбор кодировщика вообще нигде (берётся +NVENC, если он есть). Это разрозненно и не даёт увидеть общую цену. + +Нужно одно окно, где собрано всё, что можно выключить ради скорости: + +- ключ и прозрачность в предпросмотре (сейчас в меню «Вид»); +- наложение в предпросмотре вообще — на слабой машине может не хватать и + его, тогда честнее показывать одну основу; +- кодировщик для пересборки: аппаратный или процессорный, и какой + именно; +- где держать кэш и когда его убирать; +- прилипание клипов и курсора (сейчас кнопками, но им место и здесь). + +Почему это важно: разработка идёт на машине с быстрой видеокартой, и +всё, что здесь считается «недорогим», на другой может оказаться +неподъёмным. Пока цифры замеряны только на одной машине, у человека +должна быть возможность выключить любую из этих трат. + ## Мелочи, до которых не дошли руки **Значок файла программы под Windows.** Значок окна задаётся из diff --git a/README.md b/README.md index 3a66955..a4f52b7 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,18 @@ xdg-mime query default application/x-nlve-project липнут при перетаскивании, курсор — при щелчке, и одно бывает нужно без другого. +**Ключ и прозрачность в предпросмотре** (меню «Вид»). Включено — окно +показывает то же, что даст сборка: выбитый цвет и приглушение считаются +на лету. Выключено — оверлей виден целиком, со своим фоном: так понятно, +куда он встал и где его края. + +Считается это по площади самого оверлея, а не всего кадра. Замер на +2688x1512 (600 кадров, десять секунд материала): основа 2.07 с, с +наложением 2.43 с, с ключом и прозрачностью 2.66 с. То есть наложение +стоит 0.36 с, ключ добавляет ещё 0.23 с — вчетверо быстрее реального +времени. На слабой машине это может быть заметнее, потому выключатель и +есть. + **Схлопывание.** Кнопка внизу показывает и режим, и состояние: утоплена — автосхлопывание включено, и тогда при перетаскивании клипа всё, что начинается не раньше него, едет следом. Стрелка рядом открывает выбор diff --git a/docs/session-logs/2026-08-31_наложение-в-предпросмотре.md b/docs/session-logs/2026-08-31_наложение-в-предпросмотре.md index 2d72c77..c24bd3d 100644 --- a/docs/session-logs/2026-08-31_наложение-в-предпросмотре.md +++ b/docs/session-logs/2026-08-31_наложение-в-предпросмотре.md @@ -61,8 +61,33 @@ mpv 0.37, видео 2688x1512 HEVC 87 Мбит/с, 600 кадров (10 с ма и картинку, и звук. - `./build.sh tests` — все проверки пройдены. -## Что осталось +## Продолжение: ключ прямо в предпросмотре -Ключ в предпросмотре не применяется — так решено сознательно. Стоит он -недорого (фильтр работает по небольшой площади), и при желании это -делается переключателем. +По просьбе сделано и это. Ключ и прозрачность считаются в той же цепочке +плеера, отдельным механизмом не обзаводились: строки фильтров теперь +общие со сборкой (`ColorKeyFilter`, `OpacityFilter` в `uTransform`), так +что окно и готовый ролик не могут разойтись по формату записи. + +Переключатель — в меню «Вид», состояние переживает перезапуск. Замер на +тяжёлом материале 2688x1512, 600 кадров: + +| Что | Время | +|---|---| +| только основа | 2.07 с | +| плюс наложение | 2.43 с | +| плюс ключ и прозрачность | 2.66 с | + +Проверено снимками окна: с ключом сквозь пурпур видно нижнее видео, +без ключа оверлей виден плашкой целиком. + +В бэклог записано окно настроек программы: переключателей, которые стоят +машинного времени, становится много, и живут они кто где. + +## Западня с настройками + +Тестовые запуски шли с подменённым HOME, и это не изолировало настройки: +в сессии задан `XDG_CONFIG_HOME`, и программа читала настоящий файл +пользователя (оттуда и бралась высота панели предпросмотра в 883 точки, +из-за которой монтаж не помещался в окно). Файл не пострадал — проверено +по содержимому, — но теперь тестовые скрипты задают `XDG_CONFIG_HOME`, +`XDG_CACHE_HOME` и `XDG_DATA_HOME` своими. diff --git a/src/uFfmpegCmd.pas b/src/uFfmpegCmd.pas index c902428..b08f31e 100644 --- a/src/uFfmpegCmd.pas +++ b/src/uFfmpegCmd.pas @@ -235,7 +235,7 @@ function ParseProgressLine(const ALine: string; var ASeconds: Double; implementation uses - StrUtils; + StrUtils, uTransform; { Общие параметры вывода в формате проекта. Объявлено заранее, потому что используется выше по тексту, чем определено. } @@ -318,12 +318,8 @@ begin масштабом. Цвет записывается как 0xRRGGBB — порядок обратный тому, в каком он хранится у нас. } if ALayers[I].KeyEnabled then - Fit := Fit + Format(',colorkey=0x%.2x%.2x%.2x:%s:%s', - [ALayers[I].KeyColor and $FF, - (ALayers[I].KeyColor shr 8) and $FF, - (ALayers[I].KeyColor shr 16) and $FF, - SecondsToStr(ALayers[I].KeySimilarity / 100), - SecondsToStr(ALayers[I].KeyBlend / 100)]); + Fit := Fit + ',' + ColorKeyFilter(ALayers[I].KeyColor, + ALayers[I].KeySimilarity, ALayers[I].KeyBlend); Graph := Graph + Format('[%d:v]%s,format=yuva420p[v%d];', [I, Fit, I]); end; diff --git a/src/uFlatten.pas b/src/uFlatten.pas index 8386ef6..b639f8b 100644 --- a/src/uFlatten.pas +++ b/src/uFlatten.pas @@ -140,11 +140,17 @@ function FlattenProject(AProject: TProject; под ними видно то, что лежит ниже, — а выдаются отдельным списком со временем и местом в кадре. + AWithKey — применять ли к оверлею цветовой ключ и прозрачность прямо в + плеере. Тогда окно показывает то же, что даст сборка. Стоит это + недорого: фильтр работает по площади самого оверлея, а не всего кадра, + но на слабой машине лишним не будет, поэтому решает вызывающий. + Для сборки ролика это не применяется: там всё сводится по-настоящему. } function FlattenPreview(AProject: TProject; out AVideo, AAudio: TFlatSegmentArray; out AOverlays: TPreviewOverlays; - out AMissing: TGenRequests): Boolean; + out AMissing: TGenRequests; + AWithKey: Boolean = False): Boolean; { Путь к сгенерированному клипу. Имя выводится из содержимого запроса, поэтому одинаковые запросы дают один и тот же файл, и он делается @@ -1034,7 +1040,8 @@ end; function FlattenPreview(AProject: TProject; out AVideo, AAudio: TFlatSegmentArray; out AOverlays: TPreviewOverlays; - out AMissing: TGenRequests): Boolean; + out AMissing: TGenRequests; + AWithKey: Boolean): Boolean; var Hidden: TFPList; MissA: TGenRequests; @@ -1108,6 +1115,17 @@ begin Item.Info.Width, Item.Info.Height, AProject.Master.Width, AProject.Master.Height, Ov.X, Ov.Y, Ov.Width, Ov.Height); + { Порядок тот же, что при сборке: сперва вписали в кадр, потом + выбили цвет, потом приглушили. Иначе смягчение края разошлось бы + с тем, что даст готовый ролик. } + if AWithKey then + begin + if Clip.ColorKey.Enabled then + Ov.Filter := Ov.Filter + ',' + ColorKeyFilter(Clip.ColorKey.Color, + Clip.ColorKey.Similarity, Clip.ColorKey.Blend); + if OpacityFilter(Clip.Opacity) <> '' then + Ov.Filter := Ov.Filter + ',' + OpacityFilter(Clip.Opacity); + end; SetLength(AOverlays, N + 1); AOverlays[N] := Ov; Inc(N); diff --git a/src/uMainForm.pas b/src/uMainForm.pas index 2a34a93..c60e5c0 100644 --- a/src/uMainForm.pas +++ b/src/uMainForm.pas @@ -129,6 +129,9 @@ type первую. } FErrorCount: Integer; FLogButton: TThemedButton; + { Пункт «Ключ и прозрачность в предпросмотре»: галку в нём приходится + держать за руку — она не следует за настройкой сама. } + FKeyPreviewItem: TMenuItem; { Показанная доля выполнения. Отдельно от настоящей, потому что растёт она по своим правилам — см. AdvanceJobBar. } FJobShown: Double; @@ -238,6 +241,7 @@ type procedure AdvanceJobBar; procedure LogButtonClick(Sender: TObject); procedure MenuToggleLog(Sender: TObject); + procedure MenuTogglePreviewKey(Sender: TObject); procedure MenuAbout(Sender: TObject); procedure MenuAddText(Sender: TObject); { Открывает редактор текста для клипа. True — что-то изменилось. } @@ -583,8 +587,10 @@ procedure TMainForm.BuildMenu; FMenu.Items.Add(Result); end; - procedure AddItem(AParent: TMenuItem; const ACaption: string; - AHandler: TNotifyEvent; AShortCut: TShortCut = 0); + { Возвращает созданный пункт: у пунктов с галкой её потом надо ставить + и снимать, а искать пункт по названию — способ ненадёжный. } + function AddItem(AParent: TMenuItem; const ACaption: string; + AHandler: TNotifyEvent; AShortCut: TShortCut = 0): TMenuItem; var Item: TMenuItem; begin @@ -593,6 +599,7 @@ procedure TMainForm.BuildMenu; Item.OnClick := AHandler; Item.ShortCut := AShortCut; AParent.Add(Item); + Result := Item; end; procedure AddSep(AParent: TMenuItem); @@ -675,6 +682,11 @@ begin AddItem(MView, 'Отдалить', @MenuZoomOut, ShortCut(VK_SUBTRACT, [ssCtrl])); AddSep(MView); AddItem(MView, 'Журнал сообщений', @MenuToggleLog, ShortCut(VK_L, [ssCtrl])); + AddSep(MView); + FKeyPreviewItem := AddItem(MView, 'Ключ и прозрачность в предпросмотре', + @MenuTogglePreviewKey); + FKeyPreviewItem.AutoCheck := False; + FKeyPreviewItem.Checked := FSettings.PreviewKey; MProject := AddTop('Проект'); AddItem(MProject, 'Сведения о проекте...', @MenuProjectInfo); @@ -1882,7 +1894,8 @@ begin { Уплощение для предпросмотра: клипы, которые кладутся поверх, идут отдельно. Плеер положит их сам, ничего не собирая. } - if not FlattenPreview(FProject, Segments, AudioSegs, Overlays, Missing) then + if not FlattenPreview(FProject, Segments, AudioSegs, Overlays, Missing, + FSettings.PreviewKey) then begin FPlayer.Stop; FLastLoadedEdl := ''; @@ -3111,6 +3124,29 @@ end; { К началу самого раннего клипа по всем дорожкам. Если монтаж начинается с пятой секунды, ноль — не то место, куда хотят попасть. } +{ Показывать ли в предпросмотре то, что даст сборка. + + Выключено — оверлей виден целиком, со своим фоном: так понятно, куда он + встал и где его края. Включено — выбитый цвет и прозрачность считаются + на лету, и окно совпадает с готовым роликом. + + Считается это не бесплатно, хотя и недорого: фильтр работает по площади + оверлея, а не всего кадра. Замерено на 2688x1512: наложение стоит 0.36 с + на десять секунд материала, ключ добавляет ещё 0.23 с. } +procedure TMainForm.MenuTogglePreviewKey(Sender: TObject); +begin + FSettings.PreviewKey := not FSettings.PreviewKey; + FKeyPreviewItem.Checked := FSettings.PreviewKey; + { Список кусков не изменился, а показывать надо иначе — заставляем + пересобрать цепочку. } + FLastLoadedEdl := ''; + SchedulePreview; + if FSettings.PreviewKey then + SetStatus('Предпросмотр показывает ключ и прозрачность.') + else + SetStatus('Предпросмотр показывает оверлей как есть.'); +end; + procedure TMainForm.MenuToggleRipple(Sender: TObject); begin ToggleRipple; diff --git a/src/uSettings.pas b/src/uSettings.pas index 7b3ca08..b4b3684 100644 --- a/src/uSettings.pas +++ b/src/uSettings.pas @@ -37,6 +37,7 @@ type FVideoPanelHeight: Integer; FLogVisible: Boolean; FPlayheadSnap: Boolean; + FPreviewKey: Boolean; FDepsReminder: Boolean; FRecentProjects: TStringList; function FileName: string; @@ -70,6 +71,8 @@ type property LogVisible: Boolean read FLogVisible write FLogVisible; { Прилипание курсора воспроизведения к границам клипов. } property PlayheadSnap: Boolean read FPlayheadSnap write FPlayheadSnap; + { Применять ли ключ и прозрачность прямо в предпросмотре. } + property PreviewKey: Boolean read FPreviewKey write FPreviewKey; { Напоминать ли о недостающих зависимостях при запуске. Гасится кнопкой «Не напоминать» в самом напоминании. } property DepsReminder: Boolean read FDepsReminder write FDepsReminder; @@ -103,6 +106,7 @@ begin FVideoPanelHeight := DefaultVideoHeight; FLogVisible := True; FPlayheadSnap := True; + FPreviewKey := True; FDepsReminder := True; end; @@ -184,6 +188,7 @@ begin FVideoPanelHeight := Obj.Get('video_panel_height', FVideoPanelHeight); FLogVisible := Obj.Get('log_visible', FLogVisible); FPlayheadSnap := Obj.Get('playhead_snap', FPlayheadSnap); + FPreviewKey := Obj.Get('preview_key', FPreviewKey); FDepsReminder := Obj.Get('deps_reminder', FDepsReminder); Arr := Obj.Find('recent_projects', jtArray) as TJSONArray; @@ -236,6 +241,7 @@ begin Root.Add('video_panel_height', FVideoPanelHeight); Root.Add('log_visible', FLogVisible); Root.Add('playhead_snap', FPlayheadSnap); + Root.Add('preview_key', FPreviewKey); Root.Add('deps_reminder', FDepsReminder); Arr := TJSONArray.Create; diff --git a/src/uTransform.pas b/src/uTransform.pas index 0c8b91a..acddee3 100644 --- a/src/uTransform.pas +++ b/src/uTransform.pas @@ -22,7 +22,7 @@ unit uTransform; interface uses - Classes, SysUtils; + Classes, SysUtils, uAppTypes; type { Как соотносятся кадр исходника и кадр проекта. } @@ -82,6 +82,16 @@ function OverlayFilter(const AT: TMediaTransform; ASrcWidth, ASrcHeight, ADstWidth, ADstHeight: Integer; out AX, AY, AWidth, AHeight: Integer): string; +{ Фильтр цветового ключа: этот цвет становится прозрачным. + + Один на всех: и сборка, и предпросмотр должны выбивать одинаково, иначе + окно обещает одно, а готовый ролик даёт другое. Цвет записывается как + 0xRRGGBB — порядок обратный тому, в каком он хранится у нас. } +function ColorKeyFilter(AColor: TRgb; ASimilarity, ABlend: Integer): string; + +{ Фильтр приглушения по альфа-каналу. Пусто, если приглушать нечего. } +function OpacityFilter(const AOpacity: Double): string; + function PreviewFitFilter(ADstWidth, ADstHeight: Integer): string; function FitModeToStr(AMode: TFitMode): string; @@ -91,7 +101,7 @@ function FitModeCaption(AMode: TFitMode): string; implementation uses - Math, uAppTypes; + Math; function DefaultTransform: TMediaTransform; begin @@ -143,6 +153,28 @@ begin (ASrcWidth = ADstWidth) and (ASrcHeight = ADstHeight); end; +{ Дробное число через точку: у ffmpeg разделитель всегда точка, а в + русской локали Format вставил бы запятую и развалил бы всю цепочку. } +function FilterNum(const AValue: Double): string; +begin + Result := SecondsToStr(AValue); +end; + +function ColorKeyFilter(AColor: TRgb; ASimilarity, ABlend: Integer): string; +begin + Result := Format('colorkey=0x%.2x%.2x%.2x:%s:%s', + [AColor and $FF, (AColor shr 8) and $FF, (AColor shr 16) and $FF, + FilterNum(ASimilarity / 100), FilterNum(ABlend / 100)]); +end; + +function OpacityFilter(const AOpacity: Double): string; +begin + if AOpacity >= 0.999 then + Result := '' + else + Result := 'colorchannelmixer=aa=' + FilterNum(Max(0, AOpacity)); +end; + function PreviewFitFilter(ADstWidth, ADstHeight: Integer): string; var W, H: string;