Variable linewidth in HTML5 canvas line - html

I was trying to contribute to this post, HTML5 Canvas and Line Width
but it was deleted because it's not an official answer, because technically I'm also asking a question using the following code I get the same problem.
"I'm drawing line graphs on a canvas. The lines draw fine. The graph is scaled, every segment is drawn, color are ok, etc. My only problem is visually the line width varies. It's almost like the nib of a caligraphy pen. If the stroke is upward the line is thin, if the stroke is horizontal, the line is thicker.
My line thickness is constant, and my strokeStyle is set to black. I don't see any other properties of the canvas that affect such a varying line width but there must be.
"
<html>
<head>
<style>html{font-family:Verdana;}</style>
<script type="text/javascript">
var canvas ;
var context ;
var Val_max;
var Val_min;
var sections;
var xScale;
var yScale;
var Samsung = [21000,21000,23000,22000,22000,23000,23000];
function init() {
// set these values for your data
sections = 7;
Val_max = 25000;
Val_min = 10000;
var stepSize = 1500;
var columnSize = 75;
var rowSize = 75;
var margin = 10;
var xAxis = [""," Monday "," Tuesday"," Wednesday"," Thursday"," Friday"," Saturday"," Sunday"]//;
//
canvas = document.getElementById("canvas");
context = canvas.getContext("2d");
context.fillStyle = "#4d4d4d"
context.font = "10 pt Arial"
yScale = (canvas.height - columnSize - margin) / (Val_max - Val_min);
xScale = (canvas.width - rowSize) / sections;
context.strokeStyle="#4d4d4d"; // color of grid lines
context.beginPath();
// print Parameters on X axis, and grid lines on the graph
for (i=1;i<=sections;i++) {
var x = i * xScale;
context.fillText(xAxis[i], x,columnSize - margin);
context.moveTo(x, columnSize);
context.lineTo(x, canvas.height - margin);
}
// print row header and draw horizontal grid lines
var count = 0;
for (scale=Val_max;scale>=Val_min;scale = scale - stepSize) {
var y = columnSize + (yScale * count * stepSize);
context.fillText(scale, margin,y + margin);
context.moveTo(rowSize,y)
context.lineTo(canvas.width,y)
count++;
}
context.stroke();
context.lineWidth=20;
context.translate(rowSize,canvas.height + Val_min * yScale);
context.scale(1,-1 * yScale);
// Color of each dataplot items
context.strokeStyle="#2FBC3A";
plotData(Samsung);
}
function plotData(dataSet) {
// context.beginPath();
// context.moveTo(0, dataSet[0]);
// for (i=1;i<sections;i++) {
// context.lineTo(i * xScale, dataSet[i]);
// }
// context.stroke();
var love=0;
for (i=1;i<sections;i++) {
context.beginPath();
context.moveTo(love, dataSet[i-1]);
context.lineTo(i * xScale, dataSet[i]);
love=i*xScale;
context.stroke();
}
}
</script>
</head>
<body onLoad="init()">
<div align="center">
<canvas id="canvas" height="400" width="650">
</canvas>
<br>
<!--Legends for Dataplot -->
<span style="color:#4d4d4d"> Graph </span>
</div>
</body>
</html>

You are changing your context's scaleY in a non-uniform way.
So all the drawings after this operation will get shrunk on the Y axis.
To avoid that, apply this scaling only on your coordinates, at the time of drawing i.e
context.scale(1, -1 * yScale);
...
context.lineTo(x, y);
becomes
context.lineTo(x, y * -1 * yScale);
This way, your coordinate gets correctly scaled, but your stroke keeps its correct scale.
Also, you were drawing each segment separately, which would produce some holes in between of every segments, so I took the liberty of merging them in a single sub-path.
var canvas;
var context;
var Val_max;
var Val_min;
var sections;
var xScale;
var yScale;
var Samsung = [21000, 21000, 23000, 22000, 22000, 23000, 23000];
function init() {
// set these values for your data
sections = 7;
Val_max = 25000;
Val_min = 10000;
var columnSize = 75;
var rowSize = 75;
var margin = 10;
canvas = document.getElementById("canvas");
context = canvas.getContext("2d");
context.fillStyle = "#4d4d4d";
yScale = (canvas.height - columnSize - margin) / (Val_max - Val_min);
xScale = (canvas.width - rowSize) / sections;
context.lineWidth = 20;
context.translate(rowSize, canvas.height + Val_min * yScale);
//context.scale(1,-1 * yScale);
// ^-- Don't do that.
context.strokeStyle = "#2FBC3A";
plotData(Samsung);
}
function plotData(dataSet) {
var love = 0;
// make a single path from all the segments
context.beginPath();
for (var i = 0; i < sections; i++) {
// Here we scale the coordinate, not the drawings
context.lineTo(i * xScale, dataSet[i] * -1 * yScale);
love = i * xScale;
}
context.stroke();
}
init();
<canvas id="canvas" height="400" width="650">
</canvas>

Related

How to draw a square on canvas

I'm trying to draw squares at the corners of a canvas. The top ones work but I should be able to draw the third square which I have partially drawn.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<script type="application/javascript">
function getRandomInt(max) {
return Math.floor(Math.random() * max);
}
var t = getRandomInt(10);
function draw() {
function getRandomInt(max) {
return Math.floor(Math.random() * max);
}
var t = getRandomInt(10);
const canvas = document.getElementById('canvas');
if (canvas.getContext) {
const ctx = canvas.getContext('2d');
setInterval(myTimer, t*100);
function myTimer() {
var i = 0;
var x = window.innerWidth;
var y = window.innerHeight;
y = y + 100;
//draw crosshair
console.log("x = "+ x);
console.log("y = "+ y);
console.log("i = "+ i);
ctx.beginPath();
var crosshairlength = 20;
var lengthxminus = crosshairlength;
var lengthxplus = crosshairlength;
var lengthyminus = crosshairlength;
var lengthyplus = crosshairlength;
//horizontal line
ctx.moveTo((x/2)-lengthxminus, y/2);
ctx.lineTo((x/2)+lengthxplus, y/2);
//vertical line
ctx.moveTo(x/2, (y/2)-lengthyminus);
ctx.lineTo(x/2, (y/2)+lengthyplus);
var t = 10;
//top left
ctx.moveTo(0,0);
ctx.lineTo(t,0);
ctx.lineTo(t,t);
ctx.lineTo(0,t);
ctx.lineTo(0,0);
//top right
ctx.moveTo(x-t,0);
ctx.lineTo(x,0);
ctx.lineTo(x,t);
ctx.lineTo(x-t,t);
ctx.lineTo(x-t,0);
//bottom right
ctx.moveTo(x-t,y-t);
ctx.lineTo(x,y-t);
ctx.lineTo(x,y);
//bottom left
ctx.stroke();
}
i++;
}
}
</script>
</head>
<body onload="draw();">
<canvas id="canvas" width="1846" height="768"></canvas>
</body>
</html>
Either the canvas is the not the size of the viewport or I am misunderstanding the coordinate system which I believe has the positive y-axis going down and the origin is in the top left.
window.innerWidth and window.innerHeight represent the width and height of the viewport. Your <canvas> width and height are set by its width and height attributes.
If you want the canvas to be the size of the viewport, change these attributes, though changing these attributes come with great side-effects like reassigning a new pixel buffer, and clearing all the properties of the attached context, so it's better to do so only when really needed:
if (canvas.width !== x) {
canvas.width = x;
}
if (canvas.height !== y) {
canvas.height = y;
}
And if you wanted to use the size of the canvas rather than the one of the viewport, then you want
function myTimer() {
var i = 0;
var x = canvas.width;
var y = canvas.height;

How to let a user remove an object in canvas 2D from an arbitrary position

I've got some simple barchart making code written which allows a user to add a barchart but I'd also like to allow them to remove a barchart of choice from the canvas. I don't think this should be overly difficult but I'm relatively new to html and I'm quite unsure how to go about it. Any help would be greatly appreciated.
Here's the code I've written.
<html>
<head>
<script>
var barVals = [];
function draw() {
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
// calculate highest bar value (used to scale the rest)
var highest = 0;
for (var b=0; b<barVals.length; b++) {
if (barVals[b]>highest)
highest=barVals[b];
}
// we have 8 horizontal lines so calculate an appropriate scale
var lineSpacing = 1;
var highestLine = 7*lineSpacing;
while (highestLine<highest) {
lineSpacing *= 10;
highestLine = 7*lineSpacing;
}
// grey background
ctx.fillStyle = "rgb(200,200,200)";
ctx.fillRect (0, 0, 600, 450);
// draw and (if we have any data to scale from) label horizontal lines
var lineNum = 0;
ctx.fillStyle="white";
ctx.font="16px sans-serif";
for (y=0; y<=350; y+=50) {
// line
ctx.beginPath();
ctx.moveTo(50,y+50);
ctx.lineTo(550,y+50);
ctx.stroke();
// label (the 6 is an offset to centre the text vertically on the line)
if (barVals.length>0) {
ctx.fillText(lineSpacing*lineNum, 10, 400-y+6);
lineNum++;
}
}
// draw boxes (widths based on how many we have)
var barWidth = 500/barVals.length;
var halfBarWidth = barWidth/2;
for (b=0; b<barVals.length; b++) {
// calculate size of box and draw it
var x = 60+b*barWidth;
var hgt = (barVals[b]/highestLine)*350; // as fraction of highest line
if (b%2==0)
ctx.fillStyle = "red";
else
ctx.fillStyle = "blue";
ctx.fillRect(x,400-hgt,barWidth,hgt);
// calculate position of text and draw it
ctx.fillStyle="white";
var metrics = ctx.measureText(barVals[b]);
var halfTextWidth = metrics.width/2;
x = 60+halfBarWidth+(b*barWidth)-halfTextWidth;
ctx.fillText(barVals[b], x, 420-hgt);
}
}
function addBar() {
var textBoxObj = document.getElementById("barVal");
barVals.push(parseInt(textBoxObj.value)); // add new value to end of array. As an integer not a string!!
draw(); // redraw
textBoxObj.value = 0;
}
</script>
</head>
<body onload="draw();">
<center>
<canvas id="canvas" width="600" height="450"></canvas>
<form>
<BR>
<input type=button value='Add Bar' onclick='addBar();'> <input id='barVal' value=0>
</form>
</body>
</html>
Removing a specific chart isn't much different from adding. In fact you almost have everything you need right in your code yet.
Let's take a look at it. As soon as you click on the "Add Bar" button it will add a value from the associated textbox to the barVal array. For example, if there has been a value of 5 and 12 and you would trace the contents of barVal to the console using
console.log(barVal);
you would see this
Array [ 5, 12 ]
So 5 is stored at the first position and 12 at the second inside the array. With this knowledge in mind, what about adding a function which simply removes a specific element from the array? Here comes the array.splice() function into play. You can pass it an index inside the array and a number of elements it should remove.
e.g. if we want to get rid of the 12, we'd call barVal.splice(1,1);
After the element has been removed it's just a matter of updating your graph by calling draw() again. Now you might wonder why we pass 1 as the index, as we want to remove the second element - that's because indexes start counting from 0.
Here's an example:
var barVals = [];
function draw() {
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
// calculate highest bar value (used to scale the rest)
var highest = 0;
for (var b = 0; b < barVals.length; b++) {
if (barVals[b] > highest)
highest = barVals[b];
}
// we have 8 horizontal lines so calculate an appropriate scale
var lineSpacing = 1;
var highestLine = 7 * lineSpacing;
while (highestLine < highest) {
lineSpacing *= 10;
highestLine = 7 * lineSpacing;
}
// grey background
ctx.fillStyle = "rgb(200,200,200)";
ctx.fillRect(0, 0, 600, 450);
// draw and (if we have any data to scale from) label horizontal lines
var lineNum = 0;
ctx.fillStyle = "white";
ctx.font = "16px sans-serif";
for (y = 0; y <= 350; y += 50) {
// line
ctx.beginPath();
ctx.moveTo(50, y + 50);
ctx.lineTo(550, y + 50);
ctx.stroke();
// label (the 6 is an offset to centre the text vertically on the line)
if (barVals.length > 0) {
ctx.fillText(lineSpacing * lineNum, 10, 400 - y + 6);
lineNum++;
}
}
// draw boxes (widths based on how many we have)
var barWidth = 500 / barVals.length;
var halfBarWidth = barWidth / 2;
for (b = 0; b < barVals.length; b++) {
// calculate size of box and draw it
var x = 60 + b * barWidth;
var hgt = (barVals[b] / highestLine) * 350; // as fraction of highest line
if (b % 2 == 0)
ctx.fillStyle = "red";
else
ctx.fillStyle = "blue";
ctx.fillRect(x, 400 - hgt, barWidth, hgt);
// calculate position of text and draw it
ctx.fillStyle = "white";
var metrics = ctx.measureText(barVals[b]);
var halfTextWidth = metrics.width / 2;
x = 60 + halfBarWidth + (b * barWidth) - halfTextWidth;
ctx.fillText(barVals[b], x, 420 - hgt);
}
}
function addBar() {
var textBoxObj = document.getElementById("barVal");
barVals.push(parseInt(textBoxObj.value)); // add new value to end of array. As an integer not a string!!
draw(); // redraw
textBoxObj.value = 0;
}
function removeBar() {
var textBoxObj = document.getElementById("removeBarVal");
barVals.splice(parseInt(textBoxObj.value), 1);
draw(); // redraw
}
draw();
<center>
<canvas id="canvas" width="600" height="450"></canvas>
<form>
<br>
<input type=button value='Add Bar' onclick='addBar();'> <input id='barVal' value=0>
<input type=button value='Remove Bar' onclick='removeBar();'> <input id='removeBarVal' value=0>
</form>
</center>

Draw gradient bevel around polygon

Basically I need to create a falloff texture for given polygon. For instance this is the image I have
What I need to create is this, but with bevel gradient from white to black, consider the green part as gradient.
I've got the coordinates of all the vertices and the thickness of the bevel. I'm rendering using HTML5 2d canvas. Basically the most obvious solution would be to calculate every pixel's distance to the polygon and if it's within the thickness parameter, calculate the color and color the pixel. But that's heavy calculations and would be slow, even for smallest possible texture for my needs. So are there any tricks I can do with canvas to achieve this?
Just draw the polygon's outline at different stroke widths changing the colour for each step down in width.
The snippet shows one way of doing it. Draws 2 polygons with line joins "miter" and "round"
"use strict";
const canvas = document.createElement("canvas");
canvas.height = innerHeight;
canvas.width = innerWidth;
canvas.style.position = "absolute";
canvas.style.top = canvas.style.left = "0px";
const ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
// poly to draw
var poly = [0.1,0.2,0.4,0.5,0.2,0.8];
var poly1 = [0.6,0.1,0.9,0.5,0.8,0.9];
// convert rgb style colour to array
function rgb2Array(rgb){
var arr1 = rgb.split("(")[1].split(")")[0].split(",");
var arr = [];
while(arr1.length > 0){
arr.push(Number(arr1.shift()));
}
return arr;
}
// convert array to rgb colour
function array2rgb(arr){
return "rgb("+Math.floor(arr[0])+","+Math.floor(arr[1])+","+Math.floor(arr[2])+")"
}
// lerps array from to. Amount is from 0 # from 1 # to. res = is the resulting array
function lerpArr(from,to,amount,res){
var i = 0;
if(res === undefined){
res = [];
}
while(i < from.length){
res[i] = (to[i]-from[i]) * amount + from[i];
i++;
}
return res;
}
// draw gradient outline
// poly is the polygon verts
// width is the outline width
// fillStyle is the polygon fill style
// rgb1 is the outer colour
// rgb2 is the inner colour of the outline gradient
function drawGradientOutline(poly,width,fillStyle,rgb1,rgb2){
ctx.beginPath();
var i = 0;
var w = canvas.width;
var h = canvas.height;
ctx.moveTo(poly[i++] * w,poly[i++] * h);
while(i < poly.length){
ctx.lineTo(poly[i++] * w,poly[i++] * h);
}
ctx.closePath();
var col1 = rgb2Array(rgb1);
var col2 = rgb2Array(rgb2);
i = width * 2;
var col = [];
while(i > 0){
ctx.lineWidth = i;
ctx.strokeStyle = array2rgb(lerpArr(col1,col2,1- i / (width * 2),col));
ctx.stroke();
i -= 1;
}
ctx.fillStyle = fillStyle;
ctx.fill();
}
ctx.clearRect(0,0,canvas.width,canvas.height)
ctx.lineJoin = "miter";
drawGradientOutline(poly,20,"black","rgb(255,0,0)","rgb(255,255,0)")
ctx.lineJoin = "round";
drawGradientOutline(poly1,20,"black","rgb(255,0,0)","rgb(255,255,0)")

How to render a d3.js map on canvas free of blurriness

Try as I might, I've been unable to render a d3.js county map without causing the map to blur significantly.
I'm using the usual tricks: My canvas style width is half that of my attribute width. I translate the context of the drawing half a pixel to offset any unwanted effects.
But it's still terribly blurry.
Can someone share the pattern for a crisp d3.js map made for canvas elements?
function drawQuintiles() {
var width = 960,
height = 500;
var projection = d3.geo.albers()
.scale(666);
var canvas = d3.select("#quintiles")
.append("canvas")
.attr("class",'canvasarea');
var context = canvas.node().getContext("2d");
var ratio = (window.devicePixelRatio / context.webkitBackingStorePixelRatio) || 1;
d3.select('.canvasarea')
.attr("width", width * ratio).attr("height", height * ratio)
.style("width", width + "px").style("height", height + "px");
context.scale(ratio, ratio);
var path = d3.geo.path()
.projection(projection)
.context(context);
d3.json("/data/us-counties.json", function(error, us) {
if (error) throw error;
context.strokeStyle = '#333';
context.beginPath();
var strokeWidth = 0.5;
var iTranslate = (strokeWidth % 2) / 2;
context.translate(iTranslate, 0);
context.lineWidth = strokeWidth;
context.lineCap = "round";
path(topojson.feature(us, us.objects.counties));
context.stroke();
});
}
This is the code I ended on. Removing the scale and the translate hack has the map rendering properly.
function drawQuintiles() {
var width = 1600;
d3.json("/data/us-counties.json", function(error, data) {
var projection = d3.geo.albersUsa();
var path = d3.geo.path().projection(projection);
var tracts = topojson.feature(data, data.objects.counties);
projection.scale(1).translate([0, 0]);
var b = path.bounds(tracts);
var whRatio = ((b[1][0] - b[0][0]) / (b[1][1] - b[0][1]));
var height = (width / 2) * whRatio;
var s = .98 / Math.max((b[1][0] - b[0][0]) / width, (b[1][1] - b[0][1]) / height),
t = [(width - s * (b[1][0] + b[0][0])) / 2, (height - s * (b[1][1] + b[0][1])) / 2];
projection.scale(s).translate(t);
var canvas = d3.select("#quintiles")
.append("canvas")
.attr("class",'canvasarea');
var context = canvas.node().getContext("2d");
var ratio = window.devicePixelRatio || 1;
d3.select('.canvasarea')
.attr("width", width ).attr("height", height )
.style("width", ((width * ratio) ) + "px").style("height", ((height * ratio) ) + "px");
var path = d3.geo.path()
.projection(projection)
.context(context);
if (error) throw error;
context.strokeStyle = '#333';
context.beginPath();
var strokeWidth = 0.5;
context.lineWidth = strokeWidth;
context.lineCap = "round";
path(topojson.feature(data, data.objects.counties));
context.stroke();
});
}
drawQuintiles();

HTML5 Canvas Challenge

I am creating an HTML5 app that will display a bunch of shapes in different colors. I am having trouble display more than one of any shape.
Here is a JSFiddle link to my project: http://jsfiddle.net/tithos/3uyLc/
Here is one of the things I tried:
$("#go").click(function() {
var number = $("#number option:selected").val();
var shape = $("#shape option:selected").val();
var size = $("#size option:selected").val();
var offset = size;
var i = 0;
var shift = 0;
while(i < number){
switch(shape){
case '1':
console.log(shift);
square((offset+shift), size);
shift = (shift + size);
break;
case '2':
circle(offset, size);
break;
case '3':
triangle(offset, size);
break;
}
i++;
}
});
This, when repeated 16 times, gives me "0121212121212121212121212121212" in the concole. It is concatenating, not adding. Why?
Any help or insights are welcome
Thanks,
Tim
Since .val() returns a string you are using + operator between two strings, which is the concatenation operator. Use parseInt to convert a string to integer.
In the first few lines, you need to parseInt from each of the .val() functions. So:
var number = $("#number option:selected").val();
var shape = $("#shape option:selected").val();
var size = $("#size option:selected").val();
becomes
var number = parseInt($("#number option:selected").val());
var shape = $("#shape option:selected").val();
var size = parseInt($("#size option:selected").val());
but the size and "offset" calculations are all done in the wrong place. they need to be done in the main loop while the drawShape methods each have the task of drawing a given shape in a given location of a specified size. http://jsfiddle.net/3uyLc/39/
Here's the fixed code:
jQuery.noConflict();
(function($) {
$("#clear").click(function() {
console.log("clear!");
var c=document.getElementById("canvas");
var context=c.getContext("2d");
context.clearRect(0, 0, canvas.width, canvas.height);
});
function square(offset, size){
var color = $("#color option:selected").val();
var c=document.getElementById("canvas");
var context=c.getContext("2d");
context.fillStyle = color;
context.fillRect(offset,0,size,size);
}
function circle(offset, size){
var color = $("#color option:selected").val();
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
var radius = size / 2;
var x = offset + radius;
var y = radius;
context.beginPath();
context.arc(x, y, radius, 0, 2 * Math.PI, false);
context.lineWidth = 1;
context.fillStyle = color;
context.fill();
//context.fillStyle="#ff0000";
//context.fillRect(x-1, y-1, 2, 2);
}
function triangle(offset, size){
console.log(offset);
var color = $("#color option:selected").val();
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
var width = size;
var height = size;
// Draw a path
context.beginPath();
//top of triangle
context.moveTo(offset + width/2, 0);
//top to right
context.lineTo(offset + width, size);
//bottom of triangle
context.lineTo(offset, size);
context.closePath();
// Fill the path
context.fillStyle = color;
context.fill();
}
$("#go").click(function() {
var number = parseInt($("#number option:selected").val());
var shape = $("#shape option:selected").val();
var size = parseInt($("#size option:selected").val()) * 10;
var i = 0;
var position = 0;
var padding = size * 0.5; //leave space between the shapes 1/2 as large as the shape itself
while(i < number){
switch(shape){
case '1':
square(position, size);
break;
case '2':
circle(position, size);
break;
case '3':
triangle(position, size);
break;
}
i++;
// calculate the position of the next shape
position = position + size + padding;
}
});
})(jQuery);​