Home > OS >  Implementing stroke drawing similar to InkCanvas
Implementing stroke drawing similar to InkCanvas

Time:05-09

My problem effectively boils down to accurate mouse movement detection.

I need to create my own implementation of an InkCanvas and have succeeded for the most part, except for drawing strokes accurately.

void onm ouseMove(object sneder, MouseEventArgs e)
{
    var position = e.GetPosition(this);
    
    if (!Rect.Contains(position))
        return;
    
    var ratio = new Point(Width / PixelDisplay.Size.X, Height / PixelDisplay.Size.Y);
    var intPosition = new IntVector(Math2.FloorToInt(position.X / ratio.X), Math2.FloorToInt(position.Y / ratio.Y));

    DrawBrush.Draw(intPosition, PixelDisplay);
    UpdateStroke(intPosition); // calls CaptureMouse
}

This works. The Bitmap (PixelDisplay) is updated and all is well. However, any kind of quick mouse movement causes large skips in the drawing. I've narrowed down the problem to e.GetPosition(this), which blocks the event long enough to be inaccurate.

There's this question which is long beyond revival, and its answers are unclear or simply don't have a noticeable difference. After some more testing, the stated solution and similar ideas fail specifically because of e.GetPosition.

I know InkCanvas uses similar methods after looking through the source; detect the device, if it's a mouse, get its position and capture. I see no reason for the same process to not work identically here.

CodePudding user response:

I ended up being able to partially solve this.

var position = e.GetPosition(this);

if (!Rect.Contains(position))
    return;

if (DrawBrush == null)
    return;

var ratio = new Point(Width / PixelDisplay.Size.X, Height / PixelDisplay.Size.Y);
var intPosition = new IntVector(Math2.FloorToInt(position.X / ratio.X), Math2.FloorToInt(position.Y / ratio.Y));
// Calculate pixel coordinates based on the control height

var lastPoint = CurrentStroke?.Points.LastOrDefault(new IntVector(-1, -1));
// Uses System.Linq to grab the last stroke, if it exists

PixelDisplay.Lock();
// My special locking mechanism, effectively wraps Bitmap.Lock

if (lastPoint != new IntVector(-1, -1)) // Determine if we're in the middle of a stroke
{
    var alphaAdd = 1d / new IntVector(intPosition.X - lastPoint.Value.X, intPosition.Y - lastPoint.Value.Y).Magnitude; 
// For some interpolation, calculate 1 / distance (magnitude) of the two points.
// Magnitude formula: Math.Sqrt(Math.Pow(X, 2)   Math.Pow(Y, 2));
    var alpha = 0d;

    var xDiff = intPosition.X - lastPoint.Value.X;
    var yDiff = intPosition.Y - lastPoint.Value.Y;

    while (alpha < 1d)
    {
        alpha  = alphaAdd;

        var adjusted = new IntVector(
            Math2.FloorToInt((position.X   (xDiff * alpha)) / ratio.X),
            Math2.FloorToInt((position.Y   (yDiff * alpha)) / ratio.Y));
        // Inch our way towards the current intPosition
        
        DrawBrush.Draw(adjusted, PixelDisplay); // Draw to the bitmap
        UpdateStroke(intPosition);
    }
}

DrawBrush.Draw(intPosition, PixelDisplay); // Draw the original point
UpdateStroke(intPosition);

PixelDisplay.Unlock();

This implementation interpolates between the last point and the current one to fill in any gaps. It's not perfect when using a very small brush size for example, but is a solution nonetheless.

Some remarks

IntVector is a lazily implemented Vector2 by me, just using integers instead. Math2 is a helper class. FloorToInt is short for (int)MathF.Round(...))

  •  Tags:  
  • wpf
  • Related