作者:shaojunsun
在设计产业中,虽然设计师和产品经理的角色各有不同,但两者之间的界限正变得日益模糊。设计师在考虑用户体验和产品功能的同时,产品经理也被要求具备一定的设计思维。今天,我们将探讨在设计思维转型为产品开发的过程中,如何打造一款简化创意流程的灵感设计插件工具。
在设计领域,捕捉和实现灵感通常涉及一系列复杂的步骤。从概念草图到最终的视觉呈现,设计师必须对每一个环节进行细致的打磨。但这个过程常常受到技术限制和沟通障碍的制约。作为一名追求效率的设计师,我一直在寻找一种工具,能够帮助我提升工作效率。
如何将设计思维转型为产品开发,研发一款简化创意流程的灵感工具?因为设计师在创意过程中经常面临技术限制和沟通难题,所以我尝试去开发一款能够简化创意流程的灵感工具。这款工具将在短时间内帮助设计师将新概念的视觉效果快速呈现于眼前,加速品牌形象的更新和用户界面设计的迭代过程。
具体来说,希望具备以下能力:快速捕捉和展示设计灵感,帮助设计师迅速将概念转化为视觉设计稿。支持快速探索和更新视觉语言,配合产品经理快速确定视觉风格方向。在新网站构建过程中,能够帮助快速迭代和测试不同的设计方案,加速界面设计流程。在技术评估方面,我们需要考虑:工具的界面设计是否直观易用,以适应不同水平的设计师。技术实现的可行性,包括所需的算法、数据处理能力和图形渲染技术。产品的市场定位和目标用户群体,以及与现有竞品的差异化优势。开发成本、时间和资源的预估,确保项目的经济可行性。这样我们可以确保这款灵感工具不仅能够满足设计师的核心需求,而且在技术和市场层面都是可行的。
通过DesignGenie插件可以快速捕获网页上的创意,将其转化成 figma 等工具中可以操作的设计资源。
为设计师提供了一种全新的工作方式:安装插件:在Figma社区里搜索DesignGenie并运行。
快速直达:DesignGenie 插件链接:https://www.figma.com/community/plugin/1398619471957761832/designgenie
"DesignGenie" —— 功能亮点:1.无限导入:用户无限次网站导入,大幅提高工作效率。2.多视口导入:支持多视口,满足不同设备的设计需求。3.主题模式:深色模式/浅色模式。(源网页是否支持) 4.编辑与协作:在Figma编辑网页元素,团队协作实时反馈。5.提炼功能:提炼生成视觉设计稿页面中优秀的图标等元素。6.字体配置:生成同时可以快速匹配需要替换的字体样式。7.高度还原:保留原有网页布局、样式,高度还原实际效果。8.协作便捷:支持团队共享,提升产设研协作效率。
高级功能(此功能目前处于灰度测试阶段):
1.可创建自动布局。2.导入需要登录的页面:对于需要登录状态的页面,用户可以使用DesignGenie Chrome插件。登录并访问所需网页后,点击插件图标,下载.h2d文件,然后将该文件拖放到Figma插件中。
DesignGenie插件核心工作就是获取到html结构和对应的资源,并将这些内容转化成设计稿结构。
因此主要任务分为 获取、解析、生成 三个阶段:1.第一个阶段获取html我们在服务端使用 playwright 工具抓取目标页面内容。那么我们是如何组织html的内容和结构的?
通过 nodeType 和 tagName 对 elem.node 进行分类, 通过 elem.childNodes 进行子节点获取和计算嵌套关系。
如下下图所示,我们将 html.dom 主要分为了 FrameObj, TextObj, SvgObj, ImageObj, VideoObj,RectangleObj 6种类型。
1.1 FrameObjFrameObj 对应 figma中的 FrameNode (The frame node is a container used to define a layout hierarchy. It is similar to
in HTML. )用于于定义布局层次结构的容器 。因此主要的属性包含 Rect(x,y,width,height)、children、styles。
Rect属性如何获取?
通过 Element.getBoundingClientRect() 方法返回一个 DOMRect 对象,根据其提供了元素的大小及其相对于视口的位置我们可以计算得到 Rect。
Children属性如何获取?
通过 Node.childNodes 返回包含指定节点的子节点的集合处理 div的嵌套关系。
Styles属性如何获取?
通过使用 window.getComputedStyle(element, [pseudoElt]); 我们能够获取到所需的全部Style属性。并且会将 transform:rotate、shew 等属性计算后给出 matrix 属性,通过matrix直接对应figma的 relativeTransform。
1.2 TextObjnodeType === TEXT_NODE (3) 是一个文本节点,文本 ITextObj 继承了 IBaseObj。在Rect内容的基础上 记录了 value 和 font。这里的 value 一般从 TextNode.nodeValue 取。但是在input,select,text 中 TextNode.nodeValue空,会兜底获取 placeholder.
1.3 SvgObjnodeType === TEXT_NODE (1) && elem.tagName == 'SVG' 会将 elem 分类为一个 svgObj. 同时记录 svg = elem.outerHTML
1.4 ImageObj&VideoObjimage & video 的获取过程类似,获取对应标签的 src属性即可。值得注意的是在 video 中 src 属性可能包裹在 source 标签下。
1.5 RectangeleObj这是一个特殊分类,用来记录 elem 中的伪元素。在伪元素中无法通过elem.getBoundingClientRect() 直接获取对应的 x, y。因此需要伪元素的 Rect 需要根据父级&定位属性进行叠加计算。
首先根据 position 进行分类:
相对定位节点根据父级 Rect 属性进行 padding 的相对位置计算。在 absolute 定位中 通过 marginLeft + left ,marginTop + top ... 的方式计算定位。会存在各方向都有margin值需要确定优先级 left >right , top >bottom, 及left存在只需叠加 marginleft即可,忽略right属性。同时要处理好 % 和 px 单位的转换。在 fixed 定位中 与absolute计算过程一致,父级是在最外层 html.Node 定位上进行计算。1.6资源内容assets在html中我们通过获取了需要的内容资源。如字体 和图片等。部分网站的资源在插件端通过src请求可能会有跨域问题,因此通过playwright的route监控直接在服务端直接加载到对应内容处理好 content & mimeType 保持在 assets 中直接发给插件端。
page.route('**/*.{woff,woff2,ttf,otf,eot,svg,jpg,jpeg,png,webp,gif}**', () => { // 加载资源转u8a保存 })export interface Asset { content?: uint8array; mimeType: image/svg+xml | image/png | image/jpeg | image/gif | image/webp | image/jpg; base64Encoded?: true;}2.第二个阶段解析 DSL解析层是本插件的核心部分:在这个阶段对 获取到的 htmlObj 进行计算映射到 figma 的node属性。如下图:我们将根据获取到的内容 分类成 IFrameNode、IRectangleNode、ITextNode 以及figma对应的 Radius、Stroke、Fills 等属性。
2.1Positon属性在获取阶段的 obj 都是从左上角{ x: 0, y: 0}, 开始计算位置的。用 FrameObj.children 记录层级关系。在 figma FrameNode 中 子级节点的x,y 是相对父级进行定位。通过递归 IFrameNode的子节点 计算相对父级的x,y , 最外层节点在生成时相对画布给定可见区 x,y,就能得到 所有节点在 figma 中的位置。
2.2 Fill 属性在填充属性已经支持实现了 SolidPaint,GradientPaint,ImagePaint 和 VideoPaint 所有的figma Paint类型。
SolidPaint属性的计算
interface SolidPaint { readonly type: 'SOLID' readonly color: RGB readonly opacity?: number}填充在 TextNode 中通过 styles.color + styles.opacity计算,其它节点通过 styles.backgroundColor+ styles.opacity 计算。
GradientPaint属性的计算
interface GradientPaint { readonly type: 'GRADIENT_LINEAR' | 'GRADIENT_RADIAL' | 'GRADIENT_ANGULAR' | 'GRADIENT_DIAMOND' readonly gradientTransform: Transform readonly gradientStops: ReadonlyArray<ColorStop>}渐变需要计算的核心属性是 gradientTransform 和 gradientStops。
如下图所示:先对 渐变css部分进行拆词:
expect(parseGradient('linear-gradient(to left, red, blue)')).toMatchInlineSnapshot(` [ { "colorStops": [ { "rgba": { "a": 1, "b": 0, "g": 0, "r": 255, }, "type": "color-stop", }, { "rgba": { "a": 1, "b": 255, "g": 0, "r": 0, }, "type": "color-stop", }, ], "gradientLine": { "type": "side-or-corner", "value": "left", }, "type": "linear-gradient", }, ] `);gradientStops:通过下面这个结构获取转化:
export type ColorStop = { type: 'color-stop'; rgba: RgbaColor; position?: Length;};export type AngularColorStop = { type: 'angular-color-stop'; rgba: RgbaColor; angle?: Length | [Length, Length];};export type ColorHint = { type: 'color-hint'; hint: Length;};gradientTransform: transform核心代码如下图所示,通过获取到的 渐变拆词对象计算对应的 缩放,角度和偏移。通过figma中初始化tranfrom位置获取到对于的 gradientTransform。
import { rotate, translate, compose, scale } from 'transformation-matrix'; const gradientLength = calculateLength(parsedGradient, width, height); const [sx, sy] = calculateScale(parsedGradient); const rotationAngle = calculateRotationAngle(parsedGradient); const [tx, ty] = calculateTranslationToCenter(parsedGradient); const gradientTransform = compose(translate(0, 0.5), scale(sx, sy), rotate(rotationAngle), translate(tx, ty));ImagePaint属性的计算
interface ImagePaint { readonly type: 'IMAGE' readonly scaleMode: 'FILL' | 'FIT' | 'CROP' | 'TILE' readonly imageHash: string | null}imageHash的获取:通过 figma Api.createImage(data: Uint8Array): { hash, width, height } 上传图片可以获取 image在figma fill 中对应的 imageHash 。
scaleMode的计算:根据 style.backgroundSize 计算 scaleMode。
if (backgroundSize === 'cover') { // 图像将被缩放以覆盖整个容器 this.fills = [ { type: 'IMAGE', scaleMode: 'FILL', imageHash: imageHash, }, ]; } else if (backgroundSize === 'contain') { // 图像将保持其比例缩放以适应容器的尺寸,整个图像会显示在容器内,可能会有空白区域 this.fills = [ { type: 'IMAGE', scaleMode: 'FIT', imageHash: imageHash, }, ]; } else { // 通过 background-size 设置图像的缩放,并使用 background-position 来控制图像的位置,裁剪掉超出容器的部分 this.fills = [ { type: 'IMAGE', scaleMode: 'CROP', imageHash: imageHash, }, ]; }VideoPaint属性的计算
interface VideoPaint { readonly type: 'VIDEO' readonly scaleMode: 'FILL' | 'FIT' | 'CROP' | 'TILE' readonly videoHash: string | null}videoPaint的计算过程与 ImagePaint 类似,通过 createVideoAsync(data: Uint8Array): Promise获取对应的 videoHash。
2.3 Stroke 属性在获取阶段我们能够取到 borderColor, borderWidth, borderStyle。通过 borderStyle 计算 strokeAlign('CENTER' | 'INSIDE' | 'OUTSIDE'), borderColor 计算 strokes(SolidPaint[]), borderWidth 计算 strokeWeight(number)。
2.4 Effects 属性通过 boxShadow 计算Effects 属性,目前支持 InnerShadowEffect 和 DropShadowEffect。
{ readonly type: 'INNER_SHADOW' | 'DROP_SHADOW' readonly color: RGBA readonly offset: Vector readonly radius: number readonly spread?: number readonly visible: boolean readonly blendMode: BlendMode readonly boundVariables?: { [field in VariableBindableEffectField]?: VariableAlias }}我们在获取阶段取到的 "rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgb(217, 219, 227) 0px 0px 0px 1px inset" ,为了方便计算我们实现了 parseCSSNodes 对 css 内容进行拆词。从拆词内容 shadowDetails.push(extractPixelValue(node['value']));const [offsetX, offsetY, blurRadius, spreadRadius] = shadowDetails计算对应属性。
const boxShadow = 'rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgb(217, 219, 227) 0px 0px 0px 1px inset'; const nodes = parseCSSNodes(boxShadow); expect(nodes).toMatchInlineSnapshot(` [ { "nodes": [ { "sourceEndIndex": 15, "sourceIndex": 5, "type": "rgba", "value": "rgba(0, 0, 0, 0)", }, ], "sourceEndIndex": 16, "sourceIndex": 0, "type": "function", "value": "rgba", }, { "sourceEndIndex": 17, "sourceIndex": 16, "type": "space", "value": " ", }, { "sourceEndIndex": 20, "sourceIndex": 17, "type": "word", "value": "0px", }, ... { "nodes": [ { "sourceEndIndex": 85, "sourceIndex": 72, "type": "rgb", "value": "rgb(217, 219, 227)", }, ], "sourceEndIndex": 86, "sourceIndex": 68, "type": "function", "value": "rgb", }, { "sourceEndIndex": 108, "sourceIndex": 103, "type": "word", "value": "inset", }, ] `);3.第三个阶段生成 Design基于解析阶段生产的DSL 数据结构,我们能够通过figma.api 直接生成设计稿。
下图是生成部分的部分代码, 解析后的数据结构进行递归创建 ,使用 figma.currentPage.appendChild 和 frameNode.appendChild 设置层级关系。
figma.createFrame()figma.createText()figma.createNodeFromSvg()figma.createRectangle()export const createFigmaNode = async (node: IFrameNode, parentNodeId: string | undefined) => { if (node.type === 'FRAME') { const nodeId = await toFrameNode(node, parentNodeId); node.children.forEach(async (child: IDslNode) => { await createFigmaNode(child as IFrameNode, nodeId); }); } if (node.type === 'TEXT' && parentNodeId) { await toTextNode(node, parentNodeId); } if (node.type === 'SVG' && parentNodeId) { await toSvgNode(node, parentNodeId); } if (node.type === 'RECTANGLE' && parentNodeId) { await toRectangleNode(node, parentNodeId); }};职业转型是一段充满挑战和成长的旅程,我相信随着时间的推移,我会有新的认知和理解。虽然我已经踏上了产品经理的职业道路,但我仍然深深热爱设计。设计带给我的创意和满足感是我灵魂的一部分。在未来的职业生涯中,我希望能够继续发挥设计师的创造力,同时运用产品经理的战略思维,打造出既满足用户需求又能实现商业价值的优秀产品。
总结下感悟:平衡设计和产品思维设计师注重细节和用户体验,而产品经理则需要考虑商业价值和用户需求之间的平衡。这要求我在创意和实用性之间找到最佳平衡点,同时确保每一个设计决策都能带来实际的商业收益。
学习新技能除了设计之外,我还需要学习许多新技能,包括项目管理、数据分析、市场策略等。这些新技能的掌握不仅需要时间和精力,还需要在实际项目中不断实践和改进。
适应新的角色作为产品经理,我需要为产品的最终结果负责。这意味着在必要时,我需要做出艰难的决策,比如调整产品方向或终止某些不具备商业前景的项目。这种责任感和决策压力是我在设计师岗位上不曾经历过的。
用户体验与用户价值的平衡设计师追求卓越的用户体验,而产品经理则更注重用户价值与商业价值的交换。用户体验是价值交换过程中的润滑剂,但其重要性取决于产品类别和具体情境。例如,在阅读类产品中,字体和行间距的优化对提升用户体验至关重要,而在工具类产品中,这些细节对用户价值的贡献可能较小。
资源分配与试错对于团队而言,在资源有限的情况下,如何合理分配资源进行试错,以及如何确定用户对产品的需求和价值认可,是持续思考和权衡的重点。这要求我在有限的资源条件下,制定出最有效的产品策略,以最大化团队的工作效率和产品的市场表现。
市场定位与用户识别产品设计不仅限于创意实现,更重要的是市场定位、用户识别、产品市场契合度(PMF)的验证和产品策略的调整。我的目标是缩短从创意到市场反馈的周期,提高产品的迭代速度和市场响应能力。
期待在未来的道路上,和大家一起探索未知,一起成长。