I'm attempting to flip my canvas horizontally as I follow along with this awesome mario bros tutorial. As I've also seen in many other answers about flipping a canvas, I added these lines updating the context:
define(name, x, y, width, height) {
const { image, } = this;
const buffer = document.createElement('canvas');
buffer.width = width;
buffer.height = height;
const context = buffer.getContext('2d');
context.scale(-1, 1); // <<< THIS ONE
context.translate(width, 0); // <<< AND THIS ONE
context.drawImage(
image,
x,
y,
width,
height, // what part of the image to draw
0, 0, width, height); // where to draw it
this.tiles.set(name, buffer); // store the info about the tile in our map
}
The code, previously, worked awesomely. But when I add these lines and refresh my browser, the entire canvas is gone! I can't imagine things have changed so much in the past 2 1/2 years since the video was made as to introduce a breaking change here?!?! (I would imagine it will have not changed at all!)
What's wrong?
I use this for doing what you want:
function horizontalFlip(img,x,y){
/* Move to x + image width */
context.translate(x+img.width, y);
/* scaleX by -1; this causes horizontal flip */
context.scale(-1,1);
/* Draw the image
No need for x,y since we've already translated */
context.drawImage(img,0,0);
/* Clean up - reset transformations to default */
context.setTransform(1,0,0,1,0,0);
}
This function works as expected and here a variation for sprites.
function flipSpriteHorizontal(img, x, y, spriteX, spriteY, spriteW, spriteH){
/* Move to x + image width
adding img.width is necessary because we're flipping from
the right side of the image so after flipping it's still at [x,y] */
context.translate(x + spriteW, y);
/* ScaleX by -1, this performs a horizontal flip */
context.scale(-1, 1);
/* Draw the image
No need for x,y since we've already translated */
context.drawImage(img,
spriteX, spriteY, spriteW, spriteH,
0, 0, spriteW, spriteH
);
/* Clean up - reset transformations to default */
context.setTransform(1, 0, 0, 1, 0, 0);
}
Related
I have no idea why but the image that I read from canvas gets flipped on y axis.
The ultimate goal is the read a portion of WebGL canvas and extract it as JPG/PNG.
Workflow is the following:
gl.readPixels
create 2D canvas
load Uint8Array pixels to 2D canvas as imageData
get 2D canvas as blob
create object URL
use it as image src
Here's my code: https://jsitor.com/acM-2WTzd
I'm really sorry about the length (almost 300) but it's WebGL, there's so much boilerplate and setup.
I've tried to debug it for several hours and I have no idea (granted it could be the shader, I'm quite new at that).
If you have any additional question, please feel free to ask!
Unlike context.getImageData(), gl.readPixels() reads pixel data starting from the bottom-left corner, not from the top-left corner. You can apply a transformation on tempCanvas and draw it onto itself after putting the image data like this:
context.putImageData(imageData, 0, 0);
// add the following
context.translate(0, cropHeight);
context.scale(1, -1);
context.drawImage(tempCanvas, 0, 0);
Alternatively, you can manually rearrange the pixel data before returning it from your getPixels() function:
function getPixels(x, y, width, height) {
const length = width * height * 4;
const row = width * 4;
const end = (height - 1) * row;
const arr = new Uint8Array(length);
const pixels = new Uint8Array(length);
if (draw) draw();
gl.readPixels(x, y, width, height, gl.RGBA, gl.UNSIGNED_BYTE, arr);
for (let i = 0; i < length; i += row) {
pixels.set(arr.subarray(i, i + row), end - i);
}
return pixels;
}
I'm trying to build an app where user can add various objects (rectangles, circles) and he can use mouse wheel to zoom-in and zoom-out.
For this zooming I set up event handler like this:
TheCanvas.on('mouse:wheel', function(options){
var p = new fabric.Point(
options.e.clientX,
options.e.clientY
);
var direction = (options.e.deltaY > 0) ? 0.9 : 1.1;
var newZoom = TheCanvas.getZoom() * direction;
// restrict too big/small zoom here:
if ((newZoom > 50) || (newZoom < 0.7)) return false;
TheCanvas.zoomToPoint( p, newZoom );
}
Everything worked fine until now. Now I want to draw a crosshair over all objects on the canvas. Something like this:
So I made my own custom object like:
CrossHairClass = fabric.util.createClass(fabric.Object, {
strokeDashArray: [1,2], // I want lines to be dashed
........
My problem is:
When user zooms with the mouse wheel, my cross-hair lines zoom their thickness too and also small dashes get bigger. But I don't want that. I want my cross-hair lines be a "hair" lines = ideally 1 pixel thick all the time regardless zoom factor of the canvas. And fine dashed line too.
Render function of my Class:
_render: function (ctx) {
// I tried it like this
var zoom = TheCanvas.getZoom();
var scale = (1/zoom) * 3.333; // with this scale it visually looked the best
// I have to scale it in X and Y while I want small dashes to stay small and also thickness of the line to stay "hair-line"
this.scaleX = this.scaleY = scale;
this.width = CROSSHAIR_SIZE / scale; // my constant from elsewhere
ctx.lineWidth = 1;
ctx.beginPath();
// this example is for horizontal line only
ctx.moveTo(-this.width / 2, 0);
ctx.lineTo(this.width / 2, 0);
this._renderStroke(ctx);
}
I tried various combinations of multiplying or dividing by scale factor or zoom factor but if I finally had lines thin, I couldn't keep their size, which must be constant (in pixels) regardless of canvas zoom. Please help.
P.S.: now I got an idea. Maybe I should create another canvas, over my current canvas and draw this crosshair on the upper canvas, which will not zoom?
EDIT 1
Based on the answer from #andreabogazzi I tried various approaches, but this finally worked out! Thanks! :)
_render: function (ctx) {
var zoom = TheCanvas.getZoom();
// ctx.save(); // this made no difference
// ctx.setTransform(1/zoom, 0, 0, 1/zoom, 0, 0); // this didn't work
this.setTransformMatrix([1/zoom, 0, 0, 1/zoom, 0, 0]);
ctx.strokStyle = 'red';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(-this.widthHalf, 0); // widthHalf computed elsewhere
ctx.lineTo(this.widthHalf, 0);
this._renderStroke(ctx); // I use this instead of ctx.stroke() while this ensures my line is still nicely dashed
// ctx.restore(); // this made no difference
}
Since you created a custom class, you have to invert the zoom of your canvas before drawing.
On the _render function of your subclass, since you should be positioned in the center of your crosshair, apply a transform matrix of scale type, with scale factor of 1/zoomLevel and everything should work.
I would say the correct way is:
_render: function (ctx) {
var zoom = TheCanvas.getZoom();
ctx.save(); // this is done anyway but if you add custom ctx transform is good practice to wrap it in a save/restore couple
ctx.transform(1/zoom, 0, 0, 1/zoom, 0, 0);
ctx.strokStyle = 'red';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(-this.widthHalf, 0); // widthHalf computed elsewhere
ctx.lineTo(this.widthHalf, 0);
this._renderStroke(ctx); // I use this instead of ctx.stroke() while this ensures my line is still nicely dashed
ctx.restore(); // this is done anyway but if you add custom ctx transform is good practice to wrap it in a save/restore couple
}
Now it happens that this object get cached from the fabricJS cache system that will probably create the cache depending on the canvas zoom too.
I have no understanding of the final use of this object, but you should include this calculation also in the cache canvas size calculation.
I have an array like this, the idea is to output images in different directions and flip them vertically and horizonatly on a canvas by scaling.
[{
"pos":{
"x":411,
"y":401.5
},
"scale":{
"x":1,
"y":1
}
},{
"pos":{
"x":411,
"y":271.59625
},
"scale":{
"x":-1,
"y":1
}
}]
The problem is that I'm scaling the canvas instead of the images, the canvas is multiple times bigger than the images i'm placing on it.
images.forEach((image) => {
// center borde köars innan loopen egentligen
let pos = center(image.pos)
cc.save()
cc.scale(image.scale.x, image.scale.y)
cc.drawImage(window.video, pos.x, pos.y)
cc.restore()
})
How do I scale the image, called window.video, instead of the entire canvas?
To render a scaled image on the canvas.
function drawImage(image,x,y,scaleX,scaleY){
ctx.setTransform(scaleX, 0, 0, scaleY, x, y); // set scale and translation
ctx.drawImage(image,0,0);
}
// when done drawing images you need to reset the transformation back to default
ctx.setTransform(1,0,0,1,0,0); // set default transform
If you want the image flipped but still drawn at the same top left position use
function drawImage(image, x, y, scaleX, scaleY){
ctx.setTransform(scaleX, 0, 0, scaleY, x, y); // set scale and translation
x = scaleX < 0 ? -image.width : 0; // move drawing position if flipped
y = scaleY < 0 ? -image.height : 0;
ctx.drawImage(image, x, y);
}
And to draw about the center of the image
function drawImage(image, x, y, scaleX, scaleY){ // x y define center
ctx.setTransform(scaleX, 0, 0, scaleY, x, y); // set scale and translation
ctx.drawImage(image, -image.width / 2, -image.height / 2);
}
EDIT: As noted below, this doesn't work with negative values...
drawImage can take 2 extra arguments for sizing the image, so the following should work:
images.forEach((image) => {
// center borde köars innan loopen
let pos = center(image.pos)
cc.save()
cc.drawImage(window.video, pos.x, pos.y, window.video.width * image.scale.x, window.video.height * image.scale.y)
cc.restore()
})
Documentation: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Using_images
I`m new with canvas so thanks for your patience.
I wrote an engine that is creating 2 different layers in 2 canvas elements which are one over another. They contain some generated pictures, which aren`t important here.
I'm trying to create an effect which will display bottom layer when I move mouse over the top layer and click.
Something like this:
This is what I have tried so far:
To use transparency on canvas element and display bottom canvas (fast but not usable)
Re-create a clipping region.
Whenever I press the mouse I store current coordinates and re-render the canvas with updated clipping region
Updating clipping region is slow if I use stroke to create shadows + I`m not sure how to remove lines from it (see picture).
If I remove shadow effect, it works really fast, but I need to have it.
The only thing that comes on my mind how to speed this, is to save coordinates of every click, and then to re-calculate that into 1 shape and drop a shadow on it - I`ll still have lines, but it will be faster because there won`t be thousand of circles to draw...
Any help will be most appreciated!
You can take advantage of the browser's built in interpolation by using it as a pseudo low-pass filter, but first by painting it black:
Copy the top layer to the bottom layer
Set source-in comp. mode
Draw all black
Set source-in comp. mode
Scale down image to 25%
Scale the 25% region back up to 50% of original (or double of current)
Scale the now 50% region back up to 100% of original. It will be blurred.
Depending on how much blur you want you can add additional steps. That being said: blurred shadow is an intensive operation no matter how it is twisted and turned. One can make compromise to only render the shadow on mouse up for example (as in the demo below).
Example
Example using two layers. Top layer let you draw anything, bottom will show shadow version at the bottom later while drawing.
var ctx = document.getElementById("top").getContext("2d"),
bctx = document.getElementById("bottom").getContext("2d"),
bg = new Image(),
isDown = false;
bg.src = "http://i.imgur.com/R2naCpK.png";
ctx.fillStyle = "#27f";
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.globalCompositeOperation = "destination-out"; // "eraser"
ctx.canvas.onmousedown = function(e) {isDown = true};
window.onmousemove = function(e) {
if (!isDown) return;
var pos = getPos(ctx.canvas, e);
ctx.beginPath();
ctx.moveTo(pos.x + 10, pos.y);
ctx.arc(pos.x, pos.y, 10, 0, 2*Math.PI); // erase while drawing
ctx.fill();
};
window.onmouseup = function(e) {
if (isDown) {
isDown = false;
makeShadow();
}
};
function makeShadow(){
var w = bctx.canvas.width,
h = bctx.canvas.height,
offset = 7,
alpha = 0.75;
// reset alpha
bctx.globalAlpha = 1;
// normal comp mode to clear as it is faster than using "copy"
bctx.globalCompositeOperation = "source-over";
bctx.clearRect(0, 0, w, h);
// copy top-layer to bottom-layer
bctx.drawImage(ctx.canvas, 0, 0);
// comp. mode will only draw in to non-alpha pixels next
bctx.globalCompositeOperation = "source-in";
// black overlay
bctx.fillRect(0, 0, w, h);
// copy mode so we don't need an extra canvas
bctx.globalCompositeOperation = "copy";
// step 1: reduce to 50% (quality related - create more steps to increase blur/quality)
bctx.drawImage(bctx.canvas, 0, 0, w, h, 0, 0, w * 0.5, h * 0.5);
bctx.drawImage(bctx.canvas, 0, 0, w * 0.5, h * 0.5, 0, 0, w * 0.25, h * 0.25);
bctx.drawImage(bctx.canvas, 0, 0, w * 0.25, h * 0.25, 0, 0, w * 0.5, h * 0.5);
// shadow transparency
bctx.globalAlpha = alpha;
// step 2: draw back up to 100%, draw offset
bctx.drawImage(bctx.canvas, 0, 0, w * 0.5, h * 0.5, offset, offset, w, h);
// comp in background image
bctx.globalCompositeOperation = "destination-over";
bctx.drawImage(bg, 0, 0, w, h);
}
function getPos(canvas, e) {
var r = canvas.getBoundingClientRect();
return {x: e.clientX - r.left, y: e.clientY - r.top};
}
div {position:relative;border:1px solid #000;width:500px;height:500px}
canvas {position:absolute;left:0;top:0}
#bottom {background:#eee}
<div>
<canvas id="bottom" width=500 height=500></canvas>
<canvas id="top" width=500 height=500></canvas>
</div>
hey guys i have a function that strokes a line based on the angle received from the user and also moves an image using some basic maths. the only problem is i am unable to rotate image based on that angle as if i put this inside animation frame loop it doesn't work .please help
function move()
{
var theCanvas=document.getElementById("canvas1");
var context=theCanvas.getContext("2d");
context.clearRect(0, 0, theCanvas.width, theCanvas.height );
// context.fillStyle = "#EEEEEE";
//context.fillRect(0, 0,theCanvas.width, theCanvas.height );
context.beginPath();
context.setLineDash([3,2]);
context.lineWidth=10;
context.strokeStyle="black";
context.moveTo(x1,y1);
context.lineTo(x2,y2);
context.stroke();
context.drawImage(srcImg,x1+x_fact,y1-100+y_fact);
x_fact=x_fact+0.5;
y_fact=m*x_fact+c;
requestAnimationFrame(move);
}
move();
now please suggest me a way to rotate the image on the angle input only once so it can face according to the path and move in it.
thank you in advance.
If you want to rotate the image by its corner use something like this:
... your other code here ...
var x = x1+x_fact, // for simplicity
y = y1-100+y_fact;
context.save(); // save state
context.translate(x, y); // translate to origin of rotation
context.rotate(angle); // rotate, provide angle in radians
context.drawImage(srcImg, 0, 0); // as we are translated we draw at [0,0]
context.restore(); // restore state removing transforms
If you want to rotate it by center simply translate, rotate and then translate back 50% of the image's width and height.
save/restore are relative expensive operations - you can simply reset transformation all together after each draw if you don't need transformations elsewhere:
context.setTransform(1, 0, 0, 1, 0, 0); // instead of restore(), remove save()