11import { Color , LruCache , Vector } from "@graphif/data-structures" ;
22import md5 from "md5" ;
3- import { FONT , replaceTextWhenProtect } from "../../../../utils/font" ;
3+ import { FONT , getTextSize , replaceTextWhenProtect } from "../../../../utils/font" ;
44import { Project , service } from "../../../Project" ;
55import { Settings } from "../../../service/Settings" ;
66
@@ -15,98 +15,125 @@ export class TextRenderer {
1515
1616 constructor ( private readonly project : Project ) { }
1717
18- private hash ( text : string , fontSize : number , width : number ) : string {
19- // md5(text)_fontSize_width
18+ private hash ( text : string , size : number ) : string {
19+ // md5(text)_fontSize
2020 const textHash = md5 ( text ) ;
21- return `${ textHash } _${ fontSize } _ ${ width } ` ;
21+ return `${ textHash } _${ size } ` ;
2222 }
23- private getCache ( text : string , fontSize : number , width : number ) {
24- const cacheKey = this . hash ( text , fontSize , width ) ;
23+ private getCache ( text : string , size : number ) {
24+ const cacheKey = this . hash ( text , size ) ;
2525 const cacheValue = this . cache . get ( cacheKey ) ;
2626 return cacheValue ;
2727 }
2828 /**
29- * 获取text和width相同 ,fontSize最接近的缓存图片
29+ * 获取text相同 ,fontSize最接近的缓存图片
3030 */
31- /**
32- * 获取 text 和 width 相同,fontSize 最接近的缓存图片
33- */
34- private getCacheNearestSize ( text : string , fontSize : number , width : number ) : ImageBitmap | undefined {
35- let best : ImageBitmap | undefined = undefined ;
36- let minDelta = Infinity ;
37- for ( const key of this . cache . keys ( ) ) {
38- const parts = key . split ( "_" ) ;
39- if ( parts . length !== 3 ) continue ;
31+ private getCacheNearestSize ( text : string , size : number ) : ImageBitmap | undefined {
32+ const textHash = md5 ( text ) ;
33+ let nearestBitmap : ImageBitmap | undefined ;
34+ let minDiff = Infinity ;
4035
41- const [ textHash , cachedFontSizeStr , cachedWidthStr ] = parts ;
36+ // 遍历缓存中所有key
37+ for ( const key of this . cache . keys ( ) ) {
38+ // 解构出textHash和fontSize
39+ const [ cachedTextHash , cachedFontSizeStr ] = key . split ( "_" ) ;
4240 const cachedFontSize = Number ( cachedFontSizeStr ) ;
43- const cachedWidth = Number ( cachedWidthStr ) ;
4441
45- if ( textHash === md5 ( text ) && cachedWidth === width ) {
46- const delta = Math . abs ( cachedFontSize - fontSize ) ;
47- if ( delta < minDelta ) {
48- minDelta = delta ;
49- best = this . cache . get ( key ) ;
42+ // 只处理相同text的缓存
43+ if ( cachedTextHash === textHash ) {
44+ const diff = Math . abs ( cachedFontSize - size ) ;
45+ if ( diff < minDiff ) {
46+ minDiff = diff ;
47+ nearestBitmap = this . cache . get ( key ) ;
5048 }
5149 }
5250 }
5351
54- return best ;
52+ return nearestBitmap ;
53+ }
54+
55+ private buildCache ( text : string , size : number , color : Color ) {
56+ const textSize = getTextSize ( text , size ) ;
57+ const canvas = new OffscreenCanvas ( textSize . x , textSize . y ) ;
58+ const ctx = canvas . getContext ( "2d" ) ! ;
59+ ctx . textBaseline = "middle" ;
60+ ctx . textAlign = "left" ;
61+ ctx . font = `${ size } px normal ${ FONT } ` ;
62+ ctx . fillStyle = color . toString ( ) ;
63+ ctx . fillText ( text , 0 , size / 2 ) ;
64+ createImageBitmap ( canvas ) . then ( ( bmp ) => {
65+ const cacheKey = this . hash ( text , size ) ;
66+ this . cache . set ( cacheKey , bmp ) ;
67+ // console.log("[TextRenderer] 缓存已建立 %s", cacheKey);
68+ } ) ;
69+ return canvas ;
5570 }
5671
5772 /**
5873 * 从左上角画文本
59- * @param text
60- * @param location
61- * @param fontSize
62- * @param color
6374 */
64- renderOneLineText ( text : string , location : Vector , fontSize : number , color : Color = Color . White ) : void {
65- // alphabetic, top, hanging, middle, ideographic, bottom
75+ renderText ( text : string , location : Vector , size : number , color : Color = Color . White ) : void {
76+ if ( text . trim ( ) . length === 0 ) return ;
77+ text = Settings . sync . protectingPrivacy ? replaceTextWhenProtect ( text ) : text ;
78+ // 如果有缓存,直接渲染
79+ const cache = this . getCache ( text , size ) ;
80+ if ( cache ) {
81+ this . project . canvas . ctx . drawImage ( cache , location . x , location . y ) ;
82+ return ;
83+ }
84+ if ( Settings . sync . textScalingBehavior !== "cacheEveryTick" ) {
85+ // 如果摄像机正在缩放,就找到大小最接近的缓存图片,然后位图缩放
86+ const currentScale = this . project . camera . currentScale . toFixed ( 2 ) ;
87+ const targetScale = this . project . camera . targetScale . toFixed ( 2 ) ;
88+ if ( currentScale !== targetScale ) {
89+ if ( Settings . sync . textScalingBehavior === "nearestCache" ) {
90+ // 文字应该渲染成什么大小
91+ const textSize = getTextSize ( text , size ) ;
92+ const nearestBitmap = this . getCacheNearestSize ( text , size ) ;
93+ // console.log("[TextRenderer] 缩放状态下 (%f -> %f),使用缓存图片 %o", currentScale, targetScale, nearestBitmap);
94+ if ( nearestBitmap ) {
95+ this . project . canvas . ctx . drawImage (
96+ nearestBitmap ,
97+ location . x ,
98+ location . y ,
99+ Math . round ( textSize . x ) ,
100+ Math . round ( textSize . y ) ,
101+ ) ;
102+ return ;
103+ }
104+ } else if ( Settings . sync . textScalingBehavior === "temp" ) {
105+ this . renderTempText ( text , location , size , color ) ;
106+ return ;
107+ }
108+ }
109+ }
110+ this . project . canvas . ctx . drawImage ( this . buildCache ( text , size , color ) , location . x , location . y ) ;
111+ }
112+ /**
113+ * 渲染临时文字,不构建缓存,不使用缓存
114+ */
115+ renderTempText ( text : string , location : Vector , size : number , color : Color = Color . White ) : void {
116+ if ( text . trim ( ) . length === 0 ) return ;
66117 text = Settings . sync . protectingPrivacy ? replaceTextWhenProtect ( text ) : text ;
67118 this . project . canvas . ctx . textBaseline = "middle" ;
68119 this . project . canvas . ctx . textAlign = "left" ;
69- if ( Settings . sync . textIntegerLocationAndSizeRender ) {
70- this . project . canvas . ctx . font = `${ Math . round ( fontSize ) } px ${ FONT } ` ;
71- } else {
72- this . project . canvas . ctx . font = `${ fontSize } px normal ${ FONT } ` ;
73- }
120+ this . project . canvas . ctx . font = `${ size } px normal ${ FONT } ` ;
74121 this . project . canvas . ctx . fillStyle = color . toString ( ) ;
75- if ( Settings . sync . textIntegerLocationAndSizeRender ) {
76- this . project . canvas . ctx . fillText ( text , Math . floor ( location . x ) , Math . floor ( location . y + fontSize / 2 ) ) ;
77- } else {
78- this . project . canvas . ctx . fillText ( text , location . x , location . y + fontSize / 2 ) ;
79- }
122+ this . project . canvas . ctx . fillText ( text , location . x , location . y + size / 2 ) ;
80123 }
81124
82125 /**
83126 * 从中心位置开始绘制文本
84- * @param text
85- * @param centerLocation
86- * @param size
87- * @param color
88- * @param shadowColor
89127 */
90128 renderTextFromCenter ( text : string , centerLocation : Vector , size : number , color : Color = Color . White ) : void {
91- text = Settings . sync . protectingPrivacy ? replaceTextWhenProtect ( text ) : text ;
92- this . project . canvas . ctx . textBaseline = "middle" ;
93- this . project . canvas . ctx . textAlign = "center" ;
94- if ( Settings . sync . textIntegerLocationAndSizeRender ) {
95- this . project . canvas . ctx . font = `${ Math . round ( size ) } px normal ${ FONT } ` ;
96- } else {
97- this . project . canvas . ctx . font = `${ size } px normal ${ FONT } ` ;
98- }
99- this . project . canvas . ctx . fillStyle = color . toString ( ) ;
100- if ( Settings . sync . textIntegerLocationAndSizeRender ) {
101- this . project . canvas . ctx . fillText ( text , Math . floor ( centerLocation . x ) , Math . floor ( centerLocation . y ) ) ;
102- } else {
103- this . project . canvas . ctx . fillText ( text , centerLocation . x , centerLocation . y ) ;
104- }
105- // 重置阴影
106- this . project . canvas . ctx . shadowBlur = 0 ; // 阴影模糊程度
107- this . project . canvas . ctx . shadowOffsetX = 0 ; // 水平偏移
108- this . project . canvas . ctx . shadowOffsetY = 0 ; // 垂直偏移
109- this . project . canvas . ctx . shadowColor = "none" ;
129+ if ( text . trim ( ) . length === 0 ) return ;
130+ const textSize = getTextSize ( text , size ) ;
131+ this . renderText ( text , centerLocation . subtract ( textSize . divide ( 2 ) ) , size , color ) ;
132+ }
133+ renderTempTextFromCenter ( text : string , centerLocation : Vector , size : number , color : Color = Color . White ) : void {
134+ if ( text . trim ( ) . length === 0 ) return ;
135+ const textSize = getTextSize ( text , size ) ;
136+ this . renderTempText ( text , centerLocation . subtract ( textSize . divide ( 2 ) ) , size , color ) ;
110137 }
111138
112139 /**
@@ -126,6 +153,29 @@ export class TextRenderer {
126153 lineHeight : number = 1.2 ,
127154 limitLines : number = Infinity ,
128155 ) : void {
156+ if ( text . trim ( ) . length === 0 ) return ;
157+ let currentY = 0 ; // 顶部偏移量
158+ let textLineArray = this . textToTextArrayWrapCache ( text , fontSize , limitWidth ) ;
159+ // 限制行数
160+ if ( limitLines < textLineArray . length ) {
161+ textLineArray = textLineArray . slice ( 0 , limitLines ) ;
162+ textLineArray [ limitLines - 1 ] += "..." ; // 最后一行加省略号
163+ }
164+ for ( const line of textLineArray ) {
165+ this . renderText ( line , location . add ( new Vector ( 0 , currentY ) ) , fontSize , color ) ;
166+ currentY += fontSize * lineHeight ;
167+ }
168+ }
169+ renderTempMultiLineText (
170+ text : string ,
171+ location : Vector ,
172+ fontSize : number ,
173+ limitWidth : number ,
174+ color : Color = Color . White ,
175+ lineHeight : number = 1.2 ,
176+ limitLines : number = Infinity ,
177+ ) : void {
178+ if ( text . trim ( ) . length === 0 ) return ;
129179 text = Settings . sync . protectingPrivacy ? replaceTextWhenProtect ( text ) : text ;
130180 let currentY = 0 ; // 顶部偏移量
131181 let textLineArray = this . textToTextArrayWrapCache ( text , fontSize , limitWidth ) ;
@@ -135,7 +185,7 @@ export class TextRenderer {
135185 textLineArray [ limitLines - 1 ] += "..." ; // 最后一行加省略号
136186 }
137187 for ( const line of textLineArray ) {
138- this . renderOneLineText ( line , location . add ( new Vector ( 0 , currentY ) ) , fontSize , color ) ;
188+ this . renderTempText ( line , location . add ( new Vector ( 0 , currentY ) ) , fontSize , color ) ;
139189 currentY += fontSize * lineHeight ;
140190 }
141191 }
@@ -149,6 +199,7 @@ export class TextRenderer {
149199 lineHeight : number = 1.2 ,
150200 limitLines : number = Infinity ,
151201 ) : void {
202+ if ( text . trim ( ) . length === 0 ) return ;
152203 text = Settings . sync . protectingPrivacy ? replaceTextWhenProtect ( text ) : text ;
153204 let currentY = 0 ; // 顶部偏移量
154205 let textLineArray = this . textToTextArrayWrapCache ( text , size , limitWidth ) ;
@@ -167,6 +218,34 @@ export class TextRenderer {
167218 currentY += size * lineHeight ;
168219 }
169220 }
221+ renderTempMultiLineTextFromCenter (
222+ text : string ,
223+ centerLocation : Vector ,
224+ size : number ,
225+ limitWidth : number ,
226+ color : Color ,
227+ lineHeight : number = 1.2 ,
228+ limitLines : number = Infinity ,
229+ ) : void {
230+ if ( text . trim ( ) . length === 0 ) return ;
231+ text = Settings . sync . protectingPrivacy ? replaceTextWhenProtect ( text ) : text ;
232+ let currentY = 0 ; // 顶部偏移量
233+ let textLineArray = this . textToTextArrayWrapCache ( text , size , limitWidth ) ;
234+ // 限制行数
235+ if ( limitLines < textLineArray . length ) {
236+ textLineArray = textLineArray . slice ( 0 , limitLines ) ;
237+ textLineArray [ limitLines - 1 ] += "..." ; // 最后一行加省略号
238+ }
239+ for ( const line of textLineArray ) {
240+ this . renderTempTextFromCenter (
241+ line ,
242+ centerLocation . add ( new Vector ( 0 , currentY - ( ( textLineArray . length - 1 ) * size ) / 2 ) ) ,
243+ size ,
244+ color ,
245+ ) ;
246+ currentY += size * lineHeight ;
247+ }
248+ }
170249
171250 textArrayCache : LruCache < string , string [ ] > = new LruCache ( 100 ) ;
172251
@@ -195,7 +274,7 @@ export class TextRenderer {
195274 private textToTextArray ( text : string , fontSize : number , limitWidth : number ) : string [ ] {
196275 let currentLine = "" ;
197276 // 先渲染一下空字符串,否则长度大小可能不匹配,造成蜜汁bug
198- this . renderOneLineText ( "" , Vector . getZero ( ) , fontSize , Color . White ) ;
277+ this . renderText ( "" , Vector . getZero ( ) , fontSize , Color . White ) ;
199278 const lines : string [ ] = [ ] ;
200279
201280 for ( const char of text ) {
0 commit comments