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 )
Related
I am trying to implement an animation of drawing an arcTo line on Canvas. For a straight line for example, the animation would be as follows
c = canvas.getContext("2d");
width = window.innerWidth;
height = window.innerHeight;
complete = false
var percent = 1
function drawEdge(x1, y1, x2, y2, color){
c.beginPath();
c.lineWidth = 10;
c.strokeStyle = color;
c.moveTo(x1, y1);
c.lineTo(x2, y2);
c.stroke();
c.closePath();
}
function getPosition(x1, y1, x2, y2, percentageBetweenPoints){
let xPosition = x1 + (x2 - x1) * (percentageBetweenPoints / 100);
let yPosition = y1 + (y2 - y1) * (percentageBetweenPoints / 100);
const position = {
x: xPosition,
y: yPosition,
}
return position
}
function drawLine(){
if (!complete){
requestAnimationFrame(drawLine);
}
if (percent >= 100){
complete = true;
percent = 100;
} else{
percent = percent + 1;
}
position = getPosition(300,300,1000,300,percent);
c.clearRect(0, 0 , width, height);
drawEdge(300,300,position.x,position.y, "black");
}
drawLine()
This creates an animation of a line being drawn across the screen. However, I am having trouble doing the same thing for arcTo lines. Is there any way to implement this?
You are looking for something like this?
let ctx = canvas.getContext('2d');
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.font = 'bold 18px Arial';
requestAnimationFrame(draw);
function draw(t) {
t = t % 5e3 / 5e3;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.arc(canvas.width/2, canvas.height/2, 50, 0, t * 2 * Math.PI);
ctx.stroke();
ctx.fillText((t*100).toFixed(0), canvas.width/2, canvas.height/2);
requestAnimationFrame(draw);
}
<canvas id=canvas></canvas>
To Hack or not to Hack?
There are two ways to do this
Calculate the start, end, and length of each line segment, the start, end angle, direction (CW or CCW), and center of each arc segment. Basically repeating all the maths and logic (around 50 lines of code) that makes arcTo such a useful render function.
You can get details on how to approach the full solution from html5 canvas triangle with rounded corners
Use ctx.lineDash with a long dash and a long space. Move the dash over time with ctx.lineDashOffset giving the appearance of a line growing in length (see demo). The dash offset value is reversed, starting at max length and ending when zero.
NOTE there is one problem with this method. You don't know the length of the line, and thus you don`t know how long it will take for the line to be completed. You can make an estimation. To know the length of the line you must do all the calculations (well there abouts)
The Hack
As the second method is the easiest to implement and covers most needs I will demo that method.
Not much to say about it, it animates a path created by ctx.arcTo
Side benefit is it will animated any path rendered using ctx.stroke
requestAnimationFrame(mainLoop);
// Line is defined in unit space.
// Origin is at center of canvas, -1,-1 top left, 1, 1 bottom right
// Unit box is square and will be scaled to fit the canvas size.
// Note I did not use ctx.setTransform to better highlight what is scaled and what is not.
const ctx = canvas.getContext("2d");
var w, h, w2, h2; // canvas size and half size
var linePos; // current dash offset
var scale; // canvas scale
const LINE_WIDTH = 0.05; // in units
const LINE_STYLE = "#000"; // black
const LINE_SPEED = 1; // in units per second
const MAX_LINE_LENGTH = 9; // in units approx
const RADIUS = 0.08; //Arc radius in units
const SHAPE = [[0.4, 0.2], [0.8, 0.2], [0.5, 0.5], [0.95, 0.95], [0.0, 0.5], [-0.95, 0.95], [-0.5, 0.5], [-0.8, 0.2], [-0.2, 0.2], [-0.2, -0.2], [-0.8, -0.2], [-0.5, -0.5], [-0.95, -0.95], [0.0, -0.5], [0.95,-0.95], [0.5, -0.5], [0.8, -0.2], [0.2, -0.2], [0.2, 0.2], [0.6, 0.2], [0.8, 0.2]];
function sizeCanvas() {
w2 = (w = canvas.width = innerWidth) / 2;
h2 = (h = canvas.height = innerHeight) / 2;
scale = Math.min(w2, h2);
resetLine();
}
function addToPath(shape) {
var p1, p2;
for (p2 of shape) {
!p2.length ?
ctx.closePath() :
(p1 ? ctx.arcTo(p1[0] * scale + w2, p1[1] * scale + h2, p2[0] * scale + w2, p2[1] * scale + h2, RADIUS * scale) :
ctx.lineTo(p2[0] * scale + w2, p2[1] * scale + h2)
);
p1 = p2;
}
}
function resetLine() {
ctx.setLineDash([MAX_LINE_LENGTH * scale, MAX_LINE_LENGTH * scale]);
linePos = MAX_LINE_LENGTH * scale;
ctx.lineWidth = LINE_WIDTH * scale;
ctx.lineJoin = ctx.lineCap = "round";
}
function mainLoop() {
if (w !== innerWidth || h !== innerHeight) { sizeCanvas() }
else { ctx.clearRect(0, 0, w, h) }
ctx.beginPath();
addToPath(SHAPE);
ctx.lineDashOffset = (linePos -= LINE_SPEED * scale * (1 / 60));
ctx.stroke();
if (linePos <= 0) { resetLine() }
requestAnimationFrame(mainLoop);
}
body {
padding: 0px,
margin: 0px;
}
canvas {
position: absolute;
top: 0px;
left: 0px;
}
<canvas id="canvas"></canvas>
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.
I'm trying to draw in a canvas in a react component. The component draws a line and a number of squares depending on the length of an array passed to it as props inclining rotating all of them depending on another prop.
I have a loop that draws it perfectly until it reaches the 5th iteration, then something happens and it start to mess with the context restoration after the rotation. There is only one change of value in that loop ( initialX) Debugging the loop in the browser the rotate method is called the same times as the restore. I'm really confused by this behaviour, it is a very simple draw and I can't see where is my mistake.
This is what I'm getting
This is the same sketch without applying rotation
class Sketch extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
let canvas = document.getElementById("plano");
let detector = this.props.detector
let X, Y;
if (canvas && canvas.getContext && detector) {
inicializarCanvas(detector);
function inicializarCanvas(detector) {
let ctx = canvas.getContext("2d");
let s = getComputedStyle(canvas);
let w = s.width;
let h = s.height;
canvas.width = w.split("px")[0];
canvas.height = h.split("px")[0];
X = canvas.width / 2;
Y = canvas.height / 2;
//draw beam
ctx.moveTo( canvas.width / 3, canvas.height / 2);
ctx.lineTo(0, canvas.height / 2);
ctx.strokeStyle = "#f00";
ctx.stroke();
ctx.restore();
ctx.restore();
ctx.save();
drawBlades(ctx, canvas.width, canvas.height, detector)
}
function drawBlades(ctx, x, y, detector) {
let initialX = x / 3
let initialY = y / 4
let thick = 20
let margin = 5
let rotation = (90 - detector.angle) * Math.PI / 180
let i = 0
ctx.save();
let canvas = document.getElementById("plano");
let ctx2 = canvas.getContext("2d");
ctx2.save();
console.log("blade draw")
//This loop is messing up at the 5th iteration
for (; i < detector.blades.length; i++) {
ctx2.strokeStyle = "#000000";
ctx2.translate(initialX, initialY);
ctx2.rotate(rotation);
ctx2.strokeRect(0, 0, thick, y / 2);
ctx2.restore()
// this is the only variable in that changes of
// value in the loop
initialX = margin + thick + initialX
}
ctx2.save()
}
}
}
render() {
return (
<div className='sketch'>
<canvas width="400" height="150" id="plano">
Canvas not compatible with your browser
</canvas>
</div>
)
}
};
You're restoring your context in each iteration but you don't save it.
Try to add a ctx2.save() and it will work.
for (; i < detector.blades.length; i++) {
ctx2.save(); // save the context
ctx2.strokeStyle = "#000000";
ctx2.translate(initialX, initialY);
ctx2.rotate(rotation);
ctx2.strokeRect(0, 0, thick, y / 2);
ctx2.restore()
// this is the only variable in that changes of
// value in the loop
initialX = margin + thick + initialX
}
I have a image of width * height = 1442 * 1303,
I am able to read them and render to canvas successfully via webgl's texture2D.
In client side ,I am having a arraybuffer that gets the image data which is of size = width*height*4.
So, How to maintain aspect ratio of the image when my canvas width and height is window.innerWidth*0.90 and window.innerHeight*0.90.
Also, I have to directly render using arraybuffer via WEBGL 2dTexture so, I can't use any 2d canvs API such as drawImage. Please suggest something.
There's literally a million answers to this question.
First there's the size of your image, then the size you decide to draw it, and the size of the canvas, followed by the size the canvas is displayed. There's the positions of the vertices your using as well which could be anything.
See this article on WebGL which points out that WebGL uses clip space coordinates (-1 to +1) and this article points out that the size a canvas is displayed is separate from its resolution.
Let's assume you want to draw the image as large as possible and fit it to the canvas.
So first let's look up the size the canvas is being displayed
var canvasDisplayWidth = gl.canvas.clientWidth;
var canvasDisplayHeight = gl.canvas.clientHeight;
Let's assume we want to draw the image as large as possible so
first try fitting the width to the canvas
var imageDisplayWidth = canvasDisplayWidth;
var imageDisplayHeight = img.height * imageDisplayWidth / img.width;
Now let's check if it fit? If not let's use the height
if (imageDrawHeight > canvasDisplayHeight) {
imageDisplayHeight = canvasDisplayHeight;
imageDisplayWidth = img.width * imageDisplayHeight / img.height;
}
Now we need to convert imageDisplayWidth and imageDisplayHeight to the size of pixels in the canvas. Note: If the canvas is being displayed the same size
as the its resolution you can skip this step as the display size and the draw size will be the same.
// make our image take into account the pixel aspect
var canvasPixelsAcrossPerDisplayPixel = gl.canvas.width / canvasDisplayWidth;
var canvasPixelsDownPerDisplayPixel = gl.canvas.height / canvasDisplayHeight;
var imageDrawWidth = imageDisplayWidth * canvasPixelsAcrossPerDisplayPixel;
var imageDrawHeight = imageDisplayHeight * canvasPixelsDownPerDisplayPixel;
Now we need to convert that to clip space
var clipWidth = imageDrawWidth / canvas.width;
var clipHeight = imageDrawHeight / canvas.height;
Now, given a unit quad we can just scale it to fit that size.
var m = m4.identity();
// convert our square unit quad match the size we want
m4.scale(m, [clipWidth, clipHeight, 1], m);
// move our unit square from 0,0 (the center) to the bottom, top corner
m4.translate(m, [-1, 1, 0], m);
// scale our unit sqaure to cover the clip space
m4.scale(m, [2, -2, 1], m);
Now can draw with that matrix and our unit quad
var m4 = twgl.m4;
var gl = twgl.getWebGLContext(document.getElementById("c"));
var programInfo = twgl.createProgramInfo(gl, ["vs", "fs"]);
var arrays = {
position: {
numComponents: 2,
data: [
0, 0,
1, 0,
0, 1,
0, 1,
1, 0,
1, 1,
],
},
};
var bufferInfo = twgl.createBufferInfoFromArrays(gl, arrays);
// Lets make a texture using a 2d canvas
// There's a circle in the middle. If our
// code is correct it will be a circle when
// drawn (not an oval or ellipse)
var ctx = document.createElement("canvas").getContext("2d");
ctx.canvas.width = 100;
ctx.canvas.height = 75;
ctx.fillStyle = "red";
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.fillStyle = "blue";
ctx.fillRect(10, 10, ctx.canvas.width - 20, ctx.canvas.height - 20);
ctx.strokeStyle = "yellow";
ctx.lineWidth = 20;
ctx.beginPath();
ctx.arc(
ctx.canvas.width / 2, ctx.canvas.height / 2,
Math.min(ctx.canvas.width, ctx.canvas.height) / 2 - 20,
0, Math.PI * 2, false);
ctx.stroke();
var img = ctx.canvas;
var tex = twgl.createTexture(gl, {
src: img,
});
var canvasDisplayWidth = gl.canvas.clientWidth;
var canvasDisplayHeight = gl.canvas.clientHeight;
// Let's assume we want to draw the image as large as possible so
// first try fitting the width to the canvas
var imageDisplayWidth = canvasDisplayWidth;
var imageDisplayHeight = img.height * imageDisplayWidth / img.width;
// Now let's check if it fit? If not let's use the height
if (imageDisplayHeight > canvasDisplayHeight) {
imageDisplayHeight = canvasDisplayHeight;
imageDisplayWidth = img.width * imageDisplayHeight / img.height;
}
// Now we need to convert `imageDisplayWidth` and `imageDisplayHeight` to the size of pixels
// in the canvas. Note: If the canvas is being displayed the same size
// as the its resolution you can skip this step
var canvasPixelsAcrossPerDisplayPixel = gl.canvas.width / canvasDisplayWidth;
var canvasPixelsDownPerDisplayPixel = gl.canvas.height / canvasDisplayHeight;
var imageDrawWidth = imageDisplayWidth * canvasPixelsAcrossPerDisplayPixel;
var imageDrawHeight = imageDisplayHeight * canvasPixelsDownPerDisplayPixel;
// Now we need to convert that to clip space
var clipWidth = imageDrawWidth / gl.canvas.width;
var clipHeight = imageDrawHeight / gl.canvas.height;
// Now, given a unit quad we can just scale it to fit that size.
var m = m4.identity();
// convert our square unit quad to something to match the image's aspect
m4.scale(m, [clipWidth, clipHeight, 1], m);
// move our unit square from 0,0 (the center) to the bottom, left corner
m4.translate(m, [-1, 1, 0], m);
// scale our unit square to cover the clip space
m4.scale(m, [2, -2, 1], m);
var uniforms = {
texture: tex,
matrix: m,
};
gl.useProgram(programInfo.program);
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
twgl.setUniforms(programInfo, uniforms);
twgl.drawBufferInfo(gl, gl.TRIANGLES, bufferInfo);
<script id="vs" type="notjs">
attribute vec4 position;
uniform mat4 matrix;
varying vec2 v_texcoord;
void main() {
gl_Position = matrix * position;
// using position since we know it's a unit quad
v_texcoord = position.xy;
}
</script>
<script id="fs" type="notjs">
precision mediump float;
uniform sampler2D texture;
varying vec2 v_texcoord;
void main() {
gl_FragColor = texture2D(texture, v_texcoord);
}
</script>
<script src="https://twgljs.org/dist/twgl-full.min.js"></script>
<canvas id="c" width="50" height="100" style="width: 300px; height: 150px; border: 1px solid black;"></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 )