I am not sure if I have used the right word here. I guess easing means it does not follow the mouse immediately but with some delay?
At the moment the iris is rotating to my mouse direction. What if I want it to have same effect as this?. Is it very hard to do so or just require simple code changes? Is there a standard way/solution for this kind of problem?
Here is my current code. It can also be found at Rotating Iris .
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
class Circle {
constructor(options) {
this.cx = options.x;
this.cy = options.y;
this.radius = options.radius;
this.color = options.color;
this.angle = options.angle;
this.binding();
}
binding() {
const self = this;
window.addEventListener('mousemove', (e) => {
self.calculateAngle(e);
});
}
calculateAngle(e) {
if (!e) return;
let rect = canvas.getBoundingClientRect(),
vx = e.clientX - this.cx,
vy = e.clientY - this.cy;
this.angle = Math.atan2(vy, vx);
}
renderEye() {
ctx.setTransform(1, 0, 0, 1, this.cx, this.cy);
ctx.rotate(this.angle);
let eyeRadius = this.radius / 3;
ctx.beginPath();
ctx.arc(this.radius / 2, 0, eyeRadius, 0, Math.PI * 2);
ctx.fill();
}
render() {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.beginPath();
ctx.arc(this.cx, this.cy, this.radius, 0, Math.PI * 2);
ctx.closePath();
ctx.strokeStyle = '#09f';
ctx.lineWidth = 1;
ctx.stroke();
this.renderMessage();
this.renderEye();
}
renderMessage() {
ctx.font = "18px serif";
ctx.strokeStyle = 'black';
ctx.fillText('Angle: ' + this.angle, 30, canvas.height - 40);
}
}
var rotatingCircle = new Circle({
x: 320,
y: 160,
radius: 40,
color: 'black',
angle: Math.random() * Math.PI * 2
});
function animate() {
rotatingCircle.render();
requestAnimationFrame(animate);
}
animate();
<canvas id='canvas' style='width: 700; height: 500;'></canvas>
Updated with possible solution:
I actually followed the link I posted in the question and use a similar way to ease the rotation, which I think is similar to what #Blindman67 categories as non-deterministic easing.
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
class Circle {
constructor(options) {
this.cx = options.x;
this.cy = options.y;
this.radius = options.radius;
this.color = options.color;
this.toAngle = 0;
this.angle = options.angle;
this.velocity = 0;
this.maxAccel = 0.04;
this.binding();
}
binding() {
const self = this;
window.addEventListener('mousemove', (e) => {
self.calculateAngle(e);
});
}
calculateAngle(e) {
if (!e) return;
let rect = canvas.getBoundingClientRect(),
// mx = parseInt(e.clientX - rect.left),
// my = parseInt(e.clientY - rect.top),
vx = e.clientX - this.cx,
vy = e.clientY - this.cy;
this.toAngle = Math.atan2(vy, vx);
}
clip(x, min, max) {
return x < min ? min : x > max ? max : x;
}
renderEye() {
ctx.setTransform(1, 0, 0, 1, this.cx, this.cy);
let radDiff = 0;
if (this.toAngle != undefined) {
radDiff = this.toAngle - this.angle;
}
if (radDiff > Math.PI) {
this.angle += 2 * Math.PI;
} else if (radDiff < -Math.PI) {
this.angle -= 2 * Math.PI;
}
let easing = 0.06;
let targetVel = radDiff * easing;
this.velocity = this.clip(targetVel, this.velocity - this.maxAccel, this.velocity + this.maxAccel);
this.angle += this.velocity;
ctx.rotate(this.angle);
let eyeRadius = this.radius / 3;
ctx.beginPath();
ctx.arc(this.radius / 2, 0, eyeRadius, 0, Math.PI * 2);
ctx.fill();
}
render() {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.beginPath();
ctx.arc(this.cx, this.cy, this.radius, 0, Math.PI * 2);
ctx.closePath();
ctx.strokeStyle = '#09f';
ctx.lineWidth = 1;
ctx.stroke();
this.renderMessage();
this.renderEye();
}
renderMessage() {
ctx.font = "18px serif";
ctx.strokeStyle = 'black';
ctx.fillText('Angle: ' + this.angle.toFixed(3), 30, canvas.height - 40);
ctx.fillText('toAngle: ' + this.toAngle.toFixed(3), 30, canvas.height - 20);
}
}
var rotatingCircle = new Circle({
x: 250,
y: 130,
radius: 40,
color: 'black',
angle: Math.random() * Math.PI * 2
});
function animate() {
rotatingCircle.render();
requestAnimationFrame(animate);
}
animate();
<canvas id='canvas' style='width: 700; height: 500;'></canvas>
There are many ways to do easing. Two methods I will describe in short are deterministic easing and (surprisingly) non-deterministic. The difference being is that the destination of the ease is either known (determined) or unknown (awaiting more user input)
Deterministic easing.
For this you have a starting value and an end value. What you want to do is find a position between the two based on some time value. That means that the start and end values need to also be associated with a time.
For example
var startVal = 10;
var startTime = 100;
var endVal = 100;
var endTime = 200;
You will want to find the value at time 150 halfway between the two. To do this you convert the time to a fraction where the time 100 (start) returns 0 and the time 200 (end) return 1, we call this normalised time. You can then multiply the difference between the start and end values by this fraction to find the offset.
So for a time value 150 to get the value (theValue) we do the following.
var time = 150;
var timeDif = endTime - startTime
var fraction = (startTime - time) / timeDif; // the normalised time
var valueDif = endVal - startVal;
var valueOffset = valueDif * fraction;
var theValue = startVal + valueOffset;
or more concise.
// nt is normalised time
var nt = (startTime - time) / (endTime - startTime)
var theValue = startVal + (endVal - startVal) * nt;
Now to apply a easing we need to modify the normalised time. A easing function simply takes a value from 0 to 1 inclusive and modifies it. So if you input 0.25 the easing function returns 0.1, or 0.5 return 0.5 and 0.75 returns 0.9. As you can see the modification changes the rate of change over the time.
An example of an easing function.
var easeInOut = function (n, pow) {
n = Math.min(1, Math.max(0, n)); // clamp n
var nn = Math.pow( n, pow);
return (nn / ( nn + Math.pow(1 - n, pow)))
}
This function takes two inputs, the fraction n (0 to 1 inclusive) and the power. The power determines the amount of easing. If pow = 1 then the is no easing and the function returns n. If pow = 2 then the function is the same as the CSS ease in out function, starts slowly speeds up then slows down at the end. if pow < 1 and pow > 0 then the ease start quickly slows down midway and then speeds up to the end.
To use the easing function in the above easing value example
// nt is normalised time
var nt = (startTime - time) / (endTime - startTime);
nt = easeInOut(nt,2); // start slow speed up, end slow
var theValue = startVal + (endVal - startVal) * nt;
That is how deterministic easing is done
An excellent easing function page Easing examples and code and another page for a quick visual easing referance
Non-deterministic easing
You may not know what the end result of the easing function is as at any time it may change due to new user input, if you use the above methods and change the end value mid way through the ease the result will be inconsistent and ugly. If you have ever done calculus you may recognise that the ease function above is a polynomial and is thus the anti derivative of a simpler function. This function simply determines the amount of change per time step. So for the non deterministic solution all we know is the change for the next time step. For an ease in function (start quick and slow down as we approch the destination) we keep a value to represent the current speed (the rate of change) and modify that speed as needed.
const ACCELERATION_COEFFICIENT = 0.3;
const DRAG_COEFFICIENT = 0.99;
var currentVal = 100;
var destinationVal = 200;
var currentSpeed = 0;
Then for each time step you do the following
var accel = destinationVal - currentVal; // get the acceleration
accel *= ACCELERATION_COEFFICIENT; // modify it so we are not there instantly
currentSpeed += accel; // add that to the speed
currentSpeed *= DRAG_COEFFICIET; // add some drag to further ease the function as it approaches destination
currentVal += currentSpeed; // add the speed to the current value
Now the currentVal will approch the destination value, if the destination changes than the rate of change (speed) also changes in a consistent way. The currentVal may never get to the destination if the destination is always changing, if however the destination stops changing the current val will approch and eventually stop at destination (by stop I mean the speed will get so small as to be pointless)
This methods behaviour is very dependent on the two coefficients so playing with those values will vary the easing. Some values will give you an over shoot with a bit of a wobble, others will be very slow like moving through molasses.
You can also make it much more complex by adding a second rate of change, thus you can have accelerating acceleration, this will simulate things like air resistance that changes the acceleration over time. You can also add maximums to the rate of change to set speed limits.
That should help you do your easing.
More info
For more info see these answers How would I animate and How to scale between two points
Non-deterministic easing applied to your example
I have added the easing to your function but it has introduced a new problem that will happen when using cyclic values such as angle. As I will not go into it in this answer you can find a solution to that problem in Finding the smallest angle.
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const ACCELERATION_COEFFICIENT = 0.15;
const DRAG_COEFFICIENT = 0.5;
class Circle {
constructor(options) {
this.cx = options.x;
this.cy = options.y;
this.radius = options.radius;
this.color = options.color;
this.angle = options.angle;
this.angleSpeed = 0;
this.currentAngle = this.angle;
this.binding();
}
binding() {
const self = this;
window.addEventListener('mousemove', (e) => {
self.calculateAngle(e);
});
}
calculateAngle(e) {
if (!e) return;
let rect = canvas.getBoundingClientRect(),
vx = e.clientX - this.cx,
vy = e.clientY - this.cy;
this.angle = Math.atan2(vy, vx);
}
renderEye() {
// this should be in a separate function
this.angleSpeed += (this.angle - this.currentAngle) * ACCELERATION_COEFFICIENT;
this.angleSpeed *= DRAG_COEFFICIENT;
this.currentAngle += this.angleSpeed;
ctx.setTransform(1, 0, 0, 1, this.cx, this.cy);
ctx.rotate(this.currentAngle);
let eyeRadius = this.radius / 3;
ctx.beginPath();
ctx.arc(this.radius / 2, 0, eyeRadius, 0, Math.PI * 2);
ctx.fill();
}
render() {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.beginPath();
ctx.arc(this.cx, this.cy, this.radius, 0, Math.PI * 2);
ctx.closePath();
ctx.strokeStyle = '#09f';
ctx.lineWidth = 1;
ctx.stroke();
this.renderMessage();
this.renderEye();
}
renderMessage() {
ctx.font = "18px serif";
ctx.strokeStyle = 'black';
ctx.fillText('Angle: ' + this.angle, 30, canvas.height - 40);
}
}
var rotatingCircle = new Circle({
x: 320,
y: 160,
radius: 40,
color: 'black',
angle: Math.random() * Math.PI * 2
});
function animate() {
rotatingCircle.render();
requestAnimationFrame(animate);
}
animate();
<canvas id='canvas' style='width: 700; height: 500;'></canvas>
As far as I know, with h5 canvas, you may need to write the ease functions yourself. However, css3 animations have several built-in ease functions, you can write .foo {transition: bottom 1s ease} and when .foo elements' bottom style property changes, they'll move in the velocities defined by the ease function. See: https://developer.mozilla.org/en-US/docs/Web/CSS/transition https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Transitions/Using_CSS_transitions .
Also, check out these amazing animated BB-8 s(built with css animations): http://codepen.io/mdixondesigns/pen/PPEJwz http://codepen.io/Chyngyz/pen/YWwYGq http://codepen.io/bullerb/pen/gMpxNZ
Related
I started learning JavaScript a few weeks ago and I decided to try and make my first game from scratch, in the code below I am trying to make the game so the balls fall each time with different position, color and speed and after that there will be another ball that ill be able to move with my mouse and will try to dodge the balls, so my question is is there a better way than the one I did to spawn more balls , because if I want to spawn like 5-6 more the code will look so bad and I am sure there is a better way of doing that, I am still learning so if you can hit me with a simple solution and explain it.
var canvas = document.getElementById("Canv");
var ctx = canvas.getContext("2d");
var x = random(1, 801);
var x2 = random(1, 801);
var y = 10;
var y2 = 10;
var ballRadius = random(2, 51);
var ballRadius2 = random(2, 51);
var color = "#" + ((1 << 24) * Math.random() | 0).toString(16);
var color2 = "#" + ((1 << 24) * Math.random() | 0).toString(16);
var dy = random(1, 6);
var dy2 = random(1, 6);
function random(min, max) {
return Math.random() * (max - min) + min;
}
function drawBall() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.arc(x, y, ballRadius, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.fill();
ctx.closePath();
}
function drawBall2() {
ctx.beginPath();
ctx.arc(x2, y2, ballRadius2, 0, Math.PI * 2);
ctx.fillStyle = color2;
ctx.fill();
ctx.closePath();
}
function draw() {
drawBall();
drawBall2();
y += dy;
y2 += dy2;
}
setInterval(draw, 10);
I want to know methods to simplify my code so I know for future projects.
Consider the following code:
The main changes I made are the addition of the function createBall, allowing the creation of multiple new balls per tick, and the removal of balls that are out of view. I tried to add comments to the new parts, if you have any questions, let me know!
var canvas = document.getElementById("Canv");
var ctx = canvas.getContext("2d");
const desiredNumberOfBalls = 10;
let i = 0;
let balls = []; // Track each individual ball
const viewLimit = canvas.height; // The vertical distance a ball has when leaving sight
// For now we create a static paddle
const paddle = {
x: 200,
y: viewLimit - 10,
width: 50,
height: 10,
}
// This is a very rough calculation based on https://math.stackexchange.com/a/2100319 by checking first the corners and then the top edge
paddle.hitsBall = function(ball) {
let hit = false;
if (ball.y + ball.radius >= paddle.y) {
// the ball is at the same level as the paddle
if (Math.sqrt((ball.x - paddle.x) ** 2 + (ball.y - paddle.y) ** 2) < ball.radius) {
// the upper left part of the paddle touches the ball
hit = true;
} else if (Math.sqrt((ball.x - paddle.x - paddle.width) ** 2 + (ball.y - paddle.y) ** 2) < ball.radius) {
// the upper right part of the paddle touches the ball
hit = true;
} else if (ball.x >= paddle.x && ball.x <= paddle.x + paddle.width) {
// the top edge of the paddle touches the ball
hit = true;
}
}
if (hit) console.log("Hit!");
return hit;
}
function createBall() {
let ball = {
id: i++,
x: random(1, canvas.width),
y: 10,
radius: random(2, 51),
color: "#" + ((1 << 24) * Math.random() | 0).toString(16),
speed: random(1, 6)
};
return ball;
}
function random(min, max) {
return Math.random() * (max - min) + min;
}
function drawBall(ball) {
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
ctx.fillStyle = ball.color;
ctx.fill();
ctx.closePath();
}
function drawPaddle() {
ctx.beginPath();
ctx.rect(paddle.x, paddle.y, paddle.width, paddle.height);
ctx.fillStyle = 'darkred';
ctx.fill();
ctx.closePath();
}
function clearView() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
function draw() {
clearView();
// Move all existing balls by their designated speed
balls.forEach((ball) => {
ball.y += ball.speed;
});
// Implicitly delete all balls that have exited the viewport
// by only retaining the balls that are still inside the viewport
balls = balls.filter((ball) => ball.y <= viewLimit);
// Implicitly delete all balls that touch the paddle
// by only retaining the balls that don't
balls = balls.filter((ball) => !paddle.hitsBall(ball));
// Create newBallsPerTick new balls
while (balls.length < desiredNumberOfBalls) {
balls.push(createBall());
}
// Draw the paddle
drawPaddle();
// Draw the position of every ball - both the pre-existing ones
// and the ones we just generated
balls.forEach((ball) => {
drawBall(ball);
});
}
setInterval(draw, 1000 / 60);
canvas {
width: 600px;
height: 400px;
}
<canvas id="Canv" height="400" width="600"></canvas>
I am trying to achieve a tracing effect where the lines have a faded trail. The way I am trying to do it is simply by drawing the solid background once, and then on further frames draw a transparent background before drawing the new lines, so that you can still see a little of the image before it.
The issue is that I do want the lines to fade out completely after some time, but they seem to leave a permanent after image, even after drawing over them repeatedly.
I've tried setting different globalCompositeOperation(s) and it seemed like I was barking up the wrong tree there.
This code is called once
//initiate trace bg
traceBuffer.getContext("2d").fillStyle = "rgba(0, 30, 50, 1)";
traceBuffer.getContext("2d").fillRect(0, 0, traceBuffer.width, traceBuffer.height);
then inside the setInterval function it calls
//draw transparent background
ctx.fillStyle = "rgba(0, 30, 50, 0.04)";
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
//set drawing settings
ctx.strokeStyle = "#AAAAAA";
ctx.lineWidth = 4;
for (let i = 0; i < tracer.layer2.length; i++){
ctx.beginPath();
ctx.moveTo(newX, newY);
ctx.lineTo(oldX, oldY);
ctx.stroke();
}
Here's an example: https://i.imgur.com/QTkeIVf.png
On the left is what I am currently getting, and on the right is the edit of what I actually want to happen.
This is how I would do it. I would build a history of the particles moving on the track. The older the position the smaller the value of the alpha value for the fill. Also for a nicer effect I would reduce the size of the circle.
I hope this is what you need.
PS: I would have loved to have your curve. Since I don't have it I've drawn a different one.
const hypotrochoid = document.getElementById("hypotrochoid");
const ctx = hypotrochoid.getContext("2d");
const cw = (hypotrochoid.width = 300);
const ch = (hypotrochoid.height = 300);
const cx = cw / 2,
cy = ch / 2;
ctx.lineWidth = 1;
ctx.strokeStyle = "#d9d9d9";
// variables for the hypotrochoid
let a = 90;
let b = 15;
let h = 50;
// an array where to save the points used to draw the track
let track = [];
//add points to the track array. This will be used to draw the track for the particles
for (var t = 0; t < 2 * Math.PI; t += 0.01) {
let o = {};
o.x = cx + (a - b) * Math.cos(t) + h * Math.cos((a - b) / b * t);
o.y = cy + (a - b) * Math.sin(t) - h * Math.sin((a - b) / b * t);
track.push(o);
}
// a function to draw the track
function drawTrack(ry) {
ctx.beginPath();
ctx.moveTo(ry[0].x, ry[0].y);
for (let t = 1; t < ry.length; t++) {
ctx.lineTo(ry[t].x, ry[t].y);
}
ctx.closePath();
ctx.stroke();
}
// a class of points that are moving on the track
class Point {
constructor(pos) {
this.pos = pos;
this.r = 3;//the radius of the circle
this.history = [];
this.historyLength = 40;
}
update(newPos) {
let old_pos = {};
old_pos.x = this.pos.x;
old_pos.y = this.pos.y;
//save the old position in the history array
this.history.push(old_pos);
//if the length of the track is longer than the max length allowed remove the extra elements
if (this.history.length > this.historyLength) {
this.history.shift();
}
//gry the new position on the track
this.pos = newPos;
}
draw() {
for (let i = 0; i < this.history.length; i++) {
//calculate the alpha value for every element on the history array
let alp = i * 1 / this.history.length;
// set the fill style
ctx.fillStyle = `rgba(0,0,0,${alp})`;
//draw an arc
ctx.beginPath();
ctx.arc(
this.history[i].x,
this.history[i].y,
this.r * alp,
0,
2 * Math.PI
);
ctx.fill();
}
}
}
// 2 points on the track
let p = new Point(track[0]);
let p1 = new Point(track[~~(track.length / 2)]);
let frames = 0;
let n, n1;
function Draw() {
requestAnimationFrame(Draw);
ctx.clearRect(0, 0, cw, ch);
//indexes for the track position
n = frames % track.length;
n1 = (~~(track.length / 2) + frames) % track.length;
//draw the track
drawTrack(track);
// update and draw the first point
p.update(track[n]);
p.draw();
// update and draw the second point
p1.update(track[n1]);
p1.draw();
//increase the frames counter
frames++;
}
Draw();
canvas{border:1px solid}
<canvas id="hypotrochoid"></canvas>
I have pie chart like grow/shrink animation, but in the middle of it, there is showing up fill circle on one frame, how can I get rid of it?
var canvas = document.querySelector('canvas');
var ctx = canvas.getContext('2d');
var increase = Math.PI * 2 / 100;
var max = Math.PI * 2;
var angle = 0;
var start = false;
setInterval(function() {
ctx.clearRect(0, 0, 32, 32);
ctx.fillStyle = '#F00A0A';
ctx.beginPath();
if (start) {
ctx.arc(16, 16, 12, angle, 0);
} else {
ctx.arc(16, 16, 12, 0, angle);
}
ctx.lineTo(16, 16);
ctx.fill();
angle += increase;
if (angle >= max) {
angle = 0;
start = !start;
}
}, 20);
<canvas width="32" height="32"/>
I've tried this:
if (angle !== 0) {
ctx.beginPath();
if (start) {
ctx.arc(16, 16, 12, angle, 0);
} else {
ctx.arc(16, 16, 12, 0, angle);
}
ctx.lineTo(16, 16);
ctx.fill();
}
but this don't work, when arc change side it show full circle for one frame.
The easiest solution is to just offset your angle ever so slightly.
That way circle isn't "complete" when you change drawing mode:
var canvas = document.body.appendChild(document.createElement('canvas'));
var canvasSize = canvas.width = canvas.height = 100;
var ctx = canvas.getContext('2d');
ctx.fillStyle = '#F00A0A';
var increase = Math.PI * 2 / 100;
var max = Math.PI * 2;
var angle = 0.0000001;
var start = false;
function update() {
ctx.clearRect(0, 0, canvasSize, canvasSize);
ctx.beginPath();
ctx.arc(canvasSize / 2, canvasSize / 2, canvasSize / 3, angle, 0, start);
ctx.lineTo(canvasSize / 2, canvasSize / 2);
ctx.fill();
angle += increase;
if (angle >= max) {
angle = angle % max;
start = !start;
}
requestAnimationFrame(update);
}
update();
I'm trying to resize a rotated shape on canvas. My problem is that when I call the rendering function, the shape starts "drifting" depending on the shape angle. How can I prevent this?
I've made a simplified fiddle demonstrating the problem, when the canvas is clicked, the shape is grown and for some reason it drifts upwards.
Here's the fiddle: https://jsfiddle.net/x5gxo1p7/
<style>
canvas {
position: absolute;
box-sizing: border-box;
border: 1px solid red;
}
</style>
<body>
<div>
<canvas id="canvas"></canvas>
</div>
</body>
<script type="text/javascript">
var canvas = document.getElementById('canvas');
canvas.width = 300;
canvas.height= 150;
var ctx = canvas.getContext('2d');
var counter = 0;
var shape = {
top: 120,
left: 120,
width: 120,
height: 60,
rotation: Math.PI / 180 * 15
};
function draw() {
var h2 = shape.height / 2;
var w2 = shape.width / 2;
var x = w2;
var y = h2;
ctx.save();
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.translate(75,37.5)
ctx.translate(x, y);
ctx.rotate(Math.PI / 180 * 15);
ctx.translate(-x, -y);
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, shape.width, shape.height);
ctx.restore();
}
canvas.addEventListener('click', function() {
shape.width = shape.width + 15;
window.requestAnimationFrame(draw.bind(this));
});
window.requestAnimationFrame(draw.bind(this));
</script>
In the "real" code the shape is resized when the resize-handle is clicked and moved but I think this example demonstrates the problem sufficiently.
EDIT: updated fiddle to clarify the issue:
https://jsfiddle.net/x5gxo1p7/9/
Always use local coordinates to define shapes.
When rendering content that is intended to be transformed the content should be in its own (local) coordinate system. Think of a image. the top left pixel is always at 0,0 on the image no matter where you render it. The pixels are at their local coordinates, when rendered they are moved to the (world) canvas coordinates via the current transformation.
So if you make your shape with coordinates set to its local, making the rotation point at its local origin (0,0) the display coordinates are stored separately as world coordinates
var shape = {
top: -30, // local coordinates with rotation origin
left: -60, // at 0,0
width: 120,
height: 60,
world : {
x : canvas.width / 2,
y : canvas.height / 2,
rot : Math.PI / 12, // 15deg clockwise
}
};
Now you don't have to mess about with translating forward and back... blah blah total pain.
Just
ctx.save();
ctx.translate(shape.world.x,shape.world.y);
ctx.rotate(shape.world.rot);
ctx.fillRect(shape.left, shape.top, shape.width, shape.height)
ctx.restore();
or event quicker and eliminating the need to use save and restore
ctx.setTransform(1,0,0,1,shape.world.x,shape.world.y);
ctx.rotate(shape.world.rot);
ctx.fillRect(shape.left, shape.top, shape.width, shape.height);
The local shape origin (0,0) is where the transformation places the translation.
This greatly simplifies a lot of the work that has to be done
var canvas = document.getElementById('canvas');
canvas.width = 300;
canvas.height= 150;
var ctx = canvas.getContext('2d');
ctx.fillStyle = "black";
ctx.strokeStyle = "red";
ctx.lineWidth = 2;
var shape = {
top: -30, // local coordinates with rotation origin
left: -60, // at 0,0
width: 120,
height: 60,
world : {
x : canvas.width / 2,
y : canvas.height / 2,
rot : Math.PI / 12, // 15deg clockwise
}
};
function draw() {
ctx.setTransform(1,0,0,1,0,0); // to clear use default transform
ctx.clearRect(0, 0, canvas.width, canvas.height);
// you were scaling the shape, that can be done via a transform
// once you have moved the shape to the world coordinates.
ctx.setTransform(1,0,0,1,shape.world.x,shape.world.y);
ctx.rotate(shape.world.rot);
// after the transformations have moved the local to the world
// you can ignore the canvas coordinates and work within the objects
// local. In this case showing the unscaled box
ctx.strokeRect(shape.left, shape.top, shape.width, shape.height);
// and a line above the box
ctx.strokeRect(shape.left, shape.top - 5, shape.width, 1);
ctx.scale(0.5,0.5); // the scaling you were doing
ctx.fillRect(shape.left, shape.top, shape.width, shape.height);
}
canvas.addEventListener('click', function() {
shape.width += 15;
shape.left -= 15 / 2;
shape.world.rot += Math.PI / 45; // rotate to illustrate location
// of local origin
var distToMove = 15/2;
shape.world.x += Math.cos(shape.world.rot) * distToMove;
shape.world.y += Math.sin(shape.world.rot) * distToMove;
draw();
});
// no need to use requestAnimationFrame (RAF) if you are not animation
// but its not wrong. Nor do you need to bind this (in this case
// this = window) to the callback RAF does not bind a context
// to the callback
/*window.requestAnimationFrame(draw.bind(this));*/
requestAnimationFrame(draw); // functionaly identical
// or just
/*draw()*/ //will work
body { font-family : Arial,"Helvetica Neue",Helvetica,sans-serif; font-size : 12px; color : #242729;} /* SO font currently being used */
canvas { border: 1px solid red; }
<canvas id="canvas"></canvas>
<p>Click to grow "and rotate" (I add that to illustrate the local origin)</p>
<p>I have added a red box and a line above the box, showing how using the local coordinates to define a shape makes it a lot easier to then manipulate that shape when rendering "see code comments".</p>
Try this. You had ctx.translate() used where it was not entirely necessary. That caused the problems.
<script type="text/javascript">
var canvas = document.getElementById('canvas');
canvas.width = 300;
canvas.height= 150;
var ctx = canvas.getContext('2d');
var counter = 0;
var shape = {
top: 120,
left: 120,
width: 120,
height: 60,
rotation: Math.PI / 180 * 15
};
function draw() {
ctx.save();
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.translate(75,37.5)
ctx.rotate(Math.PI / 180 * 15);
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, shape.width, shape.height);
ctx.restore();
}
canvas.addEventListener('click', function() {
shape.width = shape.width + 15;
window.requestAnimationFrame(draw.bind(this));
});
window.requestAnimationFrame(draw.bind(this));
</script>
This is happening because the x and y are set as the half value of the shape size, which completely changes its position.
You should set a point for the center of the shape, anyway. I set this point as ctx.canvas.[width or height] / 2, the half of the canvas.
var h2 = shape.height / 2;
var w2 = shape.width / 2;
var x = (ctx.canvas.width / 2) - w2;
var y = (ctx.canvas.height / 2) - h2;
ctx.save();
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.translate(x + (shape.width / 2), y + (shape.height / 2));
ctx.rotate(((shape.rotation * Math.PI) / 180) * 15);
ctx.fillStyle = '#000';
ctx.fillRect(-shape.width / 2, -shape.height / 2, shape.width, shape.height);
ctx.restore();
Fiddle.
Found a solution, problem was that I wasn't calculating the new center point coordinates.
The new fiddle with solution: https://jsfiddle.net/HTxGb/151/
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
canvas.width =500;
canvas.height = 500;
var x = canvas.width/2;
var y = canvas.height/2;
var rectw = 20;
var recth = 20;
var rectx = -rectw/2;
var recty = -recth/2;
var rotation = 0;
var addedRotation = Math.PI/12;
var addedWidth = 20;
var addedHeight = 10;
var draw = function() {
ctx.save();
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.translate(x, y);
ctx.rotate(rotation);
ctx.fillRect(rectx, recty, rectw, recth);
ctx.restore();
}
document.getElementById('growXRight').addEventListener('click', function() {
rectx -= addedWidth/2;
x += addedWidth/2 * Math.cos(rotation);
y -= addedWidth/2 * Math.sin(-rotation);
rectw += addedWidth;
draw();
})
document.getElementById('growXLeft').addEventListener('click', function() {
rectx -= addedWidth/2;
x -= addedWidth/2 * Math.cos(rotation);
y += addedWidth/2 * Math.sin(-rotation);
rectw += addedWidth;
draw();
})
document.getElementById('growYTop').addEventListener('click', function() {
recty -= addedHeight/2;
x += addedHeight/2 * Math.sin(rotation);
y -= addedHeight/2 * Math.cos(-rotation);
recth += addedHeight;
draw();
})
document.getElementById('growYBottom').addEventListener('click', function() {
recty -= addedHeight/2;
x -= addedHeight/2 * Math.sin(rotation);
y += addedHeight/2 * Math.cos(-rotation);
recth += addedHeight;
draw();
})
document.getElementById('rotatePlus').addEventListener('click', function() {
rotation += addedRotation;
rotation = rotation % (Math.PI*2);
if(rotation % Math.PI*2 < 0) {
rotation += Math.PI*2;
}
draw();
})
document.getElementById('rotateMinus').addEventListener('click', function() {
rotation -= addedRotation;
rotation = rotation % (Math.PI*2);
if(rotation % Math.PI*2 < 0) {
rotation += Math.PI*2;
}
draw();
})
draw();
I'm working on a game to play in canvas, and was wanting to add some ambiance to a background layer using javascript. To start, here is the code...
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext("2d");
var x = canvas.width/2;
var y = canvas.height-150;
var dx = Math.random() * (-5 * 5) + 15;
var dy = -15;
function drawDot() {
ctx.beginPath();
ctx.arc(x, y, 10, 0, Math.PI*2);
ctx.fillStyle = "black";
ctx.fill();
ctx.closePath();
};
function move() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawDot();
x += dx;
y += dy;
};
setInterval(move, 50);
If you run that, you can see that what I have is a black ball that moves up and off the page inside a random conal space. What I need help with is figuring out the best way to:
A. Populate it with more balls (maybe like 2-3) that are on their own random trajectory, and
B. Make it so those 2-3 balls are constantly animating inside the random cone area off the page from the same starting area (kind of like a constant spurting fountain effect).
A problem I can already see is that by using the console log, the 1 working ball I have now just keeps going off into infinity outside the canvas, so when I try to add a new one it won't run the function. I'm very new to javascript and especially canvas so apologies if this is obvious!
Thank you for any help with this!
There is a good tutorial by Seb Lee-Delisle on this exact problem here:
https://vimeo.com/36278748
Basically you have to encapsulate each Dot so it knows about its own position and acceleration.
EDIT 1
Here is an example using you own code:
document.body.innerHTML = '<canvas height="600" width="600" id="myCanvas"></canvas>';
clearInterval(interval);
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
var dotArray = [];
function Dot() {
var dot = {
y : canvas.height / 2,
x : canvas.width / 2,
speedX : Math.random() * ( - 5 * 5) + 15,
speedY : Math.random() * ( - 5 * 5) + 15,
age : 0,
draw : function () {
this.x += this.speedX;
this.y += this.speedY;
ctx.beginPath();
ctx.arc(this.x, this.y, 10, 0, Math.PI * 2);
ctx.fillStyle = 'black';
ctx.fill();
ctx.closePath();
}
};
return dot;
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (var i = 0; i < dotArray.length; i++) {
dotArray[i].draw();
dotArray[i].age++;
if (dotArray[i].age > 20) {
dotArray.splice(i, 1);
}
}
dotArray.push(new Dot());
}
draw();
var interval = setInterval(draw, 1000 / 60);