Java FX版的毛笔效果

由于项目中有实现毛笔的效果的要求,所以在网上找了一个Android的demo,然后将其改成了Java FX版本的,算法来自网上某个Android的demo(不好意思忘记了原来的url,如果原版作者有意见可以联系我)。

package brush;

import javafx.scene.paint.Color;

/**
 * @des  笔的设置,但是有些笔的设置最好不要放在这里,不要笔的颜色和宽度
 */
public interface IPenConfig {

    /**
     * 清除画布
     */
    int STROKE_TYPE_ERASER = 0;

    /**
     * 钢笔
     */
     int STROKE_TYPE_PEN = 1;// 钢笔
    /**
     * 毛笔
     */
    int STROKE_TYPE_BRUSH = 2;// 毛笔

    //设置笔的宽度
    int PEN_WIDTH=60;
    //笔的颜色
    Color PEN_CORLOUR= Color.valueOf("#FF4081");

    //这个控制笔锋的控制值
     float DIS_VEL_CAL_FACTOR = 0.02f;
    //手指在移动的控制笔的变化率  这个值越大,线条的粗细越加明显
    //float WIDTH_THRES_MAX = 0.6f;
     float WIDTH_THRES_MAX = 10f;
    //绘制计算的次数,数值越小计算的次数越多,需要折中
     int STEPFACTOR = 10;
}
package brush;


/**
 * @des 每个点的控制,关心三个因素:笔的宽度,坐标,透明数值
 */
public class ControllerPoint {
    public float x;
    public float y;

    public float width;
    public int alpha = 255;
    public ControllerPoint() {
    }

    public ControllerPoint(float x, float y) {
        this.x = x;
        this.y = y;
    }


    public void set(float x, float y, float w) {
        this.x = x;
        this.y = y;
        this.width = w;
    }


    public void set(ControllerPoint point) {
        this.x = point.x;
        this.y = point.y;
        this.width = point.width;
    }


    public String toString() {
        String str = "X = " + x + "; Y = " + y + "; W = " + width;
        return str;
    }


}
package brush;


/**
 * @des  对点的位置和宽度控制的bezier曲线,主要是两个点,都包含了宽度和点的坐标
 */
public class Bezier {
    //控制点的,
    private ControllerPoint mControl = new ControllerPoint();
    //距离
    private ControllerPoint mDestination = new ControllerPoint();
    //下一个需要控制点
    private ControllerPoint mNextControl = new ControllerPoint();
    //资源的点
    private ControllerPoint mSource = new ControllerPoint();

    public Bezier() {
    }

    /**
     * 初始化两个点,
     * @param last 最后的点的信息
     * @param cur 当前点的信息,当前点的信息,当前点的是根据事件获得,同时这个当前点的宽度是经过计算的得出的
     */
    public void init(ControllerPoint last, ControllerPoint cur)
    {
        init(last.x, last.y, last.width, cur.x, cur.y, cur.width);
    }

    public void init(float lastx, float lasty, float lastWidth, float x, float y, float width)
    {
        //资源点设置,最后的点的为资源点
        mSource.set(lastx, lasty, lastWidth);
        float xmid = getMid(lastx, x);
        float ymid = getMid(lasty, y);
        float wmid = getMid(lastWidth, width);
        //距离点为平均点
        mDestination.set(xmid, ymid, wmid);
        //控制点为当前的距离点
        mControl.set(getMid(lastx,xmid),getMid(lasty,ymid),getMid(lastWidth,wmid));
        //下个控制点为当前点
        mNextControl.set(x, y, width);
    }

    public void addNode(ControllerPoint cur){
        addNode(cur.x, cur.y, cur.width);
    }

    /**
     * 替换就的点,原来的距离点变换为资源点,控制点变为原来的下一个控制点,距离点取原来控制点的和新的的一半
     * 下个控制点为新的点
     * @param x 新的点的坐标
     * @param y 新的点的坐标
     * @param width
     */
    public void addNode(float x, float y, float width){
        mSource.set(mDestination);
        mControl.set(mNextControl);
        mDestination.set(getMid(mNextControl.x, x), getMid(mNextControl.y, y), getMid(mNextControl.width, width));
        mNextControl.set(x, y, width);
    }

    /**
     * 结合手指抬起来的动作,告诉现在的曲线控制点也必须变化,其实在这里也不需要结合着up事件使用
     * 因为在down的事件中,所有点都会被重置,然后设置这个没有多少意义,但是可以改变下个事件的朝向改变
     * 先留着,因为后面如果需要控制整个颜色的改变的话,我的依靠这个方法,还有按压的时间的变化
     */
    public void end() {
        mSource.set(mDestination);
        float x = getMid(mNextControl.x, mSource.x);
        float y = getMid(mNextControl.y, mSource.y);
        float w = getMid(mNextControl.width, mSource.width);
        mControl.set(x, y, w);
        mDestination.set(mNextControl);
    }

    /**
     *
     * @param t 控制点
     * @return
     */
    public ControllerPoint getPoint(double t){
        float x = (float)getX(t);
        float y = (float)getY(t);
        float w = (float)getW(t);
        ControllerPoint point = new ControllerPoint();
        point.set(x,y,w);
        return point;
    }

    /**
     * 三阶曲线的控制点
     * @param p0
     * @param p1
     * @param p2
     * @param t
     * @return
     */
    private double getValue(double p0, double p1, double p2, double t){
        double A = p2 - 2 * p1 + p0;
        double B = 2 * (p1 - p0);
        double C = p0;
        return A * t * t + B * t + C;
    }

    private double getX(double t) {
        return getValue(mSource.x, mControl.x, mDestination.x, t);
    }

    private double getY(double t) {
        return getValue(mSource.y, mControl.y, mDestination.y, t);
    }

    private double getW(double t){
        return getWidth(mSource.width, mDestination.width, t);
    }

    /**
     *
     * @param x1 一个点的x
     * @param x2 一个点的x
     * @return
     */
    private float getMid(float x1, float x2) {
        return (float)((x1 + x2) / 2.0);
    }

    private double getWidth(double w0, double w1, double t){
        return w0 + (w1 - w0) * t;
    }

}
package brush;



import java.util.ArrayList;

import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.shape.Ellipse;

public  class BasePenExtend  {

    public ArrayList mHWPointList = new ArrayList<>();

    public ArrayList mPointList = new ArrayList();

    public ControllerPoint mLastPoint = new ControllerPoint(0, 0);

    //笔的宽度信息
    public double mBaseWidth;

    public double mLastVel;

    public double mLastWidth;

    public Bezier mBezier = new Bezier();

    protected ControllerPoint mCurPoint;

    public BasePenExtend() {

    }

    public void setPaint(double width) {

        mBaseWidth = width;
    }

    public void draw(Pane pane) {
        //点的集合少 不去绘制
        if (mHWPointList == null || mHWPointList.size() < 1)
            return;
        //当控制点的集合很少的时候,需要画个小圆,但是需要算法
        if (mHWPointList.size() < 2) {
            ControllerPoint point = mHWPointList.get(0);
            //由于此问题在算法上还没有实现,所以暂时不给他画圆圈
            //canvas.drawCircle(point.x, point.y, point.width, mPaint);
        } else {
            mCurPoint = mHWPointList.get(0);
            drawNeetToDo(pane);
        }
    }


    public boolean onTouchEvent(int mouseType,MouseEvent event, Pane canvas) {
        // event会被下一次事件重用,这里必须生成新的,否则会有问题
       
        switch (mouseType) {
            case 0:
                onDown(event);
                return true;
            case 1:
                onMove(event);
                return true;
            case 2:
                onUp(event, canvas);
                return true;
            default:
                break;
        }
        return false;
    }

    /**
     * 按下的事件
     *
     * @param mElement
     */
    public void onDown(MouseEvent mElement) {
       
        mPointList.clear();
        //如果在brush字体这里接受到down的事件,把下面的这个集合清空的话,那么绘制的内容会发生改变
        //不清空的话,也不可能
        mHWPointList.clear();
        //记录down的控制点的信息
        ControllerPoint curPoint = new ControllerPoint((float)mElement.getSceneX(), (float)mElement.getSceneY());
        //如果用笔画的画我的屏幕,记录他宽度的和压力值的乘,但是哇,
        /*if (mElement.tooltype == MotionEvent.TOOL_TYPE_STYLUS) {
            mLastWidth = mElement.pressure * mBaseWidth;
        } else {*/
        //如果是手指画的,我们取他的0.8
        mLastWidth = 0.8 * mBaseWidth;
       /* }*/
        //down下的点的宽度
        curPoint.width = (float) mLastWidth;
        mLastVel = 0;
        mPointList.add(curPoint);
        //记录当前的点
        mLastPoint = curPoint;
    }

   

    /**
     * 手指移动的事件
     *
     * @param mElement
     */
    public void onMove(MouseEvent mElement) {

        ControllerPoint curPoint = new ControllerPoint((float)mElement.getSceneX(), (float)mElement.getSceneY());
        double deltaX = curPoint.x - mLastPoint.x;
        double deltaY = curPoint.y - mLastPoint.y;
        //deltaX和deltay平方和的二次方根 想象一个例子 1+1的平方根为1.4 (x²+y²)开根号
        //同理,当滑动的越快的话,deltaX+deltaY的值越大,这个越大的话,curDis也越大
        double curDis = Math.hypot(deltaX, deltaY);
        //我们求出的这个值越小,画的点或者是绘制椭圆形越多,这个值越大的话,绘制的越少,笔就越细,宽度越小
        double curVel = curDis * IPenConfig.DIS_VEL_CAL_FACTOR;
        double curWidth;
        //点的集合少,我们得必须改变宽度,每次点击的down的时候,这个事件
        if (mPointList.size() < 2) {
            /*if (mElement.tooltype == MotionEvent.TOOL_TYPE_STYLUS) {
                curWidth = mElement.pressure * mBaseWidth;
            } else {*/
            curWidth = calcNewWidth(curVel, mLastVel, curDis, 1.5,
                    mLastWidth);
          /*  }*/
            curPoint.width = (float) curWidth;
            mBezier.init(mLastPoint, curPoint);
        } else {
            mLastVel = curVel;
          /*  if (mElement.tooltype == MotionEvent.TOOL_TYPE_STYLUS) {
                curWidth = mElement.pressure * mBaseWidth;
            } else {*/
            //由于我们手机是触屏的手机,滑动的速度也不慢,所以,一般会走到这里来
            //阐明一点,当滑动的速度很快的时候,这个值就越小,越慢就越大,依靠着mlastWidth不断的变换
            curWidth = calcNewWidth(curVel, mLastVel, curDis, 1.5,
                    mLastWidth);
            /*}*/
            curPoint.width = (float) curWidth;
            mBezier.addNode(curPoint);
        }
        //每次移动的话,这里赋值新的值
        mLastWidth = curWidth;
        mPointList.add(curPoint);
        moveNeetToDo(curDis);
        mLastPoint = curPoint;
    }


    /**
     * 手指抬起来的事件
     *
     * @param mElement
     * @param canvas
     */
    public void onUp(MouseEvent mElement, Pane canvas) {

        mCurPoint = new ControllerPoint((float)mElement.getSceneX(), (float)mElement.getSceneY());
        double deltaX = mCurPoint.x - mLastPoint.x;
        double deltaY = mCurPoint.y - mLastPoint.y;
        double curDis = Math.hypot(deltaX, deltaY);
        //如果用笔画的画我的屏幕,记录他宽度的和压力值的乘,但是哇,这个是不会变的
       /* if (mElement.tooltype == MotionEvent.TOOL_TYPE_STYLUS) {
            mCurPoint.width = (float) (mElement.pressure * mBaseWidth);
        } else {*/
        mCurPoint.width = 0;
       /* }*/

        mPointList.add(mCurPoint);

        mBezier.addNode(mCurPoint);

        int steps = 1 + (int) curDis / IPenConfig.STEPFACTOR;
        double step = 1.0 / steps;
        for (double t = 0; t < 1.0; t += step) {
            ControllerPoint point = mBezier.getPoint(t);
            mHWPointList.add(point);
        }
        //
        mBezier.end();
        for (double t = 0; t < 1.0; t += step) {
            ControllerPoint point = mBezier.getPoint(t);
            mHWPointList.add(point);
        }

        // 手指up 我画到纸上上
        draw(canvas);
        //每次抬起手来,就把集合清空,在水彩笔的那个地方,如果啊,我说如果不清空的话,每次抬起手来,
        // 在onDown下去的话,最近画的线的透明度有改变,所以这里clear下线的集合
        clear();
    }

    /**
     * @param curVel
     * @param lastVel
     * @param curDis
     * @param factor
     * @param lastWidth
     * @return
     */
    public double calcNewWidth(double curVel, double lastVel, double curDis,
                               double factor, double lastWidth) {
        double calVel = curVel * 0.6 + lastVel * (1 - 0.6);
        //返回指定数字的自然对数
        //手指滑动的越快,这个值越小,为负数
        double vfac = Math.log(factor * 2.0f) * (-calVel);
        //此方法返回值e,其中e是自然对数的基数。
        //Math.exp(vfac) 变化范围为0 到1 当手指没有滑动的时候 这个值为1 当滑动很快的时候无线趋近于0
        //在次说明下,当手指抬起来,这个值会变大,这也就说明,抬起手太慢的话,笔锋效果不太明显
        //这就说明为什么笔锋的效果不太明显
        double calWidth = mBaseWidth * Math.exp(vfac);

        //滑动的速度越快的话,mMoveThres也越大
        double mMoveThres = curDis * 0.01f;
        //对之值最大的地方进行控制
        if (mMoveThres > IPenConfig.WIDTH_THRES_MAX) {
            mMoveThres = IPenConfig.WIDTH_THRES_MAX;
        }
        // TODO: 2018/2/24   以下的方法 可以删除掉  原因是抽取了一下 ,本来不应该在这里的出现的  不好意思 
//        //滑动越慢的情况下,得到的calWidth 和上面的calwidth 相差的值不一样
//
//        //滑动的越快的话,第一个判断会走
//        if (Math.abs(calWidth - mBaseWidth) / mBaseWidth > mMoveThres) {
//            if (calWidth > mBaseWidth) {
//                calWidth = mBaseWidth * (1 + mMoveThres);
//            } else {
//                calWidth = mBaseWidth * (1 - mMoveThres);
//            }
//            //滑动的越慢的话,第二个判断会走  基本上在屏幕上手指基本上没有走动的时候 ,就会走这个方法
//        } else if (Math.abs(calWidth - lastWidth) / lastWidth > mMoveThres) {
//            if (calWidth > lastWidth) {
//                calWidth = lastWidth * (1 + mMoveThres);
//            } else {
//                calWidth = lastWidth * (1 - mMoveThres);
//            }
//        }
        return calWidth;
    }

    /**
     * event.getPressure(); //LCD可以感应出用户的手指压力,当然具体的级别由驱动和物理硬件决定的,我的手机上为1
     *
     * @param motionEvent
     * @return
     */
   /* public MotionElement createMotionElement(MotionEvent motionEvent) {
        MotionElement motionElement = new MotionElement(motionEvent.getX(), motionEvent.getY(),
                motionEvent.getPressure(), motionEvent.getToolType(0));
        return motionElement;
    }*/

    public void clear() {
        mPointList.clear();
        mHWPointList.clear();
    }

    /**
     * 当现在的点和触摸点的位置在一起的时候不用去绘制
     * 但是这里也可以优化,当一直处于onDown事件的时候,其实这个方法一只在走
     *
     * @param canvas
     * @param point
     * @param paint
     */
    // TODO: 2017/10/18  这里可以优化 当一直处于onDown事件的时候,其实这个方法一直在走,优化的点是,处于down事件,这里不需要走
    protected void drawToPoint(Pane canvas, ControllerPoint point) {
        if ((mCurPoint.x == point.x) && (mCurPoint.y == point.y)) {
            return;
        }
        //水彩笔的效果和钢笔的不太一样,交给自己去实现
        doNeetToDo(canvas, point);
    }

    /**
     * 判断笔是否为空 节约性能,每次切换笔的时候就不用重复设置了
     *
     * @return
     */
   /* public boolean isNull() {
        return mPaint == null;
    }*/


    protected void drawNeetToDo(Pane pane) {
        for (int i = 1; i < mHWPointList.size(); i++) {
            ControllerPoint point = mHWPointList.get(i);
            drawToPoint(pane, point);
            mCurPoint = point;
        }
    }


    protected void moveNeetToDo(double curDis) {
        int steps = 1 + (int) curDis / IPenConfig.STEPFACTOR;
        double step = 1.0 / steps;
        for (double t = 0; t < 1.0; t += step) {
            ControllerPoint point = mBezier.getPoint(t);
            mHWPointList.add(point);
        }
    }

    protected void doNeetToDo(Pane canvas, ControllerPoint point) {
        drawLine(canvas, mCurPoint.x, mCurPoint.y, mCurPoint.width, point.x,
                point.y, point.width);
    }

    /**
     * 其实这里才是关键的地方,通过画布画椭圆,每一个点都是一个椭圆,这个椭圆的所有细节,逐渐构建出一个完美的笔尖
     * 和笔锋的效果,我觉得在这里需要大量的测试,其实就对低端手机进行排查,看我们绘制的笔的宽度是多少,绘制多少个椭圆
     * 然后在低端手机上不会那么卡,当然你哪一个N年前的手机给我,那也的卡,只不过需要适中的范围里面
     *
     * @param canvas
     * @param x0
     * @param y0
     * @param w0
     * @param x1
     * @param y1
     * @param w1
     * @param paint
     */
    private void drawLine(Pane canvas, double x0, double y0, double w0, double x1, double y1, double w1) {
        //求两个数字的平方根 x的平方+y的平方在开方记得X的平方+y的平方=1,这就是一个园
        double curDis = Math.hypot(x0 - x1, y0 - y1);
        int steps = 1;
        if (mBaseWidth < 6) {
            steps = 1 + (int) (curDis / 2);
        } else if (mBaseWidth > 60) {
            steps = 1 + (int) (curDis / 4);
        } else {
            steps = 1 + (int) (curDis / 3);
        }
        double deltaX = (x1 - x0) / steps;
        double deltaY = (y1 - y0) / steps;
        double deltaW = (w1 - w0) / steps;
        double x = x0;
        double y = y0;
        double w = w0;

        for (int i = 0; i < steps; i++) {
            //都是用于表示坐标系中的一块矩形区域,并可以对其做一些简单操作
            //精度不一样。Rect是使用int类型作为数值,RectF是使用float类型作为数值。
            //            Rect rect = new Rect();
//            RectF oval = new RectF();
        	Ellipse ellipse = new Ellipse();
        	float centerX=((float) (x - w / 4.0f)+(float) (x + w / 4.0f))/2;
        	float centerY=((float) (y - w / 2.0f)+(float) (y + w / 2.0f))/2;
        	ellipse.setCenterX(centerX);
        	ellipse.setCenterY(centerY);
            ellipse.setRadiusX((Math.abs((float) (x - w / 4.0f)-(float) (x + w / 4.0f)))/2);      	
        	ellipse.setRadiusY(Math.abs(((float) (y - w / 2.0f)-(float) (y + w / 2.0f))/2));
          /*  oval.set((float) (x - w / 4.0f), (float) (y - w / 2.0f), (float) (x + w / 4.0f), (float) (y + w / 2.0f));
            // oval.set((float)(x+w/4.0f), (float)(y+w/4.0f), (float)(x-w/4.0f), (float)(y-w/4.0f));
*/            //最基本的实现,通过点控制线,绘制椭圆
            canvas.getChildren().add(ellipse);
            x += deltaX;
            y += deltaY;
            w += deltaW;
        }
    }

}
/**
 * 
 */
package brush;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;

/**
 * @author gj
 *
 * 2018年6月6日
 */
public class Test extends Application {

	/* (non-Javadoc)
	 * @see javafx.application.Application#start(javafx.stage.Stage)
	 */
	@Override
	public void start(Stage arg0) throws Exception {
		// TODO Auto-generated method stub
		
		Pane pane = new Pane();
		pane.setPrefSize(800, 900);
		BasePenExtend basePenExtend=new BasePenExtend();
		basePenExtend.setPaint(50);
		basePenExtend.draw(pane);
		pane.setOnMousePressed(e->{
			basePenExtend.onTouchEvent(0, e, pane);
		});
		
		pane.setOnMouseDragged(e->{			
			basePenExtend.onTouchEvent(1, e, pane);
			basePenExtend.draw(pane);
		});
		pane.setOnMouseReleased(e->{
			basePenExtend.onTouchEvent(2, e, pane);
		});
		
		arg0.setScene(new Scene(pane));
		arg0.show();		
	}

	public static void main(String[] args) {
		launch(args);
	}
}

 

你可能感兴趣的:(项目总结)