C
You've gone the right way, you're going to break it to the individual. PathYou're a good idea.Let's make it work.For starters, the functionality of the loser is already complicated, so we'll take it into a separate one. UserControl♪ Then, every controller, let him take responsibility for one line. So we can't find a piece of geometry, just cut that line with help. ClipHuh. At the entrance UserControl We'll give the results of the geometry of the text. Create)<UserControl x:Class="KaraokeText.SingleLine"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Path Name="Target" Stroke="Black" StrokeThickness="0.5">
<Path.Fill>
<LinearGradientBrush StartPoint="0,0.5" EndPoint="1,0.5">
<GradientStop Color="Black" Offset="0" x:Name="TargetFrom"/>
<GradientStop Color="White" Offset="0" x:Name="TargetTo" />
</LinearGradientBrush>
</Path.Fill>
<Path.Resources>
<Storyboard x:Key="AnimationStoryboard">
<DoubleAnimation Duration="00:00:00.25"
Storyboard.TargetName="TargetFrom"
Storyboard.TargetProperty="Offset">
<DoubleAnimation.EasingFunction>
<CubicEase EasingMode="EaseIn"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation Duration="00:00:00.25"
Storyboard.TargetName="TargetTo"
Storyboard.TargetProperty="Offset">
<DoubleAnimation.EasingFunction>
<CubicEase EasingMode="EaseOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</Path.Resources>
</Path>
</UserControl>
The code-behind will be animation:public partial class SingleLine : UserControl
{
List<Rect> boundingBoxes;
double extent;
public SingleLine()
{
InitializeComponent();
}
public SingleLine(Geometry geo, List<Rect> boundingBoxes, double totalExtent) : this()
{
Target.Data = geo;
extent = totalExtent;
Rect clip = boundingBoxes.Aggregate(Rect.Union);
Clip = new RectangleGeometry(clip);
this.boundingBoxes = boundingBoxes;
}
public async Task Play()
{
var storyboard = (Storyboard)Target.Resources["AnimationStoryboard"];
var fromAnimation = (DoubleAnimation)storyboard.Children[0];
var toAnimation = (DoubleAnimation)storyboard.Children[1];
foreach (var b in boundingBoxes)
{
await Task.Delay(250); // перерыв между буквами
fromAnimation.From = b.Left / extent;
fromAnimation.To = b.Right / extent;
toAnimation.From = b.Left / extent;
toAnimation.To = b.Right / extent;
storyboard.Begin();
await Task.Delay(250); // дождёмся конца анимации
}
}
}
Why do we need to be so complicated? Clip and totalExtent? Unfortunately, I didn't find a way to eat just the right part of the geometry. That's why we're getting in. Total geometry, we only want to show the current line. To that end, we are computing a rectangle corresponding to the necessary part of the geometry (current line) and distracting the balance with the help of the ClipHuh. But our calculation of the coefficientsb.Left / extent (e.g.) require percentage of total width PathHa, not the width of the current line! (laughs) Path gets the geometry of the whole line, including the other lines, too.) That's why we have to transfer the overall width.Now the basic code. It was easier because part of the functionality was separated. We can't put one in it, fixed. PathBecause we don't know the number of lines in advance. So the contrasts will be added dynamically.The main window looks just like:<Window x:Class="KaraokeText.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Тест" Height="350" Width="525">
<Grid HorizontalAlignment="Center" VerticalAlignment="Center" Name="Container">
<TextBlock Name="Source" FontSize="24" Visibility="Hidden" TextWrapping="Wrap"
Text="А-а, в Африке реки вот такой ширины 
А-а, в Африке горы вот такой вышины"/>
</Grid>
</Window>
And code-behind:public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Loaded += (o, args) => Create(); // вначале запустим Create
PreviewKeyDown += (o, args) => Play(); // а по нажатию клавиши - Play
}
// список контролов, отображающих строки
List<SingleLine> lineControls = new List<SingleLine>();
void Create() // https://msdn.microsoft.com/en-us/library/ms745816(v=vs.110).aspx
{
TextBlock tb = Source;
var text = tb.Text;
FormattedText formattedText = new FormattedText(
text,
CultureInfo.GetCultureInfo("en-US"),
FlowDirection.LeftToRight,
new Typeface(
tb.FontFamily,
tb.FontStyle,
tb.FontWeight,
tb.FontStretch),
tb.FontSize,
Brushes.Black);
// установили максимальную ширину, чтобы текст был разбит на части
formattedText.MaxTextWidth = Source.ActualWidth;
var boundingBoxes = // побуквенная ширина и позиции
Enumerable.Range(0, text.Length)
.Where(k => !char.IsWhiteSpace(text[k]))
.Select(k => formattedText.BuildHighlightGeometry(new Point(), k, 1)
.Bounds)
.ToList();
// вычисляем охватывающий прямоугольник всех прямоугольников
var totalBb = boundingBoxes.Aggregate(Rect.Union);
var totalExtent = totalBb.Width;
List<List<Rect>> boundingBoxesByLine = new List<List<Rect>>();
List<Rect> currentLine = null;
double lastRectBottom = double.NegativeInfinity;
foreach (var rect in boundingBoxes)
{
// проверка на новую строку. если верх текущего прямоугольника там же,
// где низ предыдущего прямоугольника, или ещё ниже - новая строка, иначе нет
if (rect.Top >= lastRectBottom)
{
// добавим старую строку в список строк
if (currentLine != null)
boundingBoxesByLine.Add(currentLine);
// новый пустой контейнер прямоугольников для новой строки
currentLine = new List<Rect>();
}
currentLine.Add(rect);
lastRectBottom = rect.Bottom;
}
if (currentLine != null) // последнюю строку не теряем
boundingBoxesByLine.Add(currentLine);
// стащили геометрию у текста...
var geo = formattedText.BuildGeometry(new Point());
// строим по контролу для каждой строки:
foreach (var line in boundingBoxesByLine)
{
// ... отдавая ему геометрию:
var lineControl = new SingleLine(geo, line, totalExtent);
Container.Children.Add(lineControl);
lineControls.Add(lineControl);
}
}
async void Play()
{
// проигрываем просто построчно
foreach (var line in lineControls)
await line.Play();
}
}
Everything!Result: