TextSprite.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. // /**
  2. // * adapted from http://stemkoski.github.io/Three.js/Sprite-Text-Labels.html
  3. // */
  4. import * as THREE from "../../../libs/three.js/build/three.module.js";
  5. import Sprite from './Sprite.js'
  6. import Common from '../utils/Common.js';
  7. export class TextSprite extends THREE.Object3D{
  8. //注:为了分两层控制scale,不直接extend Sprite
  9. constructor( options={}){
  10. super()
  11. let map = new THREE.Texture();
  12. map.minFilter = THREE.LinearFilter;//清晰一些?
  13. map.magFilter = THREE.LinearFilter;
  14. this.sprite = new Sprite( Object.assign({
  15. root:this
  16. }
  17. ,options,
  18. {
  19. map,
  20. })
  21. )
  22. this.add(this.sprite)
  23. this.fontWeight = options.fontWeight == void 0 ? 'Bold' : options.fontWeight
  24. this.rectBorderThick = options.rectBorderThick || 0
  25. this.textBorderThick = options.textBorderThick || 0
  26. this.fontface = 'Arial';
  27. this.fontsize = options.fontsize || 16;
  28. this.lineSpace = options.lineSpace
  29. this.textBorderColor = options.textBorderColor ? Common.CloneObject(options.textBorderColor):{ r: 0, g: 0, b: 0, a: 1.0 };
  30. this.backgroundColor = options.backgroundColor ? Common.CloneObject(options.backgroundColor):{ r: 255, g: 255, b: 255, a: 1.0 };
  31. this.textColor = options.textColor ? Common.CloneObject(options.textColor):{r: 0, g: 0, b: 0, a: 1.0};
  32. this.borderColor = options.borderColor ? Common.CloneObject(options.borderColor):{ r: 0, g: 0, b: 0, a: 0.0 };
  33. this.borderRadius = options.borderRadius || 6;
  34. this.margin = options.margin
  35. this.textAlign = options.textAlign || 'center'
  36. this.name = options.name
  37. this.transform2Dpercent = options.transform2Dpercent
  38. this.maxLineWidth = options.maxLineWidth
  39. this.setText(options.text)
  40. }
  41. setText(text){
  42. if(text == void 0)text = ''
  43. if (this.text !== text) {
  44. if (!(text instanceof Array)) {
  45. this.text = text.split('\n') //如果是input手动输入的\n这里会是\\n且不会被拆分, 绘制的依然是\n。
  46. //this.text = [text + '']
  47. } else this.text = text
  48. this.updateTexture()
  49. }
  50. }
  51. /* setText(text){
  52. if(text == void 0)text = ''
  53. if (this.text !== text) {
  54. if (!(text instanceof Array)) {
  55. this.text = text.split('\n') //如果是input手动输入的\n这里会是\\n且不会被拆分, 绘制的依然是\n。
  56. if(this.maxRowWordsCount){//每行显示最大字数
  57. this.text.forEach(str=>{
  58. if(str.length > this.maxRowWordsCount){
  59. }
  60. })
  61. }
  62. //this.text = [text + '']
  63. } else this.text = text
  64. this.updateTexture()
  65. }
  66. } */
  67. setTextColor(color){
  68. this.textColor = Common.CloneObject(color);
  69. this.updateTexture();
  70. }
  71. setBorderColor(color){
  72. this.borderColor = Common.CloneObject(color);
  73. this.updateTexture();
  74. }
  75. setBackgroundColor(color){
  76. this.backgroundColor = Common.CloneObject(color);
  77. this.updateTexture();
  78. }
  79. setPos(pos){
  80. this.position.copy(pos)
  81. this.sprite.waitUpdate()
  82. }
  83. updatePose(){
  84. this.sprite.waitUpdate()
  85. }
  86. setUniforms(name,value){
  87. this.sprite.setUniforms(name,value)
  88. }
  89. updateTexture(){
  90. //canvas原点在左上角
  91. let canvas = document.createElement('canvas');
  92. let context = canvas.getContext('2d');
  93. const r = window.devicePixelRatio //不乘会模糊
  94. context.font = this.fontWeight + ' ' + this.fontsize * r + 'px ' + this.fontface; //context["font-weight"] = 100; //语法与 CSS font 属性相同。
  95. let textMaxWidth = 0, infos = []
  96. context.textBaseline = 'alphabetic' // "middle" //设置文字基线。当起点y设置为0时,只有该线以下的部分被绘制出来。middle时文字显示一半(但是对该字体所有字的一半,有的字是不一定显示一半的,尤其汉字),alphabetic时是英文字母的那条基线。
  97. let textHeightAll = 0
  98. let texts = []
  99. if(this.maxLineWidth){
  100. this.text.forEach((words)=>{
  101. if(!words){texts.push("");return;}
  102. texts = texts.concat( breakLinesForCanvas( words, context, this.maxLineWidth ) )
  103. })
  104. }else{
  105. texts = this.text
  106. }
  107. for (let text of texts) {
  108. let metrics = context.measureText(text)
  109. let textWidth = metrics.width
  110. infos.push(metrics)
  111. textMaxWidth = Math.max(textMaxWidth, textWidth)
  112. textHeightAll += metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent //文字真实高度
  113. }
  114. let margin = (this.margin ? new THREE.Vector2().copy(this.margin) : new THREE.Vector2(this.fontsize, this.fontsize * 0.8 )).multiplyScalar(r);
  115. const lineSpace = (this.lineSpace || this.fontsize * 0.5) * r
  116. let rectBorderThick = this.rectBorderThick * r,
  117. textBorderThick = this.textBorderThick * r
  118. let spriteWidth = 2 * (margin.x + rectBorderThick + textBorderThick ) + textMaxWidth //还要考虑this.textshadowColor,太麻烦了不写了
  119. let spriteHeight = 2 * (margin.y + rectBorderThick + textBorderThick * texts.length) + lineSpace * (texts.length - 1)+ textHeightAll;
  120. //canvas宽高只会向下取整数,所以为了防止拉伸模糊这里必须先取整
  121. spriteWidth = Math.floor(spriteWidth)
  122. spriteHeight = Math.floor(spriteHeight)
  123. context.canvas.width = spriteWidth;
  124. context.canvas.height = spriteHeight;
  125. context.font = this.fontWeight + ' ' + this.fontsize * r + 'px ' + this.fontface; //为何要再写一遍??
  126. if(spriteWidth>4000){
  127. console.error('spriteWidth',spriteWidth,'spriteHeight',spriteHeight,this.fontsize,r,texts,margin)
  128. }
  129. let expand = 0//Math.max(1, Math.pow(this.fontsize / 16, 1.1)) * r // 针对英文大部分在baseLine之上所以降低一点,或者可以识别当不包含jgqp时才加这个值 . 但即使都是汉字也会不同,如"哈哈"和"粉色",前者居中后者不
  130. context.strokeStyle = 'rgba(' + this.borderColor.r + ',' + this.borderColor.g + ',' + this.borderColor.b + ',' + this.borderColor.a + ')';
  131. context.lineWidth = rectBorderThick
  132. context.fillStyle = 'rgba(' + this.backgroundColor.r + ',' + this.backgroundColor.g + ',' + this.backgroundColor.b + ',' + this.backgroundColor.a + ')';
  133. this.roundRect(context, rectBorderThick / 2 , rectBorderThick / 2, spriteWidth - rectBorderThick, spriteHeight - rectBorderThick, this.borderRadius * r);
  134. context.fillStyle = 'rgba(' + this.textColor.r + ',' + this.textColor.g + ',' + this.textColor.b + ',' + this.textColor.a + ')'
  135. let y = margin.y + rectBorderThick
  136. for (let i = 0; i < texts.length; i++) {
  137. //文字y向距离从textBaseline向上算
  138. let actualBoundingBoxAscent = infos[i].fontBoundingBoxAscent == void 0 ? this.fontsize * r * 0.8 : infos[i].fontBoundingBoxAscent //有的流览器没有。只能大概给一个
  139. y += actualBoundingBoxAscent + textBorderThick
  140. let textLeftSpace = this.textAlign == 'center' ? (textMaxWidth - infos[i].width) / 2 : this.textAlign == 'left' ? 0 : textMaxWidth - infos[i].width
  141. let x = rectBorderThick + textBorderThick + margin.x + textLeftSpace
  142. // text color
  143. if (this.textBorderThick) {
  144. context.strokeStyle = 'rgba(' + this.textBorderColor.r + ',' + this.textBorderColor.g + ',' + this.textBorderColor.b + ',' + this.textBorderColor.a + ')'
  145. context.lineWidth = this.textBorderThick * r
  146. context.strokeText(texts[i], x, y)
  147. }
  148. if (this.textshadowColor) {
  149. context.shadowOffsetX = 0
  150. context.shadowOffsetY = 0
  151. context.shadowColor = this.textshadowColor //'red'
  152. context.shadowBlur = (this.textShadowBlur || this.fontSize/3) * r
  153. }
  154. context.fillText(texts[i], x, y)
  155. let actualBoundingBoxDescent = infos[i].fontBoundingBoxDescent == void 0 ? this.fontsize * r * 0.2 : infos[i].fontBoundingBoxDescent
  156. y += actualBoundingBoxDescent + textBorderThick + lineSpace
  157. }
  158. let texture = new THREE.Texture(canvas);
  159. texture.minFilter = THREE.LinearFilter;
  160. texture.magFilter = THREE.LinearFilter;
  161. texture.needsUpdate = true;
  162. //this.material.needsUpdate = true;
  163. if(this.sprite.material.map){
  164. this.sprite.material.map.dispose()
  165. }
  166. this.sprite.material.map = texture;
  167. let oldScale = this.sprite.scale.clone()
  168. this.sprite.scale.set(spriteWidth * 0.01 / r, spriteHeight * 0.01 / r, 1.0);
  169. if(!oldScale.equals(this.sprite.scale)){
  170. this.updateTransform2D()
  171. this.sprite.waitUpdate() //重新计算各个viewport的matrix
  172. }
  173. }
  174. roundRect(ctx, x, y, w, h, r){
  175. ctx.beginPath();
  176. ctx.moveTo(x + r, y);
  177. ctx.lineTo(x + w - r, y);
  178. ctx.arcTo(x + w, y, x + w, y + r, r );//圆弧。前四个参数同quadraticCurveTo
  179. //ctx.quadraticCurveTo(x + w, y, x + w, y + r); //二次贝塞尔曲线需要两个点。第一个点是用于二次贝塞尔计算中的控制点,第二个点是曲线的结束点。
  180. ctx.lineTo(x + w, y + h - r);
  181. ctx.arcTo(x + w, y + h, x + w - r, y + h, r );
  182. ctx.lineTo(x + r, y + h);
  183. ctx.arcTo(x, y + h, x, y + h - r, r );
  184. ctx.lineTo(x, y + r);
  185. ctx.arcTo(x, y, x + r, y, r );
  186. ctx.closePath();
  187. ctx.fill();
  188. ctx.stroke();
  189. }
  190. updateTransform2D(){
  191. if(this.transform2Dpercent){
  192. ['x','y'].forEach((axis)=>{
  193. let percent = this.transform2Dpercent[axis]
  194. this.sprite.position.y = this.sprite.scale.y * percent
  195. })
  196. }
  197. }
  198. dispose(){
  199. this.sprite.material.uniforms.map.value.dispose()
  200. this.parent && this.parent.remove(this)
  201. this.sprite.dispose()
  202. this.removeAllListeners()
  203. this.dispatchEvent('dispose')
  204. }
  205. }
  206. function findBreakPoint(text, width, context) {
  207. var min = 0;
  208. var max = text.length - 1;
  209. while (min <= max) {
  210. var middle = Math.floor((min + max) / 2);
  211. var middleWidth = context.measureText(text.substr(0, middle)).width;
  212. var oneCharWiderThanMiddleWidth = context.measureText(text.substr(0, middle + 1)).width;
  213. if (middleWidth <= width && oneCharWiderThanMiddleWidth > width) {
  214. return middle;
  215. }
  216. if (middleWidth < width) {
  217. min = middle + 1;
  218. } else {
  219. max = middle - 1;
  220. }
  221. }
  222. return -1;
  223. }
  224. function breakLinesForCanvas(text, context, width, font) {
  225. var result = [];
  226. var breakPoint = 0;
  227. if (font) {
  228. context.font = font;
  229. }
  230. while ((breakPoint = findBreakPoint(text, width, context)) !== -1) {
  231. result.push(text.substr(0, breakPoint));
  232. text = text.substr(breakPoint);
  233. }
  234. if (text) {
  235. result.push(text);
  236. }
  237. return result;
  238. } //'使用很寻常的二分查找,如果某一个位置之前的文字宽度小于等于设定的宽度,并且它之后一个字之前的文字宽度大于设定的宽度,那么这个位置就是文本的换行点。上面只是找到一个换行点,对于输入的一段文本,需要循环查找,直到不存在这样的换行点为止, 完整的代码如下',
  239. //待改进:breakLinesForCanvas后的lineSpace稍小于预设的[]换行的lineSpace, 用于测量标签
  240. /*
  241. function wrapText(text, maxWidth) {
  242. // 创建一个 canvas 元素来测量文本宽度
  243. const canvas = document.createElement('canvas');
  244. const ctx = canvas.getContext('2d');
  245. // 设置字体样式
  246. ctx.font = '16px Arial';
  247. let currentLine = '';
  248. const lines = [];
  249. // 拆分文本为单词数组
  250. const words = text.split(' ');
  251. for (let i = 0; i < words.length; i++) {
  252. const word = words[i];
  253. const wordWidth = ctx.measureText(word).width;
  254. const currentLineWidth = ctx.measureText(currentLine).width;
  255. if (currentLineWidth + wordWidth < maxWidth) {
  256. // 如果当前行加上这个单词不会超出最大宽度,就将它添加到当前行
  257. currentLine += (currentLine ? ' ' : '') + word;
  258. } else {
  259. // 如果当前行加上这个单词会超出最大宽度,就将当前行添加到结果数组,并开始新的一行
  260. lines.push(currentLine.trim());
  261. currentLine = word;
  262. }
  263. }
  264. // 添加最后一行
  265. if (currentLine) {
  266. lines.push(currentLine.trim());
  267. }
  268. return lines;
  269. }
  270. */