/**
* Copyright (c) 2016-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.keyframes.util;
import java.util.Locale;
import com.facebook.keyframes.KFPath;
public abstract class VectorCommand {
private enum SVGCommand {
m(ArgFormat.RELATIVE, 2),
M(ArgFormat.ABSOLUTE, 2),
q(ArgFormat.RELATIVE, 4),
Q(ArgFormat.ABSOLUTE, 4),
c(ArgFormat.RELATIVE, 6),
C(ArgFormat.ABSOLUTE, 6),
l(ArgFormat.RELATIVE, 2),
L(ArgFormat.ABSOLUTE, 2);
public final ArgFormat argFormat;
public final int argCount;
SVGCommand(ArgFormat argFormat, int argCount) {
this.argFormat = argFormat;
this.argCount = argCount;
}
}
enum ArgFormat {
RELATIVE,
ABSOLUTE
}
private static final String SVG_ARG_DELIMITER = ",";
public static VectorCommand createVectorCommand(String svgCommandString) {
SVGCommand cmd = SVGCommand.valueOf(svgCommandString.substring(0, 1));
String[] argsAsString = svgCommandString.substring(1).split(SVG_ARG_DELIMITER);
float[] args = new float[argsAsString.length];
int i = 0;
for (String arg : argsAsString) {
args[i++] = Float.parseFloat(arg);
}
switch (cmd) {
case m:
case M: {
if (checkArguments(cmd, args)) {
return new MoveToCommand(cmd.argFormat, args);
} else {
throw new IllegalArgumentException(String.format(
Locale.US,
"VectorCommand MoveTo requires two arguments, but got %s",
args.toString()));
}
}
case q:
case Q: {
if (checkArguments(cmd, args)) {
return new QuadraticToCommand(cmd.argFormat, args);
} else {
throw new IllegalArgumentException(String.format(
Locale.US,
"VectorCommand QuadraticTo requires four arguments, but got %s",
args.toString()));
}
}
case c:
case C: {
if (checkArguments(cmd, args)) {
return new CubicToCommand(cmd.argFormat, args);
} else {
throw new IllegalArgumentException(String.format(
Locale.US,
"VectorCommand CubicTo requires six arguments, but got %s",
args.toString()));
}
}
case l:
case L: {
if (checkArguments(cmd, args)) {
return new LineToCommand(cmd.argFormat, args);
} else {
throw new IllegalArgumentException(String.format(
Locale.US,
"VectorCommand LineTo requires two arguments, but got %s",
args.toString()));
}
}
default: {
throw new IllegalArgumentException(String.format(
Locale.US,
"Unhandled vector command: %s",
svgCommandString));
}
}
}
public static boolean checkArguments(SVGCommand command, float[] args) {
return command.argCount == args.length;
}
/**
* This function converts a lower order argument array to a higher one.
* @param fromArgs The original arguments
* @param destArgs The array to convert fromArgs upwards into
* @return destArgs representing the original arguments in higher order form.
*/
public static float[] convertUp(float[] startPoint, float[] fromArgs, float[] destArgs) {
if (fromArgs.length >= destArgs.length) {
throw new IllegalArgumentException("convertUp should only be called to convert a lower " +
"order argument array to a higher one.");
}
if (fromArgs.length == 2) {
if (destArgs.length == 4) {
destArgs[0] = (startPoint[0] + fromArgs[0]) / 2f;
destArgs[1] = (startPoint[1] + fromArgs[1]) / 2f;
destArgs[2] = fromArgs[0];
destArgs[3] = fromArgs[1];
} else if (destArgs.length == 6) {
destArgs[0] = startPoint[0] + (fromArgs[0] - startPoint[0]) / 3f;
destArgs[1] = startPoint[1] + (fromArgs[1] - startPoint[1]) / 3f;
destArgs[2] = fromArgs[0] + (startPoint[0] - fromArgs[0]) / 3f;
destArgs[3] = fromArgs[1] + (startPoint[1] - fromArgs[1]) / 3f;
destArgs[4] = fromArgs[0];
destArgs[5] = fromArgs[1];
} else {
throw new IllegalArgumentException(String.format(
"Unknown conversion from %d args to %d",
fromArgs.length,
destArgs.length));
}
} else if (fromArgs.length == 4) {
if (destArgs.length == 6) {
destArgs[0] = startPoint[0] + 2f / 3f * (fromArgs[0] - startPoint[0]);
destArgs[1] = startPoint[1] + 2f / 3f * (fromArgs[1] - startPoint[1]);
destArgs[2] = fromArgs[2] + 2f / 3f * (fromArgs[0] - fromArgs[2]);
destArgs[3] = fromArgs[3] + 2f / 3f * (fromArgs[1] - fromArgs[3]);
destArgs[4] = fromArgs[2];
destArgs[5] = fromArgs[3];
} else {
throw new IllegalArgumentException(String.format(
"Unknown conversion from %d args to %d",
fromArgs.length,
destArgs.length));
}
} else {
throw new IllegalArgumentException(String.format(
"Unknown conversion from %d args to %d",
fromArgs.length,
destArgs.length));
}
return destArgs;
}
final ArgFormat mArgFormat;
final float[] mArgs;
private float[] mRecyclableArgArray;
public VectorCommand(ArgFormat argFormat, float[] args) {
mArgFormat = argFormat;
mArgs = args;
}
/**
* Applies this command to the path.
*/
public abstract void apply(KFPath path);
/**
* A protected method that essentially is a 'static' method describing how to apply this command,
* given a set of arguments and the format of the arguments, to a path. This allows us to have a
* generic interpolate method for all commands.
*/
protected abstract void applyInner(KFPath path, ArgFormat format, float[] args);
/**
* Returns the number of arguments for this particular vector command.
*/
private int getArgumentCount() {
return mArgs.length;
}
/**
* Calculates the path from transitioning from current path to the passed in path given the
* current progress, then writes the result into the destPath. The two commands must be the same.
*/
public void interpolate(
VectorCommand toCommand,
float progress,
KFPath destPath) {
if (mArgFormat != toCommand.mArgFormat) {
throw new IllegalArgumentException(
"Argument format must match between interpolated commands. RELATIVE and ABSOLUTE " +
"coordinates should stay consistent");
}
float[] fromArgs;
float[] toArgs;
float[] destArgs;
VectorCommand higherOrderCommand;
if (this.getArgumentCount() > toCommand.getArgumentCount()) {
// This command is higher order than toCommand.
fromArgs = mArgs;
toArgs = convertUp(destPath.getLastPoint(), toCommand.mArgs, getRecyclableArgArray());
destArgs = getRecyclableArgArray();
higherOrderCommand = this;
} else if (this.getArgumentCount() < toCommand.getArgumentCount()) {
// This command is a lower order than toCommand
fromArgs = convertUp(destPath.getLastPoint(), mArgs, toCommand.getRecyclableArgArray());
toArgs = toCommand.mArgs;
destArgs = toCommand.getRecyclableArgArray();
higherOrderCommand = toCommand;
} else {
fromArgs = mArgs;
toArgs = toCommand.mArgs;
destArgs = getRecyclableArgArray();
higherOrderCommand = this;
}
for (int i = 0, len = destArgs.length; i < len; i++) {
destArgs[i] = interpolateValue(fromArgs[i], toArgs[i], progress);
}
higherOrderCommand.applyInner(destPath, mArgFormat, destArgs);
}
private float[] getRecyclableArgArray() {
if (mRecyclableArgArray == null) {
mRecyclableArgArray = new float[mArgs.length];
}
return mRecyclableArgArray;
}
public static class MoveToCommand extends VectorCommand {
public MoveToCommand(ArgFormat argFormat, float[] args) {
super(argFormat, args);
}
@Override
public void apply(KFPath path) {
applyInner(path, mArgFormat, mArgs);
}
@Override
protected void applyInner(KFPath path, ArgFormat format, float[] args) {
switch (format) {
case RELATIVE: {
path.rMoveTo(args[0], args[1]);
break;
}
case ABSOLUTE: {
path.moveTo(args[0], args[1]);
break;
}
default: {
throw new IllegalArgumentException(String.format(
Locale.US,
"No such argument format %s",
format));
}
}
}
@Override
public void interpolate(
VectorCommand toCommand,
float progress,
KFPath destPath) {
if (!(toCommand instanceof MoveToCommand)) {
throw new IllegalArgumentException("MoveToCommand should only be interpolated with other " +
"instances of MoveToCommand");
}
super.interpolate(toCommand, progress, destPath);
}
}
public static class QuadraticToCommand extends VectorCommand {
public QuadraticToCommand(ArgFormat argFormat, float[] args) {
super(argFormat, args);
}
@Override
public void apply(KFPath path) {
applyInner(path, mArgFormat, mArgs);
}
@Override
protected void applyInner(KFPath path, ArgFormat format, float[] args) {
switch (format) {
case RELATIVE: {
path.rQuadTo(
args[0],
args[1],
args[2],
args[3]);
break;
}
case ABSOLUTE: {
path.quadTo(
args[0],
args[1],
args[2],
args[3]);
break;
}
default: {
throw new IllegalArgumentException(String.format(
Locale.US,
"No such argument format %s",
format));
}
}
}
}
public static class CubicToCommand extends VectorCommand {
public CubicToCommand(ArgFormat argFormat, float[] args) {
super(argFormat, args);
}
@Override
public void apply(KFPath path) {
applyInner(path, mArgFormat, mArgs);
}
@Override
protected void applyInner(KFPath path, ArgFormat format, float[] args) {
switch (format) {
case RELATIVE: {
path.rCubicTo(
args[0],
args[1],
args[2],
args[3],
args[4],
args[5]);
break;
}
case ABSOLUTE: {
path.cubicTo(
args[0],
args[1],
args[2],
args[3],
args[4],
args[5]);
break;
}
default: {
throw new IllegalArgumentException(String.format(
Locale.US,
"No such argument format %s",
format));
}
}
}
}
public static class LineToCommand extends VectorCommand {
public LineToCommand(ArgFormat argFormat, float[] args) {
super(argFormat, args);
}
@Override
public void apply(KFPath path) {
applyInner(path, mArgFormat, mArgs);
}
@Override
protected void applyInner(KFPath path, ArgFormat format, float[] args) {
switch (format) {
case RELATIVE: {
path.rLineTo(args[0], args[1]);
break;
}
case ABSOLUTE: {
path.lineTo(args[0], args[1]);
break;
}
default: {
throw new IllegalArgumentException(String.format(
Locale.US,
"No such argument format %s",
format));
}
}
}
}
private static float interpolateValue(float valueA, float valueB, float progress) {
return valueA + (valueB - valueA) * progress;
}
}