Нарисовать iOS 7 в стиле Squircle программно
Я пытаюсь найти способ нарисовать иконку "Squircle" в стиле iOS 7 программно, используя основную графику. Я не спрашиваю, как нарисовать скругленный прямоугольник. Белка - это суперэллипс:
![Squircle]()
который немного отличается от правильного закругленного прямоугольника: ![Rounded rectangle vs squircle]()
![Music icon in squircle]()
Это точная формула легко доступна. Тем не менее, я не могу понять, как нарисовать это, используя, например, CGPath, не говоря уже о его заполнении, и я могу довольно легко изменить его размер. Все это, будучи совершенно точным с формулой.
Ответы
Ответ 1
Цитата из Википедии: Superellipse
При n = 1/2, в частности, каждая из четырех дуг представляет собой квадратичную кривую Безье, определяемую двумя осями; в результате каждая дуга является отрезком параболы.
Так почему бы не попытаться приблизиться к Squircle, используя кривые Безье? Обе кривые (Безье и Squircle) определяются параметрическими уравнениями.
Класс UIBezierPath имеет метод: addCurveToPoint:controlPoint1:controlPoint2:
Добавляет кубическую кривую Безье к тракту приемников.
ПРИМЕЧАНИЕ. Использование метода addQuadCurveToPoint:controlPoint:
дает худшие результаты.
Я использовал этот метод и что произошло в результате:
red line
- закругленный прямоугольник, blue line
- прямоугольник из четырех кривых Безье
![Rounded rectangle vs cubic Bezier curve]()
Если этот результат интересен - рисунок ниже.
ПРИМЕЧАНИЕ. Для достижения более точного соответствия кривой Безье может потребоваться изменить координаты четырех corner points
(теперь они соответствуют углам прямоугольника, на котором начертана фигура).
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
//set rect size for draw
float rectSize = 275.;
CGRect rectangle = CGRectMake(CGRectGetMidX(rect) - rectSize/2, CGRectGetMidY(rect) - rectSize/2, rectSize, rectSize);
//Rounded rectangle
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
UIBezierPath* roundedPath = [UIBezierPath bezierPathWithRoundedRect:rectangle cornerRadius:rectSize/4.7];
[roundedPath stroke];
//Rectangle from Fours Bezier Curves
CGContextSetStrokeColorWithColor(context, [UIColor blueColor].CGColor);
UIBezierPath *bezierCurvePath = [UIBezierPath bezierPath];
//set coner points
CGPoint topLPoint = CGPointMake(CGRectGetMinX(rectangle), CGRectGetMinY(rectangle));
CGPoint topRPoint = CGPointMake(CGRectGetMaxX(rectangle), CGRectGetMinY(rectangle));
CGPoint botLPoint = CGPointMake(CGRectGetMinX(rectangle), CGRectGetMaxY(rectangle));
CGPoint botRPoint = CGPointMake(CGRectGetMaxX(rectangle), CGRectGetMaxY(rectangle));
//set start-end points
CGPoint midRPoint = CGPointMake(CGRectGetMaxX(rectangle), CGRectGetMidY(rectangle));
CGPoint botMPoint = CGPointMake(CGRectGetMidX(rectangle), CGRectGetMaxY(rectangle));
CGPoint topMPoint = CGPointMake(CGRectGetMidX(rectangle), CGRectGetMinY(rectangle));
CGPoint midLPoint = CGPointMake(CGRectGetMinX(rectangle), CGRectGetMidY(rectangle));
//Four Bezier Curve
[bezierCurvePath moveToPoint:midLPoint];
[bezierCurvePath addCurveToPoint:topMPoint controlPoint1:topLPoint controlPoint2:topLPoint];
[bezierCurvePath moveToPoint:midLPoint];
[bezierCurvePath addCurveToPoint:botMPoint controlPoint1:botLPoint controlPoint2:botLPoint];
[bezierCurvePath moveToPoint:midRPoint];
[bezierCurvePath addCurveToPoint:topMPoint controlPoint1:topRPoint controlPoint2:topRPoint];
[bezierCurvePath moveToPoint:midRPoint];
[bezierCurvePath addCurveToPoint:botMPoint controlPoint1:botRPoint controlPoint2:botRPoint];
[bezierCurvePath stroke];
CGContextRestoreGState(context);
Ответ 2
Заполненная версия принятого ответа, также портированная на Swift:
override func draw(_ rect: CGRect) {
super.draw(rect)
guard let context = UIGraphicsGetCurrentContext() else {
return
}
context.saveGState()
let rect = self.bounds
let rectSize: CGFloat = rect.width
let rectangle = CGRect(x: rect.midX - rectSize / 2, y: rect.midY - rectSize / 2, width: rectSize, height: rectSize)
let topLPoint = CGPoint(x: rectangle.minX, y: rectangle.minY)
let topRPoint = CGPoint(x: rectangle.maxX, y: rectangle.minY)
let botLPoint = CGPoint(x: rectangle.minX, y: rectangle.maxY)
let botRPoint = CGPoint(x: rectangle.maxX, y: rectangle.maxY)
let midRPoint = CGPoint(x: rectangle.maxX, y: rectangle.midY)
let botMPoint = CGPoint(x: rectangle.midX, y: rectangle.maxY)
let topMPoint = CGPoint(x: rectangle.midX, y: rectangle.minY)
let midLPoint = CGPoint(x: rectangle.minX, y: rectangle.midY)
let bezierCurvePath = UIBezierPath()
bezierCurvePath.move(to: midLPoint)
bezierCurvePath.addCurve(to: topMPoint, controlPoint1: topLPoint, controlPoint2: topLPoint)
bezierCurvePath.addCurve(to: midRPoint, controlPoint1: topRPoint, controlPoint2: topRPoint)
bezierCurvePath.addCurve(to: botMPoint, controlPoint1: botRPoint, controlPoint2: botRPoint)
bezierCurvePath.addCurve(to: midLPoint, controlPoint1: botLPoint, controlPoint2: botLPoint)
context.setFillColor(UIColor.lightGray.cgColor)
bezierCurvePath.fill()
context.restoreGState()
}
идеально подходит для использования в подклассе UIView.
Ответ 3
Основываясь на ответах Руслана и Сункаса выше, я создал путь, который соединяет суперэллиптические "углы" с отрезками прямых линий; то есть суперэллиптический аналог правильного закругленного прямоугольника (как маска, видимая по краям симулятора iPhone X):
extension UIBezierPath {
static func superellipse(in rect: CGRect, cornerRadius: CGFloat) -> UIBezierPath {
// (Corner radius can't exceed half of the shorter side; correct if
// necessary:)
let minSide = min(rect.width, rect.height)
let radius = min(cornerRadius, minSide/2)
let topLeft = CGPoint(x: rect.minX, y: rect.minY)
let topRight = CGPoint(x: rect.maxX, y: rect.minY)
let bottomLeft = CGPoint(x: rect.minX, y: rect.maxY)
let bottomRight = CGPoint(x: rect.maxX, y: rect.maxY)
// The two points of the segment along the top side (clockwise):
let p0 = CGPoint(x: rect.minX + radius, y: rect.minY)
let p1 = CGPoint(x: rect.maxX - radius, y: rect.minY)
// The two points of the segment along the right side (clockwise):
let p2 = CGPoint(x: rect.maxX, y: rect.minY + radius)
let p3 = CGPoint(x: rect.maxX, y: rect.maxY - radius)
// The two points of the segment along the bottom side (clockwise):
let p4 = CGPoint(x: rect.maxX - radius, y: rect.maxY)
let p5 = CGPoint(x: rect.minX + radius, y: rect.maxY)
// The two points of the segment along the left side (clockwise):
let p6 = CGPoint(x: rect.minX, y: rect.maxY - radius)
let p7 = CGPoint(x: rect.minX, y: rect.minY + radius)
let path = UIBezierPath()
path.move(to: p0)
path.addLine(to: p1)
path.addCurve(to: p2, controlPoint1: topRight, controlPoint2: topRight)
path.addLine(to: p3)
path.addCurve(to: p4, controlPoint1: bottomRight, controlPoint2: bottomRight)
path.addLine(to: p5)
path.addCurve(to: p6, controlPoint1: bottomLeft, controlPoint2: bottomLeft)
path.addLine(to: p7)
path.addCurve(to: p0, controlPoint1: topLeft, controlPoint2: topLeft)
return path
}
}
Точки с p0
по p7
в коде можно визуализировать на следующей диаграмме:
![enter image description here]()
Если вы пропустите прямоугольник, который на самом деле является квадратом, а радиус угла равен половине длины стороны или превышает ее, сегменты прямой линии сжимаются (p0
"сливается" с p1
, p2
с p3
и т.д.), И вы получаете стандартный суперэллипс
Ответ 4
Это не отличный ответ, так как он не доходит до сути того, что вы просите, так как это программно, но вы можете:
- Загрузите SVG для формы значка iOS7 здесь: http://dribbble.com/shots/1127699-iOS-7-icon-shape-PSD
- Импортировать его в проект Xcode
- Добавить PocketSVG в ваш проект: https://github.com/arielelkin/PocketSVG
-
Загрузите SVG и преобразуйте его в UIBezierPath, оттуда вы можете масштабировать и преобразовывать, как вам нравится:
PocketSVG *myVectorDrawing = [[PocketSVG alloc] initFromSVGFileNamed:@"iOS_7_icon_shape"];
UIBezierPath *myBezierPath = myVectorDrawing.bezier;
// Apply your transforms here:
[myBezierPath applyTransform:CGAffineTransformMakeScale(2.5, 2.5)];
[myBezierPath applyTransform:CGAffineTransformMakeTranslation(10, 50)];
CAShapeLayer *myShapeLayer = [CAShapeLayer layer];
myShapeLayer.path = myBezierPath.CGPath;
myShapeLayer.strokeColor = [[UIColor redColor] CGColor];
myShapeLayer.lineWidth = 2;
myShapeLayer.fillColor = [[UIColor clearColor] CGColor];
[self.view.layer addSublayer:myShapeLayer];
** Возможно, стоит отметить, что форма не может быть точным суперэллипсом в любом случае: http://i.imgur.com/l0ljVRo.png
Ответ 5
Это было бы довольно легко сделать в OpenGL ES с шейдером. Просто нарисуйте квад и передайте x и y как атрибуты вершин. В шейдере фрагмента подключите x и y к уравнению. Если результат <= 1, то фрагмент находится внутри формы. Если я смогу получить свободное время, я могу попробовать это и опубликовать здесь.
Если вы хотите использовать CGPath, я думаю, что ключ должен параметризовать x и y в терминах t, который идет от 0 до 2π. Затем оценивайте x и y через регулярные интервалы. Я постараюсь понять это и в свободное время, но моя математика выглядит ржавой.
Кстати, я уверен, что Apple не использует эту формулу. См. Ссылку, которую опубликовал @millimoose: http://blog.mikeswanson.com/post/62341902567/unleashing-genetic-algorithms-on-the-ios-7-icon
Ответ 6
в iOS 13/Xcode 11 теперь вы можете использовать CALayerCornerCurve
пример
yourLayer.cornerCurver = CALayerCornerCurve.continuous
источник: https://developer.apple.com/documentation/quartzcore/calayercornercurve