Calculating exact animation delay

328 views Asked by At

I am trying to animate an svg element as following

a. I want to increase the width of the rect and control the rate of animation speed with a cubic-beziervalue, in this case it is cubic-bezier(0, 0, 0.58, 1).

b. There are also 3 lines which I also want to rotate, each of them exactly when the width of green rect reaches them at it's animation cycle and not before that.

problem

What I tried so far

I came across this post and my understanding was that in the bezier curve graph, for a given output ratio (progression of animation -plotted on y axis), the time ratio (time ratio plotted on x axis) can be calculated, by solving the following equation.

cubic-bez

Since, I can already calculate, at what phase of the animation the green rect is going to come across those lines {50/125, 80/125, 125/125}, they become the Y value and I can use them to solve for t in y(t) and pass on that t value to solve x by passing on t value in x(t), which becomes my time ratio and I can use that to calculate the delay of individual lines.

S1

This is what the code looks like, which unfortunately does not do what I wanted.

I am not sure whether the math has gone wrong here or something else needs to be done. I can feel that I am close, but not sure what needs to be done here for the exact output.

const svg = document.querySelector("svg");

//max px to increase the width animation
var mxWidth = 125;

//animation duration of the green rect
var dur = 2000;

//set style of the green rect
var x = document.querySelector("[class=hitter");
x.style.setProperty('--v2', `${(mxWidth/svg.viewBox.baseVal.width)*100}%`);
x.style.setProperty('--dur', `${dur}ms`)


//what are the x corodinates of the lines
var lnX = [50, 80, 125];

//at what output ratio (width increase) the green rect is likely to 
//come across the lines
var progPct = lnX.map(x => x / mxWidth);

//what is the desired cubic bez
var cubicBezCurvVal = "0, 0, 0.58, 1"
    //split bezier curve value
var cleanVal = cubicBezCurvVal.split(',');
//clean space with map -retunrns new array with the function, original array unchnaged
var cleanVal = cleanVal.map((x) => parseFloat(x.replace(/ /g, '')));

//p0
const p0 = { x: 0, y: 0 };
//p3
const p3 = { x: 1, y: 1 };
//p1
const p1 = { x: cleanVal[0], y: cleanVal[1] };
//p2
const p2 = { x: cleanVal[2], y: cleanVal[3] };

const x0 = p0.x;
const y0 = p0.y;

const x1 = p1.x;
const y1 = p1.y;

const x2 = p2.x;
const y2 = p2.y;

const x3 = p3.x;
const y3 = p3.y;

const coefficient_a_y = y3 - 3 * y2 + 3 * y1 - y0;
const coefficient_b_y = 3 * (y2 - 2 * y1 + y0);
const coefficient_c_y = 3 * (y1 - y0);
const coefficient_d_y = y0 - y1;

function getY(prgRatio) {

    const outputRatio = prgRatio /*y value of cubic bezier curve */

    //get all the t values of y of the cubic equation
    var solution_t_y = [
        { sol: 1, cubicBezierProgressRatio_y: outputRatio - coefficient_d_y },
        { sol: 2, cubicBezierProgressRatio_y: (-1 * coefficient_b_y + Math.sqrt(Math.pow(coefficient_b_y, 2) - (4 * coefficient_a_y * coefficient_c_y))) / (2 * coefficient_a_y) },
        { sol: 3, cubicBezierProgressRatio_y: (-1 * coefficient_b_y - Math.sqrt(Math.pow(coefficient_b_y, 2) - (4 * coefficient_a_y * coefficient_c_y))) / (2 * coefficient_a_y) }
    ]

    var solTY = solution_t_y.filter(x => x.cubicBezierProgressRatio_y > 0 && x.cubicBezierProgressRatio_y < 1);

    function getX(t) {
        return Math.pow(1 - t, 3) * x0 +
            3 * Math.pow(1 - t, 2) * t * x1 +
            3 * (1 - t) * Math.pow(t, 2) * x2 +
            Math.pow(t, 3) * x3;
    }
    solTY.forEach(
        (a, i) => {
            a.cubicBezierTimeRatio_x = getX(a.cubicBezierProgressRatio_y)
        }
    )
    return solTY;
}

var timeRatio = [];

progPct.forEach(
    (a) => timeRatio.push(getY(a))
)

//get all the receivers
var impacter = document.querySelectorAll("[class^='axisLines']");


timeRatio.forEach(
    (a, i) => {
        (i == 2) ? impacter[i].style.setProperty('--del', `${dur}ms`): impacter[i].style.setProperty('--del', `${(a[0].cubicBezierTimeRatio_x)*dur}ms`)
    }
)
.hitter {
    visibility: hidden;
    /*hide default*/
    animation-name: moveWidth;
    animation-delay: 0s;
    animation-duration: var(--dur);
    animation-iteration-count: 1;
    animation-timing-function: cubic-bezier(0, 0, 0.58, 1);
    animation-direction: normal;
    animation-fill-mode: forwards;
}

[class^="axisLine"] {
    transform-box: fill-box;
    transform-origin: bottom;
    stroke-width: 1;
    color: green;
    animation: rotate 1s cubic-bezier(0, 0, 0.58, 1) var(--del) 1 alternate forwards;
    /*rotate 1s cubic-bezier(0, 0, 0.58, 1) var(--del) 1 alternate forwards;*/
}

@keyframes moveWidth {
    0% {
        visibility: visible;
        /*show again*/
        width: 0%;
    }
    100% {
        visibility: visible;
        /*Edit-2 maintains visibility after animation overs*/
        width: var(--v2);
    }
}

@keyframes rotate {
    0% {
        transform: rotate(0deg);
    }
    50% {
        transform: rotate(7.5deg);
    }
}
<!DOCTYPE html>
<html>


<body>
    <link rel="stylesheet" href="style.css">

    </link>
    <svg class="layer1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 200">
        
        <rect class="bg" id="bg" width="200" height="100" fill="#e6e6e6"></rect>
        <rect class="hitter" id="hitter" x="0" y="40" width="0" height="5" fill="#92D050" stroke="#92D050"
            stroke-width="0.1" ></rect>
        <line class="axisLines0" id="axisLines0" x1="50" x2="50" y1="0" y2="100" stroke="#ED7D31" stroke-width=".5"
            ></line>
        <line class="axisLines1" id="axisLines1" x1="80" x2="80" y1="0" y2="100" stroke="#548235" stroke-width=".5"
            ></line>
        <line class="axisLines2" id="axisLines2" x1="125" x2="125" y1="0" y2="100" stroke="#00B0F0" stroke-width=".5">
        </line>

        
    </svg>
    <script src="index3.js"></script>
</body>


</html>

1

There are 1 answers

1
Alexandr_TT On

When solving such problems, when several animations are interconnected, it is better to use SMIL SVG (in my opinion).
Since smil has a powerful tool that allows you to flexibly set the conditions for sequential start, pause, stop animations. If there is a need to further change the logic of the work of many animations, then it is enough to change the logical chains in the begin attribute.
begin="dash1.end+1s"

The example below runs multiple animations one after the other, but we don't need to count the timings and delays for each animation.

Animation starts after clicking on the canvas

#tube {
stroke:red;
}
.axisLine {
stroke:green;
}
<svg id="svg1" xmlns="http://www.w3.org/2000/svg"  xmlns:xlink="http://www.w3.org/1999/xlink"
          viewBox="0 0 400 200" >  
         

<rect x="20" y="50" width="200" height="50"  fill="none" stroke="black" />
 <path id="tube" stroke-dasharray="0,200" d="M20,75 220,75" >
   <animate id="dash1" attributeName="stroke-dasharray" begin="svg1.click" dur="1s" values="0 200;50 150" fill="freeze" restart="never" /> 
   <animate id="dash2"  attributeName="stroke-dasharray" begin="dash1.end+1s" dur="1s" values="50 150;100 100" fill="freeze" /> 
    <animate id="dash3"  attributeName="stroke-dasharray" begin="dash2.end+1s" dur="1s" values="100 100;150 50" fill="freeze" />
      <animate id="dash4"  attributeName="stroke-dasharray" begin="dash3.end+1s" dur="1s" values="150 50;200 0" fill="freeze" />
 </path> 
               <!-- vertical green lines -->
 <path id="line1" class="axisLine"  d="M70,50 70,100"/>    
  <path id="line2" class="axisLine"  d="M120,50 120,100"/>  
    <path id="line3" class="axisLine" d="M170,50 170,100"  />
     <path id="line4" class="axisLine" d="M220,50 220,100"  />
</svg>

The example below runs multiple animations one after the other, but we don't need to count the timings and delays for each animation:

  • The first animation of the growth of the horizontal line - id="dash1" starts after clicking on the canvas begin="svg1.click"
  • The second green vertical line rotation animation starts after the end of the first animation begin="dash1.end"
  • The third animation in order - the horizontal growth of the line will begin after the end of the rotation of the green line

#tube {
stroke:brown;
}
.axisLine {
stroke:green;
}
<svg id="svg1" xmlns="http://www.w3.org/2000/svg"  xmlns:xlink="http://www.w3.org/1999/xlink"
          viewBox="0 0 400 200" >  
         

<rect x="20" y="50" width="200" height="50"  fill="none" stroke="black" />
 <path id="tube" stroke-dasharray="0,200" d="M20,75 220,75" >
   <animate id="dash1" attributeName="stroke-dasharray" begin="svg1.click" dur="1s" values="0 200;50 150" fill="freeze" restart="never" /> 
   <animate id="dash2"  attributeName="stroke-dasharray" begin="dash1.end+1s" dur="1s" values="50 150;100 100" fill="freeze" /> 
    <animate id="dash3"  attributeName="stroke-dasharray" begin="dash2.end+1s" dur="1s" values="100 100;150 50" fill="freeze" />
      <animate id="dash4"  attributeName="stroke-dasharray" begin="dash3.end+1s" dur="1s" values="150 50;200 0" fill="freeze" />
 </path> 
               <!-- Vertical line animation-->
 <path id="line1" class="axisLine"  d="M70,50 70,100">
      <animateTransform id="aT1"
         attributeName="transform"
         type="rotate"
         begin="dash1.end" dur="1s"
         values="
          0 70 100;
          14 70 100;
       0 70 100"
  />          
 </path> 
  <path id="line2" class="axisLine"  d="M120,50 120,100"/>  
    <path id="line3" class="axisLine" d="M170,50 170,100"  />
     <path id="line4" class="axisLine" d="M220,50 220,100"  />
</svg>     

Below is the complete code:

#tube {

stroke:brown;
}
.axisLine {
stroke:green;
}
<svg id="svg1" xmlns="http://www.w3.org/2000/svg"  xmlns:xlink="http://www.w3.org/1999/xlink"
          viewBox="0 0 400 200" >  
         

<rect x="20" y="50" width="200" height="50"  fill="none" stroke="black" />
 <path id="tube" stroke-dasharray="0,200" d="M20,75 220,75" >
           <!-- Line Growth Animation   -->
  <animate id="dash1" attributeName="stroke-dasharray" begin="svg1.click" dur="1s" values="0 200;50 150" fill="freeze" restart="never" /> 
   <animate id="dash2"  attributeName="stroke-dasharray" begin="aT1.end+0.2s" dur="1s" values="50 150;100 100" fill="freeze" /> 
    <animate id="dash3"  attributeName="stroke-dasharray" begin="aT2.end+0.2s" dur="1s" values="100 100;150 50" fill="freeze" />
      <animate id="dash4"  attributeName="stroke-dasharray" begin="aT3.end+0.2s" dur="1s" values="150 50;200 0" fill="freeze" />
 </path> 
               <!-- Animation of rotation of vertical lines -->
 <path id="line1" class="axisLine"  d="M70,50 70,100">
      <animateTransform id="aT1"
         attributeName="transform"
         type="rotate"
         begin="dash1.end" dur="1s"
         values="
         0 70 100;
         14 70 100;
         0 70 100"
      />          
 </path> 
   <path id="line2" class="axisLine"  d="M120,50 120,100">
      <animateTransform id="aT2"
         attributeName="transform"
         type="rotate"
         begin="dash2.end" dur="1s"
         values="
         0 120 100;
         14 120 100;
         0 120 100"
      />          
    </path> 
<path id="line3" class="axisLine" d="M170,50 170,100"  >
   <animateTransform id="aT3"
     attributeName="transform"
     type="rotate"
     begin="dash3.end" dur="1s"
     values="
      0 170 100;
      14 170 100;
       0 170 100"
  />          
</path> 
    <path id="line4" class="axisLine" d="M220,50 220,100" >
        <animateTransform id="aT4"
         attributeName="transform"
         type="rotate"
         begin="dash4.end" dur="1s"
         values="
         0 220 100;
         14 220 100;
         0 220 100"
      />          
    </path> 
</svg>     

In the following example, I will double the time of 1 and 3 of the line growth animation - dur="2s" Note: that the logic of the application will not break, the animations will still go one after another
If CSS animations were used, then timings and delays would have to be recalculated for each animation.

Animation starts after clicking on the canvas

#tube {
stroke:brown;
}
.axisLine {
stroke:green;
}
<svg id="svg1" xmlns="http://www.w3.org/2000/svg"  xmlns:xlink="http://www.w3.org/1999/xlink"
          viewBox="0 0 400 200" >  
<rect x="20" y="50" width="200" height="50"  fill="none" stroke="black" />
 <path id="tube" stroke-dasharray="0,200" d="M20,75 220,75" >
           <!-- Line Growth Animation   -->
  <animate id="dash1" attributeName="stroke-dasharray" begin="svg1.click" dur="2s" values="0 200;50 150" fill="freeze"  restart="never"/> 
   <animate id="dash2"  attributeName="stroke-dasharray" begin="aT1.end+0.2s" dur="1s" values="50 150;100 100" fill="freeze" /> 
    <animate id="dash3"  attributeName="stroke-dasharray" begin="aT2.end+0.2s" dur="2s" values="100 100;150 50" fill="freeze" />
      <animate id="dash4"  attributeName="stroke-dasharray" begin="aT3.end+0.2s" dur="1s" values="150 50;200 0" fill="freeze" />
 </path> 
               <!-- Animation of rotation of vertical lines -->
 <path id="line1" class="axisLine"  d="M70,50 70,100">
      <animateTransform id="aT1"
         attributeName="transform"
         type="rotate"
         begin="dash1.end" dur="1s"
         values="0 70 100;14 70 100;0 70 100"
      />          
 </path> 
   <path id="line2" class="axisLine"  d="M120,50 120,100">
      <animateTransform id="aT2"
         attributeName="transform"
         type="rotate"
         begin="dash2.end" dur="1s"
         values="0 120 100;14 120 100;0 120 100"
      />          
    </path> 
<path id="line3" class="axisLine" d="M170,50 170,100"  >
   <animateTransform id="aT3"
     attributeName="transform"
     type="rotate"
     begin="dash3.end" dur="1s"
     values="0 170 100;14 170 100;0 170 100"
  />          
</path> 
    <path id="line4" class="axisLine" d="M220,50 220,100" >
        <animateTransform id="aT4"
         attributeName="transform"
         type="rotate"
         begin="dash4.end" dur="1s"
         values="0 220 100;14 220 100;0 220 100"
      />          
    </path> 
</svg>