В последнее время я работаю над проектом, связанным с обработкой изображений в веб-версии, которую можно рассматривать как яму холста впервые. В требованиях проекта есть функция добавления водяного знака на картинку. Мы знаем, что обычным способом добавления водяных знаков к изображениям на стороне браузера является использование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)
}
}
}
}
Новичкам в канве, если есть ошибки или недочеты, просьба указать.
Эта статья была впервые опубликована в моем блоге (Нажмите здесь, чтобы просмотреть), добро пожаловать, чтобы следовать.
использованная литература
Небольшие советы: используйте холст для синтеза водяных знаков изображения во внешнем интерфейсе.