最近重构某项目的仪表板,代码屎山一样没眼看!果断弃之!从零开始!自己搞更香!
1.功能需求
- 左侧和顶部有刻度标尺,跟着画板滚动而变化
- 缩略图,可以通过移动视图框来控制画板滚动
- 画板可放缩,对应标尺和缩略图比例跟着变
- 移动画板位置,对应标尺和缩略图位置跟着变
- 适应屏幕大小和实际大小
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; }