diff --git a/.changeset/clever-turtles-fry.md b/.changeset/clever-turtles-fry.md new file mode 100644 index 000000000..4144c11e7 --- /dev/null +++ b/.changeset/clever-turtles-fry.md @@ -0,0 +1,5 @@ +--- +"@tokens-studio/figma-plugin": patch +--- + +Fix for linear gradient start/end points being outside the node bounding box when using angles that aren't divisible by 45. diff --git a/packages/tokens-studio-for-figma/src/plugin/figmaTransforms/gradients.test.ts b/packages/tokens-studio-for-figma/src/plugin/figmaTransforms/gradients.test.ts index a72b59ae0..dddaf1a42 100644 --- a/packages/tokens-studio-for-figma/src/plugin/figmaTransforms/gradients.test.ts +++ b/packages/tokens-studio-for-figma/src/plugin/figmaTransforms/gradients.test.ts @@ -25,8 +25,8 @@ describe('convertStringtoFigmaGradient', () => { }, ], gradientTransform: [ - [0.5000000000000001, -0.5, 0.49999999999999994], - [0.5, 0.5000000000000001, -5.551115123125784e-17], + [0.5, -0.5, 0.5], + [0.5, 0.5, 0], ], }, }; @@ -64,8 +64,8 @@ describe('convertStringtoFigmaGradient', () => { }, ], gradientTransform: [ - [0.5000000000000001, -0.5, 0.49999999999999994], - [0.5, 0.5000000000000001, -5.551115123125784e-17], + [0.5, -0.5, 0.5], + [0.5, 0.5, 0], ], }, }; @@ -103,8 +103,8 @@ describe('convertStringtoFigmaGradient', () => { }, ], gradientTransform: [ - [0.5000000000000001, -0.5, 0.49999999999999994], - [0.5, 0.5000000000000001, -5.551115123125784e-17], + [0.5, -0.5, 0.5], + [0.5, 0.5, 0], ], }, }; @@ -118,25 +118,25 @@ describe('convertStringtoFigmaGradient', () => { r: 0, g: 0, b: 0, - a: 1, + a: 1 }, - position: 0, + position: 0 }, { color: { r: 1, g: 1, b: 1, - a: 1, + a: 1 }, - position: 1, - }, + position: 1 + } ], gradientTransform: [ - [6.123233995736766e-17, 1, -6.123233995736766e-17], - [-1, 6.123233995736766e-17, 1], + [0, 1, 0], + [-1, 0, 1], ], - }, + } }; const test5 = { @@ -148,22 +148,22 @@ describe('convertStringtoFigmaGradient', () => { r: 0.9019607843137255, g: 0.39215686274509803, b: 0.396078431372549, - a: 1, + a: 1 }, - position: 0, + position: 0 }, { color: { r: 0.5686274509803921, g: 0.596078431372549, b: 0.8980392156862745, - a: 1, + a: 1 }, - position: 1, - }, + position: 1 + } ], - gradientTransform: [[1, 0, 0], [0, 1, 0]], - }, + gradientTransform: [[1, 0, 0], [0, 1, 0]] + } }; const test6 = { @@ -175,33 +175,64 @@ describe('convertStringtoFigmaGradient', () => { r: 0.9019607843137255, g: 0.39215686274509803, b: 0.396078431372549, - a: 1, + a: 1 }, - position: 0, + position: 0 }, { color: { r: 0.5686274509803921, g: 0.596078431372549, b: 0.8980392156862745, - a: 1, + a: 1 }, - position: 1, - }, + position: 1 + } ], gradientTransform: [ - [6.123233995736766e-17, 1, -6.123233995736766e-17], - [-1, 6.123233995736766e-17, 1], + [0, 1, 0], + [-1, 0, 1], ], }, }; + const test7 = { + input: 'linear-gradient(106.84deg, #FF0000 5.61%, #cc00ff00 89.41%)', + output: { + gradientStops: [ + { + color: { + r: 1, + g: 0, + b: 0, + a: 1, + }, + position: 0.056100000000000004, + }, + { + color: { + r: 0.8, + g: 0, + b: 1, + a: 0, + }, + position: 0.8941, + }, + ], + gradientTransform: [ + [0.9160738743, 0.2772769934, -0.0966754339], + [-0.2772769934, 0.9160738743, 0.1806015595], + ], + } + } + expect(convertStringToFigmaGradient(test1.input)).toEqual(test1.output); expect(convertStringToFigmaGradient(test2.input)).toEqual(test2.output); expect(convertStringToFigmaGradient(test3.input)).toEqual(test3.output); expect(convertStringToFigmaGradient(test4.input)).toEqual(test4.output); expect(convertStringToFigmaGradient(test5.input)).toEqual(test5.output); expect(convertStringToFigmaGradient(test6.input)).toEqual(test6.output); + expect(convertStringToFigmaGradient(test7.input)).toEqual(test7.output); }); describe('convertFigmaGradientToString', () => { diff --git a/packages/tokens-studio-for-figma/src/plugin/figmaTransforms/gradients.ts b/packages/tokens-studio-for-figma/src/plugin/figmaTransforms/gradients.ts index 6f3daca65..cfbfaccf1 100644 --- a/packages/tokens-studio-for-figma/src/plugin/figmaTransforms/gradients.ts +++ b/packages/tokens-studio-for-figma/src/plugin/figmaTransforms/gradients.ts @@ -1,5 +1,5 @@ import { figmaRGBToHex, extractLinearGradientParamsFromTransform } from '@figma-plugin/helpers'; -import { Matrix, inverse } from 'ml-matrix'; +import { Matrix } from 'ml-matrix'; import { convertToFigmaColor } from './colors'; export function convertDegreeToNumber(degreeString: string): number { @@ -17,6 +17,12 @@ export function convertFigmaGradientToString(paint: GradientPaint) { return `linear-gradient(${angleInDeg + 90}deg, ${gradientStopsString})`; } +const roundToPrecision = (value, precision = 10) => { + const roundToPrecisionVal = 10 ** precision; + return Math.round((value + Number.EPSILON) * roundToPrecisionVal) / roundToPrecisionVal; +}; + +// 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) { const parts = value.substring(value.indexOf('(') + 1, value.lastIndexOf(')')).split(', ').map((s) => s.trim()); @@ -63,39 +69,41 @@ export function convertStringToFigmaGradient(value: string) { const degrees = -(angle - 90); const rad = degrees * (Math.PI / 180); - const scale = angle % 90 === 0 ? 1 : Math.sqrt(1 + Math.tan(angle * (Math.PI / 180)) ** 2); - // start by transforming to the gradient center + let normalizedAngleRad = Math.abs(rad) % (Math.PI / 2); + if (normalizedAngleRad > Math.PI / 4) { + // adjust angle after 45 degrees to scale down correctly towards 90 degrees + normalizedAngleRad = Math.PI / 2 - normalizedAngleRad; + } + + const sin = Math.sin(rad); + const cos = Math.cos(rad); + + let scale = 1; + + const normalisedCos = Math.cos(normalizedAngleRad); + scale = normalisedCos; + // Implement fallback if bugs are caused by obscure node types. This appears to be unnecessary + // if (!['RECTANGLE', 'FRAME', 'VECTOR'].includes(node?.type || '')) { + // // Old scale computation: + // scale = angle % 90 === 0 ? 1 : Math.sqrt(1 + Math.tan(angle * (Math.PI / 180)) ** 2); + // } + + const scaledCos = cos * scale; + const scaledSin = sin * scale; + + // start by transforming to the gradient center, to keep the gradient centered after scaling // which for figma is .5 .5 as it is a relative transform + const tx = 0.5 - 0.5 * scaledCos + 0.5 * scaledSin; + const ty = 0.5 - 0.5 * scaledSin - 0.5 * scaledCos; + const transformationMatrix = new Matrix([ - [1, 0, 0.5], - [0, 1, 0.5], + [roundToPrecision(scaledCos), roundToPrecision(-scaledSin), roundToPrecision(tx)], + [roundToPrecision(scaledSin), roundToPrecision(scaledCos), roundToPrecision(ty)], [0, 0, 1], - ]).mmul( - // we can then multiply this with the rotation matrix - new Matrix([ - [Math.cos(rad), Math.sin(rad), 0], - [-Math.sin(rad), Math.cos(rad), 0], - [0, 0, 1], - ]), - ).mmul( - // next we need to multiply it with a scale matrix to fill the entire shape - new Matrix([ - [scale, 0, 0], - [0, scale, 0], - [0, 0, 1], - ]), - ).mmul( - // lastly we need to translate it back to the 0,0 origin - // by negating the center transform - new Matrix([ - [1, 0, -0.5], - [0, 1, -0.5], - [0, 0, 1], - ]), - ); - - const gradientTransformMatrix = inverse(transformationMatrix).to2DArray(); + ]); + + const gradientTransformMatrix = transformationMatrix.to2DArray(); const gradientStops = parts.map((stop, i, arr) => { const seperatedStop = stop.split(' ');