Skip to content

Instantly share code, notes, and snippets.

@yagudaev
Created August 9, 2022 21:48
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save yagudaev/0c2b89674c6aee8b38cd379752ef58d0 to your computer and use it in GitHub Desktop.
Save yagudaev/0c2b89674c6aee8b38cd379752ef58d0 to your computer and use it in GitHub Desktop.
Figma convert `gradientHandlePositions` to `transformGradient` and vice-versa
import {
convertGradientHandlesToTransform,
convertTransformToGradientHandles
} from "./setPropertyFill"
describe("convertGradientHandlesToTransform", () => {
it("identity matrix", () => {
expect(
convertGradientHandlesToTransform([
{
x: 0,
y: 0.5
},
{
x: 1,
y: 0.5
},
{
x: 0,
y: 1
}
])
).toEqual([
[1, 0, 0],
[0, 1, 0]
])
})
it('scale "up" matrix', () => {
expect(
convertGradientHandlesToTransform([
{
x: 0,
y: 0.25
},
{
x: 0.5,
y: 0.25
},
{
x: 0,
y: 0.5
}
])
).toEqual([
[2, 0, 0],
[0, 2, 0]
])
})
it("complex transform", () => {
expect(
convertGradientHandlesToTransform([
{
x: 0.06041662833364192,
y: 0.9474249294453632
},
{
x: 0.9397033965856045,
y: 0.05248769196422948
},
{
x: 0.5078852470742088,
y: 1.4138814828771724
}
])
).toEqual(
[
[0.5754421949386597, -0.5520178079605103, 0.4882291555404663],
[0.5520178079605103, 0.542364239692688, -0.04720045626163483]
].map((row) => row.map((v) => expect.closeTo(v, 15)))
)
})
})
describe("convertTransformToGradientHandles", () => {
it("identity matrix", () => {
expect(
convertTransformToGradientHandles([
[1, 0, 0],
[0, 1, 0],
[0, 0, 1]
])
).toEqual([
{
x: 0,
y: 0.5
},
{
x: 1,
y: 0.5
},
{
x: 0,
y: 1
}
])
})
// TODO: test adds row if missing
it('matrix with scale "up"', () => {
expect(
convertTransformToGradientHandles([
[2, 0, 0],
[0, 2, 0],
[0, 0, 1]
])
).toEqual([
{
x: 0,
y: 0.25
},
{
x: 0.5,
y: 0.25
},
{
x: 0,
y: 0.5
}
])
})
it("complex matrix", () => {
expect(
convertTransformToGradientHandles([
[0.5754421949386597, -0.5520178079605103, 0.4882291555404663],
[0.5520178079605103, 0.542364239692688, -0.04720045626163483],
[0, 0, 1]
])
).toEqual(
[
{
x: 0.06041662833364192,
y: 0.9474249294453632
},
{
x: 0.9397033965856045,
y: 0.05248769196422948
},
{
x: 0.5078852470742088,
y: 1.4138814828771724
}
].map(({ x, y }) => ({ x: expect.closeTo(x, 15), y: expect.closeTo(y, 15) }))
)
})
})
import * as math from "mathjs"
const identityMatrixHandlePositions = [
[0, 1, 0],
[0.5, 0.5, 1],
[1, 1, 1]
]
export function convertGradientHandlesToTransform(
gradientHandlePositions: [
{ x: number; y: number },
{ x: number; y: number },
{ x: number; y: number }
]
) {
const gh = gradientHandlePositions
const d = [
[gh[0].x, gh[1].x, gh[2].x],
[gh[0].y, gh[1].y, gh[2].y],
[1, 1, 1]
]
const o = identityMatrixHandlePositions
const m = math.multiply(o, math.inv(d))
return [m[0], m[1]]
}
export function convertTransformToGradientHandles(transform: number[][]) {
const inverseTransform = math.inv(transform)
// point matrix
const mp = math.multiply(inverseTransform, identityMatrixHandlePositions)
return [
{ x: mp[0][0], y: mp[1][0] },
{ x: mp[0][1], y: mp[1][1] },
{ x: mp[0][2], y: mp[1][2] }
]
}
@klavsbuss
Copy link

thanks man.
i was wondering - is those corrdinates relative to object or to whole figma document node?

gradientHandlePositions: [
    { x: number; y: number },
    { x: number; y: number },
    { x: number; y: number }
  ]

also which coordinate defines which gradient handle?

@yagudaev
Copy link
Author

@klavsbuss, the gradient coordinates are relative to the object. The top left coordinate would be (0,0) and bottom right (1, 1). This happens regardless of the size and shape of the surface the paint is covering. So a rectangle and prefect square have no difference in coordinates.

From a UI perspective, to place the handles, it is just a matter for multiplying the width and height by x and y, respectively. This is why the REST API works this way, it's easy to render the UI.

The identity matrix handle positions are always (0, 0.5), (1, 0.5) and (1, 0). If you look at the matrix on line 3 you will recognize that.

From there, I needed to figure out the transform that was applied to these points to get them to their new handle positions.

This Khan Academy course was super helpful for that: https://www.khanacademy.org/math/precalculus/x9e81a4f98389efdf:matrices/x9e81a4f98389efdf:matrices-as-transformations/a/matrices-as-transformations.

You can see it in this diagram more clearly:

D18038F9-8A70-4306-B9C5-92BA61F92736_1_102_a

Note the diagram actually demonstrates the above unit tests.

Note 2: (M^-1) * O = D
Where:
M^-1 is the inverse of the transformation matrix provided (2nd matrix, which we are trying to find)
O is Original Handle positions (top of diagram)
D is the Destination Handle position (bottom diagram)

Solving for M, I got M = O * (D^-1)

@DanielPopOut
Copy link

Thanks for this 😍 Amazing

@yagudaev
Copy link
Author

@DanielPopOut glad you found it useful 😁. Ended up using some of these ideas in https://www.figma.com/community/plugin/1157089605295322526/CSS-Gradient-to-Figma

@alexbourt
Copy link

This was extremely useful, thanks!

@prakhart111
Copy link

Thanks for this!!
Also, how can I convert this matrix for the radial_gradiant() of CSS?

@yagudaev
Copy link
Author

yagudaev commented Jun 8, 2023

@prakhart111 I haven't yet done the math of Radial Gradient, but looking at the W3C spec will really help here: https://www.w3.org/TR/css-images-3/#radial-gradients -- they specify how points are placed in color stops.

You can use that an observing Figma's default behaviour and how it differs from CSS to figure out how to do that

@prakhart111
Copy link

Thanks, @yagudaev
And I figured out the maths but forgot to update it here.
Also found a repository with some useful helper functions for such applications.

Here's the radial gradient function from that repository.
https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/extractRadialOrDiamondGradientParams.ts#L11

@yagudaev
Copy link
Author

@prakhart111 Amazing, thank you for the link, didn't know they had that function there.

If you have time, love to add support to my Open Source Plugin: https://github.com/yagudaev/css-gradient-to-figma. Happy to review PR or point you in the right direction 😁

@Dinothan-IdeaBits
Copy link

Dinothan-IdeaBits commented Aug 10, 2023

how to import mathjs in figma plugin app?

once I add the import statement, plugin is not run on Figma.

i need to this converter to my plugin

setPropertyFill.ts

@yagudaev
Copy link
Author

@Dinothan-IdeaBits take a look at: https://github.com/yagudaev/css-gradient-to-figma/blob/main/src/shared/math.ts

You can try the repo too, should give you a good idea

@KyrieChen
Copy link

KyrieChen commented Apr 7, 2024

Thanks for this!!This has been of great assistance to me.

By the way, can I ask you some questions? It has been bothering me for a long time.

Q1: How do you find the identity matrix? [[0, 0.5], [1, 0.5]]
Q2: Why do we need to use the inverse of the transformation matrix instead of transformation matrix itself? Isn't the transformation matrix representing the transformation from identity matrix to new matrix ?
Q3: When we want to obtain the coordinates of the gradient line, why do we need to multiply GradientHandles by the width and height of the shape?

I look forward to your response. Thank you very much.

@yagudaev
Copy link
Author

Glad it is helpful @KyrieChen 😊.

Q1) The identity matrix is always [[1, 0], [0, 1]] (x and y in normal positions). The initial position of a gradient when created through figma is just that. I found it through observation. I created it through the UI and through the plugin api and looked at the values.

Q2) The inverse is used to solve the equation. We have 3 points from the REST API, they are the points that Figma displays when you look at the gradient in the UI (the 3 dots). Our second thing that is known is the initial values of the 3 points when we create a brand new gradient without any changes. We use that to solve for the transformation matrix needed to change the points.

Note I tried to solve this initially by breaking it down to SRT (scale, rotate, transform) values independently. The math there is much more complex and I got a large margin of error +/-10%.

Using linear algebra instead of trigonometry was far easier here.

Q3) As far as I remember Figma uses a normalized vector [0, 1], so to fill a shape you need to scale it. This causes some surprises compared to typical Computer Graphics and probably when I ended up doing that.

Hope this help, sorry it's been almost 2 years so forgot some of this 😊.

Btw give the css gradient repo a try, lots of goodies there. Also got the help of html.to.design CTO there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment