diff --git a/src-tauri/src/services/clip_exporter.rs b/src-tauri/src/services/clip_exporter.rs index 38b4979..70352e7 100644 --- a/src-tauri/src/services/clip_exporter.rs +++ b/src-tauri/src/services/clip_exporter.rs @@ -547,17 +547,52 @@ pub fn vtt_to_ass_with_karaoke( let out_path = Path::new(temp_dir).join("karaoke.ass"); - let font_size = style.map_or(45, |s| (s.font_size as f64 * 2.5).round() as u32); + // Font + let font_name = style.map_or("Arial".to_string(), |s| s.font_family.clone()); + let font_size = style.map_or(50, |s| (s.font_size as f64 * 2.75).round() as u32); + let bold_flag: i32 = if style.map_or(true, |s| s.bold) { -1 } else { 0 }; + + // Colors let primary_colour = style.map_or("&H00FFFFFF".to_string(), |s| hex_to_ass_color(&s.text_color)); - let secondary_colour = "&H73CCCCCC".to_string(); - let outline_colour = "&H00000000".to_string(); - let back_colour = style.map_or("&H80000000".to_string(), |s| opacity_to_ass_back_colour(s.background_opacity)); + let outline_colour = style.map_or("&H00000000".to_string(), |s| hex_to_ass_color(&s.outline_color)); + + // Dimmed color (SecondaryColour) — auto derives from text color, custom uses direct color + let secondary_colour = style.map_or("&H73CCCCCC".to_string(), |s| { + if s.dimmed_color_mode == "custom" { + hex_to_ass_color(&s.dimmed_color) + } else { + let alpha = ((1.0 - s.dimmed_opacity.clamp(0.0, 1.0)) * 255.0).round() as u8; + hex_to_ass_color_with_alpha(&s.text_color, alpha) + } + }); + + // Background/Shadow mutual exclusivity + let bg_enabled = style.map_or(true, |s| s.background_enabled); + let shadow_enabled = style.map_or(false, |s| s.shadow_enabled); let text_outline = style.map_or(true, |s| s.text_outline); - let (border_style, outline_val, shadow_val) = if text_outline { - (4, 2, 0) + + let (border_style, outline_val, shadow_val, back_colour) = if bg_enabled { + // Background mode: box + optional outline, no shadow + let bc = style.map_or("&H80000000".to_string(), |s| { + let alpha = ((1.0 - s.background_opacity.clamp(0.0, 1.0)) * 255.0).round() as u8; + hex_to_ass_color_with_alpha(&s.background_color, alpha) + }); + let bs = if text_outline { 4 } else { 3 }; + let ol = if text_outline { 2 } else { 0 }; + (bs, ol, 0, bc) + } else if shadow_enabled { + // Shadow mode: outline + shadow, no box + let sc = style.map_or("&H00000000".to_string(), |s| hex_to_ass_color(&s.shadow_color)); + let sd = style.map_or(2, |s| s.shadow_depth) as i32; + let ol = if text_outline { 2 } else { 0 }; + (1, ol, sd, sc) } else { - (3, 0, 0) + // Neither: just outline if enabled + let ol = if text_outline { 2 } else { 0 }; + (1, ol, 0, "&HFF000000".to_string()) // fully transparent back }; + + let word_highlight = style.map_or(true, |s| s.word_highlight); let margin_v: i32 = 40; let mut output = String::new(); @@ -573,9 +608,9 @@ pub fn vtt_to_ass_with_karaoke( output.push_str("[V4+ Styles]\n"); output.push_str("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n"); output.push_str(&format!( - "Style: Default,Arial,{},{},{},{},{},0,0,0,0,100,100,0,0,{},{},{},2,20,20,{},1\n", - font_size, primary_colour, secondary_colour, outline_colour, back_colour, - border_style, outline_val, shadow_val, margin_v + "Style: Default,{},{},{},{},{},{},{},0,0,0,100,100,0,0,{},{},{},2,20,20,{},1\n", + font_name, font_size, primary_colour, secondary_colour, outline_colour, back_colour, + bold_flag, border_style, outline_val, shadow_val, margin_v )); output.push('\n'); @@ -584,8 +619,9 @@ pub fn vtt_to_ass_with_karaoke( let spoken_lines = extract_spoken_lines(&content); let gap_threshold = 2.0; - let crossfade_ms = 300; - let crossfade_s = crossfade_ms as f64 / 1000.0; + let fade_ms = 100; // per-page fade in/out duration (sequential, no overlap) + let fade_s = fade_ms as f64 / 1000.0; + let hold_s = 100.0 / 1000.0; // hold fully visible after last word before fading out let pages = group_into_pages(&spoken_lines, gap_threshold); let page_count = pages.len(); @@ -615,21 +651,22 @@ pub fn vtt_to_ass_with_karaoke( let is_first = page_idx == 0; let is_last = page_idx == page_count - 1; - let display_start = if is_first { - natural_start - } else { - (natural_start - crossfade_s).max(0.0) - }; + // No overlap — each page fades in/out independently (sequential) + let display_start = natural_start; let display_end = if is_last { natural_end } else { - natural_end + crossfade_s + natural_end + hold_s + fade_s }; - let fade_in = if is_first { 0 } else { crossfade_ms }; - let fade_out = if is_last { 0 } else { crossfade_ms }; + let fade_in = if is_first { 0 } else { fade_ms }; + let fade_out = if is_last { 0 } else { fade_ms }; - let karaoke_text = build_page_karaoke_text(&page_lines); + let page_text = if word_highlight { + build_page_karaoke_text(&page_lines) + } else { + page_lines.iter().map(|l| l.plain_text.as_str()).collect::>().join("\\N") + }; output.push_str(&format!( "Dialogue: 0,{},{},Default,,0,0,0,,{{\\an2\\pos({},{})\\fad({},{})}}{}\n", @@ -637,7 +674,7 @@ pub fn vtt_to_ass_with_karaoke( format_ass_timestamp(display_end), cx, y_bottom, fade_in, fade_out, - karaoke_text + page_text )); }