@@ -28,6 +28,13 @@ function vtkTextActor(publicAPI, model) {
2828 yResolution : 1 ,
2929 } ) ;
3030
31+ function normalizeLineBreaks ( text ) {
32+ return text
33+ . replace ( / \\ r \\ n / g, '\n' )
34+ . replace ( / \\ n / g, '\n' )
35+ . replace ( / \\ r / g, '\r' ) ;
36+ }
37+
3138 function createImageData ( text ) {
3239 const fontSizeScale = publicAPI . getProperty ( ) . getFontSizeScale ( ) ;
3340 const fontStyle = publicAPI . getProperty ( ) . getFontStyle ( ) ;
@@ -43,27 +50,45 @@ function vtkTextActor(publicAPI, model) {
4350 const ctx = canvas . getContext ( '2d' , { willReadFrequently : true } ) ;
4451
4552 // Set the text properties to measure
46- const textSize = fontSizeScale ( resolution ) * dpr ;
53+ const normalizedText = normalizeLineBreaks ( text ) ;
54+ const textSize = fontSizeScale ( resolution ) ;
55+ const lines = normalizedText . split ( / \r \n | \r | \n / ) ;
4756
4857 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 ;
58+ ctx . textBaseline = 'alphabetic' ;
59+ ctx . textAlign = 'left' ;
60+
61+ const lineMetrics = lines . map ( ( line ) => {
62+ const metrics = ctx . measureText ( line ) ;
63+ return {
64+ left : metrics . actualBoundingBoxLeft || 0 ,
65+ right : metrics . actualBoundingBoxRight || metrics . width || 0 ,
66+ actualAscent : metrics . actualBoundingBoxAscent || 0 ,
67+ actualDescent : metrics . actualBoundingBoxDescent || 0 ,
68+ } ;
69+ } ) ;
70+ const maxActualAscent = lineMetrics . reduce (
71+ ( value , metrics ) => Math . max ( value , metrics . actualAscent ) ,
72+ 0
73+ ) ;
74+ const maxActualDescent = lineMetrics . reduce (
75+ ( value , metrics ) => Math . max ( value , metrics . actualDescent ) ,
76+ 0
77+ ) ;
78+ const maxLeft = lineMetrics . reduce (
79+ ( value , metrics ) => Math . max ( value , metrics . left ) ,
80+ 0
81+ ) ;
82+ const maxRight = lineMetrics . reduce (
83+ ( value , metrics ) => Math . max ( value , metrics . right ) ,
84+ 0
85+ ) ;
86+ const lineAscent = maxActualAscent ;
87+ const lineDescent = maxActualDescent ;
88+ const baselineOffset = lineAscent ;
89+ const lineHeight = Math . max ( lineAscent + lineDescent , 1 ) ;
90+ const textWidth = maxLeft + maxRight ;
91+ const textHeight = Math . max ( lines . length * lineHeight , lineHeight ) ;
6792
6893 // Update canvas size to fit text and ensure it is at least 1x1 pixel
6994 const width = Math . max ( Math . round ( textWidth * dpr ) , 1 ) ;
@@ -72,9 +97,7 @@ function vtkTextActor(publicAPI, model) {
7297 canvas . width = width ;
7398 canvas . height = height ;
7499
75- // Vertical flip
76- ctx . translate ( 0 , height ) ;
77- ctx . scale ( 1 , - 1 ) ;
100+ ctx . setTransform ( dpr , 0 , 0 , - dpr , 0 , height ) ;
78101
79102 // Clear the canvas
80103 ctx . clearRect ( 0 , 0 , width , height ) ;
@@ -89,8 +112,8 @@ function vtkTextActor(publicAPI, model) {
89112 ctx . imageSmoothingQuality = 'high' ;
90113 ctx . font = `${ fontStyle } ${ textSize } px "${ fontFamily } "` ;
91114 ctx . fillStyle = vtkMath . floatRGB2HexCode ( fontColor ) ;
92- ctx . textBaseline = 'middle ' ;
93- ctx . textAlign = 'center ' ;
115+ ctx . textBaseline = 'alphabetic ' ;
116+ ctx . textAlign = 'left ' ;
94117
95118 // Set shadow
96119 if ( shadowColor ) {
@@ -100,8 +123,12 @@ function vtkTextActor(publicAPI, model) {
100123 ctx . shadowBlur = shadowBlur ;
101124 }
102125
103- // Draw the text
104- ctx . fillText ( text , width / 2 + hAdjustment , height / 2 + vAdjustment ) ;
126+ const x = maxLeft ;
127+ const baseline = baselineOffset ;
128+ const lineHeightPx = lineHeight ;
129+ lines . forEach ( ( line , index ) => {
130+ ctx . fillText ( line , x , baseline + index * lineHeightPx ) ;
131+ } ) ;
105132
106133 // Update plane dimensions to match text size
107134 plane . set ( {
0 commit comments