Skip to content

Commit

Permalink
fix: set the mask-type to match the browser's style
Browse files Browse the repository at this point in the history
  • Loading branch information
Jackie1210 committed Sep 20, 2023
1 parent fc356ec commit ec08e15
Show file tree
Hide file tree
Showing 14 changed files with 89 additions and 38 deletions.
21 changes: 2 additions & 19 deletions src/builder/background-image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,17 +161,6 @@ function normalizeStops(
}
}

if (from === 'mask') {
return stops.map((stop) => {
const color = cssColorParse(stop.color)
if (color.alpha === 0) {
return { ...stop, color: `rgba(0, 0, 0, 1)` }
} else {
return { ...stop, color: `rgba(255, 255, 255, ${color.alpha})` }
}
})
}

return stops
}

Expand Down Expand Up @@ -452,14 +441,8 @@ export default async function backgroundImage(
const [src, imageWidth, imageHeight] = await resolveImageData(
image.slice(4, -1)
)
const resolvedWidth =
from === 'mask'
? imageWidth || dimensionsWithoutFallback[0]
: dimensionsWithoutFallback[0] || imageWidth
const resolvedHeight =
from === 'mask'
? imageHeight || dimensionsWithoutFallback[1]
: dimensionsWithoutFallback[1] || imageHeight
const resolvedWidth = dimensionsWithoutFallback[0] || imageWidth
const resolvedHeight = dimensionsWithoutFallback[1] || imageHeight

return [
`satori_bi${id}`,
Expand Down
23 changes: 18 additions & 5 deletions src/builder/mask-image.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { buildXMLString } from '../utils.js'
import buildBackgroundImage from './background-image.js'
import type { MaskProperty } from '../parser/mask.js'
import type { MaskParsedRes } from '../parser/mask.js'

const genMaskImageId = (id: string) => `satori_mi-${id}`

Expand All @@ -17,15 +17,16 @@ export default async function buildMaskImage(
): Promise<[string, string]> {
if (!style.maskImage) return ['', '']
const { left, top, width, height, id } = v
const maskImage = style.maskImage as unknown as MaskProperty[]
const length = maskImage.length
const maskImage = style.maskImage as unknown as MaskParsedRes
const images = maskImage.detail
const length = images.length
if (!length) return ['', '']
const miId = genMaskImageId(id)

let mask = ''

for (let i = 0; i < length; i++) {
const m = maskImage[i]
const m = images[i]

const [_id, def] = await buildBackgroundImage(
{ id: `${miId}-${i}`, left, top, width, height },
Expand All @@ -45,7 +46,19 @@ export default async function buildMaskImage(
})
}

mask = buildXMLString('mask', { id: miId }, mask)
mask = buildXMLString(
'mask',
{
id: miId,
// FIXME: although mask-type's default value is luminance, but we can get the same result with what browser renders unless
// i set mask-type with alpha
style: [
`mask-type: ${maskImage.type}`,
`-webkit-mask-type: ${maskImage.type}`,
].join(';'),
},
mask
)

return [miId, mask]
}
4 changes: 2 additions & 2 deletions src/handler/expand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import parseTransformOrigin, {
ParsedTransformOrigin,
} from '../transform-origin.js'
import { isString, lengthToNumber, v } from '../utils.js'
import { MaskProperty, parseMask } from '../parser/mask.js'
import { MaskParsedRes, parseMask } from '../parser/mask.js'

// https://react-cn.github.io/react/tips/style-props-value-px.html
const optOutPx = new Set([
Expand Down Expand Up @@ -248,7 +248,7 @@ type MainStyle = {
color: string
fontSize: number
transformOrigin: ParsedTransformOrigin
maskImage: MaskProperty[]
maskImage: MaskParsedRes
opacity: number
textTransform: string
whiteSpace: string
Expand Down
54 changes: 42 additions & 12 deletions src/parser/mask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { getPropertyName } from 'css-to-react-native'

function getMaskProperty(style: Record<string, string | number>, name: string) {
const key = getPropertyName(`mask-${name}`)
return (style[key] || style[`WebkitM${key.substring(1)}`]) as string
return (
(style[key] || style[`WebkitM${key.substring(1)}`] || '') as string
).split(',')
}

export interface MaskProperty {
Expand All @@ -12,6 +14,12 @@ export interface MaskProperty {
repeat: string
origin: string
clip: string
mode: string
}

export interface MaskParsedRes {
type: string
detail: MaskProperty[]
}

function splitMaskImages(maskImage) {
Expand Down Expand Up @@ -44,21 +52,43 @@ function splitMaskImages(maskImage) {

export function parseMask(
style: Record<string, string | number>
): MaskProperty[] {
): MaskParsedRes {
const maskImage = (style.maskImage || style.WebkitMaskImage) as string

const common = {
position: getMaskProperty(style, 'position') || '0% 0%',
size: getMaskProperty(style, 'size') || '100% 100%',
repeat: getMaskProperty(style, 'repeat') || 'repeat',
origin: getMaskProperty(style, 'origin') || 'border-box',
clip: getMaskProperty(style, 'origin') || 'border-box',
position: getMaskProperty(style, 'position'),
size: getMaskProperty(style, 'size'),
repeat: getMaskProperty(style, 'repeat'),
origin: getMaskProperty(style, 'origin'),
clip: getMaskProperty(style, 'origin'),
mode: getMaskProperty(style, 'mode'),
}

let maskImages = splitMaskImages(maskImage).filter((v) => v && v !== 'none')
const images = splitMaskImages(maskImage).filter((v) => v && v !== 'none')

return maskImages.reverse().map((m) => ({
image: m,
...common,
}))
const result = []

for (let i = 0, n = images.length; i < n; i++) {
result[i] = {
image: images[i],
position: common.position[i] || '0% 0%',
size: common.size[i] || '',
repeat: common.repeat[i] || 'repeat',
origin: common.origin[i] || 'border-box',
clip: common.clip[i] || 'border-box',
// https://drafts.fxtf.org/css-masking/#the-mask-mode
// match-source(default), alpha, luminance
// image -> alpha:
// 1. url()
// 2.gradient
// mask-source -> luminance(e.g url(mask#id))
// we do rarely use mask-source in satori, so here we just set alpha by default
mode: common.mode[i] || 'alpha',
}
}

return {
type: (getMaskProperty(style, 'type')[0] || 'alpha') as string,
detail: result,
}
}
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
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/mask-image.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,31 @@ describe('Mask-*', () => {
)
expect(toImage(svg, 100)).toMatchImageSnapshot()
})

it('should render correctly with real image as mask-image', async () => {
const svg = await satori(
<div
style={{
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
fontSize: 32,
fontWeight: 600,
background: 'red',
maskImage:
'url(https://fxbssdl.kgimg.com/bss/fxams/f2846cbe8d1c89ce84191b5c05ce1df9.png)',
maskSize: '100px 100px',
maskRepeat: 'no-repeat', // just for reference in html
}}
></div>,
{ width: 100, height: 100, fonts }
)

expect(toImage(svg, 100)).toMatchImageSnapshot()
})
it('should support mask-position', async () => {
const svg = await satori(
<div
Expand Down

0 comments on commit ec08e15

Please sign in to comment.