Skip to content

Commit 90f9847

Browse files
committed
✨ feat: support svg radial gradient
1 parent 1072418 commit 90f9847

File tree

8 files changed

+316
-195
lines changed

8 files changed

+316
-195
lines changed

src/model/Layer/Svg.ts

Lines changed: 74 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,7 @@ import Fill from '../Style/Fill';
1818
import Color from '../Style/Color';
1919

2020
import { defaultExportOptions } from '../utils';
21-
import {
22-
getRenderedSvgString,
23-
getUseReplacement,
24-
inlineStyles,
25-
optimizeSvgString,
26-
} from '../../utils/svg';
21+
2722
import { getGroupLayout } from '../../utils/layout';
2823

2924
import {
@@ -32,6 +27,7 @@ import {
3227
FrameType,
3328
ShapeGroupType,
3429
SvgDefsStyle,
30+
SvgLayerType,
3531
} from '../type';
3632

3733
interface SvgInitParams extends Partial<BaseLayerParams> {
@@ -242,80 +238,6 @@ class Svg extends BaseLayer {
242238
return scale;
243239
}
244240

245-
/**
246-
* 从 Url 初始化 Svg
247-
* @param url Url
248-
* @param frame 尺寸
249-
*/
250-
static async initFromUrl(url: string, frame: Frame) {
251-
let data;
252-
try {
253-
data = await fetch(url, {
254-
mode: 'cors',
255-
});
256-
} catch (error) {
257-
const maybeCorsError = error.toString().includes('Failed to fetch');
258-
if (maybeCorsError) {
259-
const corsPrefix = `https://cors-anywhere.herokuapp.com/`;
260-
data = await fetch(corsPrefix + url, {
261-
mode: 'cors',
262-
});
263-
console.warn(
264-
'该图片存在跨域问题! 请在服务器端设置允许图片跨域,以提升解析速度:',
265-
url,
266-
);
267-
}
268-
}
269-
if (!data) return;
270-
271-
let svgString = await data.text();
272-
273-
const { x, y, width, height } = frame;
274-
275-
svgString = await getRenderedSvgString(svgString, { width, height });
276-
277-
return new Svg({ svgString, x, y, width, height });
278-
}
279-
280-
/**
281-
* 将 Svg Node 转为 SvgString
282-
* @param svgNode
283-
*/
284-
static getSVGString = async (svgNode: Element): Promise<string> => {
285-
// NOTE: this code modifies the original node by inlining all styles
286-
// this is not ideal and probably fixable
287-
const queue = Array.from(svgNode.children);
288-
289-
while (queue.length) {
290-
const node = queue.pop();
291-
292-
if (
293-
!(node instanceof SVGElement) ||
294-
node instanceof SVGDefsElement ||
295-
node instanceof SVGTitleElement
296-
) {
297-
continue;
298-
}
299-
300-
if (node instanceof SVGUseElement) {
301-
const replacement = getUseReplacement(node);
302-
303-
if (replacement) {
304-
node.parentNode!.replaceChild(replacement, node);
305-
queue.push(replacement);
306-
}
307-
continue;
308-
}
309-
310-
if (node) {
311-
inlineStyles(<SVGElement>node);
312-
Array.from(node.children).forEach((child) => queue.push(child));
313-
}
314-
}
315-
316-
return optimizeSvgString(svgNode.outerHTML);
317-
};
318-
319241
/**
320242
* 一致化缠绕规则参数
321243
* @param ruleStr
@@ -348,7 +270,7 @@ class Svg extends BaseLayer {
348270
* 解析 Svgson 变成 layer
349271
* @param node
350272
*/
351-
parseSvgson = (node: svgson.INode): any => {
273+
parseSvgson = (node: svgson.INode): SvgLayerType => {
352274
switch (node.name) {
353275
// 全局定义
354276
case 'defs':
@@ -396,16 +318,17 @@ class Svg extends BaseLayer {
396318

397319
const shapeGroupType = Svg.pathToShapeGroup(path);
398320

321+
const isClose = !shapeGroupType.shapes.every((shape) => !shape.isClose);
399322
const shapePaths = this.shapeGroupDataToLayers(shapeGroupType);
400323

401324
if (shapePaths.length === 1) {
402325
const shapePath = shapePaths[0];
403-
shapePath.style = this.parseNodeAttrToStyle(node.attributes);
326+
shapePath.style = this.parseNodeAttrToStyle(node.attributes, isClose);
404327
}
405328
const shapeGroup = new ShapeGroup(shapeGroupType.frame);
406329

407330
shapeGroup.addLayers(shapePaths);
408-
shapeGroup.style = this.parseNodeAttrToStyle(node.attributes);
331+
shapeGroup.style = this.parseNodeAttrToStyle(node.attributes, isClose);
409332

410333
return shapeGroup;
411334
}
@@ -430,13 +353,52 @@ class Svg extends BaseLayer {
430353
y: parseFloat(attributes.y2) / 100,
431354
},
432355
stops: defsNode.children.map((item) => {
433-
const {
434-
// TODO 有待改造 Stop 方法
435-
// offset,
436-
stopColor,
437-
} = item.attributes;
438-
// const color = new Color(stopColor);
439-
return stopColor;
356+
const { offset, stopColor, stopOpacity } = item.attributes;
357+
const color = new Color(stopColor);
358+
359+
return {
360+
color: [
361+
color.red,
362+
color.green,
363+
color.blue,
364+
Number(stopOpacity) || 1,
365+
],
366+
offset: parseFloat(offset) / 100,
367+
};
368+
}),
369+
});
370+
case 'radialGradient':
371+
console.log(attributes);
372+
return new Gradient({
373+
type: SketchFormat.GradientType.Radial,
374+
name: attributes.id,
375+
from: {
376+
// 解析得到的是 109% 这样的值
377+
x: parseFloat(attributes.fx) / 100,
378+
y: parseFloat(attributes.fy) / 100,
379+
},
380+
to: {
381+
x: (parseFloat(attributes.cx) + parseFloat(attributes.r)) / 100,
382+
y: parseFloat(attributes.cy) / 100,
383+
},
384+
// radius: parseFloat(attributes.r) / 100,
385+
stops: defsNode.children.map((item) => {
386+
const { offset, stopColor, stopOpacity } = item.attributes;
387+
const color = new Color(stopColor);
388+
389+
console.log(stopOpacity);
390+
391+
const opacity = Number(stopOpacity);
392+
393+
return {
394+
color: [
395+
color.red,
396+
color.green,
397+
color.blue,
398+
isNaN(opacity) ? 1 : opacity,
399+
],
400+
offset: parseFloat(offset) / 100,
401+
};
440402
}),
441403
});
442404
case 'style':
@@ -454,14 +416,19 @@ class Svg extends BaseLayer {
454416
/**
455417
* 解析 Node 的 Attribute 变成 style
456418
* @param attributes node 的属性
419+
* @param isClose
457420
*/
458-
parseNodeAttrToStyle = (attributes: svgson.INode['attributes']) => {
421+
parseNodeAttrToStyle = (
422+
attributes: svgson.INode['attributes'],
423+
isClose: boolean = true,
424+
) => {
459425
const {
460426
stroke,
461427
strokeWidth,
462428
fill: fillStr,
463429
style: styleString,
464430
class: className,
431+
opacity,
465432
} = attributes;
466433

467434
const style = new Style();
@@ -479,10 +446,12 @@ class Svg extends BaseLayer {
479446
}
480447
}
481448

482-
// 直接使用自带的 fill
483-
484-
const baseFill = this.getFillByString(fillStr);
485-
if (baseFill) style.fills.push(baseFill);
449+
// 如果闭合了的话
450+
// 直接使用默认填充
451+
if (isClose) {
452+
const baseFill = this.getFillByString(fillStr);
453+
if (baseFill) style.fills.push(baseFill);
454+
}
486455

487456
// 如果存在currentColor 则采用 inline Style 的 fill
488457
if (fillStr === 'currentColor' && styleObj?.fill) {
@@ -497,6 +466,8 @@ class Svg extends BaseLayer {
497466
});
498467
}
499468

469+
// 设置不透明度
470+
style.opacity = Number(opacity) || 1;
500471
return style;
501472
};
502473

@@ -696,18 +667,24 @@ class Svg extends BaseLayer {
696667
return classStyle?.rules.find((r) => r.className === `.${className}`);
697668
};
698669

670+
/**
671+
* 根据 fill 字符填充 Fill 对象
672+
* @param fill 填充文本
673+
*/
699674
private getFillByString = (fill: string) => {
700675
if (fill === 'none') return;
701676

702-
if (!fill)
677+
// TODO 针对 path 类型的对象 如果没有 fill 不能默认填充黑色
678+
if (!fill) {
703679
return new Fill({ type: SketchFormat.FillType.Color, color: '#000' });
680+
}
704681

705682
if (fill.startsWith('url')) {
706683
// 说明来自 defs
707684
const id = /url\(#(.*)\)/.exec(fill)?.[1];
708685
// 从 defs 中拿到相应的配置项
709686
const defsFill = this.defs.find(
710-
(def) => def.class === 'gradient' && def.name === id,
687+
(def) => def?.class === 'gradient' && def.name === id,
711688
);
712689

713690
switch (defsFill?.class) {

src/model/Style/Gradient.ts

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,8 @@
11
import SketchFormat from '@sketch-hq/sketch-file-format-ts';
22

3-
import { GradientType } from '@sketch-hq/sketch-file-format-ts/dist/cjs/v3-types';
4-
import Color, { ColorParam } from './Color';
3+
import Color from './Color';
54
import BaseStyle from '../Base/BaseStyle';
6-
import { CGPoint } from '../../type';
7-
8-
export interface GradientProps {
9-
type?: SketchFormat.GradientType;
10-
to?: CGPoint;
11-
from?: CGPoint;
12-
stops?: ColorParam[];
13-
name?: string;
14-
}
5+
import { CGPoint, ColorStop, GradientProps } from '../../type';
156

167
/**
178
* 渐变对象
@@ -24,7 +15,7 @@ class Gradient extends BaseStyle {
2415
this.name = 'gradient';
2516
return;
2617
}
27-
const { from, to, stops, type, name } = props;
18+
const { from, to, stops, type, name, radius } = props;
2819

2920
if (from) {
3021
this.from = from;
@@ -33,11 +24,27 @@ class Gradient extends BaseStyle {
3324
this.to = to;
3425
}
3526
if (stops) {
36-
this.stops = stops.map((color) => new Color(color));
27+
this.stops = stops.map((stopParam, index) => {
28+
// 判断是对象类型的 stop 参数
29+
if (typeof stopParam === 'object' && 'color' in stopParam) {
30+
return {
31+
color: new Color(stopParam.color),
32+
offset: stopParam.offset
33+
? stopParam.offset
34+
: index / (this.stops.length - 1),
35+
};
36+
}
37+
38+
// 不然就是颜色类型的 stop 参数
39+
return { color: new Color(stopParam) };
40+
});
3741
}
3842
if (type) {
3943
this.type = type;
4044
}
45+
if (type === SketchFormat.GradientType.Radial && radius) {
46+
this.ellipseLength = radius;
47+
}
4148
this.name = name || 'gradient';
4249
}
4350

@@ -51,7 +58,7 @@ class Gradient extends BaseStyle {
5158
/**
5259
* 色彩节点
5360
*/
54-
stops: Color[] = [];
61+
stops: ColorStop[] = [];
5562

5663
/**
5764
* 终点
@@ -61,7 +68,12 @@ class Gradient extends BaseStyle {
6168
/**
6269
* 渐变类型
6370
* */
64-
type: SketchFormat.GradientType = GradientType.Linear;
71+
type: SketchFormat.GradientType = SketchFormat.GradientType.Linear;
72+
73+
/**
74+
* 如果是 Radial 渐变,由这个参数控制椭圆长轴
75+
*/
76+
ellipseLength: number = 1;
6577

6678
/**
6779
* 转为 Sketch JSON 对象
@@ -72,7 +84,7 @@ class Gradient extends BaseStyle {
7284

7385
return {
7486
_class: SketchFormat.ClassValue.Gradient,
75-
elipseLength: 0, // 这个字段应该是废弃字段
87+
elipseLength: this.ellipseLength,
7688
from: `{${from.x}, ${from.y}}`,
7789
gradientType: this.type,
7890
to: `{${to.x}, ${to.y}}`,
@@ -83,10 +95,19 @@ class Gradient extends BaseStyle {
8395
/**
8496
* 将 stop 数组转换为 Sketch 使用的对象
8597
* */
86-
getSketchStop = (color: Color, index: number): SketchFormat.GradientStop => ({
98+
getSketchStop = (
99+
colorStop: ColorStop,
100+
index: number,
101+
): SketchFormat.GradientStop => ({
87102
_class: 'gradientStop',
88-
color: color.toSketchJSON(),
89-
position: index / (this.stops.length - 1),
103+
color: colorStop.color.toSketchJSON(),
104+
105+
position:
106+
// 如果有 offset 则使用 offset
107+
colorStop.offset
108+
? colorStop.offset
109+
: // 否则均分
110+
index / (this.stops.length - 1),
90111
});
91112
}
92113

src/model/type.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,15 @@ export interface SvgDefsStyle {
122122
class: 'classStyle';
123123
rules: CssStyleRule[];
124124
}
125+
export type SvgLayerType =
126+
| Group
127+
| ShapeGroup
128+
| Ellipse
129+
| Rectangle
130+
| Text
131+
| undefined
132+
| SvgLayerType[];
133+
125134
export interface CssStyleRule {
126135
className: string;
127136
styles: { [key: string]: string };

0 commit comments

Comments
 (0)