UIBezierPath 减去路径

发布于 2024-12-27 02:45:34 字数 978 浏览 3 评论 0 原文

通过使用 [UIBezierPath bezierPathWithRoundedRect:byRoundingCorners:cornerRadii:],我可以创建一个圆角视图,如下所示:

我如何从这条路径中减去另一条路径(或其他方式),以创建如下路径:

subtracted view

有什么办法我可以做这样的事情吗? 伪代码:

UIBezierPath *bigMaskPath = [UIBezierPath bezierPathWithRoundedRect:bigView.bounds 
                                 byRoundingCorners:(UIRectCornerTopLeft|UIRectCornerTopRight)
                                       cornerRadii:CGSizeMake(18, 18)];
UIBezierPath *smallMaskPath = [UIBezierPath bezierPathWithRoundedRect:smalLView.bounds 
                                     byRoundingCorners:(UIRectCornerTopLeft|UIRectCornerTopRight)
                                           cornerRadii:CGSizeMake(18, 18)];

UIBezierPath *finalPath = [UIBezierPath pathBySubtractingPath:smallMaskPath fromPath:bigMaskPath];

By using [UIBezierPath bezierPathWithRoundedRect:byRoundingCorners:cornerRadii:], I am able to create a rounded view, such as this:

rounded view

How could I subtract another path from this one (or some other way), to create a path like this:

subtracted view

Is there any way I can do something like this?
Pseudocode:

UIBezierPath *bigMaskPath = [UIBezierPath bezierPathWithRoundedRect:bigView.bounds 
                                 byRoundingCorners:(UIRectCornerTopLeft|UIRectCornerTopRight)
                                       cornerRadii:CGSizeMake(18, 18)];
UIBezierPath *smallMaskPath = [UIBezierPath bezierPathWithRoundedRect:smalLView.bounds 
                                     byRoundingCorners:(UIRectCornerTopLeft|UIRectCornerTopRight)
                                           cornerRadii:CGSizeMake(18, 18)];

UIBezierPath *finalPath = [UIBezierPath pathBySubtractingPath:smallMaskPath fromPath:bigMaskPath];

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(9

葬﹪忆之殇 2025-01-03 02:45:34

实际上,对于大多数情况,有一种更简单的方法,例如在 Swift 中:

path.append(cutout.reversing())

这有效,因为默认填充规则是 非零缠绕规则

Actually there is a much simpler way for most cases, example in Swift:

path.append(cutout.reversing())

This works because the default fill rule is the non-zero winding rule.

故事↓在人 2025-01-03 02:45:34

如果你想抚摸减法的路径,你就得靠你自己了。 Apple 不提供返回(或仅描画)一条路径与另一条路径相减的 API。

如果您只想填充减去的路径(如示例图像中所示),您可以使用剪切路径来完成。不过,你必须使用一个技巧。当您向剪切路径添加路径时,新的剪切路径是旧剪切路径与添加路径的交集。因此,如果您只是将 smallMaskPath 添加到剪切路径,您最终将只填充 smallMaskPath 内的区域,这与您想要的相反。

您需要做的是将现有剪切路径与 smallMaskPath相交。幸运的是,您可以使用奇偶缠绕规则轻松地做到这一点。您可以在 Quartz 2D 编程指南

基本思想是,我们创建一个包含两个子路径的复合路径:您的 smallMaskPath 和一个完全包围您的 smallMaskPath 以及您可能想要填充的所有其他像素的巨大矩形。由于奇偶规则,smallMaskPath 内部的每个像素将被视为复合路径外部,而 smallMaskPath 外部的每个像素将被视为复合路径内部。复合路径。

那么让我们创建这个复合路径。我们将从巨大的矩形开始。并且没有比无限矩形更大的矩形:

UIBezierPath *clipPath = [UIBezierPath bezierPathWithRect:CGRectInfinite];

现在我们通过添加 smallMaskPath 使其成为复合路径:

[clipPath appendPath:smallMaskPath];

接下来我们设置路径以使用奇偶规则:

clipPath.usesEvenOddFillRule = YES;

在我们剪辑之前这条路径,我们应该保存图形状态,以便完成后可以撤消对剪切路径的更改:

CGContextSaveGState(UIGraphicsGetCurrentContext()); {

现在我们可以修改剪切路径:

    [clipPath addClip];

并且我们可以填充 bigMaskPath

    [[UIColor orangeColor] setFill];
    [bigMaskPath fill];

最后我们恢复图形状态,撤消更改到剪切路径:

} CGContextRestoreGState(UIGraphicsGetCurrentContext());

以下是代码,以防您想要复制/粘贴它:

UIBezierPath *clipPath = [UIBezierPath bezierPathWithRect:CGRectInfinite];
[clipPath appendPath:smallMaskPath];
clipPath.usesEvenOddFillRule = YES;

CGContextSaveGState(UIGraphicsGetCurrentContext()); {
    [clipPath addClip];
    [[UIColor orangeColor] setFill];
    [bigMaskPath fill];
} CGContextRestoreGState(UIGraphicsGetCurrentContext());

If you want to stroke the subtracted path, you are on your own. Apple doesn't provide an API that returns (or just strokes) the subtraction of one path from another.

If you just want to fill the subtracted path (as in your example image), you can do it using the clipping path. You have to use a trick, though. When you add a path to the clipping path, the new clipping path is the intersection of the old clipping path and the added path. So if you just add smallMaskPath to the clipping path, you will end up filling only the region inside smallMaskPath, which is the opposite of what you want.

What you need to do is intersect the existing clipping path with the inverse of smallMaskPath. Fortunately, you can do that pretty easily using the even-odd winding rule. You can read about the even-odd rule in the Quartz 2D Programming Guide.

The basic idea is that we create a compound path with two subpaths: your smallMaskPath and a huge rectangle that completely encloses your smallMaskPath and every other pixel you might want to fill. Because of the even-odd rule, every pixel inside of smallMaskPath will be treated as outside of the compound path, and every pixel outside of smallMaskPath will be treated as inside of the compound path.

So let's create this compound path. We'll start with the huge rectangle. And there is no rectangle more huge than the infinite rectangle:

UIBezierPath *clipPath = [UIBezierPath bezierPathWithRect:CGRectInfinite];

Now we make it into a compound path by adding smallMaskPath to it:

[clipPath appendPath:smallMaskPath];

Next we set the path to use the even-odd rule:

clipPath.usesEvenOddFillRule = YES;

Before we clip to this path, we should save the graphics state so that we can undo the change to the clipping path when we're done:

CGContextSaveGState(UIGraphicsGetCurrentContext()); {

Now we can modify the clipping path:

    [clipPath addClip];

and we can fill bigMaskPath:

    [[UIColor orangeColor] setFill];
    [bigMaskPath fill];

Finally we restore the graphics state, undoing the change to the clipping path:

} CGContextRestoreGState(UIGraphicsGetCurrentContext());

Here's the code all together in case you want to copy/paste it:

UIBezierPath *clipPath = [UIBezierPath bezierPathWithRect:CGRectInfinite];
[clipPath appendPath:smallMaskPath];
clipPath.usesEvenOddFillRule = YES;

CGContextSaveGState(UIGraphicsGetCurrentContext()); {
    [clipPath addClip];
    [[UIColor orangeColor] setFill];
    [bigMaskPath fill];
} CGContextRestoreGState(UIGraphicsGetCurrentContext());
酒中人 2025-01-03 02:45:34

这样就可以了,根据需要调整大小:

CGRect outerRect = {0, 0, 200, 200};
CGRect innerRect  = CGRectInset(outerRect,  30, 30);

UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:outerRect cornerRadius:10];

[path appendPath:[UIBezierPath bezierPathWithRoundedRect:innerRect cornerRadius:5]];
path.usesEvenOddFillRule = YES;

[[UIColor orangeColor] set];
[path fill];

获得所需效果的另一种非常简单的方法是绘制外部圆角矩形,更改颜色,然后在其上绘制内部圆角矩形。

This should do it, adjust sizes as you like:

CGRect outerRect = {0, 0, 200, 200};
CGRect innerRect  = CGRectInset(outerRect,  30, 30);

UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:outerRect cornerRadius:10];

[path appendPath:[UIBezierPath bezierPathWithRoundedRect:innerRect cornerRadius:5]];
path.usesEvenOddFillRule = YES;

[[UIColor orangeColor] set];
[path fill];

Another really simple way to get the effect you're after is to just draw the outer roundrect, change colors, and draw the inner one over it.

月下凄凉 2025-01-03 02:45:34

使用 @Patrick Pijnappel 答案,您可以准备测试游乐场以进行快速测试

import UIKit
import PlaygroundSupport

let view = UIView(frame: CGRect(x: 0, y: 0, width: 375, height: 647))
view.backgroundColor = UIColor.green

let someView = UIView(frame: CGRect(x:50, y: 50, width:250, height:250))
someView.backgroundColor = UIColor.red
view.addSubview(someView)

let shapeLayer = CAShapeLayer()
shapeLayer.frame = someView.bounds
shapeLayer.path = UIBezierPath(roundedRect: someView.bounds, 
                               byRoundingCorners: [UIRectCorner.bottomLeft,UIRectCorner.bottomRight] ,
                                cornerRadii: CGSize(width: 5.0, height: 5.0)).cgPath

someView.layer.mask = shapeLayer
someView.layer.masksToBounds = true

let rect = CGRect(x:0, y:0, width:200, height:100)
let cornerRadius:CGFloat = 5
let subPathSideSize:CGFloat = 25

let path = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius)
let leftSubPath = UIBezierPath(arcCenter: CGPoint(x:0, y:rect.height / 2), 
                                radius: subPathSideSize / 2, startAngle:  CGFloat(Double.pi / 2), endAngle: CGFloat(Double.pi + Double.pi / 2), clockwise: false)
leftSubPath.close()

let rightSubPath = UIBezierPath(arcCenter: CGPoint(x:rect.width, y:rect.height / 2), 
                               radius: subPathSideSize / 2, startAngle:  CGFloat(Double.pi / 2), endAngle: CGFloat(Double.pi + Double.pi / 2), clockwise: true)
rightSubPath.close()

path.append(leftSubPath)
path.append(rightSubPath.reversing())
path.append(path)

let mask = CAShapeLayer()
mask.frame = shapeLayer.bounds
mask.path = path.cgPath

someView.layer.mask = mask 

view
PlaygroundPage.current.liveView = view

在此处输入图像描述

在此处输入图像描述

Using @Patrick Pijnappel answer, you can prepare test playground for quick testing

import UIKit
import PlaygroundSupport

let view = UIView(frame: CGRect(x: 0, y: 0, width: 375, height: 647))
view.backgroundColor = UIColor.green

let someView = UIView(frame: CGRect(x:50, y: 50, width:250, height:250))
someView.backgroundColor = UIColor.red
view.addSubview(someView)

let shapeLayer = CAShapeLayer()
shapeLayer.frame = someView.bounds
shapeLayer.path = UIBezierPath(roundedRect: someView.bounds, 
                               byRoundingCorners: [UIRectCorner.bottomLeft,UIRectCorner.bottomRight] ,
                                cornerRadii: CGSize(width: 5.0, height: 5.0)).cgPath

someView.layer.mask = shapeLayer
someView.layer.masksToBounds = true

let rect = CGRect(x:0, y:0, width:200, height:100)
let cornerRadius:CGFloat = 5
let subPathSideSize:CGFloat = 25

let path = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius)
let leftSubPath = UIBezierPath(arcCenter: CGPoint(x:0, y:rect.height / 2), 
                                radius: subPathSideSize / 2, startAngle:  CGFloat(Double.pi / 2), endAngle: CGFloat(Double.pi + Double.pi / 2), clockwise: false)
leftSubPath.close()

let rightSubPath = UIBezierPath(arcCenter: CGPoint(x:rect.width, y:rect.height / 2), 
                               radius: subPathSideSize / 2, startAngle:  CGFloat(Double.pi / 2), endAngle: CGFloat(Double.pi + Double.pi / 2), clockwise: true)
rightSubPath.close()

path.append(leftSubPath)
path.append(rightSubPath.reversing())
path.append(path)

let mask = CAShapeLayer()
mask.frame = shapeLayer.bounds
mask.path = path.cgPath

someView.layer.mask = mask 

view
PlaygroundPage.current.liveView = view

enter image description here

enter image description here

如日中天 2025-01-03 02:45:34

2019 - 太容易了

我对其他答案感到惊讶,因为这非常容易做到。可能有一些我不明白的要求。但是:

p = UIBezierPath(rect: .. )
let hole = UIBezierPath(ovalIn: ... )
p.append(hole.reversing())
p.usesEvenOddFillRule = false

无论您是否进行切割,这都可以完美地工作。

没有问题,即使它们确实重叠:

在此输入图像描述

在此输入图像描述

(这是制作“凹口”或“缩进”的好方法。)

该技术是

  1. 外部路径顺时针方向,内部路径逆时针方向
  2. 使用计数规则又名非零规则

(在示例中,我实际上是在使用它作为 UIView 上的掩码...)

layerToUseAsUIViewMask.path = p
layer.mask = layerToUseAsUIViewMask

为了节省任何人输入漏洞代码...

let hole = UIBezierPath(ovalIn: CGRect(
    origin: CGPoint(x: 70, y: 10), .. use -10 for the second demo above.
    size: CGSize(width: 50, height: 50))
)

注意如果您不确定两条路径的运行方式,当然只需尝试每个

p.append(hole.reversing()) // that's acw currently on iOS

p.append(hole) // that's cw currently on iOS

直到它起作用。


类似的有用提示:

制作发光盒:

https://stackoverflow.com/a/59092828/294884

2019 - too easy

I'm surprised at the other answers, because this is extremely easy to do. Could be there's some requirements I don't understand. But:

p = UIBezierPath(rect: .. )
let hole = UIBezierPath(ovalIn: ... )
p.append(hole.reversing())
p.usesEvenOddFillRule = false

This does work perfectly whether or not you have cutting.

There is no problem, even if they do overlap:

enter image description here

and

enter image description here

(This is a great way to make a "notch" or "indentation".)

The technique is

  1. Outer path goes cw, inner path goes ccw
  2. Use counting rule aka non-zero-rule

(In the example, I'm literally using it as a mask on a UIView...)

layerToUseAsUIViewMask.path = p
layer.mask = layerToUseAsUIViewMask

To save anyone typing the hole code ...

let hole = UIBezierPath(ovalIn: CGRect(
    origin: CGPoint(x: 70, y: 10), .. use -10 for the second demo above.
    size: CGSize(width: 50, height: 50))
)

Note If you're not sure which way the two paths are running, of course just try each of

p.append(hole.reversing()) // that's acw currently on iOS

and

p.append(hole) // that's cw currently on iOS

until it works.


Similar useful tip:

Make a glow box:

https://stackoverflow.com/a/59092828/294884

醉南桥 2025-01-03 02:45:34

我一直在用头撞墙,试图找出如何使用多个重叠的 CGPath 来做到这一点。如果您重叠多次,它会重新填充上面提供的解决方案。事实证明,真正实现具有多个重叠路径的“减去”效果的方法是将上下文的混合模式设置为清除。

CGContextSetBlendMode(ctx, kCGBlendModeClear);

I've been beating my head against the wall trying to figure out how to do this with multiple overlapping CGPaths. If you overlap more than once, it refills with the solutions provided above. It turns out the way to truly attain a 'subtract' effect with multiple overlapping paths is to just set the context's blend mode to clear.

CGContextSetBlendMode(ctx, kCGBlendModeClear);

一瞬间的火花 2025-01-03 02:45:34

Patrick 通过使用非零缠绕规则为用户 NSResponder 的答案提供了改进/替代方案。这是 Swift 的完整实现,适合任何正在寻找扩展答案的人。

UIGraphicsBeginImageContextWithOptions(CGSize(width: 200, height: 200), false, 0.0)
let context = UIGraphicsGetCurrentContext()

let rectPath = UIBezierPath(roundedRect: CGRectMake(0, 0, 200, 200), cornerRadius: 10)
var cutoutPath = UIBezierPath(roundedRect: CGRectMake(30, 30, 140, 140), cornerRadius: 10)

rectPath.appendPath(cutoutPath.bezierPathByReversingPath())

UIColor.orangeColor().set()
outerForegroundPath.fill()

let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

这是要点。

您可以将其放入情节提要中以查看和使用输出。

结果

Patrick provided an improvement/alternative to user NSResponder's answer by using the non-zero winding rule. Here is a full implementation in Swift for anyone that is looking for an expanded answer.

UIGraphicsBeginImageContextWithOptions(CGSize(width: 200, height: 200), false, 0.0)
let context = UIGraphicsGetCurrentContext()

let rectPath = UIBezierPath(roundedRect: CGRectMake(0, 0, 200, 200), cornerRadius: 10)
var cutoutPath = UIBezierPath(roundedRect: CGRectMake(30, 30, 140, 140), cornerRadius: 10)

rectPath.appendPath(cutoutPath.bezierPathByReversingPath())

UIColor.orangeColor().set()
outerForegroundPath.fill()

let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

Here's a gist of this.

You can put this into a Storyboard to see and play around with the output.

The result

甜柠檬 2025-01-03 02:45:34

这是视图上的两个透明洞:

在此处输入图像描述

@IBDesignable
class HoleView: UIView {
    
    @IBInspectable
    var radius1: CGFloat = 10 { didSet { updateMask() } }
    
    @IBInspectable
    var XPositionPercent1: CGFloat = 90 { didSet { updateMask() } }
    
    @IBInspectable
    var YPositionPercent1: CGFloat = 50 { didSet { updateMask() } }
    
    
    @IBInspectable
    var radius2: CGFloat = 10 { didSet { updateMask() } }
    
    @IBInspectable
    var XPositionPercent2: CGFloat = 10 { didSet { updateMask() } }
    
    @IBInspectable
    var YPositionPercent2: CGFloat = 50 { didSet { updateMask() } }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        updateMask()
    }
    
    private func updateMask() {
        
        let path = UIBezierPath(rect: bounds)
        path.usesEvenOddFillRule = false
        
        
        // MARK: Hole 1
        let XPos1 = (bounds.width * XPositionPercent1) / 100
        let YPos1 = (bounds.height * YPositionPercent1) / 100
        let center1 = CGPoint(x: XPos1, y: YPos1)
        
        let hole1 = UIBezierPath(arcCenter: center1,
                                 radius: radius1,
                                 startAngle: 0,
                                 endAngle: 2 * .pi,
                                 clockwise: true)
        path.append(hole1)
        
        // MARK: Hole 2
        let XPos2 = (bounds.width * XPositionPercent2) / 100
        let YPos2 = (bounds.height * YPositionPercent2) / 100
        let center2 = CGPoint(x: XPos2, y: YPos2)
        let hole2 = UIBezierPath(arcCenter: center2,
                                 radius: radius2,
                                 startAngle: 0,
                                 endAngle: 2 * .pi,
                                 clockwise: true)
        path.append(hole2)
        
        
        
        let mask1 = CAShapeLayer()
        mask1.fillRule = .evenOdd
        mask1.path = path.cgPath
        
        layer.mask = mask1
    }
}

Here is two transparent hole on View:

enter image description here

@IBDesignable
class HoleView: UIView {
    
    @IBInspectable
    var radius1: CGFloat = 10 { didSet { updateMask() } }
    
    @IBInspectable
    var XPositionPercent1: CGFloat = 90 { didSet { updateMask() } }
    
    @IBInspectable
    var YPositionPercent1: CGFloat = 50 { didSet { updateMask() } }
    
    
    @IBInspectable
    var radius2: CGFloat = 10 { didSet { updateMask() } }
    
    @IBInspectable
    var XPositionPercent2: CGFloat = 10 { didSet { updateMask() } }
    
    @IBInspectable
    var YPositionPercent2: CGFloat = 50 { didSet { updateMask() } }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        updateMask()
    }
    
    private func updateMask() {
        
        let path = UIBezierPath(rect: bounds)
        path.usesEvenOddFillRule = false
        
        
        // MARK: Hole 1
        let XPos1 = (bounds.width * XPositionPercent1) / 100
        let YPos1 = (bounds.height * YPositionPercent1) / 100
        let center1 = CGPoint(x: XPos1, y: YPos1)
        
        let hole1 = UIBezierPath(arcCenter: center1,
                                 radius: radius1,
                                 startAngle: 0,
                                 endAngle: 2 * .pi,
                                 clockwise: true)
        path.append(hole1)
        
        // MARK: Hole 2
        let XPos2 = (bounds.width * XPositionPercent2) / 100
        let YPos2 = (bounds.height * YPositionPercent2) / 100
        let center2 = CGPoint(x: XPos2, y: YPos2)
        let hole2 = UIBezierPath(arcCenter: center2,
                                 radius: radius2,
                                 startAngle: 0,
                                 endAngle: 2 * .pi,
                                 clockwise: true)
        path.append(hole2)
        
        
        
        let mask1 = CAShapeLayer()
        mask1.fillRule = .evenOdd
        mask1.path = path.cgPath
        
        layer.mask = mask1
    }
}
吲‖鸣 2025-01-03 02:45:34

我没有使用减法,而是通过使用 CGPathAddLineToPoint 和 CGPathAddArcToPoint 来创建 CGPath 来解决。这也可以通过具有类似代码的 UIBiezerPath 来完成。

Instead of subtracting, I was able to solve by creating a CGPath using CGPathAddLineToPoint and CGPathAddArcToPoint. This can also be done with a UIBiezerPath with similar code.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文