网站建设用途,音乐推广平台有哪些,温州网站建设服务,如何修改网站的关键词大家好#xff0c;我是前端西瓜哥。好久没写图形编辑器开发的文章了。
今天来讲讲控制点。它是图形编辑器的不可缺少的基础功能。
控制点是吸附在图形上的一些小矩形和圆形点击区域#xff0c;在控制点上拖拽鼠标#xff0c;能够实时对被选中进行属性的更新。
比如使用旋…大家好我是前端西瓜哥。好久没写图形编辑器开发的文章了。
今天来讲讲控制点。它是图形编辑器的不可缺少的基础功能。
控制点是吸附在图形上的一些小矩形和圆形点击区域在控制点上拖拽鼠标能够实时对被选中进行属性的更新。
比如使用旋转控制点可以更新图形的旋转角度使用缩放控制点调整图形的宽高。
这两个都是通用的控制点此外还有给特定图形使用的专有控制点像是矩形的圆角控制点可拖动调整圆角大小。这些比较特别。后面会专门出一篇文章讲这个。
需求描述
选中图形会出现旋转控制点和缩放控制点然后操作控制点。 关于控制点的位置示意图如下。 缩放控制点有 8 个。
首先是 西北nw、东北ne、东南se、西南sw缩放控制点。它们在选中图形的四个角鹿可以同时更新图形的宽高。
接着是 东e、南s、西w、北n缩放控制点拖拽它们只更新图形的宽或高。
它们是不可见的但在 hover 上去光标会变成缩放的光标。这类控制点的点击区域见下图。 旋转控制点有 4 个对应四个角落分别为nwRotation、neRotation、seRotation、swRotation。
同样它们是透明的但 hover 上去光标会变成旋转光标。 旋转控制点有另外一种风格就是只在图形的某个方向通常是正上方有一个可见旋转控制点。下面是 Canva 编辑器的效果 我更喜欢第一种风格画面会更清爽一些。
实现思路
整体实现思路很简单
根据图形的包围盒计算这些控制点的位置设置好宽高渲染设置为不可见的控制点跳过渲染hover 或点击时编辑器会做 图形拾取会和渲染顺序相反的顺序遍历控制点调用控制点图形的 hitTest 方法找到第一个被点中的图形返回对应控制点的类型和光标。然后编辑器更新光标并根据控制点类型进入对应逻辑。如果你是用 html/svg 的方案图形拾取可以不用自己做。
代码设计
我们抽象一个控制点管理类 ControlHandleManager 和控制点类 ControlHandle。
ControlHandle 类记录以下信息
graph图形对象记录控制点的左上角位置、宽高、颜色、是否可见并带了一个点击区域方法cx / cy控制点的中点位置getCursor()获取光标方法hover 时返回一个需要设置的光标值。 这里直接用图形编辑器绘制图形用到的图形类。 通常你使用的渲染图形库是会有 创建 ControlHandle 对象。
我们需要创建的控制点对象为
// 右下角ns的控制点
const se new ControlHandle({graph: new Rect({objectName: se, // 控制点类型标识放其他地方也行cx: 0, // x 和 y 会根据选中图形的包围盒更新cy: 0,width: 6,height: 6,fill: white,stroke: blue,strokeWidth: 1,}),getCursor: (type, rotation) {// ...return se-rezise} ,
});这个对象会保存到控制点管理类的 transformHandles 属性中。
transformHandles 是一个映射表类型标识字符串映射到控制点对象。
class ControlHandleManager {visible false;transformHandles;constructor() {// 映射表 type - 控制点this.transformHandles {se: new ControlHandle(/* ... */),n: new ControlHandle(/* ... */),nwRoation: new ControlHandle(/* ... */),// ...}}
}渲染
当我们选中图形时调用渲染方法。
此时会调用 ControlHandleManager 的 draw 渲染方法渲染控制点。
根据包围盒计算控制点的中点位置。这个包围盒有 x、y、width、height、rotation 属性。我们需要计算这个包围盒的四个顶点的位置包围盒外扩一定距离后的四个顶点的位置四条线段的中点的位置。
class ControlHandleManager {// .../** 渲染控制点 */draw(rect: IRectWithRotation) {// calculate handle positionconst handlePoints (() {const cornerPoints rectToPoints(rect);const cornerRotation rectToPoints(offsetRect(rect, size / 2 / zoom));const midPoints rectToMidPoints(rect);return {...cornerPoints,...midPoints,nwRotation: { ...cornerRotation.nw },neRotation: { ...cornerRotation.ne },seRotation: { ...cornerRotation.se },swRotation: { ...cornerRotation.sw },};})();}
}遍历控制点对象赋值上对应的中点坐标cx、cy。调整 n/s/w/e 的宽高它们的宽高是跟随
// 整个顺序是有意义的是渲染顺序
const types [n,e,s,w,nwRotation,neRotation,seRotation,swRotation,nw,ne,se,sw,
] as const;// 更新 cx 和 cy
for (const type of types) {const point handlePoints[type];const handle this.transformHandles.get(type);handle.cx point.x;handle.cy point.y;
}// n/s/w/e 比较特殊n/s 的宽和包围盒宽度相等w/e 高等于包围盒高。
const neswHandleWidth 9;
const n this.transformHandles.get(n)!;
const s this.transformHandles.get(s)!;
const w this.transformHandles.get(w)!;
const e this.transformHandles.get(e)!;
n.graph.width s.graph.width rect.width * zoom;
n.graph.height s.graph.height neswHandleWidth;
w.graph.height e.graph.height rect.height * zoom;
w.graph.width e.graph.width neswHandleWidth;接着就是遍历 transformHandles基于 cx 和 cy 更新图形的 x/y然后绘制。
this.transformHandles.forEach((handle) {// 场景坐标转视口坐标const { x, y } this.editor.sceneCoordsToViewport(handle.cx, handle.cy);const graph handle.graph;graph.x x - graph.width / 2;graph.y y - graph.height / 2;graph.rotation rect.rotation;// 不可见的图形不渲染本地调试的时候可以让它可见if (!graph.getVisible()) {return;}graph.draw();
});渲染逻辑到此结束。
控制点拾取
然后就是在选择工具下hover 到控制点上对光标进行设置。并且在按下鼠标时能够拿到对应的控制点类型进行对应的旋转或缩放操作。
控制点拾取逻辑为
以渲染顺序相反的方向遍历控制点调用 hitTest 方法检测光标是否在控制点的点击区域上。
如果在返回 type 和 cursor否则返回 null。
class ControlHandleManager {// .../** 获取在光标位置的控制点的信息 */getHandleInfoByPoint(hitPoint: IPoint) {const hitPointVW this.editor.sceneCoordsToViewport(hitPoint.x,hitPoint.y,);for (let i types.length - 1; i 0; i--) {const type types[i];const handle this.transformHandles.get(type);// 是否点中当前控制点const isHit handle.graph.hitTest(hitPointVW.x,hitPointVW.y,handleHitToleration,);if (isHit) {return {handleName: type, // 控制点类型cursor: handle.getCursor(type, rotation), // 光标};}}}
}反向很重要应为可能会有控制点发生重叠此时应该是在更上方的控制点也就是后渲染的控制点优先被选中。
光标
getCursor 返回的光标值是动态的会因为包围盒的角度不同而变化这里会有一个简单的转换。
const getResizeCursor (type: string, rotation: number): ICursor {let dDegree 0;switch (type) {case se:case nw:dDegree -45;break;case ne:case sw:dDegree 45;break;case n:case s:dDegree 0;break;case e:case w:dDegree 90;break;default:console.warn(unknown type, type);}const degree rad2Deg(rotation) dDegree;// 这个 degree 精度是很高的// 设置光标时会做一个舍入匹配一个合法的接近光标值比如 ne-resizereturn { type: resize, degree };
}旋转光标同理。
此外浏览器支持的 resize 光标值是有限的。 为了更好的效果是实现 resize0 resize179 代表不同角度的一共 180 个自定义 resize 光标。
或者做一个 “四舍五入”转为浏览器支持的那几种 resize 角度但这样光标效果不是很好看起来光标并没有和控制点垂直算是一种妥协。 旋转光标更是不存在了我们要设计 rotation0 ~ rotation179 共 360 个自定义光标。当然我们可以让精度降一下比如只实现偶数值的旋转角度的光标比如 rotation0、rotation2、rotation4也要 180 个。
关于自定义光标的实现方案本文不深入讲解会单独写一篇文章讨论。
坐标系
有个容易忽略的问题就是控制点是绘制在哪个坐标系中的
是场景坐标系还是视口坐标系。
如果在场景坐标系中图形会随画布的缩放或移动 “放大缩小”比如一根 2px 的线条在 zoom 为 50% 的画布下显示的效果是 1px。
控制点的宽高是不应该跟随 zoom 而变化的。
如果你绘制在视口坐标系宽高不需要考虑只要转换一下 xy。如果在场景坐标中x、y 不用转换但是宽高要除以 zoom。
缩放和旋转图形
如何缩放和旋转图形就超出本文的话题范围了但如果你感兴趣的话可以看我的这几篇文章
《图形编辑器开发实现缩放图形》
《图形编辑器旋转选中的元素》
结尾
我是前端西瓜哥欢迎关注我学习更多图形编辑器知识。