I'm currently developing a 2D graphic library in JavaScript, and now I'm sticking with the background texture transforming problems.
I want to set the background texture (property fillStyle) of a canvas context (CanvasRenderingContext2D) to an image (CanvasPattern).
It's easy to assign an image to the fillStyle.
But the only problem is that, the image can't actually be translated, scaled nor skewed.
I've looked up MDN, it says there's a prototype called setTransform().
With this API you can transform the image by an SVGMatrix, that seems a little annoying.
Not only you'd have to create an redundant <svg> element, it's also an experimental API, and it CAN'T work in Google Chrome.
It's just impossible to solve using a regular way.
Is there any 'hacky' ways to do this?
First draw the path then set the transform.The path stays where it was while the fill is transformed.
The example rotates the pattern inside two boxes.
const ctx = canvas.getContext('2d');
var pattern;
const img = new Image();
img.src = 'https://mdn.mozillademos.org/files/222/Canvas_createpattern.png';
img.onload = () => pattern = ctx.createPattern(img, 'repeat');
requestAnimationFrame(mainLoop);
function mainLoop(time){
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.fillStyle = pattern;
ctx.beginPath();
ctx.setTransform(1,0,0,1,0,0);
ctx.rect(100,100,200,200); // create the path for rectangle
ctx.setTransform(1,0,0,1,300,200); // Set the transform for the pattern
ctx.rotate(time / 1000);
ctx.fill();
ctx.beginPath();
ctx.setTransform(1,0,0,1,0,0);
ctx.rect(150,0,100,400); // create the path for the rectangle
ctx.setTransform(0.2,0,0,0.2,150,200); // Set the transform for the pattern
ctx.rotate(-time / 1000);
ctx.fill();
requestAnimationFrame(mainLoop);
}
<canvas id="canvas" width="400" height="400" style="border:2px solid black;"></canvas>
Set the transform on the CanvasRenderingContext2D, not on the CanvasPattern. This is much better supported, and you do not neet the SVGMatrix object.
The resulting painted area is the transformed one, so this might only be usefull if you want the whole canvas to have a uniform pattern.
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var img = new Image();
img.src = 'https://mdn.mozillademos.org/files/222/Canvas_createpattern.png';
img.onload = function() {
var pattern = ctx.createPattern(img, 'repeat');
ctx.fillStyle = pattern;
ctx.setTransform(0.8, 0.3, 0, 0.8, 0, 0)
ctx.fillRect(0, -172, 450, 700); //make sure the whole canvas is covered
};
<canvas id="canvas" width="400" height="400"></canvas>
Related
I am working on an app which uses canvas. I draw some shapes, one over another which can be filled with colours or images and overlap each other. I use clip() to clip images to fit shape, but when I change globalCompositeOperation to multiply it makes clip() stop working. I've created a simple example to present what my problem is.
Please try to open it in Google Chrome and in Mozilla Firefox. While in Chrome image can be clipped then set as a multiply of lower layer, in Mozilla after applying multiply, clipping stops working. Any ideas to solve this issue?
// Grab the Canvas and Drawing Context
var canvas = document.getElementById('c');
var ctx = canvas.getContext('2d');
// Create an image element
var img = document.createElement('IMG');
// When the image is loaded, draw it
img.onload = function () {
ctx.fillStyle="red"
ctx.fillRect(0,0,100,100)
// Save the state, so we can undo the clipping
ctx.save();
// Create a shape, of some sort
ctx.beginPath();
ctx.moveTo(10, 10);
ctx.lineTo(100, 30);
ctx.lineTo(180, 10);
ctx.lineTo(200, 60);
ctx.arcTo(180, 70, 120, 0, 10);
ctx.lineTo(200, 180);
ctx.lineTo(100, 150);
ctx.lineTo(70, 180);
ctx.lineTo(20, 130);
ctx.lineTo(50, 70);
ctx.closePath();
// Clip to the current path
ctx.clip();
ctx.globalCompositeOperation="multiply";
ctx.drawImage(img, 0, 0);
// Undo the clipping
ctx.restore();
}
// Specify the src to load the image
img.src = "http://i.imgur.com/gwlPu.jpg";
body {
background: #CEF;
}
<canvas id="c" width="200" height="158"></canvas>
This sounds like a known issue on Windows version of FireFox...
Only FF devs could have provided a real fix, but since the bug has been reported at least two years ago, I wouldn't expect it to be fixed anytime soon.
Now, to workaround the issue, you could draw in two steps:
on a first offscreen canvas do the clipping,
then do the blending on the main canvas using the now clipped one.
This may incur a little memory overhead, but having a second canvas ready might also come handy if you do a lot of compositing/blending.
// Grab the Canvas and Drawing Context
var canvas = document.getElementById('c');
var ctx = canvas.getContext('2d');
// create an off-screen copy of the context
var ctx2 = canvas.cloneNode().getContext('2d');
// Create an image element
var img = document.createElement('IMG');
// When the image is loaded, draw it
img.onload = function () {
// Do the clipping on the offscreen canvas
ctx2.save();
// Create a shape, of some sort
ctx2.beginPath();
ctx2.moveTo(10, 10);
ctx2.lineTo(100, 30);
ctx2.lineTo(180, 10);
ctx2.lineTo(200, 60);
ctx2.arcTo(180, 70, 120, 0, 10);
ctx2.lineTo(200, 180);
ctx2.lineTo(100, 150);
ctx2.lineTo(70, 180);
ctx2.lineTo(20, 130);
ctx2.lineTo(50, 70);
ctx2.closePath();
ctx2.clip();
// draw the image
ctx2.drawImage(img, 0, 0);
ctx2.restore();
// Now go back on the main canvas
ctx.fillStyle = "red";
ctx.fillRect(0,0,100,100);
// do the blending with our now clipped image
ctx.globalCompositeOperation="multiply";
ctx.drawImage(ctx2.canvas,0,0);
}
// Specify the src to load the image
img.src = "http://i.imgur.com/gwlPu.jpg";
body {
background: #CEF;
}
<canvas id="c" width="200" height="158"></canvas>
But for this exact case, i.e multiply blending, you can actually simply get rid of the clipping altogether on a single canvas.
Indeed, if I'm not mistaken, multiply doesn't really cares of which layer is top or bottom, the result will just be the same.
So you could simply do your compositing first, and as a final step do the blending over the composited image.
// Grab the Canvas and Drawing Context
var canvas = document.getElementById('c');
var ctx = canvas.getContext('2d');
// Create an image element
var img = document.createElement('IMG');
// When the image is loaded, draw it
img.onload = function () {
ctx.fillStyle="red";
// Create a shape, of some sort
ctx.beginPath();
ctx.moveTo(10, 10);
ctx.lineTo(100, 30);
ctx.lineTo(180, 10);
ctx.lineTo(200, 60);
ctx.arcTo(180, 70, 120, 0, 10);
ctx.lineTo(200, 180);
ctx.lineTo(100, 150);
ctx.lineTo(70, 180);
ctx.lineTo(20, 130);
ctx.lineTo(50, 70);
ctx.closePath();
// fill the current path
ctx.fill();
// draw only where our previous shape was drawn
ctx.globalCompositeOperation="source-atop";
ctx.drawImage(img, 0, 0);
// multiply blending should work the same in two directions
ctx.globalCompositeOperation="multiply";
ctx.fillRect(0,0,100,100);
}
// Specify the src to load the image
img.src = "http://i.imgur.com/gwlPu.jpg";
body {
background: #CEF;
}
<canvas id="c" width="200" height="158"></canvas>
And of course, you could very well get rid of clipping thanks to compositing and use a second offscreen canvas. (Probably my personal favorite).
I'm working on a canvas element, and I thought I'd add some simple graphics elements, but for some reason they are grinding the fps to a halt. Without them it's 60fps, with them it slows down to 3-4 fps within a minute of it running:
ctx.rect(0, 0, cnv.width, cnv.height);
ctx.fillStyle = ctx.createPattern(albImg[8], "repeat");
ctx.fill();
ctx.lineWidth="1";
ctx.strokeStyle="#5d92de";
ctx.rect(173.5,638.5,623,98);
ctx.stroke();
What am I doing wrong?
Animation slows with each new animation loop
#DanielBengtsson, Yes, as you've discovered, use strokeRect.
Alternatively, you can add ctx.beginPath before ctx.rect. What's happening is that all previous rects are being redrawn since the last beginPath so you are really drawing hundreds of rects as you animate.
// alternative with beginPath so previous rects will not redraw and
// cause slowdowns.
ctx.lineWidth="1";
ctx.strokeStyle="#5d92de";
ctx.beginPath();
ctx.rect(173.5,638.5,623,98);
ctx.stroke();
Repeating Background Pattern -- wait for the image to fully load before trying to use it
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var cw = canvas.width;
var ch = canvas.height;
var img = new Image();
img.onload = start;
img.src = "https://dl.dropboxusercontent.com/u/139992952/multple/emoticon1.png";
function start() {
ctx.fillStyle = ctx.createPattern(img, "repeat");
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
#canvas{border:1px solid red; margin:0 auto; }
<canvas id="canvas" width=300 height=300></canvas>
Say I have created a polygon-shaped image by creating a shape in HTML5 canvas and then filling it with an image, e.g. as below:
Now I want to round the corners on this hexagon.
There is a lineJoin = "round" property available but this doesn't seem to work (I believe because the shape is filled and there is no outer line to round).
Does anyone have any idea how to do this with HTML5 canvas or any other means?
Here is the code used to create the image:
var ctx = document.getElementById('myCanvas').getContext('2d');
var a = ctx.canvas.width, r = a / 5;
var side = Math.sqrt((4/3) * r * r);
// Draw your image onto the canvas (here I'll just fill the
// surface with red
var img = new Image();
img.src = "https://upload.wikimedia.org/wikipedia/commons/0/03/Mountain_Bluebird.jpg";
img.onload = function () {
var pattern = ctx.createPattern(img, "no-repeat");
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, a, a);
// Switch the blending mode
ctx.globalCompositeOperation = 'destination-in';
// Draw the hexagon shape to mask the image
ctx.beginPath();
ctx.moveTo(0, side/2);
ctx.lineTo(0, 3*side/2);
ctx.lineTo(r, 2*side);
ctx.lineTo(2*r, 3*side/2);
ctx.lineTo(2*r, side/2);
ctx.lineTo(r, 0);
ctx.closePath();
ctx.fill();
};
<canvas width="1000" height="1000" id="myCanvas"></canvas>
Just change the order you draw in and then change globalCompositeOperation to 'source-in'.
I made some adjustments because some of the corners in your code were getting clipped off but didn't adjust the image position (I hope that's easy enough to do)
Preview
You need to adjust the image position by the way like I said
Snippet
var ctx = document.getElementById('myCanvas').getContext('2d');
var a = ctx.canvas.width, r = a / 5;
var side = Math.sqrt((4/3) * r * r) - 20;
// Draw your image onto the canvas (here I'll just fill the
// surface with red
var img = new Image();
img.src = "https://upload.wikimedia.org/wikipedia/commons/0/03/Mountain_Bluebird.jpg";
img.onload = function () {
ctx.lineJoin = "round";
ctx.lineWidth = 50;
// Draw the hexagon shape to mask the image
ctx.beginPath();
ctx.moveTo(50, 50 + side/2);
ctx.lineTo(50, 50 + 3*side/2);
ctx.lineTo(50 + r, 50 + 2*side);
ctx.lineTo(50 + 2*r, 50 + 3*side/2);
ctx.lineTo(50 + 2*r, 50 + side/2);
ctx.lineTo(50 + r, 50);
ctx.closePath();
ctx.stroke();
ctx.fill();
// Switch the blending mode
ctx.globalCompositeOperation = 'source-in';
var pattern = ctx.createPattern(img, "no-repeat");
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, a, a);
};
<canvas width="1000" height="1000" id="myCanvas"></canvas>
There is no automatic way of doing this. The canvas API is pretty low-level, so it doesn't know that you've drawn a shape and want to detect where all the edges are. What you can do, but it would be a bit of a hassle to do is use bezierCurveTo. This method takes size arguments and can create curves. Combine that with your lines, and you can create rounded corners.
bezierCurveTo
You could also add circles at all the corners, with the arc method.
I'm playing with canvas in HTML5 and Javascript and I have a problem:
I'd like to apply transformations used on the current image to multiple images.
What I did:
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var img = new Image();
img.onload = function() {
//transformation stuff like:
canvas.height = img.height;
canvas.width = img.width;
ctx.drawImage(img, -img.width / 2, -img.height / 2, img.width, img.height);
ctx.beginPath();
ctx.lineTo(42, 42);
ctx.stroke();
ctx.lineTo(42, 24);
ctx.stroke();
...
ctx.rotate(Math.PI / 2);
...
};
img.src = //base64Img;
So I will apply a lot of transformations like draw some lines, crop, zoomIn etc...
How can I apply this to multiple files (more than 200) once (when these transformations are done) ?
Obviously, it will be done in multiples functions like a function to rotate, to draw a line etc.
Thank you for your help.
Put your transformations, path drawings & image drawing into a function with arguments that tell the function how each image will be treated:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var img=new Image();
img.onload=start;
img.src="https://dl.dropboxusercontent.com/u/139992952/stackoverflow/house32x32transparent.png";
function start(){
// Note: img coordinates are [centerX,centerY] rather than the usual [left,top]
drawTransformedImage(img,25,50,0,.75);
drawTransformedImage(img,75,50,Math.PI*1/6,1);
drawTransformedImage(img,150,50,Math.PI*2/6,2);
drawTransformedImage(img,225,50,Math.PI*3/6,1);
drawTransformedImage(img,275,50,Math.PI*4/6,.5);
}
function drawTransformedImage(img,cx,cy,radAngle,scale){
// save incoming styling
var lw=ctx.lineWidth;
var ss=ctx.strokeStyle;
// cache often used half-sizes
var iwHalf=img.width/2;
var ihHalf=img.height/2;
ctx.lineWidth=2;
// do the specified transformations
ctx.translate(cx,cy);
ctx.rotate(radAngle);
ctx.scale(scale,scale);
// draw the image
ctx.drawImage(img,-iwHalf,-ihHalf);
// stroke some paths
ctx.beginPath();
ctx.moveTo(-iwHalf,ihHalf);
ctx.lineTo(-iwHalf,-ihHalf);
ctx.strokeStyle='orange';
ctx.stroke();
ctx.beginPath();
ctx.moveTo(-iwHalf,-ihHalf);
ctx.lineTo(+iwHalf,-ihHalf);
ctx.strokeStyle='blue';
ctx.stroke();
// clean up: reset transformations and stylings
ctx.setTransform(1,0,0,1,0,0);
ctx.lineWidth=lw;
ctx.strokeStyle=ss;
}
body{ background-color: white; }
#canvas{border:1px solid red;}
<canvas id="canvas" width=300 height=150></canvas>
Transforming an Image
Your example does not show a image being transformed, making your question unclear.
The transform is independent of the image, it is used to transform pixel coordinates drawn onto the canvas. It does not affect the image. You can set the transform and then draw the 200 images and they will all have the same transformation applied when their content is rendered to the canvas.
Code example
To transform the image you must create a canvas, set the transform, then render the image onto that canvas. The canvas is now the transformed image.
An example of transforming an image.
var mirrorImage = function (image, vertical, horizontal) {
var imageResult, ctx, vF, hF, posX, posY;
// create new canvas
imageResult = document.createElement("canvas");
// set the pixels size to match the image
imageResult.width = image.width;
imageResult.height = image.height;
// create a drawable surface
ctx = imageResult.getContext("2d");
// create the mirror transformation
hF = horizontal ? -1, 0;
vF = vertical ? -1 : 0;
posX = horizontal ? image.width, 0;
posY = vertical ? image.height : 0;
// Apply the transform to the new image
ctx.setTransform(hF, 0, 0, vF, posX, posY);
// transform the original image by drawing it onto the new
ctx.drawImage(image, 0, 0);
// return the new image.
return imageResult;
}
// create image
var img = new Image();
img.src = "ship.png";
// when loaded transform the image
img.onload = function () {
img = mirrorImage(img, true, true);
// the image has been transformed.
}
To do that to 200 images you have to call mirrorImage (or what ever you are doing) for each image.
Say I have a canvas that's a rectangle. I want to take that canvas and split it diagonally and be able to manipulate those pieces to do whatever I want.
My end goal is to have a rectangle split diagonally and the 2 pieces animate off screen in opposite directions. I was thinking of doing this entirely within the canvas in an animation loop or converting each segment into an image element and using CSS3 animations to move those pieces. I'm just trying to figure out the best way to do this.
The code, codepen link, and image below are just to illustrate where I want my canvas to be split. You'll see it's not an exact split with equal sides.
http://codepen.io/FranciscoG/pen/YPjzbQ
<div id="container">
<img id="dog" src="http://i.imgur.com/1GUzYh9.jpg" width="375"
height="667">
</div>
<script>
var container = document.getElementById('container');
var dogImg = document.getElementById('dog');
function convertImageToCanvas(image) {
var canvas = document.createElement("canvas");
canvas.width = image.width;
canvas.height = image.height;
canvas.getContext("2d").drawImage(image, 0, 0);
return canvas;
}
function drawLine(canvas) {
var context = canvas.getContext('2d');
context.beginPath();
context.moveTo(0,0);
context.lineTo(345, 0);
context.lineTo(0, 567);
context.lineTo(0,0);
context.stroke();
context.closePath();
return canvas;
};
var newDog = convertImageToCanvas(dogImg);
var divided = drawLine(newDog);
container.innerHTML = "";
container.appendChild(divided)
</script>
You could always use clipping but note that this would involve save/restore calls which is a relative slow business. There has been suggested to get resetClip() included in the specs but it seem to be hard to get the message across to the group why it is needed.
In any case, I would recommend the following approach:
Create an object or function that can reproduce a single mask (path) of one of the half-side of the image (ie. the diagonal line with containing box).
For left half: Draw image, set composite mode to destination-in, draw mask, extract canvas as image
For right half: Draw image, set composite mode to destination-out, draw mask, extract canvas as image
Now put the to images (just use the canvas elements directly) inside a container so that they are stacked on top of each other.
Animate using a transition or animation class.
var img = new Image(375, 667);
img.onload = setup;
img.src = "http://i.imgur.com/1GUzYh9.jpg";
function setup() {
var path = [0,0, 345,0, 0, 567]; // last point not needed..
var left = createCanvas(this, path, "destination-in");
var right = createCanvas(this, path, "destination-out");
var cont = document.getElementById("cont");
cont.appendChild(left);
cont.appendChild(right);
// animate here by setting animation/transition class or using JS:
var x = 0;
(function loop() {
left.style.left = x + "px"; // translate is smoother, but need
right.style.left = -x + "px"; // prefix in some browser. Update as needed..
x-=5; if (x < -400) x = 0;
requestAnimationFrame(loop);
})();
function createCanvas(img, path, mode) {
var canvas = document.createElement("canvas"),
ctx = canvas.getContext("2d");
canvas.width = img.width;
canvas.height = img.height;
// draw image
ctx.drawImage(img, 0, 0);
// create mask
ctx.moveTo(path[0], path[1]);
for(var i = 2; i < path.length; i += 2) ctx.lineTo(path[i], path[i+1]);
ctx.closePath();
// composite mode and create half
ctx.globalCompositeOperation = mode;
ctx.fill();
return canvas
}
}
#cont {
position:relative;width:375px;height:667px;overflow:hidden;
}
#cont>canvas {position:absolute;left:0;right:0;}
<div id="cont"></div>
You can use context.clip to achieive your image-splitting effect
context.clip restricts an image to being drawn withing a specified path.
You can define several of these clipping regions to divide your image into several pieces (paths).
Then in an animation loop, you can clear the canvas and redraw each of these clipping regions with an changing offset. The left clipping region will move (offset) leftward in each loop. The right clipping region will move (offset) rightward in each loop.
Here's example code and a Demo:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
var nextTime=0;
var duration=1000/60*3;
var offset=0;
var paths=[];
var img=new Image();
img.onload=start;
img.src="https://dl.dropboxusercontent.com/u/139992952/multple/Dog-With-Cute-Cat.jpg";
function start(){
cw=canvas.width=img.width;
ch=canvas.height=img.height;
paths.push({path:[{x:0,y:0},{x:150,y:0},{x:0,y:ch}],direction:-1});
paths.push({path:[{x:150,y:0},{x:0,y:ch},{x:cw,y:ch},{x:cw,y:0}],direction:1});
requestAnimationFrame(animate);
}
function draw(){
ctx.clearRect(0,0,cw,ch);
for(var i=0;i<paths.length;i++){
var path=paths[i].path;
var offX=offset*paths[i].direction;
ctx.save();
ctx.beginPath();
var pt=path[0];
ctx.moveTo(pt.x+offX,pt.y);
for(var j=1;j<path.length;j++){
var pt=path[j];
ctx.lineTo(pt.x+offX,pt.y);
}
ctx.closePath();
ctx.stroke();
ctx.clip();
ctx.drawImage(img,offX,0);
ctx.restore();
}
}
function animate(time){
if(offset<cw){requestAnimationFrame(animate);}else{log('done');}
if(time<nextTime){return;}
nextTime=time+duration;
draw();
offset++;
}
body{ background-color: ivory; padding:10px; }
#canvas{border:1px solid red;}
<canvas id="canvas" width=300 height=300></canvas>