Skip to content

Implement gradient brush similar to PathGradientBrush (#969)#989

Merged
antonfirsov merged 7 commits intoSixLabors:masterfrom
mysticfall:master
Sep 1, 2019
Merged

Implement gradient brush similar to PathGradientBrush (#969)#989
antonfirsov merged 7 commits intoSixLabors:masterfrom
mysticfall:master

Conversation

@mysticfall
Copy link
Copy Markdown
Contributor

@mysticfall mysticfall commented Aug 25, 2019

Prerequisites

  • I have written a descriptive pull-request title
  • I have verified that there are no overlapping pull-requests open
  • I have verified that I am following matches the existing coding patterns and practice as demonstrated in the repository. These follow strict Stylecop rules 👮.
  • I have provided test coverage for my change (where applicable)

Description

A gradient brush implementation that works similar to System.Drawing.Drawing2D.PathGradientBrush.

This fixes #969, but only convex paths are supported for now.

A gradient brush implementation that works similar to
System.Drawing.Drawing2D.PathGradientBrush.

This fixes #969, but only convex paths are supported for now.
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Aug 25, 2019

CLA assistant check
All committers have signed the CLA.

@mysticfall
Copy link
Copy Markdown
Contributor Author

mysticfall commented Aug 25, 2019

To maintainers: I think I need some help about this PR. I wrote test cases for this feature, but I don't know what to do with the text fixture (reference output images).

Should I fork SixLabors/Imagesharp.Tests.Images also and make a separate PR? Or maybe I should just attach those images here (which I'll do it just in case)?

Sorry if it's something obvious, but it's the first time I've ever made a PR that spans across multiple Git submodules.

As to the code itself, I think it works as intended for simple convex shapes but it probably won't work for conclave ones, and I haven't compared its output with that of the original System.Drawing.Drawing2D.PathGradientBrush.

There's an implicit restriction to the ILineSegment[] lines constructor argument that it must constitute a closed path (polygon). I thought about adding a validation check, but realized other brush implementations don't seem to do any input validation, so I just leaved it as it is for consistency.

Still, I wanted to ensure that the lines argument would represent a closed path (so that users can omit the last segment). I thought invoking new Polygon(lines) would automatically make it a closed path, as the implementation of Path.AsClosedPath suggests. But it looks like it doesn't work that way, so I also leaved it as it is.

FillRectangleWithDifferentColors_Rgba32_Blank10x10
FillTriangleWithDifferentColors_Rgba32_Blank10x10
FillWithCustomCenterColor_Rgba32_Blank10x10
ShouldRotateTheColorsWhenThereAreMorePoints_Rgba32_Blank10x10

@JimBobSquarePants
Copy link
Copy Markdown
Member

@mysticfall If you fork the Imagesharp.Tests.Images.Repo and make a separate PR there we can merge that and update the submodule reference in this PR accordingly.

We're going to simplify all this for the future, it's far too complicated and disjointed a process currently.

@mysticfall
Copy link
Copy Markdown
Contributor Author

@JimBobSquarePants Sure, I can do that. Thank for the instruction :)


PointF end = point + (PointF)(direction * this.maxDistance);

(Edge edge, PointF intersection) = this.edges
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Linq here will negatively affect performance.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it involve extra allocations or something similar? I'm not very familiar with internal details about LINQ but I'll try to replace it with a for-iteration, if it's indeed the case.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The performance impacts are coming from stuff like extra allocations, virtual calls, deeper stack etc.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@antonfirsov Thanks for the clarification. I just want to know exactly how and when using LINQ would affect the performance. I've made this PR to be used for my game project, so the knowledge would benefit me in that aspect too.

I'll try to dig deeper into that LINQ chain to see if there's significant overhead before rewriting it to simple for loops.

Copy link
Copy Markdown
Contributor Author

@mysticfall mysticfall Sep 1, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still not sure if the original code would incur significant overhead, but I just replaced it with a plain foreach-loop just in case.

I'm not a seasoned C# developer, so if anyone could help me understand where the performance caveat lies, or how to find out such a problem, it would be much appreciated.

Copy link
Copy Markdown
Contributor Author

@mysticfall mysticfall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Submitted a pull request for test fixtures as SixLabors/Imagesharp.Tests.Images#11.


PointF end = point + (PointF)(direction * this.maxDistance);

(Edge edge, PointF intersection) = this.edges
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it involve extra allocations or something similar? I'm not very familiar with internal details about LINQ but I'll try to replace it with a for-iteration, if it's indeed the case.

antonfirsov added a commit to SixLabors/Imagesharp.Tests.Images that referenced this pull request Aug 30, 2019
@antonfirsov
Copy link
Copy Markdown
Member

@mysticfall merged your PR. If you push a submodule update, your tests should pass.

private static Color CalculateCenterColor(Color[] colors) =>
new Color(colors.Select(c => c.ToVector4()).Aggregate((p1, p2) => p1 + p2) / colors.Length);

private static float DistanceBetween(PointF p1, PointF p2) => ((Vector2)(p2 - p1)).Length();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Squared distance is enough for your purpose. (dx^2 + dy^2 instead of sqrt( dx^2 + dy^2 ))

Copy link
Copy Markdown
Member

@antonfirsov antonfirsov Aug 30, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor Author

@mysticfall mysticfall Sep 1, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upon inspection, I found that DistanceBetween cannot be modified that way without affecting the output, since the brush relies on a ratio of actual distances.

I replaced Length() used in FindIntersection method as suggested, however, as it's only used to find the closest edge.

return this.buffer
.Take(intersections)
.Select(p => (point: p, distance: ((Vector2)(p - start)).Length()))
.OrderBy(v => v.distance)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another optimization hint:
Finding a closest intersection might be enough you don't need to order the whole ordered list.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed unnecessary OrderBy invocations. Thanks!

Use LengthSquared() instead of Length() when it's possible.
Avoid unnecessary ordering of elements.
@codecov
Copy link
Copy Markdown

codecov bot commented Sep 1, 2019

Codecov Report

Merging #989 into master will increase coverage by 0.02%.
The diff coverage is 96.29%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #989      +/-   ##
==========================================
+ Coverage   89.63%   89.66%   +0.02%     
==========================================
  Files        1098     1100       +2     
  Lines       48690    48906     +216     
  Branches     3433     3447      +14     
==========================================
+ Hits        43643    43851     +208     
- Misses       4293     4297       +4     
- Partials      754      758       +4
Impacted Files Coverage Δ
...eSharp.Tests/Drawing/FillPathGradientBrushTests.cs 100% <100%> (ø)
...ImageSharp.Drawing/Processing/PathGradientBrush.cs 90.9% <90.9%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update a79c203...39af4c9. Read the comment docs.

Copy link
Copy Markdown
Member

@antonfirsov antonfirsov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mysticfall thanks for the changes!
Just one final point: we need arg check in public API-s. If we do it quickly, you'll have a chance to have this feature in beta7 ;)
If you are AFK at the moment I can help you out pushing the change.

Regarding performance impacts of LINQ:
I haven't forgotten your question, just give me a little time to gather my thoughts :) Since you're a Unity dev, I think it is really important for you to be aware of this stuff :)

/// <param name="colors">Array of colors that correspond to each point in the polygon.</param>
/// <param name="centerColor">Color at the center of the gradient area to which the other colors converge.</param>
public PathGradientBrush(ILineSegment[] lines, Color[] colors, Color centerColor)
{
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need arg. check here:
lines, colors: not null, and counts are matching

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I considered adding them but thought it wasn't a convention after looking at other brush implementations (see my first comment).

Would it be ok if I just throw ArgumentNullException / ArgumentOutOfRangeException as I see fit? And, should I also add associated tests?

P. S.: I'm a Godot developer by the way, so yeah I really should be aware of such things :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's done. Please let me know if you want me to change anything else.

@JimBobSquarePants JimBobSquarePants mentioned this pull request Sep 1, 2019
8 tasks
@antonfirsov antonfirsov merged commit 43d8241 into SixLabors:master Sep 1, 2019
antonfirsov pushed a commit to antonfirsov/ImageSharp that referenced this pull request Nov 11, 2019
…SixLabors#989)

* Implement gradient brush similar to PathGradientBrush (SixLabors#969)

A gradient brush implementation that works similar to
System.Drawing.Drawing2D.PathGradientBrush.

This fixes SixLabors#969, but only convex paths are supported for now.

* Update submodule to add test fixtures for SixLabors#989

* Performance optimization

Use LengthSquared() instead of Length() when it's possible.
Avoid unnecessary ordering of elements.

* Avoid using LINQ in a hotspot

* Validate arguments for the public constructor
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement equivalent of System.Drawing.Drawing2D.PathGradientBrush

4 participants