В последнее время я работаю над проектом, связанным с обработкой изображений в веб-версии, которую можно рассматривать как яму холста впервые. В требованиях проекта есть функция добавления водяного знака на картинку. Мы знаем, что обычным способом добавления водяных знаков к изображениям на стороне браузера является использование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
Просто на холсте.
Выше приведено введение. Но немного проблематично то, что есть еще одна функция, которую необходимо реализовать в требовании добавления водяного знака, а именно то, что пользователь может переключать положение водяного знака. Мы, естественно, задаемся вопросом, может ли это быть достигнутоcanvas
изundo
Функция, когда пользователь переключает положение водяного знака, сначала отменяет предыдущий шагdrawImage
операции, а затем перерисовать положение изображения водяного знака.
restore
/save
?
Самое эффективное и удобное - это обязательно проверитьcanvas 2D
Имеет ли родной API эту функцию. После некоторых поисков,restore
/save
Эта пара API входит в зону прямой видимости. Давайте взглянем на описание этих двух 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, font, textAlign, textBaseline, direction, 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 包含 canvas 给定的矩形图像数据
*/
ImageData ctx.getImageData(sx, sy, sw, sh);
/*
* @param { Object } imagedata 包含像素值的对象
* @param { Number } dx 源图像数据在目标画布中的位置偏移量(x 轴方向的偏移量)
* @param { Number } dy 源图像数据在目标画布中的位置偏移量(y 轴方向的偏移量)
*/
void ctx.putImageData(imagedata, dx, dy);
Давайте рассмотрим простое приложение:
class WrappedCanvas {
constructor (canvas) {
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);
}
}
}
Мы инкапсулировалиcanvas
изdrawImage
方法,每次调用该方法之前都会保存上一个状态的快照到模拟的栈中。 в исполненииundo
Во время работы возьмите последний сохраненный снимок из стека, а затем перерисуйте холст, чтобы реализовать операцию отмены. Фактические тесты также соответствуют ожиданиям.
оптимизация производительности
В предыдущем разделе мы грубо реализовали этоcanvas
функция отмены. Почему ты говоришь грубо? Очевидная причина в том, что производительность этой схемы не очень хорошая. Наше решение эквивалентно перерисовке всего холста каждый раз. Предполагая, что операций много, мы сохраним много предварительно сохраненных данных изображения в стеке моделирования, то есть в памяти. Кроме того, когда рисунок слишком сложен,getImageData
а такжеputImageData
Эти два метода могут вызвать серьезные проблемы с производительностью. Существует подробное обсуждение stackoverflow:Why is putImageData so slow?. Мы также можем получить это из jsperfпрецедентданные, чтобы убедиться в этом. Таобао ФЭД вЛучшие практики холстаТакже упоминается в «старайтесь не использовать в анимацииputImageData
Кроме того, в статье также упоминается пункт «насколько это возможно вызывать эти API с низкими издержками рендеринга». Мы можем начать думать о том, как оптимизировать отсюда.
Как я уже говорил, мы записываем каждую операцию, сохраняя снимок всего холста.Думая с другой точки зрения, если мы сохраняем каждое действие рисования в массив, при выполнении операции отмены сначала очищаем холст, а затем перерисовываем это действие рисования. Массив также может реализовать функцию отмены операции. С точки зрения целесообразности, во-первых, это позволяет уменьшить объем данных, сохраняемых в памяти, а во-вторых, позволяет избежать использования высоких накладных расходов на рендеринг.putImageData
. кdrawImage
Для сравнения объектов см. это на jsperfпрецедент, между ними существует разница в производительности на порядок.
Усовершенствованный метод применения примерно следующий:
class WrappedCanvas {
constructor (canvas) {
this.ctx = canvas.getContext('2d');
this.width = this.ctx.canvas.width;
this.height = this.ctx.canvas.height;
this.executionArray = [];
}
drawImage (...params) {
this.executionArray.push({
method: 'drawImage',
params: params
});
this.ctx.drawImage(...params);
}
clearCanvas () {
this.ctx.clearRect(0, 0, this.width, this.height);
}
undo () {
if (this.executionArray.length > 0) {
// 清空画布
this.clearCanvas();
// 删除当前操作
this.executionArray.pop();
// 逐个执行绘图动作进行重绘
for (let exe of this.executionArray) {
this.ctx[exe.method](...exe.params)
}
}
}
}
Новичкам в канве, если есть ошибки или недочеты, просьба указать.
Эта статья была впервые опубликована в моем блоге (Нажмите здесь, чтобы просмотреть), добро пожаловать, чтобы следовать.
использованная литература
Небольшие советы: используйте холст для синтеза водяных знаков изображения во внешнем интерфейсе.