Cannot draw thin line in canvas? - javascript

I followed a tutorial to create a canvas graph using js. The code plotting is this:
function plotData(context, dataSet, sections, xScale) {
context.lineWidth = 1;
context.outlineWidth = 0;
context.strokeWidth = 0;
context.beginPath();
context.moveTo(0, dataSet[0]);
for (i=0; i<sections; i++) {
context.lineTo(i * xScale, dataSet[i]);
}
context.stroke();
}
I am calling this function with an array holding the points where to go.. The x value is calculated per column (xScale) and the resulting graph if i use more than 1 source of data shows up fine. Screenshot when working fine:
http://s21.postimg.org/vlc1qg9iv/Screen_Shot_2016_04_08_at_15_48_42.png
But when i remove the 2 last data lines and leave only 1 line (so when the graph has a smaller difference between graph max and min values it shows up like this:
http://s16.postimg.org/ex0fakef9/Screen_Shot_2016_04_08_at_15_44_21.png
It is in this screenshot that you can clearly see, that while it should draw a line, the line is not really a 1px line but a shape, much like a (badly) distorted line?
I am not sure if i am doing something wrong or i am plainly ignoring something? The height of the canvas is fixed and it is always calculated using:
canvas = $('#canvas-container canvas')[0];
canvas.width = $('#canvas-container').width() * 0.9;
canvas.height = $('#canvas-container').width() / 1.45;
Thanks!
Codepen of the exact effect (from the exact tutorial) can be found here:
https://codepen.io/anon/pen/JXMwBy?editors=1111
(notice there are 2 more lines of graph data i commented out and in doing so i made the Val_max and Val_min vars different to "stretch" the data in the Y line)

You are stretching the Y axis on every operation after this line:
context.scale(1,-1 * yScale);
Instead, remove the line above and multiply the y values when you draw the line in plotData().
// multiply all Y values by -yScale to flip and scale
context.moveTo(0, dataSet[0] * -yScale);
for (i=1;i<sections;i++) {
context.lineTo(i * xScale, dataSet[i] * -yScale);
}

Related

Javascript Canvas issue: Why do my points on the canvas not correspond properly to the height of the graph?

I'm trying to make a line graph using the canvas that looks like a typical line graph and uses typical Cartesian coordinates like we learned in algebra;
starts with 0,0 at the bottom left, and the position x-axis is to be determined by the number of items to chart.
However, the position of the points doesn't match the input (although the shape of the graph is correct, indicating I'm doing something right). What am I doing wrong?
I've rewritten and tweaked the formula for converting numerous times
function newLineGraph(parent, width, height, dataArray) {
//this makes the element using my own code, no observable error here
var canvas = newCanvas(parent, width, height);
var canvasContext = canvas.getContext("2d");
var spaceBetweenEntries = width / dataArray.length;
var largestNumber = findHighestNumber(dataArray);
canvasContext.beginPath();
canvasContext.moveTo(0, 0);
var n = 0;
while (dataArray[n]) {
var x = spaceBetweenEntries * n;
var y = height - dataArray[n];
console.log("x,y", x, y);
canvasContext.lineTo(x, y);
n++;
}
canvasContext.stroke();
return canvas;
}
edit: fixed the image so you can see the canvas size
The resulting graph is much smaller than the intended graph; for example
newLineGraph("body",55,45,[1,40,10]);
produces a graph with a small ^ shape in the corner, rather than properly starting at the bottom. However, the console logs show " 0 44" "18.333333333333332 5","36.666666666666664 35" which I believe should produce a graph that fits the whole chart nicely.
The first lineTo will always have x as 0 so I assume the first line isn't drawing like you intended. It is more like a |/\ shape instead of \/\.
Set x like this:
var x = spaceBetweenEntries * (n + 1);
Edit
As you can see in this fiddle your chart renders at the right points with the coordinates you posted. I implemented the newCanvas function like I expect it to behave. So are we missing some other code that modifies the canvas width and height?
function newCanvas(parent, width, height) {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
document.querySelector(parent).appendChild(canvas);
return canvas;
}
The problem was using style.width and style.height to modify the canvas height, instead of canvas.height and canvas.width

What is an alternative method for drawing dashed lines on an HTML5 Canvas?

Question Background:
I am working on a site that, along with other data, draws vertical and horizontal lines on an HTML canvas. That page can be converted to a PDF file when the user wants to download the report. Originally we were using one default style line to draw the graph values. Recently we added a second type of data on the graph and used context.setLineDash([x,x]) to draw dashed lines for the second data type. This works great in browsers. However, when the PDF converter software tries to convert a report with dashed lines, the dashed lines do not show up in the resulting PDF.
After some troubleshooting, I narrowed the problem down to the setLineDash() property. It appears our converter sofware can understand normal style lines but does not understand the setLineDash() property. The converter software is several years old and I have been informed that an updated version of the converter will not be bought. I also discovered that there is no support for our version from the creator.
Question: Since I am unable to update our HTML to PDF converter software or get support for it directly, can anyone provide an example of an alternative way to draw a dashed line on a canvas without using setLineDash()?
EDIT
#K3N,
As per the instructions on the notification I received when you marked this question a duplicate of this other question, I am editing to explain how it is different.
I believe that though the answers to both questions will likely be similar, my question is not a duplicate of the question you indicated. I concede that both questions are asking for a way to draw dashed lines on a canvas. However, the other question is asking how to implement a dashed line by any method. My question specifically states that I cannot use the setLineDash() property to draw a dashed line. This difference limits the possible answers and I believe it is enough to make both questions sufficiently distinct.
You can create line segments.
The function will draw a dashed line from the info in the dashArr eg [2,2] will draw a line 2 pixels then a gap 2 pixels and repeat.
function dashedLine(x1,y1,x2,y2,dashArr){
// get the normalised line vector from start to end
var nx = x2 - x1;
var ny = y2 - y1;
const dist = Math.sqrt(nx * nx + ny * ny); // get the line length
nx /= dist;
ny /= dist;
var dashIdx = 0; // the index into the dash array
var i = 0; // the current line position in pixels
ctx.beginPath(); // start a path
while(i < dist){ // do while less than line length
// get the line seg dash length
var dashLen = dashArr[(dashIdx ++) % dashArr.length];
// draw the dash
ctx.moveTo(x1 + nx * i, y1 + ny * i);
i = Math.min(dist,i + dashLen);
ctx.lineTo(x1 + nx * i, y1 + ny * i);
// add the spacing
i += dashArr[(dashIdx ++) % dashArr.length];
if(i <= 0) { // something is wrong so exit rather than endless loop
break;
}
}
ctx.stroke(); // stroke
}
function dashedLine(x1,y1,x2,y2,dashArr){
var nx = x2 - x1;
var ny = y2 - y1;
const dist = Math.sqrt(nx * nx + ny * ny);
nx /= dist;
ny /= dist;
var dashIdx = 0;
var i = 0;
ctx.beginPath();
while(i < dist){
var dashLen = dashArr[(dashIdx ++) % dashArr.length];
ctx.moveTo(x1 + nx * i, y1 + ny * i);
i = Math.min(dist,i + dashLen);
ctx.lineTo(x1 + nx * i, y1 + ny * i);
i += dashArr[(dashIdx ++) % dashArr.length];
if(i <= 0) { // something is wrong so exit rather than endless loop
break;
}
}
ctx.stroke()
}
const ctx = canvas.getContext("2d");
dashedLine(0,0,300,150,[5,5]);
canvas { border : 2px solid black; }
<canvas id="canvas"></canvas>
I was also facing a similar problem and I used a different approach to solve this problem. I am posting it in case someone else is having the similar problem.
You can set the stroke pattern on canvas context. Stroke pattern can be any canvas pattern. So here I created an image of 1 pixel height and 6 pixel width. First three pixels were black and other three were white. Now I created the image to create a repeating pattern.
var linePattern;
imageToUsedAsPattern.onload = function() {
linePattern = context.createPattern(imageToUsedAsPattern, "repeat");
context.strokeStyle=linePattern;
}
var imageToUsedAsPattern = new Image();
imageToUsedAsPattern.src = "images/linePatterns.jpg";
Now all the calls to context.stroke will use the pattern to draw strokes. Like if you create a line from the top left corner of the canvas to the bottom right corner it will be a dashed line.
context.moveTo(0,0);
context.lineTo(canvas.width,canvas.height);
context.stroke();
See the full explanation at the following link
https://shamailamahmood.blogspot.com/2019/02/html5-canvas-drawing-draw-dotted-or.html

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.

Canvas Graph plotting data incorrectly

I have made a simple graph in a canvas but am having difficulty with two issues.
The first issue is setting the vertical axis with an appropriate scale automatically with enough room for each data value in an array. Ideally i'd like the numbers to be more rounded to the nearest million or thousand etc depending on it's actual value ranges rather than a value like 33145 as the first scale line.
Currently one value is too high for the scale and is not being drawn on the canvas because it is out of bounds.
The second issue, is the points don't seem to be plotting in their correct location, which I am unsure where my mistake was.
I made a JSFiddle as for the most part it might be a bit confusing without seeing it in action:
http://jsfiddle.net/ezttywzr/
This is how i plot my data and draw my vertical axis:
Vertical Axis:
var x = 0,
y,
range = data.max() - data.min(),
valueStep = range / 10,
// get width of largest number
margin = 3 + ctx.measureText(data.min() + (valueStep*10)).width,
pixelStep = (graph.height-40) / 10,
verticalP = pixelStep,
output;
// draw left hand values
for(var i = 0; i < 11; i++){
output = data.min() + (valueStep*i);
y = graph.height-20 - (verticalP + i*pixelStep);
ctx.fillText(output,x,y+6);
ctx.beginPath();
ctx.moveTo(margin, y);
ctx.lineTo(x2,y);
ctx.stroke();
}
Data Plotting:
var y = graph.height,
x = margin,
pos,
valueStep = (graph.width-(margin*2)) / data.length,
pixelRange = graph.height-20,
pp = range / pixelRange;
for(var i = 0; i < data.length; i++){
x += valueStep;
pos = x - (valueStep/2);
ctx.beginPath();
ctx.moveTo(x, graph.height-20);
ctx.lineTo(x, graph.height);
ctx.stroke();
ctx.fillText('Week '+(i+1),pos-(ctx.measureText('Week '+(i+1)).width/2),y);
ctx.beginPath();
ctx.arc(pos,(graph.height-20)-(verticalP+(data[i]/pp)),2,0,2*Math.PI);
ctx.stroke();
ctx.fill();
}
Nice job so far.
I made a few changes: http://jsfiddle.net/ezttywzr/2/
To get the scale I used
STEP = data.max() / NUM_HORIZONTAL_LINES
Where NUM_HORIZONTAL_LINES is the number of horizontal lines you want above the x-axis. In this case I used 10.
This means the first line will be 1 * STEP, the second will be 2 * STEP, the third will be 3 * STEP and so on..
This scale is convenient because it guarantees that the max value fits on the graph. In fact, the max value is on the top line because of the way we defined the scale.
Once we have our scale it's easy to calculate the position of the points relative to the x-axis. It's simply:
(PIXELS_PER_STEP / STEP) * VALUE
To go a step further you can do some math to round the top point of the graph up and pick a scale with that has nice round numbers.

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

Categories

Resources