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

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

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

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

相关文章
|
算法 Windows
Winform控件优化之实现无锯齿的圆角窗体(或任意图形的无锯齿丝滑的窗体或控件)【借助LayeredWindow】
在一般能搜到的所有实现圆角窗体的示例中,都有着惨不忍睹的锯齿...而借助于Layered Windows,是可以实现丝滑无锯齿效果的Form窗体的,其具体原理就是分层窗体....
1607 0
Winform控件优化之实现无锯齿的圆角窗体(或任意图形的无锯齿丝滑的窗体或控件)【借助LayeredWindow】
|
30天前
|
数据可视化
在线可视化设计教你玩转圆角、阴影
在线可视化设计教你玩转圆角、阴影
28 1
|
6月前
|
定位技术
Pyglet综合应用|推箱子游戏地图编辑器之图片跟随鼠标
Pyglet综合应用|推箱子游戏地图编辑器之图片跟随鼠标
59 0
|
6月前
|
前端开发
Flutter笔记:光影动画按钮、滚动图标卡片组等
Flutter笔记:光影动画按钮、滚动图标卡片组等
79 0
|
C++ Python
Photoshop绘制立体风格的拾色器图标
Photoshop绘制立体风格的拾色器图标
52 0
|
算法 前端开发 数据可视化
语雀新画板
语雀新画板
382 0
|
定位技术
【Axure教程】拖拉拽放大缩小和移动元件
【Axure教程】拖拉拽放大缩小和移动元件
|
JSON Android开发 数据格式
原生app开发技巧——底部导航栏动画效果按钮制作方法之采用photoshop制作gif动画-过渡动画关键帧
原生app开发技巧——底部导航栏动画效果按钮制作方法之采用photoshop制作gif动画-过渡动画关键帧
原生app开发技巧——底部导航栏动画效果按钮制作方法之采用photoshop制作gif动画-过渡动画关键帧
|
前端开发 数据可视化 开发工具
Tooltips提示框的视觉效果:SVG方案实现
Tooltips常被称为提示框(或信息提示框),提示框能够以较强的交互性、自由度为用户提供相应的提示信息。
Tooltips提示框的视觉效果:SVG方案实现