text.ts 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. import BoundingRect, { RectLike } from '../core/BoundingRect';
  2. import { Dictionary, TextAlign, TextVerticalAlign, BuiltinTextPosition } from '../core/types';
  3. import LRU from '../core/LRU';
  4. import { DEFAULT_FONT, platformApi } from '../core/platform';
  5. let textWidthCache: Dictionary<LRU<number>> = {};
  6. export function getWidth(text: string, font: string): number {
  7. font = font || DEFAULT_FONT;
  8. let cacheOfFont = textWidthCache[font];
  9. if (!cacheOfFont) {
  10. cacheOfFont = textWidthCache[font] = new LRU(500);
  11. }
  12. let width = cacheOfFont.get(text);
  13. if (width == null) {
  14. width = platformApi.measureText(text, font).width;
  15. cacheOfFont.put(text, width);
  16. }
  17. return width;
  18. }
  19. /**
  20. *
  21. * Get bounding rect for inner usage(TSpan)
  22. * Which not include text newline.
  23. */
  24. export function innerGetBoundingRect(
  25. text: string,
  26. font: string,
  27. textAlign?: TextAlign,
  28. textBaseline?: TextVerticalAlign
  29. ): BoundingRect {
  30. const width = getWidth(text, font);
  31. const height = getLineHeight(font);
  32. const x = adjustTextX(0, width, textAlign);
  33. const y = adjustTextY(0, height, textBaseline);
  34. const rect = new BoundingRect(x, y, width, height);
  35. return rect;
  36. }
  37. /**
  38. *
  39. * Get bounding rect for outer usage. Compatitable with old implementation
  40. * Which includes text newline.
  41. */
  42. export function getBoundingRect(
  43. text: string,
  44. font: string,
  45. textAlign?: TextAlign,
  46. textBaseline?: TextVerticalAlign
  47. ) {
  48. const textLines = ((text || '') + '').split('\n');
  49. const len = textLines.length;
  50. if (len === 1) {
  51. return innerGetBoundingRect(textLines[0], font, textAlign, textBaseline);
  52. }
  53. else {
  54. const uniondRect = new BoundingRect(0, 0, 0, 0);
  55. for (let i = 0; i < textLines.length; i++) {
  56. const rect = innerGetBoundingRect(textLines[i], font, textAlign, textBaseline);
  57. i === 0 ? uniondRect.copy(rect) : uniondRect.union(rect);
  58. }
  59. return uniondRect;
  60. }
  61. }
  62. export function adjustTextX(x: number, width: number, textAlign: TextAlign): number {
  63. // TODO Right to left language
  64. if (textAlign === 'right') {
  65. x -= width;
  66. }
  67. else if (textAlign === 'center') {
  68. x -= width / 2;
  69. }
  70. return x;
  71. }
  72. export function adjustTextY(y: number, height: number, verticalAlign: TextVerticalAlign): number {
  73. if (verticalAlign === 'middle') {
  74. y -= height / 2;
  75. }
  76. else if (verticalAlign === 'bottom') {
  77. y -= height;
  78. }
  79. return y;
  80. }
  81. export function getLineHeight(font?: string): number {
  82. // FIXME A rough approach.
  83. return getWidth('国', font);
  84. }
  85. export function measureText(text: string, font?: string): {
  86. width: number
  87. } {
  88. return platformApi.measureText(text, font);
  89. }
  90. export function parsePercent(value: number | string, maxValue: number): number {
  91. if (typeof value === 'string') {
  92. if (value.lastIndexOf('%') >= 0) {
  93. return parseFloat(value) / 100 * maxValue;
  94. }
  95. return parseFloat(value);
  96. }
  97. return value;
  98. }
  99. export interface TextPositionCalculationResult {
  100. x: number
  101. y: number
  102. align: TextAlign
  103. verticalAlign: TextVerticalAlign
  104. }
  105. /**
  106. * Follow same interface to `Displayable.prototype.calculateTextPosition`.
  107. * @public
  108. * @param out Prepared out object. If not input, auto created in the method.
  109. * @param style where `textPosition` and `textDistance` are visited.
  110. * @param rect {x, y, width, height} Rect of the host elment, according to which the text positioned.
  111. * @return The input `out`. Set: {x, y, textAlign, textVerticalAlign}
  112. */
  113. export function calculateTextPosition(
  114. out: TextPositionCalculationResult,
  115. opts: {
  116. position?: BuiltinTextPosition | (number | string)[]
  117. distance?: number // Default 5
  118. global?: boolean
  119. },
  120. rect: RectLike
  121. ): TextPositionCalculationResult {
  122. const textPosition = opts.position || 'inside';
  123. const distance = opts.distance != null ? opts.distance : 5;
  124. const height = rect.height;
  125. const width = rect.width;
  126. const halfHeight = height / 2;
  127. let x = rect.x;
  128. let y = rect.y;
  129. let textAlign: TextAlign = 'left';
  130. let textVerticalAlign: TextVerticalAlign = 'top';
  131. if (textPosition instanceof Array) {
  132. x += parsePercent(textPosition[0], rect.width);
  133. y += parsePercent(textPosition[1], rect.height);
  134. // Not use textAlign / textVerticalAlign
  135. textAlign = null;
  136. textVerticalAlign = null;
  137. }
  138. else {
  139. switch (textPosition) {
  140. case 'left':
  141. x -= distance;
  142. y += halfHeight;
  143. textAlign = 'right';
  144. textVerticalAlign = 'middle';
  145. break;
  146. case 'right':
  147. x += distance + width;
  148. y += halfHeight;
  149. textVerticalAlign = 'middle';
  150. break;
  151. case 'top':
  152. x += width / 2;
  153. y -= distance;
  154. textAlign = 'center';
  155. textVerticalAlign = 'bottom';
  156. break;
  157. case 'bottom':
  158. x += width / 2;
  159. y += height + distance;
  160. textAlign = 'center';
  161. break;
  162. case 'inside':
  163. x += width / 2;
  164. y += halfHeight;
  165. textAlign = 'center';
  166. textVerticalAlign = 'middle';
  167. break;
  168. case 'insideLeft':
  169. x += distance;
  170. y += halfHeight;
  171. textVerticalAlign = 'middle';
  172. break;
  173. case 'insideRight':
  174. x += width - distance;
  175. y += halfHeight;
  176. textAlign = 'right';
  177. textVerticalAlign = 'middle';
  178. break;
  179. case 'insideTop':
  180. x += width / 2;
  181. y += distance;
  182. textAlign = 'center';
  183. break;
  184. case 'insideBottom':
  185. x += width / 2;
  186. y += height - distance;
  187. textAlign = 'center';
  188. textVerticalAlign = 'bottom';
  189. break;
  190. case 'insideTopLeft':
  191. x += distance;
  192. y += distance;
  193. break;
  194. case 'insideTopRight':
  195. x += width - distance;
  196. y += distance;
  197. textAlign = 'right';
  198. break;
  199. case 'insideBottomLeft':
  200. x += distance;
  201. y += height - distance;
  202. textVerticalAlign = 'bottom';
  203. break;
  204. case 'insideBottomRight':
  205. x += width - distance;
  206. y += height - distance;
  207. textAlign = 'right';
  208. textVerticalAlign = 'bottom';
  209. break;
  210. }
  211. }
  212. out = out || {} as TextPositionCalculationResult;
  213. out.x = x;
  214. out.y = y;
  215. out.align = textAlign;
  216. out.verticalAlign = textVerticalAlign;
  217. return out;
  218. }