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