用Canvas实现一个大气球送给你

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

[[423338]]

一、背景

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

二、实现

在实现这个看似是圆鼓鼓的气球之前,先了解一下其实现思路,主要分为以下几个部分:

  1. 实现球体部分;
  2. 实现气球口子部分;
  3. 实现气球的线部分;
  4. 进行颜色填充;
  5. 实现动画;

气球.PNG

2.1 球体部分实现

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

三次贝塞尔曲线.gif

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

  1. 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. 紧接着按照这个方法就可以实现整个的气球球体部分的外观。
  1. function draw() { 
  2.     const canvas = document.getElementById('canvas'); 
  3.     const ctx = canvas.getContext('2d'); 
  4.  
  5.     ctx.translate(250, 250); 
  6.     drawCoordiante(ctx); 
  7.     ctx.save(); 
  8.     ctx.beginPath(); 
  9.     ctx.moveTo(0, -80); 
  10.     ctx.bezierCurveTo(45, -80, 80, -45, 80, 0); 
  11.     ctx.bezierCurveTo(80, 85, 45, 120, 0, 120); 
  12.     ctx.bezierCurveTo(-45, 120, -80, 85, -80, 0); 
  13.     ctx.bezierCurveTo(-80, -45, -45, -80, 0, -80); 
  14.     ctx.stroke(); 
  15.     ctx.restore(); 
  16.  
  17. function drawCoordiante(ctx) { 
  18.     ctx.beginPath(); 
  19.     ctx.moveTo(-120, 0); 
  20.     ctx.lineTo(120, 0); 
  21.     ctx.moveTo(0, -120); 
  22.     ctx.lineTo(0, 120); 
  23.     ctx.closePath(); 
  24.     ctx.stroke(); 

2.2 口子部分实现

口子部分可以简化为一个三角形,效果如下所示:

  1. function draw() { 
  2.     const canvas = document.getElementById('canvas'); 
  3.     const ctx = canvas.getContext('2d'); 
  4.  
  5.     …… 
  6.  
  7.     ctx.save(); 
  8.     ctx.beginPath(); 
  9.     ctx.moveTo(0, 120); 
  10.     ctx.lineTo(-5, 130); 
  11.     ctx.lineTo(5, 130); 
  12.     ctx.closePath(); 
  13.     ctx.stroke(); 
  14.     ctx.restore(); 

2.3 线部分实现

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

  1. function draw() { 
  2.     const canvas = document.getElementById('canvas'); 
  3.     const ctx = canvas.getContext('2d'); 
  4.  
  5.     …… 
  6.  
  7.     ctx.save(); 
  8.     ctx.beginPath(); 
  9.     ctx.moveTo(0, 120); 
  10.     ctx.lineTo(0, 300); 
  11.     ctx.stroke(); 
  12.     ctx.restore(); 

2.4 进行填充

气球部分的填充用了圆形渐变效果,相比于纯色来说更加漂亮一些。

  1. function draw() { 
  2.     const canvas = document.getElementById('canvas'); 
  3.     const ctx = canvas.getContext('2d'); 
  4.  
  5.     ctx.fillStyle = getBalloonGradient(ctx, 0, 0, 80, 210); 
  6.     …… 
  7.      
  8.  
  9. function getBalloonGradient(ctx, x, y, r, hue) { 
  10.     const grd = ctx.createRadialGradient(x, y, 0, x, y, r); 
  11.     grd.addColorStop(0, 'hsla(' + hue + ', 100%, 65%, .95)'); 
  12.     grd.addColorStop(0.4, 'hsla(' + hue + ', 100%, 45%, .85)'); 
  13.     grd.addColorStop(1, 'hsla(' + hue + ', 100%, 25%, .80)'); 
  14.     return grd; 

2.5 动画效果及整体代码

上述流程已经将一个静态的气球部分绘制完毕了,要想实现动画效果只需要利用requestAnimationFrame函数不断循环调用即可实现。下面直接抛出整体代码,方便同学们观察效果进行调试,整体代码如下所示:

  1. let posX = 225; 
  2. let posY = 300; 
  3. let points = getPoints(); 
  4. draw(); 
  5.  
  6. function draw() { 
  7.     const canvas = document.getElementById('canvas'); 
  8.     const ctx = canvas.getContext('2d'); 
  9.     ctx.clearRect(0, 0, canvas.width, canvas.height); 
  10.     if (posY < -200) { 
  11.         posY = 300; 
  12.         posX += 300 * (Math.random() - 0.5); 
  13.         points = getPoints(); 
  14.     } 
  15.     else { 
  16.         posY -= 2; 
  17.     } 
  18.     ctx.save(); 
  19.     ctx.translate(posX, posY); 
  20.     drawBalloon(ctx, points); 
  21.     ctx.restore(); 
  22.  
  23.     window.requestAnimationFrame(draw); 
  24.  
  25. function drawBalloon(ctx, points) { 
  26.     ctx.scale(points.scale, points.scale); 
  27.     ctx.save(); 
  28.     ctx.fillStyle = getBalloonGradient(ctx, 0, 0, points.R, points.hue); 
  29.     // 绘制球体部分 
  30.     ctx.moveTo(points.p1.x, points.p1.y); 
  31.     ctx.bezierCurveTo(points.pC1to2A.x, points.pC1to2A.y, points.pC1to2B.x, points.pC1to2B.y, points.p2.x, points.p2.y); 
  32.     ctx.bezierCurveTo(points.pC2to3A.x, points.pC2to3A.y, points.pC2to3B.x, points.pC2to3B.y, points.p3.x, points.p3.y); 
  33.     ctx.bezierCurveTo(points.pC3to4A.x, points.pC3to4A.y, points.pC3to4B.x, points.pC3to4B.y, points.p4.x, points.p4.y); 
  34.     ctx.bezierCurveTo(points.pC4to1A.x, points.pC4to1A.y, points.pC4to1B.x, points.pC4to1B.y, points.p1.x, points.p1.y); 
  35.  
  36.     // 绘制气球钮部分 
  37.     ctx.moveTo(points.p3.x, points.p3.y); 
  38.     ctx.lineTo(points.knowA.x, points.knowA.y); 
  39.     ctx.lineTo(points.knowB.x, points.knowB.y); 
  40.     ctx.fill(); 
  41.     ctx.restore(); 
  42.  
  43.     // 绘制线部分 
  44.     ctx.save(); 
  45.     ctx.strokeStyle = '#000000'
  46.     ctx.lineWidth = 1; 
  47.     ctx.beginPath(); 
  48.     ctx.moveTo(points.p3.x, points.p3.y); 
  49.     ctx.lineTo(points.lineEnd.x, points.lineEnd.y); 
  50.     ctx.stroke(); 
  51.     ctx.restore(); 
  52.  
  53. function getPoints() { 
  54.     const offset = 35; 
  55.     return { 
  56.         scale: 0.3 + Math.random() / 2, 
  57.         hue: Math.random() * 255, 
  58.         R: 80, 
  59.         p1: { 
  60.             x: 0, 
  61.             y: -80 
  62.         }, 
  63.         pC1to2A: { 
  64.             x: 80 - offset, 
  65.             y: -80 
  66.         }, 
  67.         pC1to2B: { 
  68.             x: 80, 
  69.             y: -80 + offset 
  70.         }, 
  71.         p2: { 
  72.             x: 80, 
  73.             y: 0 
  74.         }, 
  75.         pC2to3A: { 
  76.             x: 80, 
  77.             y: 120 - offset 
  78.         }, 
  79.         pC2to3B: { 
  80.             x: 80 - offset, 
  81.             y: 120 
  82.         }, 
  83.         p3: { 
  84.             x: 0, 
  85.             y: 120 
  86.         }, 
  87.         pC3to4A: { 
  88.             x: -80 + offset, 
  89.             y: 120 
  90.         }, 
  91.         pC3to4B: { 
  92.             x: -80, 
  93.             y: 120 - offset 
  94.         }, 
  95.         p4: { 
  96.             x: -80, 
  97.             y: 0 
  98.         }, 
  99.         pC4to1A: { 
  100.             x: -80, 
  101.             y: -80 + offset 
  102.         }, 
  103.         pC4to1B: { 
  104.             x: -80 + offset, 
  105.             y: -80 
  106.         }, 
  107.         knowA: { 
  108.             x: -5, 
  109.             y: 130 
  110.         }, 
  111.         knowB: { 
  112.             x: 5, 
  113.             y: 130 
  114.         }, 
  115.         lineEnd: { 
  116.             x: 0, 
  117.             y: 250 
  118.         } 
  119.     }; 
  120.  
  121. function getBalloonGradient(ctx, x, y, r, hue) { 
  122.     const grd = ctx.createRadialGradient(x, y, 0, x, y, r); 
  123.     grd.addColorStop(0, 'hsla(' + hue + ', 100%, 65%, .95)'); 
  124.     grd.addColorStop(0.4, 'hsla(' + hue + ', 100%, 45%, .85)'); 
  125.     grd.addColorStop(1, 'hsla(' + hue + ', 100%, 25%, .80)'); 
  126.     return grd; 

 

责任编辑:武晓燕 来源: 前端点线面
相关推荐

2017-08-29 15:34:10

CanvasWASM算法

2022-12-22 08:22:17

Python图像图像处理

2017-11-27 13:39:29

Python大数据搜索引擎

2017-12-27 14:51:12

Kotlin谷歌Java

2018-07-03 15:20:36

Promise函数借钱

2020-09-06 22:59:35

Linux文件命令

2021-05-07 07:59:52

WebFluxSpring5系统

2021-04-15 11:37:47

NumpyPython代码

2020-12-20 10:07:57

Canvas图形验证码javascript

2012-05-30 09:40:55

Linux锅炉

2018-05-04 09:14:09

Git技巧shell命令

2018-06-16 08:35:57

UnixLinux命令

2014-04-14 15:54:00

print()Web服务器

2021-06-25 10:38:05

JavaScript编译器前端开发

2022-02-28 00:14:30

人工智能数据机器学习

2015-07-27 10:34:55

大数据大忽悠

2023-01-30 16:21:24

Linux外观

2017-06-05 12:06:00

2021-02-14 19:24:45

SpringRegistrar对象

2019-02-11 11:16:13

点赞
收藏

51CTO技术栈公众号