HTML5 Canvas - Anti-aliasing and Paint Bucket / Flood Fill - html

Having trawled Stack Overflow and Google it seems to me that there is no way to disable antialiasing when drawing lines on an HTML5 canvas.
This makes for nice looking lines, but causes me a problem when it comes time to applying a paint bucket / flood fill algorithm.
Part of my application requires that users draw on a canvas, freestyle drawing with basic tools like line size, color... and a paint bucket.
Because lines are rendered with antialiasing they are not a consistent color... with that in mind consider the following:
Draw a thick line in black
Decide at some point later that the line should be red
Apply flood fill to black line
My flood fill algorithm fills the bulk of the line with red, but the edges that were antialiased are detected as being outside the area that should be filled... hence remain (greys / blues(?) left over from the black line).
The flood fill algorithm does not incorporate something akin to 'tolerance' like Photoshop does... I have considered something like that but am unsure it would help as I don't think the anti-aliasing does something simple like render grey next to a black line, I think it's more advanced than that and the anti-aliasing takes into consideration the surrounding colors and blends.
Does anyone have any suggestions as to how I can end up with a better paint bucket / flood fill that COMPLETELY flood fills / replaces an existing line or section of a drawing?

If you simply want to change a color of a line: don't use bucket paint fill at all.
Store all your lines and shapes as objects/arrays and redraw them when needed.
This not only allow you to change canvas size without losing everything on it, but to change a color is simply a matter of changing a color property on your object/array and redraw, as well as scaling everything based on vectors instead of raster.
This will be faster than a bucket fill as redrawing is handled in most part internally and not by pixel-by-pixel in JavaScript as is needed with a bucket fill.
That being said: you cannot, unfortunately, disable anti-alias for shapes and lines, only for images (using the imageSmoothingEnabled property).
An object could look like this:
function myLine(x1, y1, x2, y2, color) {
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
this.color = color;
return this;
}
And then allocate it by:
var newLine = new myLine(x1, y1, x2, y2, color);
Then store this to an array:
/// globally:
var myLineStack = [];
/// after x1/x2/y1/y2 and color is achieved in the draw function:
myLineStack.push(new myLine(x1, y1, x2, y2, color));
Then it is just a matter of iterating through the objects when an update is needed:
/// some index to a line you want to change color for:
myLineStack[index].color = newColor;
/// Redraw all (room for optimizations here...)
context.clearRect( ... );
for(var i = 0, currentLine; currentLine = myLineStack[i]; i++) {
/// new path
context.beginPath();
/// set the color for this line
context.strokeStyle = currentLine.color;
/// draw the actual line
context.moveTo(currentLine.x1, currentLine.y1);
context.lineTo(currentLine.x2, currentLine.y2);
context.stroke();
}
(For optimizations you can for example clear only the area that needs redraw and draw a single index. You can also group lines/shapes with the same colors and draw then with a single setting of strokeStyle etc.)

You can not always redraw the canvas, you may have used filters that can not be reversed, or just use so many fill and stroke calls it would be impractical to redraw.
I have my own flood fill based on a simple fill stack that paints to a tolerances and does its best to lessen anti-aliasing artifacts. Unfortunately if you have anti-aliasing on repeated fills will grow the filled region.
Below is the function, adapt it as suited, it is a direct lift from my code with comments added.
// posX,posY are the fill start position. The pixel at the location is used to test tolerance.
// RGBA is the fill colour as an array of 4 bytes all ranged 0-255 for R,G,B,A
// diagonal if true the also fill into pixels that touch at the corners.
// imgData canvas pixel data from ctx.getImageData method
// tolerance Fill tolerance range 0 only allow exact same colour to fill to 255
// fill all but the extreme opposite.
// antiAlias if true fill edges to reduce anti-Aliasing artifacts.
Bitmaps.prototype.floodFill = function (posX, posY, RGBA, diagonal,imgData,tolerance,antiAlias) {
var data = imgData.data; // image data to fill;
antiAlias = true;
var stack = []; // paint stack to find new pixels to paint
var lookLeft = false; // test directions
var lookRight = false;
var w = imgData.width; // width and height
var h = imgData.height;
var painted = new Uint8ClampedArray(w*h); // byte array to mark painted area;
var dw = w*4; // data width.
var x = posX; // just short version of pos because I am lazy
var y = posY;
var ind = y * dw + x * 4; // get the starting pixel index
var sr = data[ind]; // get the start colour tha we will use tollerance against.
var sg = data[ind+1];
var sb = data[ind+2];
var sa = data[ind+3];
var sp = 0;
var dontPaint = false; // flag to indicate if checkColour can paint
// function checks a pixel colour passes tollerance, is painted, or out of bounds.
// if the pixel is over tollerance and not painted set it do reduce anti alising artifacts
var checkColour = function(x,y){
if( x<0 || y < 0 || y >=h || x >= w){ // test bounds
return false;
}
var ind = y * dw + x * 4; // get index of pixel
var dif = Math.max( // get the max channel differance;
Math.abs(sr-data[ind]),
Math.abs(sg-data[ind+1]),
Math.abs(sb-data[ind+2]),
Math.abs(sa-data[ind+3])
);
if(dif < tolerance){ // if under tollerance pass it
dif = 0;
}
var paint = Math.abs(sp-painted[y * w + x]); // is it already painted
if(antiAlias && !dontPaint){ // mitigate anti aliasing effect
// if failed tollerance and has not been painted set the pixel to
// reduce anti alising artifact
if(dif !== 0 && paint !== 255){
data[ind] = RGBA[0];
data[ind+1] = RGBA[1];
data[ind+2] = RGBA[2];
data[ind+3] = (RGBA[3]+data[ind+3])/2; // blend the alpha channel
painted[y * w + x] = 255; // flag pixel as painted
}
}
return (dif+paint)===0?true:false; // return tollerance status;
}
// set a pixel and flag it as painted;
var setPixel = function(x,y){
var ind = y * dw + x * 4; // get index;
data[ind] = RGBA[0]; // set RGBA
data[ind+1] = RGBA[1];
data[ind+2] = RGBA[2];
data[ind+3] = RGBA[3];
painted[y * w + x] = 255; // 255 or any number >0 will do;
}
stack.push([x,y]); // push the first pixel to paint onto the paint stack
while (stack.length) { // do while pixels on the stack
var pos = stack.pop(); // get the pixel
x = pos[0];
y = pos[1];
dontPaint = true; // turn off anti alising
while (checkColour(x,y-1)) { // find the bottom most pixel within tolerance;
y -= 1;
}
dontPaint = false; // turn on anti alising if being used
//checkTop left and right if alowing diagonal painting
if(diagonal){
if(!checkColour(x-1,y) && checkColour(x-1,y-1)){
stack.push([x-1,y-1]);
}
if(!checkColour(x+1,y) && checkColour(x+1,y-1)){
stack.push([x+1,y-1]);
}
}
lookLeft = false; // set look directions
lookRight = false; // only look is a pixel left or right was blocked
while (checkColour(x,y)) { // move up till no more room
setPixel(x,y); // set the pixel
if (checkColour(x - 1,y)) { // check left is blocked
if (!lookLeft) {
stack.push([x - 1, y]); // push a new area to fill if found
lookLeft = true;
}
} else
if (lookLeft) {
lookLeft = false;
}
if (checkColour(x+1,y)) { // check right is blocked
if (!lookRight) {
stack.push([x + 1, y]); // push a new area to fill if found
lookRight = true;
}
} else
if (lookRight) {
lookRight = false;
}
y += 1; // move up one pixel
}
// check down left
if(diagonal){ // check for diagnal areas and push them to be painted
if(checkColour(x-1,y) && !lookLeft){
stack.push([x-1,y]);
}
if(checkColour(x+1,y) && !lookRight){
stack.push([x+1,y]);
}
}
}
// all done
}
There is a better way that gives high quality results, the above code can be adapted to do this by using the painted array to mark the paint edges and then after the fill has completed scan the painted array and apply a convolution filter to each edge pixel you have marked. The filter is directional (depending on which sides are painted) and the code too long for this answer. I have pointed you in the right direction and the infrastructure is above.
Another way to improve the image quality is to super sample the image you are drawing to. Hold a second canvas that is double the size of the image being painted. Do all you drawing to that image and display it to the user on another canvas with CTX.imageSmoothingEnabled and ctx.setTransform(0.5,0,0,0.5,0,0) half size, when done and the image is ready half its size manually with the following code (don't rely on canvas imageSmoothingEnabled as it gets it wrong.)
Doing this will greatly improve the quality of your final image and with the above fill almost completely eliminate anti-aliasing artifacts from flood fills.
// ctxS is the source canvas context
var w = ctxS.canvas.width;
var h = ctxS.canvas.height;
var data = ctxS.getImageData(0,0,w,h);
var d = data.data;
var x,y;
var ww = w*4;
var ww4 = ww+4;
for(y = 0; y < h; y+=2){
for(x = 0; x < w; x+=2){
var id = y*ww+x*4;
var id1 = Math.floor(y/2)*ww+Math.floor(x/2)*4;
d[id1] = Math.sqrt((d[id]*d[id]+d[id+4]*d[id+4]+d[id+ww]*d[id+ww]+d[id+ww4]*d[id+ww4])/4);
id += 1;
id1 += 1;
d[id1] = Math.sqrt((d[id]*d[id]+d[id+4]*d[id+4]+d[id+ww]*d[id+ww]+d[id+ww4]*d[id+ww4])/4);
id += 1;
id1 += 1;
d[id1] = Math.sqrt((d[id]*d[id]+d[id+4]*d[id+4]+d[id+ww]*d[id+ww]+d[id+ww4]*d[id+ww4])/4);
id += 1;
id1 += 1;
d[id1] = Math.sqrt((d[id]*d[id]+d[id+4]*d[id+4]+d[id+ww]*d[id+ww]+d[id+ww4]*d[id+ww4])/4);
}
}
ctxS.putImageData(data,0,0); // save imgData
// grab it again for new image we don't want to add artifacts from the GPU
var data = ctxS.getImageData(0,0,Math.floor(w/2),Math.floor(h/2));
var canvas = document.createElement("canvas");
canvas.width = Math.floor(w/2);
canvas.height =Math.floor(h/2);
var ctxS = canvas.getContext("2d",{ alpha: true });
ctxS.putImageData(data,0,0);
// result canvas with downsampled high quality image.

Related

Why does Canvas's putImageData not work when I specify target location?

In trying to find documentation for Canvas context's putImageData() method, I've found things like this:
context.putImageData(imgData,x,y,dirtyX,dirtyY,dirtyWidth,dirtyHeight);
(from http://www.w3schools.com/tags/canvas_putimagedata.asp)
According to the documentation I've read, x and y are an index into the source image, whereas dirtyX and dirtyY specify coordinates in the target canvas where to draw the image. Yet, as you'll see from the example below (and JSFiddle) a call to putImageData(imgData,x,y) works while putImageData(imgData, 0, 0, locX, locY) doesn't. I'm not sure why.
EDIT:
I guess my real question is why the top row of the image is black, and there are only 7 rows, not 8. The images should start at the top-left of the Canvas. They DO start at the left (and have 8 columns). Why do they not start at the top?
Answer: that's due to divide by 0 on this line when yLoc is 0:
xoff = imgWidth / (yLoc/3);
The JSFiddle:
http://jsfiddle.net/WZynM/
Code:
<html>
<head>
<title>Canvas tutorial</title>
<script type="text/javascript">
var canvas;
var context; // The canvas's 2d context
function setupCanvas()
{
canvas = document.getElementById('myCanvas');
if (canvas.getContext)
{
context = canvas.getContext('2d');
context.fillStyle = "black"; // this is default anyway
context.fillRect(0, 0, canvas.width, canvas.height);
}
}
function init()
{
loadImages();
startGating();
}
var images = new Array();
var gatingTimer;
var curIndex, imgWidth=0, imgHeight;
// Load images
function loadImages()
{
for (n = 1; n <= 16; n++)
{
images[n] = new Image();
images[n].src = "qxsImages/frame" + n + ".png";
// document.body.appendChild(images[n]);
console.log("width = " + images[n].width + ", height = " + images[n].height);
}
curIndex = 1;
imgWidth = images[1].width;
imgHeight = images[1].height;
}
function redrawImages()
{
if (imgWidth == 0)
return;
curIndex++;
if (curIndex > 16)
curIndex = 1;
// To do later: use images[1].width and .height to layout based on image size
for (var x=0; x<8; x++)
{
for (var y=0; y<8; y++)
{
//if (x != 1)
// context.drawImage(images[curIndex], x*150, y*100);
// context.drawImage(images[curIndex], x*150, y*100, imgWidth/2, imgHeight/2); // scale
// else
self.drawCustomImage(x*150, y*100);
}
}
}
function drawCustomImage(xLoc, yLoc)
{
// create a new pixel array
imageData = context.createImageData(imgWidth, imgHeight);
pos = 0; // index position into imagedata array
xoff = imgWidth / (yLoc/3); // offsets to "center"
yoff = imgHeight / 3;
for (y = 0; y < imgHeight; y++)
{
for (x = 0; x < imgWidth; x++)
{
// calculate sine based on distance
x2 = x - xoff;
y2 = y - yoff;
d = Math.sqrt(x2*x2 + y2*y2);
t = Math.sin(d/6.0);
// calculate RGB values based on sine
r = t * 200;
g = 125 + t * 80;
b = 235 + t * 20;
// set red, green, blue, and alpha:
imageData.data[pos++] = Math.max(0,Math.min(255, r));
imageData.data[pos++] = Math.max(0,Math.min(255, g));
imageData.data[pos++] = Math.max(0,Math.min(255, b));
imageData.data[pos++] = 255; // opaque alpha
}
}
// copy the image data back onto the canvas
context.putImageData(imageData, xLoc, yLoc); // Works... kinda
// context.putImageData(imageData, 0, 0, xLoc, yLoc, imgWidth, imgHeight); // Doesn't work. Why?
}
function startGating()
{
gatingTimer = setInterval(redrawImages, 1000/25); // start gating
}
function stopGating()
{
clearInterval(gatingTimer);
}
</script>
<style type="text/css">
canvas { border: 1px solid black; }
</style>
</head>
<body onload="setupCanvas(); init();">
<canvas id="myCanvas" width="1200" height="800"></canvas>
</body>
</html>
http://jsfiddle.net/WZynM/
You just had your coordinates backwards.
context.putImageData(imageData, xLoc, yLoc, 0, 0, imgWidth, imgHeight);
Live Demo
xLoc, and yLoc are where you are putting it, and 0,0,imgWidth,imgHeight is the data you are putting onto the canvas.
Another example showing this.
A lot of the online docs seem a bit contradictory but for the seven param version
putImageData(img, dx, dy, dirtyX, dirtyY, dirtyRectWidth, dirtyRectHeight)
the dx, and dy are your destination, the next four params are the dirty rect parameters, basically controlling what you are drawing from the source canvas. One of the most thorough descriptions I can find was in the book HTML5 Unleashed by Simon Sarris (pg. 165).
Having been using this recently, I've discovered that Loktar above has hit upon a VERY important issue. Basically, some documentation of this method online is incorrect, a particularly dangerous example being W3Schools, to which a number of people will turn to for reference.
Their documentation states the following:
Synopsis:
context.putImageData(imgData,x,y,dirtyX,dirtyY,dirtyWidth,dirtyHeight);
Arguments:
imgData: Specifies the ImageData object to put back onto the canvas
x : The x-coordinate, in pixels, of the upper-left corner of the ImageData object [WRONG]
y : The y-coordinate, in pixels, of the upper-left corner of the ImageData object [WRONG]
dirtyX : Optional. The horizontal (x) value, in pixels, where to place the image on the canvas [WRONG]
dirtyY : Optional. The vertical (y) value, in pixels, where to place the image on the canvas [WRONG]
dirtyWidth : Optional. The width to use to draw the image on the canvas
dirtyHeight: Optional. The height to use to draw the image on the canvas
As Loktar states above, the CORRECT synopsis is as follows:
Correct Synopsis:
context.putImageData(imgData, canvasX, canvasY, srcX ,srcY, srcWidth, srcHeight);
Arguments:
imgData: Specifies the ImageData object to put back onto the canvas (as before);
canvasX : The x coordinate of the location on the CANVAS where you are plotting your imageData;
canvasY : The y coordinate of the location on the CANVAS where you are plotting your ImageData;
srcX : Optional. The x coordinate of the top left hand corner of your ImageData;
srcY : Optional. The y coordinate of the top left hand corner of your ImageData;
srcWidth : Optional. The width of your ImageData;
srcHeight : Optional. The height of your ImageData.
Use the correct synopsis above, and you won't have the problems that have been encountered above.
I'll give a big hat tip to Loktar for finding this out initially, but I thought it apposite to provide an expanded answer in case others run into the same problem.

How to draw dashed lines with using rectangles in AS3?

I'm using starling framework for my game project and it hasn't got any draw dashed line method. Because of this they suggest me to draw dashed lines with using small rectangles which is called quads.
My math is not enough for it, could you give a sample method for rectangles with dashed lines occurring.
Thanks..
This class by Andy Woodruff draws dashed lines
/*
DashedLine class
by Andy Woodruff (http://cartogrammar.com/blog || awoodruff#gmail.com)
May 2008
Still in progress; I'll get a more perfected version eventually. For now take it as is.
This is a Sprite with the capability to do basic lineTo drawing with dashed lines.
Example:
var dashy:DashedLine = new DashedLine(2,0x333333,new Array(3,3,10,3,5,8,7,13));
dashy.moveTo(120,120);
dashy.beginFill(0xcccccc);
dashy.lineTo(220,120);
dashy.lineTo(220,220);
dashy.lineTo(120,220);
dashy.lineTo(120,120);
dashy.endFill();
*/
package com.cartogrammar.drawing {
import flash.display.Shape;
import flash.display.Sprite;
import flash.geom.Point;
import flash.display.CapsStyle;
public class DashedLine extends Sprite {
var lengthsArray:Array = new Array(); // array of dash and gap lengths (dash,gap,dash,gap....)
var lineColor:uint; // line color
var lineWeight:Number; // line weight
var lineAlpha:Number = 1; // line alpha
var curX:Number = 0; // stores current x as it changes with lineTo and moveTo calls
var curY:Number = 0; // same as above, but for y
var remainingDist:Number = 0; // stores distance between the end of the last full dash or gap and the end coordinates specified in lineTo
var curIndex = null; // current index in the length array, so we know which dash or gap to draw
var arraySum:Number = 0; // total length of the dashes and gaps... not currently being used for anything, but maybe useful?
var startIndex:int = 0; // array index (the particular dash or gap) to start with in a lineTo--based on the last dash or gap drawn in the previous lineTo (along with remainingDist, this is so our line can properly continue around corners!)
var fill:Shape = new Shape(); // shappe in the background to be used for fill (if any)
var stroke:Shape = new Shape(); // shape in the foreground to be used for the dashed line
public function DashedLine(weight:Number = 0, color:Number = 0, lengthsArray:Array = null){
if (lengthsArray != null){ // if lengths array was specified, use it
this.lengthsArray = lengthsArray;
} else { // if unspecified, use a default 5-5 line
this.lengthsArray = [5,5];
}
if (this.lengthsArray.length % 2 != 0){ // if array has more dashes than gaps (i.e. an odd number of values), add a 5 gap to the end
lengthsArray.push(5);
}
// sum the dash and gap lengths
for (var i:int in lengthsArray){
arraySum += lengthsArray[i];
}
// set line weight and color properties from constructor arguments
lineWeight = weight;
lineColor = color;
// set the lineStyle according to specified properties - beyond weight and color, we use the defaults EXCEPT no line caps, as they interfere with the desired gaps
stroke.graphics.lineStyle(lineWeight,lineColor,lineAlpha,false,"none",CapsStyle.NONE);
// add fill and stroke shapes
addChild(fill);
addChild(stroke);
}
// basic moveTo method
public function moveTo(x:Number,y:Number):void{
stroke.graphics.moveTo(x,y); // move to specified x and y
fill.graphics.moveTo(x,y);
// keep track of x and y
curX = x;
curY = y;
// reset remainingDist and startIndex - if we are moving away from last line segment, the next one will start at the beginning of the dash-gap sequence
remainingDist = 0;
startIndex = 0;
}
// lineTo method
public function lineTo(x:Number,y:Number):void{
var slope:Number = (y - curY)/(x - curX); // get slope of segment to be drawn
// record beginning x and y
var startX:Number = curX;
var startY:Number = curY;
// positive or negative direction for each x and y?
var xDir:int = (x < startX) ? -1 : 1;
var yDir:int = (y < startY) ? -1 : 1;
// keep drawing dashes and gaps as long as either the current x or y is not beyond the destination x or y
outerLoop : while (Math.abs(startX-curX) < Math.abs(startX-x) || Math.abs(startY-curY) < Math.abs(startY-y)){
// loop through the array to draw the appropriate dash or gap, beginning with startIndex (either 0 or determined by the end of the last lineTo)
for (var i:int = startIndex; i < lengthsArray.length; i++){
var dist:Number = (remainingDist == 0) ? lengthsArray[i] : remainingDist; // distance to draw is either the dash/gap length from the array or remainingDist left over from the last lineTo if there is any
// get increments of x and y based on distance, slope, and direction - see getCoords()
var xInc:Number = getCoords(dist,slope).x * xDir;
var yInc:Number = getCoords(dist,slope).y * yDir;
// if the length of the dash or gap will not go beyond the destination x or y of the lineTo, draw the dash or gap
if (Math.abs(startX-curX) + Math.abs(xInc) < Math.abs(startX-x) || Math.abs(startY-curY) + Math.abs(yInc) < Math.abs(startY-y)){
if (i % 2 == 0){ // if even index in the array, it is a dash, hence lineTo
stroke.graphics.lineTo(curX + xInc,curY + yInc);
} else { // if odd, it's a gap, so moveTo
stroke.graphics.moveTo(curX + xInc,curY + yInc);
}
// keep track of the new x and y
curX += xInc;
curY += yInc;
curIndex = i; // store the current dash or gap (array index)
// reset startIndex and remainingDist, as these will only be non-zero for the first loop (through the array) of the lineTo
startIndex = 0;
remainingDist = 0;
} else { // if the dash or gap can't fit, break out of the loop
remainingDist = getDistance(curX,curY,x,y); // get the distance between the end of the last dash or gap and the destination x/y
curIndex = i; // store the current index
break outerLoop; // break out of the while loop
}
}
}
startIndex = curIndex; // for next time, the start index is the last index used in the loop
if (remainingDist != 0){ // if there is a remaining distance, line or move from current x/y to the destination x/y
if (curIndex % 2 == 0){ // even = dash
stroke.graphics.lineTo(x,y);
} else { // odd = gap
stroke.graphics.moveTo(x,y);
}
remainingDist = lengthsArray[curIndex] - remainingDist; // remaining distance (which will be used at the beginning of the next lineTo) is now however much is left in the current dash or gap after that final lineTo/moveTo above
} else { // if there is no remaining distance (i.e. the final dash or gap fits perfectly), we're done with the current dash or gap, so increment the start index for next time
if (startIndex == lengthsArray.length - 1){ // go to the beginning of the array if we're at the end
startIndex = 0;
} else {
startIndex++;
}
}
// at last, the current x and y are the destination x and y
curX = x;
curY = y;
fill.graphics.lineTo(x,y); // simple lineTo (invisible line) on the fill shape so that the fill (if one was started via beginFill below) follows along with the dashed line
}
// returns a point with the vertical and horizontal components of a diagonal given the distance and slope
private function getCoords(distance:Number,slope:Number):Point {
var angle:Number = Math.atan(slope); // get the angle from the slope
var vertical:Number = Math.abs(Math.sin(angle)*distance); // vertical from sine of angle and length of hypotenuse - using absolute value here and applying negative as needed in lineTo, because this number doesn't always turn out to be negative or positive exactly when I want it to (haven't thought through the math enough yet to figure out why)
var horizontal:Number = Math.abs(Math.cos(angle)*distance); // horizontal from cosine
return new Point(horizontal,vertical); // return the point
}
// basic Euclidean distance
private function getDistance(startX:Number,startY:Number,endX:Number,endY:Number):Number{
var distance:Number = Math.sqrt(Math.pow((endX-startX),2) + Math.pow((endY-startY),2));
return distance;
}
// clear everything and reset the lineStyle
public function clear():void{
stroke.graphics.clear();
stroke.graphics.lineStyle(lineWeight,lineColor,lineAlpha,false,"none",CapsStyle.NONE);
fill.graphics.clear();
moveTo(0,0);
}
// set lineStyle with specified weight, color, and alpha
public function lineStyle(w:Number=0,c:Number=0,a:Number=1):void{
lineWeight = w;
lineColor = c;
lineAlpha = a;
stroke.graphics.lineStyle(lineWeight,lineColor,lineAlpha,false,"none",CapsStyle.NONE);
}
// basic beginFill
public function beginFill(c:uint,a:Number=1):void{
fill.graphics.beginFill(c,a);
}
// basic endFill
public function endFill():void{
fill.graphics.endFill();
}
}
}
Thanks For Your Replies, I Wrote My Own Class Which Draws Rectangles To Create A Dashed Lines.
Thanks..

Manipulate pixels before drawing on the canvas

I'm using the following code to draw a base64 image on the canvas. I get the base64 string from a query in PHP. With globalAlpha i can set the alpha of the whole image to 0. I need to manipulate the alpha of random pixels with a form. So when I submit 7 with the form, 7 random pixels need to be set from alpha 0 to 255.
Is it possible to manipulate the alpha of this image and after that, draw it to the canvas? It's very important that the original image remains secret.
var complex = "<?php echo str_replace("\n", "", $DBimage); ?>";
var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");
var image = new Image();
image.src = "data:image/png;base64," + complex;
image.onload= function(){
ctx.drawImage(image, 0, 0);
}
<canvas id="rectangle" width="300" height="300" style="border:solid black 1px; </canvas>
​
the javascript
var canvas = document.getElementById('rectangle');
//replace this rectangle drawing by your image
if (canvas.getContext) {
var context = canvas.getContext('2d');
context.fillStyle = "rgb(150,29,28)";
context.fillRect(10, 10, 280, 280);
}
var imgd = context.getImageData(0, 0, canvas.width, canvas.height);
var numberOfAlphaPix = 10000; //this is the number of pixel for which you want to change the alpha channel, I let you do the job to retrieve this number as you wish
//fill an array with numbers that we'll pop to get unique random values
var rand = [];
// Loop over each pixel with step of 4 to store only alpha channel in array ( R=0 ,G=1 , B=2, A=3 )
for (var i = 3; i < imgd.data.length; i += 4) {
rand.push(i);
}
//shuffle the array
for (var i = rand.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var tmp = rand[i];
rand[i] = rand[j];
rand[j] = tmp;
}
for (var i = 0; i < numberOfAlphaPix; i++) {
imgd.data[rand.pop()] = 255; //set alpha channel to 255
}
// Draw the ImageData object at the given (x,y) coordinates.
context.putImageData(imgd, 0, 0);​
Try it here http://jsfiddle.net/LuEzG/5/
I'm not sure what you mean "stay secret", but I think you mean:
show only a few pixels at a time
Make it impossible for people to view source and discover the image (with a bit of JavaScript)
If those are the only requirements (And if they are requirements) then you'll have to:
decode the image on the server
Pick a few random pixels on the server and send the data for those pixels (RGB values) to the client
Use canvas to show those few pixels that were received
The nice thing about this approach is that you don't need to use ImageData at all. You can just fillRect with a fillStyle of the RGB values for each pixel recieved.
The not-so-nice thing about this approach is that it means you have to do a lot more work on the server, but if you want the image to be completely hidden from the client, it's the only way.

Does Canvas redraw itself every time anything changes?

I have done some research on how canvas works. It is supposed to be "immediate mode" means that it does not remember what its drawing looks like, only the bitmap remains everytime anything changes.
This seems to suggest that canvas does not redraw itself on change.
However, when I tested canvas on iPad (basically I keep drawing parallel lines on the canvas), the frame rate degrades rapidly when there are more lines on the canvas. Lines are drawn more slowly and in a more jumpy way.
Does this mean canvas actually have to draw the whole thing on change? Or there is other reason for this change in performance?
The HTML canvas remembers the final state of pixels after each stroke/fill call is made. It never redraws itself. (The web browser may need to re-blit portions of the final image to the screen, for example if another HTML object is moved over the canvas and then away again, but this is not the same as re-issuing the drawing commands.
The context always remembers its current state, including any path that you have been accumulating. It is probable that you are (accidentally) not clearing your path between 'refreshes', and so on the first frame you are drawing one line, on the second frame two lines, on the third frame three lines, and so forth. (Are you calling ctx.closePath() and ctx.beginPath()? Are you clearing the canvas between drawings?)
Here's an example showing that the canvas does not redraw itself. Even at tens of thousands of lines I see the same frame rate as with hundreds of lines (capped at 200fps on Chrome, ~240fps on Firefox 8.0, when drawing 10 lines per frame).
var lastFrame = new Date, avgFrameMS=5, lines=0;
function drawLine(){
ctx.beginPath();
ctx.moveTo(Math.random()*w,Math.random()*h);
ctx.lineTo(Math.random()*w,Math.random()*h);
ctx.closePath();
ctx.stroke();
var now = new Date;
var frameTime = now - lastFrame;
avgFrameMS += (frameTime-avgFrameMS)/20;
lastFrame = now;
setTimeout(drawLine,1);
lines++;
}
drawLine();
// Show the stats infrequently
setInterval(function(){
fps.innerHTML = (1000/avgFrameMS).toFixed(1);
l.innerHTML = lines;
},1000);
Seen in action: http://phrogz.net/tmp/canvas_refresh_rate.html
For more feedback on what your code is actually doing versus what you suspect it is doing, share your test case with us.
Adding this answer to be more general.
It really depends on what the change is. If the change is simply to add another path to the previously drawn context, then the canvas does not have to be redrawn. Simply add the new path to the present context state. The previously selected answer reflects this with an excellent demo found here.
However, if the change is to translate or "move" an already drawn path to another part of the canvas, then yes, the whole canvas has to be redrawn. Imagine the same demo linked above accumulating lines while also rotating about the center of the canvas. For every rotation, the canvas would have to be redrawn, with all previously drawn lines redrawn at the new angle. This concept of redrawing on translation is fairly self-evident, as the canvas has no method of deleting from the present context. For simple translations, like a dot moving across the canvas, one could draw over the dot's present location and redraw the new dot at the new, translated location, all on the same context. This may or may not be more operationally complex than just redrawing the whole canvas with the new, translated dot, depending on how complex the previously drawn objects are.
Another demo to demonstrate this concept is when rendering an oscilloscope trace via the canvas. The below code implements a FIFO data structure as the oscilloscope's data, and then plots it on the canvas. Like a typical oscilloscope, once the trace spans the width of the canvas, the trace must translate left to make room for new data points on the right. To do this, the canvas must be redrawn every time a new data point is added.
function rand_int(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1) + min); //The maximum is inclusive and the minimum is inclusive
}
function Deque(max_len) {
this.max_len = max_len;
this.length = 0;
this.first = null;
this.last = null;
}
Deque.prototype.Node = function(val, next, prev) {
this.val = val;
this.next = next;
this.prev = prev;
};
Deque.prototype.push = function(val) {
if (this.length == this.max_len) {
this.pop();
}
const node_to_push = new this.Node(val, null, this.last);
if (this.last) {
this.last.next = node_to_push;
} else {
this.first = node_to_push;
}
this.last = node_to_push;
this.length++;
};
Deque.prototype.pop = function() {
if (this.length) {
let val = this.first.val;
this.first = this.first.next;
if (this.first) {
this.first.prev = null;
} else {
this.last = null;
}
this.length--;
return val;
} else {
return null;
}
};
Deque.prototype.to_string = function() {
if (this.length) {
var str = "[";
var present_node = this.first;
while (present_node) {
if (present_node.next) {
str += `${present_node.val}, `;
} else {
str += `${present_node.val}`
}
present_node = present_node.next;
}
str += "]";
return str
} else {
return "[]";
}
};
Deque.prototype.plot = function(canvas) {
const w = canvas.width;
const h = canvas.height;
const ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, w, h);
//Draw vertical gridlines
ctx.beginPath();
ctx.setLineDash([2]);
ctx.strokeStyle = "rgb(124, 124, 124)";
for (var i = 1; i < 9; i++) {
ctx.moveTo(i * w / 9, 0);
ctx.lineTo(i * w / 9, h);
}
//Draw horizontal gridlines
for (var i = 1; i < 10; i++) {
ctx.moveTo(0, i * h / 10);
ctx.lineTo(w, i * h / 10);
}
ctx.stroke();
ctx.closePath();
if (this.length) {
var present_node = this.first;
var x = 0;
ctx.setLineDash([]);
ctx.strokeStyle = "rgb(255, 51, 51)";
ctx.beginPath();
ctx.moveTo(x, h - present_node.val * (h / 10));
while (present_node) {
ctx.lineTo(x * w / 9, h - present_node.val * (h / 10));
x++;
present_node = present_node.next;
}
ctx.stroke();
ctx.closePath();
}
};
const canvas = document.getElementById("canvas");
const deque_contents = document.getElementById("deque_contents");
const button = document.getElementById("push_to_deque");
const min = 0;
const max = 9;
const max_len = 10;
var deque = new Deque(max_len);
deque.plot(canvas);
button.addEventListener("click", function() {
push_to_deque();
});
function push_to_deque() {
deque.push(rand_int(0, 9));
deque_contents.innerHTML = deque.to_string();
deque.plot(canvas);
}
body {
font-family: Arial;
}
.centered {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
}
<div class="centered">
<p>Implementation of a FIFO deque data structure in JavaScript to mimic oscilloscope functionality. Push the button to push random values to the deque object. After the maximum length is reached, the first item pushed in is popped out to make room for the next value. The values are plotted in the canvas. The canvas must be redrawn to translate the data, making room for the new data.
</p>
<div>
<button type="button" id="push_to_deque">push</button>
</div>
<div>
<h1 id="deque_contents">[]</h1>
</div>
<div>
<canvas id="canvas" width="800" height="500" style="border:2px solid #D3D3D3; margin: 10px;">
</canvas>
</div>
</div>

Insane Graphics.lineStyle behavior

I'd like some help with a little project of mine.
Background:
i have a little hierarchy of Sprite derived classes (5 levels starting from the one, that is the root application class in Flex Builder). Width and Height properties are overriden so that my class always remembers it's requested size (not just bounding size around content) and also those properties explicitly set scaleX and scaleY to 1, so that no scaling would ever be involved. After storing those values, draw() method is called to redraw content.
Drawing:
Drawing is very straight forward. Only the deepest object (at 1-indexed level 5) draws something into this.graphics object like this:
var gr:Graphics = this.graphics;
gr.clear();
gr.lineStyle(0, this.borderColor, 1, true, LineScaleMode.NONE);
gr.beginFill(0x0000CC);
gr.drawRoundRectComplex(0, 0, this.width, this.height, 10, 10, 0, 0);
gr.endFill();
Further on:
There is also MouseEvent.MOUSE_WHEEL event attached to the parent of the object that draws. What handler does is simply resizes that drawing object.
Problem:
Screenshot
When resizing sometimes that hairline border line with LineScaleMode.NONE set gains thickness (quite often even >10 px) + it quite often leaves a trail of itself (as seen in the picture above and below blue box (notice that box itself has one px black border)). When i set lineStile thickness to NaN or alpha to 0, that trail is no more happening.
I've been coming back to this problem and dropping it for some other stuff for over a week now.
Any ideas anyone?
P.S. Grey background is that of Flash Player itself, not my own choise.. :D
As requested, a bit more:
Application is supposed to be a calendar-timeline with a zooming "feature" (project for a course at university). Thus i have these functions that have something to do with resizing:
public function performZoom():void
{
// Calculate new width:
var newDayWidth:Number = view.width / 7 * this.calModel.zoom;
if (newDayWidth < 1)
{
newDayWidth = 1;
}
var newWidth:int = int(newDayWidth * timeline.totalDays);
// Calculate day element Height/Width ratio:
var headerHeight:Number = this.timeline.headerAllDay;
var proportion:Number = 0;
if (this.view.width != 0 && this.view.height != 0)
{
proportion = 1 / (this.view.width / 7) * (this.view.height - this.timeline.headerAllDay);
}
// Calculate new height:
var newHeight:int = int(newDayWidth * proportion + this.timeline.headerAllDay);
// Calculate mouse position scale on X axis:
var xScale:Number = 0;
if (this.timeline.width != 0)
{
xScale = newWidth / this.timeline.width;
}
// Calculate mouse position scale on Y axis:
var yScale:Number = 0;
if (this.timeline.height - this.timeline.headerAllDay != 0)
{
yScale = (newHeight - this.timeline.headerAllDay) / (this.timeline.height - this.timeline.headerAllDay);
}
this.timeline.beginUpdate();
// Resize the timeline
this.timeline.resizeElement(newWidth, newHeight);
this.timeline.endUpdate();
// Move timeline:
this.centerElement(xScale, yScale);
// Reset timeline local mouse position:
this.centerMouse();
}
public function resizeElement(widthValue:Number, heightValue:Number):void
{
var prevWidth:Number = this.myWidth;
var prevHeight:Number = this.myHeight;
if (widthValue != prevWidth || heightValue != prevHeight)
{
super.width = widthValue;
super.height = heightValue;
this.scaleX = 1.0;
this.scaleY = 1.0;
this.myHeight = heightValue;
this.myWidth = widthValue;
if (!this.docking)
{
this.origHeight = heightValue;
this.origWidth = widthValue;
}
this.updateAnchorMargins();
onResizeInternal(prevWidth, prevHeight, widthValue, heightValue);
}
}
Yes. I know.. a lot of core, and a lot of properties, but in fact most of the stuff has been disabled at the end and the situation is as described at the top.
this didn't work:
gr.lineStyle(); // Reset line style
Can we see your resizing code?
Also try clearing your line style as well as your fill:
gr.lineStyle(0, this.borderColor, 1, true, LineScaleMode.NONE);
gr.beginFill(0x0000CC);
gr.drawRoundRectComplex(0, 0, this.width, this.height, 10, 10, 0, 0);
gr.endFill();
gr.lineStyle();//<---- add this line
I don't know whether it's flash bug or what it is, but i finally found the solution.
The thing is that in my case when resizing in a nutshell you get like this:
this.width = this.stage.stageWidth / 7 * 365;
When i switch to maximized window this.width gains value 86k+. When i added this piece of code to draw horizontal line, it fixed itself:
var recWidth:Number = this.width;
var i:int;
var newEnd:Number = 0;
for (i = 1; newEnd < recWidth; i++)
{
newEnd = 100 * i;
if (newEnd > recWidth)
{
newEnd = recWidth;
}
gr.lineTo(newEnd, 0);
}
I don't know what is going on.. This is inconvenient...