1 /**
  2  * Constructs a new image with default properties. Images are not typically
  3  * constructed directly, but by adding to a panel or an existing mark via
  4  * {@link pv.Mark#add}.
  5  *
  6  * @class Represents an image, either a static resource or a dynamically-
  7  * generated pixel buffer. Images share the same layout and style properties as
  8  * bars. The external image resource is specified via the {@link #url}
  9  * property. The optional fill, if specified, appears beneath the image, while
 10  * the optional stroke appears above the image.
 11  *
 12  * <p>Dynamic images such as heatmaps are supported using the {@link #image}
 13  * psuedo-property. This function is passed the <i>x</i> and <i>y</i> index, in
 14  * addition to the current data stack. The return value is a {@link pv.Color},
 15  * or null for transparent. A string can also be returned, which will be parsed
 16  * into a color; however, it is typically much faster to return an object with
 17  * <tt>r</tt>, <tt>g</tt>, <tt>b</tt> and <tt>a</tt> attributes, to avoid the
 18  * cost of parsing and object instantiation.
 19  *
 20  * <p>See {@link pv.Bar} for details on positioning properties.
 21  *
 22  * @extends pv.Bar
 23  */
 24 pv.Image = function() {
 25   pv.Bar.call(this);
 26 };
 27 
 28 pv.Image.prototype = pv.extend(pv.Bar)
 29     .property("url", String)
 30     .property("imageWidth", Number)
 31     .property("imageHeight", Number);
 32 
 33 pv.Image.prototype.type = "image";
 34 
 35 /**
 36  * The URL of the image to display. The set of supported image types is
 37  * browser-dependent; PNG and JPEG are recommended.
 38  *
 39  * @type string
 40  * @name pv.Image.prototype.url
 41  */
 42 
 43 /**
 44  * The width of the image in pixels. For static images, this property is
 45  * computed implicitly from the loaded image resources. For dynamic images, this
 46  * property can be used to specify the width of the pixel buffer; otherwise, the
 47  * value is derived from the <tt>width</tt> property.
 48  *
 49  * @type number
 50  * @name pv.Image.prototype.imageWidth
 51  */
 52 
 53 /**
 54  * The height of the image in pixels. For static images, this property is
 55  * computed implicitly from the loaded image resources. For dynamic images, this
 56  * property can be used to specify the height of the pixel buffer; otherwise, the
 57  * value is derived from the <tt>height</tt> property.
 58  *
 59  * @type number
 60  * @name pv.Image.prototype.imageHeight
 61  */
 62 
 63 /**
 64  * Default properties for images. By default, there is no stroke or fill style.
 65  *
 66  * @type pv.Image
 67  */
 68 pv.Image.prototype.defaults = new pv.Image()
 69     .extend(pv.Bar.prototype.defaults)
 70     .fillStyle(null);
 71 
 72 /**
 73  * Specifies the dynamic image function. By default, no image function is
 74  * specified and the <tt>url</tt> property is used to load a static image
 75  * resource. If an image function is specified, it will be invoked for each
 76  * pixel in the image, based on the related <tt>imageWidth</tt> and
 77  * <tt>imageHeight</tt> properties.
 78  *
 79  * <p>For example, given a two-dimensional array <tt>heatmap</tt>, containing
 80  * numbers in the range [0, 1] in row-major order, a simple monochrome heatmap
 81  * image can be specified as:
 82  *
 83  * <pre>vis.add(pv.Image)
 84  *     .imageWidth(heatmap[0].length)
 85  *     .imageHeight(heatmap.length)
 86  *     .image(pv.ramp("white", "black").by(function(x, y) heatmap[y][x]));</pre>
 87  *
 88  * For fastest performance, use an ordinal scale which caches the fixed color
 89  * palette, or return an object literal with <tt>r</tt>, <tt>g</tt>, <tt>b</tt>
 90  * and <tt>a</tt> attributes. A {@link pv.Color} or string can also be returned,
 91  * though this typically results in slower performance.
 92  *
 93  * @param {function} f the new sizing function.
 94  * @returns {pv.Layout.Pack} this.
 95  */
 96 pv.Image.prototype.image = function(f) {
 97   /** @private */
 98   this.$image = function() {
 99       var c = f.apply(this, arguments);
100       return c == null ? pv.Color.transparent
101           : typeof c == "string" ? pv.color(c)
102           : c;
103     };
104   return this;
105 };
106 
107 /** @private Scan the proto chain for an image function. */
108 pv.Image.prototype.bind = function() {
109   pv.Bar.prototype.bind.call(this);
110   var binds = this.binds, mark = this;
111   do {
112     binds.image = mark.$image;
113   } while (!binds.image && (mark = mark.proto));
114 };
115 
116 /** @private */
117 pv.Image.prototype.buildImplied = function(s) {
118   pv.Bar.prototype.buildImplied.call(this, s);
119   if (!s.visible) return;
120 
121   /* Compute the implied image dimensions. */
122   if (s.imageWidth == null) s.imageWidth = s.width;
123   if (s.imageHeight == null) s.imageHeight = s.height;
124 
125   /* Compute the pixel values. */
126   if ((s.url == null) && this.binds.image) {
127 
128     /* Cache the canvas element to reuse across renders. */
129     var canvas = this.$canvas || (this.$canvas = document.createElement("canvas")),
130         context = canvas.getContext("2d"),
131         w = s.imageWidth,
132         h = s.imageHeight,
133         stack = pv.Mark.stack,
134         data;
135 
136     /* Evaluate the image function, storing into a CanvasPixelArray. */
137     canvas.width = w;
138     canvas.height = h;
139     data = (s.image = context.createImageData(w, h)).data;
140     stack.unshift(null, null);
141     for (var y = 0, p = 0; y < h; y++) {
142       stack[1] = y;
143       for (var x = 0; x < w; x++) {
144         stack[0] = x;
145         var color = this.binds.image.apply(this, stack);
146         data[p++] = color.r;
147         data[p++] = color.g;
148         data[p++] = color.b;
149         data[p++] = 255 * color.a;
150       }
151     }
152     stack.splice(0, 2);
153   }
154 };
155