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()