图形编辑器开发:缩放和旋转控制点

2023-11-28 17:01

本文主要是介绍图形编辑器开发:缩放和旋转控制点,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

大家好,我是前端西瓜哥。好久没写图形编辑器开发的文章了。

今天来讲讲控制点。它是图形编辑器的不可缺少的基础功能。

控制点是吸附在图形上的一些小矩形和圆形点击区域,在控制点上拖拽鼠标,能够实时对被选中进行属性的更新。

比如使用旋转控制点可以更新图形的旋转角度,使用缩放控制点调整图形的宽高。

这两个都是通用的控制点,此外还有给特定图形使用的专有控制点,像是矩形的圆角控制点,可拖动调整圆角大小。这些比较特别。后面会专门出一篇文章讲这个。

需求描述

选中图形,会出现旋转控制点和缩放控制点,然后操作控制点。

关于控制点的位置,示意图如下。

缩放控制点有 8 个。

首先是 西北(nw)、东北(ne)、东南(se)、西南(sw)缩放控制点。它们在选中图形的四个角鹿,可以同时更新图形的宽高。

接着是 东(e)、南(s)、西(w)、北(n)缩放控制点,拖拽它们只更新图形的宽或高。

它们是不可见的,但在 hover 上去光标会变成缩放的光标。这类控制点的点击区域见下图。

旋转控制点有 4 个,对应四个角落,分别为:nwRotation、neRotation、seRotation、swRotation

同样它们是透明的,但 hover 上去光标会变成旋转光标。

旋转控制点有另外一种风格,就是只在图形的某个方向(通常是正上方)有一个可见旋转控制点。下面是 Canva 编辑器的效果:

我更喜欢第一种风格,画面会更清爽一些。

实现思路

整体实现思路很简单:

  1. 根据图形的包围盒,计算这些控制点的位置,设置好宽高;
  2. 渲染,设置为不可见的控制点跳过渲染;
  3. hover 或点击时,编辑器会做 图形拾取,会和渲染顺序相反的顺序遍历控制点,调用控制点图形的 hitTest 方法找到第一个被点中的图形,返回对应控制点的类型和光标。然后编辑器更新光标,并根据控制点类型进入对应逻辑。如果你是用 html/svg 的方案,图形拾取可以不用自己做。

代码设计

我们抽象一个控制点管理类 ControlHandleManager 和控制点类 ControlHandle。

ControlHandle 类记录以下信息:

  1. graph:图形对象,记录控制点的左上角位置、宽高、颜色、是否可见,并带了一个点击区域方法;
  2. cx / cy:控制点的中点位置;
  3. 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 渲染方法,渲染控制点。

  1. 根据包围盒计算控制点的中点位置。这个包围盒有 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 },};})();}
}
  1. 遍历控制点对象,赋值上对应的中点坐标: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;
  1. 接着就是遍历 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 而变化的。

如果你绘制在视口坐标系,宽高不需要考虑,只要转换一下 x,y。如果在场景坐标中,x、y 不用转换,但是宽高要除以 zoom。

缩放和旋转图形

如何缩放和旋转图形就超出本文的话题范围了,但如果你感兴趣的话,可以看我的这几篇文章:

《图形编辑器开发:实现缩放图形》

《图形编辑器:旋转选中的元素》

结尾

我是前端西瓜哥,欢迎关注我,学习更多图形编辑器知识。

这篇关于图形编辑器开发:缩放和旋转控制点的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/430010

相关文章

这15个Vue指令,让你的项目开发爽到爆

1. V-Hotkey 仓库地址: github.com/Dafrok/v-ho… Demo: 戳这里 https://dafrok.github.io/v-hotkey 安装: npm install --save v-hotkey 这个指令可以给组件绑定一个或多个快捷键。你想要通过按下 Escape 键后隐藏某个组件,按住 Control 和回车键再显示它吗?小菜一碟: <template

Hadoop企业开发案例调优场景

需求 (1)需求:从1G数据中,统计每个单词出现次数。服务器3台,每台配置4G内存,4核CPU,4线程。 (2)需求分析: 1G / 128m = 8个MapTask;1个ReduceTask;1个mrAppMaster 平均每个节点运行10个 / 3台 ≈ 3个任务(4    3    3) HDFS参数调优 (1)修改:hadoop-env.sh export HDFS_NAMENOD

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

嵌入式QT开发:构建高效智能的嵌入式系统

摘要: 本文深入探讨了嵌入式 QT 相关的各个方面。从 QT 框架的基础架构和核心概念出发,详细阐述了其在嵌入式环境中的优势与特点。文中分析了嵌入式 QT 的开发环境搭建过程,包括交叉编译工具链的配置等关键步骤。进一步探讨了嵌入式 QT 的界面设计与开发,涵盖了从基本控件的使用到复杂界面布局的构建。同时也深入研究了信号与槽机制在嵌入式系统中的应用,以及嵌入式 QT 与硬件设备的交互,包括输入输出设

OpenHarmony鸿蒙开发( Beta5.0)无感配网详解

1、简介 无感配网是指在设备联网过程中无需输入热点相关账号信息,即可快速实现设备配网,是一种兼顾高效性、可靠性和安全性的配网方式。 2、配网原理 2.1 通信原理 手机和智能设备之间的信息传递,利用特有的NAN协议实现。利用手机和智能设备之间的WiFi 感知订阅、发布能力,实现了数字管家应用和设备之间的发现。在完成设备间的认证和响应后,即可发送相关配网数据。同时还支持与常规Sof

活用c4d官方开发文档查询代码

当你问AI助手比如豆包,如何用python禁止掉xpresso标签时候,它会提示到 这时候要用到两个东西。https://developers.maxon.net/论坛搜索和开发文档 比如这里我就在官方找到正确的id描述 然后我就把参数标签换过来

Linux_kernel驱动开发11

一、改回nfs方式挂载根文件系统         在产品将要上线之前,需要制作不同类型格式的根文件系统         在产品研发阶段,我们还是需要使用nfs的方式挂载根文件系统         优点:可以直接在上位机中修改文件系统内容,延长EMMC的寿命         【1】重启上位机nfs服务         sudo service nfs-kernel-server resta

poj 2187 凸包or旋转qia壳法

题意: 给n(50000)个点,求这些点与点之间距离最大的距离。 解析: 先求凸包然后暴力。 或者旋转卡壳大法。 代码: #include <iostream>#include <cstdio>#include <cstdlib>#include <algorithm>#include <cstring>#include <cmath>#include <s

【区块链 + 人才服务】区块链集成开发平台 | FISCO BCOS应用案例

随着区块链技术的快速发展,越来越多的企业开始将其应用于实际业务中。然而,区块链技术的专业性使得其集成开发成为一项挑战。针对此,广东中创智慧科技有限公司基于国产开源联盟链 FISCO BCOS 推出了区块链集成开发平台。该平台基于区块链技术,提供一套全面的区块链开发工具和开发环境,支持开发者快速开发和部署区块链应用。此外,该平台还可以提供一套全面的区块链开发教程和文档,帮助开发者快速上手区块链开发。

Vue3项目开发——新闻发布管理系统(六)

文章目录 八、首页设计开发1、页面设计2、登录访问拦截实现3、用户基本信息显示①封装用户基本信息获取接口②用户基本信息存储③用户基本信息调用④用户基本信息动态渲染 4、退出功能实现①注册点击事件②添加退出功能③数据清理 5、代码下载 八、首页设计开发 登录成功后,系统就进入了首页。接下来,也就进行首页的开发了。 1、页面设计 系统页面主要分为三部分,左侧为系统的菜单栏,右侧