基于 Vue 2 和 Canvas 的流动水效果组件

基于 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>