# Canvas 实战: 水波图

## 参考

 Canvas制作水波图实现原理 https://mp.weixin.qq.com/s/-nLlq5qI6OzXgJEkaxEgjA

# 正文

## 实现原理

(图形使用 ggb 线上工具实现)

• 周期 T = 2 π / b T = 2\pi / b
• 振幅 R = a R = a
• 水平位移 W = c W = c (向左)
• 垂直位移 H = d H = d (向下)

1. 增加振幅(a)并向上偏移(d)

## 绘图开始

/canvas-wave
|- index.html
|- index.css
|- index.js


• index.html
<!DOCTYPE html>
<html lang="en">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas Wave</title>
<body>
<h1>使用 Canvas 实现水波图</h1>

<div class="wrapper">
<canvas id="canvas"></canvas>
</div>

<script src="index.js"></script>
</body>
</html>

• index.css
body {
margin: 0;
text-align: center;
}

.wrapper {
width: 75vw;
height: 80vh;
margin: 0 auto;
}


## 绘制基本 sin 图形

• index.js
window.onload = function () {
// 获取 canvas 上下文
const canvas = document.querySelector('#canvas')
const canvasWidth = canvas.width = 500
const canvasHeight = canvas.height = 500
const ctx = canvas.getContext('2d')

drawSin(ctx)

function drawSin (ctx) {
const points = []
const startX = 0
const waveWidth = 0.05 // 波浪周期，公式中替代 b
const waveHeight = 20 // 波浪高度，公式中替代 a

ctx.beginPath()
for (let x = startX ; x < startX + canvasWidth ; x += 20 / canvasWidth) {
// 计算高度
let y = waveHeight * Math.sin((startX + x) * waveWidth)
y += canvasHeight / 2 // 置于图中线
points.push([x, y])
ctx.lineTo(x, y)
}
ctx.lineTo(canvasWidth, canvasHeight)
ctx.lineTo(startX, canvasHeight)
ctx.lineTo(...points[0])
ctx.stroke()
}
}


## 使波形流动

• index.js
window.onload = function () {
const canvas = document.querySelector('#canvas')
const canvasWidth = canvas.width = 500
const canvasHeight = canvas.height = 500

// 记录当前偏移量
let xOffset = 0
// 偏移量移动速度间距，60 帧/s -> 一秒移动 6
const speed = 0.1

requestAnimationFrame(draw)

// 每帧进行重绘
function draw () {
const ctx = canvas.getContext('2d')
// 清除上一帧的图形
ctx.clearRect(0, 0, canvasWidth, canvasHeight)
// 图形重绘
drawSin(ctx, xOffset)
// 递归调用 -> 等到下一帧进行重绘
xOffset += speed
requestAnimationFrame(draw)
}

function drawSin (ctx, xOffset) {
const points = []
const startX = 0
const waveWidth = 0.05
const waveHeight = 20

ctx.beginPath()
for (let x = startX ; x < startX + canvasWidth ; x += 20 / canvasWidth) {
// a * sin(b * x) -> a * sin(b * x + c)
let y = waveHeight * Math.sin((startX + x) * waveWidth + xOffset)
y += canvasHeight / 2
points.push([x, y])
ctx.lineTo(x, y)
}
ctx.lineTo(canvasWidth, canvasHeight)
ctx.lineTo(startX, canvasHeight)
ctx.lineTo(...points[0])
ctx.stroke()
}
}


## 上色并切边

• index.js
window.onload = function () {
const canvas = document.querySelector('#canvas')
const canvasWidth = canvas.width = 500
const canvasHeight = canvas.height = 500

let xOffset = 0
const speed = 0.1
const blue1 = '#3399FF'

drawCircle() // 初始化切边圆形
const ctx = canvas.getContext('2d')
ctx.strokeStyle = blue1 // 以蓝色填满
ctx.fillStyle = blue1 // 以蓝色填满
requestAnimationFrame(draw)

function drawCircle () {
const ctx = canvas.getContext('2d')
const r = canvasWidth / 2
const lineWidth = 10
const cR = r - lineWidth
ctx.fillStyle = '#000000' // 黑边
ctx.lineWidth = lineWidth
ctx.beginPath()
// 画圆并切边
ctx.arc(r, r, cR, 0, 2 * Math.PI)
ctx.stroke()
ctx.clip()
}

function draw () {
const ctx = canvas.getContext('2d')
ctx.clearRect(0, 0, canvasWidth, canvasHeight)
drawSin(ctx, xOffset)
xOffset += speed
requestAnimationFrame(draw)
}

function drawSin (ctx, xOffset) {
const points = []
const startX = 0
const waveWidth = 0.05
const waveHeight = 20

ctx.beginPath()
for (let x = startX ; x < startX + canvasWidth ; x += 20 / canvasWidth) {
let y = waveHeight * Math.sin((startX + x) * waveWidth + xOffset)
y += canvasHeight / 2
points.push([x, y])
ctx.lineTo(x, y)
}
ctx.lineTo(canvasWidth, canvasHeight)
ctx.lineTo(startX, canvasHeight)
ctx.lineTo(...points[0])
ctx.stroke()
// 图形填满颜色
ctx.fill()
}
}


## 完成

• index.js
window.onload = function () {
const canvas = document.querySelector('#canvas')
const canvasWidth = canvas.width = 500
const canvasHeight = canvas.height = 500

let xOffset = 0
const speed = 0.1
const blue1 = '#3399FF'
const blue2 = '#3366FF'

drawCircle()
requestAnimationFrame(draw)

function drawCircle () {
const ctx = canvas.getContext('2d')
const r = canvasWidth / 2
const lineWidth = 10
const cR = r - lineWidth
ctx.fillStyle = '#000000'
ctx.lineWidth = lineWidth
ctx.beginPath()
ctx.arc(r, r, cR, 0, 2 * Math.PI)
ctx.stroke()
ctx.clip()
}

function draw () {
const ctx = canvas.getContext('2d')
ctx.clearRect(0, 0, canvasWidth, canvasHeight)
// 背景的波浪
ctx.strokeStyle = blue1
ctx.fillStyle = blue1
drawSin(ctx, xOffset, 3, 0.03, 12)
// 前景的波浪
ctx.strokeStyle = blue2
ctx.fillStyle = blue2
drawSin(ctx, xOffset, 0, 0.05, 15)
xOffset += speed
requestAnimationFrame(draw)
}

// 添加后参数：画布上下文、水平偏移量、垂直偏移量、波浪周期、波浪高度
function drawSin (ctx, xOffset = 0, yOffset = 0, waveWidth = 0.05, waveHeight = 20) {
const points = []
const startX = 0

ctx.beginPath()
for (let x = startX ; x < startX + canvasWidth ; x += 20 / canvasWidth) {
let y = waveHeight * Math.sin((startX + x) * waveWidth + xOffset)
y += canvasHeight / 2 - yOffset // 添加垂直偏移量
points.push([x, y])
ctx.lineTo(x, y)
}
ctx.lineTo(canvasWidth, canvasHeight)
ctx.lineTo(startX, canvasHeight)
ctx.lineTo(...points[0])
ctx.stroke()
ctx.fill()
}
}


06-06 2万+
07-20 459
12-09 1万+
03-30 1014
06-14 154
09-28
06-06