Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/UI/Assets/Languages/Danish.json
Original file line number Diff line number Diff line change
Expand Up @@ -2480,8 +2480,11 @@
"AdvancedEffectHeartsDescription": "Bezier-tegnede hjerter i tre former regner blidt fra toppen af ​​skærmen, vælter og falmer gennem hver undertekst",
"AdvancedEffectWordSpacing": "Ordafstand",
"AdvancedEffectWordSpacingDescription": "Øger afstanden mellem ord ved hjælp af \\fsp-tagget for bedre læsbarhed",
"AdvancedEffectFancyKaraokeAutoDetectActiveWord": "Auto-detekter aktivt ord",
"AdvancedEffectFancyKaraokeGlow": "Aktiv ord glød",
"AdvancedEffectFancyKaraokeActiveColor": "Aktiv farve",
"AdvancedEffectFancyKaraokeInactiveColor": "Inaktiv farve",
"AdvancedEffectFancyKaraokeInactiveOpacity": "Inaktiv opacitet (0-255)",
"AdvancedEffectFancyKaraokeGlowColor": "Aktiv ord glød farve",
"AdvancedEffectWordSpacingPixels": "Mellemrum (pixels)",
"AdvancedEffectSlowZoomOut": "Langsom zoom ud",
"AdvancedEffectSlowZoomOutDescription": "Tekst starter lidt større og krymper forsigtigt til normal størrelse i løbet af underteksternes varighed",
Expand Down
5 changes: 4 additions & 1 deletion src/UI/Assets/Languages/English.json
Original file line number Diff line number Diff line change
Expand Up @@ -2481,8 +2481,11 @@
"advancedEffectHeartsDescription": "Bezier-drawn hearts in three shapes rain gently from the top of the screen, tumbling and fading throughout each subtitle",
"advancedEffectWordSpacing": "Word spacing",
"advancedEffectWordSpacingDescription": "Increases spacing between words using the \\fsp tag for better readability",
"advancedEffectFancyKaraokeAutoDetectActiveWord": "Auto-detect active word",
"advancedEffectFancyKaraokeGlow": "Active word glow",
"advancedEffectFancyKaraokeActiveColor": "Active color",
"advancedEffectFancyKaraokeInactiveColor": "Inactive color",
"advancedEffectFancyKaraokeInactiveOpacity": "Inactive opacity (0-255)",
"advancedEffectFancyKaraokeGlowColor": "Active word glow color",
"advancedEffectWordSpacingPixels": "Spacing (pixels)",
"advancedEffectSlowZoomOut": "Slow zoom-out",
"advancedEffectSlowZoomOutDescription": "Text starts slightly larger and gently shrinks to normal size over the subtitle duration",
Expand Down
5 changes: 4 additions & 1 deletion src/UI/Assets/Languages/Italian.json
Original file line number Diff line number Diff line change
Expand Up @@ -2480,8 +2480,11 @@
"AdvancedEffectHeartsDescription": "Cuori disegnati Bezier in tre forme piovono dolcemente dalla parte superiore dello schermo, cadendo e svanendo durante ogni sottotitolo",
"AdvancedEffectWordSpacing": "Spaziatura parole",
"AdvancedEffectWordSpacingDescription": "Aumenta la spaziatura tra le parole usando il tag \\fsp per una migliore leggibilità",
"AdvancedEffectFancyKaraokeAutoDetectActiveWord": "Rileva automaticamente la parola attiva",
"AdvancedEffectFancyKaraokeGlow": "Bagliore parola attiva",
"AdvancedEffectFancyKaraokeActiveColor": "Colore attivo",
"AdvancedEffectFancyKaraokeInactiveColor": "Colore inattivo",
"AdvancedEffectFancyKaraokeInactiveOpacity": "Opacità non attiva (0-255)",
"AdvancedEffectFancyKaraokeGlowColor": "Colore bagliore parola attiva",
"AdvancedEffectWordSpacingPixels": "Spaziatura (pixel)",
"AdvancedEffectSlowZoomOut": "Zoom - lento",
"AdvancedEffectSlowZoomOutDescription": "Il testo inizia leggermente più grande e si riduce gradualmente alle dimensioni normali per tutta la durata del sottotitolo",
Expand Down
5 changes: 4 additions & 1 deletion src/UI/Assets/Languages/Ukrainian.json
Original file line number Diff line number Diff line change
Expand Up @@ -2475,8 +2475,11 @@
"advancedEffectHeartsDescription": "Криволінійні серця в трьох формах падають згори екрану, перекочуючись і зникаючи впродовж кожного субтитра",
"advancedEffectWordSpacing": "Пропуск між словами",
"advancedEffectWordSpacingDescription": "Збільшує відстань між словами за допомогою тегу \\fsp для кращого читання",
"advancedEffectFancyKaraokeAutoDetectActiveWord": "Автоматично визначати активне слово",
"advancedEffectFancyKaraokeGlow": "Виділіть активне слово",
"advancedEffectFancyKaraokeActiveColor": "Активний колір",
"advancedEffectFancyKaraokeInactiveColor": "Неактивний колір",
"advancedEffectFancyKaraokeInactiveOpacity": "Непрозорість неактивного (0-255)",
"advancedEffectFancyKaraokeGlowColor": "Колір підсвічування активного слова",
"advancedEffectWordSpacingPixels": "Пропуск (пікселі)",
"advancedEffectSlowZoomOut": "Повільне зменшення",
"advancedEffectSlowZoomOutDescription": "Текст починається трохи більшим і повільно зменшується до нормального розміру протягом тривалості субтитрів",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,37 @@ public AssaApplyAdvancedEffectWindow(AssaApplyAdvancedEffectViewModel vm)
}
else if (item is AdvancedEffectFancyKaraoke fancyKaraokeItem)
{
var autoDetectActiveWordRow = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8, Margin = new Thickness(0, 6, 0, 0) };
autoDetectActiveWordRow.Children.Add(new TextBlock { Text = Se.Language.Assa.AdvancedEffectFancyKaraokeAutoDetectActiveWord, VerticalAlignment = VerticalAlignment.Center, FontSize = 12 });
autoDetectActiveWordRow.Children.Add(UiUtil.MakeCheckBox(fancyKaraokeItem, nameof(AdvancedEffectFancyKaraoke.AutoDetectActiveWord)));

var activeGlowRow = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8, Margin = new Thickness(0, 4, 0, 0) };
activeGlowRow.Children.Add(new TextBlock { Text = Se.Language.Assa.AdvancedEffectFancyKaraokeGlow, VerticalAlignment = VerticalAlignment.Center, FontSize = 12 });
var applyGlowCheckBox = UiUtil.MakeCheckBox(fancyKaraokeItem, nameof(AdvancedEffectFancyKaraoke.ApplyGlow));
activeGlowRow.Children.Add(applyGlowCheckBox);
var activeGlowColorPicker = UiUtil.MakeColorPicker(fancyKaraokeItem, nameof(AdvancedEffectFancyKaraoke.GlowColor));
// Show/hide the row based on the checkbox state
activeGlowColorPicker.Bind(ColorPicker.IsVisibleProperty, new Binding(nameof(CheckBox.IsChecked)) { Source = applyGlowCheckBox, Mode = BindingMode.OneWay });
activeGlowRow.Children.Add(activeGlowColorPicker);

var activeColorRow = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8, Margin = new Thickness(0, 4, 0, 0) };
activeColorRow.Children.Add(new TextBlock { Text = Se.Language.Assa.AdvancedEffectFancyKaraokeActiveColor, VerticalAlignment = VerticalAlignment.Center, FontSize = 12 });
activeColorRow.Children.Add(UiUtil.MakeColorPicker(fancyKaraokeItem, nameof(AdvancedEffectFancyKaraoke.ActiveWordColor)));

var inactiveColorRow = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8, Margin = new Thickness(0, 4, 0, 0) };
inactiveColorRow.Children.Add(new TextBlock { Text = Se.Language.Assa.AdvancedEffectFancyKaraokeInactiveColor, VerticalAlignment = VerticalAlignment.Center, FontSize = 12 });
inactiveColorRow.Children.Add(UiUtil.MakeColorPicker(fancyKaraokeItem, nameof(AdvancedEffectFancyKaraoke.InactiveWordColor)));

var opacityRow = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8, Margin = new Thickness(0, 6, 0, 0) };
opacityRow.Children.Add(new TextBlock { Text = Se.Language.Assa.AdvancedEffectFancyKaraokeInactiveOpacity, VerticalAlignment = VerticalAlignment.Center, FontSize = 12 });
opacityRow.Children.Add(UiUtil.MakeNumericUpDownInt(0, 255, 0x90, 130, fancyKaraokeItem, nameof(AdvancedEffectFancyKaraoke.InactiveAlpha)));
var colorRow = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8, Margin = new Thickness(0, 4, 0, 0) };
colorRow.Children.Add(new TextBlock { Text = Se.Language.Assa.AdvancedEffectFancyKaraokeGlowColor, VerticalAlignment = VerticalAlignment.Center, FontSize = 12 });
colorRow.Children.Add(UiUtil.MakeColorPicker(fancyKaraokeItem, nameof(AdvancedEffectFancyKaraoke.GlowColor)));

var settingsStack = new StackPanel { Spacing = 2 };
settingsStack.Children.Add(autoDetectActiveWordRow);
settingsStack.Children.Add(activeGlowRow);
settingsStack.Children.Add(activeColorRow);
settingsStack.Children.Add(inactiveColorRow);
settingsStack.Children.Add(opacityRow);
settingsStack.Children.Add(colorRow);
panel.Children.Add(settingsStack);
}
else if (item is AdvancedEffectWordSpacing wordSpacingItem)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,57 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace Nikse.SubtitleEdit.Features.Assa.AssaApplyAdvancedEffect.Effects;

/// <summary>
/// Fancy karaoke effect for subtitles with a pre-marked active word (via {\u1} underline
/// or a highlight \1c color). Dims inactive words and pops the active word with a glow.
///
/// Set AutoDetectActiveWord = true to auto-sequence whole words (word-by-word karaoke).
/// Set ActiveWordColor to choose the color used for the active word.
/// Set ApplyGlow to enable/disable the glow effect for the active word.
/// </summary>
public class AdvancedEffectFancyKaraoke : IAdvancedEffectDisplay
{
public string Name => Se.Language.Assa.AdvancedEffectFancyKaraoke;
public string Description => Se.Language.Assa.AdvancedEffectFancyKaraokeDescription;
public bool UsesAudio => false;

/// <summary>
/// When true, ignore explicit active-word markup and auto-sequence whole words
/// (one subtitle per word, timing = duration / wordCount).
/// </summary>
public bool AutoDetectActiveWord { get; set; } = false;

/// <summary>
/// Alpha value used for inactive words (ASS \alpha uses hex).
/// </summary>
public int InactiveAlpha { get; set; } = 100;

/// <summary>
/// Glow color used for the active word's 3c (ASS BGR).
/// </summary>
public Color GlowColor { get; set; } = Color.FromRgb(0xFF, 0xDD, 0x00); // &H00DDFF& in ASS BGR

/// <summary>
/// Color used for the active word's primary color (\1c). Default white.
/// </summary>
public Color ActiveWordColor { get; set; } = Color.FromRgb(0xFF, 0xFF, 0xFF);

/// <summary>
/// Color used for inactive words' primary color (\1c). Default dim gray.
/// This ensures color is explicitly reset after the active word so subsequent words are not colored.
/// </summary>
public Color InactiveWordColor { get; set; } = Color.FromRgb(0x80, 0x80, 0x80);

/// <summary>
/// When true, apply the glow (3c, blur, scale transform) to the active word.
/// When false, the active word will be colored but without the glow/pop styling.
/// </summary>
public bool ApplyGlow { get; set; } = true;

public override string ToString() => Name;

private static string ToAssColor(Color color) => $"&H{color.B:X2}{color.G:X2}{color.R:X2}&";
Expand All @@ -32,13 +68,27 @@ public List<SubtitleLineViewModel> ApplyEffect(
var result = new List<SubtitleLineViewModel>();
foreach (var sub in subtitles)
{
// If auto mode is enabled, attempt auto-sequencing by words.
if (AutoDetectActiveWord)
{
var auto = TryAutoSequenceByWords(sub);
if (auto != null)
{
result.AddRange(auto);
continue;
}
// If auto failed (e.g., no words), fall through to normal behavior.
}

// Normal behavior: parse active word from explicit markup and render single line.
var parsed = ParseActiveWord(sub.Text);
var newSub = new SubtitleLineViewModel(sub, generateNewId: true);
string posTags = ExtractPositionalTags(sub.Text);
var sb = new StringBuilder();
if (!string.IsNullOrEmpty(posTags)) sb.Append(posTags);

string inactiveTags = $"{{\\alpha&H{InactiveAlpha:X2}&\\bord0\\shad0\\blur0\\fscx100\\fscy100}}";
// Inactive tags now explicitly set \1c to InactiveWordColor so color resets after active word.
string inactiveTags = $"{{\\alpha&H{InactiveAlpha:X2}&\\1c{ToAssColor(InactiveWordColor)}\\bord0\\shad0\\blur0\\fscx100\\fscy100}}";

if (parsed == null || string.IsNullOrEmpty(parsed.Value.Active))
{
Expand All @@ -55,8 +105,22 @@ public List<SubtitleLineViewModel> ApplyEffect(
if (!string.IsNullOrEmpty(before))
sb.Append(inactiveTags).Append(before);

sb.Append("{\\alpha&H00&\\1c&HFFFFFF&\\bord2\\shad0\\blur4\\3c" + ToAssColor(GlowColor) +
"\\fscx115\\fscy115\\t(0,250,\\fscx100\\fscy100)}").Append(active);
// Build active tag depending on ApplyGlow
string activeTag;
if (ApplyGlow)
{
activeTag = "{\\alpha&H00&\\1c" + ToAssColor(ActiveWordColor) +
"\\bord2\\shad0\\blur4\\3c" + ToAssColor(GlowColor) +
"\\fscx115\\fscy115\\t(0,250,\\fscx100\\fscy100)}";
}
else
{
// No glow: only set alpha and primary color; keep border/shadow minimal
activeTag = "{\\alpha&H00&\\1c" + ToAssColor(ActiveWordColor) +
"\\bord0\\shad0\\blur0\\fscx100\\fscy100}";
}

sb.Append(activeTag).Append(active);

if (!string.IsNullOrEmpty(after))
sb.Append(inactiveTags).Append(after);
Expand All @@ -68,6 +132,125 @@ public List<SubtitleLineViewModel> ApplyEffect(
return result;
}

/// <summary>
/// Attempt to auto-sequence the subtitle by whole words.
/// Returns a list of generated SubtitleLineViewModel if successful, otherwise null.
/// </summary>
private List<SubtitleLineViewModel>? TryAutoSequenceByWords(SubtitleLineViewModel sub)
{
// Remove SSA tags for tokenization and normalize newlines.
var cleanText = Utilities.RemoveSsaTags(sub.Text);
if (string.IsNullOrWhiteSpace(cleanText)) return null;
cleanText = cleanText.Replace("\r\n", "\n").Replace("\r", "\n");

// Tokenize into runs of non-whitespace (words/punctuation) and whitespace (spaces/newlines)
var matches = Regex.Matches(cleanText, @"\S+|\s+");
var tokens = new List<string>(matches.Count);
foreach (Match m in matches) tokens.Add(m.Value);

// Count words (non-whitespace tokens)
var wordCount = tokens.Count(t => !string.IsNullOrWhiteSpace(t));
if (wordCount == 0) return null;

var totalMs = sub.Duration.TotalMilliseconds;
var msPerWord = totalMs / wordCount;

string posTags = ExtractPositionalTags(sub.Text);
// Inactive tags explicitly include InactiveWordColor so color is reset for non-active words and spaces.
string inactiveTags = $"{{\\alpha&H{InactiveAlpha:X2}&\\1c{ToAssColor(InactiveWordColor)}\\bord0\\shad0\\blur0\\fscx100\\fscy100}}";

// Build active tag depending on ApplyGlow
string activeTags;
if (ApplyGlow)
{
activeTags = "{\\alpha&H00&\\1c" + ToAssColor(ActiveWordColor) +
"\\bord2\\shad0\\blur4\\3c" + ToAssColor(GlowColor) +
"\\fscx115\\fscy115\\t(0,250,\\fscx100\\fscy100)}";
}
else
{
activeTags = "{\\alpha&H00&\\1c" + ToAssColor(ActiveWordColor) +
"\\bord0\\shad0\\blur0\\fscx100\\fscy100}";
}

var result = new List<SubtitleLineViewModel>();

for (int w = 0; w < wordCount; w++)
{
var line = new SubtitleLineViewModel(sub, generateNewId: true);
var start = sub.StartTime.Add(TimeSpan.FromMilliseconds(w * msPerWord));
var end = w == wordCount - 1
? sub.EndTime
: sub.StartTime.Add(TimeSpan.FromMilliseconds((w + 1) * msPerWord));
line.StartTime = start;
line.EndTime = end;

// Build text for this word index
var sb = new StringBuilder();
if (!string.IsNullOrEmpty(posTags)) sb.Append(posTags);

string? currentColor = null;
int localWordCounter = 0;

foreach (var token in tokens)
{
if (string.IsNullOrWhiteSpace(token))
{
// whitespace may contain newlines; convert '\n' to SSA line break and ensure whitespace is dimmed
if (token.Contains("\n"))
{
var parts = token.Split('\n');
for (var p = 0; p < parts.Length; p++)
{
if (parts[p].Length > 0)
{
// Ensure spaces before newline are dimmed
if (currentColor != inactiveTags)
{
sb.Append(inactiveTags);
currentColor = inactiveTags;
}
sb.Append(parts[p]);
}
if (p < parts.Length - 1)
{
sb.Append(@"\N");
currentColor = null; // reset color after line break
}
}
}
else
{
// regular spaces: ensure they are dimmed (inactive)
if (currentColor != inactiveTags)
{
sb.Append(inactiveTags);
currentColor = inactiveTags;
}
sb.Append(token);
}
continue;
}

// Non-whitespace token = a word/punctuation group
// Only the current word is active
var colorTag = localWordCounter == w ? activeTags : inactiveTags;
if (colorTag != currentColor)
{
sb.Append(colorTag);
currentColor = colorTag;
}
sb.Append(token);
localWordCounter++;
}

line.Text = sb.ToString();
result.Add(line);
}

return result;
}

private static string ExtractPositionalTags(string text)
{
var firstBlock = System.Text.RegularExpressions.Regex.Match(text, @"^\{([^}]*)\}");
Expand Down Expand Up @@ -113,9 +296,6 @@ private static (string Before, string Active, string After)? ParseActiveWord(str
string highlightColor;
if (colorCounts.Count == 1)
{
// Only one color tag in the line — treat it as the active word only if it is
// distinctly colorful (e.g. \c&H00FFFF& cyan). A plain white or grey tag is
// just a style reset and should not be mistaken for a highlight.
var singleColor = colorCounts.Keys.First();
if (!IsColorfulAssColor(singleColor)) return null;
highlightColor = singleColor;
Expand Down Expand Up @@ -184,4 +364,3 @@ private static bool IsColorfulAssColor(string assColor)
return Math.Max(r, Math.Max(g, b)) - Math.Min(r, Math.Min(g, b)) > 40;
}
}

Loading