前端实现通过短视频生成gif图

2,313 阅读3分钟

准备工作

  • 1个gif图
  • 1个短video,最好只有几秒
  • 下载gif.js源码,用于生成gif图
  • 安装local-web-server,因为直接访问webWorker(gif.js有使用到)会出现跨域问题,安装后任何文件的访问都可以使用http协议了
  • 下载FileSaver.js,可将生成的gif图保存到本地

使用canvas截gif图

最初始的想法是想用gif图来生成带文字的gif图,但是发现使用canvas绘图时总是截gif的第一帧图,代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    * {
      margin: 0;
      padding: 0;
    }
    #result {
      background: #eee;
    }
    .wrap {
      display: inline-block;
      vertical-align: top;
      text-align: center;
    }

    canvas {
      background: #eee;
    }
  </style>
</head>

<body>
  <img src="./gifs/throw_computer.gif" alt="" id="imgEle">
  <div class="wrap">
    <canvas id="canvas"></canvas>
    <div>截图预览</div>
    <button id="btn">截图</button>
  </div>
</body>
<script>
  function main() {
    let btn = document.getElementById('btn')
    // 画布元素
    let canvas = document.getElementById('canvas')
    let context = canvas.getContext('2d')
    // 绘制图片
    let img = document.getElementById('imgEle')
    img.onload = function () {
      btn.addEventListener('click', function () {
        context.clearRect(0, 0, canvas.width, canvas.height)
        context.drawImage(img, 0, 0, img.width, img.height)
      })
    }
  }
  main()
</script>
</html>

效果:
image.png
点击这个截图button,总是截取gif的第一帧图,故放弃使用gif来生成有文字的gif的想法。

使用canvas截取视频图片再生成gif

生成gif需要使用到 gif.js 以及 gif.worker.js ,效果如下:
说明:最左边的是原视频,中间是在播放视频时用canvas实时绘制图片而生成的动态预览,最右边是在视频播放完后生成跟视频内容一样的的gif图。
image.png
代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    * {
      margin: 0;
      padding: 0;
    }

    #result {
      background: #eee;
    }
    .wrap {
      display: inline-block;
      vertical-align: top;
      text-align: center;
    }
    .video-wrap{
      position: relative;
    }
    canvas {
      background: #eee;
    }

    #text {
      font-size: 20px;
      color: #fff;
      position: absolute;
      top: 20px;
      left: 20px;
    }
  </style>
</head>

<body>
  <div class="wrap video-wrap">
    <video id="videoEle" controls width="448" height="250">
      <source src="./videos/Saturday_night.mp4" />
    </video>
    <div id="text">test</div>
  </div>

  <div class="wrap">
    <canvas id="canvas"></canvas>
    <div>截图预览</div>
  </div>
  <div class="wrap">
    <img id="result" src="./gifs/loading.gif"></img>
    <div>gif预览</div>
  </div>
  <div>
    <button id="btn">保存GIF</button>
  </div>
</body>
<!-- gif.js与与gif.worker.js放置在同一文件夹下 -->
<script src="./gifs.js/gif.js"></script> 
<script src="./js/FileSaver.js"></script>
<script src="./index.js"></script>
</html>
// index.js
let canvas = document.getElementById('canvas')
let context = canvas.getContext('2d')
// 画布元素
let gifCanvas = document.createElement('canvas')
let gifCxt = gifCanvas.getContext('2d')
let video = document.getElementById('videoEle')
let result = document.getElementById('result')
let gif = null

function main() {
  listenVideo()
}
function listenVideo() {
  let timeout = null
  let width, height
  video.addEventListener('canplay', function () {
    // 浏览器可以播放媒体文件了,但估计没有足够的数据来支撑播放到结束,不需要停止缓存更多的内容
    width = video.width
    height = video.height
    result.width = canvas.width = gifCanvas.width = width
    result.height = canvas.height = gifCanvas.height = height
  })
  video.addEventListener('play', function () {
    // 点击播放按钮
    initGif(width, height)
    timeout = setInterval(drawImg, 60)
  })
  video.addEventListener('pause', function () {
    // 点击暂停
    clear(timeout)
  })
  video.addEventListener('ended', function () {
    // console.log(gif)
    // 播放完成
    gif.render()
    clear(timeout)
  })
}
function drawImg() {
  context.clearRect(0, 0, canvas.width, canvas.height)
  gifCxt.clearRect(0, 0, gifCanvas.width, gifCanvas.height)
  context.drawImage(video, 0, 0, video.width, video.height)
  gifCxt.drawImage(video, 0, 0, video.width, video.height)
  addGifFrame()
}
function addGifFrame() {
  // 这里写在setInterval里已经延时过了,所以delay设置为0.01(若设置为0,则会被重置为初始值500)
  gif.addFrame(gifCxt, { copy: true, delay: 0.01 })
}

function clear(timeout) {
  timeout && clearInterval(timeout)
}
function initGif(width, height) {
  gif = new GIF({
    width: width,
    height: height,
    workerScript: './gifs.js/gif.worker.js', // gif.worker.js放置的位置
    worker: 4,
  })
  // 设置文字属性,下一节需要用到
  gifCxt.font = context.font = '22px serif' // 设置大小
  gifCxt.fillStyle = context.fillStyle = '#fff' // 设置颜色
  gif.on('finished', function (blob) {
    let url = URL.createObjectURL(blob)
    result.setAttribute('src', url)
    // TODO:压缩gif图,现在的太大(好像需要放到服务器端压缩,前端不太好压缩)
    document.getElementById('btn').addEventListener('click', function () {
      // 保存为 hello world.gif,saveAs方法是FileSaver.js里面的方法
      saveAs(url, 'hello world.gif')
    })
  })
}
main()

给生成的gif添上动态文字

为演示方便,这里只是简单的添加视频秒数。 index.js 添加的代码如下:

// index.js
// 其他代码都一样,修改 drawImg 方法
function drawImg() {
  context.clearRect(0, 0, canvas.width, canvas.height)
  gifCxt.clearRect(0, 0, gifCanvas.width, gifCanvas.height)
  context.drawImage(video, 0, 0, video.width, video.height)
  gifCxt.drawImage(video, 0, 0, video.width, video.height)
  setText() // 添加文字
  addGifFrame()
}
// 添加文字
function setText() {
  let videoCurrentTime = video.currentTime
  let time = parseInt(videoCurrentTime)
  let str = '这是第' + time + 's'
  context.fillText(str, 150, 80)
  gifCxt.fillText(str, 150, 80)
}

这是生成的gif图:

待改进

现在生成的gif图挺大的,并且视频来生成gif图还是感觉不太方便,用gif图然后配上文字直接生成gif图感觉更好点,可以参考这个:libgif-js。另外,现在生成的gif图感觉不是很流畅,不如中间那个用canvas实时获取视频截图流畅,暂时不知道什么原因。

参考:

纯前端实现可传图可字幕台词定制的GIF表情生成器