-
Notifications
You must be signed in to change notification settings - Fork 277
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
Add Outline Object Effect #971
Merged
cameronwhite
merged 15 commits into
PintaProject:master
from
potatoes1286:patch-outline2
Sep 16, 2024
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
2a72ef2
add outlineeffect2
potatoes1286 7f4a737
Fixes and improvements to outlineeffect2
potatoes1286 6adff56
Merge remote-tracking branch 'master/master' into patch-outline2
potatoes1286 072bad2
Add tests for outline2
potatoes1286 bc0ab3b
rename outline2 -> outlineobject
potatoes1286 1060e13
add comment
potatoes1286 9c0b1f5
Add test pngs
potatoes1286 b1fe662
Add more outline object tests
potatoes1286 d38185c
Fix and optimization
potatoes1286 97892f3
Change Outline > OutlineEdge, change Feather name to Feather Object
potatoes1286 9ba6bdf
Changes to Outline Object
potatoes1286 0ca05a7
Add outline object to uninitializer
potatoes1286 7213f32
Update tests
potatoes1286 e3e05cd
Remove unused blendalpha
potatoes1286 5798412
run dotnet format
potatoes1286 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
This file contains 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
This file contains 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
This file contains 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
This file contains 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,194 @@ | ||
using System; | ||
using System.Collections.Concurrent; | ||
using System.Linq; | ||
using System.Threading.Tasks; | ||
using Cairo; | ||
using Pinta.Core; | ||
using Pinta.Gui.Widgets; | ||
|
||
namespace Pinta.Effects; | ||
|
||
public sealed class OutlineObjectEffect : BaseEffect | ||
{ | ||
|
||
public override string Icon => Pinta.Resources.Icons.EffectsStylizeOutline; | ||
|
||
// Takes two passes, so must be multithreaded internally | ||
public sealed override bool IsTileable => false; | ||
|
||
public override string Name => Translations.GetString ("Outline Object"); | ||
|
||
public override bool IsConfigurable => true; | ||
|
||
public override string EffectMenuCategory => Translations.GetString ("Object"); | ||
|
||
public OutlineObjectData Data => (OutlineObjectData) EffectData!; // NRT - Set in constructor | ||
|
||
private readonly IChromeService chrome; | ||
private readonly ISystemService system; | ||
private readonly IPaletteService palette; | ||
|
||
public OutlineObjectEffect (IServiceProvider services) | ||
{ | ||
chrome = services.GetService<IChromeService> (); | ||
system = services.GetService<ISystemService> (); | ||
palette = services.GetService<IPaletteService> (); | ||
EffectData = new OutlineObjectData (); | ||
} | ||
|
||
public override void LaunchConfiguration () | ||
=> chrome.LaunchSimpleEffectDialog (this); | ||
|
||
protected override void Render (ImageSurface src, ImageSurface dest, RectangleI roi) | ||
{ | ||
int top = roi.Top; | ||
int bottom = roi.Bottom; | ||
int left = roi.Left; | ||
int right = roi.Right; | ||
int srcHeight = src.Height; | ||
int srcWidth = src.Width; | ||
int radius = Data.Radius; | ||
int threads = system.RenderThreads; | ||
|
||
ColorBgra primaryColor = palette.PrimaryColor.ToColorBgra (); | ||
ColorBgra secondaryColor = palette.SecondaryColor.ToColorBgra (); | ||
ConcurrentBag<PointI> borderPixels = new ConcurrentBag<PointI> (); | ||
|
||
// First pass | ||
// Clean up dest, then collect all border pixels | ||
Parallel.For (top, bottom + 1, new ParallelOptions { MaxDegreeOfParallelism = threads }, y => { | ||
var srcData = src.GetReadOnlyPixelData (); | ||
|
||
// reset dest to src | ||
// Removing this causes preview to not update to lower radius levels | ||
var srcRow = srcData.Slice (y * srcWidth, srcWidth); | ||
var dstRow = dest.GetPixelData ().Slice (y * srcWidth, srcWidth); | ||
srcRow.CopyTo (dstRow); | ||
|
||
// Produces different behaviour at radius == 0 and radius == 1 | ||
// When radius == 0, only consider direct border pixels | ||
// When radius == 1, consider border pixels on diagonal | ||
Span<PointI> pixels = stackalloc PointI[8]; | ||
// Collect a list of pixels that surround the object (border pixels) | ||
for (int x = left; x <= right; x++) { | ||
PointI potentialBorderPixel = new (x, y); | ||
if (Data.OutlineBorder && (x == 0 || x == srcWidth - 1 || y == 0 || y == srcHeight - 1)) { | ||
borderPixels.Add (potentialBorderPixel); | ||
} else if (src.GetColorBgra (srcData, srcWidth, potentialBorderPixel).A <= Data.Tolerance) { | ||
// Test pixel above, below, left, & right | ||
pixels[0] = new (x - 1, y); | ||
pixels[1] = new (x + 1, y); | ||
pixels[2] = new (x, y - 1); | ||
pixels[3] = new (x, y + 1); | ||
int pixelCount = 4; | ||
if (radius == 1) { | ||
// if radius == 1, also test pixels on diagonals | ||
pixels[4] = new (x - 1, y - 1); | ||
pixels[5] = new (x - 1, y + 1); | ||
pixels[6] = new (x + 1, y - 1); | ||
pixels[7] = new (x + 1, y + 1); | ||
pixelCount = 8; | ||
} | ||
|
||
for (int i = 0; i < pixelCount; i++) { | ||
var px = pixels[i].X; | ||
var py = pixels[i].Y; | ||
if (px < 0 || px >= srcWidth || py < 0 || py >= srcHeight) | ||
continue; | ||
if (src.GetColorBgra (srcData, srcWidth, new PointI (px, py)).A > Data.Tolerance) { | ||
borderPixels.Add (potentialBorderPixel); | ||
// Remove comments below to draw border pixels | ||
// You will also have to comment out the 2nd pass because it will overwrite this | ||
//int pos = srcWidth * y + x; | ||
//borderData[pos].Bgra = 0; | ||
//borderData[pos].A = 255; | ||
|
||
break; | ||
} | ||
} | ||
|
||
|
||
} | ||
} | ||
}); | ||
|
||
|
||
// Second pass | ||
// Generate outline and blend to dest | ||
Parallel.For (top, bottom + 1, new ParallelOptions { MaxDegreeOfParallelism = threads }, y => { | ||
// otherwise produces nothing at radius == 0 | ||
if (radius == 0) | ||
radius = 1; | ||
var relevantBorderPixels = borderPixels.Where (borderPixel => borderPixel.Y > y - radius && borderPixel.Y < y + radius).ToArray (); | ||
var destRow = dest.GetPixelData ().Slice (y * srcWidth, srcWidth); | ||
Span<ColorBgra> outlineRow = stackalloc ColorBgra[destRow.Length]; | ||
|
||
for (int x = left; x <= right; x++) { | ||
byte highestAlpha = 0; | ||
|
||
// optimization: no change if destination has max alpha already | ||
if (destRow[x].A == 255) | ||
continue; | ||
|
||
if (Data.FillObjectBackground && destRow[x].A >= Data.Tolerance) | ||
highestAlpha = 255; | ||
|
||
// Grab nearest border pixel, and calculate outline alpha based off it | ||
foreach (var borderPixel in relevantBorderPixels) { | ||
if (borderPixel.X == x && borderPixel.Y == y) | ||
highestAlpha = 255; | ||
|
||
if (highestAlpha == 255) | ||
break; | ||
|
||
if (borderPixel.X > x - radius && borderPixel.X < x + radius) { | ||
var dx = borderPixel.X - x; | ||
var dy = borderPixel.Y - y; | ||
float distance = MathF.Sqrt (dx * dx + dy * dy); | ||
if (distance <= radius) { | ||
float mult = 1 - distance / radius; | ||
if (mult <= 0) | ||
continue; | ||
byte alpha = (byte) (255 * mult); | ||
if (alpha > highestAlpha) | ||
highestAlpha = alpha; | ||
} | ||
} | ||
} | ||
|
||
// Handle color gradient / no alpha gradient option | ||
var color = primaryColor; | ||
if (Data.ColorGradient) | ||
color = ColorBgra.Blend (secondaryColor, primaryColor, highestAlpha); | ||
if (!Data.AlphaGradient && highestAlpha != 0) | ||
highestAlpha = 255; | ||
|
||
outlineRow[x] = color.NewAlpha (highestAlpha).ToPremultipliedAlpha (); | ||
} | ||
// Performs alpha blending | ||
new UserBlendOps.NormalBlendOp ().Apply (outlineRow, destRow); | ||
outlineRow.CopyTo (destRow); | ||
}); | ||
} | ||
|
||
public sealed class OutlineObjectData : EffectData | ||
{ | ||
[Caption ("Radius"), MinimumValue (0), MaximumValue (100)] | ||
public int Radius { get; set; } = 6; | ||
|
||
[Caption ("Tolerance"), MinimumValue (0), MaximumValue (255)] | ||
public int Tolerance { get; set; } = 20; | ||
|
||
[Caption ("Alpha Gradient")] | ||
public bool AlphaGradient { get; set; } = true; | ||
|
||
[Caption ("Color Gradient")] | ||
public bool ColorGradient { get; set; } = true; | ||
|
||
[Caption ("Outline Border")] | ||
public bool OutlineBorder { get; set; } = false; | ||
|
||
[Caption ("Fill Object Background")] | ||
public bool FillObjectBackground { get; set; } = true; | ||
} | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains 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
This file contains 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
Oops, something went wrong.
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.
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'm not entirely sure if I'm understanding the use case for the Fill Object Background toggle, but it might be more clear to label it as making the object opaque since it's not filling with a new colour?
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.
To show, 1st is pic before, 2nd is without, 3rd is with fill object background
I'm also not too sure what to name this, but I decided "fill object background" was most fitting.
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.
Thanks, yeah that seems like a reasonable label then 👍