写在最前
近期使用Canvas实现一款关于"元球融合"的效果,本想是用 threejs 蹭一波跨窗口球体融合特效的热度,但发现已经为时已晚了。索性就用Canvas实现元球融合效果吧,冲!
原理
原理非常非常简单,只需求出这4个点的位置然后画出三阶贝塞尔曲线就行。
步骤
- 初始化画布
- 画圆
- 获取圆上点P1坐标
- 获取圆过点P1切线上的一点P3
- 画贝塞尔曲线
初始化画布
js
let c1 = { x: 300, y: 430, radius: 80 };
let c2 = { x: 0, y: 0, radius: 80 };
let canvas = document.querySelector("#canvas") as HTMLCanvasElement;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
context = canvas.getContext("2d")!;
setCanvasBg();
js
/**
* 设置背景色
*/
function setCanvasBg() {
context.fillStyle = "#000000";
context.fillRect(0, 0, canvas.width, canvas.height);
}
画圆
封装画圆函数
js
/**
* 画圆
* @param x
* @param y
* @param radius
*/
function drawCircle(x, y, radius) {
context.save();
context.beginPath();
context.arc(x, y, radius, 0, 2 * Math.PI);
context.closePath();
context.stroke();
context.fillStyle = "#FFFFFF";
context.fill();
context.restore();
}
我们写一个draw函数用于绘制
js
function draw(){
// 画圆
drawCircle(c1.x, c1.y, c1.radius);
drawCircle(c2.x, c2.y, c2.radius);
// 画贝塞尔曲线
}
获取圆上坐标
想要得到较为丝滑的效果,圆上点坐标与切线点坐标是需要可变化的。
我们可以看到当小球在同一水平上,两小球距离越远,在圆上的点是会稍微往两球中间挪动一段距离。
所以我们需要根据距离去设置一个最大高度的比例变量
点会在Max和MIN的方向上移动
并且两个圆上的点会根据两球位置进行一个角度的旋转,这个角度就是两球圆心的角度
所以我们将两球圆心的坐标相减,使用 Math.atan2 API可以求出角度
所以我们很容易得到以下获取圆上坐标函数
let radian = Math.atan2(c2.y - c1.y, c2.x - c1.x);
js
/**
* 获取圆上坐标
* @param {*} maxHeightPercent
*/
function getCirclePoint(circle, maxHeightPercent, isReverse, radian) {
let { x: cx, y: cy, radius } = circle;
let thetaInRadians = Math.asin(maxHeightPercent);
let x = Math.cos(thetaInRadians + radian) * radius;
let y = Math.sin(thetaInRadians + radian) * radius;
return {
x: cx + (isReverse ? -x : x),
y: cy + y,
};
}
这样线上点就求出来了
获取圆过点切线上的坐标
设定切线上坐标点为T,只需要求出 线段PD与线段DT的长度。
PT 斜边 是常量我们设置的,且 ∠PTD = ∠TVC ,∠TVC = 180 - PCV - CPV; 已知切线VP 与 CP夹角为90度;则∠PVC = 90 - ∠PCV; PCV依然可以用 Math.atan2 将P,C两点带入则可以求出。
所以我们可以很轻松的得出下面函数
js
/**
* 获取切线上的一点
*/
function getTangentPoi(center, point, isReverse) {
let distance = getCircleDistance();
let length = (isReverse ? -distance : distance) / 4;
const r = Math.atan2(point.y - center.y, point.x - center.x);
let angle = 90 + (r / Math.PI) * 180;
const radian = (angle / 180) * Math.PI;
let x = Math.cos(radian) * length;
let y = Math.sin(radian) * length;
return {
x: point.x + x,
y: point.y + y,
};
}
中间的4个点就是切线上的点
画贝塞尔曲线
canvas有贝塞尔曲线API,直接用就好了
js
/**
* canvas画贝塞尔曲线
*/
function drawBezierPath(maxHeightPercent) {
let radian = Math.atan2(c2.y - c1.y, c2.x - c1.x);
let p1Top = getCirclePoint(c1, -maxHeightPercent, false, radian);
let p1Bottom = getCirclePoint(c1, maxHeightPercent, false, radian);
let p2Top = getCirclePoint(c2, -maxHeightPercent, true, -radian);
let p2Bottom = getCirclePoint(c2, maxHeightPercent, true, -radian);
let c1tangentTop = getTangentPoi(getCircleCenterPoi(c1), p1Top, false);
let c1tangentBottom = getTangentPoi(getCircleCenterPoi(c1), p1Bottom, true);
let c2tangentTop = getTangentPoi(getCircleCenterPoi(c2), p2Top, true);
let c2tangentBottom = getTangentPoi(getCircleCenterPoi(c2), p2Bottom, false);
context.beginPath();
context.moveTo(p1Top.x, p1Top.y);
context.bezierCurveTo(
c1tangentTop.x,
c1tangentTop.y,
c2tangentTop.x,
c2tangentTop.y,
p2Top.x,
p2Top.y
);
context.lineTo(p2Bottom.x, p2Bottom.y);
context.bezierCurveTo(
c2tangentBottom.x,
c2tangentBottom.y,
c1tangentBottom.x,
c1tangentBottom.y,
p1Bottom.x,
p1Bottom.y
);
context.strokeStyle = "1";
context.lineTo(p1Top.x, p1Top.y);
context.closePath();
context.stroke();
context.fillStyle = "#FFFFFF";
context.fill();
}
至此 元球融合就算完成了
结尾
喜欢的话帮忙点个赞 + 关注吧,将持续更新 Threejs 相关的文章,谢谢浏览!
往期链接: