canvas--标尺

项目开发过程中,会遇到标尺的功能,标尺可以通过canvas来实现,具体实现如下:

1、html代码:

<canvas id='canvas' width='800' height='800' />

2、css代码:

body {
    background: #eeeeee;
}
#canvas {
    background: #ffffff;
    cursor: pointer;
    margin-left: 10px;
    margin-top: 10px;
    -webkit-box-shadow: 4px 4px 8px rgba(0,0,0,0.5);
    -moz-box-shadow: 4px 4px 8px rgba(0,0,0,0.5);
    box-shadow: 4px 4px 8px rgba(0,0,0,0.5);
}

3、js代码:

var canvas = document.getElementById('canvas'),
context = canvas.getContext('2d'),
AXIS_MARGIN = 0,
AXIS_ORIGIN = { x: AXIS_MARGIN, y: canvas.height-AXIS_MARGIN },
AXIS_TOP   = AXIS_MARGIN,
AXIS_RIGHT = canvas.width-AXIS_MARGIN,
HORIZONTAL_TICK_SPACING = 50,
VERTICAL_TICK_SPACING = 50,
AXIS_WIDTH  = AXIS_RIGHT - AXIS_ORIGIN.x,
AXIS_HEIGHT = AXIS_ORIGIN.y - AXIS_TOP,
NUM_VERTICAL_TICKS   = AXIS_HEIGHT / VERTICAL_TICK_SPACING,
NUM_HORIZONTAL_TICKS = AXIS_WIDTH  / HORIZONTAL_TICK_SPACING,
TICK_WIDTH = 20,
TICKS_LINEWIDTH = 0.5,
TICKS_COLOR = 'navy',
AXIS_LINEWIDTH = 1.0,
AXIS_COLOR = 'blue';
function drawAxes() {
    context.save(); 
    context.strokeStyle = AXIS_COLOR;
    context.lineWidth = AXIS_LINEWIDTH;
    drawHorizontalAxis();
    drawVerticalAxis();
    context.lineWidth = 0.5;
    context.lineWidth = TICKS_LINEWIDTH;
    context.strokeStyle = TICKS_COLOR;
    drawVerticalAxisTicks();
    drawHorizontalAxisTicks();
    context.restore();
}
function drawHorizontalAxis() {
    context.beginPath();
    context.moveTo(AXIS_ORIGIN.x, AXIS_MARGIN);
    context.lineTo(AXIS_RIGHT,    AXIS_MARGIN)
    context.closePath();
    context.stroke();
}
function drawVerticalAxis() {
    context.beginPath();
    context.moveTo(AXIS_ORIGIN.x, AXIS_MARGIN);
    context.lineTo(AXIS_ORIGIN.x, AXIS_ORIGIN.y);
    context.closePath();
    context.stroke();
}
function drawVerticalAxisTicks() {
    var deltaY;
    for (var i = 1; i < NUM_VERTICAL_TICKS; ++i) {
        context.beginPath();
        if (i % 5 === 0) {
            deltaX = TICK_WIDTH;
            context.moveTo(0, 0 + HORIZONTAL_TICK_SPACING * i);
            context.lineTo(20, 0 + HORIZONTAL_TICK_SPACING * i);
            context.textAlign = 'left';
            context.fillText(i * HORIZONTAL_TICK_SPACING, 30, 0 + HORIZONTAL_TICK_SPACING * i);
        }else {
            deltaX = TICK_WIDTH/2;
        }            
        context.moveTo(AXIS_ORIGIN.x, i * VERTICAL_TICK_SPACING);
        context.lineTo(AXIS_ORIGIN.x + deltaX, i * VERTICAL_TICK_SPACING);
        context.stroke();
    }
}
function drawHorizontalAxisTicks() {
    var deltaY;
    for (var i=1; i < NUM_HORIZONTAL_TICKS; ++i) {
        context.beginPath();
        if (i % 5 === 0) {
            deltaY = TICK_WIDTH;
            context.moveTo(0 + VERTICAL_TICK_SPACING * i, 0);
            context.lineTo(VERTICAL_TICK_SPACING * i, 20);
            context.textAlign = 'left';
            context.fillText(i * VERTICAL_TICK_SPACING, 0 + VERTICAL_TICK_SPACING * i, 30);
         }else {
            deltaY = TICK_WIDTH/2;
         }           
        context.moveTo(AXIS_ORIGIN.x + i * HORIZONTAL_TICK_SPACING, AXIS_MARGIN);
        context.lineTo(AXIS_ORIGIN.x + i * HORIZONTAL_TICK_SPACING, AXIS_MARGIN + deltaY);
        context.stroke();
    }
}
drawAxes();

4、效果:


<think>我们使用Canvas绘制标尺,主要思路是:1.创建两个Canvas元素,分别作为水平标尺和垂直标尺。2.根据画布的缩放比例和偏移量,在标尺上绘制刻度线和数字。3.标尺的位置应该与画布内容同步,即当画布被拖拽或缩放时,标尺的刻度需要更新。在Vue3中,我们可以将标尺作为独立的Canvas元素,放置在画布的顶部和左侧。步骤:1.在模板中创建两个Canvas元素用于标尺,一个水平(顶部),一个垂直(左侧)。2.使用ref获取这两个Canvas元素。3.在mounted生命周期中初始化标尺的绘制。4.当缩放或拖拽时,重新绘制标尺。注意:标尺的刻度需要根据当前的缩放比例(scale)来调整,同时也要考虑画布的偏移量(offset)。绘制标尺的函数:-水平标尺:从左到右绘制刻度线,刻度线的间隔根据缩放比例调整(例如,每50像素一个主刻度,每10像素一个次刻度)。-垂直标尺:从上到下绘制刻度线。关键点:由于画布可能被拖拽,标尺的起始位置也要相应调整。因此,在绘制标尺时,我们需要根据偏移量来调整标尺的起点。实现代码:1.模板部分:```html<template><divclass="ruler-container"><!--水平标尺--><canvasref="horizontalRuler"class="rulerhorizontal"></canvas><!--垂直标尺--><canvasref="verticalRuler"class="rulervertical"></canvas><!--画布区域(之前实现的)--><divclass="canvas-area"...>...</div></div></template>```2.样式部分:```css.ruler-container{position:relative;}.ruler{position:absolute;background-color:#f5f5f5;z-index:10;}.horizontal{top:0;left:20px;/*留出垂直标尺的位置*/width:calc(100%-20px);height:20px;}.vertical{top:20px;/*留出水平标尺的位置*/left:0;width:20px;height:calc(100%-20px);}```3.脚本部分:```javascriptimport{ref,onMounted,watch}from'vue';//在setup中consthorizontalRuler=ref(null);constverticalRuler=ref(null);//已有的scale和offsetconstscale=ref(1);constoffset=ref({x:0,y:0});//绘制标尺的函数constdrawRulers=()=>{drawHorizontalRuler();drawVerticalRuler();};constdrawHorizontalRuler=()=>{constcanvas=horizontalRuler.value;if(!canvas)return;constctx=canvas.getContext('2d');constwidth=canvas.width;constheight=canvas.height;//清除画布ctx.clearRect(0,0,width,height);//设置标尺样式ctx.strokeStyle='#000';ctx.fillStyle='#000';ctx.lineWidth=1;ctx.font='10pxArial';//根据缩放比例计算刻度间隔//我们希望在标尺上每50像素(缩放后)显示一个主刻度,但当缩放比例很大时,可能需要更密集的刻度//这里我们动态调整:当scale增大时,实际间隔的像素会变小,所以我们要增加绘制的密度//我们设定一个最小间隔(以像素为单位),避免刻度过于密集constminStep=50;//最小步长(像素),即当scale=1时,每50像素一个主刻度//实际步长=最小步长/scale,但这样会导致缩放后刻度变密。但我们希望标尺上显示的刻度值不变,所以刻度线之间的实际距离(像素)会随着scale变化//另一种思路:标尺上刻度的数值是实际坐标,与缩放无关,所以刻度线的位置应该是(实际坐标*scale)+偏移量//我们以画布的原点(考虑偏移)为起点,绘制刻度//计算标尺的可见区域(相对于画布内容的可见部分)//由于我们的画布区域有偏移和缩放,标尺需要显示当前视口对应的刻度//获取容器的宽度(可见区域宽度)constcontainerWidth=canvas.parentElement.clientWidth-20;//减去垂直标尺的宽度//计算当前水平方向可见的画布内容范围(实际坐标)conststartX=-offset.value.x/scale.value;constendX=startX+containerWidth/scale.value;//确定主刻度的步长(实际坐标),我们希望标尺上每50像素(实际)一个主刻度,但转换为标尺上的像素距离是50*scale//为了避免标尺上刻度过于密集,我们动态调整步长(按实际坐标),使得在标尺上相邻主刻度之间的像素距离不小于50像素(即当scale很小时,实际步长要变大)letstep=50;//实际坐标的步长letstepInPixel=step*scale.value;//如果stepInPixel小于50,说明刻度太密,需要增大实际步长while(stepInPixel<50){step*=2;stepInPixel=step*scale.value;}//从startX开始,以step为步长,直到endX//找到第一个大于等于startX的step的倍数letfirstTick=Math.floor(startX/step)*step;for(letx=firstTick;x<endX;x+=step){//计算该刻度在标尺上的位置(像素)constpixelX=(x-startX)*scale.value;//绘制主刻度线ctx.beginPath();ctx.moveTo(pixelX,0);ctx.lineTo(pixelX,15);ctx.stroke();//绘制刻度值ctx.fillText(x.toString(),pixelX+2,10);}};//垂直标尺类似,只是方向不同constdrawVerticalRuler=()=>{//类似水平标尺的逻辑};//在mounted中初始化onMounted(()=>{//设置Canvas的宽高(注意:Canvas的宽高属性与CSS宽高不同)horizontalRuler.value.width=horizontalRuler.value.clientWidth;horizontalRuler.value.height=horizontalRuler.value.clientHeight;verticalRuler.value.width=verticalRuler.value.clientWidth;verticalRuler.value.height=verticalRuler.value.clientHeight;drawRulers();});//监听scale和offset的变化,重新绘制标尺watch([scale,offset],()=>{drawRulers();},{deep:true});```注意:上述代码中的drawVerticalRuler需要类似实现。另外,由于标尺的绘制需要考虑性能,避免过于频繁的重绘。优化:可以使用requestAnimationFrame来节流。另外,在缩放和拖拽时,标尺的绘制可能会比较频繁,因此需要确保绘制效率。例如,只绘制可见区域的刻度。以上是使用原生Canvas绘制标尺的基本方法。如果需要更复杂的功能(如更精细的刻度、刻度样式等),可以进一步扩展。引用:关于Canvas的绘制,可以参考MDN文档[^1]。注意:由于标尺的位置与画布内容的位置是联动的,因此我们根据画布的偏移量和缩放比例来计算标尺的刻度位置。实现中可能遇到的难点:-标尺的起始位置(零点)与画布内容的零点对齐,并且随着拖拽而移动。-缩放时,标尺的刻度值不变,但刻度线的间距会变化(因为同样的实际距离在缩放后占用的像素不同)。因此,我们通过将实际坐标乘以缩放比例得到在标尺上的像素位置,然后加上偏移量(注意:标尺本身是不缩放的,所以偏移量是画布容器内的偏移,但我们的标尺和画布容器是同步的)。另一种思路:标尺的绘制也可以不根据实际坐标,而是根据屏幕坐标(即标尺的像素位置直接对应屏幕像素),但这样标尺显示的数字应该是实际坐标(即除以缩放比例后的值)。我们采用的方式:标尺上显示的是实际坐标(不受缩放影响),但刻度线的位置是根据实际坐标乘以缩放比例,再减去当前画布的偏移量(因为画布内容移动了,标尺也要相应移动)来计算的。具体公式:对于水平标尺上的一个点(实际坐标x),在标尺Canvas上的位置为:pixelX=(x*scale)+offset.x[因为画布内容通过transform:translate(offset.x,offset.y)移动了,所以标尺上对应的点也要加上这个偏移]但是注意:我们的标尺是固定在容器上的,不随画布内容移动,所以标尺上绘制的刻度位置应该只与画布内容的可见部分有关。因此,我们计算可见区域的起始实际坐标(startX=-offset.x/scale),然后在这个范围内绘制刻度。因此,在水平标尺上,一个实际坐标x对应的像素位置(相对于标尺Canvas)为:pixelX=(x-startX)*scale而startX=-offset.x/scale,所以:pixelX=(x+offset.x/scale)*scale=x*scale+offset.x但是,这个pixelX是相对于标尺Canvas的,而标尺Canvas的宽度等于容器的宽度(减去垂直标尺宽度)。所以,我们只需要绘制在0到容器宽度之间的刻度。因此,我们循环从startX到endX(实际坐标),步长为step(实际坐标),然后计算每个x对应的pixelX,如果pixelX在0到容器宽度之间,就绘制。垂直标尺同理。由于在绘制标尺时,我们只绘制可见区域内的刻度,这样可以提高性能。最后,在缩放和拖拽时,需要调用drawRulers()更新标尺。以上就是在Vue3中使用Canvas绘制标尺的方法。</think>在 Vue3 中使用 HTML5 Canvas 绘制标尺,可以按照以下步骤实现: ### 实现方法 ```vue <template> <div class="ruler-container"> <!-- 水平标尺 --> <canvas ref="horizontalRuler" class="ruler horizontal"></canvas> <!-- 垂直标尺 --> <canvas ref="verticalRuler" class="ruler vertical"></canvas> <!-- 画布区域 --> <div class="canvas-area"> <canvas ref="mainCanvas"></canvas> </div> </div> </template> <script setup> import { ref, onMounted, watch, nextTick } from 'vue'; // 标尺Canvas引用 const horizontalRuler = ref(null); const verticalRuler = ref(null); const mainCanvas = ref(null); // 标尺参数 const scale = ref(1); const offset = ref({ x: 0, y: 0 }); const rulerSize = 20; // 标尺宽度/高度 // 初始化 onMounted(() => { initCanvases(); drawRulers(); }); // 初始化Canvas尺寸 const initCanvases = () => { const container = horizontalRuler.value.parentElement; const width = container.clientWidth; const height = container.clientHeight; // 设置标尺尺寸 horizontalRuler.value.width = width - rulerSize; horizontalRuler.value.height = rulerSize; verticalRuler.value.width = rulerSize; verticalRuler.value.height = height - rulerSize; // 设置主画布尺寸 mainCanvas.value.width = width - rulerSize; mainCanvas.value.height = height - rulerSize; }; // 绘制标尺 const drawRulers = () => { drawHorizontalRuler(); drawVerticalRuler(); }; // 绘制水平标尺 const drawHorizontalRuler = () => { const canvas = horizontalRuler.value; const ctx = canvas.getContext('2d'); const width = canvas.width; // 清除画布 ctx.clearRect(0, 0, width, rulerSize); // 设置样式 ctx.strokeStyle = '#666'; ctx.fillStyle = '#333'; ctx.font = '10px Arial'; ctx.lineWidth = 1; // 计算刻度参数 const baseStep = 50; // 基础刻度间距 const scaledStep = baseStep * scale.value; // 计算可见区域的起始位置 const startPos = Math.floor(-offset.value.x / scaledStep) * scaledStep; // 绘制刻度线 for (let pos = startPos; pos < width + Math.abs(offset.value.x); pos += scaledStep) { const pixelPos = pos + offset.value.x; // 跳过超出可见区域的刻度 if (pixelPos < 0 || pixelPos > width) continue; // 主刻度(每100像素) if (Math.round(pos / (scaledStep * 2)) % 2 === 0) { ctx.beginPath(); ctx.moveTo(pixelPos, 0); ctx.lineTo(pixelPos, 15); ctx.stroke(); // 刻度值(显示实际坐标) const actualPos = Math.round(pos / scale.value); ctx.fillText(actualPos, pixelPos + 2, 12); } // 次刻度 else { ctx.beginPath(); ctx.moveTo(pixelPos, 5); ctx.lineTo(pixelPos, 10); ctx.stroke(); } } // 绘制标尺边框 ctx.beginPath(); ctx.moveTo(0, rulerSize - 0.5); ctx.lineTo(width, rulerSize - 0.5); ctx.stroke(); }; // 绘制垂直标尺(逻辑类似) const drawVerticalRuler = () => { const canvas = verticalRuler.value; const ctx = canvas.getContext('2d'); const height = canvas.height; ctx.clearRect(0, 0, rulerSize, height); ctx.strokeStyle = '#666'; ctx.fillStyle = '#333'; ctx.font = '10px Arial'; ctx.lineWidth = 1; const baseStep = 50; const scaledStep = baseStep * scale.value; const startPos = Math.floor(-offset.value.y / scaledStep) * scaledStep; for (let pos = startPos; pos < height + Math.abs(offset.value.y); pos += scaledStep) { const pixelPos = pos + offset.value.y; if (pixelPos < 0 || pixelPos > height) continue; if (Math.round(pos / (scaledStep * 2)) % 2 === 0) { ctx.beginPath(); ctx.moveTo(0, pixelPos); ctx.lineTo(15, pixelPos); ctx.stroke(); // 垂直文本需要旋转 ctx.save(); ctx.translate(8, pixelPos + 15); ctx.rotate(-Math.PI / 2); const actualPos = Math.round(pos / scale.value); ctx.fillText(actualPos, 0, 0); ctx.restore(); } else { ctx.beginPath(); ctx.moveTo(5, pixelPos); ctx.lineTo(10, pixelPos); ctx.stroke(); } } ctx.beginPath(); ctx.moveTo(rulerSize - 0.5, 0); ctx.lineTo(rulerSize - 0.5, height); ctx.stroke(); }; // 响应缩放变化 watch(scale, () => { nextTick(drawRulers); }); // 响应偏移变化 watch(offset, () => { nextTick(drawRulers); }, { deep: true }); </script> <style scoped> .ruler-container { position: relative; width: 100%; height: 100vh; overflow: hidden; } .ruler { position: absolute; background-color: #f5f5f5; z-index: 10; } .ruler.horizontal { top: 0; left: 20px; /* 垂直标尺宽度 */ width: calc(100% - 20px); height: 20px; } .ruler.vertical { top: 20px; /* 水平标尺高度 */ left: 0; width: 20px; height: calc(100% - 20px); } .canvas-area { position: absolute; top: 20px; left: 20px; width: calc(100% - 20px); height: calc(100% - 20px); border: 1px solid #ddd; } </style> ``` ### 关键实现原理 1. **标尺坐标系转换**: - 实际坐标到屏幕坐标的转换公式: $$ \text{screenX} = (\text{actualX} \times \text{scale}) + \text{offsetX} $$ - 反向转换公式: $$ \text{actualX} = \frac{\text{screenX} - \text{offsetX}}{\text{scale}} $$ 2. **动态刻度计算**: ```javascript // 根据缩放比例调整刻度密度 const baseStep = 50; const scaledStep = baseStep * scale.value; // 计算可见区域的起始位置 const startPos = Math.floor(-offset.value.x / scaledStep) * scaledStep; ``` 3. **性能优化技巧**: - 只绘制可见区域内的刻度 - 使用 `nextTick` 确保 DOM 更新后重绘 - 通过 `Math.floor` 和 `Math.round` 优化计算 4. **垂直标尺文本处理**: ```javascript // 旋转文本绘制方向 ctx.save(); ctx.translate(8, pixelPos + 15); ctx.rotate(-Math.PI / 2); ctx.fillText(actualPos, 0, 0); ctx.restore(); ``` ### 与画布联动实现 要使标尺与主画布联动(缩放/拖拽时同步更新): ```javascript // 缩放处理函数 const handleZoom = (delta, mouseX, mouseY) => { const newScale = Math.max(0.1, Math.min(3, scale.value + delta)); // 计算偏移量保持缩放中心 const ratio = (newScale - scale.value) / scale.value; offset.value.x -= (mouseX - offset.x) * ratio; offset.value.y -= (mouseY - offset.y) * ratio; scale.value = newScale; }; // 拖拽处理函数 const handleDrag = (deltaX, deltaY) => { offset.value = { x: offset.value.x + deltaX, y: offset.value.y + deltaY }; }; ``` ### 高级优化建议 1. **标尺零点指示器**: ```javascript // 在零点位置添加特殊标记 ctx.fillStyle = 'red'; ctx.fillRect(0, 0, 3, rulerSize); ``` 2. **视口边界约束**: ```javascript // 防止过度拖拽 const maxOffsetX = (mainCanvas.value.width * scale.value) - mainCanvas.value.width; offset.value.x = Math.max(-maxOffsetX, Math.min(0, offset.value.x)); ``` 3. **使用Fabric.js增强功能**: ```javascript import { fabric } from 'fabric'; // 创建Fabric画布 const initFabric = () => { const canvas = new fabric.Canvas(mainCanvas.value); // 添加对象... }; ``` 使用 Fabric.js 可以简化复杂图形操作[^1]。 4. **标尺缓存优化**: ```javascript // 创建离屏Canvas缓存静态部分 const offscreenCanvas = document.createElement('canvas'); // 绘制静态内容... // 绘制时复用缓存 ctx.drawImage(offscreenCanvas, 0, 0); ``` ### 数学公式说明 1. **坐标转换公式**: - 屏幕坐标到实际坐标: $$ \text{actualX} = \frac{\text{screenX} - \text{offsetX}}{\text{scale}} $$ - 实际坐标到屏幕坐标: $$ \text{screenX} = (\text{actualX} \times \text{scale}) + \text{offsetX} $$ 2. **缩放中心保持公式**: $$ \Delta x = (\text{mouseX} - \text{offsetX}) \times \left(\frac{\text{newScale}}{\text{oldScale}} - 1\right) $$ 3. **刻度密度自适应公式**: $$ \text{step} = \text{baseStep} \times \text{scale} \times 2^{\lfloor \log_2(\frac{1}{\text{scale}}) \rfloor} $$ 此实现创建了精确的标尺系统,支持动态缩放和拖拽,并通过Canvas API高效渲染刻度。实际应用中可结合 Fabric.js 等库增强功能[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值