htmlcanvasrotationtrigonometrycover

Scale rotated image to fill HTML5 Canvas using JavaScript trigonometry?


Below is the code I am currently using. With 0 rotation, the image is correctly scaled to fill the canvas. (similar to background-size: cover, except using JavaScript on a Canvas)

I'm trying to add a rotate feature, with the following features.

  1. Keep image centered while rotating. I tried using width / 2 in translate, and then the opposite in drawImage, but as you can see the image did not stay centered. I'm not sure if this is a conflict with my earlier x and y centering code, or if trigonometry is required here?

  2. Automatically scale the image further to cover the canvas. These are arbitrary rotations, not 90 degree increments. I don't want to crop any more of the image than necessary to fill the canvas corners. This is much more complex trigonometry than I am used to dealing with.

I think this can be done with Math.sin and Math.cos, but it's been a very long time since I've used them. I'm especially uncertain about how to achieve #2. Any help would be appreciated.

var canvas = document.querySelector('canvas')
var context = canvas.getContext('2d')
var image = new Image()
image.src = 'https://i.sstatic.net/7FsbT.jpg'

image.onload = function () {
  
  var maxScaleX = canvas.width / image.width
  var maxScaleY = canvas.height / image.height
  scale = maxScaleX > maxScaleY ? maxScaleX : maxScaleY
  
  var width = image.width * scale
  var height = image.height * scale
  var x = (width - canvas.width) / -2
  var y = (height - canvas.height) / -2
  
  var degrees = 30
  
  context.setTransform(1, 0, 0, 1, 0, 0) // reset previous translate and/or rotate
  context.translate(width / 2, height / 2)
  context.rotate(degrees * Math.PI / 180)
  
  context.drawImage(image, x - width / 2, y - height / 2, width, height)

}
<canvas width="350" height="150" style="border: solid 1px black"></canvas>


Solution

  • +1 to Blindman67's answer for this to work.

    I shortened the code and simplified for my use case. You should be able to just include function drawBestFit() into your app and use it to fill any canvas with any loaded image.

    For a better understanding of how this works, view Blindman67's original answer - especially the demonstration snippet at the bottom.

    var canvas = document.querySelector('canvas')
    var context = canvas.getContext('2d')
    var image = new Image()
    image.src = 'https://i.sstatic.net/7FsbT.jpg'
    
    image.onload = function() {
    
      var degrees = 0
    
      loop()
    
      function loop() {
        degrees += .5
        drawBestFit(context, degrees * Math.PI / 180, image)
        requestAnimationFrame(loop)
      }
    
      function drawBestFit(ctx, angle, image) {
        
        var dist = Math.sqrt(Math.pow(canvas.width, 2) + Math.pow(canvas.height, 2))
        var diagAngle = Math.asin(canvas.height / dist)
    
        var a1 = ((angle % (Math.PI * 2)) + Math.PI * 4) % (Math.PI * 2)
        if (a1 > Math.PI)
          a1 -= Math.PI
        if (a1 > Math.PI / 2 && a1 <= Math.PI)
          a1 = (Math.PI / 2) - (a1 - (Math.PI / 2))
        
        var ang1 = Math.PI / 2 - diagAngle - Math.abs(a1)
        var ang2 = Math.abs(diagAngle - Math.abs(a1))
        
        var scale1 = Math.cos(ang1) * dist / image.height
        var scale2 = Math.cos(ang2) * dist / image.width
        
        var scale = Math.max(scale1, scale2)
        
        var dx = Math.cos(angle) * scale
        var dy = Math.sin(angle) * scale
        ctx.setTransform(dx, dy, -dy, dx, canvas.width / 2, canvas.height / 2)
        
        ctx.drawImage(image, -image.width / 2, -image.height / 2, image.width, image.height)
        
        ctx.setTransform(1, 0, 0, 1, 0, 0) // reset transformations when done
    
      }
    
    }
    <canvas width="350" height="200" style="border: solid 1px black"></canvas>