/*******************************************************************************
* 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.paints;
import java.util.ArrayList;
import android.graphics.Canvas;
import android.graphics.LinearGradient;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.RectF;
import android.graphics.Shader.TileMode;
import com.pixate.freestyle.cg.math.PXVector;
import com.pixate.freestyle.util.ObjectPool;
import com.pixate.freestyle.util.ObjectUtil;
import com.pixate.freestyle.util.PXColorUtil;
import com.pixate.freestyle.util.PXLog;
/**
* PX linear gradient representation.
*/
public class PXLinearGradient extends PXGradient {
public enum PXLinearGradientDirection {
TO_TOP, TO_TOP_RIGHT, TO_RIGHT, TO_BOTTOM_RIGHT, TO_BOTTOM, TO_BOTTOM_LEFT, TO_LEFT, TO_TOP_LEFT
};
public enum PXAngleType {
ANGLE, POINTS, DIRECTION
};
private static final String TAG = "PXLinearGradient";
private static final PointF POINT_ZERO = new PointF();
private float angle;
private PXAngleType angleType;
private PXLinearGradientDirection gradientDirection;
private PointF p1;
private PointF p2;
/**
* Constructs a new {@link PXLinearGradient} with a default top-to-bottom
* direction, and zero start & end points.
*/
public PXLinearGradient() {
angle = 90.0f;
p1 = POINT_ZERO;
p2 = POINT_ZERO;
angleType = PXAngleType.ANGLE;
}
/**
* The angle to be used when calculating the rendering of this gradient.
* Note that setting this value overrides any values set using points or
* gradient directions.
*/
public float getAngle() {
return angle;
}
/**
* Sets the angle
*
* @param angle
*/
public void setAngle(float angle) {
this.angle = angle;
angleType = PXAngleType.ANGLE;
}
/**
* Angles in Android and CSS differ. This is a convenience property that
* allows angles to follow the CSS specification's definition of an angle.
* Note that setting this value overrides any values set using points or
* gradient directions.
*/
public float getCssAngle() {
return angle + 90.0f;
}
/**
* Sets a CSS angle.
*
* @param angle
*/
public void setCssAngle(float angle) {
this.angle = angle - 90.0f;
}
/**
* Angles in Android and Photoshop differ. This is a convenience property
* that allows angles to follow Photoshop's definition of an angle. Note
* that setting this value overrides any values set using points or gradient
* directions.
*/
public float getPsAngle() {
return -angle;
}
/**
* Sets a Photoshop angle.
*
* @param angle
*/
public void setPsAngle(float angle) {
this.angle = -angle;
}
/**
* The first point in the gradient. Note that setting this point overrides
* any values set by angle or gradient direction.
*/
public PointF getP1() {
return p1;
}
/**
* Sets the first point in the gradient.
*
* @param p1
*/
public void setP1(PointF p1) {
this.p1 = p1;
angleType = PXAngleType.POINTS;
}
/**
* The last point in the gradient. Note that setting this point overrides
* any values set by angle or gradient direction.
*/
public PointF getP2() {
return p2;
}
/**
* Sets the last point in the gradient.
*
* @param p2
*/
public void setP2(PointF p2) {
this.p2 = p2;
angleType = PXAngleType.POINTS;
}
public void setGradientDirection(PXLinearGradientDirection gradientDirection) {
this.gradientDirection = gradientDirection;
angleType = PXAngleType.DIRECTION;
}
/*
* (non-Javadoc)
* @see
* com.pixate.freestyle.cg.paints.PXPaint#applyFillToPath(android.graphics
* .Path, android.graphics.Paint, android.graphics.Canvas)
*/
public void applyFillToPath(Path path, Paint paint, Canvas context) {
context.save();
// TODO Clip to path? (may cause gradient distortions on the edges?)
// transform gradient space
context.concat(transform);
// placeholders for gradient points
PointF point1, point2;
if (angleType == PXAngleType.POINTS) {
if (gradientUnits == PXGradientUnits.USER_SPACE) {
point1 = this.p1;
point2 = this.p2;
} else {
// linear-gradient points are based on the shape's bbox, so grab
// that
RectF pathBounds = new RectF();
path.computeBounds(pathBounds, true);
// grab the x,y offset which we will apply later
float left = pathBounds.left;
float top = pathBounds.top;
// grab the positions within the bbox for each point
float p1x = pathBounds.width() * p1.x;
float p1y = pathBounds.height() * p1.y;
float p2x = pathBounds.width() * p2.x;
float p2y = pathBounds.height() * p2.y;
// create final points by offsetting the bbox coordinates by the
// bbox origin
point1 = new PointF(left + p1x, top + p1y);
point2 = new PointF(left + p2x, top + p2y);
}
} else {
RectF pathBounds = new RectF();
path.computeBounds(pathBounds, true);
float angle = this.angle;
if (angleType == PXAngleType.DIRECTION) {
switch (gradientDirection) {
case TO_TOP:
angle = 270.0f;
break;
case TO_TOP_RIGHT: {
PXVector toBottomRight = PXVector.vectorWithStartPoint(new PointF(
pathBounds.left, pathBounds.top), new PointF(pathBounds.right,
pathBounds.bottom));
angle = (float) (Math.toDegrees(toBottomRight.angle()) - 90.0f);
break;
}
case TO_RIGHT:
angle = 0.0f;
break;
case TO_BOTTOM_RIGHT: {
PXVector toBottomLeft = PXVector.vectorWithStartPoint(new PointF(
pathBounds.right, pathBounds.top), new PointF(pathBounds.left,
pathBounds.bottom));
angle = (float) (Math.toDegrees(toBottomLeft.angle()) - 90.0f);
break;
}
case TO_BOTTOM:
angle = 90.0f;
break;
case TO_BOTTOM_LEFT: {
PXVector toTopLeft = PXVector.vectorWithStartPoint(new PointF(
pathBounds.right, pathBounds.bottom), new PointF(pathBounds.left,
pathBounds.top));
angle = (float) (Math.toDegrees(toTopLeft.angle()) - 90.0f);
break;
}
case TO_LEFT:
angle = 180.0f;
break;
case TO_TOP_LEFT: {
PXVector toTopRight = PXVector.vectorWithStartPoint(new PointF(
pathBounds.left, pathBounds.bottom), new PointF(pathBounds.right,
pathBounds.top));
angle = (float) (Math.toDegrees(toTopRight.angle()) - 90.0f);
break;
}
}
}
// normalize between 0 and 360
angle = angle % 360.0f;
while (angle < 0.0f) {
angle += 360.0f;
}
// calculate end points of gradient based on angle
if (angle == 0) {
point1 = new PointF(pathBounds.left, pathBounds.top);
point2 = new PointF(pathBounds.right, pathBounds.top);
} else if (angle == 90) {
point1 = new PointF(pathBounds.left, pathBounds.top);
point2 = new PointF(pathBounds.left, pathBounds.bottom);
} else if (angle == 180) {
point1 = new PointF(pathBounds.right, pathBounds.top);
point2 = new PointF(pathBounds.left, pathBounds.top);
} else if (angle == 270) {
point1 = new PointF(pathBounds.left, pathBounds.bottom);
point2 = new PointF(pathBounds.left, pathBounds.top);
} else {
// find active corner and it's opposite
PointF endCorner = new PointF();
// NOTE: assumes angle is in half-open interval [0,360)
if (0.0f <= angle && angle < 90.0f) {
// top-left
endCorner = new PointF(pathBounds.left, pathBounds.top);
} else if (90.0f <= angle && angle < 180.0f) {
// top-right
endCorner = new PointF(pathBounds.right, pathBounds.top);
} else if (180.0f <= angle && angle < 270.0f) {
// bottom-right
endCorner = new PointF(pathBounds.right, pathBounds.bottom);
} else if (270.0f <= angle && angle < 360.0f) {
// bottom-left
endCorner = new PointF(pathBounds.left, pathBounds.bottom);
} else {
// error
PXLog.e(TAG, "Angle not within the half-closed interval [0,360): %f", angle);
}
// find center
PointF center = new PointF(pathBounds.centerX(), pathBounds.centerY());
// get corner and angle vectors
float radians = (float) Math.toRadians(angle);
PXVector cornerVector = PXVector.vectorWithStartPoint(center, endCorner);
PXVector angleVector = PXVector.vectorWithStartPoint(new PointF(), new PointF(
(float) Math.cos(radians), (float) Math.sin(radians)));
// project corner vector onto angle vector
PXVector projection = cornerVector.projectOnto(angleVector);
// apply results
point1 = new PointF(center.x + projection.getX(), center.y + projection.getY());
point2 = new PointF(center.x - projection.getX(), center.y - projection.getY());
}
}
// do the gradient
Paint p = ObjectPool.paintPool.checkOut(paint);
p.setAntiAlias(true);
p.setShader(getGradient(point1, point2));
// apply the blending mode
p.setXfermode(blendingMode);
// draw
context.drawPath(path, p);
// restore coordinate system
context.restore();
// Check the paint back into the pool
ObjectPool.paintPool.checkIn(p);
}
public PXPaint lightenByPercent(float percent) {
PXLinearGradient result = createCopyWithoutColors();
// copy and lighten colors
for (int color : colors) {
result.addColor(PXColorUtil.lightterByPercent(color, percent));
}
return result;
}
public PXPaint darkenByPercent(float percent) {
PXLinearGradient result = createCopyWithoutColors();
// copy and darken colors
for (int color : colors) {
result.addColor(PXColorUtil.darkenByPercent(color, percent));
}
return result;
}
private PXLinearGradient createCopyWithoutColors() {
PXLinearGradient result = new PXLinearGradient();
// copy properties
result.setAngle(angle);
result.setP1(p1);
result.setP2(p2);
result.setGradientDirection(gradientDirection);
// copy PXGradient properties, but not colors
result.setTransform(new Matrix(transform));
result.offsets = new ArrayList<Float>(offsets);
return result;
}
/*
* (non-Javadoc)
* @see com.pixate.freestyle.cg.paints.PXGradient#equals(java.lang.Object)
*/
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (other instanceof PXLinearGradient && super.equals(other)) {
PXLinearGradient gradient = (PXLinearGradient) other;
if (gradient.angleType == angleType) {
if (angleType == PXAngleType.POINTS) {
return ObjectUtil.areEqual(p1, gradient.p1)
&& ObjectUtil.areEqual(p2, gradient.p2);
} else {
return angle == gradient.angle;
}
}
}
return false;
}
/**
* Allocate and initialize a new linear gradient using the specified
* starting and ending colors
*
* @param startColor The starting color of this gradient
* @param endColor The ending color of this gradient
*/
public static PXLinearGradient gradientFromStartColor(int startColor, int endcolor) {
PXLinearGradient lg = new PXLinearGradient();
lg.addColor(startColor); // TODO: withOffset:0.0f?
lg.addColor(endcolor); // TODO: withOffset:1.0f?
return lg;
}
private LinearGradient getGradient(PointF point1, PointF point2) {
adjustGradientColors();
int[] colors = new int[this.colors.size()];
for (int i = 0; i < colors.length; i++) {
colors[i] = this.colors.get(i);
}
float[] positions = null;
if (!offsets.isEmpty()) {
positions = new float[this.offsets.size()];
for (int i = 0; i < positions.length; i++) {
positions[i] = this.offsets.get(i);
}
}
try {
LinearGradient gradient = new LinearGradient(point1.x, point1.y, point2.x, point2.y,
colors, positions, TileMode.CLAMP);
return gradient;
} catch (Exception e) {
if (PXLog.isLogging()) {
PXLog.e(TAG, e, "Error while instantiating a LinearGradient");
}
return null;
}
}
}