@@ -83,6 +83,23 @@ class RadialAxialShadingPattern extends BaseShadingPattern {
8383 return this . _type === "radial" ;
8484 }
8585
86+ // Returns true when the smaller circle's center (p0 when r0 ≤ r1) lies
87+ // outside the larger circle. In that case the canvas radial gradient picks
88+ // t > 1 solutions for points inside the outer circle and maps them to the
89+ // transparent stop we append for extendEnd=false, making the gradient
90+ // invisible. A two-pass draw (reversed first, normal on top) fixes this
91+ // (see #20851).
92+ _isCircleCenterOutside ( ) {
93+ if ( ! this . isRadial ( ) || this . _r0 > this . _r1 ) {
94+ return false ;
95+ }
96+ const dist = Math . hypot (
97+ this . _p0 [ 0 ] - this . _p1 [ 0 ] ,
98+ this . _p0 [ 1 ] - this . _p1 [ 1 ]
99+ ) ;
100+ return dist > this . _r1 ;
101+ }
102+
86103 _createGradient ( ctx , transform = null ) {
87104 let grad ;
88105 let firstPoint = this . _p0 ;
@@ -125,6 +142,41 @@ class RadialAxialShadingPattern extends BaseShadingPattern {
125142 return grad ;
126143 }
127144
145+ _createReversedGradient ( ctx , transform = null ) {
146+ // Swapped circles: (p1, r1) → (p0, r0), with color stops reversed.
147+ let firstPoint = this . _p1 ;
148+ let secondPoint = this . _p0 ;
149+ if ( transform ) {
150+ firstPoint = firstPoint . slice ( ) ;
151+ secondPoint = secondPoint . slice ( ) ;
152+ Util . applyTransform ( firstPoint , transform ) ;
153+ Util . applyTransform ( secondPoint , transform ) ;
154+ }
155+ let r0 = this . _r1 ;
156+ let r1 = this . _r0 ;
157+ if ( transform ) {
158+ const scale = new Float32Array ( 2 ) ;
159+ Util . singularValueDecompose2dScale ( transform , scale ) ;
160+ r0 *= scale [ 0 ] ;
161+ r1 *= scale [ 0 ] ;
162+ }
163+ const grad = ctx . createRadialGradient (
164+ firstPoint [ 0 ] ,
165+ firstPoint [ 1 ] ,
166+ r0 ,
167+ secondPoint [ 0 ] ,
168+ secondPoint [ 1 ] ,
169+ r1
170+ ) ;
171+ const reversedStops = this . _colorStops
172+ . map ( ( [ t , c ] ) => [ 1 - t , c ] )
173+ . reverse ( ) ;
174+ for ( const [ t , c ] of reversedStops ) {
175+ grad . addColorStop ( t , c ) ;
176+ }
177+ return grad ;
178+ }
179+
128180 getPattern ( ctx , owner , inverse , pathType ) {
129181 let pattern ;
130182 if ( pathType === PathType . STROKE || pathType === PathType . FILL ) {
@@ -193,6 +245,10 @@ class RadialAxialShadingPattern extends BaseShadingPattern {
193245 }
194246 applyBoundingBox ( tmpCtx , this . _bbox ) ;
195247
248+ if ( this . _isCircleCenterOutside ( ) ) {
249+ tmpCtx . fillStyle = this . _createReversedGradient ( tmpCtx ) ;
250+ tmpCtx . fill ( ) ;
251+ }
196252 tmpCtx . fillStyle = this . _createGradient ( tmpCtx ) ;
197253 tmpCtx . fill ( ) ;
198254
@@ -203,6 +259,15 @@ class RadialAxialShadingPattern extends BaseShadingPattern {
203259 // Shading fills are applied relative to the current matrix which is also
204260 // how canvas gradients work, so there's no need to do anything special
205261 // here.
262+ if ( this . _isCircleCenterOutside ( ) ) {
263+ // Draw the reversed gradient first so the normal gradient can
264+ // correctly overlay it (see _isCircleCenterOutside for details).
265+ ctx . save ( ) ;
266+ applyBoundingBox ( ctx , this . _bbox ) ;
267+ ctx . fillStyle = this . _createReversedGradient ( ctx ) ;
268+ ctx . fillRect ( - 1e10 , - 1e10 , 2e10 , 2e10 ) ;
269+ ctx . restore ( ) ;
270+ }
206271 applyBoundingBox ( ctx , this . _bbox ) ;
207272 pattern = this . _createGradient ( ctx ) ;
208273 }
0 commit comments