blob: 94bb8e67727ab5ebb2d3bbc7d51d5b221866f8cf [file] [log] [blame]
////////////////////////////////////////////////////////////////////////////////
//
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You 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 mx.charts.chartClasses
{
import flash.display.Graphics;
import flash.geom.Point;
import flash.geom.Rectangle;
import mx.graphics.IFill;
import mx.graphics.IStroke;
import mx.graphics.LinearGradient;
import mx.graphics.RadialGradient;
import mx.graphics.SolidColor;
/**
* A set of internal graphics rendering utilities
* used by the various chart classes.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public class GraphicsUtilities
{
include "../../core/Version.as";
//--------------------------------------------------------------------------
//
// Class constants
//
//--------------------------------------------------------------------------
/**
* @private
*/
private static var LINE_FORM:Object =
{
segment: 0,
step: 1,
vertical: 2,
horizontal: 3,
reverseStep: 4,
curve: 5
}
//--------------------------------------------------------------------------
//
// Class methods
//
//--------------------------------------------------------------------------
/**
* Applies the values in a Stroke to the provded Graphics object. This utility function
* applies the Stroke passed in, or clears the line style of the provided Graphics object
* if the <code>ls</code> parameter is set to <code>null</code>.
*
* @param g The Graphics object to modify.
* @param ls The IStroke instance to apply; set to <code>null</code> to render with no Stroke.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public static function setLineStyle(g:Graphics, ls:IStroke):void
{
if (!ls)
g.lineStyle(0, 0, 0);
else
ls.apply(g,null,null);
}
/**
* Fill a rectangle using the provided IFill and IStroke objects.
* This utility function fills the provided rectangle in the Graphics object
* with the provided Fill and Stroke. If no Stroke is provided, the rectangle is filled
* with no border. If no Fill is provided, the rectangle is drawn with no fill.
*
* @param g The Graphics object to draw into.
* @param left The left of the rectangle to fill.
* @param top The top of the rectangle to fill.
* @param right The right of the rectangle to fill.
* @param bottom The bottom of the rectangle to fill.
* @param fill The IFill object to fill the rectangle with. Set this parameter to null to draw a rectangle with no Fill.
* @param ls The stroke object to draw the rectangle with. Set this parameter to null to draw a rectangle with no Stroke.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public static function fillRect(g:Graphics, left:Number, top:Number,
right:Number, bottom:Number,
fill:Object = null, ls:Object = null):void
{
var fillIsColor:Boolean;
g.moveTo(left, top);
if (fill != null)
{
if (fill is IFill)
{
fillIsColor = false;
fill.begin(g,
new Rectangle(left, top, right - left, bottom - top),null);
}
else
{
fillIsColor = true;
g.beginFill(uint(fill));
}
}
if (ls != null)
ls.apply(g,null,null);
g.lineTo(right, top);
g.lineTo(right, bottom);
g.lineTo(left, bottom);
g.lineTo(left, top);
if (fill != null)
g.endFill();
}
/**
* @private
*/
private static function convertForm(v:Object):Number
{
if (typeof(v) == "number")
{
return Number(v);
}
else
{
switch (v)
{
case "segment":
default:
{
return LINE_FORM.segment;
}
case "curve":
{
return LINE_FORM.curve;
}
case "step":
{
return LINE_FORM.step;
}
case "horizontal":
{
return LINE_FORM.horizontal;
}
case "vertical":
{
return LINE_FORM.vertical;
}
case "reverseStep":
{
return LINE_FORM.reverseStep;
}
}
}
return NaN;
}
/**
* Draws a line connecting the datapoints using one of various forms.
* @param g The Graphics object to draw into.
* @param pts An Array of datapoints that define the line.
* @param start The index of the first point in the <code>pts</code> Array to use when drawing the line.
* @param end The index of the last point, exclusive, to use when drawing the line.
* @param hProp The name of the property in the objects contained in the <code>pts</code> Array that defines the horizontal position of the points in the line.
* @param vProp The name of the property in the objects contained in the <code>pts</code> Array that defines the vertical position of the points in the line.
* @param stroke The Stroke used to render the line.
* @param form The form to render the line with. Valid values are "segment","horizontal","vertical","step","reverseStep", or "curve". These forms are similar to the LineSeries class's <code>form</code> property.
* @param moveToStart Indicates whether to first move the pen to the beginning of the line before drawing. Pass <code>false</code> to use this function to continue a line in progress.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public static function drawPolyLine(g:Graphics, pts:Array /* of Object */,
start:int, end:int,
hProp:String, vProp:String,
stroke:IStroke, form:Object,
moveToStart:Boolean = true):void
{
if (start == end)
return;
var originalStart:int = start;
form = convertForm(form);
if (stroke)
stroke.apply(g,null,null);
var len:Number;
var incr:int;
var i:int;
var w:Number;
var c:Number;
var a:Number;
var reverse:Boolean = start > end;
if (reverse)
incr = -1;
else
incr = 1;
if (moveToStart)
g.moveTo(pts[start][hProp], pts[start][vProp]);
else
g.lineTo(pts[start][hProp], pts[start][vProp]);
start += incr;
if (form == LINE_FORM.segment)
{
for (i = start; i != end;i += incr)
{
g.lineTo(pts[i][hProp], pts[i][vProp]);
}
}
else if (form == LINE_FORM.step)
{
for (i = start; i != end; i += incr)
{
g.lineTo(pts[i][hProp], pts[i - incr][vProp]);
g.lineTo(pts[i][hProp], pts[i][vProp]);
}
}
else if (form == LINE_FORM.reverseStep)
{
for (i = start; i != end; i += incr)
{
g.lineTo(pts[i - incr][hProp], pts[i][vProp]);
g.lineTo(pts[i][hProp], pts[i][vProp]);
}
}
else if (form == LINE_FORM.horizontal)
{
if (reverse)
{
for (i = start; i != end; i += incr)
{
g.lineStyle(0, 0, 0);
g.lineTo(pts[i - incr][hProp], pts[i][vProp]);
if (stroke)
stroke.apply(g,null,null);
g.lineTo(pts[i][hProp], pts[i][vProp]);
}
}
else
{
for (i = start; i != end; i += incr)
{
g.lineStyle(0, 0, 0);
g.lineTo(pts[i - incr][hProp], pts[i - incr][vProp]);
if (stroke)
stroke.apply(g,null,null);
g.lineTo(pts[i][hProp], pts[i - incr][vProp]);
}
}
}
else if (form == LINE_FORM.vertical)
{
if (reverse)
{
for (i = start; i != end; i += incr)
{
g.lineStyle(0, 0, 0);
g.lineTo(pts[i - incr][hProp], pts[i][vProp]);
if (stroke)
stroke.apply(g,null,null);
g.lineTo(pts[i][hProp], pts[i][vProp]);
}
}
else
{
for (i = start; i != end; i += incr)
{
g.lineStyle(0, 0, 0);
g.lineTo(pts[i][hProp], pts[i - incr][vProp]);
if (stroke)
stroke.apply(g,null,null);
g.lineTo(pts[i][hProp], pts[i][vProp]);
}
}
}
else if (form == LINE_FORM.curve)
{
start = originalStart;
var innerEnd:int = end - incr;
// Check for coincident points at the head of the list.
// We'll skip over any of those
while (start != end && ((start + incr) != end))
{
if (pts[start + incr][hProp] != pts[start][hProp] ||
pts[start + incr][vProp] != pts[start][vProp])
{
break;
}
start += incr;
}
if (start == end || start + incr == end)
return;
if (Math.abs(end - start) == 2)
{
g.lineTo(pts[start + incr][hProp], pts[start + incr][vProp]);
return;
}
var tanLeft:Point = new Point();
var tanRight:Point = new Point();
var tangentLengthPercent:Number = 0.25;
if (reverse)
tangentLengthPercent *= -1;
var j:int= start;
// First, find the normalized vector
// from the 0th point TO the 1st point
var v1:Point = new Point();
var v2:Point = new Point(pts[j + incr][hProp] - pts[j][hProp],
pts[j + incr][vProp] - pts[j][vProp]);
var tan:Point = new Point();
var p1:Point = new Point();
var p2:Point = new Point();
var mp:Point = new Point();
len = Math.sqrt(v2.x * v2.x + v2.y * v2.y);
v2.x /= len;
v2.y /= len;
// Now later on we'll be using the tangent to the curve
// to define the control point.
// But since we're at the end, we don't have a tangent.
// Instead, we'll just use the first segment itself as the tangent.
// The effect will be that the first curve will start along the
// polyline.
// Now extend the tangent to get a control point.
// The control point is expressed as a percentage
// of the horizontal distance beteen the two points.
var tanLenFactor:Number = pts[j + incr][hProp] - pts[j][hProp];
var prevNonCoincidentPt:Object = pts[j];
// Here's the basic idea.
// On any given iteration of this loop, we're going to draw the
// segment of the curve from the nth-1 sample to the nth sample.
// To do that, we're going to compute the 'tangent' of the curve
// at the two samples.
// We'll use these two tangents to find two control points
// between the two samples.
// Each control point is W pixels along the tangent at the sample,
// where W is some percentage of the horizontal distance
// between the samples.
// We then take the two control points, and find the midpoint
// of the line between them.
// Then we're ready to draw.
// We draw two quadratic beziers, one from sample N-1
// to the midpoint, with control point N-1, and one
// from the midpoint to sample N, with the control point N.
for (j += incr; j != innerEnd; j += incr)
{
// Check and see if the next point is coincident.
// If it is, we'll skip forward.
if (pts[j + incr][hProp] == pts[j][hProp] &&
pts[j + incr][vProp] == pts[j][vProp])
{
continue;
}
// v1 is the normalized vector from the nth point
// to the nth-1 point.
// Since we already computed from nth-1 to nth,
// we can just invert it.
v1.x = -v2.x
v1.y = -v2.y;
// Now compute the normalized vector from nth to nth+1.
v2.x = pts[j + incr][hProp] - pts[j][hProp];
v2.y = pts[j + incr][vProp] - pts[j][vProp];
len = Math.sqrt(v2.x * v2.x + v2.y * v2.y);
v2.x /= len;
v2.y /= len;
// Now compute the 'tangent' of the C1 curve
// formed by the two vectors.
// Since they're normalized, that's easy to find...
// It's the vector that runs between the two endpoints.
// We normalize it, as well.
tan.x = v2.x - v1.x;
tan.y = v2.y - v1.y;
var tanlen:Number = Math.sqrt(tan.x * tan.x + tan.y * tan.y);
tan.x /= tanlen;
tan.y /= tanlen;
// Optionally, if the vertical direction of the curve
// reverses itself, we'll pin the tangent to be horizontal.
// This works well for typical, well spaced chart lines,
// not so well for arbitrary curves.
if (v1.y * v2.y >= 0)
tan = new Point(1, 0);
// Find the scaled tangent we'll use
// to calculate the control point.
tanLeft.x = -tan.x * tanLenFactor * tangentLengthPercent;
tanLeft.y = -tan.y * tanLenFactor * tangentLengthPercent;
if (j == (incr+start))
{
// The first segment starts along the polyline,
// so we only draw a single quadratic.
// g.moveTo(pts[j - incr].x, pts[j - incr].y);
g.curveTo(pts[j][hProp] + tanLeft.x,
pts[j][vProp] + tanLeft.y,
pts[j][hProp],
pts[j][vProp]);
}
else
{
// Determine the two control points...
p1.x = prevNonCoincidentPt[hProp] + tanRight.x;
p1.y = prevNonCoincidentPt[vProp] + tanRight.y;
p2.x = pts[j][hProp] + tanLeft.x;
p2.y = pts[j][vProp] + tanLeft.y;
// and the midpoint of the line between them.
mp.x = (p1.x+p2.x)/2
mp.y = (p1.y+p2.y)/2;
// Now draw our two quadratics.
g.curveTo(p1.x, p1.y, mp.x, mp.y);
g.curveTo(p2.x, p2.y, pts[j][hProp], pts[j][vProp]);
}
// We're about to move on to the nth to the nth+1 segment
// of the curve...so let's flip the tangent at n,
// and scale it for the horizontal distance from n to n+1.
tanLenFactor = pts[j + incr][hProp] - pts[j][hProp];
tanRight.x = tan.x * tanLenFactor * tangentLengthPercent;
tanRight.y = tan.y * tanLenFactor * tangentLengthPercent;
prevNonCoincidentPt = pts[j];
}
// Now in theory we're going to draw our last curve,
// which, like the first, is only a single quadratic,
// ending at the last sample.
// If we try and draw two curves back to back, in reverse order,
// they don't quite match.
// I'm not sure whether this is expected, based on the definition
// of a quadratic bezier, or a bug in the player.
// Regardless, to work around this, we'll draw the last segment
// backwards.
g.curveTo(prevNonCoincidentPt[hProp] + tanRight.x,
prevNonCoincidentPt[vProp] + tanRight.y,
pts[j][hProp], pts[j][vProp]);
}
}
/**
* Draws an arc in the target Graphics object.
* @param g The Graphics object to draw into.
* @param x The horizontal origin of the arc.
* @param y The vertical origin of the arc.
* @param startAngle The starting angle, in radians, of the arc.
* @param arc The sweep, in radians, of the arc.
* @param radius The horizontal radius, in pixels, of the arc
* @param yRadius The vertical radius, in pixels, of the arc. If unspecified, it is assumed to be the same as the radius
* @param continueFlag Indicates whether the routine should move the graphics pen to the beginning of the arc before drawing.
* Set to <code>true</code> to continue drawing a line that is already in progress.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public static function drawArc(g:Graphics, x:Number, y:Number,
startAngle:Number, arc:Number,
radius:Number, yRadius:Number = NaN,
continueFlag:Boolean = false):void
{
if (isNaN(yRadius))
yRadius = radius;
var segAngle:Number
var theta:Number
var angle:Number
var angleMid:Number
var segs:Number
var ax:Number
var ay:Number
var bx:Number
var by:Number
var cx:Number
var cy:Number;
if (Math.abs(arc) > 2 * Math.PI)
arc = 2 * Math.PI;
segs = Math.ceil(Math.abs(arc) / (Math.PI / 4));
segAngle = arc / segs;
theta = -segAngle;
angle = -startAngle;
if (segs > 0)
{
ax = x + Math.cos(startAngle) * radius;
ay = y + Math.sin(-startAngle) * yRadius;
if (continueFlag == true)
g.lineTo(ax, ay);
else
g.moveTo(ax, ay);
for (var i:uint = 0; i < segs; i++)
{
angle += theta;
angleMid = angle - theta / 2;
bx = x + Math.cos(angle) * radius;
by = y + Math.sin(angle) * yRadius;
cx = x + Math.cos(angleMid) * (radius / Math.cos(theta / 2));
cy = y + Math.sin(angleMid) * (yRadius / Math.cos(theta / 2));
g.curveTo(cx, cy, bx, by);
}
}
}
/**
* Converts a style value into a Fill object. This convenience method converts a value assigned through styles into a Fill object
* that can be used to fill an area on the screen. If the value is numeric, this function converts it into a corresponding SolidColor.
* @param v The value to convert into a Fill.
* @return The corresponding IFill object.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public static function fillFromStyle(v:Object):IFill
{
if (v is IFill)
return IFill(v);
else if (v != null)
return IFill(new SolidColor(uint(v)));
else
return null;
}
/**
* Converts a fill value into a solid color. This convenience method pulls a color value out of a Fill
* that best approximates the Fill on the screen.
* @param f The Fill object to extract a color from.
* @return A color value representing the Fill.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public static function colorFromFill(f:Object):uint
{
var color:uint;
if (f == null)
{
}
else if (f is SolidColor)
{
color = SolidColor(f).color;
}
else if (f is LinearGradient &&
LinearGradient(f).entries.length > 0)
{
color = LinearGradient(f).entries[0].color;
}
else if (f is RadialGradient &&
RadialGradient(f).entries.length > 0)
{
color = RadialGradient(f).entries[0].color;
}
else if (f is Number || f is uint || f is int)
{
color = uint(f);
}
return color;
}
}
}