SixLabors / ImageSharp

:camera: A modern, cross-platform, 2D Graphics library for .NET
https://sixlabors.com/products/imagesharp/
Other
7.39k stars 851 forks source link

Brushes #5

Closed JimBobSquarePants closed 7 years ago

JimBobSquarePants commented 7 years ago

From @JimBobSquarePants on December 1, 2015 12:48

This issue wraps several brush types.

I think we should implement at least:

I'm envisioning something like :

PathBrush brush = new PathBrush
{
    Color = Color.Black,
    Thickness = 10, // the thickness to draw a line
    Path = vectors // A list of vectors to follow
}

Image.Draw(brush)

The method would draw a Bezier curve following the given path, color, and thickness.

Copied from original issue: JimBobSquarePants/ImageProcessor#264

JimBobSquarePants commented 7 years ago

From @dampee on December 21, 2015 12:57

If I get through my color space, I'll see what I can do for January. Any startingpoints, hints, ... ?

JimBobSquarePants commented 7 years ago

That'd be great if you could!

Honestly.... Not got much of a clue. I think the source for Cairo might be a place to start looking around as I think a few libraries wrap around it. I'm trying to get my head around this just now.

https://github.com/mono/sysdrawing-coregraphics/blob/master/System.Drawing.Drawing2D/PathGradientBrush.cs

JimBobSquarePants commented 7 years ago

From @mweber26 on March 16, 2016 12:49

I don't understand this issue. What are the brushes for? There doesn't appear to be any drawing code to use them.

JimBobSquarePants commented 7 years ago

I've expanded on the issue. We'd obviously have to create bezier classes, a draw method, etc

I found a good primer on Beziers here. http://pomax.github.io/bezierinfo/

JimBobSquarePants commented 7 years ago

From @cartman300 on May 25, 2016 0:34

Is there any progress on this? I have quite a good idea how to implement these. Might give it a go.

JimBobSquarePants commented 7 years ago

@cartman300 None whatsoever.

I'm not the greatest mathemagician in the world so I struggle with this kind of stuff so If you know how to do it please give it a go. I would really appreciate it!

I'm essentially looking at first implementing path drawing. I think we should be able to break bezier and quadratic curves into neat methods and take advantage of the new vector types for things like Vector.Dot etc. Being able to set line thickness would very useful also.

Cheers!

JimBobSquarePants commented 7 years ago

From @cartman300 on May 25, 2016 23:11

example

I've implemented this in my fork

JimBobSquarePants commented 7 years ago

@cartman300 This is incredible! I'm super happy to see this come together. :smile:

The syntax is great also; really clean and pretty much exactly what I wanted. I had a quick look through the source code and I can see that you're still working on thickness. What's the status of antialiasing?

Buzzing with this demo. I appreciate it so much!

JimBobSquarePants commented 7 years ago

From @cartman300 on May 29, 2016 0:23

Eh, i forgot antialiasing was a thing. I should definitely implement it. Maybe even some kind of serial image processor, because stuff like this can't be parallelized easily (in fact, it makes it slower). I also have to fix the bézier paths because they don't stack easily, got no idea why yet.

JimBobSquarePants commented 7 years ago

Excellent. Thanks!

You can actually turn off the parallel nature of the processors by overriding the Parallelism property, setting it to 1. I do that here in Resizer

JimBobSquarePants commented 7 years ago

Hey @cartman300, Just found this excellent document on line drawing where at the end they deal with line thickness and antialiasing. We can certainly simplify the code sample by applying some of the Vector methods. I'm hoping it would be as useful to you as it looks.

Bresenham.pdf

JimBobSquarePants commented 7 years ago

From @cartman300 on June 5, 2016 23:34

Yes that's useful, but in the following weeks i'm not really sure if i'm gonna have any time to do any programming. But i'm certainly gonna have time after June ends.

Didn't forget about this.

JimBobSquarePants commented 7 years ago

@cartman300 No worries :smile: . I've got a busy time ahead of me also with travel and other projects.

JimBobSquarePants commented 7 years ago

Hey @cartman300 Just a quick checkin. Do you reckon you would get any time soon to have a further look at this?

JimBobSquarePants commented 7 years ago

From @cartman300 on August 5, 2016 1:26

OH yes thanks for reminding me :V I'm gonna take a look tomorrow

JimBobSquarePants commented 7 years ago

@cartman300 Sweet! That's great news!

JimBobSquarePants commented 7 years ago

From @cartman300 on August 6, 2016 6:51

Okay i reforked the repo and restarted the work. Had kind of a hard time figuring out where to put the brush stuffs (might need refactoring later).

Antialiasing should be a separate filter so you can choose different methods and also because i have no idea how to fill polygons with edge antialiasing with reasonable performance.

JimBobSquarePants commented 7 years ago

Yeah, we can always move it around. I'm deliberately keeping a very flat namespace for the library so feature discovery is easy.

Once we can get a working version we can work on improving performance so don't worry about that at this stage. I still have a lot of optimisation work to o on the project.

Just to note, whatever solution you do plese make sure you are using the generic Image<T,TP> terminology throughout so that the methods can be reused for different pixel formats. I f you get stuck at all or spot anything daft I have done please let me know.

JimBobSquarePants commented 7 years ago

From @cartman300 on August 6, 2016 17:1

How would i go on about directly manipulating pixels in an Image<T, TP>? When i lock it, i get a generic type T for the pixels and i have no idea what to do with it.

JimBobSquarePants commented 7 years ago

Good question. The IPackedVector<TP> interface allows for conversion to and from Vector4 where the X, Y, Z, & W components represent each RGBA component in applicable packed vector implementations.

Here's an example of me using those methds to change the alpha component of each pixel.

https://github.com/JimBobSquarePants/ImageProcessor/blob/93be57e7c664506115d8dcb098fd6f404f201332/src/ImageProcessorCore/Filters/Processors/AlphaProcessor.cs#L61

JimBobSquarePants commented 7 years ago

I'm moving this to v1.1 as it's far too big a scope of functionality to include just now. Hopefully we can all club together and get a really great API working for this.

JimBobSquarePants commented 7 years ago

Hey @cartman300 what happened to the source code from when you wrote the demo? I was going to see if I could continue your work but various methods seem to be missing.

JimBobSquarePants commented 7 years ago

From @cartman300 19th September 2016

The source code was very ugly and i deleted the old repository so i can re-clone the project again and reimplement it again. Got unexpectedly very busy lately so i can't guarantee anything, can give general guidelines tho.

Currently what's on top of my mind: The easiest way would be to write a "software renderer" of some kind to render polygons to an image, that part can be later reused.

After that you can use math to generate points on a path with variable resolution, you can change different path types by just changing the formula (just going along the line and spitting out points)

To give thickness to the lines, you can inflate by generating concentric circles with a variable radius (relative to each point along the line) and use math to generate convex polygons for each path segment and draw them in-order

JimBobSquarePants commented 7 years ago

From @cartman300 19th September 2016

This part might come in handy, isn't the most efficient but should get you started https://github.com/cartman300/Libraria/blob/master/LibrariaShared/Maths.cs#L39

JimBobSquarePants commented 7 years ago

More useful links

http://build-failed.blogspot.com.au/2016/08/creating-simple-tileserver-with-net.html http://devmag.org.za/2011/04/05/bzier-curves-a-tutorial/ http://pomax.github.io/bezierinfo/

JimBobSquarePants commented 7 years ago

From @eByte23 October 7th 2016

I'll have a play around over the weekend and see how I go. Can't promise anything but I'll take a look.

tocsoft commented 7 years ago

Hi, Thought I would give drawing/brushes a punt thought I should let you guys see my prototype (https://github.com/tocsoft/ImageSharp/tree/drawing) and allow you to comment on it and if/when you think its work pulling I can make a PR for it.

First lets give you some something shiny to look at:

Shinies

Simple line

var image = new Image(500, 500);
using (FileStream output = File.OpenWrite($"result.png"))
{
    image
        .BackgroundColor(Color.Blue)
        .DrawLine(Brushes.HotPink, 5, new[] {
                new Point(10, 10),
                new Point(200, 150),
                new Point(50, 300)
        })
        .Save(output);
}

image

Polygon outline

var image = new Image(500, 500);
using (FileStream output = File.OpenWrite($"result.png"))
{
    image
        .BackgroundColor(Color.Blue)
        .DrawPolygon(Brushes.HotPink, 5, new[] {
                new Point(10, 10),
                new Point(200, 150),
                new Point(50, 300)
        })
        .Save(output);
}

image

Polygon Filled

var image = new Image(500, 500);
using (FileStream output = File.OpenWrite($"result.png"))
{
    image
        .BackgroundColor(Color.Blue)
        .FillPolygon(Brushes.HotPink, new[] {
                new Point(10, 10),
                new Point(200, 150),
                new Point(50, 300)
        })
        .Save(output);
}

image

Bezier Curve - Path

var image = new Image(500, 500);

using (FileStream output = File.OpenWrite($"{path}/Simple.png"))
{
    image
        .BackgroundColor(Color.Blue)
        .DrawBeziers(Brushes.HotPink, 5, new[] {
            new PointF(10, 400),
            new PointF(30, 10),
            new PointF(240, 30),
            new PointF(300, 400)
        })
        .Save(output);
}

image

Bezier Curve - Filled

var image = new Image(500, 500);

using (FileStream output = File.OpenWrite($"{path}/Simple.png"))
{
    image
        .BackgroundColor(Color.Blue)
        .Fill(Brushes.HotPink,new BezierPolygon(new[] {
            new PointF(10, 400),
            new PointF(30, 10),
            new PointF(240, 30),
            new PointF(300, 400)
            }))
        .Save(output);
}

image

Complex Polygon

var simplePath = new LinearPolygon(
                new Point(10, 10),
                new Point(200, 150),
                new Point(50, 300));

var hole1 = new LinearPolygon(
                new Point(37, 85),
                new Point(93, 85),
                new Point(65, 137));

var image = new Image(500, 500);

using (FileStream output = File.OpenWrite($"{path}/Simple.png"))
{
    image
    .BackgroundColor(Color.Blue)
    .Fill(Color.HotPink, new ComplexPolygon(simplePath, hole1))
    .Save(output);
}

image

Path made up of Linear and Bezier segments

var image = new Image(500, 500);

var linerSegemnt = new LinearLineSegment(
                new Point(10, 10),
                new Point(200, 150),
                new Point(50, 300)
        );
var bazierSegment = new BezierLineSegment(new Point(50, 300),
    new Point(500, 500),
    new Point(60, 10),
    new Point(10, 400));

var p = new CorePath(linerSegemnt, bazierSegment);

using (FileStream output = File.OpenWrite($"{path}/Simple.png"))
{
    image
        .BackgroundColor(Color.Blue)
        .DrawPath(Color.HotPink, 5, p)
        .Save(output);
}

image

Details

Started off with vector drawing (Paths, Polygons etc) and a solid colour brush.

I tried to stick to a public API (extension methods on top of ImageBase<TColor,TBacked>) that are similar to those found in System.Drawing.

I decided a Path/Shape aren't really the thing as Brushes so I split there interfaces. An IBrush then becomes a source of colors and an IShape/IPath in turn just control a region of the image to apply the brush to. (therefore there isn't a PathBrush as it doesn't end up making much sense in this model)

I feel the current method for drawing outlines with details in IPen isn't quite right. At the moment the current method is limited to a solid line with rounded endcaps/corners and thus I feel it need re-engineering to support things line line patterns (dashes etc) and end caps etc. The way I envision this working is for an IPen to take in an IPath and based on the IPens settings convert it into an IShape then pass that over to the FillShapeProcessor to fill with the IPens configured IBrush.

There is additional scope to optimise Shapes somewhat, at the moment we support arbitrary polygons via ILineSegments, but we can easily add specific implementation for well know shapes Rectangles, Circles etc and they should be able to implement the interface in a mush more efficient manor than the generic implementation.

None of this code (I would think) is particularly optimised, and I'm probably doing some horrible things with colors and allocating way to much memory but its a start.

JimBobSquarePants commented 7 years ago

Hi @tocsoft

I think shiny might be a bit of an understatement. This is simply awesome to see!

You're way out of my comfort zone here (A bit mathsy for me) so the only thing I will be able to offer will be optimization tips and API guidance. (On that not it might be an idea to rebase your fork from my master branch. I've been simplifying things)

The output looks beautiful though and the API neat and tidy and in keeping with what I have tried to do with the rest of the library.

I know that @EvK was having a play around with this also but I haven't seen any public output there yet. There was mention in the conversations we had on Gitter about difficulty offsetting thick Bezier curves. Is this a problem you have already solved? Perhaps you could both share ideas?

I notice you have created a couple of new types PointF and RectangleF. I've been contemplating removing Point and using Vector2 only. What would be your thoughts on that? I've found that every method I've used Point for so far has involved casting to/from Vector2 so I'm very much inclined to drop it. That looks like it would simplify your API also.

Truly blown away by this. Thanks for making the effort to contribute 💯

JimBobSquarePants commented 7 years ago

The only thing I've spotted so far a bit off is the direct use of Color. I'd avoid that and stick to TColor for now like BackgroundColor does. Eventually there'll probably have to be static implementations of colors for the different IPackedPixel types but I'm not in any rush to do that as each color can pack a vector or use a constructor anyway.

tocsoft commented 7 years ago

You're way out of my comfort zone here (A bit mathsy for me)

Its alright, its a bit mathsy for me too... its just stuff I've cobbled together for different sources, I definitely don't understand all the maths in there. 😁

I notice you have created a couple of new types PointF and RectangleF. I've been contemplating removing Point and using Vector2 only. What would be your thoughts on that? I've found that every method I've used Point for so far has involved casting to/from Vector2 so I'm very much inclined to drop it. That looks like it would simplify your API also.

I think I like having Point and PointF for a public API as they are more prescriptive to end users but as soon as you get into internals then I can see using Vector2 all the way.

The outlines are a bit of a hack at the moment all i'm doing is filling the region that the thickness distance away from the shapes edge, which is why the IPen stuff needs fixing.

Also curves in general are a massive cheat in here, I'm cheating by converting all the curves into a number of liner segments that are very short thus visually identical at the pixel scales I've used thus far, but I imagine that for large images you will see noticeable issues with the curves but that would be solvable by just generating more segments. (possibly calculating the optimal count based on the curve length rather then hard coding it like is happening now.)

tocsoft commented 7 years ago

I did have a version that used TColor but changed my mind from it (can't remember why now), but the closer I look I can't see any issue moving over to it.

tocsoft commented 7 years ago

Now has some initial pattern support.

Not sure if its really worth porting all the System.Drawing HatchStyles in? is there going to be any real want for them?

more shinys

these have been blown up 4 times otherwise you can struggle to see the patterns at times

Backward Diagnal

backwarddiagnal_transparentx4

Forward Diagnal

forwarddiagnal_transparentx4

Horizontal

horizontal_transparentx4

Min

min_transparentx4

Percent10

percent10_transparentx4

Percent20

percent20_transparentx4

Vertical

vertical_transparentx4

tocsoft commented 7 years ago

The only thing I've spotted so far a bit off is the direct use of Color. I'd avoid that and stick to TColor for now like BackgroundColor does. Eventually there'll probably have to be static implementations of colors for the different IPackedPixel types but I'm not in any rush to do that as each color can pack a vector or use a constructor anyway.

I had a play with just using TColor throughout but it felt rather dirty when you wanted to instantiate brushes and pens manually instead of using extension methods, or if you wanted to retain one for use between manipulations (and don't use var).

I want to create a new SolidBrush(new Color("ff5565")) not a , new SolidBrush<Color, uint>(new Color("ff5565")) for an end user what is the uint doing there it would end up being some magic you would have to know to do to get it to work. Also you don't want the confusion around why doesn't this brush work with this image etc. It Just Should Work™️.

So what I've done it I've made the standard IBrush implementation depend on Color directly but as soon as you start to process the brush we create an IBrushApplicator<TColor, TPacked> at which point we convert the color to the really TColor type and use that throughout the rendering.

This allows a single instance of an IBrush to be used across multiple images with multiple TColor types and still wok efficiently.

JimBobSquarePants commented 7 years ago

I had a play with just using TColor throughout but it felt rather dirty when you wanted to instantiate brushes and pens manually instead of using extension methods, or if you wanted to retain one for use between manipulations (and don't use var).

That's not gonna work I'm afraid.... The different IPackedPixel<TPacked> vectors operate at different ranges when represented by a Vector4. E.g. Byte4 operates on a scale of 0 to 255, Color at 0 to 1, NormalizedShort4 at -1 to 1, and probably the weirdest Short2 which operates at -32767 to 32767 for the first component pair and 0 to 1 for the second component pair. (These are all straight from XNA and MonoGame). The ToBytes and FromBytes methods handle this scaling but we only ever want to use them in the individual image formats.

The way I have handled this is to make a concrete implementation of my generic types that I know I am going to have to create instances of for Color.

Image is really Image<Color,uint> as are the non-generic PixelAccessor and PixelArea.

I think I like having Point and PointF for a public API as they are more prescriptive to end users but as soon as you get into internals then I can see using Vector2 all the way.

Ok, let's keep them for now but I might still drop them in the future.

Don't worry about doing all the HatchBrush styles, I think what you have done so far is more than enough, If someone desperately needs them then they probably know how to write one anyway. Let's focus on correctness and optimization of the algorithms. Regarding that maybe this is useful?

http://www.angusj.com/delphi/clipper.php

tocsoft commented 7 years ago

Bugger... guess I'll create some standard wrappers based on Color versions, I'll put them in and see how it feels then.

I suspect there's going to be some issues dealing with opacity consistently for all those TColors I'm currently quite dependent on that for anti-aliasing, I think I'm going to need a battery of tests for them all.

I did see clipper but the licence made me pause as I wasn't sure it's compatiblity with MIT but looking over it it should be fine to include code from there, I'll have a look to see what I can lift from there to make things work smoothly.

JimBobSquarePants commented 7 years ago

Since you would be multiplying the alpha component by a value it should be ok for most cases. Some implementations simply ignore alpha anyway so there's nothing you can do about that.

Yeah, the license is an odd one but as long as we include an original copyright we should be ok.

JimBobSquarePants commented 7 years ago

Oh mean to say. Check out Vector4BlendTransforms.PremultipliedLerp for a nicer blending function. It automatically premultiplies the values for you when blending. You get a much nicer output.

tocsoft commented 7 years ago

status update, I've migrated the code to TColor and to use Vector4BlendTransforms.PremultipliedLerp for blending.

We now have things like IBrush<TColor, TPacked> with simplimentations like SolidBrush<TColor, TPacked> and I even created the default wrapper for Color so you can do things like new SolidBush(Color.HotPink) which is ends up being an IBrush<Color, uint>.

Also I have a couple of new shinies for you, ComplexPolygons now work and Pens have a sensible api allowing for path patterns.

Shiney

Complex polygons

Automaticaly get simplified and support overlapping holes and outlines this is using the code from http://www.angusj.com/delphi/clipper.php image also works for outlines image

Pen - line drawing

Pens now work too, you can draw a line with any custom pattern image

also work with polygons, both simple and complex image

JimBobSquarePants commented 7 years ago

That's a treasure trove of shiny now! The pen path stuff looks simple enough to use. Looking forward to a final PR

tocsoft commented 7 years ago

Closing this as we have most of this now.

New issue for gradient bushes is #86