In a html canvas, I am trying to generate a drop shadow on an image with transparent pieces in it. This image is generated by code and then drawn to the canvas using: ctx.putImageData(dst, 0, 0)
The problem is that the following code is not generating any shadow:
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
ctx.shadowBlur = 15;
ctx.shadowColor = 'rgba(0,0,0,1)';
ctx.putImageData(dst, 0, 0);
Any help would be appreciated
ctx.putImageData() will replace the pixels in your context with the ones contained in the ImageData that you puts.
There is no context's property like shadowBlur, nor filter, nor globalCompositeOperation, nor even matrix tranforms that will affect it. Even transparent pixels in your ImageData will be transparent in the context.
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'salmon';
ctx.fillRect(0,0,300,150);
ctx.translate(120, 50);
ctx.rotate(Math.PI/3);
ctx.translate(-25, -25);
ctx.filter = 'blur(5px)';
ctx.globalCompositeOperation = 'lighter';
ctx.fillStyle = '#0000FF';
ctx.fillRect(0,0,50,50);
setTimeout(() => {
// at this time, all previous filters, transform, gCO are still active
const bluerect = ctx.createImageData(50,50);
const data = new Uint32Array(bluerect.data.buffer);
data.fill(0xFFFF0000); // blue
ctx.putImageData(bluerect, 0, 0); // same as our previous fillRect();
// a transparent ImageData (smaller)
const transrect = ctx.createImageData(25, 25);
ctx.putImageData(transrect, 170, 50); // push a bit farther;
}, 1500);
body {
background: lightblue;
}
<canvas id="canvas"></canvas>
So, how to deal with an ImageData and still be able to apply the context's properties on it?
Go through a second off-screen canvas, on which you will put your ImageData, and that you will then draw on your main canvas. drawImage accepts an HTMLCanvasElement as source, and it is affected by context properties like shadowBlur:
const ctx = canvas.getContext('2d');
ctx.shadowBlur = 12;
ctx.shadowColor = "red";
// our ImageData
const bluerect = ctx.createImageData(50,50);
const data = new Uint32Array(bluerect.data.buffer);
data.fill(0xFFFF0000); // blue
// create a new canvas, the size of our ImageData
const offscreen = document.createElement('canvas');
offscreen.width = bluerect.width;
offscreen.height = bluerect.height;
// put our ImageData on it
offscreen.getContext('2d')
.putImageData(bluerect, 0, 0);
// draw it on main canvas
ctx.drawImage(offscreen, 50, 50);
<canvas id="canvas"></canvas>
Now, new browsers have also the ability to do it without the use of a second browser, by generating an ImageBitmap from the ImageData, but this operation is asynchronous, so you may still prefer the old way.
const ctx = canvas.getContext('2d');
ctx.shadowBlur = 12;
ctx.shadowColor = "red";
// our ImageData
const bluerect = ctx.createImageData(50,50);
const data = new Uint32Array(bluerect.data.buffer);
data.fill(0xFFFF0000); // blue
// create an ImageBitmap from our ImageData
createImageBitmap(bluerect)
.then(bitmap => { // later
ctx.drawImage(bitmap, 50, 50);
});
<canvas id="canvas"></canvas>
Related
Is there a way how to fill circle with lines, in same way as it shown in the picture, without math computations for x1y1-x2y2 points on circle body? I mean, something like configuration for gradient or anything else?
Code taken from this mdn article
Result:
// Create a pattern, offscreen
const patternCanvas = document.createElement("canvas");
const patternContext = patternCanvas.getContext("2d");
// Give the pattern a width and height of 50
patternCanvas.width = 25;
patternCanvas.height = 25;
// Give the pattern a background color and draw an arc
patternContext.fillStyle = "white";
patternContext.strokeStyle = "green";
patternContext.lineWidth = 2
patternContext.fillRect(0, 0, patternCanvas.width, patternCanvas.height);
patternContext.moveTo(0, 0);
patternContext.lineTo(25, 25);
patternContext.stroke();
// Create our primary canvas and fill it with the pattern
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const pattern = ctx.createPattern(patternCanvas, "repeat");
ctx.fillStyle = pattern;
ctx.arc(50, 50, 50, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
// Add our primary canvas to the webpage
document.body.appendChild(canvas);
Without math
canvas {
background-image: url("https://i.stack.imgur.com/Q2svp.png");
background-repeat: no-repeat;
}
<canvas width="275.994" height="283.991"></canvas>
I'm pretty new to canvas and haven't worked with it before but I thought it would be a good fit for the following task. While working on it I got doubts and I still don't know if the task is even possible to implement using canvas.
Exemplary graphic of the masks and images and the result that I want to achieve (and the actual results that I got).
The outlines are just there to better illustrate the images
dimensions.
The masks are SVG images which are preloaded using promises before
they are drawn and they change per iteration. So on the first
iteration it's mask A for image 1 and on the second iteration mask
B for image 2.
Simplified pseudo code example:
const items = [1, 2];
for (let i = 0; i < items.length; i++) {
ctx.drawImage(preloadedMask[i], x, y, canvasWidth, canvasHeight);
ctx.globalCompositeOperation = 'source-in';
img[i] = new Image();
img[i].onload = () => {
ctx.drawImage(img[i], 0, 0, canvasWidth, canvasHeight);
ctx.globalCompositeOperation = 'source-over';
//ctx.globalCompositeOperation = 'source-out';
};
img[i].src = `images/${i+1}.jpg`;
}
When I remove the globalCompositeOperation and the images the masks are perfectly drawn next to each other like I expected.
But as soon as I add a globalCompositeOperation it gets complicated and I am super confused to be honest.
I tried every possible globalCompositeOperation value in the onload callback - but it doesn't change much. I think I have to change the globalCompositeOperation after the mask is drawn for each iteration to a different value - but I am out of ideas.
Is there any way to achieve my desired output as described in the graphic or should I ditch canvas for this task?
What you're trying to achieve isn't that easy unfortunately - at least if you're using SVGs which are treated as images and directly drawn to the canvas.
Suppose we have the following svg masks and images
If we take the first mask and the first image and use the following code:
context.drawImage(maskA,0,0,width,height);
context.globalCompositeOperation = "source-in";
context.drawImage(imageA,0,0,width,height);
we get the desired output:
If we repeat the process and do the same for the second mask:
context.drawImage(maskB,0,0,width,height);
context.globalCompositeOperation = "source-in";
context.drawImage(imageB,0,0,width,height);
we'll just see an empty canvas. Why? We set globalCompositeOperation to 'source-in' and the previous canvas and the second mask (maskB) don't have any overlapping regions. That means we're effectively erasing the canvas.
If we try to compensate and either save/restore the context or reset globalCompositeOperation to it's initial state
context.save();
context.drawImage(maskA,0,0,width,height);
context.globalCompositeOperation = "source-in";
context.drawImage(imageA,0,0,width,height);
context.restore();
context.drawImage(maskB,0,0,width,height);
context.globalCompositeOperation = "source-in";
context.drawImage(imageB,0,0,width,height);
we still don't succeed:
So the trick here is this:
make sure both the svgs and images to be masked are fully loaded
create a new empty canvas the size of your target canvas
draw the first mask onto the new canvas
set it's globalCompositeOperation to 'source-in'
draw the first image onto the new canvas
draw the new canvas to the target canvas
erase the new canvas and repeat the previous steps to compose your final image
Here's an example (just click 'Run code snippet'):
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
let imagesLoaded = 0;
let imageA = document.getElementById("imageA");
let imageB = document.getElementById("imageB");
let width = canvas.width;
let height = canvas.height;
function loaded() {
imagesLoaded++;
if (imagesLoaded == 4) {
let tempCanvas = document.createElement("canvas");
let tempContext = tempCanvas.getContext("2d");
tempCanvas.width = width;
tempCanvas.height = height;
tempContext.save();
tempContext.drawImage(document.getElementById("semiCircleA"), 0, 0, width, height);
tempContext.globalCompositeOperation = "source-in";
tempContext.drawImage(imageA, 0, 0, width, 160);
ctx.drawImage(tempCanvas, 0, 0, width, height);
tempContext.restore();
tempContext.clearRect(0, 0, width, height);
tempContext.drawImage(document.getElementById("semiCircleB"), 0, 0, width, height);
tempContext.globalCompositeOperation = "source-in";
tempContext.drawImage(imageB, 0, 0, width, height);
ctx.drawImage(tempCanvas, 0, 0, width, height);
}
}
document.getElementById("semiCircleA").onload = loaded;
document.getElementById("semiCircleB").onload = loaded;
imageA.onload = loaded;
imageA.src = "https://picsum.photos/id/237/160/160";
imageB.onload = loaded;
imageB.src = "https://picsum.photos/id/137/160/160";
<h1>Final Canvas</h1>
<canvas id="canvas" width=160 height=160>
</canvas>
<br>
<h1>Sources</h1>
<img id="semiCircleA" src='data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="160px" height="160px">
<path d="M80,0 A80,80 0 0,0 80,160"/>
</svg>'>
<img id="semiCircleB" src='data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="160px" height="160px">
<path d="M80,0 A80,80 0 0,1 80,160"/>
</svg>'>
<img id="imageA">
<img id="imageB">
A canvas can be a layer
The canvas like any element is easy to create and can be treated like an image, or if you are familiar with photoshop, a canvas can be a layer.
To create a blank canvas
// Returns the renderable image (canvas)
function CreateImage(width, height) {
return Object.assign(document.createElement("canvas"), {width, height});
}
To copy a canvas or image like object
// Image can be any image like element including canvas. Returns the renderable image
function CopyImage(img, width = img.width, height = img.height, smooth = true) {
const can = createImage(width, height});
can.ctx = can.getContext("2d");
can.ctx.imageSmoothingEnabled = smooth;
can.ctx.drawImage(img, 0, 0, width, height);
return can;
}
Loading
Never load images in a render loop. The image onload event will not respect the order you assign the src. Thus the rendering of images in onload will not always be in the order you wish.
Load all images and wait before rendering.
An example of loading a set of images. The function loadImages returns a promise that will resolve when all images have loaded.
const images = {
maskA: "imageUrl",
maskB: "imageUrl",
imgA: "imageUrl",
imgB: "imageUrl",
};
function loadImages(imgList, data) {
return new Promise((done, loadingError) => {
var count = 0;
const imgs = Object.entries();
for (const [name, src] of imgs) {
imgList[name] = new Image;
imgList[name].src = src;
count ++;
imgList[name].addEventListener("load", () => {
count--;
if (count === 0) { done({imgs: imgList, data}) }
}, {once, true)
);
imgList[name].addEventListener("error", () => {
for (const [name, src] of imgs) { imgList[name] = src }
loadingError(new Error("Could not load all images"));
}, {once, true)
);
}
});
}
Rendering
It is best to create functions to do repeated tasks. One task you are repeating is masking, the following function uses a canvas as a destination, an image, and a mask
function maskImage(ctx, img, mask, x = 0, y = 0, w = ctx.canvas.height, h = ctx.canvas.width, clear = true) {
ctx.globalCompositeOperation = "source-over";
clear && ctx.clearRect(0, 0, ctx.canvas.height, ctx.canvas.width);
ctx.drawImage(img, x, y, w, h);
ctx.globalCompositeOperation = "destination-in";
ctx.drawImage(mask, 0, 0, w, h);
return ctx.canvas; // return the renderable image
}
Once you have some utilities set up to help coordinate the loading and rendering you can composite your finial result
// assumes ctx is the context to render to
loadImages(images, {ctx}).then(({imgs, {ctx}} => {
const w = ctx.canvas.width, h = ctx.canvas.height;
ctx.clearRect(0, 0, w, h);
const layer = copyImage(ctx.canvas);
ctx.drawImage(maskImage(layer.ctx, imgs.imgA, imgs.maskA), 0, 0, w, h);
ctx.drawImage(maskImage(layer.ctx, imgs.imgB, imgs.maskB), 0, 0, w, h);
// if you no longer need the images always remove them from memory to avoid hogging
// client's resources.
imgs = {}; // de-reference images so that GC can clean up.
}
You can now layer as many masked images as needed. Because functions where created for each sub task it is easy to create more complicated rendering without needing to write verbose and repeated code, in both this project and future projects.
I have come up with something that suits my needs based on the threads on this subject.
It turns out that in order to scale an image for pattern fill you need 2 canvases. The first canvas creates the scaled image which is used in the second canvas. All of the examples I found display both canvases, but I only want to display the second canvas
Snip:
var c = document.getElementById("DrawQuote");
var ctx = c.getContext("2d");
ctx.clearRect(10, 10, c.width, c.height);
Then ctx context is used to draw some rectangles and polygons, I omitted the code
Then I need to draw a rectangle with scaled image pattern
Snip:
// create the canvas with scaled image
var imgtoscale = new Image();
imgtoscale.src = "blah.jpg";
var canvas1 = document.getElementById("TempCanvas");
var ctx1 = canvas1.getContext("2d");
canvas1.width = imgtoscale.width / Scale / 4;
canvas1.height = imgtoscale.height / Scale / 4;
ctx1.drawImage(imgtoscale, 0, 0, canvas1.width, canvas1.height);
// draw a rectangle filled with the scaled image from above
// ctx is my main canvas, the one I want to display
ctx.fillStyle = ctx.createPattern(canvas1, "repeat");
ctx.fillRect(400, 40, 300, 300);
2 images are displayed:
1-the scaled image to the TempCanvas element
2-the rectangle with scaled fill pattern to the DrawQuote element
But I only want to display the DrawQuote element
Not sure what "threads" you saw, but I guess they were displaying the second canvas only to make clear what happens, however, you really don't need to append that second canvas in the document, it can stay "offscreen":
// the only one visible
var canvas = document.getElementById( 'canvas' );
var ctx = canvas.getContext( '2d' );
var Scale = 2;
var imgtoscale = new Image();
imgtoscale.src = "https://upload.wikimedia.org/wikipedia/commons/thumb/0/04/Woodward_Iowa_Tornado_Damage.JPG/1024px-Woodward_Iowa_Tornado_Damage.JPG";
imgtoscale.onload = function() {
// we create a new canvas element
var canvas1 = document.createElement( 'canvas' );
var ctx1 = canvas1.getContext( '2d' );
canvas1.width = imgtoscale.width / Scale / 4;
canvas1.height = imgtoscale.height / Scale / 4;
ctx1.drawImage( imgtoscale, 0, 0, canvas1.width, canvas1.height );
// draw a rectangle filled with the scaled image from above
// ctx is my main canvas, the one I want to display
ctx.fillStyle = ctx.createPattern( canvas1, 'repeat' );
ctx.fillRect( 100, 100, 300, 300 );
};
canvas { border: 1px solid }
<canvas id="canvas" width="500" height="500"></canvas>
Now note that what these threads missed is that the fillStyle is dependent on the current transformation matrix of the context, so you don't even need a second canvas to do what you want:
First declare the path you wish to fill at normal scale, then set the context scale to what you want, and finally fill.
// the only canvas
var canvas = document.getElementById( 'canvas' );
var ctx = canvas.getContext( '2d' );
var Scale = 2;
var imgtoscale = new Image();
imgtoscale.src = "https://upload.wikimedia.org/wikipedia/commons/thumb/0/04/Woodward_Iowa_Tornado_Damage.JPG/1024px-Woodward_Iowa_Tornado_Damage.JPG";
imgtoscale.onload = function() {
// create the CanvasPattern directly from the <img>
ctx.fillStyle = ctx.createPattern( imgtoscale, 'repeat' );
// declare your path to fill
ctx.beginPath();
ctx.rect( 100, 100, 300, 300 );
// change the scaling (of the fillStyle)
ctx.scale( 1 / Scale / 4, 1 / Scale / 4 );
ctx.fill();
// reset back to default?
ctx.setTransform( 1, 0, 0, 1, 0, 0 );
};
canvas { border: 1px solid }
<canvas id="canvas" width="500" height="500"></canvas>
I'm trying to place text over a background color. I think the issue is that the "fillStyle" is being applied to both the text and the background. I want the text to be black. What am I doing wrong here?
Below is my code:
var canvas = document.createElement("canvas");
canvas.width = 200;
canvas.height = 200;
var ctx = canvas.getContext("2d");
ctx.fillText("hello", 0, 0);
ctx.fillStyle = "#E7E0CA";
ctx.fillRect(0, 0, 200, 200);
var img = document.createElement("img");
img.src = canvas.toDataURL("image/png");
document.body.appendChild(img);
Here's a link to the fiddle: https://jsfiddle.net/jessecookedesign/9rsy9gjn/36/
Unlike HTML where you define a list of what you want to appear, when working with a canvas it's like you're painting. So each "drawing" operation you do (like fillRect or fillText) will go on top of any existing content and cover it up.
Similarly since you're actually painting, rather than defining objects, you need to set the fill style before drawing. Using the analogy, you need to select the color of paint you'll use before you put paint to canvas.
Finally, the fillText method takes a position as the start of the baseline of the text. Since (0, 0) is the top left of the canvas, your text will get drawn above the bounds of the canvas an not be visible, so you need to move it down e.g. fillText("Hello World", 10, 100);
Correcting for those issues you get something like the following (and skipping the steps involved in converting to an img tag):
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
// Draw a black background
context.fillStyle = "black";
context.fillRect(0, 0, 200, 200);
// Draw the text
context.fillStyle = "#E7E0CA";
context.fillText("Hello world", 10, 100);
<canvas id="canvas" width="200" height="200"></canvas>
Wrong order - You're drawing the rectangle over the text.
The text has the same color as the rectangle
There's no font specified
The position (0,0) is out of bounds
Try it like this:
var ctx = canvas.getContext("2d");
ctx.fillStyle = "#E7E0CA";
ctx.fillRect(0, 0, 200, 200);
ctx.fillStyle = "black";
ctx.font="20px Georgia";
ctx.fillText("hello",10,30);
There are several issues:
You need to first fill the background, then fill the text.
Your text is above the canvas area — try a lower position.
This code has the correct order, and a position for the text that isn’t outside the canvas bounds.
var ctx = canvas.getContext("2d");
ctx.fillStyle = "#E7E0CA";
ctx.fillRect(0, 0, 200, 200);
ctx.fillStyle = "#000000";
ctx.fillText("hello", 10, 10);
With the changed order, of course you need to choose a new color for the text, in this case "#000000".
Alternatively, you could save and restore your canvas state:
var ctx = canvas.getContext("2d");
ctx.save();
ctx.fillStyle = "#E7E0CA";
ctx.fillRect(0, 0, 200, 200);
ctx.restore();
ctx.fillText("hello", 10, 10);
Whenever you access canvas of html page,
whatever you draw first, will display first.
so if you want to display your colored box first fill it first, then write your text by providing color ,font and position of text. for example,
<script>
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
ctx.fillStyle = "#E7E0CA";//your rect color
ctx.fillRect(0, 0, 200, 200);//your rect size
ctx.fillStyle = "#000000";//color for your text
ctx.font="30px Arial";//font style and size
ctx.fillText("hello world",25,50);//text and location
</script>
I am new to canvas, I have an image myimg.jpg, I have converted this image into canvas and i am trying to apply some pattern image for heel.
I am not able to do it. Here is my screenshot:
How can I get it done.
<div id="myId">
<canvas id="canvaswrapper" width="660" height="540"></canvas>
</div>
function drawImage(){
var ctx = $("canvas")[0].getContext("2d"),
img = new Image();
img.onload = function(){
ctx.drawImage(img, 0, 0, 500, 500);
ctx.beginPath();
var img2= new Image();
var w;
var h;
img2.src = "http://www.gravatar.com/avatar/e555bd971bc2f4910893cd5b785c30ff?s=128&d=identicon&r=PG";
var pattern = ctx.createPattern(img2, "repeat");
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, w, h);
ctx.arc(300,305,50,0,2*Math.PI);
ctx.fill();
ctx.stroke();
};
img.src = "myimg.jpg";
}
drawImage();
You can define the area you want to fill using an image mask that fits on top of your image - this step is something for Photoshop/GIMP.
For example, having your shoe as-is:
Create a mask for it leaving the heal in the original position (it makes it easier to draw it back in - you can always crop it and draw it using an offset instead). Important: background must be transparent:
Then super-impose the pattern using these steps:
Load the pattern and define is as a fill-pattern
Draw the mask into the empty canvas
Optional step: Adjust transformations if needed (translate, scale)
Choose composite mode "source-atop"
Fill the canvas
Choose composite mode "destination-atop"
Draw the main image on top (which will show behind the mask/pattern)
Optional step: draw in original mask image using blending mode "multiply" to add shadow and highlights (does not work in IE). This will help creating an illusion of depth. For IE, drawing it on top using a reduced alpha or a separate image only containing shadows etc. can be an option
Result
Example
var iShoe = new Image, iMask = new Image, iPatt = new Image, count = 3;
iShoe.onload = iMask.onload = iPatt.onload = loader;
iShoe.src = "http://i.stack.imgur.com/hqL1C.png";
iMask.src = "http://i.stack.imgur.com/k5XWN.png";
iPatt.src = "http://i.stack.imgur.com/CEQ10.png";
function loader() {
if (--count) return; // wait until all images has loaded
var ctx = document.querySelector("canvas").getContext("2d"),
pattern = ctx.createPattern(iPatt, "repeat");
// draw in mask
ctx.drawImage(iMask, 0, 0);
// change comp mode
ctx.globalCompositeOperation = "source-atop";
// fill mask
ctx.scale(0.5, 0.5); // scale: 0.5
ctx.fillStyle = pattern; // remember to double the area to fill:
ctx.fillRect(0, 0, ctx.canvas.width*2, ctx.canvas.height*2);
ctx.setTransform(1,0,0,1,0,0); // reset transform
// draw shoe behind mask
ctx.globalCompositeOperation = "destination-atop";
ctx.drawImage(iShoe, 0, 0);
// to make it more realistic, add mask in blend mode (does not work in IE):
ctx.globalCompositeOperation = "multiply";
if (ctx.globalCompositeOperation === "multiply") {
ctx.drawImage(iMask, 0, 0);
}
}
<canvas width=281 height=340></canvas>