MotionPaths.java

/*
 * Copyright (C) 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package androidx.constraintlayout.core.motion;

import static androidx.constraintlayout.core.motion.MotionWidget.UNSET;

import androidx.constraintlayout.core.motion.key.MotionKeyPosition;
import androidx.constraintlayout.core.motion.utils.Easing;
import androidx.constraintlayout.core.motion.utils.Utils;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Set;

/**
 * This is used to capture and play back path of the layout.
 * It is used to set the bounds of the view (view.layout(l, t, r, b))
 */
public class MotionPaths implements Comparable<MotionPaths> {
    public static final String TAG = "MotionPaths";
    public static final boolean DEBUG = false;
    public static final boolean OLD_WAY = false; // the computes the positions the old way
    static final int OFF_POSITION = 0;
    static final int OFF_X = 1;
    static final int OFF_Y = 2;
    static final int OFF_WIDTH = 3;
    static final int OFF_HEIGHT = 4;
    static final int OFF_PATH_ROTATE = 5;

    // mode and type have same numbering scheme
    public static final int PERPENDICULAR = MotionKeyPosition.TYPE_PATH;
    public static final int CARTESIAN = MotionKeyPosition.TYPE_CARTESIAN;
    public static final int SCREEN = MotionKeyPosition.TYPE_SCREEN;
    static String[] sNames = {"position", "x", "y", "width", "height", "pathRotate"};
    public String mId;
    Easing mKeyFrameEasing;
    int mDrawPath = 0;
    float mTime;
    float mPosition;
    float mX;
    float mY;
    float mWidth;
    float mHeight;
    float mPathRotate = Float.NaN;
    float mProgress = Float.NaN;
    int mPathMotionArc = UNSET;
    String mAnimateRelativeTo = null;
    float mRelativeAngle = Float.NaN;
    Motion mRelativeToController = null;

    HashMap<String, CustomVariable> mCustomAttributes = new HashMap<>();
    int mMode = 0; // how was this point computed 1=perpendicular 2=deltaRelative
    int mAnimateCircleAngleTo; // since angles loop there are 4 ways we can pic direction

    public MotionPaths() {
    }

    /**
     * set up with Cartesian
     */
    void initCartesian(MotionKeyPosition c, MotionPaths startTimePoint, MotionPaths endTimePoint) {
        float position = c.mFramePosition / 100f;
        MotionPaths point = this;
        point.mTime = position;

        mDrawPath = c.mDrawPath;
        float scaleWidth = Float.isNaN(c.mPercentWidth) ? position : c.mPercentWidth;
        float scaleHeight = Float.isNaN(c.mPercentHeight) ? position : c.mPercentHeight;
        float scaleX = endTimePoint.mWidth - startTimePoint.mWidth;
        float scaleY = endTimePoint.mHeight - startTimePoint.mHeight;

        point.mPosition = point.mTime;

        float path = position; // the position on the path

        float startCenterX = startTimePoint.mX + startTimePoint.mWidth / 2;
        float startCenterY = startTimePoint.mY + startTimePoint.mHeight / 2;
        float endCenterX = endTimePoint.mX + endTimePoint.mWidth / 2;
        float endCenterY = endTimePoint.mY + endTimePoint.mHeight / 2;
        float pathVectorX = endCenterX - startCenterX;
        float pathVectorY = endCenterY - startCenterY;
        point.mX = (int) (startTimePoint.mX + pathVectorX * path - scaleX * scaleWidth / 2);
        point.mY = (int) (startTimePoint.mY + pathVectorY * path - scaleY * scaleHeight / 2);
        point.mWidth = (int) (startTimePoint.mWidth + scaleX * scaleWidth);
        point.mHeight = (int) (startTimePoint.mHeight + scaleY * scaleHeight);

        float dxdx = Float.isNaN(c.mPercentX) ? position : c.mPercentX;
        float dydx = Float.isNaN(c.mAltPercentY) ? 0 : c.mAltPercentY;
        float dydy = Float.isNaN(c.mPercentY) ? position : c.mPercentY;
        float dxdy = Float.isNaN(c.mAltPercentX) ? 0 : c.mAltPercentX;
        point.mMode = MotionPaths.CARTESIAN;
        point.mX = (int) (startTimePoint.mX + pathVectorX * dxdx
                + pathVectorY * dxdy - scaleX * scaleWidth / 2);
        point.mY = (int) (startTimePoint.mY + pathVectorX * dydx
                + pathVectorY * dydy - scaleY * scaleHeight / 2);

        point.mKeyFrameEasing = Easing.getInterpolator(c.mTransitionEasing);
        point.mPathMotionArc = c.mPathMotionArc;
    }

    /**
     * takes the new keyPosition
     */
    public MotionPaths(int parentWidth,
            int parentHeight,
            MotionKeyPosition c,
            MotionPaths startTimePoint,
            MotionPaths endTimePoint) {
        if (startTimePoint.mAnimateRelativeTo != null) {
            initPolar(parentWidth, parentHeight, c, startTimePoint, endTimePoint);
            return;
        }
        switch (c.mPositionType) {
            case MotionKeyPosition.TYPE_SCREEN:
                initScreen(parentWidth, parentHeight, c, startTimePoint, endTimePoint);
                return;
            case MotionKeyPosition.TYPE_PATH:
                initPath(c, startTimePoint, endTimePoint);
                return;
            default:
            case MotionKeyPosition.TYPE_CARTESIAN:
                initCartesian(c, startTimePoint, endTimePoint);
                return;
        }
    }

    void initPolar(int parentWidth,
            int parentHeight,
            MotionKeyPosition c,
            MotionPaths s,
            MotionPaths e) {
        float position = c.mFramePosition / 100f;
        this.mTime = position;
        mDrawPath = c.mDrawPath;
        this.mMode = c.mPositionType; // mode and type have same numbering scheme
        float scaleWidth = Float.isNaN(c.mPercentWidth) ? position : c.mPercentWidth;
        float scaleHeight = Float.isNaN(c.mPercentHeight) ? position : c.mPercentHeight;
        float scaleX = e.mWidth - s.mWidth;
        float scaleY = e.mHeight - s.mHeight;
        this.mPosition = this.mTime;
        mWidth = (int) (s.mWidth + scaleX * scaleWidth);
        mHeight = (int) (s.mHeight + scaleY * scaleHeight);
        @SuppressWarnings("unused") float startfactor = 1 - position;
        @SuppressWarnings("unused") float endfactor = position;

        switch (c.mPositionType) {
            case MotionKeyPosition.TYPE_SCREEN:
                this.mX = Float.isNaN(c.mPercentX) ? (position * (e.mX - s.mX) + s.mX)
                        : c.mPercentX * Math.min(scaleHeight, scaleWidth);
                this.mY = Float.isNaN(c.mPercentY)
                        ? (position * (e.mY - s.mY) + s.mY) : c.mPercentY;
                break;

            case MotionKeyPosition.TYPE_PATH:
                this.mX = (Float.isNaN(c.mPercentX)
                        ? position : c.mPercentX) * (e.mX - s.mX) + s.mX;
                this.mY = (Float.isNaN(c.mPercentY)
                        ? position : c.mPercentY) * (e.mY - s.mY) + s.mY;
                break;
            default:
            case MotionKeyPosition.TYPE_CARTESIAN:
                this.mX = (Float.isNaN(c.mPercentX)
                        ? position : c.mPercentX) * (e.mX - s.mX) + s.mX;
                this.mY = (Float.isNaN(c.mPercentY)
                        ? position : c.mPercentY) * (e.mY - s.mY) + s.mY;
                break;
        }

        this.mAnimateRelativeTo = s.mAnimateRelativeTo;
        this.mKeyFrameEasing = Easing.getInterpolator(c.mTransitionEasing);
        this.mPathMotionArc = c.mPathMotionArc;
    }

    // @TODO: add description
    public void setupRelative(Motion mc, MotionPaths relative) {
        double dx = mX + mWidth / 2 - relative.mX - relative.mWidth / 2;
        double dy = mY + mHeight / 2 - relative.mY - relative.mHeight / 2;
        mRelativeToController = mc;

        mX = (float) Math.hypot(dy, dx);
        if (Float.isNaN(mRelativeAngle)) {
            mY = (float) (Math.atan2(dy, dx) + Math.PI / 2);
        } else {
            mY = (float) Math.toRadians(mRelativeAngle);
        }
    }

    void initScreen(int parentWidth,
            int parentHeight,
            MotionKeyPosition c,
            MotionPaths startTimePoint,
            MotionPaths endTimePoint) {
        float position = c.mFramePosition / 100f;
        MotionPaths point = this;
        point.mTime = position;

        mDrawPath = c.mDrawPath;
        float scaleWidth = Float.isNaN(c.mPercentWidth) ? position : c.mPercentWidth;
        float scaleHeight = Float.isNaN(c.mPercentHeight) ? position : c.mPercentHeight;

        float scaleX = endTimePoint.mWidth - startTimePoint.mWidth;
        float scaleY = endTimePoint.mHeight - startTimePoint.mHeight;

        point.mPosition = point.mTime;

        float path = position; // the position on the path

        float startCenterX = startTimePoint.mX + startTimePoint.mWidth / 2;
        float startCenterY = startTimePoint.mY + startTimePoint.mHeight / 2;
        float endCenterX = endTimePoint.mX + endTimePoint.mWidth / 2;
        float endCenterY = endTimePoint.mY + endTimePoint.mHeight / 2;
        float pathVectorX = endCenterX - startCenterX;
        float pathVectorY = endCenterY - startCenterY;
        point.mX = (int) (startTimePoint.mX + pathVectorX * path - scaleX * scaleWidth / 2);
        point.mY = (int) (startTimePoint.mY + pathVectorY * path - scaleY * scaleHeight / 2);
        point.mWidth = (int) (startTimePoint.mWidth + scaleX * scaleWidth);
        point.mHeight = (int) (startTimePoint.mHeight + scaleY * scaleHeight);

        point.mMode = MotionPaths.SCREEN;
        if (!Float.isNaN(c.mPercentX)) {
            parentWidth -= (int) point.mWidth;
            point.mX = (int) (c.mPercentX * parentWidth);
        }
        if (!Float.isNaN(c.mPercentY)) {
            parentHeight -= (int) point.mHeight;
            point.mY = (int) (c.mPercentY * parentHeight);
        }

        point.mAnimateRelativeTo = mAnimateRelativeTo;
        point.mKeyFrameEasing = Easing.getInterpolator(c.mTransitionEasing);
        point.mPathMotionArc = c.mPathMotionArc;
    }

    void initPath(MotionKeyPosition c, MotionPaths startTimePoint, MotionPaths endTimePoint) {

        float position = c.mFramePosition / 100f;
        MotionPaths point = this;
        point.mTime = position;

        mDrawPath = c.mDrawPath;
        float scaleWidth = Float.isNaN(c.mPercentWidth) ? position : c.mPercentWidth;
        float scaleHeight = Float.isNaN(c.mPercentHeight) ? position : c.mPercentHeight;

        float scaleX = endTimePoint.mWidth - startTimePoint.mWidth;
        float scaleY = endTimePoint.mHeight - startTimePoint.mHeight;

        point.mPosition = point.mTime;

        float path = Float.isNaN(c.mPercentX) ? position : c.mPercentX; // the position on the path

        float startCenterX = startTimePoint.mX + startTimePoint.mWidth / 2;
        float startCenterY = startTimePoint.mY + startTimePoint.mHeight / 2;
        float endCenterX = endTimePoint.mX + endTimePoint.mWidth / 2;
        float endCenterY = endTimePoint.mY + endTimePoint.mHeight / 2;
        float pathVectorX = endCenterX - startCenterX;
        float pathVectorY = endCenterY - startCenterY;
        point.mX = (int) (startTimePoint.mX + pathVectorX * path - scaleX * scaleWidth / 2);
        point.mY = (int) (startTimePoint.mY + pathVectorY * path - scaleY * scaleHeight / 2);
        point.mWidth = (int) (startTimePoint.mWidth + scaleX * scaleWidth);
        point.mHeight = (int) (startTimePoint.mHeight + scaleY * scaleHeight);
        float perpendicular = Float.isNaN(c.mPercentY)
                ? 0 : c.mPercentY; // the position on the path
        float perpendicularX = -pathVectorY;
        float perpendicularY = pathVectorX;

        float normalX = perpendicularX * perpendicular;
        float normalY = perpendicularY * perpendicular;
        point.mMode = MotionPaths.PERPENDICULAR;
        point.mX = (int) (startTimePoint.mX + pathVectorX * path - scaleX * scaleWidth / 2);
        point.mY = (int) (startTimePoint.mY + pathVectorY * path - scaleY * scaleHeight / 2);
        point.mX += normalX;
        point.mY += normalY;

        point.mAnimateRelativeTo = mAnimateRelativeTo;
        point.mKeyFrameEasing = Easing.getInterpolator(c.mTransitionEasing);
        point.mPathMotionArc = c.mPathMotionArc;
    }

    private static float xRotate(float sin, float cos, float cx, float cy, float x, float y) {
        x = x - cx;
        y = y - cy;
        return x * cos - y * sin + cx;
    }

    private static float yRotate(float sin, float cos, float cx, float cy, float x, float y) {
        x = x - cx;
        y = y - cy;
        return x * sin + y * cos + cy;
    }

    private boolean diff(float a, float b) {
        if (Float.isNaN(a) || Float.isNaN(b)) {
            return Float.isNaN(a) != Float.isNaN(b);
        }
        return Math.abs(a - b) > 0.000001f;
    }

    void different(MotionPaths points, boolean[] mask, String[] custom, boolean arcMode) {
        int c = 0;
        boolean diffx = diff(mX, points.mX);
        boolean diffy = diff(mY, points.mY);
        mask[c++] |= diff(mPosition, points.mPosition);
        mask[c++] |= diffx || diffy || arcMode;
        mask[c++] |= diffx || diffy || arcMode;
        mask[c++] |= diff(mWidth, points.mWidth);
        mask[c++] |= diff(mHeight, points.mHeight);

    }

    void getCenter(double p, int[] toUse, double[] data, float[] point, int offset) {
        float v_x = mX;
        float v_y = mY;
        float v_width = mWidth;
        float v_height = mHeight;
        float translationX = 0, translationY = 0;
        for (int i = 0; i < toUse.length; i++) {
            float value = (float) data[i];

            switch (toUse[i]) {
                case OFF_X:
                    v_x = value;
                    break;
                case OFF_Y:
                    v_y = value;
                    break;
                case OFF_WIDTH:
                    v_width = value;
                    break;
                case OFF_HEIGHT:
                    v_height = value;
                    break;
            }
        }
        if (mRelativeToController != null) {
            float[] pos = new float[2];
            float[] vel = new float[2];

            mRelativeToController.getCenter(p, pos, vel);
            float rx = pos[0];
            float ry = pos[1];
            float radius = v_x;
            float angle = v_y;
            // TODO Debug angle
            v_x = (float) (rx + radius * Math.sin(angle) - v_width / 2);
            v_y = (float) (ry - radius * Math.cos(angle) - v_height / 2);
        }

        point[offset] = v_x + v_width / 2 + translationX;
        point[offset + 1] = v_y + v_height / 2 + translationY;
    }

    void getCenter(double p,
            int[] toUse,
            double[] data,
            float[] point,
            double[] vdata,
            float[] velocity) {
        float v_x = mX;
        float v_y = mY;
        float v_width = mWidth;
        float v_height = mHeight;
        float dv_x = 0;
        float dv_y = 0;
        float dv_width = 0;
        float dv_height = 0;

        float translationX = 0, translationY = 0;
        for (int i = 0; i < toUse.length; i++) {
            float value = (float) data[i];
            float dvalue = (float) vdata[i];

            switch (toUse[i]) {
                case OFF_X:
                    v_x = value;
                    dv_x = dvalue;
                    break;
                case OFF_Y:
                    v_y = value;
                    dv_y = dvalue;
                    break;
                case OFF_WIDTH:
                    v_width = value;
                    dv_width = dvalue;
                    break;
                case OFF_HEIGHT:
                    v_height = value;
                    dv_height = dvalue;
                    break;
            }
        }
        float dpos_x = dv_x + dv_width / 2;
        float dpos_y = dv_y + dv_height / 2;

        if (mRelativeToController != null) {
            float[] pos = new float[2];
            float[] vel = new float[2];
            mRelativeToController.getCenter(p, pos, vel);
            float rx = pos[0];
            float ry = pos[1];
            float radius = v_x;
            float angle = v_y;
            float dradius = dv_x;
            float dangle = dv_y;
            float drx = vel[0];
            float dry = vel[1];
            // TODO Debug angle
            v_x = (float) (rx + radius * Math.sin(angle) - v_width / 2);
            v_y = (float) (ry - radius * Math.cos(angle) - v_height / 2);
            dpos_x = (float) (drx + dradius * Math.sin(angle) + Math.cos(angle) * dangle);
            dpos_y = (float) (dry - dradius * Math.cos(angle) + Math.sin(angle) * dangle);
        }

        point[0] = v_x + v_width / 2 + translationX;
        point[1] = v_y + v_height / 2 + translationY;
        velocity[0] = dpos_x;
        velocity[1] = dpos_y;
    }

    void getCenterVelocity(double p, int[] toUse, double[] data, float[] point, int offset) {
        float v_x = mX;
        float v_y = mY;
        float v_width = mWidth;
        float v_height = mHeight;
        float translationX = 0, translationY = 0;
        for (int i = 0; i < toUse.length; i++) {
            float value = (float) data[i];

            switch (toUse[i]) {
                case OFF_X:
                    v_x = value;
                    break;
                case OFF_Y:
                    v_y = value;
                    break;
                case OFF_WIDTH:
                    v_width = value;
                    break;
                case OFF_HEIGHT:
                    v_height = value;
                    break;
            }
        }
        if (mRelativeToController != null) {
            float[] pos = new float[2];
            float[] vel = new float[2];
            mRelativeToController.getCenter(p, pos, vel);
            float rx = pos[0];
            float ry = pos[1];
            float radius = v_x;
            float angle = v_y;
            // TODO Debug angle
            v_x = (float) (rx + radius * Math.sin(angle) - v_width / 2);
            v_y = (float) (ry - radius * Math.cos(angle) - v_height / 2);
        }

        point[offset] = v_x + v_width / 2 + translationX;
        point[offset + 1] = v_y + v_height / 2 + translationY;
    }

    void getBounds(int[] toUse, double[] data, float[] point, int offset) {
        @SuppressWarnings("unused") float v_x = mX;
        @SuppressWarnings("unused") float v_y = mY;
        float v_width = mWidth;
        float v_height = mHeight;
        for (int i = 0; i < toUse.length; i++) {
            float value = (float) data[i];

            switch (toUse[i]) {
                case OFF_X:
                    v_x = value;
                    break;
                case OFF_Y:
                    v_y = value;
                    break;
                case OFF_WIDTH:
                    v_width = value;
                    break;
                case OFF_HEIGHT:
                    v_height = value;
                    break;
            }
        }
        point[offset] = v_width;
        point[offset + 1] = v_height;
    }

    double[] mTempValue = new double[18];
    double[] mTempDelta = new double[18];

    // Called on the start Time Point
    void setView(float position,
            MotionWidget view,
            int[] toUse,
            double[] data,
            double[] slope,
            double[] cycle) {
        float v_x = mX;
        float v_y = mY;
        float v_width = mWidth;
        float v_height = mHeight;
        float dv_x = 0;
        float dv_y = 0;
        float dv_width = 0;
        float dv_height = 0;
        @SuppressWarnings("unused") float delta_path = 0;
        float path_rotate = Float.NaN;
        @SuppressWarnings("unused") String mod;

        if (toUse.length != 0 && mTempValue.length <= toUse[toUse.length - 1]) {
            int scratch_data_length = toUse[toUse.length - 1] + 1;
            mTempValue = new double[scratch_data_length];
            mTempDelta = new double[scratch_data_length];
        }
        Arrays.fill(mTempValue, Double.NaN);
        for (int i = 0; i < toUse.length; i++) {
            mTempValue[toUse[i]] = data[i];
            mTempDelta[toUse[i]] = slope[i];
        }

        for (int i = 0; i < mTempValue.length; i++) {
            if (Double.isNaN(mTempValue[i]) && (cycle == null || cycle[i] == 0.0)) {
                continue;
            }
            double deltaCycle = (cycle != null) ? cycle[i] : 0.0;
            float value = (float) (Double.isNaN(mTempValue[i])
                    ? deltaCycle : mTempValue[i] + deltaCycle);
            float dvalue = (float) mTempDelta[i];

            switch (i) {
                case OFF_POSITION:
                    delta_path = value;
                    break;
                case OFF_X:
                    v_x = value;
                    dv_x = dvalue;

                    break;
                case OFF_Y:
                    v_y = value;
                    dv_y = dvalue;
                    break;
                case OFF_WIDTH:
                    v_width = value;
                    dv_width = dvalue;
                    break;
                case OFF_HEIGHT:
                    v_height = value;
                    dv_height = dvalue;
                    break;
                case OFF_PATH_ROTATE:
                    path_rotate = value;
                    break;
            }
        }

        if (mRelativeToController != null) {
            float[] pos = new float[2];
            float[] vel = new float[2];

            mRelativeToController.getCenter(position, pos, vel);
            float rx = pos[0];
            float ry = pos[1];
            float radius = v_x;
            float angle = v_y;
            float dradius = dv_x;
            float dangle = dv_y;
            float drx = vel[0];
            float dry = vel[1];

            // TODO Debug angle
            float pos_x = (float) (rx + radius * Math.sin(angle) - v_width / 2);
            float pos_y = (float) (ry - radius * Math.cos(angle) - v_height / 2);
            float dpos_x = (float) (drx + dradius * Math.sin(angle)
                    + radius * Math.cos(angle) * dangle);
            float dpos_y = (float) (dry - dradius * Math.cos(angle)
                    + radius * Math.sin(angle) * dangle);
            dv_x = dpos_x;
            dv_y = dpos_y;
            v_x = pos_x;
            v_y = pos_y;
            if (slope.length >= 2) {
                slope[0] = dpos_x;
                slope[1] = dpos_y;
            }
            if (!Float.isNaN(path_rotate)) {
                float rot = (float) (path_rotate + Math.toDegrees(Math.atan2(dv_y, dv_x)));
                view.setRotationZ(rot);
            }

        } else {

            if (!Float.isNaN(path_rotate)) {
                float rot = 0;
                float dx = dv_x + dv_width / 2;
                float dy = dv_y + dv_height / 2;
                if (DEBUG) {
                    Utils.log(TAG, "dv_x       =" + dv_x);
                    Utils.log(TAG, "dv_y       =" + dv_y);
                    Utils.log(TAG, "dv_width   =" + dv_width);
                    Utils.log(TAG, "dv_height  =" + dv_height);
                }
                rot += (float) (path_rotate + Math.toDegrees(Math.atan2(dy, dx)));
                view.setRotationZ(rot);
                if (DEBUG) {
                    Utils.log(TAG, "Rotated " + rot + "  = " + dx + "," + dy);
                }
            }
        }

        // Todo: develop a concept of Float layout in MotionWidget widget.layout(float ...)
        int l = (int) (0.5f + v_x);
        int t = (int) (0.5f + v_y);
        int r = (int) (0.5f + v_x + v_width);
        int b = (int) (0.5f + v_y + v_height);
        int i_width = r - l;
        int i_height = b - t;
        if (OLD_WAY) { // This way may produce more stable with and height but risk gaps
            l = (int) v_x;
            t = (int) v_y;
            i_width = (int) v_width;
            i_height = (int) v_height;
            r = l + i_width;
            b = t + i_height;
        }

        // MotionWidget must do Android View measure if layout changes
        view.layout(l, t, r, b);
        if (DEBUG) {
            if (toUse.length > 0) {
                Utils.log(TAG, "setView " + mod);
            }
        }
    }

    void getRect(int[] toUse, double[] data, float[] path, int offset) {
        float v_x = mX;
        float v_y = mY;
        float v_width = mWidth;
        float v_height = mHeight;
        @SuppressWarnings("unused") float delta_path = 0;
        float rotation = 0;
        @SuppressWarnings("unused") float alpha = 0;
        @SuppressWarnings("unused") float rotationX = 0;
        @SuppressWarnings("unused") float rotationY = 0;
        float scaleX = 1;
        float scaleY = 1;
        float pivotX = Float.NaN;
        float pivotY = Float.NaN;
        float translationX = 0;
        float translationY = 0;

        @SuppressWarnings("unused") String mod;

        for (int i = 0; i < toUse.length; i++) {
            float value = (float) data[i];

            switch (toUse[i]) {
                case OFF_POSITION:
                    delta_path = value;
                    break;
                case OFF_X:
                    v_x = value;
                    break;
                case OFF_Y:
                    v_y = value;
                    break;
                case OFF_WIDTH:
                    v_width = value;
                    break;
                case OFF_HEIGHT:
                    v_height = value;
                    break;
            }
        }

        if (mRelativeToController != null) {
            float rx = mRelativeToController.getCenterX();
            float ry = mRelativeToController.getCenterY();
            float radius = v_x;
            float angle = v_y;
            // TODO Debug angle
            v_x = (float) (rx + radius * Math.sin(angle) - v_width / 2);
            v_y = (float) (ry - radius * Math.cos(angle) - v_height / 2);
        }

        float x1 = v_x;
        float y1 = v_y;

        float x2 = v_x + v_width;
        float y2 = y1;

        float x3 = x2;
        float y3 = v_y + v_height;

        float x4 = x1;
        float y4 = y3;

        float cx = x1 + v_width / 2;
        float cy = y1 + v_height / 2;

        if (!Float.isNaN(pivotX)) {
            cx = x1 + (x2 - x1) * pivotX;
        }
        if (!Float.isNaN(pivotY)) {

            cy = y1 + (y3 - y1) * pivotY;
        }
        if (scaleX != 1) {
            float midx = (x1 + x2) / 2;
            x1 = (x1 - midx) * scaleX + midx;
            x2 = (x2 - midx) * scaleX + midx;
            x3 = (x3 - midx) * scaleX + midx;
            x4 = (x4 - midx) * scaleX + midx;
        }
        if (scaleY != 1) {
            float midy = (y1 + y3) / 2;
            y1 = (y1 - midy) * scaleY + midy;
            y2 = (y2 - midy) * scaleY + midy;
            y3 = (y3 - midy) * scaleY + midy;
            y4 = (y4 - midy) * scaleY + midy;
        }
        if (rotation != 0) {
            float sin = (float) Math.sin(Math.toRadians(rotation));
            float cos = (float) Math.cos(Math.toRadians(rotation));
            float tx1 = xRotate(sin, cos, cx, cy, x1, y1);
            float ty1 = yRotate(sin, cos, cx, cy, x1, y1);
            float tx2 = xRotate(sin, cos, cx, cy, x2, y2);
            float ty2 = yRotate(sin, cos, cx, cy, x2, y2);
            float tx3 = xRotate(sin, cos, cx, cy, x3, y3);
            float ty3 = yRotate(sin, cos, cx, cy, x3, y3);
            float tx4 = xRotate(sin, cos, cx, cy, x4, y4);
            float ty4 = yRotate(sin, cos, cx, cy, x4, y4);
            x1 = tx1;
            y1 = ty1;
            x2 = tx2;
            y2 = ty2;
            x3 = tx3;
            y3 = ty3;
            x4 = tx4;
            y4 = ty4;
        }

        x1 += translationX;
        y1 += translationY;
        x2 += translationX;
        y2 += translationY;
        x3 += translationX;
        y3 += translationY;
        x4 += translationX;
        y4 += translationY;

        path[offset++] = x1;
        path[offset++] = y1;
        path[offset++] = x2;
        path[offset++] = y2;
        path[offset++] = x3;
        path[offset++] = y3;
        path[offset++] = x4;
        path[offset++] = y4;
    }

    /**
     * mAnchorDpDt
     */
    void setDpDt(float locationX,
            float locationY,
            float[] mAnchorDpDt,
            int[] toUse,
            double[] deltaData,
            double[] data) {

        float d_x = 0;
        float d_y = 0;
        float d_width = 0;
        float d_height = 0;

        float deltaScaleX = 0;
        float deltaScaleY = 0;

        @SuppressWarnings("unused") float mPathRotate = Float.NaN;
        float deltaTranslationX = 0;
        float deltaTranslationY = 0;

        String mod = " dd = ";
        for (int i = 0; i < toUse.length; i++) {
            float deltaV = (float) deltaData[i];
            if (DEBUG) {
                mod += " , D" + sNames[toUse[i]] + "/Dt= " + deltaV;
            }
            switch (toUse[i]) {
                case OFF_POSITION:
                    break;
                case OFF_X:
                    d_x = deltaV;
                    break;
                case OFF_Y:
                    d_y = deltaV;
                    break;
                case OFF_WIDTH:
                    d_width = deltaV;
                    break;
                case OFF_HEIGHT:
                    d_height = deltaV;
                    break;

            }
        }
        if (DEBUG) {
            if (toUse.length > 0) {
                Utils.log(TAG, "setDpDt " + mod);
            }
        }

        float deltaX = d_x - deltaScaleX * d_width / 2;
        float deltaY = d_y - deltaScaleY * d_height / 2;
        float deltaWidth = d_width * (1 + deltaScaleX);
        float deltaHeight = d_height * (1 + deltaScaleY);
        float deltaRight = deltaX + deltaWidth;
        float deltaBottom = deltaY + deltaHeight;
        if (DEBUG) {
            if (toUse.length > 0) {

                Utils.log(TAG, "D x /dt           =" + d_x);
                Utils.log(TAG, "D y /dt           =" + d_y);
                Utils.log(TAG, "D width /dt       =" + d_width);
                Utils.log(TAG, "D height /dt      =" + d_height);
                Utils.log(TAG, "D deltaScaleX /dt =" + deltaScaleX);
                Utils.log(TAG, "D deltaScaleY /dt =" + deltaScaleY);
                Utils.log(TAG, "D deltaX /dt      =" + deltaX);
                Utils.log(TAG, "D deltaY /dt      =" + deltaY);
                Utils.log(TAG, "D deltaWidth /dt  =" + deltaWidth);
                Utils.log(TAG, "D deltaHeight /dt =" + deltaHeight);
                Utils.log(TAG, "D deltaRight /dt  =" + deltaRight);
                Utils.log(TAG, "D deltaBottom /dt =" + deltaBottom);
                Utils.log(TAG, "locationX         =" + locationX);
                Utils.log(TAG, "locationY         =" + locationY);
                Utils.log(TAG, "deltaTranslationX =" + deltaTranslationX);
                Utils.log(TAG, "deltaTranslationX =" + deltaTranslationX);
            }
        }

        mAnchorDpDt[0] = deltaX * (1 - locationX) + deltaRight * locationX + deltaTranslationX;
        mAnchorDpDt[1] = deltaY * (1 - locationY) + deltaBottom * locationY + deltaTranslationY;
    }

    void fillStandard(double[] data, int[] toUse) {
        float[] set = {mPosition, mX, mY, mWidth, mHeight, mPathRotate};
        int c = 0;
        for (int i = 0; i < toUse.length; i++) {
            if (toUse[i] < set.length) {
                data[c++] = set[toUse[i]];
            }
        }
    }

    boolean hasCustomData(String name) {
        return mCustomAttributes.containsKey(name);
    }

    int getCustomDataCount(String name) {
        CustomVariable a = mCustomAttributes.get(name);
        if (a == null) {
            return 0;
        }
        return a.numberOfInterpolatedValues();
    }

    int getCustomData(String name, double[] value, int offset) {
        CustomVariable a = mCustomAttributes.get(name);
        if (a == null) {
            return 0;
        } else if (a.numberOfInterpolatedValues() == 1) {
            value[offset] = a.getValueToInterpolate();
            return 1;
        } else {
            int n = a.numberOfInterpolatedValues();
            float[] f = new float[n];
            a.getValuesToInterpolate(f);
            for (int i = 0; i < n; i++) {
                value[offset++] = f[i];
            }
            return n;
        }
    }

    void setBounds(float x, float y, float w, float h) {
        this.mX = x;
        this.mY = y;
        mWidth = w;
        mHeight = h;
    }

    @Override
    public int compareTo(MotionPaths o) {
        return Float.compare(mPosition, o.mPosition);
    }

    // @TODO: add description
    public void applyParameters(MotionWidget c) {
        MotionPaths point = this;
        point.mKeyFrameEasing = Easing.getInterpolator(c.mMotion.mTransitionEasing);
        point.mPathMotionArc = c.mMotion.mPathMotionArc;
        point.mAnimateRelativeTo = c.mMotion.mAnimateRelativeTo;
        point.mPathRotate = c.mMotion.mPathRotate;
        point.mDrawPath = c.mMotion.mDrawPath;
        point.mAnimateCircleAngleTo = c.mMotion.mAnimateCircleAngleTo;
        point.mProgress = c.mPropertySet.mProgress;
        if (c.mWidgetFrame != null && c.mWidgetFrame.widget != null) {
            point.mRelativeAngle = c.mWidgetFrame.widget.mCircleConstraintAngle;
        }

        Set<String> at = c.getCustomAttributeNames();
        for (String s : at) {
            CustomVariable attr = c.getCustomAttribute(s);
            if (attr != null && attr.isContinuous()) {
                this.mCustomAttributes.put(s, attr);
            }
        }
    }

    // @TODO: add description
    public void configureRelativeTo(Motion toOrbit) {
        @SuppressWarnings("unused") double[] p = toOrbit.getPos(mProgress); // get the position
        // in the orbit
    }
}