Анимация элементов SVG при горизонтальной прокрутке

const slides = document.querySelectorAll('.rect');
slides.forEach((i) => {
  if (i) {
    const observer = new IntersectionObserver((entries) => {
      observerCallback(entries, observer, i)
    });
    observer.observe(i);
  }
})
const observerCallback = (entries, observer, header) => {
  entries.forEach((entry, i) => {
    if (entry.isIntersecting) {
      Array.from(document.querySelectorAll('.rect')).forEach((el) => el.classList.add('transparent'));
      entry.target.classList.remove('transparent');
    }
  });
};
.svg-wrapper {
  width: 100%;
  max-width: 300px;
  height: 150px;
  overflow-y: hidden;
  overflow-x: scroll;
}

.transparent {
  opacity: 0.3;
}
<div class="svg-wrapper">
  <svg width="1200" height="120" viewBox="0 0 1200 120" xmlns="http://www.w3.org/2000/svg">

  <rect x="10" y="10" width="100" height="100" fill="red" class="rect" />
  <rect x="300" y="20" width="80" height="100" fill="green" class="rect" />
  <rect x="500" y="50" width="100" height="20" fill="blue" class="rect" />
  <rect x="650" y="10" width="100" height="100" fill="magenta" class="rect" />
  <rect x="800" y="20" width="80" height="100" fill="yellow" class="rect" />
  <rect x="1000" y="50" width="100" height="20" fill="black" class="rect" />
</svg>
</div>

Есть svg с фиксированной шириной и хаотично накиданными внутри него элементами (в данном случае квадраты). Он находится внутри блока с шириной 100% (max-width:300px я поставил для наглядности примера).
Необходимо, чтобы квадратам, которые появляются в видимой части блока при прокрутке, присваивался определенный класс. А когда они исчезают - класс убирался. Как это сделать на Javascript? Спасибо!


Ответы (2 шт):

Автор решения: Leonid

Способ наверняка неэффективный, но как вариант прямолинейного решения:

const wrapper = document.querySelector('.svg-wrapper');
const rects = [...document.querySelectorAll('rect')];

wrapper.addEventListener('scroll', e => {
    let w = e.target.getBoundingClientRect().width;
    let x = e.target.scrollLeft;
    rects.forEach(rect => {
        let rectX = +rect.getAttribute('x');
        let rectW = +rect.getAttribute('width');
        if(rectX > x && rectX + rectW < x + w){
            rect.classList.add('transparent');
        } else {
            rect.classList.remove('transparent');
        }
    })
})
.svg-wrapper {
    width: 100%;
    max-width: 300px;
    height: 150px;
    overflow-y: hidden;
    overflow-x:scroll;
}

.transparent {
    opacity: 0.5;
}
<div class="svg-wrapper">
    <svg width="1200" height="120" viewBox="0 0 1200 120" xmlns="http://www.w3.org/2000/svg">
        <rect x="10" y="10" width="100" height="100" fill="red" />
        <rect x="300" y="20" width="80" height="100" fill="green" />
        <rect x="500" y="50" width="100" height="20" fill="blue" />
        <rect x="650" y="10" width="100" height="100" fill="magenta" />
        <rect x="800" y="20" width="80" height="100" fill="yellow" />
        <rect x="1000" y="50" width="100" height="20" fill="black" />
    </svg>
</div>

Если реальный размер SVG отличается, то можно ввести коэффициент для корректировки позиции и размеров отдельных элементов: svg.getBoundingClientRect().width/svg.width.animVal.value svg.getBoundingClientRect().width/svg.getAttribute('viewBox').split(' ')[2]

const wrapper = document.querySelector('.svg-wrapper');
const rects = [...document.querySelectorAll('rect')];
const svg = document.querySelector('svg');

wrapper.addEventListener('scroll', e => {
    let k = svg.getBoundingClientRect().width/svg.getAttribute('viewBox').split(' ')[2];
    let w = e.target.getBoundingClientRect().width;
    let x = e.target.scrollLeft;
    rects.forEach(rect => {
        let rectX = rect.getAttribute('x')*k;
        let rectW = rect.getAttribute('width')*k;
        if(rectX > x && rectX + rectW < x + w){
            rect.classList.add('transparent');
        } else {
            rect.classList.remove('transparent');
        }
    })
})
.svg-wrapper {
    width: 100%;
    max-width: 300px;
    height: 150px;
    overflow-y: hidden;
    overflow-x:scroll;
}

.transparent {
    opacity: 0.5;
}

svg {
    height: 200px;
    width: auto;
}
<div class="svg-wrapper">
    <svg width="1200" height="120" viewBox="0 0 1200 120" xmlns="http://www.w3.org/2000/svg">
        <rect x="10" y="10" width="100" height="100" fill="red" />
        <rect x="300" y="20" width="80" height="100" fill="green" />
        <rect x="500" y="50" width="100" height="20" fill="blue" />
        <rect x="650" y="10" width="100" height="100" fill="magenta" />
        <rect x="800" y="20" width="80" height="100" fill="yellow" />
        <rect x="1000" y="50" width="100" height="20" fill="black" />
    </svg>
</div>

Можно просто вычислять rect.getBoundingClientRect() на каждой итерации, получается еще проще код:

const wrapper = document.querySelector('.svg-wrapper');
const wRect = wrapper.getBoundingClientRect();
const rects = [...document.querySelectorAll('rect')];

wrapper.addEventListener('scroll', () => {
    rects.forEach(rect => {
        let {left,right} = rect.getBoundingClientRect();
        if(left > wRect.left && right < wRect.right){
            rect.classList.add('transparent');
        } else {
            rect.classList.remove('transparent');
        }
    })
})
.svg-wrapper {
    width: 100%;
    max-width: 300px;
    height: 150px;
    overflow-y: hidden;
    overflow-x:scroll;
    margin-left: 50px;
}

.transparent {
    opacity: 0.5;
}

svg {
    height: 200px;
    width: auto;
}
<div class="svg-wrapper">
    <svg width="1200" height="120" viewBox="0 0 1200 120" xmlns="http://www.w3.org/2000/svg">
        <rect x="10" y="10" width="100" height="100" fill="red" />
        <rect x="300" y="20" width="80" height="100" fill="green" />
        <rect x="500" y="50" width="100" height="20" fill="blue" />
        <rect x="650" y="10" width="100" height="100" fill="magenta" />
        <rect x="800" y="20" width="80" height="100" fill="yellow" />
        <rect x="1000" y="50" width="100" height="20" fill="black" />
    </svg>
</div>

→ Ссылка
Автор решения: yar85

Решение через IntersectionObserver - как оказалось, в данный момент он не работает с SVG в Chromium браузерах (есть баг)... посмотреть пример в действии можно, например, в Firefox:

const root = document.querySelector('.svg-wrapper'),
      slides = root.querySelectorAll('.rect'),
      observerCb = entries => entries.forEach(entry => {
        entry.target.classList.toggle('opaque', entry.isIntersecting);
        /* отладочный вывод */ debug.textContent += `${entry.target.getAttribute('fill')} теперь ${entry.isIntersecting ? '' : 'не '}отображается\n`; debug.scrollTop = debug.scrollHeight;
      }),
      obs = new IntersectionObserver(observerCb, { root });
slides.forEach(slide => obs.observe(slide));
html, body { margin: 0; padding: 0; height: 100%; }
body { display: flex; }

.svg-wrapper {
  width: 100%;
  max-width: 300px;
  height: 150px;
  overflow-y: hidden;
  overflow-x: scroll;
}

.svg-wrapper svg .rect { opacity: 0.3; }
.svg-wrapper svg .rect.opaque { opacity: 1; transition: opacity 1s ease; }


#debug { flex: 1 1; max-height: 100%; margin: 0 0 0 0.5rem; padding: 0.3rem; box-sizing: border-box; overflow-y: scroll; font-family: monospace; background: #eee; }
<div class="svg-wrapper">
  <svg width="1200" height="120" viewBox="0 0 1200 120" xmlns="http://www.w3.org/2000/svg">
    <rect x="10" y="10" width="100" height="100" fill="red" class="rect" />
    <rect x="300" y="20" width="80" height="100" fill="green" class="rect" />
    <rect x="500" y="50" width="100" height="20" fill="blue" class="rect" />
    <rect x="650" y="10" width="100" height="100" fill="magenta" class="rect" />
    <rect x="800" y="20" width="80" height="100" fill="yellow" class="rect" />
    <rect x="1000" y="50" width="100" height="20" fill="black" class="rect" />
  </svg>
</div>
<pre id="debug"></pre>

Суть бага Хромиум-браузеров заключается похоже в том, что структура отвечающая за intersectionRect объекта наблюдаемого SVG-элемента не заполняется должным образом, в результате чего свойство isIntersecting у таких entry-объектов всегда в значении false, и коллбэк соотв. не вызывается.

Призываю каждого заинтересованного в устранении бага поставить в трекере (по ссылке) звездочку - возможно, это привлечет внимание разработчиков к проблеме: сейчас ей назначен довольно низкий приоритет и категория "Feature" (это выглядит некорректным, учитывая то что проблема по факту является дефектом).

→ Ссылка