15158846557 在线咨询 在线咨询
15158846557 在线咨询
所在位置: 首页 > 营销资讯 > 网站运营 > 手把手教你用 Chrome 制作 GIF 截图

手把手教你用 Chrome 制作 GIF 截图

时间:2023-05-16 07:18:02 | 来源:网站运营

时间:2023-05-16 07:18:02 来源:网站运营

手把手教你用 Chrome 制作 GIF 截图:最近想要作一个网页端的截图工具,用于提 Issue 的时候能更方便的附图。为了能体现操作步骤,最好是能截动图,也就是能生成 GIF。纯粹的靠引入 JS SDK 实现有点不现实,好在我们内部统一使用 Chrome,可以利用 Chrome Extension 的能力来做这件事。

本文会重点阐述以下几点内容:

  1. 截图方案对比,说一说 html2canvas 与 Chrome Extension 的优劣。
  2. 了解 Chrome Extension 几个概念。
  3. 如何实现区域框选以及图片裁剪。
  4. GIF 是如何合成的。
  5. 如何用 canvas 实现图片编辑器,包括文字添加,形状添加,自由绘制等。

html2canvas 方案

要在页面上生成截图,我们可以借助 html2canvas 库,首先将需要装换的 dom 结构装换为 canvas,再将 canvas 装换为图片。这也是我最早考虑的方案。

这个方案本身没毛病,但是在实际中发现,html2canvas 无法保证所有的 CSS 都能得到正确的渲染,html2canvas 的 官网 也明确列出了目前还不支持的 CSS 样式。还有一点就是 html2canvas 需要额外处理跨域图片

如果是网站内部做截图的话,这个方案是比较合适的,CSS 的问题可控,图片可以使用代理。但是作为通用工具的话,样式以及图片的问题都无法保证,这个方案就有点差强人意。

Chrome Extension 方案

Chrome Extension 提供了另外一种制作工具思路。

首先,Chrome Extension 可以运行在任意的页面上,不需要业务系统修改代码,非常适合做一些通用功能。其次,Chrome Extension 提供了很多的能力,其中就有用于截屏的 API。

从性能上来讲,html2canvas 的原理是通过拷贝 dom 节点以及节点样式,将其重新渲染为 canvas,整个流程执行在主线程上。Chrome Extension 运行在其独立的线程中(这里特指 Chrome Extension 的 background)。而且就截屏操作看,其内部调用的是计算机底层能力,性能无疑是更有优势。当然,这种方案的弊端也很明显,功能和平台强绑定,只适用于支持 Chrome Extension 的平台, 目前除了可以运行在 Chrome 上之外,还可以运行在 Edge 以及所有 webkit 内核的国产浏览器上。

总体而言,这两种方案适用于不同的场景,html2canvas 更加通用一些,目前也大量用于线上业务,Chrome Extension 定位在于制作网页工具,场景比较单一。

Chrome Extension 结构

Chrome Extension 本身是一个由 html,js 和 css 组成的 web 软件。比较特殊的点在于它规定了一些展现方式,通讯方式等。 Chrome Extension 的开发细节就不多加赘述了,这里只列出几个后续提到的概念。

manifest.json:这是 Chrome Extension 最重要也是必不可少的文件,用来配置所有和插件相关的配置,必须放在根目录。

content-scripts:指的是 Chrome Extension 向页面注入的 js,通过注入的 js 代码我们可以控制页面的一些行为。content-scripts 可以访问页面的 DOM,但是只能调用少量的 chrome 扩展 API,基于这个特性,content-scripts 一般负责展示类工作。

background:Chrome Extension 的核心,几乎可以调用所有的 Chrome 扩展 API(除了devtools)。它的生命周期是 Chrome Extension 中所有类型页面中最长的,随着浏览器的打开而打开,随着浏览器的关闭而关闭。我们一般在 background 中实现任务核心逻辑。

popup:是点击该 Chrome Extension 图标时打开的一个小窗口网页,焦点离开网页就立即关闭,我们一般在这个页面做一些参数的配置。

接下来进入正题,除去 manifest.json,整个项目我们分为四个部分。分别是参数配置、区域框选、生成截图以及最终的编辑截图

参数配置

大部分的 Chrome Extension 在安装之后,用户第一直觉就是点击图标,此时 popup 被激活。

我在 manifest.json 中定义了默认的 popup 路径为 popup.html,我们在根目录下创建 popup.html,按照正常的 html 写就可以了。

"browser_action": { "default_icon": "images/ic_black_16.png", "default_title": "GIF 截图", "default_popup": "popup.html"},这个 html 可以引用第三方的 js,css。为了方便后续界面绘制,我们引入 jquery,select2。

在这个页面中,我们配置两个截图需要的参数。

  1. 每秒帧数,该参数决定我们生成 GIF 的平滑度。
  2. 截图质量,决定生成图片的画质。
popup 的生命周期很短,如何将配置的参数记录起来呢?

Chrome 为扩展应用提供了存储 API,以便将扩展中需要保存的数据写入本地磁盘。首先需要在 manifest.json 申请对应的存储权限。

"permissions": [ "storage"]这样我们就能使用 chrome.storage.sync 或者是 chrome.storage.local 这两种储存区域了。两种储存区域的区别在于,sync 储存的区域会自动将数据同步到 Google 账户中。local 则少了这个过程。在离线情况下,两种储存的表现一致

// 画质数据初始化chrome.storage.sync.get({quality: 10}, function(items) { const quality = items.quality; qualityEle.val(quality).trigger('change');});// 画质数据变更qualityEle.on("select2:select",function(e){ const quality = e.target.value; chrome.storage.sync.set({quality});});content-scripts 和 background 都可以拿到这里存储的数据,后续会根据配置的参数进行截图操作。

最终配置界面如下所示。

截图区域框选

content-scripts 会随着当前页面一起初始化。content-scripts 可以操作当前的 dom,所以截图区域框选由 content-scripts 负责。

background 会跟随浏览器一起打开,在 background 中,我们可以为右键菜单增加自定义栏目(需要 contextMenus 权限)。如下,我们增加一个【GIF 截图】的按钮,用于向 content-scripts 发送一条消息。

// 右键菜单演示chrome.contextMenus.create({ title: "GIF 截图", onclick: function(){ sendMessageToContentScript({cmd: 'prepare capture'}); }});// 发送信息到 ContentScriptfunction sendMessageToContentScript(message, callback) { chrome.tabs.query({active: true}, (tabs) => { chrome.tabs.sendMessage(tabs[0].id, message, (response) => { if(callback) callback(response); }); });}content-scripts 同样定义在 manifest.json 中,如下定义表示,在任意的页面都依次注入指定的 js 和 css。run_at 控制 js 文件注入的时机,"document_start" 表示在 css 文件之后,dom 构建或其他脚本运行之前,注入 js 文件。

"content_scripts": [ { "matches": ["<all_urls>"], "js": ["libs/jquery/jquery.min.js", "src/content-script.js"], "css": ["css/custom.css"], "run_at": "document_start" }],content_scripts 中监听来自 background 的消息。为当前页面生成一个遮罩层,用户在遮罩层上进行框选,框选区域为需要截图的区域。

chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { ...});这里有一个比较有意思的点,就是 遮罩层如何做比较合适?

这里的遮罩层和我们做模态框时候的遮罩层不太一样,因为模态框是“浮在”遮罩层之上的,而这里需要的是“镂空”。常见的做法是在“镂空”区域四周绘制四个矩形作为遮罩层,但是这种做法做起来比较复杂,而且截图场景下镂空区域会频繁变化,每次变化我们都需要修改四个矩形的大小。

我推荐的方案是使用 border + pointer-events 的方式绘制遮罩层。我们先使用一个透明矩形覆盖当前页面。然后设置 border 颜色为遮罩层颜色,通过改变 border-width 在四个方向上的值,就能实现遮罩层的变化。现在唯一的问题就是我们需要让“镂空”区域响应事件,我们可以 设置遮罩层的 pointer-events 为 none,就能阻止当前层对点击、状态变化和鼠标指针变化的影响,相当于当前层没有了。

截图区域框选的重点在于处理好 mousedown,mousemove 和 mouseup 这三个事件。

鼠标按下的时候,需要根据当前鼠标所在位置和点击的元素,区分接下的行为是创建新的选区,还是更改选区的大小,或是移动当前选区。若是创建新的选区,则为新的选取绑定拖动事件,截图事件;若当前位置已经在选区上了,判断是移动还是更新大小。鼠标移动的时候需要判断当前行为以及鼠标位置,更新选区大小或是移动选区;鼠标松开后,移除无效事件。 这部分代码比较琐碎,实现的效果如下所示。

我们通过这个步骤,获得了截图区域,接下来就是将这个区域发送给 background,进行截图了。

普通截图与 GIF 截图

使用 PS 做过 GIF 的同学应该都知道,制作 GIF 最简单的方法就是使用多张图片合成,原理类似于传统动画片。JS 制作 GIF 的原理也是这样的,根据我们设置的帧数对选区进行多次的截图,然后通过这些截图合成一张 GIF。因此,普通截图和 GIF 截图背后的原理是一样的,都是使用的 chrome 扩展的截屏 API 实现。

对于普通截图,由于只需要调用一次截屏 API,将选区的坐标作为参数传递给 background。这里使用 chrome.runtime.sendMessage 和 background 进行通讯。

chrome.storage.sync.get({quality: 10}, function(items) { const quality = items.quality; const params = { sx: topX + 10, sy: topY + 30, sWidth: bottomX - topX - 20, sHeight: bottomY - topY - 60, quality }; chrome.runtime.sendMessage({cmd: 'capture screen', params});});而 GIF 截图场景下,我们需要多次调用 截屏 API。比如当帧数设置为 20 的时候,我们设置的定时器会在 1s 中发送 20 个请求。这时候用长连接比较合适。长连接类似 WebSocket 会一直建立连接,双方可以随时互发消息。长连接使用 chrome.tabs.connect 或者是 chrome.runtime.connect。

chrome.storage.sync.get({frame: 10, quality: 10}, function(items) { const frame = items.frame; const quality = items.quality; //通道名称 port = chrome.runtime.connect({name: "shenmax"}); chrome.storage.sync.get({frame: 10, quality: 10}, function(items) { //发送消息 port.postMessage({cmd: "start recording", params: { width: clientWidth, height: clientHeight, quality, frame }}); // 定时发送截图信息 recordTimer = setInterval(recordingFrame, 1e3 / frame); });});接下来就是 background 的部分,之前提过,background 的权限很高,生命周期很长,核心的功能一般写在这里。当然,使用截屏 API(chrome.tabs.captureVisibleTab)之前需要在 manifest.json 中申请 tabs 权限。

function capture(quality) { return new Promise((resolve, reject) => { try { chrome.tabs.captureVisibleTab(null, { quality : 10 * quality }, function(dataUrl) { resolve(dataUrl) }); } catch (e) { reject(e) } })}chrome.tabs.captureVisibleTab 用于捕获页面的可见区域,也就是截屏。这个 API 不支持获取一部分屏幕元素,因此,我们获得的是整个屏幕的数据,需要进行裁剪。

如何对图像进项裁剪? 我们需要借助 canvas 的能力了,流程如下。

  1. 加载图片并将图片绘制到 canvas 中。
  2. 利用 canvas 的 drawImage 函数来裁剪图。
  3. 将 canvas 转化为 Image。
context.drawImage(img,sx,sy,swidth,sheight,x,y,width,height);drawImage 方法参数比较多,img 表示要使用的图像、画布或视频;sx 表示开始剪切的 x 坐标位置;sy 表示开始剪切的 y 坐标位置;swidth 表示被剪切图像的宽度;sheight 表示被剪切图像的高度;x 表示在画布上放置图像的 x 坐标位置;y 表示在画布上放置图像的 y 坐标位置。width 表示要使用的图像的宽度;height 表示要使用的图像的高度。

// 图片切割function slice(dataUrl, sx, sy, sWidth, sHeight) { return new Promise((resolve, reject) => { try { // 创建画布 let canvas = document.getElementsByTagName('canvas'); if (canvas.length === 0) { canvas = document.createElement("canvas"); } canvas.width = sWidth; canvas.height = sHeight; const context = canvas.getContext("2d"); dataUrl2Img(dataUrl).then((img) => { // 裁剪 canvas context.drawImage(img, sx, sy, sWidth, sHeight, 0, 0, sWidth, sHeight); resolve(canvas); }); } catch (e) { reject(e) } })}// dataUrl 转变为 imgfunction dataUrl2Img(dataUrl) { return new Promise((resolve, reject) => { try { const img = new Image(); img.src = dataUrl; img.onload = function(){ resolve(img); }; } catch (e) { reject(e) } });}这里要考虑下性能问题,chrome.tabs.captureVisibleTab 的调用是比较消耗性能的,一般而言,如果生成 png 图片的话,大概需要 70-80 ms,而生成 jpg 只需要 30 - 40 ms。 默认情况下就是生成 jpg 图片。由于这两者性能存在巨大差异,导致我们生成 png 图的话,每秒调用次数最好不要超过 10,而 jpg 可以达到 20

当前我们有了一系列的截图,并不用急着将其生成 GIF,生成 GIF 的时机我们放在图片下载的时候。先看图片编辑。

图片编辑

基本所有的截图功能都会带一个图片编辑的能力,方便我们在生成图片上做一些信息标注或是重点框选。如何在网页上实现这个功能呢?

图片编辑功能依赖于 Canvas 绘图能力。canvas 支持我们绘制任意的二维图形。流程是这样的。

  1. 在当前的截图上方覆盖一个等大小的 canvas 元素。
  2. 借助 canvas 的 api 在 canvas 上绘制图形。
  3. 使用 canvas 的 drawImage 方法将当前 canvas 元素和截图合并。drawImage 方法支持传入一个 canvas。
  4. 将合并后的 canvas 变成 Image 导出。
GIF 中的每张图片要依次执行上述的步骤三和步骤四。

frameList.forEach((canvas) => { const newCvs = document.createElement("canvas"); newCvs.width = initWidth; newCvs.height = initHeight; newCvs.getContext("2d").drawImage(canvas, 0, 0); newCvs.getContext("2d").drawImage(cvs, 0, 0, initWidth, initHeight); canvasList.push(newCvs);});canvas 原生 API 比较基础,我们使用一个 Canvas 库 fabric 来实现图片编辑。Fabric 是一个强大而简单的 JS Canvas 库,我们能通过使用它实现在 Canvas 上创建、填充图形、给图形填充渐变颜色。 组合图形(包括组合图形、图形文字、图片等)等一系列功能。

以实现一个矩形框为例,我们监听 mouseDown 事件,鼠标落下的时候,使用 fabric 生成一个有边框,颜色透明的矩形。

fabricCanvas.on('mouse:down', (opt) => { // 当前位置 const pos = opt.absolutePointer; switch (select) { case rectangle: if (!start) { x = pos.x; y = pos.y; start = true; addRectangle(pos); } break; ... }}function addRectangle(pos) { const color = colorConfig.val(); const borderSize = rectangleConfig.val(); const square = new fabric.Rect({ strokeWidth: parseInt(borderSize, 10), stroke: color, editingBorderColor: color, width: 1, height: 1, left: pos.x, top: pos.y, fill: 'transparent', padding: 5, cornerSize: 5, cornerColor: color, rotatingPointOffset: 20, }); fabricCanvas.add(square); fabricCanvas.setActiveObject(square);}鼠标移动的时候根据鼠标当前位置更新矩形的大小。

fabricCanvas.on('mouse:move', (opt) => { const pos = opt.absolutePointer; switch (select) { case rectangle: if (start) { resizeRectangle(x, y, pos); } break; default: break }});function resizeRectangle(initX, initY, pos) { const x = Math.min(pos.x, initX), y = Math.min(pos.y, initY), w = Math.abs(pos.x - initX), h = Math.abs(pos.y - initY); const square = fabricCanvas.getActiveObject(); if (square) { square.set('top', y).set('left', x).set('width', w).set('height', h); fabricCanvas.renderAll(); }}鼠标离开的时候完成矩形添加,移除操作标记。

fabricCanvas.on('mouse:up', () => { switch (select) { case rectangle: if (start) { const square = fabricCanvas.getActiveObject(); fabricCanvas.add(square); changeOperation(pointer); x = y = 0; start = false; } break; ... }});

生成 GIF

用户保存的时候,将之前的全部截图拼接为一个 GIF。这部分工作依赖于 gif.js。gif.js 是一个可直接在浏览器上运行的 JavaScript GIF 编码器。不仅如此,gif.js 还支持通过 web worker 的方式合成 gif。代码如下所示。合成 gif 的时间比较久,建议不要生成太大的 GIF。

function makeGif(imgList) { const gifWorker = new GIF({ workers: 4, quality: 10, dither: true, workerScript:'./libs/gif/gif.worker.js' }); return new Promise((resolve, reject) => { try { imgList.forEach((img) => { // 一帧时长 1e3 / frame gifWorker.addFrame(img, {delay: 1e3 / frame}); }); //最后生成一个blob对象 gifWorker.on('finished', function(blob) { resolve(URL.createObjectURL(blob)); }); gifWorker.render(); } catch (e) { reject(e) } })}

总结

目前 Chrome 上有很多优秀的扩展,随着 Chrome 市场份额的扩大,Chrome Extension 也逐渐演变为一种解决方案。如果你的公司也在大量的使用 Chrome,可以想想是不是一些场景更适合放在 Chrome Extension 上呢?

最后总结下使用 Chrome Extension 制作 GIF 插件的流程。

  1. 使用 popup 配置基本参数。
  2. 在 content-scripts 中实现截图区域选择。
  3. 发送消息给 background,background 调用 chrome.tabs.captureVisibleTab 截取可视区域。
  4. 利用 canvas drawImage 方法切割图片。
  5. 使用 fabric 自由绘制图形,再次使用 canvas drawImage 方法将截图和 fabric 绘制的图形进行合并。
  6. 使用 gif.js 在 web worker 中快速生成 gif 截图。
目前还有一点需要做的,就是可以将截图编辑放在截图区域选择的时候,这样能在操作的同时做一些标记,更加方便。

该项目的源码我放在 GitHub 上

如果您觉得有所收获,就请点个赞吧!

关键词:把手

74
73
25
news

版权所有© 亿企邦 1997-2025 保留一切法律许可权利。

为了最佳展示效果,本站不支持IE9及以下版本的浏览器,建议您使用谷歌Chrome浏览器。 点击下载Chrome浏览器
关闭