Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Animate marquee on SVG curve

I have a text that is wrapping around an SVG circle which is scaling depending on window size – thanks to user enxaneta https://stackoverflow.com/a/56036245/10727821. I want to animate the text so that it would be revolving around the center like a marquee. For this, my code currently looks like this:

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>

This is working well, except the text gets "swallowed" at the end of the curve (see attached image). I'd like to have it make a full rotation without any interruption. I have tried changing the so variable to a negative value, but this ends up in the text being too far offset so it would only slowly creep onto the page. I was thinking to prepend a text fragment after a certain time, but this wouldn't take into account the startOffset movement and would probably not work…

Thankful for any hints, also those using JS libraries or plugins!

enter image description here

like image 453
bruno Avatar asked May 08 '19 13:05

bruno


People also ask

What is SVG animation and how to use it?

Scalable Vector Graphics (SVG) is an XML-based image similar to HTML that can help create animation elements. Several methods are used to animate SVG, such as Synchronized Multimedia Integration Language (SMIL), styling, and scripting. Designers usually depend on Adobe for animation, which can help with SVG animation too.

How to create CSS animated SVG using Haiku?

SVG Artista can help create CSS animated SVG using code that works on modern browsers. One can pick up the SVG graphic, play the toolbar button, copy the code, and get down to editing to get the proper animation to the file. Haiku’s Animator can help create intuitive and engaging animation for websites and apps.

What are scalable vector graphics (SVGS) in web design?

Scalable Vector Graphics (SVGs) help in solving a part of that problem, and do it very well. Although they have their limitations, SVGs can be very helpful for certain occasions and, if you have a good design team, you can also create a more visually stunning experience without putting undue burden the web browser or hampering the load times.

What are the two attributes used for dashes in SVG?

For our first technique, we are going to take advantage of two SVG attributes: stroke-dasharray and stroke-dashoffset. The stroke-dasharray attribute controls the pattern of dashes and gaps used to stroke the path.


1 Answers

The main idea is that the path has to coil twice. And when the startOffset is at 50% you make it 0. Also because the length of the path is changing when you resize the window you need to recalculate the font-size. I hope it helps.

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

The OP is commenting:

This snippet seems to be working fine for the use case, but when I try to apply it to another font family, the dimensions are off and the two loops start overlapping

One easy fix would be setting the attribute textLength equal to the length of the path divided by 2 (since the path is coiling twice - is twice as long as it should be). Also you need tu use lengthAdjust="spacingAndGlyphs" that is controlling how the text is stretched or compressed into that length.

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>

You may also need to add/remove some Coming Soon • if the text becomes too stretched / squished.

UPDATE 2

Apparently the last solution do not work on Firefox. This is another solution to this problem.

Initially I'm setting the font size much bigger than needed. Next I check if the text length is bigger than half path length, and if so I'm reducing the font size. I'm doing this in a 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>
like image 199
enxaneta Avatar answered Oct 20 '22 12:10

enxaneta