feat: Enhance video editing effects with real-time controls and fullscreen support
- Added ripple effect with adjustable intensity and frequency, including time controls for start and end. - Implemented fade effect with time-based controls for smoother transitions. - Introduced text overlay functionality with time controls for displaying text during specific intervals. - Created a fullscreen mode for video playback, allowing users to toggle fullscreen and exit with ESC or F11. - Added keyboard shortcuts for playback control, frame navigation, and seeking. - Improved UI with clear buttons for managing effects and enhanced interaction for clip resizing and moving. - Updated effect application methods to support real-time preview during editing.
This commit is contained in:
parent
c9fc7c8d80
commit
8f64345335
788
video_editor.py
788
video_editor.py
@ -126,6 +126,27 @@ class ShortsEditorGUI:
|
|||||||
self.is_playing = False # Whether video is currently playing
|
self.is_playing = False # Whether video is currently playing
|
||||||
self.timeline_is_playing = False # Whether timeline playback is active
|
self.timeline_is_playing = False # Whether timeline playback is active
|
||||||
self.play_thread = None # Background thread for video playback
|
self.play_thread = None # Background thread for video playback
|
||||||
|
self.current_frame = None # Currently displayed frame (for fullscreen sharing)
|
||||||
|
|
||||||
|
# Real-time effect state management with time controls
|
||||||
|
# These variables control which effects are currently active and their timing
|
||||||
|
self.effects_enabled = {
|
||||||
|
'ripple': False, # Whether ripple effect is currently active
|
||||||
|
'fade': False, # Whether fade effect is currently active
|
||||||
|
'text': False, # Whether text overlay is currently active
|
||||||
|
'blur': False, # Whether blur effect is currently active
|
||||||
|
'sepia': False, # Whether sepia effect is currently active
|
||||||
|
'invert': False # Whether color invert effect is currently active
|
||||||
|
}
|
||||||
|
|
||||||
|
# Time-based effect controls - when effects should be active
|
||||||
|
self.effect_times = {
|
||||||
|
'ripple': {'start': 0.0, 'end': 0.0},
|
||||||
|
'fade': {'start': 0.0, 'end': 0.0},
|
||||||
|
'text': {'start': 0.0, 'end': 0.0}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.effect_start_time = 0.0 # When current effect animation started
|
||||||
|
|
||||||
# Timeline display and interaction state
|
# Timeline display and interaction state
|
||||||
# Controls how the timeline is rendered and how users interact with it
|
# Controls how the timeline is rendered and how users interact with it
|
||||||
@ -353,6 +374,20 @@ class ShortsEditorGUI:
|
|||||||
self.video_canvas = tk.Canvas(video_container, bg='black', highlightthickness=0)
|
self.video_canvas = tk.Canvas(video_container, bg='black', highlightthickness=0)
|
||||||
self.video_canvas.grid(row=0, column=0, sticky="nsew")
|
self.video_canvas.grid(row=0, column=0, sticky="nsew")
|
||||||
|
|
||||||
|
# Fullscreen button overlay - positioned over video canvas
|
||||||
|
# This button allows users to view video in fullscreen mode
|
||||||
|
self.fullscreen_btn = tk.Button(video_container, text="⛶",
|
||||||
|
command=self.toggle_fullscreen,
|
||||||
|
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'],
|
||||||
|
font=('Arial', 14, 'bold'), width=2, height=1,
|
||||||
|
relief="flat", bd=1, cursor="hand2",
|
||||||
|
activebackground=self.colors['accent_blue'])
|
||||||
|
self.fullscreen_btn.place(x=10, y=10) # Position in top-left corner of video
|
||||||
|
|
||||||
|
# Initialize fullscreen state
|
||||||
|
self.is_fullscreen = False
|
||||||
|
self.fullscreen_window = None
|
||||||
|
|
||||||
# Professional Timeline Workspace - Multi-track editing environment
|
# Professional Timeline Workspace - Multi-track editing environment
|
||||||
# Fixed height prevents timeline from shrinking when window is resized
|
# Fixed height prevents timeline from shrinking when window is resized
|
||||||
# This is where users perform the majority of their editing work
|
# This is where users perform the majority of their editing work
|
||||||
@ -369,22 +404,6 @@ class ShortsEditorGUI:
|
|||||||
left_controls = tk.Frame(header_frame, bg=self.colors['bg_secondary'])
|
left_controls = tk.Frame(header_frame, bg=self.colors['bg_secondary'])
|
||||||
left_controls.pack(side="left")
|
left_controls.pack(side="left")
|
||||||
|
|
||||||
# Editing mode selector - determines how mouse interactions behave
|
|
||||||
# Different modes enable different types of editing operations
|
|
||||||
tk.Label(left_controls, text="Mode:", font=self.fonts['caption'],
|
|
||||||
bg=self.colors['bg_secondary'], fg=self.colors['text_secondary']).pack(side="left")
|
|
||||||
|
|
||||||
# Mode selection dropdown with professional editing modes
|
|
||||||
# select: Default mode for selecting and moving clips
|
|
||||||
# cut: Razor tool for splitting clips at specific points
|
|
||||||
# trim: For adjusting clip start/end points
|
|
||||||
# ripple: Moves clips and automatically adjusts following clips
|
|
||||||
self.mode_var = tk.StringVar(value="select")
|
|
||||||
mode_combo = ttk.Combobox(left_controls, textvariable=self.mode_var, width=8,
|
|
||||||
values=["select", "cut", "trim", "ripple"], state="readonly")
|
|
||||||
mode_combo.pack(side="left", padx=(5, 10))
|
|
||||||
mode_combo.bind('<<ComboboxSelected>>', self.on_mode_change)
|
|
||||||
|
|
||||||
# Professional timeline assistance features
|
# Professional timeline assistance features
|
||||||
# These options help users align and position clips precisely
|
# These options help users align and position clips precisely
|
||||||
self.snap_var = tk.BooleanVar(value=True)
|
self.snap_var = tk.BooleanVar(value=True)
|
||||||
@ -481,7 +500,7 @@ class ShortsEditorGUI:
|
|||||||
|
|
||||||
# Professional tips label
|
# Professional tips label
|
||||||
tips_label = tk.Label(external_scrollbar_frame,
|
tips_label = tk.Label(external_scrollbar_frame,
|
||||||
text="💡 CTRL+Click: Select clips • Delete/Backspace: Delete selected • Click: Move playhead",
|
text="💡 CTRL+Click: Select/Deselect clips • Delete: Remove selected • Drag orange edges: Resize • Playhead disabled when clip selected",
|
||||||
bg=self.colors['bg_primary'], fg=self.colors['text_secondary'],
|
bg=self.colors['bg_primary'], fg=self.colors['text_secondary'],
|
||||||
font=('Segoe UI', 8))
|
font=('Segoe UI', 8))
|
||||||
tips_label.pack(side="left", padx=5, pady=2)
|
tips_label.pack(side="left", padx=5, pady=2)
|
||||||
@ -599,6 +618,116 @@ class ShortsEditorGUI:
|
|||||||
self.tools_notebook.add(export_frame, text="Export")
|
self.tools_notebook.add(export_frame, text="Export")
|
||||||
self.create_export_tools(export_frame)
|
self.create_export_tools(export_frame)
|
||||||
|
|
||||||
|
# Setup keyboard controls for play/pause and other shortcuts
|
||||||
|
self.setup_keyboard_controls()
|
||||||
|
|
||||||
|
def setup_keyboard_controls(self):
|
||||||
|
"""Setup keyboard shortcuts for video playback and editing"""
|
||||||
|
# Make the main window focusable for keyboard events
|
||||||
|
self.editor_window.focus_set()
|
||||||
|
|
||||||
|
# Bind keyboard shortcuts
|
||||||
|
# Spacebar: Play/Pause toggle
|
||||||
|
self.editor_window.bind('<space>', lambda e: self.toggle_play_pause())
|
||||||
|
self.editor_window.bind('<Key-space>', lambda e: self.toggle_play_pause())
|
||||||
|
|
||||||
|
# Left/Right arrows: Frame by frame navigation
|
||||||
|
self.editor_window.bind('<Left>', lambda e: self.seek_backward())
|
||||||
|
self.editor_window.bind('<Right>', lambda e: self.seek_forward())
|
||||||
|
|
||||||
|
# Shift + Left/Right: Jump by seconds
|
||||||
|
self.editor_window.bind('<Shift-Left>', lambda e: self.seek_backward(1.0))
|
||||||
|
self.editor_window.bind('<Shift-Right>', lambda e: self.seek_forward(1.0))
|
||||||
|
|
||||||
|
# Home/End: Go to beginning/end
|
||||||
|
self.editor_window.bind('<Home>', lambda e: self.seek_to_start())
|
||||||
|
self.editor_window.bind('<End>', lambda e: self.seek_to_end())
|
||||||
|
|
||||||
|
# F11: Toggle fullscreen
|
||||||
|
self.editor_window.bind('<F11>', lambda e: self.toggle_fullscreen())
|
||||||
|
self.editor_window.bind('<KeyPress-F11>', lambda e: self.toggle_fullscreen())
|
||||||
|
|
||||||
|
# ESC: Exit fullscreen (when not in fullscreen window)
|
||||||
|
self.editor_window.bind('<Escape>', lambda e: self.handle_escape())
|
||||||
|
|
||||||
|
print("⌨️ Keyboard controls enabled:")
|
||||||
|
print(" SPACE: Play/Pause")
|
||||||
|
print(" ←/→: Frame by frame")
|
||||||
|
print(" Shift+←/→: Skip by seconds")
|
||||||
|
print(" Home/End: Go to start/end")
|
||||||
|
print(" F11: Toggle fullscreen")
|
||||||
|
print(" ESC: Exit fullscreen")
|
||||||
|
|
||||||
|
def toggle_play_pause(self):
|
||||||
|
"""Toggle between play and pause"""
|
||||||
|
if not self.current_clip:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.is_playing:
|
||||||
|
self.timeline_pause()
|
||||||
|
print("⏸️ Paused (SPACE)")
|
||||||
|
else:
|
||||||
|
self.timeline_play()
|
||||||
|
print("▶️ Playing (SPACE)")
|
||||||
|
|
||||||
|
def seek_backward(self, seconds=None):
|
||||||
|
"""Seek backward by frame or specified seconds"""
|
||||||
|
if not self.current_clip:
|
||||||
|
return
|
||||||
|
|
||||||
|
if seconds is None:
|
||||||
|
# Frame by frame (1/30th of a second)
|
||||||
|
seconds = 1/30
|
||||||
|
|
||||||
|
new_time = max(0, self.current_time - seconds)
|
||||||
|
self.current_time = new_time
|
||||||
|
self.display_frame_at_time(new_time)
|
||||||
|
self.update_time_display()
|
||||||
|
self.update_timeline()
|
||||||
|
|
||||||
|
def seek_forward(self, seconds=None):
|
||||||
|
"""Seek forward by frame or specified seconds"""
|
||||||
|
if not self.current_clip:
|
||||||
|
return
|
||||||
|
|
||||||
|
if seconds is None:
|
||||||
|
# Frame by frame (1/30th of a second)
|
||||||
|
seconds = 1/30
|
||||||
|
|
||||||
|
new_time = min(self.video_duration, self.current_time + seconds)
|
||||||
|
self.current_time = new_time
|
||||||
|
self.display_frame_at_time(new_time)
|
||||||
|
self.update_time_display()
|
||||||
|
self.update_timeline()
|
||||||
|
|
||||||
|
def seek_to_start(self):
|
||||||
|
"""Jump to the beginning of the video"""
|
||||||
|
if not self.current_clip:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.current_time = 0.0
|
||||||
|
self.display_frame_at_time(0.0)
|
||||||
|
self.update_time_display()
|
||||||
|
self.update_timeline()
|
||||||
|
print("⏮️ Jumped to start (HOME)")
|
||||||
|
|
||||||
|
def seek_to_end(self):
|
||||||
|
"""Jump to the end of the video"""
|
||||||
|
if not self.current_clip:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.current_time = self.video_duration
|
||||||
|
self.display_frame_at_time(self.video_duration)
|
||||||
|
self.update_time_display()
|
||||||
|
self.update_timeline()
|
||||||
|
print("⏭️ Jumped to end (END)")
|
||||||
|
|
||||||
|
def handle_escape(self):
|
||||||
|
"""Handle ESC key press"""
|
||||||
|
if self.is_fullscreen:
|
||||||
|
self.exit_fullscreen()
|
||||||
|
# Could add other ESC behaviors here (like deselecting clips)
|
||||||
|
|
||||||
def create_track_controls(self):
|
def create_track_controls(self):
|
||||||
"""Create professional track control panel with road lines"""
|
"""Create professional track control panel with road lines"""
|
||||||
# Clear existing track controls
|
# Clear existing track controls
|
||||||
@ -1262,6 +1391,68 @@ class ShortsEditorGUI:
|
|||||||
font=self.fonts['caption'], relief="flat", bd=0, cursor="hand2")
|
font=self.fonts['caption'], relief="flat", bd=0, cursor="hand2")
|
||||||
btn.pack(fill="x", pady=2)
|
btn.pack(fill="x", pady=2)
|
||||||
|
|
||||||
|
# Ripple Effect controls
|
||||||
|
ripple_frame = tk.LabelFrame(scrollable_frame, text="🌊 Ripple Effect", font=self.fonts['body'],
|
||||||
|
bg=self.colors['bg_secondary'], fg=self.colors['text_primary'],
|
||||||
|
relief="flat", bd=1)
|
||||||
|
ripple_frame.pack(fill="x", padx=10, pady=5)
|
||||||
|
|
||||||
|
# Time controls for ripple effect
|
||||||
|
time_frame = tk.Frame(ripple_frame, bg=self.colors['bg_secondary'])
|
||||||
|
time_frame.pack(fill="x", pady=5)
|
||||||
|
|
||||||
|
# Start time
|
||||||
|
start_frame = tk.Frame(time_frame, bg=self.colors['bg_secondary'])
|
||||||
|
start_frame.pack(side="left", fill="x", expand=True, padx=5)
|
||||||
|
tk.Label(start_frame, text="Start Time (s):", font=self.fonts['caption'],
|
||||||
|
bg=self.colors['bg_secondary'], fg=self.colors['text_secondary']).pack(anchor="w")
|
||||||
|
self.ripple_start_var = tk.DoubleVar(value=0.0)
|
||||||
|
start_entry = tk.Entry(start_frame, textvariable=self.ripple_start_var, width=8,
|
||||||
|
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'],
|
||||||
|
font=self.fonts['body'], relief="flat", bd=1)
|
||||||
|
start_entry.pack(anchor="w")
|
||||||
|
|
||||||
|
# End time
|
||||||
|
end_frame = tk.Frame(time_frame, bg=self.colors['bg_secondary'])
|
||||||
|
end_frame.pack(side="left", fill="x", expand=True, padx=5)
|
||||||
|
tk.Label(end_frame, text="End Time (s):", font=self.fonts['caption'],
|
||||||
|
bg=self.colors['bg_secondary'], fg=self.colors['text_secondary']).pack(anchor="w")
|
||||||
|
self.ripple_end_var = tk.DoubleVar(value=5.0)
|
||||||
|
end_entry = tk.Entry(end_frame, textvariable=self.ripple_end_var, width=8,
|
||||||
|
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'],
|
||||||
|
font=self.fonts['body'], relief="flat", bd=1)
|
||||||
|
end_entry.pack(anchor="w")
|
||||||
|
|
||||||
|
# Ripple intensity control
|
||||||
|
ripple_controls = tk.Frame(ripple_frame, bg=self.colors['bg_secondary'])
|
||||||
|
ripple_controls.pack(fill="x", pady=5)
|
||||||
|
|
||||||
|
tk.Label(ripple_controls, text="Intensity:", font=self.fonts['caption'],
|
||||||
|
bg=self.colors['bg_secondary'], fg=self.colors['text_secondary']).pack(anchor="w", padx=10)
|
||||||
|
|
||||||
|
self.ripple_intensity_var = tk.DoubleVar(value=0.5)
|
||||||
|
ripple_scale = tk.Scale(ripple_frame, from_=0.1, to=2.0, resolution=0.1,
|
||||||
|
orient="horizontal", variable=self.ripple_intensity_var,
|
||||||
|
bg=self.colors['bg_secondary'], fg=self.colors['text_primary'],
|
||||||
|
highlightthickness=0, troughcolor=self.colors['bg_tertiary'])
|
||||||
|
ripple_scale.pack(fill="x", pady=5, padx=10)
|
||||||
|
|
||||||
|
# Ripple frequency control
|
||||||
|
tk.Label(ripple_frame, text="Frequency:", font=self.fonts['caption'],
|
||||||
|
bg=self.colors['bg_secondary'], fg=self.colors['text_secondary']).pack(anchor="w", padx=10)
|
||||||
|
|
||||||
|
self.ripple_frequency_var = tk.DoubleVar(value=10.0)
|
||||||
|
frequency_scale = tk.Scale(ripple_frame, from_=1.0, to=50.0, resolution=1.0,
|
||||||
|
orient="horizontal", variable=self.ripple_frequency_var,
|
||||||
|
bg=self.colors['bg_secondary'], fg=self.colors['text_primary'],
|
||||||
|
highlightthickness=0, troughcolor=self.colors['bg_tertiary'])
|
||||||
|
frequency_scale.pack(fill="x", pady=5, padx=10)
|
||||||
|
|
||||||
|
ripple_btn = tk.Button(ripple_frame, text="🌊 Apply Ripple Effect", command=self.apply_ripple_effect,
|
||||||
|
bg=self.colors['accent_blue'], fg='white', font=self.fonts['button'],
|
||||||
|
relief="flat", bd=0, cursor="hand2")
|
||||||
|
ripple_btn.pack(fill="x", padx=10, pady=5)
|
||||||
|
|
||||||
def create_video_effects_tools(self, parent):
|
def create_video_effects_tools(self, parent):
|
||||||
"""Create video effects tools"""
|
"""Create video effects tools"""
|
||||||
# Create scrollable frame for video effects
|
# Create scrollable frame for video effects
|
||||||
@ -1286,7 +1477,33 @@ class ShortsEditorGUI:
|
|||||||
relief="flat", bd=1)
|
relief="flat", bd=1)
|
||||||
fade_frame.pack(fill="x", padx=10, pady=5)
|
fade_frame.pack(fill="x", padx=10, pady=5)
|
||||||
|
|
||||||
fade_btn = tk.Button(fade_frame, text="🌅 Add Fade In/Out", command=self.apply_fade,
|
# Time controls for fade effect
|
||||||
|
fade_time_frame = tk.Frame(fade_frame, bg=self.colors['bg_secondary'])
|
||||||
|
fade_time_frame.pack(fill="x", pady=5)
|
||||||
|
|
||||||
|
# Start time
|
||||||
|
fade_start_frame = tk.Frame(fade_time_frame, bg=self.colors['bg_secondary'])
|
||||||
|
fade_start_frame.pack(side="left", fill="x", expand=True, padx=5)
|
||||||
|
tk.Label(fade_start_frame, text="Start Time (s):", font=self.fonts['caption'],
|
||||||
|
bg=self.colors['bg_secondary'], fg=self.colors['text_secondary']).pack(anchor="w")
|
||||||
|
self.fade_start_var = tk.DoubleVar(value=0.0)
|
||||||
|
fade_start_entry = tk.Entry(fade_start_frame, textvariable=self.fade_start_var, width=8,
|
||||||
|
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'],
|
||||||
|
font=self.fonts['body'], relief="flat", bd=1)
|
||||||
|
fade_start_entry.pack(anchor="w")
|
||||||
|
|
||||||
|
# End time
|
||||||
|
fade_end_frame = tk.Frame(fade_time_frame, bg=self.colors['bg_secondary'])
|
||||||
|
fade_end_frame.pack(side="left", fill="x", expand=True, padx=5)
|
||||||
|
tk.Label(fade_end_frame, text="End Time (s):", font=self.fonts['caption'],
|
||||||
|
bg=self.colors['bg_secondary'], fg=self.colors['text_secondary']).pack(anchor="w")
|
||||||
|
self.fade_end_var = tk.DoubleVar(value=2.0)
|
||||||
|
fade_end_entry = tk.Entry(fade_end_frame, textvariable=self.fade_end_var, width=8,
|
||||||
|
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'],
|
||||||
|
font=self.fonts['body'], relief="flat", bd=1)
|
||||||
|
fade_end_entry.pack(anchor="w")
|
||||||
|
|
||||||
|
fade_btn = tk.Button(fade_frame, text="🌅 Apply Fade Effect", command=self.apply_fade,
|
||||||
bg=self.colors['accent_blue'], fg='white', font=self.fonts['button'],
|
bg=self.colors['accent_blue'], fg='white', font=self.fonts['button'],
|
||||||
relief="flat", bd=0, cursor="hand2")
|
relief="flat", bd=0, cursor="hand2")
|
||||||
fade_btn.pack(fill="x", padx=10, pady=5)
|
fade_btn.pack(fill="x", padx=10, pady=5)
|
||||||
@ -1308,11 +1525,46 @@ class ShortsEditorGUI:
|
|||||||
width=25)
|
width=25)
|
||||||
text_entry.pack(fill="x", pady=5)
|
text_entry.pack(fill="x", pady=5)
|
||||||
|
|
||||||
text_btn = tk.Button(text_frame, text="📝 Add Text", command=self.apply_text,
|
# Time controls for text effect
|
||||||
|
text_time_frame = tk.Frame(text_frame, bg=self.colors['bg_secondary'])
|
||||||
|
text_time_frame.pack(fill="x", pady=5)
|
||||||
|
|
||||||
|
# Start time
|
||||||
|
text_start_frame = tk.Frame(text_time_frame, bg=self.colors['bg_secondary'])
|
||||||
|
text_start_frame.pack(side="left", fill="x", expand=True, padx=5)
|
||||||
|
tk.Label(text_start_frame, text="Start Time (s):", font=self.fonts['caption'],
|
||||||
|
bg=self.colors['bg_secondary'], fg=self.colors['text_secondary']).pack(anchor="w")
|
||||||
|
self.text_start_var = tk.DoubleVar(value=1.0)
|
||||||
|
text_start_entry = tk.Entry(text_start_frame, textvariable=self.text_start_var, width=8,
|
||||||
|
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'],
|
||||||
|
font=self.fonts['body'], relief="flat", bd=1)
|
||||||
|
text_start_entry.pack(anchor="w")
|
||||||
|
|
||||||
|
# End time
|
||||||
|
text_end_frame = tk.Frame(text_time_frame, bg=self.colors['bg_secondary'])
|
||||||
|
text_end_frame.pack(side="left", fill="x", expand=True, padx=5)
|
||||||
|
tk.Label(text_end_frame, text="End Time (s):", font=self.fonts['caption'],
|
||||||
|
bg=self.colors['bg_secondary'], fg=self.colors['text_secondary']).pack(anchor="w")
|
||||||
|
self.text_end_var = tk.DoubleVar(value=4.0)
|
||||||
|
text_end_entry = tk.Entry(text_end_frame, textvariable=self.text_end_var, width=8,
|
||||||
|
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'],
|
||||||
|
font=self.fonts['body'], relief="flat", bd=1)
|
||||||
|
text_end_entry.pack(anchor="w")
|
||||||
|
|
||||||
|
text_btn = tk.Button(text_frame, text="📝 Apply Text", command=self.apply_text,
|
||||||
bg=self.colors['accent_green'], fg='white', font=self.fonts['button'],
|
bg=self.colors['accent_green'], fg='white', font=self.fonts['button'],
|
||||||
relief="flat", bd=0, cursor="hand2")
|
relief="flat", bd=0, cursor="hand2")
|
||||||
text_btn.pack(fill="x", padx=10, pady=5)
|
text_btn.pack(fill="x", padx=10, pady=5)
|
||||||
|
|
||||||
|
# Clear all effects button
|
||||||
|
clear_frame = tk.Frame(scrollable_effects_frame, bg=self.colors['bg_secondary'])
|
||||||
|
clear_frame.pack(fill="x", padx=10, pady=10)
|
||||||
|
|
||||||
|
clear_btn = tk.Button(clear_frame, text="🗑️ Clear All Effects", command=self.clear_all_effects,
|
||||||
|
bg=self.colors['accent_red'], fg='white', font=self.fonts['button'],
|
||||||
|
relief="flat", bd=0, cursor="hand2")
|
||||||
|
clear_btn.pack(fill="x")
|
||||||
|
|
||||||
def create_audio_effects_tools(self, parent):
|
def create_audio_effects_tools(self, parent):
|
||||||
"""Create audio effects tools"""
|
"""Create audio effects tools"""
|
||||||
# Create scrollable frame for audio effects
|
# Create scrollable frame for audio effects
|
||||||
@ -1536,6 +1788,12 @@ class ShortsEditorGUI:
|
|||||||
if not ret:
|
if not ret:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Apply real-time effects to the frame
|
||||||
|
frame = self.apply_effects_to_frame(frame)
|
||||||
|
|
||||||
|
# Store current frame for fullscreen use (prevents video file conflicts)
|
||||||
|
self.current_frame = frame.copy()
|
||||||
|
|
||||||
# Convert BGR to RGB
|
# Convert BGR to RGB
|
||||||
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||||
|
|
||||||
@ -1582,6 +1840,10 @@ class ShortsEditorGUI:
|
|||||||
else:
|
else:
|
||||||
self.display_frame_at_time_opencv(time_sec)
|
self.display_frame_at_time_opencv(time_sec)
|
||||||
|
|
||||||
|
# Update fullscreen display if active
|
||||||
|
if self.is_fullscreen and self.fullscreen_canvas:
|
||||||
|
self.display_frame_fullscreen()
|
||||||
|
|
||||||
def display_frame_at_time_moviepy(self, time_sec):
|
def display_frame_at_time_moviepy(self, time_sec):
|
||||||
"""Display a specific frame using MoviePy"""
|
"""Display a specific frame using MoviePy"""
|
||||||
try:
|
try:
|
||||||
@ -1747,19 +2009,20 @@ class ShortsEditorGUI:
|
|||||||
fill=clip_color, outline=outline_color, width=outline_width,
|
fill=clip_color, outline=outline_color, width=outline_width,
|
||||||
tags=f"clip_{i}")
|
tags=f"clip_{i}")
|
||||||
|
|
||||||
# Add resize handles (small rectangles on the edges)
|
# Add resize handles (small rectangles on the edges) - smaller for precision
|
||||||
handle_size = 8
|
handle_size = 8 if is_selected else 6 # Bigger when selected for easier clicking
|
||||||
handle_color = '#FFD700' if is_selected else 'white'
|
handle_color = '#FFD700' if is_selected else '#999999' # Gold when selected, gray when not
|
||||||
|
handle_outline = '#FF6B35' if is_selected else 'gray' # Orange outline when selected
|
||||||
|
|
||||||
# Left resize handle
|
# Left resize handle
|
||||||
left_handle = self.timeline_canvas.create_rectangle(
|
left_handle = self.timeline_canvas.create_rectangle(
|
||||||
clip_x, clip_y, clip_x + handle_size, clip_y + clip_height,
|
clip_x, clip_y, clip_x + handle_size, clip_y + clip_height,
|
||||||
fill=handle_color, outline='gray', width=1, tags=f"resize_left_{i}")
|
fill=handle_color, outline=handle_outline, width=2 if is_selected else 1, tags=f"resize_left_{i}")
|
||||||
|
|
||||||
# Right resize handle
|
# Right resize handle
|
||||||
right_handle = self.timeline_canvas.create_rectangle(
|
right_handle = self.timeline_canvas.create_rectangle(
|
||||||
clip_x + clip_width - handle_size, clip_y, clip_x + clip_width, clip_y + clip_height,
|
clip_x + clip_width - handle_size, clip_y, clip_x + clip_width, clip_y + clip_height,
|
||||||
fill=handle_color, outline='gray', width=1, tags=f"resize_right_{i}")
|
fill=handle_color, outline=handle_outline, width=2 if is_selected else 1, tags=f"resize_right_{i}")
|
||||||
|
|
||||||
# Make clip draggable and resizable
|
# Make clip draggable and resizable
|
||||||
self.setup_clip_interaction(clip_rect, left_handle, right_handle, i)
|
self.setup_clip_interaction(clip_rect, left_handle, right_handle, i)
|
||||||
@ -1777,24 +2040,44 @@ class ShortsEditorGUI:
|
|||||||
"""Setup interaction for clip resizing and moving"""
|
"""Setup interaction for clip resizing and moving"""
|
||||||
|
|
||||||
def start_resize_left(event):
|
def start_resize_left(event):
|
||||||
|
# Resize handles should always work, regardless of CTRL state
|
||||||
self.resizing_clip = {'index': clip_index, 'side': 'left', 'start_x': event.x}
|
self.resizing_clip = {'index': clip_index, 'side': 'left', 'start_x': event.x}
|
||||||
self.timeline_canvas.configure(cursor="sb_h_double_arrow")
|
self.timeline_canvas.configure(cursor="sb_h_double_arrow")
|
||||||
|
print(f"🔧 Started resizing left edge of clip {clip_index}")
|
||||||
|
# Stop event propagation to prevent selection
|
||||||
|
return "break"
|
||||||
|
|
||||||
def start_resize_right(event):
|
def start_resize_right(event):
|
||||||
|
# Resize handles should always work, regardless of CTRL state
|
||||||
self.resizing_clip = {'index': clip_index, 'side': 'right', 'start_x': event.x}
|
self.resizing_clip = {'index': clip_index, 'side': 'right', 'start_x': event.x}
|
||||||
self.timeline_canvas.configure(cursor="sb_h_double_arrow")
|
self.timeline_canvas.configure(cursor="sb_h_double_arrow")
|
||||||
|
print(f"🔧 Started resizing right edge of clip {clip_index}")
|
||||||
|
# Stop event propagation to prevent selection
|
||||||
|
return "break"
|
||||||
|
|
||||||
def start_move_clip(event):
|
def start_move_clip(event):
|
||||||
|
# Only start moving if not a CTRL+click (which is for selection)
|
||||||
|
if not (event.state & 0x4): # Not CTRL key
|
||||||
self.moving_clip = {'index': clip_index, 'start_x': event.x}
|
self.moving_clip = {'index': clip_index, 'start_x': event.x}
|
||||||
self.timeline_canvas.configure(cursor="hand2")
|
self.timeline_canvas.configure(cursor="hand2")
|
||||||
|
print(f"🔄 Started moving clip {clip_index}")
|
||||||
|
else:
|
||||||
|
print(f"🎯 CTRL+click detected - selection handled by timeline_click")
|
||||||
|
|
||||||
def on_resize_drag(event):
|
def on_resize_drag(event):
|
||||||
if hasattr(self, 'resizing_clip') and self.resizing_clip:
|
if hasattr(self, 'resizing_clip') and self.resizing_clip:
|
||||||
|
# Always handle resizing immediately - no threshold needed for resize handles
|
||||||
self.handle_clip_resize(event)
|
self.handle_clip_resize(event)
|
||||||
elif hasattr(self, 'moving_clip') and self.moving_clip:
|
elif hasattr(self, 'moving_clip') and self.moving_clip:
|
||||||
|
# Only move if we've moved at least 5 pixels to avoid accidental moves
|
||||||
|
dx = abs(event.x - self.moving_clip['start_x'])
|
||||||
|
if dx > 5:
|
||||||
self.handle_clip_move(event)
|
self.handle_clip_move(event)
|
||||||
|
|
||||||
def end_interaction(event):
|
def end_interaction(event):
|
||||||
|
was_resizing = hasattr(self, 'resizing_clip') and self.resizing_clip
|
||||||
|
was_moving = hasattr(self, 'moving_clip') and self.moving_clip
|
||||||
|
|
||||||
if hasattr(self, 'resizing_clip'):
|
if hasattr(self, 'resizing_clip'):
|
||||||
self.resizing_clip = None
|
self.resizing_clip = None
|
||||||
if hasattr(self, 'moving_clip'):
|
if hasattr(self, 'moving_clip'):
|
||||||
@ -1805,6 +2088,8 @@ class ShortsEditorGUI:
|
|||||||
if hasattr(self, 'selected_timeline_clip') and self.selected_timeline_clip == clip_index:
|
if hasattr(self, 'selected_timeline_clip') and self.selected_timeline_clip == clip_index:
|
||||||
print(f"🎯 Clip {clip_index} modified and still selected")
|
print(f"🎯 Clip {clip_index} modified and still selected")
|
||||||
|
|
||||||
|
# Only update timeline if we weren't resizing (resize already updates continuously)
|
||||||
|
if not was_resizing:
|
||||||
self.update_timeline()
|
self.update_timeline()
|
||||||
|
|
||||||
# Bind resize handles
|
# Bind resize handles
|
||||||
@ -1854,8 +2139,13 @@ class ShortsEditorGUI:
|
|||||||
if new_duration >= 0.5: # Minimum 0.5 second duration
|
if new_duration >= 0.5: # Minimum 0.5 second duration
|
||||||
clip['duration'] = new_duration
|
clip['duration'] = new_duration
|
||||||
|
|
||||||
|
# Update the start_x for continuous resizing
|
||||||
self.resizing_clip['start_x'] = event.x
|
self.resizing_clip['start_x'] = event.x
|
||||||
|
|
||||||
|
# Update the timeline to show the changes
|
||||||
|
self.update_timeline()
|
||||||
|
print(f"🔧 Resized clip {clip_index}: start={clip['start_time']:.2f}s, duration={clip['duration']:.2f}s")
|
||||||
|
|
||||||
def handle_clip_move(self, event):
|
def handle_clip_move(self, event):
|
||||||
"""Handle clip moving"""
|
"""Handle clip moving"""
|
||||||
if not self.moving_clip or not hasattr(self, 'timeline_clips'):
|
if not self.moving_clip or not hasattr(self, 'timeline_clips'):
|
||||||
@ -1981,7 +2271,12 @@ class ShortsEditorGUI:
|
|||||||
# CTRL+Click: Select clip without moving playhead
|
# CTRL+Click: Select clip without moving playhead
|
||||||
self.select_clip_at_position(click_x, click_y)
|
self.select_clip_at_position(click_x, click_y)
|
||||||
else:
|
else:
|
||||||
# Regular click: Move playhead (existing behavior)
|
# Check if a clip is currently selected
|
||||||
|
if hasattr(self, 'selected_timeline_clip') and self.selected_timeline_clip is not None:
|
||||||
|
print("🚫 Playhead movement disabled - clip is selected. Use CTRL+Click empty area to deselect.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Regular click: Move playhead (only if no clip is selected)
|
||||||
if not self.current_clip:
|
if not self.current_clip:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -1996,6 +2291,7 @@ class ShortsEditorGUI:
|
|||||||
self.display_frame_at_time(self.current_time)
|
self.display_frame_at_time(self.current_time)
|
||||||
self.update_timeline()
|
self.update_timeline()
|
||||||
self.update_time_display()
|
self.update_time_display()
|
||||||
|
print(f"⏭️ Playhead moved to {self.current_time:.2f}s")
|
||||||
|
|
||||||
def select_clip_at_position(self, click_x, click_y):
|
def select_clip_at_position(self, click_x, click_y):
|
||||||
"""Select clip at the clicked position without moving playhead"""
|
"""Select clip at the clicked position without moving playhead"""
|
||||||
@ -2160,6 +2456,142 @@ class ShortsEditorGUI:
|
|||||||
self.update_timeline()
|
self.update_timeline()
|
||||||
self.update_time_display()
|
self.update_time_display()
|
||||||
|
|
||||||
|
def toggle_fullscreen(self):
|
||||||
|
"""Toggle fullscreen mode for video player"""
|
||||||
|
if not self.current_clip:
|
||||||
|
messagebox.showwarning("No Video", "Please load a video first.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.is_fullscreen:
|
||||||
|
# Enter fullscreen mode
|
||||||
|
self.enter_fullscreen()
|
||||||
|
else:
|
||||||
|
# Exit fullscreen mode
|
||||||
|
self.exit_fullscreen()
|
||||||
|
|
||||||
|
def enter_fullscreen(self):
|
||||||
|
"""Enter fullscreen video mode"""
|
||||||
|
self.is_fullscreen = True
|
||||||
|
|
||||||
|
# Create fullscreen window
|
||||||
|
self.fullscreen_window = tk.Toplevel(self.editor_window)
|
||||||
|
self.fullscreen_window.title("Video Fullscreen")
|
||||||
|
self.fullscreen_window.configure(bg='black')
|
||||||
|
|
||||||
|
# Make window fullscreen
|
||||||
|
self.fullscreen_window.attributes('-fullscreen', True)
|
||||||
|
self.fullscreen_window.attributes('-topmost', True)
|
||||||
|
|
||||||
|
# Create fullscreen video canvas
|
||||||
|
self.fullscreen_canvas = tk.Canvas(self.fullscreen_window, bg='black', highlightthickness=0)
|
||||||
|
self.fullscreen_canvas.pack(fill="both", expand=True)
|
||||||
|
|
||||||
|
# Add exit button (ESC key or click)
|
||||||
|
exit_btn = tk.Button(self.fullscreen_window, text="✕ Exit (ESC/F11)",
|
||||||
|
command=self.exit_fullscreen,
|
||||||
|
bg='#333333', fg='white',
|
||||||
|
font=('Arial', 12, 'bold'), relief="flat", bd=0, cursor="hand2")
|
||||||
|
exit_btn.place(x=20, y=20)
|
||||||
|
|
||||||
|
# Bind ESC key to exit fullscreen
|
||||||
|
self.fullscreen_window.bind('<Escape>', lambda e: self.exit_fullscreen())
|
||||||
|
self.fullscreen_window.bind('<Button-1>', lambda e: self.exit_fullscreen())
|
||||||
|
|
||||||
|
# Bind keyboard controls for fullscreen mode
|
||||||
|
self.fullscreen_window.bind('<space>', lambda e: self.toggle_play_pause())
|
||||||
|
self.fullscreen_window.bind('<Key-space>', lambda e: self.toggle_play_pause())
|
||||||
|
self.fullscreen_window.bind('<Left>', lambda e: self.seek_backward())
|
||||||
|
self.fullscreen_window.bind('<Right>', lambda e: self.seek_forward())
|
||||||
|
self.fullscreen_window.bind('<Shift-Left>', lambda e: self.seek_backward(1.0))
|
||||||
|
self.fullscreen_window.bind('<Shift-Right>', lambda e: self.seek_forward(1.0))
|
||||||
|
self.fullscreen_window.bind('<Home>', lambda e: self.seek_to_start())
|
||||||
|
self.fullscreen_window.bind('<End>', lambda e: self.seek_to_end())
|
||||||
|
self.fullscreen_window.bind('<F11>', lambda e: self.exit_fullscreen())
|
||||||
|
|
||||||
|
self.fullscreen_window.focus_set()
|
||||||
|
|
||||||
|
# Display current frame in fullscreen
|
||||||
|
self.display_frame_fullscreen()
|
||||||
|
|
||||||
|
# Update fullscreen button text
|
||||||
|
self.fullscreen_btn.config(text="🗗")
|
||||||
|
|
||||||
|
def exit_fullscreen(self):
|
||||||
|
"""Exit fullscreen video mode"""
|
||||||
|
if self.fullscreen_window:
|
||||||
|
self.fullscreen_window.destroy()
|
||||||
|
self.fullscreen_window = None
|
||||||
|
self.fullscreen_canvas = None
|
||||||
|
|
||||||
|
self.is_fullscreen = False
|
||||||
|
# Restore fullscreen button text
|
||||||
|
self.fullscreen_btn.config(text="⛶")
|
||||||
|
|
||||||
|
def display_frame_fullscreen(self):
|
||||||
|
"""Display current video frame in fullscreen canvas"""
|
||||||
|
if not self.fullscreen_canvas or not self.current_clip:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Instead of reading from video file again (which conflicts with playback),
|
||||||
|
# get the frame that's currently displayed in the main canvas
|
||||||
|
if hasattr(self, 'current_frame') and self.current_frame is not None:
|
||||||
|
frame = self.current_frame.copy()
|
||||||
|
else:
|
||||||
|
# Fallback: read frame only if not playing
|
||||||
|
if self.is_playing:
|
||||||
|
return # Don't interfere with playback
|
||||||
|
|
||||||
|
# Calculate frame number for OpenCV
|
||||||
|
fps = self.current_clip.get(cv2.CAP_PROP_FPS)
|
||||||
|
frame_number = int(self.current_time * fps)
|
||||||
|
|
||||||
|
# Set video position
|
||||||
|
self.current_clip.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
|
||||||
|
|
||||||
|
# Read frame
|
||||||
|
ret, frame = self.current_clip.read()
|
||||||
|
if not ret:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Apply effects to frame if any are active
|
||||||
|
frame = self.apply_effects_to_frame(frame)
|
||||||
|
|
||||||
|
# Get fullscreen canvas dimensions
|
||||||
|
self.fullscreen_window.update_idletasks()
|
||||||
|
canvas_width = self.fullscreen_canvas.winfo_width()
|
||||||
|
canvas_height = self.fullscreen_canvas.winfo_height()
|
||||||
|
|
||||||
|
if canvas_width > 1 and canvas_height > 1:
|
||||||
|
# Calculate scaling to fit screen while maintaining aspect ratio
|
||||||
|
frame_height, frame_width = frame.shape[:2]
|
||||||
|
scale_w = canvas_width / frame_width
|
||||||
|
scale_h = canvas_height / frame_height
|
||||||
|
scale = min(scale_w, scale_h)
|
||||||
|
|
||||||
|
new_width = int(frame_width * scale)
|
||||||
|
new_height = int(frame_height * scale)
|
||||||
|
|
||||||
|
# Resize frame
|
||||||
|
resized_frame = cv2.resize(frame, (new_width, new_height))
|
||||||
|
|
||||||
|
# Convert to RGB and create PhotoImage
|
||||||
|
frame_rgb = cv2.cvtColor(resized_frame, cv2.COLOR_BGR2RGB)
|
||||||
|
pil_image = Image.fromarray(frame_rgb)
|
||||||
|
photo = ImageTk.PhotoImage(pil_image)
|
||||||
|
|
||||||
|
# Center the image
|
||||||
|
x = canvas_width // 2
|
||||||
|
y = canvas_height // 2
|
||||||
|
|
||||||
|
# Display image
|
||||||
|
self.fullscreen_canvas.delete("all")
|
||||||
|
self.fullscreen_canvas.create_image(x, y, image=photo)
|
||||||
|
self.fullscreen_canvas.image = photo # Keep reference
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error displaying fullscreen frame: {e}")
|
||||||
|
|
||||||
def _start_timeline_playback(self):
|
def _start_timeline_playback(self):
|
||||||
"""Start the timeline playback loop"""
|
"""Start the timeline playback loop"""
|
||||||
def playback_loop():
|
def playback_loop():
|
||||||
@ -2417,78 +2849,193 @@ class ShortsEditorGUI:
|
|||||||
messagebox.showerror("Volume Error", f"Could not adjust volume: {e}")
|
messagebox.showerror("Volume Error", f"Could not adjust volume: {e}")
|
||||||
|
|
||||||
def apply_fade(self):
|
def apply_fade(self):
|
||||||
"""Apply fade in/out effects"""
|
"""Apply fade effect with time-based controls"""
|
||||||
if not self.current_clip:
|
if not self.current_video:
|
||||||
messagebox.showwarning("No Video", "Please load a video first.")
|
messagebox.showwarning("No Video", "Please load a video first.")
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
# Get time range from UI
|
||||||
if MOVIEPY_AVAILABLE and hasattr(self.current_clip, 'fadein'):
|
start_time = self.fade_start_var.get()
|
||||||
# Use MoviePy for advanced fade effects
|
end_time = self.fade_end_var.get()
|
||||||
fade_duration = min(1.0, self.video_duration / 4)
|
|
||||||
|
|
||||||
if hasattr(self.current_clip, 'fadein') and hasattr(self.current_clip, 'fadeout'):
|
# Validate time range
|
||||||
self.current_clip = self.current_clip.fadein(fade_duration).fadeout(fade_duration)
|
if start_time < 0 or end_time <= start_time or end_time > self.video_duration:
|
||||||
messagebox.showinfo("Success", f"Fade effects applied ({fade_duration:.1f}s)")
|
messagebox.showwarning("Invalid Time Range",
|
||||||
else:
|
f"Please enter valid times between 0 and {self.video_duration:.1f} seconds.\nStart time must be less than end time.")
|
||||||
messagebox.showinfo("Not Available", "Fade effects not available in this MoviePy version")
|
|
||||||
else:
|
|
||||||
# OpenCV mode - export video with fade effect
|
|
||||||
if not self.current_video:
|
|
||||||
messagebox.showwarning("No Video", "No video file loaded.")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
timestamp = datetime.now().strftime("%H%M%S")
|
# Set effect time range
|
||||||
base_name = os.path.splitext(os.path.basename(self.current_video))[0]
|
self.effect_times['fade']['start'] = start_time
|
||||||
output_path = os.path.join(os.path.dirname(self.current_video),
|
self.effect_times['fade']['end'] = end_time
|
||||||
f"{base_name}_faded_{timestamp}.mp4")
|
self.effects_enabled['fade'] = True
|
||||||
|
|
||||||
self.apply_fade_opencv(self.current_video, output_path)
|
messagebox.showinfo("Fade Effect",
|
||||||
self.load_video(output_path)
|
f"Fade effect applied from {start_time:.1f}s to {end_time:.1f}s\nYou can see it in the video preview during this time range.")
|
||||||
messagebox.showinfo("Success", f"Fade effects applied and saved as:\n{os.path.basename(output_path)}")
|
|
||||||
|
|
||||||
except Exception as e:
|
# Refresh current frame to show effect
|
||||||
messagebox.showerror("Fade Error", f"Could not apply fade effects: {e}")
|
self.display_frame_at_time(self.current_time)
|
||||||
|
|
||||||
def apply_fade_opencv(self, input_path, output_path):
|
def apply_ripple_effect(self):
|
||||||
"""Apply fade effects using OpenCV"""
|
"""Apply ripple effect with time-based controls"""
|
||||||
cap = cv2.VideoCapture(input_path)
|
if not self.current_video:
|
||||||
fps = cap.get(cv2.CAP_PROP_FPS)
|
messagebox.showwarning("No Video", "Please load a video first.")
|
||||||
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
return
|
||||||
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
||||||
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
||||||
|
|
||||||
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
# Get time range from UI
|
||||||
out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
|
start_time = self.ripple_start_var.get()
|
||||||
|
end_time = self.ripple_end_var.get()
|
||||||
|
|
||||||
# Calculate fade frames (1 second fade in/out)
|
# Validate time range
|
||||||
fade_frames = min(int(fps), total_frames // 4)
|
if start_time < 0 or end_time <= start_time or end_time > self.video_duration:
|
||||||
|
messagebox.showwarning("Invalid Time Range",
|
||||||
|
f"Please enter valid times between 0 and {self.video_duration:.1f} seconds.\nStart time must be less than end time.")
|
||||||
|
return
|
||||||
|
|
||||||
frame_count = 0
|
# Set effect time range
|
||||||
while True:
|
self.effect_times['ripple']['start'] = start_time
|
||||||
ret, frame = cap.read()
|
self.effect_times['ripple']['end'] = end_time
|
||||||
if not ret:
|
self.effects_enabled['ripple'] = True
|
||||||
break
|
|
||||||
|
|
||||||
# Apply fade in
|
# Start effect animation timer
|
||||||
if frame_count < fade_frames:
|
self.effect_start_time = time.time()
|
||||||
alpha = frame_count / fade_frames
|
|
||||||
frame = cv2.convertScaleAbs(frame, alpha=alpha, beta=0)
|
|
||||||
|
|
||||||
# Apply fade out
|
messagebox.showinfo("Ripple Effect",
|
||||||
elif frame_count >= total_frames - fade_frames:
|
f"Ripple effect applied from {start_time:.1f}s to {end_time:.1f}s\nYou can see it in the video preview during this time range.\n\nDEBUG: Effect enabled = {self.effects_enabled['ripple']}\nCurrent time = {self.current_time:.2f}s")
|
||||||
alpha = (total_frames - frame_count) / fade_frames
|
|
||||||
frame = cv2.convertScaleAbs(frame, alpha=alpha, beta=0)
|
|
||||||
|
|
||||||
out.write(frame)
|
# Refresh current frame to show effect
|
||||||
frame_count += 1
|
self.display_frame_at_time(self.current_time)
|
||||||
|
|
||||||
cap.release()
|
def apply_ripple_to_frame(self, frame):
|
||||||
out.release()
|
"""Apply ripple effect to a single frame in real-time"""
|
||||||
|
intensity = self.ripple_intensity_var.get()
|
||||||
|
frequency = self.ripple_frequency_var.get()
|
||||||
|
|
||||||
|
# Calculate animation time for moving ripples
|
||||||
|
current_time = time.time()
|
||||||
|
animation_time = current_time - self.effect_start_time
|
||||||
|
|
||||||
|
height, width = frame.shape[:2]
|
||||||
|
center_x, center_y = width // 2, height // 2
|
||||||
|
|
||||||
|
# Use numpy for vectorized operations (fast real-time processing)
|
||||||
|
y, x = np.ogrid[:height, :width]
|
||||||
|
|
||||||
|
# Calculate distance from center using numpy arrays
|
||||||
|
distance = np.sqrt((x - center_x)**2 + (y - center_y)**2)
|
||||||
|
|
||||||
|
# Calculate ripple displacement with animation - make it more visible
|
||||||
|
ripple = np.sin(distance / frequency - animation_time * 5) * intensity * 3 # Multiply by 3 for more intensity
|
||||||
|
|
||||||
|
# Create displacement maps
|
||||||
|
map_x = (x + ripple).astype(np.float32)
|
||||||
|
map_y = (y + ripple * 0.5).astype(np.float32)
|
||||||
|
|
||||||
|
# Apply the ripple effect
|
||||||
|
rippled_frame = cv2.remap(frame, map_x, map_y, cv2.INTER_LINEAR)
|
||||||
|
|
||||||
|
# DEBUG: Add visible red border to show effect is active
|
||||||
|
cv2.rectangle(rippled_frame, (5, 5), (width-5, height-5), (0, 0, 255), 8)
|
||||||
|
cv2.putText(rippled_frame, "RIPPLE ACTIVE", (10, 40), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 3)
|
||||||
|
|
||||||
|
return rippled_frame
|
||||||
|
|
||||||
|
def apply_fade_to_frame(self, frame):
|
||||||
|
"""Apply fade in/out effect to a single frame in real-time"""
|
||||||
|
# Get effect time range
|
||||||
|
start_time = self.effect_times['fade']['start']
|
||||||
|
end_time = self.effect_times['fade']['end']
|
||||||
|
effect_duration = end_time - start_time
|
||||||
|
|
||||||
|
# Calculate fade amount based on current position within effect range
|
||||||
|
relative_time = self.current_time - start_time
|
||||||
|
fade_duration = min(0.5, effect_duration / 4) # Fade duration is 25% of effect duration or 0.5s, whichever is smaller
|
||||||
|
|
||||||
|
alpha = 1.0 # Default to full opacity
|
||||||
|
|
||||||
|
# Fade in at the beginning of effect
|
||||||
|
if relative_time < fade_duration:
|
||||||
|
alpha = relative_time / fade_duration
|
||||||
|
# Fade out at the end of effect
|
||||||
|
elif relative_time > (effect_duration - fade_duration):
|
||||||
|
alpha = (effect_duration - relative_time) / fade_duration
|
||||||
|
|
||||||
|
# Apply fade by adjusting frame brightness
|
||||||
|
alpha = max(0.0, min(1.0, alpha)) # Clamp between 0 and 1
|
||||||
|
faded_frame = cv2.multiply(frame, alpha)
|
||||||
|
|
||||||
|
# DEBUG: Add blue border to show fade is active
|
||||||
|
height, width = frame.shape[:2]
|
||||||
|
cv2.rectangle(faded_frame, (5, 5), (width-5, height-5), (255, 0, 0), 5)
|
||||||
|
cv2.putText(faded_frame, f"FADE ALPHA:{alpha:.2f}", (10, height-20), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 0, 0), 2)
|
||||||
|
|
||||||
|
return faded_frame.astype(np.uint8)
|
||||||
|
|
||||||
|
def apply_text_to_frame(self, frame):
|
||||||
|
"""Apply text overlay to a single frame in real-time"""
|
||||||
|
# Get text content
|
||||||
|
text_content = self.text_var.get().strip() if hasattr(self, 'text_var') and self.text_var.get().strip() else "Sample Text"
|
||||||
|
|
||||||
|
# Create a copy to avoid modifying original
|
||||||
|
text_frame = frame.copy()
|
||||||
|
|
||||||
|
# Text settings
|
||||||
|
font = cv2.FONT_HERSHEY_SIMPLEX
|
||||||
|
font_scale = 2
|
||||||
|
font_color = (255, 255, 255) # White text
|
||||||
|
thickness = 3
|
||||||
|
|
||||||
|
# Get text size to center it
|
||||||
|
text_size = cv2.getTextSize(text_content, font, font_scale, thickness)[0]
|
||||||
|
height, width = frame.shape[:2]
|
||||||
|
|
||||||
|
# Position text at bottom center
|
||||||
|
text_x = (width - text_size[0]) // 2
|
||||||
|
text_y = height - 50 # 50 pixels from bottom
|
||||||
|
|
||||||
|
# Add black outline for better readability
|
||||||
|
cv2.putText(text_frame, text_content, (text_x, text_y), font, font_scale, (0, 0, 0), thickness + 2)
|
||||||
|
# Add white text on top
|
||||||
|
cv2.putText(text_frame, text_content, (text_x, text_y), font, font_scale, font_color, thickness)
|
||||||
|
|
||||||
|
return text_frame
|
||||||
|
|
||||||
|
def apply_effects_to_frame(self, frame):
|
||||||
|
"""Apply all enabled real-time effects to a frame based on current time"""
|
||||||
|
|
||||||
|
# DEBUG: Draw a red rectangle when any effect should be active
|
||||||
|
debug_active = False
|
||||||
|
|
||||||
|
# Check if ripple effect should be active at current time
|
||||||
|
if (self.effects_enabled['ripple'] and
|
||||||
|
self.effect_times['ripple']['start'] <= self.current_time <= self.effect_times['ripple']['end']):
|
||||||
|
print(f"🌊 Applying ripple at time {self.current_time:.2f}s")
|
||||||
|
debug_active = True
|
||||||
|
frame = self.apply_ripple_to_frame(frame)
|
||||||
|
|
||||||
|
# Check if fade effect should be active at current time
|
||||||
|
if (self.effects_enabled['fade'] and
|
||||||
|
self.effect_times['fade']['start'] <= self.current_time <= self.effect_times['fade']['end']):
|
||||||
|
print(f"🌅 Applying fade at time {self.current_time:.2f}s")
|
||||||
|
debug_active = True
|
||||||
|
frame = self.apply_fade_to_frame(frame)
|
||||||
|
|
||||||
|
# Check if text overlay should be active at current time
|
||||||
|
if (self.effects_enabled['text'] and
|
||||||
|
self.effect_times['text']['start'] <= self.current_time <= self.effect_times['text']['end']):
|
||||||
|
print(f"📝 Applying text at time {self.current_time:.2f}s")
|
||||||
|
debug_active = True
|
||||||
|
frame = self.apply_text_to_frame(frame)
|
||||||
|
|
||||||
|
# DEBUG: Draw a green rectangle in top-left corner when effects are active
|
||||||
|
if debug_active:
|
||||||
|
cv2.rectangle(frame, (10, 10), (100, 50), (0, 255, 0), 3)
|
||||||
|
cv2.putText(frame, "EFFECT", (15, 35), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
|
||||||
|
|
||||||
|
return frame
|
||||||
|
|
||||||
def apply_text(self):
|
def apply_text(self):
|
||||||
"""Apply text overlay"""
|
"""Apply text overlay with time-based controls"""
|
||||||
if not self.current_clip:
|
if not self.current_video:
|
||||||
messagebox.showwarning("No Video", "Please load a video first.")
|
messagebox.showwarning("No Video", "Please load a video first.")
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -2499,71 +3046,42 @@ class ShortsEditorGUI:
|
|||||||
messagebox.showwarning("No Text", "Please enter text to overlay.")
|
messagebox.showwarning("No Text", "Please enter text to overlay.")
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
# Get time range from UI
|
||||||
if MOVIEPY_AVAILABLE and hasattr(self.current_clip, 'duration'):
|
start_time = self.text_start_var.get()
|
||||||
# Use MoviePy for text overlay
|
end_time = self.text_end_var.get()
|
||||||
text_clip = TextClip(text_content, fontsize=50, color='white', font='Arial-Bold')
|
|
||||||
text_clip = text_clip.with_duration(self.current_clip.duration)
|
|
||||||
text_clip = text_clip.with_position(('center', 'bottom'))
|
|
||||||
|
|
||||||
# Composite with video
|
# Validate time range
|
||||||
self.current_clip = CompositeVideoClip([self.current_clip, text_clip])
|
if start_time < 0 or end_time <= start_time or end_time > self.video_duration:
|
||||||
messagebox.showinfo("Success", f"Text overlay added: '{text_content}'")
|
messagebox.showwarning("Invalid Time Range",
|
||||||
else:
|
f"Please enter valid times between 0 and {self.video_duration:.1f} seconds.\nStart time must be less than end time.")
|
||||||
# OpenCV mode - export video with text overlay
|
|
||||||
if not self.current_video:
|
|
||||||
messagebox.showwarning("No Video", "No video file loaded.")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
timestamp = datetime.now().strftime("%H%M%S")
|
# Set effect time range
|
||||||
base_name = os.path.splitext(os.path.basename(self.current_video))[0]
|
self.effect_times['text']['start'] = start_time
|
||||||
output_path = os.path.join(os.path.dirname(self.current_video),
|
self.effect_times['text']['end'] = end_time
|
||||||
f"{base_name}_with_text_{timestamp}.mp4")
|
self.effects_enabled['text'] = True
|
||||||
|
|
||||||
self.apply_text_opencv(self.current_video, output_path, text_content)
|
messagebox.showinfo("Text Overlay",
|
||||||
self.load_video(output_path)
|
f"Text '{text_content}' applied from {start_time:.1f}s to {end_time:.1f}s\nYou can see it in the video preview during this time range.")
|
||||||
messagebox.showinfo("Success", f"Text '{text_content}' added and saved as:\n{os.path.basename(output_path)}")
|
|
||||||
|
|
||||||
except Exception as e:
|
# Refresh current frame to show effect
|
||||||
messagebox.showerror("Text Error", f"Could not add text overlay: {e}")
|
self.display_frame_at_time(self.current_time)
|
||||||
|
|
||||||
def apply_text_opencv(self, input_path, output_path, text):
|
def clear_all_effects(self):
|
||||||
"""Apply text overlay using OpenCV"""
|
"""Clear all active effects"""
|
||||||
cap = cv2.VideoCapture(input_path)
|
# Disable all effects
|
||||||
fps = cap.get(cv2.CAP_PROP_FPS)
|
for effect in self.effects_enabled:
|
||||||
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
self.effects_enabled[effect] = False
|
||||||
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
||||||
|
|
||||||
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
# Reset all effect times
|
||||||
out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
|
for effect in self.effect_times:
|
||||||
|
self.effect_times[effect]['start'] = 0.0
|
||||||
|
self.effect_times[effect]['end'] = 0.0
|
||||||
|
|
||||||
# Text settings
|
messagebox.showinfo("Effects Cleared", "All effects have been cleared from the video.")
|
||||||
font = cv2.FONT_HERSHEY_SIMPLEX
|
|
||||||
font_scale = min(width, height) / 800 # Scale font based on video size
|
|
||||||
color = (255, 255, 255) # White color
|
|
||||||
thickness = max(1, int(font_scale * 2))
|
|
||||||
|
|
||||||
# Get text size for positioning
|
# Refresh current frame to remove all effects
|
||||||
(text_width, text_height), baseline = cv2.getTextSize(text, font, font_scale, thickness)
|
self.display_frame_at_time(self.current_time)
|
||||||
x = (width - text_width) // 2
|
|
||||||
y = height - 50 # Bottom position
|
|
||||||
|
|
||||||
while True:
|
|
||||||
ret, frame = cap.read()
|
|
||||||
if not ret:
|
|
||||||
break
|
|
||||||
|
|
||||||
# Add black outline for better visibility
|
|
||||||
cv2.putText(frame, text, (x-2, y-2), font, font_scale, (0, 0, 0), thickness+2, cv2.LINE_AA)
|
|
||||||
cv2.putText(frame, text, (x+2, y+2), font, font_scale, (0, 0, 0), thickness+2, cv2.LINE_AA)
|
|
||||||
|
|
||||||
# Add white text
|
|
||||||
cv2.putText(frame, text, (x, y), font, font_scale, color, thickness, cv2.LINE_AA)
|
|
||||||
|
|
||||||
out.write(frame)
|
|
||||||
|
|
||||||
cap.release()
|
|
||||||
out.release()
|
|
||||||
|
|
||||||
def apply_resize(self, target_width, target_height):
|
def apply_resize(self, target_width, target_height):
|
||||||
"""Apply resize to video"""
|
"""Apply resize to video"""
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user