I am at an embarrassing standstill in developing a simple educational app in Flash Pro (AS3).
I'm trying to make a simple graph showing vertical peaks when something happens (and a flat line when idle). The graph is realtime since it starts with the program and is updated every other frame. The line starts at one end of the "Graph" object and continues until it reaches the other side (say, over 60s). After it reaches the end, the entire graph starts to scroll to the left to make room for more data, which is added at the end. It is designed to keep on scrolling and recording data forever (until user stops/pauses it).
Here's an example: http://img442.imageshack.us/img442/4784/runninggraph.png
What's the best way to implement such a graph? A simple scrolling graph made up of a baseline and ticks.
Shouldn't be so hard... or so I thought. I tried 4 different ways (two of which proved 100% working but too slow). There must be something better out there! Maybe matrix transformations + bitmapFill?
END OF QUESTION; simplified code follows.
public class Graph extends Sprite
{
include "constants.as";
const pixelFactor:Number = (1 / GRAPH_TIMESCALE) * 1050; //60,000ms
const amountToMove:Number = POLLING_RATE * pixelFactor, inverseTS:Number = 1.0/GRAPH_TIMESCALE;
var graphHolder:GraphHolder; // A simple (large) sprite with many Graph instances
var baseLine:Shape = new Shape(), stretched:Boolean = false;
var apArray:Array = [], apRecycle:Vector.<Shape> = new <Shape>[];
var apLength:int = 0, firstPeak:Shape;
//var numberIndicator:TextField = new TextField();
public function Graph(graphHolder:GraphHolder)
{
//Set variables
this.graphHolder = graphHolder;
mouseEnabled = false; mouseChildren = false;
addChild(baseLine);
with (baseLine)
{
x = 55; y = 10; // the base-line. Starts 55 pixels out so labels can be added
graphics.lineStyle(2,0x000000); // Thickness 2, color black
graphics.lineTo(1050,0);
scaleX = 0.0005; // base-line starts as a little dot, expands with time
}
}
// When not scrolling, draws idle baseline. When scrolling, drags peaks backwards.
function drawIdle(timeElapsed:int):void
{
if (timeElapsed <= GRAPH_TIMESCALE) baseLine.scaleX = inverseTS * timeElapsed;
else
{
//if (graphNo < 1) graphHolder.scrollAxis(pixelFactor);
if (!stretched) { baseLine.scaleX = 1.001; stretched = true; }
// scroll all peaks
if (apLength)
{
for (var i:int = 0; i < apLength; i++)
{
apArray[i].x -= amountToMove;
}
firstPeak = apArray[0];
if (firstPeak.x < 55)
{
firstPeak.visible = false;
apRecycle[apRecycle.length] = firstPeak; // peaks are recycled instead of removed
apArray.shift(); // apArray is a linked-list Array; less shift cost
--apLength;
}
}
}
//lbd.copyPixels(graphHolder.baseCache,lbr,new Point(timeElapsed * pixelFactor,0), null, null, true);
//line.graphics.lineTo(timeElapsed * pixelFactor,10);
}
function drawAP(timeElapsed:int):void
{
// Check for a peak in the recycle array
if (apRecycle.length)
{
firstPeak = apRecycle.pop();
firstPeak.x = ( (timeElapsed > GRAPH_TIMESCALE) ? 1050 : (timeElapsed * pixelFactor) ) + 54; //55-1
firstPeak.visible = true;
apArray[apArray.length] = firstPeak;
}
else
{
// Line drawn from baseline up 7 pixels.
var peakShape:Shape = new Shape();
//peakShape.cacheAsBitmap = true;
addChild(peakShape);
with (peakShape)
{
//cacheAsBitmap = true;
graphics.lineStyle(2, 0x000000);
graphics.moveTo(1, 10);
graphics.lineTo(1, 3);
x = ( (timeElapsed > GRAPH_TIMESCALE) ? 1050 : (timeElapsed * pixelFactor) ) + 54; //55-1
apArray[apArray.length] = peakShape;
}
}
++apLength;
}
/* Reset baseline as tiny dot, remove peaks, reset peak array.
All peaks in recycle remain to draw from during next cycle. */
function resetGraph():void
{
baseLine.scaleX = 0.0005; stretched = false;
for each (var peak:Shape in apArray) removeChild(peak);
apArray.length = 0; apLength = 0;
}
}
So again, essentially, there should be tickmarks on the graph indicating "actions" that occur in realtime. The graph starts scrolling to the left after it reaches the end of the screen (at N seconds). In other words, it keeps scrolling indefinitely after it hits the edge of its designated space. Here are the 3 ways I've attempted to implement this -- none of them are fast, and some of them have other problems.
Create and draw a new line segment at the corresponding timepoint on the graph -- the baseline segment (tiny horizontal line) and the tickmark (vertical). The graph only goes up to N seconds, but all tickmarks at times N+x are still drawn as if the graph went on forever to the right. I then use a mask for the rectangular area of the graph and literally move the graph to the left over time once it reaches N seconds.
- This was slow to begin with, but led to incredible slowdown after a few minutes because Flash is apparently still processing the whole graph to the left, off-screen.
- Drawing line segments directly to the graph object is bad because each segment is apparently an object, and each of these myriad objects has no explicit (or implicit) reference after it is drawn. ("Out of sight, out of mind" == slow and wrong.)
Create a bitmapdata object containing the shape of a tickmark (small verticle line) and the baseline (tiny horizontal line), and every frame copypixel the next bitmap (line segment) into place on the graph object (now also a bitmap). ScrollRect to scroll to the left. Simple, very much like #1 except using bitmapData and copyPixel instead of display objects.
- This is bad news because after 2048 pixels, no more data can be drawn to the bitmap.
- Scaling the app caused all the bitmaps to appear rough and jagged, and for gaps to be introduced (despite flagging each bitmap "smoothing on"), likely because of the sheer number of line segments comprising a graph that are all disparate and drawn near other pixel boundaries.
- Scrollrect cannot be accelerated by the GPU (and the app needs to work on mobile devices)
- Maintaining concurrent bitmaps, copying and pasting one onto the other to refresh the graph ("blitting"?) is tedious, but it is also very inaccurate at the subpixel level, unlike the vector techniques. Also, the calculations required to either recalculate all peaks from scratch each frame or create, destroy, and merge multiple width-2048 bitmaps in the background may prove more costly in the long run.
Have one Shape, a horizontal line (baseline), "scale up" to fill the graph, then stop (N seconds). Hence, it is the "baseline" since it appears to stretch across the graph with time and then do nothing after N seconds. Each tick atop the baseline, then, is a Shape object and is drawn on top of the baseline. A foreach loop then changes the x coordinates of all ticks until they reach the left of the graph, at which point they are made invisible and added to a recycle vector which will be used, if it contains anything, for any subsequent peaks coming in from the right (drawn at N).
- Still slow, but much faster than #1 and maintains constant speed when the graph starts scrolling.
- I can't figure out how to speed up the process. I've tried caching the background and the graph and even the line on top as bitmaps (even tried with cacheasbitmapmatrix), but it seems the plain vector rendering is fastest on the debug player.
Tween. Actually, haven't yet implemented the graph itself with tween, but so far, replacing my enterFrame movement handlers (which change coordinates of objects manually each frame) with Tweens has made all the animations much, much slower. Go figure. So I don't think it will help on the graph... I also can't use TweenLite (licensing).
With #3, I think I'm on the right track, but I fear I just don't know enough about the details of AVM rendering. Should I use bitmaps instead of Shapes for the peaks? Should I use the same technique as #1 with #3's objects (i.e. adding the peaks to a big moving "tickSprite" but still "recycling" the peaks when they go offscreen)? In this case, the containing Sprite, despite absence of ticks offscreen, will continue to be pushed to the left offscreen. Will it still take up resources linearly if it is completely empty to the left of the screen?
Suspected speed culprits in #3: - large number of moving Shapes and the for loops required to move them. - Features of the vectors, bitmaps, and backgrounds. For instance, the Graph object itself is just a simple container for a Background shape (cached as bitmap) which is an alpha 0.75 gray rectangle behind the graph, a bunch of labels and axes (also cached as bitmaps), and the graph shapes (one scaling "line" Shape for the baseline and numerous "tick" Shapes stored in a vector)... - Users can drag normal vector objects behind the Graph, which might kill any performance benefit in caching the graph's children as bitmaps since they have to be rendered in motion over everything behind the alpha 0.75 background shape.