Skip to content

Instantly share code, notes, and snippets.

@yagudaev
Created August 9, 2022 21:48
Show Gist options
  • 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] }
]
}
@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.

@urosrakovicdev
Copy link

Hi guys, Im not very good in handling matrix and vectors, maybe you guys can help me. I need to convert figma gradient into css (Opposite to your css->figma plugin), but I have problem calculating gradient angle from 'gradientTransform' matrix from figma's plugin api. I would really appreciate it if someone could help me out with this. :)

@yagudaev
Copy link
Author

yagudaev commented Jun 1, 2024

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