diff --git a/README.md b/README.md index 23ee6a7..ebdb6fd 100644 --- a/README.md +++ b/README.md @@ -224,6 +224,13 @@ Ctrl+Shift+R, который схлопывает дыру под курсоро двойной. Ручка стоит посередине именно видимой части, а не всего клипа: до середины часового клипа, уехавшей за край экрана, мышью не дотянуться. +**Затухания тянутся за ручки в верхних углах клипа.** Взять их можно и +промахнувшись: зона захвата шире нарисованного квадратика и уходит внутрь +клипа. Пока затухание нулевое, ручка стоит в самом углу и целиться есть +куда; стоит её сдвинуть — она висит посреди клипа сама по себе, и попасть +в девять точек почти нельзя. Над зоной захвата меняется курсор — по нему +и видно, где взять (`tests/fadegrab.sh`). + Громкость, отличная от обычной, — единственное место, где звук перекодируется: изменить громкость, не тронув сам звук, нельзя. Видео при этом по-прежнему копируется. Проверено: клип с громкостью 30% дал в ролике diff --git a/src/uTimelineCtrl.pas b/src/uTimelineCtrl.pas index 9409da4..3fee7f3 100644 --- a/src/uTimelineCtrl.pas +++ b/src/uTimelineCtrl.pas @@ -211,6 +211,14 @@ type procedure DrawFades(ACanvas: TCanvas; AClip: TClip; const ABody: TRect); { Ручка затухания. AIn = True — появление (левый угол), иначе уход. } function FadeHandleRect(AClip: TClip; ATop: Integer; AIn: Boolean): TRect; + { Зона, в которой ручку затухания можно взять. Она шире нарисованной + ручки: пока затухание нулевое, ручка стоит в углу клипа и целиться + есть куда, а после протяжки она висит посреди клипа сама по себе, и + попасть в девять точек почти нельзя. } + function FadeGrabRect(AClip: TClip; ATop: Integer; AIn: Boolean): TRect; + { Какую из двух ручек затухания берут в этой точке. Пусто — ни одну. } + function FadeAtPoint(AClip: TClip; const AX, AY, ATop: Integer; + out AIn: Boolean): Boolean; { Полоса перекрытия двух клипов одной дорожки: там сам собой выходит переход, и это надо видеть. } procedure DrawOverlaps(ACanvas: TCanvas; ATrack: TTrack; ATop: Integer); @@ -1192,6 +1200,52 @@ begin Exit(Rect(0, 0, 0, 0)); end; +function TTimelineControl.FadeGrabRect(AClip: TClip; ATop: Integer; + AIn: Boolean): TRect; +const + { По сторонам — чтобы не целиться по горизонтали в девять точек. Вниз — + потому что ручка сидит на верхней грани клипа, и рука уводит курсор + внутрь клипа, а не выше него. } + PadX = 6; + PadDown = 7; +begin + Result := FadeHandleRect(AClip, ATop, AIn); + if Result.Right <= Result.Left then + Exit; + Dec(Result.Left, PadX); + Inc(Result.Right, PadX); + Inc(Result.Bottom, PadDown); + if Result.Left < FHeaderWidth then + Result.Left := FHeaderWidth; +end; + +function TTimelineControl.FadeAtPoint(AClip: TClip; const AX, AY, + ATop: Integer; out AIn: Boolean): Boolean; +var + RIn, ROut: TRect; + HitIn, HitOut: Boolean; +begin + AIn := False; + RIn := FadeGrabRect(AClip, ATop, True); + ROut := FadeGrabRect(AClip, ATop, False); + HitIn := (RIn.Right > RIn.Left) and PtInRect(RIn, Point(AX, AY)); + HitOut := (ROut.Right > ROut.Left) and PtInRect(ROut, Point(AX, AY)); + + { Зоны расширены, и на коротком клипе они могут налезть друг на друга. + Тогда берём ту ручку, к которой ближе: другого разумного ответа нет. } + if HitIn and HitOut then + HitOut := Abs(AX - (ROut.Left + ROut.Right) div 2) < + Abs(AX - (RIn.Left + RIn.Right) div 2); + + if HitOut then + begin + AIn := False; + Exit(True); + end; + Result := HitIn; + AIn := HitIn; +end; + procedure TTimelineControl.DrawFades(ACanvas: TCanvas; AClip: TClip; const ABody: TRect); var @@ -2028,7 +2082,7 @@ var Part: THeaderPart; VolHandle: TRect; CropRect: TRect; - FadeHandle: TRect; + FadeIsIn: Boolean; begin inherited MouseDown(Button, Shift, X, Y); SetFocus; @@ -2204,23 +2258,12 @@ begin { Ручки затухания стоят в верхних углах, а там же начинается зона растягивания за край. Приоритет у ручек: они мелкие, промахнуться мимо них легче, а растянуть клип можно, взявшись за край ниже. } - FadeHandle := FadeHandleRect(Clip, TrackTop(TrackIndexAt(Y)), True); - if (FadeHandle.Right > FadeHandle.Left) and - PtInRect(FadeHandle, Point(X, Y)) then + if FadeAtPoint(Clip, X, Y, TrackTop(TrackIndexAt(Y)), FadeIsIn) then begin - FAction := taFadeIn; - FProject.BeginChange; - FPressX := X; - FPressY := Y; - FMovedWhileDown := False; - Invalidate; - Exit; - end; - FadeHandle := FadeHandleRect(Clip, TrackTop(TrackIndexAt(Y)), False); - if (FadeHandle.Right > FadeHandle.Left) and - PtInRect(FadeHandle, Point(X, Y)) then - begin - FAction := taFadeOut; + if FadeIsIn then + FAction := taFadeIn + else + FAction := taFadeOut; FProject.BeginChange; FPressX := X; FPressY := Y; @@ -2278,6 +2321,7 @@ var Clip: TClip; Track, TargetTrack: TTrack; VolHandle: TRect; + FadeIsIn: Boolean; begin inherited MouseMove(Shift, X, Y); VolHandle := Rect(0, 0, 0, 0); @@ -2307,6 +2351,12 @@ begin (X >= VolHandle.Left) and (X <= VolHandle.Right) and (Y >= VolHandle.Top - 3) and (Y <= VolHandle.Bottom + 3) then Cursor := crSizeNS + { Ручка затухания мелкая и после протяжки висит посреди клипа, где + её ничем не выдать. Пусть выдаёт курсор: он меняется над зоной + захвата, и становится видно, где взять. } + else if (Clip <> nil) and (Idx >= 0) and + FadeAtPoint(Clip, X, Y, TrackTop(Idx), FadeIsIn) then + Cursor := crSizeWE else if Edge <> 0 then Cursor := crSizeWE else if Clip <> nil then diff --git a/tests/fadegrab.sh b/tests/fadegrab.sh new file mode 100755 index 0000000..e83a073 --- /dev/null +++ b/tests/fadegrab.sh @@ -0,0 +1,67 @@ +#!/bin/sh +# Повторный захват рампы затухания. +# +# Первый раз ручка стоит в правом верхнем углу клипа — целиться есть куда. +# Второй раз она висит посреди клипа сама по себе, и целятся в неё на +# глаз: чуть ниже и чуть в стороне от середины. Оба захвата обязаны +# сработать. +# Запуск (редактор должен собираться в этом дереве, экран :99 поднят): +# tests/fadegrab.sh [<путь к nlve>] +BIN=${1:-$PWD/nlve} +cd "$(dirname "$0")/.." +BIN=${1:-$PWD/nlve} +PROJ=$PWD/test/поток/рампа.nlve +# Монтаж для проверки: два клипа без затуханий. В репозиторий не входит — +# ссылается на съёмку, которой здесь нет. +[ -f test/поток/пара.nlve ] || { echo "нет монтажа test/поток/пара.nlve"; exit 2; } +cp test/поток/пара.nlve "$PROJ" +for p in $(pgrep -x nlve); do + tr '\0' '\n' < /proc/$p/environ 2>/dev/null | grep -qx "DISPLAY=:99" && kill $p +done +sleep 1 +rm -rf test/поток/профиль-рампа; mkdir -p test/поток/профиль-рампа +rm -f test/поток/*.autosave +SP=$PWD/test/поток/профиль-рампа +XDG_CONFIG_HOME=$SP XDG_CACHE_HOME=$SP XDG_DATA_HOME=$SP DISPLAY=:99 \ + setsid "$BIN" "$PROJ" >/dev/null 2>&1 < /dev/null & +sleep 14 +X="timeout 8 env DISPLAY=:99 xdotool" +$X windowmove 2097206 0 0 windowsize 2097206 1500 860 2>/dev/null +sleep 2 + +fade() { + $X key ctrl+s + sleep 2 + python3 -c " +import json +d=json.load(open('$PROJ')) +for t in d['tracks']: + for c in t['clips']: + if c['kind']=='video' and c['tl_start']<0.001: + print('%.3f' % c.get('fade_out',0)); raise SystemExit +print('нет клипа') +" +} + +echo " затухание до правок: $(fade) с" +$X mousemove 783 490 mousedown 1; sleep 0.3 +$X mousemove 745 490; sleep 0.2; $X mousemove 720 490; sleep 0.2 +$X mouseup 1; sleep 1.5 +A=$(fade) +echo " после первого захвата (ручка в углу): $A с" + +$X mousemove 724 495 mousedown 1; sleep 0.3 +$X mousemove 700 495; sleep 0.2; $X mousemove 680 495; sleep 0.2 +$X mouseup 1; sleep 1.5 +B=$(fade) +echo " после второго захвата (ручка посреди клипа, мимо центра): $B с" + +python3 -c " +a, b = $A, $B +if a <= 0.01: + print(' ПЛОХО: не сработал даже первый захват') +elif b <= a + 0.01: + print(' ПЛОХО: второй захват не сработал') +else: + print(' ok: обе протяжки сработали') +"