Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix custom linear gradient angles #3109

Merged
merged 13 commits into from
Dec 10, 2024
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
5 changes: 5 additions & 0 deletions .changeset/clever-turtles-fry.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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],
],
},
};
Expand Down Expand Up @@ -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],
],
},
};
Expand Down Expand Up @@ -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],
],
},
};
Expand All @@ -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 = {
Expand All @@ -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 = {
Expand All @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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());

Expand Down Expand Up @@ -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();
akshay-gupta7 marked this conversation as resolved.
Show resolved Hide resolved
]);

const gradientTransformMatrix = transformationMatrix.to2DArray();

const gradientStops = parts.map((stop, i, arr) => {
const seperatedStop = stop.split(' ');
Expand Down
Loading