我做了一个在线白板!(二)

简介: 我做了一个在线白板!(二)

世界太小了


有一天我们的小矩形说,世界这么大,它想去看看,确实,屏幕就这么大,矩形肯定早就待腻了,作为万能的画布操控者,让我们来满足它的要求。


我们新增两个状态变量:scrollXscrollY,记录画布水平和垂直方向的滚动偏移量,以垂直方向的偏移量来介绍,当鼠标滚动时,增加或减少scrollY,但是这个滚动值我们不直接应用到画布上,而是在绘制矩形的时候加上去,比如矩形用来的y100,我们向上滚动了100px,那么实际矩形绘制的时候的y=100-100=0,这样就达到了矩形也跟着滚动的效果。


// 当前滚动值
let scrollY = 0;
// 监听事件
const bindEvent = () => {
  // ...
  canvas.value.addEventListener("mousewheel", onMousewheel);
};
// 鼠标移动事件
const onMousewheel = (e) => {
  if (e.wheelDelta < 0) {
    // 向下滚动
    scrollY += 50;
  } else {
    // 向上滚动
    scrollY -= 50;
  }
  // 重新渲染所有元素
  renderAllElements();
};


然后我们再绘制矩形时加上这个滚动偏移量:


class Rectangle {
    render() {
        ctx.save();
        let _x = this.x;
        let _y = this.y - scrollY;
        let canvasPos = screenToCanvas(_x, _y);
        // ...
    }
}



是不是很简单,但是问题又来了,因为滚动后会发现我们又无法激活矩形了,而且绘制矩形也出问题了:

image.png


原因和矩形旋转一样,滚动只是最终绘制的时候加上了滚动值,但是矩形的x、y仍旧没有变化,因为绘制时是减去了scrollY,那么我们获取到的鼠标的clientY不妨加上scrollY,这样刚好抵消了,修改一下鼠标按下和鼠标移动的函数:


const onMousedown = (e) => {
    let _clientX = e.clientX;
    let _clientY = e.clientY + scrollY;
    mousedownX = _clientX;
    mousedownY = _clientY;
    // ...
}
const onMousemove = (e) => {
    if (!isMousedown) {
        return;
    }
    let _clientX = e.clientX;
    let _clientY = e.clientY + scrollY;
    if (currentType.value === "selection") {
        if (isAdjustmentElement) {
            let ox = _clientX - mousedownX;
            let oy = _clientY - mousedownY;
            if (hitActiveElementArea === "body") {
                // 进行移动操作
            } else if (hitActiveElementArea === "rotate") {
                // ...
        let or = getTowPointRotate(
                  center.x,
                  center.y,
                  mousedownX,
                  mousedownY,
                  _clientX,
                  _clientY
                );
                // ...
            }
        }
    }
    // ...
    // 更新矩形的大小
    activeElement.width = _clientX - mousedownX;
    activeElement.height = _clientY - mousedownY;
    // ...
}

image.png

反正把之前所有使用e.clientY的地方都修改成加上scrollY后的值。


image.png


距离产生美


有时候矩形太小了我们想近距离看看,有时候太大了我们又想离远一点,怎么办呢,很简单,加个放大缩小的功能!


新增一个变量scale


// 当前缩放值
let scale = 1;


然后当我们绘制元素前缩放一下画布即可:


// 渲染所有元素
const renderAllElements = () => {
  clearCanvas();
  ctx.save();// ++
  // 整体缩放
  ctx.scale(scale, scale);// ++
  allElements.forEach((element) => {
    element.render();
  });
  ctx.restore();// ++
};


添加两个按钮,以及两个放大缩小的函数:


// 放大
const zoomIn = () => {
  scale += 0.1;
  renderAllElements();
};
// 缩小
const zoomOut = () => {
  scale -= 0.1;
  renderAllElements();
};


image.png



问题又又又来了朋友们,我们又无法激活矩形以及创造新矩形又出现偏移了:


image.png


还是老掉牙的原因,无论怎么滚动缩放旋转,矩形的x、y本质都是不变的,没办法,转换吧:


image.png


同样是修改鼠标的clientX、clientY,先把鼠标坐标转成画布坐标,然后缩小画布的缩放值,最后再转成屏幕坐标即可:


const onMousedown = (e) => {
  // 处理缩放
  let canvasClient = screenToCanvas(e.clientX, e.clientY);// 屏幕坐标转成画布坐标
  let _clientX = canvasClient.x / scale;// 缩小画布的缩放值
  let _clientY = canvasClient.y / scale;
  let screenClient = canvasToScreen(_clientX, _clientY)// 画布坐标转回屏幕坐标
  // 处理滚动
  _clientX = screenClient.x;
  _clientY = screenClient.y + scrollY;
  mousedownX = _clientX;
  mousedownY = _clientY;
  // ...
}
// onMousemove方法也是同样处理


image.png


能不能整齐一点


如果我们想让两个矩形对齐,靠手来操作是很难的,解决方法一般有两个,一是增加吸附的功能,二是通过网格,吸附功能是需要一定计算量的,本来咱们就不富裕的性能就更加雪上加霜了,所以咱们选择使用网格。


先来增加个画网格的方法:


// 渲染网格
const renderGrid = () => {
  ctx.save();
  ctx.strokeStyle = "#dfe0e1";
  let width = canvas.value.width;
  let height = canvas.value.height;
  // 水平线,从上往下画
  for (let i = -height / 2; i < height / 2; i += 20) {
    drawHorizontalLine(i);
  }
  // 垂直线,从左往右画
  for (let i = -width / 2; i < width / 2; i += 20) {
    drawVerticalLine(i);
  }
  ctx.restore();
};
// 绘制网格水平线
const drawHorizontalLine = (i) => {
  let width = canvas.value.width;
  // 不要忘了绘制网格也需要减去滚动值
  let _i = i - scrollY;
  ctx.beginPath();
  ctx.moveTo(-width / 2, _i);
  ctx.lineTo(width / 2, _i);
  ctx.stroke();
};
// 绘制网格垂直线
const drawVerticalLine = (i) => {
  let height = canvas.value.height;
  ctx.beginPath();
  ctx.moveTo(i, -height / 2);
  ctx.lineTo(i, height / 2);
  ctx.stroke();
};


代码看着很多,但是逻辑很简单,就是从上往下扫描和从左往右扫描,然后在绘制元素前先绘制一些网格:


const renderAllElements = () => {
  clearCanvas();
  ctx.save();
  ctx.scale(scale, scale);
  renderGrid();// ++
  allElements.forEach((element) => {
    element.render();
  });
  ctx.restore();
};


进入页面就先调用一下这个方法即可显示网格:


onMounted(() => {
  initCanvas();
  bindEvent();
  renderAllElements();// ++
});


image.png


到这里我们虽然绘制了网格,但是实际上没啥用,它并不能限制我们,我们需要绘制网格的时候让矩形贴着网格的边,这样绘制多个矩形的时候就能轻松的实现对齐了。


这个怎么做呢,很简单,因为网格也相当于是从左上角开始绘制的,所以我们获取到鼠标的clientX、clientY后,对网格的大小进行取余,然后再减去这个余数,即可得到最近可以吸附到的网格坐标:


image.png


如上图所示,网格大小为20,鼠标坐标是(65,65)x、y都取余计算65%20=5,然后均减去5得到吸附到的坐标(60,60)


接下来修改onMousedownonMousemove函数,需要注意的是这个吸附仅用于绘制图形,点击检测我们还是要使用未吸附的坐标:


const onMousedown = (e) => {
    // 处理缩放
    // ...
    // 处理滚动
    _clientX = screenClient.x;
    _clientY = screenClient.y + scrollY;
    // 吸附到网格
    let gridClientX = _clientX - _clientX % 20;
    let gridClientY = _clientY - _clientY % 20;
    mousedownX = gridClientX;// 改用吸附到网格的坐标
    mousedownY = gridClientY;
    // ...
    // 后面进行元素检测的坐标我们还是使用_clientX、_clientY,保存矩形当前状态的坐标需要换成使用gridClientX、gridClientY
    activeElement.save(gridClientX, gridClientY, hitArea);
    // ...
}
const onMousemove = (e) => {
    // 处理缩放
    // ...
    // 处理滚动
    _clientX = screenClient.x;
    _clientY = screenClient.y + scrollY;
    // 吸附到网格
    let gridClientX = _clientX - _clientX % 20;
    let gridClientY = _clientY - _clientY % 20;
    // 后面所有的坐标都由_clientX、_clientY改成使用gridClientX、gridClientY
}


image.png


当然,上述的代码还是有不足的,当我们滚动或缩小后,网格就没有铺满页面了:


image.png


解决起来也不难,比如上图,缩小以后,水平线没有延伸到两端,因为缩小后相当于宽度变小了,那我们只要绘制水平线时让宽度变大即可,那么可以除以缩放值:


const drawHorizontalLine = (i) => {
  let width = canvas.value.width;
  let _i = i + scrollY;
  ctx.beginPath();
  ctx.moveTo(-width / scale / 2, _i);// ++
  ctx.lineTo(width / scale / 2, _i);// ++
  ctx.stroke();
};


垂直线也是一样。


而当发生滚动后,比如向下滚动,那么上方的水平线没了,那我们只要补画一下上方的水平线,水平线我们是从-height/2开始向下画到height/2,那么我们就从-height/2开始再向上补画:


const renderGrid = () => {
    // ...
    // 水平线
    for (let i = -height / 2; i < height / 2; i += 20) {
        drawHorizontalLine(i);
    }
    // 向下滚时绘制上方超出部分的水平线
    for (
        let i = -height / 2 - 20;
        i > -height / 2 + scrollY;
        i -= 20
    ) {
        drawHorizontalLine(i);
    }
    // ...
}


限于篇幅就不再展开,各位可以阅读源码或自行完善。


照个相吧


如果我们想记录某一时刻矩形的美要怎么做呢,简单,导出成图片就可以了。


导出图片不能简单的直接把画布导出就行了,因为当我们滚动或放大后,矩形也许都在画布外了,或者只有一个小矩形,而我们把整个画布都导出了也属实没有必要,我们可以先计算出所有矩形的公共外包围框,然后另外创建一个这么大的画布,把所有元素在这个画布里也绘制一份,然后再导出这个画布即可。


计算所有元素的外包围框可以先计算出每一个矩形的四个角的坐标,注意是要旋转之后的,然后再循环所有元素进行比较,计算出minx、maxx、miny、maxy即可。


// 获取多个元素的最外层包围框信息
const getMultiElementRectInfo = (elementList = []) => {
  if (elementList.length <= 0) {
    return {
      minx: 0,
      maxx: 0,
      miny: 0,
      maxy: 0,
    };
  }
  let minx = Infinity;
  let maxx = -Infinity;
  let miny = Infinity;
  let maxy = -Infinity;
  elementList.forEach((element) => {
    let pointList = getElementCorners(element);
    pointList.forEach(({ x, y }) => {
      if (x < minx) {
        minx = x;
      }
      if (x > maxx) {
        maxx = x;
      }
      if (y < miny) {
        miny = y;
      }
      if (y > maxy) {
        maxy = y;
      }
    });
  });
  return {
    minx,
    maxx,
    miny,
    maxy,
  };
}
// 获取元素的四个角的坐标,应用了旋转之后的
const getElementCorners = (element) => {
  // 左上角
  let topLeft = getElementRotatedCornerPoint(element, "topLeft")
  // 右上角
  let topRight = getElementRotatedCornerPoint(element, "topRight");
  // 左下角
  let bottomLeft = getElementRotatedCornerPoint(element, "bottomLeft");
  // 右下角
  let bottomRight = getElementRotatedCornerPoint(element, "bottomRight");
  return [topLeft, topRight, bottomLeft, bottomRight];
}
// 获取元素旋转后的四个角坐标
const getElementRotatedCornerPoint = (element, dir) => {
  // 元素中心点
  let center = getRectangleCenter(element);
  // 元素的某个角坐标
  let dirPos = getElementCornerPoint(element, dir);
  // 旋转元素的角度
  return getRotatedPoint(
    dirPos.x,
    dirPos.y,
    center.x,
    center.y,
    element.rotate
  );
};
// 获取元素的四个角坐标
const getElementCornerPoint = (element, dir) => {
  let { x, y, width, height } = element;
  switch (dir) {
    case "topLeft":
      return {
        x,
        y,
      };
    case "topRight":
      return {
        x: x + width,
        y,
      };
    case "bottomRight":
      return {
        x: x + width,
        y: y + height,
      };
    case "bottomLeft":
      return {
        x,
        y: y + height,
      };
    default:
      break;
  }
};


代码很多,但是逻辑很简单,计算出了所有元素的外包围框信息,接下来就可以创建一个新画布以及把元素绘制上去:


// 导出为图片
const exportImg = () => {
  // 计算所有元素的外包围框信息
  let { minx, maxx, miny, maxy } = getMultiElementRectInfo(allElements);
  let width = maxx - minx;
  let height = maxy - miny;
  // 替换之前的canvas
  canvas.value = document.createElement("canvas");
  canvas.value.style.cssText = `
    position: absolute;
    left: 0;
    top: 0;
    border: 1px solid red;
    background-color: #fff;
  `;
  canvas.value.width = width;
  canvas.value.height = height;
  document.body.appendChild(canvas.value);
  // 替换之前的绘图上下文
  ctx = canvas.value.getContext("2d");
  // 画布原点移动到画布中心
  ctx.translate(canvas.value.width / 2, canvas.value.height / 2);
  // 将滚动值恢复成0,因为在新画布上并不涉及到滚动,所有元素距离有多远我们就会创建一个有多大的画布
  scrollY = 0;
  // 渲染所有元素
  allElements.forEach((element) => {
    // 这里为什么要减去minx、miny呢,因为比如最左上角矩形的坐标为(100,100),所以min、miny计算出来就是100、100,而它在我们的新画布上绘制时应该刚好也是要绘制到左上角的,坐标应该为0,0才对,所以所有的元素坐标均需要减去minx、miny
    element.x -= minx;
    element.y -= miny;
    element.render();
  });
};


image.png


当然,我们替换了用来的画布元素、绘图上下文等,实际上应该在导出后恢复成原来的,篇幅有限就不具体展开了。


白白


作为喜新厌旧的我们,现在是时候跟我们的小矩形说再见了。


删除可太简单了,直接把矩形从元素大家庭数组里把它去掉即可:


const deleteActiveElement = () => {
  if (!activeElement) {
    return;
  }
  let index = allElements.findIndex((element) => {
    return element === activeElement;
  });
  allElements.splice(index, 1);
  renderAllElements();
};


image.png


小结


以上就是白板的核心逻辑,是不是很简单,如果有下一篇的话笔者会继续为大家介绍一下箭头的绘制、自由书写、文字的绘制,以及如何按比例缩放文字图片等这些需要固定长宽比例的图形、如何缩放自由书写折线这些由多个点构成的元素,敬请期待,白白~


相关文章
|
弹性计算 安全 网络安全
搭建简易多人在线视频会议系统
本场景将介绍使用音视频服务单间一个简易的视频会议室。
|
存储 监控 数据可视化
|
存储 XML 机器学习/深度学习
VisionOn 一款集流程图、思维导图、白板于一体的轻量级在线制图工具
在工作和学习过程中,通过可视化的图形,有助于清晰高效地表达我们的灵感、想法、思想。 工欲善其事,必先利其器。 目前,思维导图软件已经有 Xmind、Mindnode、 MindMeister 、亿图图示、 Gitmind,流程图软件包括 Microsoft Visio、 Draw.io、ProcessOn,白板软件包括 Miro、 无边记、 BoardMix 博思白板、Excalidraw. 今天介绍一款简单、好用、强大、高颜值、性价比高的制图工具 —— VisionOn.
358 0
|
Web App开发 前端开发
|
程序员 Windows
ZoomIt|白板助手
ZoomIt|白板助手
479 0
|
存储 移动开发 安全
ProcessOn2022网页版在线思维导图
ProcessOn是一个在线协作绘图平台,为用户提供强大、易用的作图工具!支持在线创作流程图、思维导图、组织结构图、网络拓扑图、BPMN、UML图、UI界面原型设计、iOS界面原型设计等。...
939 0
|
前端开发 流计算
我做了一个在线白板(三)
前两篇给大家介绍了一下矩形的绘制、选中、拖动、旋转、伸缩,以及放大缩小、网格模式、导出图片等功能,本文继续为各位介绍一下箭头的绘制、自由书写、文字的绘制,以及如何按比例缩放文字图片等这些需要固定长宽比例的图形、如何缩放自由书写折线这些由多个点构成的元素。
155 0
我做了一个在线白板(三)
|
前端开发 JavaScript
我做了一个在线白板!(一)
我做了一个在线白板!(一)
332 0
我做了一个在线白板!(一)
|
存储
《秋风日常第一期》白板协作工具 LeanBoard
今天打算改变一下公众号的目标方向(手动狗头),也是和一位朋友闲聊之后来的灵感,决定以后分享一些日常学习到知识,或者看到好玩,有用的工具。今天通过 github daily,看到一款比较好用的并且精简的白板工具 ———— LeanBoard。
《秋风日常第一期》白板协作工具 LeanBoard
|
Web App开发 文字识别 Java
压箱底的10款在线工具平台
我是JavaPub,《最少必要面试题》已在更新中。我是JavaPub,《最少必要面试题》已在更新中。我是JavaPub,《最少必要面试题》已在更新中。我是JavaPub,《最少必要面试题》已在更新中。我是JavaPub,《最少必要面试题》已在更新中。我是JavaPub,《最少必要面试题》已在更新中。
188 0
压箱底的10款在线工具平台