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..y2
y2..y3
y3..y4
的间隔,最后算出间隔平均值从小到大排序
x1 .. x2
,计算x1 .. x2
的间距, 同理,如果有x1 .. x4
, 就从小到大排序,并计算x1..x2
x2..x3
x3..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
结语
本文分析的比较简单,因为太难表达了。。涉及的细节很多,有不少没有列举出来,并且主要针对自动布局做了分析。
整体来看,实现并不复杂,搞懂了这些还不够,后续再结合业内其他方案,做进一步调研。