mirror of
https://github.com/penpot/penpot-exporter-figma-plugin.git
synced 2025-01-03 05:10:13 -05:00
parent
538c076374
commit
23e97fb3d7
9 changed files with 97 additions and 47 deletions
5
.changeset/sour-readers-wash.md
Normal file
5
.changeset/sour-readers-wash.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"penpot-exporter": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Added support for linear and radial gradients
|
|
@ -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)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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]]
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
43
plugin-src/utils/calculateRadialGradient.ts
Normal file
43
plugin-src/utils/calculateRadialGradient.ts
Normal 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];
|
||||||
|
};
|
|
@ -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';
|
||||||
|
|
Loading…
Reference in a new issue