公开文章

我用 Canvas 做了一张可以无限放大的画

刷到无限缩放画以后,我用 Canvas 2D 和 SVG 风插画复刻了一个可交互版本,顺便拆一下它背后的递归构图和缩放矩阵。

大家好,我是唐人。

前几天刷到一个视频:一张画可以一直放大,从画室钻进画板,从海岛钻进海底,再钻进另一个世界。

第一反应当然是:这得画多久?

但盯着看了一会儿,我发现它最有意思的地方,不是“分辨率有多高”,而是它的结构。

它不是一张真正无限大的图。

它更像一组套娃:每一层画面里,都提前藏好了下一层入口。

于是我就想,用代码能不能复刻一个简化版?

这篇文章就先做一个 Canvas 2D + SVG 风插画 的版本。它不追求照片级细节,只先把那个“不断往画里钻”的感觉跑起来。

Demo 在这里:

demo/index.html

无限放大,不是无限分辨率

如果真要做一张可以无限放大的图片,靠普通位图肯定不现实。

图片再大,也总有像素用完的时候。你放到一定程度,画面就会糊。

所以这类视频的关键不是“有一张超级大图”,而是“每一层都藏着下一层”。

比如第一层是画室,画室里有一块画板;画板里画着海岛。镜头靠近画板时,画板里的海岛慢慢填满整个屏幕,于是观众就觉得自己进入了下一层世界。

再往后,海岛里有背包,背包里藏着海底;海底有洞口,洞口里藏着城市;城市里有屏幕,屏幕里藏着星空。

这就是递归构图。

不是一张图无限大,而是每一层都给下一层留了门。

infinite-zoom-canvas-art-01-principle

代码里真正重要的三个东西

我这版 Demo 里,核心不是复杂图形,而是三个变量:

const sceneIndex = Math.floor(depth) % scenes.length;
const nextIndex = (sceneIndex + 1) % scenes.length;
const t = depth - Math.floor(depth);

sceneIndex 表示当前在哪一层。

nextIndex 表示当前层的入口里,应该预览哪一层。

t 是当前层的缩放进度,从 01。当它接近 1 时,镜头已经几乎钻进入口,下一层就会接管整个画面。

真正的缩放也不复杂。每一层都有一个入口矩形:

portal: { x: 402, y: 332, w: 196, h: 130 }

然后镜头从整张画面,慢慢插值到这个入口区域:

function currentView(scene, t) {
  const p = scene.portal;
  const eased = smooth(t);
  return {
    x: lerp(0, p.x, eased),
    y: lerp(0, p.y, eased),
    w: lerp(WORLD, p.w, eased),
    h: lerp(WORLD, p.h, eased),
  };
}

这段代码的意思很直白:

一开始看整张世界。

随着 t 变大,镜头一点点缩到入口。

入口最终铺满屏幕,下一层就出现了。

入口要画得像“真的通向下一层”

只做缩放还不够。

如果画板里是空白的,镜头再怎么缩也没有惊喜。

所以每一层的入口里,我会先把下一层缩小画进去:

function withPortal(ctx, portal, drawNext) {
  ctx.save();
  roundedRect(ctx, portal.x, portal.y, portal.w, portal.h, 18);
  ctx.clip();
  ctx.translate(portal.x, portal.y);
  ctx.scale(portal.w / WORLD, portal.h / WORLD);
  drawNext();
  ctx.restore();
}

这里做了三件事:

先裁剪出入口区域。

再把坐标系移动到入口左上角。

最后把下一层完整世界缩小,塞进这个入口里。

这就是“画中画”的关键。

你看到的是一个小画框,但它里面其实已经提前画好了下一层世界。

为什么我先选 Canvas,而不是 SVG

SVG 很适合做矢量风,放大后线条也清晰。

但这次我先选 Canvas,是因为它更适合讲“镜头”和“世界”的关系。

Canvas 里我可以自己控制坐标系、缩放比例、裁剪区域和动画帧。你会很明显地看到:无限缩放不是魔法,而是每一帧都在算一个新的 viewport。

当然,后面完全可以升级。

如果要做得更像插画工具,可以把每一层改成 SVG group;如果要做更炫的效果,可以用 WebGL 或 Three.js;如果要做真正大图,可以引入瓦片加载,像地图一样按需渲染。

但第一版没必要一下子做太重。

先把核心机制跑通,反而更容易讲清楚。

写在最后

这个 Demo 只是一个小玩具,但我觉得它很适合做技术文章。

因为它有一个很好的入口:读者先被画面吸引,然后你再把它拆成几个工程问题。

画面为什么能一直放大?

入口怎么和下一层对齐?

Canvas 怎么把世界坐标变成屏幕坐标?

为什么它看起来像无限,其实只是递归和循环?

这些问题都不复杂,但组合起来就很有意思。

我后面可能继续把它做成一个小工具:可以自己编辑每一层场景,设置入口位置,然后自动生成一段无限缩放动画。

如果你也刷到过那种“一直放大”的画,你觉得下一版应该做成 SVG 编辑器,还是直接上 WebGL?

评论区聊聊。

关注公众号

不错过后续文章和配套资源

扫码关注「唐人Console」,获取文章更新、配套资源和领取入口。 需要解锁时,文章页会生成专属口令。

唐人Console二维码扫码关注