用Canvas實現(xiàn)一個大氣球送給你
一、背景
近期在做一個氣球掛件的特效需求,值此契機,來跟大家分享一下如何利用canvas以及對應的數(shù)學知識構(gòu)造一個栩栩如生的氣球。
二、實現(xiàn)
在實現(xiàn)這個看似是圓鼓鼓的氣球之前,先了解一下其實現(xiàn)思路,主要分為以下幾個部分:
- 實現(xiàn)球體部分;
 - 實現(xiàn)氣球口子部分;
 - 實現(xiàn)氣球的線部分;
 - 進行顏色填充;
 - 實現(xiàn)動畫;
 
氣球.PNG
2.1 球體部分實現(xiàn)
對于這樣的氣球的球體部分,大家都有什么好的實現(xiàn)思路的?相信大家肯定會有多種多樣的實現(xiàn)方案,我也是在看到某位大佬的效果后,感受到了利用四個三次貝塞爾曲線實現(xiàn)這個效果的妙處。為了看懂后續(xù)代碼,先了解一下三次貝塞爾曲線的原理。(注:引用了CSDN上某位大佬的文章,寫的很好,下圖引用于此)
三次貝塞爾曲線.gif
在上圖中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]
 
上述已經(jīng)列出了三次貝塞爾曲線的效果圖和公式,但是通過這個怎么跟我們的氣球掛上鉤呢?下面通過幾張圖就理解了:
如上圖所示,就是實現(xiàn)整個氣球球體的思路,具體解釋如下所示:
- A圖中起始點為p1,終止點為p2,控制點為c1、c2,讓兩個控制點重合,繪制出的效果并不是很像氣球的一部分,此時就要通過改變控制點來改變其外觀;
 - 改變控制點c1、c2,c1中y值不變,減小x值;c2中x值不變,增大y值(注意canvas中坐標方向即可),改變后就得到了圖B的效果,此時就跟氣球外觀很像了;
 - 緊接著按照這個方法就可以實現(xiàn)整個的氣球球體部分的外觀。
 
- 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 口子部分實現(xiàn)
口子部分可以簡化為一個三角形,效果如下所示:
- 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 線部分實現(xiàn)
線實現(xiàn)的比較簡單,就用了一段直線實現(xiàn)
- 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 動畫效果及整體代碼
上述流程已經(jīng)將一個靜態(tài)的氣球部分繪制完畢了,要想實現(xiàn)動畫效果只需要利用requestAnimationFrame函數(shù)不斷循環(huán)調(diào)用即可實現(xiàn)。下面直接拋出整體代碼,方便同學們觀察效果進行調(diào)試,整體代碼如下所示:
- 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;
 - }
 






















 
 
 








 
 
 
 