7

I've created a canvas with width=16 and height=16. Then I used WebGL to render an image to it. This is what it looks like:

enter image description here

Afterwards, I scaled the canvas by using width: 256px and height: 256px. I also set image-rendering to pixelated:

      canvas {
        image-rendering: optimizeSpeed;             /* STOP SMOOTHING, GIVE ME SPEED  */
        image-rendering: -moz-crisp-edges;          /* Firefox                        */
        image-rendering: -o-crisp-edges;            /* Opera                          */
        image-rendering: -webkit-optimize-contrast; /* Chrome (and eventually Safari) */
        image-rendering: pixelated; /* Chrome */
        image-rendering: optimize-contrast;         /* CSS3 Proposed                  */
        -ms-interpolation-mode: nearest-neighbor;   /* IE8+                           */
        width: 256px;
        height: 256px;
      }

This is the result:

enter image description here

The image is blurred. Why? I'm using Safari 12.0.2 on OSX Mojave.

MaiaVictor
  • 51,090
  • 44
  • 144
  • 286
  • HTML5 Canvas element produce raster based images. They will pixilate (blur) by their nature. – Randy Casburn Jan 28 '19 at 00:09
  • How are you drawing the image? Got come code? – Matt Way Jan 28 '19 at 00:18
  • @MattWay I didn't post the code because it is too big to serve as a minimal example, and making a simple demo on WebGL is too laborious. Since we have a satisfactory answer already I don't think that is needed. But if you want I can post it here. – MaiaVictor Jan 28 '19 at 13:46

2 Answers2

6

Safari does not yet support image-rendering: pixelated; on WebGL. Filed a bug

Also crisp-edges does not != pixelated. crisp-edges could be any number of algorithms. It does not mean pixelated. It means apply some algorithm that keeps crisp edges of which there are tons of algorithms.

The spec itself shows examples:

Given this image:

enter image description here

This is pixelated:

enter image description here

IMPORTANT: See update at bottom Where as a browser is allowed to use a variety of algorithms for crisp-edges so for example the result could be

enter image description here

So in other words your CSS may not produce the results you expect. If a browser doesn't support pixelated but does support crisp-edges and if they use an algorithm like above then you won't a pixelated look.

The most performant way to draw pixelated graphics without image-rendering: pixelated is to draw to a small texture and then draw that texture to the canvas with NEAREST filtering.

const vs = `
attribute vec4 position;
void main() {
  gl_Position = position;
}
`;
const fs = `
precision mediump float;
void main() {
  gl_FragColor = vec4(1, 0, 0, 1);
}
`;

const screenVS = `
attribute vec4 position;
varying vec2 v_texcoord;
void main() {
  gl_Position = position;
  // because we know position goes from -1 to 1
  v_texcoord = position.xy * 0.5 + 0.5;
}
`;
const screenFS = `
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D u_tex;
void main() {
  gl_FragColor = texture2D(u_tex, v_texcoord);
}
`;

const gl = document.querySelector('canvas').getContext('webgl', {antialias: false});

// compile shaders, link programs, look up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
const screenProgramInfo = twgl.createProgramInfo(gl, [screenVS, screenFS]);


const width = 16;
const height = 16;
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

const fb = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);

// create buffers and put data in
const quadBufferInfo = twgl.createBufferInfoFromArrays(gl, {
  position: { 
    numComponents: 2,
    data: [
      -1, -1, 
       1, -1,
      -1,  1,
      -1,  1, 
       1, -1,
       1,  1,
    ],
  }
});


render();

function render() {
  // draw at 16x16 to texture
  gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
  gl.viewport(0, 0, width, height);
  gl.useProgram(programInfo.program);
  // bind buffers and set attributes
  twgl.setBuffersAndAttributes(gl, programInfo, quadBufferInfo);
  
  gl.drawArrays(gl.TRIANGLES, 0, 3);  // only draw the first triangle
  
  // draw texture to canvas
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
  gl.useProgram(screenProgramInfo.program);
  // bind buffers and set attributes
  twgl.setBuffersAndAttributes(gl, screenProgramInfo, quadBufferInfo);
  // uniforms default to 0 so in this simple case
  // no need to bind texture or set uniforms since
  // we only have 1 texture, it's on texture unit 0
  // and the uniform defaults to 0
  
  gl.drawArrays(gl.TRIANGLES, 0, 6);
}
<canvas width="256" height="256"></canvas>
<script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>

Note: if you're rendering 3D or for some other reason need a depth buffer you'll need to add a depth renderbuffer attachment to the framebuffer.

Note that optimizeSpeed is not a real option either. It's been long deprecated and like crisp-edges is up to the browser to interpret.

Update

The spec changed in Feb 2021. crisp-edges now means "use nearest neighbor" and pixelated means "keep it looking pixelated" which can be translated as "if you want to then do something better than nearest neighbor that keeps the image pixelated". See this answer

gman
  • 100,619
  • 31
  • 269
  • 393
  • 1
    Safari does support `image-rendering: pixelated` on , and 2d canvas. – Kaiido Jan 28 '19 at 04:20
  • 1
    And even though it is true `crisp-edges` may use any number of algos (nobody said otherwise btw), this doesn't mean it can be any algo. The rules are "*preserves contrast and edges in the image, and which does not smooth colors or introduce blur to the image in the process*". The result in OP's screenshot could not be the one of any allowed algo, but as per my previous comment, it doesn't matter since Safari supports pixelated. – Kaiido Jan 28 '19 at 04:30
  • 1
    crisp-edges is ambiguous intentionally. It allows algorithms that try to guess the original intent of the image from like waifu, HQX, XBR, Super-Eagle. Those algorithms do not smooth colors or blur. They try to keep edges crisp. Their intent can be summed up as "you took a high res vector image and scaled it down, now scale it back up to look like the original vector image" – gman Jan 28 '19 at 04:34
  • No, they will preserve crisp edges for some definition of crisp. [example](https://imgur.com/jEmbN39). Most of [these algorithms](https://en.wikipedia.org/wiki/Pixel-art_scaling_algorithms) fit the definition for crisp edges. The [spec itself shows the difference between pixelated and crisp-edges](https://drafts.csswg.org/css-images-3/#example-448b3881) – gman Jan 28 '19 at 05:00
  • 1
    No need to dv ;-) And once again, all this is irrelevant, the rule that gets applied is the latest supported one, `pixelated`. Keep only your workaround and you'll get my up vote, since it is better explained as in my lazy bullet list. – Kaiido Jan 28 '19 at 06:19
  • 1
    your answer is wrong. (1) having to write a pixelation shader, no idea why that complication. (2) suggests drawing to a canvas with imageSmoothingEnabled but that's not supported everywhere WebGL is (3) you reference some irrelevant thing called CSS-Houdini which maybe be years in the future, will still not work on old browsers, and may never ship (4) putting pixelated last doesn't help if it doesn't exist (firefox) and the fact that crisp-edges may not give you the results you want. Browsers could change it any day to have results like in the spec. – gman Jan 28 '19 at 06:33
  • 1
    conversely my answer gives a solution that works everywhere WebGL does and points out why falling back to crisp-edges if pixelated is not supported is not actually a solution – gman Jan 28 '19 at 06:33
  • 1
    Ok I edited *pixelation shader* since what I meant was to do it ourself, by any mean, I don't care. The real point is that this is a Webkit bug, that it can't be feature detected and that it needs a workaround to be applied anywhere. And you (maybe unintentionally) missed the fact that my point 2 consists of both using imageSmoothingEnabled (which btw with the different browser prefixes has a good browser support, at least comparable to webgl), and upscaling using CSS (which would cover all UAs OPs hoped to cover). – Kaiido Jan 28 '19 at 06:41
  • 1
    If you don't care why do you keep coming back to comment on my answer? You don't know all the UAs he hoped to cover. You gave him a half-baked solution for which he'll likely have to ask another question when he runs into it not working elsewhere or if any browser that doesn't support pixelated but changes their crisp-edges implementation to something else. Even optimize-contrast does not equal "pixelated" and says so its outdated spec. – gman Jan 28 '19 at 10:12
2

This is a very old Webkit bug, from before the Blink fork happened. Since then, Blink fixed it, Webkit still hasn't.
You may want to let them know it's still a problem by commenting on the still open issue.

As for a workaround, there are several, but no perfect one.

  • The first one would be to draw your scene at the correct size directly and make the pixelation yourself.
  • An other one would be to render your webgl canvas on a 2d canvas (that you would resize using your CSS trick, or render directly at the correct size using the 2d context imageSmoothingEnabled property.
  • CSS-Houdini will probably allow us to workaround this issue ourselves.

But the real problem here is to find out if you need this workaround. I don't see any mean to feature-test this case (at least without Houdini), so this means that you'd either have to do ugly user-agent detection, or to apply the workaround to everyone.

Kaiido
  • 123,334
  • 13
  • 219
  • 285