一、image-rendering 介紹
CSS 中有一個有趣的特性叫 image-rendering,它可以通過演算法來更好地顯示被縮放的圖片。
假設我們有一張尺寸較小的二維碼截圖(下方左),將其放大 10 倍後影像會被虛化(下方右):
這時給放大的圖片加上 image-rendering: pixelated
的特性,CSS 會通過演算法將其畫素化展示,使其影像輪廓具有更銳利的邊緣:
該特性非常適合應用在色彩單一、輪廓分明、需要被放大的圖片上,可以營造出一種偽向量的既視感(減少放大後的失真)。
對於色彩豐富、細節較多的照片,image-rendering: pixelated
使用後會營造出一種馬賽克的外觀:
這離本文標題所希望實現的馬賽克效果還有段距離 —— 目前圖片需要被放大後才能顯示出效果,而我們希望能在保有原圖尺寸的基礎上,給圖片覆蓋等尺寸馬賽克。
然而 image-rendering
特性對尺寸未發生縮放的元素是不會生效的。
二、踩坑等尺寸馬賽克的實現
等尺寸馬賽克的原理相當於先把一張照片模糊化,然後再經過銳化演算法處理得到各種小方格。
image-rendering: pixelated
幫我們實現了“銳化”的步驟,我們得想想怎麼實現“模糊”。
首先使用濾鏡的模糊方案是行不通的,因為 image-rendering
和影像縮放係數強相關,所以應當思考可以怎樣利用圖片的縮放能力。
這裡得說一句,WEB 上的圖片像極了 Photoshop 裡的智慧物件 —— 你可以任意修改它的尺寸(例如放大很多倍讓其變模糊),但最後再把圖片改回原本的大小時,圖片會變回原來的樣子(沒有任何失真)。
如何保留圖片放大後的“模糊”資訊,是優先需要解決的問題。
聰明的小夥伴已經想到了可以嘗試使用 canvas
來處理,畢竟 canvas
可以輕鬆獲取、繪製影像,且繪製出來的影像資訊是純資料的,而非圖形物件(Image
),故經其放大繪製的圖片資料再進行縮小繪製(到原尺寸)會失真(這正好是我們所希望發生的)。
但這裡也存在一些坑:
- 外部影像通過
image-rendering: pixelated
演算法處理後顯示的資訊,canvas
是無法拿到的,因為那是顯示層的東西。canvas
拿到的依舊是未經銳化的、模糊的原生影像內容; canvas
本身如果沒有縮放的話,給canvas
新增image-rendering: pixelated
沒有任何意義。
這意味著你無法把圖片在 canvas
外面放大銳化,然後再寫入 canvas
去縮小繪製(並不斷迭代處理)來得到銳化後的原尺寸圖片。
三、有趣的 canvas 拉伸
在解決上述問題時,我們先來看看 canvas
一個有趣的特性。
如果我們在 canvas
標籤裡定義了寬高:
<canvas width="100" height="50" ></canvas>
同時又給 canvas
在樣式中定義了另一個寬高:
canvas {
width: 200px;
height: 200px;
}
那麼 canvas
會以哪個尺寸來顯示呢?
答案是以 CSS 的尺寸來顯示,但畫布的內容尺寸會以畫布標籤內定義的寬高為準。這意味著雖然我們看到的是 200px * 200px
的畫布,但它的內容實際被拉伸了(寬被拉伸了 2 倍,高被拉伸了 4 倍)。
注:左邊為畫布,右邊為原圖
這也是 canvas
作為可替換元素的一個特性 —— CSS 無法修改其內容。試想一下,如果 CSS 可以動態地修改 canvas
內容的尺寸,意味著 canvas
的內容會被裁剪掉一部分,或者多出來一部分空白區域,這顯然是不可取的。所以 canvas
在保留內容完整的前提下,整體伸縮到樣式規定尺寸,是合理的瀏覽器行為。
利用 canvas
的這個特性,我們可以這樣來實現等尺寸馬賽克:
- 建立一個畫布,通過樣式規定好其寬高,並設定
image-rendering: pixelated
特性; - 計算圖片最佳展示尺寸(以類似
background-size: contain
的形式展示); - 將畫布的寬高(非樣式)設定為樣式寬高的
1/N
; - 繪製影像,繪製的影像寬高為最佳展示尺寸的
1/N
。
如此一來,我們實際繪製了一個尺寸僅為最佳尺寸 1/N
的影像,再通過 canvas
的 N
倍放大又變回了視覺上的最佳尺寸。影像因為走的 canvas
繪製,所以放大回最佳尺寸後會保持模糊,從而滿足了 image-rendering
的匹配需求。
注:這裡提到的“最佳尺寸”,指的是步驟 2 裡“確保完整展示影像”所對應的最佳尺寸,而非圖片原生尺寸。
四、程式碼實現
我們按照上方步驟來書寫對應程式碼,當然我們希望靈活一些,例如上述的 N
可以由使用者自定義。另外本章的程式碼可以在 Github 上獲取。
HTML 部分
主要為選擇圖片的 <input>
控制元件、畫布、方便畫布獲取影像的 <img>
、供使用者自定義縮放倍數的文字框、執行按鈕:
<input id="file" type="file" accept="image/*" />
<canvas id="canvas"></canvas>
<img id="img-raw" />
<label for="compress-times">壓縮倍數:</label>
<input id="compress-times" type="number" value="12">
<button>馬賽克化</button>
CSS 部分
我們需要通過樣式規定好畫布的外觀尺寸,並配置 image-rendering: pixelated
特性。另外 <img>
標籤只是一個傳遞使用者所選圖片到畫布的中介,可以直接隱藏:
canvas {
display: block;
border: gray solid 1px;
width: 600px;
height: 600px;
image-rendering: pixelated;
}
img {
display: none;
}
JS 部分
let imgBlobUrl;
const file = document.getElementById('file');
const img = document.getElementById('img-raw');
const compressTimes = document.getElementById('compress-times');
const defaultCompressTimes = compressTimes.value | 0;
const canvas = document.getElementById('canvas');
const button = document.querySelector('button');
const boundingRect = canvas.getBoundingClientRect();
const ctx = canvas.getContext('2d');
const canvas_w = boundingRect.width;
const canvas_h = boundingRect.height;
// 以 background-size: contain 形式設定圖片尺寸
function matchImgSizeToCanvas(imgElem = img) {
let w = imgElem.width;
let h = imgElem.height;
if (w > canvas_w || h > canvas_h) {
let radio = Math.max(h / canvas_h, w / canvas_w);
radio = Number(radio.toFixed(2));
imgElem.width = parseInt(w / radio);
imgElem.height = parseInt(h / radio);
}
}
// 繪製 1/N 大小的影像,畫布寬高屬性設為樣式寬高的 1/N,從而實現畫布內容的 N 倍放大
function run() {
let ct = parseInt(compressTimes.value) || defaultCompressTimes;
canvas.width = parseInt(canvas_w / ct);
canvas.height = parseInt(canvas_h / ct);
ctx.drawImage(img, 0, 0, parseInt(img.width / ct), parseInt(img.height / ct));
}
function cleanCanvas() {
ctx.clearRect(0, 0, canvas_w, canvas_h);
}
function reset() {
img.removeAttribute('width');
img.removeAttribute('height');
cleanCanvas();
matchImgSizeToCanvas(img);
run();
}
file.addEventListener('change', function (e) {
window.URL.revokeObjectURL(imgBlobUrl);
const picFile = this.files[0];
imgBlobUrl = window.URL.createObjectURL(picFile);
img.onload = function init() {
reset();
}
img.src = imgBlobUrl;
}, false);
button.addEventListener('click', reset, false);
執行效果:
五、馬賽克外掛封裝
通過上方示例我們學習瞭如何利用 canvas
特性來設計等尺寸的馬賽克效果,現在我們嘗試把該功能封裝為一個簡易外掛,可以讓頁面上的圖片列表一鍵馬賽克化。
外掛的實現方案也很簡單 —— 使用者點選按鈕時,往圖片容器上插入一個和容器等尺寸的畫布(尺寸通過樣式設定),再繪製覆蓋畫布的影像,並縮小畫布的寬高屬性來放大畫布內容:
外掛指令碼
/** @file mosaic.js **/
class Mosaic {
constructor(url, container, options = {}) {
if (typeof container === 'string') {
container = document.querySelector(container);
}
if (!url || !container?.style) {
console.error('引數不正確');
}
this.url = url;
this.options = options;
this.container = container;
this.init();
}
init() {
const img = new Image();
const canvas = document.createElement('canvas');
canvas.style.position = 'absolute';
canvas.style.zIndex = 999;
canvas.style.imageRendering = 'pixelated';
this.img = img;
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
const containerBoundingRect = this.container.getBoundingClientRect();
const container_w = containerBoundingRect.width;
const container_h = containerBoundingRect.height;
// 通過樣式初始化畫布尺寸為容器尺寸
canvas.style.width = container_w + 'px';
canvas.style.height = container_h + 'px';
img.onload = () => {
this.run(container_w, container_h);
}
img.src = this.url;
}
run(w, h) {
// 縮小倍數,可以由引數傳入,預設為 12
const compressTimes = parseInt(this.options.compressTimes) || 12;
let compress_w = parseInt(w / compressTimes);
let compress_h = parseInt(h / compressTimes);
// 修改畫布尺寸屬性為 1/縮小倍數
this.canvas.width = compress_w;
this.canvas.height = compress_h;
// 繪製圖片覆蓋縮小後的畫布
this.ctx.drawImage(this.img, 0, 0, compress_w, compress_h);
this.container.prepend(this.canvas);
this.img = null;
}
remove() {
this.container.removeChild(this.canvas);
this.canvas = null;
}
}
export default Mosaic;
外掛使用頁
/** @file plugin-demo.html **/
<head>
<style>
ul {
list-style: none;
margin: 0;
padding: 0;
}
li {
float: left;
line-height: 0;
margin: 0 20px 20px 0;
}
li>img {
max-height: 180px;
}
div {
display: block;
clear: both;
}
</style>
</head>
<body>
<ul>
<li><img src="./assert/0.png" /></li>
<li><img src="./assert/1.png" /></li>
<li><img src="./assert/2.png" /></li>
<li><img src="./assert/3.png" /></li>
</ul>
<div>
<button id="generate">鋪上馬賽克</button>
<button id="remove">移除馬賽克</button>
</div>
<script type="module">
import Mosaic from './mosaic.js';
let liElems = document.querySelectorAll('li');
let mosaicList = [];
document.querySelector('#generate').onclick = () => {
remove();
for (let i = 0; i < liElems.length; i++) {
let liElem = liElems[i];
let url = liElem.querySelector('img').src;
let mosaic = new Mosaic(url, liElem);
mosaicList.push(mosaic);
}
}
function remove() {
mosaicList.forEach((mosaic) => {
mosaic.remove();
});
mosaicList.length = 0;
}
document.querySelector('#remove').onclick = remove;
</script>
</body>
執行效果:
六、相容性
image-rendering
的相容性可以從 caniuse 上查到,目前覆蓋率如下:
影響較大的主要還是在 IE、UC,以及安卓 4.4.4 版本的瀏覽器,需要酌情考慮是否在產品上使用此 CSS 特性。
以上便是本文全部內容,相關程式碼可以在 Github 上獲取。
希望能令你有所收穫,共勉~