Source: progress/donut.js

/**
 * @namespace ludo.progress
 */
/**
 * Donut Progress bar
 *  The progress bar is created using SVG. It is made out of 4 elements in this stacking order(bottom to top)
 *
 *  1) The background svg path rendered with the css class '.ludo-progress-bg' and styles defined in bgStyles object
 *  2) Eventual background image defined in bgPattern. If the background path(1) has a border, the background image will be
 *  shrinked to fit inside. The background image will be repeated when smaller than the progress bar. If bigger, it will be scaled
 *  down.
 *  3) Progress Bar SVG path
 *  4) Eventual background image defined in frontPattern.
 * 
 * @augments ludo.progress.Bar
 * @param {Object} config
 * @param {Function} config.innerRadius - Function returning inner radius. Arguments to this function : 1) outer radius.
 * default: function(outerRadius){ return outerRadius * 0.5 }
 * @param {Number} config.steps Number of progress bar steps, default = 10
 * @param {Number} config.progress Initial step, default = 0
 * @param {Number} config.startAngle Start angle in range 0-360. Default = 0(top)
 * @param {float} config.textSizeRatio Size of text relative to inner radius, default: 0.3
 * @param {float} config.bgStyles SVG background styles
 * @param {float} config.barStyles SVG moving bar styles
 * @param {float} config.textStyles Styling of text on progress bar
 * @param {String} config.bgPattern Path to background image for the progress bar background. 
 * @param {String} config.frontPattern Path to background image for the progress bar. The background images will be repeated if smaller than the progress bar. If bigger, it will be scaled down.
 * @param {Function} config.easing Easing function for animation. default: ludo.svg.easing.linear
 * @param {Number} config.animationDuration Animation duration in milliseconds (1/1000s) - default: 100
 * @fires ludo.progress.Bar#change Fired when value is changed. Arguments. 1) Percent completed 2) current step 3) number of steps, 4) ludo.progress.Bar 4) Current percentage.
 * If animated, the change event will be triggered once animation is complete.
 * @fires ludo.progress.Bar#animate Fired during progress animation and when value is changed. This is a good event
 * to listen to when you want to update texts for the progress bar. Arguments. 1) Animated percent completed

 */
ludo.progress.Donut = new Class({
    Extends: ludo.progress.Bar,
    steps: 10,
    progress: 0,
    textSizeRatio: 0.3,

    outerBorderWidth: 0,
    innerBorderWidth: 0,

    startAngle : 0,



    __construct:function(config){
        this.parent(config);
        this.setConfigParams(config, ['innerRadius', 'startAngle']);
    },


    applyPattern:function(){

        var s = this.svg();


        if(this.bgPattern){
            this.els.bgPatternPath = s.$('path');
            s.append(this.els.bgPatternPath);
            this.els.bgPatternPath.setPattern(this.els.bgPattern);
            this.els.bgPatternPath.set('fill-rule', 'evenodd');
        }


        if(this.frontPattern){
            this.els.frontPatternPath = s.$('path');
            this.els.frontGroup.append(this.els.frontPatternPath);
            this.els.frontPatternPath.setPattern(this.els.frontPattern);
            this.els.frontPatternPath.clip(this.els.clipPath);
            this.els.frontPatternPath.set('fill-rule', 'evenodd');
        }
    },


    renderBar: function () {
        var s = this.svg();

        this.createStyles();
        this.createClipPath();

        this.createPattern();


        this.els.bg = s.$('path');
        this.els.bg.addClass('ludo-progress-donut-bg-svg');
        s.append(this.els.bg);
        this.els.bg.set('fill-rule', 'evenodd');

        if(this.bgStyles){
            this.els.bg.css(this.bgStyles);
            if(this.bgStyles['stroke-width'] != undefined){
                this.outerBorderWidth = parseInt(this.bgStyles['stroke-width']);
            }
        }

        this.els.frontGroup = s.$('g');
        s.append(this.els.frontGroup);

        this.els.bar = s.$('path');
        this.els.bar.addClass('ludo-progress-donut-bar-svg');
        this.els.frontGroup.append(this.els.bar);
        this.els.bar.set('fill-rule', 'evenodd');
        this.els.frontGroup.clip(this.els.clipPath);
        if(this.barStyles){
            this.els.bar.css(this.barStyles);
            if(this.barStyles['stroke-width'] != undefined){
                this.outerBorderWidth = parseInt(this.barStyles['stroke-width']);
            }
        }
        if (this._text != undefined) {
            this.text(this._text);
        }

        this.applyPattern();

        this.els.frontGroup.toFront();

        this.els.debug = s.$('path');
        s.append(this.els.debug);
        this.els.debug.css('fill', '#ff0000');

        if(this.els.frontPatternPath){
            this.els.frontPatternPath.toFront();
        }

    },

    createClipPath: function () {
        var s = this.svg();
        this.els.clipPath = s.$('clipPath');
        s.appendDef(this.els.clipPath);
        this.els.clip = s.$('path');
        this.els.clipPath.append(this.els.clip);

    },

    createStyles: function () {
        var s = this.svg();
        var cls = 'ludo-progress-donut-bg';
        var styles = ludo.svg.Util.pathStyles(cls);
        this.outerBorderWidth = parseInt(styles['stroke-width']);

        s.addStyleSheet(cls + '-svg', styles);

        cls = 'ludo-progress-donut-bar';
        styles = ludo.svg.Util.pathStyles(cls);
        this.innerBorderWidth = parseInt(styles['stroke-width']);

        s.addStyleSheet(cls + '-svg', styles);
    },

    resize: function (size) {

        this.parent(size);

        this.resizeItems();

        this.ratio(this.progress / this.steps, false);


    },


    updatePatternSize: function () {
        var s = this.rect();

        if (this.patternSize != undefined) {

            if(this.patternSize.x > s){
                this.els.bgImage.set('width', s);
                this.els.bgImage.set('height', s);
            }

            this.els.bgPattern.set('width', Math.min(1, this.patternSize.x /s));
            this.els.bgPattern.set('height', Math.min(1, this.patternSize.y / s));
        }

        if(this.frontPatternSize){

            if(this.frontPatternSize.x > s){
                this.els.frontImage.set('width', s);
                this.els.frontImage.set('height', s);
            }
            this.els.frontPattern.set('width', Math.min(1, this.frontPatternSize.x /s));
            this.els.frontPattern.set('height', Math.min(1, this.frontPatternSize.y / s));
        }
    },
    
    resizeItems: function () {
        var s = this.rect();
        var c = s / 2;
        var radius = c - 2;
        var innerRadius = this.innerRadius(radius);

        this.els.bg.set('d', this.bgPath(radius, innerRadius));

        this.els.bar.set('d', this.bgPath(radius - this.outerBorderWidth, innerRadius + this.outerBorderWidth));

        if(this.els.bgPatternPath){
            this.els.bgPatternPath.set('d', this.bgPath(radius - this.outerBorderWidth, innerRadius + this.outerBorderWidth));
        }

        if(this.els.frontPatternPath){
            var p = this.bgPath(radius - this.outerBorderWidth - this.innerBorderWidth, innerRadius + this.outerBorderWidth + this.innerBorderWidth);
            this.els.frontPatternPath.set('d', p);
        }



        this.updatePatternSize();

        this.positionTextNode();
    },


    /**
     * Display text on progress bar
     * @param {String} txt
     * @memberof ludo.progress.Donut.prototype
     */
    text: function (txt) {
        if (this.els.textNode == undefined) {
            this.els.textNode = this.svg().$('text');
            var styles = ludo.svg.Util.textStyles('ludo-progress-donut-text');
            this.svg().addStyleSheet('ludo-progress-donut-text-svg', styles);
            this.els.textNode.addClass('ludo-progress-donut-text-svg');
            this.els.textNode.set('text-anchor', 'middle');
            this.els.textNode.set('alignment-baseline', 'central');
            this.svg().append(this.els.textNode);
            if (this.textStyles != undefined) {
                this.els.textNode.css(this.textStyles);
            }
            this.positionTextNode();
        }

        this.els.textNode.text(txt);
    },

    positionTextNode: function () {
        if (this.els.textNode) {
            var c = this.rect() / 2;
            this.els.textNode.set('x', c);
            this.els.textNode.set('y', c);
            this.els.textNode.css('font-size',  this.innerRadius(this.rect()) * this.textSizeRatio);
        }
    },

    bgPath: function (radius, innerRadius) {
        return this.circlePath(radius) + this.circlePath(innerRadius);
    },

    circlePath: function (r) {
        var c = this.rect() / 2;

        var s = c - r;
        var e = c + r;

        return [
            'M', s, c,
            'A', r, r, 90, 0, 1, c, s,
            'A', r, r, 90, 0, 1, e, c,
            'A', r, r, 90, 0, 1, c, e,
            'A', r, r, 90, 0, 1, s, c
        ].join(' ');

    },

    rect: function () {
        return Math.min(this.svg().width, this.svg().height);
    },

    ratio: function (ratio, animate) {
        if (arguments.length == 0) {
            return this.progress / this.steps;
        }
        ratio = ludo.util.clamp(ratio, 0, 1);
        if(animate){
            var p= this.clipArray(ratio).join(' ');
            var s = this.rect();
            var c = s / 2;
            var a = this.startAngle;
            var diff = ratio - this.lastRatio;
            this.els.clip.set('d', this.clipArray(this.lastRatio).join(' '));
            this.els.clip.animate({
                d : p
            },{
                easing:this.easing,
                duration:this.animationDuration,
                step:function(value, delta, elapsed){
                    if(ratio == 1 && elapsed == 1)value[9] = 359.99999;
                    var d = value[9] - 90 + a;

                    var r = ludo.geometry.toRadians(d);
                    var x = c + (Math.cos(r) * 30000);
                    var y = c + (Math.sin(r) * 30000);
                    value[12] = x;
                    value[13] = y;
                    return value;
                },
                progress: function (t) {
                    var prs = Math.min(100, (this.lastRatio + (diff * t)) * 100);
                    this.fireEvent('animate', prs);
                }.bind(this),
                complete:function(){
                    this.lastRatio = ratio;
                    this.fireEvent('animate', this.lastRatio * 100);
                    this.onChange();
                }.bind(this)
            });
        }else{
            var path = this.clipArray(Math.min(ratio, 0.9999999));
            this.els.clip.set('d', path.join(' '));

            if(ratio != this.lastRatio){
                this.onChange();
            }
            this.fireEvent('animate', this.lastRatio * 100);
            this.lastRatio = ratio;
        }
    },

    clipArray: function (ratio) {
        var degrees = 360 * ratio;
        var radians = ludo.geometry.toRadians(degrees - 90 + this.startAngle);
        var s = this.rect();
        var c = s / 2;

        var radius = 30000;

        var radStart = ludo.geometry.toRadians(- 90 + this.startAngle);
        var xStart = c + (Math.cos(radStart) * radius)
        var yStart = c + (Math.sin(radStart) * radius)

        var x = c + (Math.cos(radians) * radius);
        var y = c + (Math.sin(radians) * radius);

        return [
            'M', c, c,
            'L', xStart, yStart,
            'A', radius, radius, degrees , 1, 1, x, y, 'Z'
        ]

    },

    innerRadius:function(outerRadius){
        return outerRadius * 0.5;
    }
});