I'm trying to draw lines progressively (currently using recursive functions) in a canvas element, and I'm able to successfully draw pairs of lines that are parallel to the x or y axes, this way:
function line(xa, ya, xb, yb) {
context.beginPath();
context.moveTo(xa, ya);
context.lineTo(xb, yb);
context.stroke();
}
(function drawLine(i){
line(155, i, 155, i-2);
line(245, i, 245, i-2);
if (i > 35) {
setTimeout(function(){
drawLine(i-2);
}, 10);
}
})(57);
And I get this:
(context.lineWidth is set to 10 and context.lineCap is set to round)
However, I've tried several ways of drawing pairs of lines that aren't strictly horizontal nor vertical but I'm only able to get something like this:
(function drawLine(i){
line(155, i, 155+(57-i)/2, i-2);
line(245, i, 245-(57-i)/2, i-2);
if (i > 35) {
setTimeout(function(){
drawLine(i-2);
}, 10);
}
})(57);
(changing the value of context.lineWidth or context.lineCap doesn't solve the problem)
Is there a way to draw any kind of line progressively in a canvas element?
In your first version, you draw lines from a point based on the current value of i to a point based on the value of i in the next iteration. But in the second version, the x value of your start point is a constant. Base the start point on i, and the end point on i - 2:
let c = document.querySelector('canvas');
let context = c.getContext('2d');
context.lineWidth = 10;
context.lineCap = 'round';
function line(xa, ya, xb, yb) {
context.beginPath();
context.moveTo(xa, ya);
context.lineTo(xb, yb);
context.stroke();
}
(function drawLine(i){
line(155 + (57 - i) / 2, i, 155 + (57 - (i - 2)) / 2, (i - 2));
line(245 - (57 - i) / 2, i, 245 - (57 - (i - 2)) / 2, (i - 2));
if (i > 35) {
setTimeout(function(){
drawLine(i-2);
}, 10);
}
})(57);
<canvas></canvas>
The easiest way is to use Canvas.js:
const canvas = new Canvas('my-canvas', 200, 200).start();
const line1 = new Canvas.Line({
from: {
x: 50,
y: 70
},
to: {
x: 60,
y: 30
},
lineWidth: 7,
lineCap: 'round',
lineLength: 0.1
});
canvas.addElement(line1);
line1.animate('lineLength', {lineLength: 1, duration: 500});
const line2 = new Canvas.Line({
from: {
x: 90,
y: 70
},
to: {
x: 80,
y: 30
},
lineWidth: 7,
lineCap: 'round',
lineLength: 0.1
});
canvas.addElement(line2);
line2.animate('lineLength', {lineLength: 1, duration: 500});
<script src="https://gustavgenberg.github.io/handy-front-end/Canvas.js"></script>
Using line dash to animate along paths.
A simple way to animate along any path is to use the line dash and line dash offset.
const ctx = canvas.getContext('2d');
ctx.lineWidth = 10;
ctx.lineCap = 'round';
function drawLines(){
function drawLine(x1, y1, x2, y2){
ctx.moveTo(x1,y1);
ctx.lineTo(x2,y2);
}
drawLine(10,10,490,90);
drawLine(10,190,490,110);
}
var lineLength = 30; // pixels
var distanceBetween = 400;
var lineSpeed = 300; //pixels per second
ctx.setLineDash([lineLength, distanceBetween]);
function animateLines(time){
ctx.lineDashOffset = -time * lineSpeed / 1000;
ctx.stroke();
}
function loop(time){
ctx.clearRect(0,0,ctx.canvas.width,ctx.canvas.height);
ctx.beginPath();
drawLines();
animateLines(time);
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
<canvas id="canvas" width=500 height=200></canvas>
Related
How do I emulate Gravity with canvas Objects having only a few variables to work with.
I created the base canvas, game, time, score, everything, even gameStates, but I'm stuck on the part where you "add the velocity and the gravity factor into the Player Y variables".
I tried multiplying the gravity factor by a specified value, and then adding it to the yVel and then adding that to the actual Y value, but I cant translate the positioning correctly.
I think if I figured out how to "create gravity" creating jumping, moving right, and moving left wouldnt be too difficult.
Heres the main code im using to look for the platforms:
map.plates represents an array full of arrays which each contain 4 values for a plate (platform plate)
e is the map.plates.Arrays. playY is basically the player's exactly Y Height,, all rendered into fillRect();
function detectGravity() {
map.plates.forEach(e => {
if (playY > e[1] && playX <= e[0] && playX >= e[0] + e[2]) {
} else {
playY += 0; // Gravity Calculations here
}
});
}
I dont really know if I should include anything else here, but if you want the whole project, see the snippet below.
If theres anything wrong with the question, please tell me, I havent been on here in nearly half a year.
Full code incase codepen dies (suggested in comments):
"esversion: 6";
const can = document.querySelector(".block"),
ctx = can.getContext("2d"),
mScore = 100,
map = {
plates: [
[25, 25, 25, 2],
[75, 25, 25, 2],
[125, 25, 25, 2],
[175, 25, 25, 2],
[225, 25, 25, 2],
[25, 75, 25, 2],
[75, 62, 25, 2],
[125, 50, 25, 2],
[175, 38, 25, 2],
[25, 87, 25, 2],
[75, 100, 25, 2]
],
moneys: [
[25, 25],
[125, 25],
[225, 25],
[75, 62],
[75, 100]
],
player: [25, 25, 2, 2],
badSpt: []
};
let score = 0,
time = 60,
gameOn = 0;
let playX,
playY,
velX,
velY,
grav = 1.05;
can.addEventListener("click", startGame);
function startGame() {
if (gameOn != 1) {
gameOn = 1;
init();
gameTime = setInterval(() => {
if (time != 0) {
time -= 1;
}
}, 1000);
}
}
function init() {
can.width = 300;
can.height = 300;
drawEnviron();
drawLevel();
drawPlayer();
drawGame();
drawPixels();
if (time == 0) {
clearInterval(gameTime);
time = 60;
gameOn = 2;
}
window.requestAnimationFrame(init);
}
function drawEnviron() {
with (ctx) {
fillStyle = "#000000";
fillRect(0, 0, can.width, can.height);
fillStyle = "rgba(255, 255, 255, 0.5)";
fillRect(0, 0, can.width, can.height);
fillStyle = "#000000";
fillRect(0, 0, can.width, can.height / 15);
fillStyle = "#ffffff";
font = can.height / 15 + "px Verdana";
fillText("Score: " + score + "/" + mScore, 1, can.height / 19);
fillText("Time: " + time, can.width / 1.5 + 6, can.height / 19);
}
}
function drawLevel() {
map.plates.forEach(e => {
ctx.fillStyle = "#ffffff";
ctx.fillRect(e[0], can.height / 15 + e[1], e[2], e[3]);
});
map.moneys.forEach(e => {
ctx.beginPath();
ctx.fillStyle = "#fcba03";
ctx.arc(e[0] + 12.5, e[1] + 12.5, 4, 0, 2 * Math.PI);
ctx.fill();
});
}
function detectGravity() {
map.plates.forEach(e => {
if (playY > e[1] && playX <= e[0] && playX >= e[0] + e[2]) {
} else {
playY += 0;
}
});
}
function drawPlayer() {
const a = map.player;
if (gameOn == 0 || gameOn == 2) {
playX = a[0];
playY = a[1];
velX = 0;
velY = 0;
}
ctx.beginPath();
ctx.fillStyle = "#ff0000";
ctx.arc(playX + 12.5, playY + 12.5, 4, 0, 2 * Math.PI);
ctx.fill();
}
function drawGame() {
if (gameOn == 0) {
can.style.animation = "none";
with (ctx) {
fillStyle = "rgba(0, 0, 0, 0.5)";
fillRect(0, 0, can.width, can.height);
strokeStyle = "#000000";
lineWidth = 5;
fillStyle = "#ffffff";
textAlign = "center";
strokeText("Click to Start", 150, can.height / 4);
fillText("Click to Start", 150, can.height / 4);
}
} else if (gameOn == 2) {
can.style.animation = "0.2s flash infinite";
with (ctx) {
fillStyle = "rgba(0, 0, 0, 0.5)";
fillRect(0, 0, can.width, can.height);
strokeStyle = "#000000";
lineWidth = 5;
fillStyle = "#ff0000";
textAlign = "center";
strokeText("-- Game Over --", 150, can.height / 4);
fillText("-- Game Over --", 150, can.height / 4);
}
} else {
can.style.animation = "none";
}
}
function drawPixels() {
var fw = (can.width / 2) | 0,
fh = (can.height / 2) | 0;
ctx.imageSmoothingEnabled = ctx.mozImageSmoothingEnabled = ctx.msImageSmoothingEnabled = ctx.webkitImageSmoothingEnabled = false;
ctx.drawImage(can, 0, 0, fw, fh);
ctx.drawImage(can, 0, 0, fw, fh, 0, 0, can.width, can.height);
}
init();
* {
box-sizing: border-box;
overflow: hidden;
}
.block {
border: 2px solid black;
}
#keyframes flash {
0%, 100% {
border: 2px solid black;
}
50% {
border: 2px solid red;
}
}
<canvas class="block"></canvas>
Simple step based gravity.
Gravity
Gravity manifests as a change in speed over time (acceleration). It has a direction and magnitude (a vector)
We define the gravity vector going down the canvas
const gravity = {x: 0, y: 1};
Normally we apply gravity over time units of seconds. This is not a handy unit for animation. In this case we can define it as pixels per frame. A frame is 1/60th of a second. Thus the gravity defined above having a magnitude of 1 pixel per tick squared. So in one second an object would be traveling at 60 pixels per tick or 3600 pixels per second.
This is a little too fast for most animations so we can slow it down somewhat
const gravity = {x: 0, y: 0.1};
The object
An object has a position (a coordinate) and a velocity (a vector) having a direction and magnitude.
const object = {
pos: {x: 0, y: 0}, // position
vel: {x, 0, y: 0}, // velocity
}
To simulate gravity on this object we can add a behavior in the form of a function. In this case we can call it update. In the update function we accelerate the object, by adding the gravity vector to the velocity vector (object.vel). Then we update the position by adding the velocity vector object.vel to the position coordinate object.pos
const gravity = {x: 0, y: 0.1};
const object = {
pos: {x: 0, y: 0}, // position
vel: {x, 0, y: 0}, // velocity
update() {
this.vel.x += gravity.x;
this.vel.y += gravity.y;
this.pos.x += this.vel.x;
this.pos.y += this.vel.y;
}
}
The world
By its self this object will fall forever so we need to have it interact with the world. We can define a ground line. In its most basic a line at a y position on the canvas.
const ground = ctx.canvas.height; // ground at bottom of canvas.
To interact we need to add to the objects update function. In this we check the object position against the ground. If the position is below the ground, we move the position up away from the ground the same distance it has moved into the ground, and reverse the velocity (bounced).
We can define the springyness of the ground as a fraction of the velocity.
We also need to give the object a size.
Thus we get.
const gravity = {x: 0, y: 0.1};
const ground = ctx.canvas.height; // ground at bottom of canvas.
const bounce = 0.5;
const object = {
pos: {x: 0, y: 0}, // position
vel: {x, 0, y: 0}, // velocity
size: {w: 10, h: 10},
update() {
this.vel.x += gravity.x;
this.vel.y += gravity.y;
this.pos.x += this.vel.x;
this.pos.y += this.vel.y;
const g = ground - this.size.h; // adjust for size
if(this.pos.y >= g) {
this.pos.y = g - (this.pos.y - g); //
this.vel.y = -Math.abs(this.vel.y) * bounce; // change velocity to moving away.
}
}
}
Then all that is needed is to call update every frame and draw the object at the correct position.
Demo
Putting it into practice.
A simple box called object falls from the top of the canvas and hits the ground (bottom of the canvas) bounces a bit and stop. (Click to reset)
Update: I forgot to check if the object is at rest.
The math will have the box vibrate and never really stop moving if we don't add a little extra code to update.
The box will now appear to come to a complete stop when its bounce is less than gravity. See comment // check for rest.
const ctx = canvas.getContext("2d");
canvas.width = innerWidth-4;
canvas.height = innerHeight-4;
requestAnimationFrame(mainLoop); // starts the animation
const gravity = {x: 0, y: 0.1};
const ground = ctx.canvas.height; // ground at bottom of canvas.
const bounce = 0.9; // very bouncy
const object = {
pos: {x: ctx.canvas.width / 2, y: 0}, // position halfway on canvas
vel: {x: 0, y: 0}, // velocity
size: {w: 10, h: 10},
update() {
this.vel.x += gravity.x;
this.vel.y += gravity.y;
this.pos.x += this.vel.x;
this.pos.y += this.vel.y;
const g = ground - this.size.h; // adjust for size
if(this.pos.y >= g) {
this.pos.y = g - (this.pos.y - g); //
this.vel.y = -Math.abs(this.vel.y) * bounce;
if (this.vel.y >= -gravity.y) { // check for rest.
this.vel.y = 0;
this.pos.y = g - gravity.y;
}
}
},
draw() { ctx.fillRect(this.pos.x, this.pos.y, this.size.w, this.size.h) },
reset() { this.pos.y = this.vel.y = this.vel.x = 0 },
}
function mainLoop() {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
object.update(); // move object
object.draw();
requestAnimationFrame(mainLoop);
}
canvas.addEventListener("click", object.reset.bind(object));
body {
margin: 0px;
padding: 0px;
}
canvas {
position: absolute;
top: 0px;
left: 0px;
border: 1px solid black;
}
<canvas id="canvas"></canvas>
I need to animate a circle moving in a html canvas. For this purpose, I decided to use a typical sprite animation technique which consist in the following general steps:
Initialize the background (in this case, just a gray rectangle)
Calculate new sprite's coordinates
Draw the sprite (the circle)
Reset the backgound
Goto 2
My problem is that the result looks like all the old circles seem to be redrawn each time I call ctx.fill() despite I am reseting the canvas.
What am I doing wrong? Any suggestion?
var canvas;
var ctx;
var canvasPos;
function rgbToHex(r, g, b) {
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
}
function init() {
canvas = document.getElementById("target_temp");
ctx = canvas.getContext("2d");
canvasPos = { x: canvas.offsetLeft, y: canvas.offsetTop };
drawSlider();
}
var y = 7;
function animate() {
y = y + 1;
knob.setPosition(8, y);
knob.clear();
knob.draw();
if (y < 93) {
setTimeout(animate, 10);
}
}
function drawSlider() {
ctx = canvas.getContext("2d");
ctx.fillStyle = "#d3d3d3";
ctx.fillRect(0, 0, 16, 100);
}
var knob = {
position: { x: 8, y: 7 },
oldPosition: { x: 8, y: 7 },
setPosition(_x, _y) {
this.oldPosition = this.position;
this.position.x = _x;
this.position.y = _y
},
clear() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawSlider();
},
draw() {
ctx.fillStyle = rgbToHex(0, 0, 112);
ctx.arc(8, this.position.y, 7, 0, 2 * Math.PI);
ctx.fill();
}
}
window.onload = function () { init(); animate(); };
<!DOCTYPE html>
<html>
<boyd>
<canvas id="target_temp" width="16px" height="100px"></canvas>
</boyd>
</html>
I see you already figured out to use the beginPath but I will disagree with the location I will put that at the very beginning of the function animate, take a look at the code below, I refactored your code, got rid of some unused variables, and made the slider an object just like you have for the knob
var canvas = document.getElementById("target_temp");
var ctx = canvas.getContext("2d");
var slider = {
width: 16,
height: 100,
draw() {
ctx.fillStyle = "#d3d3d3";
ctx.fillRect(0, 0, this.width, this.height);
}
}
var knob = {
position: {x: 8, y: 7},
radius: 8,
draw() {
ctx.fillStyle = "#00D";
ctx.arc(this.position.x, this.position.y, this.radius, 0, 2 * Math.PI);
ctx.fill();
}
}
function animate() {
ctx.beginPath()
ctx.clearRect(0, 0, canvas.width, canvas.height);
slider.draw();
if (knob.position.y + knob.radius < slider.height)
knob.position.y++;
knob.draw();
setTimeout(animate, 10);
}
window.onload = function() {
animate()
};
<canvas id="target_temp"></canvas>
Ok, I found the cause: the arc() method stacks the circle as part of the path, so, for it to work, we need to reset the path like this:
draw() {
ctx.fillStyle = rgbToHex(0, 2*y, 107);
ctx.beginPath();
ctx.arc(8, this.position.y, 7, 0, 2 * Math.PI);
ctx.fill();
}
I have a bubble chart with big bubbles. I want to draw a cross or anything in the center of each bubble, but I can't find any solutions. The following picture show you the context:
I use Chart.js 2.5.0 on meteor.
Your question seemed quite interesting to me, therefore, I constructed the following chartjs plugin, that will help fulfill your requirement.
Chart.plugins.register({
afterDraw: c => {
let datasets = c.data.datasets;
datasets.forEach((e, i) => {
let isHidden = e._meta[0].hidden;
if (!isHidden) {
let data = c.getDatasetMeta(i).data;
data.forEach(e => {
let ctx = c.chart.ctx;
let x = e._model.x;
let y = e._model.y;
let r = e._model.radius;
// draw a cross
// or you can draw anything using general canvas methods
ctx.save();
ctx.beginPath();
ctx.moveTo(x - r / 4, y - r / 4);
ctx.lineTo(x + r / 4, y + r / 4);
ctx.moveTo(x + r / 4, y - r / 4);
ctx.lineTo(x - r / 4, y + r / 4);
ctx.strokeStyle = 'white';
ctx.lineWidth = 2;
ctx.stroke();
ctx.restore();
});
}
});
}
});
ᴅᴇᴍᴏ
Chart.plugins.register({
afterDraw: c => {
let datasets = c.data.datasets;
datasets.forEach((e, i) => {
let isHidden = e._meta[0].hidden;
if (!isHidden) {
let data = c.getDatasetMeta(i).data;
data.forEach(e => {
let ctx = c.chart.ctx;
let x = e._model.x;
let y = e._model.y;
let r = e._model.radius;
// draw a cross
// or you can draw anything using general canvas methods
ctx.save();
ctx.beginPath();
ctx.moveTo(x - r / 4, y - r / 4);
ctx.lineTo(x + r / 4, y + r / 4);
ctx.moveTo(x + r / 4, y - r / 4);
ctx.lineTo(x - r / 4, y + r / 4);
ctx.strokeStyle = 'white';
ctx.lineWidth = 2;
ctx.stroke();
ctx.restore();
});
}
});
}
});
let ctx = document.querySelector('#c').getContext('2d');
let chart = new Chart(ctx, {
type: 'bubble',
data: {
labels: ['Jan', 'Feb', 'Mar'],
datasets: [{
label: 'John',
data: [
{ x: 5, y: 5, r: 10 },
{ x: 10, y: 10, r: 15 },
{ x: 16, y: 15, r: 18 }
],
backgroundColor: '#76d1bf'
}, {
label: 'Smith',
data: [
{ x: 3, y: 10, r: 10 },
{ x: 7, y: 11, r: 15 },
{ x: 12, y: 6, r: 18 }
],
backgroundColor: '#827ada'
}]
},
options: {
responsive: false,
scales: {
xAxes: [{
ticks: {
min: 2,
max: 18,
stepSize: 4
}
}],
yAxes: [{
ticks: {
min: 0,
max: 20,
stepSize: 4
}
}]
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.5.0/Chart.min.js"></script>
<canvas id="c" height="200"></canvas>
Chart.js has always been a very straight forward and simple library. One of the advantages this has is that it is easy to customize. This is also one of the good points of JavaScript, source code is always available for what ever library you use.
There is how ever one draw back with customization. Once you have made changes to the library you will have to use that particular version because customization is not something that the authors will consider when making changes.
So to use the code below you should go to the github page and download the project and use that version of chart.js for your site. I dont change the original, most of the time each customization is specific for a particular case and the code is customised at the clients side.
The change is very simple. First backup the function that draws a point
Chart.canvasHelpers.defaultDrawPoint = Chart.canvasHelpers.drawPoint;
This is done so you can pass on all calls that you are not interested in back to the standard handler.
Next write the intercept code by replacing the function you just backed up.
Chart.canvasHelpers.drawPoint = function(ctx, pointStyle, radius, x, y){
Looking at the original source code you can work out how it draws the circles and just trap that behavior passing on all other argument variants to the original.
If pointStyle is undefined or === "circle" you handle that your self
// pointStyle undefined is default
// pointStyle === "circle" is the default named style
if(pointStyle === undefined || pointStyle === "circle"){
// next 4 lines copied from the source
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.closePath();
ctx.fill();
You then add your custom code to render what ever you like. It is important that you save the current 2D context as you dont want to have to worry that you break something further down the line.
// draw a cross
ctx.save(); // save the state
ctx.strokeStyle = "white";
ctx.strokeWidth = 4;
ctx.beginPath();
ctx.moveTo(x - radius *0.3, y - radius *0.3);
ctx.lineTo(x + radius *0.3, y + radius *0.3);
ctx.moveTo(x + radius *0.3, y - radius *0.3);
ctx.lineTo(x - radius *0.3, y + radius *0.3);
ctx.stroke();
Then restore the state of the 2D context
ctx.restore(); // restore the state
The else handles the standard calls you are not interested in
}else{ // all other styles pass on to default handler
Chart.canvasHelpers.defaultDrawPoint(ctx, pointStyle, radius, x, y);
}
For Chart.js this is particularly useful as it gives a way to customize and get the animations as well.
Chart.canvasHelpers.defaultDrawPoint = Chart.canvasHelpers.drawPoint;
Chart.canvasHelpers.drawPoint = function(ctx, pointStyle, radius, x, y){
// PointStyle undefined is default
// PointStyle === "circle" is the default named style
if(pointStyle === undefined || pointStyle === "circle"){
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.closePath();
ctx.fill();
// custom code here
ctx.save(); // save the state
ctx.strokeStyle = "white";
ctx.strokeWidth = 4;
ctx.beginPath();
ctx.moveTo(x - radius *0.3, y - radius *0.3);
ctx.lineTo(x + radius *0.3, y + radius *0.3);
ctx.moveTo(x + radius *0.3, y - radius *0.3);
ctx.lineTo(x - radius *0.3, y + radius *0.3);
ctx.stroke();
ctx.restore(); // restor the state
}else{ // all other styles pass on to default handler
Chart.canvasHelpers.defaultDrawPoint(ctx, pointStyle, radius, x, y);
}
}
// some utils to add data
// returns a random int
const rand = (min, max = min + (min = 0))=> Math.floor( Math.random() * (max-min) + min);
// returns a random data point {x,y,r}
const randData = ()=>({x : rand(0,50), y: rand(5,50), r: rand(4,20)});
// create a chart.
const ctx = canvas.getContext("2d");
const chart = new Chart(ctx, {
type: "bubble",
data: {
datasets: [{
label: "Random Data",
backgroundColor: "#7AF",
data: (()=>{
var dat = [];
for(var i = 0; i < 10; i++){ dat.push(randData()) }
return dat;
})(),
}]
},
options: { responsive: false } // this is required to work or it throws
// not sure why by it is not due to the
// changes
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.5.0/Chart.min.js"></script>
<canvas id=canvas height=200 width=400></canvas>
To roll back the changes simply set the function back to the original.
Chart.canvasHelpers.drawPoint = Chart.canvasHelpers.defualtDrawPoint;
And remove the extra reference
Chart.canvasHelpers.defualtDrawPoint = undefined;
grunt's answer did not work for me. I don't know if there are different formats or if it's written in an outdated version but I have modified his solution to the following so it would work in my project. Basically I have
removed the register since it wasn't in line with what the example for background-color was like and
found out that the data points' info isn't as nested anymore so I changed the way the properties are accessed, too
plugins: [
{
id: 'background-colour',
beforeDraw: (chart) => {
const ctx = chart.ctx;
ctx.save();
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, width, height);
ctx.restore();
}
},
{
id: 'abc',
afterDraw: (c) => {
let datasets = c.data.datasets;
datasets.forEach((e, i) => {
let isHidden = e.hidden;
if (!isHidden) {
let data = c.getDatasetMeta(i).data;
data.forEach(e => {
let ctx = c.ctx;
let x = e.x;
let y = e.y;
let r = e.options.radius as number;
ctx.save();
ctx.beginPath();
ctx.moveTo(x - r / 4, y - r / 4);
ctx.lineTo(x + r / 4, y + r / 4);
ctx.moveTo(x + r / 4, y - r / 4);
ctx.lineTo(x - r / 4, y + r / 4);
ctx.strokeStyle = 'white';
ctx.lineWidth = 2;
ctx.stroke();
ctx.restore();
});
}
});
}
}
]
I have a simple canvas animation: two rectangles move in two different directions. However, I feel this could be simplified more.
http://jsfiddle.net/tmyie/R5wx8/6/
var canvas = document.getElementById('canvas'),
c = canvas.getContext('2d'),
x = 10,
y = 15,
a = 20,
b = 50;
function move() {
c.clearRect(0, 0, 500, 300);
c.fillRect(0, y, 5, 5),
c.fillRect(b, 5, 15, 15);
x++;
y++;
b++
if (y > canvas.height || x > canvas.width) {
y = 0;
x = 0;
}
}
setInterval(move, 100);
For example, what happens if I wanted to create another three shapes? At the moment, I'd have to create more variables for each coordinate:
x++;
y++;
b++
Is there a way I could turn each rectangle into its own object?
You can certainly turn them into objects, for example:
function Rect(x, y, w, h, dltX, dltY, color) {
var me = this;
this.x = x;
this.y = y;
this.width = w;
this.height = h;
this.deltaX = dltX || 0; /// and deltas can be optional
this.deltaY = dltY || 0;
this.color = color || '#000'; /// color is optional
this.update = function(ctx) {
me.x += me.deltaX;
me.y += me.deltaY;
ctx.fillStyle = me.color;
ctx.fillRect(me.x, me.y, me.width, me.height);
}
}
The deltaX and deltaY are how much you want to move the rectangle for each update. If you set these to for example 1 then x and y will be increased with 1 each time update() is called.
Using deltas makes it easy to create bounces (see demo below) by simply reversing the delta value (ie. delta = -delta) as well as things such as acceleration, variate speed, you can feed them through trigonometric functions to have the object move in a specific angle and so forth.
You can used fixed values if you desire but you will discover that deltas are beneficial in the long run (ref. comment: it's actually a very classic method used in for instance the first Pong games :-) ).
Online demo here
Now that we have defined the object we can simply create instances of it and store them in an array:
var rects = [
new Rect(10, 10, 100, 100, 1, -2),
new Rect(100, 1, 50, 50, 2, 1, '#f00'),
...
]
From here it's simply a matter of iterating the array to update each object:
function move() {
ctx.clearRect(0, 0, width, height);
for(var i = 0, r; r = rects[i]; i++) {
/// check any conditions here
r.update(ctx);
}
requestAnimationFrame(move);
}
requestAnimationFrame(move); /// start loop
Here's a slightly simpler version, though in the long term I'd recommend Ken's. In mine the rects are still just property bags, with no behavior on their own.
var canvas = document.getElementById('canvas'),
c = canvas.getContext('2d'),
rects = [{x:0, y:15, w:5, h:5, vx:0, vy:1},
{x:50, y:5, w:15, h:15, vx:1, vy:0}];
function move() {
c.clearRect(0, 0, 500, 300);
for (var i=0; i < rects.length; i++) {
var rect = rects[i];
c.fillRect(rect.x, rect.y, rect.w, rect.h),
rect.x += rect.vx;
rect.y += rect.vy;
}
}
setInterval(move, 100);
I'm attempting to create a simple pie chart like shown in the graphic below:
The chart will show the results for a quiz where a user can choose either a, b or c. They're 10 questions and the user can only choose one option per question.
What I want to do is show the pie chart with each segment being a percentage of 100% by passing in the values for either a,b, or c.
I have the following so far:
var greenOne = "#95B524";
var greenTwo = "#AFCC4C";
var greenThree = "#C1DD54";
function CreatePieChart() {
var chart = document.getElementById('piechart');
var canvas = chart.getContext('2d');
canvas.clearRect(0, 0, chart.width, chart.height);
var total = 100;
var a = 3;
var b = 4;
var c = 3;
for (var i = 0; i < 3; i++) {
canvas.fillStyle = "#95B524";
canvas.beginPath();
canvas.strokeStyle = "#fff";
canvas.lineWidth = 3;
canvas.arc(100, 100, 100, 0, Math.PI * 2, true);
canvas.closePath();
canvas.stroke();
canvas.fill();
}
}
CreatePieChart();
<canvas id="piechart" width="200" height="200"></canvas>
The colors are specific to the size of the segment, so green one is used for the largest and green three for the smallest.
Even after searching Google and triple-checking my radians values, etc. I was still having trouble with this, so I have created a jsFiddle for people to play with as a live example and will post the code below as well. (Update: in the fiddle v2, stroke and labels are added also.)
var canvas = document.getElementById("can");
var ctx = canvas.getContext("2d");
var lastend = 0;
var data = [200, 60, 15]; // If you add more data values make sure you add more colors
var myTotal = 0; // Automatically calculated so don't touch
var myColor = ["red", "green", "blue"]; // Colors of each slice
for (var e = 0; e < data.length; e++) {
myTotal += data[e];
}
for (var i = 0; i < data.length; i++) {
ctx.fillStyle = myColor[i];
ctx.beginPath();
ctx.moveTo(canvas.width / 2, canvas.height / 2);
ctx.arc(
canvas.width / 2, // x
canvas.height / 2, // y
canvas.height / 2, // radius
lastend, // startingAngle (radians)
lastend + Math.PI * 2 * (data[i] / myTotal), // endingAngle (radians)
false // antiClockwise (boolean)
);
ctx.lineTo(canvas.width / 2, canvas.height / 2);
ctx.fill();
lastend += Math.PI * 2 * (data[i] / myTotal);
}
<canvas id="can" width="200" height="200" />
I like the previous answer, but I felt it was lacking in code clarity and it didn't really cover how to utilize labels.
I moved the values into a data object array for easy declaration. Other values, like percentage, I explicitly declared as a property on the data object, or as a separate variable. This, I think, makes it easier to read.
The refactoring also made it easier to tie the values to input boxes if that's something you're interested in.
To see what I mean and play with the values check out this CodePen: http://codepen.io/zfrisch/pen/pRbZeb
var data = [
{
label: "one",
value: 100,
color: "white",
},
{
label: "two",
value: 100,
color: "skyBlue",
},
{
label: "three",
value: 100,
color: "yellow",
},
];
var total = 0;
for (obj of data) {
total += obj.value;
}
var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");
var previousRadian;
var middle = {
x: canvas.width / 2,
y: canvas.height / 2,
radius: canvas.height / 2,
};
//background
ctx.beginPath();
ctx.arc(middle.x, middle.y, middle.radius, 0, 2 * Math.PI);
ctx.closePath();
ctx.stroke();
ctx.fillStyle = "black";
ctx.fill();
//end of background
for (obj of data) {
previousRadian = previousRadian || 0;
obj.percentage = parseInt((obj.value / total) * 100);
ctx.beginPath();
ctx.fillStyle = obj.color;
obj.radian = Math.PI * 2 * (obj.value / total);
ctx.moveTo(middle.x, middle.y);
//middle.radius - 2 is to add border between the background and the pie chart
ctx.arc(
middle.x,
middle.y,
middle.radius - 2,
previousRadian,
previousRadian + obj.radian,
false
);
ctx.closePath();
ctx.fill();
ctx.save();
ctx.translate(middle.x, middle.y);
ctx.fillStyle = "black";
ctx.font = middle.radius / 10 + "px Arial";
ctx.rotate(previousRadian + obj.radian);
var labelText = "'" + obj.label + "' " + obj.percentage + "%";
ctx.fillText(labelText, ctx.measureText(labelText).width / 2, 0);
ctx.restore();
previousRadian += obj.radian;
}
<canvas id="myCanvas" width="500" height="500"></canvas>
Here is a pie chart without using external libraries, using html5 canvas :
See the code
But it's better to use libraries for drawing charts. in apex-charts there is an option called sparkline, which helps you to remove the extra stuffs and draw a minimal and clean chart.
Here is a clean donut chart using apex-charts library. (Extra stuffs are removed with sparkline option):
var options = {
series: [620, 40],
labels: ['Finished', 'Unfinished'],
chart: {
type: 'donut',
sparkline: {
enabled: true,
}
},
plotOptions: {
pie: {
donut: {
labels: {
show: true,
total: {
showAlways: false,
show: true,
label: 'Total'
}
}
}
}
},
};
var chart = new ApexCharts(document.querySelector("#chart"), options);
chart.render();
See it on codepen
I had the same problem before but I was able to solve this problem later.
What I was missing was I was drawing an arch in context and was trying to fill it, due to which the color was spreading all across the circle because now the context was bound only between a radius line from center to the starting point of arch and the arch to bound the context.
But there was no other boundary the line from the end of arch to the center, as soon as I draw that line using the following:
ctx.lineTo(center coordinates of circle);
I have a complete boundary of a pie, so now if I fill the color in context it will not get spread inside the whole circle but will be limited to that pie.