/*
* Copyright (C) 2020 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.utils;
import androidx.constraintlayout.core.motion.CustomAttribute;
import androidx.constraintlayout.core.motion.CustomVariable;
import androidx.constraintlayout.core.motion.MotionWidget;
import java.text.DecimalFormat;
/**
* This engine allows manipulation of attributes by wave shapes oscillating in time
*
*
*/
public abstract class TimeCycleSplineSet {
private static final String TAG = "SplineSet";
protected CurveFit mCurveFit;
protected int mWaveShape = 0;
protected int[] mTimePoints = new int[10];
protected float[][] mValues = new float[10][3];
protected int mCount;
protected String mType;
protected float[] mCache = new float[3];
protected static final int CURVE_VALUE = 0;
protected static final int CURVE_PERIOD = 1;
protected static final int CURVE_OFFSET = 2;
protected static float sVal2PI = (float) (2 * Math.PI);
protected boolean mContinue = false;
protected long mLastTime;
protected float mLastCycle = Float.NaN;
@Override
public String toString() {
String str = mType;
DecimalFormat df = new DecimalFormat("##.##");
for (int i = 0; i < mCount; i++) {
str += "[" + mTimePoints[i] + " , " + df.format(mValues[i]) + "] ";
}
return str;
}
public void setType(String type) {
mType = type;
}
/**
* @param period cycles per second
*/
protected float calcWave(float period) {
float p = period;
switch (mWaveShape) {
default:
case Oscillator.SIN_WAVE:
return (float) Math.sin(p * sVal2PI);
case Oscillator.SQUARE_WAVE:
return (float) Math.signum(p * sVal2PI);
case Oscillator.TRIANGLE_WAVE:
return 1 - Math.abs(p);
case Oscillator.SAW_WAVE:
return ((p * 2 + 1) % 2) - 1;
case Oscillator.REVERSE_SAW_WAVE:
return (1 - ((p * 2 + 1) % 2));
case Oscillator.COS_WAVE:
return (float) Math.cos(p * sVal2PI);
case Oscillator.BOUNCE:
float x = 1 - Math.abs((p * 4) % 4 - 2);
return 1 - x * x;
}
}
public CurveFit getCurveFit() {
return mCurveFit;
}
protected void setStartTime(long currentTime) {
mLastTime = currentTime;
}
// @TODO: add description
public void setPoint(int position, float value, float period, int shape, float offset) {
mTimePoints[mCount] = position;
mValues[mCount][CURVE_VALUE] = value;
mValues[mCount][CURVE_PERIOD] = period;
mValues[mCount][CURVE_OFFSET] = offset;
mWaveShape = Math.max(mWaveShape, shape); // the highest value shape is chosen
mCount++;
}
public static class CustomSet extends TimeCycleSplineSet {
String mAttributeName;
KeyFrameArray.CustomArray mConstraintAttributeList;
KeyFrameArray.FloatArray mWaveProperties = new KeyFrameArray.FloatArray();
float[] mTempValues;
float[] mCustomCache;
public CustomSet(String attribute, KeyFrameArray.CustomArray attrList) {
mAttributeName = attribute.split(",")[1];
mConstraintAttributeList = attrList;
}
// @TODO: add description
@Override
public void setup(int curveType) {
int size = mConstraintAttributeList.size();
int dimensionality = mConstraintAttributeList.valueAt(0).numberOfInterpolatedValues();
double[] time = new double[size];
mTempValues = new float[dimensionality + 2];
mCustomCache = new float[dimensionality];
double[][] values = new double[size][dimensionality + 2];
for (int i = 0; i < size; i++) {
int key = mConstraintAttributeList.keyAt(i);
CustomAttribute ca = mConstraintAttributeList.valueAt(i);
float[] waveProp = mWaveProperties.valueAt(i);
time[i] = key * 1E-2;
ca.getValuesToInterpolate(mTempValues);
for (int k = 0; k < mTempValues.length; k++) {
values[i][k] = mTempValues[k];
}
values[i][dimensionality] = waveProp[0];
values[i][dimensionality + 1] = waveProp[1];
}
mCurveFit = CurveFit.get(curveType, time, values);
}
// @TODO: add description
@Override
public void setPoint(int position, float value, float period, int shape, float offset) {
throw new RuntimeException("don't call for custom attribute "
+ "call setPoint(pos, ConstraintAttribute,...)");
}
// @TODO: add description
public void setPoint(int position,
CustomAttribute value,
float period,
int shape,
float offset) {
mConstraintAttributeList.append(position, value);
mWaveProperties.append(position, new float[]{period, offset});
mWaveShape = Math.max(mWaveShape, shape); // the highest value shape is chosen
}
// @TODO: add description
public boolean setProperty(MotionWidget view, float t, long time, KeyCache cache) {
mCurveFit.getPos(t, mTempValues);
float period = mTempValues[mTempValues.length - 2];
float offset = mTempValues[mTempValues.length - 1];
long delta_time = time - mLastTime;
if (Float.isNaN(mLastCycle)) { // it has not been set
mLastCycle = cache.getFloatValue(view, mAttributeName, 0); // check the cache
if (Float.isNaN(mLastCycle)) { // not in cache so set to 0 (start)
mLastCycle = 0;
}
}
mLastCycle = (float) ((mLastCycle + delta_time * 1E-9 * period) % 1.0);
mLastTime = time;
float wave = calcWave(mLastCycle);
mContinue = false;
for (int i = 0; i < mCustomCache.length; i++) {
mContinue |= mTempValues[i] != 0.0;
mCustomCache[i] = mTempValues[i] * wave + offset;
}
view.setInterpolatedValue(mConstraintAttributeList.valueAt(0), mCustomCache);
if (period != 0.0f) {
mContinue = true;
}
return mContinue;
}
}
// @TODO: add description
public void setup(int curveType) {
if (mCount == 0) {
System.err.println("Error no points added to " + mType);
return;
}
Sort.doubleQuickSort(mTimePoints, mValues, 0, mCount - 1);
int unique = 0;
for (int i = 1; i < mTimePoints.length; i++) {
if (mTimePoints[i] != mTimePoints[i - 1]) {
unique++;
}
}
if (unique == 0) {
unique = 1;
}
double[] time = new double[unique];
double[][] values = new double[unique][3];
int k = 0;
for (int i = 0; i < mCount; i++) {
if (i > 0 && mTimePoints[i] == mTimePoints[i - 1]) {
continue;
}
time[k] = mTimePoints[i] * 1E-2;
values[k][0] = mValues[i][0];
values[k][1] = mValues[i][1];
values[k][2] = mValues[i][2];
k++;
}
mCurveFit = CurveFit.get(curveType, time, values);
}
protected static class Sort {
static void doubleQuickSort(int[] key, float[][] value, int low, int hi) {
int[] stack = new int[key.length + 10];
int count = 0;
stack[count++] = hi;
stack[count++] = low;
while (count > 0) {
low = stack[--count];
hi = stack[--count];
if (low < hi) {
int p = partition(key, value, low, hi);
stack[count++] = p - 1;
stack[count++] = low;
stack[count++] = hi;
stack[count++] = p + 1;
}
}
}
private static int partition(int[] array, float[][] value, int low, int hi) {
int pivot = array[hi];
int i = low;
for (int j = low; j < hi; j++) {
if (array[j] <= pivot) {
swap(array, value, i, j);
i++;
}
}
swap(array, value, i, hi);
return i;
}
private static void swap(int[] array, float[][] value, int a, int b) {
int tmp = array[a];
array[a] = array[b];
array[b] = tmp;
float[] tmpv = value[a];
value[a] = value[b];
value[b] = tmpv;
}
}
public static class CustomVarSet extends TimeCycleSplineSet {
String mAttributeName;
KeyFrameArray.CustomVar mConstraintAttributeList;
KeyFrameArray.FloatArray mWaveProperties = new KeyFrameArray.FloatArray();
float[] mTempValues;
float[] mCustomCache;
public CustomVarSet(String attribute, KeyFrameArray.CustomVar attrList) {
mAttributeName = attribute.split(",")[1];
mConstraintAttributeList = attrList;
}
// @TODO: add description
@Override
public void setup(int curveType) {
int size = mConstraintAttributeList.size();
int dimensionality = mConstraintAttributeList.valueAt(0).numberOfInterpolatedValues();
double[] time = new double[size];
mTempValues = new float[dimensionality + 2];
mCustomCache = new float[dimensionality];
double[][] values = new double[size][dimensionality + 2];
for (int i = 0; i < size; i++) {
int key = mConstraintAttributeList.keyAt(i);
CustomVariable ca = mConstraintAttributeList.valueAt(i);
float[] waveProp = mWaveProperties.valueAt(i);
time[i] = key * 1E-2;
ca.getValuesToInterpolate(mTempValues);
for (int k = 0; k < mTempValues.length; k++) {
values[i][k] = mTempValues[k];
}
values[i][dimensionality] = waveProp[0];
values[i][dimensionality + 1] = waveProp[1];
}
mCurveFit = CurveFit.get(curveType, time, values);
}
// @TODO: add description
@Override
public void setPoint(int position, float value, float period, int shape, float offset) {
throw new RuntimeException("don't call for custom attribute "
+ "call setPoint(pos, ConstraintAttribute,...)");
}
// @TODO: add description
public void setPoint(int position,
CustomVariable value,
float period,
int shape,
float offset) {
mConstraintAttributeList.append(position, value);
mWaveProperties.append(position, new float[]{period, offset});
mWaveShape = Math.max(mWaveShape, shape); // the highest value shape is chosen
}
// @TODO: add description
public boolean setProperty(MotionWidget view, float t, long time, KeyCache cache) {
mCurveFit.getPos(t, mTempValues);
float period = mTempValues[mTempValues.length - 2];
float offset = mTempValues[mTempValues.length - 1];
long delta_time = time - mLastTime;
if (Float.isNaN(mLastCycle)) { // it has not been set
mLastCycle = cache.getFloatValue(view, mAttributeName, 0); // check the cache
if (Float.isNaN(mLastCycle)) { // not in cache so set to 0 (start)
mLastCycle = 0;
}
}
mLastCycle = (float) ((mLastCycle + delta_time * 1E-9 * period) % 1.0);
mLastTime = time;
float wave = calcWave(mLastCycle);
mContinue = false;
for (int i = 0; i < mCustomCache.length; i++) {
mContinue |= mTempValues[i] != 0.0;
mCustomCache[i] = mTempValues[i] * wave + offset;
}
mConstraintAttributeList.valueAt(0).setInterpolatedValue(view, mCustomCache);
if (period != 0.0f) {
mContinue = true;
}
return mContinue;
}
}
}