1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package androidx.constraintlayout.core.motion.utils;
17 
18 import java.util.Arrays;
19 
20 /**
21  * This generates variable frequency oscillation curves
22  *
23  *
24  */
25 public class Oscillator {
26     public static String TAG = "Oscillator";
27     float[] mPeriod = {};
28     double[] mPosition = {};
29     double[] mArea;
30     public static final int SIN_WAVE = 0; // theses must line up with attributes
31     public static final int SQUARE_WAVE = 1;
32     public static final int TRIANGLE_WAVE = 2;
33     public static final int SAW_WAVE = 3;
34     public static final int REVERSE_SAW_WAVE = 4;
35     public static final int COS_WAVE = 5;
36     public static final int BOUNCE = 6;
37     public static final int CUSTOM = 7;
38     String mCustomType;
39     MonotonicCurveFit mCustomCurve;
40     int mType;
41     double mPI2 = Math.PI * 2;
42     @SuppressWarnings("unused") private boolean mNormalized = false;
43 
Oscillator()44     public Oscillator() {
45     }
46 
47     @Override
toString()48     public String toString() {
49         return "pos =" + Arrays.toString(mPosition) + " period=" + Arrays.toString(mPeriod);
50     }
51 
52     // @TODO: add description
setType(int type, String customType)53     public void setType(int type, String customType) {
54         mType = type;
55         mCustomType = customType;
56         if (mCustomType != null) {
57             mCustomCurve = MonotonicCurveFit.buildWave(customType);
58         }
59     }
60 
61     // @TODO: add description
addPoint(double position, float period)62     public void addPoint(double position, float period) {
63         int len = mPeriod.length + 1;
64         int j = Arrays.binarySearch(mPosition, position);
65         if (j < 0) {
66             j = -j - 1;
67         }
68         mPosition = Arrays.copyOf(mPosition, len);
69         mPeriod = Arrays.copyOf(mPeriod, len);
70         mArea = new double[len];
71         System.arraycopy(mPosition, j, mPosition, j + 1, len - j - 1);
72         mPosition[j] = position;
73         mPeriod[j] = period;
74         mNormalized = false;
75     }
76 
77     /**
78      * After adding point every thing must be normalized
79      */
normalize()80     public void normalize() {
81         double totalArea = 0;
82         double totalCount = 0;
83         for (int i = 0; i < mPeriod.length; i++) {
84             totalCount += mPeriod[i];
85         }
86         for (int i = 1; i < mPeriod.length; i++) {
87             float h = (mPeriod[i - 1] + mPeriod[i]) / 2;
88             double w = mPosition[i] - mPosition[i - 1];
89             totalArea = totalArea + w * h;
90         }
91         // scale periods to normalize it
92         for (int i = 0; i < mPeriod.length; i++) {
93             mPeriod[i] *= (float) (totalCount / totalArea);
94         }
95         mArea[0] = 0;
96         for (int i = 1; i < mPeriod.length; i++) {
97             float h = (mPeriod[i - 1] + mPeriod[i]) / 2;
98             double w = mPosition[i] - mPosition[i - 1];
99             mArea[i] = mArea[i - 1] + w * h;
100         }
101         mNormalized = true;
102     }
103 
getP(double time)104     double getP(double time) {
105         if (time <= 0.0) {
106             return 0.0;
107         } else if (time >= 1.0) {
108             return 1.0;
109         }
110         // At this point, `index` is guaranteed to be != 0 for any time value (assuming mPosition
111         // includes 0.0)
112         int index = Arrays.binarySearch(mPosition, time);
113         if (index < 0) {
114             index = -index - 1;
115         }
116 
117         double m = (mPeriod[index] - mPeriod[index - 1])
118                 / (mPosition[index] - mPosition[index - 1]);
119         return mArea[index - 1]
120                 + (mPeriod[index - 1] - m * mPosition[index - 1]) * (time - mPosition[index - 1])
121                 + m * (time * time - mPosition[index - 1] * mPosition[index - 1]) / 2;
122     }
123 
124     // @TODO: add description
getValue(double time, double phase)125     public double getValue(double time, double phase) {
126         double angle = phase + getP(time); // angle is / by 360
127         switch (mType) {
128             default:
129             case SIN_WAVE:
130                 return Math.sin(mPI2 * angle);
131             case SQUARE_WAVE:
132                 return Math.signum(0.5 - angle % 1);
133             case TRIANGLE_WAVE:
134                 return 1 - Math.abs((angle * 4 + 1) % 4 - 2);
135             case SAW_WAVE:
136                 return ((angle * 2 + 1) % 2) - 1;
137             case REVERSE_SAW_WAVE:
138                 return (1 - ((angle * 2 + 1) % 2));
139             case COS_WAVE:
140                 return Math.cos(mPI2 * (phase + angle));
141             case BOUNCE:
142                 double x = 1 - Math.abs((angle * 4) % 4 - 2);
143                 return 1 - x * x;
144             case CUSTOM:
145                 return mCustomCurve.getPos(angle % 1, 0);
146         }
147     }
148 
getDP(double time)149     double getDP(double time) {
150         if (time <= 0.0) {
151             return 0.0;
152         } else if (time >= 1.0) {
153             return 1.0;
154         }
155         // At this point, `index` is guaranteed to be != 0 for any time value (assuming mPosition
156         // includes 0.0)
157         int index = Arrays.binarySearch(mPosition, time);
158         if (index < 0) {
159             index = -index - 1;
160         }
161         double m = (mPeriod[index] - mPeriod[index - 1])
162                 / (mPosition[index] - mPosition[index - 1]);
163         return m * time + (mPeriod[index - 1] - m * mPosition[index - 1]);
164     }
165 
166     // @TODO: add description
getSlope(double time, double phase, double dphase)167     public double getSlope(double time, double phase, double dphase) {
168         double angle = phase + getP(time);
169 
170         double dangle_dtime = getDP(time) + dphase;
171         switch (mType) {
172             default:
173             case SIN_WAVE:
174                 return mPI2 * dangle_dtime * Math.cos(mPI2 * angle);
175             case SQUARE_WAVE:
176                 return 0;
177             case TRIANGLE_WAVE:
178                 return 4 * dangle_dtime * Math.signum((angle * 4 + 3) % 4 - 2);
179             case SAW_WAVE:
180                 return dangle_dtime * 2;
181             case REVERSE_SAW_WAVE:
182                 return -dangle_dtime * 2;
183             case COS_WAVE:
184                 return -mPI2 * dangle_dtime * Math.sin(mPI2 * angle);
185             case BOUNCE:
186                 return 4 * dangle_dtime * ((angle * 4 + 2) % 4 - 2);
187             case CUSTOM:
188                 return mCustomCurve.getSlope(angle % 1, 0);
189         }
190     }
191 }
192