viewport(x, y, width, height) scales clip space's -1 to 1 into a rectangle of the drawing buffer (pixels from the bottom-left), distorting the image if the aspects differ. The scissor test cuts instead: with SCISSOR_TEST enabled, scissor() discards fragments outside its box, and it also limits clear():
<canvas id="c" width="480" height="120" style="width:480px;height:120px"></canvas>
<script src="webgl-helper.js"></script>
<script>
const gl = GLH.context('c');
GLH.load(gl, `#version 300 es
out vec3 vColor;
void main() {
vColor = vec3[3](vec3(0.88, 0.6, 0.06), vec3(0.12, 0.37, 0.55),
vec3(0.16, 0.62, 0.56))[gl_VertexID];
gl_Position = vec4(vec2[3](vec2(0, -0.8), vec2(-0.8, 0.8), vec2(0.8))[gl_VertexID], 0, 1);
}`, `#version 300 es
precision mediump float; in vec3 vColor; out vec4 c; void main() { c = vec4(vColor, 1); }`);
gl.enable(gl.SCISSOR_TEST);
gl.scissor(0, 0, 480, 85); // keep only the bottom 85 rows
gl.clearColor(0.85, 0.82, 0.76, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT); // clear() obeys the scissor box too
gl.viewport(0, 0, 120, 120); // left: square, undistorted
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.viewport(120, 0, 360, 120); // right: stretched 3:1
gl.drawArrays(gl.TRIANGLES, 0, 3);
window.__done = true;
</script>
The viewport is set when the context is created and never updated: after canvas.width = 600 it still read 0, 0, 300, 150 here, so call gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight) after resizing. All enable() capabilities start disabled except DITHER.
<!doctype html>
<style>
body { margin: 0; background: #f7f4ee; }
canvas { display: block; width: 100%; max-width: 480px; }
</style>
<canvas id="c" width="480" height="120"></canvas>
<script>
const gl = document.getElementById('c').getContext('webgl2');
gl.clearColor(0.93, 0.91, 0.87, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
const program = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, `#version 300 es
out vec3 vColor;
void main() {
vColor = vec3[3](vec3(0.88, 0.6, 0.06), vec3(0.12, 0.37, 0.55),
vec3(0.16, 0.62, 0.56))[gl_VertexID];
gl_Position = vec4(vec2[3](vec2(0, -0.8), vec2(-0.8, 0.8), vec2(0.8))[gl_VertexID], 0, 1);
}`], [gl.FRAGMENT_SHADER, `#version 300 es
precision mediump float; in vec3 vColor; out vec4 c; void main() { c = vec4(vColor, 1); }`]]) {
const s = gl.createShader(type);
gl.shaderSource(s, src); gl.compileShader(s);
gl.attachShader(program, s);
}
gl.linkProgram(program);
gl.useProgram(program);
gl.bindVertexArray(gl.createVertexArray());
gl.enable(gl.SCISSOR_TEST);
gl.scissor(0, 0, 480, 85); // keep only the bottom 85 rows
gl.clearColor(0.85, 0.82, 0.76, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT); // clear() obeys the scissor box too
gl.viewport(0, 0, 120, 120); // left: square, undistorted
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.viewport(120, 0, 360, 120); // right: stretched 3:1
gl.drawArrays(gl.TRIANGLES, 0, 3);
</script>