HTML:SVG+Canvas+Video学习

2 阅读16分钟

HTML 是网页使用的语言,定义了网页的结构和内容。浏览器访问网站,其实就是从服务器下载 HTML 代码,然后渲染出网页。HTML 的全名是“超文本标记语言”(HyperText Markup Language),它的最大特点就是支持超链接,点击链接就可以跳转到其他网页,从而构成了整个互联网。1999年,HTML 4.01 版发布,成为广泛接受的 HTML 标准。2014年,HTML 5 发布,这是目前正在使用的版本。下面我主要给大家介绍其中的图像和多媒体相关的部分。

SVG

SVG代表可缩放矢量图形,它基本上以 XML 格式定义基于矢量的图形。**SVG 图形在缩放或调整大小时不会损失任何质量。**SVG 文件中的每个元素和每个属性都可以设置动画。

SVG 的优点:与其他图像格式(如 JPEG 和 GIF)相比,使用 SVG 的优点是:

  • SVG 图像可以使用任何文本编辑器创建和编辑。
  • SVG 图像可以被搜索、索引、脚本化和压缩。
  • SVG 图像是可缩放的。
  • SVG 图像可以在任何分辨率下高质量打印。

基本使用

SVG可以直接插入网页称为DOM的一部分,然后使用js和css进行操作。也可以写在一个独立文件之中,然后用<img><object><embed><iframe>等标签插入网页。css中也可以使用svg文件。

将svg的代码都放在顶层标签svg之中。

<svg width="100" height="100" viewBox="50 50 50 50">
  <circle id="mycircle" cx="50" cy="50" r="50" />
</svg>

其中,

<svg>width属性和height属性,指定了 SVG 图像在 HTML 元素中所占据的宽度和高度。除了相对单位,也可以采用绝对单位(单位:像素)。如果不指定这两个属性,SVG 图像的大小默认为300像素(宽)x 150像素(高)。

<viewBox>属性的值有四个数字,分别是左上角的横坐标和纵坐标、视口的宽度和高度。

视口必须适配所在的空间。viewBox指定了视口大小是50x50,由于svg的大小是100x100,所以视口会放大取适配svg中图像的大小

基本形状

  • 标签:用来绘制圆形,<circle>标签的cxcyr属性分别为横坐标、纵坐标和半径,单位为像素。坐标都是相对于<svg>画布的左上角原点

  • 标签:用来绘制直线。<line>标签的x1属性和y1属性,表示线段起点的横坐标和纵坐标;x2属性和y2属性,表示线段终点的横坐标和纵坐标;

  • 标签:用于绘制一根折线,<polyline>points属性指定了每个端点的坐标,横坐标与纵坐标之间与逗号分隔,点与点之间用空格分隔。

  • 标签:用于绘制矩形。<rect>x属性和y属性,指定了矩形左上角端点的横坐标和纵坐标;width属性和height属性指定了矩形的宽度和高度(单位像素)。

  • 标签:用于绘制椭圆。<ellipse>cx属性和cy属性,指定了椭圆中心的横坐标和纵坐标(单位像素);rx属性和ry属性,指定了椭圆横向轴和纵向轴的半径(单位像素)。

  • 标签:用于绘制多边形。<polygon>points属性指定了每个端点的坐标,横坐标与纵坐标之间与逗号分隔,点与点之间用空格分隔。

  • 标签:用于制路径。<path>d属性表示绘制顺序,它的值是一个长字符串,每个字母表示一个绘制动作,后面跟着坐标。

    • M:移动到(moveto)
    • L:画直线到(lineto)
    • Z:闭合路径
  • 标签:用于绘制文本。<text>x属性和y属性,表示文本区块基线(baseline)起点的横坐标和纵坐标。

  • 标签:用于复制一个形状。<use>href属性指定所要复制的节点,x属性和y属性是<use>左上角的坐标。另外,还可以指定widthheight坐标。

  • 标签:用于将多个形状组成一个组(group),方便复用。

  • 标签:用于自定义形状,它内部的代码不会显示,仅供引用。

  • 标签:用于自定义一个形状,该形状可以被引用来平铺一个区域

  • 标签:用于插入图片文件。

  • 标签:用于产生动画效果。

  • 使用代码

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>SVG</title>
</head>
<body>
  <h2>SVG相关标签</h2>
  <!--圆形-->
  <svg style="width: 200;height: 200;border: 1px solid #000;">
    <circle cx="100" cy="100" r="50" stroke="black" stroke-width="2" fill="grey"></circle>
  </svg>
   <!--直线-->
  <svg style="width: 200;height: 200;border: 1px solid #000;">
    <line x1="10" y1="10" x2="180" y2="180" style="stroke:pink;stroke-width:3" />
  </svg>
    <!-- 折线 -->
  <svg style="width: 200;height: 200;border: 1px solid #000;">
    <polyline points="10,10 180,20  150,150  10,10" style="stroke:pink;stroke-width:3;fill:bisque" />
  </svg>
  <!--矩形-->
  <svg style="width: 200;height: 200;border: 1px solid #000;">
    <rect x="50" y="50" width="100" height="100" style="fill:cadetblue;stroke-width: 3;stroke: black" />
    <!-- <rect x="50" y="50" width="100" height="100" rx="20" ry="20" style="fill:chocolate;stroke-width: 3;stroke: black" /> -->
  </svg>
  <!-- 椭圆 -->
  <svg style="width: 200;height: 200;border: 1px solid #000;">
    <ellipse cx="100" cy="100" ry="40" rx="80" stroke="black" stroke-width="5" fill="silver"/>
  </svg>
  <!-- 多边形 -->
  <svg style="width: 200;height: 200;border: 1px solid #000;">
    <polygon points="100,10 40,198 190,78 10,78 160,198" style="fill: grey; stroke: orange;stroke-width: 5; fill-rule: evenodd" />
  </svg>
  <!-- 路径 -->
  <svg style="width: 200;height: 200;border: 1px solid #000;">
    <path d="
      M 18,3
      L 46,3
      L 46,40
      L 61,40
      L 32,68
      L 3,40
      L 18,40
      Z
    "></path>
    <text x="100" y="50">这是一个路径</text>
  </svg>
  <!-- use复制 -->
  <svg style="width: 200;height: 200;border: 1px solid #000;">
    <circle id="myCircle" cx="100" cy="100" r="20" />
    <use href="#myCircle" x="50" y="50" fill="yellow" stroke="blue" />
    <text x="0" y="50">use的位置是相对复制的圆形计算的</text>
  </svg>
  <!-- group标签 -->
  <svg style="width: 200;height: 200;border: 1px solid #000;">
    <g id="myGroup">
      <text x="25" y="20">圆形</text>
      <circle cx="50" cy="50" r="20"/>
    </g>
  
    <use href="#myGroup" x="50" y="50" fill="blue" />
    <use href="#myGroup" x="100" y="100" fill="white" stroke="blue" />
  </svg>
  <!-- defs标签 -->
  <svg style="width: 200;height: 200;border: 1px solid #000;">
    <defs>
      <g id="myDefs">
        <text x="25" y="20">圆形</text>
        <circle cx="50" cy="50" r="20"/>
      </g>
    </defs>

    <use href="#myDefs" x="50" y="50" fill="blue" />
    <use href="#myDefs" x="100" y="100" fill="white" stroke="blue" />
  </svg>
  <!-- pattern标签 -->
  <svg style="width: 200;height: 200;border: 1px solid #000;">
    <defs>
      <pattern id="dots" x="0" y="0" width="100" height="100" patternUnits="userSpaceOnUse">
        <circle fill="#bee9e8" cx="50" cy="50" r="35" />
      </pattern>
    </defs>
    <rect x="0" y="0" width="100%" height="100%" fill="url(#dots)" />
  </svg>
  <!-- 动画 -->
  <svg style="width: 200;height: 200;border: 1px solid #000;">
    <rect x="0" y="0" width="50" height="50" fill="#feac5e">
      <animate attributeName="x" from="0" to="200" dur="2s" repeatCount="indefinite" />
      <!-- <animate attributeName="y" from="0" to="200" dur="2s" repeatCount="indefinite" /> -->
    </rect>
  </svg>
 
</body>
</html>

js操作

DOM操作

var mycircle = document.getElementById('mycircle');

mycircle.addEventListener('click', function(e) {
  console.log('circle clicked - enlarging');
  mycircle.setAttribute('r', 60);
}, false);

由于 SVG 文件就是一段 XML 文本,因此可以通过读取 XML 代码的方式,读取 SVG 源码。使用XMLSerializer实例的serializeToString()方法,获取 SVG 元素的代码。

var svgString = new XMLSerializer()
  .serializeToString(document.querySelector('svg'));

SVG转为Canvas:首先,需要新建一个Image对象,将 SVG 图像指定到该Image对象的src属性。然后,当图像加载完成后,再将它绘制到<canvas>元素。 效果展示

var img = new Image();
var svg = new Blob([svgString], {type: "image/svg+xml;charset=utf-8"});
var DOMURL = self.URL || self.webkitURL || self;
var url = DOMURL.createObjectURL(svg);
img.src = url;
img.onload = function () {
  var canvas = document.getElementById('canvas');
  var ctx = canvas.getContext('2d');
  ctx.drawImage(img, 0, 0);
};

渐变

  • 线性渐变:在 SVG 文件的defs 元素内部,创建一个<linearGradient> 节点。

    其中线性渐变包括x1、x2、y1、y2这四个属性,用来控制渐变的大小和方向。

    • 当y1和y2相等,而x1和x2不同时,可创建水平渐变
    • 当x1和x2相等,而y1和y2不同时,可创建垂直渐变
    • 当x1和x2不同,且y1和y2不同时,可创建角形渐变

    offset用来设置色标位置 stop-color用来设置色标颜色 stop-opacity用来设置色标的透明度

<svg width="100" height="230" version="1.1" xmlns="http://www.w3.org/2000/svg">
    <defs>
      <linearGradient id="Gradient1" >
        <stop  offset="0%" stop-color="lavenderblush" stop-opacity="1" />
        <stop  offset="50%" stop-color="darksalmon" stop-opacity="1" />
        <stop  offset="100%" stop-color="rgb(221, 88, 43)" stop-opacity="1"/>
      </linearGradient>
    </defs>
  
    <rect id="rect1" x="0" y="10" rx="15" ry="15" width="100" height="100"  fill="url(#Gradient1)" />
  </svg>
  • 径向渐变:它是从一个点开始发散绘制渐变。在文档的defs中添加一个<radialGradient>元素。

    与线性渐变的x1、y1、x2、y2属性不同,径向渐变使用cx、cy、r、fx、fy这五个属性来设置渐变。

    r 设置圆的半径 cx、cy 定义渐变的中心点坐标 fx、fy 定义渐变的焦点坐标

<svg width="250" height="250" version="1.1" xmlns="http://www.w3.org/2000/svg">
    <defs>
      <radialGradient id="RadialGradient1">
        <stop offset="0%" stop-color="red" />
        <stop offset="100%" stop-color="pink" />
      </radialGradient>
    </defs>
  
    <rect x="10"  y="10" rx="15" ry="15" width="100" height="100" fill="url(#RadialGradient1)" />
  </svg>

剪切和遮罩

剪切:clipPath属性用于剪切,ClipPath里的元素不显示,只是用来确定剪切下来要展示的部分。

 <svg width="250" height="250">
  <defs>
    <clipPath id="cut-off-bottom">
      <rect x="0" y="0" width="200" height="100" fill="pink"/>
    </clipPath>
  </defs>
  <circle cx="100" cy="100" r="100" clip-path="url(#cut-off-bottom)" fill="green"/>
</svg>

**遮罩:**遮罩类似于我们的mask效果,会覆盖在原本的元素上。这里我们以常见的“元素淡出”效果举例。

<svg width="250" height="250">
  <defs>
    <linearGradient id="Gradient">
      <stop offset="0" stop-color="white" stop-opacity="0" />
      <stop offset="1" stop-color="white" stop-opacity="1" />
    </linearGradient>
    <mask id="Mask">
      <rect x="0" y="0" width="200" height="200" fill="url(#Gradient)"  />
    </mask>
  </defs>

  <rect x="0" y="0" width="200" height="200" fill="green" />
  <rect x="0" y="0" width="200" height="200" fill="red" mask="url(#Mask)" />
</svg>

滤镜

在 SVG 当中,滤镜功能无疑是最强大的功能。它让我们对输出的图像可以进行像素级的控制。通过使用一些内置的滤镜功能可以快速达到我们期望的显示效果。

什么是滤镜?滤镜是用于图像的呈现特殊效果的一个工具。通过滤镜我们能够原子级参与图像的显像过程,控制最终的图片输出

在css中内置了很多滤镜,例如高斯模糊(blur)、灰度(grayscale)等,SVG 元素通过 filter 属性设置滤镜。常见的部分滤镜如下:

  • <feGaussianBlur > - 模糊滤镜

  • <feOffset > - 位移滤镜

  • <feMerge> - 多滤镜叠加滤镜

  • <feBlend> -混合模式滤镜,包含normal( 正常)、multiply (正片叠底)、screen (滤色)、darken ( 变暗)、lighten(变亮)

  • <feImage> -提供像素数据作为输出

  • <feFlood>- 填充效果。

  • <feDropShadow>- 图像投影

  • <feDiffuseLighting>-光照处理。

  • 这里我们使用最常见的滤镜进行使用举例

    SourceAlpha 与 SourceGraphic 具有相同的规则除了 SourceAlpha 只使用元素的**非透明部分 **

<svg width="500" height="500" >
    <defs>
      <filter id="filter" width="200" height="200">
        <feGaussianBlur in="SourceAlpha" stdDeviation="5" result="blur" />
        <feOffset in="blur" dx="10" dy="10" result="offsetBlur" />

        <feMerge>
          <feMergeNode in="offsetBlur" />
          <!-- 将元素本身作为输入 -->
          <feMergeNode in="SourceGraphic" /> 
        </feMerge>
      </filter>
    </defs>
    <image x="0" y="0" width="100" height="100" 
    xlink:href="./src/daidai.png" filter="url(#filter)"></image>
  </svg>

Canvas

<canvas>元素用于生成图像。它本身就像一个画布,JavaScript 通过操作它的 API,在上面生成图像。它的底层是一个个像素,基本上<canvas>是一个可以用 JavaScript 操作的位图(bitmap)。它与 SVG 图像的区别在于,<canvas>是脚本调用各种方法生成图像,SVG 则是一个 XML 文件,通过各种子元素生成图像。

和video等标签类似,使用 Canvas API 之前,需要在网页里面新建一个<canvas>元素。每个canvas元素都对应一个上下文对象(CanvasRenderingContext2D),Cnavas API就定义在这个对象上。getContext()方法,返回的就是CanvasRenderingContext2D对象。

var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

注意,Canvas API 需要getContext方法指定参数2d,表示该<canvas>节点生成 2D 的平面图像。如果参数是webgl,就表示用于生成 3D 的立体图案,这部分属于 WebGL API。

绘制图形

  • 路径:

    • beginPath():开始绘制路径
    • closePath():结束路径,返回到起点,如果已经闭合或者只有一个点,无效果
    • moveTo():设置新路径的起点
    • lineTo():使用直线连接到当前坐标
    • fill():路径内部填充颜色
    • stroke():路径线条颜色
  • 矩形

    • ctx.rect(x,y,width,height) - 绘制矩形路径
    • ctx.strokeRect(x,y,width,height) - 绘制矩形
    • ctx.fillRect(x,y,width,height) - 绘制填充矩形
    • ctx.clearRect(x,y,width,height) - 清除矩形区域
  • 弧线:

    • ctx.arc(x,y,radius,start,end,anticlockwise) - 绘制圆形或扇形,anticlockwise顺时针还是逆时针
  • 文本

    • strokeText(string,x,y) - 绘制空心文字
    • fillText(string,x,y) - 绘制实心文字
  • 渐进色和图像填充

    • createLinearGradient(x1,y1,x2,y2) - 设置线性渐变色
    • ctx.createRadialGradient(x0, y0, r0, x1, y1, r1)-设置辐射渐变色。x0y0是辐射起始的圆的圆心坐标,r0是起始圆的半径,x1y1是辐射终止的圆的圆心坐标,r1是终止圆的半径。
  • 阴影

    • shadowOffsetX - 设置水平位移
    • shadowOffsetY - 设置垂直位移
    • shadowBlur - 设置模糊度
    • shadowColor - 阴影颜色

  • pattern:用于定义填充或描边样式
 const canvas = document.getElementById('myCanvas');
    const ctx = canvas.getContext('2d');

    const img = new Image();
    img.src = './src/daidai.png';

    img.onload = function() {
      const pattern = ctx.createPattern(img, 'repeat');
      ctx.fillStyle = pattern;
      ctx.fillRect(0, 0,2000, 2000);
    };

图像变换

  • rotate-图像旋转,它接受一个弧度值作为参数,表示顺时针旋转的度数。
  • scale-图像缩放,它接受两个参数,分别是x轴方向的缩放因子和y轴方向的缩放因子。
  • translate-图像平移,它接受两个参数,分别是 x 轴和 y 轴移动的距离(单位像素)。
  • transform-通过一个变换矩阵完成图像变换,接受一个变换矩阵的六个元素作为参数,完成缩放、旋转、移动和倾斜等变形。
  • setTransform-取消前面的图像变换,参数与transform()方法完全一致。

图像处理

  • drawImage() - 对图片进行重绘

  • getImageData()-读取 canvas 的内容,返回一个对象,包含了每个像素的信息。

  • putImageData() - 将ImageData对象的像素绘制在<canvas>画布上

  • toDataURL-对图像数据做出修改后,将 canvas 数据重新转化成一般的图像文件格式,然后可以进行另存本地或转发功能。

  • save - 保存上下文环境,将画布的当前样式保存到堆栈,相当于在内存之中产生一个样式快照

  • restore - 恢复到上一次保存的上下文环境

    • 粒子demo

      一个像素是有4个值(R,G,B,A)组成的。也就是说,数组信息每四个为一个像素点。因此,有以下规则,

      第一个像素信息为:RGBA(data[0],data[1],data[2],data[3])

      第二个像素信息为:RGBA(data[4],data[5],data[6],data[7])

      .....

      第N个像素信息为: RGBA(data[(n-1)*4],data[(n-1)*4+1],data[(n-1)*4+2],data[(n-1)*4+3])

  const canvas = document.getElementById('canvas');
  const ctx = canvas.getContext('2d');
  const image = document.getElementById('image');
  const canvas1 = document.getElementById('canvas1');
  const ctx1 = canvas1.getContext('2d');
  canvas1.width = 500;
  canvas1.height = 500;
  canvas.width = 500;
  canvas.height = 500;

  const particleSize = 4;
  const particleSpacing = 10;

  window.onload = function(){
      document.querySelector("#File").onchange=function(){
        var Reader = new FileReader();     // 创建文件读取对象 
        Reader.readAsDataURL(this.files[0]); //读取Blob,获取DataURL
        Reader.onload = function(){
          let img = new Image();
          img.src = Reader.result;      //将result赋值给Image对象的src
          img.onload = function(){
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, canvas.width, canvas.height);
            const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
            createParticles(imageData);
          }
          
        }
      }
    }
  function createParticles(imageData) {
    ctx1.clearRect(0, 0, canvas.width, canvas.height);

    for (let y = 0; y < canvas.height; y += particleSpacing) {
      for (let x = 0; x < canvas.width; x += particleSpacing) {
        const i = (y * canvas.width + x) * 4;
        const r = imageData.data[i];
        const g = imageData.data[i + 1];
        const b = imageData.data[i + 2];
        const a = imageData.data[i + 3];

        if (a) {
          ctx1.fillStyle = `rgba(${r}, ${g}, ${b}, ${a / 255})`;
          ctx1.beginPath();
          ctx1.arc(x, y, particleSize, 0, Math.PI * 2);
          ctx1.fill();
        }
      }
    }
  }

HTML SVG 和 HTML Canvas 之间的区别:

  • SVG 是一种用 XML 描述 2D 图形的语言,而 Canvas 使用 JavaScript 动态绘制 2D 图形。
  • 如果 SVG 对象的属性发生更改,浏览器可以自动重新渲染形状,而 Canvas 则逐像素渲染。在canvas中,一旦绘制了图形,浏览器就会忘记它。
  • SVG 与分辨率无关,而 Canvas 与分辨率相关。
  • SVG 支持事件处理程序,而 Canvas 不支持事件处理程序。

video

基本使用

<video>标签是一个块级元素,是H5新标签,用于放置视频。如果浏览器支持加载的视频格式,就会显示一个播放器,否则显示<video>内部的子元素。

<video src="example.mp4" controls>
  <p>你的浏览器不支持 HTML5 视频,请下载<a href="example.mp4">视频文件</a></p>
</video>

其中,video具备以下属性:

  • src:视频文件的网址。
  • controls:播放器是否显示控制栏。如果不想使用浏览器默认的播放器,而想使用自定义播放器,就不要使用该属性。
  • width:视频播放器的宽度。
  • height:视频播放器的高度。
  • autoplay:视频是否自动播放。
  • loop:视频是否循环播放。
  • muted:是否默认静音。
  • poster:视频播放器的封面图片的 URL。
  • preload:视频播放之前,是否缓冲视频文件。这个属性仅适合没有设置autoplay的情况。它有三个值,分别是none(不缓冲)、metadata(仅仅缓冲视频文件的元数据)、auto(可以缓冲整个文件)。
  • playsinline:iPhone 的 Safari 浏览器播放视频时,会自动全屏,该属性可以禁止这种行为。该属性为布尔属性。
  • crossorigin:是否采用跨域的方式加载视频。它可以取两个值,分别是anonymous(跨域请求时,不发送用户凭证,主要是 Cookie),use-credentials(跨域时发送用户凭证)。
  • currentTime:指定当前播放位置(双精度浮点数,单位为秒)。如果尚未开始播放,则会从这个属性指定的位置开始播放。
  • duration:该属性只读,指示时间轴上的持续播放时间(总长度),值为双精度浮点数(单位为秒)。如果是流媒体,没有已知的结束时间,属性值为+Infinity

API

我们可以通过HTMLVideoElement接口使用js来控制视频播放的一些方法。

  • play(): 开始播放视频,返回一个Promise。播放成功开始时,promise被解析,否则拒绝它。如果视频已经在播放,不执行任何操作。

  • pause(): 在当前位置暂停视频播放器,如果已暂停不执行任何操作。

  • load():重新初始化视频元素并重新加载视频资源。注意:load会触发重新小苗和重新获取,因此preload属性将确定实际再次获取的数据量。

  • canPlayType(type):检查浏览器是否支持特定的视频格式,返回内容为三个值,“可能”、“也许”或空字符串

  • addTextTrack():动态的将文本轨道加入到视频中,文本轨道可以用于字母、说明等。

  • currentTime: 该属性返回当前播放时间(以秒为单位)。如果将他设置成新值,他会自动将视频快速跳转到相应的时间戳。

    实践:自定义视频播放器

    实现一个播放器,包含播放暂停、退出、快进快退、进度条等功能。

    • demo
// js
const media = document.querySelector("video");
const controls = document.querySelector(".controls");
// 播放暂停
const play = document.querySelector(".play");
function playPauseMedia() {
  if(media.paused) {
    play.innerHTML = '暂停';
    media.play();
  }else {
    play.innerHTML = '播放';
    media.pause();
  }
}
play.addEventListener("click", playPauseMedia);


// 停止
const stop = document.querySelector(".stop");
function stopMedia() {
  media.pause();
  media.currentTime = 0;  // 没有stop方法,只能暂停后设置currentTime为0
  play.innerHTML = '播放';  
  // // 快退或快进功能处于活动状态时按下播放/暂停或停止按钮 
  // active.set('rwd', false);
  // active.set('fwd', false);
  // clearInterval(intervalRwd);
  // clearInterval(intervalFwd);

}
stop.addEventListener("click", stopMedia);


// 快退 & 快进
const rwd = document.querySelector(".rwd");
const fwd = document.querySelector(".fwd");
rwd.addEventListener("click", mediaBackward);
fwd.addEventListener("click", mediaForward);

let intervalFwd; // 定时器
let intervalRwd;
let active = new Map(); // 用于判断快退或快进功能是否处于活动状态

function mediaBackward() {
  clearInterval(intervalFwd); // 快退和快进功能互斥,清除另一个功能的定时器
  active.set('fwd', false);  // 快进功能处于非活动状态
  if(active.get('rwd')) {   // 快退功能处于活动状态,按下按钮后,清除定时器,恢复播放
    active.set('rwd', false);
    clearInterval(intervalRwd);
    media.play();
  }else {   // 快退功能处于非活动状态,按下按钮后,设置快退功能为活动状态,暂停播放,设置定时器
    active.set('rwd', true);
    media.pause();
    intervalRwd = setInterval(windBackward, 500);
  }
}
function windBackward() {  // 快退功能
  if (media.currentTime <= 2) {  // 当前时间小于2s时,设置快退功能为非活动状态,清除定时器,停止播放
    active.set('rwd', false);
    clearInterval(intervalRwd);
    stopMedia();
  } else {  // 当前时间大于2s时,快退2s
    media.currentTime -= 2;
  }
}

function mediaForward() {
  clearInterval(intervalRwd);
  active.set('rwd', false);

  if (active.get('fwd')) {
    active.set('fwd', false);
    clearInterval(intervalFwd);
    media.play();
  } else {
    active.set('fwd', true);
    media.pause();
    intervalFwd = setInterval(windForward, 500);
  }
}

function windForward() {
  if (media.currentTime >= media.duration - 2) {
    active.set('fwd', false);
    clearInterval(intervalFwd);
    stopMedia();
  } else {
    media.currentTime += 2;
  }
}


// 进度时间
const timerWrapper = document.querySelector(".timer");
const timer = document.querySelector(".timer span");
const timerBar = document.querySelector(".timer div");
function setTime() { // 设置当前时间
  const minutes = Math.floor(media.currentTime / 60); // 分钟,向下取整,不足两位补0
  const seconds = Math.floor(media.currentTime - minutes * 60); // 秒数,向下取整,不足两位补0

  const minuteValue = minutes.toString().padStart(2, "0");
  const secondValue = seconds.toString().padStart(2, "0");

  const mediaTime = `${minuteValue}:${secondValue}`;
  timer.textContent = mediaTime;

  const barLength =
    timerWrapper.clientWidth * (media.currentTime / media.duration);  // 进度条长度,当前时间/总时间
  timerBar.style.width = `${barLength}px`;  // 设置进度条长度
}
media.addEventListener("timeupdate", setTime); // 监听当前时间变化

// 画中画
media.addEventListener('click', () => {
  if (media !== document.pictureInPictureElement) {
    media.requestPictureInPicture();  // 进入画中画
  } else {
    document.exitPictureInPicture();  // 退出画中画
  }
});



media.removeAttribute("controls");
controls.style.visibility = "visible";

// HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>自定义视频播放器</title>
  <link rel="stylesheet" href="./myVideo.css"> 
</head>
<body>
  <div class="player">
    <video controls >
      <source src="./src/test.mp4" type="video/mp4" />
      <track src="captions.vtt" kind="captions" srclang="en" label="English" default>
    </video>
    <div class="controls">
      <button class="play" >播放</button>
      <button class="stop" >停止</button>
      <div class="timer">
        <div></div>
        <span aria-label="timer">00:00</span>
      </div>
      <button class="rwd" >快退</button>
      <button class="fwd" >快进</button>
    </div>
  </div>
  <script src="./myVideo.js"></script>
  
</body>
</html>

// css
.controls {
  visibility: hidden;
  opacity: 0.5;
  width: 400px;
  border-radius: 10px;
  position: absolute;
  bottom: 20px;
  left: 50%;
  margin-left: -200px;
  background-color: black;
  box-shadow: 3px 3px 5px black;
  transition: 1s all;
  display: flex;
}

.player:hover .controls,
.player:focus-within .controls {
  opacity: 1;
}
button:before {
  font-size: 20px;
  position: relative;
  content: attr(data-icon);
  color: #aaa;
  text-shadow: 1px 1px 0px black;
}
.timer {
  line-height: 38px;
  font-size: 10px;
  font-family: monospace;
  text-shadow: 1px 1px 0px black;
  color: white;
  flex: 5;
  position: relative;
}

.timer div {
  position: absolute;
  background-color: rgba(255, 255, 255, 0.2);
  left: 0;
  top: 0;
  width: 0;
  height: 38px;
  z-index: 2;
}

.timer span {
  position: absolute;
  z-index: 3;
  left: 19px;
}
video::cue {
  background-color: rgba(0, 0, 0, 0.8);
  color: #fff;
  font-size: 16px;
  padding: 4px;
}