Skip to content

Commit 0e7afe2

Browse files
committed
feat: webkit-text-fill-color
1 parent eab60ee commit 0e7afe2

7 files changed

Lines changed: 81 additions & 9 deletions

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,12 @@ Satori uses the same Flexbox [layout engine](https://yogalayout.com) as React Na
321321
<td>Supported</td>
322322
<td></td>
323323
</tr>
324+
<tr>
325+
<td><code>WebkitTextFillColor</code></td>
326+
<td><code>WebkitTextFillColor</code></td>
327+
<td>Supported</td>
328+
<td></td>
329+
</tr>
324330

325331
</tbody>
326332
</table>

src/builder/text.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ export default function buildText(
147147
'text',
148148
{
149149
...shapeProps,
150-
fill: style.color,
150+
fill: style.WebkitTextFillColor || style.color,
151151
opacity: opacity !== 1 ? opacity : undefined,
152152
},
153153
escapeHTML(content)

src/handler/expand.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,10 @@ function handleSpecialCase(
219219
}
220220
}
221221

222+
if (name === 'WebkitTextFillColor') {
223+
return { WebkitTextFillColor: value.toString().trim() }
224+
}
225+
222226
if (name === 'textDecorationSkipInk') {
223227
const normalized = value.toString().trim().toLowerCase()
224228
if (!['auto', 'none', 'all'].includes(normalized)) {
@@ -303,6 +307,7 @@ type MainStyle = {
303307
textShadowRadius: number[]
304308
WebkitTextStrokeWidth: number
305309
WebkitTextStrokeColor: string
310+
WebkitTextFillColor: string
306311
textDecorationSkipInk: 'auto' | 'none' | 'all'
307312
}
308313

src/text/index.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,17 @@ export default async function* buildTextNodes(
7474
letterSpacing,
7575
_inheritedBackgroundClipTextPath,
7676
_inheritedBackgroundClipTextHasBackground,
77+
WebkitTextFillColor,
7778
flexShrink,
7879
} = parentStyle
7980

81+
const textFillColor = WebkitTextFillColor as string | undefined
82+
const isTransparentText = textFillColor
83+
? isFullyTransparent(textFillColor)
84+
: isFullyTransparent(parentStyle.color) ||
85+
(!!_inheritedBackgroundClipTextHasBackground &&
86+
isOpaqueWhite(parentStyle.color))
87+
8088
const {
8189
words,
8290
requiredBreaks,
@@ -517,9 +525,7 @@ export default async function* buildTextNodes(
517525
shadowOffset: textShadowOffset,
518526
shadowRadius: textShadowRadius,
519527
},
520-
isFullyTransparent(parentStyle.color) ||
521-
(_inheritedBackgroundClipTextHasBackground &&
522-
isOpaqueWhite(parentStyle.color))
528+
isTransparentText
523529
)
524530

525531
filter = buildXMLString('defs', {}, filter)
@@ -880,12 +886,9 @@ export default async function* buildTextNodes(
880886
}>` +
881887
buildXMLString('path', {
882888
fill:
883-
filter &&
884-
(isFullyTransparent(parentStyle.color) ||
885-
(_inheritedBackgroundClipTextHasBackground &&
886-
isOpaqueWhite(parentStyle.color)))
889+
filter && isTransparentText
887890
? 'black'
888-
: parentStyle.color,
891+
: textFillColor || parentStyle.color,
889892
d: mergedPath,
890893
transform: matrix ? matrix : undefined,
891894
opacity: opacity !== 1 ? opacity : undefined,
Loading
Loading

test/background-clip.test.tsx

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,64 @@ describe('backgroundClip', () => {
103103
expect(toImage(svg)).toMatchImageSnapshot()
104104
})
105105

106+
it('should render WebkitTextFillColor as white over gradient', async () => {
107+
const svg = await satori(
108+
<div
109+
style={{
110+
display: 'flex',
111+
alignItems: 'center',
112+
justifyContent: 'center',
113+
width: '100%',
114+
height: '100%',
115+
fontSize: 32,
116+
fontWeight: 600,
117+
background:
118+
'radial-gradient(61.04% 180% at 55.38% 0%, #55f114 6.77%, #02ff44 26.56%, #14f13f 40.1%, #02D7FF 67.71%, #1496f3 67.72%, #02D7FF 100%)',
119+
backgroundClip: 'text',
120+
WebkitTextFillColor: '#ffffff',
121+
color: '#ffffff',
122+
textShadow:
123+
'0px 0px 2px #1677f1,0px 0px 3px #1677f1,0px 0px 4px #1677f1,0px 0px 5px #1677f1,0px 0px 5px #1677f1',
124+
}}
125+
>
126+
Hello
127+
</div>,
128+
{
129+
width: 200,
130+
height: 50,
131+
fonts,
132+
}
133+
)
134+
expect(toImage(svg, 200)).toMatchImageSnapshot()
135+
})
136+
137+
it('should render WebkitTextFillColor transparent as gradient text', async () => {
138+
const svg = await satori(
139+
<div
140+
style={{
141+
display: 'flex',
142+
alignItems: 'center',
143+
justifyContent: 'center',
144+
width: '100%',
145+
height: '100%',
146+
fontSize: 32,
147+
background: 'linear-gradient(90deg, #ff00bc, #6400ff)',
148+
backgroundClip: 'text',
149+
WebkitTextFillColor: 'transparent',
150+
color: '#ffffff',
151+
}}
152+
>
153+
Hello
154+
</div>,
155+
{
156+
width: 200,
157+
height: 50,
158+
fonts,
159+
}
160+
)
161+
expect(toImage(svg, 200)).toMatchImageSnapshot()
162+
})
163+
106164
it('should preserve color', async () => {
107165
const svg = await satori(
108166
<div

0 commit comments

Comments
 (0)