How to shade the circle in canvas - html

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 )

Related

Centering on a canvas object within an HTML5 canvas

I have an Html5 canvas which i am drawing squares to.
The canvas itself is roughly the size of the window.
When i detect a click on a square i would like to translate the canvas so that the square is roughly in the center of the window. Any insights, hints, or straight-forward replies are welcome.
Here is what i tried so far:
If a square is at point (1000, 1000) I would simply translate the canvas (-1000, -1000). I know i need to add an offset so that it is centered in the window. However, the canvas always ends up off of the visible window (too far in the upper-left corner somewhere).
A more complex scenario:
Ultimately i would like to be able to center on a clicked object on a canvas that is transformed (rotated & skewed). I'm going for an isometric effect which seems to work really well. I'm wondering if this transformation affects the centering logic/math at all?
Transforming from screen to world and back
When working with non standard axis (or projections) such as isometrix it is always best to use a transformation matrix. It will cover every possible 2D projection with the same simple functions.
The coordinates of the iso world are called world coordinates. All you objects are stored as world coordinates. When you render them you project those coordinates to the screen coordinates using a transformation matrix.
The matrix, not a movie.
The matrix represents the direction and size in screen coordinates of the world
x and y axis and the screen location of the world origin (0,0)
For iso that is
x axis across 1 down 0.5
y axis across -1 down 0.5
z axis up 1 (-1 as up is the reverse of down) but this example does not use z
So the matrix as an array
const isoMat = [1,0.5,-1,0.5,0,0]; // ISO (pixel art) dimorphic projection
The first two are the x axis, the next two the y axis and the last two values are the screen coordinates of the origin.
Use the matrix to transform points
You apply a matrix to a point, this transforms the point from one coordinate system to another. You can also convert back via a inverse transform.
World to screen
You will need to convert from world coordinates to screen coordinates.
function worldToScreen(pos,retPos){
retPos.x = pos.x * isoMat[0] + pos.y * isoMat[2] + isoMat[4];
retPos.y = pos.x * isoMat[1] + pos.y * isoMat[3] + isoMat[5];
}
In the demo I ignore the origin as I set that at the center of the canvas at all times. Thus remove the origin from that function
function worldToScreen(pos,retPos){
retPos.x = pos.x * isoMat[0] + pos.y * isoMat[2];
retPos.y = pos.x * isoMat[1] + pos.y * isoMat[3];
}
Screen to world.
You will also need to convert from the screen coordinates to the world. For this you need to use the inverse transform. It's a bit like the inverse of multiply a * 2 = b is the inverse of b / 2 = a
There is a standard method for calculating the inverse matrix as follows
const invMatrix = []; // inverse matrix
// I call the next line cross, most call it the determinant which I
// think is stupid as it is effectively a cross product and is used
// like you would use a cross product. Anyways I digress
const cross = isoMat[0] * isoMat[3] - isoMat[1] * isoMat[2];
invMatrix[0] = isoMat[3] / cross;
invMatrix[1] = -isoMat[1] / cross;
invMatrix[2] = -isoMat[2] / cross;
invMatrix[3] = isoMat[0] / cross;
Then we have a function that converts from the screen x,y to the world position
function screenToWorld(pos,retPos){
const x = pos.x - isoMat[4];
const y = pos.y - isoMat[5];
retPos.x = x * invMatrix[0] + y * invMatrix[2];
retPos.y = x * invMatrix[1] + y * invMatrix[3];
}
So you get the mouse coords as screen pixels, use the above function to convert to world coords. Then you can use the world coords to find the object you are looking for.
To move a world object to the screen center you convert its coords to screen coords, add the position on the screen (the canvas center) and set the transform matrix origin to that location.
The demo
The demo creates a set of boxes in world coordinates. It sets the 2D context transform to the isoMat (isometric projection) via ctx.setTransform(
Every frame I convert the mouse screen coords to world coords then use that to check which box the mouse is over.
If the mouse button is down I then convert that box from world coords to screen and add the screen center. To smooth the step the new screen center is chased (smoothed)..
Well you should be able to work it out in the code, any problems ask in the comments.
const ctx = canvas.getContext("2d");
const moveSpeed = 0.4;
const boxMin = 20;
const boxMax = 50;
const boxCount = 100;
const boxArea = 2000;
// some canvas vals
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
var globalTime;
const U = undefined;
// Helper function
const doFor = (count, cb) => { var i = 0; while (i < count && cb(i++) !== true); };
const eachOf = (array, cb) => { var i = 0; const len = array.length; while (i < len && cb(array[i], i++, len) !== true ); };
const setOf = (count, cb) => {var a = [],i = 0; while (i < count) { a.push(cb(i ++)) } return a };
const randI = (min, max = min + (min = 0)) => (Math.random() * (max - min) + min) | 0;
const rand = (min, max = min + (min = 0)) => Math.random() * (max - min) + min;
// mouse function and object
const mouse = {x : 0, y : 0, button : false, world : {x : 0, y : 0}}
function mouseEvents(e){
mouse.x = e.pageX;
mouse.y = e.pageY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["down","up","move"].forEach(name => document.addEventListener("mouse"+name,mouseEvents));
// boxes in world coordinates.
const boxes = [];
function draw(){
if(this.dead){
ctx.fillStyle = "rgba(0,0,0,0.5)";
ctx.fillRect(this.x,this.y,this.w,this.h);
}
ctx.strokeStyle = this.col;
ctx.globalAlpha = 1;
ctx.strokeRect(this.x,this.y,this.w,this.h);
// the rest is just overkill
if(this.col === "red"){
this.mr = 10;
}else{
this.mr = 1;
}
this.mc += (this.mr-this.m) * 0.45;
this.mc *= 0.05;
this.m += this.mc;
for(var i = 0; i < this.m; i ++){
const m = this.m * (i + 1);
ctx.globalAlpha = 1-(m / 100);
ctx.strokeRect(this.x-m,this.y-m,this.w,this.h);
}
}
// make random boxes.
function createBoxes(){
boxes.length = 0;
boxes.push(...setOf(boxCount,()=>{
return {
x : randI(cw- boxArea/ 2, cw + boxArea/2),
y : randI(ch- boxArea/ 2, ch + boxArea/2),
w : randI(boxMin,boxMax),
h : randI(boxMin,boxMax),
m : 5,
mc : 0,
mr : 5,
col : "black",
dead : false,
draw : draw,
isOver : isOver,
}
}));
}
// use mouse world coordinates to find box under mouse
function isOver(x,y){
return x > this.x && x < this.x + this.w && y > this.y && y < this.y + this.h;
}
var overBox;
function findBox(x,y){
if(overBox){
overBox.col = "black";
}
overBox = undefined;
eachOf(boxes,box=>{
if(box.isOver(x,y)){
overBox = box;
box.col = "red";
return true;
}
})
}
function drawBoxes(){
boxes.forEach(box=>box.draw());
}
// next 3 values control the movement of the origin
// rather than move instantly the currentPos chases the new pos.
const currentPos = {x :0, y : 0};
const newPos = {x :0, y : 0};
const chasePos = {x :0, y : 0};
// this function does the chasing
function updatePos(){
chasePos.x += (newPos.x - currentPos.x) * moveSpeed;
chasePos.y += (newPos.y - currentPos.y) * moveSpeed;
chasePos.x *= moveSpeed;
chasePos.y *= moveSpeed;
currentPos.x += chasePos.x;
currentPos.y += chasePos.y;
}
// ISO matrix and inverse matrix plus 2world and 2 screen
const isoMat = [1,0.5,-1,0.5,0,0];
const invMatrix = [];
const cross = isoMat[0] * isoMat[3] - isoMat[1] * isoMat[2];
invMatrix[0] = isoMat[3] / cross;
invMatrix[1] = -isoMat[1] / cross;
invMatrix[2] = -isoMat[2] / cross;
invMatrix[3] = isoMat[0] / cross;
function screenToWorld(pos,retPos){
const x = pos.x - isoMat[4];
const y = pos.y - isoMat[5];
retPos.x = x * invMatrix[0] + y * invMatrix[2];
retPos.y = x * invMatrix[1] + y * invMatrix[3];
}
function worldToScreen(pos,retPos){
retPos.x = pos.x * isoMat[0] + pos.y * isoMat[2];// + isoMat[4];
retPos.y = pos.x * isoMat[1] + pos.y * isoMat[3];// + isoMat[5];
}
// main update function
function update(timer){
// standard frame setup
globalTime = timer;
ctx.setTransform(1,0,0,1,0,0); // reset transform
ctx.globalAlpha = 1; // reset alpha
if(w !== innerWidth || h !== innerHeight){
cw = (w = canvas.width = innerWidth) / 2;
ch = (h = canvas.height = innerHeight) / 2;
createBoxes();
}else{
ctx.clearRect(0,0,w,h);
}
ctx.fillStyle = "black";
ctx.font = "28px arial";
ctx.textAlign = "center";
ctx.fillText("Click on a box to center it.",cw,28);
// update position
updatePos();
isoMat[4] = currentPos.x;
isoMat[5] = currentPos.y;
// set the screen transform to the iso matrix
// all drawing can now be done in world coordinates.
ctx.setTransform(isoMat[0], isoMat[1], isoMat[2], isoMat[3], isoMat[4], isoMat[5]);
// convert the mouse to world coordinates
screenToWorld(mouse,mouse.world);
// find box under mouse
findBox(mouse.world.x, mouse.world.y);
// if mouse down and over a box
if(mouse.button && overBox){
mouse.button = false;
overBox.dead = true; // make it gray
// get the screen coordinates of the box
worldToScreen({
x:-(overBox.x + overBox.w/2),
y:-(overBox.y + overBox.h/2),
},newPos
);
// move it to the screen center
newPos.x += cw;
newPos.y += ch;
}
// forget what the following function does, think it does something like draw boxes, but I am guessing.. :P
drawBoxes();
requestAnimationFrame(update);
}
requestAnimationFrame(update);
canvas { position : absolute; top : 0px; left : 0px; }
<canvas id="canvas"></canvas>

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

CreateJS Radial gradient with matrix

I'm converting a Flash application to HTML5 Canvas. Most of the development is finished but for handling the colors there is a code like this in the flash application:
matrix = new Matrix ();
matrix.createGradientBox (600, ColorHeight * 1200, 0, 80, ColorHeight * -600);
Animation_gradient_mc.clear ();
Animation_gradient_mc.beginGradientFill (fillType, colors, alphas, ratios, matrix, spreadMethod, interpolationMethod, focalPointRatio);
The declaration for a radial gradient in CreateJS is the following:
beginRadialGradientFill(colors, ratios, x0, y0, r0, x1, y1, r1 )
Does anyone know a method to apply a Matrix to a gradient fill?
Any help would be appreciated.
Thanks in advance
Edit
Here are some examples of the gradient I'm trying to reproduce:
As you can see it starts off as a standard radial gradient.
However, it can also appear stretched, I think this is where the matrix helps.
I've attempted to create the same effect by creating a createjs.Graphics.Fill with a matrix but it doesn't seem to be doing anything:
var matrix = new VacpMatrix();
matrix.createGradientBox(
600,
discharge_gradient.color_height * 1200,
0,
80,
discharge_gradient.color_height * -600
);
// test_graphics.append(new createjs.Graphics.Fill('#0000ff', matrix));
console.log('matrix', matrix);
test_graphics.append(new createjs.Graphics.Fill('#ff0000', matrix).radialGradient(
discharge_gradient.colors,
discharge_gradient.ratios,
discharge_gradient.x0,
discharge_gradient.y0,
discharge_gradient.r0,
discharge_gradient.x1,
discharge_gradient.y1,
discharge_gradient.r1
));
var discharge_shape = new createjs.Shape(test_graphics);
I extended the Matrix2d class to add a createGradientBox method using code from the openfl project:
p.createGradientBox = function (width, height, rotation, tx, ty) {
if (_.isUndefined(rotation) || _.isNull(rotation)) {
rotation = 0;
}
if (_.isUndefined(tx) || _.isNull(tx)) {
tx = 0;
}
if (_.isUndefined(ty) || _.isNull(ty)) {
ty = 0;
}
var a = width / 1638.4,
d = height / 1638.4;
// Rotation is clockwise
if (rotation != 0) {
var cos = math.cos(rotation),
sin = math.sin(rotation);
this.b = sin * d;
this.c = -sin * a;
this.a = a * cos;
this.d = d * cos;
} else {
this.b = 0;
this.c = 0;
}
this.tx = tx + width / 2;
this.ty = ty + height / 2;
}
I hope the extra information is useful.
I don't know createJS enough, nor Flash Matrix object, but to make this kind of ovalGradient with the native Canvas2d API, you will need to transform the context's matrix.
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var horizontalScale = .3;
var verticalScale = 1;
var gradient = ctx.createRadialGradient(100/horizontalScale, 100/verticalScale, 100, 100/horizontalScale,100/verticalScale,0);
gradient.addColorStop(0,"green");
gradient.addColorStop(1,"red");
// shrink the context's matrix
ctx.scale(horizontalScale, verticalScale)
// draw your gradient
ctx.fillStyle = gradient;
// stretch the rectangle which contains the gradient accordingly
ctx.fillRect(0,0, 200/horizontalScale, 200/verticalScale);
// reset the context's matrix
ctx.setTransform(1,0,0,1,0,0);
canvas{ background-color: ivory;}
<canvas id="canvas" width="200" height="200"></canvas>
So if you are planning to write some kind of a function to reproduce it, have a look at ctx.scale(), ctx.transform() and ctx.setTransform().
EDIT
As you noticed, this will also shrink your drawn shapes, also, you will have to calculate how much you should "unshrink" those at the drawing, just like I did with the fillRect. (agreed, this one was an easy one)
Here is a function that could help you with more complicated shapes. I didn't really tested it (only with the given example), so it may fail somehow, but it can also give you an idea on how to deal with it :
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
function shrinkedRadial(ctx, shapeArray, xScale, yScale, gradientOpts) {
// scaling by 0 is like not drawing
if (!xScale || !yScale) return;
var gO = gradientOpts;
// apply our scale on the gradient options we passed
var gradient = ctx.createRadialGradient(gO.x0 / xScale, gO.y0 / yScale, gO.r0, gO.x1 / xScale, gO.y1 / yScale, gO.r1);
gradient.addColorStop(gO.c1_pos, gO.c1_fill);
gradient.addColorStop(gO.c2_pos, gO.c2_fill);
// shrink the context's matrix
ctx.scale(xScale, yScale);
ctx.fillStyle = gradient;
// execute the drawing operations' string
shapeArray.forEach(function(str) {
var val = str.split(' ');
var op = shapesRef[val[0]];
if (val[1]) {
var pos = val[1].split(',').map(function(v, i) {
// if even, it should be an y axis, otherwise an x one
return i % 2 ? v / yScale : v / xScale;
});
ctx[op].apply(ctx, pos);
} else {
// no parameters
ctx[op]();
}
});
// apply our gradient
ctx.fill();
// reset the transform matrix
ctx.setTransform(1, 0, 0, 1, 0, 0);
}
// just for shortening our shape drawing operations
// notice how arc operations are omitted, it could be implemented but...
var shapesRef = {
b: 'beginPath',
fR: 'fillRect',
m: 'moveTo',
l: 'lineTo',
bC: 'bezierCurveTo',
qC: 'quadraticCurveTo',
r: 'rect',
c: 'closePath'
};
var gradientOpts = {
x0: 232,
y0: 55,
r0: 70,
x1: 232,
y1: 55,
r1: 0,
c1_fill: 'red',
c1_pos: 0,
c2_fill: 'green',
c2_pos: 1
}
var shapes = ['b', 'm 228,133', 'bC 209,121,154,76,183,43', 'bC 199,28,225,34,233,59', 'bC 239,34,270,29,280,39', 'bC 317,76,248,124,230,133']
// our shape is drawn at 150px from the right so we do move the context accordingly, but you won't have to.
ctx.translate(-150, 0);
shrinkedRadial(ctx, shapes, .3, 1, gradientOpts);
ctx.font = '15px sans-serif';
ctx.fillStyle = 'black';
ctx.fillText('shrinked radialGradient', 3, 20);
// how it looks like without scaling :
ctx.translate(50, 0)
var gO = gradientOpts;
var gradient = ctx.createRadialGradient(gO.x0, gO.y0, gO.r0, gO.x1, gO.y1, gO.r1);
gradient.addColorStop(gO.c1_pos, gO.c1_fill);
gradient.addColorStop(gO.c2_pos, gO.c2_fill);
ctx.fillStyle = gradient;
shapes.forEach(function(str) {
var val = str.split(' ');
var op = shapesRef[val[0]];
if (val[1]) {
var pos = val[1].split(',');
ctx[op].apply(ctx, pos);
} else {
ctx[op]();
}
});
ctx.fill();
ctx.font = '15px sans-serif';
ctx.fillStyle = 'black';
ctx.fillText('normal radialGradient', 160, 20);
<canvas id="canvas" width="400" height="150"></canvas>
A standard matrix would adjust inputs:
Width, angle Horizontal, angle Vertical, Height, pos X, pos Y in that order,
Here you are using gradientBox which is not the usual type of AS3 matrix. Expected input:Width, Height, Rotation, pos X, pos Y
I don't use createJS so I'm gunna guess this (you build on it)...
Your usual beginRadialGradientFill(colors, ratios, x0, y0, r0, x1, y1, r1 )
becomes like below (as though gradientBox matrix is involved):
beginRadialGradientFill(colors, ratios, posX, posY, Rotation, Width, Height, Rotation )

Why does Canvas's putImageData not work when I specify target location?

In trying to find documentation for Canvas context's putImageData() method, I've found things like this:
context.putImageData(imgData,x,y,dirtyX,dirtyY,dirtyWidth,dirtyHeight);
(from http://www.w3schools.com/tags/canvas_putimagedata.asp)
According to the documentation I've read, x and y are an index into the source image, whereas dirtyX and dirtyY specify coordinates in the target canvas where to draw the image. Yet, as you'll see from the example below (and JSFiddle) a call to putImageData(imgData,x,y) works while putImageData(imgData, 0, 0, locX, locY) doesn't. I'm not sure why.
EDIT:
I guess my real question is why the top row of the image is black, and there are only 7 rows, not 8. The images should start at the top-left of the Canvas. They DO start at the left (and have 8 columns). Why do they not start at the top?
Answer: that's due to divide by 0 on this line when yLoc is 0:
xoff = imgWidth / (yLoc/3);
The JSFiddle:
http://jsfiddle.net/WZynM/
Code:
<html>
<head>
<title>Canvas tutorial</title>
<script type="text/javascript">
var canvas;
var context; // The canvas's 2d context
function setupCanvas()
{
canvas = document.getElementById('myCanvas');
if (canvas.getContext)
{
context = canvas.getContext('2d');
context.fillStyle = "black"; // this is default anyway
context.fillRect(0, 0, canvas.width, canvas.height);
}
}
function init()
{
loadImages();
startGating();
}
var images = new Array();
var gatingTimer;
var curIndex, imgWidth=0, imgHeight;
// Load images
function loadImages()
{
for (n = 1; n <= 16; n++)
{
images[n] = new Image();
images[n].src = "qxsImages/frame" + n + ".png";
// document.body.appendChild(images[n]);
console.log("width = " + images[n].width + ", height = " + images[n].height);
}
curIndex = 1;
imgWidth = images[1].width;
imgHeight = images[1].height;
}
function redrawImages()
{
if (imgWidth == 0)
return;
curIndex++;
if (curIndex > 16)
curIndex = 1;
// To do later: use images[1].width and .height to layout based on image size
for (var x=0; x<8; x++)
{
for (var y=0; y<8; y++)
{
//if (x != 1)
// context.drawImage(images[curIndex], x*150, y*100);
// context.drawImage(images[curIndex], x*150, y*100, imgWidth/2, imgHeight/2); // scale
// else
self.drawCustomImage(x*150, y*100);
}
}
}
function drawCustomImage(xLoc, yLoc)
{
// create a new pixel array
imageData = context.createImageData(imgWidth, imgHeight);
pos = 0; // index position into imagedata array
xoff = imgWidth / (yLoc/3); // offsets to "center"
yoff = imgHeight / 3;
for (y = 0; y < imgHeight; y++)
{
for (x = 0; x < imgWidth; x++)
{
// calculate sine based on distance
x2 = x - xoff;
y2 = y - yoff;
d = Math.sqrt(x2*x2 + y2*y2);
t = Math.sin(d/6.0);
// calculate RGB values based on sine
r = t * 200;
g = 125 + t * 80;
b = 235 + t * 20;
// set red, green, blue, and alpha:
imageData.data[pos++] = Math.max(0,Math.min(255, r));
imageData.data[pos++] = Math.max(0,Math.min(255, g));
imageData.data[pos++] = Math.max(0,Math.min(255, b));
imageData.data[pos++] = 255; // opaque alpha
}
}
// copy the image data back onto the canvas
context.putImageData(imageData, xLoc, yLoc); // Works... kinda
// context.putImageData(imageData, 0, 0, xLoc, yLoc, imgWidth, imgHeight); // Doesn't work. Why?
}
function startGating()
{
gatingTimer = setInterval(redrawImages, 1000/25); // start gating
}
function stopGating()
{
clearInterval(gatingTimer);
}
</script>
<style type="text/css">
canvas { border: 1px solid black; }
</style>
</head>
<body onload="setupCanvas(); init();">
<canvas id="myCanvas" width="1200" height="800"></canvas>
</body>
</html>
http://jsfiddle.net/WZynM/
You just had your coordinates backwards.
context.putImageData(imageData, xLoc, yLoc, 0, 0, imgWidth, imgHeight);
Live Demo
xLoc, and yLoc are where you are putting it, and 0,0,imgWidth,imgHeight is the data you are putting onto the canvas.
Another example showing this.
A lot of the online docs seem a bit contradictory but for the seven param version
putImageData(img, dx, dy, dirtyX, dirtyY, dirtyRectWidth, dirtyRectHeight)
the dx, and dy are your destination, the next four params are the dirty rect parameters, basically controlling what you are drawing from the source canvas. One of the most thorough descriptions I can find was in the book HTML5 Unleashed by Simon Sarris (pg. 165).
Having been using this recently, I've discovered that Loktar above has hit upon a VERY important issue. Basically, some documentation of this method online is incorrect, a particularly dangerous example being W3Schools, to which a number of people will turn to for reference.
Their documentation states the following:
Synopsis:
context.putImageData(imgData,x,y,dirtyX,dirtyY,dirtyWidth,dirtyHeight);
Arguments:
imgData: Specifies the ImageData object to put back onto the canvas
x : The x-coordinate, in pixels, of the upper-left corner of the ImageData object [WRONG]
y : The y-coordinate, in pixels, of the upper-left corner of the ImageData object [WRONG]
dirtyX : Optional. The horizontal (x) value, in pixels, where to place the image on the canvas [WRONG]
dirtyY : Optional. The vertical (y) value, in pixels, where to place the image on the canvas [WRONG]
dirtyWidth : Optional. The width to use to draw the image on the canvas
dirtyHeight: Optional. The height to use to draw the image on the canvas
As Loktar states above, the CORRECT synopsis is as follows:
Correct Synopsis:
context.putImageData(imgData, canvasX, canvasY, srcX ,srcY, srcWidth, srcHeight);
Arguments:
imgData: Specifies the ImageData object to put back onto the canvas (as before);
canvasX : The x coordinate of the location on the CANVAS where you are plotting your imageData;
canvasY : The y coordinate of the location on the CANVAS where you are plotting your ImageData;
srcX : Optional. The x coordinate of the top left hand corner of your ImageData;
srcY : Optional. The y coordinate of the top left hand corner of your ImageData;
srcWidth : Optional. The width of your ImageData;
srcHeight : Optional. The height of your ImageData.
Use the correct synopsis above, and you won't have the problems that have been encountered above.
I'll give a big hat tip to Loktar for finding this out initially, but I thought it apposite to provide an expanded answer in case others run into the same problem.

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/