Skip to content

Commit

Permalink
fix: clipping behavior of children with transform (#635)
Browse files Browse the repository at this point in the history
  • Loading branch information
jozsefsallai committed Sep 17, 2024
1 parent ff80448 commit c55e4da
Show file tree
Hide file tree
Showing 9 changed files with 147 additions and 9 deletions.
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,8 +279,7 @@ Note:
2. There is no `z-index` support in SVG. Elements that come later in the document will be painted on top.
3. `box-sizing` is set to `border-box` for all elements.
4. `calc` isn't supported.
5. `overflow: hidden` and `transform` can't be used together.
6. `currentcolor` support is only available for the `color` property.
5. `currentcolor` support is only available for the `color` property.

### Language and Typography

Expand Down Expand Up @@ -346,7 +345,7 @@ await satori(
)
```

Same characters can be rendered differently in different locales, you can specify the locale when necessary to force it to render with a specific font and locale. Check out [this example](https://og-playground.vercel.app/?share=nVLdSsMwFH6VcEC86VgdXoyweTMVpyiCA296kzWnbWaalCZ160rfwAcRH8Bn0rcwWVdQEYTdnJzz_ZyEnNNArDkChQkXz5EixNha4rRpfE4IF6aQrKbkOJG4OQ461OfnosTYCq0cF2tZ5apnMxRpZh18EoZHPbgW3Ga_sIJxLlS6Q4sNGbnQU0yKVM0t5sa3R2Wx7KlVZaxI6pl2oPLX_KQTh1-yXEj_6LlnAhLBLXOJYJLMY61MBN_VD2KLlIzGe2jJ4qe01JXiMy116bqsM2Gxc7Stj2edcmIKpohkKp1GsGKD6_sI9hQhn2-vHy_ve-HQK_9ybbPB7O4Q1-LxENfVzX-uydDtgTshAF348RqgDeymB3QchgF04wV66guOyyoFmjBpMADM9Uos6sLvk13vKtfH__FFvkQO1JYVtu0X) to learn more.
Same characters can be rendered differently in different locales, you can specify the locale when necessary to force it to render with a specific font and locale. Check out [this example](https://og-playground.vercel.app/?share=nVLdSsMwFH6VcEC86VgdXoyweTMVpyiCA296kzWnbWaalCZ160rfwAcRH8Bn0rcwWVdQEYTdnJzz_ZyEnNNArDkChQkXz5EixNha4rRpfE4IF6aQrKbkOJG4OQ461OfnosTYCq0cF2tZ5apnMxRpZh18EoZHPbgW3Ga_sIJxLlS6Q4sNGbnQU0yKVM0t5sa3R2Wx7KlVZaxI6pl2oPLX_KQTh1-yXEj_6LlnAhLBLXOJYJLMY61MBN_VD2KLlIzGe2jJ4qe01JXiMy116bqsM2Gxc7Stj2edcmIKpohkKp1GsGKD6_sI9hQhn2-vHy_ve-HQK_9ybbPB7O4Q1-LxENfVzX-uydDtgTshAF348RqgDeymB3QchgF04wV66guOyyoFmjBpMADM9Uos6sLvk13vKtfH__FFvkQO1JYVtu0X) to learn more.

Supported locales are exported as the `Locale` enum type.

Expand Down
40 changes: 39 additions & 1 deletion src/builder/border-radius.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// TODO: Support the `border-radius: 10px / 20px` syntax.
// https://developer.mozilla.org/en-US/docs/Web/CSS/border-radius

import { lengthToNumber } from '../utils.js'
import { buildXMLString, lengthToNumber } from '../utils.js'

// Getting the intersection of a 45deg ray with the elliptical arc x^2/rx^2 + y^2/ry^2 = 1.
// Reference:
Expand Down Expand Up @@ -66,6 +66,44 @@ function resolveRadius(
const radiusZeroOrNull = (_radius?: [number, number]) =>
_radius && _radius[0] !== 0 && _radius[1] !== 0

export function getBorderRadiusClipPath(
{
id,
borderRadiusPath,
borderType,
left,
top,
width,
height,
}: {
id: string
borderRadiusPath?: string
borderType?: 'rect' | 'path'
left: number
top: number
width: number
height: number
},
style: Record<string, number | string>
) {
const rectClipId = `satori_brc-${id}`
const defs = buildXMLString(
'clipPath',
{
id: rectClipId,
},
buildXMLString(borderType, {
x: left,
y: top,
width,
height,
d: borderRadiusPath ? borderRadiusPath : undefined,
})
)

return [defs, rectClipId]
}

export default function radius(
{
left,
Expand Down
7 changes: 7 additions & 0 deletions src/builder/content-mask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ export default function contentMask(
buildXMLString('rect', {
...contentArea,
fill: '#fff',
// add transformation matrix to mask if overflow is hidden AND a
// transformation style is defined, otherwise children will be clipped
// incorrectly
transform:
style.overflow === 'hidden' && style.transform && matrix
? matrix
: undefined,
mask: style._inheritedMaskId
? `url(#${style._inheritedMaskId})`
: undefined,
Expand Down
8 changes: 8 additions & 0 deletions src/builder/overflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ export default function overflow(
width,
height,
d: path ? path : undefined,
// add transformation matrix to clip path if overflow is hidden AND a
// transformation style is defined, otherwise children will be clipped
// relative to the parent's original plane instead of the transformed
// plane
transform:
style.overflow === 'hidden' && style.transform && matrix
? matrix
: undefined,
})
)
}
Expand Down
41 changes: 36 additions & 5 deletions src/builder/rect.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ParsedTransformOrigin } from '../transform-origin.js'

import backgroundImage from './background-image.js'
import radius from './border-radius.js'
import radius, { getBorderRadiusClipPath } from './border-radius.js'
import { boxShadow } from './shadow.js'
import transform from './transform.js'
import overflow from './overflow.js'
Expand Down Expand Up @@ -163,9 +163,9 @@ export default async function rect(
fill,
d: path ? path : undefined,
transform: matrix ? matrix : undefined,
'clip-path': currentClipPath,
'clip-path': style.transform ? undefined : currentClipPath,
style: cssFilter ? `filter:${cssFilter}` : undefined,
mask: maskId,
mask: style.transform ? undefined : maskId,
})
)
.join('')
Expand All @@ -184,6 +184,9 @@ export default async function rect(
style
)

// border radius for images with transform property
let imageBorderRadius = undefined

// If it's an image (<img>) tag, we add an extra layer of the image itself.
if (isImage) {
// We need to subtract the border and padding sizes from the image size.
Expand All @@ -207,6 +210,21 @@ export default async function rect(
? 'xMidYMid slice'
: 'none'

if (style.transform) {
imageBorderRadius = getBorderRadiusClipPath(
{
id,
borderRadiusPath: path,
borderType: type,
left,
top,
width,
height,
},
style
)
}

shape += buildXMLString('image', {
x: left + offsetLeft,
y: top + offsetTop,
Expand All @@ -216,8 +234,16 @@ export default async function rect(
preserveAspectRatio,
transform: matrix ? matrix : undefined,
style: cssFilter ? `filter:${cssFilter}` : undefined,
'clip-path': `url(#satori_cp-${id})`,
mask: miId ? `url(#${miId})` : `url(#satori_om-${id})`,
'clip-path': style.transform
? imageBorderRadius
? `url(#${imageBorderRadius[1]})`
: undefined
: `url(#satori_cp-${id})`,
mask: style.transform
? undefined
: miId
? `url(#${miId})`
: `url(#satori_om-${id})`,
})
}

Expand Down Expand Up @@ -269,9 +295,14 @@ export default async function rect(
return (
(defs ? buildXMLString('defs', {}, defs) : '') +
(shadow ? shadow[0] : '') +
(imageBorderRadius ? imageBorderRadius[0] : '') +
clip +
(opacity !== 1 ? `<g opacity="${opacity}">` : '') +
(style.transform && currentClipPath && maskId
? `<g clip-path="${currentClipPath}" mask="${maskId}">`
: '') +
(backgroundShapes || shape) +
(style.transform && currentClipPath && maskId ? '</g>' : '') +
(opacity !== 1 ? `</g>` : '') +
(shadow ? shadow[1] : '') +
extra
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 25 additions & 0 deletions test/image.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,31 @@ describe('Image', () => {
expect(toImage(svg, 100)).toMatchImageSnapshot()
})

it('should have a separate border radius clip path when transform is used', async () => {
const svg = await satori(
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
overflow: 'hidden',
}}
>
<img
width='100%'
height='100%'
src='https://via.placeholder.com/150'
style={{
transform: 'rotate(45deg) translate(30px, 15px)',
borderRadius: '20px',
}}
/>
</div>,
{ width: 100, height: 100, fonts }
)
expect(toImage(svg, 100)).toMatchImageSnapshot()
})

it('should support transparent image with background', async () => {
const svg = await satori(
<div
Expand Down
30 changes: 30 additions & 0 deletions test/transform.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,4 +176,34 @@ describe('transform', () => {
expect(toImage(svg, 100)).toMatchImageSnapshot()
})
})

describe('behavior with parent overflow', () => {
it('should not inherit parent clip-path', async () => {
const svg = await satori(
<div
style={{
display: 'flex',
width: 20,
height: 20,
overflow: 'hidden',
}}
>
<div
style={{
width: 15,
height: 15,
backgroundColor: 'red',
transform: 'rotate(45deg) translate(15px, 5px)',
}}
/>
</div>,
{
width: 100,
height: 100,
fonts,
}
)
expect(toImage(svg, 100)).toMatchImageSnapshot()
})
})
})

0 comments on commit c55e4da

Please sign in to comment.