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.
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.