Skip to content

Commit

Permalink
feat: add clip-path basic usage (#459)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jackie1210 authored Apr 24, 2023
1 parent c1469ee commit a3e26c1
Show file tree
Hide file tree
Showing 14 changed files with 408 additions and 21 deletions.
50 changes: 50 additions & 0 deletions src/builder/clip-path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { buildXMLString } from '../utils.js'
import { createShapeParser } from '../parser/shape.js'

export function genClipPathId(id: string) {
return `satori_cp-${id}`
}
export function genClipPath(id: string) {
return `url(#${genClipPathId(id)})`
}

export function buildClipPath(
v: {
left: number
top: number
width: number
height: number
path: string
matrix: string | undefined
id: string
currentClipPath: string | string
src?: string
},
style: Record<string, string | number>,
inheritedStyle: Record<string, string | number>
) {
if (style.clipPath === 'none') return ''

const parser = createShapeParser(v, style, inheritedStyle)
const clipPath = style.clipPath as string

let tmp: { type: string; [p: string]: string | number } = { type: '' }

for (const k of Object.keys(parser)) {
tmp = parser[k](clipPath)
if (tmp) break
}

if (tmp) {
const { type, ...rest } = tmp
return buildXMLString(
'clipPath',
{
id: genClipPathId(v.id),
'clip-path': v.currentClipPath,
},
buildXMLString(type, rest)
)
}
return ''
}
50 changes: 32 additions & 18 deletions src/builder/overflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import { buildXMLString } from '../utils.js'
import mask from './content-mask.js'
import { buildClipPath, genClipPathId } from './clip-path.js'

export default function overflow(
{
Expand All @@ -27,10 +28,38 @@ export default function overflow(
currentClipPath: string | string
src?: string
},
style: Record<string, string | number>
style: Record<string, string | number>,
inheritableStyle: Record<string, string | number>
) {
let overflowClipPath = ''
const clipPath =
style.clipPath && style.clipPath !== 'none'
? buildClipPath(
{ left, top, width, height, path, id, matrix, currentClipPath, src },
style as Record<string, number>,
inheritableStyle
)
: ''

if (style.overflow !== 'hidden' && !src) {
return ''
overflowClipPath = ''
} else {
const _id = clipPath ? `satori_ocp-${id}` : genClipPathId(id)

overflowClipPath = buildXMLString(
'clipPath',
{
id: _id,
'clip-path': currentClipPath,
},
buildXMLString(path ? 'path' : 'rect', {
x: left,
y: top,
width,
height,
d: path ? path : undefined,
})
)
}

const contentMask = mask(
Expand All @@ -46,20 +75,5 @@ export default function overflow(
style
)

return (
buildXMLString(
'clipPath',
{
id: `satori_cp-${id}`,
'clip-path': currentClipPath,
},
buildXMLString(path ? 'path' : 'rect', {
x: left,
y: top,
width,
height,
d: path ? path : undefined,
})
) + contentMask
)
return clipPath + overflowClipPath + contentMask
}
6 changes: 5 additions & 1 deletion src/builder/rect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import transform from './transform.js'
import overflow from './overflow.js'
import { buildXMLString } from '../utils.js'
import border, { getBorderClipPath } from './border.js'
import { genClipPath } from './clip-path.js'

export default async function rect(
{
Expand Down Expand Up @@ -126,11 +127,14 @@ export default async function rect(
? `url(#satori_bct-${id})`
: clipPathId
? `url(#${clipPathId})`
: style.clipPath
? genClipPath(id)
: undefined

const clip = overflow(
{ left, top, width, height, path, id, matrix, currentClipPath, src },
style as Record<string, number>
style as Record<string, number>,
inheritableStyle
)

// Each background generates a new rectangle.
Expand Down
7 changes: 5 additions & 2 deletions src/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,12 @@ export default async function* layout(
;(computedStyle.transform as any).__parent = inheritedStyle.transform
}

// If the element has `overflow` set to `hidden`, we need to create a clip
// If the element has `overflow` set to `hidden` or clip-path is set, we need to create a clip
// path and use it in all its children.
if (computedStyle.overflow === 'hidden') {
if (
computedStyle.overflow === 'hidden' ||
(computedStyle.clipPath && computedStyle.clipPath !== 'none')
) {
newInheritableStyle._inheritedClipPathId = `satori_cp-${id}`
newInheritableStyle._inheritedMaskId = `satori_om-${id}`
}
Expand Down
237 changes: 237 additions & 0 deletions src/parser/shape.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
import { lengthToNumber } from '../utils.js'
import { default as buildBorderRadius } from '../builder/border-radius.js'
import { getStylesForProperty } from 'css-to-react-native'

const regexMap = {
circle: /circle\((.+)\)/,
ellipse: /ellipse\((.+)\)/,
path: /path\((.+)\)/,
polygon: /polygon\((.+)\)/,
inset: /inset\((.+)\)/,
}

export function createShapeParser(
{
width,
height,
}: {
width: number
height: number
},
style: Record<string, string | number>,
inheritedStyle: Record<string, string | number>
) {
function parseCircle(str: string) {
const res = str.match(regexMap['circle'])

if (!res) return null

const [, value] = res
const [radius, pos = ''] = value.split('at').map((v) => v.trim())
const { x, y } = resolvePosition(pos, width, height)

return {
type: 'circle',
r: lengthToNumber(
radius,
inheritedStyle.fontSize as number,
Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2)) / Math.sqrt(2),
inheritedStyle,
true
),
cx: lengthToNumber(
x,
inheritedStyle.fontSize as number,
width,
inheritedStyle,
true
),
cy: lengthToNumber(
y,
inheritedStyle.fontSize as number,
height,
inheritedStyle,
true
),
}
}
function parseEllipse(str: string) {
const res = str.match(regexMap['ellipse'])

if (!res) return null

const [, value] = res
const [radius, pos = ''] = value.split('at').map((v) => v.trim())
const [rx, ry] = radius.split(' ')
const { x, y } = resolvePosition(pos, width, height)

return {
type: 'ellipse',
rx: lengthToNumber(
rx || '50%',
inheritedStyle.fontSize as number,
width,
inheritedStyle,
true
),
ry: lengthToNumber(
ry || '50%',
inheritedStyle.fontSize as number,
height,
inheritedStyle,
true
),
cx: lengthToNumber(
x,
inheritedStyle.fontSize as number,
width,
inheritedStyle,
true
),
cy: lengthToNumber(
y,
inheritedStyle.fontSize as number,
height,
inheritedStyle,
true
),
}
}
function parsePath(str: string) {
const res = str.match(regexMap['path'])

if (!res) return null

const [fillRule, d] = resolveFillRule(res[1])

return {
type: 'path',
d,
'fill-rule': fillRule,
}
}
function parsePolygon(str: string) {
const res = str.match(regexMap['polygon'])

if (!res) return null

const [fillRule, points] = resolveFillRule(res[1])

return {
type: 'polygon',
'fill-rule': fillRule,
points: points
.split(',')
.map((v) =>
v
.split(' ')
.map((k, i) =>
lengthToNumber(
k,
inheritedStyle.fontSize as number,
i === 0 ? width : height,
inheritedStyle,
true
)
)
.join(' ')
)
.join(','),
}
}
function parseInset(str: string) {
const res = str.match(regexMap['inset'])

if (!res) return null

const [inset, radius] = (
res[1].includes('round') ? res[1] : `${res[1].trim()} round 0`
).split('round')
const radiusMap = getStylesForProperty('borderRadius', radius, true)
const r = Object.values(radiusMap)
.map((s) => String(s))
.map(
(s, i) =>
lengthToNumber(
s,
inheritedStyle.fontSize as number,
i === 0 || i === 2 ? height : width,
inheritedStyle,
true
) || 0
)
const offsets = Object.values(getStylesForProperty('margin', inset, true))
.map((s) => String(s))
.map(
(s, i) =>
lengthToNumber(
s,
inheritedStyle.fontSize as number,
i === 0 || i === 2 ? height : width,
inheritedStyle,
true
) || 0
)
const x = offsets[3]
const y = offsets[0]
const w = width - (offsets[1] + offsets[3])
const h = height - (offsets[0] + offsets[2])

if (r.some((v) => v > 0)) {
const d = buildBorderRadius(
{ left: x, top: y, width: w, height: h },
{ ...style, ...radiusMap }
)

return { type: 'path', d }
}

return {
type: 'rect',
x,
y,
width: w,
height: h,
}
}

return {
parseCircle,
parseEllipse,
parsePath,
parsePolygon,
parseInset,
}
}

function resolveFillRule(str: string) {
const [, fillRule = 'nonzero', d] =
str.replace(/('|")/g, '').match(/^(nonzero|evenodd)?,?(.+)/) || []

return [fillRule, d]
}

function resolvePosition(position: string, xDelta: number, yDelta: number) {
const pos = position.split(' ')
const res: { x: number | string; y: number | string } = {
x: pos[0] || '50%',
y: pos[1] || '50%',
}

pos.forEach((v) => {
if (v === 'top') {
res.y = 0
} else if (v === 'bottom') {
res.y = yDelta
} else if (v === 'left') {
res.x = 0
} else if (v === 'right') {
res.x = xDelta
} else {
res.x = xDelta / 2
res.y = yDelta / 2
}
})

return res
}
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.
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.
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.
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.
Loading

1 comment on commit a3e26c1

@vercel
Copy link

@vercel vercel bot commented on a3e26c1 Apr 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.