module VideoSlideGenerators def generate_intro_slide(slide) animation_duration = slide[:duration] hold_duration = slide[:hold_duration] || 1.0 total_duration = animation_duration + hold_duration frames = (total_duration * 30).to_i frames.times do |frame| total_progress = frame.to_f / frames animation_progress = [total_progress / (animation_duration / total_duration), 1.0].min # Smooth ease-in-out animation eased_progress = ease_in_out(animation_progress) create_frame do |magick| magick.size "1920x1080" # Dynamic gradient background gradient_stop = (50 + eased_progress * 50).to_i magick << "gradient:#1DB954-#191414" # Animated overlay overlay_opacity = 0.4 + 0.2 * Math.sin(animation_progress * Math::PI * 4) magick.fill "rgba(0,0,0,#{overlay_opacity})" magick.draw "rectangle 0,0 1920,1080" # Main title with scale animation title_scale = 0.8 + 0.2 * eased_progress magick.font "DejaVu-Sans-Bold" magick.pointsize (120 * title_scale).to_i magick.fill "white" magick.gravity "Center" magick.annotate "+0-120", slide[:title] # Subtitle with delayed fade subtitle_opacity = [(animation_progress - 0.3) / 0.7, 0].max magick.pointsize 55 magick.fill "rgba(255,255,255,#{subtitle_opacity})" magick.annotate "+0-20", slide[:subtitle] # Tagline with final fade tagline_opacity = [(animation_progress - 0.6) / 0.4, 0].max magick.pointsize 35 magick.fill "rgba(29,185,84,#{tagline_opacity})" magick.annotate "+0+60", slide[:tagline] # Pulsing accent if animation_progress > 0.8 pulse = Math.sin((animation_progress - 0.8) * Math::PI * 20) * 0.3 + 0.7 magick.fill "rgba(29,185,84,#{pulse * 0.5})" magick.draw "circle 960,540 960,#{540 + 200 * pulse}" end end end end def generate_roast_slide(slide) animation_duration = slide[:duration] hold_duration = slide[:hold_duration] || 1.0 total_duration = animation_duration + hold_duration frames = (total_duration * 30).to_i frames.times do |frame| total_progress = frame.to_f / frames animation_progress = [total_progress / (animation_duration / total_duration), 1.0].min create_frame do |magick| magick.size "1920x1080" magick << "xc:#191414" # Subtle background pattern pattern_opacity = 0.1 + 0.05 * Math.sin(animation_progress * Math::PI * 3) magick.fill "rgba(29,185,84,#{pattern_opacity})" (0..20).each do |i| y = i * 54 magick.draw "rectangle 0,#{y} 1920,#{y+1}" end # Main text with typing effect chars_to_show = (slide[:title].length * animation_progress).to_i visible_title = slide[:title][0, chars_to_show] magick.font "DejaVu-Sans-Bold" magick.pointsize 85 magick.fill "white" magick.gravity "Center" magick.annotate "+0-80", visible_title # Subtitle with fade if animation_progress > 0.4 subtitle_opacity = (animation_progress - 0.4) / 0.6 magick.pointsize 50 magick.fill "rgba(255,255,255,#{subtitle_opacity})" magick.annotate "+0+20", slide[:subtitle] end # Tagline with final reveal if animation_progress > 0.7 tagline_opacity = ((animation_progress - 0.7) / 0.3) magick.pointsize 30 magick.fill "rgba(29,185,84,#{tagline_opacity})" magick.annotate "+0+100", slide[:tagline] end end end end def generate_stats_slide(slide) animation_duration = slide[:duration] hold_duration = slide[:hold_duration] || 1.0 total_duration = animation_duration + hold_duration frames = (total_duration * 30).to_i frames.times do |frame| total_progress = frame.to_f / frames animation_progress = [total_progress / (animation_duration / total_duration), 1.0].min create_frame do |magick| magick.size "1920x1080" magick << "xc:#191414" # Title magick.font "DejaVu-Sans-Bold" magick.pointsize 75 magick.fill "white" magick.gravity "Center" magick.annotate "+0-350", slide[:title] # Animate stats with staggered entrance slide[:stats].each_with_index do |stat, index| stat_delay = index * 0.25 stat_progress = [(animation_progress - stat_delay) / (1.0 - stat_delay), 0].max next if stat_progress <= 0 # Smooth entry animation entry_progress = ease_in_out([stat_progress, 1.0].min) y_offset = -150 + (index * 180) # Scale and fade animation scale = 0.5 + 0.5 * entry_progress opacity = entry_progress # Main stat with number animation if stat_progress > 0.3 number_progress = [(stat_progress - 0.3) / 0.7, 1.0].min animated_value = (stat[:value].to_i * number_progress).to_i.to_s magick.pointsize (90 * scale).to_i magick.fill "rgba(29,185,84,#{opacity})" magick.annotate "+0+#{y_offset - 30}", sprintf(stat[:label], animated_value) end # Subtext with delayed fade if stat_progress > 0.6 subtext_opacity = (stat_progress - 0.6) / 0.4 magick.pointsize 28 magick.fill "rgba(179,179,179,#{subtext_opacity})" magick.annotate "+0+#{y_offset + 40}", stat[:subtext] end end end end end def generate_top_list_slide(slide) animation_duration = slide[:duration] hold_duration = slide[:hold_duration] || 1.0 total_duration = animation_duration + hold_duration frames = (total_duration * 30).to_i frames.times do |frame| total_progress = frame.to_f / frames animation_progress = [total_progress / (animation_duration / total_duration), 1.0].min create_frame do |magick| magick.size "1920x1080" magick << "xc:#191414" # Title with glow effect glow_radius = 20 + 10 * Math.sin(animation_progress * Math::PI * 3) magick.font "DejaVu-Sans-Bold" magick.pointsize 90 magick.fill "#1DB954" magick.gravity "Center" magick.annotate "+0-380", slide[:title] # Subtitle magick.pointsize 32 magick.fill "#B3B3B3" magick.annotate "+0-320", slide[:subtitle] # Animate list items with bounce slide[:items].each_with_index do |item, index| item_delay = index * 0.15 item_progress = [(animation_progress - item_delay) * 3, 0].max next if item_progress <= 0 # Bounce animation bounce_progress = [item_progress, 1.0].min bounce_offset = bounce_progress < 1.0 ? -30 * Math.sin(bounce_progress * Math::PI) : 0 y_pos = -180 + (index * 90) + bounce_offset opacity = [item_progress, 1.0].min # Rank with medal emojis rank_emoji = case index when 0 then "🥇" when 1 then "🥈" when 2 then "🥉" else "#{index + 1}." end magick.pointsize 45 magick.fill "rgba(29,185,84,#{opacity})" magick.gravity "West" magick.annotate "+300+#{y_pos}", rank_emoji # Item name with emphasis magick.pointsize index == 0 ? 55 : 42 magick.fill "rgba(255,255,255,#{opacity})" magick.annotate "+#{index == 0 ? 420 : 400}+#{y_pos}", item[:name] # Value magick.pointsize 30 magick.fill "rgba(179,179,179,#{opacity})" magick.gravity "East" magick.annotate "-300+#{y_pos}", item[:value] # Roast text with delayed reveal if item_progress > 0.7 roast_opacity = (item_progress - 0.7) * 3.33 magick.pointsize 24 magick.fill "rgba(29,185,84,#{roast_opacity})" magick.gravity "West" magick.annotate "+420+#{y_pos + 35}", item[:roast] end end end end end def generate_highlight_slide(slide) animation_duration = slide[:duration] hold_duration = slide[:hold_duration] || 1.0 total_duration = animation_duration + hold_duration frames = (total_duration * 30).to_i frames.times do |frame| total_progress = frame.to_f / frames animation_progress = [total_progress / (animation_duration / total_duration), 1.0].min create_frame do |magick| magick.size "1920x1080" # Dynamic background with movement bg_offset = (animation_progress * 100).to_i magick << "gradient:#1DB954-#191414" magick.fill "rgba(25,20,20,0.6)" magick.draw "rectangle 0,0 1920,1080" # Animated particles if total_progress > 0.2 20.times do |i| particle_progress = (total_progress - 0.2 + i * 0.02) % 1.0 x = 100 + (i * 90) + (particle_progress * 200) y = 200 + Math.sin(particle_progress * Math::PI * 2) * 50 opacity = Math.sin(particle_progress * Math::PI) * 0.3 magick.fill "rgba(29,185,84,#{opacity})" magick.draw "circle #{x},#{y} #{x+5},#{y+5}" end end # Main title with dramatic scale title_scale = 0.7 + 0.3 * ease_in_out(animation_progress) title_glow = Math.sin(animation_progress * Math::PI * 2) * 0.2 + 0.8 magick.font "DejaVu-Sans-Bold" magick.pointsize (140 * title_scale).to_i magick.fill "rgba(255,255,255,#{title_glow})" magick.gravity "Center" magick.annotate "+0-200", slide[:title] # Subtitle with slide-in if animation_progress > 0.3 subtitle_offset = (1 - (animation_progress - 0.3) / 0.7) * 200 magick.pointsize 48 magick.fill "rgba(255,255,255,0.9)" magick.annotate "+#{subtitle_offset.to_i}+120", slide[:subtitle] end # Main stat with counter animation if animation_progress > 0.5 stat_progress = (animation_progress - 0.5) / 0.5 magick.pointsize 75 magick.fill "#1DB954" magick.annotate "+0-60", slide[:main_stat] end # Flavor text with fade in if animation_progress > 0.6 flavor_opacity = (animation_progress - 0.6) / 0.4 magick.pointsize 35 magick.fill "rgba(255,255,255,#{flavor_opacity})" magick.annotate "+0+20", slide[:flavor_text] end # Secondary text if animation_progress > 0.8 secondary_opacity = (animation_progress - 0.8) / 0.2 magick.pointsize 28 magick.fill "rgba(179,179,179,#{secondary_opacity})" magick.annotate "+0+70", slide[:secondary_text] end end end end def generate_time_analysis_slide(slide) animation_duration = slide[:duration] hold_duration = slide[:hold_duration] || 1.0 total_duration = animation_duration + hold_duration frames = (total_duration * 30).to_i frames.times do |frame| total_progress = frame.to_f / frames animation_progress = [total_progress / (animation_duration / total_duration), 1.0].min create_frame do |magick| magick.size "1920x1080" magick << "xc:#191414" # Title magick.font "DejaVu-Sans-Bold" magick.pointsize 65 magick.fill "white" magick.gravity "Center" magick.annotate "+0-350", slide[:title] # Subtitle magick.pointsize 36 magick.fill "#B3B3B3" magick.annotate "+0-280", slide[:subtitle] # Subtext with attitude if animation_progress > 0.3 subtext_opacity = ((animation_progress - 0.3) / 0.7) magick.pointsize 28 magick.fill "rgba(29,185,84,#{subtext_opacity})" magick.annotate "+0-230", slide[:subtext] end # Animated bar chart max_bar_height = 250 bar_width = 200 chart_start_x = 260 chart_bottom_y = 750 slide[:bars].each_with_index do |bar, index| bar_delay = index * 0.1 bar_progress = [(animation_progress - bar_delay) / (1.0 - bar_delay), 0].max next if bar_progress <= 0 # Bar animation with bounce height_progress = ease_out_bounce([bar_progress, 1.0].min) bar_height = (bar[:percentage] / 100.0 * max_bar_height * height_progress).to_i x_pos = chart_start_x + (index * (bar_width + 60)) y_pos = chart_bottom_y - bar_height # Bar gradient magick.fill "#1DB954" magick.draw "rectangle #{x_pos},#{y_pos} #{x_pos + bar_width},#{chart_bottom_y}" # Bar glow effect glow_opacity = 0.3 * height_progress magick.fill "rgba(29,185,84,#{glow_opacity})" magick.draw "rectangle #{x_pos-5},#{y_pos-5} #{x_pos + bar_width+5},#{chart_bottom_y+5}" # Label magick.pointsize 28 magick.fill "white" magick.gravity "Center" magick.annotate "+#{x_pos - 960 + bar_width/2}+#{chart_bottom_y - 540 + 40}", bar[:label] # Value on top of bar if bar_height > 30 magick.pointsize 24 magick.fill "white" magick.font "DejaVu-Sans-Bold" magick.annotate "+#{x_pos - 960 + bar_width/2}+#{y_pos - 540 + 20}", bar[:value] end end end end end def generate_surprise_slide(slide) animation_duration = slide[:duration] hold_duration = slide[:hold_duration] || 1.0 total_duration = animation_duration + hold_duration frames = (total_duration * 30).to_i frames.times do |frame| total_progress = frame.to_f / frames animation_progress = [total_progress / (animation_duration / total_duration), 1.0].min create_frame do |magick| magick.size "1920x1080" # Surprise burst background burst_progress = ease_out_expo(animation_progress) magick << "radial-gradient:#1DB954-#191414" # Animated burst rays if animation_progress > 0.1 12.times do |i| angle = (i * 30 + animation_progress * 360) % 360 ray_length = 300 * burst_progress start_x = 960 start_y = 540 end_x = start_x + Math.cos(angle * Math::PI / 180) * ray_length end_y = start_y + Math.sin(angle * Math::PI / 180) * ray_length magick.stroke "rgba(255,255,255,0.1)" magick.strokewidth 3 magick.draw "line #{start_x},#{start_y} #{end_x},#{end_y}" end end # Title with dramatic entrance title_scale = animation_progress < 0.5 ? animation_progress * 4 : 1.0 + Math.sin((animation_progress - 0.5) * Math::PI * 10) * 0.1 magick.font "DejaVu-Sans-Bold" magick.pointsize (120 * title_scale).to_i magick.fill "white" magick.gravity "Center" magick.annotate "+0-150", slide[:title] # Subtitle with typewriter effect if animation_progress > 0.3 chars_to_show = (((animation_progress - 0.3) / 0.7) * slide[:subtitle].length).to_i visible_subtitle = slide[:subtitle][0, chars_to_show] magick.pointsize 42 magick.fill "rgba(255,255,255,0.9)" magick.annotate "+0-60", visible_subtitle end # Main stat with number roll if animation_progress > 0.6 magick.pointsize 80 magick.fill "#1DB954" magick.annotate "+0+30", slide[:main_stat] end # Flavor text with final reveal if animation_progress > 0.7 flavor_opacity = ((animation_progress - 0.7) / 0.3) magick.pointsize 32 magick.fill "rgba(255,255,255,#{flavor_opacity})" magick.annotate "+0+100", slide[:flavor_text] end end end end def generate_recap_slide(slide) animation_duration = slide[:duration] hold_duration = slide[:hold_duration] || 1.0 total_duration = animation_duration + hold_duration frames = (total_duration * 30).to_i frames.times do |frame| total_progress = frame.to_f / frames animation_progress = [total_progress / (animation_duration / total_duration), 1.0].min create_frame do |magick| magick.size "1920x1080" # Final gradient with subtle animation gradient_opacity = 0.8 + 0.2 * Math.sin(animation_progress * Math::PI * 2) magick << "gradient:#1DB954-#191414" magick.fill "rgba(25,20,20,#{gradient_opacity * 0.4})" magick.draw "rectangle 0,0 1920,1080" # Floating particles finale 50.times do |i| particle_progress = (total_progress + i * 0.02) % 1.0 x = (i * 38.4) % 1920 y = 100 + Math.sin(particle_progress * Math::PI + i) * 300 opacity = Math.sin(particle_progress * Math::PI) * 0.2 magick.fill "rgba(29,185,84,#{opacity})" magick.draw "circle #{x},#{y} #{x+3},#{y+3}" end # Title with celebration title_scale = 1.0 + Math.sin(animation_progress * Math::PI * 4) * 0.05 magick.font "DejaVu-Sans-Bold" magick.pointsize (85 * title_scale).to_i magick.fill "white" magick.gravity "Center" magick.annotate "+0-300", slide[:title] # Subtitle magick.pointsize 38 magick.fill "rgba(255,255,255,0.9)" magick.annotate "+0-230", slide[:subtitle] # Stats grid with staggered animation stats_per_row = 2 # Animate stats with staggered entrance slide[:stats].each_with_index do |stat, index| stat_delay = index * 0.2 stat_progress = [(animation_progress - stat_delay) / (1.0 - stat_delay), 0].max next if stat_progress <= 0 row = index / stats_per_row col = index % stats_per_row x_offset = (col - 0.5) * 400 y_offset = -100 + (row * 100) opacity = [stat_progress, 1.0].min scale = 0.8 + 0.2 * ease_in_out(stat_progress) # Stat label magick.pointsize (32 * scale).to_i magick.fill "rgba(179,179,179,#{opacity})" magick.annotate "+#{x_offset.to_i}+#{y_offset}", stat[:label] # Stat value magick.pointsize (45 * scale).to_i magick.fill "rgba(29,185,84,#{opacity})" magick.annotate "+#{x_offset.to_i}+#{y_offset + 40}", stat[:value] end # Final message with fade if animation_progress > 0.8 message_opacity = ((animation_progress - 0.8) / 0.2) magick.pointsize 35 magick.fill "rgba(255,255,255,#{message_opacity})" magick.annotate "+0+200", slide[:final_message] end end end end def generate_one_time_uses_slide(slide) animation_duration = slide[:duration] hold_duration = slide[:hold_duration] || 1.0 total_duration = animation_duration + hold_duration frames = (total_duration * 30).to_i frames.times do |frame| total_progress = frame.to_f / frames animation_progress = [total_progress / (animation_duration / total_duration), 1.0].min create_frame do |magick| magick.size "1920x1080" magick << "xc:#191414" # Title with funky animation title_bounce = Math.sin(animation_progress * Math::PI * 4) * 5 magick.font "DejaVu-Sans-Bold" magick.pointsize 75 magick.fill "#1DB954" magick.gravity "Center" magick.annotate "+0-#{350 + title_bounce.to_i}", slide[:title] # Subtitle if animation_progress > 0.2 subtitle_opacity = (animation_progress - 0.2) / 0.8 magick.pointsize 40 magick.fill "rgba(179,179,179,#{subtitle_opacity})" magick.annotate "+0-280", slide[:subtitle] end # List items with staggered animation slide[:items].each_with_index do |item, index| item_start = 0.3 + (index * 0.08) if animation_progress > item_start item_progress = (animation_progress - item_start) / (1.0 - item_start) opacity = [item_progress * 1.2, 1.0].min # Calculate position for 2 columns row = index / 2 col = index % 2 x_offset = col == 0 ? -300 : 300 y_offset = -150 + (row * 80) # Slide in from sides slide_offset = (1 - ease_in_out(item_progress)) * (col == 0 ? -100 : 100) # Substance name magick.pointsize 42 magick.fill "rgba(255,255,255,#{opacity})" magick.gravity "Center" magick.annotate "+#{x_offset + slide_offset.to_i}+#{y_offset}", item[:name] # "Just once" text magick.pointsize 28 magick.fill "rgba(29,185,84,#{opacity * 0.8})" magick.annotate "+#{x_offset + slide_offset.to_i}+#{y_offset + 35}", item[:value] # Roast text if late enough if item_progress > 0.5 && item[:roast] roast_opacity = (item_progress - 0.5) * 2 magick.pointsize 22 magick.fill "rgba(179,179,179,#{roast_opacity * 0.7})" magick.font "DejaVu-Sans-Oblique" magick.annotate "+#{x_offset + slide_offset.to_i}+#{y_offset + 60}", item[:roast] magick.font "DejaVu-Sans" end end end end end end end