如何让画笔平滑中间没有线条

Posted

技术标签:

【中文标题】如何让画笔平滑中间没有线条【英文标题】:how to make the brush smooth without lines in the middle 【发布时间】:2015-02-16 23:06:00 【问题描述】:

大家好, 正如您在之前的画笔中看到的,中间有线条, 它不是那么平滑 如何使它顺利? (如何删除该行) 我用混合创建它

<Grid x:Name="LayoutRoot">
        <Grid.Background>
            <LinearGradientBrush EndPoint="0.452,1.962" StartPoint="1.164,-0.352">
                <GradientStop Color="#FF202020" Offset="0"/>
                <GradientStop Color="#FF545454" Offset="1"/>
            </LinearGradientBrush>
        </Grid.Background>
    </Grid>

【问题讨论】:

你能发布你创建画笔的代码吗?这是渐变画笔吗? 好的,你现在可以检查代码了。 【参考方案1】:

条带是梯度算法的产物。它必须将该区域分成带子,每个带子都填充了稍微不同的颜色。边缘实际上是一种视觉错觉,其效果是使它们比您想象的更明显。要减少这种影响,您需要减小每个波段的宽度。

解决办法是:

    填充较小的区域 - 每个条带都较窄。 增加波段数。加大两个极端之间的对比。 提高显示器的颜色分辨率。如果您有更多颜色可供选择,那么两种最终颜色之间的可用范围就会更大。

我确实意识到这些解决方案要么 a) 不可能,要么 b) 不切实际。这是一个你将不得不忍受的问题。

一种实用的解决方案可能是将画笔替换为在 Photoshop 或其他图像处理包中创建的图像。这可能会为您提供条带较少的图像 - 但是您会受到图像大小的限制 - 如果没有像素化,您将无法放大它。

【讨论】:

【参考方案2】:

前段时间,我为我的 WPF 项目编写了平滑线性渐变。它消除了条带,但有两个警告:

不能用于数据绑定颜色或DynamicResource。 它目前只支持垂直渐变,因为这就是我所需要的

它被实现为动态创建的ImageBrush,使用Ordered Dithering。另外,它也是一个MarkupExtension,因为不能简单地继承任何Brush 类(它们都是密封的)。

/// <summary>
/// Brush that lets you draw vertical linear gradient without banding.
/// </summary>
[MarkupExtensionReturnType(typeof(Brush))]
public class SmoothLinearGradientBrush : MarkupExtension

    private static PropertyInfo dpiX_;
    private static PropertyInfo dpiY_;
    private static byte[,] bayerMatrix_ = 
    
         1, 9, 3, 11 ,
         13, 5, 15, 7 ,
         1, 9, 3, 11 ,
         16, 8, 14, 6 
    ;

    static SmoothLinearGradientBrush()
    
        dpiX_ = typeof(SystemParameters).GetProperty("DpiX", BindingFlags.NonPublic | BindingFlags.Static);
        dpiY_ = typeof(SystemParameters).GetProperty("Dpi", BindingFlags.NonPublic | BindingFlags.Static);
    

    /// <summary>
    /// Gradient color at the top
    /// </summary>
    public Color From  get; set; 

    /// <summary>
    /// Gradient color at the bottom
    /// </summary>
    public Color To  get; set; 

    public override object ProvideValue(IServiceProvider serviceProvider)
    
        //If user changes dpi/virtual screen height during applicaiton lifetime,
        //wpf will scale the image up for us.
        int width = 20;
        int height = (int)SystemParameters.VirtualScreenHeight;
        int dpix = (int)dpiX_.GetValue(null);
        int dpiy = (int)dpiY_.GetValue(null);


        int stride = 4 * ((width * PixelFormats.Bgr24.BitsPerPixel + 31) / 32);

        //dithering parameters
        double bayerMatrixCoefficient = 1.0 / (bayerMatrix_.Length + 1);
        int bayerMatrixSize = bayerMatrix_.GetLength(0);

        //Create pixel data of image
        byte[] buffer = new byte[height * stride];

        for (int line = 0; line < height; line++)
        
            double scale = (double)line / height;

            for (int x = 0; x < width * 3; x += 3)
            
                //scaling of color
                double blue = ((To.B * scale) + (From.B * (1.0 - scale)));
                double green = ((To.G * scale) + (From.G * (1.0 - scale)));
                double red = ((To.R * scale) + (From.R * (1.0 - scale)));

                //ordered dithering of color
                //source: http://en.wikipedia.org/wiki/Ordered_dithering
                buffer[x + line * stride] = (byte)(blue + bayerMatrixCoefficient * bayerMatrix_[x % bayerMatrixSize, line % bayerMatrixSize]);
                buffer[x + line * stride + 1] = (byte)(green + bayerMatrixCoefficient * bayerMatrix_[x % bayerMatrixSize, line % bayerMatrixSize]);
                buffer[x + line * stride + 2] = (byte)(red + bayerMatrixCoefficient * bayerMatrix_[x % bayerMatrixSize, line % bayerMatrixSize]);
            
        

        var image = BitmapSource.Create(width, height, dpix, dpiy, PixelFormats.Bgr24, null, buffer, stride);
        image.Freeze();
        var brush = new ImageBrush(image);
        brush.Freeze();
        return brush;
    

在资源字典中的使用:

<local:SmoothLinearGradientBrush x:Key="WindowBackgroundBrush" 
                                 From="StaticResource WindowBackgroundColorLight" 
                                 To="StaticResource WindowBackgroundColorDark" />

然后是控制风格:

<Style ...>
    <Setter Property="Background" Value="StaticResource WindowBackgroundBrush" />
</Style>

【讨论】:

【参考方案3】:

我的另一个答案中的一个廉价而肮脏的选择是这样的:

将渐变添加到容器,给它一个小的负边距,使其溢出一点,向渐变添加一个 BlurEffect,然后在父容器上打开 ClipToBounds。这样,梯度会以牺牲性能为代价变得更好。

但是,根据您的用例,这可能不可行。

例子:

<Grid Height="26" Margin="-5,0" ClipToBounds="True">
    <Grid Margin="-5">
        <Grid.Effect>
            <BlurEffect Radius="6" />
        </Grid.Effect>
        <Grid.Background>
            <LinearGradientBrush>
                <GradientStop x:Name="GradientStop7" Color="Magenta" Offset="0.0" />
                <GradientStop x:Name="GradientStop8" Color="DarkOrchid" Offset=".2" />
                <GradientStop x:Name="GradientStop9" Color="Purple" Offset="1" />
            </LinearGradientBrush>
        </Grid.Background>
    </Grid>
</Grid>

负梯度应该等于模糊半径,这样它就不会在边缘变得透明。

【讨论】:

【参考方案4】:

基于 ghord 的解决方案,我实现了一个更通用的渐变。我将它实现为 FrameworkElement 中的动态图像。您可以指定所有四个角的颜色,它特别适用于非常浅色的渐变,没有任何条纹。即使使用高 DPI 显示器,图像的缩放方式也能产生完美像素的抖动。

/// <summary>
/// A class that draws a pixel-perfect, dithered gradient image
/// </summary>
public class DitheredGradientImage : FrameworkElement

    public static readonly DependencyProperty TopLeftColorProperty =
        DependencyProperty.Register(nameof(TopLeftColor), typeof(Color), typeof(DitheredGradientImage),
    new FrameworkPropertyMetadata(Colors.Transparent, FrameworkPropertyMetadataOptions.AffectsRender, OnColorsChanged));

    public static readonly DependencyProperty TopRightColorProperty =
        DependencyProperty.Register(nameof(TopRightColor), typeof(Color), typeof(DitheredGradientImage),
            new FrameworkPropertyMetadata(Colors.Transparent, FrameworkPropertyMetadataOptions.AffectsRender, OnColorsChanged));

    public static readonly DependencyProperty BottomLeftColorProperty =
        DependencyProperty.Register(nameof(BottomLeftColor), typeof(Color), typeof(DitheredGradientImage),
            new FrameworkPropertyMetadata(Colors.Transparent, FrameworkPropertyMetadataOptions.AffectsRender, OnColorsChanged));

    public static readonly DependencyProperty BottomRightColorProperty =
        DependencyProperty.Register(nameof(BottomRightColor), typeof(Color), typeof(DitheredGradientImage),
            new FrameworkPropertyMetadata(Colors.Transparent, FrameworkPropertyMetadataOptions.AffectsRender, OnColorsChanged));

    public Color TopLeftColor
    
        get  return (Color)this.GetValue(TopLeftColorProperty); 
        set  this.SetValue(TopLeftColorProperty, value); 
    

    public Color TopRightColor
    
        get  return (Color)this.GetValue(TopRightColorProperty); 
        set  this.SetValue(TopRightColorProperty, value); 
    

    public Color BottomLeftColor
    
        get  return (Color)this.GetValue(BottomLeftColorProperty); 
        set  this.SetValue(BottomLeftColorProperty, value); 
    

    public Color BottomRightColor
    
        get  return (Color)this.GetValue(BottomRightColorProperty); 
        set  this.SetValue(BottomRightColorProperty, value); 
    

    public Color TopColor
    
        get  return (TopLeftColor == TopRightColor) ? TopLeftColor : default(Color); 
        set  TopLeftColor = value; TopRightColor = value; 
    

    public Color BottomColor
    
        get  return (BottomLeftColor == BottomRightColor) ? BottomLeftColor : default(Color); 
        set  BottomLeftColor = value; BottomRightColor = value; 
    

    public Color LeftColor
    
        get  return (TopLeftColor == BottomLeftColor) ? TopLeftColor : default(Color); 
        set  TopLeftColor = value; BottomLeftColor = value; 
    

    public Color RightColor
    
        get  return (TopRightColor == BottomRightColor) ? TopRightColor : default(Color); 
        set  TopRightColor = value; BottomRightColor = value; 
    

    public double DpiScaleX => VisualTreeHelper.GetDpi(this).DpiScaleX;
    public double DpiScaleY => VisualTreeHelper.GetDpi(this).DpiScaleY;

    public double DpiX => VisualTreeHelper.GetDpi(this).PixelsPerInchX;
    public double DpiY => VisualTreeHelper.GetDpi(this).PixelsPerInchX;

    private WriteableBitmap bitmap = null;
    private int lastPixelWidth = 0;
    private int lastPixelHeight = 0;

    protected static void OnColorsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    
        // Force redraw by resetting width and height
        if (d is DitheredGradientImage di)
        
            di.lastPixelWidth = 0;
            di.lastPixelHeight = 0;
        
    

    protected void Update(int pixelWidth, int pixelHeight)
    
        // Check if bitmap needs recreation, because it it is either
        // not initialized, or too small to fit the current element size
        if ((bitmap == null) ||
            (bitmap.PixelWidth < pixelWidth) ||
            (bitmap.PixelHeight < pixelHeight))
        
            // Oversize, so that continuous resizing doesn't lead to constant bitmap recreation
            bitmap = new WriteableBitmap(pixelWidth + 256, pixelHeight + 256, DpiX, DpiY, PixelFormats.Bgra32, null);

            // As the old bitmap is gone, reset values so it gets redrawn anyway
            lastPixelWidth = 0;
            lastPixelHeight = 0;
        

        // The gradient needs to be drawn to the exact size required
        if ((lastPixelWidth != pixelWidth) || (lastPixelHeight != pixelHeight))
        
            // Draw a line-based test pattern to verify proper pixel scaling (or the lack of)
            //DrawTestPattern(pixelWidth, pixelHeight);

            // Draw the actual gradient
            DrawGradient(pixelWidth, pixelHeight);

            // Store values to avoid redrawing when nothing has changed
            lastPixelWidth = pixelWidth;
            lastPixelHeight = pixelHeight;
        
    

    protected void DrawTestPattern(int pixelWidth, int pixelHeight)
    
        try
        
            // Reserve the back buffer for updates.
            bitmap.Lock();

            unsafe
            
                // Get a pointer to the back buffer.
                IntPtr pBackBuffer = bitmap.BackBuffer;

                for (int y = 0; y < pixelHeight; y++)
                
                    for (int x = 0; x < pixelWidth; x++)
                    
                        // Draw alternate black and white lines
                        uint colorData = (y % 2 == 0) ? 0xFF000000 : 0xFFFFFFFF;
                        *(uint*)(pBackBuffer + (y * bitmap.BackBufferStride) + (x * 4)) = colorData;
                    
                
            

            // Specify the area of the bitmap that changed.
            bitmap.AddDirtyRect(new Int32Rect(0, 0, pixelWidth, pixelHeight));
        
        finally
        
            // Release the back buffer and make it available for display.
            bitmap.Unlock();
        
    

    private const byte matrixSize = 4;

    private static readonly float[,] scaledMatrix = CalculateMatrix(matrixSize);

    private static float[,] CalculateMatrix(byte n)
    
        uint[,] uintMatrix = new uint[n, n];
        uint divisor = uint.MaxValue;
        for (ushort i = 0; i < n; i++)
        
            for (ushort j = 0; j < n; j++)
            
                uintMatrix[i, j] = BitReverse(8, BitInterleave((ushort)(i ^ j), i));
                if ((uintMatrix[i, j] != 0) && (divisor > uintMatrix[i, j]))
                    divisor = uintMatrix[i, j];
            
        

        float[,] floatMatrix = new float[n, n];
        float coefficient = 1.0f / (uintMatrix.Length * divisor);
        for (ushort i = 0; i < n; i++)
        
            for (ushort j = 0; j < n; j++)
            
                floatMatrix[i, j] = uintMatrix[i, j] * coefficient;
            
        

        return floatMatrix;
    

    /// <summary>
    /// Reverse all bits
    /// </summary>
    private static uint BitReverse(byte bits, uint value)
    
        uint left = (uint)1 << (bits - 1);
        uint right = 1;
        uint result = 0;

        for (int i = (bits - 1); i >= 1; i -= 2)
        
            result |= (value & left) >> i;
            result |= (value & right) << i;
            left >>= 1;
            right <<= 1;
        
        return result;
    

    /// <summary>
    /// Bitwise interleave of two 16-bit unsigned integers
    /// </summary>
    private static uint BitInterleave(ushort x, ushort y)
    
        uint _x = x;
        _x = (_x | (_x << 8)) & 0x00FF00FF;
        _x = (_x | (_x << 4)) & 0x0F0F0F0F;
        _x = (_x | (_x << 2)) & 0x33333333;
        _x = (_x | (_x << 1)) & 0x55555555;

        uint _y = y;
        _y = (_y | (_y << 8)) & 0x00FF00FF;
        _y = (_y | (_y << 4)) & 0x0F0F0F0F;
        _y = (_y | (_y << 2)) & 0x33333333;
        _y = (_y | (_y << 1)) & 0x55555555;

        return (uint)(_x | (_y << 1));
    

    /// <summary>
    /// Draws the gradient
    /// </summary>
    protected void DrawGradient(int pixelWidth, int pixelHeight)
    
        // Caching the color values is mandatory, JIT compiler doesn't seem to cache access
        byte topLeftR = TopLeftColor.R, topLeftG = TopLeftColor.G, topLeftB = TopLeftColor.B, topLeftA = TopLeftColor.A;
        byte topRightR = TopRightColor.R, topRightG = TopRightColor.G, topRightB = TopRightColor.B, topRightA = TopRightColor.A;
        byte bottomLeftR = BottomLeftColor.R, bottomLeftG = BottomLeftColor.G, bottomLeftB = BottomLeftColor.B, bottomLeftA = BottomLeftColor.A;
        byte bottomRightR = BottomRightColor.R, bottomRightG = BottomRightColor.G, bottomRightB = BottomRightColor.B, bottomRightA = BottomRightColor.A;

        try
        
            // Reserve the back buffer for updates.
            bitmap.Lock();

            unsafe
            
                // Get a pointer to the back buffer.
                IntPtr pBackBuffer = bitmap.BackBuffer;
                int backBufferStride = bitmap.BackBufferStride;

                for (int y = 0; y < pixelHeight; y++)
                
                    float incYScale = y / (float)pixelHeight;
                    float decYScale = 1 - (y / (float)pixelHeight);

                    for (int x = 0; x < pixelWidth; x++)
                    
                        float incXScale = x / (float)pixelWidth;
                        float decXScale = 1 - (x / (float)pixelWidth);

                        float r = (((topLeftR * decXScale) + (topRightR * incXScale)) * decYScale) +
                                    (((bottomLeftR * decXScale) + (bottomRightR * incXScale)) * incYScale);
                        float g = (((topLeftG * decXScale) + (topRightG * incXScale)) * decYScale) +
                                    (((bottomLeftG * decXScale) + (bottomRightG * incXScale)) * incYScale);
                        float b = (((topLeftB * decXScale) + (topRightB * incXScale)) * decYScale) +
                                    (((bottomLeftB * decXScale) + (bottomRightB * incXScale)) * incYScale);
                        float a = (((topLeftA * decXScale) + (topRightA * incXScale)) * decYScale) +
                                    (((bottomLeftA * decXScale) + (bottomRightA * incXScale)) * incYScale);

                        byte* p = (byte*)(pBackBuffer + (y * backBufferStride) + (x * 4));

                        p[0] = (byte)(b + scaledMatrix[x % matrixSize, y % matrixSize]);
                        p[1] = (byte)(g + scaledMatrix[x % matrixSize, y % matrixSize]);
                        p[2] = (byte)(r + scaledMatrix[x % matrixSize, y % matrixSize]);
                        p[3] = (byte)a;
                    
                

                // Specify the area of the bitmap that changed.
                bitmap.AddDirtyRect(new Int32Rect(0, 0, pixelWidth, pixelHeight));
            
        
        finally
        
            bitmap.Unlock();
        
    

    protected override void OnRender(DrawingContext dc)
    
        // Math.Round seems to be used in WPF, so it's important to use the 
        // same function, otherwise you sometimes don't get pixel-perfect drawing
        int pixelWidth = (int)Math.Round(RenderSize.Width * DpiScaleX);
        int pixelHeight = (int)Math.Round(RenderSize.Height * DpiScaleY);

        // Exit if there is nothing to draw
        if ((pixelWidth == 0) || (pixelHeight == 0)) return;

        // Refresh the bitmap
        Update(pixelWidth, pixelHeight);

        if (bitmap != null)
        
            // The bitmap will be oversized, and DrawingContext.DrawImage doesn't provide
            // any other way to specify a source image rect
            CroppedBitmap croppedBitmap = new CroppedBitmap(bitmap, new Int32Rect(0, 0, pixelWidth, pixelHeight));
            dc.DrawImage(croppedBitmap, new Rect(default(Point), RenderSize));
        
    

在 XAML 中使用它:

<local:DitheredGradientImage TopLeftColor="#e5e5e5" TopRightColor="#ffffff" BottomLeftColor="#ffffff" BottomRightColor="#f5f5f5" />

【讨论】:

以上是关于如何让画笔平滑中间没有线条的主要内容,如果未能解决你的问题,请参考以下文章

在线条中间画线[重复]

ps怎么把锯齿线条变平滑?

创建一个由左、中、右图像组成的画笔,其中中间图像平铺

分库分表如何平滑过渡?

分布式数据库中间件的实现原理介绍四:平滑扩容

消息中间件学习总结(24)—— Rocketmq 集群平滑迁移数据中心记录