Face to GIF 的制作

Face to gif 是一款简单的网络应用,可让你录制自己并生成无限循环的动画 GIF。在这篇文章中,我将向你介绍它是如何诞生的以及我在构建这个小型应用过程中学到的知识。

image of the preview window in face to gif

它起源于Chris Heilmann 的文章,文章讨论了人们在互联网表情包中丧失表达能力的问题。至少,这是我想要从中理解的内容。我认为它实际上归结为工具,就像大多数问题一样。

现在是 2000 年以后,我们仍然没有找到解决诸如发送大文件、自动可靠地在线报税或在浏览器中录制动画 GIF 等简单问题的方案。此外,由于表情包如此流行且易于获取,当人们可以用可爱的猫咪图片凑合着用时,为什么要费心尝试创作原创内容呢?我认为有些事情应该更简单。

我之前已经尝试过下载客户端生成的文件,因此我知道文本文件很简单,静态图像也不难。但我没有找到任何关于在客户端生成 GIF 文件的内容。我以为我会稍后弄清楚 GIF 部分,甚至自己编写它——这实际上能有多难,对吧?

谦卑的起点

由于 WebRTC 正在获得关注,getUserMedia 正在成为一个相当可行的 API。从网络摄像头获取流并在视频元素上显示非常容易。

navigator.getUserMedia({video: true, audio: false}, yes, no);
…
video.src = URL.createObjectURL(stream);

然后我需要捕捉稍后构成 GIF 帧的图像。这也不难。幸运的是,你可以直接使用以下方法在画布上下文中绘制视频元素:

context.drawImage(video, 0,0, width,height);

这还允许你直接缩放捕获的帧,以标准化不同的网络摄像头分辨率。只需确保你的 canvas 元素已指定了正确的宽度和高度属性,你应该没问题。此外,你应该将其 display: none; 或从 DOM 中删除,以避免不必要的绘制。


要捕捉帧,只需根据你所需的帧率设置一个间隔并缓存帧到数组中。

setInterval(function () {
  context.drawImage(video, 0,0, width,height);
  frames.push(context.getImageData(0,0, width,height));
}, 67);

请注意,在这种情况下无需使用 requestAnimationFrame。即使页面不可见,视频流也会继续播放——所以我猜捕捉它也是有意义的。更重要的是,你需要帧之间特定的间隔,该间隔很可能最终不会是每秒 60 帧。

停止间隔后——也就是停止“录制”后——你会得到很多帧,每帧都有来自网络摄像头视频流的大量像素数据。所有这些数据永远不会离开电脑上显示的网页。

有一段时间,我考虑添加一个“下载原始数据”按钮,以便人们可以做其他事情,而不仅仅是制作自己的 GIF。我决定先解决 GIF 部分,然后再考虑花哨的功能。

GIF 编写器

在阅读了太多关于 GIF89a、抖动和 LZW 算法的内容后,我怯懦地决定看看是否可以找到现成的库。我很幸运地找到一个演示,该演示将一系列图像组合成动画 GIF——全部使用 JavaScript。我很快将该库改造到我的小型应用中,事情又开始运转起来。

gifworker.sendMessage({ images: frames, delay: 67 });
...
gifworker.onmessage = function(event) {
  var img = document.createElement('img');
  img.src = event.data.gifDataString;
  document.body.appendChild(img);
};

接下来需要执行以下操作

  1. 编写一个二进制头部,将文件描述为 GIF98a 文件。
  2. 编写一个块,描述宽度、高度和循环控制。
  3. 从图像数据列表中写入每个帧。
  4. 编写一个尾部 59——也就是分号。

使用 Web Workers 在单独的线程中执行繁重的操作,保持 UI 响应成为轻而易举的事情。处理完成后,库会提供一个 base64 编码的字符串,表示 GIF 文件。这可以用作图像的数据 URL。

我生命中的某个阶段,非常密集地使用数据 URL,我会为客户提供仅包含一个 HTML 文件的模型,该文件包含 base64 编码的图像、CSS 和 JavaScript,并且不需要互联网连接即可工作。而且这在 IE 中无法工作。

但这一次我即将面临另一组问题……

保存文件

如果数据 URL 足够小,则可以保存。如果你想保存一个太长且通过数据 URL 显示的 GIF,浏览器甚至不会让你尝试这样做。尝试使用链接上的 download 属性来巧妙地解决问题也没有帮助。

image of the generated gif and its options in face to gif

虽然数据 URL 确实很酷,但它们的长度是有限制的。我不想在这个应用中强加看似过时的限制。

我稍微修改了库,以便它提供原始字节而不是 base64 字符串,然后我使用原始字节创建一个Blob,然后使用 URL.createObjectURL 创建了一些我可以将其设置为图像源属性的内容。

var blob = new Blob([uint8array], {type: "image/gif"});
img.src = URL.createObjectURL(blob);

这种使用用户生成资源作为源属性的方法比旧的数据 URL 方法更可靠且更具可扩展性。这也简化了图像的保存。

我使用了一个技巧来实现应用中的下载链接:我放置一个简单的锚点,带有一个空的 href 属性,并附加一个简单的“click”事件处理程序。当用户点击它时,我的事件处理程序函数只需将 href 属性更改为与图像的源属性相同。浏览器会完成剩下的工作。

a.addEventListener('click', function (e) {
    a.href = img.src;
  // the real trick is to let the event bubble up
}, false);

作为网页开发者,我们花费大量时间从浏览器中劫持控制权,以便我们可以做自己的事情。事实是,我们大多数时候只需告诉浏览器去哪里,它自己到达那里的效果会比我们参与其中要好得多。

回到我的应用,我已经把它带到了一个可以完成我期望它完成的功能的地方:用我的网络摄像头录制我的脸并生成一个 GIF。

速度

应用运行良好,但更像是一首民谣而不是重金属歌曲。每个 GIF 的 1 秒需要花费我 16 秒的生命。这同样是因为我最初以 640×480 的分辨率编写 GIF 文件,还因为事实证明,如果不进行优化,对像素数据的二进制操作可能会非常缓慢。

我正在努力寻找解决方案,查看库的 TypeScript 源代码和生成的 JavaScript,以寻找改进它的方法,考虑使用 asm.js,更多地使用 TypedArrays,任何方法——当我偶然发现另一个用于编写动画 GIF 的 JavaScript 库时。

gif.js 更精简,可以使用多个 Web Workers 来处理帧,并且拥有我认为更好看的 API。在改造了这个新库、调整了设置并减半大小后,我能够以极快的速度在网络应用中生成 GIF。

唯一的缺点是我在速度上获得的提升是以压缩为代价的。仅仅 10 秒的 GIF 就会产生大约 30 MB 的数据。经过更多细致的调整,我能够将其降低到大约 5 MB/10 秒。仍然很多,但它是未压缩的,并且通过在线工具进行积极的压缩可以将其降低到 600KB。

与 Blob 一起使用的另一件很酷的事情是,你可以将它们直接附加到 FormData 对象,这意味着向 imgur.com 发起跨域 Ajax 调用以上传生成的 GIF 非常简单,并且是该网络应用中非常受欢迎的补充。

我学到的东西

  • URL.createObjectURL 是一个用于客户端生成媒体的优秀 API,它解决了你会遇到的许多问题。
  • 使用 TypedArrays 会大大提高数据密集型应用的性能。
  • 在多个并发 Web Workers 之间划分工作负载确实有效并有所帮助。
  • WebRTC 处于一个相当稳定的阶段,你可以使用大约 40% 的互联网用户的媒体设备。
  • 很容易创建一个允许用户生成内容而无需涉及你的服务器的应用。
  • 人们真的很喜欢玩弄他们的网络摄像头。我认为在网络应用中使用它们非常有意义。
  • 很容易用 GIF 数据填满 2MB,也就是 Imgur 的文件大小限制。

thumbs up!

我还想感谢Johan Nordbergnobuoka 为他们开发的 JavaScript GIF 编写库付出的辛勤工作。

你可以快速体验一下 face to gif,或者查看GitHub 上的源代码 并进行分叉,改进它并享受乐趣;就像我做的那样。

我迫不及待地想要看到 WebRTC 在移动设备上真正可用的一天!

关于 Horia Dragomir

UI 开发者,渴望且愚蠢

更多 Horia Dragomir 的文章…

关于 Robert Nyman [荣誉编辑]

技术布道者和 Mozilla Hacks 编辑。发表关于 HTML5、JavaScript 和开放网络的演讲和博客。Robert 是 HTML5 和开放网络的坚定支持者,自 1999 年以来一直从事网页前端开发工作——在瑞典和纽约市。他还在 http://robertnyman.com 定期发表博客,并且喜欢旅行和结识新朋友。

更多 Robert Nyman [荣誉编辑] 的文章…


8 条评论

  1. graste

    不错的演示,尽管如果没有释放资源的可能性,而无需等待任意垃圾回收器启动此类演示,则很难获得良好的性能。这与将多个文件拖放到浏览器并动态生成预览缩略图以在通过 XHR2 上传之前进行图像选择相同。使用多个大型图像以高性能的方式执行此操作几乎是不可能的,因为你无法随意释放 JavaScript 中的资源。

    2013 年 7 月 10 日 05:36

    1. Horia Dragomir

      @graste,我完全理解你的痛苦并分享你的挫败感。
      不过,我认为 GC 不是这里唯一的问题。根据我的经验,GC 主要影响较长时间内的流畅度,并且通常与严重的卡顿有关。

      2013 年 7 月 10 日 05:44

    2. Henri

      @graste:你可以从 JavaScript 中的 Exif 数据中提取缩略图……你只需要解析每个文件的最初 64k 即可。我设法在不到 2 秒的时间内解析并显示了 600 多张图像(未使用 Web Worker,这也有所帮助)。但我同意,拥有一个调整任意 IMG 元素大小的 API 将非常有用!

      2013 年 7 月 10 日 12:54

      1. graste

        是的,这是一个很好的观点。当我下次需要进行缩略图处理时,可能会尝试一下。至少,如果存在 Exif 数据和嵌入的缩略图。:-) 如果运气不好,将大量数据来回传输到 Web Worker 有时也会弊大于利。当尝试将它们混合使用时,新 API 似乎故意设置了障碍,因为它们有时确实可以提供更多帮助。但嘿,现在比……比如说 15 年前好多了。创建优秀的 API 是一门艺术。:-)

        2013 年 7 月 10 日 13:46

  2. Domingos Jorge Velho

    GIF 是个毒瘤。
    WebP>GIF。

    2013 年 7 月 10 日 10:56

    1. Robert Nyman [编辑]

      我想说的是,简而言之,GIF 在每个网络浏览器中都能正常工作,并且向后兼容。

      2013 年 7 月 10 日 11:07

  3. Fuzzy

    试试我的
    你可以调整所有参数
    http://gifpow.com/gifcam.php

    2013 年 7 月 18 日 07:27

    1. Horia Dragomir

      Fuzzy,你的看起来也很酷!不过,我试图避免使用 Flash 获取网络摄像头源。

      2013 年 7 月 18 日 08:16

本文的评论已关闭。