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.MotionWidget;
19 
20 import java.text.DecimalFormat;
21 import java.util.ArrayList;
22 import java.util.Collections;
23 import java.util.Comparator;
24 
25 /**
26  * Provide the engine for executing cycles.
27  * KeyCycleOscillator
28  *
29  *
30  */
31 public abstract class KeyCycleOscillator {
32     private static final String TAG = "KeyCycleOscillator";
33     private CurveFit mCurveFit;
34     private CycleOscillator mCycleOscillator;
35     private String mType;
36     private int mWaveShape = 0;
37     private String mWaveString = null;
38 
39     public int mVariesBy = 0; // 0 = position, 2=path
40     ArrayList<WavePoint> mWavePoints = new ArrayList<>();
41 
42     // @TODO: add description
makeWidgetCycle(String attribute)43     public static KeyCycleOscillator makeWidgetCycle(String attribute) {
44         if (attribute.equals(TypedValues.AttributesType.S_PATH_ROTATE)) {
45             return new PathRotateSet(attribute);
46         }
47         return new CoreSpline(attribute);
48     }
49 
50     private static class CoreSpline extends KeyCycleOscillator {
51         String mType;
52         int mTypeId;
53 
CoreSpline(String str)54         CoreSpline(String str) {
55             mType = str;
56             mTypeId = TypedValues.CycleType.getId(mType);
57         }
58 
59         @Override
setProperty(MotionWidget widget, float t)60         public void setProperty(MotionWidget widget, float t) {
61             widget.setValue(mTypeId, get(t));
62         }
63     }
64 
65     public static class PathRotateSet extends KeyCycleOscillator {
66         String mType;
67         int mTypeId;
68 
PathRotateSet(String str)69         public PathRotateSet(String str) {
70             mType = str;
71             mTypeId = TypedValues.CycleType.getId(mType);
72         }
73 
74         @Override
setProperty(MotionWidget widget, float t)75         public void setProperty(MotionWidget widget, float t) {
76             widget.setValue(mTypeId, get(t));
77         }
78 
79         // @TODO: add description
setPathRotate(MotionWidget view, float t, double dx, double dy)80         public void setPathRotate(MotionWidget view, float t, double dx, double dy) {
81             view.setRotationZ(get(t) + (float) Math.toDegrees(Math.atan2(dy, dx)));
82         }
83     }
84 
85     // @TODO: add description
variesByPath()86     public boolean variesByPath() {
87         return mVariesBy == 1;
88     }
89 
90     static class WavePoint {
91         int mPosition;
92         float mValue;
93         float mOffset;
94         float mPeriod;
95         float mPhase;
96 
WavePoint(int position, float period, float offset, float phase, float value)97         WavePoint(int position, float period, float offset, float phase, float value) {
98             mPosition = position;
99             mValue = value;
100             mOffset = offset;
101             mPeriod = period;
102             mPhase = phase;
103         }
104     }
105 
106     @Override
toString()107     public String toString() {
108         String str = mType;
109         DecimalFormat df = new DecimalFormat("##.##");
110         for (WavePoint wp : mWavePoints) {
111             str += "[" + wp.mPosition + " , " + df.format(wp.mValue) + "] ";
112         }
113         return str;
114     }
115 
setType(String type)116     public void setType(String type) {
117         mType = type;
118     }
119 
120     // @TODO: add description
get(float t)121     public float get(float t) {
122         return (float) mCycleOscillator.getValues(t);
123     }
124 
125     // @TODO: add description
getSlope(float position)126     public float getSlope(float position) {
127         return (float) mCycleOscillator.getSlope(position);
128     }
129 
getCurveFit()130     public CurveFit getCurveFit() {
131         return mCurveFit;
132     }
133 
setCustom(Object custom)134     protected void setCustom(Object custom) {
135 
136     }
137 
138     /**
139      * sets a oscillator wave point
140      *
141      * @param framePosition the position
142      * @param shape         the wave shape
143      * @param waveString    the wave string
144      * @param variesBy      only varies by path supported for now
145      * @param period        the period of the wave
146      * @param offset        the offset value
147      * @param phase         the phase of the new wavepoint
148      * @param value         the adder
149      * @param custom        The ConstraintAttribute used to set the value
150      */
setPoint(int framePosition, int shape, String waveString, int variesBy, float period, float offset, float phase, float value, Object custom)151     public void setPoint(int framePosition,
152             int shape,
153             String waveString,
154             int variesBy,
155             float period,
156             float offset,
157             float phase,
158             float value,
159             Object custom) {
160         mWavePoints.add(new WavePoint(framePosition, period, offset, phase, value));
161         if (variesBy != -1) {
162             mVariesBy = variesBy;
163         }
164         mWaveShape = shape;
165         setCustom(custom);
166         mWaveString = waveString;
167     }
168 
169     /**
170      * sets a oscillator wave point
171      *
172      * @param framePosition the position
173      * @param shape         the wave shape
174      * @param waveString    the wave string
175      * @param variesBy      only varies by path supported for now
176      * @param period        the period of the wave
177      * @param offset        the offset value
178      * @param phase         the phase of the new wavepoint
179      * @param value         the adder
180      */
setPoint(int framePosition, int shape, String waveString, int variesBy, float period, float offset, float phase, float value)181     public void setPoint(int framePosition,
182             int shape,
183             String waveString,
184             int variesBy,
185             float period,
186             float offset,
187             float phase,
188             float value) {
189         mWavePoints.add(new WavePoint(framePosition, period, offset, phase, value));
190         if (variesBy != -1) {
191             mVariesBy = variesBy;
192         }
193         mWaveShape = shape;
194         mWaveString = waveString;
195     }
196 
197     // @TODO: add description
setup(float pathLength)198     public void setup(float pathLength) {
199         int count = mWavePoints.size();
200         if (count == 0) {
201             return;
202         }
203         Collections.sort(mWavePoints, new Comparator<WavePoint>() {
204             @Override
205             public int compare(WavePoint lhs, WavePoint rhs) {
206                 return Integer.compare(lhs.mPosition, rhs.mPosition);
207             }
208         });
209         double[] time = new double[count];
210         double[][] values = new double[count][3];
211         mCycleOscillator = new CycleOscillator(mWaveShape, mWaveString, mVariesBy, count);
212         int i = 0;
213         for (WavePoint wp : mWavePoints) {
214             time[i] = wp.mPeriod * 1E-2;
215             values[i][0] = wp.mValue;
216             values[i][1] = wp.mOffset;
217             values[i][2] = wp.mPhase;
218             mCycleOscillator.setPoint(i, wp.mPosition, wp.mPeriod,
219                     wp.mOffset, wp.mPhase, wp.mValue);
220             i++;
221         }
222         mCycleOscillator.setup(pathLength);
223         mCurveFit = CurveFit.get(CurveFit.SPLINE, time, values);
224     }
225 
226     static class CycleOscillator {
227         static final int UNSET = -1; // -1 is typically used through out android to the UNSET value
228         private static final String TAG = "CycleOscillator";
229         @SuppressWarnings("unused") private final int mVariesBy;
230         Oscillator mOscillator = new Oscillator();
231         private final int mOffst = 0;
232         private final int mPhase = 1;
233         private final int mValue = 2;
234 
235         float[] mValues;
236         double[] mPosition;
237         float[] mPeriod;
238         float[] mOffsetArr; // offsets will be spline interpolated
239         float[] mPhaseArr; // phase will be spline interpolated
240         float[] mScale; // scales will be spline interpolated
241         int mWaveShape;
242         CurveFit mCurveFit;
243         double[] mSplineValueCache; // for the return value of the curve fit
244         double[] mSplineSlopeCache; // for the return value of the curve fit
245         float mPathLength;
246 
CycleOscillator(int waveShape, String customShape, int variesBy, int steps)247         CycleOscillator(int waveShape, String customShape, int variesBy, int steps) {
248             mWaveShape = waveShape;
249             mVariesBy = variesBy;
250             mOscillator.setType(waveShape, customShape);
251             mValues = new float[steps];
252             mPosition = new double[steps];
253             mPeriod = new float[steps];
254             mOffsetArr = new float[steps];
255             mPhaseArr = new float[steps];
256             mScale = new float[steps];
257         }
258 
getValues(float time)259         public double getValues(float time) {
260             if (mCurveFit != null) {
261                 mCurveFit.getPos(time, mSplineValueCache);
262             } else { // only one value no need to interpolate
263                 mSplineValueCache[mOffst] = mOffsetArr[0];
264                 mSplineValueCache[mPhase] = mPhaseArr[0];
265                 mSplineValueCache[mValue] = mValues[0];
266 
267             }
268             double offset = mSplineValueCache[mOffst];
269             double phase = mSplineValueCache[this.mPhase];
270             double waveValue = mOscillator.getValue(time, phase);
271             return offset + waveValue * mSplineValueCache[mValue];
272         }
273 
getLastPhase()274         public double getLastPhase() {
275             return mSplineValueCache[1];
276         }
277 
getSlope(float time)278         public double getSlope(float time) {
279             if (mCurveFit != null) {
280                 mCurveFit.getSlope(time, mSplineSlopeCache);
281                 mCurveFit.getPos(time, mSplineValueCache);
282             } else { // only one value no need to interpolate
283                 mSplineSlopeCache[mOffst] = 0;
284                 mSplineSlopeCache[mPhase] = 0;
285                 mSplineSlopeCache[mValue] = 0;
286             }
287             double waveValue = mOscillator.getValue(time, mSplineValueCache[mPhase]);
288             double waveSlope = mOscillator.getSlope(time,
289                     mSplineValueCache[mPhase], mSplineSlopeCache[mPhase]);
290             return mSplineSlopeCache[mOffst] + waveValue * mSplineSlopeCache[mValue]
291                     + waveSlope * mSplineValueCache[mValue];
292         }
293 
294         /**
295          *
296          */
setPoint(int index, int framePosition, float wavePeriod, float offset, float phase, float values)297         public void setPoint(int index,
298                 int framePosition,
299                 float wavePeriod,
300                 float offset,
301                 float phase,
302                 float values) {
303             mPosition[index] = framePosition / 100.0;
304             mPeriod[index] = wavePeriod;
305             mOffsetArr[index] = offset;
306             mPhaseArr[index] = phase;
307             mValues[index] = values;
308         }
309 
setup(float pathLength)310         public void setup(float pathLength) {
311             mPathLength = pathLength;
312             double[][] splineValues = new double[mPosition.length][3];
313             mSplineValueCache = new double[2 + mValues.length];
314             mSplineSlopeCache = new double[2 + mValues.length];
315             if (mPosition[0] > 0) {
316                 mOscillator.addPoint(0, mPeriod[0]);
317             }
318             int last = mPosition.length - 1;
319             if (mPosition[last] < 1.0f) {
320                 mOscillator.addPoint(1, mPeriod[last]);
321             }
322 
323             for (int i = 0; i < splineValues.length; i++) {
324                 splineValues[i][mOffst] = mOffsetArr[i];
325                 splineValues[i][mPhase] = mPhaseArr[i];
326                 splineValues[i][mValue] = mValues[i];
327                 mOscillator.addPoint(mPosition[i], mPeriod[i]);
328             }
329 
330             // TODO: add mVariesBy and get total time and path length
331             mOscillator.normalize();
332             if (mPosition.length > 1) {
333                 mCurveFit = CurveFit.get(CurveFit.SPLINE, mPosition, splineValues);
334             } else {
335                 mCurveFit = null;
336             }
337         }
338     }
339 
340     // @TODO: add description
setProperty(MotionWidget widget, float t)341     public void setProperty(MotionWidget widget, float t) {
342 
343     }
344 
345 }
346