Изучите реализацию функции отмены в рисовании на холсте.

внешний интерфейс Canvas

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

Новичкам в канве, если есть ошибки или недочеты, просьба указать.

Эта статья была впервые опубликована в моем блоге (Нажмите здесь, чтобы просмотреть), добро пожаловать, чтобы следовать.

использованная литература

Небольшие советы: используйте холст для синтеза водяных знаков изображения во внешнем интерфейсе.

Рекомендации по Canvas (производительность)

Canvas — интерфейс веб-API | MDN