低代码必备!带标尺和缩略图的画板

简介: 低代码平台经常要用到标尺和缩略图操作同步的画板,点进来就能定制你的专属低代码画板!还等什么!快看我!

最近重构某项目的仪表板,代码屎山一样没眼看!果断弃之!从零开始!自己搞更香!

1.功能需求

  1. 左侧和顶部有刻度标尺,跟着画板滚动而变化
  2. 缩略图,可以通过移动视图框来控制画板滚动
  3. 画板可放缩,对应标尺和缩略图比例跟着变
  4. 移动画板位置,对应标尺和缩略图位置跟着变
  5. 适应屏幕大小和实际大小

2. 用canvas画刻度标尺

2.1 横向刻度标尺

js

const canvas=document.getElementById('myCanvas')const padding=2;//边距const startLen=60;//开始间隔const width=500,height=24;//长宽const ctx  = canvas.getContext('2d');  const unit = 10; //间隔刻度单位    //计算出要绘制多少个刻度  const scaleCount = Math.ceil(width + startLen / unit);    /***--执行绘制---***/       canvas.width = width + startLen;    canvas.height = height;    ctx.clearRect(0, 0, width, height);    ctx.beginPath();    //绘制起点    ctx.strokeStyle = 'rgb(0, 0, 0)';    ctx.font = '10px Arial';    ctx.lineWidth = 1;    ctx.moveTo(startLen, 0);    ctx.lineTo(startLen, height);    ctx.fillText('0', startLen + padding, 13);    for (let i = 1; i <= scaleCount; i++) {    //计算每个刻度的位置      const step = startLen + Math.round(i * unit);      //10的倍数刻度大长度      if (i % 10 === 0) {        ctx.moveTo(step, 0);        ctx.lineTo(step, height);        //标注刻度值        const scaleText = i * unit + '';        ctx.fillText(scaleText, step + padding, 13);      } elseif (i % 5 === 0) {//5的倍数刻度中长度        ctx.moveTo(step, 15);        ctx.lineTo(step, height);        //标注刻度值        const scaleText = i * unit + '';        ctx.fillText(scaleText, step + padding, 13);      } else {//其他刻度小长度        ctx.moveTo(step, height - 3);        ctx.lineTo(step, height);      }    }    ctx.stroke();

2.2 纵向刻度标尺

横向和纵向的计算公式相似,但坐标计算有所不同

js

 canvas.width = height;    canvas.height = width + startLen;    ctx.clearRect(0, 0, height, width + startLen);    ctx.beginPath();    //绘制起点    ctx.strokeStyle = 'rgb(0, 0, 0)';    ctx.font = '10px Arial';    ctx.lineWidth =1;    ctx.moveTo(0, startLen);    ctx.lineTo(height, startLen);    ctx.fillText('0', padding, startLen - padding);       for (let i = 1; i <= scaleCount; i++) {     //计算每个刻度的位置      const step = startLen + Math.round(i * unit );        //10的倍数刻度大长度      if (i % 10 === 0) {        ctx.moveTo(0, step);        ctx.lineTo(height, step);        //标注刻度值        const scaleText = unit * i + '';        ctx.fillText(scaleText, padding, step - padding);      } elseif (i % 5 === 0) {//5的倍数刻度中长度        ctx.moveTo(15, step);        ctx.lineTo(height, step);        //标注刻度值        const scaleText = unit * i + '';        ctx.fillText(scaleText, padding, step - padding);      } else {//其他刻度小长度        ctx.moveTo(height - 3, step);        ctx.lineTo(height, step);      }    }    ctx.stroke();

3. 缩略图

缩略图主要由屏幕大小缩略图和可视范围缩略图组成,可视范围缩略图要对应画布移动

js

const canvas = document.getElementById('myCanvas');      const ctx = canvas.getContext('2d');      const startLen = 6;        //屏幕大小      const screenWidth = 1920;      const screenHeight = 1080;      const thumbnailSize = 0.1; //10:1的缩放比例      //缩略图大小      const canvasConfig = {        thumbnailWidth: Math.ceil(screenWidth * thumbnailSize),        thumbnailHeight: Math.ceil(screenHeight * thumbnailSize),        thumbnailWrapWidth: Math.ceil((screenWidth + 400) * thumbnailSize),        thumbnailWrapHeight: Math.ceil((screenHeight + 400) * thumbnailSize)      };      //可视范围框      const viewBox = {        viewWidth: Math.ceil(1000 * thumbnailSize),        viewHeight: Math.ceil(800 * thumbnailSize)      };      //滚动坐标      const scroll = {        scrollLeft: Math.ceil(300 * thumbnailSize),        scrollTop: Math.ceil(200 * thumbnailSize)      };      //计算出要绘制多少个刻度      canvas.width = canvasConfig.thumbnailWrapWidth;      canvas.height = canvasConfig.thumbnailWrapHeight;      //画缩略框      ctx.clearRect(0, 0, canvasConfig.thumbnailWrapWidth, canvasConfig.thumbnailWrapHeight);      ctx.beginPath();      ctx.fillStyle = 'rgba(26, 103, 255, 0.5)';      ctx.rect(startLen, startLen, canvasConfig.thumbnailWidth, canvasConfig.thumbnailHeight);      ctx.fill();      //画可视范围框      ctx.beginPath();      ctx.strokeStyle = '#1a67ff';      ctx.rect(        Math.round(scroll.scrollLeft),        Math.round(scroll.scrollTop),        viewBox.viewWidth,        viewBox.viewHeight      );      ctx.stroke();

4. 画板

画板样式

scss

  .canvas-panel-wrap {    position: absolute;    box-shadow: var(--canvas-shadow) 0030px0;    transform-origin: left top;    margin-left: 60px;    margin-top: 60px;  }

画板大小

js

//操作空间大小,预留400px作为移动showWidth() {      returnthis.screenWidth * (this.scale < 100 ? 1 : this.percent) + 400;    },    showHeight() {      returnthis.screenHeight * (this.scale < 100 ? 1 : this.percent) + 400;    },   canvasStyle: computed(() => ({          left: -scrollLeft.value + 'px',          top: -scrollTop.value + 'px',          width: editorStore.screenWidth + 'px',          height: editorStore.screenHeight + 'px',          transform: `scale(${editorStore.scale * 0.01})`        })),

5.画板缩放,标尺和缩略图同步

scale 缩放比例范围[20-200]

5.1 标尺跟随缩放

单位刻度长度,标尺刻度都需添加缩放值,重新计算,这样才能让标尺像素保持不变的情况下,刻度值对应上画板的大小

注意不可以用transform:scale来缩放标尺,会导致像素模糊问题

js

const percent = scale * 0.01;//单位刻度长度  let unit = Math.ceil(10 / percent);  if (unit < 8) {    unit = 8;  }  //计算出要绘制多少个刻度  const scaleCount = Math.ceil(width + startLen / unit);   //……       //计算每个刻度的位置        const step = startLen + Math.round(i * unit * percent);  //……

200%的标尺

70%的标尺

20%的标尺

可以看到开始的间隔0刻度开始的地方是不变的,这就是像素不变,但刻度对应

5.2 缩略图跟随缩放

缩略图不可固定比例,当缩放值大于100时会导致整张缩略图跟着变大,占据操作空间,而当缩放值小于100时则会导致整张缩略图跟着变小,不好操作,因此需要处理一下;让缩略图保持大小。

js

//缩略图比例thumbnailSize() {      if (this.scale > 100) {        return10 / this.scale;      } else {        return0.1;      }    },    //缩略图大小canvasConfig() {      return {        thumbnailWidth: Math.ceil(this.screenWidth * this.thumbnailSize * this.percent),        thumbnailHeight: Math.ceil(this.screenHeight * this.thumbnailSize * this.percent),        thumbnailWrapWidth: Math.ceil(this.showWidth * this.thumbnailSize),        thumbnailWrapHeight: Math.ceil(this.showHeight * this.thumbnailSize)      };    }

6. 移动画板

6.1 标尺跟随移动

按空格键切换显隐画布操作蒙版,通过移动蒙版来实现移动画布,通过滚轮来缩放画布

js

constonKeyAction = (e: KeyboardEvent) => {//按空格键切换显隐操作操作蒙版        if (e.keyCode == keyCode.space) {          editorStore.setMoveCanvas(!editorStore.isMoveCanvas);        }      };      //滚轮缩放画布      constonWheelAction = (e: WheelEvent) => {        if (isMoveCanvas.value) {          if (e.wheelDelta > 0) {            editorStore.setScale(editorStore.scale + 1);          } else {            editorStore.setScale(editorStore.scale - 1);          }        }      };      //注册监听动作      onMounted(() => {        window.addEventListener('keydown', onKeyAction);        window.addEventListener('wheel', onWheelAction);        refreshRuler();      });      //取消监听动作      onBeforeUnmount(() => {        window.removeEventListener('keydown', onKeyAction);        window.removeEventListener('wheel', onWheelAction);      });      

移动蒙版

js

//移动画布信息      let moveInfo = {        startX: 0,        startY: 0      };//记录开始位置onMoveCanvasDown: (e: MouseEvent) => {          e.stopPropagation();          moveInfo = {            startX: e.clientX,            startY: e.clientY          };        },        //结束鼠标操作后移动画布        onMoveCanvasUp: (e: MouseEvent) => {          e.stopPropagation();          //计算移动坐标          let left = scrollLeft.value - (e.clientX - moveInfo.startX);          let top = scrollTop.value - (e.clientY - moveInfo.startY);          //   console.log('move', left, top);          editorStore.setScroll({ left, top });        }

移动范围有效性校验,以免移动出界

js

    setScroll({ left, top }: { left: number; top: number }) {      const distance = 60;      if (left < 0) {        left = 0;      } elseif (left > this.showWidth - this.viewWidth - distance) {        left = this.showWidth - this.viewWidth - distance;      }      if (top < 0) {        top = 0;      } elseif (top > this.showHeight - this.viewHeight - distance) {        top = this.showHeight - this.viewHeight - distance;      }      this.$state.scrollLeft = Math.round(left);      this.$state.scrollTop = Math.round(top);    },

6.2 缩略图跟随移动

缩略图可视范围框是当前仪表板给的内容空间,移动这个可视范围框根据对应反比例,可映射到整个画布的移动

另外,需要监听画布大小和移动更新缩略图和可视范围位置,监听window的resize动作,更新可视范围

js

//可视范围      let dashboardDom = document.getElementById('dashboard');          if (!dashboardDom) {            return;          }          viewBox.value.viewWidth = dashboardDom.offsetWidth;          viewBox.value.viewHeight = dashboardDom.offsetHeight;//缩略图反比例,对应上缩略图比例的计算const unscale = computed(() => {        if (editorStore.scale > 100) {          return1 / editorStore.thumbnailSize;        } else {          return10;        }      });     

移动缩略图中可视范围

js

let moveInfo = {        startX: 0,        startY: 0,        isMove: false      };      //记录开始位置         onMoveStart: (e: MouseEvent) => {          moveInfo.isMove = true;          moveInfo.startX = e.clientX;          moveInfo.startY = e.clientY;        },                onMove: (e: MouseEvent) => {          if (moveInfo.isMove) {          //计算反比例移动坐标            let left = editorStore.scrollLeft + (e.clientX - moveInfo.startX) * unscale.value;            let top = editorStore.scrollTop + (e.clientY - moveInfo.startY) * unscale.value;                        editorStore.setScroll({ left, top });            moveInfo.startX = e.clientX;            moveInfo.startY = e.clientY;          }        },        //结束移动        onMoveEnd: () => {          moveInfo.isMove = false;        }

7. 大小适配可视范围

自适应比例 = (可视范围高度-边距和标尺高度/屏幕高度)*100%

js

constonFitCanvas = () => {        store.setScale(          parseInt(          ((document.getElementById('dashboard').offsetHeight - 84) / store.screenHeight) * 100          )        );      };

总结

其实,标尺和缩略图操作协同的画板并不难,主要是缩放比例计算和视图转换的问题!啦啦啦!现在你已经学会了!可以拥有一个常用的低代码画板了!

src/assets/vars.scss这里抽离了一些颜色值,方便大家配置自己想要的样式,canvas绘制的颜色值还是得手动赋上去.

css

:root{  //标尺背景颜色  --ruler-bg: #f4f7fe;  //移动蒙版背景颜色  --move-bg: rgba(0, 0, 0, 0.1);  //画布阴影  --canvas-shadow: rgba(0, 0, 0, 0.5);//右下小操作栏  --slider-icon: #32363c;  --slider-bg: #ffffff;  --canvas-slider-border: rgba(0, 0, 0, 0.1);  --thumbnail-wrap-bg: #f4f7fe;  }

GitHub地址

https://github.com/xiaolidan00/ruler-canvas

相关文章
|
缓存 Ubuntu JavaScript
踩坑记录:QML加载图片资源
踩坑记录:QML加载图片资源
1987 0
|
关系型数据库 MySQL 数据库
找不到请求的 .Net Framework Data Provider。可能没有安装
做的一个项目,框架为.net framework 数据库为mysql 出现如标题错误 检查是否安装mysql、是否安装mysql connector net 笔者是因为没有安装后者mysql connector net 下载地址: [mysql connector net](https://downloads.mysql.com/archives/c-net/ "mysql connector net") 笔者安装截图如下: ![请在此添加图片描述](https://developer-private-1258344699.cos.ap-guangzhou.myqcloud.com/c
504 0
Three添加天空盒子
这篇文章详细说明了如何在Three.js中添加天空盒(Skybox)以增强3D场景的真实感和沉浸体验。
471 6
Three添加天空盒子
|
前端开发 JavaScript 开发者
Canvas库 fabric.js可以实现哪些功能? 动图介绍
fabric.js是一个canvas库,今天整理了一下fabric.js可以实现的功能,用动图的形式分享给大家,方便快速了解fabric.js。
368 0
Canvas库 fabric.js可以实现哪些功能? 动图介绍
|
移动开发 JavaScript 前端开发
UniApp H5 跨域代理配置并使用(配置manifest.json、vue.config.js)
这篇文章介绍了在UniApp H5项目中处理跨域问题的两种方法:通过修改manifest.json文件配置h5设置,或在项目根目录创建vue.config.js文件进行代理配置,并提供了具体的配置代码示例。
UniApp H5 跨域代理配置并使用(配置manifest.json、vue.config.js)
|
消息中间件 canal 分布式计算
类似apache nifi的产品还有哪些?
【10月更文挑战第23天】类似apache nifi的产品还有哪些?
505 3
|
12月前
|
存储 关系型数据库 API
必看!淘宝商品详情数据接口调用,助力商城上货实战全流程(仅供参考)
本文介绍了一个实战案例,通过调用淘宝商品详情数据接口,实现商品信息的自动获取与上架至自建电商平台。主要步骤包括需求分析、技术选型、接口调用、数据存储、自动上货及定时更新,旨在提升工作效率,减少人工操作。
|
弹性计算 编解码 大数据
小鹏汽车核心业务迁移至阿里云倚天实例,节省20%算力成本
9月20日,在2024云栖大会上,小鹏汽车宣布车联网、官网、商城、大数据等核心业务已迁移至阿里云倚天实例,节省了超过20%的算力成本。
460 14
|
JavaScript
Vue3弹性布局(Flex)
这是一个基于 Vue 的弹性布局组件库,提供了丰富的参数配置,如宽度、方向、换行等,支持自定义对齐方式和间隙设置。在线预览展示了不同布局效果,包括单选、按钮和滑动输入条等组件的使用示例。
542 0
Vue3弹性布局(Flex)