diff --git a/examples/raster.html b/examples/raster.html
new file mode 100644
index 0000000000..52553ef66d
--- /dev/null
+++ b/examples/raster.html
@@ -0,0 +1,13 @@
+---
+template: example.html
+title: Raster Source
+shortdesc: Demonstrates pixelwise operations with a raster source.
+docs: >
+ A dynamically generated raster source.
+tags: "raster, pixel"
+---
+
diff --git a/examples/raster.js b/examples/raster.js
new file mode 100644
index 0000000000..34a95f852e
--- /dev/null
+++ b/examples/raster.js
@@ -0,0 +1,28 @@
+goog.require('ol.Map');
+goog.require('ol.View');
+goog.require('ol.layer.Image');
+goog.require('ol.source.OSM');
+goog.require('ol.source.Raster');
+
+
+var map = new ol.Map({
+ layers: [
+ new ol.layer.Image({
+ source: new ol.source.Raster({
+ sources: [new ol.source.OSM()],
+ operations: [function(pixels) {
+ var pixel = pixels[0];
+ var b = pixel[2];
+ pixel[2] = pixel[0];
+ pixel[0] = b;
+ return pixels;
+ }]
+ })
+ })
+ ],
+ target: 'map',
+ view: new ol.View({
+ center: [0, 0],
+ zoom: 2
+ })
+});
diff --git a/externs/olx.js b/externs/olx.js
index 4f8aa300cf..da432b6c26 100644
--- a/externs/olx.js
+++ b/externs/olx.js
@@ -4498,6 +4498,31 @@ olx.source.ImageVectorOptions.prototype.source;
olx.source.ImageVectorOptions.prototype.style;
+/**
+ * @typedef {{sources: Array.,
+ * operations: (Array.|undefined)}}
+ * @api
+ */
+olx.source.RasterOptions;
+
+
+/**
+ * Input sources.
+ * @type {Array.}
+ * @api
+ */
+olx.source.RasterOptions.prototype.sources;
+
+
+/**
+ * Pixel operations. Operations will be called with pixels from input sources
+ * and the final output will be assigned to the raster source.
+ * @type {Array.|undefined}
+ * @api
+ */
+olx.source.RasterOptions.prototype.operations;
+
+
/**
* @typedef {{attributions: (Array.|undefined),
* crossOrigin: (null|string|undefined),
diff --git a/src/ol/raster/operation.js b/src/ol/raster/operation.js
new file mode 100644
index 0000000000..b0430fba6b
--- /dev/null
+++ b/src/ol/raster/operation.js
@@ -0,0 +1,24 @@
+goog.provide('ol.raster.IdentityOp');
+goog.provide('ol.raster.Operation');
+
+
+/**
+ * A function that takes an array of {@link ol.raster.Pixel} as inputs, performs
+ * some operation on them, and returns an array of {@link ol.raster.Pixel} as
+ * outputs.
+ *
+ * @typedef {function(Array.): Array.}
+ * @api
+ */
+ol.raster.Operation;
+
+
+/**
+ * The identity operation for pixels. Returns the supplied input pixels as
+ * outputs.
+ * @param {Array.} inputs Input pixels.
+ * @return {Array.} The input pixels as output.
+ */
+ol.raster.IdentityOp = function(inputs) {
+ return inputs;
+};
diff --git a/src/ol/raster/pixel.js b/src/ol/raster/pixel.js
new file mode 100644
index 0000000000..d6b2d2bfd4
--- /dev/null
+++ b/src/ol/raster/pixel.js
@@ -0,0 +1,9 @@
+goog.provide('ol.raster.Pixel');
+
+
+/**
+ * An array of numbers representing pixel values.
+ * @typedef {Array.} ol.raster.Pixel
+ * @api
+ */
+ol.raster.Pixel;
diff --git a/src/ol/renderer/layerrenderer.js b/src/ol/renderer/layerrenderer.js
index f1557c9f2e..6809d8c97e 100644
--- a/src/ol/renderer/layerrenderer.js
+++ b/src/ol/renderer/layerrenderer.js
@@ -113,7 +113,6 @@ ol.renderer.Layer.prototype.createLoadedTileFinder = function(source, tiles) {
/**
- * @protected
* @return {ol.layer.Layer} Layer.
*/
ol.renderer.Layer.prototype.getLayer = function() {
diff --git a/src/ol/source/rastersource.js b/src/ol/source/rastersource.js
new file mode 100644
index 0000000000..8acfdbf9fd
--- /dev/null
+++ b/src/ol/source/rastersource.js
@@ -0,0 +1,316 @@
+goog.provide('ol.source.Raster');
+
+goog.require('goog.asserts');
+goog.require('goog.functions');
+goog.require('goog.vec.Mat4');
+goog.require('ol.ImageCanvas');
+goog.require('ol.TileQueue');
+goog.require('ol.dom');
+goog.require('ol.extent');
+goog.require('ol.layer.Image');
+goog.require('ol.layer.Tile');
+goog.require('ol.raster.IdentityOp');
+goog.require('ol.renderer.canvas.ImageLayer');
+goog.require('ol.renderer.canvas.TileLayer');
+goog.require('ol.source.Image');
+goog.require('ol.source.Tile');
+
+
+
+/**
+ * @classdesc
+ * An source that transforms data from any number of input sources source using
+ * an array of {@link ol.raster.Operation} functions to transform input pixel
+ * values into output pixel values.
+ *
+ * @constructor
+ * @extends {ol.source.Image}
+ * @param {olx.source.RasterOptions} options Options.
+ * @api
+ */
+ol.source.Raster = function(options) {
+
+ /**
+ * @private
+ * @type {Array.}
+ */
+ this.operations_ = goog.isDef(options.operations) ?
+ options.operations : [ol.raster.IdentityOp];
+
+ /**
+ * @private
+ * @type {Array.}
+ */
+ this.renderers_ = ol.source.Raster.createRenderers_(options.sources);
+
+ /**
+ * @private
+ * @type {CanvasRenderingContext2D}
+ */
+ this.canvasContext_ = ol.dom.createCanvasContext2D();
+
+ /**
+ * @private
+ * @type {ol.TileQueue}
+ */
+ this.tileQueue_ = new ol.TileQueue(
+ goog.functions.constant(1),
+ goog.bind(this.changed, this));
+
+ var layerStatesArray = ol.source.Raster.getLayerStatesArray_(this.renderers_);
+ var layerStates = {};
+ for (var i = 0, ii = layerStatesArray.length; i < ii; ++i) {
+ layerStates[goog.getUid(layerStatesArray[i].layer)] = layerStatesArray[i];
+ }
+
+ /**
+ * @private
+ * @type {olx.FrameState}
+ */
+ this.frameState_ = {
+ animate: false,
+ attributions: {},
+ coordinateToPixelMatrix: goog.vec.Mat4.createNumber(),
+ extent: null,
+ focus: null,
+ index: 0,
+ layerStates: layerStates,
+ layerStatesArray: layerStatesArray,
+ logos: {},
+ pixelRatio: 1,
+ pixelToCoordinateMatrix: goog.vec.Mat4.createNumber(),
+ postRenderFunctions: [],
+ size: [0, 0],
+ skippedFeatureUids: {},
+ tileQueue: this.tileQueue_,
+ time: Date.now(),
+ usedTiles: {},
+ viewState: /** @type {olx.ViewState} */ ({
+ rotation: 0
+ }),
+ viewHints: [],
+ wantedTiles: {}
+ };
+
+ goog.base(this, {
+ // TODO: pass along any relevant options
+ });
+
+
+};
+goog.inherits(ol.source.Raster, ol.source.Image);
+
+
+/**
+ * Update the stored frame state.
+ * @param {ol.Extent} extent The view extent (in map units).
+ * @param {number} resolution The view resolution.
+ * @param {ol.proj.Projection} projection The view projection.
+ * @return {olx.FrameState} The updated frame state.
+ * @private
+ */
+ol.source.Raster.prototype.updateFrameState_ =
+ function(extent, resolution, projection) {
+ var frameState = this.frameState_;
+
+ var center = ol.extent.getCenter(extent);
+ var width = ol.extent.getWidth(extent) / resolution;
+ var height = ol.extent.getHeight(extent) / resolution;
+
+ frameState.extent = extent;
+ frameState.focus = ol.extent.getCenter(extent);
+ frameState.size[0] = width;
+ frameState.size[1] = height;
+
+ var viewState = frameState.viewState;
+ viewState.center = center;
+ viewState.projection = projection;
+ viewState.resolution = resolution;
+ return frameState;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.source.Raster.prototype.getImage =
+ function(extent, resolution, pixelRatio, projection) {
+
+ var context = this.canvasContext_;
+ var canvas = context.canvas;
+
+ var width = ol.extent.getWidth(extent) / resolution;
+ var height = ol.extent.getHeight(extent) / resolution;
+
+ if (width !== canvas.width ||
+ height !== canvas.height) {
+ canvas.width = width;
+ canvas.height = height;
+ }
+
+ var frameState = this.updateFrameState_(extent, resolution, projection);
+ this.composeFrame_(frameState);
+
+ var imageCanvas = new ol.ImageCanvas(extent, resolution, 1,
+ this.getAttributions(), canvas);
+
+ return imageCanvas;
+};
+
+
+/**
+ * Compose the frame. This renders data from all sources, runs pixel-wise
+ * operations, and renders the result to the stored canvas context.
+ * @param {olx.FrameState} frameState The frame state.
+ * @private
+ */
+ol.source.Raster.prototype.composeFrame_ = function(frameState) {
+ var len = this.renderers_.length;
+ var imageDatas = new Array(len);
+ var pixels = new Array(len);
+
+ var context = this.canvasContext_;
+ var canvas = context.canvas;
+
+ for (var i = 0; i < len; ++i) {
+ pixels[i] = [0, 0, 0, 0];
+ imageDatas[i] = ol.source.Raster.getImageData_(
+ this.renderers_[i], canvas.width, canvas.height,
+ frameState, frameState.layerStatesArray[i]);
+ }
+
+ var targetImageData = context.getImageData(0, 0, canvas.width, canvas.height);
+ var target = targetImageData.data;
+
+ var source, pixel;
+ for (var j = 0, jj = target.length; j < jj; j += 4) {
+ for (var k = 0; k < len; ++k) {
+ source = imageDatas[k].data;
+ pixel = pixels[k];
+ pixel[0] = source[j];
+ pixel[1] = source[j + 1];
+ pixel[2] = source[j + 2];
+ pixel[3] = source[j + 3];
+ }
+ this.transformPixels_(pixels);
+ pixel = pixels[0];
+ target[j] = pixel[0];
+ target[j + 1] = pixel[1];
+ target[j + 2] = pixel[2];
+ target[j + 3] = pixel[3];
+ }
+ context.putImageData(targetImageData, 0, 0);
+
+ frameState.tileQueue.loadMoreTiles(16, 16);
+};
+
+
+/**
+ * Run pixel-wise operations to transform pixels.
+ * @param {Array.} pixels The input pixels.
+ * @return {Array.} The modified pixels.
+ * @private
+ */
+ol.source.Raster.prototype.transformPixels_ = function(pixels) {
+ for (var i = 0, ii = this.operations_.length; i < ii; ++i) {
+ pixels = this.operations_[i](pixels);
+ }
+ return pixels;
+};
+
+
+/**
+ * Get image data from a renderer.
+ * @param {ol.renderer.canvas.Layer} renderer Layer renderer.
+ * @param {number} width Data width.
+ * @param {number} height Data height.
+ * @param {olx.FrameState} frameState The frame state.
+ * @param {ol.layer.LayerState} layerState The layer state.
+ * @return {ImageData} The image data.
+ * @private
+ */
+ol.source.Raster.getImageData_ =
+ function(renderer, width, height, frameState, layerState) {
+ renderer.prepareFrame(frameState, layerState);
+ var canvas = renderer.getImage();
+ var imageTransform = renderer.getImageTransform();
+ var dx = goog.vec.Mat4.getElement(imageTransform, 0, 3);
+ var dy = goog.vec.Mat4.getElement(imageTransform, 1, 3);
+ return canvas.getContext('2d').getImageData(
+ Math.round(-dx), Math.round(-dy),
+ width, height);
+};
+
+
+/**
+ * Get a list of layer states from a list of renderers.
+ * @param {Array.} renderers Layer renderers.
+ * @return {Array.} The layer states.
+ * @private
+ */
+ol.source.Raster.getLayerStatesArray_ = function(renderers) {
+ return renderers.map(function(renderer) {
+ return renderer.getLayer().getLayerState();
+ });
+};
+
+
+/**
+ * Create renderers for all sources.
+ * @param {Array.} sources The sources.
+ * @return {Array.} Array of layer renderers.
+ * @private
+ */
+ol.source.Raster.createRenderers_ = function(sources) {
+ var len = sources.length;
+ var renderers = new Array(len);
+ for (var i = 0; i < len; ++i) {
+ renderers[i] = ol.source.Raster.createRenderer_(sources[i]);
+ }
+ return renderers;
+};
+
+
+/**
+ * Create a renderer for the provided source.
+ * @param {ol.source.Source} source The source.
+ * @return {ol.renderer.canvas.Layer} The renderer.
+ * @private
+ */
+ol.source.Raster.createRenderer_ = function(source) {
+ var renderer = null;
+ if (source instanceof ol.source.Tile) {
+ renderer = ol.source.Raster.createTileRenderer_(
+ /** @type {ol.source.Tile} */ (source));
+ } else if (source instanceof ol.source.Image) {
+ renderer = ol.source.Raster.createImageRenderer_(
+ /** @type {ol.source.Image} */ (source));
+ } else {
+ goog.asserts.fail('Unsupported source type: ' + source);
+ }
+ return renderer;
+};
+
+
+/**
+ * Create an image renderer for the provided source.
+ * @param {ol.source.Image} source The source.
+ * @return {ol.renderer.canvas.Layer} The renderer.
+ * @private
+ */
+ol.source.Raster.createImageRenderer_ = function(source) {
+ var layer = new ol.layer.Image({source: source});
+ return new ol.renderer.canvas.ImageLayer(layer);
+};
+
+
+/**
+ * Create a tile renderer for the provided source.
+ * @param {ol.source.Tile} source The source.
+ * @return {ol.renderer.canvas.Layer} The renderer.
+ * @private
+ */
+ol.source.Raster.createTileRenderer_ = function(source) {
+ var layer = new ol.layer.Tile({source: source});
+ return new ol.renderer.canvas.TileLayer(layer);
+};