diff --git a/app/src/main/java/com/hippo/animation/InterpolatorInflater.java b/app/src/main/java/com/hippo/animation/InterpolatorInflater.java index 905ef3f9a..526e1863c 100644 --- a/app/src/main/java/com/hippo/animation/InterpolatorInflater.java +++ b/app/src/main/java/com/hippo/animation/InterpolatorInflater.java @@ -31,7 +31,6 @@ import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; import android.view.animation.OvershootInterpolator; -import android.view.animation.PathInterpolator; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; diff --git a/app/src/main/java/com/hippo/animation/IpPathParser.java b/app/src/main/java/com/hippo/animation/IpPathParser.java new file mode 100644 index 000000000..f39255fb1 --- /dev/null +++ b/app/src/main/java/com/hippo/animation/IpPathParser.java @@ -0,0 +1,648 @@ +/* + * Copyright (C) 2015 Hippo Seven + * + * 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 + * 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.hippo.animation; + +import android.util.Log; + +import java.util.ArrayList; +import java.util.Arrays; + +class IpPathParser { + static final String LOGTAG = IpPathParser.class.getSimpleName(); + + /** + * @param pathData The string representing a path, the same as "d" string in svg file. + * @return the generated Path object. + */ + public static PathInterpolator.Path createPathFromPathData(String pathData) { + PathInterpolator.Path path = new PathInterpolator.Path(); + PathDataNode[] nodes = createNodesFromPathData(pathData); + if (nodes != null) { + PathDataNode.nodesToPath(nodes, path); + return path; + } + return null; + } + + /** + * @param pathData The string representing a path, the same as "d" string in svg file. + * @return an array of the PathDataNode. + */ + public static PathDataNode[] createNodesFromPathData(String pathData) { + if (pathData == null) { + return null; + } + int start = 0; + int end = 1; + + ArrayList list = new ArrayList(); + while (end < pathData.length()) { + end = nextStart(pathData, end); + String s = pathData.substring(start, end).trim(); + if (s.length() > 0) { + float[] val = getFloats(s); + addNode(list, s.charAt(0), val); + } + + start = end; + end++; + } + if ((end - start) == 1 && start < pathData.length()) { + addNode(list, pathData.charAt(start), new float[0]); + } + return list.toArray(new PathDataNode[list.size()]); + } + + /** + * @param source The array of PathDataNode to be duplicated. + * @return a deep copy of the source. + */ + public static PathDataNode[] deepCopyNodes(PathDataNode[] source) { + if (source == null) { + return null; + } + PathDataNode[] copy = new PathDataNode[source.length]; + for (int i = 0; i < source.length; i ++) { + copy[i] = new PathDataNode(source[i]); + } + return copy; + } + + /** + * @param nodesFrom The source path represented in an array of PathDataNode + * @param nodesTo The target path represented in an array of PathDataNode + * @return whether the nodesFrom can morph into nodesTo + */ + public static boolean canMorph(PathDataNode[] nodesFrom, PathDataNode[] nodesTo) { + if (nodesFrom == null || nodesTo == null) { + return false; + } + + if (nodesFrom.length != nodesTo.length) { + return false; + } + + for (int i = 0; i < nodesFrom.length; i ++) { + if (nodesFrom[i].mType != nodesTo[i].mType + || nodesFrom[i].mParams.length != nodesTo[i].mParams.length) { + return false; + } + } + return true; + } + + /** + * Update the target's data to match the source. + * Before calling this, make sure canMorph(target, source) is true. + * + * @param target The target path represented in an array of PathDataNode + * @param source The source path represented in an array of PathDataNode + */ + public static void updateNodes(PathDataNode[] target, PathDataNode[] source) { + for (int i = 0; i < source.length; i ++) { + target[i].mType = source[i].mType; + for (int j = 0; j < source[i].mParams.length; j ++) { + target[i].mParams[j] = source[i].mParams[j]; + } + } + } + + private static int nextStart(String s, int end) { + char c; + + while (end < s.length()) { + c = s.charAt(end); + if (((c - 'A') * (c - 'Z') <= 0) || (((c - 'a') * (c - 'z') <= 0))) { + return end; + } + end++; + } + return end; + } + + private static void addNode(ArrayList list, char cmd, float[] val) { + list.add(new PathDataNode(cmd, val)); + } + + private static class ExtractFloatResult { + // We need to return the position of the next separator and whether the + // next float starts with a '-'. + int mEndPosition; + boolean mEndWithNegSign; + } + + /** + * Parse the floats in the string. + * This is an optimized version of parseFloat(s.split(",|\\s")); + * + * @param s the string containing a command and list of floats + * @return array of floats + */ + private static float[] getFloats(String s) { + if (s.charAt(0) == 'z' | s.charAt(0) == 'Z') { + return new float[0]; + } + try { + float[] results = new float[s.length()]; + int count = 0; + int startPosition = 1; + int endPosition = 0; + + ExtractFloatResult result = new ExtractFloatResult(); + int totalLength = s.length(); + + // The startPosition should always be the first character of the + // current number, and endPosition is the character after the current + // number. + while (startPosition < totalLength) { + extract(s, startPosition, result); + endPosition = result.mEndPosition; + + if (startPosition < endPosition) { + results[count++] = Float.parseFloat( + s.substring(startPosition, endPosition)); + } + + if (result.mEndWithNegSign) { + // Keep the '-' sign with next number. + startPosition = endPosition; + } else { + startPosition = endPosition + 1; + } + } + return Arrays.copyOf(results, count); + } catch (NumberFormatException e) { + Log.e(LOGTAG, "error in parsing \"" + s + "\""); + throw e; + } + } + + /** + * Calculate the position of the next comma or space or negative sign + * @param s the string to search + * @param start the position to start searching + * @param result the result of the extraction, including the position of the + * the starting position of next number, whether it is ending with a '-'. + */ + private static void extract(String s, int start, ExtractFloatResult result) { + // Now looking for ' ', ',' or '-' from the start. + int currentIndex = start; + boolean foundSeparator = false; + result.mEndWithNegSign = false; + for (; currentIndex < s.length(); currentIndex++) { + char currentChar = s.charAt(currentIndex); + switch (currentChar) { + case ' ': + case ',': + foundSeparator = true; + break; + case '-': + if (currentIndex != start) { + foundSeparator = true; + result.mEndWithNegSign = true; + } + break; + } + if (foundSeparator) { + break; + } + } + // When there is nothing found, then we put the end position to the end + // of the string. + result.mEndPosition = currentIndex; + } + + /** + * Each PathDataNode represents one command in the "d" attribute of the svg + * file. + * An array of PathDataNode can represent the whole "d" attribute. + */ + public static class PathDataNode { + private char mType; + private float[] mParams; + + private PathDataNode(char type, float[] params) { + mType = type; + mParams = params; + } + + private PathDataNode(PathDataNode n) { + mType = n.mType; + mParams = Arrays.copyOf(n.mParams, n.mParams.length); + } + + /** + * Convert an array of PathDataNode to Path. + * + * @param node The source array of PathDataNode. + * @param path The target Path object. + */ + public static void nodesToPath(PathDataNode[] node, PathInterpolator.Path path) { + float[] current = new float[4]; + char previousCommand = 'm'; + for (int i = 0; i < node.length; i++) { + addCommand(path, current, previousCommand, node[i].mType, node[i].mParams); + previousCommand = node[i].mType; + } + } + + /** + * The current PathDataNode will be interpolated between the + * nodeFrom and nodeTo according to the + * fraction. + * + * @param nodeFrom The start value as a PathDataNode. + * @param nodeTo The end value as a PathDataNode + * @param fraction The fraction to interpolate. + */ + public void interpolatePathDataNode(PathDataNode nodeFrom, + PathDataNode nodeTo, float fraction) { + for (int i = 0; i < nodeFrom.mParams.length; i++) { + mParams[i] = nodeFrom.mParams[i] * (1 - fraction) + + nodeTo.mParams[i] * fraction; + } + } + + private static void addCommand(PathInterpolator.Path path, float[] current, + char previousCmd, char cmd, float[] val) { + + int incr = 2; + float currentX = current[0]; + float currentY = current[1]; + float ctrlPointX = current[2]; + float ctrlPointY = current[3]; + float reflectiveCtrlPointX; + float reflectiveCtrlPointY; + + switch (cmd) { + case 'z': + case 'Z': + path.close(); + return; + case 'm': + case 'M': + case 'l': + case 'L': + case 't': + case 'T': + incr = 2; + break; + case 'h': + case 'H': + case 'v': + case 'V': + incr = 1; + break; + case 'c': + case 'C': + incr = 6; + break; + case 's': + case 'S': + case 'q': + case 'Q': + incr = 4; + break; + case 'a': + case 'A': + incr = 7; + break; + } + for (int k = 0; k < val.length; k += incr) { + switch (cmd) { + case 'm': // moveto - Start a new sub-path (relative) + path.rMoveTo(val[k + 0], val[k + 1]); + currentX += val[k + 0]; + currentY += val[k + 1]; + break; + case 'M': // moveto - Start a new sub-path + path.moveTo(val[k + 0], val[k + 1]); + currentX = val[k + 0]; + currentY = val[k + 1]; + break; + case 'l': // lineto - Draw a line from the current point (relative) + path.rLineTo(val[k + 0], val[k + 1]); + currentX += val[k + 0]; + currentY += val[k + 1]; + break; + case 'L': // lineto - Draw a line from the current point + path.lineTo(val[k + 0], val[k + 1]); + currentX = val[k + 0]; + currentY = val[k + 1]; + break; + case 'z': // closepath - Close the current subpath + case 'Z': // closepath - Close the current subpath + path.close(); + break; + case 'h': // horizontal lineto - Draws a horizontal line (relative) + path.rLineTo(val[k + 0], 0); + currentX += val[k + 0]; + break; + case 'H': // horizontal lineto - Draws a horizontal line + path.lineTo(val[k + 0], currentY); + currentX = val[k + 0]; + break; + case 'v': // vertical lineto - Draws a vertical line from the current point (r) + path.rLineTo(0, val[k + 0]); + currentY += val[k + 0]; + break; + case 'V': // vertical lineto - Draws a vertical line from the current point + path.lineTo(currentX, val[k + 0]); + currentY = val[k + 0]; + break; + case 'c': // curveto - Draws a cubic Bézier curve (relative) + path.rCubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3], + val[k + 4], val[k + 5]); + + ctrlPointX = currentX + val[k + 2]; + ctrlPointY = currentY + val[k + 3]; + currentX += val[k + 4]; + currentY += val[k + 5]; + + break; + case 'C': // curveto - Draws a cubic Bézier curve + path.cubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3], + val[k + 4], val[k + 5]); + currentX = val[k + 4]; + currentY = val[k + 5]; + ctrlPointX = val[k + 2]; + ctrlPointY = val[k + 3]; + break; + case 's': // smooth curveto - Draws a cubic Bézier curve (reflective cp) + reflectiveCtrlPointX = 0; + reflectiveCtrlPointY = 0; + if (previousCmd == 'c' || previousCmd == 's' + || previousCmd == 'C' || previousCmd == 'S') { + reflectiveCtrlPointX = currentX - ctrlPointX; + reflectiveCtrlPointY = currentY - ctrlPointY; + } + path.rCubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY, + val[k + 0], val[k + 1], + val[k + 2], val[k + 3]); + + ctrlPointX = currentX + val[k + 0]; + ctrlPointY = currentY + val[k + 1]; + currentX += val[k + 2]; + currentY += val[k + 3]; + break; + case 'S': // shorthand/smooth curveto Draws a cubic Bézier curve(reflective cp) + reflectiveCtrlPointX = currentX; + reflectiveCtrlPointY = currentY; + if (previousCmd == 'c' || previousCmd == 's' + || previousCmd == 'C' || previousCmd == 'S') { + reflectiveCtrlPointX = 2 * currentX - ctrlPointX; + reflectiveCtrlPointY = 2 * currentY - ctrlPointY; + } + path.cubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY, + val[k + 0], val[k + 1], val[k + 2], val[k + 3]); + ctrlPointX = val[k + 0]; + ctrlPointY = val[k + 1]; + currentX = val[k + 2]; + currentY = val[k + 3]; + break; + case 'q': // Draws a quadratic Bézier (relative) + path.rQuadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]); + ctrlPointX = currentX + val[k + 0]; + ctrlPointY = currentY + val[k + 1]; + currentX += val[k + 2]; + currentY += val[k + 3]; + break; + case 'Q': // Draws a quadratic Bézier + path.quadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]); + ctrlPointX = val[k + 0]; + ctrlPointY = val[k + 1]; + currentX = val[k + 2]; + currentY = val[k + 3]; + break; + case 't': // Draws a quadratic Bézier curve(reflective control point)(relative) + reflectiveCtrlPointX = 0; + reflectiveCtrlPointY = 0; + if (previousCmd == 'q' || previousCmd == 't' + || previousCmd == 'Q' || previousCmd == 'T') { + reflectiveCtrlPointX = currentX - ctrlPointX; + reflectiveCtrlPointY = currentY - ctrlPointY; + } + path.rQuadTo(reflectiveCtrlPointX, reflectiveCtrlPointY, + val[k + 0], val[k + 1]); + ctrlPointX = currentX + reflectiveCtrlPointX; + ctrlPointY = currentY + reflectiveCtrlPointY; + currentX += val[k + 0]; + currentY += val[k + 1]; + break; + case 'T': // Draws a quadratic Bézier curve (reflective control point) + reflectiveCtrlPointX = currentX; + reflectiveCtrlPointY = currentY; + if (previousCmd == 'q' || previousCmd == 't' + || previousCmd == 'Q' || previousCmd == 'T') { + reflectiveCtrlPointX = 2 * currentX - ctrlPointX; + reflectiveCtrlPointY = 2 * currentY - ctrlPointY; + } + path.quadTo(reflectiveCtrlPointX, reflectiveCtrlPointY, + val[k + 0], val[k + 1]); + ctrlPointX = reflectiveCtrlPointX; + ctrlPointY = reflectiveCtrlPointY; + currentX = val[k + 0]; + currentY = val[k + 1]; + break; + case 'a': // Draws an elliptical arc + // (rx ry x-axis-rotation large-arc-flag sweep-flag x y) + drawArc(path, + currentX, + currentY, + val[k + 5] + currentX, + val[k + 6] + currentY, + val[k + 0], + val[k + 1], + val[k + 2], + val[k + 3] != 0, + val[k + 4] != 0); + currentX += val[k + 5]; + currentY += val[k + 6]; + ctrlPointX = currentX; + ctrlPointY = currentY; + break; + case 'A': // Draws an elliptical arc + drawArc(path, + currentX, + currentY, + val[k + 5], + val[k + 6], + val[k + 0], + val[k + 1], + val[k + 2], + val[k + 3] != 0, + val[k + 4] != 0); + currentX = val[k + 5]; + currentY = val[k + 6]; + ctrlPointX = currentX; + ctrlPointY = currentY; + break; + } + previousCmd = cmd; + } + current[0] = currentX; + current[1] = currentY; + current[2] = ctrlPointX; + current[3] = ctrlPointY; + } + + private static void drawArc(PathInterpolator.Path p, + float x0, + float y0, + float x1, + float y1, + float a, + float b, + float theta, + boolean isMoreThanHalf, + boolean isPositiveArc) { + + /* Convert rotation angle from degrees to radians */ + double thetaD = Math.toRadians(theta); + /* Pre-compute rotation matrix entries */ + double cosTheta = Math.cos(thetaD); + double sinTheta = Math.sin(thetaD); + /* Transform (x0, y0) and (x1, y1) into unit space */ + /* using (inverse) rotation, followed by (inverse) scale */ + double x0p = (x0 * cosTheta + y0 * sinTheta) / a; + double y0p = (-x0 * sinTheta + y0 * cosTheta) / b; + double x1p = (x1 * cosTheta + y1 * sinTheta) / a; + double y1p = (-x1 * sinTheta + y1 * cosTheta) / b; + + /* Compute differences and averages */ + double dx = x0p - x1p; + double dy = y0p - y1p; + double xm = (x0p + x1p) / 2; + double ym = (y0p + y1p) / 2; + /* Solve for intersecting unit circles */ + double dsq = dx * dx + dy * dy; + if (dsq == 0.0) { + Log.w(LOGTAG, " Points are coincident"); + return; /* Points are coincident */ + } + double disc = 1.0 / dsq - 1.0 / 4.0; + if (disc < 0.0) { + Log.w(LOGTAG, "Points are too far apart " + dsq); + float adjust = (float) (Math.sqrt(dsq) / 1.99999); + drawArc(p, x0, y0, x1, y1, a * adjust, + b * adjust, theta, isMoreThanHalf, isPositiveArc); + return; /* Points are too far apart */ + } + double s = Math.sqrt(disc); + double sdx = s * dx; + double sdy = s * dy; + double cx; + double cy; + if (isMoreThanHalf == isPositiveArc) { + cx = xm - sdy; + cy = ym + sdx; + } else { + cx = xm + sdy; + cy = ym - sdx; + } + + double eta0 = Math.atan2((y0p - cy), (x0p - cx)); + + double eta1 = Math.atan2((y1p - cy), (x1p - cx)); + + double sweep = (eta1 - eta0); + if (isPositiveArc != (sweep >= 0)) { + if (sweep > 0) { + sweep -= 2 * Math.PI; + } else { + sweep += 2 * Math.PI; + } + } + + cx *= a; + cy *= b; + double tcx = cx; + cx = cx * cosTheta - cy * sinTheta; + cy = tcx * sinTheta + cy * cosTheta; + + arcToBezier(p, cx, cy, a, b, x0, y0, thetaD, eta0, sweep); + } + + /** + * Converts an arc to cubic Bezier segments and records them in p. + * + * @param p The target for the cubic Bezier segments + * @param cx The x coordinate center of the ellipse + * @param cy The y coordinate center of the ellipse + * @param a The radius of the ellipse in the horizontal direction + * @param b The radius of the ellipse in the vertical direction + * @param e1x E(eta1) x coordinate of the starting point of the arc + * @param e1y E(eta2) y coordinate of the starting point of the arc + * @param theta The angle that the ellipse bounding rectangle makes with horizontal plane + * @param start The start angle of the arc on the ellipse + * @param sweep The angle (positive or negative) of the sweep of the arc on the ellipse + */ + private static void arcToBezier(PathInterpolator.Path p, + double cx, + double cy, + double a, + double b, + double e1x, + double e1y, + double theta, + double start, + double sweep) { + // Taken from equations at: http://spaceroots.org/documents/ellipse/node8.html + // and http://www.spaceroots.org/documents/ellipse/node22.html + + // Maximum of 45 degrees per cubic Bezier segment + int numSegments = Math.abs((int) Math.ceil(sweep * 4 / Math.PI)); + + double eta1 = start; + double cosTheta = Math.cos(theta); + double sinTheta = Math.sin(theta); + double cosEta1 = Math.cos(eta1); + double sinEta1 = Math.sin(eta1); + double ep1x = (-a * cosTheta * sinEta1) - (b * sinTheta * cosEta1); + double ep1y = (-a * sinTheta * sinEta1) + (b * cosTheta * cosEta1); + + double anglePerSegment = sweep / numSegments; + for (int i = 0; i < numSegments; i++) { + double eta2 = eta1 + anglePerSegment; + double sinEta2 = Math.sin(eta2); + double cosEta2 = Math.cos(eta2); + double e2x = cx + (a * cosTheta * cosEta2) - (b * sinTheta * sinEta2); + double e2y = cy + (a * sinTheta * cosEta2) + (b * cosTheta * sinEta2); + double ep2x = -a * cosTheta * sinEta2 - b * sinTheta * cosEta2; + double ep2y = -a * sinTheta * sinEta2 + b * cosTheta * cosEta2; + double tanDiff2 = Math.tan((eta2 - eta1) / 2); + double alpha = + Math.sin(eta2 - eta1) * (Math.sqrt(4 + (3 * tanDiff2 * tanDiff2)) - 1) / 3; + double q1x = e1x + alpha * ep1x; + double q1y = e1y + alpha * ep1y; + double q2x = e2x - alpha * ep2x; + double q2y = e2y - alpha * ep2y; + + p.cubicTo((float) q1x, + (float) q1y, + (float) q2x, + (float) q2y, + (float) e2x, + (float) e2y); + eta1 = eta2; + e1x = e2x; + e1y = e2y; + ep1x = ep2x; + ep1y = ep2y; + } + } + } +} diff --git a/app/src/main/java/com/hippo/animation/PathInterpolator.java b/app/src/main/java/com/hippo/animation/PathInterpolator.java index 6a42a6ff3..3ccb8cf74 100644 --- a/app/src/main/java/com/hippo/animation/PathInterpolator.java +++ b/app/src/main/java/com/hippo/animation/PathInterpolator.java @@ -25,6 +25,9 @@ import android.view.animation.Interpolator; import com.hippo.ehviewer.R; +import java.util.LinkedList; +import java.util.List; + /** * An interpolator that can traverse a Path that extends from Point * (0, 0) to (1, 1). The x coordinate along the Path @@ -45,7 +48,6 @@ public class PathInterpolator implements Interpolator { // This governs how accurate the approximation of the Path is. private static final float PRECISION = 0.002f; - private static final float ERROR_SQUARED = PRECISION * PRECISION; private float[] mX; // x coordinates in the line @@ -57,11 +59,9 @@ public class PathInterpolator implements Interpolator { * * @param path The Path to use to make the line representing the interpolator. */ - /* // TODO I think I do not need it public PathInterpolator(Path path) { initPath(path); } - */ /** * Create an interpolator for a quadratic Bezier curve. The end points @@ -100,7 +100,7 @@ public class PathInterpolator implements Interpolator { a = res.obtainAttributes(attrs, R.styleable.PathInterpolator); } parseInterpolatorFromTypeArray(a); - + //setChangingConfiguration(a.getChangingConfigurations()); a.recycle(); } @@ -108,15 +108,13 @@ public class PathInterpolator implements Interpolator { // If there is pathData defined in the xml file, then the controls points // will be all coming from pathData. if (a.hasValue(R.styleable.PathInterpolator_pathData)) { - /* TODO I think I do not need it String pathData = a.getString(R.styleable.PathInterpolator_pathData); - Path path = PathParser.createPathFromPathData(pathData); + Path path = IpPathParser.createPathFromPathData(pathData); if (path == null) { throw new InflateException("The path is null, which is created" + " from " + pathData); } initPath(path); - */ } else { if (!a.hasValue(R.styleable.PathInterpolator_controlX1)) { throw new InflateException("pathInterpolator requires the controlX1 attribute"); @@ -145,16 +143,22 @@ public class PathInterpolator implements Interpolator { } private void initQuad(float controlX, float controlY) { - float[] points = bezierApproximate(new float[]{controlX, controlY}, ERROR_SQUARED, false); - loadPointComponents(points); + Path path = new Path(); + path.moveTo(0, 0); + path.quadTo(controlX, controlY, 1f, 1f); + initPath(path); } private void initCubic(float x1, float y1, float x2, float y2) { - float[] points = bezierApproximate(new float[]{x1, y1, x2, y2}, ERROR_SQUARED, true); - loadPointComponents(points); + Path path = new Path(); + path.moveTo(0, 0); + path.cubicTo(x1, y1, x2, y2, 1f, 1f); + initPath(path); } - private void loadPointComponents(float[] pointComponents) { + private void initPath(Path path) { + float[] pointComponents = path.approximate(PRECISION); + int numPoints = pointComponents.length / 3; if (pointComponents[1] != 0 || pointComponents[2] != 0 || pointComponents[pointComponents.length - 2] != 1 @@ -228,6 +232,484 @@ public class PathInterpolator implements Interpolator { return startY + (fraction * (endY - startY)); } + private static final class PointMap { + + private int size = 0; + + private Link firstLink; + private Link lastLink; + + float totalLength = 0f; + + private static final class Link { + /** It is only for bezier **/ + float t; + float x; + float y; + /** It should be Length first **/ + float f = -1; + + private Link previous, next; + + Link(float t, float x, float y, float f, Link p, Link n) { + this.t = t; + this.x = x; + this.y = y; + this.f = f; + this.previous = p; + this.next = n; + } + } + + public static final class Iterator { + + final PointMap parent; + Link link; + + private Iterator(PointMap p, Link l) { + parent = p; + link = l; + } + + boolean hasNext() { + return link.next != null; + } + + Iterator nextIterator() { + if (hasNext()) { + return new Iterator(parent, link.next); + } else { + return null; + } + } + + // Move iterator to the new data + void addBefore(float t, float x, float y, float f) { + Link p = link.previous; + Link l = new Link(t, x, y, f, p, link); + link.previous = l; + p.next = l; + link = l; + parent.size++; + } + } + + public void add(float t, float x, float y, float f) { + Link link = new Link(t, x, y, f, null, null); + if (lastLink != null) { + link.previous = lastLink; + lastLink.next = link; + } + lastLink = link; + if (firstLink == null) { + firstLink = link; + } + size++; + } + + // Get a iterator at first data or null + public Iterator iterator() { + if (firstLink != null) { + return new Iterator(this, firstLink); + } else { + return null; + } + } + + public Iterator lastIterator() { + if (lastLink != null) { + return new Iterator(this, lastLink); + } else { + return null; + } + } + + public float[] generate() { + int index = 0; + float[] result = new float[size * 3]; + for (Link link = firstLink; link != null; link = link.next) { + result[index++] = link.f / totalLength; + result[index++] = link.x; + result[index++] = link.y; + } + return result; + } + } + + + public static class Path { + + private List mActionList = new LinkedList<>(); + + /** startX, startY, length**/ + private float[] mTemp = new float[2]; + + { + mTemp[0] = 0f; + mTemp[1] = 0f; + } + + public void close() { + // Empty + } + + /** + * Set the beginning of the next contour to the point (x,y). + * + * @param x The x-coordinate of the start of a new contour + * @param y The y-coordinate of the start of a new contour + */ + public void moveTo(float x, float y) { + mActionList.add(new MoveAction(x, y)); + } + + /** + * Set the beginning of the next contour relative to the last point on the + * previous contour. If there is no previous contour, this is treated the + * same as moveTo(). + * + * @param dx The amount to add to the x-coordinate of the end of the + * previous contour, to specify the start of a new contour + * @param dy The amount to add to the y-coordinate of the end of the + * previous contour, to specify the start of a new contour + */ + public void rMoveTo(float dx, float dy) { + mActionList.add(new RMoveAction(dx, dy)); + } + + /** + * Add a line from the last point to the specified point (x,y). + * If no moveTo() call has been made for this contour, the first point is + * automatically set to (0,0). + * + * @param x The x-coordinate of the end of a line + * @param y The y-coordinate of the end of a line + */ + public void lineTo(float x, float y) { + mActionList.add(new LineToAction(x, y)); + } + + /** + * Same as lineTo, but the coordinates are considered relative to the last + * point on this contour. If there is no previous point, then a moveTo(0,0) + * is inserted automatically. + * + * @param dx The amount to add to the x-coordinate of the previous point on + * this contour, to specify a line + * @param dy The amount to add to the y-coordinate of the previous point on + * this contour, to specify a line + */ + public void rLineTo(float dx, float dy) { + mActionList.add(new RLineToAction(dx, dy)); + } + + /** + * Add a quadratic bezier from the last point, approaching control point + * (x1,y1), and ending at (x2,y2). If no moveTo() call has been made for + * this contour, the first point is automatically set to (0,0). + * + * @param x1 The x-coordinate of the control point on a quadratic curve + * @param y1 The y-coordinate of the control point on a quadratic curve + * @param x2 The x-coordinate of the end point on a quadratic curve + * @param y2 The y-coordinate of the end point on a quadratic curve + */ + public void quadTo(float x1, float y1, float x2, float y2) { + mActionList.add(new QuadToAction(x1, y1, x2, y2)); + } + + /** + * Same as quadTo, but the coordinates are considered relative to the last + * point on this contour. If there is no previous point, then a moveTo(0,0) + * is inserted automatically. + * + * @param dx1 The amount to add to the x-coordinate of the last point on + * this contour, for the control point of a quadratic curve + * @param dy1 The amount to add to the y-coordinate of the last point on + * this contour, for the control point of a quadratic curve + * @param dx2 The amount to add to the x-coordinate of the last point on + * this contour, for the end point of a quadratic curve + * @param dy2 The amount to add to the y-coordinate of the last point on + * this contour, for the end point of a quadratic curve + */ + public void rQuadTo(float dx1, float dy1, float dx2, float dy2) { + mActionList.add(new RQuadToAction(dx1, dy1, dx2, dy2)); + } + + /** + * Add a cubic bezier from the last point, approaching control points + * (x1,y1) and (x2,y2), and ending at (x3,y3). If no moveTo() call has been + * made for this contour, the first point is automatically set to (0,0). + * + * @param x1 The x-coordinate of the 1st control point on a cubic curve + * @param y1 The y-coordinate of the 1st control point on a cubic curve + * @param x2 The x-coordinate of the 2nd control point on a cubic curve + * @param y2 The y-coordinate of the 2nd control point on a cubic curve + * @param x3 The x-coordinate of the end point on a cubic curve + * @param y3 The y-coordinate of the end point on a cubic curve + */ + public void cubicTo(float x1, float y1, float x2, float y2, + float x3, float y3) { + mActionList.add(new CubicToAction(x1, y1, x2, y2, x3, y3)); + } + + /** + * Same as cubicTo, but the coordinates are considered relative to the + * current point on this contour. If there is no previous point, then a + * moveTo(0,0) is inserted automatically. + */ + public void rCubicTo(float x1, float y1, float x2, float y2, + float x3, float y3) { + mActionList.add(new RCubicToAction(x1, y1, x2, y2, x3, y3)); + } + + /** + * Approximate the Path with a series of line segments. + * This returns float[] with the array containing point components. + * There are three components for each point, in order: + *
    + *
  • Fraction along the length of the path that the point resides
  • + *
  • The x coordinate of the point
  • + *
  • The y coordinate of the point
  • + *
+ *

Two points may share the same fraction along its length when there is + * a move action within the Path.

+ * + * @param acceptableError The acceptable error for a line on the + * Path. Typically this would be 0.5 so that + * the error is less than half a pixel. + * @return An array of components for points approximating the Path. + */ + public float[] approximate(float acceptableError) { + PointMap pm = new PointMap(); + for (PathAction pa : mActionList) { + pa.approximate(pm, mTemp, acceptableError); + } + return pm.generate(); + } + } + + private abstract static class PathAction { + public abstract void approximate(PointMap pointMap, float[] startPoint, float acceptableError); + } + + private static class MoveAction extends PathAction { + + private float mX; + private float mY; + + public MoveAction(float x, float y) { + mX = x; + mY = y; + } + + @Override + public void approximate(PointMap pointMap, float[] startPoint, float acceptableError) { + float x = mX; + float y = mY; + pointMap.add(0f, x, y, pointMap.totalLength); + startPoint[0] = x; + startPoint[1] = y; + } + } + + private static class RMoveAction extends PathAction { + + private float mDx; + private float mDy; + + public RMoveAction(float dx, float dy) { + mDx = dx; + mDy = dy; + } + + @Override + public void approximate(PointMap pointMap, float[] startPoint, float acceptableError) { + float x = startPoint[0] + mDx; + float y = startPoint[1] + mDy; + pointMap.add(0f, x, y, pointMap.totalLength); + startPoint[0] = x; + startPoint[1] = y; + } + } + + private static class LineToAction extends PathAction { + + private float mX; + private float mY; + + public LineToAction(float x, float y) { + mX = x; + mY = y; + } + + @Override + public void approximate(PointMap pointMap, float[] startPoint, float acceptableError) { + if (pointMap.size == 0) { + // No start point in map, just add it + pointMap.add(0f, startPoint[0], startPoint[1], 0f); + } + + float x = mX; + float y = mY; + float length = dist(startPoint[0], startPoint[1], x, y) + + pointMap.totalLength; + pointMap.add(0f, x, y, length); + pointMap.totalLength = length; + startPoint[0] = x; + startPoint[1] = y; + } + } + + private static class RLineToAction extends PathAction { + + private float mDx; + private float mDy; + + public RLineToAction(float dx, float dy) { + mDx = dx; + mDy = dy; + } + + @Override + public void approximate(PointMap pointMap, float[] startPoint, float acceptableError) { + if (pointMap.size == 0) { + // No start point in map, just add it + pointMap.add(0f, startPoint[0], startPoint[1], 0f); + } + + float x = startPoint[0] + mDx; + float y = startPoint[1] + mDy; + float length = (float) Math.hypot(mDx, mDy) + + pointMap.totalLength; + pointMap.add(0f, x, y, length); + pointMap.totalLength = length; + startPoint[0] = x; + startPoint[1] = y; + } + } + + private static class QuadToAction extends PathAction { + + private float mX1; + private float mY1; + private float mX2; + private float mY2; + + public QuadToAction(float x1, float y1, float x2, float y2) { + mX1 = x1; + mY1 = y1; + mX2 = x2; + mY2 = y2; + } + + @Override + public void approximate(PointMap pointMap, float[] startPoint, float acceptableError) { + float[] control = new float[]{ + startPoint[0], startPoint[1], + mX1, mY1, + mX2, mY2 + }; + bezierApproximate(pointMap, control, acceptableError, false); + startPoint[0] = control[0]; + startPoint[1] = control[1]; + } + } + + private static class RQuadToAction extends PathAction { + + private float mDx1; + private float mDy1; + private float mDx2; + private float mDy2; + + public RQuadToAction(float dx1, float dy1, float dx2, float dy2) { + mDx1 = dx1; + mDy1 = dy1; + mDx2 = dx2; + mDy2 = dy2; + } + + @Override + public void approximate(PointMap pointMap, float[] startPoint, float acceptableError) { + float startX = startPoint[0]; + float startY = startPoint[1]; + float[] control = new float[]{ + startX, startY, + startX + mDx1, startY + mDy1, + startX + mDx2, startY + mDy2 + }; + bezierApproximate(pointMap, control, acceptableError, false); + startPoint[0] = control[0]; + startPoint[1] = control[1]; + } + } + + private static class CubicToAction extends PathAction { + + private float mX1; + private float mY1; + private float mX2; + private float mY2; + private float mX3; + private float mY3; + + public CubicToAction(float x1, float y1, float x2, float y2, float x3, float y3) { + mX1 = x1; + mY1 = y1; + mX2 = x2; + mY2 = y2; + mX3 = x3; + mY3 = y3; + } + + @Override + public void approximate(PointMap pointMap, float[] startPoint, float acceptableError) { + float[] control = new float[]{ + startPoint[0], startPoint[1], + mX1, mY1, + mX2, mY2, + mX3, mY3 + }; + bezierApproximate(pointMap, control, acceptableError, true); + startPoint[0] = control[0]; + startPoint[1] = control[1]; + } + } + + private static class RCubicToAction extends PathAction { + + private float mDx1; + private float mDy1; + private float mDx2; + private float mDy2; + private float mDx3; + private float mDy3; + + public RCubicToAction(float dx1, float dy1, float dx2, float dy2, float dx3, float dy3) { + mDx1 = dx1; + mDy1 = dy1; + mDx2 = dx2; + mDy2 = dy2; + mDx3 = dx3; + mDy3 = dy3; + } + + @Override + public void approximate(PointMap pointMap, float[] startPoint, float acceptableError) { + float startX = startPoint[0]; + float startY = startPoint[1]; + float[] control = new float[]{ + startX, startY, + startX + mDx1, startY + mDy1, + startX + mDx2, startY + mDy2, + startX + mDx3, startY + mDy3, + }; + bezierApproximate(pointMap, control, acceptableError, true); + startPoint[0] = control[0]; + startPoint[1] = control[1]; + } + } + // Divides Bezier curves until linear interpolation is very close to accurate, using // errorSquared as a metric. Cubic Bezier curves can have an inflection point that improperly // short-circuit subdivision. If you imagine an S shape, the top and bottom points being the @@ -235,14 +717,27 @@ public class PathInterpolator implements Interpolator { // the point. It is clearly not the case that we can linearly interpolate at that point. // doubleCheckDivision forces a second examination between subdivisions to ensure that linear // interpolation works. - private static float[] bezierApproximate(float[] control, float errorSquared, - boolean isCubic) { - PointMap pointMap = new PointMap(); + private static void bezierApproximate(PointMap pointMap, float[] control, + float acceptableError, boolean isCubic) { + float errorSquared = acceptableError * acceptableError; - pointMap.add(0f, 0f, 0f); - pointMap.add(1f, 1f, 1f); + if (pointMap.size == 0) { + // No start point in map, just add it + pointMap.add(0f, control[0], control[1], 0f); + } - PointMap.Iterator iter = pointMap.iterator(); + PointMap.Iterator iter = pointMap.lastIterator(); + // Make sure start t is 0 + iter.link.t = 0f; + + // Add end point + if (isCubic) { + pointMap.add(1f, control[6], control[7], 0); + } else { + pointMap.add(1f, control[4], control[5], 0); + } + + // Get next iterator PointMap.Iterator next = iter.nextIterator(); boolean doubleCheckDivision = isCubic; @@ -277,7 +772,8 @@ public class PathInterpolator implements Interpolator { } } if (needsSubdivision) { - next.addBefore(midT, midPointX, midPointY); + // Add length soon + next.addBefore(midT, midPointX, midPointY, 0); } else { float length = dist(iterLink.x, iterLink.y, nextLink.x, nextLink.y); iterLink.f = pointMap.totalLength; @@ -289,15 +785,10 @@ public class PathInterpolator implements Interpolator { next = next.nextIterator(); } - iter.link.f = pointMap.totalLength; - - return pointMap.generate(); - } - - private static float dist(float x1, float y1, float x2, float y2) { - final float x = (x2 - x1); - final float y = (y2 - y1); - return (float) Math.hypot(x, y); + PointMap.Link iterLink = iter.link; + iterLink.f = pointMap.totalLength; + control[0] = iterLink.x; + control[1] = iterLink.y; } private static boolean isOutOfErrorSquared(float midX, float midY, @@ -310,17 +801,17 @@ public class PathInterpolator implements Interpolator { private static float bezierX(float t, float[] control, boolean isCubic) { if (isCubic) { - return cubicCoordinateCalculation(t, 0f, control[0], control[2], 1f); + return cubicCoordinateCalculation(t, control[0], control[2], control[4], control[6]); } else { - return quadraticCoordinateCalculation(t, 0f, control[0], 1f); + return quadraticCoordinateCalculation(t, control[0], control[2], control[4]); } } private static float bezierY(float t, float[] control, boolean isCubic) { if (isCubic) { - return cubicCoordinateCalculation(t, 0f, control[1], control[3], 1f); + return cubicCoordinateCalculation(t, control[1], control[3], control[5], control[7]); } else { - return quadraticCoordinateCalculation(t, 0f, control[1], 1f); + return quadraticCoordinateCalculation(t, control[1], control[3], control[5]); } } @@ -339,96 +830,10 @@ public class PathInterpolator implements Interpolator { + (3 * oneMinusT * tSquared * p2) + (tCubed * p3); } - private static final class PointMap { - - private int size = 0; - - private Link firstLink; - private Link lastLink; - - float totalLength = 0f; - - private static final class Link { - float t; - float x; - float y; - float f = -1; - - private Link previous, next; - - Link(float t, float x, float y, Link p, Link n) { - this.t = t; - this.x = x; - this.y = y; - this.previous = p; - this.next = n; - } - } - - public static final class Iterator { - - final PointMap parent; - Link link; - - private Iterator(PointMap p, Link l) { - parent = p; - link = l; - } - - boolean hasNext() { - return link.next != null; - } - - Iterator nextIterator() { - if (hasNext()) { - return new Iterator(parent, link.next); - } else { - return null; - } - } - - // Move iterator to the new data - void addBefore(float t, float x, float y) { - Link p = link.previous; - Link l = new Link(t, x, y, p, link); - link.previous = l; - p.next = l; - link = l; - parent.size++; - } - } - - public void add(float t, float x, float y) { - Link link = new Link(t, x, y, null, null); - if (lastLink != null) { - link.previous = lastLink; - lastLink.next = link; - } - lastLink = link; - if (firstLink == null) { - firstLink = link; - } - size++; - } - - // Get a iterator at first data or null - public Iterator iterator() { - if (firstLink != null) { - return new Iterator(this, firstLink); - } else { - return null; - } - } - - public float[] generate() { - int index = 0; - float[] result = new float[size * 3]; - for (Link link = firstLink; link != null; link = link.next) { - result[index++] = link.f / totalLength; - result[index++] = link.x; - result[index++] = link.y; - } - return result; - } + public static float dist(float x1, float y1, float x2, float y2) { + final float x = (x2 - x1); + final float y = (y2 - y1); + return (float) Math.hypot(x, y); } + }