js过圆外一点的直线与圆相切的切点坐标计算
2020-04-19 本文已影响0人
CODERLIHAO
由圆外一点P1(x1,y1)向圆(x - a)2 + (y - b)2 = R2作切线,切线与圆相切的切点是P0(x0,y0)

方法一: 公式法
先求直线P0P1直线方程
因为P0P1⊥OP0
向量OP0 = (x0 - a,y0 - b )
向量P0P1 = (x1 - x0, y1 - y0)
所以 向量P0P1 点乘 向量OP0 = 0
所以 (x0 - a)* (x1 - x0) + (y0 - b)(y1 - y0) = 0
并且 因为点P0是圆上的点,=> (x0 - a)2 + (y0 - b)2 = R2
解方程就可以求出P0(x0,y0)坐标,当然这样求法太复杂,我没有耐心解下去了,我们换个思维求我们把圆换成这样,相当于把整个系统做了一次相对位移,移动到0点
x2 + y2 = R2

那么经过点P0(x0,y0)这个圆的的切线方程就是
x0x1 + y0y1 = R2
并且x20 + y20 = R2
所以
(x21 + y21)x20 -2R2x1x0 + R4 - y21R2 = 0
这样就是求解一元二次方程
m = (x21 + y21) / R2
x0 = (-b±√(b2 - 4ac)) / (2a) = (x1 ± y1√(m-1)) / m
y0 = (y1 ± x1√(m-1)) / m
至此切点是找到了,
然后再位移下就是真正的点了

let canvas = document.getElementById("target");
canvas.width = document.body.clientWidth;
canvas.height = document.body.clientHeight;
let ctx = canvas.getContext("2d");
let isDrawing = false;
canvas.addEventListener('mousedown', e => {
let x = e.offsetX;
let y = e.offsetY;
isDrawing = true;
pointP = {x:x,y:y};
update();
});
canvas.addEventListener('mousemove', e => {
if (isDrawing === true) {
let x = e.offsetX;
let y = e.offsetY;
pointP = {x:x,y:y};
update();
}
});
canvas.addEventListener('mouseup', e => {
if (isDrawing === true) {
isDrawing = false;
}
});
let pointP = {x: 300,y: 100};
//圆心坐标
let pointCenter ={x:250,y:250};
//圆点半径
let radius = 100;
function update(){
ctx.clearRect(0,0,canvas.width,canvas.height);
//圆心
drawPoint(pointCenter.x,pointCenter.y,"#986923");
//圆
ctx.beginPath();
ctx.arc(pointCenter.x,pointCenter.y,radius,0,2*Math.PI,true);
ctx.stroke();
ctx.closePath();
drawPoint(pointP.x,pointP.y,"#986923");
let arrayQieDian = calcQieDian(pointCenter.x,pointCenter.y,radius,pointP);
arrayQieDian.forEach((point)=>{
drawPoint(point.x,point.y,"#f00");
drawLine(pointP,point);
drawLine(pointCenter,point);
});
}
update();
function drawPoint(x,y,color){
ctx.beginPath();
ctx.fillStyle=color;
ctx.arc(x,y,5,0,2*Math.PI,true);
ctx.fill();
}
function drawLine(pointStart,pointEnd) {
ctx.beginPath();
ctx.moveTo(pointStart.x,pointStart.y);
ctx.lineTo(pointEnd.x,pointEnd.y);
ctx.stroke();
}
function calcQieDian(cx,cy,radius,point) {
//将实际的点做一次转换,因为下面的计算都是暗转圆心都是在圆点计算的
let outsideX = point.x - cx;
let outsideY = point.y - cy;
let m = (Math.pow(outsideX,2)+Math.pow(outsideY,2))/Math.pow(radius,2);
//求出的结果将会有4种排列
let pointA = {
x:((outsideX+outsideY*Math.sqrt(m-1))/m),
y:((outsideY+outsideX*Math.sqrt(m-1))/m)
};
let pointB = {
x:((outsideX-outsideY*Math.sqrt(m-1))/m),
y:((outsideY-outsideX*Math.sqrt(m-1))/m)
};
let pointC = {
x:((outsideX+outsideY*Math.sqrt(m-1))/m),
y:((outsideY-outsideX*Math.sqrt(m-1))/m)
};
let pointD = {
x:((outsideX-outsideY*Math.sqrt(m-1))/m),
y:((outsideY+outsideX*Math.sqrt(m-1))/m)
};
let array = [];
//实际上只会有2个切点,利用向量垂直,点乘结果是0来判断哪个是有效的
//因为浮点数不能精确到0,所以这里用了1e-10
if(Math.abs(pointA.x*(outsideX -pointA.x ) + pointA.y * (outsideY - pointA.y)) <= 1e-10){
// 再将坐标转换回来
pointA.x += cx;
pointA.y += cy;
array.push(pointA);
}
if(Math.abs(pointB.x*(outsideX -pointB.x ) + pointB.y * (outsideY - pointB.y)) <= 1e-10){
pointB.x += cx;
pointB.y += cy;
array.push(pointB);
}
if(Math.abs(pointC.x*(outsideX -pointC.x ) + pointC.y * (outsideY - pointC.y)) <= 1e-10){
pointC.x += cx;
pointC.y += cy;
array.push(pointC);
}
if(Math.abs(pointD.x*(outsideX -pointD.x ) + pointD.y * (outsideY - pointD.y)) <= 1e-10){
pointD.x += cx;
pointD.y += cy;
array.push(pointD);
}
return array;
}
方法二:向量法

思路:只要知道P1点与水平线的角度就可以计算坐标,先计算OM与水平线的角度,角P1OM的余弦值就是P1O / OM
P1O 的长度就是半径r
let canvas = document.getElementById("target");
canvas.width = document.body.clientWidth;
canvas.height = document.body.clientHeight;
let ctx = canvas.getContext("2d");
let isDrawing = false;
let eventCircle = false;
canvas.addEventListener('mousedown', e => {
let x = e.offsetX;
let y = e.offsetY;
isDrawing = true;
if(Math.pow(x - pointCenter.x,2) + Math.pow(y - pointCenter.y,2) <= radius*radius){
eventCircle = true;
pointCenter.x = x;
pointCenter.y = y;
}else{
pointP.x = x;
pointP.y = y;
}
update();
});
canvas.addEventListener('mousemove', e => {
if (isDrawing === true) {
let x = e.offsetX;
let y = e.offsetY;
if(eventCircle){
pointCenter.x = x;
pointCenter.y = y;
}else{
pointP.x = x;
pointP.y = y;
}
update();
}
});
canvas.addEventListener('mouseup', e => {
if (isDrawing === true) {
isDrawing = false;
eventCircle = false;
}
});
let pointP = {x: 300,y: 100};
//圆心坐标
let pointCenter ={x:250,y:250};
//圆点半径
let radius = 100;
function update(){
ctx.clearRect(0,0,canvas.width,canvas.height);
//圆心
drawPoint(pointCenter.x,pointCenter.y,"#986923");
//圆
ctx.beginPath();
ctx.arc(pointCenter.x,pointCenter.y,radius,0,2*Math.PI,true);
ctx.stroke();
ctx.closePath();
drawPoint(pointP.x,pointP.y,"#986923");
let p = calcQieDian2(pointCenter.x,pointCenter.y,radius,pointP);
drawText("P1",p.p1.x,p.p1.y);
drawText("P2",p.p2.x,p.p2.y);
drawText("M",pointP.x,pointP.y);
drawText("O",pointCenter.x,pointCenter.y);
drawPoint(p.p1.x,p.p1.y,"#f00");
drawPoint(p.p2.x,p.p2.y,"#f00");
drawLine(pointCenter,p.p1);
drawLine(pointCenter,p.p2);
drawLine(pointCenter,pointP);
drawLine(pointP,p.p1);
drawLine(pointP,p.p2);
drawLine({x:0,y:pointCenter.y},{x:canvas.width,y:pointCenter.y});
}
update();
function drawPoint(x,y,color){
ctx.beginPath();
ctx.fillStyle=color;
ctx.arc(x,y,5,0,2*Math.PI,true);
ctx.fill();
}
function drawLine(pointStart,pointEnd) {
ctx.beginPath();
ctx.moveTo(pointStart.x,pointStart.y);
ctx.lineTo(pointEnd.x,pointEnd.y);
ctx.stroke();
}
function calcQieDian2(cx,cy,radius,point) {
//点到圆心的距离
let d = Math.sqrt(Math.pow(cx - point.x,2) + Math.pow(cy - point.y,2));
let vc1c2 = {x:point.x - cx,y:-point.y + cy}; //屏幕坐标系与笛卡尔坐标系是y轴是反着的
let radC1C2 = Math.acos(vc1c2.x / Math.sqrt(Math.pow(vc1c2.x,2) + Math.pow(vc1c2.y,2)));
let theta = Math.acos(radius/d);
if(point.y < cy){
let p1 = {x:cx + Math.cos(theta + radC1C2)*radius,y:cy - Math.sin(theta + radC1C2)*radius};
let p2 = {x:cx + Math.cos(theta - radC1C2)*radius,y:cy + Math.sin(theta - radC1C2)*radius};
return {p1:p1,p2:p2};
}else{
radC1C2 = Math.PI - radC1C2;
let p1 = {x:cx + Math.cos(Math.PI - theta - radC1C2)*radius,y:cy + Math.sin(Math.PI - theta - radC1C2)*radius};
let p2 = {x:cx + Math.cos(Math.PI - (theta - radC1C2))*radius,y:cy - Math.sin(Math.PI - (theta - radC1C2))*radius};
return {p1:p1,p2:p2};
}
}
function drawText(text,x,y) {
ctx.beginPath();
ctx.font="40px Arial";
ctx.fillText(text,x,y);
ctx.stroke();
}

方法三 atan2
利用atan2计算,返回(-pi,pi]之间的值,从x轴正方向逆时针旋转到点(x,y)时经过的角度

function angle(p1,p2) {
return Math.atan2(p1.y - p2.y, p1.x - p2.x);
}
function getVector(cx, cy, a, r) {
return {x:cx + r * Math.cos(a), y:cy + r * Math.sin(a)};
}
function calcQieDian3(pointCircle,radius,point) {
let d = Math.sqrt(Math.pow(pointCircle.x - point.x,2) + Math.pow(pointCircle.y - point.y,2));
const angleBetweenCenters = angle(point,pointCircle);
const spread = Math.acos(radius/ d);
const angle1 = angleBetweenCenters + spread;
const angle2 = angleBetweenCenters - spread;
const p1 = getVector(pointCircle.x,pointCircle.y, angle1, radius);
const p2 = getVector(pointCircle.x,pointCircle.y, angle2, radius);
return {p1:p1,p2:p2};
}
完整代码
let canvas = document.getElementById("target");
canvas.width = document.body.clientWidth;
canvas.height = document.body.clientHeight;
let ctx = canvas.getContext("2d");
let isDrawing = false;
let eventCircle = false;
canvas.addEventListener('mousedown', e => {
let x = e.offsetX;
let y = e.offsetY;
isDrawing = true;
if(Math.pow(x - pointCenter.x,2) + Math.pow(y - pointCenter.y,2) <= radius*radius){
eventCircle = true;
pointCenter.x = x;
pointCenter.y = y;
}else{
pointP.x = x;
pointP.y = y;
}
update();
});
canvas.addEventListener('mousemove', e => {
if (isDrawing === true) {
let x = e.offsetX;
let y = e.offsetY;
if(eventCircle){
pointCenter.x = x;
pointCenter.y = y;
}else{
pointP.x = x;
pointP.y = y;
}
update();
}
});
canvas.addEventListener('mouseup', e => {
if (isDrawing === true) {
isDrawing = false;
eventCircle = false;
}
});
let pointP = {x: 300,y: 100};
//圆心坐标
let pointCenter ={x:250,y:250};
//圆点半径
let radius = 100;
function angle(p1,p2) {
return Math.atan2(p1.y - p2.y, p1.x - p2.x);
}
function getVector(cx, cy, a, r) {
return {x:cx + r * Math.cos(a), y:cy + r * Math.sin(a)};
}
function update(){
ctx.clearRect(0,0,canvas.width,canvas.height);
//圆心
drawPoint(pointCenter.x,pointCenter.y,"#986923");
//圆
ctx.beginPath();
ctx.arc(pointCenter.x,pointCenter.y,radius,0,2*Math.PI,true);
ctx.stroke();
ctx.closePath();
drawPoint(pointP.x,pointP.y,"#986923");
let p = calcQieDian3(pointCenter,radius,pointP);
drawText("P1",p.p1.x,p.p1.y);
drawText("P2",p.p2.x,p.p2.y);
drawText("M",pointP.x,pointP.y);
drawText("O",pointCenter.x,pointCenter.y);
drawPoint(p.p1.x,p.p1.y,"#f00");
drawPoint(p.p2.x,p.p2.y,"#f00");
drawLine(pointCenter,p.p1);
drawLine(pointCenter,p.p2);
drawLine(pointCenter,pointP);
drawLine(pointP,p.p1);
drawLine(pointP,p.p2);
drawLine({x:0,y:pointCenter.y},{x:canvas.width,y:pointCenter.y});
}
update();
function drawPoint(x,y,color){
ctx.beginPath();
ctx.fillStyle=color;
ctx.arc(x,y,5,0,2*Math.PI,true);
ctx.fill();
}
function drawLine(pointStart,pointEnd) {
ctx.beginPath();
ctx.moveTo(pointStart.x,pointStart.y);
ctx.lineTo(pointEnd.x,pointEnd.y);
ctx.stroke();
}
function calcQieDian3(pointCircle,radius,point) {
let d = Math.sqrt(Math.pow(pointCircle.x - point.x,2) + Math.pow(pointCircle.y - point.y,2));
const angleBetweenCenters = angle(point,pointCircle);
const spread = Math.acos(radius/ d);
const angle1 = angleBetweenCenters + spread;
const angle2 = angleBetweenCenters - spread;
const p1 = getVector(pointCircle.x,pointCircle.y, angle1, radius);
const p2 = getVector(pointCircle.x,pointCircle.y, angle2, radius);
return {p1:p1,p2:p2};
}
function drawText(text,x,y) {
ctx.beginPath();
ctx.font="30px Arial";
ctx.fillText(text,x,y);
ctx.stroke();
}