Search code examples
javascriptaugmented-realityaframeweb-ar

Is there any way for animating the text (atext) in aframe [WebAR]?


In Web AR,I need to animate atext component in aframe. How to achieve the text animation in aframe ? Not found any properties in atext component.


Solution

  • As far as I know, there is no simple way to treat the letters as separate entities. They're even not separate meshes - the component generates one geometry containing all letters.

    Probably it would be better to create an animated model, or even to use an animated texture.


    However, with a little bit javascript we can dig into the underlying THREE.js layer and split a text into separate letters.

    One way would be to attach text components with a single letters to <a-entity> nodes.

    Having <a-entity> nodes declared we can attach animations like we would to a normal a-frame entity (especially position / rotation ones). But there comes the issue of positioning:

    <script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
    <script>
      AFRAME.registerComponent("splitter", {
        init: function() {
          // grab all <a-entity> letter nodes
          const letter_nodes = document.querySelectorAll(".letter");
          // grab the "text" configuration
          const textData = this.el.getAttribute("text");
          // create a temporary vector to store the position
          const vec = new THREE.Vector3();
          
          for (var i = 0; i < letter_nodes.length; i++) {
            // set the "text" component in each letter node
            letter_nodes[i].setAttribute('text', {
              value: textData.value[i],
              anchor: textData.align, // a-text binding
              width: textData.width // a-text binding
            })
            // set position
            vec.copy(this.el.getAttribute("position"));
            vec.x += i * 0.1; // move the letters to the right
            vec.y -= 0.2; // move them down
            letter_nodes[i].setAttribute("position", vec)
          }
        }
      })
    </script>
    <a-scene>
      <!-- original text -->
      <a-text value="foo" position="0 1.6 -1" splitter></a-text>
    
      <!-- entities  -->
      <a-entity class="letter" animation="property: position; to: 0 1.2 -1; dur: 1000; dir: alternate; loop: true"></a-entity>
      <a-entity class="letter" animation="property: rotation; to: 0 0 360; dur: 1000; dir: alternate; loop: true"></a-entity>
      <a-entity class="letter"></a-entity>
      <a-sky color="#ECECEC"></a-sky>
    </a-scene>

    First of all - we need to get the original letters spacing, this is sloppy. From what I can tell, a-frames version of THREE.TextGeometry has a property visibleGlyphs, which has the position of the glyphs (as well as their heights, and offsets). We can use it to properly position our text.

    Second of all - the position animation needs the global position. It would be better to input offsets, not target positions. To make it work, the text nodes could be child nodes of the .letter nodes.

    A "generic" component could look like this:

    <script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
    <script>
      AFRAME.registerComponent("text-splitter", {
        init: function() {
          const el = this.el;
          // i'll use the child nodes as wrapper entities for letter entities
          const letter_wrappers = el.children
    
          // wait until the text component tells us that it's ready
          this.el.addEventListener("object3dset", function objectset(evt) {
            el.removeEventListener("object3dset", objectset); // react only once
    
            const mesh = el.getObject3D("text") // grab the mesh
            const geometry = mesh.geometry // grab the text geometry
    
            // wait until the visibleGlyphs are set
            const idx = setInterval(evt => {
              if (!geometry.visibleGlyphs) return;
              clearInterval(idx);
    
              // we want data.height, data.yoffset and position from each glyph
              const glyphs = geometry.visibleGlyphs
    
              // do as many loops as there are <entity - glyph> pairs
              const iterations = Math.min(letter_wrappers.length, glyphs.length)
              const textData = el.getAttribute("text"); // original configuration
              var text = textData.value.replace(/\s+/, "") // get rid of spaces
    
              const letter_pos = new THREE.Vector3();
              for (var i = 0; i < iterations; i++) {
                // use the positions, heights, and offsets of the glyphs
                letter_pos.set(glyphs[i].position[0], glyphs[i].position[1], 0);
                letter_pos.y += (glyphs[i].data.height + glyphs[i].data.yoffset) / 2;
    
                // convert the letter local position to world
                mesh.localToWorld(letter_pos)
    
                // convert the world position to the <a-text> position
                el.object3D.worldToLocal(letter_pos)
    
                // apply the text and position to the wrappers
                const node = document.createElement("a-entity")
                node.setAttribute("position", letter_pos)
                node.setAttribute('text', {
                  value: text[i],
                  anchor: textData.align, // a-text binding
                  width: textData.width // a-text binding
                })
                letter_wrappers[i].appendChild(node)
              }
              // remove the original text
              el.removeAttribute("text")
            }, 100)
          })
        }
      })
    
    </script>
    <a-scene>
      <!-- child entities of the original a-text are used as letter wrappers -->
      <a-text value="fo o" position="0 1.6 -1" text-splitter>
        <a-entity animation="property: rotation; to: 0 0 360; dur: 1000; loop: true"></a-entity>
        <a-entity animation="property: position; to: 0 0.25 0; dur: 500; dir: alternate; loop: true"></a-entity>
        <a-entity animation="property: position; to: 0 -0.25 0; dur: 500; dir: alternate; loop: true"></a-entity>
      </a-text>
      
      <!-- just to see that the text is aligned properly-->
      <a-text value="fo o" position="0 1.6 -1"></a-text>
      <a-sky color="#ECECEC"></a-sky>
    </a-scene>


    Here's an example of dynamically adding text entities + using anime.js to set up a timeline for each letter.