Javascript Canvas Draw Order - javascript

I am plotting a bar chart with some series values in a javascript loop. Even though the code drawing the series values lies below the code that draws the bars, in the next iteration of the loop the text that has just been drawn gets overwritten.
You can see this effect taking place in the code below as the alert boxes appear.
Why do drawing operations on another part of the canvas overwrite something drawn previously in a completely different place?
Update: Someone has helpfully pointed out that using fillRect() hides this issue, but my question is rather: why is it happening in the first place?
var exampleData = {
"Series 1": 10,
"Series 2": 14,
"Series 3": 2,
"Series 4": 12
};
var BarChart = function(options) {
this.options = options;
this.canvas = options.canvas;
this.ctx = this.canvas.getContext("2d");
this.colors = options.colors;
this.plot = function() {
var maxValue = 0;
for (var categ in this.options.data) {
maxValue = Math.max(maxValue, this.options.data[categ]);
}
var noBars = Object.keys(this.options.data).length;
var barWidth = (this.canvas.height) / noBars;
var barIdx = 0;
for (categ in this.options.data) {
var barLength = Math.round(this.canvas.width * this.options.data[categ] / maxValue);
this.ctx.save();
alert("plotting series line " + categ);
this.ctx.fillStyle = this.colors[barIdx % this.colors.length];
this.ctx.rect(30, barIdx * barWidth, barLength, barWidth);
this.ctx.fill();
alert("plotting series value " + categ);
this.ctx.fillStyle = "#000000";
this.ctx.font = "24px Georgia";
this.ctx.textBaseline = "middle";
this.ctx.fillText(this.options.data[categ], 25, barIdx * barWidth + barWidth / 2); //will be covered in the loop's next iteration. Why?
this.ctx.restore();
barIdx++;
}
}
}
function init() {
var myCanvas = document.getElementById("myCanvas");
myCanvas.width = 800;
myCanvas.height = 300;
var ctx = myCanvas.getContext("2d");
var myBarChart = new BarChart({
canvas: myCanvas,
seriesName: "Example Series",
padding: 40,
data: exampleData,
colors: ["#D1E3F3", "#D1E3F3", "#D1E3F3", "#D1E3F3"]
});
myBarChart.plot();
}
document.addEventListener("DOMContentLoaded", init, false);
<canvas id="myCanvas"></canvas>

Changing :
this.ctx.rect(30, barIdx * barWidth, barLength, barWidth);
this.ctx.fill();
To instead use fillRect() fixes the problem:
this.ctx.fillRect(30, barIdx * barWidth, barLength, barWidth);
Working example here (compared to the original example here)

Related

How do I implement color collision in JavaScript using getImageData?

In my code:
var canvas = document.createElement('canvas');
canvas.width = 1600;
canvas.height = 900;
document.body.appendChild(canvas);
var similarX = 0;
var similarY = 0;
document.addEventListener('mousemove',function(event){
similarX = event.clientX;
similarY = event.clientY;
document.getElementById('body').innerHTML = "x:" + similarX + ", y:" + similarY +", imgData:" + pixelData;
var pixelData = rect.getImageData(60, 60, 1, 1).data;
})
window.addEventListener('load' , start);
var c = canvas.getContext('2d')
var rect = canvas.getContext ('2d')
var circle = canvas.getContext ('2d')
function start() {
c.clearRect(0, 0, 1600, 900);
c.fillStyle = 'green' ;
c.fillRect(similarX - 12, similarY - 50, 20, 20);
rect.fillStyle = 'black';
rect.fillRect(50,50,80,80);
window.requestAnimationFrame(start)
}
document.body.appendChild(canvas);
I want to detect what color are the pixels around the green player square so that if it touches the black box, it is detected by a getImageData() command? I tried reading the other posts, but I couldn't find a way to use them. Is there a solution that can easily be placed inside the code?

How to find intersection point of opposite lines of two lines

I am trying to find intersection point of opposite lines of two lines:
var ax=0.00, ay=0.50 ;
var bx=1.00, by=1.00 ;
var cx=2.00, cy=0.25 ;
But I am very confused about finding an opposite of a line.
Here is a jsfiddle which points are converted between 0.0-1.0
So how to find that intersection?
I've taken the liberty of clearing out your code a bit and adding a few objects in order to make recollection of data a bit easier. Added: Point, Line objects and an associated method draw() on both of them.
In order to do this, you first need to calculate the median point. This is pretty easy, with point (a,b) and (c,d) for your line, the median point is ( (a+c)/2, (b+d)/2 ). This will be where your opposite line starts from.
From there, you can calculate the opposite gradient by taking the gradient of your line (grad = (d-b)/(a-c)) and working out -1/grad (since perpendicular lines have opposite gradients). This is the opposite() function I defined.
From here, you have your two opposite lines, you just need to find the intersection. You have both equations for both lines (since you know that a line is y = g*x + r where g is the gradient and r is the y-value at origin), so you can easily figure out the value where (x,y) is the same on both lines. If you can't, go on the maths stackexchange site and post that question there.
function Point(x,y) {
this.x = x;
this.y = y;
}
Point.prototype.draw = function(color="blue") {
var diff = 0.0125 ;
(new Line(this.x-diff, this.y-diff, this.x-diff, this.y+diff)).draw("normal", color);
(new Line(this.x-diff, this.y+diff, this.x+diff, this.y+diff)).draw("normal", color);
(new Line(this.x+diff, this.y+diff, this.x+diff, this.y-diff)).draw("normal", color);
(new Line(this.x+diff, this.y-diff, this.x-diff, this.y-diff)).draw("normal", color);
}
function Line(x1, y1, x2, y2) {
this.point1 = new Point(x1, y1);
this.point2 = new Point(x2, y2);
}
Line.prototype.draw = function(style="normal", color="black") {
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
ctx.save();
if (style == "dashed-back") {
ctx.setLineDash([5,3]); }
ctx.strokeStyle = color ;
ctx.beginPath();
ctx.moveTo(xp(this.point1.x), yp(this.point1.y));
ctx.lineTo(xp(this.point2.x), yp(this.point2.y));
ctx.stroke();
ctx.restore();
return this;
}
Line.prototype.intersect = function(otherLine) {
var grad1 = (this.point2.y - this.point1.y)/(this.point2.x - this.point1.x);
var grad2 = (otherLine.point2.y - otherLine.point1.y)/(otherLine.point2.x - otherLine.point1.x);
var remainder1 = this.point1.y - this.point1.x * grad1;
var remainder2 = otherLine.point1.y - otherLine.point1.x * grad2;
var x = (remainder2 - remainder1)/(grad1 - grad2);
var y = grad1 * x + remainder1;
return new Point(x, y);
}
Line.prototype.opposite = function(style = "normal", color = "Black") {
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
ctx.save();
var midway = new Point((this.point1.x + this.point2.x)/2, (this.point1.y + this.point2.y)/2);
var invgrad = -1 * 1/(this.point2.y - this.point1.y)/(this.point2.x - this.point1.x);
return new Line(midway.x - 100, midway.y - 100*invgrad, midway.x+100, midway.y + 100 * invgrad);
}
// Normalize points for normal plot
function xp (x) { return x*300+50 ; }
function yp (y) { return 450-(y*300) ; }
// Write a text
function text (str,x,y,size=12,color="black") {
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
ctx.save();
ctx.font = size+"px Arial" ;
ctx.fillStyle = color;
ctx.fillText(str,xp(x),yp(y));
ctx.restore();
}
// Init guides
function init () {
new Line(0, 0, 0, 1).draw("dashed-back", "#888");
new Line(0, 1, 3, 1).draw("dashed-back", "#888");
new Line(3, 1, 3, 0).draw("dashed-back", "#888");
new Line(1, 0, 1, 1).draw("dashed-back", "#888");
new Line(2, 0, 2, 1).draw("dashed-back", "#888");
text("1",-0.05,0.95)
text("0",-0.05,-0.05)
text("1",1,-0.05)
text("2",2,-0.05)
text("3",3,-0.05)
}
init();
var ax=0.00, ay=0.50 ;
var bx=1.00, by=1.00 ;
var cx=2.00, cy=0.25 ;
var dx=3.00, dy=0.75 ;
new Point(ax,ay).draw("red");
new Point(bx,by).draw("red");
new Point(cx,cy).draw("red");
new Point(dx,dy).draw("red");
var line1 = new Line(ax, ay, bx, by).draw("normal", "blue");
var line2 = new Line(bx, by, cx, cy).draw("normal", "blue");
var line3 = new Line(cx, cy, dx, dy).draw("normal", "blue");
line2.opposite().draw("normal", "red");
line1.opposite().draw("normal", "orange");
line1.opposite().intersect(line2.opposite()).draw("normal", "purple");
<canvas id="myCanvas" width="1000" height="600">
Caveat: I had a pretty big error in my code - the gradient for opposite lines was miscalculated as -1 * grad as opposed to -1 / grad.

Creating an array of canvas objects

I have read that you can make an array of anything. In my efforts to go forth in my studies of javaScript/canvas, I set out to create an array of shapes. The idea is to have an array of three circles. Math.floor will be used to grab one element/circle and display it on the canvas. I put together code that, well, makes sense to me... I've created an array, I have filled the array, I am grabbing a random element from the array... I have not yet reached the point of having to display it on the canvas, because not matter my approach, all three circles are always on the canvas. It would be so cool to grasp this concept. Can you tell me why this code doesn't work? Thank you in advance.
<script>
var canvas = document.getElementById("canvas");
var c = canvas.getContext("2d");
var objects = [];
objects[0] =
[c.beginPath(),
c.lineWidth = 5,
c.strokeStyle = 'red',
c.arc(200, 200, 50, 0, Math.PI * 2, false),
c.stroke()];
objects[1] =
[c.beginPath(),
c.lineWidth = 5,
c.strokeStyle = 'dimgray',
c.arc(600, 200, 50, 0, Math.PI * 2, false),
c.stroke()];
objects[2] =
[c.beginPath(),
c.lineWidth = 5,
c.strokeStyle = 'purple',
c.arc(1000, 200, 50, 0, Math.PI * 2, false),
c.stroke()];
for (var i = 0; i < objects.length; i++) {
objects[i] = Math.floor(Math.random() * 3);}
</script>
You should make a function drawCircle which takes a canvas and a color as arguments
const drawCircle = (canvas, color = "red") =>
{
canvas.beginPath ()
canvas.lineWidth = 5
canvas.strokeStyle = color
canvas.arc (95, 50, 40, 0, 2 * Math.PI)
canvas.stroke ()
}
const getCanvas = (id) =>
document.getElementById(id).getContext("2d")
drawCircle (getCanvas('canvas-1')) // default "red"
drawCircle (getCanvas('canvas-2'), 'dimgray')
drawCircle (getCanvas('canvas-3'), 'purple')
<canvas id="canvas-1"></canvas>
<canvas id="canvas-2"></canvas>
<canvas id="canvas-3"></canvas>
We could add additional parameters for position, radius, and line width, but then how would we know which order to pass them in? A better option would be to pass a descriptor object with default values.
const drawCircle = (canvas, descriptor = {}) =>
{
const { x = 95
, y = 50
, radius = 40
, lineWidth = 5
, color = "red"
} = descriptor
canvas.beginPath ()
canvas.lineWidth = lineWidth
canvas.strokeStyle = color
canvas.arc (x, y, radius, 0, 2 * Math.PI)
canvas.stroke ()
}
const getCanvas = (id) =>
document.getElementById(id).getContext("2d")
drawCircle (getCanvas('canvas-1')) // default styles
drawCircle (getCanvas('canvas-2'), { color: 'dimgray' })
drawCircle (getCanvas('canvas-3'), { color: 'purple', radius: 10 })
<canvas id="canvas-1"></canvas>
<canvas id="canvas-2"></canvas>
<canvas id="canvas-3"></canvas>
Above, we use a named parameter descriptor but we could also could've inlined it. Because the descriptor contains so many properties, the readability suffers a bit and makes the previous version of our function a little nicer.
const drawCircle = (canvas, { x=95, y=50, radius=40, lineWidth=5, color="red" } = {}) => {
canvas.beginPath ()
canvas.lineWidth = lineWidth
canvas.strokeStyle = color
canvas.arc (x, y, radius, 0, 2 * Math.PI)
canvas.stroke ()
}
Now, using drawCircle you could create presets, like
const FatPurpleCircle = canvas =>
drawCircle (canvas, { color: "purple", lineWidth: 10 })
const SmallBlueCircle = canvas =>
drawCircle (canvas, { color: "blue", radius: 5 })
And you could have an array of these presets called circles. Given a function sample that returns a random element of an array, we can select a random circle function, and use it to draw its contents on a supplied canvas
const circles =
[ FatPurpleCircle, SmallBlueCircle, ... ]
const sample = (arr = []) => {
const size = arr.length
const rand = Math.floor (Math.random () * size)
return arr[rand]
}
const drawRandomCircle =
sample (circles)
drawRandomCircle (getCanvas('canvas-1'))
One option you have is to store each "canvas" in a function, which are then all stored in an array. This will end up having close to the same format that you had previously, but via a different method. #Patrick Roberts mentioned a similar method in his comment.
var objects = [
function() {
var canvas = document.getElementById("canvas-1");
var c = canvas.getContext("2d");
c.beginPath();
c.lineWidth = 5;
c.strokeStyle = 'red';
c.arc(95,50,40,0,2*Math.PI);
c.stroke();
},
function() {
var canvas = document.getElementById("canvas-2");
var c = canvas.getContext("2d");
c.beginPath();
c.lineWidth = 5;
c.strokeStyle = 'dimgray';
c.arc(95,50,40,0,2*Math.PI);
c.stroke();
},
function() {
var canvas = document.getElementById("canvas-3");
var c = canvas.getContext("2d");
c.beginPath();
c.lineWidth = 5;
c.strokeStyle = 'purple';
c.arc(95,50,40,0,2*Math.PI);
c.stroke();
}
];
objects[0]();
objects[2]();
objects[1]();
Here is a Codepen demonstrating this implementation.
As to why it doesn't work, #Xofox explained it very well in his comment:
[...] you can’t store lines of code in an array. c.arc(600, 200, 50, 0, Math.PI * 2, false) will execute that method, then return undefined. The array stores the result undefined.

How to (re)fill circle in canvas - requestAnimationFrame - HTML, JS

how can I fill the "new" canvas circle that appears next to the older one.
There is no problem with rectangle for example:
**
ctx.fillStyle = 'rgba('+quadratto.r+','+quadratto.g+','+quadratto.b+',1)';
quadratto.x += quadratto.speedX;
quadratto.y += quadratto.speedY;
quadratto.speedY += quadratto.speedY*(-0.15);
ctx.fillRect(quadratto.x-quadratto.h/4, quadratto.y-quadratto.h/2, 2, 2);**
What I want to do?
I'm creating animation in canvas where random-sized-color circle will appear and
it will move in a specified direction. The new canvas layaer will appear in the next frame (fps) with a new(old) circle.
var myCanvasPattern = document.createElement('canvas');
myCanvasPattern.width = window.innerWidth;
myCanvasPattern.height = window.innerHeight;
document.body.appendChild(myCanvasPattern);
var ryC = myCanvasPattern.getContext('2d');
function lottery(min, max) {
return Math.floor(Math.random()*(max-min+1))+min;
}
var allQuadro = [];
var fps = 50;
var lastTime = 0;
animationLoop();
function animationLoop(time){
requestAnimationFrame( animationLoop );
if(time-lastTime>=1000/fps){
lastTime = time;
for(var i=0;i<10;i++){
allQuadro.push({
r : lottery(0, 240),
g : lottery(0, 240),
b : lottery(0, 240),
circleR : lottery(10, 30),
x : myCanvasPattern.width/2,
y : myCanvasPattern.height/2,
speedX : lottery(-1000,1000)/100,
speedY : lottery(-1000,1000)/100
})
}
ryC.fillStyle = 'rgba(255,255,255,0.2)';
ryC.fill(0,0,myCanvasPattern.width, myCanvasPattern.height);
for(var i=0; i<allQuadro.length;i++){
var circle = allQuadro[i];
ryC.fillStyle = 'rgba('+circle.r+','+circle.g+','+circle.b+',1)';
circle.x += circle.speedX;
circle.y += circle.speedY;
//HERE's THE PROBLEM BELOW. HOW TO CREATE NEW ONE THAT APPEARS NEXT TO PREVIOUS ONE WITH NEW RANDOM COLOR
ryC.arc(circle.x-circle.circleR/2, circle.y-circle.circleR/2, circleR, 0, 2 * Math.PI);
//ryC.fill();
}
// ryC.fillStyle = 'rgba('+r+','+g+','+b+',1)';
//ryC.arc(x+speedX, y+speedY, circleR, 0, 2 * Math.PI);
//ryC.fill();
}
}
body {
margin: 0;
padding: 0;
overflow: hidden;
}
The fillRect() will fill directly to the canvas without going via a path (versus for example rect()).
The arc() on the other hand will add to a path which needs to be filled later. It also require the path to be cleared in-between the calls using beginPath().
A simple way to think about it is to wrap the necessary code into a function that acts like fillRect():
function fillArc() {
ctx.beginPath(); // clear current path
ctx.arc.apply(ctx, arguments); // apply arguments to arc() call
ctx.fill();
// notice that the arc still exist on the path after this call
// so to "truly" behave like fillRect() you could consider an
// additional beginPath() here.. it will depend on your code
}
In action:
var ctx = c.getContext("2d");
ctx.fillStyle = "#09f";
fillArc(70, 70, 70, 0, 6.28);
ctx.fillStyle = "#0a9";
fillArc(220, 70, 70, 0, 6.28);
function fillArc() {
ctx.beginPath();
ctx.arc.apply(ctx, arguments);
ctx.fill();
}
<canvas id=c></canvas>
If you are bold you can also add the method to the context itself before calling getContext():
CanvasRenderingContext2D.prototype.fillArc = function() {
this.beginPath();
this.arc.apply(this, arguments);
this.fill();
}
The use it like any other method:
ctx.fillArc( ... );
CanvasRenderingContext2D.prototype.fillArc = function() {
this.beginPath();
this.arc.apply(this, arguments);
this.fill();
}
var ctx = c.getContext("2d");
ctx.fillStyle = "#09f";
ctx.fillArc(70, 70, 70, 0, 6.28);
ctx.fillStyle = "#0a9";
ctx.fillArc(220, 70, 70, 0, 6.28);
<canvas id=c></canvas>

Chart.js - Positioning Donut Label

I have a web page that is using Chart.js. In this page, I am rendering three donut charts. In the middle of each chart, I want to show the percentage of the donut that is filled. Currently, I have the following code, which can be seen in this Fiddle.
function setupChart(chartId, progress) {
var canvas = document.getElementById(chartId);
var context = canvas.getContext('2d');
var remaining = 100 - progress;
var data = {
labels: [ 'Progress', '', ],
datasets: [{
data: [progress, remaining],
backgroundColor: [
'#8FF400',
'#FFFFFF'
],
borderColor: [
'#8FF400',
'#408A00'
],
hoverBackgroundColor: [
'#8FF400',
'#FFFFFF'
]
}]
};
var options = {
responsive: true,
maintainAspectRatio: false,
scaleShowVerticalLines: false,
cutoutPercentage: 80,
legend: {
display: false
},
animation: {
onComplete: function () {
var xCenter = (canvas.width / 2) - 20;
var yCenter = canvas.height / 2 - 40;
context.textAlign = 'center';
context.textBaseline = 'middle';
var progressLabel = data.datasets[0].data[0] + '%';
context.font = '20px Helvetica';
context.fillStyle = 'black';
context.fillText(progressLabel, xCenter, yCenter);
}
}
};
Chart.defaults.global.tooltips.enabled = false;
var chart = new Chart(context, {
type: 'doughnut',
data: data,
options: options
});
}
The chart "runs". But the problem is with the rendering of the label. The label is not vertically and horizontally centered within the middle of the donut. The position of the label changes based on the screen resolution too. You can see the label change position by resizing the frame that the charts are rendered in within the Fiddle.
My question is, how do I consistently position the percentage in the middle of the donut? My page is a responsive page so, having consistent positioning is important Yet, I'm not sure what else to do. I feel like the textAlign and textBaseline properties aren't working, or I'm misunderstanding their usage.
Thanks!
According to this answer, you could do it with a plugin in a further version. I don't know if you noticed, but if you increase the width and reduce it over and over again on your fiddle, the height of your canvas gets bigger and bigger (the chart goes further and further down).
On the version you are using, you could extend the doughnut controller like this:
http://jsfiddle.net/ivanchaer/cutkqkuz/1/
Chart.defaults.DoughnutTextInside = Chart.helpers.clone(Chart.defaults.doughnut);
Chart.controllers.DoughnutTextInside = Chart.controllers.doughnut.extend({
draw: function(ease) {
var ctx = this.chart.chart.ctx;
var data = this.getDataset();
var easingDecimal = ease || 1;
Chart.helpers.each(this.getDataset().metaData, function(arc, index) {
arc.transition(easingDecimal).draw();
var vm = arc._view;
var radius = (vm.outerRadius + vm.innerRadius) / 2;
var thickness = (vm.outerRadius - vm.innerRadius) / 2;
var angle = Math.PI - vm.endAngle - Math.PI / 2;
ctx.save();
ctx.fillStyle = vm.backgroundColor;
ctx.font = "20px Verdana";
ctx.textAlign = 'center';
ctx.textBaseline = "middle";
var text = data.data[0] + '%';
ctx.fillStyle = '#000';
ctx.fillText(text, $(ctx.canvas).width()/2, $(ctx.canvas).height()/2);
});
ctx.fillStyle = '#fff';
ctx.fill();
ctx.restore();
}
});
function setupChart(chartId, progress) {
var canvas = document.getElementById(chartId);
var context = canvas.getContext('2d');
var remaining = 100 - progress;
var deliveredData = {
labels: [
"Value"
],
datasets: [{
data: [progress, remaining],
backgroundColor: [
'#8FF400',
'#FFFFFF'
],
borderColor: [
'#8FF400',
'#408A00'
],
hoverBackgroundColor: [
'#8FF400',
'#FFFFFF'
]
}]
};
var deliveredOpt = {
cutoutPercentage: 88,
animation: false,
legend: {
display: false
},
tooltips: {
enabled: false
}
};
var chart = new Chart(canvas, {
type: 'DoughnutTextInside',
data: deliveredData,
options: deliveredOpt
});
}
setupChart('standChart', 33);
setupChart('moveChart', 66);
setupChart('exerciseChart', 99);
See this http://jsfiddle.net/uha5tseb/2/
It can be handled with getting width/height of each chart by this reference
onComplete: function (event) {
console.log(this.chart.height);
var xCenter = this.chart.width/2;
var yCenter = this.chart.height/2;
context.textAlign = 'center';
context.textBaseline = 'middle';
var progressLabel = data.datasets[0].data[0] + '%';
context.font = '20px Helvetica';
context.fillStyle = 'black';
context.fillText(progressLabel, xCenter, yCenter);
}
Hope this solves your problem!
You set your variables incorrectly when you get xCenter and yCenter. Currently they don't get the middle of the canvas.
You have:
var xCenter = (canvas.width / 2) - 20;
var yCenter = canvas.height / 2 - 40;
It should be:
var xCenter = (canvas.width / 2);
var yCenter = (canvas.height / 2);
Im not familiar with chart.js, but as far as I understood from the documentation, they provide generateLegend method for your purposes. It returns a data from legendCallback that you can display on the page outside of canvas.
Container of the legend text can be aligned relatively to the canvas's wrapper.
HTML:
<div class="chart" data-legend="">
<canvas id="standChart"></canvas>
</div>
JS:
var container = canvas.parentElement;
...
container.setAttribute('data-legend', chart.generateLegend());
Fiddle
One way to resolve this is by adding an absolutely positioned element on top of the canvas.
You can add a div element after each canvas element and then set its style to:
.precentage {
font-family: tahoma;
font-size: 28px;
left: 50%;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
}
Here is a complete JSFiddle.

Categories