1. 程式人生 > >iOS 沿曲線線性漸變的貝塞爾曲線

iOS 沿曲線線性漸變的貝塞爾曲線

大致思路是,先獲取到貝塞爾曲線上所有的點,然後在計算每個點的t值,然後根據t值來計算每個點的顏色。這種方式會在頂點的位置計算會有一些問題,整體來說只是一種思路,具體效果有待考驗。

1、獲取貝塞爾曲線上所有的點

如何獲取貝塞爾曲線上所有的點?這個其實是比較簡單的,可以利用UIBezierPath畫一條曲線,渲染到CAShapeLayer (fillColor:clearColor,strokeColor:redColor)上,然後遍歷CAShapeLayer上的畫素,只要畫素的有色值那就是需要的點。同時由於這樣渲染出的線條已經處理好了鋸齒問題(即畫素透明度), 所以為後面的處理省下了很多的事情。

2、計算每個點的 t 值

現在已經得到了需要的點,剩下的就是計算每個點的t值了。計算t值也就是一個解方程的過程,這裡說的是二次貝塞爾曲線,涉及到的就是一元二次方程。但是在畫素點的座標值都是整數型的,不是所有的點都是在曲線上的,所以解出來的 t 值多少會有些誤差,不過效果還是可以的,對整體的漸變影響不大。

// 根據 x 計算 t
- (float)baseOnXWithPoint:(CGPoint)point {
    float a = _startPoint.x - 2 * _controlPoint.x + _endPoint.x;
    float b = 2 * _controlPoint.x - 2 * _startPoint.x;
    float c = _startPoint.x - point.x;
    float condition = pow(b, 2) - 4 * a * c;
    if (a != 0 ) {
        if (condition >= 0) {
            NSArray *r = [self quadraticEquationWithA:a b:b c:c];
            if (r && r.count > 0) {
                float t = [self betterRWithRs:r targetPoint:point];
                return t;
            }
        }
    } else {
        // 一元一次方程求解
        float t = (-c)/b;
        return t;
    }
    return -1;
}

// 根據 y 計算 t
- (float)baseOnYWithPoint:(CGPoint)point {
    float a = _startPoint.y - 2 * _controlPoint.y + _endPoint.y;
    float b = 2 * _controlPoint.y - 2 * _startPoint.y;
    float c = _startPoint.y - point.y;
    float condition = pow(b, 2) - 4 * a * c;
    if ( a != 0) {
        if (condition >= 0) {
            NSArray *r = [self quadraticEquationWithA:a b:b c:c];
            if (r && r.count > 0) {
                float t = [self betterRWithRs:r targetPoint:point];
                return t;
            }
        }
    } else {
        // 一元一次方程求解
        float t = (-c)/b;
        return t;
    }

    return -1;
}
複製程式碼

這裡會有兩個方程,一個是以x為引數,一個以y為引數。這兩個方程都會用到。為什麼要用兩個方程?因為有的點通過x或者y 並不能解得結果,比如說頂點附近的點,通過點做 x 軸的 垂線,可能與曲線並不會交點,也就意味著不會有解。所以如果以x為引數無解,那就再用y為引數的方程解一次,如果還沒有解,那這個點就認為是不在線上的了。

在計算的過程中還有一個問題:如果以x 為引數計算,那麼 X 方向上頂點附近的點(如果有頂點)計算出來的t值誤差會比較大。所以在計算的時候做了一些判斷,如果是頂點附近的點,以y為引數計算

- (float)quadraticEquationWithPoint:(CGPoint)point  {
    float t = [self baseOnXWithPoint:point];
    // 如果沒有結果 即 t = -1,則依據Y從新計算
    // 如果計算的結果為 X 方向上的頂點,由於頂點位置計算不準確,所以根據Y從新計算
    if (t == -1 || fabs([self tForXAtVertexPoint] - t) < 0.1) {
        float otherT = [self baseOnYWithPoint:point];
        if (otherT == -1) {
            return t;
        }
        t = otherT;
    }
    return t;
}
複製程式碼

對於一元二次方程,是會有兩個根的情況的,所以對於解出來的結果需要進行比對,找到與目標點最接近的t值

// 篩選結果
- (float)betterRWithRs:(NSArray *)rs targetPoint:(CGPoint)point{
    CGFloat distance = NSNotFound;
    NSInteger betterIndex = 0;
    for (NSInteger i = 0; i < rs.count; i ++) {
        float t = [[rs objectAtIndex:i] floatValue];
        CGFloat x = [self xAtT:t];
        CGFloat y = [self yAtT:t];
        if (distance == NSNotFound) {
            distance = [self distanceWithPoint:CGPointMake(x, y) point1:point];
            betterIndex = i;

        } else {
            if (distance > [self distanceWithPoint:CGPointMake(x, y) point1:point]) {
                distance = [self distanceWithPoint:CGPointMake(x, y) point1:point];
                betterIndex = i;
            }
        }

    }
    float t = [rs[betterIndex] floatValue];
    if (t >= 1) {
        if ([self isNearbyTargetPoint:_endPoint x:point.x y:point.y]) {
            return 1;
        } else {
            return -1;
        }
    }

    if (t <= 0) {
        if ([self isNearbyTargetPoint:_startPoint x:point.x y:point.y]) {
            return 0;
        } else {
            return -1;
        }
    }
    return [rs[betterIndex] floatValue];
}
複製程式碼

可以先看下效果。整體來說效果還是理想的,並且也支援了線寬的問題。

漸變曲線


Demo 地址