Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: clipping behavior of children with transform #635

Merged
merged 1 commit into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
})
})
})
Loading