2
\$\begingroup\$

This function determines which images should be loaded based on which image is currently in the viewport (array index), what direction the user is scrolling, and if the image has yet to be loaded. It has a bias for what direction the user is scrolling, and proximity to the current image in viewport (delta). It attempts to match the two closest elements before and after.

The logic is mostly a set of ugly if statements, which I hope could be improved upon. Perhaps here is an opportunity for me to learn and implement a new design pattern or algorithm?

Basic example of matches.

Legend: _ = images, 0 = array index (image in viewport), X = matches (image to load)

0 X _ _ _ _ _
0 X X _ _ _ _
X 0 X _ _ _ _
X X 0 X X _ _
_ X X 0 X X _
_ _ X X 0 X X
_ _ _ X X 0 X
_ _ _ _ X X 0
_ _ _ _ _ X 0

Above, it will match the two closest elements (if available) before and after the index given the delta (distance) from the index is the same for the before and after elements.

If the user is scrolling, there is a bias towards the direction they are scrolling if the delta between the before and after elements is not equal. Eg. Using 30% bias, If scrolling right, and the closest right-side elements are 12 and 13 units away, and the closest left-side element is 10 units away we only load the 12 and 13 elements.

Y = Miss (closets element on respective before/after, but not matched due to scroll bias), --> = scroll direction

-->  _ _ _ _ _ _ X X 0 X X _ _ _ _ _ _ _
-->  _ _ _ _ Y Y _ _ _ 0 _ X X _ _ _ _ _
-->  _ _ _ _ Y Y _ _ _ _ 0 _ _ X X _ _ _
-->  _ _ _ _ Y Y _ _ _ _ 0 _ _ _ _ X X _
<--  _ _ _ _ X X _ _ _ 0 _ _ _ _ _ _ _ Y
<--  _ _ X X _ _ _ _ 0 _ _ _ _ _ _ _ _ Y
<--  X X _ _ _ _ _ _ 0 _ _ _ _ _ _ _ _ Y

Code:

queueClosestComponent(){
  //find our location in the array
  let lastIndex = this.components.indexOf(this.lastComponentInViewport);

  //creates an array of the closet items and their relative position to the last image in the viewport
  let mapped = this.components
    .map(function(item, index) {
      //map the closeness of each item
      return {
        delta: Math.abs(lastIndex - index),
        component: item,
        position: Math.sign(lastIndex - index) === 1 ? "previous" : "next"
      };
    }, this)
    .filter(function(item) {
      //remove items that are loaded or currently loading
      if (item.component.loadingState === null) {
        return true;
      }
    })
    .sortBy("delta");

  let closestNext = mapped.filterBy("position", "next").firstObject;
  let closestPrevious = mapped.filterBy("position", "previous").firstObject;

  //second closest
  let closest2ndNext = mapped.filterBy("position", "next").objectAt(1);
  let closest2ndPrevious = mapped
    .filterBy("position", "previous")
    .objectAt(1);

  //if there is no next or previous
  if (!closestNext && !closestPrevious) {
    return;
  }

  //if there is no previous
  if (closestNext && !closestPrevious) {
    //console.log("ADD: no previous: loading next");

    this.addToQueue("background", closestNext.component);
    if (closest2ndNext)
      this.addToQueue("background", closest2ndNext.component);

    return;
  } else if (!closestNext && closestPrevious) {
    //if there is no next
    //console.log("ADD: no next: loading previous");

    this.addToQueue("background", closestPrevious.component);
    if (closest2ndPrevious)
      this.addToQueue("background", closest2ndPrevious.component);

    return;
  }

  //if next and previous are the same delta, and within 3 items away
  if (
    closestNext.delta === closestPrevious.delta &&
    closestNext.delta <= 3
  ) {
    //console.log("ADD: same delta");
    this.addToQueue("background", closestNext.component);
    this.addToQueue("background", closestPrevious.component);

    return;
  }

  //when scrolling, give a preference to the item in the given direction, but regardless still try to load the closest
  let directionalFavour = 1.3; //30% favour to the given direction

  if (
    closestNext.delta / directionalFavour <= closestPrevious.delta &&
    this.direction === "next"
  ) {
    //console.log("ADD: right/down next");
    this.addToQueue("background", closestNext.component);
    if (closest2ndNext)
      this.addToQueue("background", closest2ndNext.component);

    return;
  } else if (
    closestNext.delta / directionalFavour >= closestPrevious.delta &&
    this.direction === "next"
  ) {
    //console.log("ADD: right/down previous");

    this.addToQueue("background", closestPrevious.component);
    return;
  }

  if (
    closestNext.delta >= closestPrevious.delta / directionalFavour &&
    this.direction === "previous"
  ) {
    this.addToQueue("background", closestPrevious.component);
    if (closest2ndPrevious)
      this.addToQueue("background", closest2ndPrevious.component);
    //console.log("ADD: left/up previous ");
    return;
  } else if (
    closestNext.delta <= closestPrevious.delta / directionalFavour &&
    this.direction === "previous"
  ) {
    //console.log("ADD: left/up next ");
    this.addToQueue("background", closestNext.component);
    return;
  }

}
\$\endgroup\$
2
\$\begingroup\$

There are some things we can improve even while keeping the same algorithm:

  • DRY repeated/inverted conditions
  • DRY addToQueue() calls
  • use a boolean isNext field instead of position string
  • avoid creating four intermediate arrays to find closest items by using find() instead of filter()
  • use arrow functions
  • reduce the number of return statements to just one
class Foo {
  queueClosestComponent() {
    const lastIndex = this.components.indexOf(this.lastComponentInViewport);

    const mapped = this.components
      .map((component, index) => ({
        delta: Math.abs(lastIndex - index),
        isNext: index >= lastIndex,
        component,
      }))
      .filter(item => item.component.loadingState === null)
      .sortBy('delta');

    const next = mapped.find(m => m.isNext);
    const prev = mapped.find(m => !m.isNext);
    if (!next && !prev) {
      return;
    }
    const secondNext = mapped.find(m => m !== next && m.isNext);
    const secondPrev = mapped.find(m => m !== prev && !m.isNext);

    const directionalFavour = 1.3;
    let toAdd = [];

    if (next && !prev) {
      toAdd = [next, secondNext];
    } else if (!next && prev) {
      toAdd = [prev, secondPrev];
    } else if (next.delta === prev.delta && next.delta <= 3) {
      toAdd = [next, prev];
    } else if (this.direction === 'next') {
      toAdd = next.delta / directionalFavour <= prev.delta
        ? [next, secondNext]
        : [prev];
    } else if (this.direction === 'previous') {
      toAdd = prev.delta / directionalFavour <= next.delta
        ? [prev, secondPrev]
        : [next];
    }

    toAdd.forEach(item =>
      item && this.addToQueue('background', item.component)
    );
  },
};
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.