BoltBait.com

CodeLab Tutorial Part 2 - Intermediate

How to create CodeLab plugins that require a UI



If you have not read part 1, go ahead. I'll wait...

How to Write an Effect Plugin (Part 1 of 4 - Simple)

OK, back? Good. Now, let's tackle something a little more interesting.

What if you had, in mind, an effect that requires some settable parameters? In this installment, I'll walk you through creating an effect that uses the standard 3 sliders UI found on many Paint.NET effects.

In order to follow this tutorial, you'll need the CodeLab plugin and at least a basic knowledge of C#. So, download CodeLab and learn C# before reading on.

Again, we will start with the default CodeLab script:

#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;
        }
    }
}


The effect that we will be creating is the Color Balance effect. It has the following user interface (UI):


Now, look at the first 5 lines of the default CodeLab script:

#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

CodeLab looks at these lines in order to figure out what kind of UI your effect should have. Each line represents one of the three sliders of our UI. If we didn't need all 3 lines, we would delete them starting at the bottom of the list. For example, if we only needed 2 sliders, we would delete the "Amount3" line.

There are 4 important parts to these lines that you will need to modify so that we can create the UI we need.

  • After the '=' and before the ';' contains the default value for our slider.
    int Amount1=0; //[0,100]Slider 1 Description
    Since our default is 0, we don't need to change that.
  • After the '[' and before the ',' contains the lower limit for our slider.
    int Amount1=0; //[0,100]Slider 1 Description
    Currently that is set to 0. We will change that to -100.
  • After the ',' and before the ']' contains the upper limit for our slider.
    int Amount1=0; //[0,100]Slider 1 Description
    Currently that is set to 100 which is fine.
  • After the ']' contains the slider description that we will see in the UI.
    int Amount1=0; //[0,100]Slider 1 Description
    We will change each one to be different.

After updating the first 3 lines, they should look like this:

int Amount1=0;   //[-100,100]Cyan - Red
int Amount2=0;   //[-100,100]Magenta - Green
int Amount3=0;   //[-100,100]Yellow - Blue

Now that we have our UI done, let's work on the algorithm.

Since we will not need the center, primary color, secondary color, and brush width, as the comment says, we can delete the following lines from our source:

    // 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;

Now, on to the real "meat" of our effect:

As you recall from the last lesson, we have a render function that loops through all of the pixels on the destination canvas. That descrbes this section from our default script:

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; 
	} 
}

Looking at that code, you can see that is reads one pixel from the source canvas "CurrentPixel = src[x,y];" and writes it out to the destination canvas "dst[x,y] = CurrentPixel;". What we need to do is replace those comment lines in the middle, with our algorithm. Go ahead and delete them now. Our source code should now look like this:

#region UICode
int Amount1=0;   //[-100,100]Cyan - Red
int Amount2=0;   //[-100,100]Magenta - Green
int Amount3=0;   //[-100,100]Yellow - Blue
#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 
            dst[x,y] = CurrentPixel; 
        } 
    } 
}

Since we are going to be working on the red "R", green "G", and blue "B" colors separately, we'll need some variables to hold our temporary values while we are computing our final values. We will just define them this way: int R, G, B;

Put that line above our loop like so:

#region UICode
int Amount1=0;   //[-100,100]Cyan - Red
int Amount2=0;   //[-100,100]Magenta - Green
int Amount3=0;   //[-100,100]Yellow - Blue
#endregion

void Render(Surface dst, Surface src, Rectangle rect) 
{ 
    ColorBgra CurrentPixel; 
    int R, G, B;
    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 
            dst[x,y] = CurrentPixel; 
        } 
    } 
}

OK, now, let's finally tackle that "TODO:" line.

In order to do our pixel processing, first, we need to grab the current pixel's RGB values and put them into our temporary variables. Working only on the "TODO" section now, replace that line with this:

R = (int)CurrentPixel.R;
G = (int)CurrentPixel.G;
B = (int)CurrentPixel.B;

Next, let's start processing our pixel.

Starting with the Cyan <--> Red adjustment, let's compute how the UI slider effects our R, G, and B colors.

Remember, our color wheel?


The arrows in the drawing represent the 3 sliders of our effect.

Red is a primary color and Cyan (a secondary color) is made of a combination of Green and Blue (two primary colors). Each of the 3 sliders is similarly a primary color on one end and a secondary color on the other.

The desired amount of Red/Cyan adjustment from our UI is located in the Amount1 variable. Now, let's use that amount to adjust our R, G, and B values.

When the user moves the slider toward Red, we will be getting a positive value in Amount1. So, we need to add that value to the R variable:
R = R + Amount1;

Since we have added to the Red amount, we need to subtract from the Green and Blue amounts. As Green and Blue share the same side of that slider, we will want to adjust them each half of the adjustment:
G = G - (Amount1 / 2);
B = B - (Amount1 / 2);

And, that handles the calculations for our first slider!

Now, simply duplicate that code for our other two sliders:

#region UICode
int Amount1=0;   //[-100,100]Cyan - Red
int Amount2=0;   //[-100,100]Magenta - Green
int Amount3=0;   //[-100,100]Yellow - Blue
#endregion

void Render(Surface dst, Surface src, Rectangle rect) 
{ 
    ColorBgra CurrentPixel; 
    int R, G, B;
    for(int y = rect.Top; y < rect.Bottom; y++) 
    { 
        for (int x = rect.Left; x < rect.Right; x++) 
        { 
            CurrentPixel = src[x,y]; 
            R = (int)CurrentPixel.R;
            G = (int)CurrentPixel.G;
            B = (int)CurrentPixel.B;
            // Cyan - Red adjustment
            R = R + Amount1;
            G = G - (Amount1 / 2);
            B = B - (Amount1 / 2);
            // Magenta - Green adjustment
            G = G + Amount2;
            R = R - (Amount2 / 2);
            B = B - (Amount2 / 2);
            // Yellow - Blue adjustment
            B = B + Amount3;
            R = R - (Amount3 / 2);
            G = G - (Amount3 / 2);
            dst[x,y] = CurrentPixel; 
        } 
    } 
}

So far, so good! Now we just need to reassemble CurrentPixel from its separate pieces R, G, and B. We do that by using a couple of built-in functions of Paint.NET and one function we will be adding ourselves.

// Reassemble the color from R, G, and B
CurrentPixel = ColorBgra.FromBgra(Clamp2Byte(B),Clamp2Byte(G),Clamp2Byte(R),CurrentPixel.A);


In order to make this work, we'll need to add the following function to our script:

private byte Clamp2Byte(int iValue)
{
    if (iValue<0) return 0;
    if (iValue>255) return 255;
    return (byte)iValue;
}

Basically, what this code is doing, is creating a pixel from R, G, and B by converting each individual part to a byte value (required by the color variable). During the converting process, the ClampToByte function is used so that if any of our values went below 0 they will be converted to 0. And, if any value went above 255 it will be converted to 255. Also, notice that the original Alpha value of our source pixel is used directly as our calculations did not alter it.

So, putting that in, our code now looks like this:

#region UICode
int Amount1=0;   //[-100,100]Cyan - Red
int Amount2=0;   //[-100,100]Magenta - Green
int Amount3=0;   //[-100,100]Yellow - Blue
#endregion

private byte Clamp2Byte(int iValue)
{
    if (iValue<0) return 0;
    if (iValue>255) return 255;
    return (byte)iValue;
}

void Render(Surface dst, Surface src, Rectangle rect) 
{ 
    ColorBgra CurrentPixel; 
    int R, G, B;
    for(int y = rect.Top; y < rect.Bottom; y++) 
    { 
        for (int x = rect.Left; x < rect.Right; x++) 
        { 
            CurrentPixel = src[x,y]; 
            R = (int)CurrentPixel.R;
            G = (int)CurrentPixel.G;
            B = (int)CurrentPixel.B;
            // Cyan - Red adjustment
            R = R + Amount1;
            G = G - (Amount1 / 2);
            B = B - (Amount1 / 2);
            // Magenta - Green adjustment
            G = G + Amount2;
            R = R - (Amount2 / 2);
            B = B - (Amount2 / 2);
            // Yellow - Blue adjustment
            B = B + Amount3;
            R = R - (Amount3 / 2);
            G = G - (Amount3 / 2);
            // Reassemble the color from R, G, and B
            CurrentPixel = ColorBgra.FromBgra(Clamp2Byte(B),Clamp2Byte(G),Clamp2Byte(R),CurrentPixel.A);
            dst[x,y] = CurrentPixel; 
        } 
    } 
}


...and we're done!

Note: This code could be made more efficient, but I wanted to leave it in a state that it would be readable for a beginner.

BTW, if you have any errors in your code, they will appear in the window below your code. Double-click on the first error message and you will be taken to the place in your code that has the issue.

You will probably want to play around with your effect in CodeLab and tinker with it until you are satisfy that it works correctly. That's what CodeLab is for!

Now, the fun part... time to create a DLL of our effect so that we can share it with all of our friends!

Since your code is in CodeLab, and working well, go back and make sure your first 3 lines (that describe the sliders) are correct. You may have changed them when tinkering with your code during the testing phase.

When you are happy with that, click CodeLab's "File > Save as DLL..." menu.

You will be presented with a screen asking you for the final information before the DLL is built.


You don't need your effect to show up right in the Effects menu. If you wish, you can have it show up in a sub menu under the Effects menu. In this example, I have chosen "Adjustments".

You can also fine tune the menu text. In the example above, it says "ColorBalance1", replace this with what you want to show up in the menu. I will choose "Color Balance".

Next, click the "Select Icon" link button. From there you may pick a 16x16 pixel PNG file to use as your effect's icon. This icon will show up in the Effects menu and the History window when your effect is used.

When you are ready, click the "Build" button. If successful, your new DLL will be placed in your Paint.NET/Effects folder. Restart Paint.NET and look in your Effects menu for your new effect!

I hope you've enjoyed this installment.


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

How to Write an Effect Plugin (Part 3 of 4 - Complex)


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...