How would I clip everything in the following drawing except for the S stroke? In other words get rid of all transparent space and only keep black S shape... thanks in advance!
=== PNG RENDERED IMAGE OF CANVAS ===
image located at --> http://buildasearch.com/ant/s.png
=== ACTUAL COORDINATES OF CANVAS DRAWING ===
var x = '68,67,66,65,64,63,62,61,60,59,57,56,55,54,53,52,51,51,51,51,51,51,51,52,55,56,52,58,60,59,61,62,64,65,66,68,70,71,72,74,75,76,77,78,78,79,79,79,79,79,79,79,79,79,79,79,79,78,76,74,71,67,59,56,54,52,49,47,46,45,43,42,41,40,39';
var y = '11,11,11,11,11,12,12,12,12,12,13,14,14,15,17,18,20,21,23,24,27,30,32,32,34,34,33,34,34,34,34,35,35,35,35,35,35,36,36,37,38,38,39,40,41,42,43,44,45,47,48,49,50,51,52,53,54,55,56,57,58,59,59,59,60,60,60,60,60,60,60,60,60,60,60';
It would be easier if your x and y were of the form [6, 3, 19] instead of '6,3,19'. For the purposes of this answer, I will assume that you have it done this way as it makes the code a bit easier.
It's quite possible to calculate afterwards, but it would get messy with getting the bit that you want and then resizing the canvas. It will end up easier and faster to calculate it before-hand if you can. Something like this should work:
// Data specification
var x = [68,67,66,65,64,63,62,61,60,59,57,56,55,54,53,52,51,51,51,51,51,51,51,52,55,56,52,58,60,59,61,62,64,65,66,68,70,71,72,74,75,76,77,78,78,79,79,79,79,79,79,79,79,79,79,79,79,78,76,74,71,67,59,56,54,52,49,47,46,45,43,42,41,40,39],
y = [11,11,11,11,11,12,12,12,12,12,13,14,14,15,17,18,20,21,23,24,27,30,32,32,34,34,33,34,34,34,34,35,35,35,35,35,35,36,36,37,38,38,39,40,41,42,43,44,45,47,48,49,50,51,52,53,54,55,56,57,58,59,59,59,60,60,60,60,60,60,60,60,60,60,60];
// Work out the extreme points in both dimensions
var xs = x.slice().sort(),
ys = y.slice().sort(),
minX = xs[0],
maxX = xs[xs.length-1],
minY = ys[0],
maxY = ys[ys.length-1];
// Resize the canvas
canvas.width = maxX - minX + 1;
canvas.height = maxY - minY + 1;
// Shift the points to fit on the shrunk canvas
x.forEach(function(v, i) {
x[i] = v - minX;
});
y.forEach(function(v, i) {
y[i] = v - minY;
});
Related
I have a canvas with a map. In that canvas the user is able to draw (in red) and the final result will be:
After the user as painted whatever he wants I need to calculate the bounding box coordinates of all the content so I could ultimately have:
Now I can loop through every pixel of the canvas and calculate the bounding box based on every non-empty pixel but this is quite a heavy operation. Any idea of a better logic to achieve the intended results?
You can track what is being drawn and the diameter of the points. Then min/max that for the boundary.
One way to do this is to track position and radius (brush) or boundary (irregular shape) of what is being drawn, then merge that with current min/max bound to update the new bound if needed in effect "pushing" the bounds to always match the interior.
Example
var ctx = c.getContext("2d"),
div = document.querySelector("div > div"),
// keep track of min/max for each axis
minX = Number.MAX_SAFE_INTEGER,
minY = Number.MAX_SAFE_INTEGER,
maxX = Number.MIN_SAFE_INTEGER,
maxY = Number.MIN_SAFE_INTEGER,
// brush/draw stuff for demo
radius = 10,
rect = c.getBoundingClientRect(),
isDown = false;
ctx.fillText("Draw something here..", 10, 10);
ctx.fillStyle = "red";
c.onmousedown = function() {isDown = true};
window.onmouseup = function() {isDown = false};
window.onmousemove = function(e) {
if (isDown) {
var x = e.clientX - rect.left;
var y = e.clientY - rect.top;
// When something is drawn, calculate its impact (position and radius)
var _minX = x - radius;
var _minY = y - radius;
var _maxX = x + radius;
var _maxY = y + radius;
// calc new min/max boundary
if (_minX < minX) minX = _minX > 0 ? _minX : 0;
if (_minY < minY) minY = _minY > 0 ? _minY : 0;
if (_maxX > maxX) maxX = _maxX < c.width ? _maxX : c.width;
if (_maxY > maxY) maxY = _maxY < c.height ? _maxY : c.height;
// show new bounds
showBounds();
// draw something
ctx.beginPath();
ctx.arc(x, y, radius, 0, 6.28);
ctx.fill();
}
};
function showBounds() {
// for demo, using bounds for display purposes (inclusive bound)
div.style.cssText =
"left:" + minX + "px;top:" + minY +
"px;width:" + (maxX-minX-1) + "px;height:" + (maxY-minY-1) +
"px;border:1px solid blue";
}
div {position:relative}
div > div {position:absolute;pointer-events:none}
<div>
<canvas id=c width=600 height=600></canvas>
<div></div>
</div>
I am working with HTML5 with canvas. I already draw a 2D circle.Now i want to shade the circle with a color.but the shading look like a 3D circle.Is this possible with canvas?.Thank you.
Fake smoke and mirrors
To fake a light on a sphere. I am guessing it is a sphere as you say circle and you could mean a donut. This technique will work for a donut as well.
So to lighting.
Phong Shading
The most basic lighting model is Phong (from memory). It uses the angle between the incoming light ray and the surface normal (a line going out from the surface at 90 deg). The amount of reflected light is the cosine of that angle time the light intensity.
Spheres a easy
As the sphere is symmetrical this allows us to use a radial gradient to apply the value for each pixel on the sphere and for a sphere with the light directly overhead this produces a perfect phong shaded sphere with very little effort.
The code that does that. x,y are the center of the sphere and r is the radius. The angle between the light and the surface normal is easy to calculate as you move out from the center of the sphere. It starts at zero and ends at Math.PI/2 (90deg). So the reflected value is the cosine of that angle.
var grd = ctx.createRadialGradient(x,y,0,x,y,r);
var step = (Math.PI/2)/r;
for(var i = 0; i < (Math.PI/2); i += step){
var c = "" + Math.floor(Math.max(0,255 * Math.abs(Math.cos(i)));
grd.addColorStop(i/(Math.PI/2),"rgba("+c+","+c+","+c+","1)");
}
That code creates a gradient to fit the circle.
Mod for Homer food
To do for a donut you need to modify i. The donut has an inner and outer radius (r1, r2) so inside the for loop modify i
var ii = (i/(Math.PI/2)); // normalise i
ii *= r2; // scale to outer edge
ii = ((r1+r2)/2)-ii; // get distance from center line
ii = ii / ((r2-r1)/2); // normalise to half the width;
ii = ii * Math.PI * (1/2); // scale to get the surface norm on the donut.
// use ii as the surface normal to calculate refelected light
var c = "" + Math.floor(Math.max(0,255 * Math.abs(Math.cos(ii)));
Phong Shading Sucks
By phong shading sucks big time and will not do. This also does not allow for lights that are off center or even partly behind the sphere.
We need to add the ability for off centered light. Luck has it that the radial gradients can be offset
var grd = ctx.createRadialGradient(x,y,0,x,y,r);
The first 3 numbers are the start circle of the gradient and can be positioned anywhere. The problem is that when we move the start location the phong shading model falls apart. To fix that there is a little smoke and mirrors stuff that can make the eye believe what the brain wants.
We adjust the fall off, the brightness, the spread, and the angle for each colour stop on the radial gradient depending on how far the light is from the center.
Specular highlights
This improves it a bit but still not the best. Another important component of lighting is specular reflections (the highlight). This is dependent on the angle between the reflected light and the eye. As we do not want to do all that (javascript is slow) we will cludge it via a slight modification of the phong shading. We simply multiply the surface normal by a value greater than 1. Though not perfect it works well.
Surface properties and environment
Next light is coloured, the sphere has reflective qualities that depend on frequency and there is ambient light as well. We don't want to model all this stuff so we need a way to fake it.
This can be done via compositing (Used for almost all 3D movie production). We build up the lighting one layer at a time. The 2D API provides compositing operations for us so we can create several gradients and layer them.
There is a lot more math involved but I have tried to keep it as simple as possible.
A demo
The following demo does a real time shading of a sphere (will work on all radially symmetrical objects) Apart from some setup code for canvas and mouse the demo has two parts the main loop does the compositing by layering the lights and the function createGradient creates the gradient.
The lights used can be found in the object lights and have various properties to control the layer. The first layer should use comp = source-in and lum = 1 or you will end up with the background showing through. All other layer lights can be what every you want.
The flag spec tells the shader that the light is specular and must include the specPower > 1 as I do not vet its existence.
The colours of the light is in the array col and represent Red, green and blue. The values can be greater the 256 and less than 0 as light in the natural world has a huge dynamic range and some effect need you to ramp up the incoming light way above the 255 limit of the RGB pixel.
I add a final "multiply" to the layered result. This is the magic touch in the smoke and mirror method.
If you like the code play with the values and layers. Move the mouse to change the light source location.
This is not real lighting it is fake, but who cares as long as it looks OK. lol
UPDATE
Found a bug so fixed it and while I was here, changed the code to randomize the lights when you click the left mouse button. This is so you can see the range of lighting that can be achieved when using the ctx.globalCompositeOperation in combination with gradients.
var demo = function(){
/** fullScreenCanvas.js begin **/
var canvas = (function(){
var canvas = document.getElementById("canv");
if(canvas !== null){
document.body.removeChild(canvas);
}
// creates a blank image with 2d context
canvas = document.createElement("canvas");
canvas.id = "canv";
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style.position = "absolute";
canvas.style.top = "0px";
canvas.style.left = "0px";
canvas.style.zIndex = 1000;
canvas.ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
return canvas;
})();
var ctx = canvas.ctx;
/** fullScreenCanvas.js end **/
/** MouseFull.js begin **/
if(typeof mouse !== "undefined"){ // if the mouse exists
if( mouse.removeMouse !== undefined){
mouse.removeMouse(); // remove prviouse events
}
}else{
var mouse;
}
var canvasMouseCallBack = undefined; // if needed
mouse = (function(){
var mouse = {
x : 0, y : 0, w : 0, alt : false, shift : false, ctrl : false,
interfaceId : 0, buttonLastRaw : 0, buttonRaw : 0,
over : false, // mouse is over the element
bm : [1, 2, 4, 6, 5, 3], // masks for setting and clearing button raw bits;
getInterfaceId : function () { return this.interfaceId++; }, // For UI functions
startMouse:undefined,
mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
};
function mouseMove(e) {
var t = e.type, m = mouse;
m.x = e.offsetX; m.y = e.offsetY;
if (m.x === undefined) { m.x = e.clientX; m.y = e.clientY; }
m.alt = e.altKey;m.shift = e.shiftKey;m.ctrl = e.ctrlKey;
if (t === "mousedown") { m.buttonRaw |= m.bm[e.which-1];
} else if (t === "mouseup") { m.buttonRaw &= m.bm[e.which + 2];
} else if (t === "mouseout") { m.buttonRaw = 0; m.over = false;
} else if (t === "mouseover") { m.over = true;
} else if (t === "mousewheel") { m.w = e.wheelDelta;
} else if (t === "DOMMouseScroll") { m.w = -e.detail;}
if (canvasMouseCallBack) { canvasMouseCallBack(mouse); }
e.preventDefault();
}
function startMouse(element){
if(element === undefined){
element = document;
}
mouse.element = element;
mouse.mouseEvents.forEach(
function(n){
element.addEventListener(n, mouseMove);
}
);
element.addEventListener("contextmenu", function (e) {e.preventDefault();}, false);
}
mouse.removeMouse = function(){
if(mouse.element !== undefined){
mouse.mouseEvents.forEach(
function(n){
mouse.element.removeEventListener(n, mouseMove);
}
);
canvasMouseCallBack = undefined;
}
}
mouse.mouseStart = startMouse;
return mouse;
})();
if(typeof canvas !== "undefined"){
mouse.mouseStart(canvas);
}else{
mouse.mouseStart();
}
/** MouseFull.js end **/
// draws the circle
function drawCircle(c){
ctx.beginPath();
ctx.arc(c.x,c.y,c.r,0,Math.PI*2);
ctx.fill();
}
function drawCircle1(c){
ctx.beginPath();
var x = c.x;
var y = c.y;
var r = c.r * 0.95;
ctx.moveTo(x,y - r);
ctx.quadraticCurveTo(x + r * 0.8, y - r , x + r *1, y - r / 10);
ctx.quadraticCurveTo(x + r , y + r/3 , x , y + r/3);
ctx.quadraticCurveTo(x - r , y + r/3 , x - r , y - r /10 );
ctx.quadraticCurveTo(x - r * 0.8, y - r , x , y- r );
ctx.fill();
}
function drawShadowShadow(circle,light){
var x = light.x; // get the light position as we will modify it
var y = light.y;
var r = circle.r * 1.1;
var vX = x - circle.x; // get the vector to the light source
var vY = y - circle.y;
var dist = -Math.sqrt(vX*vX+vY*vY)*0.3;
var dir = Math.atan2(vY,vX);
lx = Math.cos(dir) * dist + circle.x; // light canb not go past radius
ly = Math.sin(dir) * dist + circle.y;
var grd = ctx.createRadialGradient(lx,ly,r * 1/4 ,lx,ly,r);
grd.addColorStop(0,"rgba(0,0,0,1)");
grd.addColorStop(1,"rgba(0,0,0,0)");
ctx.fillStyle = grd;
drawCircle({x:lx,y:ly,r:r})
}
// 2D light simulation. This is just an approximation and does not match real world stuff
// based on Phong shading.
// x,y,r descript the imagined sphere
// light is the light source
// ambient is the ambient lighting
// amount is the amount of this layers effect has on the finnal result
function createGradient(circle,light,ambient,amount){
var r,g,b; // colour channels
var x = circle.x; // get lazy coder values
var y = circle.y;
var r = circle.r;
var lx = light.x; // get the light position as we will modify it
var ly = light.y;
var vX = light.x - x; // get the vector to the light source
var vY = light.y - y;
// get the distance to the light source
var dist = Math.sqrt(vX*vX+vY*vY);
// id the light is a specular source then move it to half its position away
dist *= light.spec ? 0.5 : 1;
// get the direction of the light source.
var dir = Math.atan2(vY,vX);
// fix light position
lx = Math.cos(dir)*dist+x; // light canb not go past radius
ly = Math.sin(dir)*dist+y;
// add some dimming so that the light does not wash out.
dim = 1 - Math.min(1,(dist / (r*4)));
// add a bit of pretend rotation on the z axis. This will bring in a little backlighting
var lightRotate = (1-dim) * (Math.PI/2);
// spread the light a bit when near the edges. Reduce a bit for spec light
var spread = Math.sin(lightRotate) * r * (light.spec ? 0.5 : 1);
// create a gradient
var grd = ctx.createRadialGradient(lx,ly,spread,x,y,r + dist);
// use the radius to workout what step will cover a pixel (approx)
var step = (Math.PI/2)/r;
// for each pixel going out on the radius add the caclualte light value
for(var i = 0; i < (Math.PI/2); i += step){
if(light.spec){
// fake spec light reduces dim fall off
// light reflected has sharper falloff
// do not include back light via Math.abs
r = Math.max(0,light.col[0] * Math.cos((i + lightRotate)*light.specPower) * 1-(dim * (1/3)) );
g = Math.max(0,light.col[1] * Math.cos((i + lightRotate)*light.specPower) * 1-(dim * (1/3)) );
b = Math.max(0,light.col[2] * Math.cos((i + lightRotate)*light.specPower) * 1-(dim * (1/3)) );
}else{
// light value is the source lum * the cos of the angle to the light
// Using the abs value of the refelected light to give fake back light.
// add a bit of rotation with (lightRotate)
// dim to stop washing out
// then clamp so does not go below zero
r = Math.max(0,light.col[0] * Math.abs(Math.cos(i + lightRotate)) * dim );
g = Math.max(0,light.col[1] * Math.abs(Math.cos(i + lightRotate)) * dim );
b = Math.max(0,light.col[2] * Math.abs(Math.cos(i + lightRotate)) * dim );
}
// add ambient light
if(light.useAmbient){
r += ambient[0];
g += ambient[1];
b += ambient[2];
}
// add the colour stop with the amount of the effect we want.
grd.addColorStop(i/(Math.PI/2),"rgba("+Math.floor(r)+","+Math.floor(g)+","+Math.floor(b)+","+amount+")");
}
//return the gradient;
return grd;
}
// define the circles
var circles = [
{
x: canvas.width * (1/2),
y: canvas.height * (1/2),
r: canvas.width * (1/8),
}
]
function R(val){
return val * Math.random();
}
var lights;
function getLights(){
return {
ambient : [10,30,50],
sources : [
{
x: 0, // position of light
y: 0,
col : [R(255),R(255),R(255)], // RGB intensities can be any value
lum : 1, // total lumanance for this light
comp : "source-over", // composite opperation
spec : false, // if true then use a pretend specular falloff
draw : drawCircle,
useAmbient : true,
},{ // this light is for a little accent and is at 180 degree from the light
x: 0,
y: 0,
col : [R(255),R(255),R(255)],
lum : R(1),
comp : "lighter",
spec : true, // if true then you MUST inclue spec power
specPower : R(3.2),
draw : drawCircle,
useAmbient : false,
},{
x: canvas.width,
y: canvas.height,
col : [R(1255),R(1255),R(1255)],
lum : R(0.5),
comp : "lighter",
spec : false,
draw : drawCircle,
useAmbient : false,
},{
x: canvas.width/2,
y: canvas.height/2 + canvas.width /4,
col : [R(155),R(155),R(155)],
lum : R(1),
comp : "lighter",
spec : true, // if true then you MUST inclue spec power
specPower : 2.32,
draw : drawCircle,
useAmbient : false,
},{
x: canvas.width/3,
y: canvas.height/3,
col : [R(1255),R(1255),R(1255)],
lum : R(0.2),
comp : "multiply",
spec : false,
draw : drawCircle,
useAmbient : false,
},{
x: canvas.width/2,
y: -100,
col : [R(2255),R(2555),R(2255)],
lum : R(0.3),
comp : "lighter",
spec : false,
draw : drawCircle1,
useAmbient : false,
}
]
}
}
lights = getLights();
/** FrameUpdate.js begin **/
var w = canvas.width;
var h = canvas.height;
var cw = w / 2;
var ch = h / 2;
ctx.font = "20px Arial";
ctx.textAlign = "center";
function update(){
ctx.setTransform(1,0,0,1,0,0);
ctx.fillStyle = "#A74"
ctx.fillRect(0,0,w,h);
ctx.fillStyle = "black";
ctx.fillText("Left click to change lights", canvas.width / 2, 20)
// set the moving light source to that of the mouse
if(mouse.buttonRaw === 1){
mouse.buttonRaw = 0;
lights = getLights();
}
lights.sources[0].x = mouse.x;
lights.sources[0].y = mouse.y;
if(lights.sources.length > 1){
lights.sources[1].x = mouse.x;
lights.sources[1].y = mouse.y;
}
drawShadowShadow(circles[0],lights.sources[0])
//do each sphere
for(var i = 0; i < circles.length; i ++){
// for each sphere do the each light
var cir = circles[i];
for(var j = 0; j < lights.sources.length; j ++){
var light = lights.sources[j];
ctx.fillStyle = createGradient(cir,light,lights.ambient,light.lum);
ctx.globalCompositeOperation = light.comp;
light.draw(circles[i]);
}
}
ctx.globalCompositeOperation = "source-over";
if(!STOP && (mouse.buttonRaw & 4)!== 4){
requestAnimationFrame(update);
}else{
if(typeof log === "function" ){
log("DONE!")
}
STOP = false;
var can = document.getElementById("canv");
if(can !== null){
document.body.removeChild(can);
}
}
}
if(typeof clearLog === "function" ){
clearLog();
}
update();
}
var STOP = false; // flag to tell demo app to stop
function resizeEvent(){
var waitForStopped = function(){
if(!STOP){ // wait for stop to return to false
demo();
return;
}
setTimeout(waitForStopped,200);
}
STOP = true;
setTimeout(waitForStopped,100);
}
window.addEventListener("resize",resizeEvent);
demo();
/** FrameUpdate.js end **/
As #danday74 says, you can use a gradient to add depth to your circle.
You can also use shadowing to add depth to your circle.
Here's a proof-of-concept illustrating a 3d donut:
I leave it to you to design your desired circle
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var PI=Math.PI;
drawShadow(150,150,120,50);
function drawShadow(cx,cy,r,strokewidth){
ctx.save();
ctx.strokeStyle='white';
ctx.lineWidth=5;
ctx.shadowColor='black';
ctx.shadowBlur=15;
//
ctx.beginPath();
ctx.arc(cx,cy,r-5,0,PI*2);
ctx.clip();
//
ctx.beginPath();
ctx.arc(cx,cy,r,0,PI*2);
ctx.stroke();
//
ctx.beginPath();
ctx.arc(cx,cy,r-strokewidth,0,PI*2);
ctx.stroke();
ctx.shadowColor='rgba(0,0,0,0)';
//
ctx.beginPath();
ctx.arc(cx,cy,r-strokewidth,0,PI*2);
ctx.fillStyle='white'
ctx.fill();
//
ctx.restore();
}
body{ background-color: white; }
canvas{border:1px solid red; margin:0 auto; }
<canvas id="canvas" width=300 height=300></canvas>
Various thoughts which you can investigate ...
1 use an image as the texture for the circle
2 use a gradient to fill the circle, probably a radial gradient
3 consider using an image mask, a black / white mask which defines transparency ( prob not the right solution here )
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.
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.
With the plugin i found earlier on stackoverflow. Drawing has become smooth and nice. What i want is to only get the image part which i draw cropped from the canvas as an output and not the complete canvas. Can somebody help.
This is the code i am using for my canvas now: http://jsfiddle.net/sVsZL/1/
function canvasDisplay() {
var c=document.getElementById("canvas");
canvasImage=c.toDataURL("image/png");
document.getElementById("SSMySelectedImage").src=canvasImage;
}
Adding another answer because the other one was completely off.
Live Demo
What you need essentially is to keep track of a bounding box. What I do is create an object that holds the min values and max values of where you've drawn. This enables you to keep track of how big the image is and where it begins/ends.
this.dim = {minX : 9999, minY : 9999, maxX : 0, maxY : 0};
Then I created a function that checks the bounds.
this.setDimensions = function(x,y){
if(x < this.dim.minX){
this.dim.minX = x;
}
if(y < this.dim.minY){
this.dim.minY = y;
}
if(x > this.dim.maxX){
this.dim.maxX= x;
}
if(y > this.dim.maxY){
this.dim.maxY = y;
}
}
Make sure to check during clicking or moving.
this.mousedown = function(ev) {
tool.setDimensions(ev._x,ev._y);
};
this.mousemove = function(ev) {
tool.setDimensions(ev._x,ev._y);
};
And this is just a sample function that draws the portion to a new canvas that you could then save with toDataUrl
var button = document.getElementsByTagName("input")[0];
button.addEventListener("click", function(){
var savedCanvas = document.createElement("canvas"),
savedCtx = savedCanvas.getContext("2d"),
minX = PEN.dim.minX,
minY = PEN.dim.minY,
maxX = PEN.dim.maxX,
maxY = PEN.dim.maxY,
width = maxX - minX,
height = maxY - minY;
savedCanvas.width = width;
savedCanvas.height = height;
document.body.appendChild(savedCanvas);
savedCtx.drawImage(canvas,minX,minY,width,height,0,0,width,height);
});