Search code examples
c#winforms.net-5

LinearGradientBrush results in non-smooth gradient bitmap


I'm using this function to generate a simple gradient bitmap using LinearGradientBrush which I save as a file in the end to use as a wallpaper. But the the problem is the result doesn't look "smooth", it looks rather "choppy" as you can see the colors' lines.

I use the bounds of the screen as the dimensions of the bitmap and increasing the size doesn't improve the quality (I tried 2x and even 4x).

I tried setting the InterpolationMode and SmoothingMode but neither seemed to have an effect on the end result.

Is there a way to fix this issue?

    public static Bitmap GenerateGradient(Color color1, Color color2, int width, int height, LinearGradientMode mode)
    {
        Bitmap bitmap = new(width, height);
        using (Graphics graphics = Graphics.FromImage(bitmap))
        using (LinearGradientBrush brush = new LinearGradientBrush(new Rectangle(0, 0, width, height), color1, color2, mode))
        {
            graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
            graphics.SmoothingMode = SmoothingMode.AntiAlias;
            graphics.FillRectangle(brush, new Rectangle(0, 0, width, height));
        }

        return bitmap;
    }

Edit: Here's how I call the function and save the bitmap, after re-checking the code I think the issue lies mainly with how I generate the color shades. After using named Colors instead generated shades, the result was much smoother.

//Generates a random shade of the specified color 
//by randomly adjusting the alpha value.
public static Color RandomShade(Color color)
{
    var random = new Random(DateTime.Now.Millisecond);
    int alpha = random.Next(0, 255);

    return Color.FromArgb(alpha, color);
}

private void buttonGenerate_Click(object sender, EventArgs e)
{
    int width = Screen.PrimaryScreen.Bounds.Width;
    int height = Screen.PrimaryScreen.Bounds.Height;

    Color color1 = RandomShade(Color.AliceBlue);
    Color color2 = RandomShade(Color.Navy);

    var bitmap = GenerateGradient(color1, color2, width, height, LinearGradientMode.ForwardDiagonal);            

    bitmap.Save("wallpaper.bmp", System.Drawing.Imaging.ImageFormat.Bmp);
}

Using generated shades Before

Using named colors After


Solution

  • The issue is with the RandomShade() method. By generating a shade using the alpha channel, the gradient is also interpolating this channel.

    I think an approach that would yield better results is darkening or lightening the color randomly using the other three (color) channels instead, and keep the alpha constant. In other words, vary the brightness of the colors only.

    For example:

    public static Color RandomShadeV2(Color color)
    {
        const int Range = 1000;
        const int Variance = 700;
        var random = new Random(DateTime.Now.Millisecond);
        int factor = random.Next(Range - Variance, Range + Variance + 1);
        int r = color.R * factor / Range;
        int g = color.G * factor / Range;
        int b = color.B * factor / Range;
        return Color.FromArgb(255, Math.Min(r, 255), Math.Min(g, 255), Math.Min(b, 255));
    }
    

    This will generate a shade with a brightness factor between 0.3 and 1.7 that of the original color.

    There's no particular reason for choosing those values for the brightness multiplier. These were the ones I was testing with. If you pick 0.0 to 2.0 for example, you might get very dark or very bright shades too often. The Range and Variance constants are what I use to derive the brightness factor: range is always the whole (as in 100%); and variance is what I would allow it to vary from that point, in this case 700 represents 70%, so the final factor would be between 100% - 70% = 30% = 0.3 and 100% + 70% = 170% = 1.7.