- 已开源,欢迎star:github.com/alanyf/grap…
一、工具栏
1. 工具栏的设计要求
-
添加精灵
- 文本、图片、链接等基础精灵
- 形状精灵
-
精灵样式
- 填充
- 边框色
- 边框粗细
- 边框类型(虚线、实线等)
-
精灵可自己决定是否禁用哪些默认配置栏(如文本、图片不需要边框等配置)
-
工具栏支持通过注册来扩展
- 如线的端点类型配置(圆点、方点、箭头、首尾端点是否显示等)
-
更多复杂配置,推荐使用表单引擎,在右侧弹出属性配置框来实现
- 如文本组件扩展出字体大小、加粗、下划线、文字颜色、背景色、上下标等等
- 精灵定义表单的schema,双击后触发弹出,渲染schema为表单的解决方案可采用类似
formily方案
2. 工具栏配置设计
这块的注册机制和右键菜单非常类似
interface IToolbarItem {
name: string;
title: string;
// 是否禁用
disabled?: boolean | (ctx) => boolean,
// 是否显示
condition?: boolean | (ctx) => boolean,
// 图标
icon?: () => React.ReactNode;
// 节点内容,可以包含icon 和 弹出框详情配置
content?: (ctx: { stage: IStageAPis; sprite: ISprite }) => React.ReactNode;
}
/**
* 工具栏配置
*/
export const toolbarItems: IToolbarItem[] = [
{
name: 'fill',
title: '填充',
// 是否禁用,这里可以在精灵的meta上定义一个工具栏禁用映射,或者工具栏启用映射,在每个工具栏配置数据里消费一下就可以了
disabled: ({ sprite, spriteMeta }) => spriteMeta.disabledToolbarMap.fill,
content: () => {
return (
<Popover content={<ColorSelect {...} />}>
<FillIcon />
</Popover>
);
},
},
{
name: 'stroke',
title: '边框颜色',
content: () => { ... },
},
{
name: 'strokeWidth',
title: '边框粗细',
content: () => { ... },
},
{
name: 'strokeDashArray',
title: '边框类型',
content: () => { ... },
},
];
3. 工具栏的注册机制
export class Toolbar extends React.Component<IProps, IState> {
triggerList: any[] = [];
stage: GraphicEditorCore | null = null;
itemMap: Record<string, IToolbarItem> = {};
state: IState = {
itemList: []
};
componentDidMount() {
this.stage = this.props.getStage?.();
}
public registerItemList = (_itemList: IToolbarItem[]) => {
const { itemList } = this.state;
_itemList.forEach((e) => {
this.itemMap[e.key] = e;
});
this.setState({ itemList: [...itemList, ..._itemList] });
};
public registerItem = (item: IToolbarItem) => {
const { itemList } = this.state;
this.itemMap[item.key] = item;
this.setState({ itemList: [...itemList, item] });
return () => this.remove(item);
};
public remove = (item: IToolbarItem | string) => {
const { itemList } = this.state;
const list = [...itemList];
const key = typeof item === "string" ? item : item.key;
const index = list.findIndex((e) => e.key === key);
delete this.itemMap[key];
if (index !== -1) {
list.splice(index);
this.setState({ itemList: list });
}
};
public has = (item: IToolbarItem | string) => {
const key = typeof item === "string" ? item : item.key;
return Boolean(this.itemMap[key]);
};
render() {
const { stage, sprite } = this.props;
const { itemList } = this.state;
return (
<div>
{itemList.map(item => (
<item.content
key={item.name}
stage={stage}
sprite={sprite}
/>>
))}
</div>
);
}
}
工具栏内置工具
1. 撤销、重做
借助本系列之前的一篇文章中实现的历史记录功能,这里我们只需要在工具栏上放两个icon,点击时调用 舞台 和历史记录相关的 redo ondo api即可
2. 文本框、图片、链接等最常用的精灵
之前我们介绍过有一个向舞台添加精灵的api,这里文本和链接精灵只需调用api即可,如:
// 精灵
const textSprite = {
id: "TextSprite1",
type: "TextSprite",
props: { content: "这是一段文本" },
attrs: {
coordinate: { x: 100, y: 100 },
size: { width: 160, height: 30 },
}
}
// 向画布中添加精灵
stage.apis.addSpriteToStage(textSprite);
图片组件如果想使用本地上传的图片的话,则稍微复杂一些,就要先处理为base64格式,或者将图片上传到服务器,然后再把图片链接传给图片精灵的props:
// 处理点击添加图片精灵的入口函数
const handleAddImageToStage = (e: any) => {
getImageInfo(e)
.then(params => {
addImageSprite(params);
})
.catch(err => message.error(`上传文件失败, ${err}`));
};
// 添加图片精灵
const addImageSprite = (params: { base64: string; width: number; height: number }) => {
const { base64 } = params;
let { width, height } = params;
const { stage } = this.props;
const { size } = stage.store();
if (width > size.width || height > size.height) {
const rate = Math.min(size.width / width, size.height / height);
width *= rate;
height *= rate;
}
const sprite: any = {
type: 'ImageSprite',
props: { url: base64 },
attrs: {
size: { width, height },
coordinate: {
x: (size.width - width) / 2,
y: (size.height - height) / 2,
},
},
};
// 向画布中添加精灵
return stage.apis.addSpriteToStage(sprite);
};
/**
* 获取上传图片文件的信息
* @param e
* @returns
*/
const getImageInfo = (
e: any,
): Promise<{ base64: string; width: number; height: number; img: any }> =>
new Promise((resolve, reject) => {
const file = new FileReader();
file.onload = (e: any) => {
const base64 = e.target.result;
getBase64ImageInfo(base64)
.then((info: any) => resolve(info))
.catch(err => reject(err));
};
if (e.target.files && e.target.files.length > 0) {
file.readAsDataURL(e.target.files[0]);
}
file.onerror = (err: any) => reject(err);
});
// 获取base64图片的尺寸
const getBase64ImageInfo = (
base64: string,
): Promise<{ base64: string; width: number; height: number; img: any }> =>
new Promise((resolve, reject) => {
const img = document.createElement('img');
img.src = base64;
img.onerror = (err: any) => reject(err);
img.onload = () => {
resolve({
base64,
width: img.width,
height: img.height,
img,
});
};
});
3. 填充、边框等选择颜色的颜色选择器
这里我以前写过一篇关于颜色选择器的文章,欢迎大家参考:实现超好用的React颜色选择器组件
这个组件已经发布成了npm包,欢迎大家下载安装使用~
此颜色选择器组件已经开源,欢迎大家给个star~
- 内含的拾色器功能
二、属性配置面板
这里本文就不展开具体如何使用 formily 实现 SchemaForm (数据驱动表单),感兴趣的大家可以通过 formily官网 学习使用。
关于如何实现 SchemaForm (数据驱动表单),如果大家非常感兴趣,可以在评论区说出来,如果感兴趣的人比较多的话,作者后面专门写一篇文章介绍如何实现SchemaForm。
1. 属性配置schema样例
// 表单配置schema,可以是静态数据,也可以是函数,根据精灵的属性不同返回不同的schema
const configSchema: IConfigSchema = {
content: {
type: 'string',
title: '文本',
defaultValue: '文本',
},
color: {
type: 'color',
title: '颜色',
},
backgroundColor: {
type: 'color',
title: '背景色',
},
fontSize: {
type: 'number',
title: '字大小',
options: [
{ label: '小', value: 12 },
{ label: '正常', value: 14 },
{ label: '大', value: 16 },
],
},
fontFamily: {
type: 'string',
title: '字体',
options: [
{ label: '宋体', value: '宋体' },
{ label: 'Times New Roman', value: 'Times New Roman' },
{ label: '微软雅黑', value: '微软雅黑' },
],
},
fontWeight: {
type: 'string',
title: '粗细',
options: [
{ label: '细', value: 'lighter' },
{ label: '正常', value: 'normal' },
{ label: '粗', value: 'border' },
],
},
};
2. 精灵meta
export const TextSpriteMeta: ISpriteMeta<IProps> = {
type: SpriteType,
spriteComponent: TextSprite,
initProps: {
content: '',
},
// 表单配置schema,可以是静态数据,也可以是函数,根据精灵的属性不同返回不同的schema
configSchema
};
3. 表单引擎
const ConfigForm = ({ stage, sprite, getSpriteMeta }) => {
const meta = getSpriteMeta(sprite);
const { configSchema } = meta;
return (
// 这个SchemaForm基于formily封装而来
<SchemaForm
// 传入精灵的配置schema
schema={configSchema}
// 表单变化时修改精灵的属性
onChange={(params) => stage.apis.updateSpriteProps(params)}
/>
);
};
4. 配置面板图片展示
三、其他编辑功能推荐
还有一些零散的编辑功能,我们就不专门写文章介绍了,这里列举一下:
- 保存画布为svg功能
- 保存画布为png图片功能
- 设置画布配置参数
- 背景色
- 画布高宽
- 网格配置
- 吸附配置
- 精灵元数据可视化编辑
- 用一个代码编辑框显示JSON.stringify后的精灵数据,修改后保存即可通过改数据更新画布和精灵
- 全屏功能:
stageDom.requestFullscreen()
四、总结
至此,我们的【svg实现图形编辑器系列】暂时告一段落,我们回忆一下这个系列的内容:
- 我们首先使用一个最简单的react组件demo解释了精灵、舞台概念
- 将react组件实现的精灵、舞台进行了抽象和架构优化,变成数据,且具有注册能力,舞台和精灵也抽象成了容易扩展的形式
- 结合了移动、缩放、旋转等基础但核心的编辑能力
- 结合了其他强化编辑能力
- 吸附&辅助线
- 辅助编辑锚点
- 连接线、连接桩
- 右键菜单
- 快捷键
- 撤销回退
- 多选、组合、解组等批量操作能力
- 精灵的编辑态,以及常用开发常用精灵
- 介绍了通用的工具面板
- 精灵需要复杂配置时使用配置面板
可以看到,我们逐渐强化了图形编辑器的编辑和渲染能力,使它变得越来却强大和好用。
1. 图形编辑器应用在产品中
基于此图形编辑器,我们可以进行一些抽象,暴露合理的api使它变成图形编辑器内核。
类似在线图形编辑器、思维导图、流程图、PPT等等产品都可基于此实现。大家可以探索更多的应用场景。
2. 系列文章汇总
- svg实现图形编辑器系列一:精灵系统
- svg实现图形编辑器系列二:精灵的开发和注册
- svg实现图形编辑器系列三:移动、缩放、旋转
- svg实现图形编辑器系列四:吸附&辅助线
- svg实现图形编辑器系列五:辅助编辑锚点
- svg实现图形编辑器系列六:链接线、连接桩
- svg实现图形编辑器系列七:右键菜单、快捷键、撤销回退
- svg实现图形编辑器系列八:多选、组合、解组
- svg实现图形编辑器系列九:精灵的编辑态&开发常用精灵
- svg实现图形编辑器系列十:工具栏&配置面板(最终篇)
3. 致谢
最后,非常感谢大家关注这个系列的文章~
后续作者【前端君】还会发布其他文章,关于图形编辑器有较好的思路也会来发布新的文章,欢迎大家继续关注~