C# UWP InkCanvas & Erase By Point



  • This is more of a post to see if I'm on the right train of thought before starting out.

    So, I want to have an app which you can write in with a stylus (or fingers) if you have a touch-enabled device.

    Background: There are two types of InkCanvas - one for WPF (which seems to have existed for a long time now) and one for UWP. Both do similar things but have slightly different toolkits. The reason I'm going for UWP is that it has the InkToolbar - which is a ready-made solution for easily changing pencil types, colour, a ruler and so on. The strokes can be exported as a stroke container file (i.e. readable only by the app), embedded inside a gif (so readable by both the app and any kind of image viewer) or rendered down to a png. Also, I know UWP better than WPF.

    A stroke is basically a collection of reference points coupled with a timestamp, from which the drawn lines are then rendered to the canvas. One stroke can only have one type of pencil, thus no changing of colour.

    The only problem: The included eraser tool only does "erase by stroke", i.e. if the eraser touches a stroke it gets deleted in its totality. What I need is also an "erase by point", i.e. erase only the points touched by the eraser and not the complete stroke.

    Here are my thoughts on how to go about it in order to get a proper "erase by stroke".

    a) I'd either paint a rectangle or a circle under the cursor. I'd then initially use the BoundingRect attribute of each existing stroke to see if the cursor shape collides with this boundary rectangle of the stroke to rule out any strokes which don't even have a chance of being affected. After that I'd go over the strokes where the boundary rectangle was touched to see for each stroke if one or more points inside a stroke were covered by the shape. Then I'd either outright delete only those points (if they're at the end of a stroke and there would be no "orphans") or create two/three/... new strokes containing the non-deleted strokes, after which the old strokes gets deleted in turn. This would happen every time the PointerMoved event gets called. Thus possibly expensive/laggy.

    b) I'd do a "pseudo-eraser" first, i.e. instead of an eraser you'd actually get a pencil with the colour set to the background's colour. Only after the PointerUp event I'd actually process any stroke/point deletions as detailed in a) Problems here might result from having to handle other shapes/images which could exist on the canvas as well and should not be painted over.

    Anyone have any suggestions?



  • @Rhywden
    Just to clarify: is there a semantic difference to the result between "erase" and "paint with the background colour"? The alternative being that the result is the same for all intents and purposes.
    Like, I can see if erasing a stroke exposed underlying strokes that had been made earlier (sort of a pixel-by-pixel undo stack with erasure being the undo).



  • For any practical purpose, drawing a stroke in the background color is probably good enough as an eraser. Yes, you're storing information that is no longer needed (stuff that was underneath the eraser's stroke), but it's very unlikely to cause any storage or performance issues in a simple finger-painting program.

    Trying to do the math to actually erase the stroke is just probably not worth the extra work. Plus, you've got exactly the same thing for any other stroke that crosses another. You can either convert the strokes to polygons, do a bunch of math, calculate the subtractions and the intersection (and the new color of the intersection, if your stroke is not fully opaque), and end up with a set of adjacent polygons (which, if neither of them is the background color, comes with its own set of problems), or you can just live with the fact that you're painting some pixels more than once.



  • @Watson said in C# UWP InkCanvas & Erase By Point:

    @Rhywden
    Just to clarify: is there a semantic difference to the result between "erase" and "paint with the background colour"? The alternative being that the result is the same for all intents and purposes.
    Like, I can see if erasing a stroke exposed underlying strokes that had been made earlier (sort of a pixel-by-pixel undo stack with erasure being the undo).

    Yes, there's a rather big difference: You can also select strokes by tapping on them with a finger (or select multiple by press&drag) and move them. The way this usually works is that you display the actual (normally invisible) bounding box.

    Which means that you could select the "invisible" eraser strokes by accident, move the stroke you wanted to partially erase from under the "eraser" stroke or have a bounding box which may not fit the visible strokes (i.e. too large).

    edit: Though I could see a case for disabling "stroke selection". I'm pretty sure it's a rarely used feature... will have to research that. Thanks, that's exactly the kind of input I wanted.



  • @brie said in C# UWP InkCanvas & Erase By Point:

    You can either convert the strokes to polygons, do a bunch of math, calculate the subtractions and the intersection (and the new color of the intersection, if your stroke is not fully opaque), and end up with a set of adjacent polygons

    Well, if you take the simplistic picture that a stroke is a chain of points and a thickness with the latter property only coming into play for rendering.... The erasure action would be a stroke (only the last chainlink is needed at any one time but anyway) and when that erasure stroke meets a painted stroke, then the painted stroke gets cut in two with a gap as wide as the stroke and eraser combined (the extra width to accommodate what I'm assuming are rounded endpoints of strokes).

    Upshot being that it becomes a matter of whether the current and previous points of the erasure action lie on different sides of the point chain of some stroke.

    Might look a little glitchy in action: an erasure stroke (invisibly) approaching a painted stroke won't have any effect until its centreline meets the centreline of the painted stroke, and then suddenly a whole gap opens up. But: (a) your view of this is largely blocked by the stylus/finger anyway, and (b) any more refined shaping of the stroke would mean having to design something that would support, e.g., starting with a thick painted stroke and scalloping its edges by nibbling at it with a small eraser.



  • @Watson In practice that would feel really weird and it wouldn't look right, because the ends of the line where your eraser crossed over it would have what, rounded ends? Meanwhile, it's extremely simple to just draw a second line on top of everything and call that good enough, and then it actually behaves the way it's expected to behave.


  • Banned

    @Rhywden said in C# UWP InkCanvas & Erase By Point:

    a) I'd either paint a rectangle or a circle under the cursor. I'd then initially use the BoundingRect attribute of each existing stroke to see if the cursor shape collides with this boundary rectangle of the stroke to rule out any strokes which don't even have a chance of being affected. After that I'd go over the strokes where the boundary rectangle was touched to see for each stroke if one or more points inside a stroke were covered by the shape. Then I'd either outright delete only those points (if they're at the end of a stroke and there would be no "orphans") or create two/three/... new strokes containing the non-deleted strokes, after which the old strokes gets deleted in turn. This would happen every time the PointerMoved event gets called. Thus possibly expensive/laggy.

    Consider dividing the screen space using quadtree to reduce the number of comparisons required on each update.

    @Rhywden said in C# UWP InkCanvas & Erase By Point:

    b) I'd do a "pseudo-eraser" first, i.e. instead of an eraser you'd actually get a pencil with the colour set to the background's colour. Only after the PointerUp event I'd actually process any stroke/point deletions as detailed in a) Problems here might result from having to handle other shapes/images which could exist on the canvas as well and should not be painted over.

    Is there any way to use transparent-color-keying with InkCanvas? Then you could put the background on a separate UI element underneath, and use the keyed transparent color as your background color within InkCanvas - that way, color strokes cover the background, and the eraser covers the color strokes, but eraser doesn't cover the background.



  • @brie I kind of expect the stroke would have rounded ends, just as if it had been drawn that short to start with. More accurately, I'd be expecting the endpoint to match the shape of the pen nib. Especially since erasing part of the stroke is supposed to result in separate distinct strokes (I take that to mean they can then be selected and moved individually, or multi-selected and moved as a group). Otherwise the stroke model will have to carry around its entire outline instead of just its path and choice of nib.

    Maybe the extra gap to accommodate rendering the stroke thickness at the end isn't necessary, and if someone wants to have a gap large enough to be seen will just have to use a chunky enough eraser.



  • @Watson You expect that because you have a preconceived notion of how you'd make the eraser work. A user who expects it to work like an eraser will have a different preconceived notion; they don't care how you make it work, only that it works like they expect it to.



  • @brie My original idea, which I didn't bother to post, was to have a transparency channel for every stroke, and the erasure would work on one or more of them.

    But that wouldn't have played nice with the given stroke model: "a collection of reference points coupled with a timestamp...". Which wasn't my idea.

    I am making assumptions. I'm assuming the reference points describe the path of the stroke as drawn by the user (capturing the points the stylus touched, rather than, say, the stroke's outline) and the timestamp is used for rendering order. There is mention of a (single-colour) pencil, and I am assuming it has a nonvanishing size and shape.

    I'm trying to conceive of an approach that captures the idea that erasing involves deleting some of those reference points (and "only the points touched by the eraser"). Which implies the eraser itself has a nonvanishing size and shape, otherwise it won't have any chance of touching anything.



  • @Watson I think you're overthinking it though. Just drawing a stroke that's the same color as the background (or the same texture as the background - an image could even be used, I'd assume) will give the appearance, from the user's perspective, of erasing everything underneath it.



  • @brie Which I noted in my first post in this thread.



  • @Gąska I just had a look - something like a chroma key doesn't seem to exist for Canvas.



  • @Watson The eraser is usually rather large, due to the fact that you don't want to spend ages erasing larger portions - the Microsoft Whiteboard app has an eraser size based on velocity, for example; the faster you move the eraser, the larger the eraser stencil becomes.


  • Banned

    @Rhywden said in C# UWP InkCanvas & Erase By Point:

    @Gąska I just had a look - something like a chroma key doesn't seem to exist for Canvas.

    I don't know which API in particular you're using, but I found this in Win2D:

    https://microsoft.github.io/Win2D/html/T_Microsoft_Graphics_Canvas_Effects_ChromaKeyEffect.htm

    And also a tutorial for writing pixel shaders for WPF, maybe something very similar is available to you:



  • @Gąska I'm using https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.inkcanvas

    I have the slight feeling that trying to wire Win2D into that will be ... painful.


  • Banned

    @Rhywden maybe try this?

    It's extremely overcomplicated in typical MS fashion, but it sounds like it might help you (although no guarantees).



  • @Gąska yeah, that seems like it could do the trick. Thanks!


Log in to reply