Comparing NME Flash and HTML5 targets’ drawing APIs

NME ( http://www.haxenme.org/ ) is definitely one of the most powerful tools\frameworks you can find in the Haxe world in terms of both productivity and ease of use, it’s the “perfect fit” for a flash developer and it’s simple enough to make anyone productive in few days. Let’s focus a bit on how to push objects on the screen with NME and let’s focus on comparing the HTML5 and Flash targets.
In terms of graphics APIs NME mirrors the APIs available in Flash for AS3, so if you’re familiar with Flash you will find the key concepts and classes you’re used to deal with such as:

  • DisplayObject s (and the display list concept)
  • Bitmap s and BitmapData s (and the concept of raster graphics)
  • Graphics (and the concept of vector graphics)

All these APIs’ packages mirrors the Flash APIs’ too, so flash.display.Sprite just becomes nme.display.Sprite.
The actual transcoding of the classes in nme.display.* will be an existing Flash class when targeting Flash, so if you are familiar with the runtime of Flash the behavior of the compiled swf is very predictable. Further, in case you’re not targeting the Flash runtime the APIs in nme.display.* are still available and they will be transcoded to an open and editable implementation based on existing open source projects if possible (such as the HTML5 target based on Jeash library http://jeash.com/ ).
Let’s then see some code and some tricks that may come in help when pushing some objects on the screen with NME.

We’re going to code a “Particles Fall”, a very simple program made of three functions:

  • init: prepares the environment (class variables initialization, sets the amount of particles, initializes loop)
  • generateParticles: creates numParticles instances enabling to display them
  • onEnterFrame: it’s the loop function, it contains the logic to make the particles “fall”

Each particle is a simple class extending Shape (nme.display.Shape, a simple leaf graphics container which can’t contain further display objects) with color, radius (we’re drawing simple circles) and speed variables and a drawmethod to perform the actual drawing on a given graphics context. Here’s the code in Particle.hx…

package ;

import nme.display.Shape;
import nme.display.Graphics;

class Particle extends Shape {

	public var color:Int;
	public var radius:Float;
	public var speed:Float;

	public function new () {
		super();
		color = 0;
		radius = 1.0;
		speed = 1.0;
	}

	// draws the particle on the given target Graphics
	// or on the own graphics
	public function draw(?target:Graphics):Void
	{

		if(target==null || target==graphics){
			target = graphics;
			target.clear();
		}

		target.beginFill(color);
		target.drawCircle(x,y,radius);
		target.endFill();
	}
}

We’re going to face the same program in 3 different ways in order to see the different behaviors of the same code on the non-flash targets, expecially the HTML5/Jeash one.

The display list approach:

The simplest way to display some object on the screen is leveraging the display list NME provides and delegating to each single particle its own drawing on its own graphics container. Let’s see some code…

package;

import nme.display.Sprite;
import nme.events.Event;
import nme.Lib;

class ParticlesFallDisplayList extends Sprite {

	// main function
	public static function main () {		
		Lib.current.addChild (new ParticlesFallDisplayList ());
	}

	// CONSTANTS:
	// stage width
	inline private static var STAGE_W:Int 				= 512;
	// stage height
	inline private static var STAGE_H:Int 				= 512;
	// amount of particles to display
	inline private static var NUM_PARTICLES:Int			= 500;
	// max radius of a particle
	inline private static var PARTICLE_MAX_RADIUS:Int 	= 10;

	// array containing all particles instances
	private var particles:Array;

	public function new () {

		super ();
		init();
	}

	// initialization function:
	// - initializes particles array
	// - generates particles and populates array
	// - registers an enterframe handler for looping
	private function init():Void
	{

		// initializing array
		particles = new Array();
		// generating particles
		generateParticles();
		// and putting them all on the display list
		for(i in 0...NUM_PARTICLES)
			addChild(particles[i]);
		// then add loop handler
		addEventListener(Event.ENTER_FRAME,onEnterFrame);

	}

	// generates NUM_PARTICLES particles with a random
	// radius color and speed
	private function generateParticles():Void
	{
		var particle:Particle;
		while(particles.length < NUM_PARTICLES){
			// creating a particle
			particle = new Particle();
			// setting a random radius within the max value
			particle.radius = Math.random()*PARTICLE_MAX_RADIUS;
			// getting color components from radius (bigger is brighter)
			var r:Int = cast(particle.radius/PARTICLE_MAX_RADIUS*0xFF);
			var b:Int = cast(particle.radius/PARTICLE_MAX_RADIUS*0xFF);
			var g:Int = cast(particle.radius/PARTICLE_MAX_RADIUS*0xFF);
			// and combining them to one color
			particle.color = g << 8 | b ;  			// then setting the speed (bigger is "heavier") 			particle.speed = particle.radius; 			// letting the particle draw on its own context at 0,0 			particle.draw(); 			// putting it randomly on the x axis within the width 			particle.x = Math.random()*STAGE_W; 			// but keeping it on top 			particle.y = 0.0; 			// storing a reference on the array 			particles.push( 				particle 			); 		} 		 		// then sorting on radius (bigger is nearer) 		particles.sort( 			function(p1:Particle,p2:Particle):Int { 				if(p1.radius==p2.radius) 					return 0; 				return p1.radius>p2.radius?1:-1;
			}
		);

	}

	// loop handler
	private function onEnterFrame(event:Event=null):Void
	{
		// iterating all particles
		var particle:Particle;
		for(i in 0...NUM_PARTICLES){
			// retrieving the particle
			particle = particles[i];
			// moving the particle according to its speed
			particle.y += particle.speed;
			// and moving that to the top whenever it exits the boundaries
			if(particle.y>STAGE_H){
				particle.y = 0;
				particle.x = Math.floor(Math.random()*STAGE_W);
			}

		}
	}

}

Both the compiled swf  and the generated HTML5 document run smoothly with a limited amount of particles such as 500. You may encounter some performance issues with the HTML5 document though whenever the number of particles gets increased too much or if it’s running on old browsers.
When coding haxe you’ve always to keep in mind the capabilities and the strenghts of the platform you’re targeting in order to leverage them and avoid performance issues or even lack of features.
The implementation of the display list feature made available by NME via Jeash to the HTML5 target is actually nesting nodes (mainly Canvas) in the DOM leaving the rendering job to the browser engine.
This approach comes with its pros and cons: the power of the CSS animations, but also the lack of speed when adding and removing child nodes to and from the DOM.
Hence if you’re planning to add and remove tons of display objects at the EnterFrame pace you’ve probably better to choose a different way to achieve better performances.

The graphics approach:

In the previous approach we made each particle render in its own graphics context, what if we share the main graphics context with all the particles and make them render to it? Let’s see some code…

package;

import nme.display.Sprite;
import nme.events.Event;
import nme.Lib;

class ParticlesFallGraphics extends Sprite {

	// main function
	public static function main () {		
		Lib.current.addChild (new ParticlesFallGraphics ());
	}

	// CONSTANTS:
	// stage width
	inline private static var STAGE_W:Int 				= 512;
	// stage height
	inline private static var STAGE_H:Int 				= 512;
	// amount of particles to display
	inline private static var NUM_PARTICLES:Int			= 500;
	// max radius of a particle
	inline private static var PARTICLE_MAX_RADIUS:Int 	= 10;

	// array containing all particles instances
	private var particles:Array;

	public function new () {

		super ();
		init();
	}

	// initialization function:
	// - initializes particles array
	// - generates particles and populates array
	// - registers an enterframe handler for looping
	private function init():Void
	{

		// initializing array
		particles = new Array();
		// generating particles
		generateParticles();
		// then add loop handler
		addEventListener(Event.ENTER_FRAME,onEnterFrame);

	}

	// generates NUM_PARTICLES particles with a random
	// radius color and speed
	private function generateParticles():Void
	{
		var particle:Particle;
		while(particles.length < NUM_PARTICLES){
			// creating a particle
			particle = new Particle();
			// putting it randomly on the x axis within the width
			particle.x = Math.random()*STAGE_W;
			// but keeping that on top
			particle.y = 0.0;
			// setting a random radius within the max value
			particle.radius = Math.random()*PARTICLE_MAX_RADIUS;
			// getting color components from radius (bigger is brighter)
			var r:Int = cast(particle.radius/PARTICLE_MAX_RADIUS*0xFF);
			var b:Int = cast(particle.radius/PARTICLE_MAX_RADIUS*0xFF);
			var g:Int = cast(particle.radius/PARTICLE_MAX_RADIUS*0xFF);
			// and combining them to one color
			particle.color = g << 8 | b ; 			// then setting the speed (bigger is "heavier") 			particle.speed = particle.radius; 			// storing a reference on the array 			particles.push( 				particle 			); 		} 		 		// then sorting on radius (bigger is nearer) 		particles.sort( 			function(p1:Particle,p2:Particle):Int { 				if(p1.radius==p2.radius) 					return 0; 				return p1.radius>p2.radius?1:-1;
			}
		);

	}

	// loop handler
	private function onEnterFrame(event:Event=null):Void
	{
		// clearing the main graphics context
		graphics.clear();
		// iterating all particles
		var particle:Particle;
		for(i in 0...NUM_PARTICLES){
			// retrieving the particle
			particle = particles[i];
			// moving the particle according to its speed
			particle.y += particle.speed;
			// and moving that to the top whenever it exits the boundaries
			if(particle.y>STAGE_H){
				particle.y = 0;
				particle.x = Math.floor(Math.random()*STAGE_W);
			}
			// rendering the particle into the main graphics context
			particle.draw(graphics);
		}
	}

}

In this sample we totally removed the use of the display list and relied to the drawing APIs in order to achieve the same result in a flattened canvas. Inspecting the HTML5 document you’ll see that all those little canvases containing our particles are now disappeared, replaced by a whole big one containing all the graphics.
While the compiled swf runs smoothly, the generated HTML5 document results in hugely different performances depending on the browser: it’s totally choppy on Chrome (v19) but very smooth on Safari (v5.1.5) and Firefox (v12). These performances’ differences are due to each browser’s implementation of the Canvas drawing APIs, in facts we’re stressing them by calling the Particle.draw() method at the EnterFrame pace. Be advised, this approach has a further hidden limitation: Graphics.beginBitmapFill  is still not supported ( http://www.haxenme.org/documentation/features/ ), hence if our particles were bitmaps we could not draw them this way.

The bitmap approach:

So far we’ve seen that adding\removing a DisplayObject by leveraging the display list APIs is an expensive operation, and that drawing to a graphics context may be even more costly in terms of performances and may be limitating because not all the Graphics APIs are fully implemented. The third approach in this tutorial is the blitting approach: we’re going to delegate to each particle its own rendering job and we’re just copying the result on a bitmap at each particle’s position. This way we’ve to draw each particle only once and because we’re not invalidating them we can leverage the speed of the copy operation from Canvas to Canvas. Let’s see the code…

package;

import nme.display.Sprite;
import nme.display.Bitmap;
import nme.display.BitmapData;
import nme.geom.Matrix;
import nme.events.Event;
import nme.Lib;

class ParticlesFallBitmap extends Sprite {

	// main function
	public static function main () {		
		Lib.current.addChild (new ParticlesFallBitmap ());
	}

	// CONSTANTS:
	// stage width
	inline private static var STAGE_W:Int 				= 512;
	// stage height
	inline private static var STAGE_H:Int 				= 512;
	// amount of particles to display
	inline private static var NUM_PARTICLES:Int			= 500;
	// max radius of a particle
	inline private static var PARTICLE_MAX_RADIUS:Int 	= 10;

	// array containing all particles instances
	private var particles:Array;

	// the actual bitmap canvas and its container display object
	private var canvas:BitmapData;
	private var canvasContainer:Bitmap;

	inline private static var CANVAS_CLEAR_COLOR:Int 	= 0xFFFFFF;

	public function new () {

		super ();
		init();
	}

	// initialization function:
	// - initializes particles array
	// - generates particles and populates array
	// - registers an enterframe handler for looping
	private function init():Void
	{

		// initializing array
		particles = new Array();
		// generating particles
		generateParticles();
		// initializing canvas
		canvas = new BitmapData(STAGE_W,STAGE_H,false,CANVAS_CLEAR_COLOR);
		// and adding it to the display list
		canvasContainer = new Bitmap(canvas);
		addChild(canvasContainer);
		// then add loop handler
		addEventListener(Event.ENTER_FRAME,onEnterFrame);

	}

	// generates NUM_PARTICLES particles with a random
	// radius color and speed
	private function generateParticles():Void
	{
		var particle:Particle;
		while(particles.length < NUM_PARTICLES){
			// creating a particle
			particle = new Particle();
			// setting a random radius within the max value
			particle.radius = Math.random()*PARTICLE_MAX_RADIUS;
			// getting color components from radius (bigger is brighter)
			var r:Int = cast(particle.radius/PARTICLE_MAX_RADIUS*0xFF);
			var b:Int = cast(particle.radius/PARTICLE_MAX_RADIUS*0xFF);
			var g:Int = cast(particle.radius/PARTICLE_MAX_RADIUS*0xFF);
			// and combining them to one color
			particle.color = g << 8 | b ; 			// then setting the speed (bigger is "heavier") 			particle.speed = particle.radius; 			// letting the particle draw on its own context at 0,0 			particle.draw(); 			// putting it randomly on the x axis within the width 			particle.x = Math.random()*STAGE_W; 			// but keeping that on top 			particle.y = 0.0; 			// storing a reference on the array 			particles.push( 				particle 			); 		} 		 		// then sorting on radius (bigger is nearer) 		particles.sort( 			function(p1:Particle,p2:Particle):Int { 				if(p1.radius==p2.radius) 					return 0; 				return p1.radius>p2.radius?1:-1;
			}
		);

	}

	// loop handler
	private function onEnterFrame(event:Event=null):Void
	{
		// clearing the main canvas
		canvas.fillRect(canvas.rect,CANVAS_CLEAR_COLOR);
		// creating a drawing matrix
		var matrix:Matrix = new Matrix();
		// iterating all particles
		var particle:Particle;
		for(i in 0...NUM_PARTICLES){
			// retrieving the particle
			particle = particles[i];
			// moving the particle according to its speed
			particle.y += particle.speed;
			// and moving that to the top whenever it exits the boundaries
			if(particle.y>STAGE_H){
				particle.y = 0;
				particle.x = Math.floor(Math.random()*STAGE_W);
			}
			// setting translation coordinates in drawing matrix
			matrix.tx = particle.x;
			matrix.ty = particle.y;
			// rendering the particle into the main graphics context
			canvas.draw(particle,matrix);
		}
	}

}

This approach makes both the compiled swf and the HTML5 document run smoothly with a limited amount of particles. On the HTML5 target though you can notice a huge slowdown when at first all particles are getting drawn (it’s the graphics approach case!!), and a huge performance boost after that event. This should be your preferred way when you’ve to add and remove quickly from and to the display a big amount of objects or when you’ve to deal with browsers slow in manipulating the DOM.

You can get all the code and the MonoDevelop solution on github at this repository.
Enjoy and stay tuned :)

About these ads
3 comments
  1. Franky said:

    Great stuff. Please do something with ThreadRemotingServer and communication from/to Android and Flash. Using it in NME would be of great value.

    • It will be the next one… the Socket Server is ready, I’m finishing the Android client made with AIR :D

  2. Franky said:

    Thanks for announcing it. :) This is great news.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: