Анимировать выделение шрифта на кривой SVG

186
10 июня 2022, 09:30

У меня есть текст, который движется по кругу SVG, и который масштабируется в зависимости от размера окна - Я хочу анимировать текст так, чтобы он вращался вокруг центра, как шатер. Для этого мой код в настоящее время выглядит так:

function Init(){
        let wrap = document.getElementById('wrap');
        let thePath = document.getElementById('thePath');
        let ellipse = document.getElementById('ellipse');
        let w = wrap.clientWidth;
        let h = wrap.clientHeight;
        ellipse.setAttributeNS(null,"viewBox",`0 0 ${w}  ${h}`);
        let d = `M${w/10},${h/2}A${4*w/10},${4*h/10} 0 0 0 ${9*w/10} ${5*h/10} A${4*w/10},${4*h/10} 0 0 0 ${w/10} ${5*h/10}`
    thePath.setAttributeNS(null,"d", d)
    }
    window.setTimeout(function() {
      Init();
      window.addEventListener('resize', Init, false);
    }, 15);
    let so = 0
    function Marquee(){
        let tp = document.getElementById('tp');
          requestAnimationFrame(Marquee)
          tp.setAttributeNS(null,"startOffset",so+"%");
          if(so >= 50){so = 0;}
          so+= .05
        }
    Marquee()
<div id="wrap">
    <svg id="ellipse" version="1.1" viewBox="0 0 1000 1000" preserveAspectRatio="none">
    <path id="thePath" fill="transparent" d="M100,500A400,400 0 0 0 900 500 A400,400 0 0 0 100 500"  />

       <text stroke="black" font-size="20">
          <textPath xlink:href="#thePath" dy="5" id="tp">
                Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon •
                Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon •
                Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon •
          </textPath>
        </text>
    </svg>
</div>

Это работает хорошо, за исключением того, что текст «проглатывается» в конце кривой (см. Прикрепленное изображение). Я бы хотел, чтобы он совершал полный оборот без каких-либо перерывов. Я попытался изменить переменную so на отрицательное значение, но это привело к тому, что текст оказался слишком далеким, так что он медленно пополз на страницу. Я думал добавить фрагмент текста через определенное время, но это не учитывает движение startOffset и, вероятно, не сработает

Спасибо за любые подсказки, в том числе и те, которые используют библиотеки JS или плагины!

Свободный перевод вопроса Animate marquee on SVG curve от участника @bruno.

Answer 1

Основная идея в том, что путь должен закручиваться дважды. И когда startOffset равен 50%, вы делаете его равным нулю. Также, поскольку длина пути изменяется при изменении размера окна, вам необходимо пересчитать размер шрифта. Я надеюсь, что это поможет.

function Init() {
  let w = wrap.clientWidth;
  let h = wrap.clientHeight;
  ellipse.setAttributeNS(null, "viewBox", `0 0 ${w}  ${h}`);
  let d = `M${w / 10},${h / 2}A${4 * w / 10},${4 * h / 10} 0 0 0 ${9 *
    w /
    10} ${5 * h / 10} A${4 * w / 10},${4 * h / 10} 0 0 0 ${w / 10} ${5 *
    h /
    10} A${4 * w / 10},${4 * h / 10} 0 0 0 ${9 * w / 10} ${5 * h / 10} A${4 *
    w /
    10},${4 * h / 10} 0 0 0 ${w / 10} ${5 * h / 10}`;
  thePath.setAttributeNS(null, "d", d);
  let paths_length = thePath.getTotalLength();
  tp.style.fontSize = paths_length / 205;
}
window.setTimeout(function() {
  Init();
  window.addEventListener("resize", Init, false);
}, 15);
let so = 0;
function Marquee() {
  requestAnimationFrame(Marquee);
  tp.setAttributeNS(null, "startOffset", so + "%");
  if (so >= 50) {
    so = 0;
  }
  so += 0.05;
}
Marquee();
#wrap{width:100vw; height:100vh}
  svg {
    background:#eee;
  }
<div id="wrap">
<svg id="ellipse" version="1.1" viewBox="0 0 1000 1000">  
<path id="thePath" fill="gold" d="M100,500A400,400 0 0 0 900 500 A400,400 0 0 0 100 500 A400,400 0 0 0 900 500 A400,400 0 0 0 100 500"  />
  
  
   <text stroke="#000000" >
      <textPath xlink:href="#thePath" dy="5" id="tp">
            Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon •
      </textPath>
    </text>
</svg>
</div>

UPDATE

ТС комментирует:

Этот сниппет, похоже, работает нормально для данного варианта использования, но когда я пытаюсь применить его к другому семейству шрифтов, размеры отключены, и два цикла начинают перекрываться.

Одно простое исправление - установить атрибут textLength равным длине пути, деленному на 2 (поскольку путь наматывается дважды - это в два раза длиннее, чем должно быть). Также вам нужно использовать
lengthAdjust = "spacingAndGlyphs", который контролирует, как текст растягивается или сжимается до этой длины.

function Init() {
  let w = wrap.clientWidth;
  let h = wrap.clientHeight;
  ellipse.setAttributeNS(null, "viewBox", `0 0 ${w}  ${h}`);
  let d = `M${w / 10},${h / 2}A${4 * w / 10},${4 * h / 10} 0 0 0 ${9 *
    w /
    10} ${5 * h / 10} A${4 * w / 10},${4 * h / 10} 0 0 0 ${w / 10} ${5 *
    h /
    10} A${4 * w / 10},${4 * h / 10} 0 0 0 ${9 * w / 10} ${5 * h / 10} A${4 *
    w /
    10},${4 * h / 10} 0 0 0 ${w / 10} ${5 * h / 10}`;
  thePath.setAttributeNS(null, "d", d);
  let path_length =  thePath.getTotalLength();
  
  //////////////////////////////////////////////////
  tp.setAttributeNS(null,"textLength",path_length/2)
  //////////////////////////////////////////////////
  
  tp.style.fontSize = path_length / 200;
}
window.setTimeout(function() {
  Init();
  window.addEventListener("resize", Init, false);
}, 15);
let so = 0;
function Marquee() {
  requestAnimationFrame(Marquee);
  tp.setAttributeNS(null, "startOffset", so + "%");
  if (so >= 50) {
    so = 0;
  }
  so += 0.05;
}
Marquee();
#wrap{width:100vw; height:100vh}
  svg {
    background:#eee;
    font-family:consolas;
  }
<div id="wrap">
<svg id="ellipse" version="1.1" viewBox="0 0 1000 1000">  
<path id="thePath" fill="gold" d="M100,500A400,400 0 0 0 900 500 A400,400 0 0 0 100 500 A400,400 0 0 0 900 500 A400,400 0 0 0 100 500"  />
  
  
   <text stroke="#000000" >
      <textPath xlink:href="#thePath" id="tp"  lengthAdjust="spacingAndGlyphs">Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • </textPath>
    </text>
</svg>
</div>

Вам также может потребоваться добавить / удалить некоторые Coming Soon •, если текст становится слишком растянутым / сжатым.

UPDATE 2

Видимо последнее решение выше не работает в Firefox. Вот еще одно решение данной проблемы.

Изначально я устанавливаю размер шрифта намного больше, чем нужно. Затем я проверяю, больше ли длина текста, чем половина длины пути, и если да, я уменьшаю размер шрифта. Я делаю это в цикле while loop:

function Init() {
  let w = wrap.clientWidth;
  let h = wrap.clientHeight;
  ellipse.setAttributeNS(null, "viewBox", `0 0 ${w}  ${h}`);
  let d = `M${w / 10},${h / 2}A${4 * w / 10},${4 * h / 10} 0 0 0 ${9 *
    w /
    10} ${5 * h / 10} A${4 * w / 10},${4 * h / 10} 0 0 0 ${w / 10} ${5 *
    h /
    10} A${4 * w / 10},${4 * h / 10} 0 0 0 ${9 * w / 10} ${5 * h / 10} A${4 *
    w /
    10},${4 * h / 10} 0 0 0 ${w / 10} ${5 * h / 10}`;
  thePath.setAttributeNS(null, "d", d);
  let path_length =  thePath.getTotalLength();
  
  
  //begin at a bigger size than needed
  let font_size = 100;
  ellipse.style.fontSize = font_size+"px"; 
  
  // while the text length is bigger than half path length 
  while(tp.getComputedTextLength() > path_length / 2 ){
    //reduce the font size
    font_size -=.25;
    //reset the font size 
    ellipse.style.fontSize = font_size+"px";
  }
}

window.setTimeout(function() {
  Init();
  window.addEventListener("resize", Init, false);
}, 15);
let so = 0;
function Marquee() {
  requestAnimationFrame(Marquee);
  tp.setAttributeNS(null, "startOffset", so + "%");
  if (so >= 50) {
    so = 0;
  }
  so += 0.02;
}
Marquee();
html, body {
    margin: 0;
    height: 100%;
    width: 100%;
}
body {
    font-family: "Arimo", sans-serif;
}
#wrap{
    width:100%;
    height:100%;
    position: fixed;
    top: 0;
    left: 0;  
}
text {
    text-transform: uppercase;
    font-weight: lighter;
}
<div id="wrap">
    <svg id="ellipse" version="1.1" viewBox="0 0 1000 1000">
    <path id="thePath" fill="transparent" d="M100,500A400,400 0 0 0 900 500 A400,400 0 0 0 100 500"  />

       <text stroke="black">
          <textPath xlink:href="#thePath" dy="5" id="tp" lengthAdjust="spacingAndGlyphs">Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon •</textPath>
        </text>
    </svg>
</div>

Свободный перевод ответа от участника @enxaneta.

READ ALSO
Увеличение переменной с input кнопкой - кастомный вариант?

Увеличение переменной с input кнопкой - кастомный вариант?

Как реализовать нативным JavaScript, чтобы я брал переменную с input и ещё увеличивал по клику на единицу? Обычный input type="number" не подходит, потому...

185
Webpack. Module parse failed: Unexpected token (1:0)

Webpack. Module parse failed: Unexpected token (1:0)

Попытка сборки проекта на вебпаке 54

181
Регистрация пользователя в приложении, использование нескольких аккаунтов

Регистрация пользователя в приложении, использование нескольких аккаунтов

В реализации приложения появилась трудностьМне нужно сделать систему регистрации, но смысла уходить в бек и курить spring и делать api не нужно

310
Количество пробелов Java conventions

Количество пробелов Java conventions

Мне не понятен следующий пункт Java conventions: "Four spaces should be used as the unit of indentationThe exact construction of the indentation (spaces vs

180