I'm trying to create a JavaScript object that has a method which allows a rectangle to rotate around its own origin during a rAF callback.
Things I have done:
Calculating the origin of an object within the canvas space.
Using ctx.save() and ctx.restore() - this is where my issues arise.
When I use the save() and restore() methods to push and pop the saved states within method calls for different objects it either doesn't change anything, or stops the entire animation.
The rotation in my example appears to be applied globally to the canvas (which is how the functionality is specified on MDN). I'm trying to translate around origin around multiple instances. I've spent hours on this.
Is there something going on with the inheritance mechanism in JavaScript that's not resetting my transforms for different instances of the rectangle objects in the code example?
// author: Nicholas Fazzolari
var canvas = document.querySelector('canvas');
var ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
var xCenterCanvas = innerWidth/2;
var yCenterCanvas = innerHeight/2;
// custom rectangle object
function RectangleCustom(x, y, w, h, color) {
this.w = w;
this.h = h;
this.x = x;
this.y = y;
this.color = color;
this.radians = (Math.PI/180) * 2; // change the last value to change speed
// draws a rectangle at given coordinates
this.draw = function() {
ctx.save();
ctx.fillStyle = this.color;
ctx.fillRect(this.x, this.y, this.w, this.h);
ctx.restore();
}
// rotates the rectangle around it's center relative to a given xy position
this.rotateRect = function() {
ctx.save();
ctx.translate(this.x + this.w * 0.5, this.y + this.h * 0.5);
ctx.rotate(this.radians);
ctx.translate(-this.x -this.w * 0.5, -this.y - this.h * 0.5);
//ctx.restore()
}
}
// singleton rectangles
var bkgRectangle = new RectangleCustom(0, 0, innerWidth, innerHeight, "#212121");
var redRectangle = new RectangleCustom(xCenterCanvas - 64, yCenterCanvas - 64, 128, 128, "#F44336");
// main animation loop
function mainAnimationLoop() {
// runs animation and clears the canvas each call
requestAnimationFrame(mainAnimationLoop);
ctx.clearRect(0, 0, innerWidth, innerHeight);
bkgRectangle.draw();
redRectangle.draw();
redRectangle.rotateRect();
}
mainAnimationLoop();
I have tried rotating multiple rectangles around their own origin at different positions without animation using save() and restore() - which worked.
Additionally, I have tried moving the rotate method inside of the draw method and the results were the same. My rationale was that the rotation would be applied as a function call within draw() - the rationale was clearly wrong.
Any insight towards a solution would be greatly helpful. I have included a link to the pen on codepen to see the concept in motion.
Instead of drawing the rects at (this.x, this.y) you may draw them at 0,0 and translate them to (this.x, this.y);
// author: Nicholas Fazzolari
var canvas = document.querySelector('canvas');
var ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
var xCenterCanvas = innerWidth/2;
var yCenterCanvas = innerHeight/2;
// custom rectangle object
function RectangleCustom(x, y, w, h, color) {
this.w = w;
this.h = h;
this.x = x;
this.y = y;
this.color = color;
this.radians = (Math.PI/180) * 2; // change the last value to change speed
this.rotation = 0;
// draws a rectangle at given coordinates
this.draw = function() {
this.rotation += this.radians;
ctx.save();
ctx.fillStyle = this.color;
ctx.translate(this.x, this.y);
ctx.rotate(this.rotation);
ctx.fillRect(0,0, this.w, this.h);
ctx.restore();
}
this.update = function() {
// animation updates
}
}
// singleton rectangles
var bkgRectangle = new RectangleCustom(0, 0, innerWidth, innerHeight, "#212121");
var redRectangle = new RectangleCustom(xCenterCanvas - 64, yCenterCanvas - 64, 128, 128, "#F44336");
// main animation loop
function mainAnimationLoop() {
// runs animation and clears the canvas each call
requestAnimationFrame(mainAnimationLoop);
ctx.clearRect(0, 0, innerWidth, innerHeight);
bkgRectangle.draw();
redRectangle.draw();
}
mainAnimationLoop();
<canvas></canvas>
Related
Link to JSFiddle for entire code: https://jsfiddle.net/u4mk0gdt/
I read the Mozilla docs on save() and restore() and I thought that "save" saved the current state of the entire canvas and "restore" restored the canvas to the most recent "save" state. Hence I placed the saves and restores in such a way that it should clear the white line that is drawn to canvas after is is drawn. However when I run this code the white line is never cleared from the canvas and is drawn continually without clearing.
ctx.restore();
ctx.save(); // <--should save blank canvas
//DRAW LINE
ctx.moveTo(tMatrix.x1, tMatrix.y1);
ctx.lineTo(w/2,h/2);
ctx.strokeStyle = "white";
ctx.stroke();
ctx.restore(); // <-- should restore to the "save()" above
ctx.save(); // <-- <--should save blank canvas again
As you can see, I made a lot of modifications to your code:
console.log("rotating_recs");
// create canvas and add resize
var canvas, ctx;
function createCanvas() {
canvas = document.createElement("canvas");
canvas.style.position = "absolute";
canvas.style.left = "0px";
canvas.style.top = "0px";
canvas.style.zIndex = 1000;
document.body.appendChild(canvas);
}
function resizeCanvas() {
if (canvas === undefined) {
createCanvas();
}
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx = canvas.getContext("2d");
}
resizeCanvas();
window.addEventListener("resize", resizeCanvas);
var Player = function(x, y, height, width, rot) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.rot = rot;
this.objWinX = 0; //translate the window object and then apply to this
this.objWinY = 0;
this.draw = function() {
//rotate by user.rot degrees, from the players center
ctx.translate(this.x + this.width / 2, this.y + this.height / 2)
ctx.rotate(this.rot * Math.PI / 180)
ctx.translate(-this.x - this.width / 2, -this.y - this.height / 2)
ctx.fillStyle = "grey";
ctx.fillRect(this.x, this.y, this.height, this.width);
ctx.translate(this.x + this.width / 2, this.y + this.height / 2)
ctx.rotate(-this.rot * Math.PI / 180)
ctx.translate(-this.x - this.width / 2, -this.y - this.height / 2)
}
}
var user = new Player(0, 0, 40, 40, 0);
var user2 = new Player(0, 0, 40, 40, 0);
let rot = 0;
function update(time) {
var w, h;
w = canvas.width; // get canvas size incase there has been a resize
h = canvas.height;
ctx.clearRect(0, 0, w, h); // clear the canvas
//MIDDLE RECT
/*
if you don't want this you can just translate by w/2 and h/2, but I would recommend just making the p layers position the middle
*/
user.x = w / 2 - 20;
user.y = h / 2 - 20;
user.rot += 0.5 // or whatever speed
user.draw(); //draw player -- look at the draw function I added some stuff
//LINE
/*
I don't know what you are trying to do, but I just drew the line to the user2's position,
if this doesn't work for your scenario you can change it back
*/
ctx.beginPath()
ctx.moveTo(user2.x + user2.width/2, user2.y + user2.height/2);
ctx.lineTo(w / 2, h / 2);
ctx.strokeStyle = "white";
ctx.stroke();
//FAST SPIN RECT
/*
There are multiple ways to do this, the one that I think you should do, is actually change the position of user two, this uses some very simple trigonometry, if you know this, this is a great way to do this, if not, you can do it how you did previously, and just translate to the center, rotate, and translate back. Similar to what I did with the player draw function. I am going to demonstrate the trig way here:
*/
user2.rot += 5
rot += 2;
user2.x = w/2 + (w/2) * Math.cos(rot * (Math.PI/180))
user2.y = h/2 + (w/2) * Math.sin(rot * (Math.PI/180))
user2.draw();
//RED RECT
ctx.fillStyle = 'red';
ctx.fillRect(140, 60, 40, 40);
requestAnimationFrame(update); // do it all again
}
requestAnimationFrame(update);
While I think you should add some of these modifications into you code, they are not super necessary. To fix you line problem, all you had to do was add ctx.beginPath() before you drew it. The demonstration that I made was not very good (hence demonstration), and you probably shouldn't use it exactly, but definitely look over it. The modified code for you line drawing would look like:
//LINE
ctx.beginPath()
ctx.moveTo(tMatrix.x1, tMatrix.y1);
ctx.lineTo(w/2,h/2);
ctx.strokeStyle = "white";
ctx.stroke();
ctx.restore();
ctx.save();
Hope this helps :D
Sorry for bad spelling
I have a blue circle which is rotating around the red circle and moves on canvas continuously in one direction as long as the button is pressed.
Now I want to draw with the red circle while it is moving when the button is pressed (trace of its path).
Problems:
I have tried to make changes to clearRect() but I didn't succeed. the blue circle starts to draw on the canvas while moving which I don't need.
If its not possible to do with clearRect() function, Is it possible to do this by stacking canvas layers. Please help with example
const canvas = document.getElementById('canvas1');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
let positionX = 100;
let positionY = 100;
let X = 50;
let Y = 50;
let angle = 0;
let mouseButtonDown = false;
document.addEventListener('mousedown', () => mouseButtonDown = true);
document.addEventListener('mouseup', () => mouseButtonDown = false);
function circle(){
ctx.fillStyle = 'red';
ctx.beginPath();
ctx.arc(X, Y, 20, 0, Math.PI*2);
ctx.closePath();
ctx.fill();
}
function direction(){
ctx.fillStyle = 'blue';
ctx.beginPath();
ctx.arc(positionX + X, positionY + Y, 10, 0, Math.PI*2);
ctx.closePath();
positionX = 35 * Math.sin(angle);
positionY = 35 * Math.cos(angle);
ctx.fill();
}
function animate(){
if (mouseButtonDown) {
X += positionX / 10;
Y += positionY / 10;
} else {
angle += 0.1;
}
ctx.clearRect(X-positionX,Y-positionY, 20, 20);
circle();
direction();
requestAnimationFrame(animate);
}
animate();
#canvas1{
position: absolute;
top:0;
left: 0;
width: 100%;
height: 100%;
}
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="style.css">
</head>
<body>
<canvas id="canvas1"></canvas>
<script src="script.js"></script>
</body>
</html>
Don`t stack canvas on the page
Each canvas you add to the page increases the amount of work the GPU and page compositor needs to do to render the page.
Use a second canvas that is not on the page and do the compositing by rendering the canvas to the onpage canvas using ctx.drawImage(secondCanvas, 0, 0).
This reduces the workload for the compositor, and in many cases avoid the need to do an addition image render (composite) for the second canvas I.E. onpage can require 3 drawImages (one for each canvas and once for the result) rather than 2 (once in your code and once as the result) if you use only one onpage canvas.
Using second canvas
Create a second canvas to store the drawn red lines.
You can create a copy of a canvas using
function copyCanvas(canvas, copyContent = false) {
const can = Object.assign(document.createElement("canvas"), {
width: canvas.width, height: canvas.height
});
can.ctx = can.getContext("2d");
copyContent && can.ctx.drawImage(canvas, 0, 0);
return can;
}
When you create render functions like circle, and direction pass as an argument the 2D context eg circle(ctx) so that it is easy to direct the rendering to any canvas.
function circle(ctx){
ctx.fillStyle = 'red';
ctx.beginPath();
ctx.arc(X, Y, redSize, 0, Math.PI*2);
ctx.fill();
}
// the background canvas
const bgCan = copyCanvas(canvas);
circle(bgCan.ctx); // will draw to the background canvas
Updating animation
When animating is is easiest to clear the whole canvas rather than mess about clearing only rendered pixels. Clearing rendered pixels gets complicated very quickly and will end up being many times slower than clearing the whole canvas.
After you clear the canvas draw the background canvas to the main canvas
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.drawImage(bgCan, 0, 0);
When the mouse button is down draw the circle to the background canvas and while it is up draw to the main canvas.
Example
Adds a function to copy a canvas. copyCanvas
Clears the main canvas, and draws the background canvas onto the main canvas.
Render functions circle and direction have argument ctx to direct rendering to any context.
When mouse is down circle is drawn to background canvas bgCan else to the main canvas.
requestAnimationFrame(animate);
const ctx = canvas1.getContext('2d');
canvas1.width = innerWidth;
canvas1.height = innerHeight;
const bgCan = copyCanvas(canvas1);
const redSize = 10, blueSize = 5; // circle sizes on pixels
const drawSpeed = 2; // when button down draw speed in pixels per frame
var X = 50, Y = 50;
var angle = 0;
var mouseButtonDown = false;
document.addEventListener('mousedown', () => mouseButtonDown = true);
document.addEventListener('mouseup', () => mouseButtonDown = false);
function copyCanvas(canvas) {
const can = Object.assign(document.createElement("canvas"), {
width: canvas.width, height: canvas.height
});
can.ctx = can.getContext("2d");
return can;
}
function circle(ctx){
ctx.fillStyle = 'red';
ctx.beginPath();
ctx.arc(X, Y, redSize, 0, Math.PI*2);
ctx.fill();
}
function direction(ctx){
const d = blueSize + redSize + 5;
ctx.fillStyle = 'blue';
ctx.beginPath();
ctx.arc(d * Math.sin(angle) + X, d * Math.cos(angle) + Y, blueSize, 0, Math.PI*2);
ctx.fill();
}
function animate(){
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.drawImage(bgCan, 0, 0);
if (mouseButtonDown) {
circle(bgCan.ctx);
X += Math.sin(angle) * drawSpeed;
Y += Math.cos(angle) * drawSpeed;
} else {
angle += 0.1;
circle(ctx);
}
direction(ctx);
requestAnimationFrame(animate);
}
#canvas1{
position: absolute;
top:0;
left: 0;
width: 100%;
height: 100%;
}
<canvas id="canvas1"></canvas>
BTW ctx.closePath() is like ctx.lineTo it is not the opposite to ctx.beginPath. A full arc or if you are just filling a shape you don't need to use ctx.closePath
BTW window is the default this, you don't need to include it, you dont use it to get at window.documentso why use it forwindow.innerWidth(same asinnerWidth` )
You could alter your code to keep track of the path of the red circle, with an array property, like this:
const canvas = document.getElementById('canvas1');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
let mouseButtonDown = false;
document.addEventListener('mousedown', () => mouseButtonDown = true);
document.addEventListener('mouseup', () => mouseButtonDown = false);
function drawCircle({x, y, radius, color}) {
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI*2);
ctx.fill();
}
const red = { x: 50, y: 50, radius: 20, color: "red", path: [] };
const blue = { x: 100, y: 100, radius: 10, color: "blue", angle: 0 };
function animate(){
if (mouseButtonDown) {
red.path.push({x: red.x, y: red.y}); // store the old value
red.x += (blue.x - red.x) / 10;
red.y += (blue.y - red.y) / 10;
} else {
blue.angle += 0.1;
}
blue.x = red.x + 35 * Math.sin(blue.angle);
blue.y = red.y + 35 * Math.cos(blue.angle);
ctx.clearRect(0, 0, canvas.width, canvas.height); // clear the whole canvas
for (const {x, y} of red.path) { // draw circle at all the previous positions
drawCircle({...red, x, y});
}
drawCircle(red);
drawCircle(blue);
requestAnimationFrame(animate);
}
animate();
Using 2 canvases also works and may perform better especially when the path of the red circle has gotten long, because the background canvas doesn't need to be cleared and redrawn. Add a 2nd canvas in your html page with the same positioning, and give them ids 'background' and 'foreground'. You can then adjust the code to draw the blue circle to the foreground and red circles to the background (or vice versa).
// Create 2 canvases, set them to full size and get the contexts
const backgroundCanvas = document.getElementById('background');
const foregroundCanvas = document.getElementById('foreground');
const background = backgroundCanvas.getContext("2d");
const foreground = foregroundCanvas.getContext("2d");
backgroundCanvas.width = innerWidth;
backgroundCanvas.height = innerHeight;
foregroundCanvas.width = innerWidth;
foregroundCanvas.height = innerHeight;
let mouseButtonDown = false;
document.addEventListener('mousedown', () => mouseButtonDown = true);
document.addEventListener('mouseup', () => mouseButtonDown = false);
// Create objects to represent the current properties of the red and blue circle
const red = { x: 50, y: 50, radius: 20, color: "red" };
const blue = { x: 100, y: 100, radius: 10, color: "blue", angle: 0};
function drawCircle(ctx, {x, y, radius, color}) {
//--- Draw a circle to the specified canvas context, ctx = foreground or background
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI*2);
ctx.closePath();
ctx.fill();
}
function animate(){
if (mouseButtonDown) {
red.x += (blue.x - red.x) / 10;
red.y += (blue.y - red.y) / 10;
} else {
blue.angle += 0.1;
}
blue.x = red.x + 35 * Math.sin(blue.angle);
blue.y = red.y + 35 * Math.cos(blue.angle);
drawCircle(background, red); // Draw the red circle in the background (without clearing the existing circles)
foreground.clearRect(0, 0, foregroundCanvas.width, foregroundCanvas.height); // Clear the foreground
drawCircle(foreground, blue); // Draw the blue circle on the foreground
requestAnimationFrame(animate);
}
animate();
Either way, it's convenient to abstract out the circle drawing code into a function or method, and to store the properties of the two circles in objects.
As #Blindman67's answer notes, there may be a performance cost of stacking 2 canvases, and if that is an issue you may want to try drawing the background offscreen then copying it to the on-screen canvas.
If you're not opposed to just building a particle class you can do it using them. In the snippet below I have a Circle class and a Particles class to creat what you are trying to achieve. I currently have the particles max at 500 but you can change it or delete that line all together if you ne er want them gone.
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
let mouseButtonDown = false;
//the array holding particles
let particles = [];
//the counter is only needed it you want to slow down how fast particles are being pushed and dispolayed
let counter = 0;
document.addEventListener("mousedown", () => (mouseButtonDown = true));
document.addEventListener("mouseup", () => (mouseButtonDown = false));
//ES6 constructor class
class Circle {
//sets the basic structor of the object
constructor(r, c) {
this.x = 100;
this.y = 100;
this.x2 = 50;
this.y2 = 50;
this.r = r; //will be assigned the argument passed in through the constructor by each instance created later
this.color = c; //same as above. This allows each instance to have different parameters.
this.angle = 0;
}
//this function creates the red circle
drawRed() {
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
ctx.fill();
ctx.closePath();
}
//this function creates the blue circle
drawBlue() {
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.x + this.x2, this.y + this.y2, this.r, 0, Math.PI * 2);
ctx.fill();
ctx.closePath();
}
//this function is where we'll place parameter that change our object
update() {
//makes the blue circle rotate
this.x2 = 35 * Math.sin(this.angle);
this.y2 = 35 * Math.cos(this.angle);
//mouse action is same as your code
if (mouseButtonDown) {
this.x += this.x2 / 20;
this.y += this.y2 / 20;
} else {
this.angle += 0.1;
}
}
}
//When using this type of constructor class you have to create an instance of it by calling new Object. You can create as money as you want.
let blueCircle = new Circle(10, "blue"); //passing in the radius and color in to the constructor
let redCircle = new Circle(20, "red");
//another class for the particles
class Particles {
constructor() {
this.x = redCircle.x;
this.y = redCircle.y;
this.r = redCircle.r;
this.color = redCircle.color;
}
draw() {
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
ctx.fill();
ctx.closePath();
}
}
//just wrapping all of the particle stuff into one function
function handleParticles() {
//while the mouse is held it will push particles
if (mouseButtonDown) {
particles.push(new Particles());
}
//this loops through the array and calls the draw() function for each particle
for (let i = 0; i < particles.length; i++) {
particles[i].draw();
}
//this keeps the array from getting too big.
if (particles.length > 500) {
particles.shift();
}
}
//wrap all functions into this one animate one and call requeatAnimationFrame
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
handleParticles();
//These must be called for each instance created of the object
blueCircle.drawBlue();
blueCircle.update();
redCircle.drawRed();
redCircle.update();
requestAnimationFrame(animate);
}
animate();
#canvas1{
position: absolute;
top:0;
left: 0;
width: 100%;
height: 100%;
}
<canvas id="canvas"></canvas>
I'd also like to add you can change the rate that the particles are drawn by adding a counter variable and then limiting the draw like counter % 10 == 0
EXAMPLE
add global variable let counter = 0;
then in the handleParticles function add this
function handleParticles() {
counter++
if (mouseButtonDown && counter % 10 == 0) {
particles.push(new Particles());
}
for (let i = 0; i < particles.length; i++) {
particles[i].draw();
}
if (particles.length > 500) {
particles.shift();
}
}
I am creating a game using the HTML5 Canvas element, and as one of the visual effects I would like to create a glow (like a light) effect. Previously for glow effects I found solutions involving creating shadows of shapes, but these require a solid shape or object to cast the shadow. What I am looking for is a way to create something like an ambient light glow with a source location but no object at the position.
Something I have thought of was to define a centerpoint x and y and create hundreds of concentric circles, each 1px larger than the last and each with a very low opacity, so that together they create a solid center and a transparent edge. However, this is very computationally heavy and does not seem elegant at all, as the resulting glow looks awkward.
While this is all that I am asking of and I would be more than happy to stop here, bonus points if your solution is A) computationally light, B) modifiable to create a focused direction of light, or even better, C) if there was a way to create an "inverted" light system in which the entire screen is darkened by a mask and the shade is lifted where there is light.
I have done several searches, but none have turned up any particularly illuminating results.
So I'm not quite sure what you want, but I hope the following snippet will help.
Instead of creating a lot of concentric circles, create one radialGradient.
Then you can combine this radial gradient with some blending, and even filters to modify the effect as you wish.
var img = new Image();
img.onload = init;
img.src = "https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/car.svg";
var ctx = c.getContext('2d');
var gradCtx = c.cloneNode().getContext('2d');
var w, h;
var ratio;
function init() {
w = c.width = gradCtx.canvas.width = img.width;
h = c.height = gradCtx.canvas.height = img.height;
draw(w / 2, h / 2)
updateGradient();
c.onmousemove = throttle(handleMouseMove);
}
function updateGradient() {
var grad = gradCtx.createRadialGradient(w / 2, h / 2, w / 8, w / 2, h / 2, 0);
grad.addColorStop(0, 'transparent');
grad.addColorStop(1, 'white');
gradCtx.fillStyle = grad;
gradCtx.filter = "blur(5px)";
gradCtx.fillRect(0, 0, w, h);
}
function handleMouseMove(evt) {
var rect = c.getBoundingClientRect();
var x = evt.clientX - rect.left;
var y = evt.clientY - rect.top;
draw(x, y);
}
function draw(x, y) {
ctx.clearRect(0, 0, w, h);
ctx.globalCompositeOperation = 'source-over';
ctx.drawImage(img, 0, 0);
ctx.globalCompositeOperation = 'destination-in';
ctx.drawImage(gradCtx.canvas, x - w / 2, y - h / 2);
ctx.globalCompositeOperation = 'lighten';
ctx.fillRect(0, 0, w, h);
}
function throttle(callback) {
var active = false; // a simple flag
var evt; // to keep track of the last event
var handler = function() { // fired only when screen has refreshed
active = false; // release our flag
callback(evt);
}
return function handleEvent(e) { // the actual event handler
evt = e; // save our event at each call
if (!active) { // only if we weren't already doing it
active = true; // raise the flag
requestAnimationFrame(handler); // wait for next screen refresh
};
}
}
<canvas id="c"></canvas>
I want to have it so that when i create a "component" i can set its radius to make it curved. Below is my code for component create:
function component(width, height, color, x, y) {
this.width = width;
this.height = height;
this.speedX = 0;
this.speedY = 0;
this.x = x;
this.y = y;
this.color = color;
this.update = function() {
ctx = GameArena.context;
ctx.fillStyle = this.color;
ctx.fillRect(this.x, this.y, this.width, this.height);
}
}
as you can see it specifies the width height, color and x and y positions but i can't find a way to give it a radius. The other end of my code that uses this component function is here:
PaintBrush = new component(30, 30, "Blue", 30, 320);
Help would be appreciated!
Drawing a rectangle with rounded corners can be done using arcs instead:
The arc takes the arguments:
arc(x, y, radius, startAngle, endAngle [,ccw]); // we won't need counter-clockwise
For example:
var pi2 = Math.PI * 2; // 360 deg.
var r = this.radius, w = this.width, h = this.height;
// ...
// draw rounded rectangle
ctx.beginPath();
ctx.arc(r, r, r, pi2*0.5, pi2*0.75); // top-left
ctx.arc(r+w-r*2, r, r, pi2*0.75, pi2); // top-right
ctx.arc(r+w-r*2, r+h-r*2, r, 0, pi2*0.25); // bottom-right
ctx.arc(r, r+h-r*2, r, pi2*0.25, pi2*0.5); // bottom-left
This simply draws four arc in each corner using radius and start and end angle. Since we use a single path lines will be drawn between each arc from the end of the previous arc to the beginning of the new one - which is why the order matters.
Simply fill() to close the path and fill the shape. If you want to stroke() it as well remember to use closePath() first. If you have paths added later via other objects etc., also remember to use beginPath() before you add them.
The line setting radius will also clamp it to the minimum size possible:
this.radius = Math.min(radius, Math.min(width, height)/2);
First the minimum of height and width is used divided on two. Then the minimum of radius and this result. This makes sure the radius can't be larger then half of the shortest side which would be "impossible".
A note on the setTransform() usage below - if you don't have accumulated transforms this should work fine. If you do and can't easily change it replace the setTransform()s with ctx.translate(this.x, this.y) and after finished reverse it by calling ctx.translate(-this.x, -this.y);. I would recommend using setTransforms for all your objects hover if they are transformed (rotated, scaled etc.) somehow.
Demo
var GameArena = {context: c.getContext("2d")}; // dummy
var PaintBrush = new component(200, 100, "#37f", 10, 10, 16);
PaintBrush.update();
function component(width, height, color, x, y, radius) {
this.width = width;
this.height = height;
this.speedX = 0;
this.speedY = 0;
this.x = x;
this.y = y;
this.radius = Math.min(radius, Math.min(width, height)/2); // clamp radius
this.color = color;
this.update = function() {
var pi2 = Math.PI * 2; // 360 deg.
var r = this.radius, w = this.width, h = this.height;
ctx = GameArena.context;
ctx.fillStyle = this.color;
ctx.setTransform(1,0,0,1,this.x, this.y); // transform (absolute here)
// draw rounded rectangle
ctx.beginPath();
ctx.arc(r , r , r, pi2*0.5 , pi2*0.75); // top-left
ctx.arc(w-r, r , r, pi2*0.75, pi2); // top-right
ctx.arc(w-r, h-r, r, 0 , pi2*0.25); // bottom-right
ctx.arc(r , h-r, r, pi2*0.25, pi2*0.5); // bottom-left
ctx.fill();
ctx.setTransform(1,0,0,1,0,0); // reset transform
}
}
<canvas id=c></canvas>
Here in this animation I've made two functions for two balls, but there is no second ball I am getting in this canvas.
My code for both balls-
function draw() {
ctx.clearRect(0, 0, 300, 300);
//ctx.beginPath();
//ctx.arc(x, y, 10, 0, 2 * Math.PI, true);
//ctx.closePath();
ctx.drawImage(img, x, y, 20, 20);
ctx.fill();
x += dx;
y += dy;
bounce();
}
function draw2()
{
ctx.clearRect(0,0,300,300);
ctx.beginPath();
ctx.arc(x, y, 10, 0, 2 * Math.PI, true);
ctx.closePath();
ctx.fill();
x += dx;
y += dy;
bounce();
}
Calling of functions-
function init() {
var ctx = document.getElementById("canvas").getContext("2d");
return setInterval(draw, 10);
return setInterval(draw2,20);
//This is how i am calling both function
}
Can we do this in Javascript?
Expecting result-
Both balls are coming from same position, I want when first ball bounces in canvas frame, just after 10 milliseconds another ball from draw2 () should come in frame and act the same.
Fiddle- http://jsfiddle.net/stackmanoz/B6XZC/4/
In order to get this working you will need to separate out your draw functions from your canvas clearing code, and have a tick/polling loop that is separate from the time you want your balls to appear.
You might as well use the power of JavaScript constructors to help you with your balls.
function ball( ctx, x, y, dx, dy ){
this.img = ? /// you'll have to set your image, whatever it is.
this.x = x||0;
this.y = y||0;
this.dx = dx||0;
this.dy = dy||0;
this.draw = function(){
ctx.drawImage(this.img, this.x, this.y, 20, 20);
}
this.tick = function(){
this.x += this.dx;
this.y += this.dy;
this.draw();
}
}
And then use the following to handle drawing.
function clear( ctx, cnv ){
ctx.clearRect(0, 0, 300, 300);
/// a faster way to clear can be:
/// cnv.width += 0;
/// or:
/// cnv.width = cnv.width;
}
/// you should always have a core loop that delegates to other functions/objs
function loop( cnv, ctx, balls ){
clear(ctx, cnv);
for( var i=0; i<balls.length; i++ ){
balls[i].tick()
}
}
function init() {
var cnv = document.getElementById("canvas");
var ctx = cnv.getContext("2d");
/// create the first ball and add it to your ball list
var balls = [new ball(ctx,50,0,1,1)];
/// 10ms wait before the extra ball is added
setTimeout(function(){balls.push( new ball(ctx,100,0,1,1) );},10);
/// this will be your animation loop
return setInterval(function(){loop(cnv, ctx, balls)}, 10);
}
The above has been hand-typed and not tested, and could be greatly improved.. but it should work and give you an idea.
Both draw() and draw2() clear the canvas, so you will only see the last update. Also you have a single global x,y,dx, and dy, which means both your balls are drawn at the exact same position forever.