SVGTextElement.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. import {
  2. extendPrototype,
  3. } from '../../utils/functionExtensions';
  4. import {
  5. createSizedArray,
  6. } from '../../utils/helpers/arrays';
  7. import createNS from '../../utils/helpers/svg_elements';
  8. import BaseElement from '../BaseElement';
  9. import TransformElement from '../helpers/TransformElement';
  10. import SVGBaseElement from './SVGBaseElement';
  11. import HierarchyElement from '../helpers/HierarchyElement';
  12. import FrameElement from '../helpers/FrameElement';
  13. import RenderableDOMElement from '../helpers/RenderableDOMElement';
  14. import ITextElement from '../TextElement';
  15. import SVGCompElement from './SVGCompElement'; // eslint-disable-line
  16. import SVGShapeElement from './SVGShapeElement';
  17. var emptyShapeData = {
  18. shapes: [],
  19. };
  20. function SVGTextLottieElement(data, globalData, comp) {
  21. this.textSpans = [];
  22. this.renderType = 'svg';
  23. this.initElement(data, globalData, comp);
  24. }
  25. extendPrototype([BaseElement, TransformElement, SVGBaseElement, HierarchyElement, FrameElement, RenderableDOMElement, ITextElement], SVGTextLottieElement);
  26. SVGTextLottieElement.prototype.createContent = function () {
  27. if (this.data.singleShape && !this.globalData.fontManager.chars) {
  28. this.textContainer = createNS('text');
  29. }
  30. };
  31. SVGTextLottieElement.prototype.buildTextContents = function (textArray) {
  32. var i = 0;
  33. var len = textArray.length;
  34. var textContents = [];
  35. var currentTextContent = '';
  36. while (i < len) {
  37. if (textArray[i] === String.fromCharCode(13) || textArray[i] === String.fromCharCode(3)) {
  38. textContents.push(currentTextContent);
  39. currentTextContent = '';
  40. } else {
  41. currentTextContent += textArray[i];
  42. }
  43. i += 1;
  44. }
  45. textContents.push(currentTextContent);
  46. return textContents;
  47. };
  48. SVGTextLottieElement.prototype.buildShapeData = function (data, scale) {
  49. // data should probably be cloned to apply scale separately to each instance of a text on different layers
  50. // but since text internal content gets only rendered once and then it's never rerendered,
  51. // it's probably safe not to clone data and reuse always the same instance even if the object is mutated.
  52. // Avoiding cloning is preferred since cloning each character shape data is expensive
  53. if (data.shapes && data.shapes.length) {
  54. var shape = data.shapes[0];
  55. if (shape.it) {
  56. var shapeItem = shape.it[shape.it.length - 1];
  57. if (shapeItem.s) {
  58. shapeItem.s.k[0] = scale;
  59. shapeItem.s.k[1] = scale;
  60. }
  61. }
  62. }
  63. return data;
  64. };
  65. SVGTextLottieElement.prototype.buildNewText = function () {
  66. this.addDynamicProperty(this);
  67. var i;
  68. var len;
  69. var documentData = this.textProperty.currentData;
  70. this.renderedLetters = createSizedArray(documentData ? documentData.l.length : 0);
  71. if (documentData.fc) {
  72. this.layerElement.setAttribute('fill', this.buildColor(documentData.fc));
  73. } else {
  74. this.layerElement.setAttribute('fill', 'rgba(0,0,0,0)');
  75. }
  76. if (documentData.sc) {
  77. this.layerElement.setAttribute('stroke', this.buildColor(documentData.sc));
  78. this.layerElement.setAttribute('stroke-width', documentData.sw);
  79. }
  80. this.layerElement.setAttribute('font-size', documentData.finalSize);
  81. var fontData = this.globalData.fontManager.getFontByName(documentData.f);
  82. if (fontData.fClass) {
  83. this.layerElement.setAttribute('class', fontData.fClass);
  84. } else {
  85. this.layerElement.setAttribute('font-family', fontData.fFamily);
  86. var fWeight = documentData.fWeight;
  87. var fStyle = documentData.fStyle;
  88. this.layerElement.setAttribute('font-style', fStyle);
  89. this.layerElement.setAttribute('font-weight', fWeight);
  90. }
  91. this.layerElement.setAttribute('aria-label', documentData.t);
  92. var letters = documentData.l || [];
  93. var usesGlyphs = !!this.globalData.fontManager.chars;
  94. len = letters.length;
  95. var tSpan;
  96. var matrixHelper = this.mHelper;
  97. var shapeStr = '';
  98. var singleShape = this.data.singleShape;
  99. var xPos = 0;
  100. var yPos = 0;
  101. var firstLine = true;
  102. var trackingOffset = documentData.tr * 0.001 * documentData.finalSize;
  103. if (singleShape && !usesGlyphs && !documentData.sz) {
  104. var tElement = this.textContainer;
  105. var justify = 'start';
  106. switch (documentData.j) {
  107. case 1:
  108. justify = 'end';
  109. break;
  110. case 2:
  111. justify = 'middle';
  112. break;
  113. default:
  114. justify = 'start';
  115. break;
  116. }
  117. tElement.setAttribute('text-anchor', justify);
  118. tElement.setAttribute('letter-spacing', trackingOffset);
  119. var textContent = this.buildTextContents(documentData.finalText);
  120. len = textContent.length;
  121. yPos = documentData.ps ? documentData.ps[1] + documentData.ascent : 0;
  122. for (i = 0; i < len; i += 1) {
  123. tSpan = this.textSpans[i].span || createNS('tspan');
  124. tSpan.textContent = textContent[i];
  125. tSpan.setAttribute('x', 0);
  126. tSpan.setAttribute('y', yPos);
  127. tSpan.style.display = 'inherit';
  128. tElement.appendChild(tSpan);
  129. if (!this.textSpans[i]) {
  130. this.textSpans[i] = {
  131. span: null,
  132. glyph: null,
  133. };
  134. }
  135. this.textSpans[i].span = tSpan;
  136. yPos += documentData.finalLineHeight;
  137. }
  138. this.layerElement.appendChild(tElement);
  139. } else {
  140. var cachedSpansLength = this.textSpans.length;
  141. var charData;
  142. for (i = 0; i < len; i += 1) {
  143. if (!this.textSpans[i]) {
  144. this.textSpans[i] = {
  145. span: null,
  146. childSpan: null,
  147. glyph: null,
  148. };
  149. }
  150. if (!usesGlyphs || !singleShape || i === 0) {
  151. tSpan = cachedSpansLength > i ? this.textSpans[i].span : createNS(usesGlyphs ? 'g' : 'text');
  152. if (cachedSpansLength <= i) {
  153. tSpan.setAttribute('stroke-linecap', 'butt');
  154. tSpan.setAttribute('stroke-linejoin', 'round');
  155. tSpan.setAttribute('stroke-miterlimit', '4');
  156. this.textSpans[i].span = tSpan;
  157. if (usesGlyphs) {
  158. var childSpan = createNS('g');
  159. tSpan.appendChild(childSpan);
  160. this.textSpans[i].childSpan = childSpan;
  161. }
  162. this.textSpans[i].span = tSpan;
  163. this.layerElement.appendChild(tSpan);
  164. }
  165. tSpan.style.display = 'inherit';
  166. }
  167. matrixHelper.reset();
  168. if (singleShape) {
  169. if (letters[i].n) {
  170. xPos = -trackingOffset;
  171. yPos += documentData.yOffset;
  172. yPos += firstLine ? 1 : 0;
  173. firstLine = false;
  174. }
  175. this.applyTextPropertiesToMatrix(documentData, matrixHelper, letters[i].line, xPos, yPos);
  176. xPos += letters[i].l || 0;
  177. // xPos += letters[i].val === ' ' ? 0 : trackingOffset;
  178. xPos += trackingOffset;
  179. }
  180. if (usesGlyphs) {
  181. charData = this.globalData.fontManager.getCharData(
  182. documentData.finalText[i],
  183. fontData.fStyle,
  184. this.globalData.fontManager.getFontByName(documentData.f).fFamily
  185. );
  186. var glyphElement;
  187. // t === 1 means the character has been replaced with an animated shaped
  188. if (charData.t === 1) {
  189. glyphElement = new SVGCompElement(charData.data, this.globalData, this);
  190. } else {
  191. var data = emptyShapeData;
  192. if (charData.data && charData.data.shapes) {
  193. data = this.buildShapeData(charData.data, documentData.finalSize);
  194. }
  195. glyphElement = new SVGShapeElement(data, this.globalData, this);
  196. }
  197. if (this.textSpans[i].glyph) {
  198. var glyph = this.textSpans[i].glyph;
  199. this.textSpans[i].childSpan.removeChild(glyph.layerElement);
  200. glyph.destroy();
  201. }
  202. this.textSpans[i].glyph = glyphElement;
  203. glyphElement._debug = true;
  204. glyphElement.prepareFrame(0);
  205. glyphElement.renderFrame();
  206. this.textSpans[i].childSpan.appendChild(glyphElement.layerElement);
  207. // when using animated shapes, the layer will be scaled instead of replacing the internal scale
  208. // this might have issues with strokes and might need a different solution
  209. if (charData.t === 1) {
  210. this.textSpans[i].childSpan.setAttribute('transform', 'scale(' + documentData.finalSize / 100 + ',' + documentData.finalSize / 100 + ')');
  211. }
  212. } else {
  213. if (singleShape) {
  214. tSpan.setAttribute('transform', 'translate(' + matrixHelper.props[12] + ',' + matrixHelper.props[13] + ')');
  215. }
  216. tSpan.textContent = letters[i].val;
  217. tSpan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');
  218. }
  219. //
  220. }
  221. if (singleShape && tSpan) {
  222. tSpan.setAttribute('d', shapeStr);
  223. }
  224. }
  225. while (i < this.textSpans.length) {
  226. this.textSpans[i].span.style.display = 'none';
  227. i += 1;
  228. }
  229. this._sizeChanged = true;
  230. };
  231. SVGTextLottieElement.prototype.sourceRectAtTime = function () {
  232. this.prepareFrame(this.comp.renderedFrame - this.data.st);
  233. this.renderInnerContent();
  234. if (this._sizeChanged) {
  235. this._sizeChanged = false;
  236. var textBox = this.layerElement.getBBox();
  237. this.bbox = {
  238. top: textBox.y,
  239. left: textBox.x,
  240. width: textBox.width,
  241. height: textBox.height,
  242. };
  243. }
  244. return this.bbox;
  245. };
  246. SVGTextLottieElement.prototype.getValue = function () {
  247. var i;
  248. var len = this.textSpans.length;
  249. var glyphElement;
  250. this.renderedFrame = this.comp.renderedFrame;
  251. for (i = 0; i < len; i += 1) {
  252. glyphElement = this.textSpans[i].glyph;
  253. if (glyphElement) {
  254. glyphElement.prepareFrame(this.comp.renderedFrame - this.data.st);
  255. if (glyphElement._mdf) {
  256. this._mdf = true;
  257. }
  258. }
  259. }
  260. };
  261. SVGTextLottieElement.prototype.renderInnerContent = function () {
  262. this.validateText();
  263. if (!this.data.singleShape || this._mdf) {
  264. this.textAnimator.getMeasures(this.textProperty.currentData, this.lettersChangedFlag);
  265. if (this.lettersChangedFlag || this.textAnimator.lettersChangedFlag) {
  266. this._sizeChanged = true;
  267. var i;
  268. var len;
  269. var renderedLetters = this.textAnimator.renderedLetters;
  270. var letters = this.textProperty.currentData.l;
  271. len = letters.length;
  272. var renderedLetter;
  273. var textSpan;
  274. var glyphElement;
  275. for (i = 0; i < len; i += 1) {
  276. if (!letters[i].n) {
  277. renderedLetter = renderedLetters[i];
  278. textSpan = this.textSpans[i].span;
  279. glyphElement = this.textSpans[i].glyph;
  280. if (glyphElement) {
  281. glyphElement.renderFrame();
  282. }
  283. if (renderedLetter._mdf.m) {
  284. textSpan.setAttribute('transform', renderedLetter.m);
  285. }
  286. if (renderedLetter._mdf.o) {
  287. textSpan.setAttribute('opacity', renderedLetter.o);
  288. }
  289. if (renderedLetter._mdf.sw) {
  290. textSpan.setAttribute('stroke-width', renderedLetter.sw);
  291. }
  292. if (renderedLetter._mdf.sc) {
  293. textSpan.setAttribute('stroke', renderedLetter.sc);
  294. }
  295. if (renderedLetter._mdf.fc) {
  296. textSpan.setAttribute('fill', renderedLetter.fc);
  297. }
  298. }
  299. }
  300. }
  301. }
  302. };
  303. export default SVGTextLottieElement;