前端智能化实践——图层抽象与优化

在设计稿生成代码流程中,我们需要先将图层解析为UI节点,而后再通过布局算法生成代码。

设计稿转代码基本流程

作为前端智能化的第一步,解析的UI数据关乎后续的代码还原质量,因此需要一套方案来保证解析阶段能输出通用而有效的UI节点。

针对通用性和有效两个目标,我们将解析过程分为图层抽象和图层优化两个步骤。

图层抽象

为了实现UI Nodes通用性,兼容不同的设计稿类型,如psd,sketch和xd等,我们将设计稿的图层抽象为图片Image、图形Shape、文本Text三种类型的UI节点:

  1. Shape,可用样式实现的形状图层,如纯色带边框的矩形、圆角矩形、圆形等;
  2. Text,可用样式实现的文本图层;
  3. Image,不可用样式实现的图层,如复杂图形、带纹理的形状、位图和艺术字等;

除了图层类型抽象,其它图层信息也将抽象为图元属性,可以分为三种:

  • 基础属性,比如名字、id、图层类型
  • 位置属性,比如宽高、坐标
  • 样式属性,描述图层颜色和边框等
UINode属性

UINode类接口的具体代码如下:

/**
 * 图层类接口
 */
interface UINode {
     // 图层id
      id: string = '';
     // 图层类型,包括Text,Shape,Image,Group
      type: string;
     // 图层名称
      name: string = '';
     // 宽度
      width: number = 0;
     // 高度
      height: number = 0;
     // 位置:距离左边界距离
      abX: number = 0;
     // 位置:距离上边界距离
      abY: number = 0;
     // 图层样式
      styles: UIStyle = {};
}

图层优化

解析后的图层往往包含一些无效的信息,比如图层冗余、图层零散的问题,我们需要通过数据预处理来优化UI节点信息,提高代码还原的精准度。

预处理阶段主要分为两步:1. 图层清洗 2. 图层合并;

1. 图层清洗

设计稿中会有不可见图层,删除它们不会影响视觉效果,这些图层是冗余的。


设计稿存在不可见图层

图层清洗,就是针对不可见的图层进行剔除,分为以下四种情况:

1.1 图层样式透明无背景;

const isTransparentStyle = function(node: UINode): boolean {
  const { background, border, shadows } = node.styles;
  return (
    !node.childNum
    && (node.isTransparent
      || (background
        && background.hasOpacity
        && background.type === 'color'
        && +background.color.a === 0)
      || (border && +border.color.a === 0)
      || (node.type === UINodeTypes.Shape && !background && !border && !shadows))
  );
};

1.2 图层被其它图元覆盖;

// 节点是否被覆盖
const isCovered = function(node: UINode, nodelist: Array<UINode>): boolean {
  const index = nodelist.indexOf(node);
  const arr2 = nodelist.slice(index + 1).filter(n => !isContained(n, node)); // 越往后节点的z-index越大
  return arr2.some(brother => brother.type !== QNodeTypes.QLayer
      && isBelong(node, brother)
      && !brother.hasComplexStyle); // 如果节点被兄弟覆盖,并且自己没有其它属性(shadow)影响到兄弟,则移除该节点
};

1.3 图层颜色与底层图元颜色相同;

// 节点颜色是否与背景同色
const isCamouflage = function(node: UINode, nodelist: Array<UINode>): boolean {
  const { pureColor } = node;
  if (!pureColor) return false;
  const nodeIndex = nodelist.indexOf(node);
  const bgNode = nodelist
    .slice(0, nodeIndex)
    .reverse()
    .find(n => isSameColor(pureColor, n.pureColor)
        && (!n.parent || isBelong(node, n)));
  if (!bgNode) return false;
  const bgNodeIndex = nodelist.indexOf(bgNode);
  if (bgNodeIndex + 1 < nodeIndex) return !nodelist
    .slice(bgNodeIndex + 1, nodeIndex)
    .some(n => isIntersect(node, n));
  return false;
};

1.4 图层位于可视边界外

// 节点是否在边界外
const isOutside = function(node: UINode, rootNode: UINode): boolean {
  return !(
    node.abX >= rootNode.abXops
    || node.abY >= rootNode.abYops
    || node.abX >= rootNode.abXops
    || node.abY >= rootNode.abYops
  );
};

我们定义为一个清洗函数,输入图层节点列表遍历,如果满足上述四个条件之一,则过滤掉该节点。

// 图元冗余清洗
function clean(nodes: UINode[]) {
  const [rootNode] = nodes;
  return nodes.filter((node: UINode) => {
    const needClean =
      isTransparentStyle(node) // 节点是否样式不可见
      || isOutside(node, rootNode) // 节点是否位于边界外
      || isCovered(node, nodes) // 节点是否被覆盖
      || isCamouflage(node, nodes); // 节点是否颜色伪装
    // 满足其中一种情况则视为冗余节点
    return !needClean;
  }
}

2. 图层合并

这个步骤主要是判断设计稿中哪些图层需要合并,比如下图的笑脸icon,如果不对图层进行成组而直接导出,会输出四张零散图。

零散图层

我们判断合并的思路是根据图层之间空间关系是否相交,主要分为以下两步:

2.1 判断两节点之间的相交关系

如上图,图形eye和face相交,mouth和face相交,得到相交关系A:[eye,face],相交关系B:[mouth,face]两个组,代码如下:

let isCollision = (node: UINode, brother: UINode) => !(
        (node.abY + node.height < brother.abY) || (node.abY > brother.abY + brother.height) ||
        (node.abX + node.width < brother.abX) || (node.abX > brother.abX + brother.width)
    );

2.2 多个节点合并

我们将相交关系的组(边)进行合并,比如边A中的face图层在B关系中也存在,那么将A和B进行合并,得到C:[ eye, face, mouth ]。

mergeJudge(nodelist: UINode[]): Array<Set<UINode>> {
    // 相交检测
    const groups: Array<Set<UINode>> = [];
    const relations = [];
    for (let i = 0; i < nodelist.length; i++) {
      const node = nodelist[i];
      for (let j = i + 1; j < nodelist.length; j++) {
        const brother = nodelist[j];
        if (isCollision(node, brother)) { // 判断两节点是否相交
          relations.push([node, brother]); // 相交则加入边列表
        }
      }
    }
    // 关系聚合
    relations.forEach(([node, brother]) => {
      // 查找当前边的两个端点是否已经有过成组
      let res = groups.filter(group => group.has(node) || group.has(brother));
      if (res.length) { // 已成过组
          const unionGroup = res.reduce((p, c) => p.concat([...Array.from(c)]), []);
          res.forEach(g => groups.splice(groups.indexOf(g), 1)); // 剔除原有组
          groups.push(new Set(unionGroup).add(node)
            .add(brother)); // 合并新组
      } else groups.push(new Set([node, brother])); // 否则,自成新组
    });
    return groups;
  }
}

最后根据将这些关系合并成新的节点:

// 零散图元合并
function merge(nodes: UINode[]) {
    // 根据空间关系合并图层
    if ( !nodes.length) return;
    const groupArr = mergeJudge(nodes); // 碰撞检测,输出成组列表 [[node1,node2],[node3,node4],node5]
    groupArr.map((item: UINode | UINode[]) => {
        if (item.size > 1) {
            const newNode = union([...item], UINodeTypes.Image); // 合并成图片节点
            return newNode;
        }
        return item;
    });
});

总结

本文通过图层抽象和优化两个步骤,抽象过程是将不同设计软件图层解析为统一的数据结构,接着通过图层优化,清除冗余节点和合并零散节点,得到“干净”的UI节点集合。
后续我们将介绍如何利用这些UI节点进行布局到生成最终代码。

更多关于前端智能化的课程,可以参考我之前分享的课程:https://ke.qq.com/course/2995626

文章传送:《前端智能化 ——从图片识别UI样式》https://zhuanlan.zhihu.com/p/207308196

各个设计平台的解析文档如下:
Sketch API: https://developer.sketch.com/reference/api/
PhotoShop API: https://www.adobe.com/devnet/photoshop/scripting.html
XD API: https://adobexdplatform.com/plugin-docs/reference/how-to-read.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容