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

引言

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

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

绘制图像

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

javascript 复制代码
// 创建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参数如下:

平移与缩放

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

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

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

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

投影和反投影

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

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

js 复制代码
    // 投影
    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 的位置,并根据滚轮的方向和位置进行缩放操作。

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

js 复制代码
    // 处理鼠标滚轮事件
    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画布的坐标与鼠标坐标的关系,深入理解坐标映射关系及数学计算方法。

完整代码

js 复制代码
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 };
    }
}
相关推荐
栈老师不回家10 分钟前
Vue 计算属性和监听器
前端·javascript·vue.js
前端啊龙16 分钟前
用vue3封装丶高仿element-plus里面的日期联级选择器,日期选择器
前端·javascript·vue.js
一颗松鼠20 分钟前
JavaScript 闭包是什么?简单到看完就理解!
开发语言·前端·javascript·ecmascript
小远yyds40 分钟前
前端Web用户 token 持久化
开发语言·前端·javascript·vue.js
阿伟来咯~1 小时前
记录学习react的一些内容
javascript·学习·react.js
吕彬-前端2 小时前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱2 小时前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai2 小时前
uniapp
前端·javascript·vue.js·uni-app
也无晴也无风雨2 小时前
在JS中, 0 == [0] 吗
开发语言·javascript
bysking3 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js