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 backgroundColorSupported, single value -backgroundImagelinear-gradient, repeating-linear-gradient, radial-gradient, repeating-radial-gradient, url, single value +backgroundImagelinear-gradient, repeating-linear-gradient, radial-gradient, repeating-radial-gradient, conic-gradient, repeating-conic-gradient, url, single value backgroundPositionSupport single value backgroundSizeSupport cover, contain, auto, and two-value sizes i.e. 10px 20%Example backgroundClipborder-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() + }) + }) })