BoltBait.com

CodeLab Tutorial Part 3 - Complex

How to use effect composition in CodeLab to create complex effects



If you haven't already done so, please go back and review the following:

Part 1 - Simple
Part 2 - Intermediate

Back? OK, good. Now that you've mastered those effects, it is time to move on to something a little more "complex."

Part 3 - Complex Effects

Let's define "complex" effects as those effects that call other, built-in, effects in order to compute their results. For example, what if you wanted to call Paint.NET's own Gaussian Blur effect as part of your effect? No problem! Many complex effects can be created with CodeLab. This technique is also called Effect Composition.

When designing a complex effect, you need to keep in mind the limitations of CodeLab and the Paint.NET effect system: Complex effects can call one built-in effect, then use any number of unary pixel operations and blend modes to calculate the final output pixel. You may also write any amount of custom code.

Typically, complex effects work like this:

I'm only showing the layers window here to help illustrate the fact that you have two surfaces to work with. In reality, everything happens in a single surface (and inside any selection that may be active on that surface).

Think of it this way, first you run an effect (like blur) from the source canvas to the destination canvas. Now, for your processing loop, you have two images to work from: the original image and the blurred image. Then for each pixel, you can write any type of processing you want (including any number of unary pixel operations and custom code) to modify either the source or destination pixels. Finally, you use a blending mode to mix your modified pixel with the blurred pixel on the destination canvas for the final image.

The most difficult part of creating a complex effect is coming up with the steps needed to synthesize the effect using only 2 layers and one built-in effect. For an excellent discussion of that process, read this post: http://forums.getpaint.net/index.php?/topic/3474-

Other things to keep in mind when designing a plugin: http://forums.getpaint.net/index.php?/topic/14566-

Once you have a workable idea, you'll need to collect the code necessary to implement it in CodeLab.

Here are the code blocks you'll need for the various effects you can call (I'll explain how to use them later):

// Setup for calling Gaussian Blur
GaussianBlurEffect blurEffect = new GaussianBlurEffect();
PropertyCollection bProps = blurEffect.CreatePropertyCollection();
PropertyBasedEffectConfigToken bParameters = new PropertyBasedEffectConfigToken(bProps);
bParameters.SetPropertyValue(GaussianBlurEffect.PropertyNames.Radius, Amount1); // fix
blurEffect.SetRenderInfo(bParameters, new RenderArgs(dst), new RenderArgs(src));
// Call Gaussian Blur
blurEffect.Render(new Rectangle[1] {rect},0,1);


// Setup for calling the Emboss function
EmbossEffect embossEffect = new EmbossEffect();
PropertyCollection eProps = embossEffect.CreatePropertyCollection();
PropertyBasedEffectConfigToken eParameters = new PropertyBasedEffectConfigToken(eProps);
eParameters.SetPropertyValue(EmbossEffect.PropertyNames.Angle, (double)Amount1); // fix
embossEffect.SetRenderInfo(eParameters, new RenderArgs(dst), new RenderArgs(src));
// Call the Emboss function
embossEffect.Render(new Rectangle[1] {rect},0,1);


// Setup for calling the Noise Reduction function
ReduceNoiseEffect noiseEffect = new ReduceNoiseEffect();
PropertyCollection nProps = noiseEffect.CreatePropertyCollection();
PropertyBasedEffectConfigToken nParameters = new PropertyBasedEffectConfigToken(nProps);
nParameters.SetPropertyValue(ReduceNoiseEffect.PropertyNames.Radius, 10); // fix
nParameters.SetPropertyValue(ReduceNoiseEffect.PropertyNames.Strength, Amount1); // fix
noiseEffect.SetRenderInfo(nParameters, new RenderArgs(dst), new RenderArgs(src));
// Call the Reduce Noise function
noiseEffect.Render(new Rectangle[1] {rect},0,1);


// Setup for calling the Brightness and Contrast Adjustment function
BrightnessAndContrastAdjustment bacAdjustment = new BrightnessAndContrastAdjustment();
PropertyCollection bacProps = bacAdjustment.CreatePropertyCollection();
PropertyBasedEffectConfigToken bacParameters = new PropertyBasedEffectConfigToken(bacProps);
bacParameters.SetPropertyValue(BrightnessAndContrastAdjustment.PropertyNames.Brightness, Amount1); // fix
bacParameters.SetPropertyValue(BrightnessAndContrastAdjustment.PropertyNames.Contrast, 45); // fix
bacAdjustment.SetRenderInfo(bacParameters, new RenderArgs(dst), new RenderArgs(src));
// Call the Brightness and Contrast Adjustment function
bacAdjustment.Render(new Rectangle[1] {rect},0,1);


// Setup for calling the Motion Blur effect
MotionBlurEffect blurEffect = new MotionBlurEffect();
PropertyCollection blurProps = blurEffect.CreatePropertyCollection();
PropertyBasedEffectConfigToken BlurParameters = new PropertyBasedEffectConfigToken(blurProps);
BlurParameters.SetPropertyValue(MotionBlurEffect.PropertyNames.Angle, Amount1); // fix
BlurParameters.SetPropertyValue(MotionBlurEffect.PropertyNames.Centered, Amount2); // fix
BlurParameters.SetPropertyValue(MotionBlurEffect.PropertyNames.Distance, Amount3); // fix
blurEffect.SetRenderInfo(BlurParameters, new RenderArgs(dst), new RenderArgs(src));
// Call the Motion Blur function
blurEffect.Render(new Rectangle[1] {rect},0,1);


// Setup for calling the Oil Painting effect
OilPaintingEffect oilpaintEffect = new OilPaintingEffect();
PropertyCollection oilpaintProps = oilpaintEffect.CreatePropertyCollection();
PropertyBasedEffectConfigToken oilpaintParameters = new PropertyBasedEffectConfigToken(oilpaintProps);
oilpaintParameters.SetPropertyValue(OilPaintingEffect.PropertyNames.BrushSize, Amount1); // fix
oilpaintParameters.SetPropertyValue(OilPaintingEffect.PropertyNames.Coarseness, Amount2); // fix
oilpaintEffect.SetRenderInfo(oilpaintParameters, new RenderArgs(dst), new RenderArgs(src));
// Call the Oil Painting function
oilpaintEffect.Render(new Rectangle[1] {rect},0,1);


// Setup for calling the Edge Detect effect
EdgeDetectEffect edgedetectEffect = new EdgeDetectEffect();
PropertyCollection edgeProps = edgedetectEffect.CreatePropertyCollection();
PropertyBasedEffectConfigToken EdgeParameters = new PropertyBasedEffectConfigToken(edgeProps);
EdgeParameters.SetPropertyValue(EdgeDetectEffect.PropertyNames.Angle, Amount1); // fix
edgedetectEffect.SetRenderInfo(EdgeParameters, new RenderArgs(dst), new RenderArgs(src));
// Call the Edge Detect function
edgedetectEffect.Render(new Rectangle[1] {rect},0,1);


// Setup for calling the Relief effect
ReliefEffect reliefEffect = new ReliefEffect();
PropertyCollection reliefProps = reliefEffect.CreatePropertyCollection();
PropertyBasedEffectConfigToken ReliefParameters = new PropertyBasedEffectConfigToken(reliefProps);
ReliefParameters.SetPropertyValue(ReliefEffect.PropertyNames.Angle, Amount1); // fix
reliefEffect.SetRenderInfo(ReliefParameters, new RenderArgs(dst), new RenderArgs(src));
// Call the Relief function
reliefEffect.Render(new Rectangle[1] {rect},0,1);


// Setup for calling the Sepia effect
SepiaEffect sepiaEffect = new SepiaEffect();
PropertyCollection sepiaProps = sepiaEffect.CreatePropertyCollection();
PropertyBasedEffectConfigToken SepiaParameters = new PropertyBasedEffectConfigToken(sepiaProps);
sepiaEffect.SetRenderInfo(SepiaParameters, new RenderArgs(dst), new RenderArgs(src));
// Call the Sepia function
sepiaEffect.Render(new Rectangle[1] {rect},0,1);


You can call just about any built-in effect. These are just the ones I've used when writing scripts. I'll add more here later.

Notice, in each case where you'll need to supply some parameters, I labeled the line "fix".


Unary Pixel Operations

While not necessarily complex by themselves, I will also show you how to use unary pixel operations. You can think of Unary Pixel Operations as built-in effects that can be run on a single pixel. Many of the items in Paint.NET's Adjustments menu can be called as unary pixel operations. Your effect can include as many unary pixel operations as you wish. And, you can string them together, one after another, to create an effect. They are typically used inside the innermost part of the render loop to adjust single pixels.

To include a unary pixel operation in your code, you will need to include the corresponding definition line of code from this example:

private UnaryPixelOps.Desaturate desaturateOp = new UnaryPixelOps.Desaturate();
private UnaryPixelOps.HueSaturationLightness saturationOp;
private UnaryPixelOps.Invert invertOp = new UnaryPixelOps.Invert();

Then, to use it, simply call the Apply method of the operation you've created. Here is an example of using the various unary pixel operations:

CurrentPixel = desaturateOp.Apply(CurrentPixel);
CurrentPixel = invertOp.Apply(CurrentPixel);

Notice that only a single pixel is required. After the first Apply is done, the pixel will be Black & White. Remember, you will still need to save that pixel to the destination canvas if you wish to see it.

Using HueSaturationLightness is a little more difficult. First, you must create define the operation outside of the Render function (as you did with the others above). Then, you must create the operation inside of the Render function by using any desired settings. This is done because outside of the Render function, the "Amount" variables are not accessible.

// create a saturation operation based on a UI slider
saturationOp = new UnaryPixelOps.HueSaturationLightness(0, Amount1, 0); // fix
CurrentPixel = saturationOp(CurrentPixel);

In this example, only Saturation was being modified. In reality, you can modify Hue by specifying the first parameter, Saturation as shown, and/or Lightness by specifying the third parameter.


Blending Modes

Also not very complex, blending modes are used to combine two pixels using a specified algorithm. You can include any number of built-in blending modes in your effect. These blending modes are the same ones listed in the Layer Properties page. They are typically used inside the innermost part of the render loop to combine pixels.


To include a blend operation in your code, you will need to include the corresponding line of code from this example:

private UserBlendOp normalOp = new UserBlendOps.NormalBlendOp(); // Normal
private UserBlendOp multiplyOp = new UserBlendOps.MultiplyBlendOp(); // Multiply
private UserBlendOp additiveOp = new UserBlendOps.AdditiveBlendOp(); // Additive
private UserBlendOp colorburnOp = new UserBlendOps.ColorBurnBlendOp(); // Color Burn
private UserBlendOp colordodgeOp = new UserBlendOps.ColorDodgeBlendOp(); // Color Dodge
private UserBlendOp reflectOp = new UserBlendOps.ReflectBlendOp(); // Reflect
private UserBlendOp glowOp = new UserBlendOps.GlowBlendOp(); // Glow
private UserBlendOp overlayOp = new UserBlendOps.OverlayBlendOp(); // Overlay
private UserBlendOp differenceOp = new UserBlendOps.DifferenceBlendOp(); // Difference
private UserBlendOp negationOp = new UserBlendOps.NegationBlendOp(); // Negation
private UserBlendOp lightenOp = new UserBlendOps.LightenBlendOp(); // Lighten
private UserBlendOp darkenOp = new UserBlendOps.DarkenBlendOp(); // Darken
private UserBlendOp screenOp = new UserBlendOps.ScreenBlendOp(); // Screen
private UserBlendOp xorOp = new UserBlendOps.XorBlendOp(); // Xor

Then, to use it, simply call the Apply method of the operation you've created. Here is an example of using the multiply operation:

CurrentPixel = multiplyOp.Apply(CurrentPixel, TopPixel);

You can think of the first pixel as being on a lower layer and the second pixel as being on the upper layer when the blend happens.


Putting it all together - Dreamy

Let's combine what we've learned into a single complex effect. I have an idea for a "dream"-like plugin where we will blur the source canvas to the destination canvas, then combine the source with the blurred picture using the Darken blend mode. This can be accomplished with my Gaussian Blur Plus plugin, but I like this effect so much I want to create a dedicated plugin for the effect.

Let's start with the default CodeLab script that does nothing:

#region UICode
int Amount1=0;	//[0,100]Slider 1 Description
int Amount2=0;	//[0,100]Slider 2 Description
int Amount3=0;	//[0,100]Slider 3 Description
#endregion

void Render(Surface dst, Surface src, Rectangle rect)
{
    // Delete any of these lines you don't need
    Rectangle selection = EnvironmentParameters.GetSelection(src.Bounds).GetBoundsInt();
    int CenterX = ((selection.Right - selection.Left) / 2)+selection.Left;
    int CenterY = ((selection.Bottom - selection.Top) / 2)+selection.Top;
    ColorBgra PrimaryColor = (ColorBgra)EnvironmentParameters.PrimaryColor;
    ColorBgra SecondaryColor = (ColorBgra)EnvironmentParameters.SecondaryColor;
    int BrushWidth = (int)EnvironmentParameters.BrushWidth;

    ColorBgra CurrentPixel;
    for (int y = rect.Top; y < rect.Bottom; y++)
    {
        for (int x = rect.Left; x < rect.Right; x++)
        {
            CurrentPixel = src[x,y];
            // TODO: Add pixel processing code here
            // Access RGBA values this way, for example:
            // CurrentPixel.R = (byte)PrimaryColor.R;
            // CurrentPixel.G = (byte)PrimaryColor.G;
            // CurrentPixel.B = (byte)PrimaryColor.B;
            // CurrentPixel.A = (byte)PrimaryColor.A;
            dst[x,y] = CurrentPixel;
        }
    }
}

First, let's eliminate two of the sliders (since our effect will only use one). Then, we will modify the slider left to describe our effect and have a default amount of 10, a minimum amount of 1, and a maximum amount of 20.

Also, we won't be needing any of the variables in the top section of our code, so delete them. If you're following along at home, this is what our script looks like so far:

#region UICode
int Amount1=10;	//[1,20]Radius
#endregion

void Render(Surface dst, Surface src, Rectangle rect)
{
    ColorBgra CurrentPixel;
    for (int y = rect.Top; y < rect.Bottom; y++)
    {
        for (int x = rect.Left; x < rect.Right; x++)
        {
            CurrentPixel = src[x,y];
            // TODO: Add pixel processing code here
            // Access RGBA values this way, for example:
            // CurrentPixel.R = (byte)PrimaryColor.R;
            // CurrentPixel.G = (byte)PrimaryColor.G;
            // CurrentPixel.B = (byte)PrimaryColor.B;
            // CurrentPixel.A = (byte)PrimaryColor.A;
            dst[x,y] = CurrentPixel;
        }
    }
}

Next, let's add the code necessary to call the Gaussian Blur function. We put that code above the render loops so that when our loops start, the destination canvas will already have a copy of the blurred source canvas.

#region UICode
int Amount1=10;	//[1,20]Radius
#endregion

void Render(Surface dst, Surface src, Rectangle rect)
{
    // Call the Gaussian Blur effect
    GaussianBlurEffect blurEffect = new GaussianBlurEffect();
    PropertyCollection bProps = blurEffect.CreatePropertyCollection();
    PropertyBasedEffectConfigToken bParameters = new PropertyBasedEffectConfigToken(bProps);
    bParameters.SetPropertyValue(GaussianBlurEffect.PropertyNames.Radius, Amount1);
    blurEffect.SetRenderInfo(bParameters, new RenderArgs(dst), new RenderArgs(src));
    blurEffect.Render(new Rectangle[1] {rect},0,1);
    
    ColorBgra CurrentPixel;
    for (int y = rect.Top; y < rect.Bottom; y++)
    {
        for (int x = rect.Left; x < rect.Right; x++)
        {
            CurrentPixel = src[x,y];
            // TODO: Add pixel processing code here
            // Access RGBA values this way, for example:
            // CurrentPixel.R = (byte)PrimaryColor.R;
            // CurrentPixel.G = (byte)PrimaryColor.G;
            // CurrentPixel.B = (byte)PrimaryColor.B;
            // CurrentPixel.A = (byte)PrimaryColor.A;
            dst[x,y] = CurrentPixel;
        }
    }
}


In order to use a blend operation, we will have to initialize it before the Render function by placing the following line in your code.

private UserBlendOp darkenOp = new UserBlendOps.DarkenBlendOp();

Then, we can replace the commended out section of the render loops with the actual processing we will be doing. That is, between "CurrentPixel = src[x,y];" and "dst[x,y] = CurrentPixel;" we will be combining the source pixel with the destination pixel using the darken blend op. The order of the two parameters of the blend op is important. If you don't get what you expect at first, swap them and see if it corrects the problem.

#region UICode
int Amount1=10;	//[1,20]Radius
#endregion

private UserBlendOp darkenOp = new UserBlendOps.DarkenBlendOp();

void Render(Surface dst, Surface src, Rectangle rect)
{
    // Call the Gaussian Blur effect
    GaussianBlurEffect blurEffect = new GaussianBlurEffect();
    PropertyCollection bProps = blurEffect.CreatePropertyCollection();
    PropertyBasedEffectConfigToken bParameters = new PropertyBasedEffectConfigToken(bProps);
    bParameters.SetPropertyValue(GaussianBlurEffect.PropertyNames.Radius, Amount1);
    blurEffect.SetRenderInfo(bParameters, new RenderArgs(dst), new RenderArgs(src));
    blurEffect.Render(new Rectangle[1] {rect},0,1);
    
    // Loop through all pixels and calculate final pixel values
    ColorBgra CurrentPixel;
    for (int y = rect.Top; y < rect.Bottom; y++)
    {
        for (int x = rect.Left; x < rect.Right; x++)
        {
            // Get the current pixel from source canvas
            CurrentPixel = src[x,y];
            // Combine source pixel with blured dest pixel
            // using the darken blend operation.
            CurrentPixel = darkenOp.Apply(CurrentPixel, dst[x,y]);
            // Save the results for final display.
            dst[x,y] = CurrentPixel;
        }
    }
}


Add a few comments and, we're done! You can now use the File > Save as DLL menu of CodeLab to build a DLL that you can share with your friends.


Another complex example - Feather

Here is the source code to my Object Feather plugin. In this effect, I use Gaussian Blur to soften the edges of objects. This is often used when combining cutouts.

// Title: BoltBait's Feather Object v3.0
// Author: BoltBait
// Submenu: Object
// Name: Feather
// URL: http://www.BoltBait.com/pdn
#region UICode
int Amount1 = 2; // [1,10] Feather Radius
#endregion
unsafe void Render(Surface dst, Surface src, Rectangle rect)
{
    // Call the Gaussian Blur effect
    GaussianBlurEffect blurEffect = new GaussianBlurEffect();
    PropertyCollection bProps = blurEffect.CreatePropertyCollection();
    PropertyBasedEffectConfigToken bParameters = new PropertyBasedEffectConfigToken(bProps);
    bParameters.SetPropertyValue(GaussianBlurEffect.PropertyNames.Radius, Amount1);
    blurEffect.SetRenderInfo(bParameters, new RenderArgs(dst), new RenderArgs(src));
    blurEffect.Render(new Rectangle[1] {rect},0,1);
    // Loop through all pixels fixing up the alpha values
    for (int y = rect.Top; y < rect.Bottom; y++)
    {
        ColorBgra* srcPtr = src.GetPointAddressUnchecked(rect.Left, y);
        ColorBgra* dstPtr = dst.GetPointAddressUnchecked(rect.Left, y);
        for (int x = rect.Left; x < rect.Right; x++)
        {
            ColorBgra CurrentPixel = *srcPtr;
            ColorBgra DestPixel = *dstPtr;
            // for any source pixel that is not fully transparent...
            if (CurrentPixel.A != 0)
            {
                // combine the source pixel color with the dest pixel alpha
                CurrentPixel.A = DestPixel.A;
                *dstPtr = CurrentPixel;
            }
            srcPtr++;
            dstPtr++;
        }
    }
}


Notice, that I am also using unsafe pointers. This is an advanced technique to gain a little speed in the inner loop. I don't advocate doing this as you are giving up some of the protections that writing .net code offers. But, it is a technique that many of the advanced plugin authors are using so I thought I should at least show it to you.


More Examples

Here are more examples of CodeLab scripts and VS projects that do complex effects:



Limitations of CodeLab

One limitation of this method of creating complex effects is that the built-in Clouds effect does not work. This is to say, it does work (draw clouds) but not in the way you would expect. The clouds drawn do not flow naturally from one work unit to the other. This has to do with the way the clouds effect has to be initialized.

In order for clouds to work properly, the clouds effect needs to be initialized in the OnSetRenderInfo function, which is not accessible in CodeLab. If you want to create a complex effect that uses clouds, you'll need to build it with Visual Studio. For an example, see my Render Flames effect (which includes source code).

Another limitation is that you only have two surfaces to work with, Src and Dst--and you can only write to the Dst canvas. Since the built-in effects require a source and destination surface in order to work, you will be limited to only calling ONE effect. Of course, you can call as many unary pixel operations and blending operations as you like. You just need to keep this limitation in mind as you are designing your effect. Now, that said, there are ways around this. You can create your own surface to serve as a destination for your first effect, then chain multiple effects together. Just be careful not to run out of memory--surfaces can be HUGE! This technique is beyond this tutorial and I've only used it once in all the effects I've written.


Getting Clouds to Work in CodeLab
(an imperfect example)

In the previous section, I told you that there was a limitation of CodeLab that prevented the Clouds effect from working properly. Well, that is mostly true. Here is an example script that will render Clouds properly from inside of a CodeLab script:

// Name: Make Dirty
// Submenu: Distort
// Author: BoltBait
// URL: http://www.BoltBait.com/pdn
#region UICode
int Amount1 = 1000; // [2,1000] Scale
byte Amount2 = 0; // [255] Reseed
#endregion

// Setup for calling the Render Clouds effect
private CloudsEffect cloudsEffect = new CloudsEffect();
private PropertyCollection cloudsProps = null;

// Setup for using blend ops
private UserBlendOps.MultiplyBlendOp multiplyOp = new UserBlendOps.MultiplyBlendOp();

void Render(Surface dst, Surface src, Rectangle rect)
{
    // Call the Render Clouds function 
    if (cloudsProps == null)
    {
        cloudsProps = cloudsEffect.CreatePropertyCollection();
        PropertyBasedEffectConfigToken CloudsParameters = new PropertyBasedEffectConfigToken(cloudsProps);
        CloudsParameters.SetPropertyValue(CloudsEffect.PropertyNames.Scale, (int)Amount1);
        CloudsParameters.SetPropertyValue(CloudsEffect.PropertyNames.Power, 1.0);
        CloudsParameters.SetPropertyValue(CloudsEffect.PropertyNames.BlendOp, new UserBlendOps.NormalBlendOp());
        CloudsParameters.SetPropertyValue(CloudsEffect.PropertyNames.Seed, (int)Amount2);
        cloudsEffect.SetRenderInfo(CloudsParameters, new RenderArgs(dst), new RenderArgs(src));
    }
    cloudsEffect.Render(new Rectangle[1] {rect},0,1);

    // Now in the main render loop, the dst canvas has a render of clouds
    for (int y = rect.Top; y < rect.Bottom; y++)
    {
        for (int x = rect.Left; x < rect.Right; x++)
        {
            ColorBgra CurrentPixel = src[x,y];
            CurrentPixel = multiplyOp.Apply(CurrentPixel, dst[x,y]);
            dst[x,y] = CurrentPixel;
        }
    }
}


The trick, of course, is to only initialize the Clouds effect once. That will keep all of the work units seemless. It does not fix the problem of keeping the same pattern each time the effect is called. For that, you still need to use Visual Studio as I stated in the previous post.

This works, because Paint.NET always sends a single work unit to the effect in a single thread before it sends the bulk of the work in multiple threads. Therefore, when the first work unit runs the clouds effect is initialized and then is used for all the other threads.

This is somewhat similar to defining it in the OnSetRenderInfo function of a VS solution which is done in a single thread before the effect is run. The main difference is that in the CodeLab solution it won't get reset properly at the beginning of each run.


Make Dirty

What this script does is to render a layer of clouds then use the multiply blend operation to blend the clouds layer with the original image.


Conclusion

I understand that this is an imperfect solution to using Clouds. However, this should be workable enough to get you started if your effect needs to use the clouds effect. Once you get something working it should be easy enough to transition over to a VS solution. (One good way to do that is to build a DLL in CodeLab and check the "view source" option. This will show you the entire code necessary for a VS solution. You can copy-and-paste the entire source code from that screen to a VS solution file.)


What's Next?

What is beyond complex? Ha! You'll just have to wait for the next installment to find out. Until then, have fun thinking up and building complex effects!


Once you understand this tutorial, you're probably ready for the next part:

How to Write an Effect Plugin (Part 4 of 4 - Odds and Ends)


More Information

Here is some more information that you may find useful:
CodeLab Effects Design Overview
Sample Code for User Interface Elements
Using the Code Editor
Designing a User Interface for Your Effect
Building a DLL File
CodeLab Help File
Check for CodeLab Updates
Learn C#

 

News


CodeLab 2.18 Released
(October 15, 2016)
This latest release of CodeLab for Paint.NET includes the Notepad++ editor and a full WYSIWYG help editor.
More...

HTML Editor 1.5 Released
(March 31, 2016)
This latest release is a complete rewrite adding a wysiwyg editor mode and a much improved UI.
More...

Double-Six Dominoes 3.0
(September 25, 2015)
This long-awaited refresh of the most popular dominoes game on Download.com is now available!
More...