JSFiddle - React, Tailwind, and code Playground

HTML

<script id="vert" type="text/glsl">
uniform vec2 u_offset;
attribute vec2 a_position;
attribute vec2 a_texCoord;
varying vec2 v_texCoord;
void main() {
  gl_Position = vec4(a_position.x + u_offset.x, 1.0 + 2.0 * (-a_position.y + u_offset.y), 0.0, 1.0);
  v_texCoord = a_texCoord;
}
</script>

<script id="frag" type="text/glsl">
uniform sampler2D u_sampler;
varying mediump vec2 v_texCoord;
void main() {
  gl_FragColor = texture2D(u_sampler, v_texCoord);
}
</script>

<p>
Right: passing the HTMLImageElement straight to texImage2D (good)<br>
Left: rendering the HTMLImageElement to a Canvas2D, then passing that to texImage2D (bad)<br>
Both: pixel perfect transform, nearest neighbour, rendered with blendFunc(ONE, ONE_MINUS_SRC_ALPHA)<br>
<br>
<canvas width=512 height=256 id=canvas></canvas>
</p>

<p>
Original image, premultiplied alpha baked into the PNG, but here rendered by the browser as if it was not premultiplied:<br>
<br>
<img id=image...

CSS

body { background-color: #222; color: #ddd; }

JavaScript

var canvas = document.getElementById('canvas');
var gl = canvas.getContext('webgl', {
  alpha: false,
  premultipliedAlpha: false,
});

var image = document.getElementById('image');

var texture1 = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture1);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);

var canvas2d = document.createElement('canvas');
canvas2d.width = 256;
canvas2d.height = 256;
ctx = canvas2d.getContext('2d');
ctx.drawImage(image, 0, 0);

var texture2 = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture2);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas2d);

var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]), gl.STATIC_DRAW);

var vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, document.getElementById('vert').innerText);
gl.compileShader(vertexShader);
console.log(gl.getShaderInfoLog(vertexShader));

var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, document.getElementById('frag').innerText);
gl.compileShader(fragmentShader);
console.log(gl.getShaderInfoLog(fragmentShader));

var program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.validateProgram(program);
console.log(gl.getProgramInfoLog(program));
gl.useProgram(program);

var a_position = gl.getAttribLocation(program, 'a_position');
gl.enableVertexAttribArray(a_position);
gl.vertexAttribPointer(a_position, 2, gl.FLOAT, false, 0, 0);
var a_texCoord = gl.getAttribLocation(program,...