I implemented a zoom function in my canvas just like this one: Zoom in on a point (using scale and translate)
Now I need to calculate the position of the mouse in relation to the canvas, I first tried like this:
var rect = this._canvas.getBoundingClientRect();
var x = ((event.clientX - rect.left) / (rect.right - rect.left) * this._canvas.width);
var y = ((event.clientY - rect.top) / (rect.bottom - rect.top) * this._canvas.height);
This works excellent until I zoom... I tried to do it like this:
var x = ((event.clientX - rect.left) / (rect.right - rect.left) * this._canvas.width) - this._canvas.offsetLeft ;
var y = ((event.clientY - rect.top) / (rect.bottom - rect.top) * this._canvas.height) - offset.top this._canvas.offSetTop ;
Any hint ? Or should I better use a JS library to interact with the canvas element ? If so, do you have any experience ?
Inverse Matrix
This answer include rotation as well because the scale is part of the rotation in the matrix you can't really exclude one or the other. But you can ignore the rotation (set it as zero) and just set scale and translation and it does what you want.
The inverse transform. It basically does the reverse of the standard 2D transformations. It will require that you keep track of the transformations so you can create the inverse transform, this can prove problematic in complex transforms if you wish to use ctx.rotation, ctx.scale, ctx.translate or ctx.transform. As you requirements are simple I have created a simple function to do the minimum transformation.
The following creates both the transformation matrix and the inverse transform as two arrays called matrix and invMatrix. The arguments are translation x,y (in canvas coordinates), scale, and rotation.
var matrix = [1,0,0,1,0,0];
var invMatrix = [1,0,0,1];
function createMatrix(x, y, scale, rotate){
var m = matrix; // just to make it easier to type and read
var im = invMatrix; // just to make it easier to type and read
// create the rotation and scale parts of the matrix
m[3] = m[0] = Math.cos(rotate) * scale;
m[2] = -(m[1] = Math.sin(rotate) * scale);
// add the translation
m[4] = x;
m[5] = y;
// calculate the inverse transformation
// first get the cross product of x axis and y axis
cross = m[0] * m[3] - m[1] * m[2];
// now get the inverted axis
im[0] = m[3] / cross;
im[1] = -m[1] / cross;
im[2] = -m[2] / cross;
im[3] = m[0] / cross;
}
Using the function
To use the function is simple. Just call with the desired values for position, scale and rotation.
Applying the inverse
To get the world coordinates (the transformed coordinates) from a pixel space (screen x, y) you need to apply the inverse transform
function toWorld(x,y){
var xx, yy, m, result;
m = invMatrix;
xx = x - matrix[4]; // remove the translation
yy = y - matrix[5]; // by subtracting the origin
// return the point {x:?,y:?} by multiplying xx,yy by the inverse matrix
return {
x: xx * m[0] + yy * m[2],
y: xx * m[1] + yy * m[3]
}
}
So if you want the mouse position in world space
var mouseWorldSpace = toWorld(mouse.x,mouse.y); // get the world space coordinates of the mouse
The function will convert any coordinate that is in screen space to the correct coordinate in world space.
Setting the 2D context transform
To use the transform you can set the 2D context transformation directly with
var m = matrix;
ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]);
Demo
And a demo to show it in use. A lot of extra code but I am sure you can find the parts you need. The Demo animates the transformation by rotating, scaling, and translating using createMatrix then uses toWorld to convert the mouse coordinates to the world space.
// the demo function
var demo = function(){
/** fullScreenCanvas.js begin **/
// create a full document canvas on top
var canvas = (function(){
var canvas = document.getElementById("canv");
if(canvas !== null){
document.body.removeChild(canvas);
}
// creates a blank image with 2d context
canvas = document.createElement("canvas");
canvas.id = "canv";
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style.position = "absolute";
canvas.style.top = "0px";
canvas.style.left = "0px";
canvas.style.zIndex = 1000;
canvas.ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
return canvas;
})();
var ctx = canvas.ctx;
/** fullScreenCanvas.js end **/
/** MouseFull.js begin **/
// get the mouse data . This is a generic mouse handler I use so a little over kill for this example
var canvasMouseCallBack = undefined; // if needed
var mouse = (function(){
var mouse = {
x : 0, y : 0, w : 0, alt : false, shift : false, ctrl : false,
interfaceId : 0, buttonLastRaw : 0, buttonRaw : 0,
over : false, // mouse is over the element
bm : [1, 2, 4, 6, 5, 3], // masks for setting and clearing button raw bits;
getInterfaceId : function () { return this.interfaceId++; }, // For UI functions
startMouse:undefined,
};
function mouseMove(e) {
var t = e.type, m = mouse;
m.x = e.offsetX; m.y = e.offsetY;
if (m.x === undefined) { m.x = e.clientX; m.y = e.clientY; }
m.alt = e.altKey;m.shift = e.shiftKey;m.ctrl = e.ctrlKey;
if (t === "mousedown") { m.buttonRaw |= m.bm[e.which-1];
} else if (t === "mouseup") { m.buttonRaw &= m.bm[e.which + 2];
} else if (t === "mouseout") { m.buttonRaw = 0; m.over = false;
} else if (t === "mouseover") { m.over = true;
} else if (t === "mousewheel") { m.w = e.wheelDelta;
} else if (t === "DOMMouseScroll") { m.w = -e.detail;}
if (canvasMouseCallBack) { canvasMouseCallBack(m.x, m.y); }
e.preventDefault();
}
function startMouse(element){
if(element === undefined){
element = document;
}
"mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",").forEach(
function(n){element.addEventListener(n, mouseMove);});
element.addEventListener("contextmenu", function (e) {e.preventDefault();}, false);
}
mouse.mouseStart = startMouse;
return mouse;
})();
if(typeof canvas === "undefined"){
mouse.mouseStart();
}else{
mouse.mouseStart(canvas);
}
/** MouseFull.js end **/
// some stuff to draw a grid
var gridStart= -(canvas.width/10)*4;
var gridEnd = (canvas.width/10)*14;
var gridStepMajor = canvas.width/10;
var gridStepMinor = canvas.width/20;
var minorCol = "#999";
var majorCol = "#000";
var minorWidth = 1;
var majorWidth = 3;
// some stuf to animate the transformation
var timer = 0;
var timerStep = 0.01;
//----------------------------------------------------------------------------
// the code from the answer
var matrix = [1, 0, 0, 1, 0, 0]; // normal matrix
var invMatrix = [1, 0, 0, 1]; // inverse matrix
function createMatrix(x, y, scale, rotate){
var m = matrix; // just to make it easier to type and read
var im = invMatrix; // just to make it easier to type and read
// create the scale and rotation part of the matrix
m[3] = m[0] = Math.cos(rotate) * scale;
m[2] = -(m[1] = Math.sin(rotate) * scale);
// translation
m[4] = x;
m[5] = y;
// calculate the inverse transformation
// first get the cross product of x axis and y axis
cross = m[0] * m[3] - m[1] * m[2];
// now get the inverted axies
im[0] = m[3] / cross;
im[1] = -m[1] / cross;
im[2] = -m[2] / cross;
im[3] = m[0] / cross;
}
// function to transform to world space
function toWorld(x,y){
var xx, yy, m;
m = invMatrix;
xx = x - matrix[4];
yy = y - matrix[5];
return {
x: xx * m[0] + yy * m[2] ,
y: xx * m[1] + yy * m[3]
}
}
//----------------------------------------------------------------------------
// center of canvas
var cw = canvas.width / 2;
var ch = canvas.height / 2;
// the main loop
function update(){
var i,x,y,s;
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset the transform so we can clear
ctx.clearRect(0, 0, canvas.width, canvas.height); // clear the canvas
// animate the transformation
timer += timerStep;
x = Math.cos(timer) * gridStepMajor * 5 + cw; // position
y = Math.sin(timer) * gridStepMajor * 5 + ch;
s = Math.sin(timer/1.2) + 1.5; // scale
//----------------------------------------------------------------------
// create the matrix at x,y scale = s and rotation time/3
createMatrix(x,y,s,timer/3);
// use the created matrix to set the transformation
var m = matrix;
ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]);
//----------------------------------------------------------------------------
//draw a grid
ctx.lineWidth = 2;
ctx.beginPath();
ctx.strokeStyle = majorCol ;
ctx.lineWidth = majorWidth;
for(i = gridStart; i <= gridEnd; i+= gridStepMajor){
ctx.moveTo(gridStart, i);
ctx.lineTo(gridEnd, i);
ctx.moveTo(i, gridStart);
ctx.lineTo(i, gridEnd);
}
ctx.stroke();
ctx.strokeStyle = minorCol ;
ctx.lineWidth = minorWidth;
for(i = gridStart+gridStepMinor; i < gridEnd; i+= gridStepMinor){
ctx.moveTo(gridStart, i);
ctx.lineTo(gridEnd, i);
ctx.moveTo(i, gridStart);
ctx.lineTo(i, gridEnd);
}
ctx.stroke();
//---------------------------------------------------------------------
// get the mouse world coordinates
var mouseWorldPos = toWorld(mouse.x, mouse.y);
//---------------------------------------------------------------------
// marke the location with a cross and a circle;
ctx.strokeStyle = "red";
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(mouseWorldPos.x - gridStepMajor, mouseWorldPos.y)
ctx.lineTo(mouseWorldPos.x + gridStepMajor, mouseWorldPos.y)
ctx.moveTo(mouseWorldPos.x, mouseWorldPos.y - gridStepMajor)
ctx.lineTo(mouseWorldPos.x, mouseWorldPos.y + gridStepMajor)
ctx.stroke();
ctx.fillStyle = "red";
ctx.strokeStyle = "yellow";
ctx.lineWidth = 4;
ctx.beginPath();
ctx.arc(mouseWorldPos.x, mouseWorldPos.y, 6, 0, Math.PI*2);
ctx.fill();
ctx.stroke();
ctx.fillStyle = "Blue";
ctx.setTransform(1,0,0,1,0,0);
ctx.font = "18px Arial";
var str = "Mouse canvas X: "+ mouse.x + " Y: " + mouse.y;
ctx.fillText(str , 10 ,18);
var str = "Mouse world X: "+ mouseWorldPos.x.toFixed(2) + " Y: " + mouseWorldPos.y.toFixed(2);
ctx.fillText(str , 10 ,36);
// if not over request a new animtion frame
if(!endItAll){
requestAnimationFrame(update);
}else{
// if done remove the canvas
var can = document.getElementById("canv");
if(can !== null){
document.body.removeChild(can);
}
// flag that we are ready to start again
endItAll = false;
}
}
update(); // start the animation
}
// Flag to indicate that the current execution should shut down
var endItAll = false;
// resizes but waits for the current running animnation to shut down
function resizeIt(){
endItAll = true;
function waitForIt(){
if(!endItAll){
demo();
}else{
setTimeout(waitForIt, 100);
}
}
setTimeout(waitForIt, 100);
}
// starts the demo
demo();
// listen to resize events and resize canvas if needed
window.addEventListener("resize",resizeIt)
Go step by step :
Find the coordinates of the mouse on the canvas:
var rect = canvas.getBoundingClientRect();
var xMouse = event.clientX - rect.left;
var yMouse = event.clientY - rect.top;
Normalize those coordinates so they are in [0;1] :
var relX = xMouse / canvas.width;
var relY = yMouse / canvas.height;
now say you view is defined by a rect called... well... viewRect, the position of the mouse in the view is :
var viewX = viewRect.left + relX*(viewRect.right-viewRect.left);
var viewY = viewRect.top + relY*(viewRect.bottom-viewRect.top);
When you launch your app your rect is 0,0,canvasWidth, canvasHeight.
When you click, you have to adjust your rect.
If clicking means zooming by zFactor at viewX, viewY, code will look like :
var newWidth = viewRect.width/zFactor;
var newHeight = viewRect.height/zFactor;
viewRect.left = viewX - newWidth/2;
viewRect.right = viewX + newWidth/2;
viewRect.top = viewY - newHeight/2;
viewRect.bottom = viewY + newHeight/2;
your draw method should look like :
context.save();
context.translate((viewRect.left+viewRect.right )/ 2, ...) ;
var scaleFactor = (viewRect.right+viewRect.left ) / canvasWidth;
context.scale(scaleFactor, scaleFactor);
... draw
context.restore();
Instead of keeping track of the various transformations, I inquired of the canvas for the current transform:
function mouseUp(canvas, event) {
const rect = canvas.getBoundingClientRect();
const transform = graphics.getTransform();
const canvasX = (event.clientX - rect.left - transform.e) / transform.a;
const canvasY = (event.clientY - rect.top - transform.f) / transform.d;
The doesn't deal with skew, but it gives a general idea of the approach I'm using.
Related
I am creating a smudging tool with HTML5 canvas. Now I have to shift the pixel color at the point of mouse pointer to the next position where mouse pointer moves. Is it possible to do with javascript?
<canvas id="canvas"><canvas>
var canvas = document.getElementById("canvas");
var context = canvas.getContext('2d');
var url = 'download.jpg';
var imgObj = new Image();
imgObj.src = url;
imgObj.onload = function(e) {
context.drawImage(imgObj, 0, 0);
}
function findPos(obj) {
var curleft = 0,
curtop = 0;
if (obj.offsetParent) {
do {
curleft += obj.offsetLeft;
curtop += obj.offsetTop;
} while (obj = obj.offsetParent);
return {
x: curleft,
y: curtop
};
}
return undefined;
}
function rgbToHex(r, g, b) {
if (r > 255 || g > 255 || b > 255)
throw "Invalid color component";
return ((r << 16) | (g << 8) | b).toString(16);
}
$('#canvas').mousemove(function(e) {
var pos = findPos(this);
var x = e.pageX - pos.x;
var y = e.pageY - pos.y;
console.log(x, y);
var c = this.getContext('2d');
var p = c.getImageData(x, y, 1, 1).data;
var hex = "#" + ("000000" + rgbToHex(p[0], p[1], p[2])).slice(-6);
console.log(hex)
});
I am very short on time ATM so code only.
Uses an offscreen canvas brush to get a copy of the background canvas background where the mouse was last frame. Then use a radial gradient to feather the brush using ctx.globalCompositeOperation = "destination-in". Then draw the updated brush at the next mouse position.
The main canvas is use just to display, the canvas being smeared is called background You can put whatever content you want on that canvas (eg image) and it can be any size, and you can zoom, pan, rotate the background though you will have to convert the mouse coordinates to match the background coordinates
Click drag mouse to smear colours.
const ctx = canvas.getContext("2d");
const background = createCanvas(canvas.width,canvas.height);
const brushSize = 64;
const bs = brushSize;
const bsh = bs / 2;
const smudgeAmount = 0.25; // values from 0 none to 1 full
// helpers
const doFor = (count, cb) => { var i = 0; while (i < count && cb(i++) !== true); }; // the ; after while loop is important don't remove
const randI = (min, max = min + (min = 0)) => (Math.random() * (max - min) + min) | 0;
// simple mouse
const mouse = {x : 0, y : 0, button : false}
function mouseEvents(e){
mouse.x = e.pageX;
mouse.y = e.pageY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["down","up","move"].forEach(name => document.addEventListener("mouse"+name,mouseEvents));
// brush gradient for feather
const grad = ctx.createRadialGradient(bsh,bsh,0,bsh,bsh,bsh);
grad.addColorStop(0,"black");
grad.addColorStop(1,"rgba(0,0,0,0)");
const brush = createCanvas(brushSize)
// creates an offscreen canvas
function createCanvas(w,h = w){
var c = document.createElement("canvas");
c.width = w;
c.height = h;
c.ctx = c.getContext("2d");
return c;
}
// get the brush from source ctx at x,y
function brushFrom(ctx,x,y){
brush.ctx.globalCompositeOperation = "source-over";
brush.ctx.globalAlpha = 1;
brush.ctx.drawImage(ctx.canvas,-(x - bsh),-(y - bsh));
brush.ctx.globalCompositeOperation = "destination-in";
brush.ctx.globalAlpha = 1;
brush.ctx.fillStyle = grad;
brush.ctx.fillRect(0,0,bs,bs);
}
// short cut vars
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
var globalTime;
var lastX;
var lastY;
// update background is size changed
function createBackground(){
background.width = w;
background.height = h;
background.ctx.fillStyle = "white";
background.ctx.fillRect(0,0,w,h);
doFor(64,()=>{
background.ctx.fillStyle = `rgb(${randI(255)},${randI(255)},${randI(255)}`;
background.ctx.fillRect(randI(w),randI(h),randI(10,100),randI(10,100));
});
}
// main update function
function update(timer){
globalTime = timer;
ctx.setTransform(1,0,0,1,0,0); // reset transform
ctx.globalAlpha = 1; // reset alpha
if(w !== innerWidth || h !== innerHeight){
cw = (w = canvas.width = innerWidth) / 2;
ch = (h = canvas.height = innerHeight) / 2;
createBackground();
}else{
ctx.clearRect(0,0,w,h);
}
ctx.drawImage(background,0,0);
// if mouse down then do the smudge for all pixels between last mouse and mouse now
if(mouse.button){
brush.ctx.globalAlpha = smudgeAmount;
var dx = mouse.x - lastX;
var dy = mouse.y - lastY;
var dist = Math.sqrt(dx*dx+dy*dy);
for(var i = 0;i < dist; i += 1){
var ni = i / dist;
brushFrom(background.ctx,lastX + dx * ni,lastY + dy * ni);
ni = (i+1) / dist;
background.ctx.drawImage(brush,lastX + dx * ni - bsh,lastY + dy * ni - bsh);
}
}else{
brush.ctx.clearRect(0,0,bs,bs); /// clear brush if not used
}
lastX = mouse.x;
lastY = mouse.y;
requestAnimationFrame(update);
}
requestAnimationFrame(update);
canvas { position : absolute; top : 0px; left : 0px; }
<canvas id="canvas"></canvas>
i have a canvas, inside of which i have a board/grid. When a user highlights their mouse over an intersection of the grid, i want it to show where their game peice will go. This worked perfectly fine when the board was the exact size of the canvas. I made it abit smaller by x all the way round.
So as you can see in the picture below, the green shows the canvas and the grid is the board. I put my cursor at the very bottom right corner of the green to show when it triggers. The only one that works fine is the middle one because regardless how big i make the board, the middle will always be the middle.
Any easy fix would just be to make the area with the mouseover event, the dimensions of the board instead of the canvas but the event listener is on the canvas. My code is below the image
Variables:
var canvas = document.getElementById("game-canvas");
var context = canvas.getContext("2d");
var boardSize = 13;
var border = canvas.width / 20;
var boardWidth = canvas.width - (border * 2);
var boardHeight = canvas.height - (border * 2);
var cellWidth = boardWidth / (boardSize - 1);
var cellHeight = boardHeight / (boardSize - 1);
var lastX;
var lastY;
Mouse over event:
canvas.addEventListener('mousemove', function(evt)
{
var position = getGridPoint(evt);
if ((position.x != lastX) || (position.y != lastY))
{
placeStone((position.x * cellWidth) + border, (position.y * cellWidth) + border, 'rgba(0, 0, 0, 0.2)');
}
lastX = position.x;
lastY = position.y;
});
Gets the point on the grid and converts that into a number 0 - 13 (in this case)
function getGridPoint(evt)
{
var rect = canvas.getBoundingClientRect();
var x = Math.round((evt.clientX-rect.left)/(rect.right-rect.left)*boardWidth);
var y = Math.round((evt.clientY-rect.top)/(rect.bottom-rect.top)*boardHeight);
var roundX = Math.round(x / cellWidth);
var roundY = Math.round(y / cellHeight);
return {
x: roundX,
y: roundY
};
}
And finally draws the piece on the board:
function placeStone(x, y, color)
{
var radius = cellWidth / 2;
context.beginPath();
context.arc(x, y, radius, 0, 2 * Math.PI, false);
context.fillStyle = color;
context.fill();
context.lineWidth = 5;
}
I left a couple bits out like how the grid refreshs so its not a string of circles following your mouse and stuff, to keep it as short as i can, im hoping its just a simple asnwer and nobody needs to recreate it but if you do i can include the function that refreshes the grid and draws everything. Thankyou for any advice
To get the position relative to a box
// just as an example w,h are width and height
const box = { x : 10, y : 10, w : 100, h : 100 };
// mouse is the mouse coords and relative to the topleft of canvas (0,0);
var mouse.box = {}
mouse.box.x = mouse.x - box.x;
mouse.box.y = mouse.y - box.y;
Negative values for mouse.box x,y and values greater than box width and height have mouse outside.
For more convenience you can get the mouse normalize pos in the box
mouse.box.nx = mouse.box.x / box.w;
mouse.box.ny = mouse.box.y / box.h;
The coords for nx,ny are in the range 0-1 when inside or on the edge of the box;
If you want to have grid positions then define the grid
box.gridW = 10; // grid divisions width
box.gridH = 10; // grid divisions height
Then getting the grid pos of mouse
mouse.box.gx = Math.floor(mouse.box.nx * box.gridW);
mouse.box.gy = Math.floor(mouse.box.ny * box.gridH);
const ctx = canvas.getContext("2d");
const box = { x : 50,y : 10, w : 200, h : 200, gridW : 10, gridH : 10}
function drawGrid(){
var sx = box.w / box.gridW;
var sy = box.h / box.gridH;
var bx = box.x;
var by = box.y;
for(var y = 0; y < box.gridH; y ++){
for(var x = 0; x < box.gridW; x ++){
ctx.strokeRect(x * sx + bx, y * sx + by,sx,sy);
}
}
if(mouse.box){
if(mouse.box.nx >= 0 && mouse.box.nx <= 1 &&
mouse.box.ny >= 0 && mouse.box.ny <= 1){
ctx.fillRect(mouse.box.gx * sx + bx, mouse.box.gy * sx + by,sx,sy);
}
}
}
const mouse = {};
canvas.addEventListener("mousemove",(e)=>{
mouse.x = e.pageX;
mouse.y = e.pageY;
});
function updateMouse(){
if(!mouse.box){
mouse.box = {};
}
mouse.box.x = mouse.x - box.x;
mouse.box.y = mouse.y - box.y;
mouse.box.nx = mouse.box.x / box.w;
mouse.box.ny = mouse.box.y / box.h;
mouse.box.gx = Math.floor(mouse.box.nx * box.gridW);
mouse.box.gy = Math.floor(mouse.box.ny * box.gridH);
var p = 20;
ctx.fillText("x : " + mouse.x,box.x+box.w+10,p); p+= 14;
ctx.fillText("y : " + mouse.y,box.x+box.w+10,p); p+= 20;
ctx.fillText("Box relative",box.x+box.w+10,p); p+= 14;
ctx.fillText("x : " + mouse.box.x,box.x+box.w+10,p); p+= 14;
ctx.fillText("y : " + mouse.box.y,box.x+box.w+10,p); p+= 14;
ctx.fillText("nx : " + mouse.box.nx,box.x+box.w+10,p); p+= 14;
ctx.fillText("ny : " + mouse.box.ny,box.x+box.w+10,p); p+= 14;
ctx.fillText("gx : " + mouse.box.gx,box.x+box.w+10,p); p+= 14;
ctx.fillText("gy : " + mouse.box.gy,box.x+box.w+10,p); p+= 14;
}
function mainLoop(time){
if(canvas.width !== innerWidth || canvas.height !== innerHeight){ // resize canvas if window size has changed
canvas.width = innerWidth;
canvas.height = innerHeight;
}
ctx.setTransform(1,0,0,1,0,0); // set default transform
ctx.clearRect(0,0,canvas.width,canvas.height); // clear the canvas
updateMouse();
drawGrid();
requestAnimationFrame(mainLoop);
}
requestAnimationFrame(mainLoop);
canvas {
position : absolute;
top : 0px;
left : 0px;
}
<canvas id=canvas><canvas>
Sorry for the confusing title, I don't know how to succinctly describe my question.
I'm drawing an ellipse on a canvas element using javascript and I'm trying to figure out how to detect if the mouse is clicked inside of the ellipse or not. The way I'm trying to do this is by comparing the distance from the center of the ellipse to the mouse to the radius of the ellipse at the same angle as the mouse click. Here's a terrible picture representing what I just said if it's still confusing:
Obviously this isn't working, otherwise I wouldn't be asking this, so below is a picture of the computed radius line (in red) and the mouse line (in blue). In this picture, the mouse has been clicked at a 45° angle to the center of the ellipse and I've calculated that the radius line is being drawn at about a 34.99° angle.
And below is the calculation code:
//This would be the blue line in the picture above
var mouseToCenterDistance = distanceTo(centerX, centerY, mouseX, mouseY);
var angle = Math.acos((mouseX - centerX) / mouseToCenterDistance);
var radiusPointX = (radiusX * Math.cos(angle)) + centerX;
var radiusPointY = (radiusY * Math.sin(-angle)) + centerY;
//This would be the red line in the picture above
var radius = distanceTo(centerX, centerY, radiusPointX, radiusPointY);
var clickedInside = mouseToCenterDistance <= radius;
I'm really not sure why this isn't working, I've been staring at this math forever and it seems correct. Is it correct and there's something about drawing on the canvas that's making it not work? Please help!
Ellipse line intercept
Finding the intercept includes solving if the point is inside.
If it is the ellipse draw via the 2D context the solution is as follows
// defines the ellipse
var cx = 100; // center
var cy = 100;
var r1 = 20; // radius 1
var r2 = 100; // radius 2
var ang = 1; // angle in radians
// rendered with
ctx.beginPath();
ctx.ellipse(cx,cy,r1,r2,ang,0,Math.PI * 2,true)
ctx.stroke()
To find the point on the ellipse that intersects the line from the center to x,y. To solve I normalise the ellipse so that it is a circle (well the line is moved so that the ellipse is a circle in its coordinate space).
var x = 200;
var y = 200;
var ratio = r1 / r2; // need the ratio between the two radius
// get the vector from the ellipse center to end of line
var dx = x - cx;
var dy = y - cy;
// get the vector that will normalise the ellipse rotation
var vx = Math.cos(-ang);
var vy = Math.sin(-ang);
// use that vector to rotate the line
var ddx = dx * vx - dy * vy;
var ddy = (dx * vy + dy * vx) * ratio; // lengthen or shorten dy
// get the angle to the line in normalise circle space.
var c = Math.atan2(ddy,ddx);
// get the vector along the ellipse x axis
var eAx = Math.cos(ang);
var eAy = Math.sin(ang);
// get the intercept of the line and the normalised ellipse
var nx = Math.cos(c) * r1;
var ny = Math.sin(c) * r2;
// rotate the intercept to the ellipse space
var ix = nx * eAx - ny * eAy
var iy = nx * eAy + ny * eAx
// cx,cy to ix ,iy is from the center to the ellipse circumference
The procedure can be optimised but for now that will solve the problem as presented.
Is point inside
Then to determine if the point is inside just compare the distances of the mouse and the intercept point.
var x = 200; // point to test
var y = 200;
// get the vector from the ellipse center to point to test
var dx = x - cx;
var dy = y - cy;
// get the vector that will normalise the ellipse rotation
var vx = Math.cos(ang);
var vy = Math.sin(ang);
// use that vector to rotate the line
var ddx = dx * vx + dy * vy;
var ddy = -dx * vy + dy * vx;
if( 1 >= (ddx * ddx) / (r1 * r1) + (ddy * ddy) / (r2 * r2)){
// point on circumference or inside ellipse
}
Example use of method.
function path(path){
ctx.beginPath();
var i = 0;
ctx.moveTo(path[i][0],path[i++][1]);
while(i < path.length){
ctx.lineTo(path[i][0],path[i++][1]);
}
if(close){
ctx.closePath();
}
ctx.stroke();
}
function strokeCircle(x,y,r){
ctx.beginPath();
ctx.moveTo(x + r,y);
ctx.arc(x,y,r,0,Math.PI * 2);
ctx.stroke();
}
function display() {
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0, 0, w, h);
var cx = w/2;
var cy = h/2;
var r1 = Math.abs(Math.sin(globalTime/ 4000) * w / 4);
var r2 = Math.abs(Math.sin(globalTime/ 4300) * h / 4);
var ang = globalTime / 1500;
// find the intercept from ellipse center to mouse on the ellipse
var ratio = r1 / r2
var dx = mouse.x - cx;
var dy = mouse.y - cy;
var dist = Math.hypot(dx,dy);
var ex = Math.cos(-ang);
var ey = Math.sin(-ang);
var c = Math.atan2((dx * ey + dy * ex) * ratio, dx * ex - dy * ey);
var nx = Math.cos(c) * r1;
var ny = Math.sin(c) * r2;
var ix = nx * ex + ny * ey;
var iy = -nx * ey + ny * ex;
var dist = Math.hypot(dx,dy);
var dist2Inter = Math.hypot(ix,iy);
ctx.strokeStyle = "Blue";
ctx.lineWidth = 4;
ctx.beginPath();
ctx.ellipse(cx,cy,r1,r2,ang,0,Math.PI * 2,true)
ctx.stroke();
if(dist2Inter > dist){
ctx.fillStyle = "#7F7";
ctx.globalAlpha = 0.5;
ctx.fill();
ctx.globalAlpha = 1;
}
// Display the intercept
ctx.strokeStyle = "black";
ctx.lineWidth = 2;
path([[cx,cy],[mouse.x,mouse.y]])
ctx.strokeStyle = "red";
ctx.lineWidth = 5;
path([[cx,cy],[cx + ix,cy+iy]])
ctx.strokeStyle = "red";
ctx.lineWidth = 4;
strokeCircle(cx + ix, cy + iy, 6)
ctx.fillStyle = "white";
ctx.fill();
ctx.strokeStyle = "red";
ctx.lineWidth = 4;
strokeCircle(cx, cy, 6)
ctx.fillStyle = "white";
ctx.fill();
ctx.strokeStyle = "black";
ctx.lineWidth = 2;
strokeCircle(mouse.x, mouse.y, 4)
ctx.fillStyle = "white";
ctx.fill();
}
/** SimpleFullCanvasMouse.js begin **/
//==============================================================================
// Boilerplate code from here down and not related to the answer
//==============================================================================
var w, h, cw, ch, canvas, ctx, mouse, globalTime = 0, firstRun = true;
;(function(){
const RESIZE_DEBOUNCE_TIME = 100;
var createCanvas, resizeCanvas, setGlobals, resizeCount = 0;
createCanvas = function () {
var c,
cs;
cs = (c = document.createElement("canvas")).style;
cs.position = "absolute";
cs.top = cs.left = "0px";
cs.zIndex = 1000;
document.body.appendChild(c);
return c;
}
resizeCanvas = function () {
if (canvas === undefined) {
canvas = createCanvas();
}
canvas.width = innerWidth;
canvas.height = innerHeight;
ctx = canvas.getContext("2d");
if (typeof setGlobals === "function") {
setGlobals();
}
if (typeof onResize === "function") {
if(firstRun){
onResize();
firstRun = false;
}else{
resizeCount += 1;
setTimeout(debounceResize, RESIZE_DEBOUNCE_TIME);
}
}
}
function debounceResize() {
resizeCount -= 1;
if (resizeCount <= 0) {
onResize();
}
}
setGlobals = function () {
cw = (w = canvas.width) / 2;
ch = (h = canvas.height) / 2;
}
mouse = (function () {
function preventDefault(e) {
e.preventDefault();
}
var mouse = {
x : 0,
y : 0,
w : 0,
alt : false,
shift : false,
ctrl : false,
buttonRaw : 0,
over : false,
bm : [1, 2, 4, 6, 5, 3],
active : false,
bounds : null,
crashRecover : null,
mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
};
var m = mouse;
function mouseMove(e) {
var t = e.type;
m.bounds = m.element.getBoundingClientRect();
m.x = e.pageX - m.bounds.left;
m.y = e.pageY - m.bounds.top;
m.alt = e.altKey;
m.shift = e.shiftKey;
m.ctrl = e.ctrlKey;
if (t === "mousedown") {
m.buttonRaw |= m.bm[e.which - 1];
} else if (t === "mouseup") {
m.buttonRaw &= m.bm[e.which + 2];
} else if (t === "mouseout") {
m.buttonRaw = 0;
m.over = false;
} else if (t === "mouseover") {
m.over = true;
} else if (t === "mousewheel") {
m.w = e.wheelDelta;
} else if (t === "DOMMouseScroll") {
m.w = -e.detail;
}
if (m.callbacks) {
m.callbacks.forEach(c => c(e));
}
if ((m.buttonRaw & 2) && m.crashRecover !== null) {
if (typeof m.crashRecover === "function") {
setTimeout(m.crashRecover, 0);
}
}
e.preventDefault();
}
m.addCallback = function (callback) {
if (typeof callback === "function") {
if (m.callbacks === undefined) {
m.callbacks = [callback];
} else {
m.callbacks.push(callback);
}
}
}
m.start = function (element) {
if (m.element !== undefined) {
m.removeMouse();
}
m.element = element === undefined ? document : element;
m.mouseEvents.forEach(n => {
m.element.addEventListener(n, mouseMove);
});
m.element.addEventListener("contextmenu", preventDefault, false);
m.active = true;
}
m.remove = function () {
if (m.element !== undefined) {
m.mouseEvents.forEach(n => {
m.element.removeEventListener(n, mouseMove);
});
m.element.removeEventListener("contextmenu", preventDefault);
m.element = m.callbacks = undefined;
m.active = false;
}
}
return mouse;
})();
// Clean up. Used where the IDE is on the same page.
var done = function () {
window.removeEventListener("resize", resizeCanvas)
mouse.remove();
document.body.removeChild(canvas);
canvas = ctx = mouse = undefined;
}
function update(timer) { // Main update loop
if(ctx === undefined){ return; }
globalTime = timer;
display(); // call demo code
requestAnimationFrame(update);
}
setTimeout(function(){
resizeCanvas();
mouse.start(canvas, true);
//mouse.crashRecover = done;
window.addEventListener("resize", resizeCanvas);
requestAnimationFrame(update);
},0);
})();
/** SimpleFullCanvasMouse.js end **/
If you have an ellipse of the form (x-x0)2/a2 + (y-y0)2/b2 = 1, then a point (x, y) is inside the ellipse if and only if (x-x0)2/a2 + (y-y0)2/b2 < 1. You can just test that inequality to see if the mouse is inside the ellipse.
To be able to draw a line to the edge of the ellipse: get the theta of the mouse with atan2 (don't use acos, you'll get incorrect results in quadrants III & IV), use the polar equation of the ellipse to solve for r, then convert back to rectangular coordinates and draw.
I've got the linear component of collision resolution down relatively well, but I can't quite figure out how to do the same for the angular one. From what I've read, it's something like... torque = point of collision x linear velocity. (cross product) I tried to incorporate an example I found into my code but I actually don't see any rotation at all when objects collide. The other fiddle works perfectly with a rudimentary implementation of the seperating axis theorem and the angular velocity calculations. Here's what I've come up with...
Property definitions (orientation, angular velocity, and angular acceleration):
rotation: 0,
angularVelocity: 0,
angularAcceleration: 0
Calculating the angular velocity in the collision response:
var pivotA = this.vector(bodyA.x, bodyA.y);
bodyA.angularVelocity = 1 * 0.2 * (bodyA.angularVelocity / Math.abs(bodyA.angularVelocity)) * pivotA.subtract(isCircle ? pivotA.add(bodyA.radius) : {
x: pivotA.x + boundsA.width,
y: pivotA.y + boundsA.height
}).vCross(bodyA.velocity);
var pivotB = this.vector(bodyB.x, bodyB.y);
bodyB.angularVelocity = 1 * 0.2 * (bodyB.angularVelocity / Math.abs(bodyB.angularVelocity)) * pivotB.subtract(isCircle ? pivotB.add(bodyB.radius) : {
x: pivotB.x + boundsB.width,
y: pivotB.y + boundsB.height
}).vCross(bodyB.velocity);
Updating the orientation in the update loop:
var torque = 0;
torque += core.objects[o].angularVelocity * -1;
core.objects[o].angularAcceleration = torque / core.objects[o].momentOfInertia();
core.objects[o].angularVelocity += core.objects[o].angularAcceleration;
core.objects[o].rotation += core.objects[o].angularVelocity;
I would post the code that I have for calculating the moments of inertia but there's a seperate one for every object so that would be a bit... lengthy. Nonetheless, here's the one for a circle as an example:
return this.mass * this.radius * this.radius / 2;
Just to show the result, here's my fiddle. As shown, objects do not rotate on collision. (not exactly visible with the circles, but it should work for the zero and seven)
What am I doing wrong?
EDIT: Reason they weren't rotating at all was because of an error with groups in the response function -- it rotates now, just not correctly. However, I've commented that out for now as it messes things up.
Also, I've tried another method for rotation. Here's the code in the response:
_bodyA.angularVelocity = direction.vCross(_bodyA.velocity) / (isCircle ? _bodyA.radius : boundsA.width);
_bodyB.angularVelocity = direction.vCross(_bodyB.velocity) / (isCircle ? _bodyB.radius : boundsB.width);
Note that direction refers to the "collision normal".
Angular and linear acceleration due to force vector
Angular and directional accelerations due to an applied force are two components of the same thing and can not be separated. To get one you need to solve for both.
Define the calculations
From simple physics and standing on shoulders we know the following.
F is force (equivalent to inertia)
Fv is linear force
Fa is angular force
a is acceleration could be linear or rotational depending on where it is used
v is velocity. For angular situations it is the tangential component only
m is mass
r is radius
For linear forces
F = m * v
From which we derive
m = F / v
v = F / m
For rotational force (v is tangential velocity)
F = r * r * m * (v / r) and simplify F = r * m * v
From which we derive
m = F / ( r * v )
v = F / ( r * m )
r = F / ( v * m )
Because the forces we apply are instantaneous we can interchange a acceleration and v velocity to give all the following formulas
Linear
F = m * a
m = F / a
a = F / m
Rotational
F = r * m * a
m = F / ( r * a )
a = F / ( r * m )
r = F / ( a * m )
As we are only interested in the change in velocity for both linear and rotation solutions
a1 = F / m
a2 = F / ( r * m )
Where a1 is acceleration in pixels per frame2 and a2 is acceleration in radians per frame2 ( the frame squared just denotes it is acceleration)
From 1D to 2D
Because this is a 2D solution and all above are 1D we need to use vectors. I for this problem use two forms of the 2D vector. Polar that has a magnitude (length, distance, the like...) and direction. Cartesian which has x and y. What a vector represents depends on how it is used.
The following functions are used as helpers in the solution. They are written in ES6 so for non compliant browsers you will have to adapt them, though I would not ever suggest you use these as they are written for convenience, they are very inefficient and do a lot of redundant calculations.
Converts a vector from polar to cartesian returning a new one
function polarToCart(pVec, retV = {x : 0, y : 0}) {
retV.x = Math.cos(pVec.dir) * pVec.mag;
retV.y = Math.sin(pVec.dir) * pVec.mag;
return retV;
}
Converts a vector from cartesian to polar returning a new one
function cartToPolar(vec, retV = {dir : 0, mag : 0}) {
retV.dir = Math.atan2(vec.y, vec.x);
retV.mag = Math.hypot(vec.x, vec.y);
return retV;
}
Creates a polar vector
function polar(mag = 1, dir = 0) {
return validatePolar({dir : dir,mag : mag});
}
Create a vector as a cartesian
function vector(x = 1, y = 0) {
return {x : x, y : y};
}
True is the arg vec is a vector in polar form
function isPolar(vec) {
if (vec.mag !== undefined && vec.dir !== undefined) {return true;}
return false;
}
Returns true if arg vec is a vector in cartesian form
function isCart(vec) {
if (vec.x !== undefined && vec.y !== undefined) {return true;}
return false;
}
Returns a new vector in polar form also ensures that vec.mag is positive
function asPolar(vec){
if(isCart(vec)){ return cartToPolar(vec); }
if(vec.mag < 0){
vec.mag = - vec.mag;
vec.dir += PI;
}
return { dir : vec.dir, mag : vec.mag };
}
Copy and converts an unknown vec to cart if not already
function asCart(vec){
if(isPolar(vec)){ return polarToCart(vec); }
return { x : vec.x, y : vec.y};
}
Calculations can result in a negative magnitude though this is valid for some calculations this results in the incorrect vector (reversed) this simply validates that the polar vector has a positive magnitude it does not change the vector just the sign and direction
function validatePolar(vec) {
if (isPolar(vec)) {
if (vec.mag < 0) {
vec.mag = - vec.mag;
vec.dir += PI;
}
}
return vec;
}
The Box
Now we can define an object that we can use to play with. A simple box that has position, size, mass, orientation, velocity and rotation
function createBox(x,y,w,h){
var box = {
x : x, // pos
y : y,
r : 0.1, // its rotation AKA orientation or direction in radians
h : h, // its height
w : w, // its width
dx : 0, // delta x in pixels per frame 1/60th second
dy : 0, // delta y
dr : 0.0, // deltat rotation in radians per frame 1/60th second
mass : w * h, // mass in things
update :function(){
this.x += this.dx;
this.y += this.dy;
this.r += this.dr;
},
}
return box;
}
Applying a force to an object
So now we can redefine some terms
F (force) is a vector force the magnitude is the force and it has a direction
var force = polar(100,0); // create a force 100 units to the right (0 radians)
The force is meaningless without a position where it is applied.
Position is a vector that just holds and x and y location
var location = vector(canvas.width/2, canvas.height/2); // defines a point in the middle of the canvas
Directional vector holds the direction and distance between to positional vectors
var l1 = vector(canvas.width/2, canvas.height/2); // defines a point in the middle of the canvas
var l2 = vector(100,100);
var direction = asPolar(vector(l2.x - l1.x, l2.y - l1.y)); // get the direction as polar vector
direction now has the direction from canvas center to point (100,100) and the distance.
The last thing we need to do is extract the components from a force vector along a directional vector. When you apply a force to an object the force is split into two, one is the force along the line to the object center and adds to the object acceleration, the other force is at 90deg to the line to the object center (the tangent) and that is the force that changes rotation.
To get the two components you get the difference in direction between the force vector and the directional vector from where the force is applied to the object center.
var force = polar(100,0); // the force
var forceLoc = vector(50,50); // the location the force is applied
var direction2Center = asPolar(vector(box.x - forceLoc.x, box.y - forceLoc.y)); // get the direction as polar vector
var pheta = direction2Center - force.dir; // get the angle between the force and object center
Now that you have that angle pheta the force can be split into its rotational and linear components with trig.
var F = force.mag; // get the force magnitude
var Fv = Math.cos(pheta) * F; // get the linear force
var Fa = Math.sin(pheta) * F; // get the angular force
Now the forces can be converted back to accelerations for linear a = F/m and angular a = F/(m*r)
accelV = Fv / box.mass; // linear acceleration in pixels
accelA = Fa / (box.mass * direction2Center.mag); // angular acceleration in radians
You then convert the linear force back to a vector that has a direction to the center of the object
var forceV = polar(Fv, direction2Center);
Convert is back to the cartesian so we can add it to the object deltaX and deltaY
forceV = asCart(forceV);
And add the acceleration to the box
box.dx += forceV.x;
box.dy += forceV.y;
Rotational acceleration is just one dimensional so just add it to the delta rotation of the box
box.dr += accelA;
And that is it.
Function to apply force to Box
The function if attached to the box will apply a force vector at a location to the box.
Attach to the box like so
box.applyForce = applyForce; // bind function to the box;
You can then call the function via the box
box.applyForce(force, locationOfForce);
function applyForce(force, loc){ // force is a vector, loc is a coordinate
var toCenter = asPolar(vector(this.x - loc.x, this.y - loc.y)); // get the vector to the center
var pheta = toCenter.dir - force.dir; // get the angle between the force and the line to center
var Fv = Math.cos(pheta) * force.mag; // Split the force into the velocity force along the line to the center
var Fa = Math.sin(pheta) * force.mag; // and the angular force at the tangent to the line to the center
var accel = asPolar(toCenter); // copy the direction to center
accel.mag = Fv / this.mass; // now use F = m * a in the form a = F/m to get acceleration
var deltaV = asCart(accel); // convert acceleration to cartesian
this.dx += deltaV.x // update the box delta V
this.dy += deltaV.y //
var accelA = Fa / (toCenter.mag * this.mass); // for the angular component get the rotation
// acceleration from F=m*a*r in the
// form a = F/(m*r)
this.dr += accelA;// now add that to the box delta r
}
The Demo
The demo is only about the function applyForce the stuff to do with gravity and bouncing are only very bad approximations and should not be used for any physic type of stuff as they do not conserve energy.
Click and drag to apply a force to the object in the direction that the mouse is moved.
const PI90 = Math.PI / 2;
const PI = Math.PI;
const PI2 = Math.PI * 2;
const INSET = 10; // playfeild inset
const ARROW_SIZE = 6
const SCALE_VEC = 10;
const SCALE_FORCE = 0.15;
const LINE_W = 2;
const LIFE = 12;
const FONT_SIZE = 20;
const FONT = "Arial Black";
const WALL_NORMS = [PI90,PI,-PI90,0]; // dirction of the wall normals
var box = createBox(200, 200, 50, 100);
box.applyForce = applyForce; // Add this function to the box
// render / update function
var mouse = (function(){
function preventDefault(e) { e.preventDefault(); }
var i;
var mouse = {
x : 0, y : 0,buttonRaw : 0,
bm : [1, 2, 4, 6, 5, 3], // masks for setting and clearing button raw bits;
mouseEvents : "mousemove,mousedown,mouseup".split(",")
};
function mouseMove(e) {
var t = e.type, m = mouse;
m.x = e.offsetX; m.y = e.offsetY;
if (m.x === undefined) { m.x = e.clientX; m.y = e.clientY; }
if (t === "mousedown") { m.buttonRaw |= m.bm[e.which-1];
} else if (t === "mouseup") { m.buttonRaw &= m.bm[e.which + 2];}
e.preventDefault();
}
mouse.start = function(element = document){
if(mouse.element !== undefined){ mouse.removeMouse();}
mouse.element = element;
mouse.mouseEvents.forEach(n => { element.addEventListener(n, mouseMove); } );
}
mouse.remove = function(){
if(mouse.element !== undefined){
mouse.mouseEvents.forEach(n => { mouse.element.removeEventListener(n, mouseMove); } );
mouse.element = undefined;
}
}
return mouse;
})();
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");
if(box){
box.w = canvas.width * 0.10;
box.h = box.w * 2;
box.mass = box.w * box.h;
}
}
window.addEventListener("resize",resizeCanvas);
resizeCanvas();
mouse.start(canvas)
var tempVecs = [];
function addTempVec(v,vec,col,life = LIFE,scale = SCALE_VEC){tempVecs.push({v:v,vec:vec,col:col,scale:scale,life:life,sLife:life});}
function drawTempVecs(){
for(var i = 0; i < tempVecs.length; i ++ ){
var t = tempVecs[i]; t.life -= 1;
if(t.life <= 0){tempVecs.splice(i, 1); i--; continue}
ctx.globalAlpha = (t.life / t.sLife)*0.25;
drawVec(t.v, t.vec ,t.col, t.scale)
}
}
function drawVec(v,vec,col,scale = SCALE_VEC){
vec = asPolar(vec)
ctx.setTransform(1,0,0,1,v.x,v.y);
var d = vec.dir;
var m = vec.mag;
ctx.rotate(d);
ctx.beginPath();
ctx.lineWidth = LINE_W;
ctx.strokeStyle = col;
ctx.moveTo(0,0);
ctx.lineTo(m * scale,0);
ctx.moveTo(m * scale-ARROW_SIZE,-ARROW_SIZE);
ctx.lineTo(m * scale,0);
ctx.lineTo(m * scale-ARROW_SIZE,ARROW_SIZE);
ctx.stroke();
}
function drawText(text,x,y,font,size,col){
ctx.font = size + "px "+font;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.setTransform(1,0,0,1,x,y);
ctx.globalAlpha = 1;
ctx.fillStyle = col;
ctx.fillText(text,0,0);
}
function createBox(x,y,w,h){
var box = {
x : x, // pos
y : y,
r : 0.1, // its rotation AKA orientation or direction in radians
h : h, // its height, and I will assume that its depth is always equal to its height
w : w, // its width
dx : 0, // delta x in pixels per frame 1/60th second
dy : 0, // delta y
dr : 0.0, // deltat rotation in radians per frame 1/60th second
getDesc : function(){
var vel = Math.hypot(this.dx ,this.dy);
var radius = Math.hypot(this.w,this.h)/2
var rVel = Math.abs(this.dr * radius);
var str = "V " + (vel*60).toFixed(0) + "pps ";
str += Math.abs(this.dr * 60 * 60).toFixed(0) + "rpm ";
str += "Va " + (rVel*60).toFixed(0) + "pps ";
return str;
},
mass : function(){ return (this.w * this.h * this.h)/1000; }, // mass in K things
draw : function(){
ctx.globalAlpha = 1;
ctx.setTransform(1,0,0,1,this.x,this.y);
ctx.rotate(this.r);
ctx.fillStyle = "#444";
ctx.fillRect(-this.w/2, -this.h/2, this.w, this.h)
ctx.strokeRect(-this.w/2, -this.h/2, this.w, this.h)
},
update :function(){
this.x += this.dx;
this.y += this.dy;
this.dy += 0.061; // alittle gravity
this.r += this.dr;
},
getPoint : function(which){
var dx,dy,x,y,xx,yy,velocityA,velocityT,velocity;
dx = Math.cos(this.r);
dy = Math.sin(this.r);
switch(which){
case 0:
x = -this.w /2;
y = -this.h /2;
break;
case 1:
x = this.w /2;
y = -this.h /2;
break;
case 2:
x = this.w /2;
y = this.h /2;
break;
case 3:
x = -this.w /2;
y = this.h /2;
break;
case 4:
x = this.x;
y = this.y;
}
var xx,yy;
xx = x * dx + y * -dy;
yy = x * dy + y * dx;
var details = asPolar(vector(xx, yy))
xx += this.x;
yy += this.y;
velocityA = polar(details.mag * this.dr, details.dir + PI90);
velocityT = vectorAdd(velocity = vector(this.dx, this.dy), velocityA);
return {
velocity : velocity, // only directional
velocityT : velocityT, // total
velocityA : velocityA, // angular only
pos : vector(xx, yy),
radius : details.mag,
}
},
}
box.mass = box.mass(); // Mass remains the same so just set it with its function
return box;
}
// calculations can result in a negative magnitude though this is valide for some
// calculations this results in the incorrect vector (reversed)
// this simply validates that the polat vector has a positive magnitude
// it does not change the vector just the sign and direction
function validatePolar(vec){
if(isPolar(vec)){
if(vec.mag < 0){
vec.mag = - vec.mag;
vec.dir += PI;
}
}
return vec;
}
// converts a vector from polar to cartesian returning a new one
function polarToCart(pVec, retV = {x : 0, y : 0}){
retV.x = Math.cos(pVec.dir) * pVec.mag;
retV.y = Math.sin(pVec.dir) * pVec.mag;
return retV;
}
// converts a vector from cartesian to polar returning a new one
function cartToPolar(vec, retV = {dir : 0, mag : 0}){
retV.dir = Math.atan2(vec.y,vec.x);
retV.mag = Math.hypot(vec.x,vec.y);
return retV;
}
function polar (mag = 1, dir = 0) { return validatePolar({dir : dir, mag : mag}); } // create a polar vector
function vector (x= 1, y= 0) { return {x: x, y: y}; } // create a cartesian vector
function isPolar (vec) { if(vec.mag !== undefined && vec.dir !== undefined) { return true; } return false; }// returns true if polar
function isCart (vec) { if(vec.x !== undefined && vec.y !== undefined) { return true; } return false; }// returns true if cartesian
// copy and converts an unknown vec to polar if not already
function asPolar(vec){
if(isCart(vec)){ return cartToPolar(vec); }
if(vec.mag < 0){
vec.mag = - vec.mag;
vec.dir += PI;
}
return { dir : vec.dir, mag : vec.mag };
}
// copy and converts an unknown vec to cart if not already
function asCart(vec){
if(isPolar(vec)){ return polarToCart(vec); }
return { x : vec.x, y : vec.y};
}
// normalise makes a vector a unit length and returns it as a cartesian
function normalise(vec){
var vp = asPolar(vec);
vap.mag = 1;
return asCart(vp);
}
function vectorAdd(vec1, vec2){
var v1 = asCart(vec1);
var v2 = asCart(vec2);
return vector(v1.x + v2.x, v1.y + v2.y);
}
// This splits the vector (polar or cartesian) into the components along dir and the tangent to that dir
function vectorComponentsForDir(vec,dir){
var v = asPolar(vec); // as polar
var pheta = v.dir - dir;
var Fv = Math.cos(pheta) * v.mag;
var Fa = Math.sin(pheta) * v.mag;
var d1 = dir;
var d2 = dir + PI90;
if(Fv < 0){
d1 += PI;
Fv = -Fv;
}
if(Fa < 0){
d2 += PI;
Fa = -Fa;
}
return {
along : polar(Fv,d1),
tangent : polar(Fa,d2)
};
}
function doCollision(pointDetails, wallIndex){
var vv = asPolar(pointDetails.velocity); // Cartesian V make sure the velocity is in cartesian form
var va = asPolar(pointDetails.velocityA); // Angular V make sure the velocity is in cartesian form
var vvc = vectorComponentsForDir(vv, WALL_NORMS[wallIndex])
var vac = vectorComponentsForDir(va, WALL_NORMS[wallIndex])
vvc.along.mag *= 1.18; // Elastic collision requiers that the two equal forces from the wall
vac.along.mag *= 1.18; // against the box and the box against the wall be summed.
// As the wall can not move the result is that the force is twice
// the force the box applies to the wall (Yes and currently force is in
// velocity form untill the next line)
vvc.along.mag *= box.mass; // convert to force
//vac.along.mag/= pointDetails.radius
vac.along.mag *= box.mass
vvc.along.dir += PI; // force is in the oppisite direction so turn it 180
vac.along.dir += PI; // force is in the oppisite direction so turn it 180
// split the force into components based on the wall normal. One along the norm the
// other along the wall
vvc.tangent.mag *= 0.18; // add friction along the wall
vac.tangent.mag *= 0.18;
vvc.tangent.mag *= box.mass //
vac.tangent.mag *= box.mass
vvc.tangent.dir += PI; // force is in the oppisite direction so turn it 180
vac.tangent.dir += PI; // force is in the oppisite direction so turn it 180
// apply the force out from the wall
box.applyForce(vvc.along, pointDetails.pos)
// apply the force along the wall
box.applyForce(vvc.tangent, pointDetails.pos)
// apply the force out from the wall
box.applyForce(vac.along, pointDetails.pos)
// apply the force along the wall
box.applyForce(vac.tangent, pointDetails.pos)
//addTempVec(pointDetails.pos, vvc.tangent, "red", LIFE, 10)
//addTempVec(pointDetails.pos, vac.tangent, "red", LIFE, 10)
}
function applyForce(force, loc){ // force is a vector, loc is a coordinate
validatePolar(force); // make sure the force is a valid polar
// addTempVec(loc, force,"White", LIFE, SCALE_FORCE) // show the force
var l = asCart(loc); // make sure the location is in cartesian form
var toCenter = asPolar(vector(this.x - l.x, this.y - l.y));
var pheta = toCenter.dir - force.dir;
var Fv = Math.cos(pheta) * force.mag;
var Fa = Math.sin(pheta) * force.mag;
var accel = asPolar(toCenter); // copy the direction to center
accel.mag = Fv / this.mass; // now use F = m * a in the form a = F/m
var deltaV = asCart(accel); // convert it to cartesian
this.dx += deltaV.x // update the box delta V
this.dy += deltaV.y
var accelA = Fa / (toCenter.mag * this.mass); // for the angular component get the rotation
// acceleration
this.dr += accelA;// now add that to the box delta r
}
// make a box
ctx.globalAlpha = 1;
var lx,ly;
function update(){
// clearLog();
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.lineWidth = 1;
ctx.strokeStyle = "black";
ctx.fillStyle = "#888";
ctx.fillRect(INSET, INSET, canvas.width - INSET * 2, canvas.height - INSET * 2);
ctx.strokeRect(INSET, INSET, canvas.width - INSET * 2, canvas.height - INSET * 2);
ctx.lineWidth = 2;
ctx.strokeStyle = "black";
box.update();
box.draw();
if(mouse.buttonRaw & 1){
var force = asPolar(vector(mouse.x - lx, mouse.y - ly));
force.mag *= box.mass * 0.1;
box.applyForce(force,vector(mouse.x, mouse.y))
addTempVec(vector(mouse.x, mouse.y), asPolar(vector(mouse.x - lx, mouse.y - ly)), "Cyan", LIFE, 5);
}
lx = mouse.x;
ly = mouse.y;
for(i = 0; i < 4; i++){
var p = box.getPoint(i);
// only do one collision per frame or we will end up adding energy
if(p.pos.x < INSET){
box.x += (INSET) - p.pos.x;
doCollision(p,3)
}else
if( p.pos.x > canvas.width-INSET){
box.x += (canvas.width - INSET) - p.pos.x;
doCollision(p,1)
}else
if(p.pos.y < INSET){
box.y += (INSET) -p.pos.y;
doCollision(p,0)
}else
if( p.pos.y > canvas.height-INSET){
box.y += (canvas.height - INSET) -p.pos.y;
doCollision(p,2)
}
drawVec(p.pos,p.velocity,"blue")
}
drawTempVecs();
ctx.globalAlpha = 1;
drawText(box.getDesc(),canvas.width/2,FONT_SIZE,FONT,FONT_SIZE,"black");
drawText("Click drag to apply force to box",canvas.width/2,FONT_SIZE +17,FONT,14,"black");
requestAnimationFrame(update)
}
update();
I have an arc which is rather large in size with a stroke that uses rgba values. It has a 50% alpha value and because of that, it is causing a big hit on my cpu profile for my browser.
So i want to find a way to optimize this so that where ever the arc is drawn in a canvas, it will only draw from one angle to another of which is visible on screen.
What i am having difficulty with, is working out the correct angle range.
Here is a visual example:
The top image is what the canvas actually does even if you don't see it, and the bottom one is what I am trying to do to save processing time.
I created a JSFiddle where you can click and drag the circle, though, the two angles are currently fixed:
https://jsfiddle.net/44tawd81/
Here is the draw code:
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
ctx.strokeStyle = 'red';
var radius = 50;
var pos = {
'x': canvas.width - 20,
'y': canvas.height /2
};
function draw(){
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.beginPath();
ctx.arc(pos.x,pos.y,radius,0,2*Math.PI); //need to adjust angle range
ctx.stroke();
requestAnimationFrame(draw);
}
draw();
What is the simplest way to find the angle range to draw based on it's position and size in a canvas?
Clipping a Circle
This is how to clip a circle to a rectangular region aligned to the x and y axis.
To clip the circle I search for the list of points where the circle intersects the clipping region. Starting from one side I go in a clockwise direction adding clip points as they are found. When all 4 sides are tested I then draw the arc segments that join the points found.
To find if a point has intercepted a clipping edge you find the distance the circle center is from that edge. Knowing the radius and the distance you can complete the right triangle to find the coordinates of the intercept.
For the left edge
// define the clip edge and circle
var clipLeftX = 100;
var radius = 200;
var centerX = 200;
var centerY = 200;
var dist = centerX - clipLeftX;
if(dist > radius) { // circle inside }
if(dist < -radius) {// circle completely outside}
// we now know the circle is clipped
Now calculate the distance from the circle y that the two clip points will be
// the right triangle with hypotenuse and one side know can be solved with
var clipDist = Math.sqrt(radius * radius - dist * dist);
So the points where the circle intercept the clipping line
var clipPointY1 = centerY - clipDist;
var clipPointY2 = centerY + clipDist;
With that you can work out if the two points are inside or outside the left side top or bottom by testing the two points against the top and bottom of the left line.
You will end up with either 0,1 or 2 clipping points.
Because arc requires angles to draw you need to calculate the angle from the circle center to the found points. You already have all the info needed
// dist is the x distance from the clip
var angle = Math.acos(radius/dist); // for left and right side
The hard part is making sure all the angles to the clipping point are in the correct order. The is a little fiddling about with flags to ensure that the arcs are in the correct order.
After checking all four sides you will end up with 0,2,4,6, or 8 clipping points representing the start and ends of the various clipped arcs. It is then simply iterating the arc segments and rendering them.
// Helper functions are not part of the answer
var canvas;
var ctx;
var mouse;
var resize = function(){
/** fullScreenCanvas.js begin **/
canvas = (function(){
var canvas = document.getElementById("canv");
if(canvas !== null){
document.body.removeChild(canvas);
}
// creates a blank image with 2d context
canvas = document.createElement("canvas");
canvas.id = "canv";
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style.position = "absolute";
canvas.style.top = "0px";
canvas.style.left = "0px";
canvas.style.zIndex = 1000;
canvas.ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
return canvas;
})();
ctx = canvas.ctx;
/** fullScreenCanvas.js end **/
/** MouseFull.js begin **/
var canvasMouseCallBack = undefined; // if needed
mouse = (function(){
var mouse = {
x : 0, y : 0, w : 0, alt : false, shift : false, ctrl : false,
interfaceId : 0, buttonLastRaw : 0, buttonRaw : 0,
over : false, // mouse is over the element
bm : [1, 2, 4, 6, 5, 3], // masks for setting and clearing button raw bits;
getInterfaceId : function () { return this.interfaceId++; }, // For UI functions
startMouse:undefined,
};
function mouseMove(e) {
var t = e.type, m = mouse;
m.x = e.offsetX; m.y = e.offsetY;
if (m.x === undefined) { m.x = e.clientX; m.y = e.clientY; }
m.alt = e.altKey;m.shift = e.shiftKey;m.ctrl = e.ctrlKey;
if (t === "mousedown") { m.buttonRaw |= m.bm[e.which-1];
} else if (t === "mouseup") { m.buttonRaw &= m.bm[e.which + 2];
} else if (t === "mouseout") { m.buttonRaw = 0; m.over = false;
} else if (t === "mouseover") { m.over = true;
} else if (t === "mousewheel") { m.w = e.wheelDelta;
} else if (t === "DOMMouseScroll") { m.w = -e.detail;}
if (canvasMouseCallBack) { canvasMouseCallBack(m.x, m.y); }
e.preventDefault();
}
function startMouse(element){
if(element === undefined){
element = document;
}
"mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",").forEach(
function(n){element.addEventListener(n, mouseMove);});
element.addEventListener("contextmenu", function (e) {e.preventDefault();}, false);
}
mouse.mouseStart = startMouse;
return mouse;
})();
if(typeof canvas === "undefined"){
mouse.mouseStart(canvas);
}else{
mouse.mouseStart();
}
}
/** MouseFull.js end **/
resize();
// Answer starts here
var w = canvas.width;
var h = canvas.height;
var d = Math.sqrt(w * w + h * h); // diagnal size
var cirLWidth = d * (1 / 100);
var rectCol = "black";
var rectLWidth = d * (1 / 100);
const PI2 = Math.PI * 2;
const D45_LEN = 0.70710678;
var angles = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; // declared outside to stop GC
// create a clipArea
function rectArea(x, y, x1, y1) {
return {
left : x,
top : y,
width : x1 - x,
height : y1 - y
};
}
// create a arc
function arc(x, y, radius, start, end, col) {
return {
x : x,
y : y,
r : radius,
s : start,
e : end,
c : col
};
}
// draws an arc
function drawArc(arc, dir) {
ctx.strokeStyle = arc.c;
ctx.lineWidth = cirLWidth;
ctx.beginPath();
ctx.arc(arc.x, arc.y, arc.r, arc.s, arc.e, dir);
ctx.stroke();
}
// draws a clip area
function drawRect(r) {
ctx.strokeStyle = rectCol;
ctx.lineWidth = rectLWidth;
ctx.strokeRect(r.left, r.top, r.width, r.height);
}
// clip and draw an arc
// arc is the arc to clip
// clip is the clip area
function clipArc(arc, clip){
var count, distTop, distLeft, distBot, distRight, dist, swap, radSq, bot,right;
// cir1 is used to draw the clipped circle
cir1.x = arc.x;
cir1.y = arc.y;
count = 0; // number of clip points found;
bot = clip.top + clip.height; // no point adding these two over and over
right = clip.left + clip.width;
// get distance from all edges
distTop = arc.y - clip.top;
distBot = bot - arc.y;
distLeft = arc.x - clip.left;
distRight = right - arc.x;
radSq = arc.r * arc.r; // get the radius squared
// check if outside
if(Math.min(distTop, distBot, distRight, distLeft) < -arc.r){
return; // nothing to see so go home
}
// check inside
if(Math.min(distTop, distBot, distRight, distLeft) > arc.r){
drawArc(cir1);
return;
}
swap = true;
if(distLeft < arc.r){
// get the distance up and down to clip
dist = Math.sqrt(radSq - distLeft * distLeft);
// check the point is in the clip area
if(dist + arc.y < bot && arc.y + dist > clip.top){
// get the angel
angles[count] = Math.acos(distLeft / -arc.r);
count += 1;
}
if(arc.y - dist < bot && arc.y - dist > clip.top){
angles[count] = PI2 - Math.acos(distLeft / -arc.r); // get the angle
if(count === 0){ // if first point then set direction swap
swap = false;
}
count += 1;
}
}
if(distTop < arc.r){
dist = Math.sqrt(radSq - distTop * distTop);
if(arc.x - dist < right && arc.x - dist > clip.left){
angles[count] = Math.PI + Math.asin(distTop / arc.r);
count += 1;
}
if(arc.x+dist < right && arc.x+dist > clip.left){
angles[count] = PI2-Math.asin(distTop/arc.r);
if(count === 0){
swap = false;
}
count += 1;
}
}
if(distRight < arc.r){
dist = Math.sqrt(radSq - distRight * distRight);
if(arc.y - dist < bot && arc.y - dist > clip.top){
angles[count] = PI2 - Math.acos(distRight / arc.r);
count += 1;
}
if(dist + arc.y < bot && arc.y + dist > clip.top){
angles[count] = Math.acos(distRight / arc.r);
if(count === 0){
swap = false;
}
count += 1;
}
}
if(distBot < arc.r){
dist = Math.sqrt(radSq - distBot * distBot);
if(arc.x + dist < right && arc.x + dist > clip.left){
angles[count] = Math.asin(distBot / arc.r);
count += 1;
}
if(arc.x - dist < right && arc.x - dist > clip.left){
angles[count] = Math.PI + Math.asin(distBot / -arc.r);
if(count === 0){
swap = false;
}
count += 1;
}
}
// now draw all the arc segments
if(count === 0){
return;
}
if(count === 2){
cir1.s = angles[0];
cir1.e = angles[1];
drawArc(cir1,swap);
}else
if(count === 4){
if(swap){
cir1.s = angles[1];
cir1.e = angles[2];
drawArc(cir1);
cir1.s = angles[3];
cir1.e = angles[0];
drawArc(cir1);
}else{
cir1.s = angles[2];
cir1.e = angles[3];
drawArc(cir1);
cir1.s = angles[0];
cir1.e = angles[1];
drawArc(cir1);
}
}else
if(count === 6){
cir1.s = angles[1];
cir1.e = angles[2];
drawArc(cir1);
cir1.s = angles[3];
cir1.e = angles[4];
drawArc(cir1);
cir1.s = angles[5];
cir1.e = angles[0];
drawArc(cir1);
}else
if(count === 8){
cir1.s = angles[1];
cir1.e = angles[2];
drawArc(cir1);
cir1.s = angles[3];
cir1.e = angles[4];
drawArc(cir1);
cir1.s = angles[5];
cir1.e = angles[6];
drawArc(cir1);
cir1.s = angles[7];
cir1.e = angles[0];
drawArc(cir1);
}
return;
}
var rect = rectArea(50, 50, w - 50, h - 50);
var circle = arc(w * (1 / 2), h * (1 / 2), w * (1 / 5), 0, Math.PI * 2, "#AAA");
var cir1 = arc(w * (1 / 2), h * (1 / 2), w * (1 / 5), 0, Math.PI * 2, "red");
var counter = 0;
var countStep = 0.03;
function update() {
var x, y;
ctx.clearRect(0, 0, w, h);
circle.x = mouse.x;
circle.y = mouse.y;
drawArc(circle, "#888"); // draw unclipped arc
x = Math.cos(counter * 0.1);
y = Math.sin(counter * 0.3);
rect.top = h / 2 - Math.abs(y * (h * 0.4)) - 5;
rect.left = w / 2 - Math.abs(x * (w * 0.4)) - 5;
rect.width = Math.abs(x * w * 0.8) + 10;
rect.height = Math.abs(y * h * 0.8) + 10;
cir1.col = "RED";
clipArc(circle, rect); // draw the clipped arc
drawRect(rect); // draw the clip area. To find out why this method
// sucks move this to before drawing the clipped arc.
requestAnimationFrame(update);
if(mouse.buttonRaw !== 1){
counter += countStep;
}
ctx.font = Math.floor(w * (1 / 50)) + "px verdana";
ctx.fillStyle = "white";
ctx.strokeStyle = "black";
ctx.lineWidth = Math.ceil(w * (1 / 300));
ctx.textAlign = "center";
ctx.lineJoin = "round";
ctx.strokeText("Left click and hold to pause", w/ 2, w * (1 / 40));
ctx.fillText("Left click and hold to pause", w/ 2, w * (1 / 40));
}
update();
window.addEventListener("resize",function(){
resize();
w = canvas.width;
h = canvas.height;
rect = rectArea(50, 50, w - 50, h - 50);
circle = arc(w * (1 / 2), h * (1 / 2), w * (1 / 5), 0, Math.PI * 2, "#AAA");
cir1 = arc(w * (1 / 2), h * (1 / 2), w * (1 / 5), 0, Math.PI * 2, "red");
});
The quickest way to clip a circle.
That is the quickest I could manage to do it in code. There is some room for optimization but not that much in the agorithum.
The best solution is of course to use the canvas 2D context API clip() method.
ctx.save();
ctx.rect(10,10,200,200); // define the clip region
ctx.clip(); // activate the clip.
//draw your circles
ctx.restore(); // remove the clip.
This is much quicker than the method I showed above and should be used unless you have a real need to know the clip points and arcs segments that are inside or outside the clip region.
To find the angle to draw based on circle position, canvas position, circle size, and canvas size:
Determine intersection of circle and canvas
Calculate points on the circle at which intersection occurs
You then have an isosceles triangle.
You can use cosine formula for calculation of the angle.
c^2=a^2+b^2−2abcos(α)
a and b are sides adjacent to the angle α, which are the radius of the center r. c is the distance between the two points P1 and P2. So we get:
|P1−P2|^2=2r^2−2r^2cos(α)
2r^2−|P1−P2|^2/2r2=cos(α)
α=cos−1(2r^2−|P1−P2|^2/2r^2)