From 23e97fb3d740e715ccf912f7eb84b92111aba0c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20S=C3=A1nchez?= Date: Mon, 13 May 2024 16:44:00 +0200 Subject: [PATCH] Gradients (#108) * linear gradient * radial gradients * fixes * fixes --- .changeset/sour-readers-wash.md | 5 ++ .../transformers/partials/transformFills.ts | 2 +- .../partials/transformVectorPaths.ts | 14 +--- .../text/translateStyleTextSegments.ts | 2 +- plugin-src/translators/translateFills.ts | 68 +++++++++++-------- plugin-src/translators/translateStrokes.ts | 2 +- plugin-src/utils/calculateLinearGradient.ts | 7 +- plugin-src/utils/calculateRadialGradient.ts | 43 ++++++++++++ plugin-src/utils/index.ts | 1 + 9 files changed, 97 insertions(+), 47 deletions(-) create mode 100644 .changeset/sour-readers-wash.md create mode 100644 plugin-src/utils/calculateRadialGradient.ts diff --git a/.changeset/sour-readers-wash.md b/.changeset/sour-readers-wash.md new file mode 100644 index 0000000..6c4456b --- /dev/null +++ b/.changeset/sour-readers-wash.md @@ -0,0 +1,5 @@ +--- +"penpot-exporter": minor +--- + +Added support for linear and radial gradients diff --git a/plugin-src/transformers/partials/transformFills.ts b/plugin-src/transformers/partials/transformFills.ts index f479ff2..5d11687 100644 --- a/plugin-src/transformers/partials/transformFills.ts +++ b/plugin-src/transformers/partials/transformFills.ts @@ -6,6 +6,6 @@ export const transformFills = async ( node: MinimalFillsMixin & DimensionAndPositionMixin ): Promise> => { return { - fills: await translateFills(node.fills, node.width, node.height) + fills: await translateFills(node.fills) }; }; diff --git a/plugin-src/transformers/partials/transformVectorPaths.ts b/plugin-src/transformers/partials/transformVectorPaths.ts index 83bc145..6d8755f 100644 --- a/plugin-src/transformers/partials/transformVectorPaths.ts +++ b/plugin-src/transformers/partials/transformVectorPaths.ts @@ -64,24 +64,14 @@ const transformVectorPath = async ( baseX: number, baseY: number ): Promise => { - const dimensionAndPosition = transformDimensionAndPositionFromVectorPath( - vectorPath, - baseX, - baseY - ); - return { type: 'path', name: 'svg-path', content: translateVectorPath(vectorPath, baseX + node.x, baseY + node.y), - fills: await translateFills( - vectorRegion?.fills ?? node.fills, - dimensionAndPosition.width, - dimensionAndPosition.height - ), + fills: await translateFills(vectorRegion?.fills ?? node.fills), ...(await transformStrokes(node)), ...transformEffects(node), - ...dimensionAndPosition, + ...transformDimensionAndPositionFromVectorPath(vectorPath, baseX, baseY), ...transformSceneNode(node), ...transformBlend(node), ...transformProportion(node) diff --git a/plugin-src/translators/text/translateStyleTextSegments.ts b/plugin-src/translators/text/translateStyleTextSegments.ts index b052445..a426646 100644 --- a/plugin-src/translators/text/translateStyleTextSegments.ts +++ b/plugin-src/translators/text/translateStyleTextSegments.ts @@ -49,7 +49,7 @@ const translateStyleTextSegment = async ( segment: StyleTextSegment ): Promise => { return { - fills: await translateFills(segment.fills, node.width, node.height), + fills: await translateFills(segment.fills), text: segment.characters, ...transformTextStyle(node, segment) }; diff --git a/plugin-src/translators/translateFills.ts b/plugin-src/translators/translateFills.ts index 1620f48..74b1e1b 100644 --- a/plugin-src/translators/translateFills.ts +++ b/plugin-src/translators/translateFills.ts @@ -1,19 +1,17 @@ -import { detectMimeType, rgbToHex } from '@plugin/utils'; +import { calculateRadialGradient, detectMimeType, rgbToHex } from '@plugin/utils'; import { calculateLinearGradient } from '@plugin/utils/calculateLinearGradient'; import { Fill } from '@ui/lib/types/utils/fill'; import { ImageColor } from '@ui/lib/types/utils/imageColor'; -export const translateFill = async ( - fill: Paint, - width: number, - height: number -): Promise => { +export const translateFill = async (fill: Paint): Promise => { switch (fill.type) { case 'SOLID': return translateSolidFill(fill); case 'GRADIENT_LINEAR': - return translateGradientLinearFill(fill, width, height); + return translateGradientLinearFill(fill); + case 'GRADIENT_RADIAL': + return translateGradientRadialFill(fill); case 'IMAGE': return await translateImageFill(fill); } @@ -22,15 +20,13 @@ export const translateFill = async ( }; export const translateFills = async ( - fills: readonly Paint[] | typeof figma.mixed, - width: number, - height: number + fills: readonly Paint[] | typeof figma.mixed ): Promise => { const figmaFills = fills === figma.mixed ? [] : fills; const penpotFills: Fill[] = []; for (const fill of figmaFills) { - const penpotFill = await translateFill(fill, width, height); + const penpotFill = await translateFill(fill); if (penpotFill) { // fills are applied in reverse order in Figma, that's why we unshift penpotFills.unshift(penpotFill); @@ -88,29 +84,43 @@ const translateSolidFill = (fill: SolidPaint): Fill => { }; }; -const translateGradientLinearFill = (fill: GradientPaint, width: number, height: number): Fill => { - const points = calculateLinearGradient(width, height, fill.gradientTransform); +const translateGradientLinearFill = (fill: GradientPaint): Fill => { + const points = calculateLinearGradient(fill.gradientTransform); return { fillColorGradient: { type: 'linear', - startX: points.start[0] / width, - startY: points.start[1] / height, - endX: points.end[0] / width, - endY: points.end[1] / height, + startX: points.start[0], + startY: points.start[1], + endX: points.end[0], + endY: points.end[1], width: 1, - stops: [ - { - color: rgbToHex(fill.gradientStops[0].color), - offset: fill.gradientStops[0].position, - opacity: fill.gradientStops[0].color.a * (fill.opacity ?? 1) - }, - { - color: rgbToHex(fill.gradientStops[1].color), - offset: fill.gradientStops[1].position, - opacity: fill.gradientStops[1].color.a * (fill.opacity ?? 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 + }; +}; + +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 }; diff --git a/plugin-src/translators/translateStrokes.ts b/plugin-src/translators/translateStrokes.ts index 1cfba8d..4e52f52 100644 --- a/plugin-src/translators/translateStrokes.ts +++ b/plugin-src/translators/translateStrokes.ts @@ -10,7 +10,7 @@ export const translateStrokes = async ( ): Promise => { return await Promise.all( nodeStrokes.strokes.map(async (paint, index) => { - const fill = await translateFill(paint, 0, 0); + const fill = await translateFill(paint); const stroke: Stroke = { strokeColor: fill?.fillColor, strokeOpacity: fill?.fillOpacity, diff --git a/plugin-src/utils/calculateLinearGradient.ts b/plugin-src/utils/calculateLinearGradient.ts index ecf818e..e1b4008 100644 --- a/plugin-src/utils/calculateLinearGradient.ts +++ b/plugin-src/utils/calculateLinearGradient.ts @@ -1,7 +1,7 @@ import { applyMatrixToPoint } from '@plugin/utils/applyMatrixToPoint'; 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 mxInv = matrixInvert(transform); @@ -16,8 +16,9 @@ export const calculateLinearGradient = (shapeWidth: number, shapeHeight: number, [0, 0.5], [1, 0.5] ].map(p => applyMatrixToPoint(mxInv, p)); + return { - start: [startEnd[0][0] * shapeWidth, startEnd[0][1] * shapeHeight], - end: [startEnd[1][0] * shapeWidth, startEnd[1][1] * shapeHeight] + start: [startEnd[0][0], startEnd[0][1]], + end: [startEnd[1][0], startEnd[1][1]] }; }; diff --git a/plugin-src/utils/calculateRadialGradient.ts b/plugin-src/utils/calculateRadialGradient.ts new file mode 100644 index 0000000..b908ba1 --- /dev/null +++ b/plugin-src/utils/calculateRadialGradient.ts @@ -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]; +}; diff --git a/plugin-src/utils/index.ts b/plugin-src/utils/index.ts index 724c468..4d21894 100644 --- a/plugin-src/utils/index.ts +++ b/plugin-src/utils/index.ts @@ -1,6 +1,7 @@ export * from './applyMatrixToPoint'; export * from './calculateAdjustment'; export * from './calculateLinearGradient'; +export * from './calculateRadialGradient'; export * from './detectMimeType'; export * from './getBoundingBox'; export * from './matrixInvert';