0
Fork 0
mirror of https://github.com/penpot/penpot-exporter-figma-plugin.git synced 2025-01-03 05:10:13 -05:00

Gradients (#108)

* linear gradient

* radial gradients

* fixes

* fixes
This commit is contained in:
Alex Sánchez 2024-05-13 16:44:00 +02:00 committed by GitHub
parent 538c076374
commit 23e97fb3d7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 97 additions and 47 deletions

View file

@ -0,0 +1,5 @@
---
"penpot-exporter": minor
---
Added support for linear and radial gradients

View file

@ -6,6 +6,6 @@ export const transformFills = async (
node: MinimalFillsMixin & DimensionAndPositionMixin node: MinimalFillsMixin & DimensionAndPositionMixin
): Promise<Partial<ShapeAttributes>> => { ): Promise<Partial<ShapeAttributes>> => {
return { return {
fills: await translateFills(node.fills, node.width, node.height) fills: await translateFills(node.fills)
}; };
}; };

View file

@ -64,24 +64,14 @@ const transformVectorPath = async (
baseX: number, baseX: number,
baseY: number baseY: number
): Promise<PathShape> => { ): Promise<PathShape> => {
const dimensionAndPosition = transformDimensionAndPositionFromVectorPath(
vectorPath,
baseX,
baseY
);
return { return {
type: 'path', type: 'path',
name: 'svg-path', name: 'svg-path',
content: translateVectorPath(vectorPath, baseX + node.x, baseY + node.y), content: translateVectorPath(vectorPath, baseX + node.x, baseY + node.y),
fills: await translateFills( fills: await translateFills(vectorRegion?.fills ?? node.fills),
vectorRegion?.fills ?? node.fills,
dimensionAndPosition.width,
dimensionAndPosition.height
),
...(await transformStrokes(node)), ...(await transformStrokes(node)),
...transformEffects(node), ...transformEffects(node),
...dimensionAndPosition, ...transformDimensionAndPositionFromVectorPath(vectorPath, baseX, baseY),
...transformSceneNode(node), ...transformSceneNode(node),
...transformBlend(node), ...transformBlend(node),
...transformProportion(node) ...transformProportion(node)

View file

@ -49,7 +49,7 @@ const translateStyleTextSegment = async (
segment: StyleTextSegment segment: StyleTextSegment
): Promise<PenpotTextNode> => { ): Promise<PenpotTextNode> => {
return { return {
fills: await translateFills(segment.fills, node.width, node.height), fills: await translateFills(segment.fills),
text: segment.characters, text: segment.characters,
...transformTextStyle(node, segment) ...transformTextStyle(node, segment)
}; };

View file

@ -1,19 +1,17 @@
import { detectMimeType, rgbToHex } from '@plugin/utils'; import { calculateRadialGradient, detectMimeType, rgbToHex } from '@plugin/utils';
import { calculateLinearGradient } from '@plugin/utils/calculateLinearGradient'; import { calculateLinearGradient } from '@plugin/utils/calculateLinearGradient';
import { Fill } from '@ui/lib/types/utils/fill'; import { Fill } from '@ui/lib/types/utils/fill';
import { ImageColor } from '@ui/lib/types/utils/imageColor'; import { ImageColor } from '@ui/lib/types/utils/imageColor';
export const translateFill = async ( export const translateFill = async (fill: Paint): Promise<Fill | undefined> => {
fill: Paint,
width: number,
height: number
): Promise<Fill | undefined> => {
switch (fill.type) { switch (fill.type) {
case 'SOLID': case 'SOLID':
return translateSolidFill(fill); return translateSolidFill(fill);
case 'GRADIENT_LINEAR': case 'GRADIENT_LINEAR':
return translateGradientLinearFill(fill, width, height); return translateGradientLinearFill(fill);
case 'GRADIENT_RADIAL':
return translateGradientRadialFill(fill);
case 'IMAGE': case 'IMAGE':
return await translateImageFill(fill); return await translateImageFill(fill);
} }
@ -22,15 +20,13 @@ export const translateFill = async (
}; };
export const translateFills = async ( export const translateFills = async (
fills: readonly Paint[] | typeof figma.mixed, fills: readonly Paint[] | typeof figma.mixed
width: number,
height: number
): Promise<Fill[]> => { ): Promise<Fill[]> => {
const figmaFills = fills === figma.mixed ? [] : fills; const figmaFills = fills === figma.mixed ? [] : fills;
const penpotFills: Fill[] = []; const penpotFills: Fill[] = [];
for (const fill of figmaFills) { for (const fill of figmaFills) {
const penpotFill = await translateFill(fill, width, height); const penpotFill = await translateFill(fill);
if (penpotFill) { if (penpotFill) {
// fills are applied in reverse order in Figma, that's why we unshift // fills are applied in reverse order in Figma, that's why we unshift
penpotFills.unshift(penpotFill); penpotFills.unshift(penpotFill);
@ -88,29 +84,43 @@ const translateSolidFill = (fill: SolidPaint): Fill => {
}; };
}; };
const translateGradientLinearFill = (fill: GradientPaint, width: number, height: number): Fill => { const translateGradientLinearFill = (fill: GradientPaint): Fill => {
const points = calculateLinearGradient(width, height, fill.gradientTransform); const points = calculateLinearGradient(fill.gradientTransform);
return { return {
fillColorGradient: { fillColorGradient: {
type: 'linear', type: 'linear',
startX: points.start[0] / width, startX: points.start[0],
startY: points.start[1] / height, startY: points.start[1],
endX: points.end[0] / width, endX: points.end[0],
endY: points.end[1] / height, endY: points.end[1],
width: 1, width: 1,
stops: [ stops: fill.gradientStops.map(stop => ({
{ color: rgbToHex(stop.color),
color: rgbToHex(fill.gradientStops[0].color), offset: stop.position,
offset: fill.gradientStops[0].position, opacity: stop.color.a * (fill.opacity ?? 1)
opacity: fill.gradientStops[0].color.a * (fill.opacity ?? 1) }))
}, },
{ fillOpacity: !fill.visible ? 0 : fill.opacity
color: rgbToHex(fill.gradientStops[1].color), };
offset: fill.gradientStops[1].position, };
opacity: fill.gradientStops[1].color.a * (fill.opacity ?? 1)
} const translateGradientRadialFill = (fill: GradientPaint): Fill => {
] const points = calculateRadialGradient(fill.gradientTransform);
return {
fillColorGradient: {
type: 'radial',
startX: points.start[0],
startY: points.start[1],
endX: points.end[0],
endY: points.end[1],
width: 1,
stops: fill.gradientStops.map(stop => ({
color: rgbToHex(stop.color),
offset: stop.position,
opacity: stop.color.a * (fill.opacity ?? 1)
}))
}, },
fillOpacity: !fill.visible ? 0 : fill.opacity fillOpacity: !fill.visible ? 0 : fill.opacity
}; };

View file

@ -10,7 +10,7 @@ export const translateStrokes = async (
): Promise<Stroke[]> => { ): Promise<Stroke[]> => {
return await Promise.all( return await Promise.all(
nodeStrokes.strokes.map(async (paint, index) => { nodeStrokes.strokes.map(async (paint, index) => {
const fill = await translateFill(paint, 0, 0); const fill = await translateFill(paint);
const stroke: Stroke = { const stroke: Stroke = {
strokeColor: fill?.fillColor, strokeColor: fill?.fillColor,
strokeOpacity: fill?.fillOpacity, strokeOpacity: fill?.fillOpacity,

View file

@ -1,7 +1,7 @@
import { applyMatrixToPoint } from '@plugin/utils/applyMatrixToPoint'; import { applyMatrixToPoint } from '@plugin/utils/applyMatrixToPoint';
import { matrixInvert } from '@plugin/utils/matrixInvert'; import { matrixInvert } from '@plugin/utils/matrixInvert';
export const calculateLinearGradient = (shapeWidth: number, shapeHeight: number, t: Transform) => { export const calculateLinearGradient = (t: Transform): { start: number[]; end: number[] } => {
const transform = t.length === 2 ? [...t, [0, 0, 1]] : [...t]; const transform = t.length === 2 ? [...t, [0, 0, 1]] : [...t];
const mxInv = matrixInvert(transform); const mxInv = matrixInvert(transform);
@ -16,8 +16,9 @@ export const calculateLinearGradient = (shapeWidth: number, shapeHeight: number,
[0, 0.5], [0, 0.5],
[1, 0.5] [1, 0.5]
].map(p => applyMatrixToPoint(mxInv, p)); ].map(p => applyMatrixToPoint(mxInv, p));
return { return {
start: [startEnd[0][0] * shapeWidth, startEnd[0][1] * shapeHeight], start: [startEnd[0][0], startEnd[0][1]],
end: [startEnd[1][0] * shapeWidth, startEnd[1][1] * shapeHeight] end: [startEnd[1][0], startEnd[1][1]]
}; };
}; };

View file

@ -0,0 +1,43 @@
import { applyMatrixToPoint } from '@plugin/utils/applyMatrixToPoint';
import { matrixInvert } from '@plugin/utils/matrixInvert';
export const calculateRadialGradient = (t: Transform): { start: number[]; end: number[] } => {
const transform = t.length === 2 ? [...t, [0, 0, 1]] : [...t];
const mxInv = matrixInvert(transform);
if (!mxInv) {
return {
start: [0, 0],
end: [0, 0]
};
}
const centerPoint = applyMatrixToPoint(mxInv, [0.5, 0.5]);
const rxPoint = applyMatrixToPoint(mxInv, [1, 0.5]);
const ryPoint = applyMatrixToPoint(mxInv, [0.5, 1]);
const rx = Math.sqrt(
Math.pow(rxPoint[0] - centerPoint[0], 2) + Math.pow(rxPoint[1] - centerPoint[1], 2)
);
const ry = Math.sqrt(
Math.pow(ryPoint[0] - centerPoint[0], 2) + Math.pow(ryPoint[1] - centerPoint[1], 2)
);
const angle =
Math.atan((rxPoint[1] - centerPoint[1]) / (rxPoint[0] - centerPoint[0])) * (180 / Math.PI);
return {
start: centerPoint,
end: calculateRadialGradientEndPoint(angle, centerPoint, [rx, ry])
};
};
const calculateRadialGradientEndPoint = (
rotation: number,
center: number[],
radius: number[]
): [number, number] => {
const angle = rotation * (Math.PI / 180);
const x = center[0] + radius[0] * Math.cos(angle);
const y = center[1] + radius[1] * Math.sin(angle);
return [x, y];
};

View file

@ -1,6 +1,7 @@
export * from './applyMatrixToPoint'; export * from './applyMatrixToPoint';
export * from './calculateAdjustment'; export * from './calculateAdjustment';
export * from './calculateLinearGradient'; export * from './calculateLinearGradient';
export * from './calculateRadialGradient';
export * from './detectMimeType'; export * from './detectMimeType';
export * from './getBoundingBox'; export * from './getBoundingBox';
export * from './matrixInvert'; export * from './matrixInvert';