Related
I am making a game similar to Galaga. I used to have my player at the middle of the screen and it would fire missiles in the direction of the click event, but now I want it to only fire straight up the screen (just like Galaga). How could I change my current code to accomplish this?
Here is my code:
class Missle {
constructor(x, y, radius, color, velocity) {
this.x = x
this.y = y
this.radius = radius
this.color = color
this.velocity = velocity
}
draw() { // Draws the missle
c.beginPath()
c.arc(this.x, this.y, this.radius, 0,
Math.PI * 2, false) // Draws a 360 degree circle
c.fillStyle = this.color //Sets the color to whatever color is passed in
c.fill() // Fills the circle
}
update() { // Updates classes properties over and over again
this.draw()
this.x = this.x + this.velocity.x
this.y = this.y + this.velocity.y
}
}
const missle = new Missle( // Passing parameters
canvas.width / 2, (canvas.height / 2) * 1.75, // Sets missle beginning point at center
5,
'red',
{ // Creates an object for the velocity
x: 1,
y: 1
}
)
fireBtn.addEventListener('click', (event) => { // when we click it calls this function below
const angle = Math.atan2(event.clientY - canvas.height / 2,
event.clientX - canvas.width / 2)
const velocity = { //creates velocity object
x: Math.cos(angle) * 1, // sets speed (velocity)
y: Math.sin(angle) * -6 // sets speed (velocity)
}
missles.push(new Missle(
player.x,
player.y,
//canvas.width / 2,
//(canvas.height / 2) * 1.75,
5,
'rgb(250, 0, 0, .75',
velocity
))
})
I know I need to change the way the velocity works unless there are better ways of doing so. Any help or tips would be greatly appreciated.
Rather than making the velocity use the angle between the center of the screen and the mouse position the velocity should just be your up vector. In other words change you velocity in the click event to be:
const velocity = {
x: 0,
y: -1
}
You can google "vector math" to get a better understanding of where those values are coming from.
Am I meant to draw the eyes in an alternative way for it to follow the cursor? Please help :) I am completely lost from here and have tried online solutions but they all require css in which my code doesn't. I want to run all of this purely from javascript, any tips?
function drawEyes() {
const c = document.getElementById("canvasEyes")
const ctx = c.getContext('2d');
//left eye
ctx.beginPath();
ctx.arc(75, 75, 50, 0, Math.PI * 2, false);
ctx.stroke();
//iris
ctx.beginPath();
ctx.arc(75, 75, 30, 0, Math.PI * 2, false);
ctx.stroke();
ctx.fillStyle = "black";
ctx.fill();
//pupil
ctx.beginPath();
ctx.arc(75, 75, 15, 0, Math.PI * 2, false);
ctx.stroke();
ctx.fillStyle = "blue";
ctx.fill();
//right eye
ctx.beginPath();
ctx.arc(225, 75, 50, 0, Math.PI * 2, false);
ctx.stroke();
//iris
ctx.beginPath();
ctx.arc(225, 75, 30, 0, Math.PI * 2, false);
ctx.stroke();
ctx.fillStyle = "black";
ctx.fill();
//pupil
ctx.beginPath();
ctx.arc(225, 75, 15, 0, Math.PI * 2, false);
ctx.stroke();
ctx.fillStyle = "blue";
ctx.fill();
}
Basic 2D eyes that follow mouse
The eyes follow mouse by scaling the mouse coordinates to the range of motion that the iris & pupil have within the radius of the eye.
The lookat position is relative to the top left of the canvas and assumes that the eyes are at the center of the canvas.
The scaled lookat position is then set relative to the canvas center (center of both eyes)
To prevent the iris & pupil from being drawn outside the eye use the canvas clip function to clip the iris & pupil if outside the circles of the eye.
More details
It is possible to add more details
Consider adding shading, highlights, eyelids, blink, etc.. to give the animation more depth and life, for instance...
Spheres
Eyes are spheres, you can use ellipses to draw the iris & pupil, Flattening the ellipses of the iris & pupil as they get near the edge, also rotate the ellipse in the direction of the mouse. This will make the eyes look rounder in the 3rd dimention.
Example
Basic 2D eyes. See comments for details
const ctx = canvas.getContext("2d");
// Object to hold mouse coords
const lookat = {x: 150, y: 75};
// details need to make eye look at mouse coords
const eye = {
radius: 50,
iris: 30,
// limits of movement
limMin: -0.1,
limMax: 1.1,
};
// add mouse move listener to whole page
addEventListener("mousemove",e => {
// make mouse coords relative to the canvas ignoring scroll in this case
const bounds = canvas.getBoundingClientRect();
lookat.x = e.pageX - bounds.left;// - scrollX;
lookat.y = e.pageY - bounds.top;// - scrollY;
ctx.clearRect(0, 0, 300, 150);
drawEyes(lookat);
});
drawEyes(lookat);
function drawEyes(lookat) {
var {x,y} = lookat;
// normalise lookat range from 0 to 1 across and down canvas
x /= canvas.width;
y /= canvas.height;
// limit eye movement to -0.1 to 1.1 or what ever you prefer
x = x < eye.limMin ? eye.limMin : x > eye.limMax ? eye.limMax : x;
y = y < eye.limMin ? eye.limMin : y > eye.limMax ? eye.limMax : y;
// move lookat so that 0.5 is center
x -= 0.5;
y -= 0.5;
// get range of movement of iris
const range = (eye.radius - eye.iris) * 2;
// scale the lookats to the range of movement
x *= range;
y *= range;
// draw outer eyes left, right
ctx.beginPath();
ctx.arc(75, 75, eye.radius, 0, Math.PI * 2, false);
ctx.moveTo(225 + eye.radius, 75);
ctx.arc(225, 75, eye.radius, 0, Math.PI * 2, false);
ctx.stroke();
// use eyes to create a clip so iris does not draw outside the eye.
// first save canvas state so clip can be turned off at end
ctx.save();
// turn on clip which will use the two circles currently the active path
ctx.clip();
// draw iris & pupil are offset by x,y within the clip
//iris left, right
ctx.fillStyle = "blue";
ctx.beginPath();
ctx.arc(75 + x, 75 + y, eye.iris, 0, Math.PI * 2, false);
ctx.moveTo(225 + x + eye.iris, 75 + y);
ctx.arc(225 + x, 75 + y, eye.iris, 0, Math.PI * 2, false);
ctx.fill();
//pupil left, right
ctx.fillStyle = "black";
ctx.beginPath();
ctx.arc(75 + x, 75 + y, 15, 0, Math.PI * 2, false);
ctx.moveTo(225 + x + 15, 75 + y);
ctx.arc(225 + x, 75 + y, 15, 0, Math.PI * 2, false);
ctx.fill();
// turn the clip off by restoring canvas state
ctx.restore();
}
<canvas id="canvas" width="300" height="150"></canvas>
I have a bubble chart with big bubbles. I want to draw a cross or anything in the center of each bubble, but I can't find any solutions. The following picture show you the context:
I use Chart.js 2.5.0 on meteor.
Your question seemed quite interesting to me, therefore, I constructed the following chartjs plugin, that will help fulfill your requirement.
Chart.plugins.register({
afterDraw: c => {
let datasets = c.data.datasets;
datasets.forEach((e, i) => {
let isHidden = e._meta[0].hidden;
if (!isHidden) {
let data = c.getDatasetMeta(i).data;
data.forEach(e => {
let ctx = c.chart.ctx;
let x = e._model.x;
let y = e._model.y;
let r = e._model.radius;
// draw a cross
// or you can draw anything using general canvas methods
ctx.save();
ctx.beginPath();
ctx.moveTo(x - r / 4, y - r / 4);
ctx.lineTo(x + r / 4, y + r / 4);
ctx.moveTo(x + r / 4, y - r / 4);
ctx.lineTo(x - r / 4, y + r / 4);
ctx.strokeStyle = 'white';
ctx.lineWidth = 2;
ctx.stroke();
ctx.restore();
});
}
});
}
});
ᴅᴇᴍᴏ
Chart.plugins.register({
afterDraw: c => {
let datasets = c.data.datasets;
datasets.forEach((e, i) => {
let isHidden = e._meta[0].hidden;
if (!isHidden) {
let data = c.getDatasetMeta(i).data;
data.forEach(e => {
let ctx = c.chart.ctx;
let x = e._model.x;
let y = e._model.y;
let r = e._model.radius;
// draw a cross
// or you can draw anything using general canvas methods
ctx.save();
ctx.beginPath();
ctx.moveTo(x - r / 4, y - r / 4);
ctx.lineTo(x + r / 4, y + r / 4);
ctx.moveTo(x + r / 4, y - r / 4);
ctx.lineTo(x - r / 4, y + r / 4);
ctx.strokeStyle = 'white';
ctx.lineWidth = 2;
ctx.stroke();
ctx.restore();
});
}
});
}
});
let ctx = document.querySelector('#c').getContext('2d');
let chart = new Chart(ctx, {
type: 'bubble',
data: {
labels: ['Jan', 'Feb', 'Mar'],
datasets: [{
label: 'John',
data: [
{ x: 5, y: 5, r: 10 },
{ x: 10, y: 10, r: 15 },
{ x: 16, y: 15, r: 18 }
],
backgroundColor: '#76d1bf'
}, {
label: 'Smith',
data: [
{ x: 3, y: 10, r: 10 },
{ x: 7, y: 11, r: 15 },
{ x: 12, y: 6, r: 18 }
],
backgroundColor: '#827ada'
}]
},
options: {
responsive: false,
scales: {
xAxes: [{
ticks: {
min: 2,
max: 18,
stepSize: 4
}
}],
yAxes: [{
ticks: {
min: 0,
max: 20,
stepSize: 4
}
}]
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.5.0/Chart.min.js"></script>
<canvas id="c" height="200"></canvas>
Chart.js has always been a very straight forward and simple library. One of the advantages this has is that it is easy to customize. This is also one of the good points of JavaScript, source code is always available for what ever library you use.
There is how ever one draw back with customization. Once you have made changes to the library you will have to use that particular version because customization is not something that the authors will consider when making changes.
So to use the code below you should go to the github page and download the project and use that version of chart.js for your site. I dont change the original, most of the time each customization is specific for a particular case and the code is customised at the clients side.
The change is very simple. First backup the function that draws a point
Chart.canvasHelpers.defaultDrawPoint = Chart.canvasHelpers.drawPoint;
This is done so you can pass on all calls that you are not interested in back to the standard handler.
Next write the intercept code by replacing the function you just backed up.
Chart.canvasHelpers.drawPoint = function(ctx, pointStyle, radius, x, y){
Looking at the original source code you can work out how it draws the circles and just trap that behavior passing on all other argument variants to the original.
If pointStyle is undefined or === "circle" you handle that your self
// pointStyle undefined is default
// pointStyle === "circle" is the default named style
if(pointStyle === undefined || pointStyle === "circle"){
// next 4 lines copied from the source
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.closePath();
ctx.fill();
You then add your custom code to render what ever you like. It is important that you save the current 2D context as you dont want to have to worry that you break something further down the line.
// draw a cross
ctx.save(); // save the state
ctx.strokeStyle = "white";
ctx.strokeWidth = 4;
ctx.beginPath();
ctx.moveTo(x - radius *0.3, y - radius *0.3);
ctx.lineTo(x + radius *0.3, y + radius *0.3);
ctx.moveTo(x + radius *0.3, y - radius *0.3);
ctx.lineTo(x - radius *0.3, y + radius *0.3);
ctx.stroke();
Then restore the state of the 2D context
ctx.restore(); // restore the state
The else handles the standard calls you are not interested in
}else{ // all other styles pass on to default handler
Chart.canvasHelpers.defaultDrawPoint(ctx, pointStyle, radius, x, y);
}
For Chart.js this is particularly useful as it gives a way to customize and get the animations as well.
Chart.canvasHelpers.defaultDrawPoint = Chart.canvasHelpers.drawPoint;
Chart.canvasHelpers.drawPoint = function(ctx, pointStyle, radius, x, y){
// PointStyle undefined is default
// PointStyle === "circle" is the default named style
if(pointStyle === undefined || pointStyle === "circle"){
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.closePath();
ctx.fill();
// custom code here
ctx.save(); // save the state
ctx.strokeStyle = "white";
ctx.strokeWidth = 4;
ctx.beginPath();
ctx.moveTo(x - radius *0.3, y - radius *0.3);
ctx.lineTo(x + radius *0.3, y + radius *0.3);
ctx.moveTo(x + radius *0.3, y - radius *0.3);
ctx.lineTo(x - radius *0.3, y + radius *0.3);
ctx.stroke();
ctx.restore(); // restor the state
}else{ // all other styles pass on to default handler
Chart.canvasHelpers.defaultDrawPoint(ctx, pointStyle, radius, x, y);
}
}
// some utils to add data
// returns a random int
const rand = (min, max = min + (min = 0))=> Math.floor( Math.random() * (max-min) + min);
// returns a random data point {x,y,r}
const randData = ()=>({x : rand(0,50), y: rand(5,50), r: rand(4,20)});
// create a chart.
const ctx = canvas.getContext("2d");
const chart = new Chart(ctx, {
type: "bubble",
data: {
datasets: [{
label: "Random Data",
backgroundColor: "#7AF",
data: (()=>{
var dat = [];
for(var i = 0; i < 10; i++){ dat.push(randData()) }
return dat;
})(),
}]
},
options: { responsive: false } // this is required to work or it throws
// not sure why by it is not due to the
// changes
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.5.0/Chart.min.js"></script>
<canvas id=canvas height=200 width=400></canvas>
To roll back the changes simply set the function back to the original.
Chart.canvasHelpers.drawPoint = Chart.canvasHelpers.defualtDrawPoint;
And remove the extra reference
Chart.canvasHelpers.defualtDrawPoint = undefined;
grunt's answer did not work for me. I don't know if there are different formats or if it's written in an outdated version but I have modified his solution to the following so it would work in my project. Basically I have
removed the register since it wasn't in line with what the example for background-color was like and
found out that the data points' info isn't as nested anymore so I changed the way the properties are accessed, too
plugins: [
{
id: 'background-colour',
beforeDraw: (chart) => {
const ctx = chart.ctx;
ctx.save();
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, width, height);
ctx.restore();
}
},
{
id: 'abc',
afterDraw: (c) => {
let datasets = c.data.datasets;
datasets.forEach((e, i) => {
let isHidden = e.hidden;
if (!isHidden) {
let data = c.getDatasetMeta(i).data;
data.forEach(e => {
let ctx = c.ctx;
let x = e.x;
let y = e.y;
let r = e.options.radius as number;
ctx.save();
ctx.beginPath();
ctx.moveTo(x - r / 4, y - r / 4);
ctx.lineTo(x + r / 4, y + r / 4);
ctx.moveTo(x + r / 4, y - r / 4);
ctx.lineTo(x - r / 4, y + r / 4);
ctx.strokeStyle = 'white';
ctx.lineWidth = 2;
ctx.stroke();
ctx.restore();
});
}
});
}
}
]
I am trying to test if a point is inside a rectangle area that rotates an angle around (x, y), like the image below. This is language agnostic problem but I am working with HTML5 canvas now.
Suppose the point we need to test is (x1, y1), the width of the rectangle is 100 and the height is 60. In normal cartesian coordinate system the rectangle ABCD top left point A is (canvas.width / 2, canvas.height / 2 -rect.height/2). I assume that (canvas.width / 2, canvas.height / 2) is at the middle of line AB where B is (canvas.width / 2, canvas.height / 2 + rect.height /2).
I have read some resources here and wrote a test project, but it doesn't test the correct area. In my test project I want the this effect:
if the mouse is on a point that is within the range of the testing rectangle area a dot will be displayed around the mouse. If it is outside the rectangle nothing will be displayed.
However my test project looks like this: (Note that although I used the vector based technique to test the point in a rotated rectangle area, the test area remains the rectangle before rotation)
// Detecting a point is in a rotated rectangle area
// using vector based method
const canvas = document.getElementById('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const ctx = canvas.getContext('2d');
class Rectangle {
constructor(x, y, width, height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.searchPoint = { x: 0, y: 0};
this.binding();
}
binding() {
let self = this;
window.addEventListener('mousemove', e => {
if (!e) return;
let rect = canvas.getBoundingClientRect();
let mx = e.clientX - rect.left - canvas.clientLeft;
let my = e.clientY - rect.top - canvas.clientTop;
self.searchPoint = { x: mx, y: my };
});
}
}
let rect = new Rectangle(canvas.width /2, canvas.height /2 - 30, 100, 60);
function vector(p1, p2) {
return {
x: (p2.x - p1.x),
y: (p2.y - p1.y)
};
}
function point(x, y) {
return { x, y };
}
// Vector dot operation
function dot(a, b) {
return a.x * b.x + a.y * b.y;
}
function pointInRect(p, rect, angle) {
let a = newPointTurningAngle(0, -rect.height / 2, angle);
let b = newPointTurningAngle(0, rect.height / 2, angle);
let c = newPointTurningAngle(rect.width, rect.height / 2, angle);
let AB = vector(a, b);
let AM = vector(a, p);
let BC = vector(b, c);
let BM = vector(b, p);
let dotABAM = dot(AB, AM);
let dotABAB = dot(AB, AB);
let dotBCBM = dot(BC, BM);
let dotBCBC = dot(BC, BC);
return 0 <= dotABAM && dotABAM <= dotABAB && 0 <= dotBCBM && dotBCBM <= dotBCBC;
}
function drawLine(x, y) {
ctx.strokeStyle = 'black';
ctx.lineTo(x, y);
ctx.stroke();
}
function text(text, x, y) {
ctx.font = "18px serif";
ctx.fillText(text, x, y);
}
function newPointTurningAngle(nx, ny, angle) {
return {
x: nx * Math.cos(angle) - ny * Math.sin(angle),
y: nx * Math.sin(angle) + ny * Math.cos(angle)
};
}
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.moveTo(canvas.width / 2, 0);
drawLine(canvas.width /2, canvas.height / 2);
ctx.moveTo(0, canvas.height / 2);
drawLine(canvas.width / 2, canvas.height /2);
let angle = -Math.PI / 4;
ctx.setTransform(Math.cos(angle), Math.sin(angle), -Math.sin(angle), Math.cos(angle), canvas.width / 2, canvas.height / 2);
//ctx.setTransform(1, 0, 0, 1, canvas.width/2, canvas.height / 2);
ctx.strokeStyle = 'red';
ctx.strokeRect(0, -rect.height / 2, rect.width, rect.height);
let p = newPointTurningAngle(rect.searchPoint.x - canvas.width / 2, rect.searchPoint.y - canvas.height / 2, angle);
let testResult = pointInRect(p, rect, angle);
if (testResult) {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.beginPath();
ctx.fillStyle = 'black';
ctx.arc(rect.searchPoint.x, rect.searchPoint.y, 5, 0, Math.PI * 2);
ctx.fill();
}
ctx.setTransform(1, 0, 0, 1, 0, 0);
text('searchPoint x: ' + rect.searchPoint.x + ', y: ' + rect.searchPoint.y, 60, 430);
text('x: ' + canvas.width / 2 + ', y: ' + canvas.height / 2, 60, 480);
requestAnimationFrame(animate);
}
animate();
<canvas id='canvas'></canvas>
Updated Solution
I am still using the vector based method as followed:
0 <= dot(AB,AM) <= dot(AB,AB) &&
0 <= dot(BC,BM) <= dot(BC,BC)
Now I have changed the point's rotated angle and the corner point coordinates so the point can be detected in the rectangle. The corner points are already in the rotated coordinate system so they don't need to be translated, however the point of the mouse location needs to be translated before testing it in the rectangle area.
In setTransform method the angle rotated is positive when rotated clockwise, the form is :
ctx.setTransform(angle_cosine, angle_sine, -angle_sine, angle_cosine, x, y);
So when calculating the point's new coordinate after rotating an angle, the formula need to change to this so that the angle is also positive when rotated clockwise:
new_x = x * angle_cosine + y * angle_sine;
new_y = -x * angle_sine + y * angle_cos;
// Detecting a point is in a rotated rectangle area
// using vector based method
const canvas = document.getElementById('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const ctx = canvas.getContext('2d');
class Rectangle {
constructor(x, y, width, height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.searchPoint = { x: 0, y: 0};
this.binding();
}
binding() {
let self = this;
window.addEventListener('mousemove', e => {
if (!e) return;
let rect = canvas.getBoundingClientRect();
let mx = e.clientX - rect.left - canvas.clientLeft;
let my = e.clientY - rect.top - canvas.clientTop;
self.searchPoint = { x: mx, y: my };
});
}
}
let rect = new Rectangle(canvas.width /2, canvas.height /2 - 30, 100, 60);
function vector(p1, p2) {
return {
x: (p2.x - p1.x),
y: (p2.y - p1.y)
};
}
function point(x, y) {
return { x, y };
}
// Vector dot operation
function dot(a, b) {
return a.x * b.x + a.y * b.y;
}
function pointInRect(p, rect) {
let a = { x: 0, y: -rect.height / 2};
let b = { x: 0, y: rect.height / 2};
let c = { x: rect.width, y: rect.height / 2};
text('P x: ' + p.x.toFixed() + ', y: ' + p.y.toFixed(), 60, 430);
text('A x: ' + a.x.toFixed() + ', y: ' + a.y.toFixed(), 60, 455);
text('B x: ' + b.x.toFixed() + ', y: ' + b.y.toFixed(), 60, 480);
let AB = vector(a, b);
let AM = vector(a, p);
let BC = vector(b, c);
let BM = vector(b, p);
let dotABAM = dot(AB, AM);
let dotABAB = dot(AB, AB);
let dotBCBM = dot(BC, BM);
let dotBCBC = dot(BC, BC);
return 0 <= dotABAM && dotABAM <= dotABAB && 0 <= dotBCBM && dotBCBM <= dotBCBC;
}
function drawLine(x, y) {
ctx.strokeStyle = 'black';
ctx.lineTo(x, y);
ctx.stroke();
}
function text(text, x, y) {
ctx.font = "18px serif";
ctx.fillText(text, x, y);
}
function newPointTurningAngle(nx, ny, angle) {
let cos = Math.cos(angle);
let sin = Math.sin(angle);
return {
x: nx * cos + ny * sin,
y: -nx * sin + ny * cos
};
}
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.moveTo(canvas.width / 2, 0);
drawLine(canvas.width /2, canvas.height / 2);
ctx.moveTo(0, canvas.height / 2);
drawLine(canvas.width / 2, canvas.height /2);
let angle = - Math.PI / 4;
ctx.setTransform(Math.cos(angle), Math.sin(angle), -Math.sin(angle), Math.cos(angle), canvas.width / 2, canvas.height / 2);
ctx.strokeStyle = 'red';
ctx.strokeRect(0, -rect.height / 2, rect.width, rect.height);
let p = newPointTurningAngle(rect.searchPoint.x - canvas.width / 2, rect.searchPoint.y - canvas.height / 2, angle);
ctx.setTransform(1, 0, 0, 1, 0, 0);
let testResult = pointInRect(p, rect);
if (testResult) {
ctx.beginPath();
ctx.fillStyle = 'black';
ctx.arc(rect.searchPoint.x, rect.searchPoint.y, 5, 0, Math.PI * 2);
ctx.fill();
}
ctx.setTransform(1, 0, 0, 1, 0, 0);
text('searchPoint x: ' + rect.searchPoint.x + ', y: ' + rect.searchPoint.y, 60, 412);
text('x: ' + canvas.width / 2 + ', y: ' + canvas.height / 2, 60, 510);
requestAnimationFrame(animate);
}
animate();
<canvas id='canvas'></canvas>
Assuming that you know how to check whether a dot is in the rectangle the approach to solution is to rotate and translate everything (dot and rectangle) to "normalized" coordinating system (Cartesian coordinate system that is familiar to us) and then to check it trivially.
For more information you should check Affine transformations. The good link where you could start is
http://www.mathworks.com/discovery/affine-transformation.html?requestedDomain=www.mathworks.com
As you can see on this Codepen i did (to detect 2 rotate rect collide).
You have to check the 2 projections of your point (in my case, the 4 points of a rect) and look if the projections are on the other rect
You have to handle the same thing but only for a point and a rect instead of 2 rects
All projections are not colliding
All projections are colliding
required code for codepen link
The browser always report mouse position untransformed (==unrotated).
So to test if the mouse is inside a rotated rectangle, you can:
Get the unrotated mouse position from the mouse event (relative to the canvas).
Rotate the mouse x,y versus the rotation point by the same rotation as the rectangle.
Test if the mouse is inside the rectangle. Now that both the rect and the mouse position have been similarly rotated, you can just test as if the mouse and rect were unrotated.
Annotated code and a Demo:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
function reOffset(){
var BB=canvas.getBoundingClientRect();
offsetX=BB.left;
offsetY=BB.top;
}
var offsetX,offsetY;
reOffset();
window.onscroll=function(e){ reOffset(); }
window.onresize=function(e){ reOffset(); }
var isDown=false;
var startX,startY;
var rect=makeRect(50,20,35,20,Math.PI/4,60,30);
function makeRect(x,y,w,h,angle,rotationPointX,rotationPointY){
return({
x:x,y:y,width:w,height:h,
rotation:angle,rotationPoint:{x:rotationPointX,y:rotationPointY},
});
}
drawRect(rect);
$("#canvas").mousedown(function(e){handleMouseDown(e);});
function drawRect(r){
var rx=r.rotationPoint.x;
var ry=r.rotationPoint.y;
// demo only, draw the rotation point
dot(rx,ry,'blue');
// draw the rotated rect
ctx.translate(rx,ry);
ctx.rotate(r.rotation);
ctx.strokeRect(rect.x-rx,rect.y-ry,r.width,r.height);
// always clean up, undo the transformations (in reverse order)
ctx.rotate(-r.rotation);
ctx.translate(-rx,-ry);
}
function dot(x,y,fill){
ctx.fillStyle=fill;
ctx.beginPath();
ctx.arc(x,y,3,0,Math.PI*2);
ctx.fill();
}
function handleMouseDown(e){
// tell the browser we're handling this event
e.preventDefault();
e.stopPropagation();
// get mouse position relative to canvas
mouseX=parseInt(e.clientX-offsetX);
mouseY=parseInt(e.clientY-offsetY);
// rotate the mouse position versus the rotationPoint
var dx=mouseX-rect.rotationPoint.x;
var dy=mouseY-rect.rotationPoint.y;
var mouseAngle=Math.atan2(dy,dx);
var mouseDistance=Math.sqrt(dx*dx+dy*dy);
var rotatedMouseX=rect.rotationPoint.x+mouseDistance*Math.cos(mouseAngle-rect.rotation);
var rotatedMouseY=rect.rotationPoint.y+mouseDistance*Math.sin(mouseAngle-rect.rotation);
// test if rotated mouse is inside rotated rect
var mouseIsInside=rotatedMouseX>rect.x &&
rotatedMouseX<rect.x+rect.width &&
rotatedMouseY>rect.y &&
rotatedMouseY<rect.y+rect.height;
// draw a dot at the unrotated mouse position
// green if inside rect, otherwise red
var hitColor=mouseIsInside?'green':'red';
dot(mouseX,mouseY,hitColor);
}
body{ background-color: ivory; }
#canvas{border:1px solid red; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<h4>Clicks inside rect are green, otherwise red.</h4>
<canvas id="canvas" width=512 height=512></canvas>
In the code below I am starting/stopping a flame of particles when clicking on a block. It works ok, however, I need to have about 10-20 flames able to start/stop individually and keep track of them. A friend suggested that I put each flame in a small canvas, then draw each canvas individually, which i think would be a rendering overkill, having 10-20 draw() running at the same time. How should i go about this?
One thing to note is that the position of the flame is given in particle(), then an array of particles is created, which basically represents the flame.
// init canvas
var canvas = $('canvas'),
ctx = canvas[0].getContext('2d') // world
,
ctx2 = canvas[1].getContext('2d') // fog
,
context = canvas[2].getContext('2d') // flame
,
mDown = false,
r1 = 100,
r2 = 300,
density = .4,
hideOnMove = true,
hideFill = 'rgba( 0, 0, 0, 1 )'
,
overlay = 'rgba( 0, 0, 0, 1 )',
particles = [],
particle_count = 100;
// init flame
for (var i = 0; i < particle_count; i++) {
particles.push(new particle());
}
if (!hideOnMove) {
// shouldn't be done like this, but this is a demo
canvas.get(1).remove();
}
// black out the canvas
ctx.fillStyle = overlay;
ctx.fillRect(0, 0, 1280, 800);
// set up our "eraser"
ctx.globalCompositeOperation = 'destination-out';
canvas.last()
.on('mousemove', function (ev, ev2) {
ev2 && (ev = ev2);
var pX = ev.pageX,
pY = ev.pageY;
// reveal wherever we drag
var radGrd = ctx.createRadialGradient(pX, pY, r1, pX, pY, r2);
radGrd.addColorStop(0, 'rgba( 0, 0, 0, 1 )');
radGrd.addColorStop(density, 'rgba( 0, 0, 0, .1 )');
radGrd.addColorStop(1, 'rgba( 0, 0, 0, 0 )');
ctx.fillStyle = radGrd;
ctx.fillRect(pX - r2, pY - r2, r2 * 2, r2 * 2);
// partially hide the entire map and re-reval where we are now
ctx2.globalCompositeOperation = 'source-over';
ctx2.clearRect(0, 0, 1280, 800);
ctx2.fillStyle = hideFill;
ctx2.fillRect(0, 0, 1280, 800);
var radGrd = ctx.createRadialGradient(pX, pY, r1, pX, pY, r2);
radGrd.addColorStop(0, 'rgba( 0, 0, 0, 1 )');
radGrd.addColorStop(.8, 'rgba( 0, 0, 0, .1 )');
radGrd.addColorStop(1, 'rgba( 0, 0, 0, 0 )');
ctx2.globalCompositeOperation = 'destination-out';
ctx2.fillStyle = radGrd;
ctx2.fillRect(pX - r2, pY - r2, r2 * 2, r2 * 2);
})
.trigger('mousemove', {
pageX: 150,
pageY: 150
});
function drawing() {
// clear canvas
context.clearRect(0, 0, 1280, 800);
context.globalCompositeOperation = "lighter";
for (var i = 0; i < particles.length; i++) {
var p = particles[i];
context.beginPath();
//changing opacity according to the life.
//opacity goes to 0 at the end of life of a particle
p.opacity = Math.round(p.remaining_life / p.life * 100) / 100
//a gradient instead of white fill
var gradient = context.createRadialGradient(p.location.x, p.location.y, 0, p.location.x, p.location.y, p.radius);
gradient.addColorStop(0, "rgba(" + p.r + ", " + p.g + ", " + p.b + ", " + p.opacity + ")");
gradient.addColorStop(0.5, "rgba(" + p.r + ", " + p.g + ", " + p.b + ", " + p.opacity + ")");
gradient.addColorStop(1, "rgba(" + p.r + ", " + p.g + ", " + p.b + ", 0)");
context.fillStyle = gradient;
context.arc(p.location.x, p.location.y, p.radius, Math.PI * 2, false);
context.fill();
//lets move the particles
p.remaining_life--;
p.radius--;
p.location.x += p.speed.x;
p.location.y += p.speed.y;
//regenerate particles
if (p.remaining_life < 0 || p.radius < 0) {
//a brand new particle replacing the dead one
particles[i] = new particle();
}
}
}
// set flame on/off
var myVar = 0;
var on = 0;
$('.c').css({
left: "610px",
top: "500px"
});
$('.c').click(function () {
if (on == 0) {
myVar = setInterval(drawing, 33);
on = 1;
} else {
clearInterval(myVar);
context.clearRect(0, 0, 1280, 800);
on = 0;
}
});
function particle() {
//speed, life, location, life, colors
//speed.x range = -2.5 to 2.5
//speed.y range = -15 to -5 to make it move upwards
//lets change the Y speed to make it look like a flame
this.speed = {
x: -2.5 + Math.random() * 5,
y: -15 + Math.random() * 10
};
//flame location
this.location = {
x: 640,
y: 520
};
//radius range = 10-30
this.radius = 10 + Math.random() * 20;
//life range = 20-30
this.life = 20 + Math.random() * 10;
this.remaining_life = this.life;
//colors
this.r = Math.round(Math.random() * 255);
this.g = Math.round(Math.random() * 255);
this.b = Math.round(Math.random() * 255);
}
See the full webpage here http://codepen.io/anon/pen/hxrat
You could probably increase performance if you pre-generate the particles in a bunch of small canvases. Basically generate a list of particle-images with different colors and sizes and then use those for all the particles. Opacity can still be applied when the particle image is drawn. It should be faster to just draw a small canvas at a given position with a given opacity than to draw a path with radial gradient.
Here's an example, seems to double the performance at least: http://codepen.io/anon/pen/Izqwu
I don't pre-generate particle canvases. Instead I wrote a function getParticleCanvas() which takes a color and returns a 32*32 pixel canvas (creates it once if it doesn't exist), which is then used in drawing(). The particale canvas is then drawn at the correct size and opacity. The position is rounded to the nearest pixel for performance.
Also, to reduce the number of possible particle canvases, the random colors are rounded to 8 different steps per channel:
this.r = Math.round(Math.random() * 8)*32;
this.g = Math.round(Math.random() * 8)*32;
this.b = Math.round(Math.random() * 8)*32;
You can probably decrease the radius in getParticleCanvas() from 16 to 8 without being noticeable as well.
I got some outside support and ended up doing it like this, which I think it's the best way to keep performance:
Create a small dummy canvas in memory to run the draw() of the flame
var flameCanvas = document.createElement('canvas');
flameCanvas.width = 100;
flameCanvas.height = 400;
var context = flameCanvas.getContext('2d');
Use the big canvas to display copies of the dummy canvas wherever needed.
ctx3.drawImage(flameCanvas, 420, -37, 50, 200);
ctx3.drawImage(flameCanvas, 325, -47, 60, 240);
...
The rendering runs in the dummy canvas and the image is just copied all over, which increases performance so much. The only drawback is that all the flames will be the same.
To keep track of them, I surrounded each with an if clause
if (centerFlame_on) {
ctx3.drawImage(flameCanvas, 590, 165);
}
And I update centerFlame_on to true/false based on the mouse click.