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

0

Your Answer

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.