大家好,我是前端西瓜哥。
今天我们来学习图形编辑器的网格模块要怎么设计和实现。
我正在开发的 suika 图形编辑器: https://github.com/F-star/suika 线上体验: https://blog.fstars.wang/app/suika/
网格,指的是渲染在画布上的,按照特定间距绘制垂直和水平直线,所构成的网格。
作用是让用户可以较 直观 地观察到图形的距离和大小关系,以及实现网格吸附。
考虑到性能,我们 只绘制视口范围内的网格线。其他超出的部分不同绘制出来。因为是重复图案(可以视作两条线组成的 L 形的平铺),可以考虑用纹理平铺渲染以提高性能。
网格通常渲染在图形的下方,并在画布缩放前后,维持线宽为 1 像素不变。
关于渲染实现,我之前写过 画布标尺的绘制的文章,思路其实是一样的。
let startXInScene = getClosestTimesVal(viewport.x, gridSpacingX);
// 从左往右,每移动网格间隔距离绘制一条从上到下的线
while (startXInScene <= endXInScene) {
const x = nearestPixelVal((startXInScene - viewport.x) * zoom);
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, viewport.height);
ctx.stroke();
ctx.closePath();
startXInScene += gridSpacingX;
}
也有网格线在图形上方的,比如 Figma。这样有填充内容的图形不会覆盖和它重叠的网格,就能大概知道它占据了多少格子。
但这种情况下注意给网格线 设置滤镜效果或透明度,使在与其颜色相近的图形上方也能有一个较好的渲染效果,能够被分辨出来。
网格间距通常会是可配置的。
特殊的,当网格间距设置为 1 时,就变成 像素网格 了,Figma 的网格就是像素网格,不可设置网格间距。
网格线的颜色通常是灰色,不能存在感太强。
gridSpacingX 和 gridSpacingY 通常为整数,但也可以用小数。
gridSpacingX 和 gridSpacingY 的值理论上应该相等(加上限制)。但也可以不相等,比较少见,但此时格子从正方形变成了长方形。
有时候我们觉得连续的网格,不好肉眼测量。此时我们可以引入大网格。有点类似刻度尺,没隔几个小的刻度,会绘制一个长一点的大刻度。
即每 n x n
个小格子组成一个大格子。
绘制上就是在原来网格线的基础上,再画一个放大了 n 倍的网格线。注意这个大网格颜色相比小网格颜色要不同,以看出区别。
这里我们也可以考虑做成配置化:
网格线颜色一般默认会比较浅,以免喧宾夺主。
除了网格线,还有另一种网格的表示方式:用圆点表示。
点的位置对应原来网格线与线之间的交点位置。
该效果常见于白板工具。
因为密度的降低,此时可以考虑让点跟随画布缩放而缩放(还有一个前提是画布不能放得很大)。
当缩小画布时,网格会跟随缩小。当缩放得非常小时,网格线就会显得非常密集。
为了解决网格密度过大的问题,通常我们有两种做法。
(1)视口上的网格间距小到一定程度,就不再显示。Figma 是这么做的。
// 最小间距,小于这个要把间距放大
const MIX_SPACING_IN_VIEWPORT = 8;
// 视口上的网格尺寸
const gridSpacingInViewport = zoom * Math.min(gridSpacingX, gridSpacingY);
let gridVisible = true;
// 密度过大,不绘制网格
if (gridSpacingInViewport < MIX_SPACING_IN_VIEWPORT) {
gridVisible = false;
}
(2)调整网格间距为原来的整数倍,让密度动态变化,不突破阈值。
// 最小间距,小于这个要把间距放大
const MIX_SPACING_IN_VIEWPORT = 8;
// 视口上的网格尺寸
let gridSpacingInViewport = zoom * Math.min(gridSpacingX, gridSpacingY);
while (viewportGridSpaceX <= MIX_SPACING_IN_VIEWPORT) {
gridSpacingInViewport *= smallSpacingCount;
gridSpacingX *= smallSpacingCount;
gridSpacingY *= smallSpacingCount;
}
上面两种方式也可以做成可配置化。
网格通常配套吸附效果。这样用户可以明确知道自己在用网格吸附,以及新的点大概会吸附到哪里。
找到某个值距离最近的 spacing 整数倍值的方法:
const getClosestTimesVal = (value, spacing) => {
const n = Math.floor(value / spacing);
const left = spacing * n;
const right = spacing * (n + 1);
return value - left <= right - value ? left : right;
};
则对于一个点,网格吸附后的位置为:
const gripSnapPt = {
x: getClosestTimesVal(point.x, spacingSnapX),
y: getClosestTimesVal(point.y, spacingSnapY),
}
网格吸附相关配置项:
通常吸附间距应该和网格渲染间距相同,这样吸附到网格上的界面就比较符合直觉。
但实际上是可以不一样的。尤其是网格密度过大时如果使用了动态改变网格间距的方案。
网格比较重要的大概就是这些。
我是前端西瓜哥,欢迎关注我,学习更多图形编辑器知识。