在上篇 # 手把手教你用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非常容易理解,将图形编辑器拆分为了舞台
、精灵容器
、精灵组件
三层,但是也存在以下问题:
- 精灵(素材)和舞台耦合在了一起,后续新增其他精灵都要来改这个组件,不符合开闭原则;
- 精灵的大小、位置、旋转角等通用属性每次都要手动消费,没有形成抽象;
- 精灵组件没有统一规范,不利于长久维护,大家可能写出形色各异的精灵组件;
针对以上几点,本文会对 图形编辑器的最小demo 进行重构,改善存在的缺点:
- 提供精灵注册机制,使精灵和舞台解耦,想用什么精灵注册后就可使用;
- 对精灵进行抽象建模,将精灵通用属性集成为类型,以便约束精灵的写法;
- 提供精灵组件
基类
提供了实现组件的范例,后续精灵模仿范例的写法即可;
重构图形编辑器demo
一、对精灵建模
1. 精灵元数据类型描述
- 我们将精灵的以下通用属性抽象出来:
- 唯一id
- 精灵类型type
- 位置
- 大小
- 旋转角
属性 | 属性名 | 描述 |
---|---|---|
唯一id | id | 精灵的唯一id,不可重复 |
组件属性 | props | 不同精灵的props不同,例如线段需要起始点、文本需要字体排版等 |
大小 | size | width、height |
位置 | coordinate | x、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 进行重构,将精灵和舞台进行了解耦,提升了图形编辑器的可维护性;并提供了精灵的规范以及开发精灵的范例。
后续,我们会逐步加强舞台的编辑能力,例如最核心的 移动
缩放
旋转
三件套。
系列文章汇总
- svg实现图形编辑器系列一:精灵系统
- svg实现图形编辑器系列二:精灵的开发和注册
- svg实现图形编辑器系列三:移动、缩放、旋转
- svg实现图形编辑器系列四:吸附&辅助线
- svg实现图形编辑器系列五:辅助编辑锚点
- svg实现图形编辑器系列六:链接线、连接桩
- svg实现图形编辑器系列七:右键菜单、快捷键、撤销回退
- svg实现图形编辑器系列八:多选、组合、解组
- svg实现图形编辑器系列九:精灵的编辑态&开发常用精灵
- svg实现图形编辑器系列十:工具栏&配置面板(最终篇)
🔥 demo演示源码
最后应大家要求,这里放上code sandbox的demo演示源码
: