Search code examples
d3.jssvgstring-interpolationtween

How to apply a tween function on letters instead of numbers?


For a transition that I apply on a text object in d3.js I would like to tween the text like in this example. However, my problem is that this won't work if the text is made of letters instead of numbers and until now I only found examples that only used numbers.

So my question is: Is it possible to create such a tween transition on a text object with letters?

This is how I currently apply it (using the interpolateString() function):

var mytext = svg.append("text")
    .style("fill", 'black')
    .attr("x", 0)
    .attr("y", 0)
    .style("font-size", "14")
    .text("My old text")

mytext
    .transition()
          .delay(1500)
          .duration(1000)
          .tween("text", function(d) { 
              var self = d3.select(this)
                var i = d3.interpolateString(self.text().replace(/,/g, ""), "My new text");
                    return function(t) {self.text(i(t))};
              });



Solution

  • You have a misconception regarding what interpolateString does. If you look at the API, you'll see that it...

    Returns an interpolator between the two strings a and b. The string interpolator finds numbers embedded in a and b, where each number is of the form understood by JavaScript (emphasis mine)

    So, it will not interpolate strings made with letters only.

    That being said, it lead us to the question: how do you want to interpolate the letters? For instance, you can interpolate them according to their position in the Roman alphabet. If that is the case, this is a way to do it:

    Create an array with the alphabet...

    const alphabet = " abcdefghijklmnopqrstuvwxyz".split("");
    

    ... and, in the tween function, interpolate the index of each letter in the alphabet, from the old text to the new text:

    .tween("text", function() {
        return function(t) {
          d3.select(this).text(oldText.map(function(d,i){
            return alphabet[~~(d3.interpolate(alphabet.indexOf(d), alphabet.indexOf(newText[i]))(t))]
          }).join(""))
        };
    });
    

    This is a very basic approach, which will work only if the two strings have exactly the same length. Also, pay attention to the fact that I put an space in the alphabet array, otherwise we'll get undefined for the spaces in the strings.

    Here is a demo:

    const svg = d3.select("svg")
    const oldText = "this is my old text".split("");
    const newText = "here is my new text".split("");
    const alphabet = " abcdefghijklmnopqrstuvwxyz".split("");
    const text = svg.append("text")
      .attr("y", 50)
      .attr("x", 20)
      .attr("font-size", 30)
      .text(oldText.join(""));
    text.transition()
      .delay(500)
      .duration(2000)
      .ease(d3.easeLinear)
      .tween("text", function() {
        return function(t) {
          d3.select(this).text(oldText.map(function(d, i) {
            return alphabet[~~(d3.interpolate(alphabet.indexOf(d), alphabet.indexOf(newText[i]))(t))]
          }).join(""))
        };
      });
    <script src="https://d3js.org/d3.v5.min.js"></script>
    <svg></svg>