-
-
Save vjancik/98faf183e332d1f793fd997e150eb017 to your computer and use it in GitHub Desktop.
Webgl Image Viewer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const canvas = document.getElementById("canvas"); | |
const gl = canvas.getContext("webgl", { depth: false, stencil: false }); | |
// TODO: get from GL | |
const MAX_CANVAS_DIM = 32767; | |
const MAX_TEXTURE_SIZE = gl.getParameter(gl.MAX_TEXTURE_SIZE); | |
const vertShaderSrc = ` | |
attribute vec4 a_position; | |
attribute vec2 a_texCoord; | |
uniform mat4 u_mMVP; | |
varying vec2 v_texCoord; | |
void main() { | |
gl_Position = u_mMVP * a_position; | |
v_texCoord = a_texCoord; | |
}`; | |
const fragShaderSrc = ` | |
#ifdef GL_FRAGMENT_PRECISION_HIGH | |
precision highp float; | |
precision highp int; | |
precision highp sampler2D; | |
#else | |
precision mediump float; | |
precision mediump int; | |
precision mediump sampler2D; | |
#endif | |
uniform sampler2D u_image; | |
varying vec2 v_texCoord; | |
void main() { | |
gl_FragColor = texture2D(u_image, v_texCoord); | |
gl_FragColor.rgb *= gl_FragColor.a; | |
} | |
`; | |
gl.disable(gl.CULL_FACE); | |
gl.disable(gl.BLEND); | |
//gl.frontFace(gl.CCW); | |
gl.clearColor(0, 0, 0, 0); | |
const scale = window.devicePixelRatio || 1; | |
canvas.width = canvas.clientWidth * scale; | |
canvas.height = canvas.clientHeight * scale; | |
gl.viewport(0, 0, canvas.width, canvas.height); | |
const program = gl.createProgram(); | |
const vertShader = gl.createShader(gl.VERTEX_SHADER); | |
const fragShader = gl.createShader(gl.FRAGMENT_SHADER); | |
gl.shaderSource(vertShader, vertShaderSrc); | |
gl.compileShader(vertShader); | |
gl.shaderSource(fragShader, fragShaderSrc); | |
gl.compileShader(fragShader); | |
gl.attachShader(program, vertShader); | |
gl.attachShader(program, fragShader); | |
gl.linkProgram(program); | |
const linked = gl.getProgramParameter(program, gl.LINK_STATUS); | |
if (!linked) { | |
console.error(gl.getProgramInfoLog(program)); | |
throw new Error("Failed to compile shader"); | |
} | |
function triangleStripQuad(sX, sY, width, height) { | |
const res = new Float32Array(8); | |
res[0] = res[2] = sX; | |
res[1] = res[5] = sY; | |
res[4] = res[6] = sX + width; | |
res[3] = res[7] = sY + height; | |
return res; | |
} | |
const mMVPLoc = gl.getUniformLocation(program, "u_mMVP"); | |
const positionLoc = gl.getAttribLocation(program, "a_position"); | |
const positionBuffer = gl.createBuffer(); | |
const texCoordLoc = gl.getAttribLocation(program, "a_texCoord"); | |
const texCoordBuffer = gl.createBuffer(); | |
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer); | |
gl.vertexAttribPointer(texCoordLoc, 2, gl.FLOAT, false, 0, 0); | |
gl.bufferData(gl.ARRAY_BUFFER, triangleStripQuad(0,0,1,1), gl.STATIC_DRAW); | |
gl.enableVertexAttribArray(texCoordLoc); | |
gl.useProgram(program); | |
// TODO: add function for testing whether a screen coordinate is in image | |
// TODO: mRotateate and mZoom must be applied at the same time to use the same coordinates, turn into mTransform, save all 3 separate transformations | |
// fixed ortho projection matrix looking down -z | |
const mOrtho = glMatrix.mat4.create(); | |
glMatrix.mat4.ortho(mOrtho, -canvas.width / 2, canvas.width / 2, canvas.height / 2, -canvas.height / 2, 1, -1); | |
const mMV = glMatrix.mat4.create(); | |
const mMVP = glMatrix.mat4.create(); | |
const mTransform = glMatrix.mat4.create(); | |
const fitImageScale = [1, 1, 1]; // view | |
const viewZoom = [1, 1, 1]; // view | |
let rotateRad = 0; | |
const viewAxis = [0, 0, -1]; | |
// const center = glMatrix.vec3.fromValues(0.5, 0.5, 0); // model | |
// const invCenter = glMatrix.vec3.negate(glMatrix.vec3.create(), center); | |
const render = () => { | |
glMatrix.mat4.copy(mMV, mTransform); // view | |
glMatrix.mat4.scale(mMV, mMV, fitImageScale); // model -> view | |
glMatrix.mat4.mul(mMVP, mOrtho, mMV); // view -> proj | |
gl.uniformMatrix4fv(mMVPLoc, false, mMVP); | |
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); | |
} | |
const texture = gl.createTexture(); | |
gl.bindTexture(gl.TEXTURE_2D, texture); | |
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); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); | |
const imageURL = "https://upload.wikimedia.org/wikipedia/commons/4/45/Eopsaltria_australis_-_Mogo_Campground.jpg"; | |
const image = new Image(); | |
image.crossOrigin = "anonymous"; | |
image.onload = (evt) => { | |
const width = Math.min(MAX_TEXTURE_SIZE, evt.target.width); | |
const height = Math.min(MAX_TEXTURE_SIZE, evt.target.height); | |
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); | |
gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0); | |
gl.bufferData(gl.ARRAY_BUFFER, triangleStripQuad(-width/2,-height/2,width,height), | |
gl.STATIC_DRAW); | |
gl.enableVertexAttribArray(positionLoc); | |
const imageAspectRatio = width / height; | |
const canvasAspectRatio = canvas.clientWidth / canvas.clientHeight; | |
if (imageAspectRatio > canvasAspectRatio) | |
fitImageScale[0] = fitImageScale[1] = canvas.width / width; | |
else | |
fitImageScale[0] = fitImageScale[1] = canvas.height / height; | |
if (Math.max(evt.target.width, evt.target.height) > MAX_TEXTURE_SIZE) { | |
const offscreen = new OffscreenCanvas(width, height); | |
const ctx = offscreen.getContext("2d"); | |
ctx.drawImage(evt.target, 0, 0, width, height); | |
const imageData = ctx.getImageData(0, 0, width, height); | |
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, imageData); | |
} else { | |
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, evt.target); | |
} | |
window.requestAnimationFrame(render); | |
} | |
image.src = imageURL; | |
function arrMatToRows(arrMat, res = [new Array(4), new Array(4), new Array(4), new Array(4)]) { | |
const dim = Math.sqrt(arrMat.length); | |
for (let i = 0; i < dim; ++i) { | |
for (let j = 0; j < dim; ++j) { | |
res[i][j] = arrMat[j*dim + i]; | |
} | |
} | |
return res; | |
} | |
function rowsToArrMat(rows, res = new Array(16)) { | |
const dim = rows.length; | |
for (let i = 0; i < dim; ++i) { | |
for (let j = 0; j < dim; ++j) { | |
res[i*4 + j] = typeof rows[j][i] === "number" ? rows[j][i] : rows[j][i].re; | |
} | |
} | |
return res; | |
} | |
const scrollScale = glMatrix.vec3.fromValues(1, 1, 1); | |
const scrollOrigin = glMatrix.vec3.fromValues(0, 0, 0); | |
const invScrollOrigin = glMatrix.vec3.fromValues(0, 0, 0); | |
const mZoom = glMatrix.mat4.create(); | |
const mZoomInvert = glMatrix.mat4.create(); | |
let zoomInCount = 0; | |
let invRootMatrix; | |
const invStepRotQuat = glMatrix.quat.create(); | |
const invStepRotAxis = glMatrix.vec3.fromValues(0, 0, 0); | |
let invStepRotAngle = 0; | |
const arrBuff = glMatrix.mat4.create(); | |
const rowBuff = [new Array(4), new Array(4), new Array(4), new Array(4)]; | |
// TODO: scroll only when wheel event is above the image | |
canvas.addEventListener("wheel", evt => { | |
evt.preventDefault(); | |
if (evt.deltaY < 0) { | |
scrollScale[0] = 1.2; | |
scrollScale[1] = 1.2; | |
glMatrix.vec3.mul(viewZoom, viewZoom, scrollScale); | |
scrollOrigin[0] = evt.offsetX - evt.target.clientWidth / 2; | |
scrollOrigin[1] = evt.offsetY - evt.target.clientHeight / 2; | |
glMatrix.vec3.negate(invScrollOrigin, scrollOrigin); | |
glMatrix.mat4.fromTranslation(mZoom, scrollOrigin); | |
glMatrix.mat4.scale(mZoom, mZoom, scrollScale); | |
glMatrix.mat4.translate(mZoom, mZoom, invScrollOrigin); | |
glMatrix.mat4.mul(mTransform, mZoom, mTransform); | |
++zoomInCount; | |
invRootMatrix = null; | |
} else if (evt.deltaY > 0) { | |
if (viewZoom[0] <= 1 + 2**(-15)) return; | |
if (zoomInCount && !invRootMatrix) { | |
glMatrix.mat4.invert(mZoomInvert, mTransform); | |
let eigs | |
try { | |
eigs = math.eigs(arrMatToRows(mZoomInvert, rowBuff)); | |
} catch (e) { | |
console.log("Eig decomp cocked up"); | |
console.log(mTransform, arrMatToRows(mZoomInvert, rowBuff), viewZoom, rotateRad); | |
throw e; | |
} | |
const ERoot = math.dotPow(eigs.values, 1 / zoomInCount); | |
invRootMatrix = rowsToArrMat(math.multiply(eigs.vectors, math.diag(ERoot), math.inv(eigs.vectors)), arrBuff); | |
glMatrix.mat4.getScaling(scrollScale, invRootMatrix); | |
glMatrix.mat4.getRotation(invStepRotQuat, invRootMatrix); | |
invStepRotAngle = glMatrix.quat.getAxisAngle(invStepRotAxis, invStepRotQuat); | |
} | |
glMatrix.mat4.mul(mTransform, invRootMatrix, mTransform); | |
glMatrix.vec3.mul(viewZoom, viewZoom, scrollScale); | |
rotateRad += (invStepRotAxis[2] > 0 ? -1 : 1) * invStepRotAngle; | |
--zoomInCount; | |
if (viewZoom[0] < 1.1) | |
console.log(viewZoom[0], rotateRad); | |
} | |
// console.log(glMatrix.mat4.str(mZoom)); | |
window.requestAnimationFrame(render); | |
}); | |
const panOp = document.getElementById("panOp"); | |
const rotateOp = document.getElementById("rotateOp") | |
let operation = "pan"; | |
const changeOperation = evt => { | |
operation = evt.target.value; | |
if (evt.target.id !== panOp.id) panOp.checked = false; | |
if (evt.target.id !== rotateOp.id) rotateOp.checked = false; | |
} | |
panOp.addEventListener("change", changeOperation); | |
rotateOp.addEventListener("change", changeOperation); | |
let rotateCenter = glMatrix.vec3.fromValues(0, 0, 0); | |
let invRotateCenter = glMatrix.vec3.fromValues(0, 0, 0); | |
let mousePressed = false; | |
let mouseMoved = false; | |
let crossVec = glMatrix.vec3.create(); | |
let mRotate = glMatrix.mat4.create(); | |
const startPos = new Array(2); | |
const onMouseDown = (evt) => { | |
mousePressed = true; | |
startPos[0] = evt.offsetX; | |
startPos[1] = evt.offsetY; | |
if (operation == "pan") { | |
rotateCenter[0] = 0; | |
rotateCenter[1] = 0; | |
} | |
} | |
const onMouseUp = (evt) => { | |
if (operation == "rotate" && !mouseMoved) { | |
rotateCenter[0] = startPos[0] - evt.target.clientWidth / 2; | |
rotateCenter[1] = startPos[1] - evt.target.clientHeight / 2; | |
glMatrix.vec2.negate(invRotateCenter, rotateCenter); | |
} | |
mousePressed = false; | |
mouseMoved = false; | |
} | |
const onMouseMove = (evt) => { | |
if (!mousePressed) return; | |
mouseMoved = true; | |
if (operation == "pan") { | |
const transVec = [evt.offsetX - startPos[0], evt.offsetY - startPos[1], 0]; | |
glMatrix.vec3.div(transVec, transVec, viewZoom); | |
glMatrix.mat4.rotate(mTransform, mTransform, -rotateRad, viewAxis); | |
glMatrix.mat4.translate(mTransform, mTransform, transVec); | |
glMatrix.mat4.rotate(mTransform, mTransform, rotateRad, viewAxis); | |
rotateCenter[0] += evt.offsetX - startPos[0]; | |
rotateCenter[1] += evt.offsetY - startPos[1]; | |
glMatrix.vec2.negate(invRotateCenter, rotateCenter); | |
} else if (operation == "rotate") { | |
const centerX = evt.target.clientWidth / 2 + rotateCenter[0]; | |
const centerY = evt.target.clientHeight / 2 + rotateCenter[1]; | |
const vec1 = [startPos[0] - centerX, startPos[1] - centerY]; | |
const vec2 = [evt.offsetX - centerX, evt.offsetY - centerY]; | |
const angle = glMatrix.vec2.angle(vec1, vec2); | |
glMatrix.vec2.cross(crossVec, vec1, vec2); | |
const stepRotateRad = (crossVec[2] > 0 ? -1 : 1) * angle; | |
glMatrix.mat4.fromTranslation(mRotate, rotateCenter); | |
glMatrix.mat4.rotate(mRotate, mRotate, stepRotateRad, viewAxis); | |
glMatrix.mat4.translate(mRotate, mRotate, invRotateCenter); | |
glMatrix.mat4.mul(mTransform, mRotate, mTransform); | |
rotateRad += stepRotateRad; | |
} | |
startPos[0] = evt.offsetX; | |
startPos[1] = evt.offsetY; | |
window.requestAnimationFrame(render); | |
}; | |
const initPanning = () => { | |
canvas.addEventListener("mousemove", onMouseMove); | |
canvas.addEventListener("mousedown", onMouseDown); | |
canvas.addEventListener("mouseup", onMouseUp); | |
canvas.addEventListener("mouseleave", onMouseUp); | |
}; | |
initPanning(); | |
const error = gl.getError(); | |
if (error) console.log("Error Code: " + error); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment