Video playback timeline

This article will take you to teach you how to write a timeline component by hand, the kind that Xiaobai can also learn!!!

Look at the effect first   https://live.csdn.net/v/277150

Function description: Drag and drop the time axis, zoom in and zoom out with the mouse, change the scale time with the playing video, etc.

Development framework: vue2.x (because the project uses vue2.x, of course other frameworks can be used with minor changes) 

Requirements: Have a basic understanding of canvas, it is best to understand, it is easy to understand the code. If you don’t understand it, it will not affect it. I only heard about it before I developed this project, and I don’t understand it. Hang a canvas document Canvas - Web API interface reference |  MDN

Ideas: Should there be a middle scale? Shouldn’t there be a time scale? Should there be a date/time? Drag and drop to move? Should there be a zoom function? Shouldn’t the time axis be moved according to the video playback progress? Take your own goals step by step Realization, at the beginning, I thought about using dom operations to realize the time axis function, (fixed width, cycle scale, display and hide mode switching to achieve zooming) I wrote it for about two days, and after adding the moving time, the scrolling effect was not silky, and I fell into it for a while. Contemplating... Finally, I found a document that uses canvas to implement: Hands-on and guide you to implement a timeline component-Nuggets

The basic idea of ​​this article is to refer to this article, add some functions, and also discard some functions, depending on the actual needs of the individual, without further ado, just open it..

Install moment dependency (this time component dependency will be used in the project)

npm install  moment

Vue page introduction  

 import moment from "moment";

Vue page html part

<template>
  <div class="timeLineContainer" ref="timeLineContainer">
    <canvas
      ref="canvas"
      @mousemove="onMousemove"
      @mousedown="onMousedown"
      @mousewheel="onMouseweel"
      @mouseup="onMouseup"
      @mouseout="onMouseout"
    ></canvas>
  </div>
</template>

Vue page data data

 data() {
    return {
      //时间分辨对应的层级
      currentZoomIndex: 0,
      // 中间刻度的当前时间 (默认为当天的0点减12小时,即昨天中午12点,若有操作时间则为操作后的时间)
      currentTime: new Date(moment().format("YYYY-MM-DD 00:00:00")).getTime(),
      // 时间轴左侧起点所代表的时间,默认为当天的0点减12小时,即昨天中午12点
      startTimestamp:
        new Date(moment().format("YYYY-MM-DD 00:00:00")).getTime() -
        12 * ONE_HOUR_STAMP +
        15 * 60 * 1000,
      width: null, //画布容器宽度
      height: null, //画布容器高度
      mousedown: false, // 移动开关
      ctx: null, //画布容器
      mousedownX: null, // 鼠标相当于时间轴左侧的距离
      //时间段数据 
      timeSegments: [
        {
          beginTime: new Date("2023-02-18 02:30:00").getTime(),
          endTime: new Date("2023-02-18 11:20:00").getTime(),
          style: {
            background: "#5881CF",
          },
        },
      ],
      timer: null,//定时器
    };
  },

The initialization method in methods obtains the registered length and width of the canvas container  

Reminder: When calling the init method, it is recommended to call it in this.$nextTick() after dom registration

init() {
      // 获取外层宽高
      let { width, height } = this.$refs.timeLineContainer.getBoundingClientRect();
      this.width = width;
      this.height = height;
      // 设置画布宽高为外层元素宽高
      this.$refs.canvas.width = width;
      this.$refs.canvas.height = height;
      // 获取画图上下文
      this.ctx = this.$refs.canvas.getContext("2d");
      //绘制
      this.draw();
    },

draw method

 draw() {
      this.drawScaleLine();//绘制时间刻度
      this.drawTimeSegments(); //绘制时间段
      this.drawMiddleLine();//绘制中线  绘制原则  想要谁的层级再最上面的随后绘制 前提是层级一样的时候
    },

First, draw the middle line and execute the drawMiddleLine method. Since the drawing of the canvas is frequently used, the method of drawing the line is encapsulated as

drawLine('start x-axis', start "y-axis", 'content x-axis', 'content y-axis', "content width", "content color") parameters currently only use five (custom)

    // 画中间的白色竖线
    drawMiddleLine() {
      //中线的宽度
      let lineWidth = 2;
      // 线的x坐标是时间轴的中点,y坐标即时间轴的高度
      let x = this.width / 2;
      //划线
      this.drawLine(x, 0, x, this.height, lineWidth, "#fff");
    },

drawLine method, try to understand it, if you don’t understand it, it’s a method of drawing a line

  // 画线段方法
    drawLine(x1, y1, x2, y2, lineWidth, color) {
      // 开始一段新路径
      this.ctx.beginPath();
      // 设置线段颜色
      this.ctx.strokeStyle =color || "#fff";
      // 设置线段宽度
      this.ctx.lineWidth = lineWidth || 1;
      // 将路径起点移到x1,y1
      this.ctx.moveTo(x1, y1);
      // 将路径移动到x2,y2
      this.ctx.lineTo(x2, y2);
      // 把路径画出来
      this.ctx.stroke();
    },

So far, we can see a white line (color can be customized) in the middle of our container, as shown in the figure below

 Of course, there is still a long way to go from the final goal, but at least until now, you can ensure that the code does not report an error, which means that you have succeeded ***%

Continue to clarify the purpose, draw the scale, draw the time, and only have event interaction to complete 

Define two global variables to control scaling and calculation

// 一小时的毫秒数
const ONE_HOUR_STAMP = 60 * 60 * 1000;
// 时间分辨率
const ZOOM = [0.5, 1, 2, 6, 12, 24];

Here comes the most important part. It is recommended to read and understand this section of code after all the business is completed. Read the code first.

   //画刻度
    drawScaleLine() {
      // 时间分辨率对应的每格小时数
      const ZOOM_HOUR_GRID = [1 / 60, 1 / 60, 2 / 60, 1 / 6, 0.25, 0.5];

      // 一共可以绘制的格数,时间轴的时间范围小时数除以每格代表的小时数,24/0.5=48
      let gridNum =
        ZOOM[this.currentZoomIndex] / ZOOM_HOUR_GRID[this.currentZoomIndex];

      // 一格多少毫秒,将每格代表的小时数转成毫秒数就可以了  ;
      let msPerGrid = ZOOM_HOUR_GRID[this.currentZoomIndex] * ONE_HOUR_STAMP;

      // 每格宽度,时间轴的宽度除以总格数
      let pxPerGrid = this.width / gridNum;

      // 时间偏移量,初始时间除每格时间取余数,
      let msOffset = msPerGrid - (this.startTimestamp % msPerGrid);
      // 距离偏移量,时间偏移量和每格时间比例乘每格像素
      let pxOffset = (msOffset / msPerGrid) * pxPerGrid;

      // 时间分辨率对应的时间显示判断条件
      const ZOOM_DATE_SHOW_RULE = [
        () => {
          // 全都显示
          return true;
        },
        (date) => {
          // 每五分钟显示
          return date.getMinutes() % 5 === 0;
        },
        (date) => {
          // 显示10、20、30...分钟数
          return date.getMinutes() % 10 === 0;
        },
        (date) => {
          // 显示整点和半点小时
          return date.getMinutes() === 0 || date.getMinutes() === 30;
        },
        (date) => {
          // 显示整点小时
          return date.getMinutes() === 0;
        },
        (date) => {
          // 显示2、4、6...整点小时
          return date.getHours() % 2 === 0 && date.getMinutes() === 0;
        },
      ];

      for (let i = 0; i < gridNum; i++) {
        // 横坐标就是当前索引乘每格宽度
        let x = pxOffset + i * pxPerGrid;
        // 当前刻度的时间,时间轴起始时间加上当前格子数乘每格代表的毫秒数
        let graduationTime = this.startTimestamp + msOffset + i * msPerGrid;
        // 时间刻度高度  根据刻/时/月展示高度不同  具体可以自己去定义
        let h = 0;
        let date = new Date(graduationTime);
        if (date.getHours() === 0 && date.getMinutes() === 0) {
          // 其他根据判断条件来显示
          h = this.height * 0.3;
          // 刻度线颜色
          this.ctx.fillStyle = "rgba(151,158,167,1)";
          // 显示时间
          this.ctx.fillText(
            this.graduationTitle(graduationTime),
            x - 13,// 向左平移一半
            h + 15 // 加上行高
          );
        } else if (ZOOM_DATE_SHOW_RULE[this.currentZoomIndex](date)) {
          h = this.height * 0.2;
          this.ctx.fillStyle = "rgba(151,158,167,1)";
          this.ctx.fillText(
            this.graduationTitle(graduationTime),
            x - 13,
            h + 15
          );
        } else {
          // 其他不显示时间
          h = this.height * 0.15;
        }
        this.drawLine(x, 0, x, h, 1, "#fff");
      }
    },

Among them, a graduationTitle method is used to display the date at 0:00. The content of the method is as follows

   //格式时间的,在0点时显示日期而不是时间
    graduationTitle(datetime) {
      let time = moment(datetime);
      // 0点则显示当天日期
      if (
        time.hours() === 0 &&
        time.minutes() === 0 &&
        time.milliseconds() === 0
      ) {
        return time.format("MM-DD");
      } else {
        // 否则显示小时和分钟
        return time.format("HH:mm");
      }
    },

Now we can see our scale clearly on the page, but there is no substantial interaction with the scale,

Execute according to the idea. When the mouse is clicked and dragged, the scrolling will be realized. Move the display time. When the mouse is left and dragged, the midline time will be obtained to notify the playback video when it should be played back. Just do what you say

First record the time when the mouse is clicked onMousedown method

  //鼠标按下的操作
    onMousedown(e) {
      let { left } = this.$refs.canvas.getBoundingClientRect();
      // 也是计算鼠标相当于时间轴左侧的距离
      this.mousedownX = e.clientX - left;
      // 设置一下标志位
      this.mousedown = true;
      // 缓存一下鼠标按下时的起始时间点
      this.mousedownCacheStartTimestamp = this.startTimestamp;
    },

The movement event onMousemove is divided into click movement and non-click movement. Click movement is dragging, and non-click movement is panning

    // 鼠标移动事件
    onMousemove(e) {
      // 计算出相对画布的位置
      let { left } = this.$refs.canvas.getBoundingClientRect();
      let x = e.clientX - left;
      // 计算出时间轴上每毫秒多少像素
      const PX_PER_MS =
        this.width / (ZOOM[this.currentZoomIndex] * ONE_HOUR_STAMP); // px/ms
       //拖拽时候
      if (this.mousedown) {
        // 计算鼠标当前相当于鼠标按下那个点的距离
        let diffX = x - this.mousedownX;
        // 用鼠标按下时的起始时间点减去拖动过程中的偏移量,往左拖是负值,减减得正,时间就是在增加,往右拖时间就是在减少
        this.startTimestamp =
          this.mousedownCacheStartTimestamp - Math.round(diffX / PX_PER_MS);
        // 不断刷新重绘就ok了
        this.ctx.clearRect(0, 0, this.width, this.height);
        this.draw();
      } else {
        // 计算所在位置的时间  平移时候
        let time = this.startTimestamp + x / PX_PER_MS;
        // 清除画布
        this.ctx.clearRect(0, 0, this.width, this.height);
        this.draw();
        // 绘制实时的竖线及时间
        this.drawLine(x, 0, x, this.height * 0.3, "#fff", 1);
        this.ctx.fillStyle = "#fff";
        this.ctx.fillText(
          moment(time).format("YYYY-MM-DD HH:mm:ss"),
          x - 20,
          this.height * 0.3 + 20
        );
      }
    },

When the mouse is released, it triggers the time to get the middle scale. onMouseup gets the timestamp and saves it in currentTime

//鼠标起来的操作
    onMouseup() {
      // 设置一下标志位 移动取消
      this.mousedown = false;
      //中间刻度距离左侧画布左侧距离
      let x = this.width / 2;
      // 计算出时间轴上每毫秒多少像素
      const PX_PER_MS =
        this.width / (ZOOM[this.currentZoomIndex] * ONE_HOUR_STAMP); // px/ms
      // 计算中间位置刻度的时间位置的时间
      this.currentTime = this.startTimestamp + x / PX_PER_MS;
    },

Clear the date and time of the translation when the mouse moves out of the container onMouseout event

   //鼠标移出事件
    onMouseout() {
      // 清除画布
      this.ctx.clearRect(0, 0, this.width, this.height);
      //重新绘制画布
      this.draw();
    },

So far, the basic functions of the timeline at the current position have been almost completed. Let’s take a look at the effect.

 It was mentioned before that the time scale effect can be zoomed when the mouse wheel scrolls. The currentTime in the onMouseweel event event must be the time scale of the positive line. Otherwise, the time scale will change when zooming again (the first point is to give currentTime a value when reinitializing, and the second point The second point calculates the midline scale time after dragging)

  //鼠标滚动事件
    onMouseweel(event) {
      let e = window.event || event;
      let delta = Math.max(-1, Math.min(1, e.wheelDelta || -e.detail));
      if (delta < 0) {
        // 缩小
        if (this.currentZoomIndex + 1 >= ZOOM.length - 1) {
          this.currentZoomIndex = ZOOM.length - 1;
        } else {
          this.currentZoomIndex++;
        }
      } else if (delta > 0) {
        // 放大
        if (this.currentZoomIndex - 1 <= 0) {
          this.currentZoomIndex = 0;
        } else {
          this.currentZoomIndex--;
        }
      }
      this.ctx.clearRect(0, 0, this.width, this.height);
      // 重新计算起始时间点,当前时间-新的时间范围的一半
      this.startTimestamp =
        this.currentTime - (ZOOM[this.currentZoomIndex] * ONE_HOUR_STAMP) / 2;
      this.draw();
    },

Display picture: Omit, self-test, scroll with the mouse to zoom and change the time scale

Think about it. In the end, only the time axis is left to move by itself (timer). According to the actual situation, I am here to get the intermediate playback time to request playback of the video stream. After the video is played, there is a play callback, and then the callback is directly executed in the pause Clear timer in callback

play() and pause() depends on each player's player  

 //播放
play(){
    this.timer = setInterval(() => {
      //项目中我设置的是1秒钟移动一下刻度,结合实际情况分析  需要考虑跟播放速度 我本地项目不涉及到播放速度暂未考虑
      this.startTimestamp += 1000;
      //记录中间位置刻度 否者滚动之后中间刻度位置丢失
      this.onMouseup()
      // 不断刷新重绘就ok了
      this.ctx.clearRect(0, 0, this.width, this.height);
      this.draw();
    }, 1000);
}
//暂停
pause(){
    clearInterval(this.timer); 
}

Be sure to clear the timer when the page is destroyed

  beforeDestroy() {
    clearInterval(this.timer);
  },

You're done~~~

Expand to display a custom time period in the canvas (to put it bluntly, I want to display the whole day of February 20, 2023 and mark it in the timeline)  

Call a drawTimeSegments method in the draw method. The timeSegments object in the method has format requirements. You can refer to the format of timeSegments in data. The color/style can be customized  

Distance timeSegments parameter:

timeSegments: [
        {
          beginTime: new Date("2023-02-20 00:00:00").getTime(),
          endTime: new Date("2023-02-20 23:59:59").getTime(),
          style: {
            background: "#ffff00",
          },
        },
      ],
 //绘制时间段 开始到结束时都在
    drawTimeSegments() {
      const PX_PER_MS =
        this.width / (ZOOM[this.currentZoomIndex] * ONE_HOUR_STAMP); // px/ms
      this.timeSegments.forEach((item) => {
        if (
          item.beginTime <=
            this.startTimestamp +
              ZOOM[this.currentZoomIndex] * ONE_HOUR_STAMP &&
          item.endTime >= this.startTimestamp
        ) {
          let x = (item.beginTime - this.startTimestamp) * PX_PER_MS;
          let w;
          if (x < 0) {
            x = 0;
            w = (item.endTime - this.startTimestamp) * PX_PER_MS;
          } else {
            w = (item.endTime - item.beginTime) * PX_PER_MS;
          }
          this.ctx.fillStyle = item.style.background;
          this.ctx.fillRect(x, this.height * 0.6, w, this.height * 0.3);
        }
      });
    },

 It's finally over... If there are any deficiencies, please leave a message to correct me  

Guess you like

Origin blog.csdn.net/weixin_56421672/article/details/129124292