研究 canvas 制图中撤消(undo)作用的完成方法详

2020-10-13 13:44 jianzhan

近期在做网页页面板图片解决有关的新项目,也算是初入了 canvas 的坑。新项目要求中有1个给照片加上水印的作用。大家了解,在访问器端完成照片加上水印作用,一般的做法便是应用 canvasdrawImage 方式。针对一般的生成(例如1张底图和1张 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 画布上就行。

以上便是情况详细介绍。可是略不便的是加上水印的要求中也有1个必须完成的作用是客户可以切换水印的部位。大家当然会想起能否完成 canvasundo 作用,当客户切换水印部位时,先撤消上1步 drawImage 实际操作,随后再再次绘图水印照片部位。

restore / save ?

高效率最高也是最便捷的毫无疑问是查阅 canvas 2D 原生态 API 是不是有此作用。历经1番检索, restore / save 这1对 API 进到视野。大家先看1下这两个 API 的叙述:

CanvasRenderingContext2D.restore() 是 Canvas 2D API 根据在制图情况栈中弹出顶端情况,将 canvas 修复到近期的储存情况的方式。 假如沒有储存情况,此方式不做任何更改。

CanvasRenderingContext2D.save() 是 Canvas 2D API 根据将当今情况放入栈中,储存 canvas 所有情况的方式。

乍看起来能够考虑要求。大家看1下官方示例编码:

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);

結果以下图所示:

怪异,仿佛和大家预期的結果不太1致。大家要想的結果是 save 方式启用后可以储存当今画布的快照, resolve 方式启用后可以彻底返回上1个储存的快照处的情况。

再细心科学研究1下 API。原先大家忽略1个关键定义: drawing state ,也便是绘图情况。储存到栈中的绘图情况包括下列几个一部分:

  1. 当今的转换引流矩阵
  2. 当今的裁切地区
  3. 当今的虚线目录

下列特性当今的值:strokeStyle, fillStyle, globalAlpha, lineWidth, lineCap, lineJoin, miterLimit, lineDashOffset, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor, globalCompositeOperation, font, textAlign, textBaseline, direction, imageSmoothingEnabled.

好吧, drawImage 实际操作后对画布的更改压根不存在于绘图情况中。因此,应用 resolve / save 没法完成大家必须的 undo 作用。

仿真模拟栈完成

既然原生态的 API 储存绘图情况的栈没法考虑要求,那末当然大家会想起自身仿真模拟1个储存实际操作的栈。随之而来的难题便是:每次绘图实际操作以后,应当储存甚么数据信息进栈?前面说过,大家要想的是每步绘图实际操作以后可以储存当今画布的 快照 ,假如能拿到快照数据信息,另外能运用快照数据信息修复画布的话,难题也就得到解决了。

好运的是 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);

大家看来1个简易的运用方法:

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);
        }
    }
}

大家封裝了1下 canvasdrawImage 方式,每次启用该方式以前都会储存上1个情况的快照到仿真模拟的栈中。在实行 undo 实际操作时,从栈中取下全新储存的快照,随后再次绘图画布,便可完成撤消实际操作。具体检测也合乎预期。

特性提升

上1节中大家很粗狂地完成了 canvas 的撤消作用。为何说粗狂呢?1个很不言而喻的缘故便是此计划方案特性不太好。大家的计划方案非常于每次全是再次绘图全部画布。假定实际操作流程许多,大家在仿真模拟栈也便是运行内存中就会储存许多预存的照片数据信息。另外,在绘图照片过度繁杂时, getImageDataputImageData 这两个方式会造成较为比较严重的特性难题。stackoverflow 上有详尽的探讨: Why is putImageData so slow? 。大家还能够从 jsperf 上这个检测测试用例的数据信息来认证这1点。淘宝 FED 在Canvas 最好实践活动中也提到了尽可能“不在动漫中应用 putImageData 方式”。此外,文章内容里还提到1点,“尽量启用那些3D渲染花销较低的 API”。大家能够从这里下手思索怎样开展提升。

以前说过,大家根据对全部画布储存快照的方法来纪录每一个实际操作,换个角度思索,假如大家把每次绘图的姿势储存到1个数字能量数组中,在每次实行撤消实际操作时,最先清空画布,随后重绘这个制图姿势数字能量数组,还可以完成撤消实际操作的作用。可行性层面,最先这样能够降低储存到运行内存的数据信息量,其次还防止了应用3D渲染花销较高的 putImageData 。以 drawImage 为较为目标,看 jsperf 上这个检测测试用例,2者的特性存在数量级的差别。

因而,大家觉得此提升计划方案是可行的。

改善后的运用方法大概以下:

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[exe.method](...exe.params)
            }
        }
    }
}

新人入坑 canvas,如有不正确与不够,欢迎指出。以上便是本文的所有內容,期待对大伙儿的学习培训有一定的协助,也期待大伙儿多多适用脚本制作之家。