Related
I have the problem that my Donut Chart isn't working as I want it to do. I want to create a Donut Chart like this one:
But my Donut Chart looks like this at the moment:
As you can see the strokes don't overlap in the right direction. I think this might be, because I start to draw the strokes from right to left. Instead it should draw them from left to right, so the left "rounded end" is visible not the right rounded end.
This is what I have tried so far:
//function to draw the donut chart, ctx = context, cx - cy = position, radius and arcwith
dmbChart(ctx, cx, cy, radius, arcwidth) {
var tot = 0;
var accum = 0;
var PI = Math.PI;
var PI2 = PI * 2;
var offset = -PI/2;
for(var i = 0; i < this.canvasValues.length; i++) {
tot += this.canvasValues[i];
}
//Donut Sectors Color: Draw each stroke based on the value (canvasValues) and Color (canvasColors)
for(var i = 0; i < this.canvasValues.length; i++) {
ctx.lineWidth = arcwidth;
ctx.beginPath();
ctx.lineCap = "round";
ctx.arc(cx, cy, radius, offset + PI2 * (accum/tot), offset + PI2 * ((accum + this.canvasValues[i]) / tot));
ctx.strokeStyle = this.canvasColors[i];
ctx.stroke();
accum += this.canvasValues[i];
}
}
As you can see I get the values which are the percentages how long the each stroke should be and the color. Starting on top I draw each one from top -> right -> bottom -> left and this is the result. But how can I modify it to get the result on top?
Edit:
With the help of #Helder Sepulveda I created it like this now. I changed a lot of the calculations fixed some bugs which came with the changes. The only problem now is that it doesn't start to draw at the top. As you can see the green stroke should start on the top:
function dmbChart(ctx, cx, cy, radius, arcwidth) {
var canvasValues = [30, 5, 15, 10, 10, 10, 10, 10];
var canvasColors = ["#10dc60", "#DDDDDD", "#0cd1e8", "#ffce00", "#7044ff", "#f04141", "#ffea00", "#ee82ee"];
ctx.lineWidth = arcwidth;
ctx.lineCap = "round";
var accum = canvasValues.reduce((a,b) => a + b, 0);
for (var i = canvasValues.length-1; i >= 0; i--) {
var radians = canvasValues[i] / 100 * 360 * Math.PI / 180
ctx.beginPath();
ctx.arc(cx, cy, radius, accum, accum - radians, true);
ctx.strokeStyle = canvasColors[i];
ctx.stroke();
accum -= radians;
}
ctx.beginPath();
ctx.arc(cx, cy, radius, accum, accum - (0.1 / 100 * 360 * Math.PI / 180), true);
ctx.strokeStyle = canvasColors[canvasColors.length - 1];
ctx.stroke();
}
const canvas = document.getElementById("c");
canvas.width = canvas.height = 140;
const ctx = canvas.getContext("2d");
dmbChart(ctx, 70, 70, 50, 30)
<canvas id="c"></canvas>
I'm making some assumptions on canvasValues and canvasColors to show something working:
function dmbChart(ctx, cx, cy, radius, arcwidth) {
var accum = 0;
var PI = Math.PI;
var PI2 = PI * 2;
var offset = -PI / 2;
var canvasValues = [10, 10, 10]
var canvasColors = ["red", "green", "blue"]
var tot = canvasValues.reduce((a, b) => a + b, 0)
ctx.lineWidth = arcwidth;
ctx.lineCap = "round";
for (var i = 0; i < canvasValues.length; i++) {
ctx.beginPath();
ctx.arc(cx, cy, radius, offset + PI2 * (accum / tot), offset + PI2 * ((accum + canvasValues[i]) / tot));
ctx.strokeStyle = canvasColors[i];
ctx.stroke();
accum += canvasValues[i];
}
ctx.beginPath();
ctx.arc(cx, cy, radius, offset, offset);
ctx.strokeStyle = canvasColors[0];
ctx.stroke();
}
const canvas = document.getElementById("c");
canvas.width = canvas.height = 140;
const ctx = canvas.getContext("2d");
dmbChart(ctx, 70, 70, 50, 30)
<canvas id="c"></canvas>
The idea is to draw one last "short" arc with the first value(and color) at the end of the loop
I also moved a couple of lines out of the loop:
ctx.lineWidth = arcwidth;
ctx.lineCap = "round";
that could be set just once before the loop
And here is what we talked about in the comments inverting the direction
function dmbChart(ctx, cx, cy, radius, arcwidth) {
var PI = Math.PI;
var PI2 = PI * 2;
var offset = -PI / 2;
var canvasValues = [10, 10, 10]
var canvasColors = ["red", "green", "blue"]
var tot = canvasValues.reduce((a,b) => a + b, 0)
var accum = tot;
ctx.lineWidth = arcwidth;
ctx.lineCap = "round";
for (var i = canvasValues.length-1; i >= 0; i--) {
ctx.beginPath();
ctx.arc(cx, cy, radius, offset + PI2 * (accum / tot), offset + PI2 * ((accum + canvasValues[i]) / tot));
ctx.strokeStyle = canvasColors[i];
ctx.stroke();
accum -= canvasValues[i];
}
ctx.beginPath();
p = offset + PI2 * ((tot + canvasValues[canvasValues.length-1]) / tot)
ctx.arc(cx, cy, radius, p, p);
ctx.strokeStyle = canvasColors[canvasColors.length-1];
ctx.stroke();
}
const canvas = document.getElementById("c");
canvas.width = canvas.height = 140;
const ctx = canvas.getContext("2d");
dmbChart(ctx, 70, 70, 50, 30)
<canvas id="c"></canvas>
I just had a quick thought in mind of drawing a chessboard using JS and Canvas, and I have this code that draws the boxes alright with for loops.
canvas = document.getElementById("canvas");
ctx = canvas.getContext("2d");
var x, y,
boxWidth = 30,
boxHeight = 30;
for (x = 0; x < canvas.width; x += boxWidth) {
for (y = 0; y < canvas.height; y += boxHeight) {
ctx.beginPath();
ctx.rect(x, y, boxWidth, boxHeight);
ctx.stroke();
ctx.closePath();
}
}
<canvas id="canvas" width="240" height="240"></canvas>
Now I'm wondering how I can access each odd box on the axes to change their fill colors (e.g. black, white, black, white, and so on).
I know using global variables isn't the best way, but this is a very small project and I just want to get some logic on how I can alternate the colors of the chessboard. Your help is very much appreciated!
You could also try only incrementing your values by 1 (instead of boxWidth), which would make it simpler to check if they are even or odd. Then you would need to either scale or multiply by boxWidth and boxHeight:
canvas = document.getElementById("canvas");
ctx = canvas.getContext("2d");
var x, y,
boxWidth = 30,
boxHeight = 30;
var numbRows = Math.floor(canvas.width / boxWidth),
numbCols = Math.floor(canvas.height / boxHeight);
ctx.save();
ctx.scale(boxWidth, boxHeight);
for (x = 0; x < numbRows; x++) {
for (y = 0; y < numbCols; y++) {
if ((x+y) % 2 == 0) ctx.fillStyle = 'white';
else ctx.fillStyle = 'black';
ctx.beginPath();
ctx.rect(x, y, boxWidth, boxHeight);
ctx.stroke();
ctx.closePath();
}
}
ctx.restore();
You can use fillRect to do so like this:
canvas = document.getElementById("canvas");
ctx = canvas.getContext("2d");
var x, y,
boxWidth = 30,
boxHeight = 30;
for (x = 0; x < canvas.width; x += boxWidth) {
for (y = 0; y < canvas.height; y += boxHeight) {
ctx.fillStyle = (x / boxWidth + y / boxHeight) % 2 === 0? "white": "black"; // determine which color to use depending on the index of x (x / boxWidth) an the index of y (y / boxHeight)
ctx.fillRect(x, y, boxWidth, boxHeight);
}
}
<canvas id="canvas" width="240" height="240"></canvas>
canvas = document.getElementById("canvas");
ctx = canvas.getContext("2d");
var x, y,
boxWidth = 30,
boxHeight = 30;
for (x = 0; x < canvas.width; x += boxWidth) {
for (y = 0; y < canvas.height; y += boxHeight) {
ctx.beginPath();
ctx.rect(x, y, boxWidth, boxHeight);
// fill odd boxes
(x/boxWidth + y/boxHeight) % 2 && ctx.fill()
ctx.stroke();
ctx.closePath();
}
}
<canvas id="canvas" width="240" height="240"></canvas>
Alternatives to the for loop
Another way without a loop, draw the pattern 2 by 2 square in top corner then repeat that by copying the canvas onto itself.
First create the top 2 by 2 square then fill rest of board with copies.
First 2 by 2 to 4 by 2
then 4 by 2 to 8 by 2
then 8 by 2 to 8 by 4
then 8 by 4 to 8 by 8
Example
const w= 100;
canvas.height = canvas.width = w * 8;
const ctx = canvas.getContext("2d");
ctx.fillStyle = "black";
ctx.fillRect(0, 0, w + w, w + w);
ctx.fillStyle = "white";
ctx.fillRect(0, 0, w, w);
ctx.fillRect(w, w, w, w);
ctx.drawImage(canvas, 0, 0, w * 2, w * 2, w * 2, y , w * 2, w * 2);
ctx.drawImage(canvas, 0, 0, w * 4, w * 2, w * 4, y , w * 4, w * 2);
ctx.drawImage(canvas, 0, 0, w * 8, w * 2, 0 , w * 2, w * 8, w * 2);
ctx.drawImage(canvas, 0, 0, w * 8, w * 4, 0 , w * 4, w * 8, w * 4);
Thus it gets drawn in 7 render calls, if the grid was larger then 2 more calls for 16 by 16, and every doubling in size only needs two more calls.
The pattern can be very complex but not create excessive render stress as in the next example that has shadows and different composite calls.
const squareSize = 72;
const boardSize = 8;
const borderSize = 8;
canvas.height = canvas.width = squareSize * boardSize + borderSize * 2;
const ctx = canvas.getContext("2d");
var x = borderSize;
var y = x;
var w = squareSize;
drawSquare(3, 3, canvas.width - 6, "black", "#F97");
drawSquare(x, y, w, "white", "#964");
drawSquare(w + x, y, w, "black", "#745");
ctx.drawImage(canvas, x, y, w, w, x + w, y + w, w, w);
ctx.drawImage(canvas, x + w, y, w, w, x, y + w, w, w);
ctx.drawImage(canvas, x, y, w * 2, w * 2, x + w * 2, y, w * 2, w * 2);
ctx.drawImage(canvas, x, y, w * 4, w * 2, x + w * 4, y, w * 4, w * 2);
ctx.drawImage(canvas, x, y, w * 8, w * 2, x, y + w * 2, w * 8, w * 2);
ctx.drawImage(canvas, x, y, w * 8, w * 4, x, y + w * 4, w * 8, w * 4);
drawSquare(0,0,canvas.width,"rgba(0,0,0,0.0)","rgba(0,0,0,0.05)");
// done.
// this function is only called twice.
function drawSquare(x,y,size,color,color2){
ctx.save();
ctx.shadowColor = color2;
ctx.shadowBlur = size * 0.2;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
ctx.beginPath();
ctx.rect(x,y,size,size);
ctx.clip();
ctx.lineWidth = size;
ctx.fillStyle = color;
ctx.fillRect(x,y,size,size);
ctx.globalAlpha = 0.5;
ctx.strokeRect(x - size / 2,y - size / 2, size * 2, size * 2);
ctx.shadowBlur = size * 0.5;
ctx.strokeRect(x - size / 2,y - size / 2, size * 2, size * 2);
ctx.shadowColor = "rgba(0,0,0,0)";
ctx.shadowBlur = 0;
ctx.globalAlpha = 1;
ctx.strokeStyle = color2;
ctx.lineWidth = 2;
ctx.strokeRect(x+1,y+1,size -2,size-2);
ctx.globalAlpha = 0.75;
ctx.fillRect(x+1,y+1,size-2,size-2);
ctx.globalCompositeOperation = "screen";
ctx.fillStyle = "white";
ctx.globalAlpha = 0.1;
ctx.fillRect(x,y,4,size);
ctx.fillRect(x,y,2,size);
ctx.fillRect(x+4,y,size-4,4);
ctx.fillRect(x+2,y,size-2,2);
ctx.restore();
}
canvas { border : 2px solid black; }
<canvas id="canvas" ></canvas>
Use a pattern style.
Create an offscreen canvas to hold the pattern of the top 2 by 2, draw the pattern, then assign the fillStyle of the on screen canvas to a new pattern created from the off screen canvas and fill the whole canvas.
const w = 72;
const patCan = document.createElement("canvas");
patCan.height = patCan.width = w * 2;
var ctx = patCan.getContext("2d");
ctx.fillStyle = "black";
ctx.fillRect(0, 0, w + w, w + w);
ctx.fillStyle = "white";
ctx.fillRect(0, 0, w, w);
ctx.fillRect(w, w, w, w);
// setup display canvas
canvas.height = canvas.width = w * 8;
var ctx = canvas.getContext("2d");
ctx.fillStyle = ctx.createPattern(patCan, "repeat");
ctx.fillRect(0, 0, w * 8, w * 8);
canvas { border : 8px solid green; }
<canvas id="canvas" ></canvas>
Rendering with 3 lines of code
Here's a neat little trick you can use to draw a chess board:
var ctx = c.getContext("2d");
for(var x = 0; x < c.width; x += c.width / 4) ctx.fillRect(x, 0, c.width/8, c.height);
ctx.globalCompositeOperation = "xor"; // toggle alpha channel for every 2nd line
for(var y = 0; y < c.height; y += c.height / 4) ctx.fillRect(0, y, c.width, c.height/8);
<canvas id=c width=600 height=600></canvas>
We're using canvas' size to detemine the grid size. You can of course change this and offset to anything you like. You'd still use the divisors 4 (2 cells) and 8 (1 cell) with the actual width and height.
The first step draws vertical black stripes every other column. Then we toggle alpha channel for every other row knowing that the default color is black (rgba(0,0,0,0)) using the "xor" composite mode which toggles the alpha channel.
Just remember to start with a empty canvas (which you probably are anyways due to the need to redraw moves) and to set back composite mode to "source-over" after drawing the board.
If you'd like to change the color itself simply add an extra step at the end:
ctx.globalCompositeOperation = "source-atop"; // will draw on top of what is filled
ctx.fillStyle = "#09a";
ctx.fillRect(0, 0, c.width, c.height);
The fillStyle and fillRect() can be replaced or used with an image, pattern, gradient etc.
To fill the white background simply use composite mode "destination-over" (will draw behind anything filled using the alpha channel), then draw for background.
An alternative is to use a toggle switch when filling each cell one by one:
var ctx = c.getContext("2d");
var toggle = false;
ctx.beginPath();
for(var y=0; y < c.height; y += c.height / 8) {
toggle = !toggle; // toggle for each row so they don't line up
for(var x=0; x < c.width; x += c.width / 8) {
// toggle for each cell and check, only draw if toggle = true
if (toggle = !toggle) ctx.rect(x, y, c.width / 8, c.height / 8);
}
}
ctx.fill(); // remember to use beginPath() for consecutive draw ops
<canvas id=c width=600 height=600></canvas>
Access Logic
To know if you're inside a cell you would simply calculate the mouse position relative to the canvas (see this answer on how to do that) and then quantize (using pseudo variables here, replace with real):
var cellSize = boardWidth / 8; // assumes the board is 1:1 square
var pos = getMousePos(event); // see linked answer above
var cellX = Math.floor(pos.x / cellSize) * cellSize; // start of current cell X
var cellY = Math.floor(pos.y / cellSize) * cellSize; // start of current cell Y
(to get index of cell just drop the * cellSize part).
Example:
var ctx = c.getContext("2d"), x, y, w = c.width, h = c.height, cellSize = w / 8;
render();
ctx.lineWidth = 4; ctx.strokeStyle = "red"; ctx.setLineDash([7, 7]);
// non-optimized - in production only redraw when needed (cellX/Y changes)
c.onmousemove = function(e) {
render();
var cell = getCellPos(getMousePos(e));
if (cell.x >= 0 && cell.x < w && cell.y >=0 && cell.y < h)
ctx.strokeRect(cell.x + 2, cell.y + 2, cellSize - 4, cellSize - 4);
}
function getCellPos(pos) {
return {x: Math.floor(pos.x / cellSize) * cellSize,
y: Math.floor(pos.y / cellSize) * cellSize}
}
function getMousePos(e) {
var rect = c.getBoundingClientRect();
return {x: e.clientX-rect.x, y: e.clientY-rect.y}
}
function render() {
ctx.clearRect(0, 0, w, h);
for(x = 0; x < w; x += w>>2) ctx.fillRect(x, 0, cellSize, c.height);
ctx.globalCompositeOperation = "xor"; // toggle alpha channel for every 2nd line
for(y = 0; y < h; y += h>>2) ctx.fillRect(0, y, w, cellSize);
ctx.globalCompositeOperation = "source-atop"; // fg
ctx.fillStyle = "#3c4168";
ctx.fillRect(0, 0, w, h);
ctx.globalCompositeOperation = "destination-over"; // bg
ctx.fillStyle = "#eee";
ctx.fillRect(0, 0, w, h);
ctx.globalCompositeOperation = "source-over"; // reset
}
body {background:#222;margin:20px 0 0 20px;}
<canvas id=c width=600 height=600></canvas>
I want to display several legs into a rectangular form in canvas.
Based on an array which groups the miles of my legs, I've made the algo to represent them proportionately on a canvas given.
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
var width = c.width;
var somme = 0;
var prevValue = 0;
var recapProp = [];
function drawArrow(fromx, fromy, tox, toy){
//variables to be used when creating the arrow
var headlen = 5;
var angle = Math.atan2(toy-fromy,tox-fromx);
//starting path of the arrow from the start square to the end square and drawing the stroke
ctx.beginPath();
ctx.moveTo(fromx, fromy);
ctx.lineTo(tox, toy);
ctx.strokeStyle = "blue";
ctx.lineWidth = 2;
ctx.stroke();
//starting a new path from the head of the arrow to one of the sides of the point
ctx.beginPath();
ctx.moveTo(tox, toy);
ctx.lineTo(tox-headlen*Math.cos(angle-Math.PI/7),toy-headlen*Math.sin(angle-Math.PI/7));
//path from the side point of the arrow, to the other side point
ctx.lineTo(tox-headlen*Math.cos(angle+Math.PI/7),toy-headlen*Math.sin(angle+Math.PI/7));
//path from the side point back to the tip of the arrow, and then again to the opposite side point
ctx.lineTo(tox, toy);
ctx.lineTo(tox-headlen*Math.cos(angle-Math.PI/7),toy-headlen*Math.sin(angle-Math.PI/7));
//draws the paths created above
ctx.strokeStyle = "blue";
ctx.lineWidth = 2;
ctx.stroke();
ctx.fillStyle = "blue";
ctx.fill();
}
function drawCircle(centerXFrom, centerYFrom){
var radius = 3;
ctx.beginPath();
ctx.arc(centerXFrom, centerYFrom, radius, 0, 2 * Math.PI, false);
ctx.fillStyle = 'green';
ctx.fill();
ctx.lineWidth = 1;
ctx.strokeStyle = '#003300';
ctx.stroke();
ctx.beginPath();
}
function sumTab(tabTT){
for (var i = 0; i < tabTT.length; i++){
somme += tabTT[i];
}
return somme;
}
function findProportion(tabTT){
var tailleMax = tabTT.length;
sumTab(tabTT);
for(var i = 0; i < tabTT.length; i++){
var percentLeg = (tabTT[i]/somme)*100;
var tailleLeg = ((width- 20)*percentLeg)/100 ;
recapProp.push(tailleLeg);
}
for(var i = 0; i <= recapProp.length; ++i){
console.log(prevValue);
drawCircle(prevValue +5, 5);
drawArrow(prevValue + 7, 5, prevValue+recapProp[i],5);
prevValue += recapProp[i];
}
}
var tabTT = [0,5,1,8,2];
findProportion(tabTT);
<canvas id="myCanvas" height="200" width="500"></canvas>
Then, I want to display then in a rectangular form, to make a loop (below is not rectangular, but it helps you to understand) :
I've tried to manipulate quadracticCurveTo() but that's not really conclusive..
var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
function drawArrow(fromx, fromy, tox, toy, radius){
//variables to be used when creating the arrow
var headlen = 5;
var r = fromx + tox;
var b = fromy + toy;
var angle = Math.atan2(r,b);
//starting path of the arrow from the start square to the end square and drawing the stroke
ctx.beginPath();
ctx.moveTo(fromx+radius, fromy);
ctx.lineTo(r-radius, fromy);
ctx.quadraticCurveTo(r, fromy, r, fromy+radius);
ctx.lineWidth = "2";
ctx.strokeStyle = '#ff0000';
ctx.stroke();
//starting a new path from the head of the arrow to one of the sides of the point
ctx.beginPath();
ctx.moveTo(r, b);
ctx.lineTo(r-headlen*Math.cos(angle-Math.PI/7),b-headlen*Math.sin(angle-Math.PI/7));
//path from the side point of the arrow, to the other side point
ctx.lineTo(r-headlen*Math.cos(angle+Math.PI/7),b-headlen*Math.sin(angle+Math.PI/7));
//path from the side point back to the tip of the arrow, and then again to the opposite side point
ctx.lineTo(r, b);
ctx.lineTo(r-headlen*Math.cos(angle-Math.PI/7),b-headlen*Math.sin(angle-Math.PI/7));
//draws the paths created above
ctx.strokeStyle = "blue";
ctx.lineWidth = 2;
ctx.stroke();
ctx.fillStyle = "blue";
ctx.fill();
}
drawArrow(50,5, 80,25, 25);
<canvas id="myCanvas" height="2000" width="2000"></canvas>
Finally, I've created the snippet I will need when I'll know how to curve my lines and keep its length !. I've calculated the perimeter of my canvas surface in order to re-calculate the proportions of my legs.
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
var width = c.width;
var height = c.height;
var perimetre = (width*2 + height*2);
var up = 0;
var right = 0;
var left = 0;
var bot = 0;
var somme = 0;
var prevValue = 0;
var recapProp = [];
/**********************************/
/*****<<Straight>> Arrows*********/
/********************************/
function drawArrow(fromx, fromy, tox, toy){
var headlen = 5;
var angle = Math.atan2(toy-fromy,tox-fromx);
ctx.beginPath();
ctx.moveTo(fromx, fromy);
ctx.lineTo(tox, toy);
ctx.strokeStyle = "blue";
ctx.lineWidth = 2;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(tox, toy);
ctx.lineTo(tox-headlen*Math.cos(angle-Math.PI/7),toy-headlen*Math.sin(angle-Math.PI/7));
ctx.lineTo(tox-headlen*Math.cos(angle+Math.PI/7),toy-headlen*Math.sin(angle+Math.PI/7));
ctx.lineTo(tox, toy);
ctx.lineTo(tox-headlen*Math.cos(angle-Math.PI/7),toy-headlen*Math.sin(angle-Math.PI/7));
ctx.strokeStyle = "blue";
ctx.lineWidth = 2;
ctx.stroke();
ctx.fillStyle = "blue";
ctx.fill();
}
/**********************************/
/************Points***************/
/********************************/
function drawCircle(centerXFrom, centerYFrom){
var radius = 3;
ctx.beginPath();
ctx.arc(centerXFrom, centerYFrom, radius, 0, 2 * Math.PI, false);
ctx.fillStyle = 'green';
ctx.fill();
ctx.lineWidth = 1;
ctx.strokeStyle = '#003300';
ctx.stroke();
ctx.beginPath();
}
function sumTab(tabTT){
for (var i = 0; i < tabTT.length; i++){
somme += tabTT[i];
}
return somme;
}
/***************************************************/
/************Get length for each leg***************/
/*************************************************/
function findProportion(tabTT){
var tailleMax = tabTT.length;
sumTab(tabTT);
for(var i = 0; i < tabTT.length; i++){
var percentLeg = (tabTT[i]/somme)*100;
var tailleLeg = ((perimetre - 20)*percentLeg)/100 ;
recapProp.push(tailleLeg);
}
/* For each leg I draw the circle and the arrow, due to the length calculated previously. If the length > the width of the canva, the arrow has to be curved */
for(var i = 0; i <= recapProp.length; ++i){
if(prevValue > width && top == 1){
drawCircle(prevValue +5, 5);
drawArrowBot(prevValue + 7, 5, prevValue+recapProp[i],5);
right = 1;
top = 0;
}
else if(prevValue > height && right == 1){
drawCircle(prevValue +5, 5);
drawArrowLeft(prevValue + 7, 5, prevValue+recapProp[i],5);
bot = 1;
right = 0;
}
else if (prevValue > width && bot == 1){
drawCircle(prevValue +5, 5);
drawArrowTop(prevValue + 7, 5, prevValue+recapProp[i],5);
bot = 0;
left = 0;
}
else {
drawCircle(prevValue +5, 5);
drawArrow(prevValue + 7, 5, prevValue+recapProp[i],5);
}
prevValue += recapProp[i];
}
}
var tabTT = [0,5,1,8,2];
findProportion(tabTT);
<canvas id="myCanvas" height="200" width="500" style="border:1px solid #000000;"></canvas>
I've commented all my code in order to help you understand the logic and what I want.
So, is it possible to curve the lines in a generic way?
I would probably do something like this:
Define a holding array with number of entries based on a resolution
Map the lines into that array setting 1's very there would be a line range, 0's for the gap.
Define a target shape such as an oval (can be any shape really!) which consists of equally many parts as the array resolution. Store each part and it's coordinate in an array (same length as the line array).
Morph each part using interpolation between the shape array and line array
Now you can produce the lines into almost any shape and form you desire.
Tip: you can of course skip one shape by mapping it directly the first time.
Tip 2: the shapes can be defined in normalized coordinates which makes it easier to translate and scale them.
Example
Here we define a rounded square and circle, then map the lines onto either, we can morph between the shapes to find a combination we like and use that (note: as the square in this example starts in "upper-right" corner and not where the circle has it's 0° there will be a small rotation as well, this can be dealt with separately as an exercise).
The rounded square could be a a bunny for that matter (for a more "tight" rounded square you can use cubic Bezier instead of quadratic as here). The key point is that the shape can be defined independently of the lines themselves. This may be overkill, but it's not so complicated and it's versatile, ie. generic.
See this answer for one way to add an arrow to the lines.
var ctx = document.querySelector("canvas").getContext("2d"),
resolution = 2000,
raster = new Uint8Array(resolution), // line raster array
shape = new Float32Array(resolution * 2), // target shape array (x2 for x/y)
shape2 = new Float32Array(resolution * 2),// target shape array 2
lines = [100, 70, 180, 35], // lines, lengths only
tLen = 0, // total length of lines + gaps
gap = 20, // gap in pixels
gapNorm, // normalized gap value for mapping
p = 0, // position in lines array
radius = 100, // target circle radius
angleStep = Math.PI * 2 / resolution, // angle step to reach circle / res.
cx = 150, cy = 150, // circle center
interpolation = 0.5, // t for interpolation
i;
// get total length of lines + gaps so we can normalize
for(i = 0; i < lines.length; i++) tLen += lines[i];
tLen += (lines.length - 2) * gap;
gapNorm = gap / tLen * 0.5;
// convert line and gap ranges to "on" in the lines array
for(i = 0; i < lines.length; i++) {
var sx = p, // start position in lines array
ex = p + ((lines[i] / tLen) * resolution)|0; // end position in lines array (int)
// fill array
while(sx <= ex) raster[sx++] = 1;
// update arrqay pointer incl. gap
p = ex + ((gapNorm * resolution)|0);
}
// Create a circle target shape split into same amount of segments as lines array:
p = 0; // reset pointer for shape array
for(var angle = 0; angle < Math.PI*2; angle += angleStep) {
shape[p++] = cx + radius * Math.cos(angle);
shape[p++] = cy + radius * Math.sin(angle);
}
// create a rounded rectangle
p = i = 0;
var corners = [
{x1: 250, y1: 150, cx: 250, cy: 250, x2: 150, y2: 250}, // bottom-right
{x1: 150, y1: 250, cx: 50, cy: 250, x2: 50, y2: 150}, // bottom-left
{x1: 50, y1: 150, cx: 50, cy: 50, x2: 150, y2: 50}, // upper-left
{x1: 150, y1: 50, cx: 250, cy: 50, x2: 250, y2: 150} // upper-right
],
c, cres = resolution * 0.25;
while(c = corners[i++]) {
for(var t = 0; t < cres; t++) {
var pos = getQuadraticPoint(c.x1, c.y1, c.cx, c.cy, c.x2, c.y2, t / cres);
shape2[p++] = pos.x;
shape2[p++] = pos.y;
}
}
// now we can map the lines array onto our shape depending on the values
// interpolation. Make it a reusable function so we can regulate the "morph"
function map(raster, shape, shape2, t) {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.beginPath();
for(var i = 0, x, y, x1, y1, x2, y2, prev = 0; i < resolution; i++) {
x1 = shape[i*2];
y1 = shape[i*2 + 1];
x2 = shape2[i*2];
y2 = shape2[i*2 + 1];
x = x1 + (x2 - x1) * t;
y = y1 + (y2 - y1) * t;
// do we have a change?
if (prev !== raster[i]) {
if (raster[i]) { // it's on, was off. create sub-path
ctx.moveTo(x, y);
}
else { // it's off, was on, render and reset path
ctx.stroke();
ctx.beginPath();
// create "arrow"
ctx.moveTo(x + 3, y);
ctx.arc(x, y, 3, 0, 6.28);
ctx.fill();
ctx.beginPath();
}
}
// add segment if on
else if (raster[i]) {
ctx.lineTo(x, y);
}
prev = raster[i];
}
}
ctx.fillStyle = "red";
map(raster, shape, shape2, interpolation);
document.querySelector("input").onchange = function() {
map(raster, shape, shape2, +this.value / 100);
};
function getQuadraticPoint(z0x, z0y, cx, cy, z1x, z1y, t) {
var t1 = (1 - t), // (1 - t)
t12 = t1 * t1, // (1 - t) ^ 2
t2 = t * t, // t ^ 2
t21tt = 2 * t1 * t; // 2(1-t)t
return {
x: t12 * z0x + t21tt * cx + t2 * z1x,
y: t12 * z0y + t21tt * cy + t2 * z1y
}
}
<script src="https://cdn.rawgit.com/epistemex/slider-feedback/master/sliderfeedback.min.js"></script>
<label>Interpolation: <input type="range" min=0 max=400 value=50></label><br>
<canvas width=400 height=400></canvas>
Calculate the middle control point that makes a quadratic Bezier curve become a specified length.
Given:
p0, p2: the QCurves starting and ending points.
length: the desired arc-length of the quadratic Bezier Curve.
You can calculate the control point that makes the QCurve's total arc-length equal length:
Calculate the midpoint between p0 & p2.
Calculate the angle of between p0 & p2.
Calculate a point (p1) perpendicular to that midpoint at a specified distance. This is a possible control point. The perpendicular angle is the calculated angle from step#2 minus 90 degrees.
Calculate the QCurve's arc-length using p0, p1 & p2 (calculatedLength).
You've got the right middle control point if calculatedLength equals the desired length.
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;
function reOffset(){
var BB=canvas.getBoundingClientRect();
offsetX=BB.left;
offsetY=BB.top;
}
var offsetX,offsetY;
reOffset();
window.onscroll=function(e){ reOffset(); }
var $length=$('#length');
var PI2=Math.PI*2;
var radius=5+1; // 5==fill, 1=added stroke
var p0={x:50,y:100,color:'red'};
var p2={x:175,y:150,color:'gold'};
var p1={x:0,y:0,color:'green'};
var midpoint={x:0,y:0,color:'purple'};
var perpendicularPoint={x:0,y:0,color:'cyan'};
//var points=[p0,p1,p2];
//var draggingPoint=-1;
setQLength(p0,p2,150,1);
draw();
function draw(){
ctx.clearRect(0,0,cw,ch);
ctx.beginPath();
ctx.moveTo(p0.x,p0.y);
ctx.quadraticCurveTo(p1.x,p1.y,p2.x,p2.y);
ctx.strokeStyle='blue';
ctx.lineWidth=3;
ctx.stroke();
dot(p0);
dot(p1);
dot(p2);
dot(midpoint);
dot(perpendicularPoint)
$length.text('Curve length: '+parseInt(QCurveLength(p0,p1,p2)))
}
//
function dot(p){
ctx.beginPath();
ctx.arc(p.x,p.y,radius,0,PI2);
ctx.closePath();
ctx.fillStyle=p.color;
ctx.fill();
ctx.lineWidth=1;
ctx.strokeStyle='black';
ctx.stroke();
}
function setQLength(p0,p2,length,tolerance){
var dx=p2.x-p0.x;
var dy=p2.y-p0.y;
var alength=Math.sqrt(dx*dx+dy*dy);
// impossible to fit
if(alength>length){
alert('The points are too far apart to have length='+length);
return;
}
// fit
for(var distance=0;distance<200;distance++){
// calc the point perpendicular to midpoint at specified distance
var p=pointPerpendicularToMidpoint(p0,p2,distance);
p1.x=p.x;
p1.y=p.y;
// calc the result qCurve length
qlength=QCurveLength(p0,p1,p2);
// draw the curve
draw();
// break if qCurve's length is within tolerance
if(Math.abs(length-qlength)<tolerance){
break;
}
}
return(p1);
}
function pointPerpendicularToMidpoint(p0,p2,distance){
var dx=p2.x-p0.x;
var dy=p2.y-p0.y;
var perpAngle=Math.atan2(dy,dx)-Math.PI/2;
midpoint={ x:p0.x+dx*0.50, y:p0.y+dy*0.50, color:'purple' };
perpendicularPoint={
x: midpoint.x+distance*Math.cos(perpAngle),
y: midpoint.y+distance*Math.sin(perpAngle),
color:'cyan'
};
return(perpendicularPoint);
}
// Attribution: Mateusz Matczak
// http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/
function QCurveLength(p0,p1,p2){
var a={x: p0.x-2*p1.x+p2.x, y: p0.y-2*p1.y+p2.y}
var b={x:2*p1.x-2*p0.x, y:2*p1.y-2*p0.y}
var A=4*(a.x*a.x+a.y*a.y);
var B=4*(a.x*b.x+a.y*b.y);
var C=b.x*b.x+b.y*b.y;
var Sabc=2*Math.sqrt(A+B+C);
var A2=Math.sqrt(A);
var A32=2*A*A2;
var C2=2*Math.sqrt(C);
var BA=B/A2;
if(A2==0 || BA+C2==0){
var dx=p2.x-p0.x;
var dy=p2.y-p0.y;
var length=Math.sqrt(dx*dx+dy*dy);
}else{
var length=(A32*Sabc+A2*B*(Sabc-C2)+(4*C*A-B*B)*Math.log((2*A2+BA+Sabc)/(BA+C2)))/(4*A32)
}
return(length);
};
body{ background-color: ivory; }
#canvas{border:1px solid red; margin:0 auto; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<h4 id=length>Curve length:</h4>
<h4>Red,Gold == start and end points<br>Purple == midpoint between start & end<br>Cyan == middle control point.</h4>
<canvas id="canvas" width=300 height=300></canvas>
var angle = 0;
draw = function() {
background(0, 0, 0);
for(var j = 0;j<20;j++){
fill(j*100,j*10,j);
var offset = 0;
for(var i =-27;i<20;i++){
var a = angle +offset;
var h = map(sin(a),-1,1,100,300);
ellipse(i*20+j*20,h,20,20);
offset+=10;
}
}
angle+=2;
};
I am trying to draw a smooth oval using ctx.clip property in my canvas.I have done with drawing part i am facing problem with oval arc line clarity.Any one have any idea regarding this just let me know?
Here is my code.
<canvas id="c" width="400" height="400"></canvas>
var canvas = new fabric.Canvas('c');
var ctx = canvas.getContext('2d');
var cx=180;
var cy=200;
var w=300;
var h=250;
// Quadratric curves example
ctx.beginPath();
var lx = cx - w/2,
rx = cx + w/2,
ty = cy - h/2,
by = cy + h/2;
var magic = 0.551784;
var xmagic = magic*w/2;
var ymagic = h*magic/2;
ctx.moveTo(cx,ty);
ctx.bezierCurveTo(cx+xmagic,ty,rx,cy-ymagic,rx,cy);
ctx.bezierCurveTo(rx,cy+ymagic,cx+xmagic,by,cx,by);
ctx.bezierCurveTo(cx-xmagic,by,lx,cy+ymagic,lx,cy);
ctx.bezierCurveTo(lx,cy-ymagic,cx-xmagic,ty,cx,ty);
ctx.fill();
ctx.stroke();
ctx.clip();
var text;
text = new fabric.Text('Honey', {
fontSize: 50,
left: 150,
top: 150,
lineHeight: 1,
originX: 'left',
fontFamily: 'Helvetica',
fontWeight: 'bold'
});
canvas.add(text);
Here is my fiddle link
You can see this output over here the line border of oval not much clear.
try this it will help you
//Script
var canvas = new fabric.Canvas('c');
var w;
var h;
var ctx = canvas.getContext('2d');
w=canvas.width / 4;
h=canvas.height / 2.4;
canvas.clipTo = function(ctx) {
ctx.save();
ctx.scale(2, 1.2);
ctx.arc(w, h, 90, 0, 2 * Math.PI, true);
ctx.stroke();
ctx.restore();
}
Fiddle Demo
One problem is in the nature of your display screen...
Since pixels are rectangles and you're drawing a curve, your result will have "jaggies" as the curve tries to fit itself in rectangular spaces.
You can use an optical illusion to trick the eye into seeing a less jagged oval.
An optical trick:
Reduce the contrast between the background color and the oval color.
This is not a cure...the jaggies are still there.
But the eye recognizes less contrast and perceives the oval as more smooth.
So if your design accommodates this style change, this optical illusion might help.
Here's code and a Fiddle: http://jsfiddle.net/m1erickson/vDWR3/
var cx=180;
var cy=200;
var w=300;
var h=250;
// Start with a less-contrasting background
ctx.fillStyle="#ddd";
ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.beginPath();
var lx = cx - w/2,
rx = cx + w/2,
ty = cy - h/2,
by = cy + h/2;
var magic = 0.551784;
var xmagic = magic*w/2;
var ymagic = h*magic/2;
ctx.moveTo(cx,ty);
ctx.bezierCurveTo(cx+xmagic,ty,rx,cy-ymagic,rx,cy);
ctx.bezierCurveTo(rx,cy+ymagic,cx+xmagic,by,cx,by);
ctx.bezierCurveTo(cx-xmagic,by,lx,cy+ymagic,lx,cy);
ctx.bezierCurveTo(lx,cy-ymagic,cx-xmagic,ty,cx,ty);
ctx.fillStyle="#555";
ctx.strokeStyle=ctx.fillStyle;
ctx.lineWidth=1.5;
ctx.stroke();
Here is another alternative routine, but it looks visually the same as the other methods. This is primarily to do with the finite resolution of the display device, though you may be able to make some improvement using a thicker pencil, optical illusions or performing some anti-aliasing. Otherwise I think you will have to accept what you have.
Javascript
var canvas = new fabric.Canvas('c'),
ctx = canvas.getContext("2d"),
steps = 100,
step = 2 * Math.PI / steps,
h = 200,
k = 180,
r = 150,
factor = 0.8,
theta,
x,
y,
text;
ctx.beginPath();
for (theta = 0; theta < 2 * Math.PI; theta += step) {
x = h + r * Math.cos(theta);
y = k - factor * r * Math.sin(theta);
ctx.lineTo(x, y);
}
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.clip();
text = new fabric.Text('Honey', {
fontSize: 50,
left: 150,
top: 150,
lineHeight: 1,
originX: 'left',
fontFamily: 'Helvetica',
fontWeight: 'bold'
});
canvas.add(text);
jsfiddle
Note: by changing the number of steps and the factor, you can create other shapes: circles, squares, hexagons, other polygons ....
<canvas id="thecanvas" width="400" height="400"></canvas>
<script>
var canvas = document.getElementById('thecanvas');
if(canvas.getContext)
{
var ctx = canvas.getContext('2d');
drawEllipse(ctx, 10, 10, 100, 60);
drawEllipseByCenter(ctx, 60,40,20,10);
}
function drawEllipseByCenter(ctx, cx, cy, w, h) {
drawEllipse(ctx, cx - w/2.0, cy - h/2.0, w, h);
}
function drawEllipse(ctx, x, y, w, h) {
var kappa = .5522848,
ox = (w / 2) * kappa, // control point offset horizontal
oy = (h / 2) * kappa, // control point offset vertical
xe = x + w, // x-end
ye = y + h, // y-end
xm = x + w / 2, // x-middle
ym = y + h / 2; // y-middle
ctx.beginPath();
ctx.moveTo(x, ym);
ctx.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
ctx.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
ctx.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
ctx.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
ctx.closePath();
ctx.stroke();
}
</script>
http://jsbin.com/ovuret/2/edit
A possibility is to use a thicker line, it's not great but it's better
ctx.lineWidth = 3;
http://jsfiddle.net/xadqg/7/
I also noticed that using quadraticCurveTo seems to anti alias the line http://jsfiddle.net/d4JJ8/298/ I didn't change your code to use it but the jsfiddle I posted shows that it's true. You should modify your code and try it
Using HTML5 Canvas and Javascript I need to display different values (represented by a dot maybe) at different angles inside a circle.
Example data:
val 34% # 0°,
val 54% # 12°,
val 23% # 70°,
and so on...
If I have a canvas 300 x 300px and the center of the circle is located at x: 150px and y: 150px with a radius of 150px, how would I calculate where to set my dot for the value 54% at 12 degrees?
My math is kinda terrible xD
I'd appreciate any kind of help and please ask questions if I do not make myself clear enough.
Thank you for listening and thank you in advance for you deep insights :D
EDIT (to explain in more detail):
Here is an image to illustrate what I am trying to accomplish:
I hope this makes my question a little more understandable.
(As you can see, not the same values as above)
Ty for your patience!
You may use this to convert from polar (radius, angle) coordinates to cartesian ones :
// θ : angle in [0, 2π[
function polarToCartesian(r, θ) {
return {x: r*Math.cos(θ), y: r*Math.sin(θ)};
}
For example, if you want to draw at 12°, you may compute the point like this :
var p = polarToCartesian(150, 12*2*Math.PI/360);
p.x += 150; p.y += 150;
EDIT : my polarToCartesian function takes radians as input, as many function in the Canvas API. If you're more used to degrees, you may need this :
function degreesToRadians(a) {
return Math.PI*a/180;
}
Here you go (demo)
var can = document.getElementById('mycanvas');
var ctx = can.getContext('2d');
var drawAngledLine = function(x, y, length, angle) {
var radians = angle / 180 * Math.PI;
var endX = x + length * Math.cos(radians);
var endY = y - length * Math.sin(radians);
ctx.beginPath();
ctx.moveTo(x, y)
ctx.lineTo(endX, endY);
ctx.closePath();
ctx.stroke();
}
var drawCircle = function(x, y, r) {
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();
}
var drawDot = function(x, y, length, angle, value) {
var radians = angle / 180 * Math.PI;
var endX = x + length*value/100 * Math.cos(radians);
var endY = y - length*value/100 * Math.sin(radians);
drawCircle(endX, endY, 2);
}
var drawText = function(x, y, length, angle, value) {
var radians = angle / 180 * Math.PI;
var endX = x + length*value/100 * Math.cos(radians);
var endY = y - length*value/100 * Math.sin(radians);
console.debug(endX+","+endY);
ctx.fillText(value+"%", endX+15, endY+5);
ctx.stroke();
}
var visualizeData = function(x, y, length, angle, value) {
ctx.strokeStyle = "#999";
ctx.lineWidth = "1";
drawAngledLine(x, y, length, angle);
ctx.fillStyle = "#0a0";
drawDot(x, y, length, angle, value);
ctx.fillStyle = "#666";
ctx.font = "bold 10px Arial";
ctx.textAlign = "center";
drawText(x, y, length, angle, value);
}
ctx.fillStyle = "#FFF0B3";
drawCircle(150, 150, 150);
visualizeData(150, 150, 150, 0, 34);
visualizeData(150, 150, 150, 12, 54);
visualizeData(150, 150, 150, 70, 23)
visualizeData(150, 150, 150, 120, 50);
visualizeData(150, 150, 150, -120, 80);
visualizeData(150, 150, 150, -45, 60);