From c55e4da1f5a66edc906f3ab5f901efff4b7cd6f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Sallai?= Date: Wed, 18 Sep 2024 02:36:48 +0300 Subject: [PATCH] fix: clipping behavior of children with transform (#635) --- README.md | 5 +-- src/builder/border-radius.ts | 40 ++++++++++++++++- src/builder/content-mask.ts | 7 +++ src/builder/overflow.ts | 8 ++++ src/builder/rect.ts | 41 +++++++++++++++--- ...lip-path-when-transform-is-used-1-snap.png | Bin 0 -> 773 bytes ...ld-not-inherit-parent-clip-path-1-snap.png | Bin 0 -> 425 bytes test/image.test.tsx | 25 +++++++++++ test/transform.test.tsx | 30 +++++++++++++ 9 files changed, 147 insertions(+), 9 deletions(-) create mode 100644 test/__image_snapshots__/image-test-tsx-test-image-test-tsx-image-should-have-a-separate-border-radius-clip-path-when-transform-is-used-1-snap.png create mode 100644 test/__image_snapshots__/transform-test-tsx-test-transform-test-tsx-transform-behavior-with-parent-overflow-should-not-inherit-parent-clip-path-1-snap.png diff --git a/README.md b/README.md index 426e61ac..4c318f43 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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. diff --git a/src/builder/border-radius.ts b/src/builder/border-radius.ts index 4372e073..dc2b9923 100644 --- a/src/builder/border-radius.ts +++ b/src/builder/border-radius.ts @@ -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: @@ -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 +) { + 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, diff --git a/src/builder/content-mask.ts b/src/builder/content-mask.ts index faaa864d..ca593cd8 100644 --- a/src/builder/content-mask.ts +++ b/src/builder/content-mask.ts @@ -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, diff --git a/src/builder/overflow.ts b/src/builder/overflow.ts index e6b422ff..bafdb1f1 100644 --- a/src/builder/overflow.ts +++ b/src/builder/overflow.ts @@ -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, }) ) } diff --git a/src/builder/rect.ts b/src/builder/rect.ts index 7cceb779..1db7732b 100644 --- a/src/builder/rect.ts +++ b/src/builder/rect.ts @@ -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' @@ -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('') @@ -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 () 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. @@ -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, @@ -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})`, }) } @@ -269,9 +295,14 @@ export default async function rect( return ( (defs ? buildXMLString('defs', {}, defs) : '') + (shadow ? shadow[0] : '') + + (imageBorderRadius ? imageBorderRadius[0] : '') + clip + (opacity !== 1 ? `` : '') + + (style.transform && currentClipPath && maskId + ? `` + : '') + (backgroundShapes || shape) + + (style.transform && currentClipPath && maskId ? '' : '') + (opacity !== 1 ? `` : '') + (shadow ? shadow[1] : '') + extra diff --git a/test/__image_snapshots__/image-test-tsx-test-image-test-tsx-image-should-have-a-separate-border-radius-clip-path-when-transform-is-used-1-snap.png b/test/__image_snapshots__/image-test-tsx-test-image-test-tsx-image-should-have-a-separate-border-radius-clip-path-when-transform-is-used-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..c790656e78ec7e3d1b0975e2edde6a8767cff37d GIT binary patch literal 773 zcmeAS@N?(olHy`uVBq!ia0vp^DIm8OF|e|*b~d{l^03g*C{xu8Gn$!Q9$WVHP5s*7zx(!opLtLu zlu6NBA%(L=r*VR+z$O-_X$~iZI3jTi&SmhhFyFj=M{)I@DUaV3)*Cwi2{L(*7V)Fu zIn#EN&8&Ix+Qz$0H)s2NV!xkumPbYB>>IA_2YPc;)9xFb)z7}rFxyd)$)#iI)9lo= zc*BdP`>xJn^w?ptS$;>*-y=Tr*90-Id+gM*W6P(zNvrz97BXJXe8_hG(yOh9V&1PQ zU|x6lknB9xb!B}g&wbDhYgagH5Y%_GYhel7B^l+XGuCl&puQ3zE=7;+Kz(Z#2cB@) zb=<>pnQW^Qi;*-?`Mt$~9u8^k6N`LmM7S7Fd>-L zo&9kAltT)KEDYF>GJ81+B+ATee!NDY!@%tst3!+7(lZK&W<@rLf%UwopDCXL|9;Pk_Uh)~-fjiK`FVIjy{FKY!Zsk5A5ay91LqgQu&X%Q~loCIH36FR}mt literal 0 HcmV?d00001 diff --git a/test/__image_snapshots__/transform-test-tsx-test-transform-test-tsx-transform-behavior-with-parent-overflow-should-not-inherit-parent-clip-path-1-snap.png b/test/__image_snapshots__/transform-test-tsx-test-transform-test-tsx-transform-behavior-with-parent-overflow-should-not-inherit-parent-clip-path-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..2fc8312cad027b87b3b88d935c62e2c85da3d26e GIT binary patch literal 425 zcmeAS@N?(olHy`uVBq!ia0vp^DIm zB~IK^TvDevaO*v_vN*=U?y+rSKX=`Sd1YH}vV5M;w}eAT*`r6nu;~a2n8b$CGMiw|>6y>ea_h4{f&X zs@PMS+%q}SqJOQ7zS{%|$0U|cL6srH_{pT%Ba { expect(toImage(svg, 100)).toMatchImageSnapshot() }) + it('should have a separate border radius clip path when transform is used', async () => { + const svg = await satori( +
+ +
, + { width: 100, height: 100, fonts } + ) + expect(toImage(svg, 100)).toMatchImageSnapshot() + }) + it('should support transparent image with background', async () => { const svg = await satori(
{ expect(toImage(svg, 100)).toMatchImageSnapshot() }) }) + + describe('behavior with parent overflow', () => { + it('should not inherit parent clip-path', async () => { + const svg = await satori( +
+
+
, + { + width: 100, + height: 100, + fonts, + } + ) + expect(toImage(svg, 100)).toMatchImageSnapshot() + }) + }) })