Appearance
D2C设计方案调研:FigmaToCode 设计稿转代码 布局方案剖析!
大家好,很久没发文章了,这次我又来卷D2C这一块了~~ 由于最近组内决定去做D2C, 所以或多或少得研究一下业内的一些实现方案,然而真实情况是,我们也是摸着石头过河,前期要花费很多时间去调研,研究实现方案。其中就包括 FigmaToCode 这个开源项目。
本文不会贴很多代码,不利于理解。FigmaToCode 的实现其实并不复杂,但是用它生成的结果来看,还是偏差比较多的,这也是我们可以进一步优化的地方。
FigmaToCode演示
FigmaToCode支持将设计稿转成 HTML 和 TailWind等
Figma的自动布局
其实Figma自带 flex布局的一些特性, 甚至是grid布局,如果你开发过Figma设计稿,可以看到这里:
| flex布局 | grid布局 |
|---|---|
可以看到左边flex布局,可以实现各种对齐方式,这就意味着,如果设计人员可以规范设计,能使用自动布局就尽量用,对于我们来说就可以减少很多无用的判断和处理。
目前FigmaToCode只处理flex布局,如果设计人员没有使用Auto Layout这个模式,会自己计算内部节点的位置,来断定使用了什么布局,并且强制将该节点是Auto Layout。
Figma中的节点类型
在Figma中存在很多种类型的节点,就像下面这样,当然这里并没有展示出全部的节点类型
列举下常用的节点类型:
- RECTANGLE 矩形
- ELLIPSE 椭圆
- LINE 线形
- FRAME 框架: 相当于一个容器,子节点的x, y 位置都是相对于 Frame 来定位, 有布局功能
- INSTANCE组件实例: Figma中也存在组件的概念,相当于new Component()
- COMPONENT 组件
- GROUP 组: 本身没有自己的大小和位置, 你可以理解为它内部的子节点是捆绑在这个组的。也可能子节点在Group的外面。
- TEXT 文本
- VECTOR 矢量图:可以用于创建各种形状,例如线条、多边形、圆形、椭圆形等等。可以理解为前端里的 SVG, 放大不会失真。
- SECTION 容器:和Frame类似,但是没有布局功能(Auto Layout)
Figma节点属性的作用
Figma 节点的属性可以分为以下几类:
- 基本属性:节点的 ID、名称、类型、可见性等等。
- 几何属性:节点的位置、大小、旋转、缩放等等。
- 样式属性:节点的填充、边框、阴影、透明度等等。
- 文本属性:文本节点的字体、字号、颜色、对齐方式等等。
- 布局属性:容器节点的布局方式、间距、对齐方式等等。
- 组件属性:组件节点的主组件、实例、覆盖等等。
但是当你打印出figma node属性,可以发现属性太多了。。
我们要处理每个属性吗?其实大可不必,真正需要的属性其实只有一部分,我们要做的就是要清洗数据,并构建新的节点树。
以下是 Figma 节点的常见属性及其作用:
id:节点的唯一标识符。name:节点的名称。type:节点的类型,例如RECTANGLE、TEXT、GROUP等等。visible:节点的可见性,控制节点是否在设计中可见。locked:节点的锁定状态,控制节点是否可以编辑。opacity:节点的不透明度,控制节点的透明度。blendMode:节点的混合模式,控制节点与其下方节点的混合方式。constraints:节点的约束,控制节点在其父容器中的位置和大小。layoutAlign:容器节点的对齐方式,控制容器中的子节点如何对齐。layoutMode:容器节点的布局方式,控制容器中的子节点如何排列。padding:容器节点的内边距,控制容器中的子节点与容器边缘的距离。fills:节点的填充,控制节点的填充颜色和样式。strokes:节点的边框,控制节点的边框颜色和样式。strokeWeight:节点的边框宽度,控制节点的边框粗细。cornerRadius:节点的圆角半径,控制节点的圆角大小。characters:文本节点的文本内容。fontName:文本节点的字体名称。fontSize:文本节点的字体大小。textAlignHorizontal:文本节点的水平对齐方式。textAlignVertical:文本节点的垂直对齐方式。textCase:文本节点的大小写转换方式。textDecoration:文本节点的文本修饰方式。textStyleId:文本节点的文本样式 ID。componentId:组件节点的主组件 ID。instanceId:组件实例节点的实例 ID。overrides:组件实例节点的覆盖,控制组件实例与主组件的差异。- ...
当然这并不是全部,如果要处理的情况太多,我们就需要借助其他属性进一步分析。
布局树生成
上面说了那么多,FigmaToCode是如何处理没有AutoLayout的节点呢? 我们先从一个例子开始:
在Figma中,可以认为整个是一个树的结构,FigmaToCode首先会清洗其中的数据,组成新的Node节点。在上面的例子中,我们可以得到如下的树结构:
首先声明一点:在没有使用自动布局的情况下,才会计算布局
下面我们来看看FigmaToCode是如何实现自动布局的
1.在遇到Frame节点时:
- 创建一个 Frame 节点,并拷贝
Frame上的属性,包含 name、id、布局位置、大小等信息
我们可以把创建的新节点叫做 alternate节点
如果大家有看过React源码,相信大家对这个名字不会陌生。
- 继续递归子节点
Frame和Rectangle、Text等,在这个过程中,依然会创建新节点,并拷贝其中必要的属性。
- 回溯时开始自动布局
我们知道递归是深度遍历,在递归完蓝色框部分时,会回溯到Frame这个节点,此时对于Frame来说,我们需要计算出,Frame内部节点的具体排布。
然后为Frame的Alternate节点,也就是rectangle节点,设置layoutMode 布局方式,以及其他布局信息。
布局分析
在这一步中,我们将分析Text节点和Rectangle的位置,来确定布局方式。
1. 碰撞检测
不管什么需求,必定存在元素重叠的情况,这种情况如何处理呢?
以下图为例,一个矩形中包含 一个文本节点 和 一个椭圆节点
首先建立每个元素和重叠节点 之间映射管理,例如
{
n3: [n1, n2]
}
如果 n3 包含 (n1 和 n2) , 然后再做进一步转换
n3是rectangle节点,需要将其强制转为Frame节点- 修改n1 和 n2的 x, y, 因为之前是相对于上一层的
frame, 要修改为相对于刚才转换的frame中。同时parent同样指向刚才转换的frame
2. 计算布局方向
怎么知道子元素是 水平 还是 垂直 方向的排布呢?
从小到大排序
y1 .. y2,计算y1 .. y2的间距, 同理,如果有y1 .. y4, 就从小到大排序,并计算y1..y2y2..y3y3..y4的间隔,最后算出间隔平均值从小到大排序
x1 .. x2,计算x1 .. x2的间距, 同理,如果有x1 .. x4, 就从小到大排序,并计算x1..x2x2..x3x3..x4的间隔,最后算出间隔平均值如果子元素在垂直方向上没有对齐,则检查它们是否在水平方向上对齐。如果子元素在垂直和水平方向上都没有对齐,则返回 "NONE"。最后返回对齐方向 和 平均间隔。代码逻辑如下:
jsif (!intervalY.every((d) => d >= threshold)) { if (!intervalX.every((d) => d >= threshold)) { if (avgY <= threshold) { if (avgX <= threshold) { return ["NONE", 0]; } return ["HORIZONTAL", avgX]; } return ["VERTICAL", avgY]; } return ["HORIZONTAL", avgX]; }if (!intervalY.every((d) => d >= threshold)) { if (!intervalX.every((d) => d >= threshold)) { if (avgY <= threshold) { if (avgX <= threshold) { return ["NONE", 0]; } return ["HORIZONTAL", avgX]; } return ["VERTICAL", avgY]; } return ["HORIZONTAL", avgX]; }如果是水平对齐,则使用子节点的 x 值重新排序
如果是垂直对齐,则使用子节点的 y 值重新排序
3. 计算容器padding
这一步开始计算容器的padding值
- 容器width - 最右侧元素x === paddingRight
- 容器width - 最左侧元素x === paddingLeft
- 容器height - 最下面元素 y === paddingBottom
- 容器height - 最上面元素 y === paddingTop
4. 单独计算子节点的 align-items 布局对齐
学过flex布局的同学,应该知道有align-items: stretch 这个值,如果元素的宽度或高度与容器一致,我们需要单独设置。
判断也很简单,元素的宽度或高度与容器一致,单独设置子元素的layoutAlign为STRETCH
5. 确定子节点 主轴 或 交叉轴
前面我们已经计算出了元素整体的方向,水平 或 垂直。
但是并没有精确到每个元素,我们必须明确标注元素的direction、justify-content, align-items。
确定它们在轴线上是 start, end, center三个哪种情况
在下面的函数中,接收一个子节点 和 容器父节点。我们只需要根据节点的x, y, 就可以知道在主轴 和 交叉轴中,处于什么位置。
ts
const primaryAxisDirection = (
node: AltSceneNode,
parentNode: AltFrameNode
): { primary: "MIN" | "CENTER" | "MAX"; counter: "MIN" | "CENTER" | "MAX" } => {
const nodeCenteredPosX = node.x + node.width / 2;
const parentCenteredPosX = parentNode.width / 2;
const centerXPosition = nodeCenteredPosX - parentCenteredPosX;
const nodeCenteredPosY = node.y + node.height / 2;
const parentCenteredPosY = parentNode.height / 2;
const centerYPosition = nodeCenteredPosY - parentCenteredPosY;
if (parentNode.layoutMode === "VERTICAL") {
return {
primary: getPaddingDirection(centerYPosition),
counter: getPaddingDirection(centerXPosition),
};
} else {
return {
primary: getPaddingDirection(centerXPosition),
counter: getPaddingDirection(centerYPosition),
};
}
};const primaryAxisDirection = (
node: AltSceneNode,
parentNode: AltFrameNode
): { primary: "MIN" | "CENTER" | "MAX"; counter: "MIN" | "CENTER" | "MAX" } => {
const nodeCenteredPosX = node.x + node.width / 2;
const parentCenteredPosX = parentNode.width / 2;
const centerXPosition = nodeCenteredPosX - parentCenteredPosX;
const nodeCenteredPosY = node.y + node.height / 2;
const parentCenteredPosY = parentNode.height / 2;
const centerYPosition = nodeCenteredPosY - parentCenteredPosY;
if (parentNode.layoutMode === "VERTICAL") {
return {
primary: getPaddingDirection(centerYPosition),
counter: getPaddingDirection(centerXPosition),
};
} else {
return {
primary: getPaddingDirection(centerXPosition),
counter: getPaddingDirection(centerYPosition),
};
}
};但是这样还不足以确定justify-content和align-items的值什么,我们还要知道每个子节点,在主轴/ 交叉轴 出现最多次的对齐方式。例如,有四个节点,在主轴上,有一个是center, 而另外三个是start, 统计处出现次数最多的,基本可以认定,在主轴方向上 justify-content: flex-start
结语
本文分析的比较简单,因为太难表达了。。涉及的细节很多,有不少没有列举出来,并且主要针对自动布局做了分析。
整体来看,实现并不复杂,搞懂了这些还不够,后续再结合业内其他方案,做进一步调研。