Java – How to smooth the drawing path when drawing with variable width strokes

How to smooth the drawing path when drawing with variable width strokes… here is a solution to the problem.

How to smooth the drawing path when drawing with variable width strokes

I created a sample drawing application where the user can draw with variable width strokes, and so far drawing paths with variable strokes has worked, but the lines drawn are not smooth. The code I used to implement it looks like this.

Help me with this as I’ve been sticking with this issue for the last two days.

Code that draws the path with a variable stroke width

public class FingerPaint extends GraphicsActivity {

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new MyView(this));
    }

public void colorChanged(int color)  {
    }

public class MyView extends View {

private static final float STROKE_WIDTH = 5f;    
        private Paint paint = new Paint();
        private Path mPath = new Path();
        ArrayList<Path> mPaths = new ArrayList<Path>();
        ArrayList<Integer> mStrokes = new ArrayList<Integer>();

private float lastTouchX;
        private float lastTouchY;
        private final RectF dirtyRect = new RectF();
        private int lastStroke = -1;
        int variableWidthDelta = 0;
        
private static final float STROKE_DELTA = 0.0001f;  for float comparison
        private static final float STROKE_INCREMENT = 0.01f;  amount to interpolate
        private float currentStroke = STROKE_WIDTH;
        private float targetStroke = STROKE_WIDTH;
        
private float mX, mY;
        private static final float TOUCH_TOLERANCE = 4;

public MyView(Context context)  {
            super(context);

paint.setAntiAlias(true);
            paint.setDither(true);
            paint.setStyle(Paint.Style.STROKE);
            paint.setStrokeJoin(Paint.Join.ROUND);
            paint.setStrokeCap(Paint.Cap.ROUND);    
            paint.setStrokeWidth(STROKE_WIDTH);
        }

public void clear() {
            mPath.reset();
             Repaints the entire view.
            invalidate();
        }

@Override
        protected void onDraw(Canvas canvas)  {
            for(int i=0; i<mPaths.size(); i++) {
                paint.setStrokeWidth(mStrokes.get(i));
                canvas.drawPath(mPaths.get(i), paint);
            }
        }
    
@Override
        public boolean onTouchEvent(MotionEvent event) {
            float eventX = event.getX();
            float eventY = event.getY();
            int historySize = event.getHistorySize();

switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN: {
                    resetDirtyRect(eventX, eventY);
                  mPath.reset();
                    mPath.moveTo(eventX, eventY);
                    mX = eventX;
                    mY = eventY;
                    break;                  
                }
                case MotionEvent.ACTION_MOVE: {                 
                    if (event.getPressure()>=0.00 && event.getPressure()<0.05) {
                        variableWidthDelta = -2;
                    } else if (event.getPressure()>=0.05 && event.getPressure()<0.10) {
                        variableWidthDelta = -2;
                    } else if (event.getPressure()>=0.10 && event.getPressure()<0.15) {
                        variableWidthDelta = -2;
                    } else if (event.getPressure()>=0.15 && event.getPressure()<0.20) {
                        variableWidthDelta = -2;
                    } else if (event.getPressure()>=0.20 && event.getPressure()<0.25) {
                        variableWidthDelta = -2;
                    } else if (event.getPressure() >= 0.25 && event.getPressure()<0.30) {
                        variableWidthDelta = 1;
                    } else if (event.getPressure() >= 0.30 && event.getPressure()<0.35) {
                        variableWidthDelta = 2;
                    } else if (event.getPressure() >= 0.35 && event.getPressure()<0.40) {
                        variableWidthDelta = 3;
                    } else if (event.getPressure() >= 0.40 && event.getPressure()<0.45) {
                        variableWidthDelta = 4;
                    } else if (event.getPressure() >= 0.45 && event.getPressure()<0.60) {
                        variableWidthDelta = 5;
                    }                                          
                    
 if current not roughly equal to target
                    if( Math.abs(targetStroke - currentStroke) > STROKE_DELTA ) 
                    {
                         move towards target by the increment
                        if( targetStroke > currentStroke)
                        {
                            currentStroke = Math.min(targetStroke, currentStroke + STROKE_INCREMENT);
                        }
                        else
                        {
                            currentStroke = Math.max(targetStroke, currentStroke - STROKE_INCREMENT);
                        }
                       
} 
                    mStrokes.add((int) currentStroke);
                    
targetStroke = variableWidthDelta;
                    
float dx = Math.abs(eventX - mX);
                    float dy = Math.abs(eventY - mY);

if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
                        if(lastStroke != variableWidthDelta) {
                            mPath.lineTo(mX, mY);
                            
mPath = new Path();
                            mPath.moveTo(mX,mY);
                            mPaths.add(mPath);
                        }
                        
mPath.quadTo(mX, mY, (eventX + mX)/2, (eventY + mY)/2);
                        mX = eventX;
                        mY = eventY;
                    }

for (int i = 0; i < historySize; i++) {
                        float historicalX = event.getHistoricalX(i);
                        float historicalY = event.getHistoricalY(i);
                        expandDirtyRect(historicalX, historicalY);
                    }
                    break;
                }
                case MotionEvent.ACTION_UP: {
                    for (int i = 0; i < historySize; i++) {
                        float historicalX = event.getHistoricalX(i);
                        float historicalY = event.getHistoricalY(i);
                        expandDirtyRect(historicalX, historicalY);
                    }
                   mPath.lineTo(mX, mY);                   
                   break;
                }
            }
            
 Include half the stroke width to avoid clipping.
            invalidate();

lastTouchX = eventX;
            lastTouchY = eventY;
            lastStroke = variableWidthDelta;
            
return true;
        }
        
private void expandDirtyRect(float historicalX, float historicalY) {
            if (historicalX < dirtyRect.left) {
                dirtyRect.left = historicalX;
            }  else if (historicalX > dirtyRect.right) {
                dirtyRect.right = historicalX;
            }
            if (historicalY < dirtyRect.top) {
                dirtyRect.top = historicalY;
            } else if (historicalY > dirtyRect.bottom) {
                dirtyRect.bottom = historicalY;
            }
        }

/**
         * Resets the dirty region when the motion event occurs.
         */
        private void resetDirtyRect(float eventX, float eventY) {
             The lastTouchX and lastTouchY were set when the ACTION_DOWN
             motion event occurred.
            dirtyRect.left = Math.min(lastTouchX, eventX);
            dirtyRect.right = Math.max(lastTouchX, eventX);
            dirtyRect.top = Math.min(lastTouchY, eventY);
            dirtyRect.bottom = Math.max(lastTouchY, eventY);
        }
    }
}

The output I get

enter image description here

The output I want to achieve

enter image description here

Solution

Instead of jumping to a new stroke width as soon as a change is detected, you can set a target and interpolate it until you reach it. Your mStrokes need to be Float and not Integer.

private static final float STROKE_DELTA = 0.0001f;  for float comparison
private static final float STROKE_INCREMENT = 0.01f;  amount to interpolate
private float currentStroke = STROKE_WIDTH;
private float targetStroke = STROKE_WIDTH;

At the location where the new path is currently created for the new stroke width, do the following:

// if current not roughly equal to target
if( Math.abs(targetStroke - currentStroke) > STROKE_DELTA ) {
     move towards target by the increment
    if( targetStroke > currentStroke )
        currentStroke = Math.min(targetStroke, currentStroke + STROKE_INCREMENT);
    else
        currentStroke = Math.max(targetStroke, currentStroke - STROKE_INCREMENT);
    mPath.lineTo(mX, mY);

mPath = new Path();
    mPath.moveTo(mX,mY);
    mPaths.add(mPath);
    mStrokes.add(currentStroke);
}

You will update targetStroke where variableWidthDelta is currently set.

Related Problems and Solutions