写在开头
嘿嘿,大家好!👋
今是2025年06月15日,台风 "蝴蝶" 过境, 大雨倾盆,连下了两天,愉快的周末又宣告破产了。
蜗居两天,刷了一些资讯,看了一下火山引擎的 原动力大会,剩下时间就是刷刷视频,追追剧。
然后,继上次小编分享了两篇关于图片裁剪的文章:
想不到还有续集呢。😋😋😋
今天我们将来深入探讨图片裁剪的两个进阶功能 ------ 固定比例 与 裁剪模式 ,具体效果如下,请诸君按需食用哈。

本次咱们就不从头开始一点点实现了,建议可以看看先前的两篇文章,或者直接看文末提供的完整源码。
固定比例
固定比例裁剪功能允许用户选择固定的宽高比例进行裁剪,比如1:1的正方形、3:4的竖版照片、4:3的横版照片等(参照了某红书📕的比例)。这个功能在社交媒体时代特别重要,因为不同平台对图片尺寸有不同的要求。
裁剪范围的核心在于比例约束算法,当用户拖拽裁剪框时,我们需要实时计算并保持指定的宽高比例,咱们单独用一个函数来维护裁剪比例:
javascript
// 获取比例值的函数
function getAspectRatioValue(ratio) {
switch (ratio) {
case "1:1": return 1;
case "3:4": return 3 / 4;
case "4:3": return 4 / 3;
default: return null;
}
}
接下来是是整个功能最复杂的部分😵,咱们需要根据用户的拖拽方向和变化量,动态地调整裁剪框的尺寸,同时保持比例不变:
javascript
document.body.addEventListener("mousemove", (e) => {
// 若未处于拖拽状态,直接返回
if (!dragging) return;
// 获取鼠标当前位置并计算与起始位置的差值
const { clientX, clientY } = e;
let diffX = clientX - startPoint[0];
let diffY = clientY - startPoint[1];
// 处理移动裁剪框的逻辑(而非调整大小)
if (moving) {
// 限制移动范围,确保裁剪框不会超出容器边界
diffX = Math.min(
Math.max(diffX, -startDimension[LEFT]), // 左边界限制
startDimension[RIGHT] // 右边界限制
);
diffY = Math.min(
Math.max(diffY, -startDimension[TOP]), // 上边界限制
startDimension[BOTTOM] // 下边界限制
);
}
// 裁剪框最小尺寸限制
const minWidth = 30;
const minHeight = 30;
// 获取当前选择的裁剪比例和容器尺寸
const currentAspectRatio = cutRange.value;
const cropperWidth = cropper.clientWidth;
const cropperHeight = cropper.clientHeight;
// 固定比例模式处理(非自定义比例且非移动操作)
if (currentAspectRatio !== "custom" && !moving) {
const ratio = getAspectRatioValue(currentAspectRatio);
if (ratio) {
// 计算当前裁剪框尺寸
const currentWidth = cropperWidth - startDimension[LEFT] - startDimension[RIGHT];
const currentHeight = cropperHeight - startDimension[TOP] - startDimension[BOTTOM];
// 根据拖拽方向计算宽高变化量
let newWidth, newHeight;
let widthChange = 0;
let heightChange = 0;
// 处理左右方向的拖拽
if (direction[LEFT] === 1) {
widthChange = -diffX; // 左边缘拖拽,宽度变化与鼠标移动相反
} else if (direction[RIGHT] === -1) {
widthChange = diffX; // 右边缘拖拽,宽度变化与鼠标移动相同
}
// 处理上下方向的拖拽
if (direction[TOP] === 1) {
heightChange = -diffY; // 上边缘拖拽,高度变化与鼠标移动相反
} else if (direction[BOTTOM] === -1) {
heightChange = diffY; // 下边缘拖拽,高度变化与鼠标移动相同
}
// 判断是否为角落拖拽(同时改变宽高)
const isCornerDrag =
(direction[LEFT] === 1 || direction[RIGHT] === -1) &&
(direction[TOP] === 1 || direction[BOTTOM] === -1);
// 根据拖拽类型计算新的宽高(保持固定比例)
if (isCornerDrag) {
// 角落拖拽时,根据鼠标移动幅度更大的方向决定优先调整的维度
if (Math.abs(widthChange) > Math.abs(heightChange)) {
newWidth = Math.max(minWidth, currentWidth + widthChange);
newHeight = newWidth / ratio; // 根据比例计算高度
} else {
newHeight = Math.max(minHeight, currentHeight + heightChange);
newWidth = newHeight * ratio; // 根据比例计算宽度
}
} else {
// 单边拖拽时,根据拖拽方向决定优先调整的维度
if (direction[LEFT] === 1 || direction[RIGHT] === -1) {
newWidth = Math.max(minWidth, currentWidth + widthChange);
newHeight = newWidth / ratio; // 根据宽度计算高度
} else {
newHeight = Math.max(minHeight, currentHeight + heightChange);
newWidth = newHeight * ratio; // 根据高度计算宽度
}
}
// 边界检查 - 确保不超出容器最大尺寸
const maxWidth = cropperWidth - CROPPER_MARGIN;
const maxHeight = cropperHeight - CROPPER_MARGIN;
// 如果新尺寸超出容器,按比例缩小至最大允许尺寸
if (newWidth >= maxWidth || newHeight >= maxHeight) {
if (newWidth / maxWidth > newHeight / maxHeight) {
newWidth = maxWidth;
newHeight = newWidth / ratio;
} else {
newHeight = maxHeight;
newWidth = newHeight * ratio;
}
// 如果调整量过小,忽略此次更新以避免抖动
if (
Math.abs(newWidth - currentWidth) < 1 &&
Math.abs(newHeight - currentHeight) < 1
) {
return;
}
}
// 计算新的裁剪框位置(上下左右边距)
let newLeft = startDimension[LEFT];
let newTop = startDimension[TOP];
let newRight = startDimension[RIGHT];
let newBottom = startDimension[BOTTOM];
// 处理左右边界的调整
if (direction[LEFT] === 1) {
newRight = startDimension[RIGHT];
newLeft = cropperWidth - newWidth - newRight; // 固定右边,调整左边
} else if (direction[RIGHT] === -1) {
newLeft = startDimension[LEFT];
newRight = cropperWidth - newWidth - newLeft; // 固定左边,调整右边
} else if (!isCornerDrag) {
// 居中调整(当仅调整高度时)
const currentHorizontalCenter = startDimension[LEFT] +
(cropperWidth - startDimension[LEFT] - startDimension[RIGHT]) / 2;
const newHorizontalOffset = newWidth / 2;
newLeft = Math.max(
0,
Math.min(cropperWidth - newWidth, currentHorizontalCenter - newHorizontalOffset)
);
newRight = cropperWidth - newWidth - newLeft;
}
// 处理上下边界的调整
if (direction[TOP] === 1) {
newBottom = startDimension[BOTTOM];
newTop = cropperHeight - newHeight - newBottom; // 固定下边,调整上边
} else if (direction[BOTTOM] === -1) {
newTop = startDimension[TOP];
newBottom = cropperHeight - newHeight - newTop; // 固定上边,调整下边
} else if (!isCornerDrag) {
// 居中调整(当仅调整宽度时)
const currentVerticalCenter = startDimension[TOP] +
(cropperHeight - startDimension[TOP] - startDimension[BOTTOM]) / 2;
const newVerticalOffset = newHeight / 2;
newTop = Math.max(
0,
Math.min(cropperHeight - newHeight, currentVerticalCenter - newVerticalOffset)
);
newBottom = cropperHeight - newHeight - newTop;
}
// 最终边界检查 - 确保边距非负
if (newLeft < 0) {
newLeft = 0;
newRight = cropperWidth - newWidth;
}
if (newTop < 0) {
newTop = 0;
newBottom = cropperHeight - newHeight;
}
if (newRight < 0) {
newRight = 0;
newLeft = cropperWidth - newWidth;
}
if (newBottom < 0) {
newBottom = 0;
newTop = cropperHeight - newHeight;
}
// 更新裁剪框位置和大小
setDimension([newTop, newRight, newBottom, newLeft]);
}
} else {
// 自由调整模式(自定义比例或移动操作)
const currentDimensionNew = [0, 0, 0, 0];
// 计算新的边距值,确保不小于0且裁剪框尺寸不小于最小限制
currentDimensionNew[TOP] = Math.min(
Math.max(startDimension[TOP] + direction[TOP] * diffY, 0),
cropperHeight - currentDimensionNew[BOTTOM] - minHeight
);
currentDimensionNew[RIGHT] = Math.min(
Math.max(startDimension[RIGHT] + direction[RIGHT] * diffX, 0),
cropperWidth - currentDimensionNew[LEFT] - minWidth
);
currentDimensionNew[BOTTOM] = Math.min(
Math.max(startDimension[BOTTOM] + direction[BOTTOM] * diffY, 0),
cropperHeight - currentDimensionNew[TOP] - minHeight
);
currentDimensionNew[LEFT] = Math.min(
Math.max(startDimension[LEFT] + direction[LEFT] * diffX, 0),
cropperWidth - currentDimensionNew[RIGHT] - minWidth
);
// 更新裁剪框位置和大小
setDimension(currentDimensionNew);
}
});
这段代码是图片裁剪功能的核心逻辑,主要实现了两种裁剪模式:
- 固定比例裁剪:当用户选择了特定比例(如 1:1、3:4、4:3)时,裁剪框会始终保持这个比例进行调整。代码通过计算宽高变化量并互相约束来实现这一点。
- 自由调整裁剪:当用户选择自定义比例时,裁剪框可以自由调整大小,只需确保不小于最小尺寸限制。
这代码中包含了复杂的边界检查逻辑,确保裁剪框不会超出容器范围,同时呢,也处理了多种拖拽场景(单边拖拽、角落拖拽、移动裁剪框),通过一些细致的条件判断和计算,实现了比较流畅的裁剪交互体验,这整个过程非常考验你的基础 JS
能力唷。👈
效果:

裁剪模式
裁剪模式是这次图片裁剪的另一个功能,它的作用是决定裁剪区域外的内容如何处理。
小编我实现了三种模式:
- 去除背景 - 只保留裁剪区域。
- 保留黑边 - 用指定颜色填充裁剪区域外的部分。
- 图片模糊 - 裁剪区域外显示模糊的原图。
裁剪核心咱们还是背靠
Canvas
提供的能力。✅
模式一:去除背景
这是最简单的模式👌,直接裁剪出指定区域:
js
if (mode === "remove") {
// 去除模式:保持裁剪区域的原始尺寸
canvas.width = actualWidth;
canvas.height = actualHeight;
ctx.clearRect(0, 0, actualWidth, actualHeight);
// 直接绘制裁剪区域
ctx.drawImage(
bgImage,
actualX, actualY, actualWidth, actualHeight, // 源图区域
0, 0, actualWidth, actualHeight // 目标区域
);
}
模式二:保留黑边
这个模式会保持原始画布尺寸,用指定颜色遮罩裁剪区域外的部分:
js
else if (mode === "blackBorder") {
// 保留黑边模式
canvas.width = CANVAS_WIDTH;
canvas.height = CANVAS_HEIGHT;
ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
// 先绘制完整图片
ctx.drawImage(bgImage, 0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
// 用指定颜色填充裁剪区域外的部分
ctx.fillStyle = blackBorderColor.value;
// 分别遮罩四个区域
// 遮罩上方
ctx.fillRect(0, 0, CANVAS_WIDTH, currentDimension[TOP]);
// 遮罩下方
ctx.fillRect(0, CANVAS_HEIGHT - currentDimension[BOTTOM],
CANVAS_WIDTH, currentDimension[BOTTOM]);
// 遮罩左侧
ctx.fillRect(0, currentDimension[TOP], currentDimension[LEFT],
CANVAS_HEIGHT - currentDimension[TOP] - currentDimension[BOTTOM]);
// 遮罩右侧
ctx.fillRect(CANVAS_WIDTH - currentDimension[RIGHT], currentDimension[TOP],
currentDimension[RIGHT],
CANVAS_HEIGHT - currentDimension[TOP] - currentDimension[BOTTOM]);
}
模式三:图片模糊
这是最有趣也是最复杂的模式,我们需要在裁剪区域外显示模糊的原图,而裁剪区域内保持清晰。
js
else if (mode === "vague") {
// 图片模糊模式
const blurValue = getVagueValue(); // 获取模糊度
canvas.width = CANVAS_WIDTH;
canvas.height = CANVAS_HEIGHT;
ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
// 第一步:绘制模糊的完整图片
ctx.filter = `blur(${blurValue}px)`;
ctx.drawImage(bgImage, 0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
// 第二步:重置滤镜并绘制清晰的裁剪区域
ctx.filter = "none";
ctx.save();
// 创建裁剪路径
ctx.beginPath();
ctx.rect(
currentDimension[LEFT],
currentDimension[TOP],
CANVAS_WIDTH - currentDimension[LEFT] - currentDimension[RIGHT],
CANVAS_HEIGHT - currentDimension[TOP] - currentDimension[BOTTOM]
);
ctx.clip(); // 设置裁剪区域
// 在裁剪区域内绘制清晰图片
ctx.drawImage(bgImage, 0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
ctx.restore();
}
这种模式使用 Canvas
的 filter
属性实现模糊效果,先绘制模糊的完整图片,然后通过裁剪区域只显示清晰的部分,形成突出显示的效果。
效果:

完整源码
至此,本篇文章就写完啦,撒花撒花。

希望本文对你有所帮助,如有任何疑问,期待你的留言哦。
老样子,点赞+评论=你会了,收藏=你精通了。