Полный инструмент обрезки изображения на основе холста

Canvas
Полный инструмент обрезки изображения на основе холста

предисловие

Эта статья основана наcanvasДля реализации инструмента обрезки изображения. потому чтоcanvasКод все еще относительно длинный, попробуйте написать идеи, полный код помещен вgithubначальство.

Проблема размытия холста

это написаноcanvasВопросы, с которыми надо обращаться, ответы на это в интернете тоже есть везде, поэтому подробно их представлять не буду.

потому чтоcanvasне векторная иллюстрация, вRetinaПод экраном браузер использует несколько пикселей для отображения пикселя, в результате чегоcanvasНаконец, есть проблема размытия.

решение:

  • Получатьwindow.devicePixelRatioФизическое разрешение устройства в пикселях такое же, какCSSСоотношение разрешений пикселей.
  • canvas contextимеет атрибутbackingStorePixelRatioвизуализация презентацииcanvasРаньше для хранения информации о холсте использовалось несколько пикселей. Но это доступно только в некоторых браузерах, например.safari
  • установивcanvas.width/heightа такжеcanvas.style.width/heightправильноcanvasМасштаб обработки, соотношениеdevicePixelRatio/backingStorePixelRatio(ratio). (canvas.width/heightпредставляет фактический размер холста, аcanvas.style.width/heightУказывает размер результата рендеринга в браузере)
  • наконец пройтиcontext.scale(ratio, ratio)правильноcanvasпроцесс, исправить его рендеринг

Если вы используетеtypescriptЕсли да, то сообщитbackingStorePixelRatioНет ошибки, плюс файл определения типа для решения.

export const getPixelRatio = (context: CanvasRenderingContext2D) => {
  const backingStore =
    context.backingStorePixelRatio ||
    context.webkitBackingStorePixelRatio ||
    context.mozBackingStorePixelRatio ||
    context.msBackingStorePixelRatio ||
    context.oBackingStorePixelRatio || 1;
  return (window.devicePixelRatio || 1) / backingStore;
};
const calcCanvasSize = () => {
    //...dosth.
    canvasRef.current.style.width = `${canvasWidth}px`;
    canvasRef.current.style.height = `${canvasHeight}px`;
    canvasRef.current.width = canvasWidth * ratio;
    canvasRef.current.height = canvasHeight * ratio;  
    ctx.scale(ratio, ratio);
};
//省略不必要代码

Нарисовать изображение на холсте

Это, собственно, черезinputПолучите локальный файл изображения черезwindow.URL.createObjectURLполучатьDOMString, прими какimgизsrc. пройти черезctx.drawImageнарисовать картину, чтобыcanvasначальство.

Потому что для инструмента обрезки изображенияimgОн должен быть окрашен в нижней части, поэтому он должен пройтиglobalCompositeOperation, нарисуйте его на нижнем слое. (globalCompositeOperationПредставляет, как нарисовать исходное (новое) изображение на целевом (существующем) изображении. )

const handleChoiseImg = () => {
    if (createURL) {
      window.URL.revokeObjectURL(createURL);
    };

    createURL = window.URL.createObjectURL(inputRef.current!.files![0]);
    img = new Image();
    img.onload = () => {
      //initImageCanvas(img); 这个函数我是去获取img应该缩小比例和缩小宽高
      // calcCanvasSize(); 这个我是去获取canvas应该呈现的size
      drawImage();  //绘画img
    };
    img.src = createURL;
};

const drawImage = () => {
    // todo sth.
    ctx.save();
    ctx.globalCompositeOperation = 'destination-over';
    // ctx.translate(canvasWidth / 2, canvasHeight / 2);
    // ctx.rotate(Math.PI / 180 * rotate);
    // if (rotate % 180 !== 0) {
    //   [canvasWidth, canvasHeight] = [canvasHeight, canvasWidth];
    // };
    // ctx.translate(-canvasWidth / 2, - canvasHeight / 2);
    ctx.drawImage(
      img,
      (canvasWidth - scaleImgWidth) / 2, (canvasHeight - scaleImgHeight) / 2,
      scaleImgWidth, scaleImgHeight
    );
    // canvasWidth/Height表示canvas的宽高(style),scaleImgWidth/Height表示图片缩放后的宽高
    ctx.restore();
};

Маски и флажки

маскировка

или использоватьglobalCompositeOperationНарисуйте его поверх существующего изображения.

const drawCover = () => {
    ctx.save();
    ctx.fillStyle = 'rgba(0,0,0,0.5)';
    ctx.fillRect(0, 0, canvasSize.width, canvasSize.height);
    ctx.globalCompositeOperation = 'source-atop';
    ctx.restore();
};

флажок

На самом деле, установив флажок,clearRectОчистите область маски и раскрасьте собственную рамкуstyle, и наконецimgЖивопись находится на нижнем слое.

canvasАнимации рисуются кадр за кадром, процесс перетаскивания выделенного прямоугольника на самом деле постоянный.clearRectвесьcanvas, а затем снова пройти описанный выше процесс, то есть процесс перекраски.

const drawSelect = (x: number, y: number, w: number, h: number) => {
    ctx.clearRect(0, 0, canvasSize.width, canvasSize.height);
    //清空整个canvas
    drawCover();
    //绘画蒙层
    ctx.save();
    ctx.clearRect(x, y, w, h);
    //清空选中区域
    ctx.strokeStyle = '#5696f8';
    ctx.strokeRect(x, y, w, h);
    // 画选中框
    // todo sth. 给选中框加一些style
    ctx.restore();
    drawImage();
    // 绘画图片
};

Флажок перетащить растягивание и обработка границ

Установите флажок и перетащите и растяните его, даmouseобработка событий, вmouseDown, дайте ему идентификатор, вmouseMoveВыбранное поле постоянно обновляется и рисуется, вmouseUpФлаг отмены (это событие можно передать внешнему контейнеру).

Граница обработки праваmouseMoveОбработанное положение флажка обрабатывается и оценивается.Если оно превышает границу, оно будет исправлено. вот такoffsetXа такжеoffsetYОбработайте его, а затем судите, как изменить флажок в разных направлениях.Поскольку объем кода относительно велик, его можно полностьюgithubСмотреть.

Изображение эффекта:

Обработка поворота изображения

canvasЦентром вращения является левый верхний угол как центр, если напрямую вызыватьrotate, то результат точно не тот, что мы хотим. затем используйте егоtranslateдвигатьсяcanvasв центральную точку, а затем вызовитеrotateВращение и повторное использование после вращенияtranslateБудуcanvasВернитесь на его место.

Единственная проблема - разобратьсяrotateПосле ВасtranslateСковородаcanvasэтот разx、yценность .

Моя обработка инструмента обрезки изображения здесь заключается в его изменении после поворота.canvasизwidth/height&style width/height. В настоящее время,canvasвращается, ноimageПри перерисовке вам также необходимо нарисовать повернутое изображение, а затем использовать метод, упомянутый выше, для поворота картины.

И не забудьте пройтиsave & restoreдля сохранения и восстановления состояния чертежа.

const drawImage = () => {
    // todo sth.
    ctx.save();
    ctx.globalCompositeOperation = 'destination-over';
    ctx.translate(canvasWidth / 2, canvasHeight / 2);
    ctx.rotate(Math.PI / 180 * rotate);
    if (rotate % 180 !== 0) {
      [canvasWidth, canvasHeight] = [canvasHeight, canvasWidth];
    };
    ctx.translate(-canvasWidth / 2, - canvasHeight / 2); 
    ctx.drawImage(
      img,
      (canvasWidth - scaleImgWidth) / 2, (canvasHeight - scaleImgHeight) / 2,
      scaleImgWidth, scaleImgHeight
    );
    ctx.restore();
};

Изображение эффекта:

Масштабирование изображения

scaleОн также основан на верхнем левом углу в качестве центра масштабирования, а затем также необходимо масштабировать.save & restore, иначе это повлияет на последующие операции.

Однако я не использовалscale, но вручную измените масштаб изображения, а затем получитеscaleImgWidthа такжеscaleImgHeight, перед вызовомdrawImage. Поскольку код отображается в центре, его можно вызвать сразу после модификации.

// 修改 scaleImg 得到scaleImgWidth & scaleImgHeight
ctx.drawImage(
  img,
  (canvasWidth - scaleImgWidth) / 2, (canvasHeight - scaleImgHeight) / 2,
  scaleImgWidth, scaleImgHeight
);

Изображение эффекта:

Обработка изображений в градациях серого

Обработка оттенков серого выполняетсяgetImageDataПолучатьcanvasизImageDataТо есть пиксельные данные, которые можно обрабатывать. Затем снова передайте обработанные данные пикселей.putImageDataположить обратноcanvasначальство.

Пиксельные данные, есть четыре аспекта информации для каждого пикселя, а именноRed,Green,Blue,Alpha.

Формул обработки оттенков серого еще довольно много, и я буду использовать их здесь.(R + 2G + B) >> 2.

const imgData = ctx.getImageData(0, 0, canvasSize.width * ratio, canvasSize.height * ratio);
getGrayscaleData(imgData);
ctx.putImageData(imgData, 0, 0);

Кроме того, можно выполнять многие аналогичные обработки, такие как обработка контрастного цвета, палитра цветов и т. д.

Изображение эффекта:

Отображение выбранных изображений в реальном времени

Лишь бы перехватитьcanvasЧасть, показанная до сих пор, не очень дружелюбна. Оно должно соответствовать соответствующему положению исходной картинки, и выбирать картинку в этой позиции удобнее.

Обработка идей:

  • Недавно созданныйcanvas,БудуimgНа нем полная картина, и задача вращения решена.
  • установив флажокx y w hзначение иimg width/heightа такжеcanvas width/heightЗначение , чтобы получить значение, соответствующее выбранной части исходного изображенияx y
  • пройти черезgetImageDataполучатьImageData, и определите, требуется ли обработка оттенков серого
  • Затем повторно измените созданный вышеcanvasизwidth/heightдля выбранной части изображенияputW putH
  • БудуImageDataпройти черезputImageDataположить вcanvasсредний проходtoBlobполучатьblob
  • наконец прошлоwindow.URL.createObjectURLполучатьDOMString
export const getPhotoData = () => {
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
    // todo canvas处理

    ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
    // 处理获得putX putY putW putH
    const imgData = ctx.getImageData(putX, putY, putW, putH);
    if (grayscale) {    //灰度处理
        getGrayscaleData(imgData);
    };
    canvas.width = putW;
    canvas.height = putH;

    ctx.putImageData(imgData, 0, 0);
    return new Promise(res => {
        canvas.toBlob(e => res(e));
    });
};

const cancelChangeSelect = async () => {
    // todo sth.
    dataUrl && (window.URL.revokeObjectURL(dataUrl));
    const blob = await getPhotoData() as Blob;
    const newDataUrl = window.URL.createObjectURL(blob);
    setDataUrl(newDataUrl);
    // todo sth.
};
// 省去不关键代码

Изображение эффекта:

Скачать выбранные изображения

На самом деле это было написано чуть ли не выше, и получилосьdataUrl, используйте его какaпомеченhref, загрузка завершена. (Конечно, есть много других способов загрузки, поэтому я не буду перечислять их по одному)

полный код

загруженоgithub