AS3 pixel perfect drawing? - actionscript-3

When I'm use:
var shape:Shape = new new Shape();
shape.graphics.lineStyle(2,0);
shape.graphics.lineTo(10,10);
addChild(shape);
I get the black line I want, but I also get grey pixels floating around next to them. Is there a way to turn off whatever smoothing/anti-aliasing is adding the fuzzy pixels?

Yes, it is possible to draw pixel-perfect shapes, even with anti-aliasing on. Pixel-hinting is a must. The other half of the equation is to actually issue the drawing commands with whole-pixel coordinates.
For example, you can draw a pixel-perfectly-symmetrical rounded-rectangle with 4px radius curves with the following code. Pay careful attention to what the code is doing, particularly how the offsets relate to the border thickness.
First, keep in mind that when you're drawing filled shapes, the rasterization occurs up to, but no including the right/lower edges of the outline. So to draw a 4x4 pixel filled square, you can just call drawRect(0,0,4,4). That covers pixels 0,1,2,3,4 (5 pixels), but since it doesn't rasterize the right and lower edges, it ends up being 4 pixels. On the other hand, if you're drawing just the outline (without filling it), then you need to call drawRect(0,0,3,3), which will cover pixels 0,1,2,3, which is 4 pixels. So you actually need slightly different dimensions for the fill vs the outline to get pixel-perfect sizes.
Suppose you wanted to draw a button that's 50px wide, 20px tall, with a 4px radius on its rounded edges, which are 2px thick. In order to ensure that exactly 50x20 pixels are covered, and the outside edge of the 2px thick line buts up against the edge pixels without overflowing, you have to issue the drawing command exactly like this. You must use pixel hinting, and you must offset the rectangle by 1px inward on all sides (not half a pixel, but exactly 1). That places the center of the line exactly between pixels 0 and 1, such that it ends up drawing the 2px wide line through pixels 0 and 1.
Here is an example method that you can use:
public class GraphicsUtils
{
public static function drawFilledRoundRect( g:Graphics, x:Number, y:Number, width:Number, height:Number, ellipseWidth:Number = 0, ellipseHeight:Number = 0, fillcolor:Number = 0xFFFFFF, fillalpha:Number = 1, thickness:Number = 0, color:Number = 0, alpha:Number = 1, pixelHinting:Boolean = false, scaleMode:String = "normal", caps:String = null, joints:String = null, miterLimit:Number = 3 )
{
if (!isNaN( fillcolor))
{
g.beginFill( fillcolor, fillalpha );
g.drawRoundRect( x, y, width, height, ellipseWidth, ellipseHeight );
g.endFill();
}
if (!isNaN(color))
{
g.lineStyle( thickness, color, alpha, pixelHinting, scaleMode, caps, joints, miterLimit );
g.drawRoundRect( x, y, width, height, ellipseWidth, ellipseHeight );
}
}
}
Which you'd want to call like this:
var x:Number = 0;
var y:Number = 0;
var width:Number = 50;
var height:Number = 20;
var pixelHinting:Boolean = true;
var cornerRadius:Number = 4;
var fillColor:Number = 0xffffff; //white
var fillAlpha:Number = 1;
var borderColor:Number = 0x000000; //black
var borderAlpha:Number = 1;
var borderThickness:Number = 2;
GraphicsUtils.drawFilledRoundRect( graphics, x + (borderThickness / 2), y + (borderThickness / 2), width - borderThickness, height - borderThickness, cornerRadius * 2, cornerRadius * 2, fillColor, fillAlpha, borderThickness, borderColor, borderAlpha, pixelHinting );
That will produce a pixel-perfectly-symmetrical 2px thick filled rounded rectangle that covers exactly a 50x20 pixel region.
Its very important to notice that using a borderThickness of zero is somewhat non-sensical, and will result in an rectangle oversized by 1 pixel, because it's still drawing a one-pixel wide line, but it's failing to subtract the width (since its zero), hence you'll get an oversized rectangle.
In summary, use the algorithm above, where you add half the border thickness to the x and y coordinates, and subtract the whole border thickness from the width and height, and always use a minimum thickness of 1. That will always result in a rectangle with a border that occupies and does not overflow a pixel region equivalent to the given width and height.
If you want to see it in action, just copy and paste the following code block into a new AS3 Flash Project on the main timeline and run it, as is, since it includes everything necessary to run:
import flash.display.StageScaleMode;
import flash.display.StageAlign;
import flash.events.Event;
import flash.utils.getTimer;
import flash.display.Sprite;
import flash.display.Graphics;
stage.scaleMode = flash.display.StageScaleMode.NO_SCALE;
stage.align = flash.display.StageAlign.TOP_LEFT;
stage.frameRate = 60;
draw();
function draw():void
{
var x:Number = 10;
var y:Number = 10;
var width:Number = 50;
var height:Number = 20;
var pixelHinting:Boolean = true;
var cornerRadius:Number = 4;
var fillColor:Number = 0xffffff; //white
var fillAlpha:Number = 1;
var borderColor:Number = 0x000000; //black
var borderAlpha:Number = 1;
var borderThickness:Number = 2;
var base:Number = 1.6;
var squares:int = 10;
var rows = 4;
var thicknessSteps:Number = 16;
var thicknessFactor:Number = 4;
var offset:Number;
var maxBlockSize:Number = int(Math.pow( base, squares ));
var globalOffset:Number = maxBlockSize; //leave room on left for animation
var totalSize:Number = powerFactorial( base, squares );
var colors:Array = new Array();
for (i = 1; i <= squares; i++)
colors.push( Math.random() * Math.pow( 2, 24 ) );
for (var j:int = 0; j < thicknessSteps; j++)
{
var cycle:Number = int(j / rows);
var subCycle:Number = j % rows;
offset = cycle * totalSize;
y = subCycle * maxBlockSize;
borderThickness = (j + 1) * thicknessFactor;
for (var i:int = 0; i < squares; i++)
{
cornerRadius = Math.max( 8, borderThickness );
borderColor = colors[i];
x = globalOffset + offset + powerFactorial( base, i ); //int(Math.pow( base, i - 1 ));
width = int(Math.pow( base, i + 1 ));
height = width;
if (borderThickness * 2 > width) //don't draw if border is larger than area
continue;
drawFilledRoundRect( graphics, x + (borderThickness / 2), y + (borderThickness / 2), width - borderThickness, height - borderThickness, cornerRadius * 2, cornerRadius * 2, fillColor, fillAlpha, borderThickness, borderColor, borderAlpha, pixelHinting );
}
}
var start:uint = flash.utils.getTimer();
var duration:uint = 5000;
var sprite:Sprite = new Sprite();
addChild( sprite );
var gs:Graphics = sprite.graphics;
addEventListener( flash.events.Event.ENTER_FRAME,
function ( e:Event ):void
{
var t:uint = (getTimer() - start) % duration;
if (t > (duration / 2))
borderThickness = ((duration-t) / (duration/2)) * thicknessSteps * thicknessFactor;
else
borderThickness = (t / (duration/2)) * thicknessSteps * thicknessFactor;
//borderThickness = int(borderThickness);
cornerRadius = Math.max( 8, borderThickness );
borderColor = colors[squares - 1];
x = 0;
y = 0;
width = int(Math.pow( base, squares ));
height = width;
if (borderThickness * 2 > width) //don't draw if border is larger than area
return;
gs.clear();
drawFilledRoundRect( gs, x + (borderThickness / 2), y + (borderThickness / 2), width - borderThickness, height - borderThickness, cornerRadius * 2, cornerRadius * 2, fillColor, fillAlpha, borderThickness, borderColor, borderAlpha, pixelHinting );
}, false, 0, true );
}
function powerFactorial( base:Number, i:int ):Number
{
var result:Number = 0;
for (var c:int = 0; c < i; c++)
{
result += int(Math.pow( base, c + 1 ));
}
return result;
}
function drawFilledRoundRect( g:Graphics, x:Number, y:Number, width:Number, height:Number, ellipseWidth:Number = 0, ellipseHeight:Number = 0, fillcolor:Number = 0xFFFFFF, fillalpha:Number = 1, thickness:Number = 0, color:Number = 0, alpha:Number = 1, pixelHinting:Boolean = false, scaleMode:String = "normal", caps:String = null, joints:String = null, miterLimit:Number = 3 )
{
if (!isNaN( fillcolor))
{
g.beginFill( fillcolor, fillalpha );
g.drawRoundRect( x, y, width, height, ellipseWidth, ellipseHeight );
g.endFill();
}
if (!isNaN(color))
{
g.lineStyle( thickness, color, alpha, pixelHinting, scaleMode, caps, joints, miterLimit );
g.drawRoundRect( x, y, width, height, ellipseWidth, ellipseHeight );
}
}

Try turning on pixelHinting:
shape.graphics.lineStyle(2, 0, 1, true);
More about pixelHinting here.

You can't turn off antialiasing completely. If you want a sharp, pixelated line then unfortunately you have to draw pixel by pixel, using a Bitmap and setPixel()

Related

Detect Colour Using Action Script 3

So I have made a game in scratch which uses the following code block:
I am trying to remake this game in Adobe Animate using Action Script 3 (for a class project), is there a similar way to do this in animate?
It is possible. The trick to do it is to create a tiny-teeny BitmapData object and to draw a small portion of stage under the mouse pointer into that object so that you can obtain the pixel color value.
// BitmapData object and some service objects.
var BD:BitmapData = new BitmapData(3, 3, true);
var R:Rectangle = new Rectangle(0, 0, 3, 3);
var M:Matrix = new Matrix;
// Let's create a TextField so we can output the pixel color value.
var T:TextField = new TextField;
var TF:TextFormat = new TextFormat("_typewriter", 12, 0x000000, true, false, false, null, null, TextFormatAlign.CENTER);
T.x = 10;
T.y = 10;
T.width = 100;
T.height = 18;
T.border = true;
T.background = true;
T.selectable = false;
T.mouseEnabled = false;
T.defaultTextFormat = TF;
addChild(T);
// Lets add some semi-transparent color circles
// so we have colored things to point the mouse at.
for (var i:int = 0; i < 10; i++)
{
var aColor:uint = 0;
aColor |= int(128 + 128 * Math.random()) << 16; // RED
aColor |= int(128 + 128 * Math.random()) << 8; // GREEN
aColor |= int(128 + 128 * Math.random()); // BLUE
var anX:int = stage.stageWidth / 8 + Math.random() * stage.stageWidth * 3 / 4;
var anY:int = stage.stageHeight / 8 + Math.random() * stage.stageHeight * 3 / 4;
var aRadius:int = 50 + 100 * Math.random();
var anAlpha:Number = 0.5 + 0.5 * Math.random();
graphics.beginFill(aColor, anAlpha);
graphics.drawCircle(anX, anY, aRadius);
graphics.endFill();
}
// Now let's watch the mouse every frame.
addEventListener(Event.ENTER_FRAME, onFrame);
function onFrame(e:Event):void
{
// Get pixel color as an RRGGBB String and print it.
T.text = "#" + addZeroes(getColorUnderMouse());
}
function getColorUnderMouse():uint
{
// Adjust Matrix so that we draw the correct piece of screen.
M.tx = -root.mouseX + 1;
M.ty = -root.mouseY + 1;
// Clear the BitmapData and capture the 3x3 piece under the mouse pointer.
BD.fillRect(R, 0xFFFFFFFF);
BD.draw(root, M, null, null, R);
// Read the pixel color value at the center of 3x3 and return it.
return BD.getPixel(1, 1);
}
// This function fixes the hexabinary value with leading
// zeroes if the color value is too small (like 0 = black).
function addZeroes(value:uint, count:uint = 6):String
{
var result:String = value.toString(16).toUpperCase();
while (result.length < count)
{
result = "0" + result;
}
return result;
}

Check if image A exists in image B

I need to check if an image exists in another image using JavaScript, I need to know what are the best approaches (algorithm) and solutions (ex: librarie) to do this operations
I explained what I need to do in this image:
Using the GPU to help in image processing.
Using the 2D API and some simple tricks you can exploit the GPUs power to speed up Javascript.
Difference
To find an image you need to compare the pixels you are looking for (A) against the pixels in the image (B). If the difference between the Math.abs(A-B) === 0 then the pixels are the same.
A function to do this may look like the following
function findDif(imageDataSource, imageDataDest, xx,yy)
const ds = imageDataSource.data;
const dd = imageDataDest.data;
const w = imageDataSource.width;
const h = imageDataSource.height;
var x,y;
var dif = 0;
for(y = 0; y < h; y += 1){
for(x = 0; x < w; x += 1){
var indexS = (x + y * w) * 4;
var indexD = (x + xx + (y + yy) * imageDataDest.width) * 4;
dif += Math.abs(ds[indexS]-dd[indexD]);
dif += Math.abs(ds[indexS + 1]-dd[indexD + 1]);
dif += Math.abs(ds[indexS + 2]-dd[indexD + 2]);
}
}
return dif;
}
var source = sourceCanvas.getContext("2d").getImageData(0,0,sourceCanvas.width,sourceCanvas.height);
var dest = destinationCanvas.getContext("2d").getImageData(0,0,destinationCanvas.width,destinationCanvas.height);
if(findDif(source,dest,100,100)){ // is the image at 100,100?
// Yes image is very similar
}
Where the source is the image we are looking for and the dest is the image we want to find it in. We run the function for every location that the image may be and if the result is under a level then its a good chance we have found it.
But this is very very slow in JS. This is where the GPU can help.
Using the ctx.globalCompositeOperation = "difference"; operation we can speed up the process as it will do the difference calculation for us
When you render with the comp operation "difference" the resulting pixels are the difference between the pixels you are drawing and those that are already on the canvas. Thus if you draw on something that is the same the result is all pixels are black (no difference)
To find a similar image in the image you render the image you are testing for at each location on the canvas that you want to test for. Then you get the sum of all the pixels you just rendered on, if the result is under a threshold that you have set then the image under that area is very similar to the image you are testing for.
But we still need to count all the pixels one by one.
A GPU mean function
The comp op "difference" already does the pixel difference calculation for you, but to get the sum you can use the inbuilt image smoothing.
After you have rendered to find the difference you take that area and render it at a smaller scale with ctx.imageSmoothingEnabled = true the default setting. The GPU will do something similar to an average and can reduce the amount of work JS has to do by several orders of magnitude.
Now instead of 100s or 1000s of pixels you can reduce it down to as little at 4 or 16 depending on the accuracy you need.
An example.
Using these methods you can get a near realtime image in image search with just the basic numerical analysis.
Click to start a test. Results are shown plus the time it took. The image that is being searched for is in the top right.
//------------------------------------------------------------------------
// Some helper functions
var imageTools = (function () {
var tools = {
canvas(width, height) { // create a blank image (canvas)
var c = document.createElement("canvas");
c.width = width;
c.height = height;
return c;
},
createImage : function (width, height) {
var i = this.canvas(width, height);
i.ctx = i.getContext("2d");
return i;
},
image2Canvas(img) {
var i = this.canvas(img.width, img.height);
i.ctx = i.getContext("2d");
i.ctx.drawImage(img, 0, 0);
return i;
},
copyImage(img){ // just a named stub
return this.image2Canvas(img);
},
};
return tools;
})();
const U = undefined;
const doFor = (count, callback) => {var i = 0; while (i < count && callback(i ++) !== true ); };
const setOf = (count, callback) => {var a = [],i = 0; while (i < count) { a.push(callback(i ++)) } return a };
const randI = (min, max = min + (min = 0)) => (Math.random() * (max - min) + min) | 0;
const rand = (min, max = min + (min = 0)) => Math.random() * (max - min) + min;
const randA = (array) => array[(Math.random() * array.length) | 0];
const randG = (min, max = min + (min = 0)) => Math.random() * Math.random() * Math.random() * Math.random() * (max - min) + min;
// end of helper functions
//------------------------------------------------------------------------
function doit(){
document.body.innerHTML = ""; // clear the page;
var canvas = document.createElement("canvas");
document.body.appendChild(canvas);
var ctx = canvas.getContext("2d");
// a grid of 36 images
canvas.width = 6 * 64;
canvas.height = 6 * 64;
console.log("test");
// get a random character to look for
const digit = String.fromCharCode("A".charCodeAt(0) + randI(26));
// get some characters we dont want
const randomDigits = setOf(6,i=>{
return String.fromCharCode("A".charCodeAt(0) + randI(26));
})
randomDigits.push(digit); // add the image we are looking for
var w = canvas.width;
var h = canvas.height;
// create a canvas for the image we are looking for
const imageToFind = imageTools.createImage(64,64);
// and a larger one to cover pixels on the sides
const imageToFindExtend = imageTools.createImage(128,128);
// Draw the character onto the image with a white background and scaled to fit
imageToFindExtend.ctx.fillStyle = imageToFind.ctx.fillStyle = "White";
imageToFind.ctx.fillRect(0,0,64,64);
imageToFindExtend.ctx.fillRect(0,0,128,128);
ctx.font = imageToFind.ctx.font = "64px arial black";
ctx.textAlign = imageToFind.ctx.textAlign = "center";
ctx.textBaseline = imageToFind.ctx.textBaseline = "middle";
const digWidth = imageToFind.ctx.measureText(digit).width+8;
const scale = Math.min(1,64/digWidth);
imageToFind.ctx.fillStyle = "black";
imageToFind.ctx.setTransform(scale,0,0,scale,32,32);
imageToFind.ctx.fillText(digit,0,0);
imageToFind.ctx.setTransform(1,0,0,1,0,0);
imageToFindExtend.ctx.drawImage(imageToFind,32,32);
imageToFind.extendedImage = imageToFindExtend;
// Now fill the canvas with images of other characters
ctx.fillStyle = "white";
ctx.setTransform(1,0,0,1,0,0);
ctx.fillRect(0,0,w,h);
ctx.fillStyle = "black";
ctx.strokeStyle = "white";
ctx.lineJoin = "round";
ctx.lineWidth = 12;
// some characters will be rotated 90,180,-90 deg
const dirs = [
[1,0,0,1,0,0],
[0,1,-1,0,1,0],
[-1,0,0,-1,1,1],
[0,-1,1,0,0,1],
]
// draw random characters at random directions
doFor(h / 64, y => {
doFor(w / 64, x => {
const dir = randA(dirs)
ctx.setTransform(dir[0] * scale,dir[1] * scale,dir[2] * scale,dir[3] * scale,x * 64 + 32, y * 64 + 32);
const d = randA(randomDigits);
ctx.strokeText(d,0,0);
ctx.fillText(d,0,0);
});
});
ctx.setTransform(1,0,0,1,0,0);
// get a copy of the canvas
const saveCan = imageTools.copyImage(ctx.canvas);
// function that finds the images
// image is the image to find
// dir is the matrix direction to find
// smapleSize is the mean sampling size samller numbers are quicker
function checkFor(image,dir,sampleSize){
const can = imageTools.copyImage(saveCan);
const c = can.ctx;
const stepx = 64;
const stepy = 64;
// the image that will contain the reduced means of the differences
const results = imageTools.createImage(Math.ceil(w / stepx) * sampleSize,Math.ceil(h / stepy) * sampleSize);
const e = image.extendedImage;
// for each potencial image location
// set a clip area and draw the source image on it with
// comp mode "difference";
for(var y = 0 ; y < h; y += stepy ){
for(var x = 0 ; x < w; x += stepx ){
c.save();
c.beginPath();
c.rect(x,y,stepx,stepy);
c.clip();
c.globalCompositeOperation = "difference";
c.setTransform(dir[0],dir[1],dir[2],dir[3],x +32 ,y +32 );
c.drawImage(e,-64,-64);
c.restore();
}
}
// Apply the mean (reducing nnumber of pixels to check
results.ctx.drawImage(can,0,0,results.width,results.height);
// get the pixel data
var dat = new Uint32Array(results.ctx.getImageData(0,0,results.width,results.height).data.buffer);
// for each area get the sum of the difference
for(var y = 0; y < results.height; y += sampleSize){
for(var x = 0; x < results.width; x += sampleSize){
var val = 0;
for(var yy = 0; yy < sampleSize && y+yy < results.height; yy += 1){
var i = x + (y+yy)*results.width;
for(var xx = 0; xx < sampleSize && x + xx < results.width ; xx += 1){
val += dat[i++] & 0xFF;
}
}
// if the sum is under the threshold we have found an image
// and we mark it
if(val < sampleSize * sampleSize * 5){
ctx.strokeStyle = "red";
ctx.fillStyle = "rgba(255,0,0,0.5)";
ctx.lineWidth = 2;
ctx.strokeRect(x * (64/sampleSize),y * (64/sampleSize),64,64);
ctx.fillRect(x * (64/sampleSize),y * (64/sampleSize),64,64);
foundCount += 1;
}
}
}
}
var foundCount = 0;
// find the images at different orientations
var now = performance.now();
checkFor(imageToFind,dirs[0],4);
checkFor(imageToFind,dirs[1],6); // rotated images need larger sample size
checkFor(imageToFind,dirs[2],6);
checkFor(imageToFind,dirs[3],6);
var time = performance.now() - now;
var result = document.createElement("div");
result.textContent = "Found "+foundCount +" matching images in "+time.toFixed(3)+"ms. Click to try again.";
document.body.appendChild(result);
// show the image we are looking for
imageToFind.style.left = (64*6 + 16) + "px";
imageToFind.id = "lookingFor";
document.body.appendChild(imageToFind);
}
document.addEventListener("click",doit);
canvas {
border : 2px solid black;
position : absolute;
top : 28px;
left : 2px;
}
#lookingFor {
border : 4px solid red;
}
div {
border : 2px solid black;
position : absolute;
top : 2px;
left : 2px;
}
Click to start test.
Not perfect
The example is not perfect and will sometimes make mistakes. There is a huge amount of room for improving both the accuracy and the speed. This is just something I threw together as an example to show how to use the GPU via the 2D API. Some further maths will be needed to find the statistically good results.
This method can also work for different scales, and rotations, you can even use some of the other comp modes to remove colour and normalize contrast. I have used a very similar approch to stabilize webcam by tracking points from one frame to the next, and a veriaty of other image tracking uses.

Getting pixel data on setInterval with canvas

I want to build an animated alphabet, made up of particles. Basically, the particles transform from one letter shape to another.
My idea is to fill the letters as text on canvas real quickly (like for a frame), get the pixel data and put the particles to the correct location on setInterval. I have this code for scanning the screen right now:
var ctx = canvas.getContext('2d'),
width = ctx.canvas.width,
height = ctx.canvas.height,
particles = [],
gridX = 8,
gridY = 8;
function Particle(x, y) {
this.x = x;
this.y = y;
}
// fill some text
ctx.font = 'bold 80px sans-serif';
ctx.fillStyle = '#ff0';
ctx.fillText("STACKOVERFLOW", 5, 120);
// now parse bitmap based on grid
var idata = ctx.getImageData(0, 0, width, height);
// use a 32-bit buffer as we are only checking if a pixel is set or not
var buffer32 = new Uint32Array(idata.data.buffer);
// using two loops here, single loop with index-to-x/y is also an option
for(var y = 0; y < height; y += gridY) {
for(var x = 0; x < width; x += gridX) {
//buffer32[] will have a value > 0 (true) if set, if not 0=false
if (buffer32[y * width + x]) {
particles.push(new Particle(x, y));
}
}
}
// render particles
ctx.clearRect(0, 0, width, height);
particles.forEach(function(p) {
ctx.fillRect(p.x - 2, p.y - 2, 4, 4); // just squares here
})
But this way I am only showing one word, without any changes throughout the time. Also, I want to set up initially like 200 particles and reorganise them based on the pixel data, not create them on each scan.. How would you rewrite the code, so on every 1500ms I can pass a different letter and render it with particles?
Hopefully the different parts of this code should be clear enough : There are particles, that can draw and update, fillParticle will spawn particles out of a text string, and spawnChars will get a new part of the text rendered on a regular basis.
It is working quite well, play with the parameters if you wish, they are all at the start of the fiddle.
You might want to make this code cleaner, by avoiding globals and creating classes.
http://jsbin.com/jecarupiri/1/edit?js,output
// --------------------
// parameters
var text = 'STACKOVERFLOW';
var fontHeight = 80;
var gridX = 4,
gridY = 4;
var partSize = 2;
var charDelay = 400; // time between two chars, in ms
var spead = 80; // max distance from start point to final point
var partSpeed = 0.012;
// --------------------
var canvas = document.getElementById('cv'),
ctx = canvas.getContext('2d'),
width = ctx.canvas.width,
height = ctx.canvas.height,
particles = [];
ctx.translate(0.5,0.5);
// --------------------
// Particle class
function Particle(startX, startY, finalX, finalY) {
this.speed = partSpeed*(1+Math.random()*0.5);
this.x = startX;
this.y = startY;
this.startX = startX;
this.startY = startY;
this.finalX =finalX;
this.finalY =finalY;
this.parameter = 0;
this.draw = function() {
ctx.fillRect(this.x - partSize*0.5, this.y - partSize*0.5, partSize, partSize);
};
this.update = function(p) {
if (this.parameter>=1) return;
this.parameter += partSpeed;
if (this.parameter>=1) this.parameter=1;
var par = this.parameter;
this.x = par*this.finalX + (1-par)*this.startX;
this.y = par*this.finalY + (1-par)*this.startY;
};
}
// --------------------
// Text spawner
function fillParticle(text, offx, offy, spread) {
// fill some text
tmpCtx.clearRect(0,0,tmpCtx.canvas.width, tmpCtx.canvas.height);
tmpCtx.font = 'bold ' + fontHeight +'px sans-serif';
tmpCtx.fillStyle = '#A40';
tmpCtx.textBaseline ='top';
tmpCtx.textAlign='left';
tmpCtx.fillText(text, 0, 0);
//
var txtWidth = Math.floor(tmpCtx.measureText(text).width);
// now parse bitmap based on grid
var idata = tmpCtx.getImageData(0, 0, txtWidth, fontHeight);
// use a 32-bit buffer as we are only checking if a pixel is set or not
var buffer32 = new Uint32Array(idata.data.buffer);
// using two loops here, single loop with index-to-x/y is also an option
for(var y = 0; y < fontHeight; y += gridY) {
for(var x = 0; x < txtWidth; x += gridX) {
//buffer32[] will have a value > 0 (true) if set, if not 0=false
if (buffer32[y * txtWidth + x]) {
particles.push(new Particle(offx + x+Math.random()*spread - 0.5*spread,
offy + y+Math.random()*spread - 0.5*spread, offx+x, offy+y));
}
}
}
return txtWidth;
}
var tmpCv = document.createElement('canvas');
// uncomment for debug
//document.body.appendChild(tmpCv);
var tmpCtx = tmpCv.getContext('2d');
// --------------------------------
// spawn the chars of the text one by one
var charIndex = 0;
var lastSpawnDate = -1;
var offX = 30;
var offY = 30;
function spawnChars() {
if (charIndex>= text.length) return;
if (Date.now()-lastSpawnDate < charDelay) return;
offX += fillParticle(text[charIndex], offX, offY, spead);
lastSpawnDate = Date.now();
charIndex++;
}
// --------------------------------
function render() {
// render particles
particles.forEach(function(p) { p.draw();
});
}
function update() {
particles.forEach(function(p) { p.update(); } );
}
// --------------------------------
// animation
function animate(){
requestAnimationFrame(animate);
ctx.clearRect(0, 0, width, height);
render();
update();
//
spawnChars();
}
// launch :
animate();

Poor anti-aliasing of text drawn on Canvas

I'm drawing text on Canvas, and am disappointed with the quality of antialiasing. As far as I've been able to determine, browsers don't do subpixel antialising of text on Canvas.
Is this accurate?
This is particularly noticeable on iPhone and Android, where the resulting text isn't as crisp as text rendered by other DOM elements.
Any suggestions for high quality text out put on Canvas?
Joubert
My answer came from this link, maybe it will help someone else.
http://www.html5rocks.com/en/tutorials/canvas/hidpi/
The important code is as follows.
// finally query the various pixel ratios
devicePixelRatio = window.devicePixelRatio || 1,
backingStoreRatio = context.webkitBackingStorePixelRatio ||
context.mozBackingStorePixelRatio ||
context.msBackingStorePixelRatio ||
context.oBackingStorePixelRatio ||
context.backingStorePixelRatio || 1,
ratio = devicePixelRatio / backingStoreRatio;
// upscale the canvas if the two ratios don't match
if (devicePixelRatio !== backingStoreRatio) {
var oldWidth = canvas.width;
var oldHeight = canvas.height;
canvas.width = oldWidth * ratio;
canvas.height = oldHeight * ratio;
canvas.style.width = oldWidth + 'px';
canvas.style.height = oldHeight + 'px';
// now scale the context to counter
// the fact that we've manually scaled
// our canvas element
context.scale(ratio, ratio);
}
Try adding the following META tag to your page. This seems to fix anti-aliasing issues I've had on iPhone Safari:
<meta name="viewport" content="user-scalable=no, width=device-width,
initial-scale=0.5, minimum-scale=0.5, maximum-scale=0.5" />
I realise this is an old question, but I worked on this problem today and got it working nicely. I used Alix Axel's answer above and stripped down the code I found there (on the web.archive.org link) to the bare essentials.
I modified the solution a bit, using two canvases, one hidden canvas for the original text and a second canvas to actually show the anti-aliaised text.
Here's what I came up with... http://jsfiddle.net/X2cKa/
The code looks like this;
function alphaBlend(gamma, c1, c2, alpha) {
c1 = c1/255.0;
c2 = c2/255.0;
var c3 = Math.pow(
Math.pow(c1, gamma) * (1 - alpha)
+ Math.pow(c2, gamma) * alpha,
1/gamma
);
return Math.round(c3 * 255);
}
function process(textPixels, destPixels, fg, bg) {
var gamma = 2.2;
for (var y = 0; y < textPixels.height; y ++) {
var history = [255, 255, 255];
var pixel_number = y * textPixels.width;
var component = 0;
for (var x = 0; x < textPixels.width; x ++) {
var alpha = textPixels.data[(y * textPixels.width + x) * 4 + 1] / 255.0;
alpha = Math.pow(alpha, gamma);
history[component] = alpha;
alpha = (history[0] + history[1] + history[2]) / 3;
out = alphaBlend(gamma, bg[component], fg[component], alpha);
destPixels.data[pixel_number * 4 + component] = out;
/* advance to next component/pixel */
component ++;
if (component == 3) {
pixel_number ++;
component = 0;
}
}
}
}
function toColor(colorString) {
return [parseInt(colorString.substr(1, 2), 16),
parseInt(colorString.substr(3, 2), 16),
parseInt(colorString.substr(5, 2), 16)];
}
function renderOnce() {
var phrase = "Corporate GOVERNANCE"
var c1 = document.getElementById("c1"); //the hidden canvas
var c2 = document.getElementById("c2"); //the canvas
var textSize=40;
var font = textSize+"px Arial"
var fg = "#ff0000";
var bg = "#fff9e1";
var ctx1 = c1.getContext("2d");
var ctx2 = c2.getContext("2d");
ctx1.fillStyle = "rgb(255, 255, 255)";
ctx1.fillRect(0, 0, c1.width, c1.height);
ctx1.save();
ctx1.scale(3, 1);
ctx1.font = font;
ctx1.fillStyle = "rgb(255, 0, 0)";
ctx1.fillText(phrase, 0, textSize);
ctx1.restore();
var textPixels = ctx1.getImageData(0, 0, c1.width, c1.height);
var colorFg = toColor(fg);
var colorBg = toColor(bg);
var destPixels3 = ctx1.getImageData(0, 0, c1.width, c1.height);
process(textPixels, destPixels3, colorBg, colorFg);
ctx2.putImageData(destPixels3, 0, 0);
//for comparison, show Comparison Text without anti aliaising
ctx2.font = font;
ctx2.fillStyle = "rgb(255, 0, 0)";
ctx2.fillText(phrase, 0, textSize*2);
};
renderOnce();
I also added a comparison text object so that you can see the anti-aliasing working.
Hope this helps someone!
There is some subpixel antialiasing done, but it is up to the browser/OS.
There was a bit of an earlier discussion on this that may be of help to you.
I don't have an android or iOS device but just for kicks, try translating the context by (.5, 0) pixels before you draw and see if that makes a difference in how your text is rendered.

Optimizing my dynamic background engine for a 2d flash game in actionscript-3

Edit 2: judging on the lack of replies I start wondering if my issue is clear enough. Please tell me if I need to elaborate more.
Notice: see bottom for a code update!
Short introduction: I'm writing a 2 dimensional flash space game in actionscript. The universe is infinitely big, because of this feature, the background has to be rendered dynamically and background objects (gas clouds, stars, etc.) have to be positioned randomly.
I created a class called BackgroundEngine and it's working very well, the problem is however the rendering performance. This is how it works:
At startup, 4 background containers (each the size of the stage) are created around the player. Top left, top right, bottom left and bottom right. All background squares are added to a master container, for easy movement of the background. Now, there are 2 polling functions:
1) "garbage poller": looks for background containers that are 2 times the stage width or height away from the player's X or Y coord, respectively. If so, it will remove that background square and allow it for garbage collection.
2) "rendering poller": looks whether there is currently a background at all sides of the player (x - stageWidth, x + stageWidth, y - stageHeight, y + stageHeight). If not, it will draw a new background square at the corresponding location.
All background squares are created with the following function (the ones that are created dynamically and the four on startup):
<<< removed old code, see bottom for updated full source >>>
All the randoms you see there are making sure that the environment looks very unique on every square. This actually works great, the universe looks quite awesome.
The following assets are being used as background objects:
1) Simple stars : http://www.feedpostal.com/client/assets/background/1.png (you probably won't be able to see that one in a browser with a white background).
2) Bright stars : http://www.feedpostal.com/client/assets/background/2.png
3) White gas clouds : http://www.feedpostal.com/client/assets/background/3.png
4) Red gas clouds: http://www.feedpostal.com/client/assets/background/4.png
Important notes:
1) All assets are cached, so they don't have to be re-downloaded all the time. They are only downloaded once.
2) The images are not rotating or being scaled after they are created, so I enabled cacheAsBitmap for all objects, containers and the masterContainer.
3) I had to use PNG formats in Photoshop because GIFs did not seem to be rendered very well in flash when used with transparency.
So, the problem is that when I fly around the rendering of the background takes too much performance: the client starts "lagging" (FPS wise). Because of this, I need to optimize the background engine so that it will render much quicker. Can you folks help me out here?
Update 1:
This is what I have so far after the one response I got.
BackgroundEngine.as
package com.tommedema.background
{
import br.com.stimuli.loading.BulkLoader;
import com.tommedema.utils.Settings;
import com.tommedema.utils.UtilLib;
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.Sprite;
import flash.events.Event;
public final class BackgroundEngine extends Sprite
{
private static var isLoaded:Boolean = false;
private static var bulkLoader:BulkLoader = BulkLoader.getLoader("main");
private static var masterContainer:Sprite;
private static var containers:Array = [];
private static var stageWidth:uint;
private static var stageHeight:uint;
private static var assets:Array;
//moves the background's X coord
public static function moveX(amount:Number):void
{
if (masterContainer)
{
masterContainer.x += amount;
collectGarbage();
drawNextContainer();
}
}
//moves the background's Y coord
public static function moveY(amount:Number):void
{
if (masterContainer)
{
masterContainer.y += amount;
collectGarbage();
drawNextContainer();
}
}
//returns whether the background engine has been loaded already
public static function loaded():Boolean
{
return isLoaded;
}
//loads the background engine
public final function load():void
{
//set stage width and height
stageWidth = stage.stageWidth;
stageHeight = stage.stageHeight;
//retreive all background assets
bulkLoader.add(Settings.ASSETS_PRE_URL + "background/1.png", {id: "background/1.png"});
bulkLoader.add(Settings.ASSETS_PRE_URL + "background/2.png", {id: "background/2.png"});
bulkLoader.add(Settings.ASSETS_PRE_URL + "background/3.png", {id: "background/3.png"});
bulkLoader.add(Settings.ASSETS_PRE_URL + "background/4.png", {id: "background/4.png"});
bulkLoader.addEventListener(BulkLoader.COMPLETE, assetsComplete);
bulkLoader.start();
//set isLoaded to true
isLoaded = true;
}
//poller function for drawing next background squares
private static function drawNextContainer():void
{
var stageCenterX:Number = stageWidth / 2;
var stageCenterY:Number = stageHeight / 2;
var curContainer:Bitmap = hasBackground(stageCenterX, stageCenterY);
if (curContainer)
{
//top left
if (!hasBackground(stageCenterX - stageWidth, stageCenterY - stageHeight)) drawNewSquare(curContainer.x - stageWidth, curContainer.y - stageHeight);
//top
if (!hasBackground(stageCenterX, stageCenterY - stageHeight)) drawNewSquare(curContainer.x, curContainer.y - stageHeight);
//top right
if (!hasBackground(stageCenterX + stageWidth, stageCenterY - stageHeight)) drawNewSquare(curContainer.x + stageWidth, curContainer.y - stageHeight);
//center left
if (!hasBackground(stageCenterX - stageWidth, stageCenterY)) drawNewSquare(curContainer.x - stageWidth, curContainer.y);
//center right
if (!hasBackground(stageCenterX + stageWidth, stageCenterY)) drawNewSquare(curContainer.x + stageWidth, curContainer.y);
//bottom left
if (!hasBackground(stageCenterX - stageWidth, stageCenterY + stageHeight)) drawNewSquare(curContainer.x - stageWidth, curContainer.y + stageHeight);
//bottom
if (!hasBackground(stageCenterX, stageCenterY + stageHeight)) drawNewSquare(curContainer.x, curContainer.y + stageHeight);
//bottom right
if (!hasBackground(stageCenterX + stageWidth, stageCenterY + stageHeight)) drawNewSquare(curContainer.x + stageWidth, curContainer.y + stageHeight);
}
}
//draws the next square and adds it to the master container
private static function drawNewSquare(x:Number, y:Number):void
{
containers.push(genSquareBg());
var cIndex:uint = containers.length - 1;
containers[cIndex].x = x;
containers[cIndex].y = y;
masterContainer.addChild(containers[cIndex]);
}
//returns whether the given location has a background and if so returns the corresponding square
private static function hasBackground(x:Number, y:Number):Bitmap
{
var stageX:Number;
var stageY:Number;
for(var i:uint = 0; i < containers.length; i++)
{
stageX = masterContainer.x + containers[i].x;
stageY = masterContainer.y + containers[i].y;
if ((containers[i]) && (stageX < x) && (stageX + stageWidth > x) && (stageY < y) && (stageY + stageHeight > y)) return containers[i];
}
return null;
}
//polling function for old background squares garbage collection
private static function collectGarbage():void
{
var stageX:Number;
var stageY:Number;
for(var i:uint = 0; i < containers.length; i++)
{
if (containers[i])
{
stageX = masterContainer.x + containers[i].x;
stageY = masterContainer.y + containers[i].y;
if ((stageX < -stageWidth * 1.5) || (stageX > stageWidth * 2.5) || (stageY < -stageHeight * 1.5) || (stageY > stageHeight * 2.5))
{
containers[i].parent.removeChild(containers[i]);
containers.splice(i, 1);
}
}
}
}
//dispatched when all assets have finished downloading
private final function assetsComplete(event:Event):void
{
assets = [];
assets.push(bulkLoader.getBitmap("background/1.png")); //star simple
assets.push(bulkLoader.getBitmap("background/2.png")); //star bright
assets.push(bulkLoader.getBitmap("background/3.png")); //cloud white
assets.push(bulkLoader.getBitmap("background/4.png")); //cloud red
init();
}
//initializes startup background containers
private final function init():void
{
masterContainer = new Sprite(); //create master container
//generate default background containers
containers.push(genSquareBg()); //top left
containers[0].x = 0;
containers[0].y = 0;
containers.push(genSquareBg()); //top
containers[1].x = stageWidth;
containers[1].y = 0;
containers.push(genSquareBg()); //top right
containers[2].x = stageWidth * 2;
containers[2].y = 0;
containers.push(genSquareBg()); //center left
containers[3].x = 0;
containers[3].y = stageHeight;
containers.push(genSquareBg()); //center
containers[4].x = stageWidth;
containers[4].y = stageHeight;
containers.push(genSquareBg()); //center right
containers[5].x = stageWidth * 2;
containers[5].y = stageHeight;
containers.push(genSquareBg()); //bottom left
containers[6].x = 0;
containers[6].y = stageHeight * 2;
containers.push(genSquareBg()); //bottom
containers[7].x = stageWidth;
containers[7].y = stageHeight * 2;
containers.push(genSquareBg()); //bottom right
containers[8].x = stageWidth * 2;
containers[8].y = stageHeight * 2;
//add the new containers to the master container
for (var i:uint = 0; i <= containers.length - 1; i++)
{
masterContainer.addChild(containers[i]);
}
//display the master container
masterContainer.x = 0 - stageWidth;
masterContainer.y = 0 - stageHeight;
masterContainer.cacheAsBitmap = true;
addChild(masterContainer);
}
//duplicates a bitmap display object
private static function dupeBitmap(source:Bitmap):Bitmap {
var data:BitmapData = source.bitmapData;
var bitmap:Bitmap = new Bitmap(data);
return bitmap;
}
//draws a simple star
private static function drawStar(x:Number, y:Number, width:uint, height:uint):Sprite
{
var creation:Sprite = new Sprite();
creation.graphics.lineStyle(1, 0xFFFFFF);
creation.graphics.beginFill(0xFFFFFF);
creation.graphics.drawRect(x, y, width, height);
return creation;
}
//generates a background square
private static function genSquareBg():Bitmap
{
//set 1% margin
var width:Number = stageWidth * 0.99;
var height:Number = stageHeight * 0.99;
var startX:Number = 0 + stageWidth / 100;
var startY:Number = 0 + stageHeight / 100;
var scale:Number;
var drawAmount:uint;
var tmpBitmap:Bitmap;
var tmpSprite:Sprite;
var i:uint;
//create container
var container:Sprite = new Sprite();
//draw simple stars
drawAmount = UtilLib.getRandomInt(100, 250);
for(i = 1; i <= drawAmount; i++)
{
tmpSprite = drawStar(0, 0, 1, 1);
tmpSprite.x = UtilLib.getRandomInt(0, stageWidth);
tmpSprite.y = UtilLib.getRandomInt(0, stageHeight);
tmpSprite.alpha = UtilLib.getRandomInt(3, 10) / 10;
scale = UtilLib.getRandomInt(2, 10) / 10;
tmpSprite.scaleX = tmpSprite.scaleY = scale;
container.addChild(tmpSprite);
}
//draw bright stars
if (Math.random() >= 0.8) drawAmount = UtilLib.getRandomInt(1, 2);
else drawAmount = 0;
for(i = 1; i <= drawAmount; i++)
{
tmpBitmap = dupeBitmap(assets[1]);
tmpBitmap.alpha = UtilLib.getRandomInt(3, 7) / 10;
tmpBitmap.rotation = UtilLib.getRandomInt(0, 360);
scale = UtilLib.getRandomInt(3, 10) / 10;
tmpBitmap.scaleX = scale; tmpBitmap.scaleY = scale;
tmpBitmap.x = UtilLib.getRandomInt(startX + tmpBitmap.width, width - tmpBitmap.width);
tmpBitmap.y = UtilLib.getRandomInt(startY + tmpBitmap.height, height - tmpBitmap.height);
container.addChild(tmpBitmap);
}
//draw white clouds
drawAmount = UtilLib.getRandomInt(1, 4);
for(i = 1; i <= drawAmount; i++)
{
tmpBitmap = dupeBitmap(assets[2]);
tmpBitmap.alpha = UtilLib.getRandomInt(1, 10) / 10;
scale = UtilLib.getRandomInt(15, 30);
tmpBitmap.scaleX = scale / 10;
tmpBitmap.scaleY = UtilLib.getRandomInt(scale / 2, scale) / 10;
tmpBitmap.x = UtilLib.getRandomInt(startX, width - tmpBitmap.width);
tmpBitmap.y = UtilLib.getRandomInt(startY, height - tmpBitmap.height);
container.addChild(tmpBitmap);
}
//draw red clouds
drawAmount = UtilLib.getRandomInt(0, 1);
for(i = 1; i <= drawAmount; i++)
{
tmpBitmap = dupeBitmap(assets[3]);
tmpBitmap.alpha = UtilLib.getRandomInt(2, 6) / 10;
scale = UtilLib.getRandomInt(5, 30) / 10;
tmpBitmap.scaleX = scale; tmpBitmap.scaleY = scale;
tmpBitmap.x = UtilLib.getRandomInt(startX, width - tmpBitmap.width);
tmpBitmap.y = UtilLib.getRandomInt(startY, height - tmpBitmap.height);
container.addChild(tmpBitmap);
}
//convert all layers to a single bitmap layer and return
var bitmapData:BitmapData = new BitmapData(stageWidth, stageHeight, true, 0x000000);
bitmapData.draw(container);
container = null;
var bitmapContainer:Bitmap = new Bitmap(bitmapData);
bitmapContainer.cacheAsBitmap = true;
return bitmapContainer;
}
}
}
When the player is moving, the background moveX and moveY methods are called with the inverse direction of the player. This will also cause the collectGarbage and drawNextContainer methods to be called.
The problem with this setup is that there are a minimum of 9 containers active at all times. Top left, top, top right, center left, center, center right, bottom left, bottom and bottom right. This takes a lot of performance.
Edit: I also wonder, should I use cacheAsBitmap? If so, on which images? On the containers and the master container or on only one of them? When I enable it for all images (even the temporary sprite objects) it's actually lagging more.
Update 2:
This version is using squares that are twice as big as the stage. Only one or two squares should be loaded at a time. It is better, but I still notice a performance hit while moving. It makes the client freeze for a very brief moment. Any idea how to optimize it?
BackgroundEngine2.as
package com.tommedema.background
{
import br.com.stimuli.loading.BulkLoader;
import com.tommedema.utils.Settings;
import com.tommedema.utils.UtilLib;
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.Sprite;
import flash.events.Event;
public final class BackgroundEngine2 extends Sprite
{
//general
private static var isLoaded:Boolean = false;
private static var bulkLoader:BulkLoader = BulkLoader.getLoader("main");
private static var assets:Array;
//objects
private static var masterContainer:Sprite;
private static var containers:Array = [];
//stage
private static var stageWidth:uint;
private static var stageHeight:uint;
private static var stageCenterX:Number;
private static var stageCenterY:Number;
//moves the background's X coord
public static function moveX(amount:Number):void
{
if (!masterContainer) return;
masterContainer.x += amount;
collectGarbage();
drawNextContainer();
}
//moves the background's Y coord
public static function moveY(amount:Number):void
{
if (!masterContainer) return;
masterContainer.y += amount;
collectGarbage();
drawNextContainer();
}
//returns whether the background engine has been loaded already
public static function loaded():Boolean
{
return isLoaded;
}
//loads the background engine
public final function load():void
{
//set stage width, height and center
stageWidth = stage.stageWidth;
stageHeight = stage.stageHeight;
stageCenterX = stageWidth / 2;
stageCenterY = stageHeight / 2;
//retreive background assets
bulkLoader.add(Settings.ASSETS_PRE_URL + "background/1.png", {id: "background/1.png"});
bulkLoader.add(Settings.ASSETS_PRE_URL + "background/2.png", {id: "background/2.png"});
bulkLoader.add(Settings.ASSETS_PRE_URL + "background/3.png", {id: "background/3.png"});
bulkLoader.add(Settings.ASSETS_PRE_URL + "background/4.png", {id: "background/4.png"});
bulkLoader.addEventListener(BulkLoader.COMPLETE, assetsComplete);
bulkLoader.start();
//set isLoaded to true
isLoaded = true;
}
//poller function for drawing next background squares
private static function drawNextContainer():void
{
var curContainer:Bitmap = hasBackground(stageCenterX, stageCenterY);
if (curContainer)
{
if (!hasBackground(stageCenterX - stageWidth * 0.75, stageCenterY - stageHeight * 0.75)) //top left
drawNewSquare(curContainer.x - curContainer.width, curContainer.y - curContainer.height);
if (!hasBackground(stageCenterX, stageCenterY - stageHeight * 0.75)) //top
drawNewSquare(curContainer.x, curContainer.y - curContainer.height);
if (!hasBackground(stageCenterX + stageWidth * 0.75, stageCenterY - stageHeight * 0.75)) //top right
drawNewSquare(curContainer.x + curContainer.width, curContainer.y - curContainer.height);
if (!hasBackground(stageCenterX - stageWidth * 0.75, stageCenterY)) //center left
drawNewSquare(curContainer.x - curContainer.width, curContainer.y);
if (!hasBackground(stageCenterX + stageWidth * 0.75, stageCenterY)) //center right
drawNewSquare(curContainer.x + curContainer.width, curContainer.y);
if (!hasBackground(stageCenterX - stageWidth * 0.75, stageCenterY + stageHeight * 0.75)) //bottom left
drawNewSquare(curContainer.x - curContainer.width, curContainer.y + curContainer.height);
if (!hasBackground(stageCenterX, stageCenterY + stageHeight * 0.75)) //bottom center
drawNewSquare(curContainer.x, curContainer.y + curContainer.height);
if (!hasBackground(stageCenterX + stageWidth * 0.75, stageCenterY + stageHeight * 0.75)) //bottom right
drawNewSquare(curContainer.x + curContainer.width, curContainer.y + curContainer.height);
}
}
//draws the next square and adds it to the master container
private static function drawNewSquare(x:Number, y:Number):void
{
containers.push(genSquareBg());
var cIndex:uint = containers.length - 1;
containers[cIndex].x = x;
containers[cIndex].y = y;
masterContainer.addChild(containers[cIndex]);
}
//returns whether the given location has a background and if so returns the corresponding square
private static function hasBackground(x:Number, y:Number):Bitmap
{
var stageX:Number;
var stageY:Number;
for(var i:uint = 0; i < containers.length; i++)
{
stageX = masterContainer.x + containers[i].x;
stageY = masterContainer.y + containers[i].y;
if ((containers[i]) && (stageX < x) && (stageX + containers[i].width > x) && (stageY < y) && (stageY + containers[i].height > y)) return containers[i];
}
return null;
}
//polling function for old background squares garbage collection
private static function collectGarbage():void
{
var stageX:Number;
var stageY:Number;
for(var i:uint = 0; i < containers.length; i++)
{
if ((containers[i]) && (!isRequiredContainer(containers[i])))
{
masterContainer.removeChild(containers[i]);
containers.splice(i, 1);
}
}
}
//returns whether the given container is required for display
private static function isRequiredContainer(container:Bitmap):Boolean
{
if (hasBackground(stageCenterX, stageCenterY) == container) //center
return true;
if (hasBackground(stageCenterX - stageWidth * 0.75, stageCenterY - stageHeight * 0.75) == container) //top left
return true;
if (hasBackground(stageCenterX, stageCenterY - stageHeight * 0.75) == container) //top
return true;
if (hasBackground(stageCenterX + stageWidth * 0.75, stageCenterY - stageHeight * 0.75) == container) //top right
return true;
if (hasBackground(stageCenterX - stageWidth * 0.75, stageCenterY) == container) //center left
return true;
if (hasBackground(stageCenterX + stageWidth * 0.75, stageCenterY) == container) //center right
return true;
if (hasBackground(stageCenterX - stageWidth * 0.75, stageCenterY + stageHeight * 0.75) == container) //bottom left
return true;
if (hasBackground(stageCenterX, stageCenterY + stageHeight * 0.75) == container) //bottom center
return true;
if (hasBackground(stageCenterX + stageWidth * 0.75, stageCenterY + stageHeight * 0.75) == container) //bottom right
return true;
return false;
}
//dispatched when all assets have finished downloading
private final function assetsComplete(event:Event):void
{
assets = [];
assets.push(bulkLoader.getBitmap("background/1.png")); //star simple
assets.push(bulkLoader.getBitmap("background/2.png")); //star bright
assets.push(bulkLoader.getBitmap("background/3.png")); //cloud white
assets.push(bulkLoader.getBitmap("background/4.png")); //cloud red
init();
}
//initializes startup background containers
private final function init():void
{
masterContainer = new Sprite(); //create master container
//generate default background container
containers.push(genSquareBg()); //top left
containers[0].x = 0;
containers[0].y = 0;
masterContainer.addChild(containers[0]);
//display the master container
masterContainer.x = -(stageWidth / 2);
masterContainer.y = -(stageHeight / 2);
masterContainer.cacheAsBitmap = true;
addChild(masterContainer);
}
//duplicates a bitmap display object
private static function dupeBitmap(source:Bitmap):Bitmap {
var data:BitmapData = source.bitmapData;
var bitmap:Bitmap = new Bitmap(data);
return bitmap;
}
//draws a simple star
private static function drawStar(x:Number, y:Number, width:uint, height:uint):Sprite
{
var creation:Sprite = new Sprite();
creation.graphics.lineStyle(1, 0xFFFFFF);
creation.graphics.beginFill(0xFFFFFF);
creation.graphics.drawRect(x, y, width, height);
return creation;
}
//generates a background square
private static function genSquareBg():Bitmap
{
var width:Number = stageWidth * 2;
var height:Number = stageHeight * 2;
var startX:Number = 0;
var startY:Number = 0;
var scale:Number;
var drawAmount:uint;
var tmpBitmap:Bitmap;
var tmpSprite:Sprite;
var i:uint;
//create container
var container:Sprite = new Sprite();
//draw simple stars
drawAmount = UtilLib.getRandomInt(100, 250);
for(i = 1; i <= drawAmount; i++)
{
tmpSprite = drawStar(0, 0, 1, 1);
tmpSprite.x = UtilLib.getRandomInt(startX, width);
tmpSprite.y = UtilLib.getRandomInt(startY, height);
tmpSprite.alpha = UtilLib.getRandomInt(3, 10) / 10;
scale = UtilLib.getRandomInt(5, 15) / 10;
tmpSprite.scaleX = tmpSprite.scaleY = scale;
container.addChild(tmpSprite);
}
//draw bright stars
drawAmount = UtilLib.getRandomInt(1, 2);
for(i = 1; i <= drawAmount; i++)
{
tmpBitmap = dupeBitmap(assets[1]);
tmpBitmap.alpha = UtilLib.getRandomInt(3, 7) / 10;
tmpBitmap.rotation = UtilLib.getRandomInt(0, 360);
scale = UtilLib.getRandomInt(3, 10) / 10;
tmpBitmap.scaleX = scale; tmpBitmap.scaleY = scale;
tmpBitmap.x = UtilLib.getRandomInt(startX + tmpBitmap.width, width - tmpBitmap.width);
tmpBitmap.y = UtilLib.getRandomInt(startY + tmpBitmap.height, height - tmpBitmap.height);
container.addChild(tmpBitmap);
}
//draw white clouds
drawAmount = UtilLib.getRandomInt(2, 4);
for(i = 1; i <= drawAmount; i++)
{
tmpBitmap = dupeBitmap(assets[2]);
tmpBitmap.alpha = UtilLib.getRandomInt(1, 10) / 10;
scale = UtilLib.getRandomInt(15, 40);
tmpBitmap.scaleX = scale / 10;
tmpBitmap.scaleY = UtilLib.getRandomInt(scale / 2, scale * 2) / 10;
tmpBitmap.x = UtilLib.getRandomInt(startX, width - tmpBitmap.width);
tmpBitmap.y = UtilLib.getRandomInt(startY, height - tmpBitmap.height);
container.addChild(tmpBitmap);
}
//draw red clouds
drawAmount = UtilLib.getRandomInt(0, 2);
for(i = 1; i <= drawAmount; i++)
{
tmpBitmap = dupeBitmap(assets[3]);
tmpBitmap.alpha = UtilLib.getRandomInt(2, 6) / 10;
scale = UtilLib.getRandomInt(5, 40) / 10;
tmpBitmap.scaleX = scale; tmpBitmap.scaleY = scale;
tmpBitmap.x = UtilLib.getRandomInt(startX, width - tmpBitmap.width);
tmpBitmap.y = UtilLib.getRandomInt(startY, height - tmpBitmap.height);
container.addChild(tmpBitmap);
}
//convert all layers to a single bitmap layer and return
var bitmapData:BitmapData = new BitmapData(width, height, true, 0x000000);
bitmapData.draw(container);
container = null;
var bitmapContainer:Bitmap = new Bitmap(bitmapData);
//bitmapContainer.cacheAsBitmap = true;
return bitmapContainer;
}
}
}
ok, this should show you can really get another category of numbers with other aproaches ...
the limit here is not the number of stars, the limit is density, i.e. the number of stars visible at the same time ... with text disabled, i can get up to 700 # 30fps, on a Core2Duo, with quite a recent version of the debug player ...
i realized, flash player is not very good at clipping ... and that actually, using the most simple way, you spend a whole lot of time moving around objects, that are far from being visible ...
to really be able to optimize things, i chose to use MVC here ... not in the classic bloated way ... the idea is to handle the model, and if any elements are visible, create views for them ...
now the best aproach is to build up a spatial tree ...
you have leaves, containing objects, and nodes containing leaves or nodes
if you add an object to a leaf and it surpases a certain size, you turn it into a node with nxn leaves, redestributing its children between
any object added to the background will be added to a grid, determined by the object's coordinates ... grids are created just-in-time, an start off as leaves
the big advantage of this is, that you can quickly isolate the visible nodes/leaves.
in each iteration, only the nodes/leaves which either turn visible, or are already visible (and may become invisible), are interesting. you need not do any updates in the rest of the tree. after finding all the visible objects, you create views for objects that turn visible, update the position of those that simply stay visible, and destroy views for objects that become invisible ...
this saves an awful lot of everything ... memory and computation power ...
if you try with a huge world size (100000), you will see, that you run out of RAM quickly, long before CPU does anything ... instantiating 500000 stars uses 700MB ram, with about 50 stars visible, running at 70 fps without any tremendous CPU usage ...
the engine is just a proof of concept ... and code looks awful ... the only feature i am currently proud about is, that it supports object to occupate a certain area, which is why an object can be part of several leafs ... i think, this is something i will remove though, because it should give me a great speed up ... you also see, it stalls a little, while adding stars, which happens, when leafs flip to nodes ...
i am quite sure, this is not the best way for a spatial subdivision tree, it's just that everything i found seemed kind of useless to me ... probably someone who studied (and understood) CS, can refine my approach ... :)
other than that, i used an object pool for the views, and Haxe, since it performs better ... a thing that probably is not so clever, is to move all the views individually, instead of putting them on one layer and moving that around ...
some people also render things into BitmapDatas manually, to gain performance, which seems to work quite well (just can't find the question right now) ... you should however consider using copyPixels instead of draw ...
hope this helps ... ;)
edit: i decided to turn my detailed answer into a blog post ... have fun reading ... ;)
You may want to see if you can blit all of the pieces together into a flattend Bitmaps as you go. Draw all of the layers and then use BitmapData's draw method to combine them into a single Bitmap.
Even with cacheAsBitmap on for all of the pieces Flash is still having to combine all of those pieces every frame.
Try stretching the window of the player, both bigger and smaller. If that has a significant effect on frame rate, your fastest and easiest way to improve performance is to shrink the size of the stage. This tends to be an unpopular answer when presented to people - especially artists - but if your bottleneck is in the size of your stage, there is not much you can do in code to fix that.
What if instead of destroying background squares you just put them in a pile of "ready to go" squares that you can draw on, capping it at like 4? then you don't have to create one when you need one you just move it into the right spot and maybe shuffle the stars or something.
[would add example but i don't write AS3 code :(]
You should be able to simplify some of your math by using stored variables instead of stageCenterX + stageWidth * 0.75, and similar since they don't change.
Have you considered using
HitTestPoint instead of doing the math to check positions of containers? It's a native function, so it might be faster.
You should use a Shape instead of a Sprite if you don't need to add children to the object. e.g., your star. This might help quite a bit.
What if you created a set of star backgrounds at the start of the program. Then converted them to bitmaps, and saved them for later reuse. e.g., create a star background, convert it to bitmap data, and save this in an array. Do this, say, 10 times, and then when you need a background to just randomly select one, and apply your other shapes to it. The benefit of doing this is that you don't have to render 100-250 Sprites or Shapes each time you create a new background--that takes time.
EDIT: New idea:
Maybe you can play with the idea of only drawing the stars on the backgrounds rather than adding individual objects. The number of objects added to the screen are a big part of the problem. So I'm suggesting you draw the stars on the container directly, but with different sizes and alphas. Then scale the container down so that you get the effect you're looking for. You could reduce the display footprint from 500-1000 stars down to 0. That would be a huge improvement if you can get the effect you need from it.
Try using a single large BitmapData with it's own Bitmap, larger than the stage (although you might hit the limits of BitmapDatas if you have a really big stage and/or are using flash 9), and drawing new background images to it using the copyPixels method (a really fast way of copying pixels, faster than draw(), at least as far as I've seen).
Pan the large Bitmap around when you want and when you reach an edge, pan the bitmap to the other side, copyPixels the whole thing back to where it was previously (so the image should stay in the same place relative to the stage, but the Bitmap should move) and copyPixels new images where they are missing.
Since you are using alpha for the images as well, so you might want to check all the parameters of copyPixels if it doesn't copy alpha as you wanted it to (probably mergeAlpha?).
So, to recap, use a single large Bitmap that extends well over the boundaries of the stage, have the images ready as BitmapDatas, do the wrap trick and fill in the blanks with copyPixels from images.
I don't know if this way would perform better (the copyPixels over the whole bitmap worries me a little), but it's definitely something to try. Good luck :)