How to draw lines unaffected by scaling and zooming - javascript

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.

Related

Can I optimize this canvas draw call that takes up 30% of my memory profiling?

My game has many Laser objects. mx & my represent velocity. I use the following code to draw a line from behind the Laser 2 pixels to ahead of the Laser in the direction it's going 2 pixels.
Removing the first line of the function adjusted the % of the Profiling by ~1% but I don't like the way it looks. I think I could optimize the drawing by sorting by Linewidth but that doesn't appear to get me much.
How else could I optimize this?
Laser.prototype.draw = function(client, context) {
context.lineWidth = Laser.lineWidth;
context.beginPath();
context.moveTo(this.x - this.mx * 2, this.y - this.my * 2);
context.lineTo(this.x + this.mx * 2, this.y + this.my * 2);
context.strokeStyle = this.teamColor;
context.closePath();
context.stroke();
}
Instead of multiplying things by two, why not add them?
E.g.
context.moveTo(this.x - this.mx - this.mx, this.y - this.my - this.my);
context.lineTo(this.x + this.mx + this.mx, this.y + this.my - this.my);
Testing shows that addition is an order of magnitude faster on an imac over multiplication
https://jsfiddle.net/1c85r2pq/
Dont use moveTo or lineTo as they do not use the hardware to render and are very slow. Also your code is drawing the line twice
ctx.beginPath(); // starts a new path
ctx.moveTo(x,y); // sets the start point of a line
ctx.lineTo(xx,yy); // add a line from x,y to xx,yy
// Not needed
ctx.closePath(); // This is not like beginPath
// it is like lineTo and tells the context
// to add a line from the last point xx,yy
// back to the last moveTo which is x,y
This would half the already slow render time.
A quick way to draw lines using bitmaps.
First at the start create an image to hold the bitmap used to draw the line
function createLineSprite(col,width){
var lineSprite = document.createElement("canvas");
var lineSprite.width = 2;
var lineSprite.height = width;
lineSprite.ctx = lineSprite.getContext("2d");
lineSprite.ctx.fillStyle = col;
lineSprite.ctx.fillRect(0,0,2,width);
return lineSprite;
}
var line = createLineSprite("red",4); // create a 4 pixel wide red line sprite
Or you can use an image that you load.
To draw a line you just need to create a transform that points in the direction of the line, and draw that sprite the length of the line.
// draw a line with sprite from x,y,xx,yy
var drawLineSprite = function(sprite,x,y,xx,yy){
var nx = xx-x; // get the vector between the points
var ny = yy-y;
if(nx === 0 && ny === 0){ // nothing to draw
return;
}
var d = Math.hypot(nx,ny); // get the distance. Note IE does not have hypot Edge does
// normalise the vector
nx /= d;
ny /= d;
ctx.setTransform(nx,ny,-ny,nx,x,y); // create the transform with x axis
// along the line and origin at line start x,y
ctx.drawImage(sprite, 0, 0, sprite.width, sprite.height, 0, -sprite.height / 2, d, sprite.height);
}
To draw the line
drawSpriteLine(line,0,0,100,100);
When you are done drawing all the lines you can get the default transform back with
ctx.setTransform(1,0,0,1,0,0);
The sprite can be anything, this allows for very detailed lines and great for game lasers and the like.
If you have many different colours to draw then create one sprite (image) that has many colour on it, then in the line draw function simply draw only the part of the sprite that has the colour you want. You can stretch out a single pixel to any size so you can get many colours on a small bitmap.

In a 2D canvas, is there a way to give a sprite an outline?

I'd like to give a sprite an outline when the character gets healed/damaged/whatever but I can't think of a way to code this using the 2d canvas. If it were possible, I'd think it would be a global composite operation, but I can't think of a way to achieve it with one of them.
I did find this stackoverflow answer that recommends creating a fatter, solid color version of the original and put the original on top of it. That would give it an outline, but it seems like a lot of extra work especially considering I'm using placeholder art. Is there an easier way?
This question is different from the one linked because this is specifically about the HTML5 2D canvas. It may have a solution not available to the other question.
For what it's worth, I don't mind if the outline creates a wider border or keeps the sprite the same size, I just want the outline look.
Just draw your original image in 8 position around the original image
Change composite mode to source-in and fill with the outline color
Change composite mode back to source-over and draw in the original image at correct location
This will create a clean sharp outline with equal border thickness on every side. It is not so suited for thick outlines however. Image drawing is fast, especially when image is not scaled so performance is not an issues unless you need to draw a bunch (which in that case you would cache the drawings or use a sprite-sheet anyways).
Example:
var ctx = canvas.getContext('2d'),
img = new Image;
img.onload = draw;
img.src = "http://i.stack.imgur.com/UFBxY.png";
function draw() {
var dArr = [-1,-1, 0,-1, 1,-1, -1,0, 1,0, -1,1, 0,1, 1,1], // offset array
s = 2, // scale
i = 0, // iterator
x = 5, // final position
y = 5;
// draw images at offsets from the array scaled by s
for(; i < dArr.length; i += 2)
ctx.drawImage(img, x + dArr[i]*s, y + dArr[i+1]*s);
// fill with color
ctx.globalCompositeOperation = "source-in";
ctx.fillStyle = "red";
ctx.fillRect(0,0,canvas.width, canvas.height);
// draw original image in normal mode
ctx.globalCompositeOperation = "source-over";
ctx.drawImage(img, x, y);
}
<canvas id=canvas width=500 height=500></canvas>
Maybe it would be worth trying this :
• build a canvas 1.1 time bigger than the original sprite
• fill it with the outline color
• draw the sprite scaled by 1.1 on the canvas using destination-in globalCompositeOperation.
Then you have a bigger 'shadow' of your sprite in the outline color.
When you want to draw the outline :
• draw the 'shadow' (centered)
• draw your sprite within the shadow.
Depending on the convexity of your sprite, this will work more or less nicely, but i think it's worth trying since it avoids you doubling the number of input graphic files.
I just did a short try as proof-of-concept and it quite works :
http://jsbin.com/dogoroxelupo/1/edit?js,output
Before :
After :
html
<html>
<body>
<image src='http://www.gifwave.com/media/463554/cartoons-comics-video-games-sprites-scott-pilgrim-paul-robertson_200s.gif' id='spr'></image>
<canvas id='cv' width = 500 height= 500 ></canvas>
</body>
</html>
code
window.onload=function() {
var spr = document.getElementById('spr');
var margin = 4;
var gh = createGhost(spr, '#F80', margin);
var cv = document.getElementById('cv');
var ctx = cv.getContext('2d');
var outlined = true;
setInterval(function() {
ctx.clearRect(0,0,cv.width, cv.height);
if (outlined)
ctx.drawImage(gh, 0, 0)
ctx.drawImage(spr, 0, 0)
outlined = !outlined;
}, 400);
}
function createGhost (img, color, margin) {
var cv= document.createElement('canvas');
cv.width = img.width+2*margin;
cv.height = img.height + 2*margin;
var ctx = cv.getContext('2d');
ctx.fillStyle = color;
ctx.fillRect(0,0, cv.width, cv.height);
ctx.save();
ctx.globalCompositeOperation = 'destination-in';
var scale = cv.width/spr.width;
ctx.scale(cv.width/spr.width, cv.height/spr.height);
ctx.drawImage(img, -margin, -margin);
ctx.restore();
return cv;
}
You could use strokeRect method to outline the sprite after drawing it. It should be asy if you know your sprite's dimensions...

why is my strokeStyle transparent?

I am drawing onto an HTML5 canvas with stroke() and regardless of how or when I set globalAlpha, the stroke is being drawn with some measure of transparency. I'd like for the stroke to be completely opaque (globalAlpha=1). Is there somewhere else where the alpha is being set?
In this jsfiddle, I am drawing a grid of solid black lines onto a canvas. For me, the result shows dots at the intersections, confirming that the lines are partially transparent. Here's the gist of it:
context.globalAlpha=1;
context.strokeStyle="#000";
context.beginPath();
/* draw the grid */
context.stroke();
context.closePath;
The especially weird thing (to me) is that this problem was not occurring in my code before my last computer restart, so I'm guessing there was something hanging around in the cache that was keeping the alpha at my desired level.
I'm obviously missing something here... thanks for any help you can provide.
Real answer :
Each point in a canvas has its center in its (+0.5, +0.5) coordinate.
So to avoid artifacts, start by translating the context by (0.5, 0.5) ,
then round the coordinates.
css scaling creates artifact, deal only with canvas width and height, unless
you want to deal with hidpi devices with webGL, or render at a lower resolution
with both webGL and context2D.
-> in your case, your setup code would be (with NO css width/height set ) :
( http://jsfiddle.net/gamealchemist/x9bTX/8/ )
// parameters
var canvasHorizontalRatio = 0.9;
var canvasHeight = 300;
var hCenterCanvas = true;
// setup
var canvasWidth = Math.floor(window.innerWidth * canvasHorizontalRatio);
var cnv = document.getElementById("myCanvas");
cnv.width = canvasWidth;
cnv.height = canvasHeight;
if (hCenterCanvas)
cnv.style['margin-left'] = Math.floor((window.innerWidth - canvasWidth) * 0.5) + 'px';
var ctx = cnv.getContext("2d");
ctx.translate(0.5, 0.5);
gridContext();
The rest of the code is the same as your original code, i just changed the size of you squares to get quite the same visual aspect.
ctx.beginPath();
for (var i=60; i<canvasHeight; i+=60) {
ctx.moveTo(0,i);
ctx.lineTo(canvasWidth,i);
}
for (i=60; i<canvasWidth; i+=60) {
ctx.moveTo(i,0);
ctx.lineTo(i,canvasHeight);
}
ctx.strokeStyle="#000";
ctx.stroke();
ctx.closePath();
With those changes we go from :
to :
Edit : to ensure rounding, in fact i think most convenient is to inject the context and change moveTo, lineTo :
function gridContext() {
var oldMoveTo = CanvasRenderingContext2D.prototype.moveTo;
CanvasRenderingContext2D.prototype.moveTo = function (x,y) {
x |= 0; y |= 0;
oldMoveTo.call(this, x, y);
}
var oldLineTo = CanvasRenderingContext2D.prototype.lineTo;
CanvasRenderingContext2D.prototype.lineTo = function (x,y) {
x |= 0; y |= 0;
oldLineTo.call(this, x, y);
}
}
Obviously, you must do this for all drawing functions you need.
When drawing lines on a canvas, the line itself is exactly on the pixel grid. But because the line is one pixel wide, half of it appears in each of the pixels to either side of the grid, resulting in antialising and a line that is basically 50% transparent over two pixels.
Instead, offset your line by 0.5 pixels. This will cause it to appear exactly within the pixel.
Demo

animating a rotated image in canvas

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()

HTML5: Antialiasing leaves traces when I erase the image

I want to move a widget around on the canvas, and for various reasons I don't want to use sprites. I'm using the latest version of Chrome. In order to move the widget, I 'undraw' it and then redraw it in another place. By 'undraw', I mean that I just draw the same image in the same place, but draw it with the same color as the background, so the widget disappears completely before I draw the new one. The problem is that when I 'undraw', traces of the original image remain on the canvas. I've poked around on related questions here and haven't found anything that helps. I understand the problem of drawing a one-pixel line and getting anti-aliasing, so I set my line width to 2 (and various other non-integer values), but to no avail. Anyone have any ideas? Here's a fiddle demo, and here's the function that does the update:
function draw(){
if(previousX !== null) {
ctx.lineWidth = 1;
ctx.fillStyle = '#ffffff';
ctx.strokeStyle = '#ffffff';
drawCircle(previousX, previousY, 20);
}
ctx.lineWidth = 1;
ctx.fillStyle = '#000000';
ctx.strokeStyle = '#000000';
drawCircle(x, y, 20);
console.log('drew circle (' + x + ', ' + y + ')');
previousX = x;
previousY = y;
}
P.S. I'm just a hobbyist with no great experience in graphics, so please dumb-down your answer a bit if possible.
When your draw a shape with anti-aliasing, you are doing a solid covering of some pixels, but only a partial covering of the edge pixels. The trouble is that pixels (temporarily ignoring LCD panels) are indivisible units. So how do we partially cover pixels? We achieve this using the alpha channel.
The alpha channel (and alpha blending) combines the colour at the edge of a circle with the colour underneath it. This happens when the circle only partially covers the pixel. Here's a quick diagram to visualise this issue.
The mixing of colours causes a permanent change that is not undone by drawing the circle again in the background colour. The reason: colour mixing happens again, but that just causes the effect to soften.
In short, redrawing only covers up the pixels with total coverage. The edge pixels are not completely part of the circle, so you cannot cover up the edge effects.
If you need to erase the circle, rather think about it in terms of restoring what was originally there. You can probably copy the original content, then draw the circle, then when you want to move the circle, restore the original content and repeat the process.
This previous SO question may give you some ideas about copying canvas regions. It uses the drawImage method. The best solution would combine the getImageData and putImageData methods. I have modified your Fiddle example to show you how you might do this. You could try the following code:
var x, y, vx, vy;
var previousX = null, previousY = null;
var data = null;
function draw(){
ctx.lineWidth = 2.5;
ctx.fillStyle = '#000000';
ctx.strokeStyle = '#FF0000';
drawCircle(x, y, 20);
previousX = x;
previousY = y;
}
function drawCircle(x, y, r){
// Step 3: Replace the stuff that was underneath the previous circle
if (data != null)
{
ctx.putImageData(data, previousX - r-5, previousY - r-5);
}
// Step 1: Copy the region in which we intend to draw a circle
data = ctx.getImageData(x - r-5, y - r-5, 2 * r + 10, 2 * r + 10);
// Step 2: Draw the circle
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI*2, true);
ctx.closePath();
ctx.stroke();
ctx.fill();
}

Categories

Resources