Draw gradient bevel around polygon - html

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)")

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>

HTML5 Canvas Sweep Gradient

It looks like the HTML5 canvas does not support a "sweep gradient" - a gradient where the color stops rotate around the center, rather than emanating from the center.
Is there any way to simulate a sweep gradient on a canvas? I suppose I could do something similar with lots of little linear gradients, but at that point I'm basically rendering the gradient myself.
Indeed there is no built-in for such a thing.
Not sure what you had in mind with these "lots of little linear gradients", but you actually just need a single one, the size of your circle's circumference, and only to get the correct colors to use.
What you'll need a lot though are lines, since we'll draw these around the center point using the solid colors we had in the linearGradient.
So to render this, you just move to the center point, then draw a line using a solid color from the linear gradient, then rotate and repeat.
To get all the colors of a linearGradient, you just need to draw it and map it's ImageData to CSS colors.
The hard part though is that to be able to have an object that behaves like a CanvasGradient, we need to be able to set it as a fillStyle or strokeStyle.
This is possible by returning a CanvasPattern. An other difficulty is that gradients are virtually infinitely big. A non-repeating Pattern is not.
I didn't found a good solution to overcome this problem, but as a workaround, we can use the size of the target canvas as a limit.
Here is a rough implementation:
class SweepGrad {
constructor(ctx, x, y) {
this.x = x;
this.y = y;
this.target = ctx;
this.colorStops = [];
}
addColorStop(offset, color) {
this.colorStops.push({offset, color});
}
render() {
// get the current size of the target context
const w = this.target.canvas.width;
const h = this.target.canvas.width;
const x = this.x;
const y = this.y;
// get the max length our lines can be
const maxDist = Math.ceil(Math.max(
Math.hypot(x, y),
Math.hypot(x - w, y),
Math.hypot(x - w, y - h),
Math.hypot(x, y - h)
));
// the circumference of our maxDist circle
// this will determine the number of lines we will draw
// (we double it to avoid some antialiasing artifacts at the edges)
const circ = maxDist*Math.PI*2 *2;
// create a copy of the target canvas
const canvas = this.target.canvas.cloneNode();
const ctx = canvas.getContext('2d');
// generate the linear gradient used to get all our colors
const linearGrad = ctx.createLinearGradient(0, 0, circ, 0);
this.colorStops.forEach(stop =>
linearGrad.addColorStop(stop.offset, stop.color)
);
const colors = getLinearGradientColors(linearGrad, circ);
// draw our gradient
ctx.setTransform(1,0,0,1,x,y);
for(let i = 0; i<colors.length; i++) {
ctx.beginPath();
ctx.moveTo(0,0);
ctx.lineTo(maxDist, 0);
ctx.strokeStyle = colors[i];
ctx.stroke();
ctx.rotate((Math.PI*2)/colors.length);
}
// return a Pattern so we can use it as fillStyle or strokeStyle
return ctx.createPattern(canvas, 'no-repeat');
}
}
// returns an array of CSS colors from a linear gradient
function getLinearGradientColors(grad, length) {
const canvas = Object.assign(document.createElement('canvas'), {width: length, height: 10});
const ctx = canvas.getContext('2d');
ctx.fillStyle = grad;
ctx.fillRect(0,0,length, 10);
return ctx.getImageData(0,0,length,1).data
.reduce((out, channel, i) => {
const px_index = Math.floor(i/4);
const px_slot = out[px_index] || (out[px_index] = []);
px_slot.push(channel);
if(px_slot.length === 4) {
px_slot[3] /= 255;
out[px_index] = `rgba(${px_slot.join()})`;
}
return out;
}, []);
}
// How to use
const ctx = canvas.getContext('2d');
const redblue = new SweepGrad(ctx, 70, 70);
redblue.addColorStop(0, 'red');
redblue.addColorStop(1, 'blue');
// remeber to call 'render()' to get the Pattern back
// maybe a Proxy could handle that for us?
ctx.fillStyle = redblue.render();
ctx.beginPath();
ctx.arc(70,70,50,Math.PI*2,0);
ctx.fill();
const yellowgreenred = new SweepGrad(ctx, 290, 80);
yellowgreenred.addColorStop(0, 'yellow');
yellowgreenred.addColorStop(0.5, 'green');
yellowgreenred.addColorStop(1, 'red');
ctx.fillStyle = yellowgreenred.render();
ctx.fillRect(220,10,140,140);
// just like with gradients,
// we need to translate the context so it follows our drawing
ctx.setTransform(1,0,0,1,-220,-10);
ctx.lineWidth = 10;
ctx.strokeStyle = ctx.fillStyle;
ctx.stroke(); // stroke the circle
canvas{border:1px solid}
<canvas id="canvas" width="380" height="160"></canvas>
But beware, all this is quite computationally heavy, so be sure to use it sporadically and to cache your resulting Gradients/Patterns.

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

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/

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);

HTML5 Canvas Challenge

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