-
-
Notifications
You must be signed in to change notification settings - Fork 893
Gradient Brushes #542
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Gradient Brushes #542
Changes from 1 commit
Commits
Show all changes
54 commits
Select commit
Hold shift + click to select a range
c02f9d8
#86: started to work on Gradient Brushes: Linear gradient brush,
jongleur1983 4975eae
fix some typos in documentation.
jongleur1983 e1ee9b0
FIX bug in BrushApplicator when applying BlendPercentage
jongleur1983 6d87844
#542: fix test: indices are 0-based, so bottom left pixel is one smaller
jongleur1983 1e587bc
#542: use struct for ColorStops
jongleur1983 6dd544d
fix code styling issues
jongleur1983 ce72683
#542: reduce test output image sizes
jongleur1983 90293cc
#542: add test for vertical gradient
jongleur1983 94dbb74
#542: fix implementation of non-axial gradients and add tests
jongleur1983 7a3ba8a
#542: add tests for multi-color gradients
jongleur1983 28457c8
cleanup whitespace
jongleur1983 35d0f56
#542: add test creating black-white patterns by gradients
jongleur1983 0a642ff
#542: define debuggerDisplay of ColorStop,
jongleur1983 eafdfea
fix whitespacing
jongleur1983 1cd8656
#542: use bigger files for the tests to "hide" rounding issues:
jongleur1983 393444c
#542: fix rounding issues in tests
jongleur1983 d977ecf
#542: rename file to match other files in the solution (include generic)
jongleur1983 d43c70a
fix documentation typo
jongleur1983 72ae5fa
#542: refactor to prepare for other gradients
jongleur1983 94f1698
implement radial gradient brush.
jongleur1983 e837349
first implementation of an elliptical gradient brush
jongleur1983 a6e0402
optimization of EllipticGradientBrush
jongleur1983 75dab52
improve performance on distance calculation
jongleur1983 d29b601
implement GradientRepetitionModes
jongleur1983 c8d0256
remove not implemented Polygon Brush from documentation for now.
jongleur1983 1cac740
Merge branch 'master' into GradientBrush
jongleur1983 2032ec2
Merge branch 'master' into GradientBrush
JimBobSquarePants 4f2d9d3
Merge remote-tracking branch 'origin/master' into GradientBrush
jongleur1983 9e98752
#542: apply change requests made by @tocsoft in code review.
jongleur1983 c8b8037
Merge branch 'master' into GradientBrush
jongleur1983 7b4ec33
Merge branch 'master' into GradientBrush
JimBobSquarePants b397622
#542: refactor tests to follow the recommended pattern for drawing te…
jongleur1983 2af95be
#542: reduce test image sizes to save submudule size
jongleur1983 92b0130
rename files to add {TPixel} generic parameter.
jongleur1983 09f6a4f
#542: remove pixel checks and base class
jongleur1983 3052fe0
#542: improve tests for elliptic gradients
jongleur1983 f290896
#542: remove redundant Asserts, cleanup code
jongleur1983 e8bb3d0
Merge branch 'master' into GradientBrush
JimBobSquarePants 98ff041
Merge branch 'master' into GradientBrush
JimBobSquarePants 46c8bd9
#542: shorten test names, improve test image filenames
jongleur1983 ea7cb83
#542: code cleanup
jongleur1983 68b5e61
Merge remote-tracking branch 'SixLabors/master'
jongleur1983 11fb7d3
Merge branch 'master' into GradientBrush
jongleur1983 8f5cb5f
#542: cleanup test file names, fix naming for RadialGradientBrush tes…
jongleur1983 e3889ab
introduce TestImageExtensions.VerifyOperation(), simplify FillRadialG…
antonfirsov ce8ca97
FillLinearGradientBrushTests #1
antonfirsov e818a54
VerticalReturnsUnicolorColumns -> VerticalBrushReturnsUnicolorRows (+…
antonfirsov d53e015
finish refactoring FillLinearGradientBrushTests
antonfirsov a217e42
FillEllipticGradientBrushTests
antonfirsov 5158f0b
update submodule
antonfirsov 152725c
Merge branch 'master' into GradientBrush
antonfirsov 9fa1e3e
add tolerance to comparison in tests
antonfirsov 195a453
Merge remote-tracking branch 'origin/master' into GradientBrush
antonfirsov 5d3daaa
#542: apply naming scheme for abstract classes
jongleur1983 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
211 changes: 211 additions & 0 deletions
211
src/ImageSharp.Drawing/Processing/Drawing/Brushes/LinearGradientBrush.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,211 @@ | ||
| using System; | ||
| using System.Numerics; | ||
|
|
||
| using SixLabors.ImageSharp.Advanced; | ||
| using SixLabors.ImageSharp.Memory; | ||
| using SixLabors.ImageSharp.PixelFormats; | ||
| using SixLabors.ImageSharp.PixelFormats.PixelBlenders; | ||
| using SixLabors.Primitives; | ||
|
|
||
| namespace SixLabors.ImageSharp.Processing.Drawing.Brushes | ||
| { | ||
| /// <summary> | ||
| /// Provides an implementation of a brush for painting gradients within areas. | ||
| /// Supported right now: | ||
| /// - a set of colors in relative distances to each other. | ||
| /// - two points to gradient along. | ||
| /// </summary> | ||
| /// <typeparam name="TPixel">The pixel format</typeparam> | ||
| public class LinearGradientBrush<TPixel> : IBrush<TPixel> | ||
| where TPixel : struct, IPixel<TPixel> | ||
| { | ||
| private readonly Point p1; | ||
|
|
||
| private readonly Point p2; | ||
|
|
||
| private readonly Tuple<float, TPixel>[] keyColors; | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="LinearGradientBrush{TPixel}"/> class. | ||
| /// </summary> | ||
| /// <param name="p1">Start point</param> | ||
| /// <param name="p2">End point</param> | ||
| /// <param name="keyColors">a set of color keys and where they are. The double must be in range [0..1] and is relative between p1 and p2.</param> | ||
| public LinearGradientBrush(Point p1, Point p2, params Tuple<float, TPixel>[] keyColors) | ||
| { | ||
| this.p1 = p1; | ||
| this.p2 = p2; | ||
| this.keyColors = keyColors; | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public BrushApplicator<TPixel> CreateApplicator(ImageFrame<TPixel> source, RectangleF region, GraphicsOptions options) | ||
| => new LinearGradientBrushApplicator(source, this.p1, this.p2, this.keyColors, region, options); | ||
|
|
||
| /// <summary> | ||
| /// The linear gradient brush applicator. | ||
| /// </summary> | ||
| private class LinearGradientBrushApplicator : BrushApplicator<TPixel> | ||
| { | ||
| private readonly Point start; | ||
|
|
||
| private readonly Point end; | ||
|
|
||
| private readonly Tuple<float, TPixel>[] colorStops; | ||
|
|
||
| /// <summary> | ||
| /// the vector along the gradient, x component | ||
| /// </summary> | ||
| private readonly float alongX; | ||
|
|
||
| /// <summary> | ||
| /// the vector along the gradient, y component | ||
| /// </summary> | ||
| private readonly float alongY; | ||
|
|
||
| /// <summary> | ||
| /// the vector perpendicular to the gradient, y component | ||
| /// </summary> | ||
| private readonly float acrossY; | ||
|
|
||
| /// <summary> | ||
| /// the vector perpendicular to the gradient, x component | ||
| /// </summary> | ||
| private readonly float acrossX; | ||
|
|
||
| /// <summary> | ||
| /// helper to speed up calculation as these dont't change | ||
| /// </summary> | ||
| private readonly float aYcX; | ||
|
|
||
| /// <summary> | ||
| /// helper to speed up calculation as these dont't change | ||
| /// </summary> | ||
| private readonly float aXcY; | ||
|
|
||
| /// <summary> | ||
| /// helper to speed up calculation as these dont't change | ||
| /// </summary> | ||
| private readonly float aXcX; | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="LinearGradientBrushApplicator" /> class. | ||
| /// </summary> | ||
| /// <param name="source">The source</param> | ||
| /// <param name="start">start point of the gradient</param> | ||
| /// <param name="end">end point of the gradient</param> | ||
| /// <param name="colorStops">tuple list of colors and their respective position between 0 and 1 on the line</param> | ||
| /// <param name="region">the region, copied from SolidColorBrush, not sure if necessary! TODO</param> | ||
| /// <param name="options">the graphics options</param> | ||
| public LinearGradientBrushApplicator( | ||
| ImageFrame<TPixel> source, | ||
| Point start, | ||
| Point end, | ||
| Tuple<float, TPixel>[] colorStops, | ||
| RectangleF region, // TODO: use region, compare with other Brushes for reference. | ||
| GraphicsOptions options) | ||
| : base(source, options) | ||
| { | ||
| this.start = start; | ||
| this.end = end; | ||
| this.colorStops = colorStops; // TODO: requires colorStops to be sorted by Item1! | ||
|
|
||
| // the along vector: | ||
| this.alongX = this.start.X - this.end.X; | ||
| this.alongY = this.start.Y - this.end.Y; | ||
|
|
||
| // the cross vector: | ||
| this.acrossX = this.alongY; | ||
| this.acrossY = -this.alongX; | ||
|
|
||
| // some helpers: | ||
| this.aYcX = this.alongY * this.acrossX; | ||
| this.aXcY = this.alongX * this.acrossY; | ||
| this.aXcX = this.alongX * this.acrossX; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets the color for a single pixel | ||
| /// </summary> | ||
| /// <param name="x">The x.</param> | ||
| /// <param name="y">The y.</param> | ||
| internal override TPixel this[int x, int y] | ||
| { | ||
| get | ||
| { | ||
| // the following formula is the result of the linear equation system that forms the vector. | ||
| // TODO: this formula should be abstracted as it's the only difference between linear and radial gradient! | ||
| float onCompleteGradient = this.RatioOnGradient(x, y); | ||
|
|
||
| var localGradientFrom = this.colorStops[0]; | ||
| Tuple<float, TPixel> localGradientTo = null; | ||
|
|
||
| // TODO: ensure colorStops has at least 2 items (technically 1 would be okay, but that's no gradient) | ||
| foreach (var colorStop in this.colorStops) | ||
| { | ||
| localGradientTo = colorStop; | ||
| if (colorStop.Item1 >= onCompleteGradient) | ||
| { | ||
| // we're done here, so break it! | ||
| break; | ||
| } | ||
|
|
||
| localGradientFrom = localGradientTo; | ||
| } | ||
|
|
||
| TPixel resultColor = default; | ||
| if (localGradientFrom.Item2.Equals(localGradientTo.Item2)) | ||
| { | ||
| resultColor = localGradientFrom.Item2; | ||
| } | ||
| else | ||
| { | ||
| var fromAsVector = localGradientFrom.Item2.ToVector4(); | ||
| var toAsVector = localGradientTo.Item2.ToVector4(); | ||
| float onLocalGradient = (onCompleteGradient - localGradientFrom.Item1) / localGradientTo.Item1; // TODO: | ||
|
|
||
| Vector4 result = PorterDuffFunctions.Normal( | ||
| fromAsVector, | ||
| toAsVector, | ||
| onLocalGradient); | ||
|
|
||
| // TODO: when resultColor is a struct, what does PackFromVector4 do here? | ||
| resultColor.PackFromVector4(result); | ||
| } | ||
|
|
||
| return resultColor; | ||
| } | ||
| } | ||
|
|
||
| private float RatioOnGradient(int x, int y) | ||
| { | ||
| return ((x / this.acrossX) - (this.alongX * y / this.aYcX)) | ||
| / (1 - (this.aXcY / this.aXcX)); | ||
| } | ||
|
|
||
| internal override void Apply(Span<float> scanline, int x, int y) | ||
| { | ||
| base.Apply(scanline, x, y); | ||
|
|
||
| // Span<TPixel> destinationRow = this.Target.GetPixelRowSpan(y).Slice(x, scanline.Length); | ||
| // MemoryManager memoryManager = this.Target.MemoryManager; | ||
| // using (IBuffer<float> amountBuffer = memoryManager.Allocate<float>(scanline.Length)) | ||
| // { | ||
| // Span<float> amountSpan = amountBuffer.Span; | ||
| // | ||
| // for (int i = 0; i < scanline.Length; i++) | ||
| // { | ||
| // amountSpan[i] = scanline[i] * this.Options.BlendPercentage; | ||
| // } | ||
| // | ||
| // this.Blender.Blend(memoryManager, destinationRow, destinationRow, this.Colors.Span, amountSpan); | ||
| // } | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public override void Dispose() | ||
| { | ||
| } | ||
| } | ||
| } | ||
| } | ||
47 changes: 47 additions & 0 deletions
47
tests/ImageSharp.Tests/Drawing/FillLinearGradientBrushTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| // Copyright (c) Six Labors and contributors. | ||
| // Licensed under the Apache License, Version 2.0. | ||
|
|
||
| using System.Numerics; | ||
| using SixLabors.ImageSharp.PixelFormats; | ||
| using SixLabors.ImageSharp.Processing.Drawing; | ||
| using Xunit; | ||
|
|
||
| namespace SixLabors.ImageSharp.Tests.Drawing | ||
| { | ||
| using System; | ||
|
|
||
| using SixLabors.ImageSharp.Processing; | ||
| using SixLabors.ImageSharp.Processing.Drawing.Brushes; | ||
| using SixLabors.ImageSharp.Processing.Overlays; | ||
|
|
||
| using Point = SixLabors.Primitives.Point; | ||
|
|
||
| public class FillLinearGradientBrushTests : FileTestBase | ||
| { | ||
| [Fact] | ||
| public void LinearGradientBrushWithEqualColorsReturnsUnicolorImage() | ||
| { | ||
| string path = TestEnvironment.CreateOutputDirectory("Fill", "LinearGradientBrush"); | ||
| using (var image = new Image<Rgba32>(500, 500)) | ||
| { | ||
| LinearGradientBrush<Rgba32> unicolorLinearGradientBrush = | ||
| new LinearGradientBrush<Rgba32>( | ||
| new Point(0, 0), | ||
| new Point(500, 0), | ||
| new Tuple<float, Rgba32>(0, Rgba32.Red), | ||
| new Tuple<float, Rgba32>(1, Rgba32.Red)); | ||
|
|
||
| image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); | ||
| image.Save($"{path}/UnicolorGradient.png"); | ||
|
|
||
| using (PixelAccessor<Rgba32> sourcePixels = image.Lock()) | ||
| { | ||
| Assert.Equal(Rgba32.Red, sourcePixels[0, 0]); | ||
| Assert.Equal(Rgba32.Red, sourcePixels[9, 9]); | ||
| Assert.Equal(Rgba32.Red, sourcePixels[199, 149]); | ||
| Assert.Equal(Rgba32.Red, sourcePixels[500, 500]); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this need to be
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thanks, you're right. fixed. |
||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest to define a small private struct instead of
Tuple<float, TPixel>. It provides:Tuple<>-s are classes on the heap)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good point. Thanks. Except that the struct can't be private as it's used as a parameter in the Brush constructor. Therefore I'll use a public nested struct instead.