/** * Copyright (c) 2016 UPTech * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package org.thoughtcrime.securesms.scribbles.widget.entity; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PointF; import android.support.annotation.IntRange; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import org.thoughtcrime.securesms.util.MathUtils; import org.thoughtcrime.securesms.scribbles.viewmodel.Layer; @SuppressWarnings({"WeakerAccess"}) public abstract class MotionEntity { /** * data */ @NonNull protected final Layer layer; /** * transformation matrix for the entity */ protected final Matrix matrix = new Matrix(); /** * true - entity is selected and need to draw it's border * false - not selected, no need to draw it's border */ private boolean isSelected; /** * maximum scale of the initial image, so that * the entity still fits within the parent canvas */ protected float holyScale; /** * width of canvas the entity is drawn in */ @IntRange(from = 0) protected int canvasWidth; /** * height of canvas the entity is drawn in */ @IntRange(from = 0) protected int canvasHeight; /** * Destination points of the entity * 5 points. Size of array - 10; Starting upper left corner, clockwise * last point is the same as first to close the circle * NOTE: saved as a field variable in order to avoid creating array in draw()-like methods */ private final float[] destPoints = new float[10]; // x0, y0, x1, y1, x2, y2, x3, y3, x0, y0 /** * Initial points of the entity * @see #destPoints */ protected final float[] srcPoints = new float[10]; // x0, y0, x1, y1, x2, y2, x3, y3, x0, y0 @NonNull private Paint borderPaint = new Paint(); public MotionEntity(@NonNull Layer layer, @IntRange(from = 1) int canvasWidth, @IntRange(from = 1) int canvasHeight) { this.layer = layer; this.canvasWidth = canvasWidth; this.canvasHeight = canvasHeight; } private boolean isSelected() { return isSelected; } public void setIsSelected(boolean isSelected) { this.isSelected = isSelected; } /** * S - scale matrix, R - rotate matrix, T - translate matrix, * L - result transformation matrix * <p> * The correct order of applying transformations is : L = S * R * T * <p> * See more info: <a href="http://gamedev.stackexchange.com/questions/29260/transform-matrix-multiplication-order">Game Dev: Transform Matrix multiplication order</a> * <p> * Preconcat works like M` = M * S, so we apply preScale -> preRotate -> preTranslate * the result will be the same: L = S * R * T * <p> * NOTE: postconcat (postScale, etc.) works the other way : M` = S * M, in order to use it * we'd need to reverse the order of applying * transformations : post holy scale -> postTranslate -> postRotate -> postScale */ protected void updateMatrix() { // init matrix to E - identity matrix matrix.reset(); float widthAspect = 1.0F * canvasWidth / getWidth(); float heightAspect = 1.0F * canvasHeight / getHeight(); // fit the smallest size holyScale = Math.min(widthAspect, heightAspect); float topLeftX = layer.getX() * canvasWidth; float topLeftY = layer.getY() * canvasHeight; float centerX = topLeftX + getWidth() * holyScale * 0.5F; float centerY = topLeftY + getHeight() * holyScale * 0.5F; // calculate params float rotationInDegree = layer.getRotationInDegrees(); float scaleX = layer.getScale(); float scaleY = layer.getScale(); if (layer.isFlipped()) { // flip (by X-coordinate) if needed rotationInDegree *= -1.0F; scaleX *= -1.0F; } // applying transformations : L = S * R * T // scale matrix.preScale(scaleX, scaleY, centerX, centerY); // rotate matrix.preRotate(rotationInDegree, centerX, centerY); // translate matrix.preTranslate(topLeftX, topLeftY); // applying holy scale - S`, the result will be : L = S * R * T * S` matrix.preScale(holyScale, holyScale); } public float absoluteCenterX() { float topLeftX = layer.getX() * canvasWidth; return topLeftX + getWidth() * holyScale * 0.5F; } public float absoluteCenterY() { float topLeftY = layer.getY() * canvasHeight; return topLeftY + getHeight() * holyScale * 0.5F; } public PointF absoluteCenter() { float topLeftX = layer.getX() * canvasWidth; float topLeftY = layer.getY() * canvasHeight; float centerX = topLeftX + getWidth() * holyScale * 0.5F; float centerY = topLeftY + getHeight() * holyScale * 0.5F; return new PointF(centerX, centerY); } public void moveToCanvasCenter() { moveCenterTo(new PointF(canvasWidth * 0.5F, canvasHeight * 0.5F)); } public void moveCenterTo(PointF moveToCenter) { PointF currentCenter = absoluteCenter(); layer.postTranslate(1.0F * (moveToCenter.x - currentCenter.x) / canvasWidth, 1.0F * (moveToCenter.y - currentCenter.y) / canvasHeight); } private final PointF pA = new PointF(); private final PointF pB = new PointF(); private final PointF pC = new PointF(); private final PointF pD = new PointF(); /** * For more info: * <a href="http://math.stackexchange.com/questions/190111/how-to-check-if-a-point-is-inside-a-rectangle">StackOverflow: How to check point is in rectangle</a> * <p>NOTE: it's easier to apply the same transformation matrix (calculated before) to the original source points, rather than * calculate the result points ourselves * @param point point * @return true if point (x, y) is inside the triangle */ public boolean pointInLayerRect(PointF point) { updateMatrix(); // map rect vertices matrix.mapPoints(destPoints, srcPoints); pA.x = destPoints[0]; pA.y = destPoints[1]; pB.x = destPoints[2]; pB.y = destPoints[3]; pC.x = destPoints[4]; pC.y = destPoints[5]; pD.x = destPoints[6]; pD.y = destPoints[7]; return MathUtils.pointInTriangle(point, pA, pB, pC) || MathUtils.pointInTriangle(point, pA, pD, pC); } /** * http://judepereira.com/blog/calculate-the-real-scale-factor-and-the-angle-of-rotation-from-an-android-matrix/ * * @param canvas Canvas to draw * @param drawingPaint Paint to use during drawing */ public final void draw(@NonNull Canvas canvas, @Nullable Paint drawingPaint) { this.canvasWidth = canvas.getWidth(); this.canvasHeight = canvas.getHeight(); updateMatrix(); canvas.save(); drawContent(canvas, drawingPaint); if (isSelected()) { // get alpha from drawingPaint int storedAlpha = borderPaint.getAlpha(); if (drawingPaint != null) { borderPaint.setAlpha(drawingPaint.getAlpha()); } drawSelectedBg(canvas); // restore border alpha borderPaint.setAlpha(storedAlpha); } canvas.restore(); } private void drawSelectedBg(Canvas canvas) { matrix.mapPoints(destPoints, srcPoints); //noinspection Range canvas.drawLines(destPoints, 0, 8, borderPaint); //noinspection Range canvas.drawLines(destPoints, 2, 8, borderPaint); } @NonNull public Layer getLayer() { return layer; } public void setBorderPaint(@NonNull Paint borderPaint) { this.borderPaint = borderPaint; } protected abstract void drawContent(@NonNull Canvas canvas, @Nullable Paint drawingPaint); public abstract int getWidth(); public abstract int getHeight(); public void release() { // free resources here } public void updateEntity() {} @Override protected void finalize() throws Throwable { try { release(); } finally { //noinspection ThrowFromFinallyBlock super.finalize(); } } }