2
2
parseRadialGradient ,
3
3
RadialResult ,
4
4
RadialPropertyValue ,
5
+ ColorStop ,
5
6
} from 'css-gradient-parser'
6
7
import { buildXMLString , lengthToNumber } from '../../utils.js'
7
8
import { normalizeStops } from './utils.js'
@@ -31,6 +32,7 @@ export function buildRadialGradient(
31
32
stops : colorStops ,
32
33
position,
33
34
size,
35
+ repeating,
34
36
} = parseRadialGradient ( image )
35
37
const [ xDelta , yDelta ] = dimensions
36
38
@@ -48,7 +50,20 @@ export function buildRadialGradient(
48
50
cx = pos . x
49
51
cy = pos . y
50
52
51
- const stops = normalizeStops ( width , colorStops , inheritableStyle , false , from )
53
+ const colorStopTotalLength = calcColorStopTotalLength (
54
+ width ,
55
+ colorStops ,
56
+ repeating ,
57
+ inheritableStyle
58
+ )
59
+
60
+ const stops = normalizeStops (
61
+ colorStopTotalLength ,
62
+ colorStops ,
63
+ inheritableStyle ,
64
+ repeating ,
65
+ from
66
+ )
52
67
53
68
const gradientId = `satori_radial_${ id } `
54
69
const patternId = `satori_pattern_${ id } `
@@ -61,7 +76,18 @@ export function buildRadialGradient(
61
76
inheritableStyle . fontSize as number ,
62
77
{ x : cx , y : cy } ,
63
78
[ xDelta , yDelta ] ,
64
- inheritableStyle
79
+ inheritableStyle ,
80
+ repeating
81
+ )
82
+
83
+ const props = calcRadialGradientProps (
84
+ shape as Shape ,
85
+ inheritableStyle . fontSize as number ,
86
+ colorStops ,
87
+ [ xDelta , yDelta ] ,
88
+ inheritableStyle ,
89
+ repeating ,
90
+ spread
65
91
)
66
92
67
93
// TODO: check for repeat-x/repeat-y
@@ -79,6 +105,7 @@ export function buildRadialGradient(
79
105
'radialGradient' ,
80
106
{
81
107
id : gradientId ,
108
+ ...props ,
82
109
} ,
83
110
stops
84
111
. map ( ( stop ) =>
@@ -124,13 +151,28 @@ export function buildRadialGradient(
124
151
return result
125
152
}
126
153
127
- interface Position {
128
- type : 'keyword' | 'length'
129
- value : string
130
- }
131
-
132
154
type PositionKeyWord = 'center' | 'left' | 'right' | 'top' | 'bottom'
133
155
156
+ function calcColorStopTotalLength (
157
+ width : number ,
158
+ stops : ColorStop [ ] ,
159
+ repeating : boolean ,
160
+ inheritableStyle : Record < string , string | number >
161
+ ) {
162
+ if ( ! repeating ) return width
163
+ const lastStop = stops . at ( - 1 )
164
+ if ( ! lastStop || ! lastStop . offset || lastStop . offset . unit === '%' )
165
+ return width
166
+
167
+ return lengthToNumber (
168
+ `${ lastStop . offset . value } ${ lastStop . offset . unit } ` ,
169
+ + inheritableStyle . fontSize ,
170
+ width ,
171
+ inheritableStyle ,
172
+ true
173
+ )
174
+ }
175
+
134
176
function calcRadialGradient (
135
177
cx : RadialPropertyValue ,
136
178
cy : RadialPropertyValue ,
@@ -196,13 +238,53 @@ function calcPos(
196
238
}
197
239
198
240
type Shape = 'circle' | 'ellipse'
241
+
242
+ function calcRadialGradientProps (
243
+ shape : Shape ,
244
+ baseFontSize : number ,
245
+ colorStops : ColorStop [ ] ,
246
+ [ xDelta , yDelta ] : [ number , number ] ,
247
+ inheritableStyle : Record < string , string | number > ,
248
+ repeating : boolean ,
249
+ spread : Record < string , number >
250
+ ) {
251
+ const { r, rx, ratio = 1 } = spread
252
+ if ( ! repeating ) {
253
+ return {
254
+ spreadMethod : 'pad' ,
255
+ }
256
+ }
257
+ const last = colorStops . at ( - 1 )
258
+ const radius = shape === 'circle' ? r * 2 : rx * 2
259
+ return {
260
+ spreadMethod : 'repeat' ,
261
+ cx : '50%' ,
262
+ cy : '50%' ,
263
+ r :
264
+ last . offset . unit === '%'
265
+ ? `${
266
+ ( Number ( last . offset . value ) * Math . min ( yDelta / xDelta , 1 ) ) / ratio
267
+ } %`
268
+ : Number (
269
+ lengthToNumber (
270
+ `${ last . offset . value } ${ last . offset . unit } ` ,
271
+ baseFontSize ,
272
+ xDelta ,
273
+ inheritableStyle ,
274
+ true
275
+ ) / radius
276
+ ) ,
277
+ }
278
+ }
279
+
199
280
function calcRadius (
200
281
shape : Shape ,
201
282
endingShape : RadialResult [ 'size' ] ,
202
283
baseFontSize : number ,
203
284
centerAxis : { x : number ; y : number } ,
204
285
length : [ number , number ] ,
205
- inheritableStyle : Record < string , string | number >
286
+ inheritableStyle : Record < string , string | number > ,
287
+ repeating : boolean
206
288
) {
207
289
const [ xDelta , yDelta ] = length
208
290
const { x : cx , y : cy } = centerAxis
@@ -217,7 +299,7 @@ function calcRadius(
217
299
)
218
300
}
219
301
if ( shape === 'circle' ) {
220
- return {
302
+ Object . assign ( spread , {
221
303
r : Number (
222
304
lengthToNumber (
223
305
`${ endingShape [ 0 ] . value . value } ${ endingShape [ 0 ] . value . unit } ` ,
@@ -227,9 +309,9 @@ function calcRadius(
227
309
true
228
310
)
229
311
) ,
230
- }
312
+ } )
231
313
} else {
232
- return {
314
+ Object . assign ( spread , {
233
315
rx : Number (
234
316
lengthToNumber (
235
317
`${ endingShape [ 0 ] . value . value } ${ endingShape [ 0 ] . value . unit } ` ,
@@ -248,8 +330,10 @@ function calcRadius(
248
330
true
249
331
)
250
332
) ,
251
- }
333
+ } )
252
334
}
335
+ patchSpread ( spread , xDelta , yDelta , cx , cy , repeating , shape )
336
+ return spread
253
337
}
254
338
255
339
switch ( endingShape [ 0 ] . value ) {
@@ -273,6 +357,7 @@ function calcRadius(
273
357
spread . rx = Math . max ( Math . abs ( xDelta - cx ) , Math . abs ( cx ) )
274
358
spread . ry = Math . max ( Math . abs ( yDelta - cy ) , Math . abs ( cy ) )
275
359
}
360
+ patchSpread ( spread , xDelta , yDelta , cx , cy , repeating , shape )
276
361
return spread
277
362
case 'closest-side' :
278
363
if ( shape === 'circle' ) {
@@ -286,31 +371,79 @@ function calcRadius(
286
371
spread . rx = Math . min ( Math . abs ( xDelta - cx ) , Math . abs ( cx ) )
287
372
spread . ry = Math . min ( Math . abs ( yDelta - cy ) , Math . abs ( cy ) )
288
373
}
374
+ patchSpread ( spread , xDelta , yDelta , cx , cy , repeating , shape )
289
375
290
376
return spread
291
377
}
292
378
if ( shape === 'circle' ) {
293
379
spread . r = Math . sqrt ( fx * fx + fy * fy )
294
380
} else {
295
- // Spec: https://drafts.csswg.org/css-images/#typedef-size
296
- // Get the aspect ratio of the closest-side size.
297
- const ratio = fy !== 0 ? fx / fy : 1
381
+ Object . assign ( spread , f2r ( fx , fy ) )
382
+ }
298
383
299
- if ( fx === 0 ) {
300
- spread . rx = 0
301
- spread . ry = 0
384
+ patchSpread ( spread , xDelta , yDelta , cx , cy , repeating , shape )
385
+
386
+ return spread
387
+ }
388
+
389
+ // compare with farthest-corner to make it cover the whole container
390
+ function patchSpread (
391
+ spread : Record < string , number > ,
392
+ xDelta : number ,
393
+ yDelta : number ,
394
+ cx : number ,
395
+ cy : number ,
396
+ repeating : boolean ,
397
+ shape : Shape
398
+ ) {
399
+ if ( repeating ) {
400
+ if ( shape === 'ellipse' ) {
401
+ const mfx = Math . max ( Math . abs ( xDelta - cx ) , Math . abs ( cx ) )
402
+ const mfy = Math . max ( Math . abs ( yDelta - cy ) , Math . abs ( cy ) )
403
+
404
+ const { rx : mrx , ry : mry } = f2r ( mfx , mfy )
405
+
406
+ spread . ratio = Math . max ( mrx / spread . rx , mry / spread . ry )
407
+ if ( spread . ratio > 1 ) {
408
+ spread . rx *= spread . ratio
409
+ spread . ry *= spread . ratio
410
+ }
302
411
} else {
303
- // fx^2/a^2 + fy^2/b^2 = 1
304
- // fx^2/(b*ratio)^2 + fy^2/b^2 = 1
305
- // (fx^2+fy^2*ratio^2) = (b*ratio)^2
306
- // b = sqrt(fx^2+fy^2*ratio^2)/ratio
412
+ const mfx = Math . max ( Math . abs ( xDelta - cx ) , Math . abs ( cx ) )
413
+ const mfy = Math . max ( Math . abs ( yDelta - cy ) , Math . abs ( cy ) )
414
+
415
+ const mr = Math . sqrt ( mfx * mfx + mfy * mfy )
307
416
308
- spread . ry = Math . sqrt ( fx * fx + fy * fy * ratio * ratio ) / ratio
309
- spread . rx = spread . ry * ratio
417
+ spread . ratio = mr / spread . r
418
+ if ( spread . ratio > 1 ) {
419
+ spread . r = mr
420
+ }
310
421
}
311
422
}
423
+ }
312
424
313
- return spread
425
+ function f2r ( fx : number , fy : number ) {
426
+ // Spec: https://drafts.csswg.org/css-images/#typedef-size
427
+ // Get the aspect ratio of the closest-side size.
428
+ const ratio = fy !== 0 ? fx / fy : 1
429
+
430
+ if ( fx === 0 ) {
431
+ return {
432
+ rx : 0 ,
433
+ ry : 0 ,
434
+ }
435
+ } else {
436
+ // fx^2/a^2 + fy^2/b^2 = 1
437
+ // fx^2/(b*ratio)^2 + fy^2/b^2 = 1
438
+ // (fx^2+fy^2*ratio^2) = (b*ratio)^2
439
+ // b = sqrt(fx^2+fy^2*ratio^2)/ratio
440
+
441
+ const ry = Math . sqrt ( fx * fx + fy * fy * ratio * ratio ) / ratio
442
+ return {
443
+ ry,
444
+ rx : ry * ratio ,
445
+ }
446
+ }
314
447
}
315
448
316
449
function isSizeAllLength ( v : RadialPropertyValue [ ] ) : v is Array < {
0 commit comments