本文已参与[新人创作礼]活动,一起开启掘金创作之路
Canvas封装类 实现拖拽、缩放图片
最近公司有个需求,要实现图片可以由用户自由拖拽、缩放,同时图片还可以切换, canvas背景也可以切换,最后上传图片的坐标及缩放比例到后端
参考一些网上文章 封装了一个符合自己需求的canvas类 这边只做了PC端的监听,移动端的touch逻辑可以参考文章底部链接~
Canvas很多API可以学习参考:Canvas 跟随鼠标/手指 - Canvas 基础教程 - 简单教程,简单编程 (twle.cn)
事件监听
// pc端事件监听
this.canvasRef.addEventListener('mousedown', this.startMouse.bind(this));
this.canvasRef.addEventListener('mousemove', this.moveMouse.bind(this));
this.canvasRef.addEventListener('mouseup', this.endMouse.bind(this));
// this.canvasRef.addEventListener('mousewheel', this.mouseWheel.bind(this)); // 监听滚轮
this.canvasRef.addEventListener('wheel', this.mouseWheel.bind(this)); // 监听滚轮
this.canvasRef.addEventListener('mouseover', this.mouseOver.bind(this)); // 鼠标移过 显示提示
this.canvasRef.addEventListener('mouseout', this.mouseOut.bind(this));
图片加载
使用promise预加载,crossOrigin解决跨域。
/**
* 图片加载
* @param src
* @returns
*/
private loadImage(src: string) {
return new Promise((resolve, reject) => {
this.img = new Image();
this.img.crossOrigin = 'Anonymous';
this.img.onload = function () {
resolve('');
};
this.img.onerror = function (error) {
MessagePlugin.error('人物加载失败, 请刷新');
reject(error);
};
this.img.src = src;
});
}
移动位置
计算鼠标或者触摸相对于canvas容器的位置
/**
* 处理鼠标位置
* @param startX
* @param startY
* @returns
*/
private windowToCanvas(startX: number, startY: number) {
const { left, top, width, height } = this.canvasRef.getBoundingClientRect();
return {
x: startX - left - (width - this.canvasRef.width) / 2,
y: startY - top - (height - this.canvasRef.height) / 2,
};
}
图片移动
根据当前移动位置减去上一次移动位置。
/**
* 拖拽移动
* @param e
* @returns
*/
private moveMouse(e: MouseEvent) {
if (!this.isMove) return false;
const { pageX, pageY } = e;
this.movePos = this.windowToCanvas(pageX, pageY);
const x = this.movePos.x - this.startPos.x;
const y = this.movePos.y - this.startPos.y;
this.imgX += x;
this.imgY += y;
this.startPos = { ...this.movePos }; // 更新最新位置
this.drawImage();
}
绘制图片
绘制前,首先要清除上一次绘制
/**
* 图片绘制
*/
private drawImage() {
// 清楚上一帧绘制
this.ctx.clearRect(0, 0, this.canvasRef.width, this.canvasRef.height);
if (this.showTipStatus) this.showTip();
// 绘制图片
this.ctx.drawImage(
this.img,
this.imgX,
this.imgY,
this.img.width * this.imgScale,
this.img.height * this.imgScale,
);
}
缩放
PC端监听滚动缩小还是放大是根据滚动事件deltaY的值来判断,大于0为放大,否则为缩小;
/**
* 监听滚轮
* @param e
*/
private mouseWheel(e: WheelEvent) {
const { clientX, clientY, deltaY } = e;
const pos = this.windowToCanvas(clientX, clientY);
// 计算图片的位置
const newPos = {
x: Number(((pos.x - this.imgX) / this.imgScale).toFixed(2)),
y: Number(((pos.y - this.imgY) / this.imgScale).toFixed(2)),
};
// 判断是放大还是缩小
if (deltaY > 0) { // 放大
this.imgScale += 0.02;
if (this.imgScale >= this.MAX_SCALE) this.imgScale = this.MAX_SCALE;
} else { // 缩小
this.imgScale -= 0.02;
if (this.imgScale <= this.MINIMUM_SCALE) this.imgScale = this.MINIMUM_SCALE;
}
// 计算图片的位置, 根据当前缩放比例,计算新的位置
this.imgX = (1 - this.imgScale) * newPos.x + (pos.x - newPos.x);
this.imgY = (1 - this.imgScale) * newPos.y + (pos.y - newPos.y);
this.drawImage();
}
提示用户
这边只是显示一些文字去提示用户操作,更复杂的提示可以自己多加考虑
private showTip() {
this.ctx.font = '16px Microsoft YaHei';
this.ctx.fillText('拖拽移动, 鼠标滚轮缩放', 50, 50);
}
使用
根据不同屏幕大小可以传入不同图片缩放比例; 可以通过changeCanvasBg去改变Canvas的背景图片
let canvasClass: MapCanvas;
let canvas: HTMLCanvasElement;
const getImgScale = function (): ScaleSet {
const screenWidth = window.screen.width;
const scaleSet: ScaleSet = {
scale: 0.5,
minScale: 0.2,
maxScale: 0.7,
};
if (screenWidth < 1400) {
scaleSet.scale = 0.3;
scaleSet.minScale = 0.1;
scaleSet.maxScale = 0.4;
} else if (screenWidth > 1400 && screenWidth <= 1600) {
scaleSet.scale = 0.4;
scaleSet.minScale = 0.15;
scaleSet.maxScale = 0.5;
};
return scaleSet;
};
const changeCanvasBg = function (src: string) {
canvas.style.backgroundImage = `url(${src})`;
};
const initCanvas = function () {
canvas = document.querySelector('#vCanvas') as HTMLCanvasElement;
canvas.style.backgroundImage = `url(${canvasBg.get(store.state.bgId === -1 ? 1 : store.state.bgId)})`;
canvas.style.backgroundPosition = 'center';
canvas.style.backgroundSize = 'cover';
const scaleSet = getImgScale();
canvasClass = new MapCanvas(canvas, canvasRole.get(store.state.roleId === -1 ? 4 : store.state.roleId) || '', scaleSet.scale, scaleSet.minScale, scaleSet.maxScale);
};
onMounted(() => {
initCanvas();
});
所有代码
import { MessagePlugin } from 'tdesign-vue-next';
interface IPos {
x: number,
y: number
}
class MapCanvas {
private canvasRef: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D;
private img!: HTMLImageElement;
private imgSrc: string; // 图片url
private startPos: IPos = { x: 0, y: 0 }; // 开始坐标
private movePos!: IPos; // 存储移动坐标位置
private imgX = 0; // 图片初始化X轴位置
private imgY = 0; // 图片初始化Y轴位置
private isMove = false; // 是否移动
private imgScale: number; // 图片缩放比例
private MINIMUM_SCALE: number; // 最小缩放
private MAX_SCALE: number; // 最大缩放
private showTipStatus = true; // 是否展示Tip
constructor(canvas: HTMLCanvasElement, imgSrc: string, imgScale = 0.3, minScale = 0.1, maxScale = 0.7) {
this.imgSrc = imgSrc;
this.imgScale = imgScale;
this.MINIMUM_SCALE = minScale;
this.MAX_SCALE = maxScale;
this.canvasRef = canvas;
const { width, height } = this.canvasRef.getBoundingClientRect();
this.canvasRef.width = width;
this.canvasRef.height = height;
this.ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
this.initCanvas();
}
/**
* 初始化Canvas
*/
async initCanvas() {
await this.loadImage(this.imgSrc);
// 设置图片在Canvas居中
this.imgX = (this.canvasRef.width - this.img.width * this.imgScale) / 2;
this.imgY = (this.canvasRef.height - this.img.height * this.imgScale) / 2;
this.drawImage();
this.showTip();
// pc端事件监听
this.canvasRef.addEventListener('mousedown', this.startMouse.bind(this));
this.canvasRef.addEventListener('mousemove', this.moveMouse.bind(this));
this.canvasRef.addEventListener('mouseup', this.endMouse.bind(this));
// this.canvasRef.addEventListener('mousewheel', this.mouseWheel.bind(this)); // 监听滚轮
this.canvasRef.addEventListener('wheel', this.mouseWheel.bind(this)); // 监听滚轮
this.canvasRef.addEventListener('mouseover', this.mouseOver.bind(this)); // 鼠标移过 显示提示
this.canvasRef.addEventListener('mouseout', this.mouseOut.bind(this));
}
/**
* 改变Canvas渲染的图片
*/
public async changeImage(src: string) {
this.imgSrc = src;
const redraw = this.drawImage.bind(this);
this.img.src = this.imgSrc;
this.img.onload = redraw;
this.img.onerror = function () {
MessagePlugin.error('人物加载失败, 请刷新');
};
}
/**
* 移除监听器
*/
public removeEventListener() {
this.canvasRef.removeEventListener('mousedown', this.startMouse.bind(this));
this.canvasRef.removeEventListener('mousemove', this.moveMouse.bind(this));
this.canvasRef.removeEventListener('mouseup', this.endMouse.bind(this));
// this.canvasRef.removeEventListener('mousewheel', this.mouseWheel.bind(this)); // 监听滚轮
this.canvasRef.removeEventListener('wheel', this.mouseWheel.bind(this)); // 监听滚轮
this.canvasRef.removeEventListener('mouseover', this.mouseOver.bind(this));
this.canvasRef.removeEventListener('mouseout', this.mouseOut.bind(this));
}
/**
* 获取图片比例
* @returns
*/
public getImgScale() {
return this.imgScale;
}
/**
* 获取距离X比例
* @returns
*/
public getXScale() {
return (this.imgX / this.canvasRef.width).toFixed(3);
}
/**
* 获取距离Y比例
* @return
*/
public getYScale() {
return (this.imgY / this.canvasRef.height).toFixed(3);
}
/**
* 图片加载
* @param src
* @returns
*/
private loadImage(src: string) {
return new Promise((resolve, reject) => {
this.img = new Image();
this.img.crossOrigin = 'Anonymous';
this.img.onload = function () {
resolve('');
};
this.img.onerror = function (error) {
MessagePlugin.error('人物加载失败, 请刷新');
reject(error);
};
this.img.src = src;
});
}
/**
* 图片绘制
*/
private drawImage() {
// 清楚上一帧绘制
this.ctx.clearRect(0, 0, this.canvasRef.width, this.canvasRef.height);
if (this.showTipStatus) this.showTip();
// 绘制图片
this.ctx.drawImage(
this.img,
this.imgX,
this.imgY,
this.img.width * this.imgScale,
this.img.height * this.imgScale,
);
}
private showTip() {
this.ctx.font = '16px Microsoft YaHei';
this.ctx.fillText('拖拽移动, 鼠标滚轮缩放', 50, 50);
}
/**
* 鼠标移入,关闭提示
*/
private mouseOver() {
this.showTipStatus = false;
this.drawImage();
}
/**
* 鼠标移出,显示提示
*/
private mouseOut() {
this.showTipStatus = true;
this.drawImage();
}
/**
* 开始拖拽
* @param e
*/
private startMouse(e: MouseEvent) {
const { pageX, pageY } = e;
this.isMove = true;
this.startPos = this.windowToCanvas(pageX, pageY);
this.canvasRef.style.cursor = 'pointer';
}
/**
* 拖拽移动
* @param e
* @returns
*/
private moveMouse(e: MouseEvent) {
if (!this.isMove) return false;
const { pageX, pageY } = e;
this.movePos = this.windowToCanvas(pageX, pageY);
const x = this.movePos.x - this.startPos.x;
const y = this.movePos.y - this.startPos.y;
this.imgX += x;
this.imgY += y;
this.startPos = { ...this.movePos }; // 更新最新位置
this.drawImage();
}
/**
* 拖拽结束
*/
private endMouse() {
this.isMove = false;
this.canvasRef.style.cursor = 'default';
}
/**
* 监听滚轮
* @param e
*/
private mouseWheel(e: WheelEvent) {
const { clientX, clientY, deltaY } = e;
const pos = this.windowToCanvas(clientX, clientY);
// 计算图片的位置
const newPos = {
x: Number(((pos.x - this.imgX) / this.imgScale).toFixed(2)),
y: Number(((pos.y - this.imgY) / this.imgScale).toFixed(2)),
};
// 判断是放大还是缩小
if (deltaY > 0) { // 放大
this.imgScale += 0.02;
if (this.imgScale >= this.MAX_SCALE) this.imgScale = this.MAX_SCALE;
} else { // 缩小
this.imgScale -= 0.02;
if (this.imgScale <= this.MINIMUM_SCALE) this.imgScale = this.MINIMUM_SCALE;
}
// 计算图片的位置, 根据当前缩放比例,计算新的位置
this.imgX = (1 - this.imgScale) * newPos.x + (pos.x - newPos.x);
this.imgY = (1 - this.imgScale) * newPos.y + (pos.y - newPos.y);
this.drawImage();
}
/**
* 处理鼠标位置
* @param startX
* @param startY
* @returns
*/
private windowToCanvas(startX: number, startY: number) {
const { left, top, width, height } = this.canvasRef.getBoundingClientRect();
return {
x: startX - left - (width - this.canvasRef.width) / 2,
y: startY - top - (height - this.canvasRef.height) / 2,
};
}
}
export default MapCanvas;