Skip to content

Commit 1f3461a

Browse files
committed
Add Text styles to all frameworks.
1 parent 9752f62 commit 1f3461a

File tree

8 files changed

+189
-48
lines changed

8 files changed

+189
-48
lines changed

apps/plugin/plugin-src/code.ts

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { tailwindCodeGenTextStyles } from "./../../../packages/backend/src/tailwind/tailwindMain";
12
import {
23
run,
34
flutterMain,
@@ -8,6 +9,9 @@ import {
89
PluginSettings,
910
} from "backend";
1011
import { retrieveGenericSolidUIColors } from "backend/src/common/retrieveUI/retrieveColors";
12+
import { flutterCodeGenTextStyles } from "backend/src/flutter/flutterMain";
13+
import { htmlCodeGenTextStyles } from "backend/src/html/htmlMain";
14+
import { swiftUICodeGenTextStyles } from "backend/src/swiftui/swiftuiMain";
1115

1216
let userPluginSettings: PluginSettings;
1317

@@ -109,6 +113,7 @@ switch (figma.mode) {
109113

110114
figma.codegen.on("generate", ({ language, node }) => {
111115
const convertedSelection = convertIntoNodes([node], null);
116+
console.log("language is", language);
112117

113118
switch (language) {
114119
case "html":
@@ -122,6 +127,11 @@ switch (figma.mode) {
122127
),
123128
language: "HTML",
124129
},
130+
{
131+
title: `Text Styles`,
132+
code: htmlCodeGenTextStyles(false),
133+
language: "HTML",
134+
},
125135
];
126136
case "html_jsx":
127137
return [
@@ -134,6 +144,11 @@ switch (figma.mode) {
134144
),
135145
language: "HTML",
136146
},
147+
{
148+
title: `Text Styles`,
149+
code: htmlCodeGenTextStyles(true),
150+
language: "HTML",
151+
},
137152
];
138153
case "tailwind":
139154
return [
@@ -145,18 +160,18 @@ switch (figma.mode) {
145160
}),
146161
language: "HTML",
147162
},
148-
// {
149-
// title: `Style`,
150-
// code: tailwindMain(convertedSelection, defaultPluginSettings),
151-
// language: "HTML",
152-
// },
153163
{
154164
title: `Colors`,
155165
code: retrieveGenericSolidUIColors("Tailwind")
156166
.map((d) => `#${d.hex} <- ${d.colorName}`)
157167
.join("\n"),
158168
language: "HTML",
159169
},
170+
{
171+
title: `Text Styles`,
172+
code: tailwindCodeGenTextStyles(),
173+
language: "HTML",
174+
},
160175
];
161176
case "tailwind_jsx":
162177
return [
@@ -180,6 +195,11 @@ switch (figma.mode) {
180195
.join("\n"),
181196
language: "HTML",
182197
},
198+
{
199+
title: `Text Styles`,
200+
code: tailwindCodeGenTextStyles(),
201+
language: "HTML",
202+
},
183203
];
184204
case "flutter":
185205
return [
@@ -191,25 +211,35 @@ switch (figma.mode) {
191211
}),
192212
language: "SWIFT",
193213
},
214+
{
215+
title: `Text Styles`,
216+
code: flutterCodeGenTextStyles(),
217+
language: "SWIFT",
218+
},
194219
];
195-
case "swiftui":
220+
case "swiftUI":
196221
return [
197222
{
198223
title: `SwiftUI`,
199224
code: swiftuiMain(convertedSelection, defaultPluginSettings),
200225
language: "SWIFT",
201226
},
227+
{
228+
title: `Text Styles`,
229+
code: swiftUICodeGenTextStyles(),
230+
language: "SWIFT",
231+
},
202232
];
203233
default:
204234
break;
205235
}
206236

207237
const blocks: CodegenResult[] = [
208-
{
209-
title: `Code`,
210-
code: tailwindMain(convertedSelection, defaultPluginSettings),
211-
language: "HTML",
212-
},
238+
// {
239+
// title: `Code`,
240+
// code: tailwindMain(convertedSelection, defaultPluginSettings),
241+
// language: "HTML",
242+
// },
213243
// {
214244
// title: `Flutter`,
215245
// code: flutterMain(convertedSelection, defaultPluginSettings),

packages/backend/src/common/retrieveUI/retrieveColors.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ import {
1313
flutterColor,
1414
flutterGradient,
1515
} from "../../flutter/builderImpl/flutterColor";
16-
import { htmlColor, htmlGradient } from "../../html/builderImpl/htmlColor";
16+
import {
17+
htmlColor,
18+
htmlGradientFromFills,
19+
} from "../../html/builderImpl/htmlColor";
1720
import { calculateContrastRatio } from "./commonUI";
1821
import { FrameworkTypes } from "../../code";
1922

@@ -96,7 +99,7 @@ export const retrieveGenericLinearGradients = (
9699
exported = flutterGradient(paint);
97100
break;
98101
case "HTML":
99-
exported = htmlGradient(paint);
102+
exported = htmlGradientFromFills([paint]);
100103
break;
101104
case "Tailwind":
102105
exported = tailwindGradient(paint);
@@ -105,7 +108,10 @@ export const retrieveGenericLinearGradients = (
105108
exported = swiftuiGradient(paint);
106109
break;
107110
}
108-
colorStr.push({ cssPreview: htmlGradient(paint), exportValue: exported });
111+
colorStr.push({
112+
cssPreview: htmlGradientFromFills([paint]),
113+
exportValue: exported,
114+
});
109115
}
110116
});
111117

packages/backend/src/flutter/flutterMain.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from "./builderImpl/flutterAutoLayout";
1111

1212
let localSettings: PluginSettings;
13+
let previousExecutionCache: string[];
1314

1415
const getFullAppTemplate = (name: string, injectCode: string): string =>
1516
`import 'package:flutter/material.dart';
@@ -57,6 +58,7 @@ export const flutterMain = (
5758
settings: PluginSettings
5859
): string => {
5960
localSettings = settings;
61+
previousExecutionCache = [];
6062

6163
let result = flutterWidgetGenerator(sceneNode);
6264
switch (localSettings.flutterGenerationMode) {
@@ -166,13 +168,13 @@ const flutterContainer = (node: SceneNode, child: string): string => {
166168
};
167169

168170
const flutterText = (node: TextNode): string => {
169-
const builder = new FlutterTextBuilder()
170-
.createText(node)
171+
const builder = new FlutterTextBuilder().createText(node);
172+
previousExecutionCache.push(builder.child);
173+
174+
return builder
171175
.blendAttr(node)
172176
.textAutoSize(node)
173-
.position(node, localSettings.optimizeLayout);
174-
175-
return builder.child;
177+
.position(node, localSettings.optimizeLayout).child;
176178
};
177179

178180
const flutterFrame = (
@@ -232,3 +234,6 @@ const addSpacingIfNeeded = (node: SceneNode): string => {
232234
}
233235
return "";
234236
};
237+
238+
export const flutterCodeGenTextStyles = () =>
239+
previousExecutionCache.map((style) => `${style}`).join("\n// ---\n");

packages/backend/src/html/builderImpl/htmlColor.ts

Lines changed: 90 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { gradientAngle } from "../../common/color";
21
import { sliceNum } from "../../common/numToAutoFixed";
32
import { retrieveTopFill } from "../../common/retrieveFill";
43

@@ -50,34 +49,104 @@ export const htmlGradientFromFills = (
5049
): string => {
5150
const fill = retrieveTopFill(fills);
5251
if (fill?.type === "GRADIENT_LINEAR") {
53-
return htmlGradient(fill);
52+
return htmlLinearGradient(fill);
53+
} else if (fill?.type === "GRADIENT_ANGULAR") {
54+
return htmlAngularGradient(fill);
55+
} else if (fill?.type === "GRADIENT_RADIAL") {
56+
return htmlRadialGradient(fill);
5457
}
5558
return "";
5659
};
5760

58-
// This was separated from htmlGradient because it is going to be used in the plugin UI and it wants all gradients, not only the top one.
59-
export const htmlGradient = (fill: GradientPaint): string => {
60-
// Adjust angle for HTML.
61-
const angle = (gradientAngle(fill) + 90).toFixed(0);
61+
export const gradientAngle2 = (fill: GradientPaint): number => {
62+
const x1 = fill.gradientTransform[0][2];
63+
const y1 = fill.gradientTransform[1][2];
64+
const x2 = fill.gradientTransform[0][0] + x1;
65+
const y2 = fill.gradientTransform[1][0] + y1;
66+
const dx = x2 - x1;
67+
const dy = y1 - y2;
68+
const radians = Math.atan2(dy, dx);
69+
const unadjustedAngle = (radians * 180) / Math.PI;
70+
const adjustedAngle = unadjustedAngle + 90;
71+
return adjustedAngle;
72+
};
73+
74+
export const cssGradientAngle = (angle: number): number => {
75+
// Convert Figma angle to CSS angle.
76+
const cssAngle = angle; // Subtract 235 to make it start from the correct angle.
77+
// Normalize angle: if negative, add 360 to make it positive.
78+
return cssAngle < 0 ? cssAngle + 360 : cssAngle;
79+
};
80+
81+
export const htmlLinearGradient = (fill: GradientPaint): string => {
82+
// Adjust angle for CSS.
83+
const figmaAngle = gradientAngle2(fill);
84+
const angle = cssGradientAngle(figmaAngle).toFixed(0);
6285

6386
const mappedFill = fill.gradientStops
64-
.map((stop, index, stops) => {
65-
const alpha = (stop.color.a * (fill.opacity ?? 1)).toFixed(2);
66-
const color = `rgba(${Math.round(stop.color.r * 255)}, ${Math.round(
67-
stop.color.g * 255
68-
)}, ${Math.round(stop.color.b * 255)}, ${alpha})`;
69-
70-
// Calculate position for all stops except the first and last ones.
71-
const position =
72-
index > 0 && index < stops.length - 1
73-
? ` ${(stop.position * 100).toFixed(0)}%`
74-
: index === 0
75-
? " 0%"
76-
: " 100%";
77-
78-
return `${color}${position}`;
87+
.map((stop) => {
88+
const color = htmlColor(stop.color, stop.color.a * (fill.opacity ?? 1));
89+
const position = `${(stop.position * 100).toFixed(0)}%`;
90+
return `${color} ${position}`;
7991
})
8092
.join(", ");
8193

8294
return `linear-gradient(${angle}deg, ${mappedFill})`;
8395
};
96+
97+
export const invertYCoordinate = (y: number): number => 1 - y;
98+
99+
export const getGradientTransformCoordinates = (
100+
gradientTransform: number[][]
101+
): { centerX: string; centerY: string; radiusX: string; radiusY: string } => {
102+
const a = gradientTransform[0][0];
103+
const b = gradientTransform[0][1];
104+
const c = gradientTransform[1][0];
105+
const d = gradientTransform[1][1];
106+
const e = gradientTransform[0][2];
107+
const f = gradientTransform[1][2];
108+
109+
const scaleX = Math.sqrt(a ** 2 + b ** 2);
110+
const scaleY = Math.sqrt(c ** 2 + d ** 2);
111+
112+
const rotationAngle = Math.atan2(b, a);
113+
114+
const centerX = ((e * scaleX * 100) / (1 - scaleX)).toFixed(2);
115+
const centerY = (((1 - f) * scaleY * 100) / (1 - scaleY)).toFixed(2);
116+
117+
const radiusX = (scaleX * 100).toFixed(2);
118+
const radiusY = (scaleY * 100).toFixed(2);
119+
120+
return { centerX, centerY, radiusX, radiusY };
121+
};
122+
123+
export const htmlRadialGradient = (fill: GradientPaint): string => {
124+
const mappedFill = fill.gradientStops
125+
.map((stop) => {
126+
const color = htmlColor(stop.color, stop.color.a * (fill.opacity ?? 1));
127+
const position = `${(stop.position * 100).toFixed(0)}%`;
128+
return `${color} ${position}`;
129+
})
130+
.join(", ");
131+
132+
const { centerX, centerY, radiusX, radiusY } =
133+
getGradientTransformCoordinates(fill.gradientTransform);
134+
135+
return `radial-gradient(${radiusX}% ${radiusY}% at ${centerX}% ${centerY}%, ${mappedFill})`;
136+
};
137+
138+
export const htmlAngularGradient = (fill: GradientPaint): string => {
139+
const angle = gradientAngle2(fill).toFixed(0);
140+
const centerX = (fill.gradientTransform[0][2] * 100).toFixed(2);
141+
const centerY = (fill.gradientTransform[1][2] * 100).toFixed(2);
142+
143+
const mappedFill = fill.gradientStops
144+
.map((stop) => {
145+
const color = htmlColor(stop.color, stop.color.a * (fill.opacity ?? 1));
146+
const position = `${(stop.position * 360).toFixed(0)}deg`;
147+
return `${color} ${position}`;
148+
})
149+
.join(", ");
150+
151+
return `conic-gradient(from ${angle}deg at ${centerX}% ${centerY}%, ${mappedFill})`;
152+
};

packages/backend/src/html/htmlDefaultBuilder.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,11 @@ export class HtmlDefaultBuilder {
193193
if (paint.type === "SOLID") {
194194
const color = htmlColorFromFills([paint]);
195195
return `linear-gradient(0deg, ${color} 0%, ${color} 100%)`;
196-
} else if (paint.type === "GRADIENT_LINEAR") {
196+
} else if (
197+
paint.type === "GRADIENT_LINEAR" ||
198+
paint.type === "GRADIENT_RADIAL" ||
199+
paint.type === "GRADIENT_ANGULAR"
200+
) {
197201
return htmlGradientFromFills([paint]);
198202
}
199203

packages/backend/src/html/htmlMain.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const selfClosingTags = ["img"];
1313
export let isPreviewGlobal = false;
1414

1515
let localSettings: PluginSettings;
16+
let previousExecutionCache: { style: string; text: string }[];
1617

1718
export const htmlMain = (
1819
sceneNode: Array<SceneNode>,
@@ -21,6 +22,7 @@ export const htmlMain = (
2122
): string => {
2223
showLayerName = settings.layerName;
2324
isPreviewGlobal = isPreview;
25+
previousExecutionCache = [];
2426
localSettings = settings;
2527

2628
let result = htmlWidgetGenerator(sceneNode, settings.jsx);
@@ -114,6 +116,7 @@ export const htmlText = (node: TextNode, isJsx: boolean): string => {
114116
.textAlign(node);
115117

116118
const styledHtml = layoutBuilder.getTextSegments(node.id);
119+
previousExecutionCache.push(...styledHtml);
117120

118121
let content = "";
119122
if (styledHtml.length === 1) {
@@ -262,3 +265,12 @@ export const htmlLine = (node: LineNode, isJsx: boolean): string => {
262265

263266
return `\n<div${builder.build()}></div>`;
264267
};
268+
269+
export const htmlCodeGenTextStyles = (isJsx: boolean) => {
270+
return previousExecutionCache
271+
.map(
272+
(style) =>
273+
`// ${style.text}\n${style.style.split(isJsx ? "," : ";").join(";\n")}`
274+
)
275+
.join("\n---\n");
276+
};

0 commit comments

Comments
 (0)