天天看點

Android繪制貝塞爾曲線

一、     背景

貝塞爾曲線(Bézier curve),又稱貝茲曲線或貝濟埃曲線,是應用于二維圖形應用程式的數學曲線。一般的矢量圖形軟體通過它來精确畫出曲線。 

利用貝塞爾曲線,我們可以更平滑的畫出手勢操作的軌迹,然後實作像橡皮檫等功能

n階貝茲曲線可如下推斷。給定點P0、P1、…、Pn,其貝茲曲線即:

Android繪制貝塞爾曲線

 t 值的計算方式為:j/(N),N代表要生成的貝塞爾點個數,控制點的數量是n+1,手勢裡面通常我們可以取三個控制點,即n=2, 此時第j個貝塞爾點坐标為 B(t) = (1-t)^2*P0 + 2*(1-t)*t*P1 + t^2*P2 (j範圍是[0, N])

二、     實作

1、注冊手勢監聽

@Override
public boolean onTouchEvent(final MotionEvent event) {

    if (event != null) {
        int action = event.getAction() & MotionEvent.ACTION_MASK;
        final float x, y;
        switch (action) {
            case MotionEvent.ACTION_DOWN:

                handleActionDown(event);

                break;
            case MotionEvent.ACTION_POINTER_DOWN:
              
                break;
            case MotionEvent.ACTION_MOVE:
                handleActionMove(event);
                break;
     caseMotionEvent.ACTION_POINTER_UP:
                break;
     case MotionEvent.ACTION_UP:
              
                break;
        }
        return true;
    }
    return false;
}
           

2、然後處理ACTION_DOWN和ACTION_MOVE傳過來的手勢點:

void handleActionDown(MotionEvent event) {
    scrawlDownX = event.getX();
    scrawlDownY = event.getY();
    
    points.clear();
    PointF point = translateToGL(event.getX(), event.getY());
    addBezierPoint(point);
    
}
 

void handleActionMove(MotionEvent event) {
       if (Math.abs(scrawlDownX - event.getX()) > VALID_SPACING
             || Math.abs(scrawlDownY - event.getY()) > VALID_SPACING) {
          PointF point = translateToGL(event.getX(), event.getY());
          if (bindFBO(mMaterialMaskTexture)) {
              addBezierPoint(point);
              unBindFBO();
          }
       } 
}
           
這裡我們會根據每次傳過來的點生成一個新的貝塞爾曲線點,然後将軌迹寫入到蒙層裡面,這邊用opengl來儲存軌迹,
           
接下來看看怎樣生成貝塞爾曲線點:
           
private void addBezierPoint(PointF point) {
    if (points.size() == 3) {
        points.remove(0);
    }
    points.add(point);

    if (points.size() == 2) {
        PointF from = points.get(0);
        PointF to = midPoint(points.get(0), points.get(1));
        PointF control = midPoint(from, to);

        calculateBezierCurve(from, to, control);
    } else if (points.size() > 2) {
        PointF from = midPoint(points.get(0), points.get(1));
        PointF to = midPoint(points.get(1), points.get(2));
        PointF control = points.get(1);

        calculateBezierCurve(from, to, control);
    }
}

private PointF midPoint(PointF p1, PointF p2) {
    return new PointF((p1.x + p2.x) / 2, (p1.y + p2.y) / 2);
}

/**
 * 塗抹貝塞爾曲線兩點之間插入的點數量,影響塗抹程度值
 */
private static final int BEZIER_INSET_POINT = 5;
/**
 * 預設筆觸大小
 */
private static final float DEFAULT_PEN_SIZE_RATIO = 20 / 750f;

/**
 * 羽化的塗抹筆觸,會比實際看過去的筆觸小,是以此處需要有一個比例,使看過去塗抹的範圍與筆觸圓圈接近
*/
private static final float MASK_SCALE = 5 / 4f;

/**
 * 筆觸大小,比例值
 */
private float mPenSize = DEFAULT_PEN_SIZE_RATIO * 2 * MASK_SCALE;
/**
 * 塗抹正方形區域比例
 */
private float mEraserRatio = mPenSize;

private void calculateBezierCurve(PointF from, PointF to, PointF control) {
    int xNum = (int) ((to.x - from.x) / mEraserRatio * BEZIER_INSET_POINT);
    int yNum = (int) ((to.y - from.y) / mEraserRatio * mHeight / mWidth * BEZIER_INSET_POINT);

    // 坐标點的總數
    int N = Math.max(Math.abs(xNum), Math.abs(yNum));
    N = Math.max(N, 1);

    final float[] array = new float[8 * N * 2];

    float x, y, t;
    for (int i = 0; i < N; i++) {
        t = (float) i / N;

        // 根據貝塞爾曲線函數,獲得此時的x,y坐标
        x = (1 - t) * (1 - t) * from.x + 2 * (1 - t) * t * control.x + t * t * to.x;
        y = (1 - t) * (1 - t) * from.y + 2 * (1 - t) * t * control.y + t * t * to.y;

        addVertex2Array(array, x, y, 16 * i);
    }

    // 一個點使用6個索引
    final short[] indexArray = new short[N * 6];
    for (int i = 0; i < N; i++) {
        //leftBottom,rightBottom,leftTop
        indexArray[i * 6] = (short) (4 * i);
        indexArray[i * 6 + 1] = (short) (4 * i + 1);
        indexArray[i * 6 + 2] = (short) (4 * i + 2);

        //rightBottom,rightTop,leftTop
        indexArray[i * 6 + 3] = (short) (4 * i + 1);
        indexArray[i * 6 + 4] = (short) (4 * i + 3);
        indexArray[i * 6 + 5] = (short) (4 * i + 2);
    }

    mEraserFilter.initVertexData(array);
    mEraserFilter.setIndexData(indexArray);
    glViewport(0, 0, mWidth, mHeight);
    mEraserFilter.drawElements(mMaskTexture);
    unBindFBO();
}

private void addVertex2Array(float[] vertexArray, float x, float y, int index) {
    //leftBottom頂點坐标
    vertexArray[index] = x - mEraserRatio;
    vertexArray[index + 1] = -y + mEraserRatio * mWidth / mHeight;
    //leftBottom紋理坐标
    vertexArray[index + 2] = 0f;
    vertexArray[index + 3] = 1f;
    //rightBottom頂點坐标
    vertexArray[index + 4] = x + mEraserRatio;
    vertexArray[index + 5] = -y + mEraserRatio * mWidth / mHeight;
    //rightBottom紋理坐标
    vertexArray[index + 6] = 1f;
    vertexArray[index + 7] = 1f;
    //leftTop頂點坐标
    vertexArray[index + 8] = x - mEraserRatio;
    vertexArray[index + 9] = -y - mEraserRatio * mWidth / mHeight;
    //leftTop紋理坐标
    vertexArray[index + 10] = 0f;
    vertexArray[index + 11] = 0f;
    //rightTop頂點坐标
    vertexArray[index + 12] = x + mEraserRatio;
    vertexArray[index + 13] = -y - mEraserRatio * mWidth / mHeight;
    //rightTop紋理坐标
    vertexArray[index + 14] = 1f;
    vertexArray[index + 15] = 0f;
}
           

過程大概是:

1) 選取作為貝塞爾曲線公式裡面參數的三個點,即起點、中點、目标點,為了使軌迹更平滑,起點的選擇有兩種情況,如果隻有兩個點,那麼就選前一個點,如果超過了兩個點,會選擇前兩個點的中點;然後目标點就是目前點和前一個點的中點

2) 将選擇的三個點作為參數傳給貝塞爾曲線公式,計算得出貝塞爾點

3) 為了使點更密集,可以根據起點和終點間的距離插入一定量的貝塞爾點

4) 利用opengl在生成的貝塞爾點位置處畫矩形,因為點生成的速度和量都非常多,是以這邊用到了drawElements這種快速繪制的函數,具體使用可以參考網上的樣例

5) 經過上面操作就能将目前手勢操作的軌迹畫到蒙層裡面,相當于儲存了軌迹,然後想恢複的話可以用同樣的操作擦除蒙層的軌迹,最終将蒙層映射到原圖裡面就實作了擦除功能

6) 注意寫入FBO時是上下颠倒的,是以計算坐标時将y軸的值颠倒過來,實作繞x軸翻轉的功能

3、羽化

經過上面的處理可以看到手勢的軌迹了,但是這還沒結束,你會發現軌迹有嚴重的鋸齒現象,原因是什麼呢?就是因為我們用GL畫的是長方形,是以改善的方法就是要畫圓形 

那怎麼畫圓形呢,肯定不是那gl渲染出來的,那樣不僅效率低,而且不是我們要的效果,我們需要一張mask圖,這張圖中心是個白色圓,并且從中心到四周,顔色從白色到黑色逐漸過渡,這樣就能達到羽化的效果,然後我們把這張圖的RGB裡面的r值作為透明度,下面具體看一下實作:

1) 修改橡皮檫對應的腳本:

precision highp float;
varying vec2 texcoordOut;
uniform sampler2D masktexture;

uniform float opacity;


 void main(){
      vec4 maskColor = vec4(1.0, 0.0, 0.0, 1.0);
      vec4 textureColor =texture2D(masktexture,texcoordOut);

      gl_FragColor = maskColor *textureColor.r * opacity;

 }
           

上面的masktexture就是我們需要的那張圓形圖,opacity變量的作用是調整橡皮檫漸變擦除功能,即你要多少步内把擦除部分全部擦除,然後可以看到我們畫上去的顔色選的是masktexture的r值 

2) 設定疊加方式:

if (mMode == MODE_ERASER) {
    glBlendEquation(GL_FUNC_ADD);
    glBlendFunc(GL_ONE, GL_ONE);
} else if (mMode== MODE_RECOVER) {
    glBlendEquation(GL_FUNC_REVERSE_SUBTRACT);
    glBlendFunc(GL_ONE, GL_ONE);
}
           

這邊就用到了GL提供的圖像混合方式了,因為我們每次畫上去的時候無法拿到目前FBO裡面的資料(ios有擴充可以),是以我們需要用這種方式來将目前FBO裡面的資料和我們即将畫上去的資料做疊加(如果不考慮FBO裡面資料,會導緻畫上去的顔色影響之前的軌迹),這邊考慮了兩種模式:

a)    如果是橡皮檫模式,我們的公式會變成

resultColor = srcColor * 1 + dstColor * 1;

其中srcColor就是masktexture裡面的r值,因為這個r值是由中心到四周過渡的,是以每次畫上去的就相當于一個羽化過的圓

b)    如果是恢複模式,我們的公式變成:

resultColor = dstColor *1 – srcColor * 1;

即把我們橡皮檫模式下畫上去的顔色消除,這樣軌迹就相當于被擦除了 

c)    利用上面兩步操作後我們會生成一張橡皮檫軌迹的紋理,其中紋理上面的r值代表軌迹的透明度,然後我們将軌迹的紋理和素材原圖的紋理做混合,混合結果中素材的透明度就取決于軌迹的r值。

d)    如果要設定多少步内擦除,可以設定變量:

opacity = 1.0f/ BEZIER_INSET_POINT / mScrawlCount;
           

其中mScrawlCount就代表完全擦除的步數,而之是以要除以BEZIER_INSERT_POINT是因為在每次滑動過程中我們會插入一些貝塞爾點,而這些點每次繪制就會執行一次masktexture的繪制,這樣相當于軌迹被疊加了多次,是以需要消除這個影響

3)注意這種方式繪制的貝塞爾曲線并不是嚴格的曲線,因為我們相當于拼接多條曲線,像手勢操作這種我們不需要展示曲線給使用者看,而且對效率要求比較高的情況下可以采用曲線拼接的方式。如果你要展示曲線給别人看就必須一次性畫完一條曲線而不是一條條拼接而成。

繼續閱讀