Bermu

如何实现一个波浪 🌊

2018-08-03

之前在博客主页一直想加一种波浪的效果,但是一直没有沉下心去学习。先将三种实现方式贴到博客,方便后续引入。

首先看最low的方法

css

1
2
// html
<div class="wave"></div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// css
.wave {
position: relative;
width: 200px;
height: 200px;
background-color: rgb(118, 218, 255);
}

.wave:before,
.wave:after {
content: "";
position: absolute;
width: 400px;
height: 400px;
top: 0;
left: 50%;
background-color: rgba(255, 255, 255, .4);
transform: translate(-50%, -70%) rotate(0);
animation: rotate 8s linear infinite;
border-radius: 35%;
z-index: 10;
}

@keyframes rotate {
50% {
transform: translate(-50%, -75%) rotate(180deg);
}
100% {
transform: translate(-50%, -70%) rotate(360deg);
}
}

首先我们切开一个200x200px的画布,我们利用伪元素在其内写入变化的效果。

首先我们选定波浪的形状,通过transform与border-radius分别对其位置与形态产生改变,此时定义动画效果,将animation指定形状的位置及旋转变化,造成视觉上的移动效果。


svg

这是一种可伸缩矢量图形,非常适合做动画交互,对于path和line的绘制支持十分强大

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="300" height="200">
<g fill="rgba(106,127,239,0.1)">
<path d="M 0 70 Q 75 39, 150 70 T 300 70 T 450 70 T 600 70 T 750 70 V 100 H 0 V 0"></path>
<animateTransform attributeName="transform" attributeType="XML" type="translate" from="0" to="-300" dur="1.5s" repeatCount="indefinite"></animateTransform>
</g>
<g fill="rgba(106,127,239,0.15)">
<path d="M 0 70 Q 87.5 47, 175 70 T 350 70 T 525 70 T 700 70 T 875 70 T 1050 70 V 100 H 0 V 0"></path>
<animateTransform attributeName="transform" attributeType="XML" type="translate" from="0" to="-350" dur="3s" repeatCount="indefinite"></animateTransform>
</g>
<g fill="rgba(106,127,239,0.18)" transform="translate(-903.868 0)">
<path d="M 0 70 Q 135 36, 270 70 T 540 70 T 810 70 T 1080 70 V 100 H 0 V 0" transform="translate(-38.232284367796474, 0)"></path>
<animateTransform attributeName="transform" attributeType="XML" type="translate" from="0" to="-540" dur="2s" repeatCount="indefinite"></animateTransform>
</g>
</svg>

这里svg制定了三条轨迹,显而易见的是fill可以指定我们填充的颜色,大家可能对path里的d属性感到疑惑,我们首先要知道几个概念:

1
2
3
4
5
6
7
d属性中使用了以下几个命令:

M:M x y表示移动到(x, y)点( svg中左上角是(0,0)点 )
Q:Q x1 y1, x2 y2表示绘制二次贝塞尔曲线,x1 y1为二次贝塞尔的控制点,x2 y2为终点,可以使用贝塞尔生产曲线工具帮助生成。
T:T x y表示生成上一个二次贝塞尔曲线的镜像,其终点坐标为(x,y)
V:V y表示从当前点(x0,y0)垂直移动到(x0, y)
H:H x表示从当前点(x0, y0)水平移动到(x, y0)

但是这样只会绘制静态的路径,是无法让它蠕动起来的,为了让它充满生机,引入这个东西

1
2
3
4
5
6
7
<animateTransform
attributeName="transform"
attributeType="XML"
type="translate"
from="0" to="-300" dur="1.5s"
repeatCount="indefinite">
</animateTransform>
  • attributeName:需要运动的属性
  • type:具体运动的类型
  • from:运动初始值
  • to:运动终点值
  • dur:运动时间
  • repeatCount:重复次数,indefinite为无限循环

通过一系列的图形组装,设定不同的运动速率,即可得到较为丰富的画面

canvas

对于这种实现方式,优点是调用了GPU的加速处理,但是无疑js的计算让此变得复杂,总体来说并不建议做复杂的动画交互。

第一步

1
2
3
4
5
// 创建上下文
const canvas = document.querySelector('canvas');
const ctx = document.querySelector('canvas').getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

第二步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 设定波浪透明度
const wavesOpacities = [0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1];

// 设定相关参数
const params = {
AMPLITUDE_WAVES: canvas.height,
AMPLITUDE_MIDDLE: canvas.height / 3,
AMPLITUDE_SIDES: canvas.height / 2,
OFFSET_SPEED: 120,
SPEED: 3,
OFFSET_WAVES: 35,
NUMBER_WAVES: 3,
COLOR: ['#032bac', '#8a280b', '#ff9acc'],
NUMBER_CURVES: 2,
OFFSET_CURVE: true,
RESET: false
};
let speedInc = 0;

第三步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// 定义render方法
const render = () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

for (let j = params.NUMBER_WAVES - 1; j >= 0; j--) {

// 每个波浪的距离
let offset = speedInc + j * Math.PI * params.OFFSET_WAVES;

// 颜色及透明度变化
ctx.fillStyle = params.COLOR[j];

ctx.globalAlpha = wavesOpacities[j];

// 设定传波函数
let leftRange = ((Math.sin((offset / params.OFFSET_SPEED) + 2) + 1) / 2 * params.AMPLITUDE_SIDES) + (canvas.height - params.AMPLITUDE_SIDES) / 2;
let rightRange = ((Math.sin((offset / params.OFFSET_SPEED) + 2) + 1) / 2 * params.AMPLITUDE_SIDES) + (canvas.height - params.AMPLITUDE_SIDES) / 2;

let leftCurveRange = (Math.sin((offset / params.OFFSET_SPEED) + 2) + 1) / 2 * params.AMPLITUDE_WAVES + (canvas.height - params.AMPLITUDE_WAVES) / 2;
let rightCurveRange = (Math.sin((offset / params.OFFSET_SPEED) + 1) + 1) / 2 * params.AMPLITUDE_WAVES + (canvas.height - params.AMPLITUDE_WAVES) / 2;

let endCurveRange = ((Math.sin((offset / params.OFFSET_SPEED) + 2) + 1) / 2 * params.AMPLITUDE_MIDDLE) + (canvas.height - params.AMPLITUDE_MIDDLE) / 2;

let reverseLeftCurveRange = endCurveRange - rightCurveRange + endCurveRange;
let reverseRightCurveRange = endCurveRange - leftCurveRange + endCurveRange;

if (params.OFFSET_CURVE === false) {

leftCurveRange = rightCurveRange;
reverseRightCurveRange = reverseLeftCurveRange;

}


// 开始填充路径
ctx.beginPath();

ctx.moveTo(0, leftRange);

ctx.bezierCurveTo(canvas.width / (params.NUMBER_CURVES * 3), leftCurveRange, canvas.width / (params.NUMBER_CURVES * 3 / 2), rightCurveRange, canvas.width / params.NUMBER_CURVES, endCurveRange);

for (let i = 1; i < params.NUMBER_CURVES; i++) {

const finalRightCurveRange = i % 2 !== 0 ? rightCurveRange : reverseRightCurveRange;
const finalLeftCurveRange = i % 2 !== 0 ? leftCurveRange : reverseLeftCurveRange;

// 设定波点
const secondPtX = canvas.width * (i / params.NUMBER_CURVES) + canvas.width / (params.NUMBER_CURVES * 3);
const secondPtY = endCurveRange - finalRightCurveRange + endCurveRange;
const thirdPtX = canvas.width * (i / params.NUMBER_CURVES) + canvas.width * (2 / (params.NUMBER_CURVES * 3));
const thirdPtY = endCurveRange - finalLeftCurveRange + endCurveRange;
const lastPtX = canvas.width * ((i + 1) / params.NUMBER_CURVES);
const lastPtY = i === params.NUMBER_CURVES - 1 ? rightRange : endCurveRange;

ctx.bezierCurveTo(secondPtX, secondPtY, thirdPtX, thirdPtY, lastPtX, lastPtY);

}

ctx.lineTo(canvas.width, canvas.height);
ctx.lineTo(0, canvas.height);
ctx.lineTo(0, rightRange);

ctx.closePath();
ctx.fill();
}

// 增速
speedInc += params.SPEED;
};

最后调用TweenMax.ticker.addEventListener(‘tick’, render);,注意最后的调用函数需要引入GSAP库,它是一个高性能的web animation lib。


实现效果就为

Tags: css
使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

扫描二维码,分享此文章