How do I stroke a knocked out object? - html

In HTML5 canvas I'm trying to knock out an object with another object and stroke the final product of this operation. Is that possible at all? I'm using the following code with no success:
var tcan = document.getElementById('test');
var tctx = tcan.getContext('2d');
tctx.beginPath();
tctx.fillStyle = '#F00';
tctx.fillRect(0,0,70,70);
tctx.globalCompositeOperation = "destination-out";
tctx.fillRect(20,20,70,70);
tctx.closePath();
tctx.strokeStyle = '#FF0'; // expecting the final product to have a yellow stroke
tctx.stroke();

A Demo: http://jsfiddle.net/m1erickson/x8Maf/
You can use source-atop compositing to draw your yellow stroke only atop your existing knockout:
tctx.beginPath();
tctx.fillStyle = '#F00';
tctx.fillRect(0,0,70,70);
// knock-out compositing
tctx.globalCompositeOperation = "destination-out";
tctx.fillRect(20,20,70,70);
tctx.closePath();
// composite where new drawings appear only where overlapping existing
tctx.globalCompositeOperation = "source-atop";
tctx.strokeStyle = '#FF0'; // expecting the final product to have a yellow stroke
tctx.lineWidth=2;
tctx.strokeRect(20,20,70,70);
// restore the default compositing
tctx.globalCompositeOperation = "source-over";
A couple of hints:
You don't need beginPath if you use fillRect or strokeRect (these commands automatically beginPath for you);
All canvas strokes are half-inside and half-outside the specified dimensions. So fillRect(20,20,70,70) will actually stroke from 19.50 to 20.50.
[ Addition to answer: Here's how to stroke the knockout shape. ]
var tcan = document.getElementById('test');
var tctx = tcan.getContext('2d');
tctx.lineWidth=2;
tctx.strokeStyle = '#FF0';
tctx.fillStyle = '#F00';
tctx.fillRect(50,50,70,70);
tctx.strokeRect(50,50,70,70);
// knock-out compositing
tctx.globalCompositeOperation = "destination-out";
tctx.fillRect(70,70,70,70);
tctx.closePath();
// composite where new drawings appear only where overlapping existing
tctx.globalCompositeOperation = "source-atop";
tctx.lineWidth=4;
tctx.strokeRect(70,70,70,70);
// restore the default compositing
tctx.globalCompositeOperation = "source-over";

Related

SkiaSharp draw reversible shapes

Is there a way to draw reversible shapes with SkiaSharp?
With reversible I mean outColor = srcColor XOR dstColor, so when you draw over same color again, the original color is restored. Like in WinForms ControlPaint.DrawReversibleFrame or (old) Windows FocusRects.
I'm using SkiaSharp 2.88.0
You can use Exclusion blend mode when drawing, for example:
// draw some random image on canvas
var bitmap = SKBitmap.Decode(#"random.jpg");
var info = new SKImageInfo(256, 256);
using var surf = SKSurface.Create(info);
var canvas = surf.Canvas;
canvas.DrawBitmap(bitmap, info.Rect);
// initialize paint with Exclusion blend mode
var paint = new SKPaint {
Color = SKColors.Yellow,
Style = SKPaintStyle.Fill,
BlendMode = SKBlendMode.Exclusion
};
// draw overlapping rectangles using the paint
canvas.DrawRect(10, 10, 50, 50, paint);
canvas.DrawRect(25, 25, 50, 50, paint);
Result image:

HTML5 canvas apply color to image where shape overlays

I have this image drawn to a HTML5 canvas:
What I want to do is apply color to just a part of it.
The part where I want to apply color is defined by the following overlay image:
So, basically, I would like to guide my coloring by the overlay. So where the overlay pixels meets the main image pixels I should apply a color on the main image. At least that's how I see it working.
Notice that the overlay matches the whole image except for the lacing.
The catch is that I would like to retain the main image texture while applying the color. You can see that it has a leather texture and a "real" feel which I want to keep.
Can you please show me some methods of achieving this or share some thoughts?
Thank you!
globalCompositeOperation is your friend here.
Basically, you draw your overlay, then you set the gCO to 'source-atop' composite mode, which will make all your future drawings to only stay where there were already opaque pixels drawn, so it is important that your overlay has transparent parts.
So then you just fill a rectangle of your desired command, and finally you draw your original image, either behind, or blended to the new shape we just created.
var ctx = canvas.getContext('2d');
var loaded = 0;
function onload(){
if(++loaded === 2){
canvas.width = this.width;
canvas.height = this.height;
ctx.font = "40px sans-serif";
draw();
}
}
var original = new Image();
var overlay = new Image();
original.onload = overlay.onload = onload;
original.src = 'https://i.stack.imgur.com/vIKpI.png';
overlay.src = 'https://i.stack.imgur.com/10Tre.png';
// list of blending modes.
// Note that destination-over is a composite mode,
// which place the new drawings behind the already-there ones
var currentMode = 0;
var modes = ['destination-over', 'lighter', 'multiply', 'screen', 'overlay', 'darken',
'lighten', 'color-dodge', 'color-burn', 'hard-light', 'soft-light',
'exclusion', 'hue', 'saturation', 'color', 'luminosity' ];
function draw(){
// switch between different Blending modes
var mode = modes[currentMode];
currentMode = (currentMode+1)%(modes.length);
// clear previous
ctx.clearRect(0,0,canvas.width, canvas.height);
// draw our overlay
ctx.drawImage(overlay, 0,0);
// this will keep new drawings only where we already have existing pixels
ctx.globalCompositeOperation = 'source-atop';
ctx.fillStyle = 'red';
ctx.fillRect(0,0,canvas.width, canvas.height);
// now choose between the list of blending modes
ctx.globalCompositeOperation = mode;
// draw our original image
ctx.drawImage(original, 0,0);
// go back to default
ctx.globalCompositeOperation = 'source-over';
// just so we can know which one is shown
ctx.fillStyle = 'black';
ctx.fillText(mode, 40,40)
// do it again
setTimeout(draw, 1000)
}
canvas{
width: 100%;
}
<canvas id="canvas"></canvas>

HTML5 Canvas image resize on Chrome & easeljs

I'm struggling to make smooth image resized in canvas in Chrome. In firefox it works well, but in Chrome I'm stuck on making it smooth.
Here is the jsfiddle
http://jsfiddle.net/flashmandv/oxtrypmy/
var AVATAR_SIZE = 100;
var WHITE_BORDER_SIZE = 3;
var stage = new createjs.Stage("canvas");
var avCont = new createjs.Container();
stage.addChild(avCont);
avCont.x = avCont.y = 20;
//add white circle
var whiteBorderCircle = new createjs.Shape();
var radius = (AVATAR_SIZE+WHITE_BORDER_SIZE*2)/2;
whiteBorderCircle.graphics.beginFill("white").drawCircle(radius, radius, radius);
avCont.addChild(whiteBorderCircle);
//add avatar image mask
var avatarMask = new createjs.Shape();
avatarMask.graphics.beginFill("red").drawCircle(AVATAR_SIZE/2+WHITE_BORDER_SIZE, AVATAR_SIZE/2+WHITE_BORDER_SIZE, AVATAR_SIZE/2);
//add avatar image
var image = new Image();
image.onload = function(){
var bitmap = new createjs.Bitmap(image);
bitmap.mask = avatarMask;
var bounds = bitmap.getBounds();
bitmap.scaleX = (AVATAR_SIZE+WHITE_BORDER_SIZE*2) / bounds.width;
bitmap.scaleY = (AVATAR_SIZE+WHITE_BORDER_SIZE*2) / bounds.height;
avCont.addChild(bitmap);
stage.update();
};
image.src = 'http://files.sharenator.com/sunflowers-s800x800-423444.jpg';
Notice the jagged image
Please help
It is due to how clipping works in Chrome. Clip masks are pretty brutal in Chrome while in Firefox you get anti-aliasing along the non-straight edges.
Here is a proof-of-concept for this (run this in Chrome and in FF to see the difference):
http://jsfiddle.net/r65fcqoy/
The only way to get around this is to use composite modes instead, which basically means you need to rewrite your code unless the library you're using support this in some way.
One use of a composite mode is to use it to fill anything inside an existing drawing on the canvas.
We'll first create the filled circle we want the image to appear inside
Change comp mode to source-in and draw image
Then we go back to normal comp mode and draw the outer border
Here is an approach using vanilla JavaScript where you can control how you plug things together - this is maybe not what you're after but there is really not much option if the library as said doesn't support comp mode instead of clipping:
var canvas = document.getElementById('canvas'),
ctx = canvas.getContext('2d'),
img = new Image,
x = 70, y =70;
var AVATAR_SIZE = 100;
var WHITE_BORDER_SIZE = 3;
var radius = (AVATAR_SIZE+WHITE_BORDER_SIZE*2)/2;
img.onload = function() {
// first draw the circle for the inner image:
ctx.arc(x, y, AVATAR_SIZE*0.5, 0 , 2*Math.PI);
ctx.fill();
// now, change composite mode:
ctx.globalCompositeOperation = 'source-in';
// draw image in top
ctx.drawImage(img, x-AVATAR_SIZE*0.5, y-AVATAR_SIZE*0.5,
AVATAR_SIZE, AVATAR_SIZE);
// change back composite mode to default:
ctx.globalCompositeOperation = 'source-over';
// now draw border
ctx.beginPath();
ctx.arc(x, y, radius + 5, 0, 2*Math.PI);
ctx.closePath();
ctx.lineWidth = 10;
ctx.strokeStyle = '#ffa94e';
ctx.stroke();
};
img.src = 'http://i.stack.imgur.com/PB8lN.jpg';
<canvas id=canvas width=500 height=180></canvas>
Another solution to this would be in onload function to add another shape above the masked image to simply cover the jagged edges of the clipping mask

How do you add stroke to text in a GPU rendered Mobile app? [duplicate]

Unfortunately the filters do not work (drop shadow, glow) in GPU mode. I'm looking for an opportunity to use these effects to text in this mode. I will welcome any advice.
As Astraport mentions, you'll need to draw the textfield out to a bitmapData every time you update the text using bitmapData.draw().
If you use textField.getBounds to determine the size of the bitmapData you need, the resulting bounds rectangle will not include the extra size due to the filter (e.g. a DropShadowFilter sticks out the side of the textbox by certain pixels depending on the 'distance' and 'blur'). To ensure that you include the filters when you draw the bitmap, you'll also need to use bitmapData.generateFilterRect() to get the correct size rect.
Code snippet (untested, but general idea):
// Remember the transform matrix of the text field
var offset : Matrix = myTextField.transform.matrix.clone();
// Get the bounds of just the textfield (does not include filters)
var tfBounds : Rectangle = myTextField.getBounds( myTextField.parent );
// Create a bitmapData that is used just to calculate the size of the filters
var tempBD : BitmpaData = new BitmapData( Math.ceil(tfBounds.width), Math.ceil(tfBounds.height) );
// Make a copy of the textField bounds. We'll adjust this with the filters
var finalBounds : rectangle = tfBounds.clone();
// Step through each filter in the textField and adjust our bounds to include them all
var filterBounds : rectangle;
for each (var filter : BitmapFilter in myTextField.filters) {
filterBounds = tempBD.generateFilterRect( tfBounds, filter );
finalBounds.left = Math.min( finalBounds.left, filterBounds.left );
finalBounds.right = Math.max( finalBounds.right, filterBounds.right );
finalBounds.top = Math.min( finalBounds.top, filterBounds.top );
finalBounds.bottom = Math.max( finalBounds.bottom, filterBounds.bottom );
}
// Now draw the textfield to a new bitmpaData
var textFieldBD : BitmpaData = new BitmapData( Math.ceil(finalBounds.width), math.ceil(finalBounds.height) );
offset.tx = -finalBounds.x;
offset.ty = -finalBounds.y;
textFieldBD.draw( myTextField.parent, offset, myTextField.transform.colorTransform );
// Create a bitmap and add the bitmap data. Note: normally you would create a
// bitmap once and just update the bitmpaData
var bitmap : Bitmap = new Bitmap();
myTextField.parent.addChild( bitmap );
// Position the bitmap in same place as textField
bitmap.bitmapData = textFieldBD;
bitmap.x = myTextField.x - finalBounds.x;
bitmap.y = myTextField.y - finalBounds.y;
myTextField.visible = false;
Here is how to convert ANY DisplayObject to a Bitmap - useful for "restoring" filter effects in AIR GPU mobile rendermode. This is Pixelthis's solution, fixed, optimized and tested:
// => 'bitmap' must belong to the same parent as 'obj'. 'obj' should be invisible.
static public function Update(obj:DisplayObject, bitmap:Bitmap):void {
//trace("CacheToBmp",obj.name);
// Remember the transform matrix of the text field
var offset:Matrix = obj.transform.matrix.clone();
// Get the bounds of just the textfield (does not include filters)
var bounds:Rectangle = obj.getBounds(obj);
// Create a bitmapData that is used just to calculate the size of the filters
var tempBD:BitmapData = new BitmapData( Math.ceil(bounds.width), Math.ceil(bounds.height), false );
bounds.width = obj.width;
bounds.height = obj.height;
// Make a copy of the textField bounds. We'll adjust this with the filters
var finalBounds:Rectangle = new Rectangle(0,0,bounds.width,bounds.height);
// Step through each filter in the textField and adjust our bounds to include them all
var filterBounds:Rectangle;
for each (var filter:BitmapFilter in obj.filters) {
filterBounds = tempBD.generateFilterRect( tempBD.rect, filter );
finalBounds = finalBounds.union(filterBounds);
}
finalBounds.offset(bounds.x,bounds.y);
finalBounds.x = Math.floor(finalBounds.x);
finalBounds.y = Math.floor(finalBounds.y);
finalBounds.width = Math.ceil(finalBounds.width);
finalBounds.height = Math.ceil(finalBounds.height);
// Now draw the textfield to a new bitmpaData
var data:BitmapData = new BitmapData( finalBounds.width, finalBounds.height, false, 0 );
offset.tx = -finalBounds.x;
offset.ty = -finalBounds.y;
data.drawWithQuality( obj, offset, obj.transform.colorTransform, obj.blendMode, null, true, StageQuality.HIGH );
bitmap.bitmapData = data;
// Position the bitmap in same place as 'obj'
bitmap.x = obj.transform.matrix.tx + finalBounds.x;
bitmap.y = obj.transform.matrix.ty + finalBounds.y;
}
The basic idea is to apply the filters as normal and then draw the display object to a bitmapdata and add the bitmap to the stage. See http://forums.adobe.com/message/3934192 for an example.
If the text that you are applying this to is static it should be easy enough to do, but if you want to apply this dynamic text (for example, a score counter that will change frequently, or text that is user-editable) I imagine it may start to get annoying, but I don't know of any other solution.

Blending objects with eachother but not with background actionscript 3

I have a number of objects (represented as DisplayObjects) that i wish to blend with eachother.
However behind these objects there is a background that i do not want to involve in the blending.
So basically i want to blend these objects with eachother and afterwards use the result of this blending as a new DisplayObject (for example to put it on top of a randomly colored background).
So what i have is:
var obj1:DisplayObject = getFirstObj();
var obj2:DisplayObject = getSecObj();
var background:DisplayObject = getBackground();
obj1.blendMode = BlendMode.ADD;
obj2.blendMode = BlendMode.ADD;
A first attempt i tried was putting these objects into a common DisplayObjectContainer hoping that blending mode would only be relative to all objects contained by the same DisplayObjectContainer, but this does not seem to be the case.
var objectsPool:Sprite = new Sprite();
objectsPool.addChild( obj1 );
objectsPool.addChild( obj2 );
addChild( background );
addchild( objectsPool );
So that diddent get me anywhere.
Any help is appreciated.
EDIT: changed DisplayObjectContainer to Sprite in the last code snippet
If you put the objects into a container, and remove it from the stage, you can then draw it with the BitmapData class and create a new Bitmap object representing the combination. This will have a transparent background, and it's blendMode will be normal, allowing you to use it on the background.
var obj1:DisplayObject = getFirstObj();
var obj2:DisplayObject = getSecObj();
var background:DisplayObject = getBackground();
obj1.blendMode = BlendMode.ADD;
obj2.blendMode = BlendMode.ADD;
var objectsPool:DisplayObjectContainer = new DisplayObjectContainer();
objectsPool.addChild( obj1 );
objectsPool.addChild( obj2 );
var bmd:BitmapData = new BitmapData(objectsPool.width,objectsPool.height,true,0);
bmd.draw(objectsPool);
var drawnObject:Bitmap = new Bitmap(bmd);
addChild( background );
addchild( drawnObject );
(untested code, good luck)
Rather going to the effort of drawing a Bitmap yourself, there are options that cause Flash to rasterize a layer and it's children automatically. Try:
container.cacheAsBitmap = true;
or try:
container.blendMode = "layer";
or try:
container.filters = [new GlowFilter(0,0,0,0)];
Any of those options should cause the children to render into a Bitmap under the hood, invalidating their individual blend modes/effects on the background.