一、背景

近期在做一个气球挂件的特效需要,值此契机,来跟大家分享一下如何利用canvas以及对应的数学知识结构一个栩栩如生的气球。

二、实现

在实现这个看似是圆鼓鼓的气球之前,先理解一下其实现思路,次要分为以下几个局部:
  1. 实现球体局部;
  2. 实现气球口子局部;
  3. 实现气球的线局部;
  4. 进行色彩填充;
  5. 实现动画;

2.1 球体局部实现

对于这样的气球的球体局部,大家都有什么好的实现思路的?置信大家必定会有多种多样的实现计划,我也是在看到某位大佬的成果后,感触到了利用四个三次贝塞尔曲线实现这个成果的妙处。为了看懂后续代码,先理解一下三次贝塞尔曲线的原理。(注:援用了CSDN上某位大佬的文章,写的很好,下图援用于此)

在上图中P0为起始点、P3为终止点,P1和P2为控制点,其最终的曲线公式如下所示:

B(t)=(1−t)^3 P0+3t(1−t)^2 P1+3t ^ 2(1−t) * P2+t ^ 3P3, t∈[0,1]

上述曾经列出了三次贝塞尔曲线的效果图和公式,然而通过这个怎么跟咱们的气球挂上钩呢?上面通过几张图就了解了:

如上图所示,就是实现整个气球球体的思路,具体解释如下所示:
  1. A图中起始点为p1,终止点为p2,控制点为c1、c2,让两个控制点重合,绘制出的成果并不是很像气球的一部分,此时就要通过扭转控制点来扭转其外观;
  2. 扭转控制点c1、c2,c1中y值不变,减小x值;c2中x值不变,增大y值(留神canvas中坐标方向即可),扭转后就失去了图B的成果,此时就跟气球外观很像了;
  3. 紧接着依照这个办法就能够实现整个的气球球体局部的外观。
function draw() {    const canvas = document.getElementById('canvas');    const ctx = canvas.getContext('2d');    ctx.translate(250, 250);    drawCoordiante(ctx);    ctx.save();    ctx.beginPath();    ctx.moveTo(0, -80);    ctx.bezierCurveTo(45, -80, 80, -45, 80, 0);    ctx.bezierCurveTo(80, 85, 45, 120, 0, 120);    ctx.bezierCurveTo(-45, 120, -80, 85, -80, 0);    ctx.bezierCurveTo(-80, -45, -45, -80, 0, -80);    ctx.stroke();    ctx.restore();}function drawCoordiante(ctx) {    ctx.beginPath();    ctx.moveTo(-120, 0);    ctx.lineTo(120, 0);    ctx.moveTo(0, -120);    ctx.lineTo(0, 120);    ctx.closePath();    ctx.stroke();}

2.2 口子局部实现

口子局部能够简化为一个三角形,成果如下所示:

function draw() {    const canvas = document.getElementById('canvas');    const ctx = canvas.getContext('2d');    ……    ctx.save();    ctx.beginPath();    ctx.moveTo(0, 120);    ctx.lineTo(-5, 130);    ctx.lineTo(5, 130);    ctx.closePath();    ctx.stroke();    ctx.restore();}

2.3 线局部实现

线实现的比较简单,就用了一段直线实现

function draw() {    const canvas = document.getElementById('canvas');    const ctx = canvas.getContext('2d');    ……    ctx.save();    ctx.beginPath();    ctx.moveTo(0, 120);    ctx.lineTo(0, 300);    ctx.stroke();    ctx.restore();}

2.4 进行填充

气球局部的填充用了圆形突变成果,相比于纯色来说更加丑陋一些。
function draw() {    const canvas = document.getElementById('canvas');    const ctx = canvas.getContext('2d');    ctx.fillStyle = getBalloonGradient(ctx, 0, 0, 80, 210);    ……    }function getBalloonGradient(ctx, x, y, r, hue) {    const grd = ctx.createRadialGradient(x, y, 0, x, y, r);    grd.addColorStop(0, 'hsla(' + hue + ', 100%, 65%, .95)');    grd.addColorStop(0.4, 'hsla(' + hue + ', 100%, 45%, .85)');    grd.addColorStop(1, 'hsla(' + hue + ', 100%, 25%, .80)');    return grd;}

2.5 动画成果及整体代码

上述流程曾经将一个动态的气球局部绘制结束了,要想实现动画成果只须要利用requestAnimationFrame函数一直循环调用即可实现。上面间接抛出整体代码,不便同学们察看成果进行调试,整体代码如下所示:
let posX = 225;let posY = 300;let points = getPoints();draw();function draw() {    const canvas = document.getElementById('canvas');    const ctx = canvas.getContext('2d');    ctx.clearRect(0, 0, canvas.width, canvas.height);    if (posY < -200) {        posY = 300;        posX += 300 * (Math.random() - 0.5);        points = getPoints();    }    else {        posY -= 2;    }    ctx.save();    ctx.translate(posX, posY);    drawBalloon(ctx, points);    ctx.restore();    window.requestAnimationFrame(draw);}function drawBalloon(ctx, points) {    ctx.scale(points.scale, points.scale);    ctx.save();    ctx.fillStyle = getBalloonGradient(ctx, 0, 0, points.R, points.hue);    // 绘制球体局部    ctx.moveTo(points.p1.x, points.p1.y);    ctx.bezierCurveTo(points.pC1to2A.x, points.pC1to2A.y, points.pC1to2B.x, points.pC1to2B.y, points.p2.x, points.p2.y);    ctx.bezierCurveTo(points.pC2to3A.x, points.pC2to3A.y, points.pC2to3B.x, points.pC2to3B.y, points.p3.x, points.p3.y);    ctx.bezierCurveTo(points.pC3to4A.x, points.pC3to4A.y, points.pC3to4B.x, points.pC3to4B.y, points.p4.x, points.p4.y);    ctx.bezierCurveTo(points.pC4to1A.x, points.pC4to1A.y, points.pC4to1B.x, points.pC4to1B.y, points.p1.x, points.p1.y);    // 绘制气球钮局部    ctx.moveTo(points.p3.x, points.p3.y);    ctx.lineTo(points.knowA.x, points.knowA.y);    ctx.lineTo(points.knowB.x, points.knowB.y);    ctx.fill();    ctx.restore();    // 绘制线局部    ctx.save();    ctx.strokeStyle = '#000000';    ctx.lineWidth = 1;    ctx.beginPath();    ctx.moveTo(points.p3.x, points.p3.y);    ctx.lineTo(points.lineEnd.x, points.lineEnd.y);    ctx.stroke();    ctx.restore();}function getPoints() {    const offset = 35;    return {        scale: 0.3 + Math.random() / 2,        hue: Math.random() * 255,        R: 80,        p1: {            x: 0,            y: -80        },        pC1to2A: {            x: 80 - offset,            y: -80        },        pC1to2B: {            x: 80,            y: -80 + offset        },        p2: {            x: 80,            y: 0        },        pC2to3A: {            x: 80,            y: 120 - offset        },        pC2to3B: {            x: 80 - offset,            y: 120        },        p3: {            x: 0,            y: 120        },        pC3to4A: {            x: -80 + offset,            y: 120        },        pC3to4B: {            x: -80,            y: 120 - offset        },        p4: {            x: -80,            y: 0        },        pC4to1A: {            x: -80,            y: -80 + offset        },        pC4to1B: {            x: -80 + offset,            y: -80        },        knowA: {            x: -5,            y: 130        },        knowB: {            x: 5,            y: 130        },        lineEnd: {            x: 0,            y: 250        }    };}function getBalloonGradient(ctx, x, y, r, hue) {    const grd = ctx.createRadialGradient(x, y, 0, x, y, r);    grd.addColorStop(0, 'hsla(' + hue + ', 100%, 65%, .95)');    grd.addColorStop(0.4, 'hsla(' + hue + ', 100%, 45%, .85)');    grd.addColorStop(1, 'hsla(' + hue + ', 100%, 25%, .80)');    return grd;}

三、相干文章

如果只剩下canvas标签

canvas从入门到猪头

1.如果感觉这篇文章还不错,来个分享、点赞吧,让更多的人也看到

2.欢送关注公众号前端点线面,开启前端救赎之路。