diff --git a/components/text-animation/README.md b/components/text-animation/README.md new file mode 100644 index 00000000..55f5c8cb --- /dev/null +++ b/components/text-animation/README.md @@ -0,0 +1,112 @@ +# text-animation + +this component is used to animate *text-geometry* text,such as Chinese characters. + +## Usage + +```html + + + + + + + + + + + + + + + + + + +``` +and +```js +function textAnimation(textEl, index, position, data) { + const baseDelay = 500; // 基础延迟 + setTimeout(() => { + textEl.setAttribute('animation', { + property: 'material.opacity', + to: 1, + dur: 500 + }); + }, index * 200 + baseDelay); // 延迟确保逐字显示的效果 +} +``` +we use the `textAnimation` function to animate the text, in this situation, we set the `property` to `material.opacity` and `to` to 1, and set the `dur` to 500, which means the opacity will change from 0 to 1 in 500ms.Chinese characters will be displayed one by one with a delay of 200ms. +you can customize the `textAnimation` function according to your needs. + +## Tips + +### font +you can use facetype.js to convert ttf or otf to json format, and then use the json file as the `font` attribute of the `text-geometry` component. +### text-geometry +text-geometry is a good choice for rendering Chinese characters, but it has some limitations, such as: + +- It cannot reuse the same texture for different characters, which means the texture will be loaded multiple times. + +so I edited the `text-geometry` component to support the reuse of the same texture for different characters: + +```js +/** + * TextGeometry component for A-Frame. + */ +require('./lib/FontLoader') +require('./lib/TextGeometry') +var debug = AFRAME.utils.debug; +var error = debug('aframe-text-component:error'); +var fontLoader = new THREE.FontLoader(); +var fontCache = {}; // cache for loaded fonts +AFRAME.registerComponent('text-geometry', { + schema: { + bevelEnabled: { default: false }, + bevelSize: { default: 8, min: 0 }, + bevelThickness: { default: 12, min: 0 }, + curveSegments: { default: 12, min: 0 }, + font: { type: 'asset', default: 'https://rawgit.com/ngokevin/kframe/master/components/text-geometry/lib/helvetiker_regular.typeface.json' }, + height: { default: 0.05, min: 0 }, + size: { default: 0.5, min: 0 }, + style: { default: 'normal', oneOf: ['normal', 'italics'] }, + weight: { default: 'normal', oneOf: ['normal', 'bold'] }, + value: { default: '' } + }, + + /** + * Called when component is attached and when component data changes. + * Generally modifies the entity based on the data. + */ + update: function (oldData) { + var data = this.data; + var el = this.el; + + // check if font is already cached + if (fontCache[data.font]) { + this.createTextGeometry(fontCache[data.font]); + } else { + if (data.font.constructor === String) { + fontLoader.load(data.font, (response) => { + fontCache[data.font] = response; // cache font + this.createTextGeometry(response); + }); + } else if (data.font.constructor === Object) { + this.createTextGeometry(data.font); + } else { + error('Must provide `font` (typeface.json) or `fontPath` (string) to text component.'); + } + } + }, + + createTextGeometry: function (font) { + var data = this.data; + var el = this.el; + + var textData = AFRAME.utils.clone(data); + textData.font = font; + el.getOrCreateObject3D('mesh', THREE.Mesh).geometry = new THREE.TextGeometry(data.value, textData); + } +}); +``` \ No newline at end of file diff --git a/components/text-animation/index.js b/components/text-animation/index.js new file mode 100644 index 00000000..6c96b96f --- /dev/null +++ b/components/text-animation/index.js @@ -0,0 +1,64 @@ +AFRAME.registerComponent('text-animation', { + schema: { + text: { type: 'string' }, + font: { type: 'selector' }, + fontSize: { type: 'number', default: 0.5 }, + color: { type: 'string', default: '#FFF' }, + charsPerLine: { type: 'number', default: 10 }, + indent: { type: 'number', default: 2 }, + position: { type: 'vec3', default: { x: 0, y: 2, z: -5 } }, + letterSpacing: { type: 'number', default: 0 }, + lineHeight: { type: 'number', default: 1.5 }, + curveSegments: { type: 'number', default: 4 }, + height: { type: 'number', default: 0.05 }, + }, + init: function () { + const data = this.data; + const el = this.el; + const functionName = data._function; + delete this._function; + const characters = data.text.split(''); + let currentLine = 0; + let charIndex = 0; + let index = 0; + + const animateText = () => { + if (index >= characters.length) return; // if all characters have been animated, exit function + const char = characters[index]; + if (charIndex >= data.charsPerLine || (currentLine === 0 && charIndex >= data.charsPerLine - data.indent)) { + currentLine++; + charIndex = 0; + } + const deltaPosition = { + x: (charIndex + (currentLine === 0 ? data.indent : 0)) * (data.fontSize + data.letterSpacing), + y: -currentLine * (data.fontSize * data.lineHeight), + z: 0 + }; + const curPosition = { + x: data.position.x + deltaPosition.x, + y: data.position.y + deltaPosition.y, + z: data.position.z + deltaPosition.z + }; + const textEl = document.createElement('a-entity'); + textEl.setAttribute('text-geometry', { + value: char, + font: data.font.getAttribute('src'), + size: data.fontSize, + bevelEnabled: false, + curveSegments: data.curveSegments, + height:data.height + }); + textEl.setAttribute('material', { color: data.color, transparent: true, opacity: 0 }); + textEl.setAttribute('position', curPosition); + if (functionName && typeof window[functionName] === 'function') { + window[functionName](textEl, index, curPosition, data); + } + else { console.log('no function provided'); } + el.appendChild(textEl); + charIndex++; + index++; + requestAnimationFrame(animateText); + }; + animateText(); + } +}); diff --git a/components/text-geometry/index.js b/components/text-geometry/index.js index 57403cde..5d6c9bb9 100644 --- a/components/text-geometry/index.js +++ b/components/text-geometry/index.js @@ -3,25 +3,22 @@ */ require('./lib/FontLoader') require('./lib/TextGeometry') - var debug = AFRAME.utils.debug; - var error = debug('aframe-text-component:error'); - var fontLoader = new THREE.FontLoader(); - +var fontCache = {}; // cache for loaded fonts AFRAME.registerComponent('text-geometry', { schema: { - bevelEnabled: {default: false}, - bevelSize: {default: 8, min: 0}, - bevelThickness: {default: 12, min: 0}, - curveSegments: {default: 12, min: 0}, - font: {type: 'asset', default: 'https://rawgit.com/ngokevin/kframe/master/components/text-geometry/lib/helvetiker_regular.typeface.json'}, - height: {default: 0.05, min: 0}, - size: {default: 0.5, min: 0}, - style: {default: 'normal', oneOf: ['normal', 'italics']}, - weight: {default: 'normal', oneOf: ['normal', 'bold']}, - value: {default: ''} + bevelEnabled: { default: false }, + bevelSize: { default: 8, min: 0 }, + bevelThickness: { default: 12, min: 0 }, + curveSegments: { default: 12, min: 0 }, + font: { type: 'asset', default: 'https://rawgit.com/ngokevin/kframe/master/components/text-geometry/lib/helvetiker_regular.typeface.json' }, + height: { default: 0.05, min: 0 }, + size: { default: 0.5, min: 0 }, + style: { default: 'normal', oneOf: ['normal', 'italics'] }, + weight: { default: 'normal', oneOf: ['normal', 'bold'] }, + value: { default: '' } }, /** @@ -32,19 +29,29 @@ AFRAME.registerComponent('text-geometry', { var data = this.data; var el = this.el; - var mesh = el.getOrCreateObject3D('mesh', THREE.Mesh); - if (data.font.constructor === String) { - // Load typeface.json font. - fontLoader.load(data.font, function (response) { - var textData = AFRAME.utils.clone(data); - textData.font = response; - mesh.geometry = new THREE.TextGeometry(data.value, textData); - }); - } else if (data.font.constructor === Object) { - // Set font if already have a typeface.json through setAttribute. - mesh.geometry = new THREE.TextGeometry(data.value, data); + // check if font is already cached + if (fontCache[data.font]) { + this.createTextGeometry(fontCache[data.font]); } else { - error('Must provide `font` (typeface.json) or `fontPath` (string) to text component.'); + if (data.font.constructor === String) { + fontLoader.load(data.font, (response) => { + fontCache[data.font] = response; // cache font + this.createTextGeometry(response); + }); + } else if (data.font.constructor === Object) { + this.createTextGeometry(data.font); + } else { + error('Must provide `font` (typeface.json) or `fontPath` (string) to text component.'); + } } + }, + + createTextGeometry: function (font) { + var data = this.data; + var el = this.el; + + var textData = AFRAME.utils.clone(data); + textData.font = font; + el.getOrCreateObject3D('mesh', THREE.Mesh).geometry = new THREE.TextGeometry(data.value, textData); } });