0

In answering another question on StackOverflow, I posted what was supposed to be a simple demo showing how to achieve movement of a child <div> relative to the movement of the cursor within its parent <div>, by the manipulation of element.scrollTop and element.scrollLeft.

My demo basically works as expected, demonstrating the basics of gathering the cursor coordinates on mousemove via event.pageX and event.pageY, doing a little math to calculate the ratio by which the larger .inner child should be moved, and applying the scroll*.

However, I noticed that when moving my cursor near to the bottom and/or right of the .outer parent, the .inner child would be scrolled to its maximum ahead of my cursor reaching the edge(s).

What have I done to find a solution?
Realising that event.pageX would always be at least 1 and never more than 1 less than the width and that event.pageY would never be more than 1 less of the parent <div>'s height, I added a rudimentary function to expand the coordinate range from 0 to the full width and/or height.
Although the function does its job, it didn't cure the premature maximum scroll*.

EDIT: I had not tested this code outside SO snippets which appear to present the HTML differently depending on if it's being edited, viewed normally or expanded; Sometimes the function is required, and sometimes it's presence results in negative values.

scrollTop doesn't respond to negative values; instead, it sets itself back to 0.

If set to a value greater than the maximum available for the element, scrollTop settles itself to the maximum value.

The same is true for scrollLeft.
So this inconsistency isn't relevant; the problem was evident before I added the function.

What else?
I have repeatedly checked the math and, e.g.

  • Say we have a parent <div> measuring 100px by 100px
  • And a child<div> measuring 300px by 300px ( 3 times the parent on both axes)
  • If the cursor is at coord. { x: 90, y: 90 }
  • The scrollTop should be set to 90 * 3 = 270
  • And scrollLeft should be set to 90 * 3 = 270
  • So the child <div>'s bottom or right edges should not be aligned with those of the parent.

With that in mind, as I say, I have checked and checked again, and the math should work, but the result is unexpected.

Here's the code, with some extra bits outputting some of the the numbers in innerHTML (the console gets in the way a bit otherwise), and my question will continue under it. The extra <output> UI doesn't affect the result.

const divs = document.querySelectorAll( "div" ),
      outer_div = divs[ 0 ],
      outer_div_styles = window.getComputedStyle( outer_div ),
      inner_div_styles = window.getComputedStyle( divs[ 1 ] ),
      outer_div_width = parseInt( outer_div_styles.width ),
      outer_div_height = parseInt( outer_div_styles.height ),
      dimention_ratio = {
        x: parseInt( inner_div_styles.width ) / outer_div_width,
        y: parseInt( inner_div_styles.height ) / outer_div_height
      },
      half_odw = outer_div_width / 2,
      half_odh = outer_div_height / 2,
      expandCoords = function( e ) { // sometimes useful, never harmful
        var X = e.pageX,
            Y = e.pageY;
        if ( X < half_odw ) {
          X -= 1;
        } else if ( X > half_odw ) {
          X += 1;
        }
        if ( Y < half_odh ) {
          Y -= 1;
        } else if ( Y > half_odh ) {
          Y += 1;
        }
        return { x: X, y: Y };
      },
      // for demo convenience
      output = document.querySelector( "output" );
outer_div.addEventListener( "mousemove", function( evt ) {
  evt = expandCoords( evt );
  // for demo convenience
  output.innerHTML = "Coords: x:" + evt.x + ", y:" + evt.y + "<br>" +
                     "scrollLeft = " + ( evt.x * dimention_ratio.x ) + "<br>" +
                     "scrollTop = " + ( evt.y * dimention_ratio.y );
  outer_div.scrollLeft = evt.x * dimention_ratio.x;
  outer_div.scrollTop = evt.y * dimention_ratio.y;
}, false );
body {
  overflow: hidden;
  margin: 0;
}
.outer {
  width: 100vw;
  height: 100vh;
  overflow: hidden;
}
.inner {
  width: 1000vw; /* 10 times the width of its parent */
  height: 500vh; /* 5 times the height of its parent */
  box-shadow: inset 0 0 20px 20px green; /* no border edge highlight */
  background: radial-gradient( white, black );
}

/* for demo convenience */
output {
  position: absolute;
  top: 10vh;
  left: 10vw;
  font-family: Consolas, monospace;
  background: white;
  padding: .2em .4em .3em;
  cursor: default;
}
<div class="outer"><div class="inner"></div><output></output></div>

As you should see, the right and bottom edge(s) of the child <div> come into view long before the cursor reaches the right and/or bottom edge(s) of the parent.

Why is this, and how can it be fixed in a dynamic application?

By "dynamic application" I mean without hard coding the solution on a case by case basis.

NOTE: Although (I know) this code can be optimized in many ways, it is purely for demonstration and thus, optimisations that do not affect a fix for the specific problem are not of interest.

Community
  • 1
  • 1
Fred Gandt
  • 4,217
  • 2
  • 33
  • 41

1 Answers1

0

I figured it out

scrollTop and scrollLeft are the measure of how many pixels are scrolled on their respective axes.
This equates to how much is scrolled out of view, so there is always an amount remaining that cannot be scrolled.
This amount is equal to the respective measure of the parent.

  • If the child <div> is twice the size of the parent, then when scrolled to its maximum on either axes, only half will be scrolled.
  • If the child is three times the size, at max scroll, two thirds will be scrolled.
  • 10 times the size: 9/10ths scrolled etc.

In this particular application, the cursor coordinates should be multiplied by the dimension ratios calculated as:

( width of the child minus the width of the parent ) divided by the width of the parent
and
( height of the child minus the height of the parent ) divided by the height of the parent

So the corrected code, handling arbitrary ratios:

const divs = document.querySelectorAll( "div" ),
      outer_div = divs[ 0 ],
      outer_div_styles = window.getComputedStyle( outer_div ),
      inner_div_styles = window.getComputedStyle( divs[ 1 ] ),
      outer_div_width = parseInt( outer_div_styles.width ),
      outer_div_height = parseInt( outer_div_styles.height ),
      dimention_ratio = {
        x: ( parseInt( inner_div_styles.width ) - outer_div_width ) / outer_div_width, // fixed
        y: ( parseInt( inner_div_styles.height ) - outer_div_height ) / outer_div_height // fixed
      },
      half_odw = outer_div_width / 2,
      half_odh = outer_div_height / 2,
      expandCoords = function( e ) {
        var X = e.pageX,
            Y = e.pageY;
        if ( X < half_odw ) {
          X -= 1;
        } else if ( X > half_odw ) {
          X += 1;
        }
        if ( Y < half_odh ) {
          Y -= 1;
        } else if ( Y > half_odh ) {
          Y += 1;
        }
        return { x: X, y: Y };
      };
outer_div.addEventListener( "mousemove", function( evt ) {
  evt = expandCoords( evt );
  outer_div.scrollLeft = evt.x * dimention_ratio.x;
  outer_div.scrollTop = evt.y * dimention_ratio.y;
}, false );
body {
  overflow: hidden;
  margin: 0;
}
.outer {
  width: 100vw;
  height: 100vh;
  overflow: hidden;
}
.inner {
  width: 1234vw; /* 12.34 times the width of its parent */
  height: 567vh; /* 5.67 times the height of its parent */
  box-shadow: inset 0 0 20px 20px green; /* no border edge highlight */
  background: radial-gradient( white, black );
}
<div class="outer"><div class="inner"></div></div>
Fred Gandt
  • 4,217
  • 2
  • 33
  • 41