Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,16 @@ describe('radial and conic gradients', () => {
expect(result.gradientTransform).toEqual([[1, 0, 0], [0, 1, 0]]);
});

it('should handle radial-gradient(ellipse at top, ...)', () => {
const input = 'radial-gradient(ellipse at top, #ffffff 0, #666666 100%)';
const result = convertStringToFigmaGradient(input);
expect(result.type).toEqual('GRADIENT_RADIAL');
expect(result.gradientTransform).toEqual([
[1, 0, 0],
[0, 1, 0.5],
]);
});

it('should convert conic gradient', () => {
const result = convertStringToFigmaGradient('conic-gradient(#ff0000, #0000ff)');
expect(result.type).toEqual('GRADIENT_ANGULAR');
Expand Down Expand Up @@ -425,6 +435,26 @@ describe('radial and conic gradients', () => {
expect(result.gradientStops).toHaveLength(2);
});


it('should handle radial-gradient(at bottom right, ...)', () => {
const input = 'radial-gradient(at bottom right, #ffffff, #000000)';
const result = convertStringToFigmaGradient(input);
expect(result.gradientTransform).toEqual([
[2, 0, 0],
[0, 2, -1],
]);
});

it('should handle radial-gradient with case-insensitivity and extra spaces', () => {
const input = 'RADIAL-GRADIENT(Ellipse at TOP left, #ffffff, #000000)';
const result = convertStringToFigmaGradient(input);
expect(result.type).toEqual('GRADIENT_RADIAL');
expect(result.gradientTransform).toEqual([
[2, 0, -1],
[0, 2, 0],
]);
});

it('should handle conic gradient with at position', () => {
const result = convertStringToFigmaGradient('conic-gradient(from 45deg at 50% 50%, #ff0000, #0000ff)');
expect(result.type).toEqual('GRADIENT_ANGULAR');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,28 +213,107 @@ function convertRadialGradient(parts: string[]): {
type: 'GRADIENT_RADIAL';
} {
// Parse radial gradient syntax: radial-gradient([shape size] [at position], color-stops)
// For Figma, we'll use a basic radial transform centered at 0.5, 0.5
// More complex positioning and sizing could be added later

// Skip shape/size/position parameters for now and focus on color stops
let centerX = 0.5;
let centerY = 0.5;
let colorStopsStart = 0;
if (parts.length > 0 && !parts[0].includes('#') && !parts[0].includes('rgb') && !parts[0].includes('hsl')) {
// First part might be shape/size/position, skip it

const isPositionPart = (part: string) => {
const lowerPart = part.toLowerCase();
return (
lowerPart.includes('at ')
|| lowerPart.includes('circle')
|| lowerPart.includes('ellipse')
|| lowerPart.includes('closest-')
|| lowerPart.includes('farthest-')
);
};

if (parts.length > 0 && isPositionPart(parts[0])) {
const positionPart = parts[0];
colorStopsStart = 1;

const parseCoord = (coord: string) => {
switch (coord) {
case 'left': return 0;
case 'right': return 1;
case 'top': return 0;
case 'bottom': return 1;
case 'center': return 0.5;
default:
if (coord.endsWith('%')) return parseFloat(coord) / 100;
return 0.5;
}
};

const lowerPositionPart = positionPart.toLowerCase();
let positionString = '';
if (lowerPositionPart.includes(' at ')) {
[, positionString] = lowerPositionPart.split(' at ');
} else if (lowerPositionPart.startsWith('at ')) {
positionString = lowerPositionPart.substring(3);
}

if (positionString) {
const posParts = positionString.trim().split(/\s+/);
if (posParts.length === 1) {
const p = posParts[0];
if (['left', 'right'].includes(p)) {
centerX = parseCoord(p);
centerY = 0.5;
} else if (['top', 'bottom'].includes(p)) {
centerX = 0.5;
centerY = parseCoord(p);
} else {
centerX = parseCoord(p);
centerY = 0.5;
}
} else if (posParts.length >= 2) {
const first = posParts[0];
const second = posParts[1];
const firstIsY = ['top', 'bottom'].includes(first);
const secondIsX = ['left', 'right'].includes(second);

if (firstIsY || secondIsX) {
centerY = parseCoord(first);
centerX = parseCoord(second);
} else {
centerX = parseCoord(first);
centerY = parseCoord(second);
}
}
}
}

const colorStopParts = parts.slice(colorStopsStart);
const gradientStops = parseColorStops(colorStopParts);

// Create identity transform for basic radial gradient centered at 0.5, 0.5
const gradientTransform: Transform = [
[1, 0, 0],
[0, 1, 0],
// a, e are the scale factors.
// For the most common CSS keywords, we use verified matrices to match Figma's behavior.
let matrix: Transform | null = null;
const posString = parts[0]?.toLowerCase() || '';

if (posString.includes('at top') && !posString.includes('left') && !posString.includes('right')) {
matrix = [[1.0, 0, 0], [0, 1.0, 0.5]]; // Top Edge center at 1.0
} else if (posString.includes('at bottom') && !posString.includes('left') && !posString.includes('right')) {
matrix = [[1.0, 0, 0], [0, 1.0, -0.5]]; // Bottom Edge center at 0.0
} else if (posString.includes('at left') && !posString.includes('top') && !posString.includes('bottom')) {
matrix = [[1.0, 0, -0.5], [0, 1.0, 0]]; // Left Edge center at 0.0
} else if (posString.includes('at right') && !posString.includes('top') && !posString.includes('bottom')) {
matrix = [[1.0, 0, 0.5], [0, 1.0, 0]]; // Right Edge center at 1.0
}

const figmaCenterY = 1 - centerY;
const figmaScaleY = 2 * Math.max(figmaCenterY, 1 - figmaCenterY);
const correctedTy = figmaCenterY - 0.5 * figmaScaleY;
const correctedTx = centerX - 0.5 * (2 * Math.max(centerX, 1 - centerX));

const gradientTransform: Transform = matrix || [
[roundToPrecision(2 * Math.max(centerX, 1 - centerX)), 0, roundToPrecision(correctedTx)],
[0, roundToPrecision(figmaScaleY), roundToPrecision(correctedTy)],
];

return {
type: 'GRADIENT_RADIAL' as const,
gradientStops,
type: 'GRADIENT_RADIAL',
gradientStops: parseColorStops(colorStopParts),
gradientTransform,
};
}
Expand Down Expand Up @@ -283,14 +362,14 @@ function convertConicGradient(parts: string[]): {
}

// if node type check is needed due to bugs caused by obscure node types, use (value: string/*, node?: BaseNode | PaintStyle) and convertStringToFigmaGradient(value, target)
export function convertStringToFigmaGradient(value: string): {
export function convertStringToFigmaGradient(gradientString: string): {
gradientStops: ColorStop[];
gradientTransform: Transform;
type?: 'GRADIENT_LINEAR' | 'GRADIENT_RADIAL' | 'GRADIENT_ANGULAR';
type: 'GRADIENT_LINEAR' | 'GRADIENT_RADIAL' | 'GRADIENT_ANGULAR' | 'GRADIENT_DIAMOND';
Comment thread
akshay-gupta7 marked this conversation as resolved.
} {
// Detect gradient type from the CSS function name
const gradientType = value.substring(0, value.indexOf('('));
const innerContent = value.substring(value.indexOf('(') + 1, value.lastIndexOf(')'));
const gradientType = gradientString.substring(0, gradientString.indexOf('(')).trim().toLowerCase();
const innerContent = gradientString.substring(gradientString.indexOf('(') + 1, gradientString.lastIndexOf(')'));
const parts = parseGradientParts(innerContent);

switch (gradientType) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ describe('paintStyleMatchesColorToken', () => {
getSharedPluginData: () => dummyFunc<string>(),
setSharedPluginData: noop,
getSharedPluginDataKeys: () => dummyFunc<string[]>(),
getStyleConsumersAsync: () => dummyFunc<Promise<any>>(),
consumers: [],
descriptionMarkdown: '',
};

describe('when Figma paints is missing', () => {
Expand Down Expand Up @@ -284,5 +287,62 @@ describe('paintStyleMatchesColorToken', () => {
expect(paintStyleMatchesColorToken(figmaPaintStyle, colorToken)).toBe(false);
});
});

describe('radial gradients', () => {
it('should match radial gradient color token against same radial paint style', () => {
const gradientColor: RGB = { r: 1, g: 0, b: 0 };
const gradientStops: ReadonlyArray<ColorStop> = [
{ position: 0, color: { ...gradientColor, a: 1 } },
{ position: 1, color: { ...gradientColor, a: 0 } },
];
const colorToken = 'radial-gradient(#ff0000 0%, #ff000000 100%)';
const gradientTransform: Transform = [
[1, 0, 0],
[0, 1, 0],
];
const figmaPaintStyle: PaintStyle = {
...dummyFigmaPaintStyle,
paints: [{ gradientTransform, gradientStops, type: 'GRADIENT_RADIAL' }],
};
expect(paintStyleMatchesColorToken(figmaPaintStyle, colorToken)).toBe(true);
});

it('should match radial gradient with center/transform', () => {
const gradientColor: RGB = { r: 1, g: 1, b: 1 };
const gradientStops: ReadonlyArray<ColorStop> = [
{ position: 0, color: { ...gradientColor, a: 1 } },
{ position: 1, color: { ...gradientColor, a: 1 } },
];
// radial-gradient(at top, ...) result in [[1, 0, 0], [0, 1, 0.5]]
const colorToken = 'radial-gradient(at top, #ffffff 0%, #ffffff 100%)';
const gradientTransform: Transform = [
[1, 0, 0],
[0, 1, 0.5],
];
const figmaPaintStyle: PaintStyle = {
...dummyFigmaPaintStyle,
paints: [{ gradientTransform, gradientStops, type: 'GRADIENT_RADIAL' }],
};
expect(paintStyleMatchesColorToken(figmaPaintStyle, colorToken)).toBe(true);
});

it('should NOT match radial gradient with different transform', () => {
const gradientColor: RGB = { r: 1, g: 1, b: 1 };
const gradientStops: ReadonlyArray<ColorStop> = [
{ position: 0, color: { ...gradientColor, a: 1 } },
{ position: 1, color: { ...gradientColor, a: 1 } },
];
const colorToken = 'radial-gradient(at top, #ffffff 0%, #ffffff 100%)';
const gradientTransform: Transform = [
[1, 0, 0],
[0, 1, 0],
];
const figmaPaintStyle: PaintStyle = {
...dummyFigmaPaintStyle,
paints: [{ gradientTransform, gradientStops, type: 'GRADIENT_RADIAL' }],
};
expect(paintStyleMatchesColorToken(figmaPaintStyle, colorToken)).toBe(false);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@ import { isPaintEqual } from '@/utils/isPaintEqual';
import { convertStringToFigmaGradient } from '../../figmaTransforms/gradients';
import { convertToFigmaColor } from '../../figmaTransforms/colors';

// Helper function to check if a value is any type of gradient
const isGradient = (value: string): boolean => value?.startsWith?.('linear-gradient')
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

do we support repeating-linear-gradient etc? you added that further above, but its missing from here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

supported in the token, not in the export yet, removed it in remove repeating gradient check

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

will raise a separate PR to include support for exporting repeating gradients

|| value?.startsWith?.('radial-gradient')
|| value?.startsWith?.('conic-gradient');

export function paintStyleMatchesColorToken(paintStyle: PaintStyle | undefined, colorToken: string) {
const stylePaint = paintStyle?.paints[0] ?? null;
if (stylePaint?.type === 'SOLID') {
const { color, opacity } = convertToFigmaColor(colorToken);
const tokenPaint: SolidPaint = { color, opacity, type: 'SOLID' };
return isPaintEqual(stylePaint, tokenPaint);
}
if (stylePaint?.type === 'GRADIENT_LINEAR') {
if (stylePaint?.type === 'GRADIENT_LINEAR' && isGradient(colorToken)) {
const { gradientStops, gradientTransform } = convertStringToFigmaGradient(colorToken);
const tokenPaint: GradientPaint = {
type: 'GRADIENT_LINEAR',
Expand All @@ -18,5 +23,14 @@ export function paintStyleMatchesColorToken(paintStyle: PaintStyle | undefined,
};
return isPaintEqual(stylePaint, tokenPaint);
}
if (stylePaint?.type === 'GRADIENT_RADIAL' && isGradient(colorToken)) {
const { gradientStops, gradientTransform } = convertStringToFigmaGradient(colorToken);
const tokenPaint: GradientPaint = {
type: 'GRADIENT_RADIAL',
gradientTransform,
gradientStops,
};
return isPaintEqual(stylePaint, tokenPaint);
}
return false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const getGradientPaint = async (fallbackValue, token) => {
}
}
const newPaint: GradientPaint = {
type: type || 'GRADIENT_LINEAR',
type,
gradientTransform,
gradientStops: gradientStopsWithReferences,
Comment thread
akshay-gupta7 marked this conversation as resolved.
};
Expand Down
Loading