Html5 Canvas - Shading an outside area - html

I need to shade an OUTSIDE area, ie the shapes I draw in the shader are drawn normally, and their inverse is then shaded. Its easiest to explain with an example, and noting the bit that is not working:
// canvasBackground is the actual background
// canvasBackgroundContext is its context
// To make it simple, I will fill it with green
canvasBackgroundContext.fillStyle = "#00FF00";
canvasBackgroundContext.clearRect(0, 0, canvasBackground.width, canvasBackground.height);
// I also have a the shader
// canvasShader and canvasShaderContext with same width and height as canvasBackground
canvasShaderContext.fillStyle = "rgba(0, 0, 0, 0.25)"; // Black but slightly transparent
canvasShaderContext.clearRect(0, 0, canvasShader.width, canvasShader.height);
// Everything so far is great - now the problem
// This is wrong, because when I create the area I want to create clear, it does not work
// because when you draw a shape it does not work like clearRect, as it does not set each pixel to a clear pixel
canvasShaderContext.fillStyle = "rgba(0, 0, 0, 0.0)";
// Create the only non shaded bits in the shader, overlapping rects
canvasShaderContext.fillRect(10, 10, 50, 50);
canvasShaderContext.fillRect(40, 40, 50, 50);
// So when I do this, it should shade the entire background except for the two 50x50 overlapping rects at 10,10 and 40,40
canvasBackgroundContext.drawImage(canvasShaderContext, 0, 0);
I don't want to go to a pixel by pixel basis using getImageData, as that is slow. There must be some way of doing this.

I am not sure I fully understand what you try to achieve, but how about adding a composite mode to this:
canvasShaderContext.fillRect(10, 10, 50, 50);
canvasShaderContext.fillRect(40, 40, 50, 50);
which results in:
/// store old mode whatever that is
var oldMode = canvasShaderContext.globalCompositeOperation;
/// this uses any shape that is drawn next to punch a hole
/// in destination (current context).
canvasShaderContext.globalCompositeOperation = 'destination-out';
/// now draw the holes
canvasShaderContext.fillRect(10, 10, 50, 50);
canvasShaderContext.fillRect(40, 40, 50, 50);
/// set back to old mode
canvasShaderContext.globalCompositeOperation = oldMode;
That will also clear the alpha bits.

Related

HTML canvas: Why does a large shadow blur not show up for small objects?

Here's a demonstration:
var ctx = document.getElementById("test").getContext("2d");
ctx.shadowColor = "black";
ctx.fillStyle = "white";
ctx.shadowBlur = 10;
ctx.fillRect(10, 10, 10, 10);
ctx.shadowBlur = 50;
ctx.fillRect(70, 10, 10, 10);
ctx.fillRect(70, 70, 70, 70);
<canvas id="test" width="200" height="200"></canvas>
If I set shadowBlur=10 and then draw a small 10x10 square, I get a nice, strong shadow. The same if I set shadowBlur=50 and draw a big 70x70 square. But if I set shadowBlur=50 and then draw a small 10x10 square, I get a very faint, barely visible shadow.
Instead I would have expected a small center square and a large dark shadow all around it.
Obviously I misunderstand how the shadow blur works, so - how does it work, and how do I get a large dark shadow around a small object?
The shadowBlur uses Gaussian blur to produce the shadow internally. The object is drawn to a separate bitmap as stencil in the shadow-color and then blurred using the radius. It does not use the original shape after this step. The result is composited back (as a side-note: there was previously a disagreement on how to composite shadows so Firefox and Chrome/Opera rendered them differently - I think they have landed on source-over in both camps by now though).
If the object is very small and the blur radius very big, the averaging will be thinned by the empty remaining space around the object leaving a more faint shadow.
The only way to get a more visible shadow with the built-in method is to use a smaller radius. You can also "cheat" using a radial gradient, or draw a bigger object with shadow applied to an off-screen canvas but offset relative to the shadow itself so the object doesn't overlap it, then draw the shadow only (using clipping arguments with drawImage()) back to main canvas at desired size before drawing main object.
In newer versions of the browsers you can also produce Gaussian blurred shadows manually using the new filter property on the context with CSS filters. It do require some extra compositing steps and most likely an off-screen canvas for most scenarios, but you can with this method overdraw shadows in multiple steps with variable radii from small to bigger producing a more pronounced shadow at the cost of some performance.
Example of manually generated shadow using filter:
This allow for more complex shapes like with the built-in shadow, but offer more control of the end result. "Falloff" in this case can be controlled by using a easing-function with an initial normalized radius value inside the loop.
// note: requires filter support on context
var ctx = c.getContext("2d");
var iterations = 16, radius = 50,
step = radius / iterations;
for(var i = 1; i < iterations; i++) {
ctx.filter = "blur(" + (step * i) + "px)";
ctx.fillRect(100, 50, 10, 10);
}
ctx.filter = "none";
ctx.fillStyle = "#fff";
ctx.fillRect(100, 50, 10, 10);
<canvas id=c></canvas>
Example of gradient + filter:
This is a more cross-browser friendly solutions as if filter is not supported, at least the gradient comes close to an acceptable shadow. The only drawback is it is more limited in regards to complex shapes.
Additionally, using a variable center point for the gradient allows for mimicking fall-off, light size, light type etc.
Based on #Kaiido's example/mod in comment -
// note: requires filter support on context
var ctx = c.getContext("2d");
var grad = ctx.createRadialGradient(105,55,50,105,55,0);
grad.addColorStop(0,"transparent");
grad.addColorStop(0.33,"rgba(0,0,0,0.5)"); // extra point to control "fall-off"
grad.addColorStop(1,"black");
ctx.fillStyle = grad;
ctx.filter = "blur(10px)";
ctx.fillRect(0, 0, 300, 150);
ctx.filter = "none";
ctx.fillStyle = "#fff";
ctx.fillRect(100, 50, 10, 10);
<canvas id=c></canvas>

html5 canvas overlay cutout workaround in safari

I am trying to create an overlay that can highlight certain portions of the page. The best way I've figured out to do this is to use a canvas fit to the page, draw the shapes I need, and draw a rectangle using this trick:
ctx.rect(canvas.width, 0, 0 - canvas.width, canvas.height);
I'm not sure I can explain this well, so it might be best to look at the jsFiddle example here to get an idea of what I am trying to do.
This works perfectly in every browser except Safari. Is there any way to achieve this effect in Safari?
You can try filling the whole canvas first, then use composite mode to knock out the shapes.
Example:
ctx.fillStyle = "rgba(0, 0, 0, 0.5)";
ctx.fillRect(canvas.width, 0, 0 - canvas.width, canvas.height);
// next shape will punch hole in the draw rectangle above
ctx.globalCompositeOperation = 'destination-out';
ctx.fillStyle = "rgba(0, 0, 0, 1)"; // make sure alpha is 100%
ctx.beginPath();
ctx.moveTo(300, 60);
ctx.arc(300, 60, 50, 0, 2 * Math.PI);
ctx.moveTo(500, 160);
ctx.arc(500, 160, 50, 0, 2 * Math.PI);
drawEllipse(ctx, 103, 23, 100, 30)
drawEllipse(ctx, 503, 23, 100, 30)
ctx.fill();
ctx.globalCompositeOperation = 'source-over'; // reset comp. mode
Modifed fiddle here
Alpha (not the color) will determine how much visible the hole will be.
Hope this helps.

Tweening Sprite size using GreenSock in AS3

I've seen examples where it's possible to tween a rectangle using scaleX, but I can't find anything that tweens a circle. (The "circle" that I'm drawing is actually a donut shape and I want the outside circle to be the one that is tweened).
var resizeVar:Number = 75;
myCircle.graphics.drawCircle((myCircle.width/2), (myCircle.height/2), resizeVar);
myCircle.graphics.drawCircle((myCircle.width/2), (myCircle.height/2), 75);
I tried doing it this way, but this throws lots of errors. I don't think it's possible this way:
TweenMax.to(myCircle, 2, {resizeVar:150, ease:SlowMo.ease.config(1, 0)});
Normally with display objects, it is done this way. It doesn't work with this "donut" though:
TweenMax.to(myRectangle, 2, {scaleX:1.5, scaleY:1.5 ease:SlowMo.ease.config(1, 0)});
So my question is, how can I tween the radius size of my outside circle?
EDIT: This is how the donut is being drawn, so the resizeVar needs to change from 75 to 150.
var myCircle:Sprite = new Sprite();
myCircle.graphics.beginFill(0xbbbbbb);
myCircle.graphics.drawCircle(0, 0, 150); // this is what should be tweening/scaling
myCircle.graphics.drawCircle(0, 0, 75); // this should stay the same
myCircle.graphics.endFill();
addChild(myCircle);
You should be able to tween the scaleX and scaleY properties of ANY displayObject:
var radius:Number = 75;
var myCircle:Sprite = new Sprite();
myCircle.graphics.beginFill(0);
myCircle.graphics.drawCircle(radius/2, radius/2, radius);
myCircle.graphics.endFill();
addChild(myCircle);
TweenMax.to(myCircle, 2, {scaleX:2, scaleY:2, ease:SlowMo.ease.config(1,0)});
EDIT
This is how you would scale just the outside of the donut:
var resizeObject:Object = { innerRadius:75, outerRadius:150 };
myCircle = new Sprite();
myCircle.graphics.beginFill(0xbbbbbb);
myCircle.graphics.drawCircle(0, 0, resizeObject.outerRadius);
myCircle.graphics.drawCircle(0, 0, resizeObject.innerRadius);
myCircle.graphics.endFill();
addChild(myCircle);
TweenMax.to(resizeObject, 2, {outerRadius:300, ease:SlowMo.ease.config(1,0), onUpdate:updateCircle, onUpdateParams:[resizeObject]});
function updateCircle(resizeObject:Object):void
{
myCircle.graphics.clear();
myCircle.graphics.beginFill(0xbbbbbb);
myCircle.graphics.drawCircle(0, 0, resizeObject.outerRadius);
myCircle.graphics.drawCircle(0, 0, resizeObject.innerRadius);
myCircle.graphics.endFill();
}
The reason it works with the rectangle is that you are changing the scale of the rectangle. When you change the scale Flash Player adjusts the scale of the display object containing your graphics.
However, with the circle, you are trying to change the radius of the circle. The radius is only used when you draw the circle with the drawCircle() method. One way to tween the radius is to use your tween to re-draw the circle many times (not that ideal).
To re-draw the circle with a new radius, you can use the onUpdate callback that TweenMax offers:
TweenMax.to(myCircle, 2, {resizeVar:150, onUpdate: onUpdateCallback, onUpdateParams: [resizeVar] });
function onUpdateCallback(radius):void
{
myCircle.graphics.drawCircle(myCircle.graphics.drawCircle((myCircle.width/2), (myCircle.height/2), radius);
}
[Edit]
Note, I've added some params that you need to pass to the onUpdateCallback() function. I've also modified the function to add a radius parameter, and then use the radius when drawing the circle.
In regards to "trying to change the outside circle of this donut", this may be more complex. You might need to draw both circles of the donut. You might need to also call graphics.clear() before you draw the circle.
However, perhaps the answer from #Marcela is better, just change the scaleX and scaleY of the object you've already drawn. But if you need to get to a specified radius, the only way to do that is by re-drawing the circle(s) on each interval of the tween.

circular colorTransform

Is there a way to apply a colorTransform to a BitmapData in a circle rather than in a rectangle?
Instead of erasing rectangular parts of an image by reducing the alpha channel as in the code below, I'd like to do it in circles.
_bitmap.colorTransform(new Rectangle(mouseX-d/2, mouseY-d/2, d, d),
new ColorTransform(1, 1, 1, .5, 0, 0, 0, 1));
I do have some code which loops through the pixels, extracts the alpha value and uses setPixel but it seams significantly slower than the colorTransform function.
Try creating a circle using the drawing API (flash.display.Graphics) and then drawing that onto the bitmap data with BlendMode.ERASE. That might solve your problem, if I understand it correctly.
var circle : Shape = new Shape;
circle.graphics.beginFill(0xffcc00, 1);
circle.graphics.drawEllipse(-50, -50, 100, 100);
// Create a transformation matrix for the draw() operation, with
// a translation matching the mouse position.
var mtx : Matrix = new Matrix();
mtx.translate(mouseX, mouseY);
// Draw circle at mouse position with the ERASE blend mode, to
// set affected pixels to alpha=0.
myBitmap.draw(circle, mtx, null, BlendMode.ERASE);
I'm not 100% sure that the ERASE blend mode works satisfyingly with the draw() command, but I can't see why it shouldn't. Please let me know how it works out!

How to continuously fade a BitmapData to a certain color?

I'm drawing stuff on a bitmapData and I need to continuously fade every pixel to 0x808080 while still drawing (its for a DisplacementMapFilter)...
I think this illustrates my problem:
http://www.ventdaval.com/lab/grey.swf
The simpler approach I tried was drawing a semi-transparent grey box on it, but it never reaches a single color (i.e. 0x808081 will never be turned to 0x808080)... The same kind of thing happens with a ColorMatrixFilter trying to progressively reduce the saturation and/or contrast. (the example above is applying a filter with -10 contrast every frame).
I'm trying paletteMap now, and I'm sensing this could be the way to go, but I haven't been able to get it... any ideas?
You can try to fade it slightly more than you are fading it now, so color will go to 0x808080, you just need to do some calculations.
Yes, you can do this with paletteMap.
The third way will be to write PixelBender shader for it.
You can also use your normal fading and do paletteMap or PixelBender one once in a few frames, to speed-up all this.
I think the easiest way to do this would be with two successive color filters. That is:
var n:Number = 0.9;
var off:Number = 0x80;
var filter:ColorMatrixFilter = new ColorMatrixFilter( [
1, 0, 0, 0, -off,
0, 1, 0, 0, -off,
0, 0, 1, 0, -off,
0, 0, 0, 1, -off ] );
var filter2:ColorMatrixFilter = new ColorMatrixFilter( [
n, 0, 0, 0, off,
0, n, 0, 0, off,
0, 0, n, 0, off,
0, 0, 0, n, off ] );
function onFrame( e:Event ) {
myBMD.applyFilter( myBMD, myBMD.rect, new Point(), filter );
myBMD.applyFilter( myBMD, myBMD.rect, new Point(), filter2 );
}
The first filter offsets each channel value downwards by 128 - turning your gray-based image into a black-based image. The second filter multiplies each channel value by the modulator (0.9 in my example), and then offsets the values back up by 128.
I wound up solving this by filling the texture with 0xFF808080 every frame, then drawing my dynamic stuff to a second, transparent bitmap, doing a colortransform on it with a lower alpha multiplier (say 0.98), and then copypixeling that bitmap to the main bitmap with mergealpha on. Works a treat:
public function update():void
{
lock();
fillRect(rect, 0xFF808080);
drawTarget.lock();
drawTarget.colorTransform(rect, fadeTransform);
drawTarget.applyFilter(drawTarget, rect, pt, blurFilter);
drawTarget.unlock();
copyPixels(drawTarget, rect, pt, null, null, true);
unlock();
}