当前位置:首页 > 科技  > 软件

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

来源: 责编: 时间:2024-01-03 09:11:54 126观看
导读大家好,我是前端西瓜哥。挺久没写图形编辑器开发系列了,今天来讲讲控制点,它是图形编辑器的不可缺少的基础功能。控制点是吸附在图形上的一些小矩形和圆形点击区域,在控制点上拖拽鼠标,能够实时对被选中进行属性的更新。比

3xx28资讯网——每日最新资讯28at.com

大家好,我是前端西瓜哥。3xx28资讯网——每日最新资讯28at.com

挺久没写图形编辑器开发系列了,今天来讲讲控制点,它是图形编辑器的不可缺少的基础功能。3xx28资讯网——每日最新资讯28at.com

控制点是吸附在图形上的一些小矩形和圆形点击区域,在控制点上拖拽鼠标,能够实时对被选中进行属性的更新3xx28资讯网——每日最新资讯28at.com

比如使用旋转控制点可以更新图形的旋转角度,使用缩放控制点调整图形的宽高。3xx28资讯网——每日最新资讯28at.com

这两个都是通用的控制点,此外还有给特定图形使用的专有控制点,像是矩形的圆角控制点,可拖动调整圆角大小。这些比较特别。后面会专门出一篇文章讲这个。3xx28资讯网——每日最新资讯28at.com

需求描述

选中图形,会出现旋转控制点和缩放控制点,然后操作控制点,调整图形属性。3xx28资讯网——每日最新资讯28at.com

3xx28资讯网——每日最新资讯28at.com

控制点的类型和位置如下:3xx28资讯网——每日最新资讯28at.com

3xx28资讯网——每日最新资讯28at.com

缩放控制点有 8 个。3xx28资讯网——每日最新资讯28at.com

首先是 西北(nw)、东北(ne)、东南(se)、西南(sw)缩放控制点。它们在选中图形包围盒的四个顶点上,拖拽可同时调整图形的宽高。3xx28资讯网——每日最新资讯28at.com

接着是 东(e)、南(s)、西(w)、北(n)缩放控制点,拖拽它们只更新图形的宽或高。3xx28资讯网——每日最新资讯28at.com

它们是不可见的,但 hover 上去光标会变成缩放的光标。这几个控制点的点击区域很大。3xx28资讯网——每日最新资讯28at.com

3xx28资讯网——每日最新资讯28at.com

旋转控制点有 4 个,对应四个角落,分别为:nwRotation、neRotation、seRotation、swRotation。3xx28资讯网——每日最新资讯28at.com

同样它们是透明的,但 hover 上去光标会变成旋转光标。3xx28资讯网——每日最新资讯28at.com

3xx28资讯网——每日最新资讯28at.com

旋转控制点有另外一种风格,就是只在图形的某个方向(通常是正上方)有一个可见旋转控制点。下面是 Canva 编辑器的效果:3xx28资讯网——每日最新资讯28at.com

3xx28资讯网——每日最新资讯28at.com

我更喜欢第一种风格,画面会更清爽一些。3xx28资讯网——每日最新资讯28at.com

实现思路

整体实现思路很简单:3xx28资讯网——每日最新资讯28at.com

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

代码设计

我们需要实现控制点管理类 ControlHandleManager 和控制点类 ControlHandle。3xx28资讯网——每日最新资讯28at.com

ControlHandle 类记录以下信息:3xx28资讯网——每日最新资讯28at.com

  • graph:图形对象,记录控制点的左上角位置、宽高、颜色、是否可见,并带了一个点击区域方法。
  • cx / cy:控制点的中点位置。
  • getCursor():获取光标方法,hover 时返回一个需要设置的光标值。

这里直接用图形编辑器绘制图形用到的图形类。3xx28资讯网——每日最新资讯28at.com

通常你使用的渲染图形库是会有3xx28资讯网——每日最新资讯28at.com

创建 ControlHandle 对象。3xx28资讯网——每日最新资讯28at.com

我们需要创建的控制点对象为:3xx28资讯网——每日最新资讯28at.com

// 右下角(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 属性中。3xx28资讯网——每日最新资讯28at.com

transformHandles 是一个映射表,类型标识字符串映射到控制点对象。3xx28资讯网——每日最新资讯28at.com

class ControlHandleManager {  visible = false;  transformHandles;  constructor() {    // 映射表 type -> 控制点    this.transformHandles = {      se: new ControlHandle(/* ... */),      n: new ControlHandle(/* ... */),      nwRoation: new ControlHandle(/* ... */),      // ...    }  }}

渲染

当我们选中图形时,调用渲染方法。3xx28资讯网——每日最新资讯28at.com

此时会调用 ControlHandleManager 的 draw 渲染方法,渲染控制点。3xx28资讯网——每日最新资讯28at.com

根据包围盒计算控制点的中点位置。这个包围盒有 x、y、width、height、rotation 属性。我们需要计算这个包围盒的四个顶点的位置,包围盒外扩一定距离后的四个顶点的位置,四条线段的中点的位置。3xx28资讯网——每日最新资讯28at.com

class ControlHandleManager {  // ...    /** 渲染控制点 */  draw(rect: IRectWithRotation) {    // calculate handle position  const 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 的宽高,它们的宽高是跟随。3xx28资讯网——每日最新资讯28at.com

// 整个顺序是有意义的,是渲染顺序const types = [  'n',  'e',  's',  'w',  'nwRotation',  'neRotation',  'seRotation',  'swRotation',  'nw',  'ne',  'se',  'sw',] as const;// 更新 cx 和 cyfor (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,然后绘制。3xx28资讯网——每日最新资讯28at.com

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();});

渲染逻辑到此结束。3xx28资讯网——每日最新资讯28at.com

控制点拾取

在选择工具下,选中图形,控制点出现。3xx28资讯网——每日最新资讯28at.com

接着 hover 到控制点上,更新光标。并且在按下鼠标时,能够拿到对应的控制点类型,进行对应的旋转或缩放操作。3xx28资讯网——每日最新资讯28at.com

这里我们需要判断光标的位置是否在控制点上,即控制点拾取。3xx28资讯网——每日最新资讯28at.com

控制点拾取逻辑为:3xx28资讯网——每日最新资讯28at.com

以渲染顺序相反的方向遍历控制点,调用 hitTest 方法检测光标是否在控制点的点击区域上。3xx28资讯网——每日最新资讯28at.com

如果在,返回 type 和 cursor;否则返回 null。3xx28资讯网——每日最新资讯28at.com

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), // 光标        };      }    }  }  }

反向很重要,应为可能会有控制点发生重叠,此时应该是在更上方的控制点,也就是后渲染的控制点优先被选中。3xx28资讯网——每日最新资讯28at.com

光标

getCursor 返回的光标值是动态的,会因为包围盒的角度不同而变化,这里会有一个简单的转换。3xx28资讯网——每日最新资讯28at.com

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-resize  return { type: 'resize', degree };}

旋转光标同理。3xx28资讯网——每日最新资讯28at.com

此外,浏览器支持的 resize 光标值是有限的。3xx28资讯网——每日最新资讯28at.com

3xx28资讯网——每日最新资讯28at.com

为了更好的效果是实现 resize0 ~ resize179 代表不同角度的一共 180 个自定义 resize 光标。3xx28资讯网——每日最新资讯28at.com

或者做一个 “四舍五入”,转为浏览器支持的那几种 resize 角度,但这样光标效果不是很好,看起来光标并没有和控制点垂直,算是一种妥协。3xx28资讯网——每日最新资讯28at.com

3xx28资讯网——每日最新资讯28at.com

旋转光标更是不存在了,我们要设计 rotation0 ~ rotation179 共 360 个自定义光标。当然我们可以让精度降一下,比如只实现偶数值的旋转角度的光标,比如 rotation0、rotation2、rotation4,也要 180 个。3xx28资讯网——每日最新资讯28at.com

关于自定义光标的实现方案,本文不深入讲解,会单独写一篇文章讨论。3xx28资讯网——每日最新资讯28at.com

坐标系

有个容易忽略的问题,就是控制点是绘制在哪个坐标系中的?3xx28资讯网——每日最新资讯28at.com

是场景坐标系,还是视口坐标系。3xx28资讯网——每日最新资讯28at.com

如果在场景坐标系中,图形会随画布的缩放或移动 “放大缩小”,比如一根 2px 的线条,在 zoom 为 50% 的画布下,显示的效果是 1px。3xx28资讯网——每日最新资讯28at.com

控制点的宽高是不应该跟随  zoom 而变化的。3xx28资讯网——每日最新资讯28at.com

如果你绘制在视口坐标系,宽高不需要考虑,只要转换一下 x,y。如果在场景坐标中,x、y 不用转换,但是宽高要除以 zoom。3xx28资讯网——每日最新资讯28at.com

本文链接:http://www.28at.com/showinfo-26-56551-0.html图形编辑器开发:缩放和旋转控制点

声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com

上一篇: Uber Go 出了个静态分析工具 NilAway,还挺实用!

下一篇: 性能篇:字符串性能优化不容小觑

标签:
  • 热门焦点
  • K60至尊版狂暴引擎2.0加持:超177万跑分斩获性能第一

    K60至尊版狂暴引擎2.0加持:超177万跑分斩获性能第一

    Redmi的后性能时代战略发布会今天下午如期举办,在本次发布会上,Redmi公布了多项关于和联发科的深度合作,以及新机K60 Ultra在软件和硬件方面的特性,例如:“K60 至尊版,双芯旗舰
  • 十个可以手动编写的 JavaScript 数组 API

    十个可以手动编写的 JavaScript 数组 API

    JavaScript 中有很多API,使用得当,会很方便,省力不少。 你知道它的原理吗? 今天这篇文章,我们将对它们进行一次小总结。现在开始吧。1.forEach()forEach()用于遍历数组接收一参
  • 十个简单但很有用的Python装饰器

    十个简单但很有用的Python装饰器

    装饰器(Decorators)是Python中一种强大而灵活的功能,用于修改或增强函数或类的行为。装饰器本质上是一个函数,它接受另一个函数或类作为参数,并返回一个新的函数或类。它们通常用
  • JVM优化:实战OutOfMemoryError异常

    JVM优化:实战OutOfMemoryError异常

    一、Java堆溢出堆内存中主要存放对象、数组等,只要不断地创建这些对象,并且保证 GC Roots 到对象之间有可达路径来避免垃 圾收集回收机制清除这些对象,当这些对象所占空间超过
  • 小红书1周涨粉49W+,我总结了小白可以用的N条涨粉笔记

    小红书1周涨粉49W+,我总结了小白可以用的N条涨粉笔记

    作者:黄河懂运营一条性教育视频,被54万人“珍藏”是什么体验?最近,情感博主@公主是用鲜花做的,火了!仅仅凭借一条视频,光小红书就有超过128万人,为她疯狂点赞!更疯狂的是,这
  • 2023年,我眼中的字节跳动

    2023年,我眼中的字节跳动

    此时此刻(2023年7月),字节跳动从未上市,也从未公布过任何官方的上市计划;但是这并不妨碍它成为中国最受关注的互联网公司之一。从2016-17年的抖音强势崛起,到2018年的“头腾
  • 华为Mate 60系列用上可变灵动岛:正式版体验将会更出色

    华为Mate 60系列用上可变灵动岛:正式版体验将会更出色

    这段时间以来,关于华为新旗舰的爆料日渐密集。据此前多方爆料,今年华为将开始恢复一年双旗舰战略,除上半年推出的P60系列外,往年下半年的Mate系列也将
  • OPPO K11评测:旗舰级IMX890加持 2000元档最强影像手机

    OPPO K11评测:旗舰级IMX890加持 2000元档最强影像手机

    【Techweb评测】中端机型用户群体巨大,占了中国目前手机市场的大头,一直以来都是各手机品牌的“必争之地”,其中OPPO K系列机型一直以来都以高品质、
  • “买真退假” 这种“羊毛”不能薅

    “买真退假” 这种“羊毛”不能薅

    □ 法治日报 记者 王春   □ 本报通讯员 胡佳丽  2020年初,还在上大学的小东加入了一个大学生兼职QQ群。群主“七王”在群里介绍一些刷单赚
Top