svg实现图形编辑器系列十:工具栏&配置面板

2,650 阅读7分钟

一、工具栏

111111.png

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~

image.png

  • 内含的拾色器功能

ss

二、属性配置面板

这里本文就不展开具体如何使用 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. 配置面板图片展示

image.png

三、其他编辑功能推荐

还有一些零散的编辑功能,我们就不专门写文章介绍了,这里列举一下:

  • 保存画布为svg功能
  • 保存画布为png图片功能
  • 设置画布配置参数
    • 背景色
    • 画布高宽
    • 网格配置
    • 吸附配置
  • 精灵元数据可视化编辑
    • 用一个代码编辑框显示JSON.stringify后的精灵数据,修改后保存即可通过改数据更新画布和精灵
  • 全屏功能:stageDom.requestFullscreen()

四、总结

至此,我们的【svg实现图形编辑器系列】暂时告一段落,我们回忆一下这个系列的内容:

  • 我们首先使用一个最简单的react组件demo解释了精灵、舞台概念
  • 将react组件实现的精灵、舞台进行了抽象和架构优化,变成数据,且具有注册能力,舞台和精灵也抽象成了容易扩展的形式
  • 结合了移动、缩放、旋转等基础但核心的编辑能力
  • 结合了其他强化编辑能力
    • 吸附&辅助线
    • 辅助编辑锚点
    • 连接线、连接桩
    • 右键菜单
    • 快捷键
    • 撤销回退
  • 多选、组合、解组等批量操作能力
  • 精灵的编辑态,以及常用开发常用精灵
  • 介绍了通用的工具面板
  • 精灵需要复杂配置时使用配置面板

可以看到,我们逐渐强化了图形编辑器的编辑和渲染能力,使它变得越来却强大和好用。

1. 图形编辑器应用在产品中

基于此图形编辑器,我们可以进行一些抽象,暴露合理的api使它变成图形编辑器内核。

类似在线图形编辑器、思维导图、流程图、PPT等等产品都可基于此实现。大家可以探索更多的应用场景。

2. 系列文章汇总

  1. svg实现图形编辑器系列一:精灵系统
  2. svg实现图形编辑器系列二:精灵的开发和注册
  3. svg实现图形编辑器系列三:移动、缩放、旋转
  4. svg实现图形编辑器系列四:吸附&辅助线
  5. svg实现图形编辑器系列五:辅助编辑锚点
  6. svg实现图形编辑器系列六:链接线、连接桩
  7. svg实现图形编辑器系列七:右键菜单、快捷键、撤销回退
  8. svg实现图形编辑器系列八:多选、组合、解组
  9. svg实现图形编辑器系列九:精灵的编辑态&开发常用精灵
  10. svg实现图形编辑器系列十:工具栏&配置面板(最终篇)

3. 致谢

最后,非常感谢大家关注这个系列的文章~

后续作者【前端君】还会发布其他文章,关于图形编辑器有较好的思路也会来发布新的文章,欢迎大家继续关注~