Oct 16, 2009

Seeing Stars I’m Seeing Stars

“Oh my, starry eyed surprise. Sun down to sun rise. Dance all night. We gonna dance all night…”

Feel free to stab your eyes out at this point (Or drink a Diet Coke)

Ok so WTF am I talking about? Custom Ink Canvas rendering in WPF of course… I ran across a problem recently that I thought was blog worthy. The objective was to allow a user to draw on an InkCanvas with a custom “stencil.” The “stencil” is really just some custom shape that should be used as the stroke for the InkCanvas. This is kind of hard to explain so a picture may help

SurfaceCustomInking[1]

In this picture the stars (hence the terrible Oakenfold reference) would be the stencil. When the user drags the mouse around the canvas we want to draw the stars like shown above. This isn’t quite as obvious to implement as most things in WPF. My hope was that I could just set the Stroke to some DrawingBrush and be done. However, it isn’t that easy.

The main reason for the complication is the way that the InkCanvas collects the strokes from the user. The strokes are are collected on a background thread to ensure that all strokes will be collected, even if the UI is blocking. This is done through a DynamicRenderer object. This object collects the user input and renders all points in the stroke on a separate thread.  Once the entire stroke is collected The InkCanvas raises the OnStrokeCollected method.

The first step to solving my problem is to implement a custom DynamicRenderer  that will track the users movements on the canvas and render our custom shape along the path that the user has drawn.

public class CustomRenderer : DynamicRenderer
{
private Point prevPoint;

public DrawingGroup Stencil { get; set; }

protected override void OnStylusDown(RawStylusInput rawStylusInput)
{
// Allocate memory to store the previous point to draw from.
prevPoint = new Point(double.NegativeInfinity, double.NegativeInfinity);
base.OnStylusDown(rawStylusInput);
}

protected override void OnDraw(DrawingContext drawingContext,StylusPointCollection stylusPoints,Geometry geometry, Brush fillBrush)
{
for (int i = 0; i < stylusPoints.Count; i++)
{

var pt = (Point)stylusPoints[i];
Vector v = Point.Subtract(prevPoint, pt);

// Only draw if we are at least 4 units away
// from the end of the last ellipse. Otherwise,
// we're just redrawing and wasting cycles.
if (v.Length > 4)
{
var clone = Stencil.Clone();
clone.Transform = new TranslateTransform(pt.X, pt.Y);
drawingContext.DrawDrawing(clone);
prevPoint = pt;
}
}
}

}


The key to this snippet is in the OnDraw method override. I am iterating through the points along the path that the user has drawn then rendering a new instance of our custom drawing to the DrawingContext pipeline. Notice the TranslateTransform, I am using the translate to make sure the drawing I add to the DrawingContext pipeline follows the points in the path that the user created.



Once the stylus points are converted to strokes then the stroke is told to render itself. This is the second step in the process. We need to create a custom stroke class that renders our custom shape when it is asked to render itself.



class CustomStroke : Stroke
{
public DrawingGroup Stencil { get; set; }

public CustomStroke(StylusPointCollection stylusPoints) : base(stylusPoints)
{
}

protected override void DrawCore(DrawingContext drawingContext,DrawingAttributes drawingAttributes)
{
// Allocate memory to store the previous point to draw from.
var prevPoint = new Point(double.NegativeInfinity,
double.NegativeInfinity);

// Draw linear gradient ellipses between
// all the StylusPoints in the Stroke.
for (int i = 0; i < StylusPoints.Count; i++)
{
var pt = (Point)StylusPoints[i];
Vector v = Point.Subtract(prevPoint, pt);

// Only draw if we are at least 4 units away
// from the end of the last ellipse. Otherwise,
// we're just redrawing and wasting cycles.
if (v.Length > 4)
{
var clone = Stencil.Clone();
clone.Transform = new TranslateTransform(pt.X, pt.Y);
drawingContext.DrawDrawing(clone);
prevPoint = pt;
}
}
}
}


You can see the DrawCore method looks very similar to the OnDraw method in the custom renderer. We are doing the same rendering in both places so the code will be very similar.



The final step in this process is to create a class that inherits from InkCanvas and wire up the dynamic renderer and the custom stroke.



public class CustomRenderingInkCanvas : InkCanvas
{
readonly CustomRenderer customRenderer = new CustomRenderer();

public CustomRenderingInkCanvas(): base()
{
// Use the custom dynamic renderer on the
// custom InkCanvas.
DynamicRenderer = customRenderer;
}

protected override void OnStrokeCollected(InkCanvasStrokeCollectedEventArgs e)
{
// Remove the original stroke and add a custom stroke.
Strokes.Remove(e.Stroke);
var customStroke = new CustomStroke(e.Stroke.StylusPoints)
{
Stencil = Stencil
};

Strokes.Add(customStroke);

// Pass the custom stroke to base class' OnStrokeCollected method.
var args = new InkCanvasStrokeCollectedEventArgs(customStroke);
base.OnStrokeCollected(args);
}
}


I have cut out some code from the snippet above to focus on the important parts. The two keys are wiring up the DynamicRenderer in the public constructor and removing the default stroke and adding in our custom stroke in the StrokeCollected override



Once we put all these steps together we end up with an ink canvas that we can apply a custom stencil to and use that stencil to draw with.



The result is shown in the video below ( Turn on your sound ;)




Custom InkCanvas in WPF from Brad Cunningham on Vimeo.


Note: the lag you see in the video is just from me trying to draw using the trackpad on my laptop with one hand. The actual application doesn’t lag.

You can download the full sample project from here. I followed this MSDN article in solving this problem

Labels: ,

1 Comments:

Blogger Biju said...

Hi..I found your code usefull.I am trying to implement a custom stroke. I am trying to save the strokes into an isf file and later loading the file to the image.But the problem is that when i do inkcanvas.strokes.add(stroke),The strokescollected event is not fired and my custom renderer class will not work.Please help me on this regard.

I tried to sve strokes into a file and load in your cose also but the stars disappear..pls help

July 10, 2010 at 4:50 AM  

Post a Comment

Subscribe to Post Comments [Atom]

<< Home