Недавно я работал над проектом, связанным с обработкой изображений веб-страниц, который можно считать моим первым опытом работы с холстом. В требования проекта входит функция добавления водяных знаков к изображениям. Мы знаем, что обычный способ реализовать функцию добавления водяных знаков к изображениям на стороне браузера — использовать метод canvas
drawImage
. Для обычного синтеза (например, синтеза базового изображения и изображения водяного знака PNG) общий принцип реализации следующий:
var Canvas = document.getElementById(canvas);var ctx = Canvas.getContext('2d');// img: базовое изображение // WatermarkImg: изображение водяного знака // x, y — координаты размещения img на холсте ctx. drawImage( img, x, y);ctx.drawImage(watermarkImg, x, y);
Просто используйте drawImage()
напрямую и непрерывно, чтобы нарисовать соответствующее изображение на canvas
.
Вышеупомянутое является вводной информацией. Но что немного неприятно, так это то, что в требовании о добавлении водяного знака необходимо реализовать еще одну функцию, а именно то, что пользователь может переключать положение водяного знака. Мы, естественно, думаем о том, можем ли мы реализовать функцию undo
canvas
. Когда пользователь меняет положение водяного знака, сначала отмените предыдущую операцию drawImage
, а затем перерисуйте положение изображения водяного знака.
restore
/ save
?
Самый эффективный и удобный способ — проверить, имеет ли собственный API canvas 2D
эту функцию. После некоторых поисков появилась пара API-интерфейсов restore
/ save
. Давайте сначала посмотрим на описания этих двух API:
CanvasRenderingContext2D.restore() — это метод Canvas 2D API, предназначенный для восстановления холста до последнего сохраненного состояния путем извлечения верхнего состояния из стека состояний рисования. Если сохраненного состояния нет, этот метод не вносит изменений.
CanvasRenderingContext2D.save() — это метод Canvas 2D API, предназначенный для сохранения всего состояния холста путем помещения текущего состояния в стек.
На первый взгляд кажется, что это соответствует потребностям. Давайте посмотрим на официальный пример кода:
var Canvas = document.getElementById(canvas);var ctx = Canvas.getContext(2d);ctx.save(); // Сохраняем состояние по умолчанию ctx.fillStyle = green;ctx.fillRect(10, 10, 100, 100) ;ctx.restore(); //Восстанавливаем последнее сохраненное состояние по умолчанию ctx.fillRect(150, 75, 100, 100);
Результат показан ниже:
Странно, но, похоже, это не соответствует ожидаемым нами результатам. Результат, который мы хотим, — это иметь возможность сохранить снимок текущего холста после вызова метода save
и иметь возможность полностью вернуться к состоянию последнего сохраненного снимка после вызова метода resolve
.
Давайте подробнее рассмотрим API. Оказывается, мы упустили важное понятие: drawing state
, которое и есть состояние рисования. Состояние рисования, сохраненное в стеке, содержит следующие части:
Текущие значения следующих свойств:strokeStyle, fillStyle, globalAlpha, lineWidth, lineCap, lineJoin, miterLimit, lineDashOffset,shadowOffsetX,shadowOffsetY,shadowBlur,shadowColor, globalCompositeOperation, шрифт, textAlign, textBaseline, направление, imageSmoothingEnabled.
Ну, изменения в холсте после операции drawImage
вообще не существуют в состоянии рисования. Следовательно, использование resolve
/ save
не может обеспечить необходимую нам функцию отмены.
Поскольку стек собственного API для сохранения статуса отрисовки не может удовлетворить потребности, естественно, мы подумаем о том, чтобы самостоятельно смоделировать стек для сохранения операций. Возникает вопрос: какие данные следует сохранять в стеке после каждой операции рисования? Как упоминалось ранее, мы хотим иметь возможность сохранять снимок текущего холста после каждой операции рисования. Если мы сможем получить данные снимка и использовать данные снимка для восстановления холста, проблема будет решена.
К счастью, canvas 2D
изначально предоставляет API для получения снимков и восстановления холста через снимки — getImageData
/ putImageData
. Ниже приводится описание API:
/* * @param { Number } sx Координата x верхнего левого угла прямоугольной области данных изображения, которые необходимо извлечь * @param { Number } sy Координата y верхнего левого угла прямоугольной области данные изображения, которые нужно извлечь * @param { Number } sw Будет Ширина прямоугольной области данных изображения, которые нужно извлечь * @param { Number } sh Высота прямоугольной области данных изображения, которые нужно извлечь * @return { Object } ImageData содержит холст Данные прямоугольных данных изображения */ ImageData ctx.getImageData(sx, sy, sw, sh); /* * @param { Object } объект imagedata, содержащий значения пикселей * @param { Number } dx исходные данные изображения в целевом холсте Position offset (смещение в направлении оси X) * @param { Number } dy Смещение положения исходных данных изображения на целевом холсте (смещение в направлении оси Y) */ void ctx.putImageData(imagedata, dx, dy);
Давайте посмотрим на простое приложение:
класс WrappedCanvas { конструктор (холст) { this.ctx = Canvas.getContext('2d'); this.width = this.ctx.canvas.width; this.height = this.ctx.canvas.height; this.imgStack = [ ]; } drawImage (...params) { const imgData = this.ctx.getImageData(0, 0, this.width, this.height); this.imgStack.push(imgData);this.ctx.drawImage(...params); } undo () { if (this.imgStack.length > 0) { const imgData = this.imgStack.pop (); this.ctx.putImageData(imgData, 0, 0);
Мы инкапсулируем метод drawImage
элемента canvas
и сохраняем снимок предыдущего состояния в моделируемый стек перед каждым вызовом этого метода. При выполнении операции undo
удалите последний сохраненный снимок из стека, а затем перерисуйте холст, чтобы реализовать операцию отмены. Фактические испытания также оправдали ожидания.
В предыдущем разделе мы очень грубо реализовали функцию отмены canvas
. Почему ты говоришь грубо? Одна из очевидных причин заключается в том, что это решение работает плохо. Наше решение эквивалентно перерисовке всего холста каждый раз. Предполагая, что операций много, мы сохраним много предварительно сохраненных данных изображения в стеке моделирования, который является памятью. Кроме того, если рисование изображений слишком сложное, два метода getImageData
и putImageData
вызовут серьезные проблемы с производительностью. Есть подробное обсуждение stackoverflow: Почему putImageData работает так медленно? Мы также можем убедиться в этом по данным этого тестового примера на jsperf. Taobao FED также упомянул в лучших практиках Canvas, что старается не использовать метод putImageData
в анимации. Кроме того, в статье также упоминается, что API с меньшими затратами на рендеринг следует вызывать как можно чаще. Отсюда мы можем начать думать о том, как оптимизировать.
Как упоминалось ранее, мы записываем каждую операцию, сохраняя снимок всего холста. Если рассматривать это с другой точки зрения, если мы сохраняем каждое действие рисования в массиве, каждый раз, когда выполняется операция отмены, холст сначала очищается, а затем. Перерисовка этого массива действий рисования также может реализовать функцию отмены операции. С точки зрения осуществимости, во-первых, это позволяет уменьшить объем данных, сохраняемых в памяти, а во-вторых, позволяет избежать использования putImageData
требует более высоких накладных расходов на рендеринг. Если взять drawImage
в качестве объекта сравнения и посмотреть на этот тестовый пример на jsperf, разница в производительности между ними будет на порядок.
Поэтому мы считаем, что такое оптимизационное решение осуществимо.
Усовершенствованный метод применения примерно следующий:
класс WrappedCanvas { конструктор (холст) { this.ctx = Canvas.getContext('2d'); this.width = this.ctx.canvas.width; this.height = this.ctx.canvas.height; this.executionArray = [ ]; } drawImage (...params) { this.executionArray.push({ метод: 'drawImage', параметры: параметры });this.ctx.drawImage(...params); } ClearCanvas () { this.ctx.clearRect(0, 0, this.width, this.height); отменить () { if (this.executionArray. length > 0) { // Очищаем холст this.clearCanvas(); // Удаляем текущую операцию this.executionArray.pop() // Выполняем действия рисования одно за другим для перерисовки. (пусть exe из this.executionArray) { this[exe.method](...exe.params) } } }}
Если вы новичок в Canvas, укажите на любые ошибки или недостатки. Выше приведено все содержание этой статьи. Я надеюсь, что она будет полезна для изучения всеми. Я также надеюсь, что все поддержат сеть VeVb Wulin.