Как реализовать прокрутку при движении шарика по траектории

Только начала осваивать анимацию с JS.
Подскажите, как лучше будет реализовать данный полукруг, чтобы в дальнейшим подключить анимацию с возможностью скролла и выбора(клика) определённого текста

введите сюда описание изображения


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

Автор решения: focus.style

Уверен, что есть более практичные методы на JavaScript. Но можно попробовать набор современных инструментов CSS.

Я использовал offset-path, это путь, принимающий значения SVG-пути. Сгенерировать свой вектор SVG можно с помощью этого иснтрумента.

offset-rotate задает вращение элемента во время прохождения пути. Если не задать - элемент будет проворачиваться, находясь параллельно вектору.

offset-distance это собственно точка на дистанции нашего вектора, используется в анимации.

Надо понимать, что CSS Motion Path несколько сыроват. Но уже имеет право на рассмотрение.

В дальнейшем можно манипулировать CSS и анимацией из JS.

#red-circle {
  offset-path: path('M 200 50 C 50 150 50 350 200 450');
  offset-rotate: 0deg; 
  animation: move 3000ms infinite alternate ease-in-out;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: red; 
  position: absolute;
}

@keyframes move {
  0% {
    offset-distance: 0%;
    opacity: .7;
    transform: scale(0.7); 
  }  
  50% {
    opacity: 1;
    transform: scale(1); 
  }
  100% {
    offset-distance: 100%;
    opacity: .7;
    transform: scale(0.7); 
  }
}

#red-circle .red-title {
  position: absolute;
  right: calc(100% + 15px);
  font-size: 36px;
  line-height: 1;
}
<div id="red-circle">
  <div class="red-title">Text</div>
</div>

Обновлено

Так как наш вектор - по сути сектор окружности - можем вести себя с ним соответственно. В вопросе сказано, что Вы осваиваете JavaScript и подключать анимацию и скролл будете в дальнейшем. Не буду лишать Вас этого удовольствия. Просто покажу, как можно общаться с окружностью из JS.

let redCircle = document.querySelector("#red-circle"),
    r = 180, // размер окружности 360x360px в CSS
    left = '',
    tops = '',
    alpha = 2 * 3.1415; // не использовал Math.PI, нет необходимости в сверхточности
function drawPosition(t) {
  left = r + r * Math.cos(alpha * t); // первый r определяет центр окружности
  tops = r + r * Math.sin(alpha * t);
  redCircle.setAttribute("style", "left:" + left + "px; top: " + tops + "px; --t: " + t + ";");
  // CSS переменная --t добавлена только, чтобы удобно просмотреть в консоли динамику изменения переменной t
}
requestAnimationFrame(function step(time) {
  drawPosition(time / 10000);  // один оборот за 10 секунд
  requestAnimationFrame(step)
});
#red-parent {
  width: 360px;
  height: 360px;
  background: #eee;
  border-radius: 50%;
  position: relative;
  left: 50px;
  top: 50px;
  margin: 70px;
}

#red-circle {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: red;
  position: absolute;
  top: 0;
  left: 0;
  margin-top: -20px;
  margin-left: -20px;
}

#red-circle .red-title {
  position: absolute;
  right: calc(100% + 15px);
  font-size: 36px;
  line-height: 1;
}
<div id="red-parent">
  <div id="red-circle">
    <div class="red-title">Text</div>
  </div>
</div>

В данном примере я привязывался к текущему времени и обращался к окружности с помощью startTime и интервальной функции, которая раз в 1мс обновляла текущее время и передавала t в функцию drawPosition(). Но вместо этого можно передавать t вызывая drawPosition() любым другим способом, например, подвязав этот процесс к скроллу.

Логику работы с окружностью подсмотрел в данном видео и адаптировал к ответу.

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

Решение pure SVG

Решение без скроллинга, так как его нет в SVG, но во втором примере будет скроллинг с использованием JS

Смотрите комментарии в коде, какие строчки реализуют анимацииии движения и увеличения

n
<svg id="svg1" width="400" height="400" viewBox="10 0 400 400" xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMinYMin meet" style="border:1px solid grey">

    <!-- Траектория движения красного шарика -->
    <path id="track" d="m 401,28 c 0,0 -102,33 -103,156 -0.37,115 100,176 100.9,176.6" stroke="green" stroke-width="2" fill="none" id="theMotionPath"/>
   

    <!-- Красный круг, который будет перемещаться по траектории движения. -->
    <g id="dot">
    <circle cx="0" cy="0" r="10" fill="red" />
    <text fill="black" font-size="24" x="-60" y="7">Text</text>  
      <!-- анимация движения красного шарика -->
      <animateMotion
      begin="svg1.click"
      dur="5s"
      repeatCount="indefinite"
      fill="freeze"> 
         <mpath xlink:href="#track"></mpath>
      </animateMotion> 
   <!-- анимация увеличения красного шарика --> 
    <animateTransform  
          attributeName="transform" 
          type="scale" 
          values="1;2;1" 
          begin="svg1.click"
          dur="5s"
           repeatCount="indefinite"
          additive="sum"/>  
      </circle>
    </g>
</svg>

Решение SVG + JS scroll

<style>
.verylong {
  height: 2000px;
}

svg {
  position: fixed;
  width: 400px;
  height: 400px;
}
</style>

<svg width="400" height="400" viewBox="0 0 400 400" xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMinYMin meet" style="border:1px solid grey">

    <!-- Draw the outline of the motion path in grey, along with 2 small circles at key points -->
    <path d="m 401,28 c 0,0 -102,33 -103,156 -0.37,115 100,176 100.9,176.6" stroke="green" stroke-width="2" fill="none" id="theMotionPath"/>
   

    <!-- Красный круг, который будет перемещаться по траектории движения. -->
    <g id="dot">
    <circle cx="0" cy="0" r="10" fill="red" />
    <text fill="black" font-size="24" x="-60" y="7">Text</text>
    </g>
</svg>

<div class="verylong">
</div>
<script>
function positionTheDot() {

  // Какой процент вниз по странице 
  var scrollPercentage = (document.documentElement.scrollTop + document.body.scrollTop) / (document.documentElement.scrollHeight - document.documentElement.clientHeight);

  // Получить длину пути
  var path = document.getElementById("theMotionPath");
  var pathLen = path.getTotalLength();
  
  // Получить положение точки в <scrollPercentage> вдоль пути.
  var pt = path.getPointAtLength(scrollPercentage * pathLen);
  
  // Поместите красную точку в эту точку
  var dot = document.getElementById("dot");
  dot.setAttribute("transform", "translate("+ pt.x + "," + pt.y + ")");
  
};

// Обновить положение точки при получении события прокрутки.
window.addEventListener("scroll", positionTheDot);

// Установите начальную позицию точки.
positionTheDot();
</script>

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

const balls = document.querySelectorAll('.ball')
let ballHeight = balls[0].getBoundingClientRect().height
const balls_length = balls.length
const step = Math.PI*2 / balls_length
let counter = 0, current = 0, prev = 0, wait = false

arrange(counter)

addEventListener('mousewheel', wheel)
addEventListener('resize', () => {
  ballHeight = balls[0].getBoundingClientRect().height
  arrange(counter)
})

function arrange(counter) {
  for(let i = 0; i < balls_length; i++) {

    const angle = (i + counter) * step
    const angle_rotated = Math.PI + angle
    const cos = Math.cos(angle_rotated)
    const sin = Math.sin(angle_rotated)
    
    const ellipse_y_radius = innerHeight * 0.48
    const ellipse_x_radius = (innerWidth + innerHeight) / 2 * 0.2

    const ball_y = sin * ellipse_y_radius
    const ball_x = cos * ellipse_x_radius
    
    const ellipse_center_y = innerHeight * 0.5 - ballHeight / 2

    const ball_y_centered = ball_y + ellipse_center_y

    const scale = Math.min(1.4, Math.abs(cos) + 0.4)
    const opacity = Math.min(1, Math.abs(cos) + 0.05)

    balls[i].style.setProperty('--ty', ball_y_centered + 'px')
    balls[i].style.setProperty('--tx', ball_x + 'px')
    balls[i].style.setProperty('--sc', scale)
    balls[i].style.setProperty('--op', opacity)

    balls[i].innerText = i
  }
}


function wheel(e) {
  if(wait) return
  wait = true
  setTimeout(() => wait = false, 200)

  const direction = e.deltaY > 0.5 ? -1 : 1

  counter += direction
  counter %= balls_length

  prev = current
  current = 0

  if(counter > 0) current = balls_length - counter
  if(counter < 0) current = -1 * counter

  arrange(counter)

  balls[current].classList.add('active')
  balls[prev].classList.remove('active')
}
* {
  margin: 0;
  padding: 0;
}

body {
  height: 100vh;
  overflow: hidden;
  font-family: sans-serif;
}


.ball {
  position: absolute;
  top: 0;
  right: -5%;
  width: calc(10px + 3vw + 3vh);
  height: calc(10px + 3vw + 3vh);
  margin-left: calc(7px + 3vw);
  border-radius: 50%;
  background-color: black;

  transition: 0.6s;
  transform: translate(var(--tx), var(--ty)) scale(var(--sc));
  opacity: var(--op);
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
}

.active {
  background-color: lightblue;
}
<span class="ball active"></span>
<span class="ball"></span>
<span class="ball"></span>
<span class="ball"></span>
<span class="ball"></span>
<span class="ball"></span>
<span class="ball"></span>
<span class="ball"></span>
<span class="ball"></span>
<span class="ball"></span>
<span class="ball"></span>

→ Ссылка