Canvas以鼠标为中心缩放原理

333 阅读6分钟

示例

20250128131807_rec_.gif

简介

以鼠标为中心缩放,根据使用场景不同,分为4种。

  1. 累加式 先缩放后平移
  2. 累加式 先平移后缩放
  3. 非累加式,先缩放后平移
  4. 非累加式,先平移后缩放

先缩放在平移和先平移再缩放的区别是:重点在平移上,因为ctx.translate的单位是当前缩放倍数的单位。结尾再复述

这里说的累加式,指的是,每次缩放是在上一次的基础上进行缩放,非累加式每次都是在比例为1的基础上进行缩放。

下面将渐进式讲解

非累加式,使用的是ctx.save(),ctx.restore()

  ctx.clearRect(0, 0, canvas.width, canvas.height);

   
      ctx.save();
      ctx.translate(offset.x, offset.y);// 先平移后缩放
      ctx.scale(scale, scale);  
      ...
      ...
      // 绘制图像
      ctx.restore();
      

而累加式是没有使用ctx.save(),即是缩放倍数累计的。

前置知识

ctx.translate: 这个api是移动canvas 中心坐标轴,单位为是当前缩放倍数下的1个容量因子(这个很关键,后面会用到)。

比如缩放倍数为1,ctx.translate(10,10),那么移动为10个容量因子;

缩放倍数为0.5,ctx.translate(10,10),那么移动还是为10个容量因子。

并且是ctx.translate是累加的,把中心坐标轴从上一个位置,移动一段距离。(上面说的非累加式,是因为使用了,ctx.save(),ctx.restore())

ctx.scale(x,y): x水平方向上缩放倍数,y垂直方向上缩放倍数。大于1是放大,小于1是缩小。与ctx.translate一样都是累加的。比如当前倍数是0.5,ctx.scale(1.1,1.1),实际倍数是1.1*0.5,

const {a,d,e,f}=ctx.getTransform() :a:水平缩放因子 d:垂直缩放因子 e:水平平移 f:垂直平移

这里的结果的都是最终结果。比如基础倍数是1,经过2次缩放(1.1倍),a的值为1*1.1*1.1=1.21

这里重点说一下 e和f的值都是1倍下的值,可以说是px,使用式需要转换一下移动了多少个容量因子,e/a

 ctx.scale(0.5,0.5); // 缩放0.5
        ctx.translate(-100,0) //  100个单位
        ctx.strokeStyle = "red";
        ctx.beginPath();
        ctx.rect(0,0,200,200);
        ctx.stroke();
        const {a,d,e,f}=ctx.getTransform() 
        console.log(a,d,e,f,'e,f'); 

打印结果为:0.5 0.5 -50 0 'e,f'

ctx.translate是移动了100个单位,那么想知道最终移动多少个单位:e/a

原理

累加式

红色为最初状态,倍数为s1,没有进行translate。没有进行translate进行计算是最简单的。 蓝色框为缩放后状态,

image.png 上图为实现以鼠标中心缩放,那么L1内的容量因子与L2+L3是相等的,这里的L1长度是鼠标到canvas的距离,就是event.offsetX。

计算L1内总共多少个容量因子

const L1Capacity=L1/s1 // 试想缩放倍数为1,那么L1=100,可以放下100个容量因子,倍数为0.5,可以放入200个容量因子

我们最终需要求的是translate(x,y),就是需要移动多少个容量因子,就是L2。那么先计算出L3在缩放后能容纳多少个因子。总的容量因子减去就可以得出L2需要容量多少个因子。这就是我们要计算的答案,y轴同理 计算L2内总共需要容纳多少容量因子

const s2=(step+1)*s1 //step为0.1或者-0.9  每次缩放为1.1 或者0.9
const L3Capacity=L1/s2 // 在缩放后的状态
const L2Capacity=L1Capacity-L3Capacity 

先缩放再平移

ctx.scale(multiple,multiple) // 这里需要填缩放倍数,不是最终结果(s2),multiple为1.1 或者0.9
ctx.translate(-l2Capacity,-l2Capacity) 

合并计算

   canvas.addEventListener('wheel', function(event) {
        let {offsetY,offsetX,wheelDeltaY}=event
          event.preventDefault();
          let current = 1; 
          const step=0.1
          if(wheelDeltaY>0){
            current+=step
            // 放大
            if(a>=2 )return 
          }else{
            if(a<=0.3 )return 
            // 缩小
            current-=step
          }
          const {a,d,e,f}=ctx.getTransform()  //之前的最终缩放倍数
          ctx.scale(current,current)
        //offsetX/a对应L1Capacity   offsetX/(current*a)对应L3Capacity
        let xp=offsetX/a - offsetX/(current*a) 
        let yp=offsetY/a - offsetY/(current*a)
         ctx.translate(-xp,-yp)
       }, { passive: false });

大概过程 image.png

先平移再缩放

这里就是我踩坑的位置。因为一开始就使用的是累加式的,后面不方便改成非累加式。就硬着头皮计算,捣鼓了半天。

若果先平移那么,在倍数为s1的缩放倍数下,需要移动多少个缩放因子(腾出多少空间),然后s2的时候,可以容纳需要容纳的容量因子

有一点启发就是,L2是长度是固定的,那么他的长度是多少呢,类比offsetX。L2px=L2CapacityS2*s2=L2CapacityS1*s1,只有一个未知数,可求得L2CapacityS1,那么就是需要在s1的倍数下平移L2CapacityS1,在s2的倍数下才能容量L2CapacityS2容量因子

const s2=(step+1)*s1 //step为0.1或者-0.9  每次缩放为1.1 或者0.9
const L3Capacity=L1/s2 // 在缩放后的状态
const L2Capacity=L1Capacity-L3Capacity 
const L2CapacityS1=L2Capacity*s2/s1 = L2Capacity*(step+1) 

合并计算

 canvas.addEventListener('wheel', function(event) {
        let {offsetY,offsetX,wheelDeltaY}=event
          event.preventDefault();
          let current = 1; 
          const step=0.1
          if(wheelDeltaY>0){
            current+=step
            // 放大
            if(a>=2 )return 
          }else{
            if(a<=0.3 )return 
            // 缩小
            current-=step
          }
          const {a,d,e,f}=ctx.getTransform()  //之前的最终缩放倍数
            
          //(offsetX/a - offsetX/(current*a)) 为需要移动的容量因子 对应L2Capacity
           let xp=(offsetX/a - offsetX/(current*a))*current //对应 L2Capacity*(step+1) 
           let yp=(offsetY/a - offsetY/(current*a))*current
          // 先放大在 移动
          ctx.translate(-xp,-yp) 
          ctx.scale(current,current)
         
       }, { passive: false });

对应原本有translate

如果原本就是有translate的呢,就需要考虑e和f。原理还是一样的,鼠标点到2次边界的容量因子是一样多,只需要计算出需要移动的容量因子

现在需要求的就是l6需要容量拿多少容量因子 image.png

计算过程 L6=L3+L2-L4-L5

L3 L4 已知

L2 L5 通过e和f转换求得 L3:容量因子:L3/s1 L4:可容量因子L4/s2 L2:容量因子:e/s1 //上面所说的e是px单位,类比offsetX L5:可容纳因子 e/s2

可求得L6

       let current = 1;  
        const {a,d,e,f}=ctx.getTransform() 
        // console.log(a,d,e,f,'ad');
          if(wheelDeltaY>0){
            current+=step
            // 放大
            if(a>=2 )return 
          }else{
            if(a<=0.3 )return 
            // 缩小
            current-=step
          }
        let xp=((offsetX-e)/a - ((offsetX-e)/(current*a)))
        let yp=((offsetY-f)/a - ((offsetY-f)/(current*a)))

先缩放再平移

ctx.scale(current,current)
ctx.translate(-xp,-yp)

先平移再缩放 同理

 let xp=((offsetX-e)/a - ((offsetX-e)/(current*a)))*current
 let yp=((offsetY-f)/a - ((offsetY-f)/(current*a)))*current
ctx.translate(-xp,-yp)
ctx.scale(current,current)

先平移和后平移的区别:先平移后缩放不同,先平移x,那么平移的是上一个倍数的x个容量因子;后平移是平移当前倍数的x个容量因子

对于使用查看演唱会选票

非累加式,大家可以参考【canvas】react+canvas实现无限画布、鼠标为中心缩放、标尺、移动画布

总结

  1. 鼠标点到canvas 边界的容量因子是相等的。
  2. 在吗写代码前还是要构思好,将有关联,有影响的部分考虑清楚