diff --git a/src/ol/replay/canvas.js b/src/ol/replay/canvas.js
new file mode 100644
index 0000000000..7c863439b2
--- /dev/null
+++ b/src/ol/replay/canvas.js
@@ -0,0 +1,263 @@
+goog.provide('ol.replay.Canvas');
+
+goog.require('goog.array');
+goog.require('goog.asserts');
+goog.require('goog.vec.Mat4');
+goog.require('ol.replay');
+goog.require('ol.replay.BatchBase');
+goog.require('ol.replay.CanvasBase');
+goog.require('ol.replay.FillStyle');
+goog.require('ol.replay.StrokeStyle');
+
+
+/**
+ * @enum {number}
+ */
+ol.replay.CanvasInstructionId = {
+ ADD_PATH: 0,
+ BEGIN_PATH: 1,
+ DRAW: 2,
+ SET_FILL_STYLE: 3,
+ SET_STROKE_STYLE: 4
+};
+
+
+/**
+ * @typedef {{close: (boolean|undefined),
+ * command: ol.replay.CanvasInstructionId,
+ * fillStyle: (ol.replay.FillStyle|undefined),
+ * end: (number|undefined),
+ * start: (number|undefined),
+ * strokeStyle: (ol.replay.StrokeStyle|undefined)}}
+ * }
+ */
+ol.replay.CanvasInstruction;
+
+
+
+/**
+ * @constructor
+ * @extends {ol.replay.BatchBase}
+ * @param {ol.replay.BatchType} type Type.
+ * FIXME make this private?
+ * FIXME accumulate all coordinates between style sets in a single array.
+ * FIXME merge adjacent setStyles.
+ */
+ol.replay.CanvasBatch = function(type) {
+
+ goog.base(this);
+
+ /**
+ * @private
+ * @type {ol.replay.BatchType}
+ */
+ this.type_ = type;
+
+ /**
+ * @private
+ * @type {Array.
}
+ */
+ this.instructions_ = [];
+
+ /**
+ * @private
+ * @type {Array.}
+ */
+ this.path_ = [];
+
+};
+goog.inherits(ol.replay.CanvasBatch, ol.replay.BatchBase);
+
+
+/**
+ * @inheritDoc
+ */
+ol.replay.CanvasBatch.prototype.addPath = function(path, stride, close) {
+ goog.asserts.assert(this.type_ == ol.replay.BatchType.FILL ||
+ this.type_ == ol.replay.BatchType.FILL_AND_STROKE ||
+ this.type_ == ol.replay.BatchType.STROKE);
+ var start = this.path_.length;
+ if (stride == 2) {
+ goog.array.extend(this.path_, path);
+ } else {
+ var m = path.length;
+ var j = this.path_.length;
+ var i;
+ for (i = 0; i < m; i += stride) {
+ this.path_[j++] = path[i];
+ this.path_[j++] = path[i + 1];
+ }
+ }
+ this.instructions_.push(/** @type {ol.replay.CanvasInstruction} */ ({
+ command: ol.replay.CanvasInstructionId.ADD_PATH,
+ close: close,
+ end: this.path_.length,
+ start: start
+ }));
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.replay.CanvasBatch.prototype.beginPath = function() {
+ this.instructions_.push(/** @type {ol.replay.CanvasInstruction} */ ({
+ command: ol.replay.CanvasInstructionId.BEGIN_PATH
+ }));
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.replay.CanvasBatch.prototype.draw = function() {
+ this.instructions_.push(/** @type {ol.replay.CanvasInstruction} */ ({
+ command: ol.replay.CanvasInstructionId.DRAW
+ }));
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.replay.CanvasBatch.prototype.setFillStyle = function(fillStyle) {
+ goog.asserts.assert(this.type_ == ol.replay.BatchType.FILL ||
+ this.type_ == ol.replay.BatchType.FILL_AND_STROKE);
+ this.instructions_.push(/** @type {ol.replay.CanvasInstruction} */ ({
+ command: ol.replay.CanvasInstructionId.SET_FILL_STYLE,
+ fillStyle: fillStyle
+ }));
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.replay.CanvasBatch.prototype.setStrokeStyle = function(strokeStyle) {
+ goog.asserts.assert(this.type_ == ol.replay.BatchType.FILL_AND_STROKE ||
+ this.type_ == ol.replay.BatchType.STROKE);
+ this.instructions_.push(/** @type {ol.replay.CanvasInstruction} */ ({
+ command: ol.replay.CanvasInstructionId.SET_STROKE_STYLE,
+ strokeStyle: strokeStyle
+ }));
+};
+
+
+/**
+ * @param {CanvasRenderingContext2D} context Context.
+ * @param {goog.vec.Mat4.AnyType} transform Transform.
+ */
+ol.replay.CanvasBatch.prototype.drawInternal = function(context, transform) {
+ var fillStyle = null;
+ var strokeStyle = null;
+ var n = this.instructions_.length;
+ var close, end, i, j, path, start;
+ // FIXME re-use destination array
+ // FIXME only re-transform if transform changed
+ var pixelPath = ol.replay.transformPath(this.path_, transform);
+ for (i = 0; i < n; ++i) {
+ var instruction = this.instructions_[i];
+ switch (instruction.command) {
+ case ol.replay.CanvasInstructionId.ADD_PATH:
+ end = instruction.end;
+ goog.asserts.assert(goog.isDef(end));
+ close = instruction.close;
+ goog.asserts.assert(goog.isDef(close));
+ start = instruction.start;
+ goog.asserts.assert(goog.isDef(start));
+ context.moveTo(pixelPath[start], pixelPath[start + 1]);
+ for (j = start + 2; j < end; j += 2) {
+ context.lineTo(pixelPath[j], pixelPath[j + 1]);
+ }
+ if (close) {
+ context.closePath();
+ }
+ break;
+ case ol.replay.CanvasInstructionId.BEGIN_PATH:
+ context.beginPath();
+ break;
+ case ol.replay.CanvasInstructionId.DRAW:
+ // FIXME handle alpha
+ if (!goog.isNull(fillStyle)) {
+ context.fillStyle = ol.replay.color(fillStyle.color);
+ context.fill();
+ }
+ if (!goog.isNull(strokeStyle)) {
+ context.lineWidth = strokeStyle.width;
+ context.strokeStyle = ol.replay.color(strokeStyle.color);
+ context.stroke();
+ }
+ break;
+ case ol.replay.CanvasInstructionId.SET_FILL_STYLE:
+ goog.asserts.assert(goog.isDef(instruction.fillStyle));
+ fillStyle = instruction.fillStyle;
+ break;
+ case ol.replay.CanvasInstructionId.SET_STROKE_STYLE:
+ goog.asserts.assert(goog.isDef(instruction.strokeStyle));
+ strokeStyle = instruction.strokeStyle;
+ break;
+ }
+ }
+};
+
+
+
+/**
+ * @constructor
+ * @extends {ol.replay.CanvasBase}
+ * @param {CanvasRenderingContext2D} context Context.
+ */
+ol.replay.Canvas = function(context) {
+
+ goog.base(this);
+
+ /**
+ * @private
+ * @type {CanvasRenderingContext2D}
+ */
+ this.context_ = context;
+
+ /**
+ * @private
+ * @type {goog.vec.Mat4.Number}
+ */
+ this.transform_ = goog.vec.Mat4.createNumberIdentity();
+
+};
+goog.inherits(ol.replay.Canvas, ol.replay.CanvasBase);
+
+
+/**
+ * @inheritDoc
+ */
+ol.replay.Canvas.prototype.createBatch = function(batchType) {
+ return new ol.replay.CanvasBatch(batchType);
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.replay.Canvas.prototype.drawBatch = function(batch) {
+ goog.asserts.assert(batch instanceof ol.replay.CanvasBatch);
+ var canvasBatch = /** @type {ol.replay.CanvasBatch} */ (batch);
+ canvasBatch.drawInternal(this.context_, this.transform_);
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.replay.Canvas.prototype.setTransform = function(transform) {
+ goog.vec.Mat4.setFromArray(this.transform_, transform);
+};
+
+
+/**
+ * @param {ol.Color} color Color.
+ * @return {string} Color.
+ */
+ol.replay.color = function(color) {
+ // FIXME handle alpha
+ return 'rgb(' + color.r + ',' + color.g + ',' + color.b + ')';
+};