svg实现图形编辑器系列八:多选、组合、解组

在之前的系列文章中,我们介绍了图形编辑器基础的 移动缩放旋转 等基础编辑能力,以及很多辅助编辑的能力。本文将继续介绍提升编辑效率的功能:多选、组合、解组,以方便批量操作精灵,提升效率。

一、多选

  • 多选效果演示
    svg实现图形编辑器系列八:多选、组合、解组

1. 选中精灵变为选中精灵列表

  • activeSprite: ISprite 变为 activeSpriteList: ISprite[]

  • 对应的数据操作api也要变化


interface IState {
  spriteList: ISprite[];
  activeSpriteList: ISprite[];
}
export class GraphicEditorCore extends React.Component<IProps, IState> {
  readonly state: IState = {
    spriteList: [],
    activeSpriteList: []
  };
  // 设置选中精灵列表
  updateActiveSpriteList = (activeSpriteList: ISprite[]) => {
    this.setState({ activeSpriteList });
  };
}

2. 选框矩形大小和位置由精灵列表计算而来

class ActiveSpriteContainer extends React.Component<IProps, IState> {
  render() {
    const { activeSpriteList } = this.props;
    let activeRect: ISizeCoordinate = { width: 0, height: 0, x: 0, y: 0 };
    const angle = activeSprite?.attrs?.angle || 0;
    // 选框矩形大小和位置由精灵列表计算而来
    activeRect = getActiveSpriteRect(activeSpriteList);
    return (
      <>
        <g
          className="active-sprites-container"
          transform={`rotate(${angle || 0}, ${activeRect.x + activeRect.width / 2} ${
            activeRect.y + activeRect.height / 2
          })`}
        >
          // 省略...
        </g>
    )
  }
}

/**
 * 计算选中所有精灵的矩形区域
 * @param activeSpriteList 精灵列表
 * @param registerSpriteMetaMap 注册的精灵映射
 * @returns
 */
export const getActiveSpriteRect = (activeSpriteList: ISprite[]) => {
  const posMap = {
    minX: Infinity,
    minY: Infinity,
    maxX: 0,
    maxY: 0
  };
  activeSpriteList.forEach((sprite: ISprite) => {
    const { size, coordinate } = sprite.attrs;
    const { width = 0, height = 0 } = size;
    const { x = 0, y = 0 } = coordinate;
    if (x < posMap.minX) {
      posMap.minX = x;
    }
    if (y < posMap.minY) {
      posMap.minY = y;
    }
    if (x + width > posMap.maxX) {
      posMap.maxX = x + width;
    }
    if (y + height > posMap.maxY) {
      posMap.maxY = y + height;
    }
  });
  return {
    width: posMap.maxX - posMap.minX,
    height: posMap.maxY - posMap.minY,
    x: posMap.minX,
    y: posMap.minY
  } as ISizeCoordinate;
};

3. 点击根据事件判断选中哪个精灵

  • 点击要选中最顶层组合,dom遍历要一直向上找到直属于舞台的dom对应的精灵
/**
 * 根据类名寻找精灵
 * @param dom dom元素
 * @param className css类名
 * @return dom | null
 */
export function findSpriteDomByClass(dom: any, className: string): any {
  const domList = findParentListByClass(dom, className);
  return domList.pop();
}

/**
 * 根据类名寻找所有满足条件的父元素
 * @param dom dom元素
 * @param className css类名
 * @return dom | null
 */
export function findParentListByClass(_dom: any, _className: string): any {
  const domList: any[] = [];
  const dfs = (dom: any, className: string): any => {
    if (!dom || dom.tagName === "BODY") {
      return;
    }
    if (dom.classList.contains(className)) {
      domList.push(dom);
    }
    return dfs(dom.parentNode, className);
  };

  dfs(_dom, _className);
  return domList;
}

4. 鼠标框选

  • 框选示意图

svg实现图形编辑器系列八:多选、组合、解组


import React from 'react';
import type { ICoordinate, ISprite, IStageApis } from '../interface';
import { getStageMousePoint } from '../utils/tools';
interface IProps {
stage: IStageApis;
}
interface IState {
initMousePos: ICoordinate;
currentMousePos: ICoordinate;
}
class SelectRect extends React.Component<IProps, IState> {
readonly state = {
initMousePos: { x: 0, y: 0 },
currentMousePos: { x: 0, y: 0 },
};
private readonly onStageMouseDown = (e: any) => {
const { stage } = this.props;
if (e.target.classList.contains('lego-stage-container')) {
const { coordinate, scale = 1 } = stage.store();
const currentMousePos = getStageMousePoint(e, coordinate, scale);
this.setState({
initMousePos: { ...currentMousePos },
currentMousePos: { ...currentMousePos },
});
document.addEventListener('mousemove', this.onStageMouseMove, false);
document.addEventListener('mouseup', this.onStageMouseUp, false);
}
};
private readonly onStageMouseUp = () => {
this.setState({
initMousePos: { x: 0, y: 0 },
currentMousePos: { x: 0, y: 0 },
});
document.removeEventListener('mousemove', this.onStageMouseMove, false);
document.removeEventListener('mouseup', this.onStageMouseUp, false);
};
private readonly onStageMouseMove = (e: any) => {
const { stage } = this.props;
const { coordinate, scale = 1 } = stage.store();
const currentMousePos = getStageMousePoint(e, coordinate, scale);
this.setState({ currentMousePos });
this.handleSelectSprites();
};
// 计算框选范围内包含的所有精灵
private readonly handleSelectSprites = () => {
const { stage } = this.props;
const { spriteList } = stage.store();
const { initMousePos, currentMousePos } = this.state;
const minX = Math.min(initMousePos.x, currentMousePos.x);
const maxX = Math.max(initMousePos.x, currentMousePos.x);
const minY = Math.min(initMousePos.y, currentMousePos.y);
const maxY = Math.max(initMousePos.y, currentMousePos.y);
const activeSpriteList: ISprite[] = [];
spriteList.forEach((sprite: ISprite) => {
const { x, y } = sprite.attrs.coordinate;
const { width, height } = sprite.attrs.size;
if (x >= minX && x + width <= maxX && y >= minY && y + height <= maxY) {
activeSpriteList.push(sprite);
}
});
stage.apis.setActiveSpriteList(activeSpriteList);
};
componentDidMount() {
document.addEventListener('mousedown', this.onStageMouseDown);
}
componentWillUnmount() {
document.removeEventListener('mousedown', this.onStageMouseDown);
document.removeEventListener('mousemove', this.onStageMouseMove);
}
render() {
const { initMousePos, currentMousePos } = this.state;
return (
<rect
x={Math.min(currentMousePos.x, initMousePos.x)}
y={Math.min(currentMousePos.y, initMousePos.y)}
width={Math.abs(currentMousePos.x - initMousePos.x)}
height={Math.abs(currentMousePos.y - initMousePos.y)}
stroke="#0067ed"
strokeWidth="1"
fill="#e6f6ff"
opacity=".5"></rect>
);
}
}
export default SelectRect;

二、组合、解组

  • 组合解组效果图
    svg实现图形编辑器系列八:多选、组合、解组

1. 精灵列表变为精灵树

const spriteList = [
{
id: "Group1",
type: "GroupSprite",
props: {},
attrs: {
coordinate: { x: 100, y: 240 },
size: { width: 300, height: 260 },
angle: 0
},
children: [
{
id: "RectSprite2",
type: "RectSprite",
attrs: {
coordinate: { x: 0, y: 0 },
size: { width: 160, height: 100 },
angle: 0
}
},
{
id: "RectSprite3",
type: "RectSprite",
attrs: {
coordinate: { x: 200, y: 100 },
size: { width: 100, height: 160 },
angle: 0
}
}
]
}
]

2. 支持递归渲染


export class GraphicEditorCore extends React.Component<IProps, IState> {
// 新增递归渲染精灵的方法
renderSprite = (sprite: ISprite) => {
const { registerSpriteMetaMap } = this;
// 从注册好的精灵映射里拿到meta和精灵组件
const spriteMeta = registerSpriteMetaMap[sprite.type];
const SpriteComponent =
(spriteMeta?.spriteComponent as any) ||
(() => <text fill="red">Undefined Sprite: {sprite.type}</text>);
// 如果是组,就递归渲染
if (isGroupSprite(sprite)) {
const { children } = sprite;
return (
<Sprite key={sprite.id} sprite={sprite}>
<rect
x="0"
y="0"
width="100%"
height="100%"
fill="transparent"
></rect>
{children?.map((childSprite) => this.renderSprite(childSprite))}
</Sprite>
);
}
return (
<Sprite key={sprite.id} sprite={sprite}>
<SpriteComponent sprite={sprite} />
</Sprite>
);
};
render() {
const { registerSpriteMetaMap, stage } = this;
const { width, height } = this.props;
const { spriteList, activeSpriteList } = this.state;
return (
<Stage id="graphic-editor-stage" width={width} height={height}>
{/* 精灵列表 */}
{spriteList.map((sprite) => this.renderSprite(sprite))}
<Drag
scale={1}
stage={stage}
pressShift={false}
activeSpriteList={activeSpriteList}
registerSpriteMetaMap={registerSpriteMetaMap}
/>
</Stage>
);
}

3. 组合和解组的精灵计算


export const isGroupSprite = (sprite?: ISprite) =>
Boolean(sprite?.type?.toLocaleLowerCase()?.includes("group"));
// 多个精灵组合为一个【组合精灵】
export const makeSpriteGroup = (activeSpriteList: ISprite[]) => {
const { x, y, width, height } = getActiveSpriteRect(activeSpriteList);
const groupSprite: ISprite = {
type: "GroupSprite",
id: `Group_${Math.floor(Math.random() * 10000)}`,
attrs: {
size: { width, height },
coordinate: { x, y },
angle: 0
},
children: activeSpriteList.map((sprite) => {
const { coordinate } = sprite.attrs;
return {
...sprite,
attrs: {
...sprite.attrs,
coordinate: {
x: coordinate.x - x,
y: coordinate.y - y
}
}
};
})
};
return groupSprite;
};
// 【组合精灵】拆分为多个精灵
export const splitSpriteGroup = (sprite: ISprite) => {
const { x, y } = getActiveSpriteRect([sprite]);
if (!sprite?.children || sprite?.children?.length < 1) {
return [];
}
const getAngle = (n: any) => Number(n) || 0;
const spriteList = sprite.children.map((child: ISprite) => {
const { coordinate, angle } = child.attrs;
return {
...child,
attrs: {
...child.attrs,
// 处理角度,拆分后的精灵角度等于组合角度加自己的角度
//(PS: 这个直接加的算法不准确,应该关注精灵的旋转点,这里只简写这样写)
angle: getAngle(angle) + getAngle(sprite.attrs.angle),
// 处理定位,拆分后的精灵定位等于组合定位加自己的定位
coordinate: {
x: coordinate.x + x,
y: coordinate.y + y
}
}
};
});
return spriteList;
};

原文链接:https://juejin.cn/post/7214901105976868901 作者:前端君

(0)
上一篇 2023年3月27日 上午10:01
下一篇 2023年3月27日 上午10:11

相关推荐

发表回复

登录后才能评论