Android绘制贝塞尔曲线

一、     背景

贝塞尔曲线(Bézier curve),又称贝兹曲线或贝济埃曲线,是应用于二维图形应用程序的数学曲线。一般的矢量图形软件通过它来精确画出曲线 

利用贝塞尔曲线,我们可以更平滑的画出手势操作的轨迹,然后实现像橡皮檫等功能

n阶贝兹曲线可如下推断。给定点P0P1Pn,其贝兹曲线即:


 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_DOWNACTION_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++) {
        indexArray[i * 6] = (short) (4 * i);
        indexArray[i * 6 + 1] = (short) (4 * i + 1);
        indexArray[i * 6 + 2] = (short) (4 * i + 2);

        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) {
    vertexArray[index] = x - mEraserRatio;
    vertexArray[index + 1] = -y + mEraserRatio * mWidth / mHeight;
    vertexArray[index + 2] = 0f;
    vertexArray[index + 3] = 1f;
    vertexArray[index + 4] = x + mEraserRatio;
    vertexArray[index + 5] = -y + mEraserRatio * mWidth / mHeight;
    vertexArray[index + 6] = 1f;
    vertexArray[index + 7] = 1f;
    vertexArray[index + 8] = x - mEraserRatio;
    vertexArray[index + 9] = -y - mEraserRatio * mWidth / mHeight;
    vertexArray[index + 10] = 0f;
    vertexArray[index + 11] = 0f;
    vertexArray[index + 12] = x + mEraserRatio;
    vertexArray[index + 13] = -y - mEraserRatio * mWidth / mHeight;
    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)注意这种方式绘制的贝塞尔曲线并不是严格的曲线,因为我们相当于拼接多条曲线,像手势操作这种我们不需要展示曲线给用户看,而且对效率要求比较高的情况下可以采用曲线拼接的方式。如果你要展示曲线给别人看就必须一次性画完一条曲线而不是一条条拼接而成







猜你喜欢

转载自blog.csdn.net/u012292247/article/details/80294052