@@ -28,6 +28,18 @@ function vtkTextActor(publicAPI, model) {
2828 yResolution : 1 ,
2929 } ) ;
3030
31+ /**
32+ * Normalize escaped and platform specific line endings to `\n`.
33+ */
34+ function normalizeLineBreaks ( text ) {
35+ return text
36+ . replace ( / \\ r \\ n / g, '\n' )
37+ . replace ( / \\ n / g, '\n' )
38+ . replace ( / \\ r / g, '\n' )
39+ . replace ( / \r \n / g, '\n' )
40+ . replace ( / \r / g, '\n' ) ;
41+ }
42+
3143 function createImageData ( text ) {
3244 const fontSizeScale = publicAPI . getProperty ( ) . getFontSizeScale ( ) ;
3345 const fontStyle = publicAPI . getProperty ( ) . getFontStyle ( ) ;
@@ -43,27 +55,45 @@ function vtkTextActor(publicAPI, model) {
4355 const ctx = canvas . getContext ( '2d' , { willReadFrequently : true } ) ;
4456
4557 // Set the text properties to measure
46- const textSize = fontSizeScale ( resolution ) * dpr ;
58+ const normalizedText = normalizeLineBreaks ( text ) ;
59+ const textSize = fontSizeScale ( resolution ) ;
60+ const lines = normalizedText . split ( / \r \n | \r | \n / ) ;
4761
4862 ctx . font = `${ fontStyle } ${ textSize } px "${ fontFamily } "` ;
49- ctx . textBaseline = 'middle' ;
50- ctx . textAlign = 'center' ;
51-
52- // Measure the text
53- const metrics = ctx . measureText ( text ) ;
54- const textWidth = metrics . width / dpr ;
55-
56- const {
57- actualBoundingBoxLeft,
58- actualBoundingBoxRight,
59- actualBoundingBoxAscent,
60- actualBoundingBoxDescent,
61- } = metrics ;
62- const hAdjustment = ( actualBoundingBoxLeft - actualBoundingBoxRight ) / 2 ;
63- const vAdjustment =
64- ( actualBoundingBoxAscent - actualBoundingBoxDescent ) / 2 ;
65-
66- const textHeight = textSize / dpr - vAdjustment ;
63+ ctx . textBaseline = 'alphabetic' ;
64+ ctx . textAlign = 'left' ;
65+
66+ const lineMetrics = lines . map ( ( line ) => {
67+ const metrics = ctx . measureText ( line ) ;
68+ return {
69+ left : metrics . actualBoundingBoxLeft || 0 ,
70+ right : metrics . actualBoundingBoxRight || metrics . width || 0 ,
71+ actualAscent : metrics . actualBoundingBoxAscent || 0 ,
72+ actualDescent : metrics . actualBoundingBoxDescent || 0 ,
73+ } ;
74+ } ) ;
75+ const maxActualAscent = lineMetrics . reduce (
76+ ( value , metrics ) => Math . max ( value , metrics . actualAscent ) ,
77+ 0
78+ ) ;
79+ const maxActualDescent = lineMetrics . reduce (
80+ ( value , metrics ) => Math . max ( value , metrics . actualDescent ) ,
81+ 0
82+ ) ;
83+ const maxLeft = lineMetrics . reduce (
84+ ( value , metrics ) => Math . max ( value , metrics . left ) ,
85+ 0
86+ ) ;
87+ const maxRight = lineMetrics . reduce (
88+ ( value , metrics ) => Math . max ( value , metrics . right ) ,
89+ 0
90+ ) ;
91+ const lineAscent = maxActualAscent ;
92+ const lineDescent = maxActualDescent ;
93+ const baselineOffset = lineAscent ;
94+ const lineHeight = Math . max ( lineAscent + lineDescent , 1 ) ;
95+ const textWidth = maxLeft + maxRight ;
96+ const textHeight = Math . max ( lines . length * lineHeight , lineHeight ) ;
6797
6898 // Update canvas size to fit text and ensure it is at least 1x1 pixel
6999 const width = Math . max ( Math . round ( textWidth * dpr ) , 1 ) ;
@@ -72,9 +102,7 @@ function vtkTextActor(publicAPI, model) {
72102 canvas . width = width ;
73103 canvas . height = height ;
74104
75- // Vertical flip
76- ctx . translate ( 0 , height ) ;
77- ctx . scale ( 1 , - 1 ) ;
105+ ctx . setTransform ( dpr , 0 , 0 , - dpr , 0 , height ) ;
78106
79107 // Clear the canvas
80108 ctx . clearRect ( 0 , 0 , width , height ) ;
@@ -89,8 +117,8 @@ function vtkTextActor(publicAPI, model) {
89117 ctx . imageSmoothingQuality = 'high' ;
90118 ctx . font = `${ fontStyle } ${ textSize } px "${ fontFamily } "` ;
91119 ctx . fillStyle = vtkMath . floatRGB2HexCode ( fontColor ) ;
92- ctx . textBaseline = 'middle ' ;
93- ctx . textAlign = 'center ' ;
120+ ctx . textBaseline = 'alphabetic ' ;
121+ ctx . textAlign = 'left ' ;
94122
95123 // Set shadow
96124 if ( shadowColor ) {
@@ -100,8 +128,12 @@ function vtkTextActor(publicAPI, model) {
100128 ctx . shadowBlur = shadowBlur ;
101129 }
102130
103- // Draw the text
104- ctx . fillText ( text , width / 2 + hAdjustment , height / 2 + vAdjustment ) ;
131+ const x = maxLeft ;
132+ const baseline = baselineOffset ;
133+ const lineHeightPx = lineHeight ;
134+ lines . forEach ( ( line , index ) => {
135+ ctx . fillText ( line , x , baseline + index * lineHeightPx ) ;
136+ } ) ;
105137
106138 // Update plane dimensions to match text size
107139 plane . set ( {
0 commit comments