Skip to main content
Glama
style-extractor.ts10 kB
import { FigmaService, FigmaNode } from './figma-service.js'; export interface StyleData { nodeId: string; nodeName: string; nodeType: string; position?: { x: number; y: number; width: number; height: number; }; fills?: Array<{ type: string; color?: { r: number; g: number; b: number; a: number; }; gradientStops?: Array<{ color: { r: number; g: number; b: number; a: number }; position: number; }>; }>; strokes?: Array<{ type: string; color?: { r: number; g: number; b: number; a: number; }; }>; strokeWeight?: number; cornerRadius?: number; effects?: Array<{ type: string; visible: boolean; radius?: number; color?: { r: number; g: number; b: number; a: number; }; offset?: { x: number; y: number; }; }>; textStyle?: { fontFamily?: string; fontSize?: number; fontWeight?: number; letterSpacing?: number; lineHeight?: number; textAlign?: string; textColor?: { r: number; g: number; b: number; a: number; }; }; constraints?: { vertical: string; horizontal: string; }; } export interface ComponentStyles { fileInfo: { fileId: string; fileName: string; lastModified: string; }; styles: StyleData[]; globalStyles?: Record<string, any>; } export class FigmaStyleExtractor { constructor(private figmaService: FigmaService) {} /** * 根据Figma URL获取样式数据 */ async getStylesFromUrl(figmaUrl: string): Promise<ComponentStyles> { try { const urlInfo = this.figmaService.parseUrl(figmaUrl); const { fileId, nodeId } = urlInfo; const file = await this.figmaService.getFile(fileId); let targetNodes: FigmaNode[] = []; if (nodeId) { // 获取特定节点 const node = await this.figmaService.getNode(fileId, nodeId); if (node) { targetNodes = [node]; } } else { // 获取整个文件的主要组件 targetNodes = this.extractMainComponents(file.document); } const styles = targetNodes.map(node => this.extractNodeStyle(node)); return { fileInfo: { fileId, fileName: file.name, lastModified: file.lastModified, }, styles, globalStyles: file.styles, }; } catch (error) { throw new Error(`获取样式数据失败: ${error instanceof Error ? error.message : '未知错误'}`); } } /** * 提取节点样式 */ private extractNodeStyle(node: FigmaNode): StyleData { const style: StyleData = { nodeId: node.id, nodeName: node.name, nodeType: node.type, }; // 位置和尺寸 if (node.absoluteBoundingBox) { style.position = { x: node.absoluteBoundingBox.x, y: node.absoluteBoundingBox.y, width: node.absoluteBoundingBox.width, height: node.absoluteBoundingBox.height, }; } // 填充样式 if (node.fills && node.fills.length > 0) { style.fills = node.fills.map(fill => ({ type: fill.type, color: fill.color ? { r: Math.round(fill.color.r * 255), g: Math.round(fill.color.g * 255), b: Math.round(fill.color.b * 255), a: fill.color.a || 1, } : undefined, gradientStops: fill.gradientStops?.map((stop: any) => ({ color: { r: Math.round(stop.color.r * 255), g: Math.round(stop.color.g * 255), b: Math.round(stop.color.b * 255), a: stop.color.a || 1, }, position: stop.position, })), })); } // 描边样式 if (node.strokes && node.strokes.length > 0) { style.strokes = node.strokes.map(stroke => ({ type: stroke.type, color: stroke.color ? { r: Math.round(stroke.color.r * 255), g: Math.round(stroke.color.g * 255), b: Math.round(stroke.color.b * 255), a: stroke.color.a || 1, } : undefined, })); } // 描边粗细 if (node.strokeWeight !== undefined) { style.strokeWeight = node.strokeWeight; } // 圆角 if (node.cornerRadius !== undefined) { style.cornerRadius = node.cornerRadius; } // 效果(阴影、模糊等) if (node.effects && node.effects.length > 0) { style.effects = node.effects.map(effect => ({ type: effect.type, visible: effect.visible !== false, radius: effect.radius, color: effect.color ? { r: Math.round(effect.color.r * 255), g: Math.round(effect.color.g * 255), b: Math.round(effect.color.b * 255), a: effect.color.a || 1, } : undefined, offset: effect.offset ? { x: effect.offset.x, y: effect.offset.y, } : undefined, })); } // 文本样式 if (node.style) { style.textStyle = { fontFamily: node.style.fontFamily, fontSize: node.style.fontSize, fontWeight: node.style.fontWeight, letterSpacing: node.style.letterSpacing, lineHeight: node.style.lineHeightPx, textAlign: node.style.textAlignHorizontal, }; // 如果有文本颜色信息 if (node.fills && node.fills.length > 0 && node.type === 'TEXT') { const textFill = node.fills[0]; if (textFill.color) { style.textStyle.textColor = { r: Math.round(textFill.color.r * 255), g: Math.round(textFill.color.g * 255), b: Math.round(textFill.color.b * 255), a: textFill.color.a || 1, }; } } } // 约束 if (node.constraints) { style.constraints = { vertical: node.constraints.vertical, horizontal: node.constraints.horizontal, }; } return style; } /** * 提取文件中的主要组件 */ private extractMainComponents(document: FigmaNode): FigmaNode[] { const components: FigmaNode[] = []; const traverse = (node: FigmaNode) => { // 收集有意义的节点(非容器节点或有样式的节点) if (this.isStyledNode(node)) { components.push(node); } // 递归遍历子节点 if (node.children) { node.children.forEach(traverse); } }; if (document.children) { document.children.forEach(traverse); } return components; } /** * 判断节点是否有样式信息 */ private isStyledNode(node: FigmaNode): boolean { return !!( node.fills?.length || node.strokes?.length || node.effects?.length || node.cornerRadius !== undefined || node.style || ['RECTANGLE', 'ELLIPSE', 'POLYGON', 'TEXT', 'COMPONENT', 'INSTANCE'].includes(node.type) ); } /** * 获取指定节点的CSS样式字符串 */ generateCSS(styleData: StyleData): string { const cssRules: string[] = []; // 位置和尺寸 if (styleData.position) { cssRules.push(`width: ${styleData.position.width}px`); cssRules.push(`height: ${styleData.position.height}px`); } // 背景色 if (styleData.fills && styleData.fills.length > 0) { const fill = styleData.fills[0]; if (fill.color) { const { r, g, b, a } = fill.color; if (a === 1) { cssRules.push(`background-color: rgb(${r}, ${g}, ${b})`); } else { cssRules.push(`background-color: rgba(${r}, ${g}, ${b}, ${a})`); } } } // 边框 if (styleData.strokes && styleData.strokes.length > 0) { const stroke = styleData.strokes[0]; const weight = styleData.strokeWeight || 1; if (stroke.color) { const { r, g, b, a } = stroke.color; if (a === 1) { cssRules.push(`border: ${weight}px solid rgb(${r}, ${g}, ${b})`); } else { cssRules.push(`border: ${weight}px solid rgba(${r}, ${g}, ${b}, ${a})`); } } } // 圆角 if (styleData.cornerRadius !== undefined) { cssRules.push(`border-radius: ${styleData.cornerRadius}px`); } // 文本样式 if (styleData.textStyle) { const textStyle = styleData.textStyle; if (textStyle.fontFamily) { cssRules.push(`font-family: "${textStyle.fontFamily}"`); } if (textStyle.fontSize) { cssRules.push(`font-size: ${textStyle.fontSize}px`); } if (textStyle.fontWeight) { cssRules.push(`font-weight: ${textStyle.fontWeight}`); } if (textStyle.letterSpacing) { cssRules.push(`letter-spacing: ${textStyle.letterSpacing}px`); } if (textStyle.lineHeight) { cssRules.push(`line-height: ${textStyle.lineHeight}px`); } if (textStyle.textAlign) { cssRules.push(`text-align: ${textStyle.textAlign.toLowerCase()}`); } if (textStyle.textColor) { const { r, g, b, a } = textStyle.textColor; if (a === 1) { cssRules.push(`color: rgb(${r}, ${g}, ${b})`); } else { cssRules.push(`color: rgba(${r}, ${g}, ${b}, ${a})`); } } } // 阴影效果 if (styleData.effects && styleData.effects.length > 0) { const shadows = styleData.effects .filter(effect => effect.type === 'DROP_SHADOW' && effect.visible) .map(effect => { const x = effect.offset?.x || 0; const y = effect.offset?.y || 0; const blur = effect.radius || 0; if (effect.color) { const { r, g, b, a } = effect.color; return `${x}px ${y}px ${blur}px rgba(${r}, ${g}, ${b}, ${a})`; } return ''; }) .filter(shadow => shadow); if (shadows.length > 0) { cssRules.push(`box-shadow: ${shadows.join(', ')}`); } } return cssRules.length > 0 ? cssRules.join(';\n ') + ';' : ''; } }

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Echoxiawan/figma-mcp-full-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server