今天领导给我一个任务,就是要求画布上面需要增加一个标尺,用来识别画布内元素的坐标,于是就开始我的搬砖之旅了。
开始想着挺简单的,不就是对画布进行刻度标记吗,写着写着发现有点不连贯了,于是就开始在网上找轮子,找了一圈发现没有合适的,那就只能靠自己了。
先来看效果(按住 alt + 鼠标左键 拖动画布):
1. 网上的资料
任何需求肯定是先要在网上找一圈的,发现很多设计的网站都有实现了这样的功能,例如墨刀
的原型设计就有这样的一个功能:
![d5a8e454b5585115b47d19745c2ebc40.jpeg](https://img-blog.csdnimg.cn/img_convert/d5a8e454b5585115b47d19745c2ebc40.jpeg)
用起来还是挺丝滑的,那网上会不会有已经实现的轮子呢,于是就开始找了,首先网上所有小demo
都是画了标线不能移动和缩放的,这不能满足我的需求,于是上Github
上去找是否有人已经实现了这样的功能;
在Github
上找到了一个库确实实现了我想要的功能:github.com/MrFrankel/r…1
但是我给代码弄下来了之后发现展现的效果和demo
的效果不一样,我又懒得去看源码了,于是就放弃了这个库,还是决定自己写吧。
实现下来确实遇到了很多问题,也都解决了,也可能是规避了,开发时间很短,所以可能会有很多问题,但是现在使用起来还是OK的,下面就记录我实现的过程。
2. fabric.js 来制作标尺
fabric.js
我这里就不多介绍了,不熟悉的可以自行去了解,这里就直接开始了。
我的思路是使用两层canvas
,一层是用来画标尺的,一层是用来画画布的,这样就可以实现标尺和画布的分离。
我将标尺的canvas
放在处理图像的canvas
的下面,但是会比处理图像的canvas
要大一圈,用来显示标尺的刻度,这样就可以实现标尺的刻度和画布的刻度对齐。
布局如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<style>
html, body {
margin: 0;
padding: 0;
background: #ddd;
}
#canvasWrap {
position: relative;
width: 800px;
height: 500px;
padding: 20px;
}
#staticCanvas {
position: absolute;
left: 0;
top: 0;
}
</style>
<body>
<section id="canvasWrap">
<canvas id="staticCanvas"></canvas>
<canvas id="canvas"></canvas>
</section>
</body>
<script>
// 这里是画布的宽高,下面要 +40 是因为要给标尺留出空间
let width = 800;
let height = 500;
const canvas = new fabric.Canvas("canvas", {
width: width,
height: height,
backgroundColor: "#fff",
});
// 创建静态画布
const staticCanvas = new fabric.StaticCanvas("staticCanvas", {
width: width + 40,
height: height + 40,
backgroundColor: "#ddd"
});
</script>
</html>
![c44c204369ffab1ae5ff727688f9105a.jpeg](https://img-blog.csdnimg.cn/img_convert/c44c204369ffab1ae5ff727688f9105a.jpeg)
2.1 制作静态标尺
搞定布局之后,我们就可以开始写内容了,首先我们需要一个静态的标尺,这个标尺是不会动的,我们需要在画布上面画出来,因为标尺是没有交互的,我们就可以直接使用StaticCanvas
来画,这样就不会有交互的事件了。
上面已经写了初始化的代码了,后面的代码都是在这个基础上进行的,首先创建水平标尺,代码如下:
// length 就是标尺的长度,也是画布的宽度
const length = 800;
// 创建水平标尺线,top: 19 是因为要给标尺下面的线会有 1px 的偏差
const rulerLine = new fabric.Line([0, 19, length, 19], {
top: 19,
left: 20,
stroke: "#000",
selectable: false,
strokeWidth: 1,
});
staticCanvas.add(rulerLine);
// 创建水平标尺线上的刻度
for (let i = 0; i < length; i += 10) {
// p 是刻度的位置,+20 是因为标尺对于画布的偏移
const p = i + 20;
const linePoints = [p, 20, p, 15]
// 判断是否是 100 的倍数,如果是就需要加长刻度,并且显示数字
const isTen = i % 100 === 0;
if (isTen) {
linePoints[3] = 10;
}
// 刻度线
const line = new fabric.Line(linePoints, {
stroke: '#000',
selectable: false,
strokeWidth: 1,
});
// 刻度数字
const text = new fabric.Text(isTen ? (i).toString() : "", {
left: linePoints[0],
top: 0,
fontSize: 10,
selectable: false,
});
staticCanvas.add(line, text);
}
staticCanvas.renderAll();
最后的效果如下:
![a2f7a82188ebb2cfea5cf36587f59c49.jpeg](https://img-blog.csdnimg.cn/img_convert/a2f7a82188ebb2cfea5cf36587f59c49.jpeg)
这里其实并没有太多的难点,就是很多的线条然后组合到一起;
Line
的第一个参数是一个数组,代表的是[x1, y1, x2, y2]
,就是线条的两个点的坐标,然后就是刻度的数字,这里我是用的Text
来实现的,Text
的left
属性和Line
的x1
属性是一样的,这样就可以实现数字和刻度的对齐了。
这里可以看到的是Text
会创建很多的空文本,这个会用于后续的操作;
对于垂直的标尺,其实和水平的标尺是一样的,只是方向不一样而已,这里就可以对代码做一下优化,将这个功能封装成一个函数,代码如下:
// 定义两个常量,用于标识标尺的方向
const HORIZONTAL = "horizontal";
const VERTICAL = "vertical";
/**
* 创建标尺
* @param length 标尺长度
* @param direction 标尺方向, 默认水平
*/
function createRuler(length, direction = HORIZONTAL) {
// 定义两个方向的标尺线的初始化坐标
const rulerLinePoints = {
horizontal: [0, 19, length, 19],
vertical: [19, 0, 19, length],
}
// 创建标尺线
const rulerLine = new fabric.Line(rulerLinePoints[direction], {
top: 19,
left: 19,
stroke: "#000",
selectable: false,
strokeWidth: 1,
});
staticCanvas.add(rulerLine);
// 创建水平标尺线上的刻度
for (let i = 0; i < length; i += 10) {
const p = i + 20;
// 通过判断方向来确认刻度线的坐标
const linePoints = direction === HORIZONTAL ? [p, 20, p, 15] : [20, p, 15, p]
// 创建刻度
createScale(linePoints, i, direction);
}
}
/**
* 创建刻度
* @param linePoints 刻度线的坐标
* @param scale 刻度值
* @param direction 刻度方向
*/
function createScale(linePoints, scale, direction) {
const isTen = scale % 100 === 0;
if (isTen) {
// 判断方向来修改刻度线的长度
if (direction === HORIZONTAL) {
linePoints[3] = 10;
} else {
linePoints[2] = 10;
}
}
const line = new fabric.Line(linePoints, {
stroke: '#000',
selectable: false,
strokeWidth: 1,
});
const text = new fabric.Text(isTen ? (scale).toString() : "", {
left: direction === HORIZONTAL ? linePoints[0] : 0,
top: direction === HORIZONTAL ? 0 : linePoints[1],
fontSize: 10,
selectable: false,
});
staticCanvas.add(line, text);
}
// 初始化标尺
createRuler(width, HORIZONTAL);
createRuler(height, VERTICAL);
staticCanvas.renderAll();
最后实现的效果如下:
![d1ce110c418e38286b7f11e63112f76e.jpeg](https://img-blog.csdnimg.cn/img_convert/d1ce110c418e38286b7f11e63112f76e.jpeg)
可以看到效果还是可以的,但是现在还是一个静态的,我们需要实现的是一个可以交互的标尺,坐标轴可以移动和缩放,这样才能实现我们的需求。
2.2 实现交互标尺
可交互的标尺交互的地方不是在静态的标尺上面,而是在画布上面,所以这些东西都是需要我们在画布上进行操作,然后将操作的结果反馈到静态的标尺上面。
2.2.1 标尺坐标确定
首先我们是需要确定我们的鼠标是在画布上面的哪个位置,这样才能确定我们的标尺的位置,那我们加一个标尺的指示线,这样就可以确定我们的鼠标在画布上面的位置了。
// 坐标标识线
const horizontalLine = new fabric.Line([0, 20, 40, 20], {
stroke: "red",
selectable: false,
strokeWidth: 1,
});
staticCanvas.add(horizontalLine);
const verticalLine = new fabric.Line([20, 0, 20, 40], {
stroke: "red",
selectable: false,
strokeWidth: 1,
});
staticCanvas.add(verticalLine);
这样在标尺上面就会多出两个红色的线,这两个线就是我们的标识线:
![49a3920bbdae3baf69e6d64edd5c247b.jpeg](https://img-blog.csdnimg.cn/img_convert/49a3920bbdae3baf69e6d64edd5c247b.jpeg)
接下来就是在画布上监听鼠标的移动事件,然后将标识线移动到鼠标的位置,代码如下:
canvas.on("mouse:move", function ({ e }) {
const { layerX, layerY } = e;
horizontalLine.set({
top: layerY + 20,
});
verticalLine.set({
left: layerX + 20,
});
staticCanvas.renderAll();
});
这里可以直接使用mouse:move
事件,这个事件会在鼠标移动的时候触发,然后使用layerX
和layerY
来获取鼠标在画布上面的位置,然后将标识线移动到鼠标的位置,这样就可以实现标识线跟随鼠标移动了。
注意还是要给标识线的top
和left
加上20
,这样才能和画布的刻度对齐。
![0edd6fde660c46eafe14eb638dc8b37c.jpeg](https://img-blog.csdnimg.cn/img_convert/0edd6fde660c46eafe14eb638dc8b37c.jpeg)
这里移动过快会频繁的触发mouse:move
事件,导致标识线一直都在更新,会有一些卡顿,这里对staticCanvas
的renderAll
方法做了一个节流,这样就不会频繁的触发renderAll
方法了。
const _staticCanvasRenderAll = staticCanvas.renderAll
staticCanvas.renderAll = throttle(_staticCanvasRenderAll, 16)
节流如何实现看大家自己的实现了,可以使用lodash
的throttle
方法,也可以自己实现一个。
2.2.2 标尺拖动(画布平移)
在操作画布的时候,画布肯定是会有拖动和缩放的,这个时候标尺也是需要跟着画布一起移动和缩放的,这样才能保证标尺和画布的刻度对齐。
我们先要解决的就是拖动的问题,首先还是要在画布上面添加画布拖动的事件的监听,代码如下:
// 鼠标按下,将 isDown 设置为 true
let isDown = false;
canvas.on("mouse:down", function () {
isDown = true;
});
// 鼠标抬起,将 isDown 设置为 false
canvas.on("mouse:up", function (opt) {
isDown = false;
});
canvas.on("mouse:move", function ({ e }) {
// 只有在按下 alt 键的时候,并且鼠标左键也按下了,才会触发画布的平移
if (e.altKey === true && isDown === true) {
// fabric.Point 是一个坐标点的对象,这里用来存储鼠标移动的距离
const delta = new fabric.Point(e.movementX, e.movementY);
// 然后调用画布的 relativePan 方法来进行平移
canvas.relativePan(delta);
}
// 这里将标识线更新封装成了一个函数
markerFollow(e);
});
// 添加一个正方形来查看效果
const rect = new fabric.Rect({
left: 100,
top: 100,
width: 100,
height: 100,
fill: "red",
});
canvas.add(rect);
canvas.renderAll();
![bd2d8322775fdbca280b672bb03f8b64.jpeg](https://img-blog.csdnimg.cn/img_convert/bd2d8322775fdbca280b672bb03f8b64.jpeg)
现在搞定了画布的拖动,使用fabric
来实现这些还是挺简单的,只需要调用relativePan
方法就可以实现画布的平移了。
2.2.3 标尺拖动(正文)
接下来就是要实现标尺的平移了,思路也很简单,就是在画布平移的时候,将标尺也平移同样的距离,这样就可以实现标尺的平移了。
如果拖动的点位超出了画布的范围,那就直接移动最左边或者最右边,形成一个新的点位就好了,因为标尺上面显示的坐标点就是固定只有这么多的。
有思路就直接开始,当然在实现这一个过程中我还是遇到了很多的坑,现在已经实现了,这些坑肯定就是在代码实现的过程中直接解决了;
首先我需要记录每个点位的数值,这要才能用于更新标尺点位的数值,我们直接在createScale
方法中添加一个自定义属性来记录这些数值,代码如下:
function createScale(linePoints, scale, direction) {
const isTen = scale % 100 === 0;
if (isTen) {
// 判断方向来修改刻度线的长度
if (direction === HORIZONTAL) {
linePoints[3] = 10;
} else {
linePoints[2] = 10;
}
}
const line = new fabric.Line(linePoints, {
stroke: '#000',
selectable: false,
strokeWidth: 1,
// 新增 _attr 属性,用于记录刻度的数值和方向
_attr: {
scale: scale,
direction: direction,
}
});
const text = new fabric.Text(isTen ? (scale).toString() : "", {
left: direction === HORIZONTAL ? linePoints[0] : 0,
top: direction === HORIZONTAL ? 0 : linePoints[1],
fontSize: 10,
selectable: false,
// 新增 _attr 属性,用于记录刻度的数值和方向
_attr: {
scale: scale,
direction: direction,
}
});
staticCanvas.add(line, text);
}
然后在画布平移的时候,我们就可以获取到画布平移的距离,然后将标尺也平移同样的距离,代码如下:
function moveHorizontally(e) {
const { horizontalLines, horizontalTexts } = dataPacket();
// 找到最大值和最小值
const horizontal = horizontalLines.map(item => item._attr.scale);
let horizontalMin = Math.min(...horizontal);
let horizontalMax = Math.max(...horizontal);
// 缓存文字的偏移量,文字和点位的坐标是相同的
const textOffsets = {}
// 定义左移和右移的方法
const leftMove = (line) => {
const moveX = line.left + e.movementX;
let scale = line._attr.scale;
let options = {
left: line.left + e.movementX,
};
// 如果超出了最小值,那么就需要复用最大值减一个点位,并更新最大值
if (moveX < 20) {
scale = horizontalMax = horizontalMax + 10;
options = {
left: width + moveX,
}
}
// 缓存文字的偏移量
textOffsets[line._attr.scale] = {
scale,
options,
}
// 更新点位
resetLine(line, scale, options);
}
const rightMove = (line) => {
const moveX = line.left + e.movementX;
let scale = line._attr.scale;
let options = {
left: line.left + e.movementX,
};
// 如果超出了最大值,那么就需要复用最小值加一个点位,并更新最小值
if (moveX >= width + 20) {
scale = horizontalMin = horizontalMin - 10;
options = {
left: moveX - width,
}
}
// 缓存文字的偏移量
textOffsets[line._attr.scale] = {
scale,
options,
}
// 更新点位
resetLine(line, scale, options);
}
// 遍历所有的点位,然后进行左移和右移
let headPointer = 0;
let tailPointer = horizontalLines.length - 1;
while (headPointer < tailPointer) {
// 左移
const headLine = horizontalLines[headPointer];
leftMove(headLine);
headPointer++;
// 右移
const tailLine = horizontalLines[tailPointer];
rightMove(tailLine);
tailPointer--;
}
// 如果是奇数个点位,那么中间的点位就随便左移或者右移都可以,这里我选择左移
if (headPointer === tailPointer) {
const headLine = horizontalLines[headPointer];
leftMove(headLine);
}
// 更新文字的位置
for (let i = 0; i < horizontalTexts.length; i++) {
const text = horizontalTexts[i];
// 通过缓存的偏移量来更新文字的位置
const { scale, options } = textOffsets[text._attr.scale];
resetText(text, scale, options);
}
}
因为代码量有点大,我将这些代码封装成一个函数了,上面的代码看一下就好了,下面慢慢解读;
首先会有一个dataPacket
函数,这个函数是用来获取标尺上面的所有的点位的,主要是水平点位和垂直点位,并将这些点位排序之后返回,代码如下:
function dataPacket() {
const horizontalLines = [];
const horizontalTexts = [];
const verticalLines = [];
const verticalTexts = [];
staticCanvas.getObjects().forEach(item => {
if (item._attr) {
if (item._attr.direction === HORIZONTAL) {
if (item.type === 'line') {
horizontalLines.push(item);
} else if (item.type === 'text') {
horizontalTexts.push(item);
}
} else if (item._attr.direction === VERTICAL) {
if (item.type === 'line') {
verticalLines.push(item);
} else if (item.type === 'text') {
verticalTexts.push(item);
}
}
}
});
return {
horizontalLines: horizontalLines.sort((a, b) => a._attr.scale - b._attr.scale),
horizontalTexts: horizontalTexts.sort((a, b) => a._attr.scale - b._attr.scale),
verticalLines: verticalLines.sort((a, b) => a._attr.scale - b._attr.scale),
verticalTexts: verticalTexts.sort((a, b) => a._attr.scale - b._attr.scale),
}
}
就是通过我们定义的_attr
属性来区分是水平的还是垂直的,然后将这些点位进行排序,这样就可以保证点位的顺序是正确的;
排序是因为在移动的时候,会将超出的点位进行复用,这样就会导致点位的顺序会乱,所以需要进行排序;
然后找到最大值和最小值,最大值超出之后证明是左移,复用的时候就要变成最小值减一个点位,最小值超出之后证明是右移,复用的时候就要变成最大值加一个点位,这样就可以实现点位的复用了。
const horizontal = horizontalLines.map(item => item._attr.scale);
let horizontalMin = Math.min(...horizontal);
let horizontalMax = Math.max(...horizontal);
然后就是左移和右移的方法了,它们两个函数的内部几乎都是一样的,只是方向不一样而已;
因为方向不一样所以它们内部的逻辑一个是更新最大值,一个是更新最小值,当然这里还是有优化空间的,只是我懒得优化了;
const leftMove = (line) => {
const moveX = line.left + e.movementX;
let scale = line._attr.scale;
let options = {
left: line.left + e.movementX,
};
// 核心只有这一个
if (moveX < 20) {
scale = horizontalMax = horizontalMax + 10;
options = {
left: width + moveX,
}
}
textOffsets[line._attr.scale] = {
scale,
options,
}
resetLine(line, scale, options);
}
然后就是遍历所有的点位,然后进行左移和右移,这里使用的是双指针的方式,同时进行左移和右移;
因为在移动的过程中点位会进行复用,如果不同时进行左移和右移,会导致一些点位不能正常更新,这一块感兴趣的可以去复现一下,我就节约文章篇幅了;
// 遍历所有的点位,然后进行左移和右移
let headPointer = 0;
let tailPointer = horizontalLines.length - 1;
while (headPointer < tailPointer) {
// 左移
const headLine = horizontalLines[headPointer];
leftMove(headLine);
headPointer++;
// 右移
const tailLine = horizontalLines[tailPointer];
rightMove(tailLine);
tailPointer--;
}
// 如果是奇数个点位,那么中间的点位就随便左移或者右移都可以,这里我选择左移
if (headPointer === tailPointer) {
const headLine = horizontalLines[headPointer];
leftMove(headLine);
}
最后就是更新文字的位置了,这里我是将文字的位置缓存起来,然后在遍历所有的文字的时候,通过缓存的偏移量来更新文字的位置,这样就可以保证文字和点位的位置是一致的了。
// 更新文字的位置
for (let i = 0; i < horizontalTexts.length; i++) {
const text = horizontalTexts[i];
// 通过缓存的偏移量来更新文字的位置
const { scale, options } = textOffsets[text._attr.scale];
resetText(text, scale, options);
}
这一块确实是可以放到和点位一起的,因为之前排查问题的时候抽离出来的,感兴趣可以自己去优化一下。
水平和垂直的标尺的移动是一样的,这里就不再赘述了,下面就是实现的效果:
![fad5b7de0ce4eb78ba719f0bb1a6db3b.jpeg](https://img-blog.csdnimg.cn/img_convert/fad5b7de0ce4eb78ba719f0bb1a6db3b.jpeg)
目前的效果都很棒,最后就剩下缩放的问题了,缩放是真的难搞!!!
2.2.4 标尺缩放(画布缩放)
和上面一样,标尺缩放肯定是需要跟着画布一起缩放的,这样才能保证标尺和画布的刻度对齐。
这里我是通过监听画布的mouse:wheel
事件来实现的,代码如下:
canvas.on("mouse:wheel", function (opt) {
let delta = opt.e.deltaY;
let zoom = canvas.getZoom();
zoom += -0.001 * delta;
zoom = zoom.toFixed(2) * 1;
if (zoom > 5) zoom = 5;
if (zoom < 0.5) zoom = 0.5;
canvas.zoomToPoint({
x: opt.e.offsetX,
y: opt.e.offsetY,
}, zoom);
opt.e.preventDefault();
opt.e.stopPropagation();
});
缩放也是很简单的,就是调用zoomToPoint
方法,然后传入缩放的中心点和缩放的比例就可以了,这里我是通过鼠标的位置来确定缩放的中心点的。
![8a2e1fd22b2bc86088b34fb53842743e.jpeg](https://img-blog.csdnimg.cn/img_convert/8a2e1fd22b2bc86088b34fb53842743e.jpeg)
2.2.5 标尺缩放(正文)
缩放的时候,标尺的刻度也是需要跟着画布一起缩放的,这样才能保证标尺和画布的刻度对齐。
这里的方案也很简单,在放大的时候,标尺刻度的间距就会变大,变大之后就会有些刻度会超出标尺的范围,这个时候就移除掉超出的刻度就好了。
缩小就是相反的操作,缩小之后,标尺刻度的间距就会变小,变小之后就会有些刻度会重叠,这个时候就需要添加一些刻度来填充空白的位置。
同时放大到一定的程度之后,标尺的刻度就需要更新,之前是10:1
,后面就是5:1
了,这个时候就需要更新刻度的数值了。
这里的代码量比较大,这里需要一步一步的实现;
首先需要定义一些变量来记录标尺的缩放比例,刻度的间距,刻度的数值,代码如下:
// 根据缩放比率更新标尺
let magnificationLevels = [5, 10, 20]; // 标尺的缩放比率,目前只有 5:1,10:1,20:1 三种
let magnificationLevel = 1; // 标尺的缩放比率的索引,对应的就是 magnificationLevels 的索引
let magnification = magnificationLevels[magnificationLevel]; // 标尺的缩放比率
let currScaleMagnification = magnification * 10; // 标尺刻度文字显示的缩放比率
let perScaleMagnification = magnificationLevels[magnificationLevel - 1] * 10; // 上一个标尺刻度文字显示的缩放比率
有了这些变量之后,我们就要把之前的代码都要梳理一下了,把之前写死的值都改成变量,这样才能保证缩放的时候,标尺的刻度也会跟着变化。
这里就不贴代码了,大家可以看一下上面的代码,可以搜索一下10
、100
这两个数字,然后将这些数字都改成变量就好了。
然后就是放大和缩小的方法了,根据上面的写的思路,放大的时候就是将标尺的刻度间距变大,缩小的时候就是将标尺的刻度间距变小,代码如下:
// 根据缩放比例,调整标尺
const objects = staticCanvas.getObjects();
const length = objects.length;
// 这里就是鼠标的位置,用于确定缩放的中心点
// verticalLine 和 horizontalLine 是标识线,也是鼠标的位置
const zoomOriginX = verticalLine.left;
const zoomOriginY = horizontalLine.top;
// 遍历所有的点位,然后进行缩放
for (let i = 0; i < length; i++) {
const item = objects[i];
// 如果没有 _attr 属性,就不是标尺的点位,直接跳过
if (!item._attr) continue;
// 获取标尺的方向
const { direction } = item._attr;
// 水平方向的标尺,调整的是 left 属性
if (direction === HORIZONTAL) {
// 这里 perLeft 指的上一次的 left 值
// 因为缩放的时候,left 值会变化,所以需要记录上一次的 left 值
// 如果不记录会导致无限变大或者无限变小,无法还原
const perLeft = (item.left - zoomOriginX) / perZoom + zoomOriginX;
// 需要使用上一次的 left 值来计算新的 left 值
const left = (perLeft - zoomOriginX) * zoom + zoomOriginX;
// 超出画布的范围,就移除掉,一个是超出左边,一个是超出右边
if (left < 20) {
staticCanvas.remove(item);
} else if (left > staticCanvas.width - 20) {
staticCanvas.remove(item);
} else {
// 没有超出画布的范围,就更新 left 值
item.set({
left: left,
});
}
continue;
}
// 垂直方向的标尺,和水平方向的标尺是一样的,就是调整的是 top 属性
if (direction === VERTICAL) {
}
}
这样就完成放大之后标尺的刻度的更新了,来看看效果:
可以看到放大之后,标尺的刻度的间距变大了,但是有些刻度超出了画布的范围,这个时候就需要移除掉超出的刻度了,这里就是通过判断 left 值来移除掉超出的刻度的。
这里缩小的效果也是有的,都是通过缩放比率来计算的,但是因为缩放的时候移除了一些刻度,所以缩小的时候就需要添加一些刻度来填充空白的位置,代码如下:
// 找到最小和最大的刻度,方便后面添加刻度
const { horizontal, vertical } = staticCanvas.getObjects().reduce((prev, curr) => {
if (curr._attr) {
if (curr._attr.direction === HORIZONTAL) {
if (prev.horizontal.minLeft == null) {
prev.horizontal.minLeft = curr.left;
prev.horizontal.maxLeft = curr.left;
prev.horizontal.minScale = curr._attr.scale;
prev.horizontal.maxScale = curr._attr.scale;
}
if (curr.left < prev.horizontal.minLeft) {
prev.horizontal.minLeft = curr.left;
prev.horizontal.minScale = curr._attr.scale;
}
if (curr.left > prev.horizontal.maxLeft) {
prev.horizontal.maxLeft = curr.left;
prev.horizontal.maxScale = curr._attr.scale;
}
}
}
return prev;
}, {
horizontal: {
minLeft: null,
maxLeft: null,
minScale: null,
maxScale: null,
}
});
// 使用一个 while 循环来添加刻度,这里是添加左边的刻度
// 这里判断 20 是偏移量,距离静态画布的左边的距离
while ((horizontal.minLeft = horizontal.minLeft - magnification * zoom) > 20) {
// 每次都更新最小值,这样就可以保证刻度的数值是正确的
horizontal.minScale = horizontal.minScale - magnification;
createScale([horizontal.minLeft, 20, horizontal.minLeft, 15], horizontal.minScale, HORIZONTAL, currScaleMagnification);
}
// 这里是添加右边的刻度
while ((horizontal.maxLeft = horizontal.maxLeft + magnification * zoom) < staticCanvas.width - 20) {
horizontal.maxScale = horizontal.maxScale + magnification;
createScale([horizontal.maxLeft, 20, horizontal.maxLeft, 15], horizontal.maxScale, HORIZONTAL, currScaleMagnification);
}
这里还是一样删除了对垂直方向的处理,实现都相同,就是处理的属性不一样而已,效果如下:
现在要处理就是放大到一定的程度之后,标尺的刻度就需要更新,之前是10:1
,后面就是5:1
了,这个时候就需要更新刻度的数值了。
这里首先是要更新缩放比率,然后是更新刻度的数值,代码如下:
// 计算缩放等级
function computedMagnificationLevel(level) {
// 重置点位线的标识,就是突出显示 100、200 的那些点位
const resetScale = (lines, direction = HORIZONTAL) => {
}
// 隐藏文字
const hideText = (text) => {
}
// 缩小比率处理函数
const shrink = () => {
}
// 重置并创建刻度
const resetAndCreateScale = (lines, direction = HORIZONTAL) => {
}
// 放大比率处理函数
const magnify = () => {
}
// 计算缩放比率
perScaleMagnification = currScaleMagnification;
const perLevel = magnificationLevel;
magnificationLevel = level;
magnification = magnificationLevels[magnificationLevel];
currScaleMagnification = magnification * 10;
// 如果缩小了,就调用缩小的处理函数
if (perLevel < level) {
shrink();
} else if (perLevel > level) {
magnify();
}
}
这里定义了一堆的函数,首先是处理缩放比率的一些变量,然后就是调用放大或缩小的处理函数;
首先是缩小,缩小就是删除一些刻度,然后将突出显示的点位重新设置为普通的点位,代码如下:
const shrink = () => {
let objects = staticCanvas.getObjects();
// perScaleMagnification 可能为空
if (!perScaleMagnification) return;
// 这里是直接删除掉缩小之后不需要的刻度,通过判断 scale 的值来删除
staticCanvas.remove(...objects.filter(item => {
if (item._attr && item._attr.scale % magnification !== 0) {
return true;
}
}));
// 获取新的点位
const { horizontalLines, horizontalTexts, verticalLines, verticalTexts } = dataPacket();
// 重置点位线的标识
resetScale(horizontalLines, HORIZONTAL);
resetScale(verticalLines, VERTICAL);
// 隐藏突出显示的文字
[].concat(horizontalTexts, verticalTexts).forEach(item => {
if (item._attr.scale % perScaleMagnification === 0) {
hideText(item);
}
});
}
resetScale
和hideText
就是一些重置和隐藏的操作,代码如下:
const resetScale = (lines, direction = HORIZONTAL) => {
const getOptions = {
horizontal: (item) => {
return {
x1: item.left,
x2: item.left,
y1: 20,
y2: 15,
}
},
vertical: (item) => {
return {
x1: 20,
x2: 15,
y1: item.top,
y2: item.top,
}
}
}
for (let i = 0; i < lines.length; i++) {
const item = lines[i];
// 这里就是将之前突出显示的点位,重新设置为普通的点位
if (item._attr.scale % currScaleMagnification !== 0 && item._attr.scale % perScaleMagnification === 0) {
const options = getOptions[direction](item);
options && item.set(options);
}
}
}
const hideText = (text) => {
if (text._attr.scale % currScaleMagnification !== 0) {
text.set({ text: '' });
} else {
text.set({ text: (text._attr.scale).toString() });
}
}
然后就是放大的处理函数了,放大就是添加一些刻度,然后将突出显示的点位重新设置为普通的点位,代码如下:
const magnify = () => {
const { horizontalLines, horizontalTexts, verticalLines, verticalTexts } = dataPacket();
resetAndCreateScale(horizontalLines, HORIZONTAL);
resetAndCreateScale(verticalLines, VERTICAL);
[].concat(horizontalTexts, verticalTexts).forEach(hideText);
}
这里看着代码很少,主要的逻辑都在resetAndCreateScale
函数中,代码如下:
const resetAndCreateScale = (lines, direction = HORIZONTAL) => {
// 通过当前点位来获取新的刻度,只需要在当前刻度后面添加一个新的刻度就好了
const getLinePoint = {
horizontal: (item) => {
return [item.left + magnification * zoom, 20, item.left + magnification * zoom, 15];
},
vertical: (item) => {
return [20, item.top + magnification * zoom, 15, item.top + magnification * zoom];
}
}
// 重置为普通的点位
const getOptions = {
horizontal: (item) => {
return {
x1: item.left,
x2: item.left,
y1: 20,
y2: 10,
}
},
vertical: (item) => {
return {
x1: 20,
x2: 10,
y1: item.top,
y2: item.top,
}
}
}
for (let i = 0; i < lines.length; i++) {
const item = lines[i];
const linePoints = getLinePoint[direction](item);
createScale(linePoints, item._attr.scale + magnification, direction, currScaleMagnification);
if (item._attr.scale % currScaleMagnification === 0) {
const options = getOptions[direction](item);
options && item.set(options);
}
}
}
放大就是添加新的点位,怎么知道新的点位添加到什么地方呢?参考的就是旧的点位,只需要在旧的点位后面添加一个新的点位就好了,这里就是通过getLinePoint
来获取新的点位的的坐标。
然后就是将突出显示的点位重新设置为普通的点位,这里的处理方案和缩小的时候是一样的;
最后的效果如下:
3. 总结
实现这个小功能花费了我不小的精力,但是收获也是很大的,这里就不一一列举了;
在此期间查询了不少得资料,例如刻度怎么通过鼠标的位置进行缩放,因为放大刻度是往外扩散的,缩小是往里面收缩的,这里的应该怎么计算?
还有刻度值怎么正确显示,因为每次比率调整之后,显示在标尺上的刻度值也要同步调整,这里应该用什么方案?
然后还有拖动的时候,在开发阶段拖动的时候点位总是乱跑,最后找到问题之后通过头尾指针的方式来解决的,这里应该有更好的方案吧?
当然在这里还有一个bug,就是标尺的放大和缩小值只能是上一个的两倍,不能是任意的倍数;
因为在放大的时候是往两个刻度中间插入一个刻度,缩小的时候同理是删除一个刻度,这样就导致了放大和缩小的倍数只能是上一个的两倍;
这里的解决方案是删除所有的刻度重新进行绘制,这里我只有一个大致的方案,具体会遇到那些问题感兴趣的小伙伴可以自行尝试一下;
参考资料
[1]
github.com/MrFrankel/r…:https://github.com/MrFrankel/ruler
作者:田八
https://juejin.cn/post/7274887814579388416