昨晚我在公司楼下买了杯奶茶,那个店员一边摇杯子一边问我“哥你做啥的”,我说写前端的,他说“那你能不能把我们点单页弄得像老红白机那种马赛克风格”…我当时就笑出声了,心想这不就是网页像素化嘛,听起来玄乎,其实最关键就三行 CSS,真就三行,别急我慢慢唠,那个吸管先别咬断哈。
我先说结论哈:别指望纯 CSS 一把梭把整个 DOM都变成像素块,浏览器又不是 PS。比较稳的思路是——我用一个全屏 canvas 盖在页面上,把当前页面“截图”画进去,然后把图缩小再放大,关键一步是放大时不做平滑,你就得到像素风了。然后那“像素颗粒感”其实就靠 CSS 里那句 image-rendering: pixelated;,剩下两句是让它全屏固定住,不抖不跑。
三行 CSS 就这个(就别跟我抬杠说还能更短哈,我这已经够抠了):
#px {
position: fixed;
inset: 0;
image-rendering: pixelated;
}
对,你没看错,第三行就是灵魂。image-rendering: pixelated 这玩意儿平时你们只拿来处理 <img>,其实 canvas 也吃这一套,配合我下面 JS 关掉 imageSmoothingEnabled,像素块就出来了。
然后 JS 我给你一份能直接跑的,没用什么 html2canvas 这种库(库当然省事,但你说别搬运网上的嘛…我懂我懂)。我用的是一个“土办法”:把页面 clone 一份塞到 SVG 的 foreignObject 里,再转成图片画到 canvas。注意哈,这招在大多数现代浏览器能用,但碰到跨域图片、视频、某些字体可能会空白或者报安全限制,反正线上要做得稳得配合资源同源,这个你应该懂的,不懂也行,先玩起来。
<buttonid="toggle">像素开关</button>
<canvasid="px"></canvas>
<script>
const btn = document.querySelector('#toggle');
const canvas = document.querySelector('#px');
const ctx = canvas.getContext('2d');
let on = false;
functionresizeCanvas() {
const dpr = window.devicePixelRatio || 1;
canvas.width = Math.floor(window.innerWidth * dpr);
canvas.height = Math.floor(window.innerHeight * dpr);
canvas.style.width = window.innerWidth + 'px';
canvas.style.height = window.innerHeight + 'px';
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
functiondomToSvgDataUrl() {
const clone = document.documentElement.cloneNode(true);
// 别把我们自己的像素层也截图进去,不然套娃了
const px = clone.querySelector('#px');
if (px) px.remove();
const tg = clone.querySelector('#toggle');
if (tg) tg.remove();
// 让截图是当前滚动位置看到的内容
clone.style.width = window.innerWidth + 'px';
clone.style.height = window.innerHeight + 'px';
const html = new XMLSerializer().serializeToString(clone);
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" width="${window.innerWidth}" height="${window.innerHeight}">
<foreignObject width="100%" height="100%">
${html}
</foreignObject>
</svg>
`.trim();
return'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg);
}
asyncfunctionrenderPixelate(pixelSize = 10) {
resizeCanvas();
const url = domToSvgDataUrl();
const img = new Image();
// 这里别加 crossOrigin,dataURL 用不上,反而有些浏览器会闹脾气
awaitnewPromise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
img.src = url;
});
// 先画到一个小画布(缩小)
const w = window.innerWidth;
const h = window.innerHeight;
const sw = Math.max(1, Math.floor(w / pixelSize));
const sh = Math.max(1, Math.floor(h / pixelSize));
const off = document.createElement('canvas');
off.width = sw;
off.height = sh;
const offCtx = off.getContext('2d');
// 缩小时允许平滑没关系(你也可以关掉,看你口味)
offCtx.imageSmoothingEnabled = true;
offCtx.drawImage(img, 0, 0, sw, sh);
// 放大回主画布:关键是禁用平滑
ctx.clearRect(0, 0, w, h);
ctx.imageSmoothingEnabled = false;
ctx.drawImage(off, 0, 0, sw, sh, 0, 0, w, h);
}
functionenable() {
on = true;
canvas.style.display = 'block';
// 像素块大小你自己调,数越大越“糊成方块”
renderPixelate(12);
}
functiondisable() {
on = false;
canvas.style.display = 'none';
}
btn.addEventListener('click', () => {
if (on) disable();
else enable();
});
window.addEventListener('resize', () => {
if (on) renderPixelate(12);
});
// 初始先关掉
disable();
</script>
你看哈,核心逻辑其实就俩动作:缩小(把细节压扁)+ 放大且不平滑(把像素块保留下来)。很多人卡在“怎么截图 DOM”这一步,一上来就去研究 CSS filter,研究半天发现文字还是很锐利、边缘还是很干净,最后又开始怀疑人生…我跟你讲别怀疑,浏览器就是这样,它对矢量文字天生就要抗锯齿,你硬上滤镜只是糊,不是像素。
还有几个我踩过的小坑我顺嘴说下,免得你晚上也抱着电脑骂街:
1)你页面里如果有跨域图片(比如 CDN 不带 CORS 头),canvas 可能会被污染(tainted),后面你想导出截图会失败,甚至 drawImage 都可能异常。解决就是同源或者让资源响应带允许跨域。 2)foreignObject 不是所有奇怪环境都稳,比如某些内嵌 WebView,可能渲染出来是空白。那种场景就得降级:只像素化某个容器,或者用后端渲染截图。 3)像素层盖住页面以后,你还想点按钮?那你得给 canvas 加个 pointer-events: none;,但这就不是“三行 CSS”了哈…你可以临时在 JS 里写:canvas.style.pointerEvents='none',别让产品看见你耍赖就行。
我一般还会加个小彩蛋:滚轮滚动的时候实时刷新像素图,但刷新太频繁会卡,尤其页面复杂的时候,所以我会做个节流,大概 200ms 一次那种。你要我就给你补一句,也不复杂:
let t = 0;
window.addEventListener('scroll', () => {
if (!on) return;
const now = Date.now();
if (now - t > 200) {
t = now;
renderPixelate(12);
}
}, { passive: true });
反正你就记住一句话:三行 CSS 只是把 canvas 变成“像素显示模式”,真正把网页变成像素画,是 JS 那个缩放套路干的活。行了我这奶茶都见底了…你要是打算把这个做成“切换复古模式”的功能,记得把像素尺寸做成滑杆,让用户自己玩,产品一般很吃这套,嗯,就这样,我去回个消息,群里又有人催我看 bug 了。