API Docs for:
Show:

File: yui3-gallery/src/gallery-composite-image/js/composite-image.js

(function (Y) {
    'use strict';

    /**
     * @module gallery-composite-image
     */

    var _namespace = Y.namespace('Composite'),

        _getGetAtFunction,
        _getGetPixelIndexFunction,

        _class;

    /**
     * Image Class
     * @class Image
     * @constructor
     * @extends Base
     * @namespace Composite
     * @param {Object} config Configuration Object.
     */

    _class = function (config) {
        _class.superclass.constructor.call(this, config);
    };

    _class.ATTRS = {
        /**
         * Defines the color space for the image.
         *
         * An image can have as many channels as needed.  3 or 4 channels is
         * most common.  Usually the first channel is the red color component,
         * the second is green, the third is blue, and the fourth is alpha.
         *
         * There are various ways to represent the value of a channel.
         * Values can either be stored as integers or floating-point numbers.
         * Values may be constrained within a minimum and maximum value.
         *
         * Most commonly red, green, and blue channels are represented by
         * integers between 0 and 255.  In this case the values 0, 0, 0
         * represent black and the values 255, 255, 255 represent white.
         *
         * The alpha channel is commonly stored as a floating-point value from 0
         * to 1.  1 represents a completely visible pixel while 0 is completely
         * transparent.
         *
         * Values are not required to be constrained.  For example, sometimes it
         * is interesting or useful to store colors which have brightness beyond
         * white.
         *
         * This attribute should be set to an array of objects.  This array
         * represents the channels in order.  This array should have at least
         * one element.  This should not be a sparse array.  Each object must
         * have a mode property.  The mode property must be set to either 'f'
         * for floating-point values or 'i' for integer values.  Each object may
         * include the optional maximum and/or minimum properties.  Each object
         * may include the optional blackValue and/or whiteValue properties.  If
         * undefined, blackValue will default to 0 and whiteValue will default
         * to 255 when mode is set to 'i' or 1 otherwise.
         * 
         * @attribute channels
         * @default [
         *     {maximum: 255, minimum: 0, mode:'i'},
         *     {maximum: 255, minimum: 0, mode:'i'},
         *     {maximum: 255, minimum: 0, mode:'i'},
         *     {maximum: 1, minimum: 0, mode:'f'}
         * ]
         * @type Array
         * @writeOnce
         */
        channels: {
            value: [{
                maximum: 255,
                minimum: 0,
                mode: 'i'
            }, {
                maximum: 255,
                minimum: 0,
                mode: 'i'
            }, {
                maximum: 255,
                minimum: 0,
                mode: 'i'
            }, {
                maximum: 1,
                minimum: 0,
                mode: 'f'
            }],
            writeOnce: 'initOnly'
        },
        /**
         * Defines the pixel dimensions of the image.
         *
         * An image can have as many dimensions as needed.  2 dimensions is most
         * common.  Usually the first dimension is width and the second is
         * height.
         *
         * @attribute dimensions
         * @default [512, 512]
         * @type Array
         * @writeOnce
         */
        dimensions: {
            value: [
                512,
                512
            ],
            writeOnce: 'initOnly'
        },
        /**
         * Contains the number of pixels in the image.
         * @attribute pixelCount
         * @readOnly
         * @type Number
         */
        pixelCount: {
            readOnly: true
        }
    };

    _class.NAME = 'Composite-Image';

    Y.extend(_class, Y.Base, {
        /**
         * Clears the image.
         * This method is chainable.
         * @method clear
         * @chainable
         */
        clear: function () {
            var me = this,
                pixelData = [];
            
            pixelData.length = me._pixelData.length;
            
            me._pixelData = pixelData;

            return me;
        },
        /**
         * Returns a new identical image.
         * @method clone
         * @return {Object}
         */
        clone: function () {
            var me = this,
                other = new _class({
                    channels: me.get('channels'),
                    dimensions: me.get('dimensions')
                });

            other._pixelData = me._pixelData.slice();

            return other;
        },
        /**
         * Invokes a function for each pixel in the image.
         * This method is chainable.
         * @method eachPixel
         * @chainable
         * @param {Object} pixelParameters This object contains the following
         * members
         * <ul>
         *     <li>
         *         ctx - Object - Optional scope with which to call fn.
         *     </li>
         *     <li>
         *         fn - Function - This function is invoked once per pixel.
         *         This function will receive an object with the following
         *         parameters
         *         <ul>
         *             <li>
         *                 at - Array of image pixel coordinates.
         *             </li>
         *             <li>
         *                 chs - Array of image channel definitions.
         *             </li>
         *             <li>
         *                 dims - Array of image dimension lengths.
         *             </li>
         *             <li>
         *                 img - Reference to this image.
         *             </li>
         *             <li>
         *                 pch - Array of pixel channel indices.
         *             </li>
         *             <li>
         *                 pcnt - Number of pixels in this image.
         *             </li>
         *             <li>
         *                 pix - Integer index of this pixel.
         *             </li>
         *             <li>
         *                 pxl - Array of pixel channel values.
         *             </li>
         *         </ul>
         *         If this function returns true, eachPixel will stop and ignore
         *         the remaining pixels.
         *     </li>
         *     <li>
         *         pch - Array - Optional array of channel indexes.  If
         *         undefined, pixels will contain all channels in order.
         *     </li>
         * </ul>
         * @param {Function} callbackFunction This function is invoked after the
         * function has been invoked for each pixel in the image.  This function
         * will receive an object with the following members
         * <ul>
         *     <li>
         *         chs - Array of image channel definitions.
         *     </li>
         *     <li>
         *         dims - Array of image dimension lengths.
         *     </li>
         *     <li>
         *         img - Reference to this image.
         *     </li>
         *     <li>
         *         pcnt - Number of pixels in this image.
         *     </li>
         *     <li>
         *         pix - The last integer pixel index processed.  If eachPixel
         *         was not stopped early, this value should be equal to pcnt and
         *         not a valid pixel index.
         *     </li>
         * </ul>
         * @param {Object} contextObject Optional scope with which to call the
         * callback function.
         */
        eachPixel: function (pixelParameters, callbackFunction, contextObject) {
            var ctx = pixelParameters.ctx,
                fn = pixelParameters.fn,
                me = this,
                pixelChannels = pixelParameters.pch,
                pixelCount = me.get('pixelCount'),
            
                channels = Y.clone(me.get('channels')),
                dimensions = me.get('dimensions').slice(),
                getAt = _getGetAtFunction.apply(me, dimensions),

                thisPixel;

            thisPixel = function (pixelIndex) {
                if (pixelIndex >= pixelCount) {
                    callbackFunction.call(contextObject, {
                        chs: channels,
                        dims: dimensions,
                        img: this,
                        pcnt: pixelCount,
                        pix: pixelIndex
                    });
                    return;
                }

                Y.later(0, this, function () {
                    var at = getAt(pixelIndex),
                        me = this;

                    if (fn.call(ctx, {
                        at: at,
                        chs: channels,
                        dims: dimensions,
                        img: me,
                        pch: pixelChannels,
                        pcnt: pixelCount,
                        pix: pixelIndex,
                        pxl: me.getPixel(at, pixelChannels)
                    })) {
                        callbackFunction.call(contextObject, {
                            chs: channels,
                            dims: dimensions,
                            img: me,
                            pcnt: pixelCount,
                            pix: pixelIndex
                        });
                    } else {
                        thisPixel.call(me, pixelIndex + 1);
                    }
                });
            };

            thisPixel.call(me, 0);
            return me;
        },
        /**
         * Converts a pixel index to an at array.
         * @method getAt
         * @param {Number} pixelIndex
         * @return {Array}
         */
        getAt: function (pixelIndex) {
            return _getGetAtFunction.apply(this, this.get('dimensions'))(pixelIndex);
        },
        /**
         * Accessor method to get a pixel from the image.
         * @method getPixel
         * @param {Array} at Array containing pixel coordinates.  The length of
         * this array should match the number of dimensions of the image.
         * @param {Array} pixelChannels Optional array of channel indexes.  If
         * undefined, returned pixel will contain all channels in order.
         * @return {Array}
         */
        getPixel: function (at, pixelChannels) {
            var me = this,
                pixelData = me._pixelData,
                pixelDataIndex,

                channelsLength = me.get('channels.length');

            pixelDataIndex = _getGetPixelIndexFunction.apply(me, me.get('dimensions'))(at) * channelsLength;

            if (pixelChannels) {
                return (function (pixelData, pixelDataIndex) {
                    var i,
                        pixel = [],
                        pixelChannelsLength;

                    for (i = 0, pixelChannelsLength = pixelChannels.length; i < pixelChannelsLength; i += 1) {
                        pixel[i] = pixelData[pixelDataIndex + pixelChannels[i]];
                    }

                    return pixel;
                }(pixelData, pixelDataIndex));
            }

            return pixelData.slice(pixelDataIndex, pixelDataIndex + channelsLength);
        },
        /**
         * Converts an at array to a pixel index.
         * @method getPixelIndex
         * @param {Array} at
         * @return {Number}
         */
        getPixelIndex: function (at) {
            return _getGetPixelIndexFunction.apply(this, this.get('dimensions'))(at);
        },
        initializer: function () {
            var me = this,

                channels = me.get('channels'),
                dimensions = me.get('dimensions'),
                pixelCount = 1,
                pixelData = [];

            if (!channels.length) {
                throw 'Image must have at least one channel.';
            }

            if (!dimensions.length) {
                throw 'Image must have at least one dimension.';
            }

            Y.each(channels, function (channel) {
                channel.blackValue = channel.blackValue || 0;
                channel.whiteValue = channel.whiteValue || (channel.mode === 'i' ? 255 : 1);
            });

            Y.each(dimensions, function (dimension) {
                if (dimension <= 0) {
                    throw 'Dimension must have at least 1 pixel.';
                }

                pixelCount *= dimension;
            });

            me._set('pixelCount', pixelCount);

            pixelData.length = pixelCount * channels.length;
            
            this._pixelData = pixelData;
        },
        /**
         * Accessor method to set a pixel in the image.
         * This method is chainable.
         * @method setPixel
         * @chainable
         * @param {Array} at Array containing pixel coordinates.  The length of
         * this array should match the number of dimensions of the image.
         * @param {Array} pixel Array containing the pixel's channel values.
         * @param {Array} pixelChannels Optional array of channel indexes.  If
         * undefined, the given pixel is assumed to contain all channels in
         * order.
         */
        setPixel: function (at, pixel, pixelChannels) {
            var i,
                me = this,
                pixelChannelsLength,
                pixelData = me._pixelData,
                pixelDataIndex,

                channels = me.get('channels'),
                channelsLength = channels.length;

            pixelDataIndex = _getGetPixelIndexFunction.apply(me, me.get('dimensions'))(at) * channelsLength;

            if (pixelChannels) {
                for (i = 0, pixelChannelsLength = pixelChannels.length; i < pixelChannelsLength; i += 1) {
                    pixelData[pixelDataIndex + pixelChannels[i]] = _class.conformChannelValue(pixel[i], channels[pixelChannels[i]]);
                }
            } else {
                for (i = 0; i < channelsLength; i += 1) {
                    pixelData[pixelDataIndex + i] = _class.conformChannelValue(pixel[i], channels[i]);
                }
            }

            return this;
        }
    }, {
        /**
         * Conforms the given value to the channel's specifications.
         * @method conformChannelValue
         * @param {Number} value
         * @param {Object} channel
         * @return {Number}
         * @static
         */
        conformChannelValue: function (value, channel) {
            value = +value || 0;

            if (channel.maximum) {
                value = Math.min(value, channel.maximum);
            }

            if (channel.minimum) {
                value = Math.max(value, channel.minimum);
            }

            if (channel.mode === 'i') {
                value = Math.round(value);
            }

            return value;
        }
    });

    /**
     * Call this function with the length of each dimension followed by the
     * number of channels.  Returns a function which accepts a pixel index and
     * returns an at array.
     * @method _getGetAtFunction
     * @private
     * @return Function
     */
    _getGetAtFunction = Y.cached(function () {
        var dimensionLengths = arguments,
            floor = Math.floor,

            dimensionsLength = dimensionLengths.length;

        return Y.cached(function (pixelIndex) {
            var at = [
                    pixelIndex % dimensionLengths[0]
                ],
                i,
                j,
                product;

            for (i = 1; i < dimensionsLength; i += 1) {
                product = 1;

                for (j = 0; j < i; j += 1) {
                    product *= dimensionLengths[j];
                }

                at[i] = floor(pixelIndex / product) % i;
            }

            return at;
        });
    });

    /**
     * Call this function with the length of each dimension.
     * Returns a function which accepts dimension indices and returns a pixel
     * index.
     * @method _getGetPixelIndexFunction
     * @private
     * @return Function
     */
    _getGetPixelIndexFunction = Y.cached(function () {
        var dimensionLengths = arguments,

            dimensionsLength = dimensionLengths.length;

        return Y.cached(function () {
            var dimensionIndices = arguments,
                i,
                index = 0,
                j,
                offset;

            for (i = 0; i < dimensionsLength; i += 1) {
                offset = dimensionIndices[i];

                for (j = i - 1; j > 0; j -= 1) {
                    offset *= dimensionLengths[j];
                }

                index += offset;
            }

            return index;
        });
    });

    _namespace.Image = _class;
}(Y));