Make intro title fade optional

This commit is contained in:
hbrain 2026-05-28 16:56:35 +00:00
parent af3fd51226
commit f51273d447
2 changed files with 34 additions and 15 deletions

View file

@ -229,9 +229,10 @@ The most common visual timing/styling tweaks are exposed as constants near the t
| `INTRO_LOGO_EXTRA_HOLD` | Extra seconds the synthesized intro still remains before the first crossfade. | `6.0` |
| `INTRO_LOGO_FADE_OFFSET` | Minimum time (s) the logo stays fully visible before the first xfade is allowed to start. | `3.0` |
| `INTRO_LOGO_DEFAULT_ALPHA` | Default opacity for the intro logo (1 = fully opaque, 0 = invisible). | `1.0` |
| `INTRO_TITLE_FADE_DELAY` | Seconds after start before the main title begins fading in. | `3.0` |
| `INTRO_TITLE_START_OFFSET` | Seconds after start before the title appears (or starts fading in). | `2.0` |
| `INTRO_TITLE_FADE_LENGTH` | Seconds the fade-in/out lasts. | `1.0` |
| `INTRO_TITLE_VISIBLE` | Seconds the title stays fully visible between fade in/out. | `4.0` |
| `INTRO_TITLE_FADE_ENABLED` | If `False`, the title pops in at `INTRO_TITLE_START_OFFSET` instead of fading in. | `True` |
| `DEFAULT_IMAGE_DURATION` | Default fully visible time per still image before fades are added. | `6.0` |
| `DEFAULT_CROSSFADE_DURATION` | Default crossfade duration between media items. | `3.0` |
| `FIRST_IMAGE_DURATION_MULTIPLIER` | Multiplier applied to the first stills `--image-duration` to delay the first transition. | `2.0` |
@ -259,7 +260,7 @@ Adjust these constants and rerun `movmaker.py` to change the intro behavior with
- reads the Jamendo client ID from `JAMENDO_CLIENT_ID` or `~/.config/movmaker/jamendo.env`
- fades music in over `DEFAULT_AUDIO_FADE_IN` seconds (2s) and out over `DEFAULT_AUDIO_FADE_OUT` seconds (10s) by default
- builds a black intro still (matching the first media items resolution) when `--intro-logo` is provided, scales the logo to `INTRO_LOGO_TARGET_HEIGHT_RATIO` of the frame height (75% by default), shows it at `INTRO_LOGO_DEFAULT_ALPHA` opacity (1.0 = fully opaque) from frame 0, and keeps it on screen for `audio_fade_in + INTRO_LOGO_EXTRA_HOLD` seconds (with defaults this is `DEFAULT_AUDIO_FADE_IN + INTRO_LOGO_EXTRA_HOLD`, i.e. 8s) before allowing the first crossfade; the same clip is appended at the end so the last real scene crossfades back into the logo-on-black frame
- opening title text fades in after `INTRO_TITLE_FADE_DELAY` seconds (2s), stays visible for `INTRO_TITLE_VISIBLE` seconds (4s), then fades out over `INTRO_TITLE_FADE_LENGTH` seconds (1s)
- opening title text shows up at `INTRO_TITLE_START_OFFSET` seconds (2s). When `INTRO_TITLE_FADE_IN_ENABLED` is `True` it fades in/out using `INTRO_TITLE_FADE_LENGTH` (1s); otherwise it appears instantly and only fades out (if `INTRO_TITLE_FADE_LENGTH` > 0). It remains fully visible for `INTRO_TITLE_VISIBLE` seconds (4s).
- fades the first real clip in with the `--video-fade` duration (default `DEFAULT_VIDEO_FADE`, currently 1s) while leaving the logo intro instant-on when `INTRO_VIDEO_FADE_ENABLED` is `False`; when a logo intro is present, the reusable outro clip removes the need for a final global fade-out, so the crossfade into the outro handles the closing visuals while audio still follows `DEFAULT_AUDIO_FADE_OUT`
- displays optional per-file captions at bottom center with slightly off-white text and a soft shadow; use `|` to split caption lines
- adds persistent bottom-left title/date/place and bottom-right `Bubulescu.Org` stamps

View file

@ -36,13 +36,14 @@ FFMPEG = BASE_DIR / "ffmpeg" / "ffmpeg"
FFPROBE = BASE_DIR / "ffmpeg" / "ffprobe"
INTRO_LOGO_NAME = "intro.png"
DEFAULT_INTRO_LOGO = BASE_DIR / "img" / "moto_travel.png"
INTRO_LOGO_TARGET_HEIGHT_RATIO = 0.90
INTRO_LOGO_TARGET_HEIGHT_RATIO = 0.75
INTRO_LOGO_EXTRA_HOLD = 6.0
INTRO_LOGO_FADE_OFFSET = 3.0
INTRO_LOGO_DEFAULT_ALPHA = 1.0
INTRO_TITLE_FADE_DELAY = 2.0
INTRO_TITLE_START_OFFSET = 4.0
INTRO_TITLE_FADE_LENGTH = 1.0
INTRO_TITLE_VISIBLE = 6.0
INTRO_TITLE_FADE_ENABLED = True
DEFAULT_IMAGE_DURATION = 6.0
DEFAULT_CROSSFADE_DURATION = 3.0
FIRST_IMAGE_DURATION_MULTIPLIER = 2.0
@ -1001,17 +1002,34 @@ def build_video(
subtitle_font_size = max(40, height // 18)
main_title = data.title or title_lines[0]
subtitle = " | ".join(x for x in [data.date, data.place] if x)
fade_in_start = INTRO_TITLE_FADE_DELAY
fade_in_end = fade_in_start + INTRO_TITLE_FADE_LENGTH
visible_end = fade_in_end + INTRO_TITLE_VISIBLE
fade_out_end = visible_end + INTRO_TITLE_FADE_LENGTH
title_alpha = (
f"if(lt(t\\,{fade_in_start:.3f})\\,0\\,"
f"if(lt(t\\,{fade_in_end:.3f})\\,(t-{fade_in_start:.3f})/{INTRO_TITLE_FADE_LENGTH:.3f}\\,"
f"if(lt(t\\,{visible_end:.3f})\\,1\\,"
f"if(lt(t\\,{fade_out_end:.3f})\\,({fade_out_end:.3f}-t)/{INTRO_TITLE_FADE_LENGTH:.3f}\\,0))))"
)
title_enable = f"enable='between(t,{fade_in_start:.3f},{fade_out_end:.3f})'"
fade_in_start = INTRO_TITLE_START_OFFSET
fade_in_enabled = INTRO_TITLE_FADE_IN_ENABLED and INTRO_TITLE_FADE_LENGTH > 0
fade_out_enabled = INTRO_TITLE_FADE_LENGTH > 0
fade_in_end = fade_in_start + (INTRO_TITLE_FADE_LENGTH if fade_in_enabled else 0.0)
visible_start = fade_in_end if fade_in_enabled else fade_in_start
visible_end = visible_start + INTRO_TITLE_VISIBLE
fade_out_end = visible_end + (INTRO_TITLE_FADE_LENGTH if fade_out_enabled else 0.0)
if fade_in_enabled:
title_alpha = (
f"if(lt(t\\,{fade_in_start:.3f})\\,0\\,"
f"if(lt(t\\,{fade_in_end:.3f})\\,(t-{fade_in_start:.3f})/{INTRO_TITLE_FADE_LENGTH:.3f}\\,"
f"if(lt(t\\,{visible_end:.3f})\\,1\\,"
f"if(lt(t\\,{fade_out_end:.3f})\\,({fade_out_end:.3f}-t)/{INTRO_TITLE_FADE_LENGTH:.3f}\\,0))))"
)
else:
if fade_out_enabled:
title_alpha = (
f"if(lt(t\\,{fade_in_start:.3f})\\,0\\,"
f"if(lt(t\\,{visible_end:.3f})\\,1\\,"
f"if(lt(t\\,{fade_out_end:.3f})\\,({fade_out_end:.3f}-t)/{INTRO_TITLE_FADE_LENGTH:.3f}\\,0)))"
)
else:
title_alpha = (
f"if(lt(t\\,{fade_in_start:.3f})\\,0\\,"
f"if(lt(t\\,{visible_end:.3f})\\,1\\,0))"
)
title_enable_end = fade_out_end if fade_out_enabled else visible_end
title_enable = f"enable='between(t,{fade_in_start:.3f},{title_enable_end:.3f})'"
if subtitle:
title_y = f"(h-text_h)/2-{title_font_size // 2}"
filters.append(