Yesterday's blog entry presented a Silverlight program that made animated dots run around the outlines of text characters. The program itself was fairly simple — everything was done in XAML — but it also has a flaw in the form of an annoying visual flicker. If you look closely, you can see extra dots popping into and out of existence. My goal today: Get rid of that flicker.
In yesterday's program, the outline of each text character is defined by one or more — in the parlance of WPF and Silverlight — PathFigure objects. A PathFigure is a series of connected straight lines and curves. A character such as an S or a C is a single PathFigure. Characters with enclosed areas (such as A or B) or extra pieces (such as i) require additional PathFigure objects. For text character outlines generated from the WPF FormattedText class, each PathFigure is a series of connected segments of type LineSegment, PolyLineSegment, BezierSegment, and PolyBezierSegment. Each PathFigure has a geometric length, which is the composite length of these connected segments. If the PathFigure is rendered without transforms, this length is in units of pixels.
Yesterday's program drew the outlines of these text characters using a Path element with a dotted line, where the dots had a dimension of zero (but were visible because they had rounded caps) and separated by 1.5 units. These units are based on the LineThickness, which was set at 6 pixels. Hence, each dot-and-space combination was 9 pixels in length.
If you divide the geometric length of a PathFigure by 9 pixels, you probably won't get an integral number of dots, so when the position of the dot-and-space is animated, dots pop into and out of existence. That's what causes the flicker.
And I knew how to fix it. Rather than displaying a single Path with a single PathGeometry with the collection of PathFigure objects for the text characters, I moved the PathFigure objects into a PathFigureCollection defined as a XAML resource.
In code, I looped through these PathFigure objects, creating a PathGeometry and a Path element and a DoubleAnimation for each one. Rather than using 1.5 for the space between dots, I calculated a value based on the geometric length of the PathFigure. For this code, AVG_DASH_SPACE is a constant defined as 1.5, and STROKE_THICKNESS another constant defined as 6:
double length = GetPathFigureLength(pathFigure);
int numberDots = (int)Math.Round(length / (STROKE_THICKNESS * AVG_DASH_SPACE));
double dashSpace = length / (numberDots * STROKE_THICKNESS);
The dashSpace value is then used for the DoubleCollection for the StrokeDashArray property of the Path element as well as the animation From value. (Setting the From rather than To effectively changes the direction of the animation, so the dots run clockwise around the characters rather than counter-clockwise, which seems more natural.)
And it didn't work.
I traced the problem to my GetPathFigureLength method. This method includes code to determine the length of a Bezier spline by approximating it as a polyline. But the value I was getting was evidently not the same value that Silverlight was calculating internally. Rather than futz around with it, I went back to the Text Outline Generator program that I described yesterday and recreated the collection of PathFigure objects for a flattened PathGeometry, which means that all the PathFigure objects consist of a single PolyLineSegment that approximates all the Bezier curves. The advantage of the PolyLineSegment is that there's absolutely no ambiguity about its length.
And it still didn't work. I was still getting a little flicker right at the start of each PathFigure.
I fixed this by making the dots a little bit wider. Rather than a dimension of zero, I gave them a dimension of 0.1, which (in this program) means that they're really 0.6 pixels wider than they should be. (I also needed to accomodate that new value in the code shown above.) And that worked.
Well, not entirely: If you look closely at the places with sharp angles — the insides of the v, r, g, and h, in particular — you'll see a little flash as something is drawn very quickly and then disappears. But I'm not going to even try to fix that!
Here's the source code.