Cool idea. I guess it's not too hard if you don't care about performance.
Edit: I spend a whole day on research, did a bunch of tests and wrote own small examples. These are my results:
Option 1: Static SVG filters
Thanks to Amaury Hanser.
You can define an svg filter and use it in your css: (see https://mcmap.net/q/1610598/-pixelate-a-whole-webpage)
This solution is beautiful in every way.
However, Apple does not like you. Some filters or properties are not supported by Safari on macOS and iOS. For example: When used as css filter, Safari ignores x,y,width,height which renders most solutions useless. If you are in control of the environment (e.g. WebView, Electron, ...) this is the best solution.
Option 2: Dynamic SVG filters
Calculation effort: Once per viewport resize / page load
This should work cross-browser. Tested on latest Safari, Chrome and Firefox (macOS).
You can use a similar technique as described in Option 1.
However, you must render a dot matrix to an offscreen canvas and inject it into an svg filter. You must redo the calculation everytime the viewport size changes (e.g. after resize event).
Working codesandbox example: https://codesandbox.io/s/pixelate-page-demo-dt6w0?file=/src/index.js (click reload button in right iframe if effect is not showing)
- Create an empty svg filter inside your body
<body>
<svg>
<filter
id="pixelate"
x="0"
y="0"
width="700"
height="900"
filterUnits="userSpaceOnUse"
></filter>
</svg>
</body>
- Dynamically create and inject svg filter like below
function pixelate(tileSize = 10, sigmaGauss = 2) {
tileSize = tileSize < 1 ? 1 : tileSize;
sigmaGauss = sigmaGauss < 1 ? 1 : sigmaGauss;
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx.fillStyle = "black";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// only to make the output visible
// document.body.appendChild(canvas);
const rows = canvas.height / tileSize;
const cols = canvas.width / tileSize;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
ctx.fillStyle = "white";
ctx.fillRect(
c * tileSize - 1 + Math.floor(tileSize / 2),
r * tileSize - 1 + Math.floor(tileSize / 2),
1,
1
);
}
}
const pixelate = document.getElementById("pixelate");
pixelate.innerHTML = "";
const blur = document.createElementNS(
"http://www.w3.org/2000/svg",
"feGaussianBlur"
);
blur.setAttribute("in", "SourceGraphic");
blur.setAttribute("stdDeviation", sigmaGauss);
blur.setAttribute("result", "blurred");
const hmap = document.createElementNS(
"http://www.w3.org/2000/svg",
"feImage"
);
const hmapUrl = canvas.toDataURL();
hmap.setAttribute("href", hmapUrl);
hmap.setAttribute("result", "hmap");
const blend = document.createElementNS(
"http://www.w3.org/2000/svg",
"feBlend"
);
// blend.setAttribute("mode", "lighten");
blend.setAttribute("mode", "multiply");
blend.setAttribute("in", "blurred");
blend.setAttribute("in2", "hmap");
const morph = document.createElementNS(
"http://www.w3.org/2000/svg",
"feMorphology"
);
morph.setAttribute("operator", "dilate");
morph.setAttribute("radius", tileSize / 2);
pixelate.setAttribute("width", canvas.width);
pixelate.setAttribute("height", canvas.height);
pixelate.appendChild(blur);
pixelate.appendChild(hmap);
pixelate.appendChild(blend);
pixelate.appendChild(morph);
}
- After page load / viewport resize call
pixelate(5, 1); // 5 = tileSize, 1 = std deviation gaussian blur
- Add css. Hint: do not use
display: none;
to hide the svg as it will break in Firefox
html {
filter: url(#pixelate);
}
svg {
position: absolute;
height: 0;
}
Option 3: Overlay Canvas
Calculation effort: Every DOM change
Without a working example, that's how I would do it:
Render your page to DOM
Render your page to a canvas (see html2canvas: http://html2canvas.hertzen.com or better rasterizeHTML: https://github.com/cburgmer/rasterizeHTML.js)
Overlay the canvas position: absolute; left: 0; top: 0; width: 100%; z-index: 100;
Don't catch clicks on the canvas so the buttons/links on the rendered DOM below will work pointer-events: none;
Scale your canvas without image smoothing (see here: How to pixelate an image with canvas and javascript)
Try to prevent dynamic rerenders for optimal performance.
Option 4: WebGL Shader
Calculation effort: Every frame
The coolest method by far is to render your website via WebGL and use a shader to create the desired effect.
- You can extend Option 3, render a fullsize canvas (keep in mind to render double size for retina devices), grab the WebGl context and attach a shader
- Alternatively you could use HTML GL (http://htmlgl.com/). I wouldn't recommend it because it seems to be unmaintained and also does not support retina devices (=> therefore everything will be blurry)