Skip to content

Instantly share code, notes, and snippets.

@badamczewski
Created December 1, 2020 14:11
Show Gist options
  • Save badamczewski/06d9c86e6d78fc79905f943cb3545f51 to your computer and use it in GitHub Desktop.
Save badamczewski/06d9c86e6d78fc79905f943cb3545f51 to your computer and use it in GitHub Desktop.
Text Morphing in WPF
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace WPFAnimations
{
public static class Morph
{
public static bool Collapse(PathGeometry sourceGeometry, double progress)
{
int count = sourceGeometry.Figures.Count;
for (int i = 0; i < sourceGeometry.Figures.Count; i++)
{
count -= MorphCollapse(sourceGeometry.Figures[i], progress);
}
if (count <= 0) return true;
return false;
}
private static void MoveFigure(PathFigure source, double p, double progress)
{
PolyLineSegment segment = (PolyLineSegment)source.Segments[0];
for (int i = 0; i < segment.Points.Count; i++)
{
var fromX = segment.Points[i].X;
var fromY = segment.Points[i].Y;
var x = fromX + p;
segment.Points[i] = new Point(x, fromY);
}
var newX = source.StartPoint.X + p;
source.StartPoint = new Point(newX, source.StartPoint.Y);
}
private static bool DoFiguresOverlap(PathFigureCollection figures, int index0, int index1, int index2)
{
if (index2 < figures.Count && index0 >= 0)
{
PathGeometry g0 = new PathGeometry(new[] { figures[index2] });
PathGeometry g1 = new PathGeometry(new[] { figures[index1] });
PathGeometry g2 = new PathGeometry(new[] { figures[index0] });
var result0 = g0.FillContainsWithDetail(g1);
var result1 = g0.FillContainsWithDetail(g2);
return
(result0 == IntersectionDetail.FullyContains ||
result0 == IntersectionDetail.FullyInside) &&
(result1 == IntersectionDetail.FullyContains ||
result1 == IntersectionDetail.FullyInside);
}
return false;
}
private static bool DoFiguresOverlap(PathFigureCollection figures, int index0, int index1)
{
if (index1 < figures.Count && index0 >= 0)
{
PathGeometry g1 = new PathGeometry(new[] { figures[index1] });
PathGeometry g2 = new PathGeometry(new[] { figures[index0] });
var result = g1.FillContainsWithDetail(g2);
return result == IntersectionDetail.FullyContains || result == IntersectionDetail.FullyInside;
}
return false;
}
private static void CollapseFigure(PathFigure figure)
{
var points = ((PolyLineSegment)figure.Segments[0]).Points;
var centroid = GetCentroid(points, points.Count);
for (int p = 0; p < points.Count; p++)
{
points[p] = centroid;
}
figure.StartPoint = centroid;
}
public static void To(PathGeometry sourceGeometry, PathGeometry geometry, Range sourceRange, double progress)
{
int k = 0;
for (int i = sourceRange.Start.Value; i < sourceRange.End.Value; i++)
{
MorphFigure(sourceGeometry.Figures[i], geometry.Figures[k], progress);
k++;
}
}
public static List<PathGeometry> ToCache(PathGeometry source, PathGeometry target, double speed)
{
PowerEase powerEase = new PowerEase();
int steps = (int)(1 / speed);
double p = speed;
List<PathGeometry> cache = new List<PathGeometry>(steps);
for (int i = 0; i < steps; i++)
{
var clone = source.Clone();
var easeP = powerEase.Ease(p);
To(clone, target, easeP);
p += speed;
cache.Add(clone);
}
return cache;
}
public static void To(PathGeometry source, PathGeometry target, double progress)
{
//
// Clone figures.
//
if (source.Figures.Count < target.Figures.Count)
{
var last = source.Figures.Last();
var toAdd = target.Figures.Count - source.Figures.Count;
for (int i = 0; i < toAdd; i++)
{
var clone = last.Clone();
source.Figures.Add(clone);
}
}
//
// Contract the source, the problem here is that if we have a shape
// like 'O' where we need to cut a hole in a shape we will butcher such character
// since all excess shapes will be stored under this shape.
//
// We need to move and collapse them when moving.
// So lets collapse then to a single point.
//
else if (source.Figures.Count > target.Figures.Count)
{
var toAdd = source.Figures.Count - target.Figures.Count;
var lastIndex = target.Figures.Count - 1;
for (int i = 0; i < toAdd; i++)
{
var clone = target.Figures[lastIndex].Clone();
//var clone = target.Figures[(lastIndex - (i % (lastIndex + 1)))].Clone();
//
// This is a temp solution but it works well for now.
// We try to detect if our last shape has an overlapping geometry
// if it does then we will clone the previrous shape.
//
if (lastIndex > 0)
{
if (DoFiguresOverlap(target.Figures, lastIndex - 1, lastIndex))
{
if (DoFiguresOverlap(target.Figures, lastIndex - 2, lastIndex - 1, lastIndex))
{
clone = target.Figures[lastIndex - 3].Clone();
}
else if (lastIndex - 2 > 0)
{
clone = target.Figures[lastIndex - 2].Clone();
}
else
{
CollapseFigure(clone);
}
}
}
else
{
CollapseFigure(clone);
}
target.Figures.Add(clone);
}
}
int[] map = new int[source.Figures.Count];
for (int i = 0; i < map.Length; i++)
map[i] = -1;
//
// Morph Closest Figures.
//
for (int i = 0; i < source.Figures.Count; i++)
{
double closest = double.MaxValue;
int closestIndex = -1;
for (int j = 0; j < target.Figures.Count; j++)
{
if (map.Contains(j))
continue;
var len = Point.Subtract(source.Figures[i].StartPoint, target.Figures[j].StartPoint).LengthSquared;
if (len < closest)
{
closest = len;
closestIndex = j;
}
}
map[i] = closestIndex;
}
for (int i = 0; i < source.Figures.Count; i++)
MorphFigure(source.Figures[i], target.Figures[map[i]], progress);
}
public static void MorphFigure(PathFigure source, PathFigure target, double progress)
{
PolyLineSegment sourceSegment = (PolyLineSegment)source.Segments[0];
PolyLineSegment targetSegment = (PolyLineSegment)target.Segments[0];
if (sourceSegment.Points.Count < targetSegment.Points.Count)
{
//
// Add points to segment.
//
var toAdd = targetSegment.Points.Count - sourceSegment.Points.Count;
for (int i = 0; i < toAdd; i++)
{
sourceSegment.Points.Add(source.StartPoint);
}
}
else if (sourceSegment.Points.Count > targetSegment.Points.Count)
{
//
// Add points to segment.
//
var toAdd = sourceSegment.Points.Count - targetSegment.Points.Count;
for (int i = 0; i < toAdd; i++)
{
targetSegment.Points.Add(target.StartPoint);
}
}
//
// Interpolate from source to target.
//
if (progress >= 1)
{
for (int i = 0; i < sourceSegment.Points.Count; i++)
{
var toX = targetSegment.Points[i].X;
var toY = targetSegment.Points[i].Y;
sourceSegment.Points[i] = new Point(toX, toY);
}
source.StartPoint = new Point(target.StartPoint.X, target.StartPoint.Y);
}
else
{
for (int i = 0; i < sourceSegment.Points.Count; i++)
{
var fromX = sourceSegment.Points[i].X;
var toX = targetSegment.Points[i].X;
var fromY = sourceSegment.Points[i].Y;
var toY = targetSegment.Points[i].Y;
if (fromX != toX || fromY != toY)
{
var x = Interpolate(fromX, toX, progress);
var y = Interpolate(fromY, toY, progress);
sourceSegment.Points[i] = new Point(x, y);
}
}
if (source.StartPoint.X != target.StartPoint.X ||
source.StartPoint.Y != target.StartPoint.Y)
{
var newX = Interpolate(source.StartPoint.X, target.StartPoint.X, progress);
var newY = Interpolate(source.StartPoint.Y, target.StartPoint.Y, progress);
source.StartPoint = new Point(newX, newY);
}
}
}
public static int MorphCollapse(PathFigure source, double progress)
{
PolyLineSegment sourceSegment = (PolyLineSegment)source.Segments[0];
//
// Find Centroid
//
var centroid = GetCentroid(sourceSegment.Points, sourceSegment.Points.Count);
for (int i = 0; i < sourceSegment.Points.Count; i++)
{
var fromX = sourceSegment.Points[i].X;
var toX = centroid.X;
var fromY = sourceSegment.Points[i].Y;
var toY = centroid.Y;
var x = Interpolate(fromX, toX, progress);
var y = Interpolate(fromY, toY, progress);
sourceSegment.Points[i] = new Point(x, y);
}
var newX = Interpolate(source.StartPoint.X, centroid.X, progress);
var newY = Interpolate(source.StartPoint.Y, centroid.Y, progress);
source.StartPoint = new Point(newX, newY);
if (centroid.X - newX < 0.005)
{
return 1;
}
return 0;
}
public static Point GetCentroid(PointCollection nodes, int count)
{
double x = 0, y = 0, area = 0, k;
Point a, b = nodes[count - 1];
for (int i = 0; i < count; i++)
{
a = nodes[i];
k = a.Y * b.X - a.X * b.Y;
area += k;
x += (a.X + b.X) * k;
y += (a.Y + b.Y) * k;
b = a;
}
area *= 3;
return (area == 0) ? new Point() : new Point(x /= area, y /= area);
}
public static double Interpolate(double from, double to, double progress)
{
return from + (to - from) * progress;
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Media;
using System.Windows.Threading;
using WPFAnimations.Visuals.Animation;
namespace WPFAnimations.Visuals
{
public class VisualBase
{
protected Dispatcher dispatcher;
public double X { get; set; }
public double Y { get; set; }
public double Scale { get; set; }
public double Rotate { get; set; }
public TranslateTransform TranslateTransform { get; set; }
public RotateTransform RotateTransform { get; set; }
public ScaleTransform ScaleTransform { get; set; }
public VisualBase(Dispatcher dispatcher)
{
this.dispatcher = dispatcher;
}
public void Move(double x, double y, double scale = 1, double rotate = 0, double speed = 400, double delay = 0)
{
double scaleTranslate = 1 / scale;
double top = Y;
double left = X;
dispatcher.Invoke(() =>
{
var scaleAnim = AnimationHelper.GetDoubleAnimation(scale, speed, delay);
var animY = AnimationHelper.GetDoubleAnimation(y - (top * scaleTranslate), speed, delay);
var animX = AnimationHelper.GetDoubleAnimation(x - (left * scaleTranslate), speed, delay);
var rotateAnim = AnimationHelper.GetDoubleAnimation(rotate, speed, delay);
RotateTransform.Angle = rotate;
TranslateTransform.BeginAnimation(TranslateTransform.YProperty, animY);
TranslateTransform.BeginAnimation(TranslateTransform.XProperty, animX);
ScaleTransform.BeginAnimation(ScaleTransform.ScaleXProperty, scaleAnim);
ScaleTransform.BeginAnimation(ScaleTransform.ScaleYProperty, scaleAnim);
RotateTransform.BeginAnimation(RotateTransform.AngleProperty, rotateAnim);
});
this.Scale = scale;
}
}
}
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Windows.Threading;
using WPFAnimations.Visuals.Animation;
namespace WPFAnimations.Visuals
{
public class SimpleVisualText : VisualBase
{
private Panel canvas;
public string Text { get; set; }
public TextBlock Block { get; set; }
public LinearGradientBrush Fill { get; set; }
public SolidColorBrush Stroke { get; set; }
public GradientStop StartColor { get; set; }
public GradientStop EndColor { get; set; }
public Color PrimaryColor { get; set; }
public DoubleCollection StrokeArray { get; set; }
public double StrokeDashOffset { get; set; }
public double FontSize { get; set; }
public string FontName { get; set; }
public bool IsOptimized { get; set; }
private SimpleVisualText() : base(null) { }
public SimpleVisualText(double x, double y, Panel canvas, Dispatcher dispatcher) : base(dispatcher)
{
X = x;
Y = y;
this.canvas = canvas;
}
public void Create(string text, Color color,
string font = "Nexa Bold",
double fontSize = 88)
{
Text = text;
FontSize = fontSize;
FontName = font;
dispatcher.Invoke(() =>
{
var stopStart = new GradientStop() { Color = color, Offset = -1 };
var stopEnd = new GradientStop() { Color = Colors.Transparent, Offset = -1 };
LinearGradientBrush linearGradientBrush = new LinearGradientBrush();
linearGradientBrush.StartPoint = new Point(0, 0);
linearGradientBrush.EndPoint = new Point(1, 0);
linearGradientBrush.GradientStops = new GradientStopCollection();
linearGradientBrush.GradientStops.Add(stopStart);
linearGradientBrush.GradientStops.Add(stopEnd);
Block = new TextBlock();
Block.Text = text;
Block.FontFamily = new FontFamily(FontName);
Block.FontSize = FontSize;
Block.Foreground = linearGradientBrush;
var group = new TransformGroup();
TranslateTransform = new TranslateTransform() { X = this.X, Y = this.Y };
ScaleTransform = new ScaleTransform();
RotateTransform = new RotateTransform();
group.Children.Add(TranslateTransform);
group.Children.Add(ScaleTransform);
group.Children.Add(RotateTransform);
Block.RenderTransform = group;
StartColor = stopStart;
EndColor = stopEnd;
Fill = linearGradientBrush;
PrimaryColor = color;
canvas.Children.Add(Block);
});
}
public void Remove()
{
dispatcher.Invoke(() =>
{
canvas.Children.Remove(Block);
});
}
public void Optimize()
{
dispatcher.Invoke(() =>
{
Block.Foreground = new SolidColorBrush(PrimaryColor);
Block.Foreground.Freeze();
IsOptimized = true;
});
}
public void UnOptimize()
{
dispatcher.Invoke(() =>
{
Block.Foreground = Fill;
IsOptimized = false;
});
}
public void Show(double speed = 400, double delay = 0)
{
ShowFill(speed * 2, delay + (speed / 4));
}
public void Hide(double speed = 400, double delay = 0)
{
HideFill(speed * 2, delay + (speed / 4));
}
public void Freeze() { }
public void HideOpacity(double speed = 400, double delay = 0)
{
dispatcher.Invoke(() =>
{
this.Block.BeginAnimation(TextBlock.OpacityProperty,
AnimationHelper.GetDoubleAnimation(0, speed, delay));
});
}
public void ShowOpacity(double speed = 400, double delay = 0)
{
dispatcher.Invoke(() =>
{
this.Block.BeginAnimation(TextBlock.OpacityProperty,
AnimationHelper.GetDoubleAnimation(1, speed, delay));
});
}
public void ShowFill(double speed = 400, double delay = 0)
{
dispatcher.Invoke(() =>
{
if (IsOptimized)
UnOptimize();
var anim = AnimationHelper.GetDoubleAnimation(1, speed, delay);
StartColor.BeginAnimation(GradientStop.OffsetProperty, anim);
EndColor.BeginAnimation(GradientStop.OffsetProperty, anim);
});
}
public void HideFill(double speed = 400, double delay = 0)
{
dispatcher.Invoke(() =>
{
if (IsOptimized)
UnOptimize();
var anim = AnimationHelper.GetDoubleAnimation(-1, speed, delay);
StartColor.BeginAnimation(GradientStop.OffsetProperty, anim);
EndColor.BeginAnimation(GradientStop.OffsetProperty, anim);
});
}
}
public class VisualText : VisualBase
{
private Panel canvas;
public string Text { get; set; }
public Path Path { get; set; }
public LinearGradientBrush Fill { get; set; }
public SolidColorBrush Stroke { get; set; }
public GradientStop StartColor { get; set; }
public GradientStop EndColor { get; set; }
public Color PrimaryColor { get; set; }
public DoubleCollection StrokeArray { get; set; }
public double StrokeDashOffset { get; set; }
public double FontSize { get; set; }
public string FontName { get; set; }
public bool IsOptimized { get; set; }
private AnimationState<PathGeometry> textAnimationState;
private AnimationState<(Range source, PathGeometry target)[]> multiAnimationState;
private PowerEase powerEase = new PowerEase();
private DoubleAnimation expandAnimation = null;
private VisualText expandState;
private VisualText() : base(null) { }
public VisualText(double x, double y, Panel canvas, Dispatcher dispatcher) : base(dispatcher)
{
X = x;
Y = y;
this.canvas = canvas;
}
private PathGeometry CreateTextGeometry(
double x,
double y,
string text,
string font = "Nexa Bold",
double fontSize = 88)
{
var culture = CultureInfo.InvariantCulture;
var flow = FlowDirection.LeftToRight;
var fontFamily = new FontFamily(font);
var typeface = new Typeface(fontFamily, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal);
var formattedText = new FormattedText(text, culture, flow, typeface, fontSize, Brushes.White, 100);
var geometry = formattedText.BuildGeometry(new System.Windows.Point(x, y));
var pathGeometry = geometry.GetFlattenedPathGeometry();
var unfrozen = pathGeometry.Clone();
return unfrozen;
}
public void CreateWithoutCanvas(string text, Color color,
string font = "Nexa Bold",
double fontSize = 88)
{
Text = text;
FontSize = fontSize;
FontName = font;
var pathGeometry = CreateTextGeometry(X, Y, text, font, fontSize);
var stopStart = new GradientStop() { Color = color, Offset = -1 };
var stopEnd = new GradientStop() { Color = Colors.Transparent, Offset = -1 };
LinearGradientBrush linearGradientBrush = new LinearGradientBrush();
linearGradientBrush.StartPoint = new Point(0, 0);
linearGradientBrush.EndPoint = new Point(1, 0);
linearGradientBrush.GradientStops = new GradientStopCollection();
linearGradientBrush.GradientStops.Add(stopStart);
linearGradientBrush.GradientStops.Add(stopEnd);
Stroke = new SolidColorBrush(color);
Path = new Path();
Path.Data = pathGeometry;
Path.Fill = linearGradientBrush;
Path.StrokeDashArray = new DoubleCollection() { 2000, 2000 };
Path.StrokeDashOffset = -2000;
Path.Stroke = Stroke;
Path.Stroke.Freeze();
var group = new TransformGroup();
TranslateTransform = new TranslateTransform();
ScaleTransform = new ScaleTransform();
RotateTransform = new RotateTransform();
group.Children.Add(TranslateTransform);
group.Children.Add(ScaleTransform);
group.Children.Add(RotateTransform);
Path.RenderTransform = group;
StartColor = stopStart;
EndColor = stopEnd;
Fill = linearGradientBrush;
PrimaryColor = color;
}
public void Remove()
{
dispatcher.Invoke(() =>
{
canvas.Children.Remove(Path);
});
}
public void Create(string text, Color color,
string font = "Nexa Bold",
double fontSize = 88)
{
dispatcher.Invoke(() =>
{
CreateWithoutCanvas(text, color, font, fontSize);
canvas.Children.Add(Path);
});
}
public void Show(double speed = 400, double delay = 0)
{
ShowStroke(speed, delay);
ShowFill(speed * 2, delay + (speed / 4));
}
public void Freeze()
{
dispatcher.Invoke(() =>
{
Path.Data.Freeze();
});
}
public void Optimize()
{
dispatcher.Invoke(() =>
{
Path.Fill = new SolidColorBrush(PrimaryColor);
Path.Fill.Freeze();
Path.Stroke.Freeze();
IsOptimized = true;
});
}
public void UnOptimize()
{
dispatcher.Invoke(() =>
{
Path.Fill = Fill;
IsOptimized = false;
});
}
public void Hide(double speed = 400, double delay = 0)
{
HideStroke(speed, delay);
HideFill(speed * 2, delay + (speed / 4));
}
public void HideOpacity(double speed = 400, double delay = 0)
{
dispatcher.Invoke(() =>
{
this.Path.BeginAnimation(Path.OpacityProperty,
AnimationHelper.GetDoubleAnimation(0, speed, delay));
});
}
public void ShowOpacity(double speed = 400, double delay = 0)
{
dispatcher.Invoke(() =>
{
this.Path.BeginAnimation(Path.OpacityProperty,
AnimationHelper.GetDoubleAnimation(1, speed, delay));
});
}
public void ShowFill(double speed = 400, double delay = 0)
{
dispatcher.Invoke(() =>
{
if (IsOptimized)
UnOptimize();
var anim = AnimationHelper.GetDoubleAnimation(1, speed, delay);
StartColor.BeginAnimation(GradientStop.OffsetProperty, anim);
EndColor.BeginAnimation(GradientStop.OffsetProperty, anim);
});
}
public void HideFill(double speed = 400, double delay = 0)
{
dispatcher.Invoke(() =>
{
if (IsOptimized)
UnOptimize();
var anim = AnimationHelper.GetDoubleAnimation(-1, speed, delay);
StartColor.BeginAnimation(GradientStop.OffsetProperty, anim);
EndColor.BeginAnimation(GradientStop.OffsetProperty, anim);
});
}
public void ShowStroke(double speed = 400, double delay = 0)
{
dispatcher.Invoke(() =>
{
Path.BeginAnimation(Path.StrokeDashOffsetProperty,
AnimationHelper.GetDoubleAnimation(0, speed, delay));
});
}
public void HideStroke(double speed = 400, double delay = 0)
{
dispatcher.Invoke(() =>
{
Path.BeginAnimation(Path.StrokeDashOffsetProperty,
AnimationHelper.GetDoubleAnimation(-2000, speed, delay));
});
}
public void ExpandStep(string[] by,
double offset = -1, double fontSize = -1, string fontName = null,
double speed = 0.025, double delay = 0)
{
dispatcher.Invoke(() =>
{
(Range source, PathGeometry target)[]
pathGeometries = new (Range source, PathGeometry target)[by.Length];
int index = 0;
if (offset == -1)
offset = FontSize / 2;
double position = Path.Data.Bounds.Width + offset;
var pathGeometry = ((PathGeometry)this.Path.Data);
string font = FontName;
double size = FontSize;
if (fontName != null)
font = fontName;
if (fontSize != -1)
size = fontSize;
foreach (var item in by)
{
var to = new VisualText();
var g = to.CreateTextGeometry(X + position, Y, item, font, size);
Range range = new Range(0, 0);
pathGeometries[index++] = (range, g);
position += g.Bounds.Width + offset;
}
multiAnimationState = new AnimationState<(Range source, PathGeometry target)[]>();
multiAnimationState.ProgressIncrement = speed;
multiAnimationState.Object = pathGeometries;
multiAnimationState.Delay = delay;
CompositionTarget.Rendering += RenderStepExpand;
});
}
public void ExpandFrom(string[] by, double offset = -1, double speed = 0.025, double delay = 0)
{
dispatcher.Invoke(() =>
{
(Range source, PathGeometry target)[]
pathGeometries = new (Range source, PathGeometry target)[by.Length];
int index = 0;
if (offset == -1)
offset = FontSize / 2;
double position = Path.Data.Bounds.Width + offset;
var pathGeometry = ((PathGeometry)this.Path.Data);
foreach (var item in by)
{
var to = new VisualText();
var g = to.CreateTextGeometry(X + position, Y, item, FontName, FontSize);
var count = ((PathGeometry)Path.Data).Figures.Count;
for (int i = 0; i < g.Figures.Count; i++)
{
pathGeometry.Figures
.Add(pathGeometry.Figures[pathGeometry.Figures.Count - 1].Clone());
}
Range range = new Range(count, count + g.Figures.Count);
pathGeometries[index++] = (range, g);
position += g.Bounds.Width + offset;
}
multiAnimationState = new AnimationState<(Range source, PathGeometry target)[]>();
multiAnimationState.ProgressIncrement = speed;
multiAnimationState.Object = pathGeometries;
multiAnimationState.Delay = delay;
CompositionTarget.Rendering += RenderExpand;
});
}
public void PreExpand(string by, double speed = 400, double delay = 0)
{
VisualText to = null;
dispatcher.Invoke(() =>
{
to = new VisualText(X, Y, canvas, dispatcher);
to.Create(by, Colors.White, FontName, FontSize);
DoubleAnimation anim = new DoubleAnimation(1, TimeSpan.FromMilliseconds(speed))
{
EasingFunction = new PowerEase { EasingMode = EasingMode.EaseInOut }
};
anim.BeginTime = TimeSpan.FromMilliseconds(delay);
expandAnimation = new DoubleAnimation(1, TimeSpan.FromMilliseconds(speed))
{
EasingFunction = new PowerEase { EasingMode = EasingMode.EaseInOut }
};
expandAnimation.BeginTime = TimeSpan.FromMilliseconds(delay);
//expandAnimation.Completed += OnAnimationExpandDone;
expandState = to;
Move(X + to.Path.Data.Bounds.Width + FontSize / 2, Y);
to.StartColor.BeginAnimation(GradientStop.OffsetProperty, expandAnimation);
to.EndColor.BeginAnimation(GradientStop.OffsetProperty, anim);
});
}
public void Expand(string by, double speed = 400, double delay = 0)
{
VisualText to = null;
dispatcher.Invoke(() =>
{
by = " " + by;
to = new VisualText(X + Path.Data.Bounds.Width, Y, canvas, dispatcher);
to.Create(by, Colors.White, FontName, FontSize);
to.Path.RenderTransform = this.Path.RenderTransform;
DoubleAnimation anim = new DoubleAnimation(1, TimeSpan.FromMilliseconds(speed))
{
EasingFunction = new PowerEase { EasingMode = EasingMode.EaseInOut }
};
anim.BeginTime = TimeSpan.FromMilliseconds(delay);
expandAnimation = new DoubleAnimation(1, TimeSpan.FromMilliseconds(speed))
{
EasingFunction = new PowerEase { EasingMode = EasingMode.EaseInOut }
};
expandAnimation.BeginTime = TimeSpan.FromMilliseconds(delay);
expandAnimation.Completed += OnAnimationExpandDone;
expandState = to;
to.StartColor.BeginAnimation(GradientStop.OffsetProperty, expandAnimation);
to.EndColor.BeginAnimation(GradientStop.OffsetProperty, anim);
});
}
private void OnAnimationExpandDone(object target, EventArgs args)
{
var pathGeometry = Geometry.Combine(this.Path.Data, expandState.Path.Data, GeometryCombineMode.Union, null);
this.Path.Data = pathGeometry;
canvas.Children.Remove(expandState.Path);
expandState = null;
expandAnimation.Completed -= OnAnimationExpandDone;
}
public void MorphCollapse(double speed = 0.01, double delay = 0)
{
dispatcher.Invoke(() =>
{
textAnimationState = new AnimationState<PathGeometry>();
textAnimationState.ProgressIncrement = speed;
textAnimationState.Delay = delay;
CompositionTarget.Rendering += RenderMorphCollapse;
});
}
public void Underline(double speed = 0.01, double delay = 0)
{
dispatcher.Invoke(() =>
{
double offsetY = 10;
var pathGeometry = ((PathGeometry)this.Path.Data);
var pathGeometries = new (Range source, PathGeometry target)[1];
PathGeometry geometry = new PathGeometry();
PathFigure figure = new PathFigure();
figure.StartPoint =
new Point(
this.X + pathGeometry.Bounds.Width / 2,
pathGeometry.Bounds.Bottom + offsetY
);
var segment = new PolyLineSegment();
segment.Points.Add(new Point(figure.StartPoint.X, figure.StartPoint.Y));
figure.Segments.Add(segment);
((PathGeometry)this.Path.Data).Figures.Add(figure);
Range range = new Range(pathGeometry.Figures.Count - 1, pathGeometry.Figures.Count);
PathGeometry rectGeometry = new PathGeometry();
PathFigure rectFigure = new PathFigure();
var rectSegment = new PolyLineSegment();
rectSegment.Points = new PointCollection();
rectSegment.Points.Add(new Point(X, pathGeometry.Bounds.Bottom + offsetY));
rectSegment.Points.Add(new Point(X + pathGeometry.Bounds.Width, pathGeometry.Bounds.Bottom + offsetY));
rectSegment.Points.Add(new Point(X + pathGeometry.Bounds.Width, pathGeometry.Bounds.Bottom + offsetY + 4));
rectSegment.Points.Add(new Point(X, pathGeometry.Bounds.Bottom + offsetY + 4));
rectFigure.Segments.Add(rectSegment);
rectFigure.StartPoint = new Point(X, pathGeometry.Bounds.Bottom + offsetY);
rectGeometry.Figures.Add(rectFigure);
pathGeometries[0] = (range, rectGeometry);
multiAnimationState = new AnimationState<(Range source, PathGeometry target)[]>();
multiAnimationState.ProgressIncrement = speed;
multiAnimationState.Object = pathGeometries;
multiAnimationState.Delay = delay;
CompositionTarget.Rendering += RenderExpand;
});
}
public void MorphToApply(List<PathGeometry> cache)
{
RenderMorphCache render = null;
dispatcher.Invoke(() =>
{
render = new RenderMorphCache(cache, this.Path);
render.Run();
});
}
public List<PathGeometry> MorphToCache(string other, double speed = 0.01, double delay = 0)
{
List<PathGeometry> result = null;
dispatcher.Invoke(() =>
{
VisualText to = new VisualText();
var geometry = to.CreateTextGeometry(X, Y, other, FontName, FontSize);
result = Morph.ToCache((PathGeometry)this.Path.Data, geometry, speed);
});
return result;
}
public void MorphTo(string other, double speed = 0.01, double delay = 0)
{
MorphTo(other, FontName, FontSize, speed, delay);
}
public void MorphTo(string other, string fontName, double fontSize, double speed = 0.01, double delay = 0)
{
dispatcher.Invoke(() =>
{
textAnimationState = new AnimationState<PathGeometry>();
textAnimationState.ProgressIncrement = speed;
VisualText to = new VisualText();
var geometry = to.CreateTextGeometry(X, Y, other, fontName, fontSize);
textAnimationState.Object = geometry;
textAnimationState.Delay = delay;
CompositionTarget.Rendering += RenderMorph;
});
}
public void MorphTo(PathGeometry other, double speed = 0.01, double delay = 0)
{
dispatcher.Invoke(() =>
{
textAnimationState = new AnimationState<PathGeometry>();
textAnimationState.ProgressIncrement = speed;
textAnimationState.Object = other;
textAnimationState.Delay = delay;
CompositionTarget.Rendering += RenderMorph;
});
}
private void RenderStepExpand(object target, EventArgs e)
{
RenderingEventArgs renderingEventArgs = (RenderingEventArgs)e;
if (renderingEventArgs.RenderingTime == multiAnimationState.LastFrame)
return;
multiAnimationState.LastFrame = renderingEventArgs.RenderingTime;
if (textAnimationState.Delay > 0)
{
textAnimationState.Delay--;
return;
}
multiAnimationState.Progress += multiAnimationState.ProgressIncrement;
var progressEase = powerEase.Ease(multiAnimationState.Progress);
//
// Take the first string.
// Clone the source (last).
// Morph.
// Repeat.
//
var g = multiAnimationState.Object[multiAnimationState.State];
var pathGeometry = ((PathGeometry)this.Path.Data);
if (g.source.Start.Value == 0 && g.source.End.Value == 0)
{
int count = pathGeometry.Figures.Count;
for (int i = 0; i < g.target.Figures.Count; i++)
{
pathGeometry.Figures
.Add(pathGeometry.Figures[pathGeometry.Figures.Count - 1].Clone());
}
Range range = new Range(count, count + g.target.Figures.Count);
g.source = range;
multiAnimationState.Object[multiAnimationState.State] = g;
return;
}
//
// Take a range of figures (which is the clone of original figures and morph them)
//
Morph.To((PathGeometry)this.Path.Data, (PathGeometry)g.target, g.source,
progressEase);
if (multiAnimationState.Progress >= 1.0)
{
multiAnimationState.State++;
multiAnimationState.Progress = multiAnimationState.ProgressIncrement;
if (multiAnimationState.State >= multiAnimationState.Object.Length)
CompositionTarget.Rendering -= RenderStepExpand;
}
}
private void RenderExpand(object target, EventArgs e)
{
RenderingEventArgs renderingEventArgs = (RenderingEventArgs)e;
if (renderingEventArgs.RenderingTime == multiAnimationState.LastFrame)
return;
multiAnimationState.LastFrame = renderingEventArgs.RenderingTime;
if (textAnimationState.Delay > 0)
{
textAnimationState.Delay--;
return;
}
multiAnimationState.Progress += multiAnimationState.ProgressIncrement;
var progressEase = powerEase.Ease(multiAnimationState.Progress);
//
// Take a range of figures (which is the clone of original figures and morph them)
//
var g = multiAnimationState.Object[multiAnimationState.State];
Morph.To((PathGeometry)this.Path.Data, (PathGeometry)g.target, g.source,
progressEase);
if (multiAnimationState.Progress >= 1.0)
{
multiAnimationState.State++;
multiAnimationState.Progress = multiAnimationState.ProgressIncrement;
if (multiAnimationState.State >= multiAnimationState.Object.Length)
CompositionTarget.Rendering -= RenderExpand;
}
}
private void RenderMorph(object target, EventArgs e)
{
RenderingEventArgs renderingEventArgs = (RenderingEventArgs)e;
if (renderingEventArgs.RenderingTime == multiAnimationState.LastFrame)
return;
multiAnimationState.LastFrame = renderingEventArgs.RenderingTime;
if (textAnimationState.Delay > 0)
{
textAnimationState.Delay--;
return;
}
textAnimationState.Progress += textAnimationState.ProgressIncrement;
var progressEase = powerEase.Ease(textAnimationState.Progress);
Morph.To((PathGeometry)Path.Data, (PathGeometry)textAnimationState.Object,
progressEase);
if (textAnimationState.Progress >= 1.0)
{
Path.Data = textAnimationState.Object;
List<int> toRemove = new List<int>();
//
// Hydrate path geometry and remove figures that overlap.
//
PathGeometry geometry = (PathGeometry)Path.Data;
for (int i = 0; i < geometry.Figures.Count - 1; i++)
{
var xDiff = Math.Abs(geometry.Figures[i].StartPoint.X - geometry.Figures[i + 1].StartPoint.X);
var yDiff = Math.Abs(geometry.Figures[i].StartPoint.Y - geometry.Figures[i + 1].StartPoint.Y);
if (xDiff < 0.1 && yDiff < 0.1)
{
geometry.Figures.RemoveAt(i);
}
}
CompositionTarget.Rendering -= RenderMorph;
}
}
private void RenderMorphCollapse(object target, EventArgs e)
{
RenderingEventArgs renderingEventArgs = (RenderingEventArgs)e;
if (renderingEventArgs.RenderingTime == multiAnimationState.LastFrame)
return;
multiAnimationState.LastFrame = renderingEventArgs.RenderingTime;
if (textAnimationState.Delay > 0)
{
textAnimationState.Delay--;
return;
}
textAnimationState.Progress += textAnimationState.ProgressIncrement;
var progressEase = powerEase.Ease(textAnimationState.Progress);
if (Morph.Collapse((PathGeometry)Path.Data, progressEase))
{
CompositionTarget.Rendering -= RenderMorphCollapse;
}
if (textAnimationState.Progress >= 1.0)
{
CompositionTarget.Rendering -= RenderMorphCollapse;
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment