基于 Vue 2 和 Canvas 的流动水效果组件,它可以根据传入的参数动态调整水位和颜色。这个组件将模拟真实的水流效果,并支持自定义颜色、当前水位和最大水位。
效果视频、图片

canvas+vue2+js画一个流动的水
组件
***我加入了 $px2rem因为我的项目需要转成rem,若你的项目不需要,记得换成px***
javascript
<template>
<div
class="water-wrapper"
:style="{
width: $px2rem(width),
height: $px2rem(height)
}"
>
<!-- 动态跟随水面的标题 -->
<h4
class="water-level-title"
:style="{
left: $px2rem(10),
top: $px2rem(titleTop),
color: titleColor
}"
>
{{ waterLevelName }}: {{ waterLevel }}m
</h4>
<!-- 警戒水位线(仅当warnWaterLevel存在时显示) -->
<div
v-if="warnWaterLevel"
class="jzrz"
:style="{
top: $px2rem(warnLineTop),
width: '100%'
}"
>
<i class="el-icon-caret-bottom" style="color: #f13939"></i>
警戒水位:{{ warnWaterLevel }}m
</div>
<div class="water-container">
<canvas ref="waterCanvas" :style="{ width: '100%', height: '100%' }"></canvas>
</div>
</div>
</template>
<script>
export default {
name: 'FlowingWater',
props: {
waterLevelName: {
type: String,
default: '水位'
},
/** 当前水位值(实际米数) */
waterLevel: {
type: Number,
default: 0,
validator: value => value >= 0
},
/** 最大水位值(实际米数,用于计算比例) */
maxLevel: {
type: Number,
default: 10,
validator: value => value > 0
},
/** 容器宽度 */
width: {
type: [String, Number],
default: '100%'
},
/** 容器高度 */
height: {
type: [String, Number],
default: '75'
},
/** 波浪流动速度 */
waveSpeed: {
type: Number,
default: 0.02
},
/** 波浪振幅 */
waveAmplitude: {
type: Number,
default: 3
},
/** 标题文字颜色 */
titleColor: {
type: String,
default: '#fff'
},
/** 警戒水位 */
warnWaterLevel: {
type: [Number, String],
default: ''
}
},
data() {
return {
canvas: null,
ctx: null,
animationFrame: null,
phase: 0, // 波浪相位,用于动画效果
canvasWidth: 0,
canvasHeight: 0,
titleTop: 0, // 标题距离容器顶部的距离
warnLineTop: 0, // 警戒水位线距离容器顶部的距离
// 固定水的渐变色配置
waterColors: {
main: 'rgba(0,128,255,0.90)',
light: 'rgba(51,205,255,0.9)',
dark: 'rgba(0, 90, 180, 0.9)',
gradientStart: 'rgba(51,205,255,0.6)',
gradientEnd: 'rgba(0, 90, 180, 0.9)'
}
}
},
watch: {
// 监听水位变化,重新绘制并更新标题位置
waterLevel() {
this.drawWater()
this.calcTitlePosition()
},
// 监听警戒水位变化,重新计算位置
warnWaterLevel() {
this.calcWarnLinePosition()
},
// 监听最大水位变化,重新绘制并更新位置
maxLevel() {
this.drawWater()
this.calcTitlePosition()
this.calcWarnLinePosition()
},
// 监听尺寸变化,重新调整画布并更新位置
width() {
this.$nextTick(() => {
this.resizeCanvas()
this.calcTitlePosition()
this.calcWarnLinePosition()
})
},
height() {
this.$nextTick(() => {
this.resizeCanvas()
this.calcTitlePosition()
this.calcWarnLinePosition()
})
}
},
mounted() {
// 初始化画布
this.initCanvas()
// 调整画布尺寸
this.resizeCanvas()
// 计算初始位置
this.calcTitlePosition()
this.calcWarnLinePosition()
// 开始动画
this.startAnimation()
// 监听窗口大小变化
window.addEventListener('resize', this.handleResize)
},
beforeDestroy() {
// 停止动画
if (this.animationFrame) {
cancelAnimationFrame(this.animationFrame)
}
// 移除事件监听
window.removeEventListener('resize', this.handleResize)
},
methods: {
// 初始化画布
initCanvas() {
this.canvas = this.$refs.waterCanvas
this.ctx = this.canvas.getContext('2d')
},
// 处理窗口大小变化
handleResize() {
this.resizeCanvas()
this.calcTitlePosition()
this.calcWarnLinePosition()
},
// 调整画布尺寸
resizeCanvas() {
if (!this.canvas) return
// 获取容器实际尺寸
const container = this.canvas.parentElement
const { width, height } = container.getBoundingClientRect()
// 设置canvas实际尺寸(考虑设备像素比,使图像更清晰)
const dpr = window.devicePixelRatio || 1
this.canvas.width = width * dpr
this.canvas.height = height * dpr
this.canvasWidth = width * dpr
this.canvasHeight = height * dpr
// 调整绘图上下文比例
this.ctx.scale(dpr, dpr)
// 重绘水效果
this.drawWater()
},
// 开始动画
startAnimation() {
const animate = () => {
// 更新波浪相位,创建流动效果
this.phase += this.waveSpeed
this.drawWater()
this.animationFrame = requestAnimationFrame(animate)
}
animate()
},
// 计算标题位置(紧贴水面线上方)
calcTitlePosition() {
if (!this.canvas) return
// 获取容器实际高度
const container = this.canvas.parentElement
const { height } = container.getBoundingClientRect()
// 计算当前水面Y坐标
const maxWaterHeight = (this.waterLevel / this.maxLevel) * height
const waterY = height - maxWaterHeight
// 标题定位在水面线上方
this.titleTop = waterY
},
// 计算警戒水位线位置
calcWarnLinePosition() {
if (!this.canvas || !this.warnWaterLevel) return
// 获取容器实际高度
const container = this.canvas.parentElement
const { height } = container.getBoundingClientRect()
// 将警戒水位值转为数字
const warnLevel = Number(this.warnWaterLevel)
if (isNaN(warnLevel) || warnLevel < 0) return
// 根据警戒水位计算在容器中的位置(与水位计算逻辑一致)
const maxWarnHeight = (warnLevel / this.maxLevel) * height
this.warnLineTop = height - maxWarnHeight
},
// 绘制水效果
drawWater() {
if (!this.ctx || !this.canvasWidth || !this.canvasHeight) return
const ctx = this.ctx
const width = this.canvasWidth / (window.devicePixelRatio || 1)
const height = this.canvasHeight / (window.devicePixelRatio || 1)
// 清除画布
ctx.clearRect(0, 0, width, height)
// 计算实际水位(基于最大水位和当前水位比例)
const maxWaterHeight = (this.waterLevel / this.maxLevel) * height
const waterY = height - maxWaterHeight + 20 // 水位的Y坐标(水位的高度)
// 创建渐变效果 - 使用固定的渐变色
const gradient = ctx.createLinearGradient(0, waterY, 0, height)
gradient.addColorStop(0, this.waterColors.gradientStart)
gradient.addColorStop(1, this.waterColors.gradientEnd)
// 绘制第一层波浪
ctx.beginPath()
ctx.moveTo(0, height)
for (let x = 0; x <= width; x++) {
// 使用正弦函数创建波浪形状
const y = waterY + Math.sin(x * 0.02 + this.phase) * this.waveAmplitude
ctx.lineTo(x, y)
}
ctx.lineTo(width, height)
ctx.closePath()
// 填充颜色
ctx.fillStyle = gradient
ctx.fill()
// 绘制第二层波浪(增强流动效果)
ctx.beginPath()
ctx.moveTo(0, height)
for (let x = 0; x <= width; x++) {
// 反向的波浪,频率和振幅略有不同
const y = waterY + Math.sin(x * 0.03 - this.phase + 1) * (this.waveAmplitude * 0.6)
ctx.lineTo(x, y)
}
ctx.lineTo(width, height)
ctx.closePath()
// 使用半透明的浅色,增加层次感
ctx.fillStyle = `${this.waterColors.light}80` // 添加透明度
ctx.fill()
// 绘制水面高光效果
ctx.beginPath()
ctx.moveTo(0, waterY)
for (let x = 0; x <= width; x++) {
const y = waterY + Math.sin(x * 0.02 + this.phase) * this.waveAmplitude * 0.3
ctx.lineTo(x, y)
}
// 创建高光渐变
const highlightGradient = ctx.createLinearGradient(0, waterY - 10, 0, waterY + 10)
highlightGradient.addColorStop(0, 'rgba(255, 255, 255, 0.6)')
highlightGradient.addColorStop(1, 'rgba(255, 255, 255, 0)')
ctx.lineTo(width, waterY)
ctx.closePath()
ctx.fillStyle = highlightGradient
ctx.fill()
}
}
}
</script>
<style lang="scss" scoped>
.water-wrapper {
position: relative; // 作为标题和警戒线的定位容器
overflow: hidden;
}
.water-level-title {
position: absolute; // 绝对定位,跟随水面移动
margin: 0;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
transition: top 0.3s ease; // 水位变化时平滑过渡
pointer-events: none; // 避免标题遮挡交互
z-index: 10; // 确保在水面上方
text-shadow: 0 0 4px rgba(0, 0, 0, 0.8);
}
// 警戒水位样式
.jzrz {
position: absolute;
font-size: 12px;
color: #f13939;
border-bottom: 2px dashed #f13939;
padding-left: 10px;
box-sizing: border-box;
z-index: 20; // 层级高于水面和标题
display: flex;
align-items: center;
transition: top 0.3s ease; // 平滑过渡
}
.water-container {
width: 100%;
height: 100%;
position: relative;
z-index: 5; // 水面层级最低
}
</style>
父组件使用
javascript
<template>
<div class="GateLive-container">
<!-- 外河水位 -->
<div class="whrz">
<WaterAnimation
:waterLevel="45"
waterLevelName="外河水位"
:maxLevel="60"
width="181"
height="75"
:warnWaterLevel="55"
/>
</div>
</div>
</template>
<script>
import WaterAnimation from './WaterAnimation.vue'
export default {
name: 'GateLive',
props: {},
components: { WaterAnimation },
data() {
return {
}
},
computed: {},
watch: {},
created() {},
mounted() {},
methods: {}
}
</script>
<style lang="scss" scoped>
.GateLive-container {
margin-top: -10px;
.whrz {
position: absolute;
left: 0;
bottom: 0;
z-index: 9;
}
}
}
</style>