最近实现了一个Canvas轨迹回放功能,产品需求:可以在图片上进行留痕操作,并且是多张图片,操作完成后数据提交到服务器,客户端获取数据后,对Canvas的操作轨迹进行回放,还原图片留痕的操作过程,并且可以配上语音进行解说。以下代码仅为Demo,实现重绘功能,业务代码比较多,就不进行分享了。
以下是回放代码,由于产品要求的时间比较紧,其中部分代码参照了网上实例,代码如下:
function getBodyOffsetTop(el) {
var top = 0;
do {
top = top + el.offsetTop;
} while (el = el.offsetParent);
return top;
}
function getBodyOffsetLeft(el) {
var left = 0;
do {
left = left + el.offsetLeft;
} while (el = el.offsetParent);
return left;
}
function Drawing(canvas, options) {
typeof canvas == 'string' && (canvas = document.getElementById(canvas));
if (!canvas || !canvas.getContext) {
throw new Error(100, '你的浏览器不支持!');
}
this.starttime = 0;
this.init(canvas);
}
Drawing.prototype = {
init: function (canvas) {
this.canvas = canvas;
this.context = canvas.getContext('2d');
this.context.lineWidth = 1;
this.context.lineJons = 'round';
this.context.lineCep = 'round';
this.isButtonDown = false;
this.historyStroker = [];
this.curStroker = { color: '#000000', path: [] };
this.lastX = 0;
this.lastY = 0;
this.curColor = '#000000';
this.toolbarspos = {};
this.bindEvent();
},
bindEvent: function () {
var self = this;
this.canvas.addEventListener('mousemove', function (event) {
var x = event.pageX - getBodyOffsetLeft(this),
y = event.pageY - getBodyOffsetTop(this);
self.onMouseMove({ x: x, y: y });
}, false);
this.canvas.addEventListener('mousedown', function (event) {
var x = event.pageX - getBodyOffsetLeft(this),
y = event.pageY - getBodyOffsetTop(this);
self.onMouseDown(event, { x: x, y: y });
}, false);
this.canvas.addEventListener('mouseup', function (event) {
var x = event.pageX - getBodyOffsetLeft(this),
y = event.pageY - getBodyOffsetTop(this);
self.onMouseUp(event, { x: x, y: y });
}, false);
this.canvas.addEventListener('click', function (event) {
var x = event.pageX - getBodyOffsetLeft(this),
y = event.pageY - getBodyOffsetTop(this);
self.onClick({ x: x, y: y });
}, false);
},
onMouseMove: function (pos) {
if (this.isButtonDown) {
var p = this.toolbarspos;
for (var i in p) {
if (pos.x >= p[i].x
&& pos.x <= p[i].x + p[i].w
&& pos.y >= p[i].y
&& pos.y <= p[i].y + p[i].h) {
return;
}
}
this.context.lineTo(pos.x, pos.y);
this.context.stroke();
this.lastX = pos.x;
this.lastY = pos.y;
pos.timer = (new Date()).getTime() - this.starttime;
this.curStroker.path.push(pos);
}
},
onMouseDown: function (event, pos) {
if (this.starttime == 0) {
alert("请点击开始绘制按钮进行绘制");
return;
}
if (event.button == 0) {
var p = this.toolbarspos;
for (var i in p) {
if (pos.x >= p[i].x
&& pos.x <= p[i].x + p[i].w
&& pos.y >= p[i].y
&& pos.y <= p[i].y + p[i].h) {
return;
}
}
this.isButtonDown = true;
this.lastX = pos.x;
this.lastY = pos.y;
this.context.beginPath();
this.context.moveTo(this.lastX, this.lastY);
this.curStroker.color = this.curColor;
pos.timer = (new Date()).getTime() - this.starttime;
this.curStroker.path.push(pos);
}
},
onMouseUp: function (event, pos) {
if (event.button == 0) {
var p = this.toolbarspos;
for (var i in p) {
if (pos.x >= p[i].x
&& pos.x <= p[i].x + p[i].w
&& pos.y >= p[i].y
&& pos.y <= p[i].y + p[i].h) {
return;
}
}
this.isButtonDown = false;
this.historyStroker.push(this.curStroker);
this.curStroker = { color: this.curColor, path: [] };
}
},
ResetDrawCentre: function () {
var p = this.historyStroker, p2,
curColor = this.context.strokeStyle;
for (var i = 0; i < p.length; i++) {
this.context.strokeStyle = p[i].color;
this.context.beginPath();
for (var t = 0; t < p[i].path.length; t++) {
p2 = p[i].path[t];
if (t == 0) this.context.moveTo(p2.x, p2.y);
this.context.lineTo(p2.x, p2.y);
this.context.stroke();
}
this.context.beginPath();
}
this.context.strokeStyle = curColor;
},
testDraw: function () {
var self = this;
//this.intervalHandle = null;
if (!this.startplay) {
this.startplay = (new Date()).getTime();
}
if (!this.lineIndex) {
this.lineIndex = 0;
}
var p = this.historyStroker;
var curLine = p[this.lineIndex], curColor = p[0].color, pointIndex = 0, p2;
if (!this.testcanvas) {
this.testcanvas = document.getElementById("testcan");
this.testcontext = this.testcanvas.getContext('2d');
this.testcontext.beginPath();
}
if (this.intervalHandle) return;
this.intervalHandle = window.setInterval(function () {
var deltatime = (new Date()).getTime() - self.startplay;
p2 = p[self.lineIndex].path[pointIndex];
//绘制完成一条线
if (!p2) {
window.clearInterval(self.intervalHandle);
self.intervalHandle = null;
if (self.lineIndex < p.length - 1) {
self.lineIndex = self.lineIndex + 1;
self.testDraw();
console.log("===================")
}
}
if (deltatime >= p2.timer) {
self.testcontext.strokeStyle = curColor;
if (pointIndex == 0) {
self.testcontext.moveTo(p2.x, p2.y);
console.log(0);
}
pointIndex = pointIndex + 1;
self.testcontext.lineTo(p2.x, p2.y);
self.testcontext.strokeStyle = "#ff0000";
self.testcontext.stroke();
//testcontext.beginPath();
console.log(p2.x + "===" + p2.y);
}
}, 0)
}
};
var draw = new Drawing('can');
$("#startdraw").click(function () {
draw.starttime = (new Date()).getTime();
})
$("#testdraw").click(function () {
console.log(draw.historyStroker);
draw.testDraw();
})
实现原理:首先要记录开始绘制的时间draw.starttime,它是记录相对时间的起始时间,在移动过程中计算出每一个点的相对时间(毫秒)pos.timer = (new Date()).getTime() - this.starttime;并作为点的一个属性值进行存储,在回放过程中通过setInterval来实现,时间间隔设置为0,从点击回放按钮开始计算相对时间,如果相对时间大于或者等于某一个点的timer时,(注意:这里必须设置为大于或者等于,因为在有些浏览器里,由于setInterva内部执行效率不同,毫秒级别的时间点并不会连续,仅判断等于会不准确),绘制该点坐标,依次类推,当绘制完一条线时,进行递归操作,绘制第二条线,效果如下图所示:
![H5 canvas轨迹重绘]
Demo地址:https://github.com/mazhaohai/Play-H5-Canvas
如果需要进行图片分页,还需要给没一个点坐标添加pageid,即属于哪一张图片,如果绘制过程中图片进行了切换,需要重新设置draw.starttime,每一张图片都有自己的起始时间点;在回放过程中,如果需要多张图片自动切换,需要在图片预加载成功之后,重新设置每张图片的回放起始时间,再进行点的相对时间比对,否者会有时间差,比对不准确,原因是每张图片加载时间不同,每一张图片都要在预加载之后设置绘制起始时间和回放起始时间。