/*******************************************************************************
* Copyright 2012-present Pixate, Inc.
*
* 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 com.pixate.freestyle.cg.math;
import android.graphics.Matrix;
import android.graphics.Path;
import android.graphics.PointF;
/**
* Pixate Elliptical Arc.
*/
public class PXEllipticalArc {
public static final float M_PI_2 = (float) (Math.PI / 2.0);
public static final float TWO_PI = (float) (2.0 * Math.PI);
private static final float THRESHOLD = 0.25f;
// @formatter:off
private static float coeffs3Low[][][] = {
{
{ 3.85268f, -21.229f, -0.330434f, 0.0127842f },
{ -1.61486f, 0.706564f, 0.225945f, 0.263682f },
{ -0.910164f, 0.388383f, 0.00551445f, 0.00671814f },
{ -0.630184f, 0.192402f, 0.0098871f, 0.0102527f }
}, {
{ -0.162211f, 9.94329f, 0.13723f, 0.0124084f },
{ -0.253135f, 0.00187735f, 0.0230286f, 0.01264f },
{ -0.0695069f, -0.0437594f, 0.0120636f, 0.0163087f },
{ -0.0328856f, -0.00926032f, -0.00173573f, 0.00527385f }
}
};
private static float coeffs3High[][][] = {
{
{ 0.0899116f, -19.2349f, -4.11711f, 0.183362f },
{ 0.138148f, -1.45804f, 1.32044f, 1.38474f },
{ 0.230903f, -0.450262f, 0.219963f, 0.414038f },
{ 0.0590565f, -0.101062f, 0.0430592f, 0.0204699f }
}, {
{ 0.0164649f, 9.89394f, 0.0919496f, 0.00760802f },
{ 0.0191603f, -0.0322058f, 0.0134667f, -0.0825018f },
{ 0.0156192f, -0.017535f, 0.00326508f, -0.228157f },
{ -0.0236752f, 0.0405821f, -0.0173086f, 0.176187f }
}
};
// @formatter:on
private float cx;
private float cy;
private float a;
private float b;
private float eta1;
private float eta2;
private float sinTheta;
private float cosTheta;
/**
* Constructs a new PXEllipticalArc
*
* @param cx
* @param cy
* @param radiusX
* @param radiusY
* @param startingAngle
* @param endingAngle
*/
public PXEllipticalArc(float cx, float cy, float radiusX, float radiusY, float startingAngle,
float endingAngle) {
this.cx = cx;
this.cy = cy;
this.a = radiusX;
this.b = radiusY;
eta1 = (float) Math.atan2(Math.sin(startingAngle) / b, Math.cos(startingAngle) / a);
eta2 = (float) Math.atan2(Math.sin(endingAngle) / b, Math.cos(endingAngle) / a);
if (eta1 > 0 && startingAngle < 0) {
eta1 -= TWO_PI;
} else if (eta1 < 0 && startingAngle > 0) {
eta1 += TWO_PI;
}
if (eta2 > 0 && endingAngle < 0) {
eta2 -= TWO_PI;
} else if (eta2 < 0 && endingAngle > 0) {
eta2 += TWO_PI;
}
// make sure eta1 <= eta2 <= eta1 + 2??
// eta2 -= TWO_PI * floorf((eta2 - eta1) / TWO_PI);
//
// if ((endingAngle - startingAngle > M_PI) && (eta2 - eta1 < M_PI))
// {
// eta2 += TWO_PI;
// }
// assume axis-aligned
cosTheta = 1.0f;
sinTheta = 0.0f;
}
/**
* Adds the elliptical path to the path.
*
* @param path
* @param transform
* @return The last path point
*/
public PointF addToPath(Path path, Matrix transform) {
Matrix pTransform = transform.isIdentity() ? null : transform;
boolean found = false;
int n = 1;
while (!found && (n < 1024)) {
float dEta = (eta2 - eta1) / n;
if (dEta <= M_PI_2) {
float etaB = eta1;
found = true;
for (int i = 0; found && (i < n); ++i) {
float etaA = etaB;
etaB += dEta;
float error = estimateErrorForStartingAngle(etaA, etaB);
found = (error <= THRESHOLD);
}
}
n = n << 1;
}
float dEta = (eta2 - eta1) / n;
float etaB = eta1;
float cosEtaB = (float) Math.cos(etaB);
float sinEtaB = (float) Math.sin(etaB);
float aCosEtaB = a * cosEtaB;
float bSinEtaB = b * sinEtaB;
float aSinEtaB = a * sinEtaB;
float bCosEtaB = b * cosEtaB;
float xB = cx + aCosEtaB * cosTheta - bSinEtaB * sinTheta;
float yB = cy + aCosEtaB * sinTheta + bSinEtaB * cosTheta;
float xBDot = -aSinEtaB * cosTheta - bCosEtaB * sinTheta;
float yBDot = -aSinEtaB * sinTheta + bCosEtaB * cosTheta;
float t = (float) Math.tan(0.5f * dEta);
float alpha = (float) (Math.sin(dEta) * (Math.sqrt(4.0f + 3.0f * t * t) - 1.0f) / 3.0f);
for (int i = 0; i < n; ++i) {
// float etaA = etaB;
float xA = xB;
float yA = yB;
float xADot = xBDot;
float yADot = yBDot;
etaB += dEta;
cosEtaB = (float) Math.cos(etaB);
sinEtaB = (float) Math.sin(etaB);
aCosEtaB = a * cosEtaB;
bSinEtaB = b * sinEtaB;
aSinEtaB = a * sinEtaB;
bCosEtaB = b * cosEtaB;
xB = cx + aCosEtaB * cosTheta - bSinEtaB * sinTheta;
yB = cy + aCosEtaB * sinTheta + bSinEtaB * cosTheta;
xBDot = -aSinEtaB * cosTheta - bCosEtaB * sinTheta;
yBDot = -aSinEtaB * sinTheta + bCosEtaB * cosTheta;
float c1x = (xA + alpha * xADot);
float c1y = (yA + alpha * yADot);
float c2x = (xB - alpha * xBDot);
float c2y = (yB - alpha * yBDot);
if (pTransform != null) {
float[] points = new float[] { c1x, c1y, c2x, c2y, xB, yB };
transform.mapPoints(points);
}
// CGPathAddCurveToPoint(path, pTransform, c1x, c1y, c2x, c2y, xB,
// yB);
path.cubicTo(c1x, c1y, c2x, c2y, xB, yB);
}
return new PointF(xB, yB);
}
private float estimateErrorForStartingAngle(float etaA, float etaB) {
float eta = (etaA + etaB) * 0.5f;
float x = b / a;
float dEta = etaB - etaA;
float cos2 = (float) Math.cos(2.0f * eta);
float cos4 = (float) Math.cos(4.0f * eta);
float cos6 = (float) Math.cos(6.0f * eta);
float safety[] = { 0.001f, 4.98f, 0.207f, 0.0067f };
float c0, c1;
if (x < 0.25f) {
c0 = rationalFunction(x, coeffs3Low[0][0]) + cos2
* rationalFunction(x, coeffs3Low[0][1]) + cos4
* rationalFunction(x, coeffs3Low[0][2]) + cos6
* rationalFunction(x, coeffs3Low[0][3]);
c1 = rationalFunction(x, coeffs3Low[1][0]) + cos2
* rationalFunction(x, coeffs3Low[1][1]) + cos4
* rationalFunction(x, coeffs3Low[1][2]) + cos6
* rationalFunction(x, coeffs3Low[1][3]);
} else {
c0 = rationalFunction(x, coeffs3High[0][0]) + cos2
* rationalFunction(x, coeffs3High[0][1]) + cos4
* rationalFunction(x, coeffs3High[0][2]) + cos6
* rationalFunction(x, coeffs3High[0][3]);
c1 = rationalFunction(x, coeffs3High[1][0]) + cos2
* rationalFunction(x, coeffs3High[1][1]) + cos4
* rationalFunction(x, coeffs3High[1][2]) + cos6
* rationalFunction(x, coeffs3High[1][3]);
}
return (float) (rationalFunction(x, safety) * a * Math.exp(c0 + c1 * dEta));
}
private static float rationalFunction(float x, float[] c) {
return ((x * (x * c[0] + c[1]) + c[2]) / (x + c[3]));
}
/**
* Adds an elliptical arc to the given path.
*
* @param path
* @param m
* @param x
* @param y
* @param radiusX
* @param radiusY
* @param startAngle
* @param endAngle
* @return The last path point.
*/
public static PointF pathAddEllipticalArc(Path path, Matrix m, float x, float y, float radiusX,
float radiusY, float startAngle, float endAngle) {
PXEllipticalArc arc = new PXEllipticalArc(x, y, radiusX, radiusY, startAngle, endAngle);
if (m == null) {
return arc.addToPath(path, new Matrix());
} else {
return arc.addToPath(path, m);
}
}
}