Skip to content

Commit dbac421

Browse files
committed
feat(captions): expand animated caption presets to 21 via data-driven JSON
Moved ANIMATION_PRESETS from hardcoded dict to opencut/data/animation_presets.json (with fallback). Added 14 new presets: scale_pulse, slide_left, slide_right, color_wave, drop_in, zoom_in, zoom_out, underline_reveal, shadow_lift, neon, stagger, flash, karaoke, elastic. New presets can be added by editing the JSON file without code changes.
1 parent d958b52 commit dbac421

2 files changed

Lines changed: 254 additions & 70 deletions

File tree

opencut/core/animated_captions.py

Lines changed: 45 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
Pillow-based renderer for zero-dependency operation.
1313
"""
1414

15+
import json
1516
import logging
1617
import os
1718
import tempfile
@@ -22,77 +23,51 @@
2223
logger = logging.getLogger("opencut")
2324

2425
# ---------------------------------------------------------------------------
25-
# Animation Presets
26+
# Animation Presets — loaded from JSON, with hardcoded fallback
2627
# ---------------------------------------------------------------------------
27-
ANIMATION_PRESETS = {
28-
"pop": {
29-
"label": "Pop",
30-
"description": "Words pop in with scale bounce (CapCut style)",
31-
"active_scale": 1.15,
32-
"inactive_scale": 1.0,
33-
"active_opacity": 1.0,
34-
"inactive_opacity": 0.3,
35-
"transition_frames": 4,
36-
},
37-
"fade": {
38-
"label": "Fade",
39-
"description": "Words fade in smoothly",
40-
"active_scale": 1.0,
41-
"inactive_scale": 1.0,
42-
"active_opacity": 1.0,
43-
"inactive_opacity": 0.2,
44-
"transition_frames": 8,
45-
},
46-
"slide_up": {
47-
"label": "Slide Up",
48-
"description": "Words slide up into position",
49-
"active_scale": 1.0,
50-
"inactive_scale": 1.0,
51-
"active_opacity": 1.0,
52-
"inactive_opacity": 0.2,
53-
"slide_y": -15,
54-
"transition_frames": 6,
55-
},
56-
"typewriter": {
57-
"label": "Typewriter",
58-
"description": "Words appear one by one like typing",
59-
"active_scale": 1.0,
60-
"inactive_scale": 1.0,
61-
"active_opacity": 1.0,
62-
"inactive_opacity": 0.0,
63-
"transition_frames": 2,
64-
},
65-
"bounce": {
66-
"label": "Bounce",
67-
"description": "Words bounce in with spring easing",
68-
"active_scale": 1.2,
69-
"inactive_scale": 0.8,
70-
"active_opacity": 1.0,
71-
"inactive_opacity": 0.3,
72-
"transition_frames": 6,
73-
},
74-
"glow": {
75-
"label": "Glow",
76-
"description": "Active word gets a bright glow effect",
77-
"active_scale": 1.05,
78-
"inactive_scale": 1.0,
79-
"active_opacity": 1.0,
80-
"inactive_opacity": 0.4,
81-
"glow_radius": 8,
82-
"transition_frames": 5,
83-
},
84-
"highlight_box": {
85-
"label": "Highlight Box",
86-
"description": "Active word gets a colored background box",
87-
"active_scale": 1.0,
88-
"inactive_scale": 1.0,
89-
"active_opacity": 1.0,
90-
"inactive_opacity": 0.5,
91-
"box_color": (255, 230, 0),
92-
"box_padding": 6,
93-
"transition_frames": 3,
94-
},
95-
}
28+
_PRESETS_JSON = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "animation_presets.json")
29+
30+
31+
def _load_presets() -> Dict:
32+
try:
33+
with open(_PRESETS_JSON, "r", encoding="utf-8") as f:
34+
presets = json.load(f)
35+
for v in presets.values():
36+
if "box_color" in v and isinstance(v["box_color"], list):
37+
v["box_color"] = tuple(v["box_color"])
38+
if "glow_color" in v and isinstance(v["glow_color"], list):
39+
v["glow_color"] = tuple(v["glow_color"])
40+
if "active_color" in v and isinstance(v["active_color"], list):
41+
v["active_color"] = tuple(v["active_color"])
42+
if "underline_color" in v and isinstance(v["underline_color"], list):
43+
v["underline_color"] = tuple(v["underline_color"])
44+
return presets
45+
except (OSError, json.JSONDecodeError, KeyError) as exc:
46+
logger.warning("Failed to load animation presets from %s: %s", _PRESETS_JSON, exc)
47+
return {
48+
"pop": {"label": "Pop", "description": "Words pop in with scale bounce",
49+
"active_scale": 1.15, "inactive_scale": 1.0, "active_opacity": 1.0,
50+
"inactive_opacity": 0.3, "transition_frames": 4},
51+
"fade": {"label": "Fade", "description": "Words fade in smoothly",
52+
"active_scale": 1.0, "inactive_scale": 1.0, "active_opacity": 1.0,
53+
"inactive_opacity": 0.2, "transition_frames": 8},
54+
"typewriter": {"label": "Typewriter", "description": "Words appear one by one",
55+
"active_scale": 1.0, "inactive_scale": 1.0, "active_opacity": 1.0,
56+
"inactive_opacity": 0.0, "transition_frames": 2},
57+
"bounce": {"label": "Bounce", "description": "Words bounce in with spring easing",
58+
"active_scale": 1.2, "inactive_scale": 0.8, "active_opacity": 1.0,
59+
"inactive_opacity": 0.3, "transition_frames": 6},
60+
"glow": {"label": "Glow", "description": "Active word gets a bright glow effect",
61+
"active_scale": 1.05, "inactive_scale": 1.0, "active_opacity": 1.0,
62+
"inactive_opacity": 0.4, "glow_radius": 8, "transition_frames": 5},
63+
"highlight_box": {"label": "Highlight Box", "description": "Active word gets a colored background box",
64+
"active_scale": 1.0, "inactive_scale": 1.0, "active_opacity": 1.0,
65+
"inactive_opacity": 0.5, "box_color": (255, 230, 0), "box_padding": 6,
66+
"transition_frames": 3},
67+
}
68+
69+
70+
ANIMATION_PRESETS = _load_presets()
9671

9772

9873
# ---------------------------------------------------------------------------
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
{
2+
"pop": {
3+
"label": "Pop",
4+
"description": "Words pop in with scale bounce (CapCut style)",
5+
"active_scale": 1.15,
6+
"inactive_scale": 1.0,
7+
"active_opacity": 1.0,
8+
"inactive_opacity": 0.3,
9+
"transition_frames": 4
10+
},
11+
"fade": {
12+
"label": "Fade",
13+
"description": "Words fade in smoothly",
14+
"active_scale": 1.0,
15+
"inactive_scale": 1.0,
16+
"active_opacity": 1.0,
17+
"inactive_opacity": 0.2,
18+
"transition_frames": 8
19+
},
20+
"slide_up": {
21+
"label": "Slide Up",
22+
"description": "Words slide up into position",
23+
"active_scale": 1.0,
24+
"inactive_scale": 1.0,
25+
"active_opacity": 1.0,
26+
"inactive_opacity": 0.2,
27+
"slide_y": -15,
28+
"transition_frames": 6
29+
},
30+
"typewriter": {
31+
"label": "Typewriter",
32+
"description": "Words appear one by one like typing",
33+
"active_scale": 1.0,
34+
"inactive_scale": 1.0,
35+
"active_opacity": 1.0,
36+
"inactive_opacity": 0.0,
37+
"transition_frames": 2
38+
},
39+
"bounce": {
40+
"label": "Bounce",
41+
"description": "Words bounce in with spring easing",
42+
"active_scale": 1.2,
43+
"inactive_scale": 0.8,
44+
"active_opacity": 1.0,
45+
"inactive_opacity": 0.3,
46+
"transition_frames": 6
47+
},
48+
"glow": {
49+
"label": "Glow",
50+
"description": "Active word gets a bright glow effect",
51+
"active_scale": 1.05,
52+
"inactive_scale": 1.0,
53+
"active_opacity": 1.0,
54+
"inactive_opacity": 0.4,
55+
"glow_radius": 8,
56+
"transition_frames": 5
57+
},
58+
"highlight_box": {
59+
"label": "Highlight Box",
60+
"description": "Active word gets a colored background box",
61+
"active_scale": 1.0,
62+
"inactive_scale": 1.0,
63+
"active_opacity": 1.0,
64+
"inactive_opacity": 0.5,
65+
"box_color": [255, 230, 0],
66+
"box_padding": 6,
67+
"transition_frames": 3
68+
},
69+
"scale_pulse": {
70+
"label": "Scale Pulse",
71+
"description": "Words pulse to a larger size on activation",
72+
"active_scale": 1.3,
73+
"inactive_scale": 1.0,
74+
"active_opacity": 1.0,
75+
"inactive_opacity": 0.4,
76+
"transition_frames": 5
77+
},
78+
"slide_left": {
79+
"label": "Slide Left",
80+
"description": "Words slide in from the right",
81+
"active_scale": 1.0,
82+
"inactive_scale": 1.0,
83+
"active_opacity": 1.0,
84+
"inactive_opacity": 0.1,
85+
"slide_x": -20,
86+
"transition_frames": 5
87+
},
88+
"slide_right": {
89+
"label": "Slide Right",
90+
"description": "Words slide in from the left",
91+
"active_scale": 1.0,
92+
"inactive_scale": 1.0,
93+
"active_opacity": 1.0,
94+
"inactive_opacity": 0.1,
95+
"slide_x": 20,
96+
"transition_frames": 5
97+
},
98+
"color_wave": {
99+
"label": "Color Wave",
100+
"description": "Active word shifts to a highlight color",
101+
"active_scale": 1.0,
102+
"inactive_scale": 1.0,
103+
"active_opacity": 1.0,
104+
"inactive_opacity": 0.6,
105+
"active_color": [255, 200, 50],
106+
"transition_frames": 4
107+
},
108+
"drop_in": {
109+
"label": "Drop In",
110+
"description": "Words drop from above with gravity",
111+
"active_scale": 1.0,
112+
"inactive_scale": 1.0,
113+
"active_opacity": 1.0,
114+
"inactive_opacity": 0.0,
115+
"slide_y": 25,
116+
"transition_frames": 5
117+
},
118+
"zoom_in": {
119+
"label": "Zoom In",
120+
"description": "Words zoom from tiny to full size",
121+
"active_scale": 1.0,
122+
"inactive_scale": 0.3,
123+
"active_opacity": 1.0,
124+
"inactive_opacity": 0.0,
125+
"transition_frames": 6
126+
},
127+
"zoom_out": {
128+
"label": "Zoom Out",
129+
"description": "Words zoom from oversized to normal",
130+
"active_scale": 1.0,
131+
"inactive_scale": 1.5,
132+
"active_opacity": 1.0,
133+
"inactive_opacity": 0.0,
134+
"transition_frames": 6
135+
},
136+
"underline_reveal": {
137+
"label": "Underline Reveal",
138+
"description": "Active word gets an animated underline",
139+
"active_scale": 1.0,
140+
"inactive_scale": 1.0,
141+
"active_opacity": 1.0,
142+
"inactive_opacity": 0.5,
143+
"underline": true,
144+
"underline_color": [255, 255, 100],
145+
"transition_frames": 4
146+
},
147+
"shadow_lift": {
148+
"label": "Shadow Lift",
149+
"description": "Active word lifts with a drop shadow",
150+
"active_scale": 1.05,
151+
"inactive_scale": 1.0,
152+
"active_opacity": 1.0,
153+
"inactive_opacity": 0.5,
154+
"shadow_offset": 4,
155+
"transition_frames": 4
156+
},
157+
"neon": {
158+
"label": "Neon",
159+
"description": "Active word glows with a neon outline",
160+
"active_scale": 1.02,
161+
"inactive_scale": 1.0,
162+
"active_opacity": 1.0,
163+
"inactive_opacity": 0.3,
164+
"glow_radius": 12,
165+
"glow_color": [0, 255, 200],
166+
"transition_frames": 5
167+
},
168+
"stagger": {
169+
"label": "Stagger",
170+
"description": "Words reveal with staggered letter timing",
171+
"active_scale": 1.0,
172+
"inactive_scale": 1.0,
173+
"active_opacity": 1.0,
174+
"inactive_opacity": 0.0,
175+
"stagger_letters": true,
176+
"transition_frames": 3
177+
},
178+
"flash": {
179+
"label": "Flash",
180+
"description": "Words flash bright on activation",
181+
"active_scale": 1.0,
182+
"inactive_scale": 1.0,
183+
"active_opacity": 1.0,
184+
"inactive_opacity": 0.4,
185+
"flash_frames": 3,
186+
"transition_frames": 2
187+
},
188+
"karaoke": {
189+
"label": "Karaoke",
190+
"description": "Left-to-right color fill like karaoke displays",
191+
"active_scale": 1.0,
192+
"inactive_scale": 1.0,
193+
"active_opacity": 1.0,
194+
"inactive_opacity": 0.6,
195+
"fill_direction": "left_to_right",
196+
"active_color": [255, 220, 0],
197+
"transition_frames": 1
198+
},
199+
"elastic": {
200+
"label": "Elastic",
201+
"description": "Words stretch in with elastic overshoot",
202+
"active_scale": 1.0,
203+
"inactive_scale": 0.6,
204+
"active_opacity": 1.0,
205+
"inactive_opacity": 0.0,
206+
"overshoot": 1.15,
207+
"transition_frames": 8
208+
}
209+
}

0 commit comments

Comments
 (0)