diff --git a/docs/cinematic-quality-rules.md b/docs/cinematic-quality-rules.md index 47d2f3a..ad0603a 100644 --- a/docs/cinematic-quality-rules.md +++ b/docs/cinematic-quality-rules.md @@ -51,9 +51,11 @@ Legend: ✅ implemented · ⏳ planned (see `cinematic-highlight-poc-plan.md` P5 per-shot push-in (sub-segment splitting would reset the zoom); needs a time-varying `setpts` or a push-in that spans sub-segments. The crossfade already softens the *cut* into the payoff. -## R7 — Overlays: bold, animated, synced ⏳ (P5.12) -- Title/label text scaled to the frame, with an entrance (scale/fade) timed to the musical/edit accent — not - small static text dropped on screen. +## R7 — Overlays: bold, animated, synced ✅ +- **Rule:** captions are large (fontsize 84), thick-outlined + drop-shadowed for legibility on any background, + with a snappy entrance (0.18 s alpha punch + 34 px rise-up over 0.22 s) and a soft ease-out. `drawTextFilter`. +- **Ties to Tier 2 (R9):** the vision director produces the caption *text* ("STRIKE"); R7 makes it *land*. The + overlay is placed on the payoff beat, so it's timed to the musical/edit accent. Verified on bowling. ## R8 — Music sync: climax on the payoff ⏳ (P5.13) - **Measure:** the payoff beat's timeline position. diff --git a/src/main/java/org/example/videoclips/editing/HighlightFfmpegRenderer.java b/src/main/java/org/example/videoclips/editing/HighlightFfmpegRenderer.java index a9f7758..2d33f29 100644 --- a/src/main/java/org/example/videoclips/editing/HighlightFfmpegRenderer.java +++ b/src/main/java/org/example/videoclips/editing/HighlightFfmpegRenderer.java @@ -953,15 +953,16 @@ public class HighlightFfmpegRenderer { private String drawTextFilter(TextOverlay overlay) { String start = Double.toString(overlay.timelineStartSeconds()); String end = Double.toString(overlay.timelineEndSeconds()); - // Smooth alpha fade: ramp up over the first 0.4 s, hold, ramp down over the last 0.4 s. - String alpha = "if(lt(t\\," + start + "+0.4)\\,(t-" + start + ")/0.4\\," - + "if(lt(t\\," + end + "-0.4)\\,1\\,(" + end + "-t)/0.4))"; - // Premium caption: refined size, soft drop shadow, thin subtle border for legibility. - return ("drawtext=text='%s':x=%s:y=%s:fontsize=48:fontcolor=white" - + ":shadowcolor=black@0.55:shadowx=2:shadowy=2:borderw=1:bordercolor=black@0.35" + // Snappy entrance timed to the beat: alpha punches in over 0.18 s, holds, eases out over 0.35 s. + String alpha = "if(lt(t\\," + start + "+0.18)\\,(t-" + start + ")/0.18\\," + + "if(lt(t\\," + end + "-0.35)\\,1\\,max(0\\,(" + end + "-t)/0.35)))"; + // Rise-up entrance: start 34 px below the resting position and ease up over 0.22 s. + String y = "(" + overlayY(overlay.placement()) + ")+34*(1-min(1\\,(t-" + start + ")/0.22))"; + // Bold cinematic caption: large, thick outline + strong drop shadow so it reads on any background. + return ("drawtext=text='%s':x=%s:y=%s:fontsize=84:fontcolor=white" + + ":shadowcolor=black@0.7:shadowx=3:shadowy=3:borderw=4:bordercolor=black@0.6" + ":alpha='%s':enable='between(t\\,%s\\,%s)'") - .formatted(escapeDrawText(overlay.text()), overlayX(overlay.placement()), - overlayY(overlay.placement()), alpha, start, end); + .formatted(escapeDrawText(overlay.text()), overlayX(overlay.placement()), y, alpha, start, end); } private String overlayX(String placement) { diff --git a/src/test/java/org/example/videoclips/editing/HighlightFfmpegRendererTest.java b/src/test/java/org/example/videoclips/editing/HighlightFfmpegRendererTest.java index c2ad5b2..2d1e7cb 100644 --- a/src/test/java/org/example/videoclips/editing/HighlightFfmpegRendererTest.java +++ b/src/test/java/org/example/videoclips/editing/HighlightFfmpegRendererTest.java @@ -105,9 +105,11 @@ class HighlightFfmpegRendererTest { assertThat(filter) .contains("drawtext=text='First light.'") - .contains("fontsize=48") - .contains("shadowcolor=black@0.55:shadowx=2:shadowy=2") - .contains("alpha='if(lt(t\\,0.5+0.4)") + .contains("fontsize=84") // bold + .contains("borderw=4:bordercolor=black@0.6") // thick outline + .contains("shadowcolor=black@0.7:shadowx=3:shadowy=3") // strong shadow + .contains("alpha='if(lt(t\\,0.5+0.18)") // snappy 0.18s punch-in + .contains("34*(1-min(1\\,(t-0.5)/0.22))") // rise-up entrance animation .contains("enable='between(t\\,0.5\\,3.0)'"); }