Safari incorrectly drawing shadows on SVGs via HTML5 Canvas - javascript

I'm using HTML5 canvas in a project and occasionally need to draw drop shadows on SVGs within a canvas. I've noticed that, compared to Chrome, Safari does two things incorrectly when doing this:
Safari draws a shadow on each individual shape within an SVG
Safari crops off parts of the shadow that go beyond the SVG's bounds
These issues can be illustrated by the following code:
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
context.shadowOffsetX = 10;
context.shadowOffsetY = 10;
context.shadowColor = 'red'
var image = new Image();
image.src = 'https://storage.googleapis.com/card-conjurer/img/manaSymbols/0.svg';
image.onload = function() {
context.drawImage(image, 10, 10, 100, 100);
}
<canvas id='canvas'></canvas>
I can't embed images yet, but here are some links to images that illustrate the problem:
SVG Shadows with Google Chrome
SVG Shadows with Safari
(they are screenshots of the code above)
The results from Safari are... quite ugly, as you can see. Is there a way make Safari to render SVGs with shadows on HTML5 canvas like Chrome does?
Any help would be greatly appreciated.
Thanks so much for your time!

That's a bug, you should report it to webkit's bug-tracker.
Though you can workaround it by first drawing the image on a second canvas just to rasterize that svg image and use that canvas as source for the shadowing:
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
var image = new Image();
image.src = 'https://storage.googleapis.com/card-conjurer/img/manaSymbols/0.svg';
image.onload = function() {
const off = canvas.cloneNode();
off.getContext('2d').drawImage(image, 10, 10, 100, 100);
context.shadowOffsetX = 10;
context.shadowOffsetY = 10;
context.shadowColor = 'red';
context.drawImage(off, 0, 0);
}
<canvas id='canvas'></canvas>
In order to use a single canvas, we need to use an offset trick, but it's not always easy to do since it requires knowing clearly the position of our drawing:
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
var image = new Image();
image.src = 'https://storage.googleapis.com/card-conjurer/img/manaSymbols/0.svg';
image.onload = function() {
// first pass without shadow
context.drawImage(image, 10, 10, 100, 100);
// set shadow offsets to the position in page of bottom-right corner
context.shadowOffsetX = 10 + 110;
context.shadowOffsetY = 10 + 110;
context.shadowColor = 'red';
// draw behind
context.globalCompositeOperation = "destination-over";
// draw with inverse offset, so that the image is not visible
// but the shadow is in-screen
context.drawImage(canvas, -110, -110);
}
<canvas id='canvas'></canvas>

Related

Canvas - Blending generated noise with a gradient does not work [duplicate]

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>

Simplest way to tile an Image in JS

I have a PNG graphic I want to use as a background for my canvas in JS. It should be displayed in a tiled format. For this I'm currently doing something like this.
const ratioX = Math.ceil(canvas.width / image.width);
const ratioY = Math.ceil(canvas.height / image.height);
for (var x = 0; x < ratioX; x++) {
for (var y = 0; y < ratioY; y++) {
ctx.drawImage(image, x*image.width, y*image.height, image.width, image.height);
}
}
It's working as intended, but the performance might get improved by not doing the calculations every frame, but buffering the bigger tiled version of my image. So I'm looking for a way to create this image as an instance of the Image class or any other class I can use for drawImage().
At first I might need to get the image data (so the rgba info for every pixel). I've seen a way to do it like here, using canvas's context, but do I really need to do that? Is there no simpler way?
The second step would be to create a drawable image object out of that data. How would I do that?
I would love to see a way that's equivalent to Java's bufferedImage.getRgb(x, y) and bufferedImage.setRgb(x, y, color). Does something like this exist in JS?
As shown here in W3Schools you can use ctx createPattern to achieve what you want:
var ctx = document.getElementById("canvas").getContext("2d");
var img = new Image();
img.src = 'http://placehold.it/50/50'
img.onload = function() {
var pat = ctx.createPattern(img, 'repeat');
ctx.rect(0, 0, 350, 350);
ctx.fillStyle = pat;
ctx.fill();
};
<canvas id='canvas' width=350 height=350/>

Fill image with texture/pattern

I'm looking for a solution to change the texture/pattern for a product.
At this moment i have:
A .png picture of a couch with a transparent background
A .png picture of a texture
With the following code:
<canvas id="a" width="800" height="500">Canvas not supported on your browser</canvas>
var width = $(window).width();
var height = $(window).height();
var c = document.getElementById("a");
var ctx = c.getContext("2d");
var can2 = document.createElement('canvas');
document.body.appendChild(can2)
can2.width = c.width;
can2.height = c.height;
var ctx2 = can2.getContext("2d");
var test = new Image();
test.src = "Images/newBank.png";
test.onload = function () {
ctx2.drawImage(test, 0, 0);
};
var img = new Image();
img.src = "Images/texturetrans.png";
img.onload = function () {
ctx2.globalCompositeOperation = 'source-in';
var ptrn = ctx2.createPattern(img, 'repeat');
ctx2.fillStyle = ptrn;
ctx2.fillRect(0, 0, can2.width, can2.height);
}
`
I get this result:
As you can see, the whole object is filled with my texture. No definitions of the pillows etc. are visible anymore. Is it possible to let my texture be a sort of transparent mask?
I'm already able to change the color of the couch:
But I'd like to be able to also add a pattern to my couch!
Any help will be appreciated and I'm already very sorry for my bad English.
If you're just after an illustrative approximation you can use a combination of blending and composition modes.
First thing is to make sure your main image has transparency - this is important for composition to work (I made a rough cut-off in the following demo).
Main steps:
Draw the pattern
Draw the main image on top with blending mode multiply
Draw the main image on top with compositing mode destination-in - this will make a cut-out
If you want to reduce the size of the pattern you can either do this by using a smaller version of the image, draw to a temporary canvas at a smaller size and use that as pattern, or use the new transform methods on the pattern itself.
Demo
var img1 = new Image, img2 = new Image, cnt = 2,
canvas = document.getElementById("canvas"),
ctx = canvas.getContext("2d");
// image loading for demo (ignore)
img1.onload = img2.onload = function() {if (!--cnt) go()};
img1.src = "//i.imgur.com/8WqH9v4.png"; // sofa
img2.src = "//i.stack.imgur.com/sQlu8.png"; // pattern
// MAIN CODE ---
function go() {
// create a pattern
ctx.fillStyle = ctx.createPattern(img2, "repeat");
// fill canvas with pattern
ctx.fillRect(0, 0, canvas.width, canvas.height);
// use blending mode multiply
ctx.globalCompositeOperation = "multiply";
// draw sofa on top
ctx.drawImage(img1, 0, 0, img1.width*.5, img1.height*.5);
// change composition mode
ctx.globalCompositeOperation = "destination-in";
// draw to cut-out sofa
ctx.drawImage(img1, 0, 0, img1.width*.5, img1.height*.5);
}
<canvas id="canvas" width=600 height=400></canvas>
You can also reverse the order of which image is drawn etc., if you prefer. This is just an example of one way.
If you need accurate texture then there is no way around to either take photos or use a 3D software, or hand-drawn the textures.
NOTE: IE does not support multiply - For this you need to manually iterate through the pixels and multiply each component with each other.
You can test for support this way:
ctx.globalCompositeOperation = "multiply";
if (ctx.globalCompositeOperation === "multiply") {
// blend as above
}
else {
// iterate and blend manually
}
Blending mode luminosity is mentioned in comments and this can be used too of course. I just want to point a couple of things to consider. The first is that this is a non-separable blending mode meaning it depends on all components as it goes through the HSL color model. This makes it a bit more compute intensive.
The second is that if you end up having to do this manually (in for example IE) the code is a bit more complex to emulate, and will be noticeably slower.

Draw multiple image's on canvas with each image rotated

I'm new into HTML5 programming and I wanted to know how to rotate each image when it is added into canvas. Should each of them be placed into a canvas and then rotated? If so how can i add multiple canvas into a single canvas context.
Fiddle : http://jsfiddle.net/G7ehG/
Code
function loadImages(sources, callback) {
var images = {};
var loadedImages = 0;
var numImages = 0;
// get num of sources
for(var src in sources) {
numImages++;
}
for(var src in sources) {
images[src] = new Image();
images[src].onload = function() {
if(++loadedImages >= numImages) {
callback(images);
}
};
images[src].src = sources[src];
}
}
var canvas = document.getElementById('myCanvas');
var context = canvas.getContext('2d');
var sources = {
image1: 'http://farm3.static.flickr.com/2666/3686946460_0acfa289fa_m.jpg',
image2: 'http://farm4.static.flickr.com/3611/3686140905_cbf9824a49_m.jpg'
};
loadImages(sources, function(images) {
context.drawImage(images.image1, 100, 30, 200, 137);
context.drawImage(images.image2, 350, 55, 93, 104);
});
In your comment you mentioned that you know about context.rotate, but you don't want the context to stay rotated. That's not a problem at all. First, calling context.rotate only affects things which are drawn afterwards. Anything drawn before will stay were it was. Second, it can be easily reversed after drawing.
use context.save() to create a snapshot of all current context settings, including current rotation.
use context.rotate(angle) and draw your image. The angle is in Radian. That means a full 360° circle is Math.PI * 2. The point the image will be is rotated around is the current origin of the canvas (0:0). When you want to rotate the image around its center, use context.translate(x, y) to set the origin to where you want the center of the image to be, then rotate, and then draw the image at the coordinates -img.width/ 2, -img.height / 2
use context.restore() to return to your snapshot. Rotation and translation will now be like they were before.
Here is an example function which draws an image rotated by 45° at the coordinates 100,100:
function drawRotated(image, context) {
context.save();
context.translate(100, 100);
context.rotate(Math.PI / 4);
context.drawImage(image, -image.width / 2, -image.height / 2);
context.restore();
}

Html5 canvas pattern drawing delay

i draw a canvas(aka canvas 1) with image() then rotate it 25 degree. then i take rotated canvas to make a pattern for another canvas(aka canvas 2). then i draw this . and fill the fillstyle with newly created pattern. i noticed if alert in the middle of below code
finalsleeve_ctx.globalCompositeOperation = "source-atop";
/*****************************************
alert(sleeve.toDataURL('image/png'));
*****************************************/
var pattern = finalsleeve_ctx.createPattern(sleeve, 'repeat');
then firefox gives a correct output but if i dont do alert it does not give me correct output. crome not showing me correct output.
do i need to delay ?
here is what i have tried.
HTML
<div >
<canvas id="sleeve" width=436 height=567></canvas>
<canvas id="finalsleeve" width=436 height=567 ></canvas>
</div>
JS
var sleeve = document.getElementById('sleeve');
var sleeve_ctx = sleeve.getContext('2d');
var finalsleeve = document.getElementById('finalsleeve');
var finalsleeve_ctx = finalsleeve.getContext('2d');
function rotator2(var2,var3)
{
sleeve.width=sleeve.width;
var imageObj_rotator2 = new Image();
imageObj_rotator2.onload = function ()
{
var pattern_rotator2 = sleeve_ctx.createPattern(imageObj_rotator2, "repeat");
sleeve_ctx.fillStyle = pattern_rotator2;
sleeve_ctx.rect(0, 0, sleeve.width, sleeve.height);
sleeve_ctx.rotate(var3 * Math.PI/180);
sleeve_ctx.fill();
}
imageObj_rotator2.src = var2;
}
function drawSleeve()
{
finalsleeve.width = finalsleeve.width;
var imgsleeve = new Image();
imgsleeve.src="http://i.stack.imgur.com/FoqGC.png";
finalsleeve_ctx.drawImage(imgsleeve,0,0);
finalsleeve_ctx.globalCompositeOperation = "source-atop";
alert(sleeve.toDataURL('image/png'));
var pattern = finalsleeve_ctx.createPattern(sleeve, 'repeat');
finalsleeve_ctx.rect(0, 0, sleeve.width, sleeve.height);
finalsleeve_ctx.fillStyle = pattern;
finalsleeve_ctx.fill();
finalsleeve_ctx.globalAlpha = .10;
finalsleeve_ctx.drawImage(imgsleeve, 0, 0);
finalsleeve_ctx.drawImage(imgsleeve, 0, 0);
finalsleeve_ctx.drawImage(imgsleeve, 0, 0);
}
rotator2('http://i.stack.imgur.com/fvpMN.png','25');
drawSleeve();
Here is fiddle.
http://jsfiddle.net/EbBHz/
EDITED
Sorry, I completely misunderstood your question. I just now went back and saw the last question you posted and the goal you are trying to achieve.
To get the functionality you desire you can just create one function, you don't need two. Instead of using a second canvas in the HTML I created a temporary one using javascript.
Here is the simplified and functional code
<canvas id="sleeve" width='436' height='567'></canvas>
var sleeve = document.getElementById('sleeve');
var ctx = sleeve.getContext('2d');
function rotator2(var2, var3) {
// Draw the original sleeves
var imageObj_rotator2 = new Image();
imageObj_rotator2.src = var2;
imageObj_rotator2.onload = function () {
var imgsleeve = new Image();
imgsleeve.src="http://i.stack.imgur.com/FoqGC.png";
ctx.drawImage(imgsleeve,0,0);
// Create a second temporary canvas
var pattern = document.createElement('canvas');
pattern.width = 500;
pattern.height = 500;
var pctx = pattern.getContext('2d');
// Make the pattern that fills the generated canvas
var pattern_rotator2 = pctx.createPattern(imageObj_rotator2, "repeat");
pctx.fillStyle = pattern_rotator2;
pctx.rotate(var3 * Math.PI / 180);
// Fill the generated canvas with the rotated image pattern we just created
pctx.fillRect(0, 0, pattern.width, pattern.height);
// Create a pattern of the generated canvas
var patterned = ctx.createPattern(pattern, "repeat");
// Fills in the non-transparent part of the image with whatever the
// pattern from the second canvas is
ctx.globalCompositeOperation = "source-in";
ctx.fillStyle = patterned;
ctx.fillRect(0, 0, sleeve.width, sleeve.height);
}
}
rotator2('http://i.stack.imgur.com/fvpMN.png', '45')
The technique works alright, but only for certain angles. Here is the demo set to 45 degrees. As you can see, there is a problem: part of the sleeve is whited out. However, if you change the degree to 15 like this it works just fine. This is because when the image is being rotated in the created canvas it leaves white space before repeating. To see this issue first hand, change the width and the height of the created canvas to 30 (the default width/height of the image) like this
Note: You may have to click run once the jsfiddle tab is open, canvases don't like generating content when another tab is focused
I tried problem solving the issue including
Making the generated canvas really large (which works but KILLS load
time/crashes page sometimes)
Translating the picture in the generated canvas after rotating it
which didn't work like I had hoped
Coming up with a function to change the width/height to cover the
entire first canvas based on the rotated second-canvas-dimensions, which is by far the most promising, but I don't have the time or desire to work out a good solution
All that being said if the angle HAS to be dynamic you can work on a function for it. Otherwise just use a workaround angle/generated canvas dimensions
final result> Here is a working solution for fill rotated pattern without white at any angle
var sleeve = document.getElementById('sleeve');
var ctx = sleeve.getContext('2d');
function rotator2(var2, var3) {
var x =0;
var y=0;
//pattern size should be grater than height and width of object so that white space does't appear.
var patternSize = sleeve.width+ sleeve.height;
// Draw the original sleeves
var imageObj_rotator2 = new Image();
imageObj_rotator2.src = var2;
imageObj_rotator2.onload = function () {
var imgsleeve = new Image();
imgsleeve.src="http://i.stack.imgur.com/FoqGC.png";
ctx.drawImage(imgsleeve,0,0);
// Create a second temporary canvas
var pattern = document.createElement('canvas');
pattern.width = sleeve.width;
pattern.height = sleeve.height;
var pctx = pattern.getContext('2d');
// Make the pattern that fills the generated canvas
var pattern_rotator2 = pctx.createPattern(imageObj_rotator2, "repeat");
pctx.fillStyle = pattern_rotator2;
//moving rotation point to center of target object.
pctx.translate(x+ sleeve.width/2, y+sleeve.height/2);
pctx.rotate(var3 * Math.PI / 180);
// Fill the generated canvas with the rotated image pattern we just created and expanding size from center of rotated angle
pctx.fillRect(-patternSize/2, -patternSize/2, patternSize, patternSize);
// Create a pattern of the generated canvas
var patterned = ctx.createPattern(pattern, "no-repeat");
// Fills in the non-transparent part of the image with whatever the
// pattern from the second canvas is
ctx.globalCompositeOperation = "source-in";
ctx.fillStyle = patterned;
ctx.fillRect(x, y, sleeve.width, sleeve.height);
}
}
rotator2('http://i.stack.imgur.com/fvpMN.png', '50')

Categories

Resources