Создание компонента обрезки изображения с нуля

JavaScript

Ноль, введение

Эта статья в основном вводит, как сделать полный компонент для резки изображения с нуля

Эта статья в основном включает:

  1. Загрузить прочитанное изображение
  2. Холст рисовать картинки
    1. Разобрать информацию об изображении
    2. предварительный просмотр изображения
  3. Операция, связанная с урожаем
    1. save() и restore() Canvas
    2. Основной процесс резки
    3. Чертеж бокса
    4. Перемещение и масштабирование поля обрезки
    5. вращать
  4. выводить обрезанное изображение
    1. Используйте Canvas.toBlob() для вывода изображений
    2. Canvas' getImageData() и putImageData()
    3. Загрузить в CDN

задний план

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

Грубый процесс

300

1. Загружайте и читайте картинки

При загрузке изображения используйтеonChangeсобытие, чтобы получить файловый объект, который будет содержать файлимя, размер, тип и время модификациии другую информацию (только для чтения), вы можете использовать эту информацию, чтобы ограничить формат, тип и т. д. загруженного изображения перед предварительным просмотром изображения.

handleChange = (e) => {
        const files = Array.from(e.target.files);
        if (!files.length) {
            // 释放上传系统存储当前值,避免相同文件不触发onchange事件
            this.imageUpload.value = null; 
            return;
        }
        // 上传规则校验(比如图片格式,图片大小限制等等
        ....
}
render() {
  return (
  	<div>
      	<input
            type="file"
            onChange={this.handleChange} // 监听上传事件
            multiple="true" // 是否批量上传
            accept="image/*" // 控制上传文件的类型,image/*表示接收所有image后缀的文件
            ref={e => {
                this.imageUpload = e;
            }}
        />
    </div>
  )
}

⚠️Внимание: использоватьonChangeПри загрузке файла, если один и тот же файл выбран дважды подряд, второй раз будет вызвано тем же значением.onChangeне сработает, так чтоПосле первой загрузки вам нужно установить пустое значение ввода.

2. Рисование картинок на холсте

2.1 Анализ информации об изображении

Используя только что полученный файловый объект, мы можем разобрать некоторую ключевую информацию об изображении, напримерШирина изображения, высота и самое главное base64. Здесь мы в основномFileReader.readAsDataURLреализовать.

1

вызыватьFileReader.onloadметод, он вернет объект data-uri в кодировке base64.

// 读取图片原始信息方法
filesInfo = (file) => {
    return new Promise((res, rej) => {
        let reader = new FileReader();
        reader.readAsDataURL(file);
        reader.onload = function(e) {
          	// 实例一个Image对象,为了获取宽、高(下文预览图片时需要)
          	let image = new Image(); 
            image.onload = function() {
                res({
                    width: image.width, // 宽
                  	height: image.height, // 高
                    // 其他图片信息
                  	// ...
                });
            };
            image.src = e.target.result; // base64
          	image.crossOrigin = 'Anonymous'; //解决跨域问题
        };
    });
},

На самом деле, кроме вышеперечисленного, есть и второй способ,window.URL.createObjectURL, Заинтересованные друзья могут проверить это сами, и я не буду вдаваться в подробности в этой статье.

2.2 Предварительный просмотр изображений

Существует два типа ширины и высоты холста:

  • Ширина и высота в стиле холста: это ширина и высота всего холста, которые определяютРазмер всего контекста холста;

  • Ширина и высота атрибута элемента холста: указываетхолст размер холста.

Поэтому нашАдаптивное центрирование изображенийСтратегия:

300*300

⚠️О нассоотношение пикселей устройстваЧтобы узнать подробности, нажмите 👉Почему картинка, нарисованная холстом, сильно размыта❓

Основной способ рендеринга изображений на холсте — черезcanvas.drawImage(), 🔨Часть кода реализации выглядит следующим образом:

// 绘制图片方法
// 这里的参数就是上文的image对象
drawImage = (image) => {
  	// 获取canvas的上下文
  	this.showImg = this.canvasRef.getContext('2d');
  	// 清除画布
  	this.showImg.clearRect(0, 0, this.canvasRef.width, this.canvasRef.height);
  	// 设置默认canvas元素大小
  	const canvasDefaultSize = 300;
    // 初始化canvas画布大小, 获取等比例缩放后的canvas宽高尺寸
  	let proportion = image.width / image.height,
      	scale = proportion > 1 ? canvasDefaultSize / image.width : canvasDefaultSize / image.height,
        canvasWidth = image.width * scale * 像素比,
        canvasHeight = image.height * scale * 像素比;
    this.canvasRef.width = canvasWidth;
    this.canvasRef.height = canvasHeight;
    this.canvasRef.style.width = canvasWidth / 像素比 + 'px';
  	this.canvasRef.style.height = canvasHeight / 像素比 + 'px';
  	// ...
    // 绘制图片,这个image就是我们刚刚获取的Image对象
  	this.image = image; // 保存这个Image对象
    this.showImg.drawImage(image, 0, 0, this.canvasRef.width, this.canvasRef.height);
};
render() {
  const canvasDefaultSize = 300; // 设置默认canvas元素大小
  return (
  	<div 
      className="modal-trim"
      // 固定整个canvas的变化范围
      style={{ width: `${canvasDefaultSize}px`, height: `${canvasDefaultSize}px` }}    
    >
      	<canvas 
          ref={e => {this.canvasRef = e}} 
          // 给予一个默认初始宽高
          width={canvasDefaultSize}
          height={canvasDefaultSize}
          // ...
       	></canvas>
    </div>
  )
}
/* 部分css */
.modal-trim {
    overflow: hidden;
    position: relative;
  	/* 马赛克背景图 */
    background-image: url(https://s10.mogucdn.com/mlcdn/c45406/190723_3afckd96l9h4fh6lcb56117cld176_503x503.jpg);
    background-size: cover;
  	/* 使canvas始终居中 */
    canvas {
        cursor: default;
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%,-50%);
    }
}

3. Операции, связанные с резкой

3.1 save() и restore() Canvas

(Дети, которые разбираются в функциях сохранения() и восстановления() холста, могут сразу пропустить этот раздел)

С точки зрения непрофессионала, сохранение и восстановление используются дляпамять для сохранения состояния холста

  • context.save()Вставьте текущее состояние в стек.
  • context.restore()Состояние стека извлекается, и контекст восстанавливается до этого состояния.

Так в каком состоянии холст? Здесь есть момент, который легко понять неправильно,Состояние относится не к содержимому холста, а к свойствам рисования холста.,Например:

  • Преобразование текущей матрицы: переводtranslate(), увеличитьscale()И вращениеrotate()Ждать
  • Текущая область отсечения:clip()
  • Другие значения свойств:strokeStyle,fillStyle,lineWidth,shadowColor...Ждать

Поэтому мы понимаем, что делает это свойство? Поскольку есть только один контекст холста, мы будем привлекать большое количество переходов состояния, когда выполняем операцию обрезки, например, перерисование коробки выбора обрезки, вращая изображение и т. Д. При выполнении этих операций нам нужно восстановить свойства чертежа , например: 🌰:

function draw() {
   let ctx = document.getElementById("canvas").getContext("2d");
   ctx.save();  //默认设置
   ctx.fillStyle = "#09f";
   ctx.fillRect(15,15,120,120); //填充当前设置的#09f颜色
   ctx.restore();
   ctx.fillRect(30,30,90,90); //填充默认的黑色
}

Когда приведенный выше код рисует первый квадрат, мы заливаем его синим цветом, а второй квадрат не имеет заданного цвета, поэтому по умолчанию он черный. Эффект следующий:

300*300

Но если вы закомментируете save и restore выше, все синие квадраты будут нарисованы, потому что fillStyle меняет холстсвойства чертежа, если не восстановить прежние свойства чертежа, то все чертежи будут синими.

⚠️Примечание: save() и restore() идут парами, не разбирайте их

3.2 Основной процесс резки

Возвращаясь к теме обрезки изображения, наш общий рабочий процесс таков:

300*300

Таким образом, мы можем выполнить полную операцию обрезки, отслеживая три события мыши: onMouseDown (щелчок), onMouseMove (перемещение) и onMouseUp (отпускание). 🔨Часть кода реализации выглядит следующим образом:

// 每张图片的初始化配置
initialConfigs = () => {
  this.showImg = this.canvasRef.getContext('2d');
  this.dragging = false; // 判断是否触发裁剪操作的全局变量
  this.startX = null;
  this.startY = null;
}

// 点击事件
mouseDownEvent = (e) => {
  	// 点击时表示触发裁剪操作
    this.dragging = true;
  	// 保存当前鼠标开始坐标, 一般坐标都会乘以个像素比
  	this.startX = e.nativeEvent.offsetX;
  	this.startY = e.nativeEvent.offsetY;
}

// 移动事件
mouseMoveEvent = (e) => {
    if (!this.dragging) return;
    // 计算临时裁剪框的宽高
    let tempWidth = e.nativeEvent.offsetX - this.startX,
        tempHeight = e.nativeEvent.offsetY - this.startY;
    // 调用绘制裁剪框的方法
  	this.drawTrim(this.startX, this.startY, tempWidth, tempHeight, this.showImg)
}

// 移出/松开事件
mouseRemoveEvent = (e) => {
  	// 保存相关裁剪选择框信息
    if (this.dragging) { ... }
    // 保存后将其置为false,表示结束当前流程
    this.dragging = false;
}
render() {
  return (
  	// ...
      	<canvas 
          ref={e => {this.canvasRef = e}}
          onMouseDown={(e) => this.mouseDownEvent(e)}
          onMouseMove={(e) => this.mouseMoveEvent(e)}
          onMouseUp={(e) => this.mouseRemoveEvent(e)}
        ></canvas>
   // ...
  )
}

3.3 Рисунок рамки обрезки

Что касается реализации чертежа рамки для обрезки, наиболее часто используемые в отрасли методы, вероятно, следующие:

300*300

Как правильно складывать эти слои изображений согласно требованиям❓

Здесь нужно использоватьcanvas.globalCompositeOperationЭтот API устанавливает или возвращает способ наложения нового изображения на существующее изображение, чтобы объединить изображения для получения поля обрезки. О конкретных параметрах его рисования можно ткнуть 👉Подробное объяснение globalCompositeOperationилиMDN

Используя только что переданные параметры координат мыши, давайте нарисуем рамку обрезки и 8 пикселей границы.Не забудьте сохранить соответствующую информацию о каждой операции~ 🔨 Часть кода реализации выглядит следующим образом:

// 每张图片的初始化配置
initialConfigs = () => {
  // ...
  // 需要保存的坐标信息
  this.trimPosition = { 
    startX: null,
    startY: null,
    width: null,
    height: null
  };	// 裁剪框坐标信息
  this.borderArr = []; // 裁剪框边框节点坐标
  this.borderOption = null; // 裁剪框边框节点事件
}

// 绘制裁剪框方法
drawTrim = (startX, startY, width, height, ctx) => {
    // 每一帧都需要清除画布
    ctx.clearRect(0, 0, this.canvasRef.width, this.canvasRef.height);
  
    // 绘制蒙层
    ctx.save();
    ctx.fillStyle = 'rgba(0,0,0,0.6)'; // 蒙层颜色
    ctx.fillRect(0, 0, this.canvasRef.width, this.canvasRef.height);
  
    // 将蒙层凿开
    ctx.globalCompositeOperation = 'source-atop';
    ctx.clearRect(startX, startY, width, height); // 裁剪选择框
  
  	// 绘制8个边框像素点并保存坐标信息以及事件参数
    ctx.globalCompositeOperation = 'source-over';
  	ctx.fillStyle = '#fc178f';
    let size = 10; // 自定义像素点大小
  	ctx.fillRect(startX - size / 2, startY - size / 2, size, size);
  	// ...同理通过ctx.fillRect再画出8个像素点
    ctx.restore();
  
    // 再次使用drawImage将图片绘制到蒙层下方
    ctx.save();
    ctx.globalCompositeOperation = 'destination-over';
   	ctx.drawImage(this.image, 0, 0, this.canvasRef.width, this.canvasRef.height);
    // ...
    ctx.restore();
}

3.4 Перемещение и расширение бокса

Недостаточно реализовать окно выбора кадрирования, наши обычные шаги кадрирования также должны свободно перемещаться и растягиваться Как это реализовать❓

Мы прошли вышеперечисленноеdrawTrim()Этот метод рисует начальную рамку обрезки, и каждый раз, когда наша рамка обрезки перемещается и растягивается,То, что модифицировано, это только информация о координате после того, как рама обрезки перемещается и растянута, то есть можно пройтиdrawTrim()этот метод, чтобы перерисовать.

Итак, нам нужно изменить базовый процесс резки, описанный выше:

300*300

🔥Советы: здесь можно использоватьcanvas.isPointInPath()Чтобы определить, переместилась ли мышь в область 8 пикселей

В основном это зависит от координат рамки обрезки, которую мы сохранили выше, и информации о координатах 8 точек пикселей для определения текущего события, которое необходимо выполнить.

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

3.5 Вращение

Самая неровная точка в компоненте обрезки — это координата вращения 🔨, давайте сначала посмотрим на нее.canvas.rotate()Бар

Как мы все знаем, начало оси координат исходного холста холста находится в верхнем левом углу, то есть (0, 0) представляет собой точку в верхнем левом углу.Правый X положительный, нижний Y положительный, наоборот.

Метод поворота в холсте заключается в вращении вокруг верхнего левого угла холста (0, 0), и ось координат также будет вращаться, и на нее будет влиять перевод, то есть, если мы повернем на 90 градусов по часовой стрелке через метод поворота, изображение находится в Относительное положение на холсте изменится, а ось изменится с "Правый X положительный, нижний Y положительный"стали"Нижний X положительный, левый Y положительный".

300*300

Так как же нам вращать изображение вокруг себя❓

Пришло время упомянутьcanvas.translate()Теперь, как следует из названия, это метод, используемый для перевода начала оси координат холста. Можно ли переводить ось координат обратно в исходное положение после каждого поворота?

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

  1. Холст переводится в начало координат центральной оси этой фигуры.
  2. Повернуть холст на 45 градусов
  3. При рисовании рисунка переместите рисунок в правый верхний угол на половину расстояния от самого рисунка
300*300

⚠️Примечание: не забудьте использовать сохранение и поворот, упомянутые выше, чтобы восстановить предыдущее состояние атрибута рисунка после каждого поворота., т.к. в коде задействован еще расчет и преобразование координат, здесь только общая идея поворота картинки, если хотите узнать больше, то можете ткнуть 👉Подробное объяснение поворота холста

4. Выведите обрезанное изображение

4.1 Используйте Canvas.toBlob() для вывода изображений

При загрузке изображений мы будемфайлфайлпревратился вbase64,Повторное использованиеcanvas.drawImage()Чтобы добиться предварительного просмотра изображения, как нам преобразовать холст обратно в изображение img после того, как мы его обрежем❓

Фактически, холст предоставляет два метода преобразования 2D в изображения:

  • canvas.toDataURL()

  • canvas.toBlob()

Поскольку нашей конечной целью является загрузка в CDN, здесь мы выбираем метод canvas.toBlob():

// 获得裁剪后的图片文件
getImgTrim = (type) => {
  	this.canvasRef.toBlob((blob)=>{
        // 加个时间戳缓存
        blob.lastModifiedDate = new Date();
      	let fd = new FormData();
      	fd.append('image', blob);
      	// 图片上传cdn
      	// ...
    }, type)
}

❓: Если мне нужно превратиться в объектный файл, как это сделать

const file = new File([blob], '图片.jpg', { type: blob.type })

4.2 Canvas getImageData() и putImageData()

Давайте сначала посмотрим, как это объясняется на MDN:

  • CanvasRenderingContext2D.getImageData()вернутьImageDataОбъект, используемый для описания данных пикселей, подразумеваемых областью холста, эта область представлена ​​​​прямоугольником, начальная точка - * (sx, sy),ширинашв,Гао Вэйsh*
  • CanvasRenderingContext2D.putImageData()Является ли POLVAS 2D API для преобразования данных из существующегоImageDataМетод, с помощью которого объект рисуется в растровое изображение. Если предоставляется нарисованный прямоугольник, отрисовываются только пиксели этого прямоугольника. На этот метод не влияет матрица преобразования холста.

С точки зрения непрофессионала,getImageData()Он используется для получения данных о пикселях области холста холста и возвратаImageDataобъект иputImageData()будетImageDataПиксельные данные объекта возвращаются на холст.

Так почему же нам нужно понимать эти два API? непосредственныйcanvas.toBlob()Недостаточно выводить картинки❓

потому чтоcanvas.toBlob()Результатом является весь элемент холста холста, а не часть, которую мы обрезали, поэтому нам нужно построить новый холст для достижения:

300*300
// 获得裁剪后的图片文件
getImgTrim = (type) => {
  	// 重新构建一个canvas
  	this.saveImg = this.saveCanvasRef.getContext('2d');
  	this.saveImg.clearRect(0, 0, this.saveCanvasRef.width, this.saveCanvasRef.height);
  	// 裁剪框的像素数据
  	let { startX, startY, width, height } = this.trimPosition
  	const data = this.canvasRef.getImageData(startX, startY, width, height)
    // 输出在另一个canvas上
    this.saveImg.putImageData(data, 0, 0)
  	this.saveCanvasRef.toBlob((blob)=>{
        // ...
    }, type)
}

❓: Почему мое выходное изображение становится больше/меньше в целом?

Хоть мы и перехватили часть изображения в рамке кадрирования на второй холст, мыРассчитывается ширина и высота самого полотна(существуетпредварительный просмотр изображенияВ этом абзаце) это не соответствует ширине и высоте самого изображения, что приводит к тому, что выходное изображение становится больше/меньше в целом.

Решение: я создал третий холст в качестве преемника здесь, Идея заключается в следующем:

300*300

4.3 Загрузка в CDN

Поскольку мы хотим сделать компонент общего назначения, лучше всего объединить в один и тот же экспорт, поэтому мы выбираем здесь загрузку в CDN, а унифицированный вывод — это ссылка на изображение.

// 获得裁剪后的图片文件
getImgTrim = (cdnUrl, type) => {
  	// 重新构建一个canvas并输出
  	// ...
  	this.saveCanvasRef.toBlob((blob)=>{
        // 加个时间戳缓存
        blob.lastModifiedDate = new Date();
      	let fd = new FormData();
      	fd.append('image', blob);
      	// 创建 XMLHttpRequest 提交对象
      	let xhr = new XMLHttpRequest();
      	xhr.onreadystatechange = function () {
          	if (this.readyState === 4 && this.status === 200) {
              	// ...
            }
        }
      	// 开始上传
        xhr.withCredentials = true; // 跨域传cookie的时候有用
        xhr.open("POST", cdnUrl, true);
        xhr.setRequestHeader('Access-Control-Allow-Headers','*');
        xhr.send(fd);
    }, type)
}

❓: Почему я сообщаю о междоменной ошибке после вывода изображения

Когда выходное изображение холста загрязнение холста вызвано междоменным, вам необходимо установить crossOrigin 'Anonymous' setRequestHeader и тому подобное, в частности, отметив 👉Разблокировать N поз для междоменных изображений, экспортируемых с холста.

Суммировать

Эта статья в основном знакомит с общей реализацией полного процесса обрезки.Что касается некоторых более настраиваемых функций (пакетная обрезка, обрезка с масштабированием, обрезка по размеру и т. д.), принципы в основном те же, но как работать с информацией о пакетном изображении и обрезкой Информация.

Важнейшим моментом работы с холстом является вычисление координат, особенно координат вращения, которые необходимо тщательно и четко понимать. На самом деле, весь процесс довольно прост, если мысли ясны.

Демонстрационный адрес компонента можно проштамповать 👉github

Ссылка на ссылку