Pixelate a whole webpage

2.1k Views Asked by At

I understand how to zoom a small canvas to make a big canvas with chunky, rather than blurry, pixels.

Is there any way to make a whole webpage display with chunky 2x2 or 3x3 pixels? i.e. magnify it with a nearest neighbour zoom. I want to use normal HTML but pixelate it for a faux-8-bit look.

2

There are 2 best solutions below

7
On

You can use an svg filter for that.

Please note this solution isn't cross-browser.
As @adroste said, it won't work on Safari (Mac/iOS) or older browsers.

You'll need to tweak it to make it look the way you want, but here is a quick example:

html { filter: url("#pixelate") }
svg { display: block }
h1 { color: red }
h2 { color: blue}
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="0" height="0">
  <defs>
    <filter id="pixelate" x="0" y="0">
      <feFlood x="2" y="2" height="1" width="1"/> 
      <feComposite width="5" height="5"/>
      <feTile result="a"/>
      <feComposite in="SourceGraphic" in2="a" 
                   operator="in"/>
      <feMorphology operator="dilate"
                    radius="2.5"/>
    </filter>
  </defs>
</svg>    
<h1>
  Lorem ipsum, dolor sit amet consectetur adipisicing elit.
</h1>
<h2>
  Lorem ipsum dolor sit amet consectetur adipisicing elit. Reprehenderit, fuga.
</h2>
<p>
  Lorem ipsum, dolor sit amet consectetur adipisicing elit. Doloribus dolorem, maxime recusandae modi adipisci, praesentium qui aliquam consequatur tempore fugiat quasi minus necessitatibus excepturi enim sapiente quibusdam deleniti perferendis quisquam?
</p>

You can probably achieve this effect with different filters.
You can learn more about svf filter on MDN docs.

3
On

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://stackoverflow.com/a/66625778/6292230)

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)

  1. 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>
  1. 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);
}
  1. After page load / viewport resize call
pixelate(5, 1); // 5 = tileSize, 1 = std deviation gaussian blur
  1. 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:

  1. Render your page to DOM

  2. Render your page to a canvas (see html2canvas: http://html2canvas.hertzen.com or better rasterizeHTML: https://github.com/cburgmer/rasterizeHTML.js)

  3. Overlay the canvas position: absolute; left: 0; top: 0; width: 100%; z-index: 100;

  4. Don't catch clicks on the canvas so the buttons/links on the rendered DOM below will work pointer-events: none;

  5. 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)