svg实现图形编辑器系列二:精灵的开发和注册

1,666 阅读5分钟

在上篇 # 手把手教你用svg实现图形编辑器系列一 文章中我们介绍了图形编辑器的基本功能以及精灵舞台系统,本文将介绍如何开发一个精灵,并使用依赖反转思想,精灵与舞台解耦,实现精灵注册机制。

Demo体验链接:图形编辑器在线Demo

设计思路

在上篇文章 # 手把手教你用svg实现图形编辑器系列一 中提供了一个最小的demo演示图形编辑器的组成:

import { Stage } from "./stage";
import { Sprite } from "./sprite";
import { RectSprite } from "./sprites/rect";
import { LineSprite } from "./sprites/line";

export const StageDemo = ({ width, height }) => {
  return (
    <Stage width={width} height={height}>
      {/* 矩形精灵 */}
      <Sprite x={200} y={50}>
        <RectSprite width={400} height={240}></RectSprite>
      </Sprite>
      {/* 线段精灵 */}
      <Sprite x={100} y={350}>
        <LineSprite x1={0} y1={0} x2={300} y2={100}></LineSprite>
      </Sprite>
    </Stage>
  );
};

这个demo非常容易理解,将图形编辑器拆分为了舞台精灵容器精灵组件 三层,但是也存在以下问题:

  1. 精灵(素材)和舞台耦合在了一起,后续新增其他精灵都要来改这个组件,不符合开闭原则;
  2. 精灵的大小、位置、旋转角等通用属性每次都要手动消费,没有形成抽象;
  3. 精灵组件没有统一规范,不利于长久维护,大家可能写出形色各异的精灵组件;

针对以上几点,本文会对 图形编辑器的最小demo 进行重构,改善存在的缺点:

  • 提供精灵注册机制,使精灵和舞台解耦,想用什么精灵注册后就可使用;
  • 对精灵进行抽象建模,将精灵通用属性集成为类型,以便约束精灵的写法;
  • 提供精灵组件 基类 提供了实现组件的范例,后续精灵模仿范例的写法即可;

重构图形编辑器demo

一、对精灵建模

1. 精灵元数据类型描述

  • 我们将精灵的以下通用属性抽象出来:
    • 唯一id
    • 精灵类型type
    • 位置
    • 大小
    • 旋转角
属性属性名描述
唯一idid精灵的唯一id,不可重复
组件属性props不同精灵的props不同,例如线段需要起始点、文本需要字体排版等
大小sizewidth、height
位置coordinatex、y
旋转角angle旋转度数,范围为0-360,默认为0

// 尺寸
export interface ISize {
  width: number;
  height: number;
}
// 坐标
export interface ICoordinate {
  x: number;
  y: number;
}

// 精灵通用属性:大小、位置、旋转角
export interface ISpriteAttrs {
  size: ISize;
  coordinate: ICoordinate;
  angle: number;
}

// 精灵
export interface ISprite<IProps = any> {
  id: string;
  type: string;
  props: IProps;
  attrs: ISpriteAttrs;
}

2. 元数据举例(矩形精灵)

以下是矩形精灵的元数据描述,包含了以下信息:

  • 精灵通用基础属性
  • 矩形独有属性: 填充色、边框等
const rectMetaData = {
  id: "Rect1",
  type: "RectSprite",
  props: {
    fill: "#fdc5bf",
    stroke: 'red',
  },
  attrs: {
    coordinate: { x: 100, y: 100 },
    size: { width: 160, height: 100 },
    angle: 0
  }
}

其他类型的精灵按照此思路即可进行建模。

二、如何开发一个精灵

1. 精灵基类

  • 后面开发精灵都会继承这个类,主要定义了精灵的通用属性:

import React from 'react';
import type { IStageApis, ISprite } from '../interface';

export interface IBaseSpriteProps<IProps> {
  sprite: ISprite<IProps>;
  stage: IStageApis;
}

export class BaseSprite<IProps = any, IState = any> extends React.Component<
  IBaseSpriteProps<IProps>,
  IState
> {}

2. 矩形精灵

主要输出精灵的 元数据(Meta) ,这个meta中定义了精灵的以下信息:

  • 名字
  • 组件
  • 以后还会包含默认属性、锚点、端口等配置

import React from "react";
import { BaseSprite } from "../BaseSprite";
import type { ISpriteMeta, IDefaultGraphicProps } from "../../type";

type IProps = IDefaultGraphicProps;

// 矩形精灵
export const Rect = (props: IProps) => {
  return <rect x="0" y="0" stroke="#999" stroke-width="2" {...props}></rect>;
};

// 精灵的名字,这个应该是全局唯一的
const SpriteType = "RectSprite";

// 矩形精灵组件
export class RectSprite extends BaseSprite<IProps> {
  render() {
    const { sprite } = this.props;
    const { props, attrs } = sprite;
    const { width, height } = attrs.size;
    return (
      <>
        <Rect {...props} x={0} y={0} width={width} height={height} />
      </>
    );
  }
}

// 描述精灵的元数据
export const RectSpriteMeta: ISpriteMeta<IProps> = {
  // 类型,精灵的名字,全局唯一
  type: SpriteType,
  // 精灵组件
  spriteComponent: RectSprite
};

export default RectSpriteMeta;

三、如何注册精灵

1. 使用

我们将精灵的注册机制、渲染精灵等能力集成在 图形编辑器内核 里面,并对外暴露注册精灵向舞台中添加精灵等接口,演示如下:

import React, { useEffect, useRef } from "react";
import { GraphicEditorCore } from "./graphic-editor";
import { ISprite } from "./type";
import RectSpriteMeta from "./sprites/rect";

// 默认精灵列表
const defaultSpriteList: ISprite[] = [
  {
    id: "RectSprite1",
    type: "RectSprite",
    props: {
      fill: "#fdc5bf"
    },
    attrs: {
      coordinate: { x: 100, y: 100 },
      size: { width: 160, height: 100 },
      angle: 0
    }
  },
];

const Demo2 = () => {
  const editorRef = useRef<GraphicEditorCore>();
  useEffect(() => {
    const api = editorRef.current;
    // 注册精灵
    api?.registerSprite(RectSpriteMeta);
    // 向画布中添加精灵
    api?.addSpriteToStage(defaultSpriteList);
  }, []);
  return <GraphicEditorCore ref={editorRef as any} width={800} height={560} />;
};

export default Demo2;

2. 图形编辑器:提供注册精灵、渲染精灵、添加精灵到舞台的核心能力


import React from "react";
import { Stage } from "./stage";
import { Sprite } from "./sprite";
import { ISprite, ISpriteMeta } from "./type";

interface IProps {
  width: number;
  height: number;
  onReady?: () => void;
}

interface IState {
  spriteList: ISprite[];
}

export class GraphicEditorCore extends React.Component<IProps, IState> {
  private readonly registerSpriteMetaMap: Record<string, ISpriteMeta> = {};

  readonly state: IState = {
    spriteList: []
  };

  /**
   * 注册精灵
   * @param {ISprite} sprite
   */
  public registerSprite = (spriteMeta: ISpriteMeta) => {
    if (this.registerSpriteMetaMap[spriteMeta.type]) {
      console.warn(`Sprite ${spriteMeta.type} is already registered.`);
      return;
    }
    this.registerSpriteMetaMap[spriteMeta.type] = spriteMeta;
  };

  /**
   * 添加精灵到画布
   * @param {ISprite | ISprite[]} sprite
   */
  public addSpriteToStage = (sprite: ISprite | ISprite[]) => {
    const { spriteList } = this.state;
    const newSpriteList = [...spriteList];
    if (Array.isArray(sprite)) {
      newSpriteList.push(...sprite);
    } else {
      newSpriteList.push(sprite);
    }
    this.setState({
      spriteList: newSpriteList
    });
  };

  render() {
    const { registerSpriteMetaMap } = this;
    const { width, height } = this.props;
    const { spriteList } = this.state;
    return (
      <Stage width={width} height={height}>
        {/* 精灵列表 */}
        {spriteList.map((sprite) => {
          // 从注册好的精灵映射里拿到meta和精灵组件
          const spriteMeta = registerSpriteMetaMap[sprite.type];
          const SpriteComponent =
            (spriteMeta?.spriteComponent as any) ||
            (() => <text fill="red">Undefined Sprite: {sprite.type}</text>);
          const { attrs } = sprite;
          return (
            <Sprite
              key={sprite.id}
              x={attrs.coordinate.x}
              y={attrs.coordinate.y}
            >
              <SpriteComponent sprite={sprite} />
            </Sprite>
          );
        })}
      </Stage>
    );
  }
}


三、总结

本文对 图形编辑器的最小demo 进行重构,将精灵和舞台进行了解耦,提升了图形编辑器的可维护性;并提供了精灵的规范以及开发精灵的范例。

后续,我们会逐步加强舞台的编辑能力,例如最核心的 移动 缩放 旋转 三件套。

系列文章汇总

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

🔥 demo演示源码

最后应大家要求,这里放上code sandbox的demo演示源码:

image.png