理解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 };
    }
}
相关推荐
恋猫de小郭1 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅9 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊9 小时前
jwt介绍
前端