vue+Canvas实现图片列表逐帧播放

前言

页面展示

代码实现过程

完整代码


前言

需求背景:实现一组/多组图片可以进行一帧一帧播放,有点类似于视频播放,有进度条、暂停、上/下一张播放功能,且可以进行逐帧查看图片。

页面展示

 话不多说,一起来看看吧~

代码实现过程

  1. ​​​​第一步:成功获取数据列表之后,初始化canvas
  2. 第二步:在canvas上加载图片( drawImg() 
  3. 第三步:根据index获取图片url,调用第二步方法完成操作区按钮功能

完整代码

<template>
  <div class="videoBoxCls">
    <!-- 左侧列表 -->
    <div class="left-list">
      <div v-for="(imgList, index) in imgListAll" :key="index" class="item-cls">
        <div class="img-box" :class="[iindex == currentIdx ? 'current' : '']" @click="switchList(index)">
          <img :src="imgList.cover" alt>
        </div>
        <p class="img-title">{{ imgList.name }}</p>
      </div>
    </div>
    <!-- 右侧预览 -->
    <div class="right-con">
      <div ref="videoContainer" class="videoContainer" />
      <div class="frameBox">
        <!-- 帧进度条 -->
        <div class="frame">
          <div
            v-for="(item, index) in currentFrameList"
            :key="item.id"
            :class="['frameItem', currentFrame == item.frame ? 'currentFrane' : '']"
            @click="gon(index)"
          >
            <span class="hovFrame">{{ item.frame }}</span>
          </div>
        </div>
        <!-- 播放按钮 -->
        <div class="videoBtn">
          <span class="frame-title">Frame:{{ currentFrame }}</span>
          <div class="btn-box">
            <div class="goTopCls" @click="goFor('frist')" />
            <div class="prevCls" @click="prev" />
            <div v-if="ispause" class="play" @click="play" />
            <div v-if="!ispause" class="pause" @click="pause" />
            <div class="nextCls" @click="next" />
            <div class="goLastCls" @click="goFor('last')" />
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
export default {
  name: 'VideoDialog',
  data() {
    return {
      currentIdx: 0, // 左侧列表 当前index
      currentFrame: '', // 当前帧
      imgListAll: [{
        name: '测试',
        cover: '',
        imgArr: []
      }], // 获取左侧列表
      currentFrameList: [], // 当前播放帧列表
      // 视频预览
      canvas: null,
      ctx: null,
      timer: null, // 定时器
      infinite: 1000000000, // 循环次数
      ispause: true, // 是否是暂停状态
      plusNum: 0, // 帧总量
      plusCount: 0, // 帧总量计数器(判断循环次数)
      count: 0, // 当前帧
      fps: 25, // 25帧/秒
      loop: 'infinite', // 是否循环
      recordFrom: 0, // 记录起始帧
      recordTo: null, // 记录结束帧
      imgsLen: null // 记录帧长度
    }
  },
  watch: {
    count(newValue) {
      this.currentFrame = this.currentFrameList[newValue].frame
    }
  },
  created() {
    this.getImgAllList()
  },
  methods: {
    // 获取图片列表列表
    getImgAllList() {},
    // 左侧切换列表
    switchList(index) {
      this.currentIdx = index
      // 设置右侧封面
      this.drawImg(this.imgListAll[this.currentIdx].cover)
      this.resetData()
      this.currentFrameList = this.imgListAll[this.currentIdx].imgArr
      this.currentFrame.frame = this.currentFrameList[0].frame
      this.imgsLen = this.currentFrameList.length
    },
    // 重置数据 停止并回到第一帧或cover帧
    resetData() {
      this.ispause = true
      clearInterval(this.timer)
      this.plusNum = 0
      this.plusCount = 0

      // 重置记录
      this.recordFrom = 0
      this.recordTo = this.imgsLen - 1
      this.loop = 'infinite'
      this.infinite = 1000000000
      this.count = 0
    },
    // 初始化帧播放
    CanvasPlayFrames() {
      // 1.创建canvas
      const videoContainer = this.$refs.videoContainer
      const canvas = document.createElement('canvas')
      canvas.width = 654
      canvas.height = 480
      canvas.style.display = 'block'
      canvas.style.width = '100%'
      canvas.style.height = '100%'
      this.ctx = canvas.getContext('2d')
      videoContainer.appendChild(canvas)
      this.canvas = canvas
      // 2.初始化数据(当前播放的图片列表)
      this.currentFrameList = this.imgListAll[this.currentIdx].imgArr

      this.imgsLen = this.currentFrameList.length
      this.recordTo = this.imgsLen - 1
      // 3.设置封面
      this.drawImg(this.imgListAll[this.currentIdx].cover)
      this.currentFrame = this.currentFrameList[0].frame
    },
    // 加载图片
    drawImg(imgSrc) {
      const img = new Image()
      const that = this
      img.onload = function() {
        img.onload = null
        that.ctx.clearRect(0, 0, that.canvas.width, that.canvas.height)
        // 判断图片有没有宽度
        if (img.width !== 0) {
          // 设置画布的宽高,并使图片居中显示
          let imgWidth
          let imgHeight
          if (img.width <= 654) {
            if (img.height > 480) {
              imgHeight = 480
              imgWidth = img.width * (480 / img.height)
            } else {
              imgHeight = img.height
              imgWidth = img.width
            }
          } else {
            if (img.height > 480) {
              const a = 654 / img.width
              const b = 480 / img.height
              if (a <= b) {
                imgWidth = img.width * a
                imgHeight = img.height * a
              } else {
                imgWidth = img.width * b
                imgHeight = img.height * b
              }
            } else {
              imgWidth = 654
              imgHeight = img.height * (654 / img.width)
            }
          }
          that.ctx.drawImage(
            img,
            Math.round((654 - imgWidth) / 2),
            Math.round((480 - imgHeight) / 2),
            imgWidth,
            imgHeight
          )
        }
      }
      img.onerror = function(e) {
        console.log(e)
      }
      img.src = imgSrc
    },
    // 播放方法 | 从当前位置播放动画,会继承上次使用fromTo、form或to的属性
    play() {
      if (this.currentFrameList.length === 0) {
        return
      }
      this.fromTo(this.recordFrom, this.recordTo, this.loop)
      this.ispause = false
    },
    fromTo(from, to, loop) {
      const self = this

      // 先清除上次未执行完的动画
      clearInterval(this.timer)

      const timeFn = function timeFn() {
        if (self.ispause) {
          return
        }
        // 当总量计数器达到帧总量的时候退出
        if (self.plusNum <= self.plusCount) {
          self.resetData()
          return
        } else {
          // 未达到,继续循环
          // 帧计数器
          self.count++
          // 一次循环结束,重置keyCount为from
          if (self.count > to) {
            self.count = from
          }
          self.goto(self.count)
          // 总量计数器
          self.plusCount++
        }
      }
      // 总量计数器
      this.plusCount = 0

      loop = !loop || loop === 'infinite' ? this.infinite : loop

      // 帧总量 帧数*循环次数first
      this.plusNum = (to - from + 1) * loop
      this.ispause = true

      this.recordFrom = from
      this.recordTo = to
      this.loop = loop

      timeFn()
      this.timer = setInterval(timeFn, 1000 / this.fps)
    },
    // 暂停
    pause() {
      this.ispause = true
    },
    // 跳到某一帧
    goto(n) {
      this.count = n
      this.drawImg(this.currentFrameList[n].imgUrl)
    },
    //  上一帧
    prev() {
      this.ispause = true
      const n = this.count - 1 < 0 ? this.imgsLen - 1 : this.count - 1
      this.goto(n)
    },
    //  下一帧
    next() {
      this.ispause = true
      const n = this.count + 1 >= this.imgsLen ? 0 : this.count + 1
      this.goto(n)
    },
    // 置顶置尾
    goFor(type) {
      this.resetData()
      if (type === 'frist') {
        this.resetData()
        this.goto(0)
      } else {
        this.resetData()
        this.goto(this.currentFrameList.length - 1)
      }
    },
    //  销毁对象
    destroy() {
      clearInterval(this.timer)
      this.timer = null
      this.ctx = null
      this.canvas && this.canvas.remove()
      this.canvas = null
    }
  }
}
</script>
<style lang="scss" scoped>
.videoBoxCls {
  display: flex;
  justify-content: center;
  align-items: center;
  padding-top: 10px;
  box-sizing: border-box;
  .left-list {
    width: 225px;
    flex: 0 0 auto;
    height: 550px;
    border-right: 1px solid #ccc;
    padding: 0 16px 0 9px;
    box-sizing: border-box;
    overflow: auto;
    /* //滚动条的宽度 */
    &::-webkit-scrollbar {
      width: 5px !important;
      height: 5px !important;
    }
    .item-cls {
      width: 200px;
      position: relative;
      &:hover {
        .hoverTiltle {
          display: block;
        }
      }
      .hoverTiltle {
        display: none;
        background: #ffffff;
        padding: 0 5px;
        position: fixed;
        color: #333 !important;
        font-size: 12px;
        line-height: 24px;
        white-space: nowrap;
        z-index: 99999;
      }
      .img-box {
        width: 200px;
        height: 128px;
        cursor: pointer;
        display: flex;
        justify-content: center;
        align-items: center;
        img {
          max-width: 196px;
          max-height: 124px;
        }
      }
      .current {
        border: 2px solid #3390e0;
      }
      .img-title {
        width: 100%;
        height: 34px;
        line-height: 34px;
        text-align: center;
        padding: 0 10px;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        cursor: default;
      }
    }
  }
  .right-con {
    width: 654px;
    height: 540px;
    flex: 0 0 auto;
    margin: 0 10px 10px;
    .videoContainer {
      width: 654px;
      height: 480px;
      .el-loading-mask {
        background-color: rgba(255, 255, 255, 0.3);
      }
    }
    .frameBox {
      width: 100%;
      border: 1px solid #dbdbdb;
      .frame {
        width: 100%;
        display: flex;
        justify-content: space-between;
        align-items: flex-end;
        height: 20px;
        // background: #EDEDED;
        // padding: 0 6px;
        box-sizing: border-box;
        // border-bottom: 1px solid #DBDBDB;
        .frameItem {
          height: 12px;
          flex: 1 1 auto;
          cursor: pointer;
          position: relative;
          &:hover {
            .hovFrame {
              display: block;
            }
          }
          .hovFrame {
            position: absolute;
            top: -27px;
            left: 0;
            background: #e5e5e5;
            // height: 26px;
            line-height: 26px;
            padding: 0 5px;
            display: none;
            white-space: nowrap;
          }
        }
        .error {
          background: #ededed;
        }
        .success {
          background: #d4d4d4;
          &:hover {
            background: #3390e0;
          }
        }
        .currentFrane {
          background: #3390e0;
        }
      }
      .videoBtn {
        width: 100%;
        height: 38px;
        position: relative;
        .frame-title {
          position: absolute;
          top: 6px;
          left: 9px;
          background: #e5e5e5;
          height: 26px;
          line-height: 26px;
          padding-left: 18px;
          padding-right: 27px;
          font-size: 12px;
          color: #333;
          border-radius: 2px;
        }
        .btn-box {
          margin: 0 auto;
          width: 145px;
          height: 38px;
          display: flex;
          justify-content: space-between;
          align-items: center;
          .goTopCls {
            width: 12px;
            height: 13px;
            cursor: pointer;
            background-size: cover;
            background-image: url('~@/assets/images/videoPre/com-t.png');
            &:hover {
              background-image: url('~@/assets/images/videoPre/hover-t.png');
            }
            &:active {
              background-image: url('~@/assets/images/videoPre/click-t.png');
            }
          }
          .prevCls {
            width: 8px;
            height: 13px;
            cursor: pointer;
            background-size: cover;
            background-image: url('~@/assets/images/videoPre/com-l.png');
            &:hover {
              background-image: url('~@/assets/images/videoPre/hover-l.png');
            }
            &:active {
              background-image: url('~@/assets/images/videoPre/click-l.png');
            }
          }
          .play {
            width: 22px;
            height: 22px;
            cursor: pointer;
            background-size: cover;
            background-image: url('~@/assets/images/videoPre/com-p.png');
            &:hover {
              background-image: url('~@/assets/images/videoPre/hover-p.png');
            }
            &:active {
              background-image: url('~@/assets/images/videoPre/click-p.png');
            }
          }
          .pause {
            width: 22px;
            height: 22px;
            cursor: pointer;
            background-size: cover;
            background-image: url('~@/assets/images/videoPre/com-v.png');
            &:hover {
              background-image: url('~@/assets/images/videoPre/hover-v.png');
            }
            &:active {
              background-image: url('~@/assets/images/videoPre/click-v.png');
            }
          }
          .nextCls {
            width: 8px;
            height: 13px;
            cursor: pointer;
            background-size: cover;
            background-image: url('~@/assets/images/videoPre/com-r.png');
            &:hover {
              background-image: url('~@/assets/images/videoPre/hover-r.png');
            }
            &:active {
              background-image: url('~@/assets/images/videoPre/click-r.png');
            }
          }
          .goLastCls {
            width: 12px;
            height: 13px;
            cursor: pointer;
            background-size: cover;
            background-image: url('~@/assets/images/videoPre/com-b.png');
            &:hover {
              background-image: url('~@/assets/images/videoPre/hover-b.png');
            }
            &:active {
              background-image: url('~@/assets/images/videoPre/click-b.png');
            }
          }
        }
      }
    }
  }
}
</style>

Tips:进度条根据图片数绘制

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值