アプリケーションでグラフを描画しています。私の問題は、頂点を結ぶ線を曲線として描きたいということです。現在、私はUIBezierPath
の関数addLineToPoint:
でそれらを描いています。曲線として描きたいです。 UIBezierPath
には、この機能をサポートするために次の2つの関数があることをよく知っています。
三次曲線:addCurveToPoint:controlPoint1:controlPoint2:
二次曲線:addQuadCurveToPoint:controlPoint:
しかし、問題は私がコントロールポイントを持っていないということです。私が持っているのは2つのエンドポイントだけです。制御点を決定する方法/式も見つかりませんでした。誰かがここで私を助けることができますか?誰かがいくつかの代替案を提案できれば幸いです...
そこで、@ Fogmeisterの回答に基づいて回避策を見つけました。
UIBezierPath *path = [UIBezierPath bezierPath];
[path setLineWidth:3.0];
[path setLineCapStyle:kCGLineCapRound];
[path setLineJoinStyle:kCGLineJoinRound];
// actualPoints are my points array stored as NSValue
NSValue *value = [actualPoints objectAtIndex:0];
CGPoint p1 = [value CGPointValue];
[path moveToPoint:p1];
for (int k=1; k<[actualPoints count];k++) {
NSValue *value = [actualPoints objectAtIndex:k];
CGPoint p2 = [value CGPointValue];
CGPoint centerPoint = CGPointMake((p1.x+p2.x)/2, (p1.y+p2.y)/2);
// See if your curve is decreasing or increasing
// You can optimize it further by finding point on normal of line passing through midpoint
if (p1.y<p2.y) {
centerPoint = CGPointMake(centerPoint.x, centerPoint.y+(abs(p2.y-centerPoint.y)));
}else if(p1.y>p2.y){
centerPoint = CGPointMake(centerPoint.x, centerPoint.y-(abs(p2.y-centerPoint.y)));
}
[path addQuadCurveToPoint:p2 controlPoint:centerPoint];
p1 = p2;
}
[path stroke];
12月5日のAbid Hussainの回答を拡張します。
コードを実装しましたが、機能しましたが、結果は次のようになりました。
少し調整するだけで、私は欲しいものを手に入れることができました:
私が行ったことは、中間点を制御点として使用して、中間点にもaddQuadCurveToPointを追加することでした。以下は、Abidのサンプルに基づくコードです。それが誰かを助けることを願っています。
+ (UIBezierPath *)quadCurvedPathWithPoints:(NSArray *)points
{
UIBezierPath *path = [UIBezierPath bezierPath];
NSValue *value = points[0];
CGPoint p1 = [value CGPointValue];
[path moveToPoint:p1];
if (points.count == 2) {
value = points[1];
CGPoint p2 = [value CGPointValue];
[path addLineToPoint:p2];
return path;
}
for (NSUInteger i = 1; i < points.count; i++) {
value = points[i];
CGPoint p2 = [value CGPointValue];
CGPoint midPoint = midPointForPoints(p1, p2);
[path addQuadCurveToPoint:midPoint controlPoint:controlPointForPoints(midPoint, p1)];
[path addQuadCurveToPoint:p2 controlPoint:controlPointForPoints(midPoint, p2)];
p1 = p2;
}
return path;
}
static CGPoint midPointForPoints(CGPoint p1, CGPoint p2) {
return CGPointMake((p1.x + p2.x) / 2, (p1.y + p2.y) / 2);
}
static CGPoint controlPointForPoints(CGPoint p1, CGPoint p2) {
CGPoint controlPoint = midPointForPoints(p1, p2);
CGFloat diffY = abs(p2.y - controlPoint.y);
if (p1.y < p2.y)
controlPoint.y += diffY;
else if (p1.y > p2.y)
controlPoint.y -= diffY;
return controlPoint;
}
@ user1244109の回答を使用して、Swift 3でより良いアルゴリズムを実装します。
var data: [CGFloat] = [0, 0, 0, 0, 0, 0] {
didSet {
setNeedsDisplay()
}
}
func coordXFor(index: Int) -> CGFloat {
return bounds.height - bounds.height * data[index] / (data.max() ?? 0)
}
override func draw(_ rect: CGRect) {
let path = quadCurvedPath()
UIColor.black.setStroke()
path.lineWidth = 1
path.stroke()
}
func quadCurvedPath() -> UIBezierPath {
let path = UIBezierPath()
let step = bounds.width / CGFloat(data.count - 1)
var p1 = CGPoint(x: 0, y: coordXFor(index: 0))
path.move(to: p1)
drawPoint(point: p1, color: UIColor.red, radius: 3)
if (data.count == 2) {
path.addLine(to: CGPoint(x: step, y: coordXFor(index: 1)))
return path
}
var oldControlP: CGPoint?
for i in 1..<data.count {
let p2 = CGPoint(x: step * CGFloat(i), y: coordXFor(index: i))
drawPoint(point: p2, color: UIColor.red, radius: 3)
var p3: CGPoint?
if i == data.count - 1 {
p3 = nil
} else {
p3 = CGPoint(x: step * CGFloat(i + 1), y: coordXFor(index: i + 1))
}
let newControlP = controlPointForPoints(p1: p1, p2: p2, p3: p3)
path.addCurve(to: p2, controlPoint1: oldControlP ?? p1, controlPoint2: newControlP ?? p2)
p1 = p2
oldControlP = imaginFor(point1: newControlP, center: p2)
}
return path;
}
func imaginFor(point1: CGPoint?, center: CGPoint?) -> CGPoint? {
guard let p1 = point1, let center = center else {
return nil
}
let newX = 2 * center.x - p1.x
let diffY = abs(p1.y - center.y)
let newY = center.y + diffY * (p1.y < center.y ? 1 : -1)
return CGPoint(x: newX, y: newY)
}
func midPointForPoints(p1: CGPoint, p2: CGPoint) -> CGPoint {
return CGPoint(x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2);
}
func controlPointForPoints(p1: CGPoint, p2: CGPoint, p3: CGPoint?) -> CGPoint? {
guard let p3 = p3 else {
return nil
}
let leftMidPoint = midPointForPoints(p1: p1, p2: p2)
let rightMidPoint = midPointForPoints(p1: p2, p2: p3)
var controlPoint = midPointForPoints(p1: leftMidPoint, p2: imaginFor(point1: rightMidPoint, center: p2)!)
// this part needs for optimization
if p1.y < p2.y {
if controlPoint.y < p1.y {
controlPoint.y = p1.y
}
if controlPoint.y > p2.y {
controlPoint.y = p2.y
}
} else {
if controlPoint.y > p1.y {
controlPoint.y = p1.y
}
if controlPoint.y < p2.y {
controlPoint.y = p2.y
}
}
let imaginContol = imaginFor(point1: controlPoint, center: p2)!
if p2.y < p3.y {
if imaginContol.y < p2.y {
controlPoint.y = p2.y
}
if imaginContol.y > p3.y {
let diffY = abs(p2.y - p3.y)
controlPoint.y = p2.y + diffY * (p3.y < p2.y ? 1 : -1)
}
} else {
if imaginContol.y > p2.y {
controlPoint.y = p2.y
}
if imaginContol.y < p3.y {
let diffY = abs(p2.y - p3.y)
controlPoint.y = p2.y + diffY * (p3.y < p2.y ? 1 : -1)
}
}
return controlPoint
}
func drawPoint(point: CGPoint, color: UIColor, radius: CGFloat) {
let ovalPath = UIBezierPath(ovalIn: CGRect(x: point.x - radius, y: point.y - radius, width: radius * 2, height: radius * 2))
color.setFill()
ovalPath.fill()
}
ポイントが2つしかない場合、それらを曲線に外挿することはできません。
3つ以上のポイント(つまり、曲線に沿った複数のポイント)がある場合は、それらをたどるために大まかに曲線を描くことができます。
次のことをすると...
OK、5ポイントあるとしましょう。 p1、p2、p3、p4、およびp5。ポイントの各ペアの中間点を計算する必要があります。 m1 = p1とp2の中点(など)。。。
つまり、m1、m2、m3、およびm4があります。
これで、中間点を曲線のセクションの終点として使用し、点をクワッド曲線の制御点として使用できます。
そう...
ポイントm1に移動します。 p2を制御点として点m2に四角形の曲線を追加します。
P3を制御点として点m3に四角形の曲線を追加します。
P4を制御点として点m4に四角形の曲線を追加します。
等々...
これにより、カーブの端から離れることができます(現時点では、それらを取得する方法を思い出せません。申し訳ありません)。
私はこれについて少し作業を行い、Catmull-Romスプラインからかなり良い結果を得ました。これは、CGPointの配列(NSValuesとして格納されている)を取得し、指定されたビューに行を追加するメソッドです。
- (void)addBezierPathBetweenPoints:(NSArray *)points
toView:(UIView *)view
withColor:(UIColor *)color
andStrokeWidth:(NSUInteger)strokeWidth
{
UIBezierPath *path = [UIBezierPath bezierPath];
float granularity = 100;
[path moveToPoint:[[points firstObject] CGPointValue]];
for (int index = 1; index < points.count - 2 ; index++) {
CGPoint point0 = [[points objectAtIndex:index - 1] CGPointValue];
CGPoint point1 = [[points objectAtIndex:index] CGPointValue];
CGPoint point2 = [[points objectAtIndex:index + 1] CGPointValue];
CGPoint point3 = [[points objectAtIndex:index + 2] CGPointValue];
for (int i = 1; i < granularity ; i++) {
float t = (float) i * (1.0f / (float) granularity);
float tt = t * t;
float ttt = tt * t;
CGPoint pi;
pi.x = 0.5 * (2*point1.x+(point2.x-point0.x)*t + (2*point0.x-5*point1.x+4*point2.x-point3.x)*tt + (3*point1.x-point0.x-3*point2.x+point3.x)*ttt);
pi.y = 0.5 * (2*point1.y+(point2.y-point0.y)*t + (2*point0.y-5*point1.y+4*point2.y-point3.y)*tt + (3*point1.y-point0.y-3*point2.y+point3.y)*ttt);
if (pi.y > view.frame.size.height) {
pi.y = view.frame.size.height;
}
else if (pi.y < 0){
pi.y = 0;
}
if (pi.x > point0.x) {
[path addLineToPoint:pi];
}
}
[path addLineToPoint:point2];
}
[path addLineToPoint:[[points objectAtIndex:[points count] - 1] CGPointValue]];
CAShapeLayer *shapeView = [[CAShapeLayer alloc] init];
shapeView.path = [path CGPath];
shapeView.strokeColor = color.CGColor;
shapeView.fillColor = [UIColor clearColor].CGColor;
shapeView.lineWidth = strokeWidth;
[shapeView setLineCap:kCALineCapRound];
[view.layer addSublayer:shapeView];
}
良い解決策は、ポイントを通る直線のUIBezierPath
を作成してから、スプラインを使用して線をカーブさせることです。 Catmull-Romスプラインを実行するUIBezierPathのカテゴリを与えるこの他の回答を確認してください。 滑らかな曲線を描く-必要な方法
これが@ user1244109の答えをSwift 3
private func quadCurvedPath(with points:[CGPoint]) -> UIBezierPath {
let path = UIBezierPath()
var p1 = points[0]
path.move(to: p1)
if points.count == 2 {
path.addLine(to: points[1])
return path
}
for i in 1..<points.count {
let mid = midPoint(for: (p1, points[i]))
path.addQuadCurve(to: mid,
controlPoint: controlPoint(for: (mid, p1)))
path.addQuadCurve(to: points[i],
controlPoint: controlPoint(for: (mid, points[i])))
p1 = points[i]
}
return path
}
private func midPoint(for points: (CGPoint, CGPoint)) -> CGPoint {
return CGPoint(x: (points.0.x + points.1.x) / 2 , y: (points.0.y + points.1.y) / 2)
}
private func controlPoint(for points: (CGPoint, CGPoint)) -> CGPoint {
var controlPoint = midPoint(for: points)
let diffY = abs(points.1.y - controlPoint.y)
if points.0.y < points.1.y {
controlPoint.y += diffY
} else if points.0.y > points.1.y {
controlPoint.y -= diffY
}
return controlPoint
}
@ roman-filippovからのコードを分析している間、私はコードをいくつか単純化しました。これが完全なSwiftプレイグラウンドバージョンと、その下のObjCバージョンです。
Xの増分が規則的でない場合、元のアルゴリズムでは、ポイントがx軸上で互いに接近しているときに、いくつかの不幸な逆行線が作成されることがわかりました。次のx値を超えないように制御点を制約するだけで問題が解決するようですが、これを数学的に正当化することはできず、実験するだけです。この変更を実装する// ** added`としてマークされた2つのセクションがあります。
import UIKit
import PlaygroundSupport
infix operator °
func °(x: CGFloat, y: CGFloat) -> CGPoint {
return CGPoint(x: x, y: y)
}
extension UIBezierPath {
func drawPoint(point: CGPoint, color: UIColor, radius: CGFloat) {
let ovalPath = UIBezierPath(ovalIn: CGRect(x: point.x - radius, y: point.y - radius, width: radius * 2, height: radius * 2))
color.setFill()
ovalPath.fill()
}
func drawWithLine (point: CGPoint, color: UIColor) {
let startP = self.currentPoint
self.addLine(to: point)
drawPoint(point: point, color: color, radius: 3)
self.move(to: startP)
}
}
class TestView : UIView {
var step: CGFloat = 1.0;
var yMaximum: CGFloat = 1.0
var xMaximum: CGFloat = 1.0
var data: [CGPoint] = [] {
didSet {
xMaximum = data.reduce(-CGFloat.greatestFiniteMagnitude, { max($0, $1.x) })
yMaximum = data.reduce(-CGFloat.greatestFiniteMagnitude, { max($0, $1.y) })
setNeedsDisplay()
}
}
func scale(point: CGPoint) -> CGPoint {
return CGPoint(x: bounds.width * point.x / xMaximum ,
y: (bounds.height - bounds.height * point.y / yMaximum))
}
override func draw(_ rect: CGRect) {
if data.count <= 1 {
return
}
let path = cubicCurvedPath()
UIColor.black.setStroke()
path.lineWidth = 1
path.stroke()
}
func cubicCurvedPath() -> UIBezierPath {
let path = UIBezierPath()
var p1 = scale(point: data[0])
path.drawPoint(point: p1, color: UIColor.red, radius: 3)
path.move(to: p1)
var oldControlP = p1
for i in 0..<data.count {
let p2 = scale(point:data[i])
path.drawPoint(point: p2, color: UIColor.red, radius: 3)
var p3: CGPoint? = nil
if i < data.count - 1 {
p3 = scale(point:data [i+1])
}
let newControlP = controlPointForPoints(p1: p1, p2: p2, p3: p3)
//uncomment the following four lines to graph control points
//if let controlP = newControlP {
// path.drawWithLine(point:controlP, color: UIColor.blue)
//}
//path.drawWithLine(point:oldControlP, color: UIColor.gray)
path.addCurve(to: p2, controlPoint1: oldControlP , controlPoint2: newControlP ?? p2)
oldControlP = imaginFor(point1: newControlP, center: p2) ?? p1
//***added to algorithm
if let p3 = p3 {
if oldControlP.x > p3.x { oldControlP.x = p3.x }
}
//***
p1 = p2
}
return path;
}
func imaginFor(point1: CGPoint?, center: CGPoint?) -> CGPoint? {
//returns "mirror image" of point: the point that is symmetrical through center.
//aka opposite of midpoint; returns the point whose midpoint with point1 is center)
guard let p1 = point1, let center = center else {
return nil
}
let newX = center.x + center.x - p1.x
let newY = center.y + center.y - p1.y
return CGPoint(x: newX, y: newY)
}
func midPointForPoints(p1: CGPoint, p2: CGPoint) -> CGPoint {
return CGPoint(x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2);
}
func clamp(num: CGFloat, bounds1: CGFloat, bounds2: CGFloat) -> CGFloat {
//ensure num is between bounds.
if (bounds1 < bounds2) {
return min(max(bounds1,num),bounds2);
} else {
return min(max(bounds2,num),bounds1);
}
}
func controlPointForPoints(p1: CGPoint, p2: CGPoint, p3: CGPoint?) -> CGPoint? {
guard let p3 = p3 else {
return nil
}
let leftMidPoint = midPointForPoints(p1: p1, p2: p2)
let rightMidPoint = midPointForPoints(p1: p2, p2: p3)
let imaginPoint = imaginFor(point1: rightMidPoint, center: p2)
var controlPoint = midPointForPoints(p1: leftMidPoint, p2: imaginPoint!)
controlPoint.y = clamp(num: controlPoint.y, bounds1: p1.y, bounds2: p2.y)
let flippedP3 = p2.y + (p2.y-p3.y)
controlPoint.y = clamp(num: controlPoint.y, bounds1: p2.y, bounds2: flippedP3);
//***added:
controlPoint.x = clamp (num:controlPoint.x, bounds1: p1.x, bounds2: p2.x)
//***
// print ("p1: \(p1), p2: \(p2), p3: \(p3), LM:\(leftMidPoint), RM:\(rightMidPoint), IP:\(imaginPoint), fP3:\(flippedP3), CP:\(controlPoint)")
return controlPoint
}
}
let u = TestView(frame: CGRect(x: 0, y: 0, width: 700, height: 600));
u.backgroundColor = UIColor.white
PlaygroundPage.current.liveView = u
u.data = [0.5 ° 1, 1 ° 3, 2 ° 5, 4 ° 9, 8 ° 15, 9.4 ° 8, 9.5 ° 10, 12 ° 4, 13 ° 10, 15 ° 3, 16 ° 1]
そして、ObjCの同じコード(多かれ少なかれ。これには描画ルーチン自体は含まれていません。また、points配列に欠落データを含めることができます。
+ (UIBezierPath *)pathWithPoints:(NSArray <NSValue *> *)points open:(BOOL) open {
//based on Roman Filippov code: http://stackoverflow.com/a/40203583/580850
//open means allow gaps in path.
UIBezierPath *path = [UIBezierPath bezierPath];
CGPoint p1 = [points[0] CGPointValue];
[path moveToPoint:p1];
CGPoint oldControlPoint = p1;
for (NSUInteger pointIndex = 1; pointIndex< points.count; pointIndex++) {
CGPoint p2 = [points[pointIndex] CGPointValue]; //note: mark missing data with CGFloatMax
if (p1.y >= CGFloatMax || p2.y >= CGFloatMax) {
if (open) {
[path moveToPoint:p2];
} else {
[path addLineToPoint:p2];
}
oldControlPoint = p2;
} else {
CGPoint p3 = CGPointZero;
if (pointIndex +1 < points.count) p3 = [points[pointIndex+1] CGPointValue] ;
if (p3.y >= CGFloatMax) p3 = CGPointZero;
CGPoint newControlPoint = controlPointForPoints2(p1, p2, p3);
if (!CGPointEqualToPoint( newControlPoint, CGPointZero)) {
[path addCurveToPoint: p2 controlPoint1:oldControlPoint controlPoint2: newControlPoint];
oldControlPoint = imaginForPoints( newControlPoint, p2);
//**added to algorithm
if (! CGPointEqualToPoint(p3,CGPointZero)) {
if (oldControlPoint.x > p3.x ) {
oldControlPoint.x = p3.x;
}
//***
} else {
[path addCurveToPoint: p2 controlPoint1:oldControlPoint controlPoint2: p2];
oldControlPoint = p2;
}
}
p1 = p2;
}
return path;
}
static CGPoint imaginForPoints(CGPoint point, CGPoint center) {
//returns "mirror image" of point: the point that is symmetrical through center.
if (CGPointEqualToPoint(point, CGPointZero) || CGPointEqualToPoint(center, CGPointZero)) {
return CGPointZero;
}
CGFloat newX = center.x + (center.x-point.x);
CGFloat newY = center.y + (center.y-point.y);
if (isinf(newY)) {
newY = BEMNullGraphValue;
}
return CGPointMake(newX,newY);
}
static CGFloat clamp(CGFloat num, CGFloat bounds1, CGFloat bounds2) {
//ensure num is between bounds.
if (bounds1 < bounds2) {
return MIN(MAX(bounds1,num),bounds2);
} else {
return MIN(MAX(bounds2,num),bounds1);
}
}
static CGPoint controlPointForPoints2(CGPoint p1, CGPoint p2, CGPoint p3) {
if (CGPointEqualToPoint(p3, CGPointZero)) return CGPointZero;
CGPoint leftMidPoint = midPointForPoints(p1, p2);
CGPoint rightMidPoint = midPointForPoints(p2, p3);
CGPoint imaginPoint = imaginForPoints(rightMidPoint, p2);
CGPoint controlPoint = midPointForPoints(leftMidPoint, imaginPoint);
controlPoint.y = clamp(controlPoint.y, p1.y, p2.y);
CGFloat flippedP3 = p2.y + (p2.y-p3.y);
controlPoint.y = clamp(controlPoint.y, p2.y, flippedP3);
//**added to algorithm
controlPoint.x = clamp(controlPoint.x, p1.x, p2.x);
//**
return controlPoint;
}
すべてのポイントの近くに「水平」カーブが必要な場合:
let path = createCurve(from: points, withSmoothness: 0.5) // 0.5 smoothness
let path = createCurve(from: points, withSmoothness: 0) // 0 smoothness
/// Create UIBezierPath
///
/// - Parameters:
/// - points: the points
/// - smoothness: the smoothness: 0 - no smooth at all, 1 - maximum smoothness
private func createCurve(from points: [CGPoint], withSmoothness smoothness: CGFloat, addZeros: Bool = false) -> UIBezierPath {
let path = UIBezierPath()
guard points.count > 0 else { return path }
var prevPoint: CGPoint = points.first!
let interval = getXLineInterval()
if addZeros {
path.move(to: CGPoint(x: interval.Origin.x, y: interval.Origin.y))
path.addLine(to: points[0])
}
else {
path.move(to: points[0])
}
for i in 1..<points.count {
let cp = controlPoints(p1: prevPoint, p2: points[i], smoothness: smoothness)
path.addCurve(to: points[i], controlPoint1: cp.0, controlPoint2: cp.1)
prevPoint = points[i]
}
if addZeros {
path.addLine(to: CGPoint(x: prevPoint.x, y: interval.Origin.y))
}
return path
}
/// Create control points with given smoothness
///
/// - Parameters:
/// - p1: the first point
/// - p2: the second point
/// - smoothness: the smoothness: 0 - no smooth at all, 1 - maximum smoothness
/// - Returns: two control points
private func controlPoints(p1: CGPoint, p2: CGPoint, smoothness: CGFloat) -> (CGPoint, CGPoint) {
let cp1: CGPoint!
let cp2: CGPoint!
let percent = min(1, max(0, smoothness))
do {
var cp = p2
// Apply smoothness
let x0 = max(p1.x, p2.x)
let x1 = min(p1.x, p2.x)
let x = x0 + (x1 - x0) * percent
cp.x = x
cp2 = cp
}
do {
var cp = p1
// Apply smoothness
let x0 = min(p1.x, p2.x)
let x1 = max(p1.x, p2.x)
let x = x0 + (x1 - x0) * percent
cp.x = x
cp1 = cp
}
return (cp1, cp2)
}
/// Defines interval width, height (not used in this example) and coordinate of the first interval.
/// - Returns: (x0, y0, step, height)
internal func getXLineInterval() -> CGRect {
return CGRect.zero
}