拖拽和缩放
2024年01月19日
一、认识
二、实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>可拖拽、缩放的 Canvas 画布</title>
<style>
.canvas-container {
width: 600px;
height: 600px;
}
canvas {
user-select: none;
touch-action: none;
border: 1px solid black;
}
</style>
</head>
<body>
<div class="canvas-container">
<canvas id="canvas"></canvas>
</div>
<script>
const canvasWrapper = document.querySelector('.canvas-container');
const wrapDomStyle = getComputedStyle(canvasWrapper);
const width = parseInt(wrapDomStyle.width, 10);
const height = parseInt(wrapDomStyle.height, 10);
const canvas = document.getElementById('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
const data = [];
let scale = 1;
const maxScale = 5;
const minScale = 0.5;
const scaleFactor = 0.1;
let isDragging = false;
let activeShape = null;
let downXY = {
x: 0,
y: 0
};
let lastShapeXY = {
x: 0,
y: 0
};
let translateXY = {
x: 0,
y: 0
};
let lastTranslateXY = {
x: 0,
y: 0
};
const downInvoker = e => {
downInvoker?.value?.(e);
};
const moveInvoker = e => {
moveInvoker?.value?.(e);
};
const upInvoker = e => {
upInvoker?.value?.(e);
};
const leaveInvoker = e => {
leaveInvoker?.value?.(e);
};
const wheelInvoker = e => {
wheelInvoker?.value?.(e);
};
function getRelativeOfElPosition(e, el) {
const normalizedE = e.type.startsWith('mouse') ? e : e.targetTouches[0];
const rect = el.getBoundingClientRect();
let x = normalizedE.pageX - rect.left - window.scrollX;
let y = normalizedE.pageY - rect.top - window.scrollY;
return [x, y];
}
function isInnerRect(x0, y0, width, height, x, y) {
return x0 <= x && y0 <= y && x0 + width >= x && y0 + height >= y;
}
function isInnerCircle(x0, y0, r, x, y) {
return Math.pow(x0 - x, 2) + Math.pow(y0 - y, 2) <= Math.pow(r, 2);
}
function isInnerPath(x0, y0, x1, y1, x, y, lineWidth) {
var a1pow = Math.pow(x0 - x, 2) + Math.pow(y0 - y, 2);
var a1 = Math.sqrt(a1pow, 2);
var a2pow = Math.pow(x1 - x, 2) + Math.pow(y1 - y, 2);
var a2 = Math.sqrt(a2pow, 2);
var a3pow = Math.pow(x1 - x0, 2) + Math.pow(y1 - y0, 2);
var a3 = Math.sqrt(a3pow, 2);
var r = lineWidth / 2;
var ab = (a1pow - a2pow + a3pow) / (2 * a3);
var h = Math.sqrt(a1pow - Math.pow(ab, 2), 2);
var ad = Math.sqrt(Math.pow(a3, 2) + Math.pow(r, 2));
return h <= r && a1 <= ad && a2 <= ad;
}
function onDown(e) {
isDragging = true;
let [x, y] = getRelativeOfElPosition(e, canvas);
downXY.x = x;
downXY.y = y;
lastTranslateXY.x = translateXY.x;
lastTranslateXY.y = translateXY.y;
x = (x - translateXY.x) / scale;
y = (y - translateXY.y) / scale;
activeShape = null;
data.forEach(item => {
switch (item.type) {
case 'rect':
isInnerRect(...item.data, x, y) && (activeShape = item);
break;
case 'circle':
isInnerCircle(...item.data, x, y) && (activeShape = item);
break;
case 'line':
const lineNumber = item.data.length / 2 - 1;
let flag = false;
for (let i = 0; i < lineNumber; i++) {
let index = i * 2;
flag = isInnerPath(
item.data[index],
item.data[index + 1],
item.data[index + 2],
item.data[index + 3],
x,
y,
item.lineWidth || 1
);
if (flag) {
activeShape = item;
break;
}
}
}
});
if (!activeShape) {
canvas.style.cursor = 'grabbing';
moveInvoker.value = onMoveCanvasBoard;
} else {
lastShapeXY.x = 0;
lastShapeXY.y = 0;
canvas.style.cursor = 'all-scroll';
moveInvoker.value = onMoveCanvasShape;
}
upInvoker.value = onUp;
leaveInvoker.value = onOut;
}
function onMoveCanvasBoard(e) {
if (!isDragging) {
return;
}
const maxMoveX = canvas.width / 1;
const maxMoveY = canvas.width / 1;
const [x, y] = getRelativeOfElPosition(e, canvas);
const moveX = lastTranslateXY.x + x - downXY.x;
const moveY = lastTranslateXY.y + y - downXY.y;
translateXY.x = Math.abs(moveX) > maxMoveX ? translateXY.x : moveX;
translateXY.y = Math.abs(moveY) > maxMoveY ? translateXY.y : moveY;
render();
}
function onMoveCanvasShape(e) {
const [x, y] = getRelativeOfElPosition(e, canvas);
let moveX = x - (lastShapeXY.x || downXY.x);
let moveY = y - (lastShapeXY.y || downXY.y);
moveX /= scale;
moveY /= scale;
switch (activeShape.type) {
case 'rect':
let xr = activeShape.data[0];
let yr = activeShape.data[1];
let width = activeShape.data[2];
let height = activeShape.data[3];
activeShape.data = [xr + moveX, yr + moveY, width, height];
break;
case 'circle':
let xc = activeShape.data[0];
let yc = activeShape.data[1];
let r = activeShape.data[2];
activeShape.data = [xc + moveX, yc + moveY, r];
break;
case 'line':
const item = activeShape;
const lineNumber = item.data.length / 2;
for (let i = 0; i < lineNumber; i++) {
let index = i * 2;
item.data[index] += moveX;
item.data[index + 1] += moveY;
}
}
lastShapeXY.x = x;
lastShapeXY.y = y;
render();
}
function onUp(e) {
isDragging = false;
canvas.style.cursor = '';
moveInvoker.value = null;
}
function onOut(e) {
isDragging = false;
moveInvoker.value = null;
upInvoker.value = null;
}
function onScale(e) {
e.preventDefault();
if (!e.wheelDelta) {
return;
}
let [x, y] = getRelativeOfElPosition(e, canvas);
x = x - translateXY.x;
y = y - translateXY.y;
const moveX = (x / scale) * scaleFactor;
const moveY = (y / scale) * scaleFactor;
if (e.wheelDelta > 0) {
translateXY.x -= scale >= maxScale ? 0 : moveX;
translateXY.y -= scale >= maxScale ? 0 : moveY;
scale += scaleFactor;
} else {
translateXY.x += scale <= minScale ? 0 : moveX;
translateXY.y += scale <= minScale ? 0 : moveY;
scale -= scaleFactor;
}
scale = Math.min(maxScale, Math.max(scale, minScale));
render();
}
function draw(item) {
ctx.setTransform(scale, 0, 0, scale, translateXY.x, translateXY.y);
switch (item.type) {
case 'rect':
ctx.beginPath();
ctx.fillStyle = item.fillStyle;
ctx.fillRect(...item.data);
break;
case 'circle':
ctx.beginPath();
ctx.fillStyle = item.fillStyle;
ctx.arc(...item.data, 0, 2 * Math.PI);
ctx.fill();
break;
case 'line':
const arr = item.data.concat();
ctx.beginPath();
ctx.moveTo(arr.shift(), arr.shift());
ctx.lineWidth = data.lineWidth || 1;
do {
ctx.lineTo(arr.shift(), arr.shift());
} while (arr.length);
ctx.stroke();
}
}
function render() {
// 清除画布内容,重置画布状态, 比 ctx.clearRect 更彻底
canvas.width = width;
data.forEach(item => {
draw(item);
});
}
function push(item) {
data.push(item);
draw(item);
}
function run() {
canvas.addEventListener('mousedown', downInvoker);
document.addEventListener('mousemove', moveInvoker);
document.addEventListener('mouseup', upInvoker);
canvas.addEventListener('mouseleave', leaveInvoker);
canvas.addEventListener('touchstart', downInvoker);
document.addEventListener('touchmove', moveInvoker);
document.addEventListener('touchend', upInvoker);
document.addEventListener('mousewheel', wheelInvoker, {
passive: false
});
downInvoker.value = onDown;
wheelInvoker.value = onScale;
push({
type: 'circle',
fillStyle: 'pink',
data: [100, 100, 50]
});
push({
type: 'rect',
fillStyle: '#0f00ff',
data: [200, 200, 100, 100]
});
push({
type: 'line',
lineWidth: 4,
data: [100, 90, 200, 90]
});
}
run();
</script>
</body>
</html>