2

I have a 11500x11500 div that consists of 400 images, that obviously overflows the viewport.

I would like to pan around the whole div programmatically.

I want to generate an animation and by the time the animation is over, the whole of the div must have been panned across the viewport, top to bottom, left to right.

Right now, I am "splitting" my 11500x1500 div into tiles. The maximum width and height of each tile is the width and height of the viewport.

I store the coordinates of each tile and then I randomly choose one, pan it left-to-right and then move on to the next one.

I would like to know:

  1. whether my method is correct or whether I am missing something in my calculations/approach and it could be improved. Given the size, it is hard for me to tell whether I'm actually panning the whole of the div after all
  2. whether I can make the panning effect feel more "organic"/"natural". In order to be sure that the whole div is eventually panned, I pick each tile and pan it left-to-right, move on to the next one etc. This feels kind of rigid and too formalised. Is there a way to pan at let's say an angle or with a movement that is even more random and yet be sure that the whole div will eventually be panned ?

Thank in advance for any help.

This is the jsfiddle and this is the code (for the sake of the example/test every "image" is actually a div containing its index as text):

function forMs(time) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve()
    }, time)
  })
}

let container = document.getElementById('container')

let {
  width,
  height
} = container.getBoundingClientRect()

let minLeft = window.innerWidth - width
let minTop = window.innerHeight - height

let i = 0
while (i < 400) {
  // adding "image" to the container
  let image = document.createElement('div')

  // add some text to the "image" 
  // to know what we're looking at while panning
  image.innerHTML = ''
  let j = 0
  while (j < 100) {
    image.innerHTML += ` ${i + 1}`
    j++
  }

  container.appendChild(image)

  i++
}

let coords = []
let x = 0
while (x < width) {
  let y = 0
  while (y < height) {
    coords.push({
      x,
      y
    })
    y += window.innerHeight
  }
  x += window.innerWidth
}

async function pan() {
  if (!coords.length) {
    return;
  }

  let randomIdx = Math.floor(Math.random() * coords.length)
  let [randomCoord] = coords.splice(randomIdx, 1);

  console.log(coords.length)

  container.classList.add('fast')

  // update style in new thread so new transition-duration is applied
  await forMs(10)

  // move to new yet-unpanned area
  container.style.top = Math.max(-randomCoord.y, minTop) + 'px'
  container.style.left = Math.max(-randomCoord.x, minLeft) + 'px'

  // wait (approx.) for transition to end
  await forMs(2500)

  container.classList.remove('fast')

  // update style in new thread so new transition-duration is applied
  await forMs(10)

  //pan that area
  let newLeft = -(randomCoord.x + window.innerWidth)

  if (newLeft < minLeft) {
    newLeft = minLeft
  }

  container.style.left = newLeft + 'px'

  // wait (approx.) for transition to end
  await forMs(4500)

  // move on to next random area
  await pan()
}

pan()
html,
body {
  position: relative;
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  overflow: auto;
}

* {
  margin: 0;
  padding: 0;
}

#container {
  position: absolute;
  top: 0;
  left: 0;
  text-align: left;
  width: 11500px;
  height: 11500px;
  transition: all 4s ease-in-out;
  transition-property: top left;
  font-size: 0;
}

#container.fast {
  transition-duration: 2s;
}

#container div {
  display: inline-block;
  height: 575px;
  width: 575px;
  border: 1px solid black;
  box-sizing: border-box;
  font-size: 45px;
  overflow: hidden;
  word-break: break-all;
}
<div id="container"></div>
isherwood
  • 58,414
  • 16
  • 114
  • 157
Kawd
  • 4,122
  • 10
  • 37
  • 68

1 Answers1

1

I think following improvements can be made:

  • Hide overflow on html and body so user can not move scrollbar and disturb the flow.
  • Calculate minLeft and minTop every time to account for window resizing. You might need ResizeObserver to recalculate things.
  • Increase transition times to avoid Cybersickness. In worse case RNG will pick bottom right tile first so your container will move the longest in 2seconds! Maybe, you can zoom-out and move then zoom-in then perform pan. Or use any serpentine path which will make shorter jumps.

Performance improvements:

  • Use transform instead of top, left for animation.
  • Use will-change: transform;. will-change will let browser know what to optimize.
  • Use translate3D() instead of translate(). ref
  • Use requestAnimationFrame. Avoid setTimeout, setInterval.


This is an old but good article: https://www.paulirish.com/2012/why-moving-elements-with-translate-is-better-than-posabs-topleft/


Modified code to use transform:

function forMs(time) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve()
    }, time)
  })
}

let container = document.getElementById('container')
let stat = document.getElementById('stats');
let {
  width,
  height
} = container.getBoundingClientRect()

let minLeft = window.innerWidth - width
let minTop = window.innerHeight - height

let i = 0
while (i < 400) {
  // adding "image" to the container
  let image = document.createElement('div')

  // add some text to the "image" 
  // to know what we're looking at while panning
  image.innerHTML = ''
  let j = 0
  while (j < 100) {
    image.innerHTML += ` ${i + 1}`
    j++
  }

  container.appendChild(image)

  i++
}

let coords = []
let x = 0
while (x < width) {
  let y = 0
  while (y < height) {
    coords.push({
      x,
      y
    })
    y += window.innerHeight
  }
  x += window.innerWidth
}

let count = 0;
async function pan() {
  if (!coords.length) {
    stat.innerText = 'iteration: ' +
      (++count) + '\n tile# ' + randomIdx + ' done!!';
    stat.style.backgroundColor = 'red';
    return;
  }
  let minLeft = window.innerWidth - width
  let minTop = window.innerHeight - height


  let randomIdx = Math.floor(Math.random() * coords.length);
  randomIdx = 1; //remove after debugging
  let [randomCoord] = coords.splice(randomIdx, 1);

  stat.innerText = 'iteration: ' +
    (++count) + '\n tile# ' + randomIdx;
  console.log(coords.length + ' - ' + randomIdx)

  container.classList.add('fast')

  // update style in new thread so new transition-duration is applied
  await forMs(10)

  // move to new yet-unpanned area
  let yy = Math.max(-randomCoord.y, minTop);
  let xx = Math.max(-randomCoord.x, minLeft);
  move(xx, yy);

  // wait (approx.) for transition to end
  await forMs(2500)

  container.classList.remove('fast')

  // update style in new thread so new transition-duration is applied
  await forMs(10)

  //pan that area
  let newLeft = -(randomCoord.x + window.innerWidth)

  if (newLeft < minLeft) {
    newLeft = minLeft
  }
  xx = newLeft;

  //container.style.left = newLeft + 'px'
  move(xx, yy);

  // wait (approx.) for transition to end
  await forMs(4500)

  // move on to next random area
  await pan()
}

pan()

function move(xx, yy) {
  container.style.transform = "translate3D(" + xx + "px," + yy + "px,0px)";
}
html,
body {
  position: relative;
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  overflow: hidden;
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

#container {
  text-align: left;
  width: 11500px;
  height: 11500px;
  transition: all 4s ease-in-out;
  transition-property: transform;
  font-size: 0;
  will-change: transform;
}

#container.fast {
  transition-duration: 2s;
}

#container div {
  display: inline-block;
  height: 575px;
  width: 575px;
  border: 1px solid black;
  box-sizing: border-box;
  font-size: 45px;
  overflow: hidden;
  word-break: break-all;
}

#stats {
  border: 2px solid green;
  width: 100px;
  background-color: lightgreen;
  position: fixed;
  opacity: 1;
  top: 0;
  left: 0;
  z-index: 10;
}
<div id=stats>iteration: 1 tile# 11</div>
<div id="container"></div>


Note I haven't implemented everything in above snippet.

the Hutt
  • 16,980
  • 2
  • 14
  • 44
  • 1
    Thanks a lot for taking the time for such a thorough answer. The transform suggestion especially is really helpful as changing the top/left is indeed quite bumpy. I will update and get back to you. Thanks! – Kawd Dec 17 '21 at 17:58
  • Unfortunately, the trade-off with using CSS transform is that a lots of the images are not rendered while the transition is taking place. And they are only rendered at the end of the transition. Whereas with my original implementation, the transition is bumpier but all images are constantly rendered. – Kawd Dec 19 '21 at 14:03
  • 1
    Things are moving too fast. :( There are advanced techniques to achieve [higher fps](https://medium.com/outsystems-experts/how-to-achieve-60-fps-animations-with-css3-db7b98610108). You need to try with actual images instead of only text, in the end that will matter. If you load those many images you need to check memory requirements too. For loading times you may need to use single sprite. – the Hutt Dec 19 '21 at 14:23