微信扫码
与创始人交个朋友
我要投稿
在设计产业中,虽然设计师和产品经理的角色各有不同,但两者之间的界限正变得日益模糊。设计师在考虑用户体验和产品功能的同时,产品经理也被要求具备一定的设计思维。今天,我们将探讨在设计思维转型为产品开发的过程中,如何打造一款简化创意流程的灵感设计插件工具。
在设计领域,捕捉和实现灵感通常涉及一系列复杂的步骤。从概念草图到最终的视觉呈现,设计师必须对每一个环节进行细致的打磨。但这个过程常常受到技术限制和沟通障碍的制约。作为一名追求效率的设计师,我一直在寻找一种工具,能够帮助我提升工作效率。
因为设计师在创意过程中经常面临技术限制和沟通难题,所以我尝试去开发一款能够简化创意流程的灵感工具。这款工具将在短时间内帮助设计师将新概念的视觉效果快速呈现于眼前,加速品牌形象的更新和用户界面设计的迭代过程。
这样我们可以确保这款灵感工具不仅能够满足设计师的核心需求,而且在技术和市场层面都是可行的。
通过DesignGenie插件可以快速捕获网页上的创意,将其转化成 figma 等工具中可以操作的设计资源。
在Figma社区里搜索DesignGenie并运行。
DesignGenie 插件链接: https://www.figma.com/community/plugin/1398619471957761832/designgenie
1.无限导入:用户无限次网站导入,大幅提高工作效率。2.多视口导入:支持多视口,满足不同设备的设计需求。3.主题模式:深色模式/浅色模式。(源网页是否支持) 4.编辑与协作:在Figma编辑网页元素,团队协作实时反馈。5.提炼功能:提炼生成视觉设计稿页面中优秀的图标等元素。6.字体配置:生成同时可以快速匹配需要替换的字体样式。7.高度还原:保留原有网页布局、样式,高度还原实际效果。8.协作便捷:支持团队共享,提升产设研协作效率。
高级功能(此功能目前处于灰度测试阶段):
1.可创建自动布局。2.导入需要登录的页面:对于需要登录状态的页面,用户可以使用DesignGenie Chrome插件。登录并访问所需网页后,点击插件图标,下载.h2d文件,然后将该文件拖放到Figma插件中。
DesignGenie插件核心工作就是获取到html结构和对应的资源,并将这些内容转化成设计稿结构。
我们在服务端使用 playwright 工具抓取目标页面内容。那么我们是如何组织html的内容和结构的?
通过 nodeType 和 tagName 对 elem.node 进行分类, 通过 elem.childNodes 进行子节点获取和计算嵌套关系。
如下下图所示,我们将 html.dom 主要分为了 FrameObj, TextObj, SvgObj, ImageObj, VideoObj,RectangleObj 6种类型。
FrameObj 对应 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。
nodeType === TEXT_NODE (3) 是一个文本节点,文本 ITextObj 继承了 IBaseObj。在Rect内容的基础上 记录了 value 和 font。这里的 value 一般从 TextNode.nodeValue 取。但是在input,select,text 中 TextNode.nodeValue空,会兜底获取 placeholder.
nodeType === TEXT_NODE (1) && elem.tagName == 'SVG' 会将 elem 分类为一个 svgObj. 同时记录 svg = elem.outerHTML
image & video 的获取过程类似,获取对应标签的 src属性即可。值得注意的是在 video 中 src 属性可能包裹在 source 标签下。
这是一个特殊分类,用来记录 elem 中的伪元素。在伪元素中无法通过elem.getBoundingClientRect() 直接获取对应的 x, y。因此需要伪元素的 Rect 需要根据父级&定位属性进行叠加计算。
首先根据 position 进行分类:
在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;
}
解析层是本插件的核心部分:在这个阶段对 获取到的 htmlObj 进行计算映射到 figma 的node属性。如下图:我们将根据获取到的内容 分类成 IFrameNode、IRectangleNode、ITextNode 以及figma对应的 Radius、Stroke、Fills 等属性。
在获取阶段的 obj 都是从左上角{ x: 0, y: 0}, 开始计算位置的。用 FrameObj.children 记录层级关系。在 figma FrameNode 中 子级节点的x,y 是相对父级进行定位。通过递归 IFrameNode的子节点 计算相对父级的x,y , 最外层节点在生成时相对画布给定可见区 x,y,就能得到 所有节点在 figma 中的位置。
在填充属性已经支持实现了 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。
在获取阶段我们能够取到 borderColor, borderWidth, borderStyle。通过 borderStyle 计算 strokeAlign('CENTER' | 'INSIDE' | 'OUTSIDE'), borderColor 计算 strokes(SolidPaint[]), borderWidth 计算 strokeWeight(number)。
通过 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",
},
]
`);
基于解析阶段生产的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)的验证和产品策略的调整。我的目标是缩短从创意到市场反馈的周期,提高产品的迭代速度和市场响应能力。
期待在未来的道路上,和大家一起探索未知,一起成长。
53AI,企业落地应用大模型首选服务商
产品:大模型应用平台+智能体定制开发+落地咨询服务
承诺:先做场景POC验证,看到效果再签署服务协议。零风险落地应用大模型,已交付160+中大型企业
2024-09-04
2024-10-30
2024-12-25
2024-09-26
2024-09-03
2024-09-06
2024-10-30
2024-11-23
2024-08-18
2024-11-19