最近使用AI做还原视觉相关的提效项目,在AI还没出来之前也刚好做过Figma相关的实践和项目,也趁此机会温故知新一下,分享给有需要的童鞋~
一、Figma 原始数据结构
目前通过AI获取到的figma数据,通常都经过一层转换,不是figma的原始数据,要想获取figma的原始数据结构,你需要通过api获取,返回的应该是一个JSON格式的数据结构,主要包含以下内容:
1. 文件基本信息
{
"name": "haha 教程【一期】",// 文件名称
"lastModified": "2022-03-05T03:04:14Z",// 最近修改时间
"thumbnailUrl": "https://xxxxx",// 缩略图
"version": "2327290316033362691",
"role": "viewer",
"editorType": "figma",
"linkAccess": "inherit",
"nodes": {} // 内容信息
}
2. 内容信息(nodes)
"nodes": {
"988:122835": {
"document": {},// 文档信息
"components": {// 组件相关
"543:105272": {
"key": "a859937fe23b4182a41301d18670ff3d722a0c85",
"name": "按钮",
"description": "",
"remote": true,
"documentationLinks": []
},
"14:450": {
"key": "82d6496dfd8eb1f47924bf4e77acd32effc6506c",
"name": "tipsicon",
"description": "",
"remote": true,
"documentationLinks": []
}
},
"componentSets": {},
"schemaVersion": 0,
"styles": {}// 样式信息
}
}
3. 文档结构 (document)
"document": {
"id": "988:122835",
"name": "Frame 2036096513",
"type": "FRAME",
"scrollBehavior": "SCROLLS",
"children": [],// 页面内容
}
4. 节点信息
也就是上述children里面的内容信息,里面会包含各种各样的节点,节点类型总览如下:
| 类型 |
说明 |
有子节点 |
特殊属性 |
| DOCUMENT |
文档根 |
✅ |
- |
| CANVAS |
页面 |
✅ |
backgroundColor |
| FRAME |
画板 |
✅ |
layoutMode, clipsContent |
| GROUP |
分组 |
✅ |
- |
| SECTION |
分区 |
✅ |
FigJam 功能 |
| COMPONENT |
组件定义 |
✅ |
componentPropertyDefinitions |
| COMPONENT_SET |
变体集 |
✅ |
- |
| INSTANCE |
组件实例 |
✅ |
componentId, overrides |
| RECTANGLE |
矩形 |
❌ |
cornerRadius |
| ELLIPSE |
椭圆 |
❌ |
arcData |
| LINE |
线条 |
❌ |
- |
| VECTOR |
矢量 |
❌ |
fillGeometry |
| TEXT |
文本 |
❌ |
characters, style |
| BOOLEAN_OPERATION |
布尔运算 |
✅ |
booleanOperation |
| SLICE |
切片 |
❌ |
导出用 |
4.1. 节点通用属性
{
"id": "953:118218", // 节点唯一ID
"name": "节点名称", // 图层名称
"type": "FRAME", // 节点类型
"visible": true, // 是否可见
"locked": false, // 是否锁定
"opacity": 1.0, // 透明度 0-1
"blendMode": "PASS_THROUGH", // 混合模式
"absoluteBoundingBox": { // 绝对定位和尺寸
"x": -2981.0,
"y": 6883.0,
"width": 96.0,
"height": 96.0
},
"absoluteRenderBounds": { // 渲染定位和尺寸
"x": -2981.0,
"y": 6883.0,
"width": 96.0,
"height": 96.0
},
"constraints": { // 约束
"vertical": "TOP",
"horizontal": "LEFT"
},
"effects": [], // 效果(阴影、模糊等)
"fills": [], // 填充
"strokes": [], // 描边
"children": [] // 子节点(容器类型)
}
4.2. 各节点类型详解
1. DOCUMENT(文档根节点)
{
"id": "0:0",
"name": "Document",
"type": "DOCUMENT",
"children": [] // CANVAS 节点数组
}
2. CANVAS(页面/画布)
{
"id": "0:1",
"name": "Page Canvas",
"type": "CANVAS",
"backgroundColor": {
"r": 0.96,
"g": 0.96,
"b": 0.96,
"a": 1
},
"children": [] // 页面内的所有元素
}
每个 CANVAS 代表一个页面
3. FRAME(画板/框架)
{
"type": "FRAME",
"clipsContent": true, // 是否裁剪超出内容
"layoutMode": "VERTICAL", // 自动布局方向
"primaryAxisSizingMode": "AUTO",
"counterAxisSizingMode": "FIXED",
"paddingLeft": 16,
"paddingRight": 16,
"paddingTop": 16,
"paddingBottom": 16,
"itemSpacing": 8, // 子元素间距
"layoutAlign": "STRETCH",
"cornerRadius": 8, // 圆角
"children": []
}
这是一种最常见的节点,绝大部分节点的类型都是它
4. GROUP(组)
{
"id": "953:118473",
"name": "Group 2036096687",
"type": "GROUP",
"scrollBehavior": "SCROLLS",
"children": []
}
通常children会有很多内容,但它纯粹的分组,没有自己的样式,样式来自子元素
5. RECTANGLE(矩形)
{
"id": "953:118218",
"name": "2",
"type": "RECTANGLE",
"scrollBehavior": "SCROLLS",
"cornerRadius": 8, // 统一圆角
"rectangleCornerRadii": [
8,
8,
0,
0
], // 分别设置四角
"fills": [
{
"type": "SOLID",
"color": {
"r": 1,
"g": 0,
"b": 0,
"a": 1
}
}
],
"strokes": [
{
"type": "SOLID",
"color": {
"r": 0,
"g": 0,
"b": 0,
"a": 1
}
}
],
"strokeWeight": 1,
"strokeAlign": "INSIDE" // INSIDE | OUTSIDE | CENTER
}
6. ELLIPSE(椭圆/圆形)
{
"id": "I953:118474;953:115456",
"name": "Ellipse 12175",
"type": "ELLIPSE",
"effects": [
{
"type": "LAYER_BLUR",
"visible": true,
"radius": 4.0
}
],
"arcData": {
"startingAngle": 0.0,
"endingAngle": 6.2831854820251465, // 2π = 完整圆
"innerRadius": 0.0 // >0 为环形
},
"interactions": [],
"complexStrokeProperties": {
"strokeType": "BASIC"
}
}
7. TEXT(文本)
{
"id": "953:118226",
"name": "95% OFF",
"type": "TEXT",
"strokes": [],
"strokeWeight": 1.3939393758773804,
"strokeAlign": "OUTSIDE",
"absoluteBoundingBox": {
"x": -2971.0,
"y": 6882.847167968750,
"width": 33.0,
"height": 25.0
},
"absoluteRenderBounds": {
"x": -2969.893066406250,
"y": 6886.439941406250,
"width": 30.792480468750,
"height": 19.55957031250
},
"constraints": {
"vertical": "TOP",
"horizontal": "LEFT"
},
"characters": "95%\nOFF", // 文本内容
"characterStyleOverrides": [
21,
21,
21,
21,
20,
20,
20
],
"styleOverrideTable": {
"21": {
"fontSize": 13.0
},
"20": {
"fontSize": 8.0,
"fills": [
{
"blendMode": "NORMAL",
"type": "SOLID",
"color": {
"r": 1.0,
"g": 1.0,
"b": 1.0,
"a": 1.0
}
}
]
}
},
"lineTypes": [
"NONE",
"NONE"
],
"lineIndentations": [
0,
0
],
"style": { // 文本样式
"fontFamily": "SF Pro",
"fontPostScriptName": "SFPro-Heavy",
"fontStyle": "Heavy",
"fontWeight": 860,
"textAutoResize": "WIDTH_AND_HEIGHT",
"fontSize": 13.939393997192383,
"textAlignHorizontal": "CENTER",
"textAlignVertical": "BOTTOM",
"letterSpacing": 0.0,
"lineHeightPx": 16.634706497192383,
"lineHeightPercent": 100.0,
"lineHeightUnit": "INTRINSIC_%"
}
}
重点关注:
-
characters: 纯文本内容
-
style: 文本样式
-
characterStyleOverrides + styleOverrideTable: 处理富文本
8. VECTOR(矢量路径)
{
"type": "VECTOR",
"strokeCap": "ROUND",
"strokeJoin": "ROUND",
"fillGeometry": [], // 填充路径数据
"strokeGeometry": [] // 描边路径数据
}
9. BOOLEAN_OPERATION(布尔运算)
{
"id": "953:118223",
"name": "Union",
"type": "BOOLEAN_OPERATION",
"scrollBehavior": "SCROLLS",
"children": [],
"blendMode": "PASS_THROUGH",
"fills": [],
"strokes": [],
"strokeWeight": 1.3939393758773804,
"strokeAlign": "OUTSIDE",
"booleanOperation": "UNION",
"exportSettings": [
{
"suffix": "",
"format": "PNG",
"constraint": {
"type": "SCALE",
"value": 1.0
}
}
],
"effects": [
{
"type": "DROP_SHADOW",
"visible": true,
"color": {
"r": 0.0,
"g": 0.0,
"b": 0.0,
"a": 0.250
},
"blendMode": "NORMAL",
"offset": {
"x": -1.3939393758773804,
"y": 0.0
},
"radius": 2.7878787517547607,
"showShadowBehindNode": false
}
],
"interactions": []
}
10. INSTANCE(组件实例)
{
"id": "953:118542",
"name": "Mask group",
"type": "INSTANCE",
"scrollBehavior": "SCROLLS",
"componentId": "953:117624", // 引用的组件ID
"overrides": [], // 覆盖的属性
"children": []
}
这个也比较重要,通过 componentId 可以找到对应的 COMPONENT
11. COMPONENT(组件定义)
{
"type": "COMPONENT",
"componentPropertyDefinitions": {
"Text#1:1": {
"type": "TEXT",
"defaultValue": "Button"
},
"Variant": {
"type": "VARIANT",
"variantOptions": [
"Primary",
"Secondary"
]
}
},
"children": []
}
12. COMPONENT_SET(组件集/变体)
{
"type": "COMPONENT_SET",
"componentPropertyDefinitions": {
"State": {
"type": "VARIANT",
"variantOptions": [
"Default",
"Hover",
"Active"
]
},
"Size": {
"type": "VARIANT",
"variantOptions": [
"Small",
"Medium",
"Large"
]
}
},
"children": []
}
这个不多见,跟上述11的COMPONENT一样,只是它的一种集合体
5. 图片信息
有没有发现,上述的节点信息中并没有图片信息,但图片作为设计稿中最不可或缺的元素,怎么能没有图片类型的节点呢?
这是个好问题,然而Figma 中确实没有独立的 IMAGE 节点类型,下面让我们来了解一下图片信息是如何在Figma中存储的吧。
Figma 图片的存储方式
在 Figma 中,图片不是一种独立的节点类型,而是作为填充属性(fills) 附加在其他形状节点上:
你以为的图片存储方式或许是这样:
{ "type": "IMAGE", "src": "xxx.png" }
❌ 没有这种节点
✅ 实际是这样的:
{
"id": "953:118218",
"type": "RECTANGLE", // 载体是矩形
"fills": [
{
"blendMode": "NORMAL", // 填充类型是图片
"type": "IMAGE", // 表示填充类型是图片
"scaleMode": "FILL", // 图片缩放模式:`FILL`、`FIT`、`STRETCH`、`TILE`
"imageRef": "fb233719d2b7c04499361d3052e21e9f32a1ca8d", // 图片的唯一哈希标识符,用于获取实际图片
"imageTransform": [ // 2x3 变换矩阵,控制图片的缩放、旋转、位移
[
0.81347656250,
0.0,
0.09082031250
],
[
0.0,
0.86035162210464478,
0.093750
]
]
}
]
}
如何获取实际图片
Figma 不直接存储图片数据,而是通过 imageRef 引用。要获取实际图片,需要调用:
# 方法1:获取文件中所有图片
GET https://api.figma.com/v1/files/{file_key}/images
# 方法2:导出指定节点为图片
GET https://api.figma.com/v1/images/{file_key}?ids={node_id}&format=png
图片引用机制
┌─────────────────────────────────────────────────────┐
│ Figma JSON 数据 │
│ ┌───────────────────────────────────────────────┐ │
│ │ imageRef: "843b1885be37478f2e04f7f82e52d481..." │ │
│ └───────────────────────┬───────────────────────┘ │
└──────────────────────────┼──────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Figma 图片服务器 │
│ 通过 /v1/files/{key}/images API 获取 │
│ 返回: { imageRef: "https://s3-xxx.amazonaws.com/..."} │
└─────────────────────────────────────────────────────┘
imageTransform 变换矩阵
"imageTransform": [
[0.81347656250, 0.0, 0.09082031250], // [scaleX, skewX, translateX]
[0.0, 0.86035162210464478, 0.093750] // [skewY, scaleY, translateY]
]
- 第一行:X 轴缩放 81.3%,X 方向偏移 9.1%
- 第二行:Y 轴缩放 86%,Y 方向偏移 9.4%
总结:Figma 通过 imageRef 哈希值引用图片,实际图片数据存储在 Figma 的云端服务器,需要通过 API 单独获取图片 URL,因此这里需要考虑token过期的问题。
6. 节点解析
我们拿到这些原始数据之后,是需要处理的,因此我们需要对各种节点信息解析
const parseNode = (node) => {
if (!node) return null
// console.log('parseNode node==', node)
const base = {
id: node.id,
name: node.name,
type: node.type,
bounds: node.absoluteBoundingBox,
};
switch (node.type) {
case "TEXT":
return {
...base,
text: node.characters,
style: node.style,
};
case "RECTANGLE":
case "ELLIPSE":
return {
...base,
fills: parseFills(node.fills),
cornerRadius: node.cornerRadius,
};
case "FRAME":
case "GROUP":
case "COMPONENT":
case "INSTANCE":
return {
...base,
children: node.children?.map(parseNode) || [],
};
case "VECTOR":
return {
...base,
fills: parseFills(node.fills),
strokes: node.strokes,
};
default:
return base;
}
};
const parseFills = (fills) => {
return fills
?.filter((f) => f.visible !== false)
.map((fill) => {
console.log('fill===', fill)
switch (fill.type) {
case "SOLID":
return { type: "solid", color: rgbaToHex(fill.color) };
case "IMAGE":
return { type: "image", ref: fill.imageRef };
case "GRADIENT_LINEAR":
return { type: "gradient", stops: fill.gradientStops };
default:
return fill;
}
});
};
二、Figma位置信息转换
1. 位置信息
a. 核心位置属性
{
"layoutMode": "HORIZONTAL",
"counterAxisSizingMode": "FIXED",
"primaryAxisSizingMode": "FIXED",
"counterAxisAlignItems": "CENTER",
"primaryAxisAlignItems": "CENTER",
"paddingLeft": 8.0,
"paddingRight": 8.0,
"paddingTop": 3.0,
"paddingBottom": 3.0,
"itemSpacing": 8.0,
"layoutWrap": "NO_WRAP",
"absoluteBoundingBox": { // 绝对位置(相对于画布原点)
"x": 100,
"y": 200,
"width": 300,
"height": 150
},
"absoluteRenderBounds": { // 实际渲染边界(包含阴影等效果)
"x": 95,
"y": 195,
"width": 310,
"height": 160
},
"relativeTransform": [ // 相对于父节点的变换矩阵
[4, 0, 16], // [scaleX, skewX, translateX]
[0, 8, 12] // [skewY, scaleY, translateY]
]
}
b. 属性对比
| 属性 |
说明 |
用途 |
absoluteBoundingBox |
相对画布的绝对坐标 |
全局定位 |
absoluteRenderBounds |
包含效果的实际渲染区域 |
导出时使用 |
relativeTransform |
相对父节点的变换矩阵 |
转换相对位置的关键 |
2、计算相对位置
方法1:通过 relativeTransform 获取
const getRelativePosition = (node) => {
const transform = node?.relativeTransform;
// 处理位置换换
if (transform && transform.length && transform?.[0].length) {
return {
x: transform[0][2], // translateX
y: transform[1][2], // translateY
};
}
return null;
};
方法2:通过绝对坐标计算
// 绝对位置计算
const calculateRelativePosition = (child, parent) => {
console.log('=====calculateRelativePosition', child, parent)
if (!child) return
const childBox = child?.absoluteBoundingBox;
const parentBox = parent?.absoluteBoundingBox;
return {
x: childBox?.x - parentBox?.x || 0,
y: childBox?.y - parentBox?.y || 0,
width: childBox?.width,
height: childBox?.height,
};
};
3、转换为 CSS 的几种方式
场景1:绝对定位
function toAbsoluteCSS(node, parent) {
const rel = calculateRelativePosition(node, parent);
return {
position: "absolute",
left: `${rel.x}px`,
top: `${rel.y}px`,
width: `${rel.width}px`,
height: `${rel.height}px`,
};
}
场景2:Flex 布局(Auto Layout)
当 Figma 使用 Auto Layout 时:
const toFlexCSS = (node) => {
if (node.layoutMode === "NONE") return null;
console.log('node==', node)
return {
display: "flex",
flexDirection: node.layoutMode === "VERTICAL" ? "column" : "row",
gap: `${node.itemSpacing}px`,
padding: `${node.paddingTop}px ${node.paddingRight}px ${node.paddingBottom}px ${node.paddingLeft}px`,
alignItems: mapAlignItems(node.counterAxisAlignItems),
justifyContent: mapJustifyContent(node.primaryAxisAlignItems),
};
};
const mapAlignItems = (value) => {
consle.log('=====value', value);
// 这里处理下转换
const map = {
MIN: "flex-start",
CENTER: "center",
MAX: "flex-end",
BASELINE: "baseline",
};
return map[value] || "flex-start";
};
场景3:响应式约束(constraints)
const constraintsToCSS = (node, parent) => {
// console.log('constraintsToCSS', node, parent)
const { constraints } = node || {};
const rel = calculateRelativePosition(node, parent);
const css = { position: "absolute" };
// 水平约束
switch (constraints?.horizontal) {
case "LEFT":
css.left = `${rel.x}px`;
css.width = `${rel.width}px`;
break;
case "RIGHT":
css.right = `${parent?.absoluteBoundingBox?.width - rel.x - rel.width}px`;
css.width = `${rel.width}px`;
break;
case "LEFT_RIGHT": // 左右拉伸
css.left = `${rel.x}px`;
css.right = `${parent?.absoluteBoundingBox?.width - rel.x - rel.width}px`;
break;
case "CENTER":
css.left = "50%";
css.transform = "translateX(-50%)";
css.width = `${rel.width}px`;
break;
case "SCALE": // 按比例缩放
css.left = `${(rel.x / parent?.absoluteBoundingBox?.width) * 100}%`;
css.width = `${(rel.width / parent?.absoluteBoundingBox?.width) * 100}%`;
break;
}
//console.log('=====css1', css)
// 垂直约束(类似逻辑)
switch (constraints?.vertical) {
case "TOP":
css.top = `${rel.y}px`;
css.height = `${rel.height}px`;
break;
case "BOTTOM":
css.bottom = `${parent?.absoluteBoundingBox?.height - rel.y - rel.height}px`;
css.height = `${rel.height}px`;
break;
case "TOP_BOTTOM":
css.top = `${rel.y}px`;
css.bottom = `${parent?.absoluteBoundingBox?.height - rel.y - rel.height}px`;
break;
case "CENTER":
css.top = "50%";
css.transform = (css.transform || "") + " translateY(-50%)";
css.height = `${rel.height}px`;
break;
case "SCALE":
css.top = `${(rel.y / parent?.absoluteBoundingBox?.height) * 100}%`;
css.height = `${(rel.height / parent?.absoluteBoundingBox?.height) * 100}%`;
break;
}
console.log('=====css2', css)
return css;
}
4、完整转换示例
const figmaToCSS = (node, parent) => {
const rel = calculateRelativePosition(node, parent); // 1. 基础样式
const css = { width: `${rel.width}px`, height: `${rel.height}px` }; // 2. 判断布局方式
console.log('====rel', rel)
if (parent?.layoutMode !== "NONE") {
// 父元素是 Auto Layout,子元素不需要定位
// 使用 flex item 属性
if (node.layoutAlign === "STRETCH") {
css.alignSelf = "stretch";
}
if (node.layoutGrow === 1) {
css.flexGrow = 1;
}
} else {
// debugger
// 父元素不是 Auto Layout,使用绝对定位
Object.assign(css, constraintsToCSS(node, parent));
} // 3. 如果当前节点是 Auto Layout 容器
if (node?.layoutMode !== "NONE") {
Object.assign(css, toFlexCSS(node));
}
console.log('=====figmaToCSS css', css)
return css;
};
5、常见转换映射表
尺寸与位置
| Figma 属性 |
CSS 属性 |
说明 |
absoluteBoundingBox.x |
left |
需计算相对值 |
absoluteBoundingBox.y |
top |
需计算相对值 |
absoluteBoundingBox.width |
width |
直接使用 |
absoluteBoundingBox.height |
height |
直接使用 |
Flex 布局(Auto Layout)
| Figma 属性 |
CSS 属性 |
说明 |
layoutMode: VERTICAL |
flex-direction: column |
垂直排列 |
layoutMode: HORIZONTAL |
flex-direction: row |
水平排列 |
itemSpacing |
gap |
子元素间距 |
paddingLeft |
padding-left |
左内边距 |
paddingRight |
padding-right |
右内边距 |
paddingTop |
padding-top |
上内边距 |
paddingBottom |
padding-bottom |
下内边距 |
layoutAlign: STRETCH |
align-self: stretch |
拉伸填充 |
layoutGrow: 1 |
flex-grow: 1 |
自动扩展 |
约束定位(Constraints)
| Figma 属性 |
CSS 属性 |
说明 |
constraints.horizontal: LEFT |
left: Npx |
左对齐固定 |
constraints.horizontal: RIGHT |
right: Npx |
右对齐固定 |
constraints.horizontal: LEFT_RIGHT |
left: Npx; right: Npx |
左右拉伸 |
constraints.horizontal: CENTER |
left: 50%; transform: translateX(-50%) |
水平居中 |
constraints.horizontal: SCALE |
left: N%; width: N% |
按比例缩放 |
constraints.vertical: TOP |
top: Npx |
顶部固定 |
constraints.vertical: BOTTOM |
bottom: Npx |
底部固定 |
constraints.vertical: TOP_BOTTOM |
top: Npx; bottom: Npx |
上下拉伸 |
constraints.vertical: CENTER |
top: 50%; transform: translateY(-50%) |
垂直居中 |
constraints.vertical: SCALE |
top: N%; height: N% |
按比例缩放 |
样式属性
| Figma 属性 |
CSS 属性 |
说明 |
opacity |
opacity |
透明度 |
cornerRadius |
border-radius |
圆角 |
fills[].color |
background-color |
背景色 |
strokes[].color |
border-color |
边框色 |
strokeWeight |
border-width |
边框宽度 |
effects[] (DROP_SHADOW) |
box-shadow |
投影 |
effects[] (LAYER_BLUR) |
filter: blur() |
模糊 |
clipsContent: true |
overflow: hidden |
裁剪内容 |
自从AI面世以来,目前真正要用到figma原始数据结构的其实已经并不多,大多数情况下并不需要直接接触figma的原始JSON数据,这里分享一下相关经验,留给需要定制化处理Figma的童鞋。