Анимация элементов 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 шт):
Способ наверняка неэффективный, но как вариант прямолинейного решения:
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>
Решение через 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" (это выглядит некорректным, учитывая то что проблема по факту является дефектом).