diff --git a/README.md b/README.md
index e861a9ea..a88119ff 100644
--- a/README.md
+++ b/README.md
@@ -232,7 +232,7 @@ Satori uses the same Flexbox [layout engine](https://yogalayout.com) as React Na
| Background |
backgroundColor | Supported, single value | |
-backgroundImage | linear-gradient, repeating-linear-gradient, radial-gradient, repeating-radial-gradient, url, single value | |
+backgroundImage | linear-gradient, repeating-linear-gradient, radial-gradient, repeating-radial-gradient, conic-gradient, repeating-conic-gradient, url, single value | |
backgroundPosition | Support single value | |
backgroundSize | Support cover, contain, auto, and two-value sizes i.e. 10px 20% | Example |
backgroundClip | border-box, text | |
diff --git a/src/builder/background-image.ts b/src/builder/background-image.ts
index 66ce875d..6d05ecf4 100644
--- a/src/builder/background-image.ts
+++ b/src/builder/background-image.ts
@@ -4,6 +4,7 @@ import { buildXMLString } from '../utils.js'
import { resolveImageData } from '../handler/image.js'
import { buildLinearGradient } from './gradient/linear.js'
import { buildRadialGradient } from './gradient/radial.js'
+import { buildConicGradient } from './gradient/conic.js'
import cssColorParse from 'parse-css-color'
interface Background {
@@ -147,7 +148,9 @@ export default async function backgroundImage(
image.startsWith('linear-gradient(') ||
image.startsWith('repeating-linear-gradient(') ||
image.startsWith('radial-gradient(') ||
- image.startsWith('repeating-radial-gradient(')
+ image.startsWith('repeating-radial-gradient(') ||
+ image.startsWith('conic-gradient(') ||
+ image.startsWith('repeating-conic-gradient(')
const dimensions =
isKeywordSize && isGradient
@@ -195,6 +198,20 @@ export default async function backgroundImage(
)
}
+ if (
+ image.startsWith('conic-gradient(') ||
+ image.startsWith('repeating-conic-gradient(')
+ ) {
+ return buildConicGradient(
+ { id, width, height, repeatX, repeatY },
+ image,
+ dimensions,
+ offsets,
+ inheritableStyle,
+ from
+ )
+ }
+
if (image.startsWith('url(')) {
const [src, imageWidth, imageHeight] = await resolveImageData(
image.slice(4, -1)
diff --git a/src/builder/gradient/conic.ts b/src/builder/gradient/conic.ts
new file mode 100644
index 00000000..e3e8c8e6
--- /dev/null
+++ b/src/builder/gradient/conic.ts
@@ -0,0 +1,411 @@
+import { parseConicGradient, ColorStop } from 'css-gradient-parser'
+import { buildXMLString, lengthToNumber, calcDegree } from '../../utils.js'
+import { normalizeStops } from './utils.js'
+import cssColorParse from 'parse-css-color'
+
+interface NormalizedStop {
+ color: string
+ offset?: number
+}
+
+const SEGMENT_COUNT = 360
+
+const VALUE_RE =
+ /^-?\d+\.?\d*(%|px|em|rem|deg|rad|grad|turn|vw|vh|ch|vmin|vmax)?$/
+
+function splitRespectingParens(s: string, sep: RegExp): string[] {
+ const result: string[] = []
+ let depth = 0
+ let start = 0
+
+ for (let i = 0; i <= s.length; i++) {
+ if (i < s.length) {
+ if (s[i] === '(') depth++
+ else if (s[i] === ')') depth--
+ }
+
+ const isSep = i === s.length || (depth === 0 && sep.test(s[i]))
+ if (isSep) {
+ const part = s.slice(start, i).trim()
+ if (part) result.push(part)
+ start = i + 1
+ }
+ }
+ return result
+}
+
+function expandTwoPositionStops(input: string): string {
+ const match = input.match(/^((?:repeating-)?conic-gradient)\((.+)\)$/s)
+ if (!match) return input
+
+ const [, prefix, content] = match
+ const segments = splitRespectingParens(content, /,/)
+
+ const expanded: string[] = []
+ for (const seg of segments) {
+ const tokens = splitRespectingParens(seg, /\s+/)
+
+ if (tokens.some((t) => t === 'from' || t === 'at' || t === 'in')) {
+ expanded.push(seg)
+ continue
+ }
+
+ if (
+ tokens.length >= 3 &&
+ VALUE_RE.test(tokens[tokens.length - 1]) &&
+ VALUE_RE.test(tokens[tokens.length - 2])
+ ) {
+ const color = tokens.slice(0, -2).join(' ')
+ expanded.push(`${color} ${tokens[tokens.length - 2]}`)
+ expanded.push(`${color} ${tokens[tokens.length - 1]}`)
+ } else {
+ expanded.push(seg)
+ }
+ }
+
+ return `${prefix}(${expanded.join(', ')})`
+}
+
+function hslToRgb(h: number, s: number, l: number): [number, number, number] {
+ s /= 100
+ l /= 100
+ const c = (1 - Math.abs(2 * l - 1)) * s
+ const x = c * (1 - Math.abs(((h / 60) % 2) - 1))
+ const m = l - c / 2
+ let r = 0,
+ g = 0,
+ b = 0
+
+ if (h < 60) {
+ r = c
+ g = x
+ b = 0
+ } else if (h < 120) {
+ r = x
+ g = c
+ b = 0
+ } else if (h < 180) {
+ r = 0
+ g = c
+ b = x
+ } else if (h < 240) {
+ r = 0
+ g = x
+ b = c
+ } else if (h < 300) {
+ r = x
+ g = 0
+ b = c
+ } else {
+ r = c
+ g = 0
+ b = x
+ }
+
+ return [
+ Math.round((r + m) * 255),
+ Math.round((g + m) * 255),
+ Math.round((b + m) * 255),
+ ]
+}
+
+function parseToRGBA(color: string): [number, number, number, number] | null {
+ const parsed = cssColorParse(color)
+ if (!parsed) return null
+
+ if (parsed.type === 'hsl') {
+ const [h, s, l] = parsed.values
+ const [r, g, b] = hslToRgb(h, s, l)
+ return [r, g, b, parsed.alpha]
+ }
+
+ return [parsed.values[0], parsed.values[1], parsed.values[2], parsed.alpha]
+}
+
+function formatRGBA(c: [number, number, number, number]): string {
+ if (c[3] === 1) return `rgb(${c[0]},${c[1]},${c[2]})`
+ return `rgba(${c[0]},${c[1]},${c[2]},${c[3]})`
+}
+
+function interpolateColor(
+ t: number,
+ stops: NormalizedStop[],
+ hints?: (number | undefined)[]
+): string {
+ if (stops.length === 0) return 'transparent'
+ if (stops.length === 1) {
+ const c = parseToRGBA(stops[0].color)
+ return c ? formatRGBA(c) : stops[0].color
+ }
+
+ let i = 0
+ if (t <= stops[0].offset) i = 0
+ else if (t >= stops[stops.length - 1].offset) i = stops.length - 2
+ else {
+ while (i < stops.length - 1 && stops[i + 1].offset <= t) i++
+ }
+
+ if (i >= stops.length - 1) i = stops.length - 2
+
+ const s1 = stops[i]
+ const s2 = stops[i + 1]
+ const c1 = parseToRGBA(s1.color)
+ const c2 = parseToRGBA(s2.color)
+ if (!c1 || !c2) return s1.color
+
+ if (s1.offset === s2.offset) return formatRGBA(c2)
+
+ let localT = Math.max(
+ 0,
+ Math.min(1, (t - s1.offset) / (s2.offset - s1.offset))
+ )
+
+ if (hints && hints[i] !== undefined) {
+ const range = s2.offset - s1.offset
+ const h = (hints[i] - s1.offset) / range
+ if (h <= 0) {
+ localT = localT > 0 ? 1 : 0
+ } else if (h >= 1) {
+ localT = localT >= 1 ? 1 : 0
+ } else {
+ const p = Math.log(0.5) / Math.log(h)
+ localT = Math.pow(localT, p)
+ }
+ }
+
+ const r = Math.round(c1[0] + (c2[0] - c1[0]) * localT)
+ const g = Math.round(c1[1] + (c2[1] - c1[1]) * localT)
+ const b = Math.round(c1[2] + (c2[2] - c1[2]) * localT)
+ const a = c1[3] + (c2[3] - c1[3]) * localT
+
+ if (a === 1) return `rgb(${r},${g},${b})`
+ return `rgba(${r},${g},${b},${a})`
+}
+
+function resolvePositionPart(
+ v: string,
+ dim: number,
+ fontSize: number,
+ style: Record
+): number {
+ switch (v) {
+ case 'left':
+ case 'top':
+ return 0
+ case 'center':
+ return dim / 2
+ case 'right':
+ case 'bottom':
+ return dim
+ default:
+ return lengthToNumber(v, fontSize, dim, style, true) ?? dim / 2
+ }
+}
+
+function resolvePosition(
+ position: string,
+ xDelta: number,
+ yDelta: number,
+ fontSize: number,
+ style: Record
+): { cx: number; cy: number } {
+ if (!position || position === 'center')
+ return { cx: xDelta / 2, cy: yDelta / 2 }
+
+ const parts = position.trim().split(/\s+/)
+
+ if (parts.length === 1) {
+ const p = parts[0]
+ if (p === 'top' || p === 'bottom')
+ return {
+ cx: xDelta / 2,
+ cy: resolvePositionPart(p, yDelta, fontSize, style),
+ }
+ return {
+ cx: resolvePositionPart(p, xDelta, fontSize, style),
+ cy: yDelta / 2,
+ }
+ }
+
+ const yKeywords = new Set(['top', 'bottom'])
+ const xKeywords = new Set(['left', 'right'])
+
+ let xVal = parts[0]
+ let yVal = parts[1]
+
+ // CSS position syntax allows keywords in either order. When a y-axis
+ // keyword (top/bottom) appears first or an x-axis keyword (left/right)
+ // appears second, swap so that xVal holds the horizontal component and
+ // yVal holds the vertical component.
+ if (
+ (yKeywords.has(parts[0]) && !yKeywords.has(parts[1])) ||
+ (xKeywords.has(parts[1]) && !xKeywords.has(parts[0]))
+ ) {
+ xVal = parts[1]
+ yVal = parts[0]
+ }
+
+ return {
+ cx: resolvePositionPart(xVal, xDelta, fontSize, style),
+ cy: resolvePositionPart(yVal, yDelta, fontSize, style),
+ }
+}
+
+function calcTotalLength(stops: ColorStop[], repeating: boolean): number {
+ if (!repeating) return 360
+ const lastStop = stops.at(-1)
+ if (!lastStop?.offset) return 360
+ if (lastStop.offset.unit === '%') return 360
+ const deg = calcDegree(`${lastStop.offset.value}${lastStop.offset.unit}`)
+ return deg || 360
+}
+
+export function buildConicGradient(
+ {
+ id,
+ width,
+ height,
+ repeatX,
+ repeatY,
+ }: {
+ id: string
+ width: number
+ height: number
+ repeatX: boolean
+ repeatY: boolean
+ },
+ image: string,
+ dimensions: number[],
+ offsets: number[],
+ inheritableStyle: Record,
+ from?: 'background' | 'mask'
+) {
+ const parsed = parseConicGradient(expandTwoPositionStops(image))
+ const [xDelta, yDelta] = dimensions
+ const fontSize = inheritableStyle.fontSize as number
+
+ const startAngle = calcDegree(parsed.angle) || 0
+
+ const { cx, cy } = resolvePosition(
+ parsed.position,
+ xDelta,
+ yDelta,
+ fontSize,
+ inheritableStyle as Record
+ )
+
+ const totalLength = calcTotalLength(parsed.stops, parsed.repeating)
+
+ const stops = normalizeStops(
+ totalLength,
+ parsed.stops,
+ inheritableStyle as Record,
+ parsed.repeating,
+ from
+ )
+
+ const hints: (number | undefined)[] = []
+ const firstHasExplicitOffset =
+ parsed.stops[0]?.offset && parsed.stops[0].offset.value !== '0'
+ const idxOff = firstHasExplicitOffset ? 1 : 0
+ for (let pi = 0; pi < parsed.stops.length; pi++) {
+ const hint = (parsed.stops[pi] as any).hint
+ if (hint) {
+ const hintDeg =
+ hint.unit === '%'
+ ? (Number(hint.value) / 100) * totalLength
+ : calcDegree(`${hint.value}${hint.unit}`) || 0
+ const ni = pi + idxOff
+ if (ni < stops.length - 1) {
+ hints[ni] = hintDeg / totalLength
+ }
+ }
+ }
+ const hasHints = hints.some((h) => h !== undefined)
+
+ const radius = Math.max(
+ Math.sqrt(cx * cx + cy * cy),
+ Math.sqrt((xDelta - cx) ** 2 + cy ** 2),
+ Math.sqrt((xDelta - cx) ** 2 + (yDelta - cy) ** 2),
+ Math.sqrt(cx ** 2 + (yDelta - cy) ** 2)
+ )
+
+ const patternId = `satori_conic_pattern_${id}`
+ const clipId = `satori_conic_clip_${id}`
+
+ const slices: string[] = []
+
+ const flushSlice = (startIdx: number, endIdx: number, color: string) => {
+ const a1 = startAngle + (startIdx / SEGMENT_COUNT) * 360
+ const a2 = startAngle + (endIdx / SEGMENT_COUNT) * 360
+
+ if (endIdx - startIdx >= SEGMENT_COUNT) {
+ slices.push(
+ buildXMLString('circle', {
+ cx,
+ cy,
+ r: radius,
+ fill: color,
+ })
+ )
+ return
+ }
+
+ const r1 = ((a1 - 90) * Math.PI) / 180
+ const r2 = ((a2 - 90) * Math.PI) / 180
+ const x1 = cx + radius * Math.cos(r1)
+ const y1 = cy + radius * Math.sin(r1)
+ const x2 = cx + radius * Math.cos(r2)
+ const y2 = cy + radius * Math.sin(r2)
+ const largeArc = a2 - a1 > 180 ? 1 : 0
+ const d = `M${cx},${cy}L${x1},${y1}A${radius},${radius},0,${largeArc},1,${x2},${y2}Z`
+
+ slices.push(buildXMLString('path', { d, fill: color }))
+ }
+
+ let prevColor: string | null = null
+ let mergeStart = 0
+ const cycleDeg = parsed.repeating ? totalLength : 360
+
+ for (let i = 0; i < SEGMENT_COUNT; i++) {
+ const angleDeg = (i / SEGMENT_COUNT) * 360
+ const t = cycleDeg > 0 ? (angleDeg % cycleDeg) / cycleDeg : 0
+ const color = interpolateColor(t, stops, hasHints ? hints : undefined)
+
+ if (color !== prevColor) {
+ if (prevColor !== null) {
+ flushSlice(mergeStart, i, prevColor)
+ }
+ mergeStart = i
+ prevColor = color
+ }
+ }
+
+ if (prevColor !== null) {
+ flushSlice(mergeStart, SEGMENT_COUNT, prevColor)
+ }
+
+ const defs = buildXMLString(
+ 'pattern',
+ {
+ id: patternId,
+ x: offsets[0] / width,
+ y: offsets[1] / height,
+ width: repeatX ? xDelta / width : '1',
+ height: repeatY ? yDelta / height : '1',
+ patternUnits: 'objectBoundingBox',
+ },
+ buildXMLString(
+ 'clipPath',
+ { id: clipId },
+ buildXMLString('rect', {
+ x: 0,
+ y: 0,
+ width: xDelta,
+ height: yDelta,
+ })
+ ) + buildXMLString('g', { 'clip-path': `url(#${clipId})` }, slices.join(''))
+ )
+
+ return [patternId, defs]
+}
diff --git a/src/handler/expand.ts b/src/handler/expand.ts
index cd3b42a3..9108858c 100644
--- a/src/handler/expand.ts
+++ b/src/handler/expand.ts
@@ -176,7 +176,7 @@ function handleSpecialCase(
if (name === 'background') {
value = value.toString().trim()
if (
- /^(linear-gradient|radial-gradient|url|repeating-linear-gradient|repeating-radial-gradient)\(/.test(
+ /^(linear-gradient|radial-gradient|conic-gradient|url|repeating-linear-gradient|repeating-radial-gradient|repeating-conic-gradient)\(/.test(
value
)
) {
diff --git a/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-at-top-left-corner-1-snap.png b/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-at-top-left-corner-1-snap.png
new file mode 100644
index 00000000..70d696a4
Binary files /dev/null and b/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-at-top-left-corner-1-snap.png differ
diff --git a/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-checkerboard-pattern-1-snap.png b/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-checkerboard-pattern-1-snap.png
new file mode 100644
index 00000000..039dd31b
Binary files /dev/null and b/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-checkerboard-pattern-1-snap.png differ
diff --git a/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-on-non-square-element-1-snap.png b/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-on-non-square-element-1-snap.png
new file mode 100644
index 00000000..39cccb3f
Binary files /dev/null and b/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-on-non-square-element-1-snap.png differ
diff --git a/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-smooth-rainbow-1-snap.png b/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-smooth-rainbow-1-snap.png
new file mode 100644
index 00000000..94d1162c
Binary files /dev/null and b/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-smooth-rainbow-1-snap.png differ
diff --git a/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-three-way-smooth-blend-1-snap.png b/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-three-way-smooth-blend-1-snap.png
new file mode 100644
index 00000000..367dbbed
Binary files /dev/null and b/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-three-way-smooth-blend-1-snap.png differ
diff --git a/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-via-background-shorthand-1-snap.png b/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-via-background-shorthand-1-snap.png
new file mode 100644
index 00000000..69f60e41
Binary files /dev/null and b/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-via-background-shorthand-1-snap.png differ
diff --git a/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-with-at-position-1-snap.png b/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-with-at-position-1-snap.png
new file mode 100644
index 00000000..99516307
Binary files /dev/null and b/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-with-at-position-1-snap.png differ
diff --git a/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-with-deg-stops-1-snap.png b/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-with-deg-stops-1-snap.png
new file mode 100644
index 00000000..1e97efc6
Binary files /dev/null and b/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-with-deg-stops-1-snap.png differ
diff --git a/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-with-from-angle-1-snap.png b/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-with-from-angle-1-snap.png
new file mode 100644
index 00000000..b9ce8da1
Binary files /dev/null and b/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-with-from-angle-1-snap.png differ
diff --git a/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-with-from-angle-and-at-position-1-snap.png b/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-with-from-angle-and-at-position-1-snap.png
new file mode 100644
index 00000000..b835898c
Binary files /dev/null and b/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-with-from-angle-and-at-position-1-snap.png differ
diff --git a/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-with-hard-stops-pie-chart-1-snap.png b/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-with-hard-stops-pie-chart-1-snap.png
new file mode 100644
index 00000000..c2ba489d
Binary files /dev/null and b/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-with-hard-stops-pie-chart-1-snap.png differ
diff --git a/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-with-rgba-transparency-1-snap.png b/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-with-rgba-transparency-1-snap.png
new file mode 100644
index 00000000..f92f1d78
Binary files /dev/null and b/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-with-rgba-transparency-1-snap.png differ
diff --git a/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-with-single-color-1-snap.png b/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-with-single-color-1-snap.png
new file mode 100644
index 00000000..c7e7daa3
Binary files /dev/null and b/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-with-single-color-1-snap.png differ
diff --git a/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-with-transition-hints-1-snap.png b/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-with-transition-hints-1-snap.png
new file mode 100644
index 00000000..8e50f9f0
Binary files /dev/null and b/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-with-transition-hints-1-snap.png differ
diff --git a/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-with-turn-units-1-snap.png b/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-with-turn-units-1-snap.png
new file mode 100644
index 00000000..b9ce8da1
Binary files /dev/null and b/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-with-turn-units-1-snap.png differ
diff --git a/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-with-uneven-stops-1-snap.png b/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-with-uneven-stops-1-snap.png
new file mode 100644
index 00000000..89b41434
Binary files /dev/null and b/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-conic-gradient-with-uneven-stops-1-snap.png differ
diff --git a/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-repeating-conic-gradient-1-snap.png b/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-repeating-conic-gradient-1-snap.png
new file mode 100644
index 00000000..29a435bd
Binary files /dev/null and b/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-repeating-conic-gradient-1-snap.png differ
diff --git a/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-repeating-conic-gradient-with-hard-stops-1-snap.png b/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-repeating-conic-gradient-with-hard-stops-1-snap.png
new file mode 100644
index 00000000..4d998d8f
Binary files /dev/null and b/test/__image_snapshots__/gradient-test-tsx-test-gradient-test-tsx-gradient-conic-gradient-should-support-repeating-conic-gradient-with-hard-stops-1-snap.png differ
diff --git a/test/gradient.test.tsx b/test/gradient.test.tsx
index 82bacc05..ca180f74 100644
--- a/test/gradient.test.tsx
+++ b/test/gradient.test.tsx
@@ -656,4 +656,346 @@ describe('Gradient', () => {
)
expect(toImage(svg, 100)).toMatchImageSnapshot()
})
+
+ describe('conic-gradient', () => {
+ it('should support conic-gradient with hard stops (pie chart)', async () => {
+ const svg = await satori(
+ ,
+ {
+ width: 100,
+ height: 100,
+ fonts,
+ }
+ )
+ expect(toImage(svg, 100)).toMatchImageSnapshot()
+ })
+
+ it('should support conic-gradient checkerboard pattern', async () => {
+ const svg = await satori(
+ ,
+ {
+ width: 100,
+ height: 100,
+ fonts,
+ }
+ )
+ expect(toImage(svg, 100)).toMatchImageSnapshot()
+ })
+
+ it('should support conic-gradient with at position', async () => {
+ const svg = await satori(
+ ,
+ {
+ width: 100,
+ height: 100,
+ fonts,
+ }
+ )
+ expect(toImage(svg, 100)).toMatchImageSnapshot()
+ })
+
+ it('should support repeating-conic-gradient', async () => {
+ const svg = await satori(
+ ,
+ {
+ width: 100,
+ height: 100,
+ fonts,
+ }
+ )
+ expect(toImage(svg, 100)).toMatchImageSnapshot()
+ })
+
+ it('should support conic-gradient via background shorthand', async () => {
+ const svg = await satori(
+ ,
+ {
+ width: 100,
+ height: 100,
+ fonts,
+ }
+ )
+ expect(toImage(svg, 100)).toMatchImageSnapshot()
+ })
+
+ it('should support conic-gradient with from angle', async () => {
+ const svg = await satori(
+ ,
+ {
+ width: 100,
+ height: 100,
+ fonts,
+ }
+ )
+ expect(toImage(svg, 100)).toMatchImageSnapshot()
+ })
+
+ it('should support conic-gradient with from angle and at position', async () => {
+ const svg = await satori(
+ ,
+ {
+ width: 100,
+ height: 100,
+ fonts,
+ }
+ )
+ expect(toImage(svg, 100)).toMatchImageSnapshot()
+ })
+
+ it('should support conic-gradient smooth rainbow', async () => {
+ const svg = await satori(
+ ,
+ {
+ width: 100,
+ height: 100,
+ fonts,
+ }
+ )
+ expect(toImage(svg, 100)).toMatchImageSnapshot()
+ })
+
+ it('should support conic-gradient with single color', async () => {
+ const svg = await satori(
+ ,
+ {
+ width: 100,
+ height: 100,
+ fonts,
+ }
+ )
+ expect(toImage(svg, 100)).toMatchImageSnapshot()
+ })
+
+ it('should support conic-gradient with rgba transparency', async () => {
+ const svg = await satori(
+ ,
+ {
+ width: 100,
+ height: 100,
+ fonts,
+ }
+ )
+ expect(toImage(svg, 100)).toMatchImageSnapshot()
+ })
+
+ it('should support conic-gradient with deg stops', async () => {
+ const svg = await satori(
+ ,
+ {
+ width: 100,
+ height: 100,
+ fonts,
+ }
+ )
+ expect(toImage(svg, 100)).toMatchImageSnapshot()
+ })
+
+ it('should support conic-gradient with turn units', async () => {
+ const svg = await satori(
+ ,
+ {
+ width: 100,
+ height: 100,
+ fonts,
+ }
+ )
+ expect(toImage(svg, 100)).toMatchImageSnapshot()
+ })
+
+ it('should support repeating-conic-gradient with hard stops', async () => {
+ const svg = await satori(
+ ,
+ {
+ width: 100,
+ height: 100,
+ fonts,
+ }
+ )
+ expect(toImage(svg, 100)).toMatchImageSnapshot()
+ })
+
+ it('should support conic-gradient at top left corner', async () => {
+ const svg = await satori(
+ ,
+ {
+ width: 100,
+ height: 100,
+ fonts,
+ }
+ )
+ expect(toImage(svg, 100)).toMatchImageSnapshot()
+ })
+
+ it('should support conic-gradient with uneven stops', async () => {
+ const svg = await satori(
+ ,
+ {
+ width: 100,
+ height: 100,
+ fonts,
+ }
+ )
+ expect(toImage(svg, 100)).toMatchImageSnapshot()
+ })
+
+ it('should support conic-gradient three-way smooth blend', async () => {
+ const svg = await satori(
+ ,
+ {
+ width: 100,
+ height: 100,
+ fonts,
+ }
+ )
+ expect(toImage(svg, 100)).toMatchImageSnapshot()
+ })
+
+ it('should support conic-gradient with transition hints', async () => {
+ const svg = await satori(
+ ,
+ {
+ width: 200,
+ height: 200,
+ fonts,
+ }
+ )
+ expect(toImage(svg, 200)).toMatchImageSnapshot()
+ })
+
+ it('should support conic-gradient on non-square element', async () => {
+ const svg = await satori(
+ ,
+ {
+ width: 200,
+ height: 100,
+ fonts,
+ }
+ )
+ expect(toImage(svg, 200)).toMatchImageSnapshot()
+ })
+ })
})