How to add hover effect for each slice (html5 canvas) - html

hi can u help me to setup this code. I m not so good at html5.
This text is displayed if your browser does not support HTML5 Canvas.
$(document).ready(function() {
// initialize some variables for the chart
var
canvas = $("#canvas")[0];
var ctx = canvas.getContext('2d');
var data = [75,68,32,95,20,51];
var colors = ["#7E3817", "#C35817", "#EE9A4D", "#A0C544", "#348017", "#307D7E"];
var center = [canvas.width / 2, canvas.height / 2];
var radius = Math.min(canvas.width, canvas.height) / 2;
var lastPosition = 0, total = 0;
var pieData = [];
// total up all the data for chart
for (var i in data) { total += data[i]; }
// populate arrays for each slice
for(var i in data) {
pieData[i] = [];
pieData[i]['value'] = data[i];
pieData[i]['krasa'] = colors[i];
pieData[i]['startAngle'] = 2 * Math.PI * lastPosition;
pieData[i]['endAngle'] = 2 * Math.PI * (lastPosition + (data[i]/total));
lastPosition += data[i]/total;
}
function drawChart()
{
for(var i = 0; i < data.length; i++)
{
var gradient = ctx.createLinearGradient( 0, 0, canvas.width, canvas.height );
gradient.addColorStop( 0, "#ddd" );
gradient.addColorStop( 1, colors[i] );
ctx.beginPath();
ctx.moveTo(center[0],center[1]);
ctx.arc(center[0],center[1],radius,pieData[i]['startAngle'],pieData[i]['endAngle'],false);
ctx.lineTo(center[0],center[1]);
ctx.closePath();
ctx.fillStyle = gradient;
ctx.fill();
ctx.lineWidth = 1;
ctx.strokeStyle = "#fff";
ctx.stroke();
}
}
drawChart(); // first render
});
How to add hover effect for each slice?

After you have drawn your wedges to the canvas, they become just pixels in a larger image.
You have no way to track the individual pie wedges at this point. Therefore no way to track hovers on any particular wedge.
But...You do have several options!
Option#1 --- Make your own hit-test to determine which pie wedge you clicked on.
It would look something like this (I HAVE NOT TESTED THIS !!!)
var chartStartAngle=0; // you started drawing the pie at angle 0
function handleChartClick ( clickEvent ) {
// Get the mouse cursor position at the time of the click, relative to the canvas
var mouseX = clickEvent.pageX - this.offsetLeft;
var mouseY = clickEvent.pageY - this.offsetTop;
// Was the click inside the pie chart?
var xFromCenter = mouseX - center[0];
var yFromCenter = mouseY - center[1];
var distanceFromCenter = Math.sqrt( Math.pow( Math.abs( xFromCenter ), 2 ) + Math.pow( Math.abs( yFromCenter ), 2 ) );
if ( distanceFromCenter <= radius ) {
// You clicked inside the chart.
// So get the click angle
var clickAngle = Math.atan2( yFromCenter, xFromCenter ) - chartStartAngle;
if ( clickAngle < 0 ) clickAngle = 2 * Math.PI + clickAngle;
for ( var i in pieData ) {
if ( clickAngle >= pieData[i]['startAngle'] && clickAngle <= pieData[i]['endAngle'] ) {
// You clicked on pieData[i]
// So do your effect here!
return;
}
}
}
}
Option#2 --- Use a cavas library which allows you to keep track of each wedge in your pie chart and therefore do your hover effect. Several good libraries (among many good ones) are: EaselJs, FabricJs and KineticJs.
Elated.com has a great tutorial that shows what you're looking for. Check it out: http://www.elated.com/articles/snazzy-animated-pie-chart-html5-jquery/

Related

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

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

Make canvas circle larger

I am new to using the canvas html tag and have put a countdown on my website that uses the canvas tag that I think makes the circle and it works ok but would like to make the circles larger but not sure how to do it, I got the coding from https://codepen.io/mdkroon/pen/dBweaL, below is the coding that I currently have. I can't work out how to make the circles larger, I can make the font larger but then the text does not fit in the circles.
<div id="countdown" data-date="2020-01-14" data-time="12:00:00">
<p><strong>Windows 7 End of Life Countdown</strong></p>
<div class="canvas-container" >
<canvas id="days"></canvas>
<canvas id="hours"></canvas>
<canvas id="minutes"></canvas>
<canvas id="seconds"></canvas>
</div>
</div>
<script>
// variables
var countdown = document.getElementById('countdown');
var endDate = countdown.dataset.date || '2020-01-14';
var endTime = countdown.dataset.time || '00:00:00';
var endCountdown = new Date(endDate + 'T' + endTime);
var timer;
var specs = {
'radius': 50,
'centerX': 50,
'centerY': 50,
'thickness': 10,
'offset': -Math.PI/2,
'color': '#1abc9c',
'bgColor': '#ccc',
'idFont': 'small-caps 400 10px Verdana',
'valueFont': 'bold 30px Verdana',
'fontColor': '#000'
};
var time = {
'millisecond': 1000,
'second': 60,
'minute': 60,
'hour': 24,
'day': 365
}
var info = {};
// canvas init
var canvasElements = Array.prototype.slice.call(document.querySelectorAll('canvas'));
var canvasCtx = [];
canvasElements.forEach( function(canvas, index) {
canvas.width = specs.centerX * 2;
canvas.height = specs.centerY * 2;
canvasCtx[index] = canvas.getContext('2d');
var name = canvas.id;
info[name] = {'ctx': index, 'value': 0, 'prevValue': -1};
});
var canvasKeys = Object.keys(info);
info.days.denominator = time.day;
info.hours.denominator = time.hour;
info.minutes.denominator = time.minute;
info.seconds.denominator = time.second;
// show remaining time
function showRemainingTime() {
var now = new Date();
// calculate new values
var secondsLeft = Math.max(0, Math.floor((endCountdown - now)/1000));
info.days.value = Math.floor(secondsLeft / (time.second*time.minute*time.hour));
info.hours.value = Math.floor((secondsLeft % (time.second*time.minute*time.hour)) / (time.second*time.minute));
info.minutes.value = Math.floor((secondsLeft % (time.second*time.minute)) / time.second);
info.seconds.value = Math.floor(secondsLeft % time.second);
// update changed values only
canvasKeys.forEach( function(key) {
if(info[key].value !== info[key].prevValue){
if(key === 'days' && info[key].value > 365) {
// exception if days is more than 1 year
draw(canvasCtx[info[key].ctx], 1, key, info[key].value);
} else {
draw(canvasCtx[info[key].ctx], info[key].value/info[key].denominator, key, info[key].value);
}
info[key].prevValue = info[key].value;
}
});
}
// draw function
function draw(ctx, part, id, value) {
// calculate angles
var start = specs.offset;
var between = 2 * Math.PI * part + specs.offset;
var end = 2 * Math.PI + specs.offset;
// clear canvas
ctx.clearRect(0, 0, specs.centerX * 2, specs.centerY * 2);
// draw remaining %
ctx.fillStyle = specs.color;
ctx.beginPath();
ctx.arc(specs.centerX, specs.centerY, specs.radius, start, between);
ctx.arc(specs.centerX, specs.centerY, specs.radius - specs.thickness, between, start, true);
ctx.closePath();
ctx.fill();
// draw bg
ctx.fillStyle = specs.bgColor;
ctx.beginPath();
ctx.arc(specs.centerX, specs.centerY, specs.radius, between, end);
ctx.arc(specs.centerX, specs.centerY, specs.radius - specs.thickness, end, between, true);
ctx.closePath();
ctx.fill();
// draw text
ctx.fillStyle = specs.fontColor;
ctx.font = specs.idFont;
ctx.fillText(id, specs.radius - ctx.measureText(id).width/2, specs.thickness*3);
ctx.font = specs.valueFont;
ctx.fillText(value, specs.radius - ctx.measureText(value).width/2, specs.radius*2 - specs.thickness*3);
}
// change countdown every second
timer = setInterval(showRemainingTime, 1000);
So that this question does not go unanswered, the solution is to make the radius, centerX, and centerY values in the code sample (and codepen) larger.

Draw gradient bevel around polygon

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

How to shade the circle in canvas

I am working with HTML5 with canvas. I already draw a 2D circle.Now i want to shade the circle with a color.but the shading look like a 3D circle.Is this possible with canvas?.Thank you.
Fake smoke and mirrors
To fake a light on a sphere. I am guessing it is a sphere as you say circle and you could mean a donut. This technique will work for a donut as well.
So to lighting.
Phong Shading
The most basic lighting model is Phong (from memory). It uses the angle between the incoming light ray and the surface normal (a line going out from the surface at 90 deg). The amount of reflected light is the cosine of that angle time the light intensity.
Spheres a easy
As the sphere is symmetrical this allows us to use a radial gradient to apply the value for each pixel on the sphere and for a sphere with the light directly overhead this produces a perfect phong shaded sphere with very little effort.
The code that does that. x,y are the center of the sphere and r is the radius. The angle between the light and the surface normal is easy to calculate as you move out from the center of the sphere. It starts at zero and ends at Math.PI/2 (90deg). So the reflected value is the cosine of that angle.
var grd = ctx.createRadialGradient(x,y,0,x,y,r);
var step = (Math.PI/2)/r;
for(var i = 0; i < (Math.PI/2); i += step){
var c = "" + Math.floor(Math.max(0,255 * Math.abs(Math.cos(i)));
grd.addColorStop(i/(Math.PI/2),"rgba("+c+","+c+","+c+","1)");
}
That code creates a gradient to fit the circle.
Mod for Homer food
To do for a donut you need to modify i. The donut has an inner and outer radius (r1, r2) so inside the for loop modify i
var ii = (i/(Math.PI/2)); // normalise i
ii *= r2; // scale to outer edge
ii = ((r1+r2)/2)-ii; // get distance from center line
ii = ii / ((r2-r1)/2); // normalise to half the width;
ii = ii * Math.PI * (1/2); // scale to get the surface norm on the donut.
// use ii as the surface normal to calculate refelected light
var c = "" + Math.floor(Math.max(0,255 * Math.abs(Math.cos(ii)));
Phong Shading Sucks
By phong shading sucks big time and will not do. This also does not allow for lights that are off center or even partly behind the sphere.
We need to add the ability for off centered light. Luck has it that the radial gradients can be offset
var grd = ctx.createRadialGradient(x,y,0,x,y,r);
The first 3 numbers are the start circle of the gradient and can be positioned anywhere. The problem is that when we move the start location the phong shading model falls apart. To fix that there is a little smoke and mirrors stuff that can make the eye believe what the brain wants.
We adjust the fall off, the brightness, the spread, and the angle for each colour stop on the radial gradient depending on how far the light is from the center.
Specular highlights
This improves it a bit but still not the best. Another important component of lighting is specular reflections (the highlight). This is dependent on the angle between the reflected light and the eye. As we do not want to do all that (javascript is slow) we will cludge it via a slight modification of the phong shading. We simply multiply the surface normal by a value greater than 1. Though not perfect it works well.
Surface properties and environment
Next light is coloured, the sphere has reflective qualities that depend on frequency and there is ambient light as well. We don't want to model all this stuff so we need a way to fake it.
This can be done via compositing (Used for almost all 3D movie production). We build up the lighting one layer at a time. The 2D API provides compositing operations for us so we can create several gradients and layer them.
There is a lot more math involved but I have tried to keep it as simple as possible.
A demo
The following demo does a real time shading of a sphere (will work on all radially symmetrical objects) Apart from some setup code for canvas and mouse the demo has two parts the main loop does the compositing by layering the lights and the function createGradient creates the gradient.
The lights used can be found in the object lights and have various properties to control the layer. The first layer should use comp = source-in and lum = 1 or you will end up with the background showing through. All other layer lights can be what every you want.
The flag spec tells the shader that the light is specular and must include the specPower > 1 as I do not vet its existence.
The colours of the light is in the array col and represent Red, green and blue. The values can be greater the 256 and less than 0 as light in the natural world has a huge dynamic range and some effect need you to ramp up the incoming light way above the 255 limit of the RGB pixel.
I add a final "multiply" to the layered result. This is the magic touch in the smoke and mirror method.
If you like the code play with the values and layers. Move the mouse to change the light source location.
This is not real lighting it is fake, but who cares as long as it looks OK. lol
UPDATE
Found a bug so fixed it and while I was here, changed the code to randomize the lights when you click the left mouse button. This is so you can see the range of lighting that can be achieved when using the ctx.globalCompositeOperation in combination with gradients.
var demo = function(){
/** fullScreenCanvas.js begin **/
var canvas = (function(){
var canvas = document.getElementById("canv");
if(canvas !== null){
document.body.removeChild(canvas);
}
// creates a blank image with 2d context
canvas = document.createElement("canvas");
canvas.id = "canv";
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style.position = "absolute";
canvas.style.top = "0px";
canvas.style.left = "0px";
canvas.style.zIndex = 1000;
canvas.ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
return canvas;
})();
var ctx = canvas.ctx;
/** fullScreenCanvas.js end **/
/** MouseFull.js begin **/
if(typeof mouse !== "undefined"){ // if the mouse exists
if( mouse.removeMouse !== undefined){
mouse.removeMouse(); // remove prviouse events
}
}else{
var mouse;
}
var canvasMouseCallBack = undefined; // if needed
mouse = (function(){
var mouse = {
x : 0, y : 0, w : 0, alt : false, shift : false, ctrl : false,
interfaceId : 0, buttonLastRaw : 0, buttonRaw : 0,
over : false, // mouse is over the element
bm : [1, 2, 4, 6, 5, 3], // masks for setting and clearing button raw bits;
getInterfaceId : function () { return this.interfaceId++; }, // For UI functions
startMouse:undefined,
mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
};
function mouseMove(e) {
var t = e.type, m = mouse;
m.x = e.offsetX; m.y = e.offsetY;
if (m.x === undefined) { m.x = e.clientX; m.y = e.clientY; }
m.alt = e.altKey;m.shift = e.shiftKey;m.ctrl = e.ctrlKey;
if (t === "mousedown") { m.buttonRaw |= m.bm[e.which-1];
} else if (t === "mouseup") { m.buttonRaw &= m.bm[e.which + 2];
} else if (t === "mouseout") { m.buttonRaw = 0; m.over = false;
} else if (t === "mouseover") { m.over = true;
} else if (t === "mousewheel") { m.w = e.wheelDelta;
} else if (t === "DOMMouseScroll") { m.w = -e.detail;}
if (canvasMouseCallBack) { canvasMouseCallBack(mouse); }
e.preventDefault();
}
function startMouse(element){
if(element === undefined){
element = document;
}
mouse.element = element;
mouse.mouseEvents.forEach(
function(n){
element.addEventListener(n, mouseMove);
}
);
element.addEventListener("contextmenu", function (e) {e.preventDefault();}, false);
}
mouse.removeMouse = function(){
if(mouse.element !== undefined){
mouse.mouseEvents.forEach(
function(n){
mouse.element.removeEventListener(n, mouseMove);
}
);
canvasMouseCallBack = undefined;
}
}
mouse.mouseStart = startMouse;
return mouse;
})();
if(typeof canvas !== "undefined"){
mouse.mouseStart(canvas);
}else{
mouse.mouseStart();
}
/** MouseFull.js end **/
// draws the circle
function drawCircle(c){
ctx.beginPath();
ctx.arc(c.x,c.y,c.r,0,Math.PI*2);
ctx.fill();
}
function drawCircle1(c){
ctx.beginPath();
var x = c.x;
var y = c.y;
var r = c.r * 0.95;
ctx.moveTo(x,y - r);
ctx.quadraticCurveTo(x + r * 0.8, y - r , x + r *1, y - r / 10);
ctx.quadraticCurveTo(x + r , y + r/3 , x , y + r/3);
ctx.quadraticCurveTo(x - r , y + r/3 , x - r , y - r /10 );
ctx.quadraticCurveTo(x - r * 0.8, y - r , x , y- r );
ctx.fill();
}
function drawShadowShadow(circle,light){
var x = light.x; // get the light position as we will modify it
var y = light.y;
var r = circle.r * 1.1;
var vX = x - circle.x; // get the vector to the light source
var vY = y - circle.y;
var dist = -Math.sqrt(vX*vX+vY*vY)*0.3;
var dir = Math.atan2(vY,vX);
lx = Math.cos(dir) * dist + circle.x; // light canb not go past radius
ly = Math.sin(dir) * dist + circle.y;
var grd = ctx.createRadialGradient(lx,ly,r * 1/4 ,lx,ly,r);
grd.addColorStop(0,"rgba(0,0,0,1)");
grd.addColorStop(1,"rgba(0,0,0,0)");
ctx.fillStyle = grd;
drawCircle({x:lx,y:ly,r:r})
}
// 2D light simulation. This is just an approximation and does not match real world stuff
// based on Phong shading.
// x,y,r descript the imagined sphere
// light is the light source
// ambient is the ambient lighting
// amount is the amount of this layers effect has on the finnal result
function createGradient(circle,light,ambient,amount){
var r,g,b; // colour channels
var x = circle.x; // get lazy coder values
var y = circle.y;
var r = circle.r;
var lx = light.x; // get the light position as we will modify it
var ly = light.y;
var vX = light.x - x; // get the vector to the light source
var vY = light.y - y;
// get the distance to the light source
var dist = Math.sqrt(vX*vX+vY*vY);
// id the light is a specular source then move it to half its position away
dist *= light.spec ? 0.5 : 1;
// get the direction of the light source.
var dir = Math.atan2(vY,vX);
// fix light position
lx = Math.cos(dir)*dist+x; // light canb not go past radius
ly = Math.sin(dir)*dist+y;
// add some dimming so that the light does not wash out.
dim = 1 - Math.min(1,(dist / (r*4)));
// add a bit of pretend rotation on the z axis. This will bring in a little backlighting
var lightRotate = (1-dim) * (Math.PI/2);
// spread the light a bit when near the edges. Reduce a bit for spec light
var spread = Math.sin(lightRotate) * r * (light.spec ? 0.5 : 1);
// create a gradient
var grd = ctx.createRadialGradient(lx,ly,spread,x,y,r + dist);
// use the radius to workout what step will cover a pixel (approx)
var step = (Math.PI/2)/r;
// for each pixel going out on the radius add the caclualte light value
for(var i = 0; i < (Math.PI/2); i += step){
if(light.spec){
// fake spec light reduces dim fall off
// light reflected has sharper falloff
// do not include back light via Math.abs
r = Math.max(0,light.col[0] * Math.cos((i + lightRotate)*light.specPower) * 1-(dim * (1/3)) );
g = Math.max(0,light.col[1] * Math.cos((i + lightRotate)*light.specPower) * 1-(dim * (1/3)) );
b = Math.max(0,light.col[2] * Math.cos((i + lightRotate)*light.specPower) * 1-(dim * (1/3)) );
}else{
// light value is the source lum * the cos of the angle to the light
// Using the abs value of the refelected light to give fake back light.
// add a bit of rotation with (lightRotate)
// dim to stop washing out
// then clamp so does not go below zero
r = Math.max(0,light.col[0] * Math.abs(Math.cos(i + lightRotate)) * dim );
g = Math.max(0,light.col[1] * Math.abs(Math.cos(i + lightRotate)) * dim );
b = Math.max(0,light.col[2] * Math.abs(Math.cos(i + lightRotate)) * dim );
}
// add ambient light
if(light.useAmbient){
r += ambient[0];
g += ambient[1];
b += ambient[2];
}
// add the colour stop with the amount of the effect we want.
grd.addColorStop(i/(Math.PI/2),"rgba("+Math.floor(r)+","+Math.floor(g)+","+Math.floor(b)+","+amount+")");
}
//return the gradient;
return grd;
}
// define the circles
var circles = [
{
x: canvas.width * (1/2),
y: canvas.height * (1/2),
r: canvas.width * (1/8),
}
]
function R(val){
return val * Math.random();
}
var lights;
function getLights(){
return {
ambient : [10,30,50],
sources : [
{
x: 0, // position of light
y: 0,
col : [R(255),R(255),R(255)], // RGB intensities can be any value
lum : 1, // total lumanance for this light
comp : "source-over", // composite opperation
spec : false, // if true then use a pretend specular falloff
draw : drawCircle,
useAmbient : true,
},{ // this light is for a little accent and is at 180 degree from the light
x: 0,
y: 0,
col : [R(255),R(255),R(255)],
lum : R(1),
comp : "lighter",
spec : true, // if true then you MUST inclue spec power
specPower : R(3.2),
draw : drawCircle,
useAmbient : false,
},{
x: canvas.width,
y: canvas.height,
col : [R(1255),R(1255),R(1255)],
lum : R(0.5),
comp : "lighter",
spec : false,
draw : drawCircle,
useAmbient : false,
},{
x: canvas.width/2,
y: canvas.height/2 + canvas.width /4,
col : [R(155),R(155),R(155)],
lum : R(1),
comp : "lighter",
spec : true, // if true then you MUST inclue spec power
specPower : 2.32,
draw : drawCircle,
useAmbient : false,
},{
x: canvas.width/3,
y: canvas.height/3,
col : [R(1255),R(1255),R(1255)],
lum : R(0.2),
comp : "multiply",
spec : false,
draw : drawCircle,
useAmbient : false,
},{
x: canvas.width/2,
y: -100,
col : [R(2255),R(2555),R(2255)],
lum : R(0.3),
comp : "lighter",
spec : false,
draw : drawCircle1,
useAmbient : false,
}
]
}
}
lights = getLights();
/** FrameUpdate.js begin **/
var w = canvas.width;
var h = canvas.height;
var cw = w / 2;
var ch = h / 2;
ctx.font = "20px Arial";
ctx.textAlign = "center";
function update(){
ctx.setTransform(1,0,0,1,0,0);
ctx.fillStyle = "#A74"
ctx.fillRect(0,0,w,h);
ctx.fillStyle = "black";
ctx.fillText("Left click to change lights", canvas.width / 2, 20)
// set the moving light source to that of the mouse
if(mouse.buttonRaw === 1){
mouse.buttonRaw = 0;
lights = getLights();
}
lights.sources[0].x = mouse.x;
lights.sources[0].y = mouse.y;
if(lights.sources.length > 1){
lights.sources[1].x = mouse.x;
lights.sources[1].y = mouse.y;
}
drawShadowShadow(circles[0],lights.sources[0])
//do each sphere
for(var i = 0; i < circles.length; i ++){
// for each sphere do the each light
var cir = circles[i];
for(var j = 0; j < lights.sources.length; j ++){
var light = lights.sources[j];
ctx.fillStyle = createGradient(cir,light,lights.ambient,light.lum);
ctx.globalCompositeOperation = light.comp;
light.draw(circles[i]);
}
}
ctx.globalCompositeOperation = "source-over";
if(!STOP && (mouse.buttonRaw & 4)!== 4){
requestAnimationFrame(update);
}else{
if(typeof log === "function" ){
log("DONE!")
}
STOP = false;
var can = document.getElementById("canv");
if(can !== null){
document.body.removeChild(can);
}
}
}
if(typeof clearLog === "function" ){
clearLog();
}
update();
}
var STOP = false; // flag to tell demo app to stop
function resizeEvent(){
var waitForStopped = function(){
if(!STOP){ // wait for stop to return to false
demo();
return;
}
setTimeout(waitForStopped,200);
}
STOP = true;
setTimeout(waitForStopped,100);
}
window.addEventListener("resize",resizeEvent);
demo();
/** FrameUpdate.js end **/
As #danday74 says, you can use a gradient to add depth to your circle.
You can also use shadowing to add depth to your circle.
Here's a proof-of-concept illustrating a 3d donut:
I leave it to you to design your desired circle
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var PI=Math.PI;
drawShadow(150,150,120,50);
function drawShadow(cx,cy,r,strokewidth){
ctx.save();
ctx.strokeStyle='white';
ctx.lineWidth=5;
ctx.shadowColor='black';
ctx.shadowBlur=15;
//
ctx.beginPath();
ctx.arc(cx,cy,r-5,0,PI*2);
ctx.clip();
//
ctx.beginPath();
ctx.arc(cx,cy,r,0,PI*2);
ctx.stroke();
//
ctx.beginPath();
ctx.arc(cx,cy,r-strokewidth,0,PI*2);
ctx.stroke();
ctx.shadowColor='rgba(0,0,0,0)';
//
ctx.beginPath();
ctx.arc(cx,cy,r-strokewidth,0,PI*2);
ctx.fillStyle='white'
ctx.fill();
//
ctx.restore();
}
body{ background-color: white; }
canvas{border:1px solid red; margin:0 auto; }
<canvas id="canvas" width=300 height=300></canvas>
Various thoughts which you can investigate ...
1 use an image as the texture for the circle
2 use a gradient to fill the circle, probably a radial gradient
3 consider using an image mask, a black / white mask which defines transparency ( prob not the right solution here )

How do I keep object location from being increased exponentially after each call to draw function?

Simple animation that creates a firework-like effect on the canvas with each click. The issue is the animation is made with a setInterval(draw) and every time the canvas is redrawn the location of each particle is += particle.speed. But with each click the particles move faster and faster as it seems the speed of each particle is not reset.
As you can see with a couple clicks on the working example here: , with the first click the particles move very (correctly) slowly, but with each subsequent click the speed is increased.
JS used is pasted below as well, any help is greatly appreciated!
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
canvas.addEventListener("click", startdraw, false);
//Lets resize the canvas to occupy the full page
var W = window.innerWidth;
var H = window.innerHeight;
canvas.width = W;
canvas.height = H;
ctx.fillStyle = "black";
ctx.fillRect(0, 0, W, H);
//global variables
var radius;
radius = 10;
balls_amt = 20;
balls = [];
var locX = Math.round(Math.random()*W);
var locY = Math.round(Math.random()*H);
//ball constructor
function ball(positionx,positiony,speedX,speedY)
{
this.r = Math.round(Math.random()*255);
this.g = Math.round(Math.random()*255);
this.b = Math.round(Math.random()*255);
this.a = Math.random();
this.location = {
x: positionx,
y:positiony
}
this.speed = {
x: -2+Math.random()*4,
y: -2+Math.random()*4
};
}
function draw(){
ctx.globalCompositeOperation = "source-over";
//Lets reduce the opacity of the BG paint to give the final touch
ctx.fillStyle = "rgba(0, 0, 0, 0.1)";
ctx.fillRect(0, 0, W, H);
//Lets blend the particle with the BG
//ctx.globalCompositeOperation = "lighter";
for(var i = 0; i < balls.length; i++)
{
var p = balls[i];
ctx.beginPath();
ctx.arc(p.location.x, p.location.y, radius, Math.PI*2, false);
ctx.fillStyle = "rgba("+p.r+","+p.g+","+p.b+", "+p.a+")";
ctx.fill();
var consolelogX = p.location.x;
var consolelogY = p.location.y;
p.location.x += p.speed.x;
p.location.y += p.speed.y;
}
}
function startdraw(e){
var posX = e.pageX; //find the x position of the mouse
var posY = e.pageY; //find the y position of the mouse
for(i=0;i<balls_amt;i++){
balls.push(new ball(posX,posY));
}
setInterval(draw,20);
//ball[1].speed.x;
}
After each click startdraw is called, which starts every time a new periodical call (setInterval) for the draw method. So after the 2nd click you have 2 parallel intervals, after the 3rd you have 3 parallel intervals.
It is not exponentially, only linearly increasing :)
A possible dirty fix:
Introduce an interval global variable, and replace this row:
setInterval(draw,20);
with this one:
if (!interval) interval = setInterval(draw,20);
Or a nicer solution is to start the interval at the onLoad event.
setInterval will repeat its call every 20th ms, and returns an ID.
You can stop the repetition by calling clearInterval(ID).
var id = setInterval("alert('yo!');", 500);
clearInterval(id);