美文网首页
Metaball变形球效果实现

Metaball变形球效果实现

作者: CODERLIHAO | 来源:发表于2020-07-03 15:00 被阅读0次

效果图

Metaball

一 .求出圆的外公切线

image.png
两个圆心之间的距离,也就是两点之间的距离
const distanceBetweenCenter = Math.sqrt(Math.pow(c1.cx - c2.cx, 2) + Math.pow(c1.cy - c2.cy, 2));
计算直线C1C2与水平线的角度
const angleBetweenCenters = Math.atan2(c2.cy - c1.cy, c2.cx - c1.cx);
角P2C1C2可以根据公式求出
const spread = Math.acos((c1.radius - c2.radius) / distanceBetweenCenter);
那么就可以算出四个点的角度,注意,按照上面的图,angleBetweenCenters计算出来的是负数,spread是正数,
所以angle1就是p1点的角度,angle2就是p2点的角度,以此类推。
const angle1 = angleBetweenCenters + spread;
const angle2 = angleBetweenCenters - spread;
const angle3 = angleBetweenCenters + spread;
const angle4 = angleBetweenCenters - spread;

根据圆的半径与坐标,就可以求出切点坐标

 const p1 = getVector(c1.cx, c1.cy, angle1, c1.radius);
 const p2 = getVector(c1.cx, c1.cy, angle2, c1.radius);
 const p3 = getVector(c2.cx, c2.cy, angle3, c2.radius);
 const p4 = getVector(c2.cx, c2.cy, angle4, c2.radius);
function getVector(cx, cy, a, r) {
        return {x: cx + r * Math.cos(a), y: cy + r * Math.sin(a)};
    }

二.偏移切点位置

如果在这样的切点做变形球效果,肯定不好看,我们给出一个系数v=0.5,让切点稍微一点偏移,也就是向两圆直接偏移,注意,下面的代码和上面的角度计算不一样,不如 角P4C2C1 =Math.PI - spread,这个角度乘上v,是不是变得更小了,然后Math.PI - 角P4C2C1 也就变得更大了,这样P4就会向圆C1偏移了。

 const angle1 = angleBetweenCenters + spread * v;
 const angle2 = angleBetweenCenters - spread * v;
 const angle3 = angleBetweenCenters + Math.PI - (Math.PI - spread) * v;
 const angle4 = angleBetweenCenters - (Math.PI -(Math.PI - spread) * v);
image.png

计算控制点

这里需要用贝塞尔曲线了,而且还是三阶的,这就需要二个控制点


image.png

先计算两个圆圆心的距离totalRadius,想要随着圆的位置变化,控制点也需要变,给出d2比例参数
这里的HALF_PI是因为切点方向。

        const d2 = (dist(p1.x, p1.y, p3.x, p3.y) / totalRadius);
        const r1 = c1.radius * d2;
        const r2 = c2.radius * d2;
        const h1 = this.getVector(p1.x, p1.y, angle1 - HALF_PI, r1);
        const h2 = this.getVector(p2.x, p2.y, angle2 + HALF_PI, r1);
        const h3 = this.getVector(p3.x, p3.y, angle3 + HALF_PI, r2);
        const h4 = this.getVector(p4.x, p4.y, angle4 - HALF_PI, r2);

三.画曲线

现在我们有切点有控制点,可以画曲线了,

  ctx.beginPath();
  ctx.strokeStyle = c2.color;
  ctx.moveTo(p1.x, p1.y);
  ctx.arc(c1.cx, c1.cy, c1.radius, angle1, angle2, false);
  ctx.bezierCurveTo(h2.x, h2.y, h4.x, h4.y, p4.x, p4.y);
  ctx.arc(c2.cx, c2.cy, c2.radius, angle4, angle3, false);
  ctx.bezierCurveTo(h3.x, h3.y, h1.x, h1.y, p1.x, p1.y);
  ctx.stroke();
image.png

但是现在要是将两个圆拉远了,就会出现下面的问题,这个问题后面会解决


image.png

四.圆的重叠

我们先来解决下面这个问题


image.gif

当两个圆重叠时,随着圆重叠程度的增加,u1与u2的角度也会变得越来越大,如果这个时候,u1与u2参与
切点角度的运算,那么此时切点角度的变化就不会有那么大。


4169630-610cd26f3d437a00.png
       let u1 = 0;
       let u2 = 0;
       if (distanceBetweenCenter < totalRadius) {
           //余弦定理
           u1 = Math.acos((c1.radius * c1.radius + distanceBetweenCenter * distanceBetweenCenter - c2.radius * c2.radius) / (2 * c1.radius * distanceBetweenCenter));
           u2 = Math.acos((c2.radius * c2.radius + distanceBetweenCenter * distanceBetweenCenter - c1.radius * c1.radius) / (2 * c2.radius * distanceBetweenCenter));
       } else {
           u1 = 0;
           u2 = 0;
       }
       const angleBetweenCenters = Math.atan2(c2.cy - c1.cy, c2.cx - c1.cx);
       const spread = Math.acos((c1.radius - c2.radius) / distanceBetweenCenter);

       const angle1 = angleBetweenCenters + u1 + (spread - u1) * v;
       const angle2 = angleBetweenCenters - (u1 + (spread - u1) * v);
       const angle3 = angleBetweenCenters + Math.PI - u2 - (Math.PI - u2 - spread) * v;
       const angle4 = angleBetweenCenters - (Math.PI - u2 - (Math.PI - u2 - spread) * v);
4169630-8aa8612a555756aa.gif

最后来解决两个圆拉的太远的问题,只要超过某个距离,就可以不需要画了。

        const maxDist = c1.radius + c2.radius * 3.9;
        const distanceBetweenCenter = Math.sqrt(Math.pow(c1.cx - c2.cx, 2) + Math.pow(c1.cy - c2.cy, 2));
        if (distanceBetweenCenter >= maxDist || distanceBetweenCenter <= Math.abs(c1.radius - c2.radius)) {
            return;
        }

最后,贴出源码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style type="text/css">
        body, html {
            width: 100%;
            height: 100%;
        }
    </style>
</head>
<body>
<canvas style="width: 100%;height: 100%" id="meta-ball"></canvas>
<script type="application/javascript">
    const devicePixelRatio = window.devicePixelRatio || 1;
    let canvas = document.getElementById("meta-ball");
    canvas.width = document.body.clientWidth * devicePixelRatio;
    canvas.height = document.body.clientHeight * devicePixelRatio;
    let ctx = canvas.getContext("2d");
    const HALF_PI = Math.PI / 2;

    class Circle {

        constructor(cx, cy, radius, color) {
            this.cx = cx;
            this.cy = cy;
            this.radius = radius;
            this.color = color;
            this.dragging = false;
        }

        draw(ctx) {
            // ctx.fillStyle = this.color;
            // ctx.beginPath();
            // ctx.arc(this.cx, this.cy, this.radius, 0, 2 * Math.PI);
            // ctx.fill();

            ctx.strokeStyle = this.color;
            ctx.lineWidth = 2 * devicePixelRatio;
            ctx.beginPath();
            ctx.arc(this.cx, this.cy, this.radius, 0, 2 * Math.PI);
            ctx.stroke();

            ctx.fillStyle = this.color;
            ctx.beginPath();
            ctx.arc(this.cx, this.cy, 4 * devicePixelRatio, 0, 2 * Math.PI);
            ctx.fill();
        }

        move(x, y) {
            if (this.dragging) {
                this.cx = x;
                this.cy = y;
            }
        }
    }

    const c1 = new Circle(canvas.width * 0.4, canvas.height * 0.4, Math.min(canvas.width, canvas.height) * 0.2, '#432322');
    const c2 = new Circle(canvas.width * 0.7, canvas.height * 0.5, Math.min(canvas.width, canvas.height) * 0.1, '#432322');

    let isDrawing = false;

    canvas.addEventListener('mousedown', e => {
        isDrawing = true;
        let x = e.offsetX * devicePixelRatio;
        let y = e.offsetY * devicePixelRatio;
        if (Math.sqrt(Math.pow(x - c1.cx, 2) + Math.pow(y - c1.cy, 2)) <= c1.radius) {
            c1.dragging = true;
            c2.dragging = false;
        }

        if (Math.sqrt(Math.pow(x - c2.cx, 2) + Math.pow(y - c2.cy, 2)) <= c2.radius) {
            c1.dragging = false;
            c2.dragging = true;
        }
    });

    canvas.addEventListener('mousemove', e => {
        let x = e.offsetX * devicePixelRatio;
        let y = e.offsetY * devicePixelRatio;
        if (!isDrawing) {
            return
        }
        update(x, y);
    });
    canvas.addEventListener('mouseup', e => {
        isDrawing = false;
        c1.dragging = false;
        c2.dragging = false;
    });

    function update(x, y) {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        c1.move(x, y);
        c2.move(x, y);
        c1.draw(ctx);
        c2.draw(ctx);

        const maxDist = c1.radius + c2.radius * 3.9;
        const distanceBetweenCenter = Math.sqrt(Math.pow(c1.cx - c2.cx, 2) + Math.pow(c1.cy - c2.cy, 2));
        if (distanceBetweenCenter >= maxDist || distanceBetweenCenter <= Math.abs(c1.radius - c2.radius)) {
            return;
        }
        const totalRadius = c1.radius + c2.radius;
        const v = 0.5;
        let u1 = 0;
        let u2 = 0;
        if (distanceBetweenCenter < totalRadius) {
            //余弦定理
            u1 = Math.acos((c1.radius * c1.radius + distanceBetweenCenter * distanceBetweenCenter - c2.radius * c2.radius) / (2 * c1.radius * distanceBetweenCenter));
            u2 = Math.acos((c2.radius * c2.radius + distanceBetweenCenter * distanceBetweenCenter - c1.radius * c1.radius) / (2 * c2.radius * distanceBetweenCenter));
        } else {
            u1 = 0;
            u2 = 0;
        }
        const angleBetweenCenters = Math.atan2(c2.cy - c1.cy, c2.cx - c1.cx);
        const spread = Math.acos((c1.radius - c2.radius) / distanceBetweenCenter);

        const angle1 = angleBetweenCenters + u1 + (spread - u1) * v;
        const angle2 = angleBetweenCenters - (u1 + (spread - u1) * v);
        const angle3 = angleBetweenCenters + Math.PI - u2 - (Math.PI - u2 - spread) * v;
        const angle4 = angleBetweenCenters - (Math.PI - u2 - (Math.PI - u2 - spread) * v);

        const p1 = getVector(c1.cx, c1.cy, angle1, c1.radius);
        const p2 = getVector(c1.cx, c1.cy, angle2, c1.radius);
        const p3 = getVector(c2.cx, c2.cy, angle3, c2.radius);
        const p4 = getVector(c2.cx, c2.cy, angle4, c2.radius);

        // drawLine(c1.cx, c1.cy, p1.x, p1.y, c1.color);
        // drawLine(c1.cx, c1.cy, p2.x, p2.y, c1.color);
        // drawLine(c2.cx, c2.cy, p3.x, p3.y, c2.color);
        // drawLine(c2.cx, c2.cy, p4.x, p4.y, c2.color);
        // drawLine(c2.cx, c2.cy, c1.cx, c1.cy, c2.color);

        // drawLine(p1.x, p1.y, p3.x, p3.y, "#761242");
        // drawLine(p2.x, p2.y, p4.x, p4.y, "#761242");
        // drawCircle(p1.x, p1.y, 4 * devicePixelRatio, '#f00');
        // drawCircle(p2.x, p2.y, 4 * devicePixelRatio, '#f00');
        // drawCircle(p3.x, p3.y, 4 * devicePixelRatio, '#f00');
        // drawCircle(p4.x, p4.y, 4 * devicePixelRatio, '#f00');
        // drawLine(p1.x, p1.y, p3.x, p3.y, '#979798');
        // drawLine(p2.x, p2.y, p4.x, p4.y, '#979798');

        // drawText("p1", p1.x, p1.y, '#f00');
        // drawText("p2", p2.x, p2.y, '#f00');
        // drawText("p3", p3.x, p3.y, '#f00');
        // drawText("p4", p4.x, p4.y, '#f00');
        // drawText("c1", c1.cx, c1.cy, c1.color);
        // drawText("c2", c2.cx, c2.cy, c2.color);


        const d2 = (dist(p1.x, p1.y, p3.x, p3.y) / totalRadius);
        const r1 = c1.radius * d2;
        const r2 = c2.radius * d2;
        const h1 = this.getVector(p1.x, p1.y, angle1 - HALF_PI, r1);
        const h2 = this.getVector(p2.x, p2.y, angle2 + HALF_PI, r1);
        const h3 = this.getVector(p3.x, p3.y, angle3 + HALF_PI, r2);
        const h4 = this.getVector(p4.x, p4.y, angle4 - HALF_PI, r2);

        // ctx.beginPath();
        // ctx.fillStyle = c2.color;
        // ctx.moveTo(p1.x, p1.y);
        // ctx.arc(c1.cx, c1.cy, c1.radius, angle1, angle2, false);
        // ctx.bezierCurveTo(h2.x, h2.y, h4.x, h4.y, p4.x, p4.y);
        // ctx.arc(c2.cx, c2.cy, c2.radius, angle4, angle3, false);
        // ctx.bezierCurveTo(h3.x, h3.y, h1.x, h1.y, p1.x, p1.y);
        // ctx.fill();


        ctx.beginPath();
        ctx.strokeStyle = c2.color;
        ctx.moveTo(p1.x, p1.y);
        ctx.arc(c1.cx, c1.cy, c1.radius, angle1, angle2, false);
        ctx.bezierCurveTo(h2.x, h2.y, h4.x, h4.y, p4.x, p4.y);
        ctx.arc(c2.cx, c2.cy, c2.radius, angle4, angle3, false);
        ctx.bezierCurveTo(h3.x, h3.y, h1.x, h1.y, p1.x, p1.y);
        ctx.stroke();


        // drawLine(p1.x, p1.y, h1.x, h1.y, "#7ee987");
        // drawLine(p2.x, p2.y, h2.x, h2.y, "#7ee987");
        // drawLine(p3.x, p3.y, h3.x, h3.y, "#7ee987");
        // drawLine(p4.x, p4.y, h4.x, h4.y, "#7ee987");
        // //
        // drawCircle(h1.x, h1.y, 4 * devicePixelRatio, '#a24444');
        // drawCircle(h2.x, h2.y, 4 * devicePixelRatio, '#a24444');
        // drawCircle(h3.x, h3.y, 4 * devicePixelRatio, '#a24444');
        // drawCircle(h4.x, h4.y, 4 * devicePixelRatio, '#a24444');
        // drawText("h1", h1.x, h1.y);
        // drawText("h2", h2.x, h2.y);
        // drawText("h3", h3.x, h3.y);
        // drawText("h4", h4.x, h4.y);

    }

    function getVector(cx, cy, a, r) {
        return {x: cx + r * Math.cos(a), y: cy + r * Math.sin(a)};
    }

    function dist(x1, y1, x2, y2) {
        return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
    }


    function drawLine(x1, y1, x2, y2, color) {
        ctx.strokeStyle = color;
        ctx.beginPath();
        ctx.moveTo(x1, y1);
        ctx.lineTo(x2, y2);
        ctx.stroke()
    }

    function drawCircle(x, y, r, color) {
        ctx.fillStyle = color;
        ctx.beginPath();
        ctx.arc(x, y, r, 0, 2 * Math.PI);
        ctx.fill()
    }


    function drawText(text, x, y, color) {
        ctx.beginPath();
        ctx.fillStyle = color;
        ctx.font = 30 * devicePixelRatio + 'px Arial';
        ctx.fillText(text, x, y);
        ctx.fill()
    }


    update(0, 0);

</script>

</body>
</html>

文章参考:https://varun.ca/metaballs/

相关文章

  • Metaball变形球效果实现

    效果图 一 .求出圆的外公切线 两个圆心之间的距离,也就是两点之间的距离 计算直线C C 与水平线的角度 角P C...

  • 骨骼动画

    为了实现动画效果,我们可以使用序列图,或者可以选择骨骼动画。这里的动画效果,并不是说图片通过变形拉伸等等实现的效果...

  • iOS 自定义图片 聊天气泡制作

    实现图片拉伸,两边,上下,以及尖角不会变形,实现图片气泡聊天的效果 //气泡对话 +(UIImage*)resiz...

  • 移动端touch拖动事件和click事件冲突问题解决

    通过一个悬浮球交互功能的案例来阐述问题,以及解决办法。 实现效果 类似微信里的悬浮窗效果,苹果手机的悬浮球功能效果...

  • Android悬浮球及全局返回功能的实现

    Android悬浮球及全局返回功能的实现 先来一发效果图: 前面是返回效果,最后一下是实现home键的效果 前言 ...

  • OpenGL 综合案例(地球自转与公转)

    我们首先来看一下效果,大红球自转,小蓝球围绕着大红球转 接下来我们用OpenGL来实现这个效果: 首先我们要定义一...

  • CSS3之变形

    一、CSS3变形简介 CSS3变形是一些效果的集合,比如平移、旋转、缩放和倾斜效果,每个效果都称为变形函数(Tra...

  • CSS3 Transform——transform-origin

    关于css3变形 CSS3变形是一些效果的集合,比如平移、旋转、缩放和倾斜效果,每个效果都被称作为变形函数(Tra...

  • electron实现桌面悬浮球效果

    首先看下效果图如下: 当前效果是在mac电脑显示情况。 1、创建窗口的函数 2、调用窗口的函数 注意createS...

  • 球哥哥的成长

    一晃球球95CM了,一晃快3岁了! 最爱的变形金刚,自己坐在那,能完很长时间了,组装,变形,变形再组装,妈妈很爱看...

网友评论

      本文标题:Metaball变形球效果实现

      本文链接:https://www.haomeiwen.com/subject/hbrsqktx.html