广告位联系
返回顶部
分享到

JS图形编辑器实现标尺功能

JavaScript 来源:互联网 作者:佚名 发布时间:2023-01-19 21:34:23 人浏览
摘要

项目地址: https://github.com/F-star/suika 线上体验: https://blog.fstars.wang/app/suika/ 标尺指的是画布上边和左边的两个有刻度的尺子,作用让用户知道他正在编辑的视口所在位置范围。 我们的

项目地址:

https://github.com/F-star/suika

线上体验:

https://blog.fstars.wang/app/suika/

标尺指的是画布上边和左边的两个有刻度的尺子,作用让用户知道他正在编辑的视口所在位置范围。

我们的需求是:间隔特定的长度,绘制一个刻度,并显示这个刻度在 X 轴或 Y 轴上的位置。

先看最终实现效果:

标尺功能演示

可以看到,视口移动后,标尺上的刻度能正确地改变。此外缩放画布,标尺的步长会发生改变,保持一个比较适合的密度。

实现思路

总体实现思路:

  • 确定刻度尺的步长(step)。步长是和画布缩放比(zoom)相关的,zoom 越大,step 就越小;
  • 计算出需要绘制的所有刻度。分别为从视口从左侧到右侧,从上边到下边的范围;
  • 绘制。绘制上也是有考量的,先绘制背景,然后绘制刻度,最后绘制分界线。

步长选择

步长会根据 zoom 进行设置,目的是让视口中的标尺能绘制适宜密度的刻度。

假设我们的步长固定为 50,不跟随 zoom 改变,在 100% 看起来效果不错:

但当你缩小时,会变成下面这样:

密度过大,导致数字重叠。同样,放大时则过于稀疏,刻度很难才见到一个,没能发挥标尺的效用。

步长怎么计算呢?

理论上步长可以是 50,那么 51 好像也行,3 也行。但更建议使用 5 的倍数、2 的倍数、25 的倍数这些作为步长。

因为没有什么理论参考,所以我还是选择参考市面上的设计工具的步长变化设计。

比如 figma,zoom 落在 [100%, 200%) 的步长为 50,[200%, 500%) 则是 10 等等。

我的实现为:

1

2

3

4

5

6

7

8

9

10

11

12

const getStepByZoom = (zoom: number) => {

  // 可用的步长列表

  const steps = [1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000];

  // 看着 figma 的 step 变化想出的一个奇怪的规律

  // 然后找出可选步长列表最近的并大于它的 step 作为最终步长

  const step = 50 / zoom;

  for (let i = 0, len = steps.length; i < len; i++) {

    if (steps[i] >= step) return steps[i];

  }

  return steps[0];

};

const step = getStepByZoom(zoom);

计算范围

这里我讲解水平(x 轴)方向的情况。垂直方向同理,就不赘叙了。

首先计算出视口最左侧和最右侧的 x 坐标值。

需要视口坐标转场景坐标的知识,如果你不懂,看我这篇文章:

《图形编辑器:场景坐标、视口坐标以及它们之间的转换》

1

2

let startXInScene = viewport.x + startXInViewport / zoom; // 视口坐标转场景

let endXInScene = viewport.width + startYInViewport / zoom; // 视口坐标转场景

然后找离它们最近的落在刻度上的值。

对此,我实现了一个 getClosestVal 方法。

1

2

3

4

5

6

7

8

9

10

11

/**

 * 找出离 value 最近的 segment 的倍数值

 */

const getClosestVal = (value: number, segment: number) => {

  const n = Math.floor(value / segment);

  const left = segment * n;

  const right = segment * (n + 1);

  return value - left <= right - value ? left : right;

};

startXInScene = getClosestVal(startXInScene, step);

endXInScene = getClosestVal(endXInScene, step);

得到起点和终点,我们可以开始循环了,从 startXInScene 开始,每次循环加一个 step,直至达到末尾为止。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

ctx.textAlign = 'center'; // 文字水平居中对齐

while (startXInScene <= endXInScene) {

  ctx.strokeStyle = setting.rulerMarkStroke;

  ctx.fillStyle = setting.rulerMarkStroke;

  // 场景转回视口再绘制。刻度线不能直接在场景中绘制,因为缩放变换会导致线的粗细变化

  const x = (startXInScene - viewport.x) * zoom;

  // 绘制刻度

  ctx.beginPath();

  ctx.moveTo(x, y);

  ctx.lineTo(x, y + setting.rulerMarkSize);

  ctx.stroke();

  ctx.closePath();

  // 刻度值则用场景坐标的值

  ctx.fillText(String(startXInScene), x, y - 4);

  // +step,指针移动

  startXInScene += step;

}

垂直方向的标尺同理,只是稍微特殊的是刻度值文字需要多做一个 -90 度的旋转。

1

2

3

4

5

6

7

8

9

10

11

export const rotateInCanvas = (

  ctx: CanvasRenderingContext2D,

  angle: number,

  cx: number,

  cy: number

) => {

  ctx.translate(cx, cy);

  ctx.rotate(angle);

  ctx.translate(-cx, -cy);

};

rotateInCanvas(ctx, -HALF_PI, x, y);

绘制顺序

绘制顺序需要注意一下,先后顺序为:

  • 绘制两个标尺的背景色;
  • 绘制刻度值;
  • 用一个和背景色同色的矩形盖掉左上角那个方形,那个地方不能有刻度值,不如两个标尺的刻度会重叠。你也可以在绘制刻度值时,用裁切(ctx.clip)不让绘制到那个方形区域上;
  • 绘制两条分割线;

版权声明 : 本文内容来源于互联网或用户自行发布贡献,该文观点仅代表原作者本人。本站仅提供信息存储空间服务和不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权, 违法违规的内容, 请发送邮件至2530232025#qq.cn(#换@)举报,一经查实,本站将立刻删除。
原文链接 : https://juejin.cn/post/7188689278824611899
相关文章
  • 本站所有内容来源于互联网或用户自行发布,本站仅提供信息存储空间服务,不拥有版权,不承担法律责任。如有侵犯您的权益,请您联系站长处理!
  • Copyright © 2017-2022 F11.CN All Rights Reserved. F11站长开发者网 版权所有 | 苏ICP备2022031554号-1 | 51LA统计