2

I want a webgl canvas that I can seemlessly translate, rotate and scale with multitouch (using touch events).

So far I have a canvas that I can translate with 1 finger or scale with 2 fingers, but I'm having trouble integrating all the functionality into one.

So far I'm completely clueless how to support rotation at all.

My code:

const vertSource = `#version 300 es
in vec4 a_position;
in vec2 a_texcoord;
uniform mat4 u_matrix;
out vec2 v_texcoord;
void main() {
 gl_Position = u_matrix * a_position;
 
 v_texcoord = a_texcoord;
}`
const fragSource = `#version 300 es
precision highp float;
in vec2 v_texcoord;
uniform sampler2D u_texture;
out vec4 outColor;
void main() {
 outColor = texture(u_texture, v_texcoord);
}`
const css = `html, body, canvas {
 width: 100%;
 height: 100%;
}
body {
 overscroll-behavior-y: contain;
 overflow: hidden;
 touch-action: none;
}
*,
*::before,
*::after {
 box-sizing: border-box;
 margin: 0;
}`
function define(target, defines) {
 return Object.defineProperties(target, Object.getOwnPropertyDescriptors(defines))
}
function vec2(x, y) {
 if (x === undefined)
 x = 0
 if (y === undefined)
 y = x
 
 return define([x, y], {
 get x() { return this[0] },
 set x(value) { this[0] = value },
 get y() { return this[1] },
 set y(value) { this[1] = value },
 })
}
const mat4 = {
 translation(x, y, z = 0) {
 return [
 1, 0, 0, 0,
 0, 1, 0, 0,
 0, 0, 1, 0,
 x, y, z, 1,
 ]
 },
 xRotation(angle) {
 const c = Math.cos(angle)
 const s = Math.sin(angle)
 
 return [
 1, 0, 0, 0,
 0, c, s, 0,
 0, -s, c, 0,
 0, 0, 0, 1,
 ]
 },
 yRotation(angle) {
 const c = Math.cos(angle)
 const s = Math.sin(angle)
 
 return [
 c, 0, -s, 0,
 0, 1, 0, 0,
 s, 0, c, 0,
 0, 0, 0, 1,
 ]
 },
 zRotation(angle) {
 const c = Math.cos(angle)
 const s = Math.sin(angle)
 
 return [
 c, s, 0, 0,
 -s, c, 0, 0,
 0, 0, 1, 0,
 0, 0, 0, 1,
 ]
 },
 scale(x, y, z = 1) {
 if (y === undefined)
 y = x
 return [
 x, 0, 0, 0,
 0, y, 0, 0,
 0, 0, z, 0,
 0, 0, 0, 1,
 ]
 },
 projection(width, height, depth) {
 // Note: This matrix flips the Y axis so 0 is at the top.
 return [
 2 / width, 0, 0, 0,
 0, -2 / height, 0, 0,
 0, 0, 2 / depth, 0,
 -1, 1, 0, 1,
 ]
 },
 orthographic(left, right, bottom, top, near, far) {
 return [
 2 / (right - left), 0, 0, 0,
 0, 2 / (top - bottom), 0, 0,
 0, 0, 2 / (near - far), 0,
 
 (left + right) / (left - right),
 (bottom + top) / (bottom - top),
 (near + far) / (near - far),
 1,
 ]
 },
 mul(a, b) {
 const b00 = b[0 * 4 + 0]
 const b01 = b[0 * 4 + 1]
 const b02 = b[0 * 4 + 2]
 const b03 = b[0 * 4 + 3]
 const b10 = b[1 * 4 + 0]
 const b11 = b[1 * 4 + 1]
 const b12 = b[1 * 4 + 2]
 const b13 = b[1 * 4 + 3]
 const b20 = b[2 * 4 + 0]
 const b21 = b[2 * 4 + 1]
 const b22 = b[2 * 4 + 2]
 const b23 = b[2 * 4 + 3]
 const b30 = b[3 * 4 + 0]
 const b31 = b[3 * 4 + 1]
 const b32 = b[3 * 4 + 2]
 const b33 = b[3 * 4 + 3]
 const a00 = a[0 * 4 + 0]
 const a01 = a[0 * 4 + 1]
 const a02 = a[0 * 4 + 2]
 const a03 = a[0 * 4 + 3]
 const a10 = a[1 * 4 + 0]
 const a11 = a[1 * 4 + 1]
 const a12 = a[1 * 4 + 2]
 const a13 = a[1 * 4 + 3]
 const a20 = a[2 * 4 + 0]
 const a21 = a[2 * 4 + 1]
 const a22 = a[2 * 4 + 2]
 const a23 = a[2 * 4 + 3]
 const a30 = a[3 * 4 + 0]
 const a31 = a[3 * 4 + 1]
 const a32 = a[3 * 4 + 2]
 const a33 = a[3 * 4 + 3]
 
 return [
 b00 * a00 + b01 * a10 + b02 * a20 + b03 * a30,
 b00 * a01 + b01 * a11 + b02 * a21 + b03 * a31,
 b00 * a02 + b01 * a12 + b02 * a22 + b03 * a32,
 b00 * a03 + b01 * a13 + b02 * a23 + b03 * a33,
 b10 * a00 + b11 * a10 + b12 * a20 + b13 * a30,
 b10 * a01 + b11 * a11 + b12 * a21 + b13 * a31,
 b10 * a02 + b11 * a12 + b12 * a22 + b13 * a32,
 b10 * a03 + b11 * a13 + b12 * a23 + b13 * a33,
 b20 * a00 + b21 * a10 + b22 * a20 + b23 * a30,
 b20 * a01 + b21 * a11 + b22 * a21 + b23 * a31,
 b20 * a02 + b21 * a12 + b22 * a22 + b23 * a32,
 b20 * a03 + b21 * a13 + b22 * a23 + b23 * a33,
 b30 * a00 + b31 * a10 + b32 * a20 + b33 * a30,
 b30 * a01 + b31 * a11 + b32 * a21 + b33 * a31,
 b30 * a02 + b31 * a12 + b32 * a22 + b33 * a32,
 b30 * a03 + b31 * a13 + b32 * a23 + b33 * a33,
 ]
 },
}
const camera = vec2()
let rotation = 0
let scaling = 1
const tiles = [
 [0, 0],
 [1, 0],
 [2, 0],
 [3, 0],
 [4, 0],
 [5, 0],
 [6, 0],
 [0, 1],
 [0, 2],
 [0, 3],
 [0, 4],
 [1, 4],
 [2, 4],
 [3, 4],
 [4, 4],
 [5, 4],
 [6, 4],
 [6, 1],
 [6, 2],
 [6, 3],
]
function resizeCanvasToDisplaySize(canvas, mult = 1) {
 const width = canvas.clientWidth * mult | 0
 const height = canvas.clientHeight * mult | 0
 if (canvas.width != width || canvas.height != height) {
 canvas.width = width
 canvas.height = height
 return true
 }
 return false
}
function createShader(type, source) {
 const shader = gl.createShader(type)
 gl.shaderSource(shader, source)
 gl.compileShader(shader)
 const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS)
 if (success)
 return shader
 
 console.log(gl.getShaderInfoLog(shader))
 gl.deleteShader(shader)
 return undefined
}
function createProgram(vertexShader, fragmentShader) {
 const program = gl.createProgram()
 gl.attachShader(program, vertexShader)
 gl.attachShader(program, fragmentShader)
 gl.linkProgram(program)
 const success = gl.getProgramParameter(program, gl.LINK_STATUS)
 if (success)
 return program
 console.log(gl.getProgramInfoLog(program))
 gl.deleteProgram(program)
 return undefined
}
function setupTouchEvents() {
 let cam, p0, p1
 let rot, scale
 gl.canvas.ontouchstart = e => {
 const t0 = e.touches[0]
 const t1 = e.touches[1]
 const [x0, y0] = [t0.clientX, t0.clientY]
 const [x1, y1] = [t1?.clientX, t1?.clientY]
 
 if (e.touches.length == 1) {
 p0 = vec2(x0, y0)
 cam = vec2(camera.x, camera.y)
 }
 else {
 p0 = vec2(x0, y0)
 p1 = vec2(x1, y1)
 cam = vec2(camera.x, camera.y)
 scale = Math.hypot(p0.x - p1.x, p0.y - p1.y)
 }
 }
 gl.canvas.ontouchmove = e => {
 const t0 = e.touches[0]
 const t1 = e.touches[1]
 const [x0, y0] = [t0.clientX, t0.clientY]
 const [x1, y1] = [t1?.clientX, t1?.clientY]
 
 if (e.touches.length == 1) {
 camera.x = cam.x + (x0 - p0.x) / 32
 camera.y = cam.y + (y0 - p0.y) / 32
 }
 else {
 p0 = vec2(x0, y0)
 p1 = vec2(x1, y1)
 
 const newScale = Math.hypot(p0.x - p1.x, p0.y - p1.y)
 scaling = newScale / scale
 
 camera.x = cam.x + (x0 - p0.x) / 32
 camera.y = cam.y + (y0 - p0.y) / 32
 }
 }
 gl.canvas.ontouchend = e => {
 const t = e.changedTouches[0]
 const [x, y] = [t.clientX, t.clientY]
 
 
 if (t.identifier == 0) {
 
 }
 }
}
function main() {
 const style = document.createElement('style')
 style.textContent = css
 document.head.append(style)
 
 const canvas = document.createElement('canvas')
 document.body.append(canvas)
 
 define(globalThis, { gl: canvas.getContext('webgl2') })
 if (!gl)
 return
 
 // Create shader program
 const vertexShader = createShader(gl.VERTEX_SHADER, vertSource)
 const fragmentShader = createShader(gl.FRAGMENT_SHADER, fragSource)
 const program = createProgram(vertexShader, fragmentShader)
 
 // Find attribute and uniform locations
 const a_position = gl.getAttribLocation(program, 'a_position')
 const a_texcoord = gl.getAttribLocation(program, 'a_texcoord')
 const u_matrix = gl.getUniformLocation(program, 'u_matrix')
 // Create a vertex array object (attribute state)
 const vao = gl.createVertexArray()
 gl.bindVertexArray(vao)
 // Create position buffer
 const positionBuffer = gl.createBuffer()
 gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer)
 const positions = [
 0, 0, 0,
 0, 1, 0,
 1, 0, 0,
 1, 1, 0,
 ]
 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW)
 // Turn on the attribute
 gl.enableVertexAttribArray(a_position)
 gl.vertexAttribPointer(a_position, 3, gl.FLOAT, false, 0, 0)
 // Create texcoord buffer
 const texcoordBuffer = gl.createBuffer()
 gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer)
 const texcoords = [
 0, 0,
 0, 1,
 1, 0,
 1, 1,
 ]
 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(texcoords), gl.STATIC_DRAW)
 // Turn on the attribute
 gl.enableVertexAttribArray(a_texcoord)
 gl.vertexAttribPointer(a_texcoord, 2, gl.FLOAT, true, 0, 0)
 // Create a texture
 const texture = gl.createTexture()
 gl.activeTexture(gl.TEXTURE0 + 0)
 gl.bindTexture(gl.TEXTURE_2D, texture)
 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 2, 2, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([
 0, 0, 255, 255,
 255, 0, 0, 255,
 255, 0, 0, 255,
 0, 0, 255, 255,
 ]))
 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)
 
 // Create index buffer
 const indexBuffer = gl.createBuffer()
 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer)
 const indices = [
 0, 1, 2,
 2, 1, 3,
 ]
 gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW)
 
 setupTouchEvents()
 let lastTime = 0
 function drawScene(time) {
 // Subtract the previous time from the current time
 const delta = (time - lastTime) / 1000
 // Remember the current time for the next frame.
 lastTime = time
 
 resizeCanvasToDisplaySize(gl.canvas)
 gl.viewport(0, 0, gl.canvas.width, gl.canvas.height)
 
 // Turn on depth testing and culling
 gl.enable(gl.DEPTH_TEST)
 gl.enable(gl.CULL_FACE)
 
 // Clear color and depth information
 gl.clearColor(0, 0, 0, 1)
 gl.clearDepth(1)
 gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
 
 // Bind the attribute/buffer set we want
 gl.useProgram(program)
 gl.bindVertexArray(vao)
 
 const unit = 32
 let proj = mat4.projection(gl.canvas.clientWidth / unit, gl.canvas.clientHeight / unit, 400)
 proj = mat4.mul(proj, mat4.translation(camera.x, camera.y))
 proj = mat4.mul(proj, mat4.zRotation(rotation))
 proj = mat4.mul(proj, mat4.scale(scaling))
 
 tiles.forEach(([x, y]) => {
 // Transform matrix
 const matrix = mat4.mul(proj, mat4.translation(x, y))
 
 // Set the matrix
 gl.uniformMatrix4fv(u_matrix, false, matrix)
 
 // Draw the geometry
 gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0)
 })
 
 // Call drawScene again next frame
 requestAnimationFrame(drawScene)
 }
 requestAnimationFrame(drawScene)
}
main()
asked Jun 22, 2025 at 12:39

0

Know someone who can answer? Share a link to this question via email, Twitter, or Facebook.

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.