从圆形或甜甜圈中绘制线段

Posted

技术标签:

【中文标题】从圆形或甜甜圈中绘制线段【英文标题】:Draw segments from a circle or donut 【发布时间】:2013-03-29 18:32:01 【问题描述】:

我一直在尝试找出一种绘制线段的方法,如下图所示:

我想:

    画线段 包括渐变 包括阴影 从 0 到 n 角度为绘图设置动画

我一直在尝试使用 CGContextAddArc 和类似的电话来做到这一点,但没有走得太远。

谁能帮忙?

【问题讨论】:

您可以发布您尝试过的内容吗?一直觉得自带的绘图功能挺直观的…… 我知道你很久以前就问过了,也许找到了解决方案。但是检查我的代码,也许会发现一些东西。 github.com/sakrist/VBPieChart OP,你能重新上传你的图片吗?这个问题没有看到它是没有意义的。谢谢! :) 【参考方案1】:

这是 Rob Mayoff 答案的 Swift 3 版本。看看这种语言的效率有多高!这可能是 MView.swift 文件的内容:

import UIKit

class MView: UIView 

    var size = CGSize.zero

    override init(frame: CGRect) 
    super.init(frame: frame)
    size = frame.size
    

    required init?(coder aDecoder: NSCoder) 
        fatalError("init(coder:) has not been implemented")
    

    var niceImage: UIImage 

        let kThickness = CGFloat(20)
        let kLineWidth = CGFloat(1)
        let kShadowWidth = CGFloat(8)

        UIGraphicsBeginImageContextWithOptions(size, false, 0)

            let gc = UIGraphicsGetCurrentContext()!
            gc.addArc(center: CGPoint(x: size.width/2, y: size.height/2),
                   radius: (size.width - kThickness - kLineWidth)/2,
                   startAngle: -45°,
                   endAngle: -135°,
                   clockwise: true)

            gc.setLineWidth(kThickness)
            gc.setLineCap(.butt)
            gc.replacePathWithStrokedPath()

            let path = gc.path!

            gc.setShadow(
                offset: CGSize(width: 0, height: kShadowWidth/2),
                blur: kShadowWidth/2,
                color: UIColor.gray.cgColor
            )

            gc.beginTransparencyLayer(auxiliaryInfo: nil)

                gc.saveGState()

                    let rgb = CGColorSpaceCreateDeviceRGB()

                    let gradient = CGGradient(
                        colorsSpace: rgb,
                        colors: [UIColor.gray.cgColor, UIColor.white.cgColor] as CFArray,
                        locations: [CGFloat(0), CGFloat(1)])!

                    let bbox = path.boundingBox
                    let startP = bbox.origin
                    var endP = CGPoint(x: bbox.maxX, y: bbox.maxY);
                    if (bbox.size.width > bbox.size.height) 
                        endP.y = startP.y
                     else 
                        endP.x = startP.x
                    

                    gc.clip()

                    gc.drawLinearGradient(gradient, start: startP, end: endP,
                                          options: CGGradientDrawingOptions(rawValue: 0))

                gc.restoreGState()

                gc.addPath(path)

                gc.setLineWidth(kLineWidth)
                gc.setLineJoin(.miter)
                UIColor.black.setStroke()
                gc.strokePath()

            gc.endTransparencyLayer()


        let image = UIGraphicsGetImageFromCurrentImageContext()!
        UIGraphicsEndImageContext()
        return image
    

    override func draw(_ rect: CGRect) 
        niceImage.draw(at:.zero)
    

像这样从 viewController 调用它:

let vi = MView(frame: self.view.bounds)
self.view.addSubview(vi)

为了将度数转换为弧度,我创建了 ° 后缀运算符。所以你现在可以使用例如45°,这会将 45 度转换为弧度。 此示例适用于 Ints,如果您有需要,也可以为 Float 类型扩展这些:

postfix operator °

protocol IntegerInitializable: ExpressibleByIntegerLiteral 
  init (_: Int)


extension Int: IntegerInitializable 
  postfix public static func °(lhs: Int) -> CGFloat 
    return CGFloat(lhs) * .pi / 180
  

将此代码放入实用程序 swift 文件中。

【讨论】:

【参考方案2】:

您的问题有很多部分。

获取路径

为此类段创建路径应该不会太难。有两条弧线和两条直线。我有previously explained how you can break down a path like that 所以我不会在这里做。相反,我会幻想并通过抚摸另一条路径来创建路径。您当然可以阅读故障并自己构建路径。我所说的抚摸弧线是灰色虚线最终结果内的橙色弧线。

要描边我们首先需要它的路径。这基本上就像移动到起点并从当前角度到您希望片段覆盖的角度围绕中心绘制一个圆弧一样简单。

CGMutablePathRef arc = CGPathCreateMutable();
CGPathMoveToPoint(arc, NULL,
                  startPoint.x, startPoint.y);
CGPathAddArc(arc, NULL,
             centerPoint.x, centerPoint.y,
             radius,
             startAngle,
             endAngle,
             YES);

然后,当您拥有该路径(单弧线)时,您可以通过以一定宽度抚摸它来创建新段。生成的路径将有两条直线和两条弧线。中风从中心向内和向外的距离相等。

CGFloat lineWidth = 10.0;
CGPathRef strokedArc =
    CGPathCreateCopyByStrokingPath(arc, NULL,
                                   lineWidth,
                                   kCGLineCapButt,
                                   kCGLineJoinMiter, // the default
                                   10); // 10 is default miter limit

绘图

接下来是绘图,通常有两个主要选择:drawRect: 中的 Core Graphics 或带有 Core Animation 的形状图层。 Core Graphics 将为您提供更强大的绘图,但 Core Animation 将为您提供更好的动画性能。由于涉及路径,纯 Cora 动画将无法工作。你最终会得到奇怪的文物。然而,我们可以通过绘制层的图形上下文来使用层和核心图形的组合。

填充和抚摸片段

我们已经有了基本的形状,但是在我们添加渐变和阴影之前,我会做一个基本的填充和描边(你的图像中有一个黑色的描边)。

CGContextRef c = UIGraphicsGetCurrentContext();
CGContextAddPath(c, strokedArc);
CGContextSetFillColorWithColor(c, [UIColor lightGrayColor].CGColor);
CGContextSetStrokeColorWithColor(c, [UIColor blackColor].CGColor);
CGContextDrawPath(c, kCGPathFillStroke);

这会在屏幕上显示类似的内容

添加阴影

我要改变顺序,在渐变之前做阴影。要绘制阴影,我们需要为上下文配置阴影并绘制填充形状以使用阴影绘制它。然后我们需要恢复上下文(到阴影之前)并再次描边形状。

CGColorRef shadowColor = [UIColor colorWithWhite:0.0 alpha:0.75].CGColor;
CGContextSaveGState(c);
CGContextSetShadowWithColor(c,
                            CGSizeMake(0, 2), // Offset
                            3.0,              // Radius
                            shadowColor);
CGContextFillPath(c);
CGContextRestoreGState(c);

// Note that filling the path "consumes it" so we add it again
CGContextAddPath(c, strokedArc);
CGContextStrokePath(c);

此时结果是这样的

绘制渐变

对于渐变,我们需要一个渐变层。我在这里做了一个非常简单的两种颜色渐变,但您可以根据需要自定义它。要创建渐变,我们需要获取颜色和合适的颜色空间。然后我们可以在填充顶部(但在描边之前)绘制渐变。我们还需要将渐变蒙版到与之前相同的路径。为此,我们剪切路径。

CGFloat colors [] = 
    0.75, 1.0, // light gray   (fully opaque)
    0.90, 1.0  // lighter gray (fully opaque)
;

CGColorSpaceRef baseSpace = CGColorSpaceCreateDeviceGray(); // gray colors want gray color space
CGGradientRef gradient = CGGradientCreateWithColorComponents(baseSpace, colors, NULL, 2);
CGColorSpaceRelease(baseSpace), baseSpace = NULL;

CGContextSaveGState(c);
CGContextAddPath(c, strokedArc);
CGContextClip(c);

CGRect boundingBox = CGPathGetBoundingBox(strokedArc);
CGPoint gradientStart = CGPointMake(0, CGRectGetMinY(boundingBox));
CGPoint gradientEnd   = CGPointMake(0, CGRectGetMaxY(boundingBox));

CGContextDrawLinearGradient(c, gradient, gradientStart, gradientEnd, 0);
CGGradientRelease(gradient), gradient = NULL;
CGContextRestoreGState(c);

这完成了绘图,因为我们目前有这个结果

动画

当涉及到形状的动画时,它有all been written before: Animating Pie Slices Using a Custom CALayer。如果您尝试通过简单地为路径属性设置动画来进行绘图,您将在动画期间看到路径的一些非常时髦的扭曲。出于说明目的,下图中的阴影和渐变保持不变。

我建议您采用我在此答案中发布的绘图代码并将其应用于该文章中的动画代码。然后你应该得到你所要求的。


供参考:使用Core Animation的同一张图

简单的形状

CAShapeLayer *segment = [CAShapeLayer layer];
segment.fillColor = [UIColor lightGrayColor].CGColor;
segment.strokeColor = [UIColor blackColor].CGColor;
segment.lineWidth = 1.0;
segment.path = strokedArc;

[self.view.layer addSublayer:segment];

添加阴影

该图层具有一些与阴影相关的属性,您可以自定义这些属性。 但是您应该设置shadowPath 属性以提高性能。

segment.shadowColor = [UIColor blackColor].CGColor;
segment.shadowOffset = CGSizeMake(0, 2);
segment.shadowOpacity = 0.75;
segment.shadowRadius = 3.0;
segment.shadowPath = segment.path; // Important for performance

绘制渐变

CAGradientLayer *gradient = [CAGradientLayer layer];
gradient.colors = @[(id)[UIColor colorWithWhite:0.75 alpha:1.0].CGColor,  // light gray
                    (id)[UIColor colorWithWhite:0.90 alpha:1.0].CGColor]; // lighter gray
gradient.frame = CGPathGetBoundingBox(segment.path);

如果我们现在绘制渐变,它将位于形状顶部而不是内部。不,我们不能对形状进行渐变填充(我知道你在考虑它)。我们需要屏蔽渐变,使其超出片段。为此,我们创建 another 层作为该段的掩码。它必须是另一个层,如果掩码是层层次结构的一部分,则文档清楚地表明行为是“未定义的”。由于蒙版的坐标系将与子图层的坐标系相同,因此我们必须在设置之前平移段形状。

CAShapeLayer *mask = [CAShapeLayer layer];
CGAffineTransform translation = CGAffineTransformMakeTranslation(-CGRectGetMinX(gradient.frame),
                                                                 -CGRectGetMinY(gradient.frame));
mask.path = CGPathCreateCopyByTransformingPath(segment.path,
                                               &translation);
gradient.mask = mask;

【讨论】:

我看到所有这些答案的问题是它们使用线性渐变,它沿着直线进行过渡。我认为 OP(当然也是我)希望渐变过渡发生在沿弧线。可以这样想:如果绘制的弧是 360° 完整圆的 359°,那么在间隙的任一侧,应该有渐变颜色的一端和另一端。 @Rick 在我看来,OP 在插图中使用了线性渐变。这就是为什么我以我的方式回答的原因,我猜其他答案也是如此。如果需要,您可以针对其他类型的渐变提出专门的新问题,并参考(链接到)此问题。 @DavidRönnqvist 你能解决这个问题吗***.com/q/32498195/3767017 第一步,移动到起点我认为是多余的。弧线定义明确,无需先到达起点。 如果我有一张不规则的图片,那么我们如何在ImageView上添加CAGradientLayer呢?在Core Graphic中,我们可以使用CGContextClipToMask,现在怎么样了?【参考方案3】:

Quartz 2D Programming Guide 涵盖了您需要的一切。我建议你看一下。

但是,将所有内容放在一起可能很困难,因此我将引导您完成。我们将编写一个函数,它接受一个大小并返回一个看起来大致类似于您的片段之一的图像:

我们这样开始函数定义:

static UIImage *imageWithSize(CGSize size) 

我们需要一个常数来表示片段的厚度:

    static CGFloat const kThickness = 20;

以及勾画线段的线宽常量:

    static CGFloat const kLineWidth = 1;

以及阴影大小的常量:

    static CGFloat const kShadowWidth = 8;

接下来我们需要创建一个用于绘制的图像上下文:

    UIGraphicsBeginImageContextWithOptions(size, NO, 0); 

我在该行的末尾放了一个左大括号,因为我喜欢额外的缩进,以提醒我稍后致电UIGraphicsEndImageContext

由于我们需要调用的很多函数都是 Core Graphics(又名 Quartz 2D)函数,而不是 UIKit 函数,我们需要获取CGContext

        CGContextRef gc = UIGraphicsGetCurrentContext();

现在我们已经准备好真正开始了。首先,我们在路径中添加一个弧。弧线沿着我们要绘制的线段的中心延伸:

        CGContextAddArc(gc, size.width / 2, size.height / 2,
            (size.width - kThickness - kLineWidth) / 2,
            -M_PI / 4, -3 * M_PI / 4, YES);

现在我们将要求 Core Graphics 将路径替换为“描边”版本,以勾勒出路径。我们首先将笔画的粗细设置为我们希望段具有的粗细:

        CGContextSetLineWidth(gc, kThickness);

和we set the line cap style to “butt” so we'll have squared-off ends:

        CGContextSetLineCap(gc, kCGLineCapButt);

然后我们可以要求 Core Graphics 将路径替换为描边版本:

        CGContextReplacePathWithStrokedPath(gc);

要用线性渐变填充这条路径,我们必须告诉 Core Graphics 将所有操作裁剪到路径内部。这样做会使 Core Graphics 重置路径,但稍后我们将需要路径在边缘周围绘制黑线。所以我们将路径复制到这里:

        CGPathRef path = CGContextCopyPath(gc);

由于我们希望片段投射阴影,因此我们将在进行任何绘制之前设置阴影参数:

        CGContextSetShadowWithColor(gc,
            CGSizeMake(0, kShadowWidth / 2), kShadowWidth / 2,
            [UIColor colorWithWhite:0 alpha:0.3].CGColor);

我们将填充线段(使用渐变)和描边(绘制黑色轮廓)。我们希望两个操作都有一个阴影。我们通过开始一个透明层来告诉 Core Graphics:

        CGContextBeginTransparencyLayer(gc, 0); 

我在该行的末尾放了一个左大括号,因为我喜欢有一个额外的缩进级别,以提醒我稍后致电CGContextEndTransparencyLayer

由于我们要更改上下文的剪辑区域进行填充,但我们不想在稍后描边轮廓时进行剪辑,所以我们需要保存图形状态:

            CGContextSaveGState(gc); 

我在该行的末尾放了一个左大括号,因为我喜欢有一个额外的缩进级别,以提醒我稍后致电CGContextRestoreGState

要用渐变填充路径,我们需要创建一个渐变对象:

                CGColorSpaceRef rgb = CGColorSpaceCreateDeviceRGB();
                CGGradientRef gradient = CGGradientCreateWithColors(rgb, (__bridge CFArrayRef)@[
                    (__bridge id)[UIColor grayColor].CGColor,
                    (__bridge id)[UIColor whiteColor].CGColor
                ], (CGFloat[]) 0.0f, 1.0f );
                CGColorSpaceRelease(rgb);

我们还需要确定渐变的起点和终点。我们将使用路径边界框:

                CGRect bbox = CGContextGetPathBoundingBox(gc);
                CGPoint start = bbox.origin;
                CGPoint end = CGPointMake(CGRectGetMaxX(bbox), CGRectGetMaxY(bbox));

我们将强制水平或垂直绘制渐变,以较长者为准:

                if (bbox.size.width > bbox.size.height) 
                    end.y = start.y;
                 else 
                    end.x = start.x;
                

现在我们终于有了绘制渐变所需的一切。首先我们剪辑到路径:

                CGContextClip(gc);

然后我们绘制渐变:

                CGContextDrawLinearGradient(gc, gradient, start, end, 0);

然后我们可以释放渐变,恢复保存的图形状态:

                CGGradientRelease(gradient);
             CGContextRestoreGState(gc);

当我们调用CGContextClip 时,Core Graphics 会重置上下文的路径。路径不是已保存图形状态的一部分;这就是我们之前制作副本的原因。现在是时候使用该副本再次在上下文中设置路径了:

            CGContextAddPath(gc, path);
            CGPathRelease(path);

现在我们可以描边路径,绘制线段的黑色轮廓:

            CGContextSetLineWidth(gc, kLineWidth);
            CGContextSetLineJoin(gc, kCGLineJoinMiter);
            [[UIColor blackColor] setStroke];
            CGContextStrokePath(gc);

接下来我们告诉 Core Graphics 结束透明层。这将使它查看我们绘制的内容并在下面添加阴影:

         CGContextEndTransparencyLayer(gc);

现在我们都画完了。我们要求 UIKit 从图像上下文中创建一个UIImage,然后销毁上下文并返回图像:

    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;

您可以一起找到代码in this gist。

【讨论】:

呵呵,看来你我都花了一些时间写了一篇关于它所需要的所有核心图形的长篇解释;) 非常好!恭喜。! :) 这会线性绘制渐变,但是沿着路径绘制呢?对于较短的弧,这具有这种错觉,但对于几乎是圆形的弧,这种错觉就被打破了。圆线的一端应为 colorA,另一端应为 colorB。这可能吗?

以上是关于从圆形或甜甜圈中绘制线段的主要内容,如果未能解决你的问题,请参考以下文章

在paperjs中绘制圆形线段上的线条

看起来像甜甜圈的圆形热图

使用html5 canvas绘制圆形或弧线

使用 CAEmitterLayer 围绕圆形或 CGPath 绘制粒子

使用shader绘制矩形矩形框圆形圆形框(WebGL-Shader开发基础02)

使用shader绘制矩形矩形框圆形圆形框(WebGL-Shader开发基础02)