/* * This file is part of Oekaki Mobile. * Copyright (C) 2013 Jeremy Lam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package idv.jlchntoz.oekakimobile; import java.util.ArrayList; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.Resources; import android.graphics.*; import android.os.*; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import com.chibipaint.CPController; import com.chibipaint.engine.CPArtwork; import com.chibipaint.engine.CPBrushInfo; import com.chibipaint.util.CPBezier; import com.chibipaint.util.CPRect; public class PaintCanvas extends View implements CPController.ICPToolListener, CPController.ICPModeListener, CPArtwork.ICPArtworkListener { private static final int MSG_REDRAW = 1; private int width, height; private CPArtwork artwork; private Paint paintShader, paintHint; private Matrix transform, transform_invert; private CPMode ActiveMode, curDrawMode; private Bitmap BM, CheckerBoard; private BitmapShader CheckerBoardShader; private PointF offset; private int backgroundColor, checkerBoardColor1, checkerBoardColor2, selectionColor; private float rotation, zoom; private int screenWidth, screenHeight; public CPController controller; private ArrayList<MotionEvent> ME; private Thread TE; private Rect totalUpdateRegion; private boolean updateBM; // // Modes system: modes control the way the GUI is reacting to the user input // All the tools are implemented through modes // private CPMode colorPickerMode = new CPColorPickerMode(); private CPMode moveCanvasMode = new CPMoveCanvasMode(); private CPMode rotateCanvasMode = new CPRotateCanvasMode(); private CPMode floodFillMode = new CPFloodFillMode(); private CPMode rectSelectionMode = new CPRectSelectionMode(); private CPMode moveToolMode = new CPMoveToolMode(); // this must correspond to the stroke modes defined in CPToolInfo private CPMode drawingModes[] = { new CPFreehandMode(), new CPLineMode(), new CPBezierMode(), }; public PaintCanvas(Context c) { super(c); width = 800; height = 600; initialize(); } public PaintCanvas(Context c, int width, int height) { super(c); this.width = width; this.height = height; initialize(); } public PaintCanvas(Context c, AttributeSet a) { super(c, a); width = 800; height = 600; initialize(); } private void initialize() { curDrawMode = drawingModes[0]; ActiveMode = curDrawMode; offset = new PointF(0, 0); zoom = 1; rotation = 0; screenWidth = 0; screenHeight = 0; paintShader = new Paint(); paintHint = new Paint(); controller = new CPControllerDroid(); controller.addToolListener(this); controller.addModeListener(this); setArtWork(new CPArtwork(getContext(), width, height)); Resources res = getResources(); backgroundColor = res.getColor(R.color.BackgroundColor); checkerBoardColor1 = res.getColor(R.color.CheckerBoardColor1); checkerBoardColor2 = res.getColor(R.color.CheckerBoardColor2); selectionColor = res.getColor(R.color.SelectionColor); paintHint.setStyle(Paint.Style.STROKE); paintHint.setColor(selectionColor); paintHint.setStrokeWidth(1); doTransform(); // Generate CheckerBoard Background if (CheckerBoard == null) { CheckerBoard = Bitmap.createBitmap(20, 20, Bitmap.Config.ARGB_8888); for (int i = 0; i < 20; i++) for (int j = 0; j < 20; j++) if (Math.floor(i / 10) == 0 ^ Math.floor(j / 10) == 1) CheckerBoard.setPixel(i, j, checkerBoardColor1); else CheckerBoard.setPixel(i, j, checkerBoardColor2); } if (CheckerBoardShader == null) CheckerBoardShader = new BitmapShader(CheckerBoard, BitmapShader.TileMode.REPEAT, BitmapShader.TileMode.REPEAT); paintShader.setShader(CheckerBoardShader); } @SuppressLint("HandlerLeak") private final Handler _h = new Handler() { @Override public void handleMessage(Message m) { switch (m.what) { case MSG_REDRAW: invalidate(); break; } } }; @Override protected void onSizeChanged(int w, int h, int ow, int oh) { screenWidth = w; screenHeight = h; if (artwork != null) setOffset((screenWidth - artwork.width) / 2F, (screenHeight - artwork.height) / 2F); } public void setArtWork(CPArtwork artwork) { controller.setArtwork(artwork); this.artwork = artwork; new Thread() { @Override public void run() { width = PaintCanvas.this.artwork.width; height = PaintCanvas.this.artwork.height; BM = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); BM.setPixels(PaintCanvas.this.artwork.getDisplayBM().data, 0, width, 0, 0, width, height); repaint(); } }.start(); if (screenWidth > 0 && screenHeight > 0) setOffset((screenWidth - artwork.width) / 2F, (screenHeight - artwork.height) / 2F); artwork.addListener(this); } @Override public void onDraw(Canvas c) { c.drawColor(backgroundColor); c.drawRect(coordToDisplay(artwork.getSize()), paintShader); c.drawBitmap(BM, transform, null); ActiveMode.paint(c); } @Override public boolean onTouchEvent(MotionEvent ev) { if (ME == null) ME = new ArrayList<MotionEvent>(); ME.add(MotionEvent.obtain(ev)); // Push current state to array pending // for process if (TE == null || !TE.isAlive()) { // If the thread is died or not yet // created, make the new one. TE = new Thread() { @Override public void run() { updateBM = false; for (int i = 0; i < ME.size(); i++) { MotionEvent e = ME.get(i); switch (e.getAction() & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: ActiveMode.mousePressed(e); break; case MotionEvent.ACTION_MOVE: ActiveMode.mouseDragged(e); break; case MotionEvent.ACTION_UP: ActiveMode.mouseDragged(e); ActiveMode.mouseReleased(e); break; case MotionEvent.ACTION_POINTER_DOWN: ActiveMode.secondMousePressed(e); break; case MotionEvent.ACTION_POINTER_UP: ActiveMode.secondMouseReleased(e); break; } e.recycle(); } ME.clear(); updateBM = true; repaint(); } }; TE.start(); } return true; } @Override public void updateRegion(CPArtwork artwork, CPRect region) { // Only updating the region changed, or the app will be super-laggy. // Prevent out of bounds, before passing parameters to update, // it should be checked to make sure the values are inside the bitmap. int _left = Math.max(0, region.left), _top = Math.max(0, region.top); int _width = region.getWidth(), _height = region.getHeight(); if (_width - _left > width) _width = width + _left; if (_height - _top > height) _height = height + _top; if (totalUpdateRegion == null) totalUpdateRegion = new Rect(_left, _top, _left + _width, _top + _height); else totalUpdateRegion.union(_left, _top, _left + _width, _top + _height); repaint(); } @Override public void layerChange(CPArtwork artwork) { } @Override public void modeChange(int mode) { switch (mode) { case CPController.M_DRAW: ActiveMode = curDrawMode; break; case CPController.M_FLOODFILL: ActiveMode = floodFillMode; break; case CPController.M_RECT_SELECTION: ActiveMode = rectSelectionMode; break; case CPController.M_MOVE_TOOL: ActiveMode = moveToolMode; break; case CPController.M_ROTATE_CANVAS: ActiveMode = rotateCanvasMode; break; case CPController.M_COLOR_PICKER: ActiveMode = colorPickerMode; break; case CPController.M_MOVE_CANVAS: ActiveMode = moveCanvasMode; break; } } private void doTransform() { if (transform == null) transform = new Matrix(); if (transform_invert == null) transform_invert = new Matrix(); transform.reset(); transform.postTranslate(offset.x, offset.y); transform.postScale(zoom, zoom); transform.postRotate(rotation); transform.invert(transform_invert); } private PointF transformBy(Matrix m, PointF p) { float[] _p = new float[2]; _p[0] = p.x; _p[1] = p.y; m.mapPoints(_p); return new PointF(_p[0], _p[1]); } private RectF transformBy(Matrix m, RectF r) { float[] _p = new float[4]; _p[0] = r.left; _p[1] = r.top; _p[2] = r.right; _p[3] = r.bottom; m.mapPoints(_p); return new RectF(_p[0], _p[1], _p[2], _p[3]); } @Override public void newTool(int tool, CPBrushInfo toolInfo) { curDrawMode = drawingModes[toolInfo.strokeMode]; } public PointF coordToDocument(PointF p) { return transformBy(transform_invert, p); } public PointF coordToDisplay(PointF p) { return transformBy(transform, p); } public RectF coordToDisplay(CPRect r) { return transformBy(transform, new RectF(r.left, r.top, r.right, r.bottom)); } public PointF getOffset() { return offset; } public void setOffset(PointF p) { offset = p; doTransform(); } public void setOffset(float x, float y) { setOffset(new PointF(x, y)); } public void repaint() { if (updateBM && totalUpdateRegion != null) { BM.setPixels(artwork.getDisplayBM().data, totalUpdateRegion.left + totalUpdateRegion.top * width, width, totalUpdateRegion.left, totalUpdateRegion.top, totalUpdateRegion.width(), totalUpdateRegion.height()); totalUpdateRegion = null; updateBM = false; } repaint(0, 0, width, height); } public void repaint(float x, float y, float w, float h) { Message m = new Message(); m.what = MSG_REDRAW; _h.sendMessage(m); } public float getRotation2() { return rotation; } public void setRotation2(float r) { rotation = r; doTransform(); } public void resetRotation() { setRotation2(0); } public float getZoom() { return zoom; } public void zoomIn() { zoom *= 2; doTransform(); } public void zoomOut() { zoom /= 2; doTransform(); } public void resetZoom() { setZoom(1); } public void setZoom(float value) { zoom = value; doTransform(); } public PointF getSize() { return new PointF(width, height); } // // base class for the different modes // abstract class CPMode { // Mouse (Finger) Input public void mousePressed(MotionEvent e) { } public void mouseDragged(MotionEvent e) { } public void mouseReleased(MotionEvent e) { } public void mouseMoved(MotionEvent e) { } public void mouseClicked(MotionEvent e) { } public void mouseEntered(MotionEvent e) { } public void mouseExited(MotionEvent e) { } // Multi-touch here public void secondMousePressed(MotionEvent e) { } public void secondMouseReleased(MotionEvent e) { } // GUI drawing public void paint(Canvas g2d) { } } // // Freehand mode // // FIXME: dragLeft no longer necessary, should not specify the drag button class CPFreehandMode extends CPMode { boolean dragLeft = false; PointF smoothMouse = new PointF(0, 0); @Override public void mousePressed(MotionEvent e) { PointF p = new PointF(e.getX(), e.getY()); PointF pf = coordToDocument(p); dragLeft = true; artwork.beginStroke(pf.x, pf.y, e.getPressure()); smoothMouse = pf; } @Override public void mouseDragged(MotionEvent e) { PointF p = new PointF(e.getX(), e.getY()); PointF pf = coordToDocument(p); float smoothing = Math.min(.999f, (float) Math.pow(controller.getBrushInfo().smoothing, .3)); smoothMouse.x = (1f - smoothing) * pf.x + smoothing * smoothMouse.x; smoothMouse.y = (1f - smoothing) * pf.y + smoothing * smoothMouse.y; if (dragLeft) artwork.continueStroke(smoothMouse.x, smoothMouse.y, e.getPressure()); } @Override public void mouseReleased(MotionEvent e) { dragLeft = false; artwork.endStroke(); } } // // Line drawing mode // class CPLineMode extends CPMode { boolean dragLine = false; PointF dragLineFrom, dragLineTo; @Override public void mousePressed(MotionEvent e) { if (!dragLine) { PointF p = new PointF(e.getX(), e.getY()); dragLine = true; dragLineFrom = dragLineTo = p; } } @Override public void mouseDragged(MotionEvent e) { PointF p = new PointF(e.getX(), e.getY()); RectF r = new RectF(Math.min(dragLineFrom.x, dragLineTo.x), Math.min( dragLineFrom.y, dragLineTo.y), Math.abs(dragLineFrom.x - dragLineTo.x) + 1, Math.abs(dragLineFrom.y - dragLineTo.y) + 1); r.union(new RectF(Math.min(dragLineFrom.x, p.x), Math.min(dragLineFrom.y, p.y), Math.abs(dragLineFrom.x - p.x) + 1, Math.abs(dragLineFrom.y - p.y) + 1)); dragLineTo = p; repaint(r.left, r.top, r.width(), r.height()); } @Override public void mouseReleased(MotionEvent e) { PointF p = new PointF(e.getX(), e.getY()); PointF pf = coordToDocument(p); dragLine = false; PointF from = coordToDocument(dragLineFrom); artwork.beginStroke(from.x, from.y, 1); artwork.continueStroke(pf.x, pf.y, 1); artwork.endStroke(); RectF r = new RectF(Math.min(dragLineFrom.x, dragLineTo.x), Math.min( dragLineFrom.y, dragLineTo.y), Math.abs(dragLineFrom.x - dragLineTo.x) + 1, Math.abs(dragLineFrom.y - dragLineTo.y) + 1); repaint(r.left, r.top, r.width(), r.height()); } @Override public void paint(Canvas g2d) { if (dragLine) g2d.drawLine(dragLineFrom.x, dragLineFrom.y, dragLineTo.x, dragLineTo.y, paintHint); } } // // Bezier drawing mode // class CPBezierMode extends CPMode { // bezier drawing static final int BEZIER_POINTS = 500; static final int BEZIER_POINTS_PREVIEW = 100; boolean dragBezier = false; int dragBezierMode; // 0 Initial drag, 1 first control point, 2 second // point PointF dragBezierP0, dragBezierP1, dragBezierP2, dragBezierP3; @Override public void mousePressed(MotionEvent e) { PointF p = coordToDocument(new PointF(e.getX(), e.getY())); if (!dragBezier) { dragBezier = true; dragBezierMode = 0; dragBezierP0 = dragBezierP1 = dragBezierP2 = dragBezierP3 = p; } else { if (dragBezierMode == 1) { dragBezierP1 = p; repaint(); // FIXME: repaint only the bezier region } if (dragBezierMode == 2) { dragBezierP2 = p; repaint(); // FIXME: repaint only the bezier region } } } @Override public void mouseDragged(MotionEvent e) { PointF p = coordToDocument(new PointF(e.getX(), e.getY())); if (dragBezier && dragBezierMode == 0) { dragBezierP2 = dragBezierP3 = p; repaint(); } } @Override public void mouseReleased(MotionEvent e) { if (dragBezier) if (dragBezierMode == 0) dragBezierMode = 1; else if (dragBezierMode == 1) dragBezierMode = 2; else if (dragBezierMode == 2) { dragBezier = false; PointF p0 = dragBezierP0; PointF p1 = dragBezierP1; PointF p2 = dragBezierP2; PointF p3 = dragBezierP3; CPBezier bezier = new CPBezier(); bezier.x0 = p0.x; bezier.y0 = p0.y; bezier.x1 = p1.x; bezier.y1 = p1.y; bezier.x2 = p2.x; bezier.y2 = p2.y; bezier.x3 = p3.x; bezier.y3 = p3.y; float x[] = new float[BEZIER_POINTS]; float y[] = new float[BEZIER_POINTS]; bezier.compute(x, y, BEZIER_POINTS); artwork.beginStroke(x[0], y[0], 1); for (int i = 1; i < BEZIER_POINTS; i++) artwork.continueStroke(x[i], y[i], 1); artwork.endStroke(); repaint(); } } @Override public void paint(Canvas g2d) { if (dragBezier) { CPBezier bezier = new CPBezier(); coordToDisplay(dragBezierP0); PointF p0 = coordToDisplay(dragBezierP0); PointF p1 = coordToDisplay(dragBezierP1); PointF p2 = coordToDisplay(dragBezierP2); PointF p3 = coordToDisplay(dragBezierP3); bezier.x0 = p0.x; bezier.y0 = p0.y; bezier.x1 = p1.x; bezier.y1 = p1.y; bezier.x2 = p2.x; bezier.y2 = p2.y; bezier.x3 = p3.x; bezier.y3 = p3.y; int x[] = new int[BEZIER_POINTS_PREVIEW]; int y[] = new int[BEZIER_POINTS_PREVIEW]; bezier.compute(x, y, BEZIER_POINTS_PREVIEW); Path pt = new Path(); pt.moveTo(x[0], y[0]); for (int i = 0; i < BEZIER_POINTS_PREVIEW; i++) pt.lineTo(x[i], y[i]); g2d.drawPath(pt, paintHint); g2d.drawLine(p0.x, p0.y, p1.x, p1.y, paintHint); g2d.drawLine(p2.x, p2.y, p3.x, p3.y, paintHint); } } } // // Color picker mode // class CPColorPickerMode extends CPMode { @Override public void mousePressed(MotionEvent e) { PointF p = new PointF(e.getX(), e.getY()); PointF pf = coordToDocument(p); if (artwork.isPointWithin(pf.x, pf.y)) controller.setCurColorRgb(artwork.colorPicker(pf.x, pf.y)); } @Override public void mouseDragged(MotionEvent e) { PointF p = new PointF(e.getX(), e.getY()); PointF pf = coordToDocument(p); if (artwork.isPointWithin(pf.x, pf.y)) controller.setCurColorRgb(artwork.colorPicker(pf.x, pf.y)); } @Override public void mouseReleased(MotionEvent e) { } } // // Canvas move mode // class CPMoveCanvasMode extends CPMode { boolean dragMiddle = false, dragZoom = false; float dragMoveX, dragMoveY, oldDistance, scale, prescale; Matrix postTransform; PointF dragMoveOffset, midpoint; @Override public void mousePressed(MotionEvent e) { PointF p = new PointF(e.getX(), e.getY()); midpoint = new PointF(p.x, p.y); dragMiddle = true; dragMoveX = p.x; dragMoveY = p.y; dragMoveOffset = getOffset(); doTransform(); postTransform = new Matrix(transform); prescale = getZoom(); } @Override public void mouseDragged(MotionEvent e) { PointF p = new PointF(e.getX(), e.getY()); if (dragZoom) { scale = spacing(e) / oldDistance; midPoint(midpoint, e); postTransform.reset(); postTransform.postTranslate((p.x - dragMoveX) / prescale / scale, (p.y - dragMoveY) / prescale / scale); postTransform.postScale(scale, scale, midpoint.x / prescale, midpoint.y / prescale); PointF pd = transformBy(postTransform, dragMoveOffset); setOffset(pd.x, pd.y); setZoom(prescale * scale); repaint(); } else if (dragMiddle) { postTransform.reset(); postTransform.postTranslate((p.x - dragMoveX) / prescale, (p.y - dragMoveY) / prescale); PointF pd = transformBy(postTransform, dragMoveOffset); setOffset(pd.x, pd.y); repaint(); } } @Override public void mouseReleased(MotionEvent e) { if (dragMiddle) dragMiddle = false; } @Override public void secondMousePressed(MotionEvent e) { dragZoom = true; oldDistance = spacing(e); } @Override public void secondMouseReleased(MotionEvent e) { dragZoom = false; dragMiddle = false; } private float spacing(MotionEvent event) { float x = event.getX(0) - event.getX(1); float y = event.getY(0) - event.getY(1); return (float) Math.sqrt(x * x + y * y); } private void midPoint(PointF point, MotionEvent event) { float x = event.getX(0) + event.getX(1); float y = event.getY(0) + event.getY(1); point.set(x / 2, y / 2); } } // // Flood fill mode // class CPFloodFillMode extends CPMode { @Override public void mousePressed(MotionEvent e) { PointF p = new PointF(e.getX(), e.getY()); PointF pf = coordToDocument(p); if (artwork.isPointWithin(pf.x, pf.y)) { artwork.floodFill(pf.x, pf.y); repaint(); } } @Override public void mouseDragged(MotionEvent e) { } @Override public void mouseReleased(MotionEvent e) { } } // // CPRectSelection mode // class CPRectSelectionMode extends CPMode { PointF firstClick; CPRect curRect = new CPRect(); @Override public void mousePressed(MotionEvent e) { PointF p = coordToDocument(new PointF(e.getX(), e.getY())); curRect.makeEmpty(); firstClick = p; repaint(); } @Override public void mouseDragged(MotionEvent e) { PointF p = coordToDocument(new PointF(e.getX(), e.getY())); boolean square = false; float squareDist = Math.max(Math.abs(p.x - firstClick.x), Math.abs(p.y - firstClick.y)); if (p.x >= firstClick.x) { curRect.left = (int) firstClick.x; curRect.right = (int) (square ? firstClick.x + squareDist : p.x); } else { curRect.left = (int) (square ? firstClick.x - squareDist : p.x); curRect.right = (int) firstClick.x; } if (p.y >= firstClick.y) { curRect.top = (int) firstClick.y; curRect.bottom = (int) (square ? firstClick.y + squareDist : p.y); } else { curRect.top = (int) (square ? firstClick.y - squareDist : p.y); curRect.bottom = (int) firstClick.y; } repaint(); } @Override public void mouseReleased(MotionEvent e) { artwork.rectangleSelection(curRect); repaint(); } @Override public void paint(Canvas g2d) { if (!curRect.isEmpty()) g2d.drawRect(coordToDisplay(curRect), paintHint); } } // // CPMoveTool mode // class CPMoveToolMode extends CPMode { PointF firstClick; @Override public void mousePressed(MotionEvent e) { PointF p = coordToDocument(new PointF(e.getX(), e.getY())); firstClick = p; artwork.beginPreviewMode(false); // FIXME: The following hack avoids a slight display glitch // if the whole move tool mess is fixed it probably won't be // necessary anymore artwork.move(0, 0); } @Override public void mouseDragged(MotionEvent e) { PointF p = coordToDocument(new PointF(e.getX(), e.getY())); artwork.move((int) (p.x - firstClick.x), (int) (p.y - firstClick.y)); repaint(); } @Override public void mouseReleased(MotionEvent e) { artwork.endPreviewMode(); repaint(); } } // // Canvas rotate mode // class CPRotateCanvasMode extends CPMode { PointF firstClick; float initAngle; Matrix initTransform; boolean dragged; @Override public void mousePressed(MotionEvent e) { firstClick = new PointF(e.getX(), e.getY()); initAngle = getRotation2(); initTransform = new Matrix(transform); dragged = false; } @Override public void mouseDragged(MotionEvent e) { dragged = true; PointF p = new PointF(e.getX(), e.getY()); PointF d = getSize(); PointF center = new PointF(d.x / 2.f, d.y / 2.f); float deltaAngle = (float) Math.atan2(p.y - center.y, p.x - center.x) - (float) Math .atan2(firstClick.y - center.y, firstClick.x - center.x); Matrix rotTrans = new Matrix(); rotTrans.setRotate(deltaAngle, center.x, center.y); rotTrans.postConcat(initTransform); setRotation2(initAngle + deltaAngle); float[] pts = new float[2]; pts[0] = firstClick.x; pts[1] = firstClick.y; rotTrans.mapPoints(pts); setOffset(pts[0], pts[1]); repaint(); } @Override public void mouseReleased(MotionEvent e) { if (!dragged) resetRotation(); } } }