Hi all,
I just picked up Paint.NET the other day after someone at work gave me a recommendation. I've been looking to try use it to create some icons and other computer graphics (as opposed to photo editing).
The issue I've come across is trying to add (the appearance of) depth to a 2D shape. I had a look at some of the tutorials to figure out how to do this and they seem to suggest either using:
motion blur, which works but is a bit of a hack and you have to process the thing after to get the correct effect, or
duplicating, shifting and merging layers repeatedly, which also works but only on the axes or diagonal and is a little laborious
So I thought in for penny, in for a pound and had a crack at writing my first plug-in to automate the job. I apologise if someone's already done this as I'm new to PdN.
The plug-in is designed to work on an image on transparent background i.e. it doesn't detect white or any other colour as background and in particular if you pass it an image with no transparency there will be no effect! There are three parameters:
Depth - how deep the offset fill is calculated at. The higher you put this value the slower the effect is to calculate as the code needs to check along the depth length for every output pixel (I need to have a ponder on whether this is optimal)
Angle - the angle at which the shape is filled back at
High Quality Mode - you'll get marginally better performance if you uncheck the box but the outer edges of the fill shape are more realistic if this mode is switched on. I put this in so the image was more responsive when you were playing around with angle and depth but to be honest I can't detect much difference in performance on my PC
I find it most useful if you duplicate the original surface layer so that after you're done you've got a surface layer and a depth layer play around with separately. In the screenshots below I've done the following:
got a bit of text
rotated it (Layers -> Rotate / Zoom ...)
duplicated the layer
recoloured the top layer yellow
used Add Depth on the bottom layer
Here's the DLL: Add Depth v1.0.zip
Not sure if this bit's for the code development forum but I attach the CodeLab source code below. I'd appreciate it if anyone can point out how to make the thing more efficient or if I've got any basic errors going on. Pointers on better coding generally are welcomed as this is the first time I've tried out C++.
// Title: Add Depth
// Author: maccas
// Submenu: Render
// Name: Add Depth
// URL: http://www.getpaint.net/redirect/plugins.html
#region UICode
int Amount1 = 10; // [1,100] Depth
double Amount2 = 45; // [-180,180] Angle
bool Amount3 = false; // [0,1] High Quality Mode
#endregion
// Global variables to hold the selection bounds (assumes a single rectangle is selected)
// Saves having to call GetSelection().GetBoundsInt() with each call to GetTransPixel()
private int maxX, maxY, minX, minY;
// Routine to ensure intergrity of integer to byte conversion
private byte Clamp2Byte(int iValue)
{
if (iValue < 0) return 0;
if (iValue > 255) return 255;
return (byte)iValue;
}
// Routine to ensure intergrity of double to byte conversion
private byte Clamp2Byte(double dValue)
{
if (dValue < 0) return 0;
if (dValue > 255) return 255;
return (byte)Math.Round(dValue);
}
// Routine returns the translated pixel that is 'depth' away from [x,y] at an angle of 'angle'
private ColorBgra GetTransPixel (Surface src, int x, int y, int depth, double angle)
{
// Declare initial variables
double deltaX, deltaY;
// Get the offset amounts
// Reverse x offset as [0,0] is top left of canvas but 0 assumed to point horizontal right
deltaX = -(double)depth * Math.Cos(angle);
deltaY = (double)depth * Math.Sin(angle);
// If the offset puts us outside the bounds of the selection then quit out
if ((x + deltaX > maxX - 1) || (x + deltaX < minX + 1) ||
(y + deltaY > maxY - 1) || (y + deltaY < minY + 1))
{
// Create pixel with hex colour #FFFFFF and full transparancy
return ColorBgra.FromBgra((byte)255,(byte)255,(byte)255,(byte)0);
}
// Declare more variables as we're in range to do something
int iDeltaX, iDeltaY;
ColorBgra srcPixel1;
// Round down the offsets plus 0.5 to get the principal offset pixel
// using the 0.5 as we're measuring to the mid-point of pixels
iDeltaX = (int)Math.Floor(deltaX + 0.5);
iDeltaY = (int)Math.Floor(deltaY + 0.5);
// Get the pricipal offset pixel
srcPixel1 = src[x+iDeltaX,y+iDeltaY];
// Only mix the 4 pixels if we're working in high quality mode
if (!Amount3) return srcPixel1;
// Declare last batch of variables for the full monty calculation
ColorBgra srcPixel2, srcPixel3, srcPixel4;
double mixX, mixY;
bool xUp;
byte outR, outG, outB, outA;
// Get the x offset pixel
if ((deltaX - iDeltaX) > 0)
{
srcPixel2 = src[x+iDeltaX+1,y+iDeltaY];
mixX = 1 - (deltaX - iDeltaX);
xUp = true;
}
else
{
srcPixel2 = src[x+iDeltaX-1,y+iDeltaY];
mixX = 1 + (deltaX - iDeltaX);
xUp = false;
}
// Get the y and the xy offset pixel
if ((deltaY - iDeltaY) > 0)
{
srcPixel3 = src[x+iDeltaX,y+iDeltaY+1];
mixY = 1 - (deltaY - iDeltaY);
if (xUp)
{
srcPixel4 = src[x+iDeltaX+1,y+iDeltaY+1];
}
else
{
srcPixel4 = src[x+iDeltaX-1,y+iDeltaY+1];
}
}
else
{
srcPixel3 = src[x+iDeltaX,y+iDeltaY-1];
mixY = 1 + (deltaY - iDeltaY);
if (xUp)
{
srcPixel4 = src[x+iDeltaX+1,y+iDeltaY-1];
}
else
{
srcPixel4 = src[x+iDeltaX-1,y+iDeltaY-1];
}
}
// Compute the output pixel as the mix of the four source pixels
outB = Clamp2Byte(mixX*mixY*(double)srcPixel1.B +
(1-mixX)*mixY*(double)srcPixel2.B +
mixX*(1-mixY)*(double)srcPixel3.B +
(1-mixX)*(1-mixY)*(double)srcPixel4.;
outG = Clamp2Byte(mixX*mixY*(double)srcPixel1.G +
(1-mixX)*mixY*(double)srcPixel2.G +
mixX*(1-mixY)*(double)srcPixel3.G +
(1-mixX)*(1-mixY)*(double)srcPixel4.G);
outR = Clamp2Byte(mixX*mixY*(double)srcPixel1.R +
(1-mixX)*mixY*(double)srcPixel2.R +
mixX*(1-mixY)*(double)srcPixel3.R +
(1-mixX)*(1-mixY)*(double)srcPixel4.R);
outA = Clamp2Byte(mixX*mixY*(double)srcPixel1.A +
(1-mixX)*mixY*(double)srcPixel2.A +
mixX*(1-mixY)*(double)srcPixel3.A +
(1-mixX)*(1-mixY)*(double)srcPixel4.A);
// Make the Pixel and return the output
return ColorBgra.FromBgra(outB,outG,outR,outA);
}
// Main routine
void Render(Surface dst, Surface src, Rectangle rect)
{
// Record the dimensions of the environment to the global variables
Rectangle selection = EnvironmentParameters.GetSelection(src.Bounds).GetBoundsInt();
minX = selection.Left;
maxX = selection.Right;
minY = selection.Top;
maxY = selection.Bottom;
// Convert degrees to radians
double rad = 2*Math.PI*(Amount2/360);
// Declare some variables we're going to need
ColorBgra pixel;
byte[] outputs = new byte[Amount1];
byte maxAlpha;
for (int y = rect.Top; y < rect.Bottom; y++)
{
for (int x = rect.Left; x < rect.Right; x++)
{
// Clear the output array & max alpha counter
for (int d = 0; d < Amount1; d++) outputs[d] = 0;
maxAlpha = 0;
// Initialse pixel variable to stop compiler complaining when setting dst[x,y]
// Not sure why I need to do this!
pixel = ColorBgra.FromBgra((byte)255,(byte)255,(byte)255,(byte)0);
// Loop through the offset distances from 1 to depth and
// record the alpha of each translated pixel
// stop if we get a fully opaque pixel
for (int d = 0; d < Amount1; d++)
{
// Get the state of the translated pixel at depth d
pixel = GetTransPixel (src, x, y, d, rad);
outputs[d] = (byte)pixel.A;
// Record the max alpha value and quit out if fully opaque
if (outputs[d] > maxAlpha) maxAlpha = outputs[d];
if (maxAlpha == 255) break;
}
// Find the first pixel at Max Alpha
// No point in running GetTransPixel() again if alpha is at max
// as pixel variable already holds correct reference
if (maxAlpha != 255)
{
for (int d = 0; d < Amount1; d++)
{
if (outputs[d] == maxAlpha)
{
pixel = GetTransPixel (src, x, y, d, rad);
d = Amount1;
}
}
}
dst[x,y] = pixel;
}
}
}
I also thought it might be useful to get hold of the the bottom surface as a separate layer, which I've coded up as a different plug-in as Code Lab scripts only seem to be able to work within one layer. It's called Offset Shadow and is in the zip file above. In and of itself it's not much to write home about as you'd just duplicate and shift a layer normally but if you use the same settings as the Add Depth plug-in you get the pixel perfect bottom layer for good measure.
Enjoy!
Maccas