跳到主要内容

图像裁剪旋转与滤镜

图像编辑器是 处理型表面 + 预览型表面。滤镜和旋转在离屏完成,视口只 blit。不要对正在手势缩放的那张可见 canvas getImageData

1. 文档

export interface EditState {
source: ImageBitmap;
/** 相对 source 像素 */
crop: { x: number; y: number; w: number; h: number };
/** 90° 的倍数,顺时针 */
quarterTurns: 0 | 1 | 2 | 3;
/** CSS filter 子集,导出时烘焙 */
filter: string;
}

非 90° 自由旋转要插值,导出尺寸按 AABB 重算。产品没要求就只提供 90°,避免质量扯皮。

EXIF orientation:createImageBitmap / <img> 在各浏览器是否自动尊重 orientation 不一致。上传管线应在解码前用库剥 EXIF,或固定「按像素阵列显示」,并在 UI 标明。不要假设 iPhone 照片方向一定对。

2. 离屏烘焙

export async function rasterizeEdit(state: EditState): Promise<ImageBitmap> {
const { crop, quarterTurns, source, filter } = state;
const swapped = quarterTurns % 2 === 1;
const outW = swapped ? crop.h : crop.w;
const outH = swapped ? crop.w : crop.h;
const canvas = new OffscreenCanvas(outW, outH);
const ctx = canvas.getContext("2d", { willReadFrequently: false });
if (!ctx) throw new Error("2d unavailable");
ctx.filter = filter || "none";
ctx.translate(outW / 2, outH / 2);
ctx.rotate((quarterTurns * Math.PI) / 2);
ctx.drawImage(
source,
crop.x,
crop.y,
crop.w,
crop.h,
-crop.w / 2,
-crop.h / 2,
crop.w,
crop.h,
);
return canvas.transferToImageBitmap();
}

预览:手势中只画「源 + crop 框 + CSS transform 模拟旋转」,松手或点应用再 rasterizeEdit。实时 ctx.filter 全图 blur 会打满主线程。

直方图、曲线:必须 willReadFrequently: true 的小图(先缩小再采样),不要对 4k 原图每帧 getImageData

3. 裁剪交互

crop 矩形在图像像素空间。手柄逻辑同 框选。Shift 锁比例。双击原图重置 crop 为全图。

输出尺寸封顶:Math.min(crop.w, maxEdge),移动端导出 4k×4k×4 字节会杀页。超限先缩再滤镜。

4. 失败形态

症状原因
旋转后裁切空了仍按未旋转的 crop 去 draw
iPhone 图倒了EXIF 未处理
预览流畅导出发糊预览用了 CSS 缩放的低清层当源
调滤镜卡死在视图 canvas 全分辨率 getImageData
JPEG 黑边透明 + JPEG,见导出章

权威资料

核对日期:2026-08-26