/******************************************************************************* * 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. ******************************************************************************/ /** * Copyright (c) 2012-2013 Pixate, Inc. All rights reserved. */ package com.pixate.freestyle.cg.shapes; import android.graphics.Path; import android.graphics.PointF; import com.pixate.freestyle.cg.math.PXEllipticalArc; import com.pixate.freestyle.cg.math.PXVector; import com.pixate.freestyle.cg.parsing.PathParserHelper; import com.pixate.freestyle.util.ObjectPool; import com.pixate.freestyle.util.PXLog; /** * A PXShape sub-class used to render paths */ public class PXPath extends PXShape { private static String TAG = PXPath.class.getSimpleName(); private Path pathPath; private PointF lastPoint; public PXPath() { pathPath = new Path(); lastPoint = new PointF(); } /** * Generate a new PXPath instance using the specified data This method * parses the specifying data, generating calls to the path building methods * in this class. The data is expected to be in the form as defined by the * SVG 1.1 specification for the path data's d attribute. * * @param data A string of path data * @returns A newly allocated PXPath instance */ public static PXPath createPathFromPathData(String data) { PXPath pxPath = new PXPath(); char[] charData = data.toCharArray(); PathParserHelper helper = new PathParserHelper(charData, 0); helper.skipWhitespace(); float firstX = 0, firstY = 0; float lastX = 0, lastY = 0; float lastHandleX = 0, lastHandleY = 0; float x1, y1, x2, y2, x3, y3; char command = 0; char prevCommand = 0; int length = data.length(); while (helper.pos < length) { helper.skipWhitespace(); if (helper.pos >= length) { break; } command = charData[helper.pos]; switch (command) { case '-': case '+': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': if (prevCommand == 'm') { command = 'l'; break; } else if (prevCommand == 'M') { command = 'L'; break; } else if (prevCommand == 'Z' || prevCommand == 'z') { // possibly error state or may be implied M to first // point of subpath with command becoming l or L break; } else { command = prevCommand; break; } default: { helper.advance(); prevCommand = command; } } switch (command) { case 'M': x1 = helper.nextFloat(); y1 = helper.nextFloat(); pxPath.moveTo(x1, y1); lastX = x1; lastY = y1; firstX = x1; firstY = y1; break; case 'm': x1 = helper.nextFloat(); y1 = helper.nextFloat(); x1 += lastX; y1 += lastY; pxPath.moveTo(x1, y1); lastX = x1; lastY = y1; firstX = x1; firstY = y1; break; case 'L': x1 = helper.nextFloat(); y1 = helper.nextFloat(); pxPath.lineTo(x1, y1); lastX = x1; lastY = y1; break; case 'l': x1 = helper.nextFloat(); y1 = helper.nextFloat(); x1 += lastX; y1 += lastY; pxPath.lineTo(x1, y1); lastX = x1; lastY = y1; break; case 'C': x1 = helper.nextFloat(); y1 = helper.nextFloat(); x2 = helper.nextFloat(); y2 = helper.nextFloat(); x3 = helper.nextFloat(); y3 = helper.nextFloat(); pxPath.cubicBezierTo(x1, y1, x2, y2, x3, y3); lastHandleX = x2; lastHandleY = y2; lastX = x3; lastY = y3; break; case 'c': x1 = helper.nextFloat(); y1 = helper.nextFloat(); x2 = helper.nextFloat(); y2 = helper.nextFloat(); x3 = helper.nextFloat(); y3 = helper.nextFloat(); x1 += lastX; y1 += lastY; x2 += lastX; y2 += lastY; x3 += lastX; y3 += lastY; pxPath.cubicBezierTo(x1, y1, x2, y2, x3, y3); lastHandleX = x2; lastHandleY = y2; lastX = x3; lastY = y3; break; case 'H': x1 = helper.nextFloat(); pxPath.lineTo(x1, lastY); lastX = x1; break; case 'h': x1 = helper.nextFloat(); x1 += lastX; pxPath.lineTo(x1, lastY); lastX = x1; break; case 'V': y1 = helper.nextFloat(); pxPath.lineTo(lastX, y1); lastY = y1; break; case 'v': y1 = helper.nextFloat(); y1 += lastY; pxPath.lineTo(lastX, y1); lastY = y1; break; case 'Q': x1 = helper.nextFloat(); y1 = helper.nextFloat(); x2 = helper.nextFloat(); y2 = helper.nextFloat(); pxPath.quadraticBezierToX1(x1, y1, x2, y2); lastHandleX = x1; lastHandleY = y1; lastX = x2; lastY = y2; break; case 'q': x1 = helper.nextFloat(); y1 = helper.nextFloat(); x2 = helper.nextFloat(); y2 = helper.nextFloat(); x1 += lastX; y1 += lastY; x2 += lastX; y2 += lastY; pxPath.quadraticBezierToX1(x1, y1, x2, y2); lastHandleX = x1; lastHandleY = y1; lastX = x2; lastY = y2; break; case 'A': { PXLog.w(PXPath.class.getSimpleName(), "'A' path command, not supported"); float rx = helper.nextFloat(); float ry = helper.nextFloat(); float xAxisRotation = helper.nextFloat(); float largeArcFlag = helper.nextFloat(); float sweepFlag = helper.nextFloat(); x1 = helper.nextFloat(); y1 = helper.nextFloat(); pxPath.ellipticalArcRadius(rx, ry, xAxisRotation, (largeArcFlag > 0.0), (sweepFlag > 0.0), x1, y1); lastX = x1; lastY = y1; break; } case 'a': PXLog.w(PXPath.class.getSimpleName(), "'a' path command, not supported"); // TODO: Implement this float rx = helper.nextFloat(); float ry = helper.nextFloat(); float xAxisRotation = helper.nextFloat(); float largeArcFlag = helper.nextFloat(); float sweepFlag = helper.nextFloat(); x1 = helper.nextFloat(); y1 = helper.nextFloat(); x1 += lastX; y1 += lastY; pxPath.ellipticalArcRadius(rx, ry, xAxisRotation, (largeArcFlag > 0.0), (sweepFlag > 0.0), x1, y1); lastX = x1; lastY = y1; break; case 'S': x2 = helper.nextFloat(); y2 = helper.nextFloat(); x3 = helper.nextFloat(); y3 = helper.nextFloat(); x1 = (lastX - lastHandleX) + lastX; y1 = (lastY - lastHandleY) + lastY; pxPath.cubicBezierTo(x1, y1, x2, y2, x3, y3); lastHandleX = x2; lastHandleY = y2; lastX = x3; lastY = y3; break; case 's': x2 = helper.nextFloat(); y2 = helper.nextFloat(); x3 = helper.nextFloat(); y3 = helper.nextFloat(); x1 = (lastX - lastHandleX) + lastX; y1 = (lastY - lastHandleY) + lastY; x2 += lastX; y2 += lastY; x3 += lastX; y3 += lastY; pxPath.cubicBezierTo(x1, y1, x2, y2, x3, y3); lastHandleX = x2; lastHandleY = y2; lastX = x3; lastY = y3; break; case 'T': x2 = helper.nextFloat(); y2 = helper.nextFloat(); x1 = (lastX - lastHandleX) + lastX; y1 = (lastY - lastHandleY) + lastY; pxPath.quadraticBezierToX1(x1, y1, x2, y2); lastHandleX = x1; lastHandleY = y1; lastX = x2; lastY = y2; break; case 't': x2 = helper.nextFloat(); y2 = helper.nextFloat(); x1 = (lastX - lastHandleX) + lastX; y1 = (lastY - lastHandleY) + lastY; x2 += lastX; y2 += lastY; pxPath.quadraticBezierToX1(x1, y1, x2, y2); lastHandleX = x1; lastHandleY = y1; lastX = x2; lastY = y2; break; case 'Z': pxPath.close(); prevCommand = '\0'; lastX = firstX; lastY = firstY; break; case 'z': pxPath.close(); prevCommand = '\0'; lastX = firstX; lastY = firstY; break; default: // report error // [Pixate.configuration sendParseMessage:message]; PXLog.e(TAG, "Unrecognized or missing path command at offset: %d", helper.pos); int start = Math.max(0, helper.pos - 10); int end = Math.min(length, start + 30); PXLog.e(TAG, data.substring(start, helper.pos) + " >>> " + data.substring(helper.pos, end)); // stop scanning helper.pos = length; break; } } return pxPath; } /** * Add a close command to the current path * * @return This instance for method chaining. */ protected PXPath close() { pathPath.close(); clearPath(); return this; } /** * Add an arc of an ellipse to the current path * * @param x The x-coordinate of the center of the ellipse * @param y The y-coordinate of the center of the ellipse * @param radiusX The x-radius of the ellipse * @param radiusY The y-radius of the ellipse * @param startAngle The starting angle of the arc * @param endAngle The ending angle of the arc * @return This instance for method chaining. */ void ellipticalArc(float x, float y, float radiusX, float radiusY, float startAngle, float endAngle) { PointF lastPathPoint = PXEllipticalArc.pathAddEllipticalArc(pathPath, null, x, y, radiusX, radiusY, startAngle, endAngle); lastPoint.set(lastPathPoint); } private void ellipticalArcRadius(float radiusX, float radiusY, float xAxisRotation, boolean largeArcFlag, boolean sweepFlag, float x, float y) { if (radiusX != 0.0 || radiusY != 0.0) { float cx, cy; float startAngle, sweepAngle, endAngle; float halfDx = (lastPoint.x - x) * 0.5f; float halfDy = (lastPoint.y - y) * 0.5f; float radians = (float) Math.toRadians(xAxisRotation); float cosine = (float) Math.cos(radians); float sine = (float) Math.sin(radians); float x1p = halfDx * cosine + halfDy * sine; float y1p = halfDx * -sine + halfDy * cosine; float x1px1p = x1p * x1p; float y1py1p = y1p * y1p; float lambda = (x1px1p / (radiusX * radiusX)) + (y1py1p / (radiusY * radiusY)); // it may be impossible for the specified radii to describe // an ellipse passing through the previous point and end point. // Adjust radii, if necessary, so ellipse can pass through those // points. if (lambda > 1.0) { float factor = (float) Math.sqrt(lambda); radiusX *= factor; radiusY *= factor; } float rxrx = radiusX * radiusX; float ryry = radiusY * radiusY; float rxrxryry = rxrx * ryry; float rxrxy1py1p = rxrx * y1py1p; float ryryx1px1p = ryry * x1px1p; float numerator = rxrxryry - rxrxy1py1p - ryryx1px1p; float s; if (numerator < 1e-6) { s = 0.0f; } else { s = (float) Math.sqrt(numerator / (rxrxy1py1p + ryryx1px1p)); } if (largeArcFlag == sweepFlag) { s = -s; } float cxp = s * radiusX * y1p / radiusY; float cyp = s * -radiusY * x1p / radiusX; cx = cxp * cosine - cyp * sine + (lastPoint.x + x) * 0.5f; cy = cxp * sine + cyp * cosine + (lastPoint.y + y) * 0.5f; PXVector u = new PXVector(1f, 0f); // NOTE: SVG spec divides x-component by rx and y-component by ry PXVector v = new PXVector(x1p - cxp, y1p - cyp); PXVector w = new PXVector(-x1p - cxp, -y1p - cyp); startAngle = u.angleBetweenVector(v); sweepAngle = v.angleBetweenVector(w); if (!sweepFlag && sweepAngle > 0.0) { sweepAngle -= PXEllipticalArc.TWO_PI; } else if (sweepFlag && sweepAngle < 0.0) { sweepAngle += PXEllipticalArc.TWO_PI; } endAngle = startAngle + sweepAngle; ellipticalArc(cx, cy, radiusX, radiusY, startAngle, endAngle); } } /** * Add a qcurveto command to the current path * * @param x1 The x-coordinate of the first handle of the quadratic bezier * curve being added * @param y1 The y-coordinate of the first handle of the quadratic bezier * curve being added * @param x2 The x-coordinate of the second handle of the quadratic bezier * curve being added * @param y2 The y-coordinate of the second handle of the quadratic bezier * curve being added * @return This instance for method chaining. */ protected PXPath quadraticBezierToX1(float x1, float y1, float x2, float y2) { pathPath.quadTo(x1, y1, x2, y2); lastPoint.set(x2, y2); clearPath(); return this; } /** * Add a curveto command to the current path * * @param x1 The x-coordinate of the first handle of the cubic bezier curve * being added * @param y1 The y-coordinate of the first handle of the cubic bezier curve * being added * @param x2 The x-coordinate of the second handle of the cubic bezier curve * being added * @param y2 The y-coordinate of the second handle of the cubic bezier curve * being added * @param x3 The x-coordinate of the third handle of the cubic bezier curve * being added * @param y3 The y-coordinate of the third handle of the cubic bezier curve * being added * @return This instance for method chaining. */ protected PXPath cubicBezierTo(float x1, float y1, float x2, float y2, float x3, float y3) { pathPath.cubicTo(x1, y1, x2, y2, x3, y3); lastPoint.set(x3, y3); clearPath(); return this; } /** * Add a lineto command to the current path * * @param x The x-coordinate of the line being added * @param y The y-coordinate of the line being added * @return This instance for method chaining. */ protected PXPath lineTo(float x, float y) { pathPath.lineTo(x, y); lastPoint.set(x, y); clearPath(); return this; } /** * Add a moveto command to the current path * * @param x The x-coordinate of the new position to move to within this path * @param y The y-coordinate of the new position to move to within this path * @return This instance for method chaining. */ protected PXPath moveTo(float x, float y) { pathPath.moveTo(x, y); lastPoint.set(x, y); clearPath(); return this; } /* * (non-Javadoc) * @see com.pixate.freestyle.pxengine.cg.PXShape#newPath() */ @Override protected Path newPath() { return ObjectPool.pathPool.checkOut(pathPath); } }