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 androidx.constraintlayout.core.motion.CustomAttribute;
19 import androidx.constraintlayout.core.motion.CustomVariable;
20 import androidx.constraintlayout.core.motion.MotionWidget;
21 
22 import java.text.DecimalFormat;
23 
24 /**
25  * This engine allows manipulation of attributes by wave shapes oscillating in time
26  *
27  *
28  */
29 public abstract class TimeCycleSplineSet {
30     private static final String TAG = "SplineSet";
31     protected CurveFit mCurveFit;
32     protected int mWaveShape = 0;
33     protected int[] mTimePoints = new int[10];
34     protected float[][] mValues = new float[10][3];
35     protected int mCount;
36     protected String mType;
37     protected float[] mCache = new float[3];
38     protected static final int CURVE_VALUE = 0;
39     protected static final int CURVE_PERIOD = 1;
40     protected static final int CURVE_OFFSET = 2;
41     protected static float sVal2PI = (float) (2 * Math.PI);
42     protected boolean mContinue = false;
43     protected long mLastTime;
44     protected float mLastCycle = Float.NaN;
45 
46     @Override
toString()47     public String toString() {
48         String str = mType;
49         DecimalFormat df = new DecimalFormat("##.##");
50         for (int i = 0; i < mCount; i++) {
51             str += "[" + mTimePoints[i] + " , " + df.format(mValues[i]) + "] ";
52         }
53         return str;
54     }
55 
setType(String type)56     public void setType(String type) {
57         mType = type;
58     }
59 
60     /**
61      * @param period cycles per second
62      */
calcWave(float period)63     protected float calcWave(float period) {
64         float p = period;
65         switch (mWaveShape) {
66             default:
67             case Oscillator.SIN_WAVE:
68                 return (float) Math.sin(p * sVal2PI);
69             case Oscillator.SQUARE_WAVE:
70                 return (float) Math.signum(p * sVal2PI);
71             case Oscillator.TRIANGLE_WAVE:
72                 return 1 - Math.abs(p);
73             case Oscillator.SAW_WAVE:
74                 return ((p * 2 + 1) % 2) - 1;
75             case Oscillator.REVERSE_SAW_WAVE:
76                 return (1 - ((p * 2 + 1) % 2));
77             case Oscillator.COS_WAVE:
78                 return (float) Math.cos(p * sVal2PI);
79             case Oscillator.BOUNCE:
80                 float x = 1 - Math.abs((p * 4) % 4 - 2);
81                 return 1 - x * x;
82         }
83     }
84 
getCurveFit()85     public CurveFit getCurveFit() {
86         return mCurveFit;
87     }
88 
setStartTime(long currentTime)89     protected void setStartTime(long currentTime) {
90         mLastTime = currentTime;
91     }
92 
93     // @TODO: add description
setPoint(int position, float value, float period, int shape, float offset)94     public void setPoint(int position, float value, float period, int shape, float offset) {
95         mTimePoints[mCount] = position;
96         mValues[mCount][CURVE_VALUE] = value;
97         mValues[mCount][CURVE_PERIOD] = period;
98         mValues[mCount][CURVE_OFFSET] = offset;
99         mWaveShape = Math.max(mWaveShape, shape); // the highest value shape is chosen
100         mCount++;
101     }
102 
103     public static class CustomSet extends TimeCycleSplineSet {
104         String mAttributeName;
105         KeyFrameArray.CustomArray mConstraintAttributeList;
106         KeyFrameArray.FloatArray mWaveProperties = new KeyFrameArray.FloatArray();
107         float[] mTempValues;
108         float[] mCustomCache;
109 
CustomSet(String attribute, KeyFrameArray.CustomArray attrList)110         public CustomSet(String attribute, KeyFrameArray.CustomArray attrList) {
111             mAttributeName = attribute.split(",")[1];
112             mConstraintAttributeList = attrList;
113         }
114 
115         // @TODO: add description
116         @Override
setup(int curveType)117         public void setup(int curveType) {
118             int size = mConstraintAttributeList.size();
119             int dimensionality = mConstraintAttributeList.valueAt(0).numberOfInterpolatedValues();
120             double[] time = new double[size];
121             mTempValues = new float[dimensionality + 2];
122             mCustomCache = new float[dimensionality];
123             double[][] values = new double[size][dimensionality + 2];
124             for (int i = 0; i < size; i++) {
125                 int key = mConstraintAttributeList.keyAt(i);
126                 CustomAttribute ca = mConstraintAttributeList.valueAt(i);
127                 float[] waveProp = mWaveProperties.valueAt(i);
128                 time[i] = key * 1E-2;
129                 ca.getValuesToInterpolate(mTempValues);
130                 for (int k = 0; k < mTempValues.length; k++) {
131                     values[i][k] = mTempValues[k];
132                 }
133                 values[i][dimensionality] = waveProp[0];
134                 values[i][dimensionality + 1] = waveProp[1];
135             }
136             mCurveFit = CurveFit.get(curveType, time, values);
137         }
138 
139         // @TODO: add description
140         @Override
setPoint(int position, float value, float period, int shape, float offset)141         public void setPoint(int position, float value, float period, int shape, float offset) {
142             throw new RuntimeException("don't call for custom attribute "
143                     + "call setPoint(pos, ConstraintAttribute,...)");
144         }
145 
146         // @TODO: add description
setPoint(int position, CustomAttribute value, float period, int shape, float offset)147         public void setPoint(int position,
148                 CustomAttribute value,
149                 float period,
150                 int shape,
151                 float offset) {
152             mConstraintAttributeList.append(position, value);
153             mWaveProperties.append(position, new float[]{period, offset});
154             mWaveShape = Math.max(mWaveShape, shape); // the highest value shape is chosen
155         }
156 
157         // @TODO: add description
setProperty(MotionWidget view, float t, long time, KeyCache cache)158         public boolean setProperty(MotionWidget view, float t, long time, KeyCache cache) {
159             mCurveFit.getPos(t, mTempValues);
160             float period = mTempValues[mTempValues.length - 2];
161             float offset = mTempValues[mTempValues.length - 1];
162             long delta_time = time - mLastTime;
163 
164             if (Float.isNaN(mLastCycle)) { // it has not been set
165                 mLastCycle = cache.getFloatValue(view, mAttributeName, 0); // check the cache
166                 if (Float.isNaN(mLastCycle)) {  // not in cache so set to 0 (start)
167                     mLastCycle = 0;
168                 }
169             }
170 
171             mLastCycle = (float) ((mLastCycle + delta_time * 1E-9 * period) % 1.0);
172             mLastTime = time;
173             float wave = calcWave(mLastCycle);
174             mContinue = false;
175             for (int i = 0; i < mCustomCache.length; i++) {
176                 mContinue |= mTempValues[i] != 0.0;
177                 mCustomCache[i] = mTempValues[i] * wave + offset;
178             }
179             view.setInterpolatedValue(mConstraintAttributeList.valueAt(0), mCustomCache);
180             if (period != 0.0f) {
181                 mContinue = true;
182             }
183             return mContinue;
184         }
185     }
186 
187     // @TODO: add description
setup(int curveType)188     public void setup(int curveType) {
189         if (mCount == 0) {
190             System.err.println("Error no points added to " + mType);
191             return;
192         }
193         Sort.doubleQuickSort(mTimePoints, mValues, 0, mCount - 1);
194         int unique = 0;
195         for (int i = 1; i < mTimePoints.length; i++) {
196             if (mTimePoints[i] != mTimePoints[i - 1]) {
197                 unique++;
198             }
199         }
200         if (unique == 0) {
201             unique = 1;
202         }
203         double[] time = new double[unique];
204         double[][] values = new double[unique][3];
205         int k = 0;
206 
207         for (int i = 0; i < mCount; i++) {
208             if (i > 0 && mTimePoints[i] == mTimePoints[i - 1]) {
209                 continue;
210             }
211             time[k] = mTimePoints[i] * 1E-2;
212             values[k][0] = mValues[i][0];
213             values[k][1] = mValues[i][1];
214             values[k][2] = mValues[i][2];
215             k++;
216         }
217         mCurveFit = CurveFit.get(curveType, time, values);
218     }
219 
220     protected static class Sort {
doubleQuickSort(int[] key, float[][] value, int low, int hi)221         static void doubleQuickSort(int[] key, float[][] value, int low, int hi) {
222             int[] stack = new int[key.length + 10];
223             int count = 0;
224             stack[count++] = hi;
225             stack[count++] = low;
226             while (count > 0) {
227                 low = stack[--count];
228                 hi = stack[--count];
229                 if (low < hi) {
230                     int p = partition(key, value, low, hi);
231                     stack[count++] = p - 1;
232                     stack[count++] = low;
233                     stack[count++] = hi;
234                     stack[count++] = p + 1;
235                 }
236             }
237         }
238 
partition(int[] array, float[][] value, int low, int hi)239         private static int partition(int[] array, float[][] value, int low, int hi) {
240             int pivot = array[hi];
241             int i = low;
242             for (int j = low; j < hi; j++) {
243                 if (array[j] <= pivot) {
244                     swap(array, value, i, j);
245                     i++;
246                 }
247             }
248             swap(array, value, i, hi);
249             return i;
250         }
251 
swap(int[] array, float[][] value, int a, int b)252         private static void swap(int[] array, float[][] value, int a, int b) {
253             int tmp = array[a];
254             array[a] = array[b];
255             array[b] = tmp;
256             float[] tmpv = value[a];
257             value[a] = value[b];
258             value[b] = tmpv;
259         }
260     }
261 
262 
263     public static class CustomVarSet extends TimeCycleSplineSet {
264         String mAttributeName;
265         KeyFrameArray.CustomVar mConstraintAttributeList;
266         KeyFrameArray.FloatArray mWaveProperties = new KeyFrameArray.FloatArray();
267         float[] mTempValues;
268         float[] mCustomCache;
269 
CustomVarSet(String attribute, KeyFrameArray.CustomVar attrList)270         public CustomVarSet(String attribute, KeyFrameArray.CustomVar attrList) {
271             mAttributeName = attribute.split(",")[1];
272             mConstraintAttributeList = attrList;
273         }
274 
275         // @TODO: add description
276         @Override
setup(int curveType)277         public void setup(int curveType) {
278             int size = mConstraintAttributeList.size();
279             int dimensionality = mConstraintAttributeList.valueAt(0).numberOfInterpolatedValues();
280             double[] time = new double[size];
281             mTempValues = new float[dimensionality + 2];
282             mCustomCache = new float[dimensionality];
283             double[][] values = new double[size][dimensionality + 2];
284             for (int i = 0; i < size; i++) {
285                 int key = mConstraintAttributeList.keyAt(i);
286                 CustomVariable ca = mConstraintAttributeList.valueAt(i);
287                 float[] waveProp = mWaveProperties.valueAt(i);
288                 time[i] = key * 1E-2;
289                 ca.getValuesToInterpolate(mTempValues);
290                 for (int k = 0; k < mTempValues.length; k++) {
291                     values[i][k] = mTempValues[k];
292                 }
293                 values[i][dimensionality] = waveProp[0];
294                 values[i][dimensionality + 1] = waveProp[1];
295             }
296             mCurveFit = CurveFit.get(curveType, time, values);
297         }
298 
299         // @TODO: add description
300         @Override
setPoint(int position, float value, float period, int shape, float offset)301         public void setPoint(int position, float value, float period, int shape, float offset) {
302             throw new RuntimeException("don't call for custom attribute "
303                     + "call setPoint(pos, ConstraintAttribute,...)");
304         }
305 
306         // @TODO: add description
setPoint(int position, CustomVariable value, float period, int shape, float offset)307         public void setPoint(int position,
308                 CustomVariable value,
309                 float period,
310                 int shape,
311                 float offset) {
312             mConstraintAttributeList.append(position, value);
313             mWaveProperties.append(position, new float[]{period, offset});
314             mWaveShape = Math.max(mWaveShape, shape); // the highest value shape is chosen
315         }
316 
317         // @TODO: add description
setProperty(MotionWidget view, float t, long time, KeyCache cache)318         public boolean setProperty(MotionWidget view, float t, long time, KeyCache cache) {
319             mCurveFit.getPos(t, mTempValues);
320             float period = mTempValues[mTempValues.length - 2];
321             float offset = mTempValues[mTempValues.length - 1];
322             long delta_time = time - mLastTime;
323 
324             if (Float.isNaN(mLastCycle)) { // it has not been set
325                 mLastCycle = cache.getFloatValue(view, mAttributeName, 0); // check the cache
326                 if (Float.isNaN(mLastCycle)) {  // not in cache so set to 0 (start)
327                     mLastCycle = 0;
328                 }
329             }
330 
331             mLastCycle = (float) ((mLastCycle + delta_time * 1E-9 * period) % 1.0);
332             mLastTime = time;
333             float wave = calcWave(mLastCycle);
334             mContinue = false;
335             for (int i = 0; i < mCustomCache.length; i++) {
336                 mContinue |= mTempValues[i] != 0.0;
337                 mCustomCache[i] = mTempValues[i] * wave + offset;
338             }
339             mConstraintAttributeList.valueAt(0).setInterpolatedValue(view, mCustomCache);
340             if (period != 0.0f) {
341                 mContinue = true;
342             }
343             return mContinue;
344         }
345     }
346 }
347