理解Canvas绘制技术:缩放、平移和投影

引言

大家好,我是 simple ,我的理想是利用科技手段来解决生活中遇到的各种问题

恰巧前几天功能业务中也涉及到了这块,总结这篇文章,是因为代码中涉及到了坐标的转换和变换,包括缩放、平移等操作,这需要一定的数学基础和对Canvas坐标系统的理解。

绘制图像

在Canvas中绘制图像是一种常见的操作,可以通过创建一个Image对象并将其加载到Canvas中来实现。

// 创建Canvas元素和上下文
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

// 创建Image对象并加载图像
const image = new Image();
image.src = 'image.jpg';

// 图像加载完成后绘制到Canvas上
image.onload = function() {
    // 绘制图像到Canvas指定位置
    ctx.drawImage(image, 0, 0);

    // 可选:绘制图像并调整大小
    // ctx.drawImage(image, 0, 0, 100, 100); // 在(0,0)位置绘制,宽高为100x100像素
};

drawImage参数如下:

理解Canvas绘制技术:缩放、平移和投影

平移与缩放

ctx.drawImage(image, 10, 20, 30, 40);
  • image:要绘制的图像,可以是<img><canvas><video>元素。
  • 10:绘制的目标位置的x坐标,即图像在Canvas中的左上角的x坐标。
  • 20:绘制的目标位置的y坐标,即图像在Canvas中的左上角的y坐标。
  • 30:绘制的目标宽度,即图像在Canvas中的宽度。
  • 40:绘制的目标高度,即图像在Canvas中的高度。

了解上述参数后,那我们只需要获取鼠标按下事件的坐标,即可将图片进行拖拽,然后再于鼠标移动事件中不断更新画布移动照片,在鼠标松开事件中停止更新画布即可。鼠标滚轮的时候,我们只要调整新的参数即可对图像进行缩放。

到此正要大功告成之际,发现有一点需要优化的地方:鼠标滚轮缩放的时候,图像并不能根据鼠标位置进行缩放,而是会越偏越远。

这时我们就需要引进两个新的概念。

投影和反投影

投影是将画布上的坐标映射到实际图像上的坐标,而反投影则是将实际图像上的坐标映射到画布上的坐标。

假设我将一张 100×100 的照片放在一个 100×100 的画布上,并将照片放大了2倍,但画布大小不变。那么在画布的最右上角时,在画布的坐标为 (100, 0),而在照片中的坐标为 (50, 0)。

    // 投影
    project(v) {
        const x = (v.x - this.centerPos.x) * this.scale;
        const y = (v.y - this.centerPos.y) * this.scale;
        return { x, y };
    }

    // 反投影
    unproject(p) {
        const x = (p.x / this.scale) + this.centerPos.x;
        const y = (p.y / this.scale) + this.centerPos.y;
        return { x, y };
    }

这样的处理可以有效地在画布和实际图像之间进行坐标的转换,使得操作更加方便和直观。

滚轮缩放和鼠标位置

实现滚轮缩放和鼠标位置的基本思路是,监听鼠标滚轮事件,在事件处理函数中获取鼠标相对于 Canvas 的位置,并根据滚轮的方向和位置进行缩放操作。

将放大后的偏移量减去原始的偏移量,得到新的中心点相对于画布左上角的位置。这样就保证了画布在鼠标所在位置放大或缩小后,鼠标所在位置不变,从而形成了以鼠标为中心的缩放效果。

    // 处理鼠标滚轮事件
    handleMouseWheel(e) {
        // t1表示鼠标位置在图像坐标系中的投影。
        const t1 = this.unproject({ x: e.offsetX, y: e.offsetY });
        if (e.deltaY < 0) {
            this.scale *= 1.1;
            this.centerPos.x = t1.x - (t1.x - this.centerPos.x) / 1.1;
            this.centerPos.y = t1.y - (t1.y - this.centerPos.y) / 1.1;
        } else {
            this.scale /= 1.1;
            // this.centerPos是图像的中心点坐标
            // 实现以鼠标位置为中心进行缩放。
            this.centerPos.x = t1.x - (t1.x - this.centerPos.x) * 1.1;
            this.centerPos.y = t1.y - (t1.y - this.centerPos.y) * 1.1;
        }
        this.drawImage();
    }

总结

时刻清晰canvas画布的坐标与鼠标坐标的关系,深入理解坐标映射关系及数学计算方法。

完整代码

export default class PictureRender {
constructor(canvas, options = {}) {
this.options = options;
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.centerPos = { x: 0, y: 0 };
this.scale = 1;
this.itscale = 1;
this.image = null;
this.isDrag = true;
// 为画布添加事件监听器
this.canvas.addEventListener('mousedown', this.handleMouseDown.bind(this));
this.canvas.addEventListener('mouseup', this.handleMouseUp.bind(this));
this.canvas.addEventListener('mousemove', this.handleMouseMove.bind(this));
this.canvas.addEventListener('wheel', this.handleMouseWheel.bind(this));
this.startButtonDownPos = {};
this.startButtonDownWP = {};
this.scaleWidth = 0;
this.scaleHeight = 0;
}
// 渲染图像
render(src) {
const image = new Image();
image.src = src;
this.image = image;
image.onload = () => {
const { width = 600 } = this.options;
this.itscale = width / this.image.width;
this.scaleWidth = this.image.width * this.itscale;
this.scaleHeight = this.image.height * this.itscale;
this.canvas.width = this.scaleWidth;
this.canvas.height = this.scaleHeight;
this.drawImage();
};
}
// 绘制图像
drawImage() {
this.canvas.width = this.scaleWidth;
this.canvas.height = this.scaleHeight;
const p1 = this.project({ x: 0, y: 0 });
const p2 = this.project({ x: this.image.width, y: this.image.height });
this.ctx.drawImage(this.image, p1.x, p1.y, p2.x - p1.x, p2.y - p1.y);
}
// 重置画布
reset() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); // 清空画布
this.ctx.setTransform(1, 0, 0, 1, 0, 0); // 重置变换矩阵
this.scale = 1;
this.startButtonDownPos = {};
this.centerPos = { x: 0, y: 0 };
this.drawImage();
}
// 处理鼠标按下事件
handleMouseDown(e) {
// 判断当前是点击事件还是拖动事件
this.isDrag = false;
this.startButtonDownPos = { x: e.clientX, y: e.clientY };
this.startButtonDownWP = { x: this.centerPos.x, y: this.centerPos.y };
this.isMouseDown = true;
}
// 处理鼠标抬起事件
handleMouseUp() {
this.isMouseDown = false;
}
// 处理鼠标移动事件
handleMouseMove(e) {
if (this.isMouseDown) {
const t1 = this.unproject({ x: e.clientX, y: e.clientY });
const t2 = this.unproject(this.startButtonDownPos);
const dx = t2.x - t1.x;
const dy = t2.y - t1.y;
this.centerPos.x = this.startButtonDownWP.x + dx;
this.centerPos.y = this.startButtonDownWP.y + dy;
this.drawImage();
}
}
// 处理鼠标滚轮事件
handleMouseWheel(e) {
const t1 = this.unproject({ x: e.offsetX, y: e.offsetY });
if (e.deltaY < 0) {
this.scale *= 1.1;
this.centerPos.x = t1.x - (t1.x - this.centerPos.x) / 1.1;
this.centerPos.y = t1.y - (t1.y - this.centerPos.y) / 1.1;
} else {
this.scale /= 1.1;
this.centerPos.x = t1.x - (t1.x - this.centerPos.x) * 1.1;
this.centerPos.y = t1.y - (t1.y - this.centerPos.y) * 1.1;
}
this.drawImage();
}
// 投影
project(v) {
const x = (v.x - this.centerPos.x) * this.scale * this.itscale;
const y = (v.y - this.centerPos.y) * this.scale * this.itscale;
return { x, y };
}
// 反投影
unproject(p) {
const x = (p.x) / (this.scale * this.itscale) + this.centerPos.x;
const y = (p.y) / (this.scale * this.itscale) + this.centerPos.y;
return { x, y };
}
}

原文链接:https://juejin.cn/post/7340076087248977954 作者:simple_lau

(0)
上一篇 2024年2月28日 上午10:58
下一篇 2024年2月28日 上午11:08

相关推荐

发表回复

登录后才能评论