H5 Canvas 中操作移动,缩放和旋转

252 阅读3分钟

在一些实际的应用场景中,我们需要对照片添加一些贴纸的功能:

  • 拖动贴纸到图片上
  • 针对贴纸进行移动,缩放和旋转的操作

这些功能我们都用 canvas 来实现。

下面是简单的代码结构👇

<div id="draggable" draggable="true">Drag me</div>
<canvas id="canvas" width="600" height="300"></canvas>
#draggable {
  width: 100px;
  height: 100px;
  background-color: red;
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  font-size: 24px;
  cursor: move;
}

#canvas {
  border: 1px solid black;
}

初始画面

这里,为了方便演示,我们使用一个 div 元素来模拟贴纸 img

Canvas 绘制图片

canvas 绘制图片,我们可以使用 drawImage 方法来实现 👇

const image = new Image(); // 创建一个图片对象
img.src = "path/to/your/image.png";
// 图片加载完后操作
img.onload = () => {
  // 将贴纸绘制在 canvas 上
  ctx.drawImage(img, 0, 0, 300, 300);
}
// 图片加载错误
img.onerror = () => {
  console.error("load image error");
}

拖动贴纸到 Canvas 上

我们拖动贴纸到指定的 canvas 上,我们这里只演示鼠标拖动的功能,如果需要添加触摸的功能,可以参考鼠标功能自行添加。

const draggable = document.getElementById("draggable");
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");

draggable.addEventListener("dragstart", (e) => {
  // 需要设置一些数据才能触发拖放事件
  e.dataTransfer.setData('text/plain', null); 
});

canvas.addEventListener("dragover", (e) => {
  // 防止默认行为,使得drop事件可以被触发
  e.preventDefault(); 
});

canvas.addEventListener("drop", (e) => {
  e.preventDefault();
  const rect = canvas.getBoundingClientRect();
  const x = e.clientX - rect.left;
  const y = e.clientY - rect.top;
  
  // 在 canvas 上绘制红色的矩形
  ctx.fillStyle = "red";
  ctx.fillRect(x - 100 / 2, y - 100 / 2, 100, 100);
});

相关的动图如下👇

simple-drag.gif

Canvas 上操作贴纸

我们上面已经将贴纸拖动到 canvas 上了。那么,接下来,我们操作贴纸的功能,这里实现 移动、缩放和旋转 功能。

// 初始化参数
let isDragging = false;
let startX;
let startY;
let rect = {
  x: 100,
  y: 100,
  width: 100,
  height: 100,
  angle: 0, // 旋转的角度
  scale: 1, // 缩放大小
};

我们在移动的过程中,不断地绘制该红色的矩形:

function drawRect() {
  // 绘制前清空画布
  ctx.clearReact(0, 0, canvas.width, canvas.height);
  ctx.save();
  ctx.translate(rect.x + rect.width / 2, rect.y + rect.height / 2); // 移动
  ctx.rotate(rect.angle); // 旋转
  ctx.scale(rect.scale, rect.scale); // 缩放
  ctx.fillStyle = "red";
  ctx.fillRect(-rect.width / 2, -rect.height / 2, rect.width, rect.height);
  ctx.restore();
}

我们监听鼠标的落下,移动和抬起,来实现贴纸的移动距离和方向:

// 获取鼠标的当前位置
function getMousePos(canvas, event) {
  const rect = canvas.getBoundingClientRect();
  return {
    x: event.clientX - rect.left,
    y: event.clientY - rect.top,
  };
}

// 判断点是否在红色的矩形内
function isInsideRect(x, y) {
  const halfWidth = rect.widht / 2;
  const halfHeight = rect.height / 2;
  const cos = Math.cos(rect.angle);
  const sin = Maht.sin(rect.angle);
  const dx = x - (rect.x + halfWidth);
  const dy = y - (rect.y + halfHeight);
  const rx = dx * cos + dy * sin;
  const ry = dy * cos - dx * sin;
  return Math.abs(rx) <= halfWidth * rect.scale && Math.abs(ry) <= halfHeight * rect.scale;
}

// 监听鼠标按下
canvas.addEventListener("mousedown", (e) => {
  const mousePos = (canvas, e);
  if (isInsideRect(mousePos.x, mousePos.y)) {
    isDragging = true;
    startX = mousePos.x - rect.x;
    startY = mousePos.y - rect.y;
  }
});

// 监听鼠标移动
canvas.addEventListener("mouseMove", (e) => {
  if (isDragging) {
    const mousePos = getMousePos(canvas, e);
    rect.x = mousePos.x - startX;
    rect.y = mousePos.y - startY;
    // 重新绘制
    drawRect();
  }
});

// 监听鼠标抬起
canvas.addEventListener("mouseup", () => {
  isDragging = false;
});

我们通过监听鼠标滚轮的滑动来实现贴纸的放大和缩小:

canvas.addEventListener("mousewheel", (e) => {
  e.preventDefault();
  const delta = e.deltaY > 0 ? 0.1 : -0.1;
  rect.scale += delta;
  if (rect.scale < 1) {
    rect.scale = 1;
  }
  if (rect.scale > 2) {
    rect.scale = 2;
  }
  // 重新绘制
  drawRect();
});

然后为了方便演示,我们通过监听鼠标右键来实现矩形的旋转功能:

canvas.addEventListener("contextmenu", (e) => {
  e.preventDefault();
  rect.angle += Math.PI / 12; // 每次点击,则旋转 15 度
  // 重新绘制
  drawRect();
})

整体的一个 Gif 图演示如下 👇

move-scale-rotate.gif

Canvas 合成图片

在绘制完成之后,我们需要将 canvas 上的状态图给保存下来。那么,我们可以使用 toDataURL 来实现。

function mixFn() {
  // 首先,我们将其绘制下来
  // other ...
  if (context) {
    context.fillStyle = "#fff";
    context.fillRect(0, 0, canvas.width, canvas.height);
    // 绘制相关的贴纸和模版图
    // other ...
  }
  const image = canvas.toDataURL("image/jpeg", 1.0).substring(23);
  // 置空
  context = null;
  canvas = null;
  // 将 image 保存在指定位置即可
}

参考