Emulate object-fit using animatable properties

Viewed 102

I'm trying to make a lightbox that needs an animation that starts from a thumbnail to a fullscreen image.

On the thumbnail state, the image will be kept inside a container with a fixed aspect ratio (say, 3:2). Every image on the gallery will have to fit within this allotted space, covering its entire surface (usually object-fit: cover).

On the fullscreen state, the container will expand to something like 100vw:100vh, and the image can expand (or even contract, the case for some unusually tall image) to fit within (object-fit: contain).

The <img> must always keep its aspect ratio untouched (on both states, and during the animation).

I can animate the container's width and height (they have absolute values).

But object-fit animation is discrete.

Here is a pen coding both states and container's animation: https://codepen.io/rslemos/pen/PoNMymR (to see the animation, one should remove the expanded class from the container element).

Is there any CSS trick to emulate object-fit using linear properties?

EDIT

During the animation, the image itself - I mean, the surface covered by the bitmap, not <img> element, which could be bigger or smaller than <img> in any direction - should morph from its initial to its final size.

I've also edited the pen to show the initial state when the container aspect ratio (width:height) is < 1 (using the same image, with aspect ratio > 1).

2 Answers

The solution looks complicated but is not very complex, it's just a bunch of math...

  1. First, You need to figure out the difference between the ratio of your image and the ratio of the box you want to fit it into (either a div or the whole screen).

  2. Then you need to figure out whether the size is constrained by height of by width depending on the ratio difference (see step 1) and whether you want to cover (maximize smallest dimension) or contain (minimize largest dimension)

  3. Finally, you need to toggle between the different boxes (div or whole screen). Here I'm not accounting for the position of the div (always top left corner) because I want the math to remain simple for this demonstration.

const img = document.querySelector('img')
const div = document.querySelector('.box')

// initialize some stuff
const {offsetHeight: boxHeight, offsetWidth: boxWidth} = div
let naturalHeight, naturalWidth
img.addEventListener('load', () => {
  naturalHeight = img.naturalHeight
  naturalWidth = img.naturalWidth  
  scaleToBox()
  requestAnimationFrame(
    () => img.classList.remove('initial')
  )
})

function scaleToBox() {
  let scale
  let x = 0, y = 0
  // is image ratio wider than container ratio
  if (boxWidth / boxHeight < naturalWidth / naturalHeight) {
    // width is the largest dimension, maximize height to cover
    scale = boxHeight / naturalHeight
    // center along x axis
    x = -(naturalWidth * scale - boxWidth) / 2 / scale
  } else {
    // height is the largest dimension, maximize width to cover
    scale = boxWidth / naturalWidth
    // center along y axis
    y = -(naturalHeight * scale - boxHeight) / 2 / scale
  }
  img.style.setProperty('transform', `scale(${scale}) translate(${x}px, ${y}px)`)
}

function scaleToScreen() {
  let scale
  let x = 0, y = 0
  // is image ratio wider than container ratio
  if (innerWidth / innerHeight < naturalWidth / naturalHeight) {
    // width is the largest dimension, maximize width to contain
    scale = innerWidth / naturalWidth
    // center along y axis
    y = -(naturalHeight * scale - innerHeight) / 2 / scale
  } else {
    // height is the largest dimension, maximize height to contain
    scale = innerHeight / naturalHeight
    // center along x axis
    x = -(naturalWidth * scale - innerWidth) / 2 / scale
  }
  img.style.setProperty('transform', `scale(${scale}) translate(${x}px, ${y}px)`)
}


let state
div.addEventListener('click', () => {
  if (!naturalHeight || !naturalWidth) {
    return
  }
  
  div.classList.toggle('full')  
  if (!state) {
    scaleToScreen()
  } else {
    scaleToBox()
  }

  state = !state
})
body {
  margin: 0;
}

.box {
  position: relative;
  height: 200px;
  width: 300px;
  transition: clip-path 1s;
  clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%);
}

.box.full {
  clip-path: polygon(0 0, 100vw 0, 100vw 100vh, 0 100vh);
}

img {
  position: absolute;
  top: 0;
  left: 0;
  transform-origin: 0 0;
  transition: transform 1s;
}

img.initial {
  height: 100%;
  width: 100%;
  object-fit: cover;
  transition-duration: 0s;
}
<div class='box'>
  <img class='initial' src="https://picsum.photos/1000/400"/>
</div>

You could improve on this demo by animating the transform of the image to work even if the parent div isn't exactly on the top left corner (I didn't want to obfuscate the working principle of this demo).

If I understood your request correctly, instead of object-fit you could try and center it with position:absolute; and transform: translate().

.container > img  {
   position: absolute;
   top: 50%;
   left: 50%;
   transform: translate(-50%, -50%); 
   width: auto;
   height: auto;
   max-width: 100%;
}

Here is the modified pen: https://codepen.io/gsarig/pen/KKzOrJW

Related