HTML Canvas transparent stroke line - javascript

I want to clear a stroked rectangle in certain canvas area. My initial thought was that I just need to call context.strokeRect function again with same parameters, with changing strokeStyle to transparent before. However it isn't working. Why and how can I fix it? Note that I want to only clear stroke (border) of rectangle, not everything inside.
Edit: I want to clear only border, not everything inside, so I can't use clearRect() method. Also I can't clear whole canvas and repaint it, because canvas contains animations.
JS fiddle: https://jsfiddle.net/47okok8s/

Example below has two canvas. The background just has some text. Then I fill the foreground canvas with "red" and use globalCompositeOperation "destination-out" to remove pixels. I also set alpha to 0.5 and half remove some pixels.
const ctxB = background.getContext("2d");
const ctxF = foreground.getContext("2d");
ctxB.textAlign = "center";
ctxB.textBaseline = "middle";
ctxB.font ="20px arial";
ctxB.fillText("Some Background text",150,25);
ctxB.fillText("Some Background text",150,75);
ctxB.fillText("Some Background text",150,125);
ctxF.lineJoin = "round";
ctxF.fillStyle = "red";
ctxF.fillRect(0,0,300,150);
ctxF.lineWidth = 8;
ctxF.globalCompositeOperation = "destination-out";
ctxF.strokeRect(25,25,250,100);
ctxF.fillRect(75,50,150,50);
ctxF.globalAlpha = 0.5;
ctxF.fillRect(65,40,170,70);
ctxF.globalCompositeOperation = "source-over";
canvas {
position : absolute;
top : 0px;
left: 0px;
}
<canvas id = "background"></canvas>
<canvas id = "foreground"></canvas>

Related

Can I scale up part of an image by secluding an area?

I'm using html5 canvas and JS.
I'm drawing an image on the canvas with CanvasRenderingContext2D.drawImage(). This image is 15x15 black square, with a 1 pixel wide white border: so 16x16 total.
Is there a way to read image data in JS, and only scale up the border width of that image. So, leave the inner black square 15x15, but make the white border width 3 pixels wide?
As far as I can tell, the CanvasRenderingContext2D.getImageData() can only select an area, but you cannot seclude an area, to use the rest.
Thanks :)
You can not change the size of a canvas without clearing its content. The only way to resize the canvas is to create a second canvas, copy the existing content to the new canvas while adding the new content.
The following function will pad a canvas with a boarder by creating a second canvas. Filling it with the padding color, adding the original canvas image to it. Then resize the original and copying the new content to it.
// padCanvas(canvas, padAmount, padColor)
// canvas : HTMLCanvasElement Canvas to pad
// Note all state setting on the input canvas will be reset
// padAmount : Number Amount to pad canvas in pixels
// padColor : String Color of padding as CSS style color string. eg "White"
// Returns the original canvas,
function padCanvas(canvas, padAmount, padColor) {
// create a canvas with the padded size
const can = Object.assign(
document.createElement("canvas"), {
width: canvas.width + padAmount * 2,
height: canvas.height + padAmount * 2
}
);
// add padding color and source (image)
const ctx = can.getContext("2d");
ctx.fillStyle = padColor;
ctx.fillRect(0, 0, can.width, can.height);
ctx.drawImage(canvas, padAmount, padAmount);
// Resize original image and add new content
Object.assign(canvas, {width: can.width, height: can.height});
canvas.getContext("2d").drawImage(can, 0, 0);
return canvas;
}

javascript loaded image is blurry (PNG)

I'm having a problem drawing sprites on canvas for a school project. My code:
treeImage = new Image();
treeImage.src = "sprites/treeSprites.png";
function rocks() { //to create the rock
this.x = 1920 * Math.random(); //random location on the width of the field
this.y = ground[Math.round(this.x/3)]; //ground is an array that stores the height of the ground
this.draw = function() {
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(Math.tan((ground[Math.floor(this.x/3)]-ground[Math.floor(this.x/3)+1])/-3));
//^rotating based on its position on the ground^
ctx.drawImage(treeImage, 200, 50, 50, 50, -25, -50, 50, 50);
ctx.restore();
}
}
len = rockArray.length; //every frame
for (var i = 0;i<len;i++) {
rockArray[i].draw();
}
I only request 50×50px from the image. Exactly outside of the 50×50 there are black lines (which shouldn't interfere because I only request the square within the black lines) but when I draw the rock, the black outlines are visible. (For other reasons, I can't remove the black lines.)
I'm guessing the image JavaScript stores when I load the image is made blurry, and then when I request that part from the image, the lines around are visible too, as the blur "spreads" the lines into the square I request.
Is there a way I can prevent this?
Use ctx.imageSmoothingEnabled = false.
This will make the image sharp instead of smoothed (blurry).
(documentation)
If you draw a vertical line at x=5 and width = 1, the canvas actually draws the line from 4.5 to 5.5 this results in aliasing and a fuzzy line. A quick way to remedy that so it is a solid line is to offset the entire canvas by half a pixel before doing anthing else.
ctx.translate(-0.5, -0.5);
(documentation)

Animation: Making an image gradiently appear from side to side (in opacity)(so called "soft-wipe" effect)

Sorry if the title is confusing. I've tried my best to compose it, if you are going to understand what I am asking, feel free to suggest better title in comment.
The animation I am trying to make can easily be done by video editors, but looks to me not so easy with CSS/JS: First of all, I am not talking about sliding in the image, the image is not moving at all. I want it to appear from side to side, but in a gradient transparent manner. Imagine there is a gradient opacity mask on the image, making the opacity of its left end to be 1, and that of its right end to be 0. When this mask moves from left right, it is the animation I wanna achieve.
I can split the image to several pieces, and manipulate the opacity of each one, there has to be a certain amount pieces to make the whole animation smooth and appeal.
The other way I am thinking is to use canvas, where you can manipulate the image by pixel, as this page suggests, I can do
// get the image data object
var image = ctx.getImageData(0, 0, 500, 200);
// get the image data values
var imageData = image.data,
length = imageData.length;
// set every fourth value to 50
for(var i=3; i < length; i+=4){
imageData[i] = 50;
}
// after the manipulation, reset the data
image.data = imageData;
// and put the imagedata back to the canvas
ctx.putImageData(image, 0, 0);
However the page is only taking about a static image but not animation. I am wondering whether it would undermine performance if I use this approach. Also, this approach involve a lot of ugly calculations.
I think what I want to achieve is not very strange, so is there any javascript plugin to achieve this?
The animation I am trying to make can easily be done by video editors,
[...] the image is not moving at all. I want it to appear from side to side
In the video industry we call this a soft-wipe (AKA soft edged wipe) and it's not too complicated to make.
All you need is an alpha mask that you can make using a linear gradient. Then use translate properties with the context combined with xor composite mode to animate it.
What xor mode does is to invert the alpha channel based on the alpha channel drawn to it. The advantage of this is that the canvas element get transparent as well, so any background can show through. You can keep the default comp. mode as well which will make the background black instead.
The gradient is made like this (the color values does not matter with xor mode, just the alpha channel values):
var g = ctx.createLinearGradient(0, 0, ctx.canvas.width, 0);
g.addColorStop(0, "rgba(0,0,0,0)");
g.addColorStop(1, "rgba(0,0,0,1)");
ctx.fillStyle = g;
(see this answer for how to avoid "bright line" artifacts by creating a smoothed gradient).
Now create a function that draws a complete frame based on position t which is a normalized value combined with canvas width - have in mind we need the double width to work with: gradient + room for the gradient to exit -
function render(t) {
var w = t * ctx.canvas.width; // width based on t
ctx.drawImage(img, 0, 0); // render bg. image
ctx.translate(-ctx.canvas.width + w, 0); // translate on x-axis
ctx.fillRect(0, 0, ctx.canvas.width * 2, ctx.canvas.height); // render gradient mask
}
Call this in an animation loop until t=2, but optionally set globalCompositeOperation to xor and we're good to go. The animation loop itself will reset transformation for us:
Demo
var ctx = c.getContext("2d"),
img = new Image,
t = 0, step = 0.02
// alpha mask
var g = ctx.createLinearGradient(0, 0, ctx.canvas.width, 0);
g.addColorStop(0, "rgba(0,0,0,0)");
g.addColorStop(1, "rgba(0,0,0,1)");
ctx.fillStyle = g;
ctx.globalCompositeOperation = "xor";
// load bg image
img.onload = animate;
img.src = "http://i.imgur.com/d0tZU7n.png";
function animate() {
ctx.setTransform(1,0,0,1,0,0); // reset any transformations
ctx.clearRect(0,0,ctx.canvas.width,ctx.canvas.height);
render(t);
t += step;
if (t <= 2) requestAnimationFrame(animate); // 2 since we need double width
else {t=0; setTimeout(animate, 2000)}; // just to repeat anim. for demo
}
function render(t) {
var w = t * ctx.canvas.width; // width based on t
ctx.drawImage(img, 0, 0);
ctx.translate(-ctx.canvas.width + w, 0); // translate on x-axis
ctx.fillRect(0, 0, ctx.canvas.width*2, ctx.canvas.height);
}
body {background:url(http://i.imgur.com/OT99vSA.jpg) repeat}
<canvas width=658 height=325 id=c></canvas>
Use the context composite property to do the masking via a gradient. Create an offscreen canvas the same size as the image or the size of the display canvas whichever is smallest.
For every frame create the gradient with the appropriate colour stops (CSS color format rgba(red, green, blue, alpha)) to set the alpha values.
Clear the off screen canvas
ctxOffScreen.clearRect( 0, 0, ctxOffScreen.canvas.width, ctxOffScreen.canvas.height);
Then set the composite value for the of screen canvas to
ctxOffScreen.globalCompositeOperation = "source-over";
Render the image onto it
ctxOffScreen.drawImage(image, 0, 0, ctxOffScreen.canvas.width, ctxOffScreen.canvas.height);
Then set the comp to
ctxOffScreen.globalCompositeOperation = "destination-in";
This will match the pixels already drawn with the same alpha value that is in what every you draw next (the gradient mask)
Then set the fill style to the gradient you created and draw a rectangle over the top
ctxOffScreen.fillStyle = gradient;
ctxOffScreen.fillRect( 0, 0, ctxOffScreen.canvas.width, ctxOffScreen.canvas.height);
Then just render the offscreen canvas to the onscreen canvas
ctx.drawImage(ctxOffScreen.canvas, 0, 0);
If you use
ctxOffScreen.globalCompositeOperation = "destination-out";
instead of "destination-in" you will invert the mask you created with the gradient.
As an alternative to canvas, you can use svg. CSS gradients are not animateable, however other properties of svg are, so you can find some creative ways to animate gradients after all.
#Mask rect {
x: 400px;
transition: 1s;
}
svg:hover #Mask rect {
x: -400px;
}
svg {
border: 2px solid black;
background-color: #ee3377;
}
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="400" height="400">
<defs>
<linearGradient id="Gradient">
<stop offset="0" stop-color="white" stop-opacity="0" />
<stop offset=".5" stop-color="white" stop-opacity="1" />
</linearGradient>
<mask id="Mask">
<rect width="800" height="400" fill="url(#Gradient)" />
</mask>
</defs>
<image xlink:href="http://i.imgur.com/g3D5jNz.jpg" width="400" height="400" mask="url(#Mask)"></image>
</svg>
You could probably animate the offset property directly, I haven't tested yet.

FabricJS prevent canvas.clipTo from clipping canvas.backgroundImage

I want to set a global clipTo in my Fabric-powered Canvas that will affect all user-added layers. I want a background image and an overlay image, which are unaffected by this clip mask.
Example:
Here's what's happening in this photo:
A canvas overlay image makes the t-shirt look naturally wrinkled. This overlay image is mostly transparent
A background image in the exact shape of the t-shirt was added, which is supposed to make the t-shirt look blue
A canvas.clipTo function was added, which clips the canvas to a rectangular shape
A user-added image (the famous Fabric pug) was added
I want the user-added image (the pug) to be limited to the rectangular area.
I do not want the background image (the blue t-shirt shape) affected by the clip area.
Is there a simple way to accomplish this? I really don't want to have to add a clipTo on every single user layer rather than one tidy global clipTo.
You can play with a JS fiddle showing the problem here.
I came here with the same need and ultimately found a solution for what I'm working on. Maybe it helps:
For SVG paths, within the clipTo function you can modify the ctx directly prior to calling render(ctx) and these changes apply outside the clipped path o. Like so:
var clipPath = new fabric.Path("M 10 10 L 100 10 L 100 100 L 10 100", {
fill: 'rgba(0,0,0,0)',
});
var backgroundColor = "rgba(0,0,0, 0.2)";
var opts = {
controlsAboveOverlay: true,
backgroundColor: 'rgb(255,255,255)',
clipTo: function (ctx) {
if (typeof backgroundColor !== 'undefined') {
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, 300, 150);
}
clipPath.render(ctx);
}
}
var canvas = new fabric.Canvas('c', opts);
canvas.add(new fabric.Rect({
width: 50,
height: 50,
left: 30,
top: 30,
fill: 'rgb(255,0,0)'
}));
You can of course add an image instead of a color, or whatever else you want done. The trick I've found is to put it in the clipTo function on the ctx directly.
here's a fiddle
One (sorta hacky) solution: set a CSS background image on your canvas element, as shown in https://jsfiddle.net/qpnvo3cL/
<canvas id="c" width="500" height="500"></canvas>
<style>
background: url('http://fabricjs.com/assets/jail_cell_bars.png') no-repeat;
</style>
<script>
var canvas = window._canvas = new fabric.Canvas('c');
canvas.clipTo = function(ctx) {
ctx.rect(100,100,100,100);
}
</script>
Have you tried clipping a fabric Group? You could make the whole shirt one canvas. The center graphics would be one Group which you clip to where you want it. The white t-shirt and the blue overlay would of course not be part of the clipped group.
Here's an example of clipping a group:
var rect = new fabric.Rect({width:100, height: 100, fill: 'red' });
var circle = new fabric.Circle({ radius: 100, fill: 'green' });
var group1 = new fabric.Group([ circle, rect ], { left: 100, top: 100 });
canvas.add(group1);
group1.clipTo = function(ctx) {
ctx.rect(50,50,200,200);
};
See this jsfiddle I made: https://jsfiddle.net/uvepfag5/4/
I find clip rather slow so I tend to use globalCompositeOperation to do masking.
If you really need to use clip then use it in conjunction with save and restore.
// ctx is canvas context 2d
// pug is the image to be clipped
// draw your background
ctx.save(); // save state
ctx.rect(100,100,100,100); // set the clip area
ctx.clip(); // apply the clip
ctx.drawImage(pug,x,y); // draw the clipped image
ctx.restore(); // remove the clipping
// draw the other layers.
or you can
// draw background
ctx.globalCompositeOperation = "xor"; // set up the mask
ctx.fillRect(100,100,100,100); // draw the mask, could be an image.
// Alpha will effect the amount of masking,
// not available with clip
ctx.globalCompositeOperation = "destination-over";
ctx.drawImage(pug,x,y); // draw the image that is masked
ctx.globalCompositeOperation = "source-over";
// draw the stuff that needs to be over everything.
The advantage of composite operations is you have control over the clipping at a per pixel level, including the amount of clipping via the pixel alpha value

Responsive Canvas in Bootstrap

I'm trying to do a responsive canvas. All my tests has been doing with a 600x600 canvas and with that height and width it works OK and paint every line correctly. The problem is that I have tried this:
#myCanvas {
background-color: #ffffff;
border:1px solid #000000;
min-height: 600px;
height: 100%;
width: 100%;
}
Just for the record, myCanvas is inside a sm-col-8.
And it looks nice on my laptop and looks nice on my phone but (because of my draw() function, because it was thinking for a square) the draw starts more like in the down-left corner (nearby) and it should start at up-right corner.
So, I don't want to change my draw() function but what I'm looking for is to reescale the canvas size. I mean: If I'm in a laptop/tablet.. with 600x600, show it at that size, but if I'm on my phone which has 384x640 show it like 300x300? I don't know if it could be a good solution.
My draw function:
function drawLines(lines,i,table,xtotal,ytotal){
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
var xIni;
var xFin;
var yIni;
var yFin;
xIni = (c.width*parseInt(lines[i][1])/xtotal);
yIni = (c.height*parseInt(lines[i][2])/ytotal);
xFin = (c.width*parseInt(lines[i][3])/xtotal);
yFin = (c.height*parseInt(lines[i][4])/ytotal);
ctx.beginPath();
ctx.moveTo(xIni,c.height-yIni);
ctx.lineTo(xFin,c.height-yFin);
ctx.lineWidth=4;
ctx.strokeStyle = colorAleatorio();
ctx.stroke();
}
With Bootstrap, use:
<canvas id="canvas" class='img-responsive' style="border: 1px solid black;"></canvas>
You can make your html Canvas responsive by using the context.scale command.
The .scale command will scale the internal coordinates system used by canvas.
This means you do not need to change any of your own drawing coordinates because canvas will automatically transform your coordinates into scaled canvas coordinates for you.
// save the original width,height used in drawLines()
var origWidth=600;
var origHeight=600;
var scale=1.00;
// reference to canvas and context
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
// call this after resizing
// send in the new maximum width,height desired
function resizeAndRedraw(newMaxWidth,newMaxHeight){
// calc the global scaling factor that fits into the new size
// and also maintains the original aspect ratio
scale=Math.min((newMaxWidth/origWidth),(newMaxHeight/origHeight))
// resize the canvas while maintaining correct aspect ratio
canvas.width=origWidth*scale;
canvas.height=origHeight*scale;
// Note: changing the canvas element's width or height will
// erase the canvas so you must reissue all your drawing commands
drawLines(lines,i,table,xtotal,ytotal);
}
// call drawLines
function drawLines(lines,i,table,xtotal,ytotal){
// scale the canvas coordinate system to the current scale
// Note: This scales the coordinates used internally
// by canvas. It does not resize the canvas element
ctx.scale(s,s);
// now do your drawing commands
// You do not need to adjust your drawing coordinates because
// the Canvas will do that for you
var xIni;
var xFin;
var yIni;
var yFin;
xIni = (c.width*parseInt(lines[i][1])/xtotal);
yIni = (c.height*parseInt(lines[i][2])/ytotal);
xFin = (c.width*parseInt(lines[i][3])/xtotal);
yFin = (c.height*parseInt(lines[i][4])/ytotal);
ctx.beginPath();
ctx.moveTo(xIni,c.height-yIni);
ctx.lineTo(xFin,c.height-yFin);
ctx.lineWidth=4;
ctx.strokeStyle = colorAleatorio();
ctx.stroke();
// restore the context to it's unscaled state
ctx.scale(-s,-s);
}

Categories