返回资讯中心
外部精选
软件工程
#开发者实时流

Layers 将 103,802 个手绘圆点压缩至 82 KB 二进制格式

来源标题:Shipping 103,802 Dots in 82 KB

Layers 把手绘圆点插画打包成 82 KB 二进制格式,几何数据传输量减少 93%,并在画面无变化时完全停止渲染背景。这些插画由 Alberto Larizza 手工绘制,共 103,802 个圆点,分布在 13 幅作品中。此前原型使用的 JSON 文件体积达 8MB。

Hacker News 最新speckx2026/11/3 · 3 天前30 分钟阅读

以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。

动画 Dot2 用 82 KB 承载 103,802 个点 Jake Casto 11 分钟阅读· 2026 年 10 月 25 日

我们如何把手绘点阵艺术打包成一种非常紧凑的二进制格式,将几何数据的传输量减少 93%,并在画面没有变化时完全停止渲染背景。

我们确实在卖 AI 驱动的搜索,日常在 Layers 也用 AI/LLM 做很多事情,但艺术生成不在其中,而且是刻意为之。

uselayers.com 上的每一幅插画都由 Alberto Larizza 绘制。Alberto 是我最亲密的朋友之一,也是一位出色的图案设计师。他来自托斯卡纳,在来美国之前,曾为意大利的 Gucci 等品牌做图案设计。

他全部用点来画。13 幅插画一共 103,802 个点,每一个点都是今年早些时候耗时数月手工放置的。

我们一直希望它们能动起来。这个月,我们的产品设计师之一 Sumit 终于动手做了,原型非常漂亮:

悬停在其中一个上面,点会被推开然后弹回。点击它,会有一道冲击波穿过。

有一个问题:原型里有一个 8MB 的 JSON 文件,包含了 SVG 中的每一个点。我得想办法把这些数据可行地送进浏览器,并让它在低端设备上也能正常工作,页面上不出现任何抖动或卡顿。

全部 13 幅 Layers 插画都由手工放置的点渲染而成:上排是 merchandising、搜索、推荐和其他产品标识,下排是分类插画。

175 MB 的 Illustrator 导出文件

Alberto 用 Illustrator 做图。以 SVG 导出文件的形式发过来(65 个文件,几个 zip 包总共约 175MB),其中 13 个最终用在了网站上。

Illustrator 不会把一个点导出成一个点。你最终得到的是 <circle>、<ellipse> 和 <path> 元素的混合体,任何被裁剪或扩展过的点都会以一个小贝塞尔路径的形式导出,并带有自己的 fill 属性。

安全挂锁是个完美的例子:2,510 个 circle、470 个 ellipse 和 3,644 个 path,精确来说就是大约 730KB 里的 6,624 个点。

浏览器每个点只需要 x、y 和半径,所以第一步就是把每个 SVG 转换成这样:

对数刻度条形图:约 175 MB 的 Illustrator SVG 导出文件,1.25 MB 的 Float32 导出,519 KB 的未压缩 DOT2,以及 82 KB 的 Brotli 压缩 DOT2。

关于转换其实没什么好说的。这篇文章剩下的部分讲的是如何把 1.25MB 降到 82KB。

最初的版本为每幅插画提供一个 Float32Array:[width, height, x, y, r, x, y, r, …],小端序。每个点 12 字节。

// The original loader
const floats = new Float32Array(await res.arrayBuffer())

return {
 width: floats[0],
 height: floats[1],
 dots: Array.from(floats.subarray(2)),

Float32 大约有 7 位有效数字。最宽的示例在源单位下是 900px,渲染时只有几百个 CSS 像素。412.3871 和 412.3906 最终落在同一个像素上,所以这些数字大部分实际上都被浪费了。

(同一个版本还让 hero 背景循环以 60fps 持续运行,即使鼠标只是静止不动。这个后面再说。)

在我决定用任何格式之前,我花了一些时间摆弄这些文件。

我注意到的第一件事是行。如果按 y 排序再按 x 排序,78% 到 99% 的点与前一个点的 y 完全相同。

Alberto 是一行一行构建图案的,这在数字里也体现了出来。

merchandising 插画的一处裁剪,用细线连接同一行中的点,上方两条条带显示 90% 的 y 差值为零,而 x 差值没有一个是零。

半径也相当有限。最初的九幅插画每幅都有大约 104 到 228 个不同的半径值。最新的三幅有 900 多个(同样,这是我们的 SVG 转换器从边界框读取半径带来的浮点噪声)。8 位量化无论如何都会把这个抹平。

X 才是熵所在。几乎每个 x 都是小数,而一行中相邻的点只相隔几个像素。

我根本没有写什么聪明的编解码器。我只是把数据布局成让压缩器能看到行的样子。

DOT2 就是我最终采用的格式。它在部署时通过一个小型 Node 脚本构建,解码器大约 40 行。

merchandising 插画的 DOT2 布局:24 字节的头部,24 KB 的 x 差值,24 KB 的 y 差值和 12 KB 的半径。单独压缩时,这些平面分别缩小到 6.0 KB、2.0 KB 和 1.6 KB。

一次压缩一个平面,y 差值缩小到原始大小的 8%,x 差值降到 25%。

压缩前每个点 5 字节。用 Brotli 平均是 0.79。

const x = Math.round((dots[j] / width) * 65535)
const y = Math.round((dots[j + 1] / height) * 65535)

view.setUint16(HEADER + i * 2, (x - previousX) & 65535, true)
view.setUint16(HEADER + count * 2 + i * 2, (y - previousY) & 65535, true)
view.setUint8(
 HEADER + count * 4 + i,
 Math.round((dots[j + 2] / maxRadius) * 255),
)

解码器一侧:

x = (x + view.getUint16(HEADER + i * 2, true)) & 65535
dots[i * 3] = (x / 65535) * width

在 900px 的插画上,一个 uint16 步长约为 0.014px。所有资源往返转换后的最差中心误差:小于 0.011px。最差半径误差:小于 0.0065px。

这确实是有损的。我不打算就此相信数学,所以下面还有一个逐像素的比较。

解码器在分配内存前进行校验。它检查 DOT2 魔数(同时也是版本号),检查标志位,将数量上限设为 100k,检查 width 和 height 的边界,并要求缓冲区大小恰好为 24 + count x 5 字节。

任何一项不对就会抛出异常,然后你继续看到原本就在那里的 PNG。

写到一半时,我准备解释每一步为何有用,结果意识到我其实从未真正测量过这些步骤,只测量了最终数字。

于是我回过头,对每一个中间布局、全部 13 个资源都跑了一遍 Brotli q11:

全部 13 幅插画的条形图:Float32 源文件 1.25 MB,Float32 加 Brotli 224 KB,量化交错 227 KB,量化排序交错 240 KB,量化排序平面 83 KB,DOT2 82 KB。

(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)

正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →