function ElementScroller (settings) {
    
    var self = this;
    
    // defalut settings
    this.defaultSettings = {
        top: '50%',
        marginTop: '0',
        scrollInterval: 20,
        scrollFraction: 0.1,
        element: '',
        latency: 150,
        threshold: 1
    }
    
    // apply user settings
    this.settings = $.extend(this.defaultSettings, settings);
    
    // find the scrolling element
    this.element = $(this.settings.element);
    // parse offsets and margins
    this.position = {
        top: this.parseCssValue(this.settings.top)
    }
    this.margin = {
        top: this.parseCssValue(this.settings.marginTop)
    }
    
    this.lastChange = null;
    
    // set basic css
    this.element.css({position: 'absolute'});
    
    // bind to resize and scroll events
    $(window).scroll(function(){self.onChange()});
    $(window).resize(function(){self.onChange()});
    
    // initiate
    this.onChange();
    
}

ElementScroller.prototype.onChange = function (){
    
    var self = this;
    
    // clear pending timeout before setting a new one
    clearTimeout(this.lastChange);
    this.lastChange = setTimeout(function(){self.animate()}, this.settings.latency);
    
}

ElementScroller.prototype.animate = function (){
    
    var self = this;
    var a;
    if (Math.abs(this.move(this.settings.scrollFraction).top) > this.settings.threshold){
        setTimeout(function(){
            self.animate();
        }, this.settings.scrollInterval);
    }
    
}

ElementScroller.prototype.move = function (fraction) {

    fraction = typeof fraction == 'undefined' ? 1 : fraction;

    var actual = this.getCurrentOffset();
    var target = this.getTargetOffset();
    
    var difference = {
        top: target.top - actual.top
    }

    this.element.css({top: actual.top + difference.top * fraction})
    
    return {top: difference.top * (1 - fraction)};
    
}

ElementScroller.prototype.getCurrentOffset = function () {
    
    return this.element.offset();
    
}

ElementScroller.prototype.getTargetOffset = function () {
    
    var top = this.computeAbsoluteValue(this.position.top, $(window).height()) + $(window).scrollTop();
    var marginTop = this.computeAbsoluteValue(this.margin.top, top);
    
    return {top: top + marginTop};
    
}

ElementScroller.prototype.computeAbsoluteValue = function (what, from) {
    
    var result = 0;
    
    if (what.unit == 'px')
        result = Number(what.value);
    
    if (what.unit == '%')
        result = what.value * from / 100;
    
    return result;
    
}

ElementScroller.prototype.parseCssValue = function (value) {
    
    // convert to string to match with a regex
    value = value.toString();
    
    // parse
    var valueArray = value.match(/^([-\.\d]+)(px|%)?$/i);
    
    // validate
    if (valueArray == null)
        throw 'Invalid value \'' + value + '\'.';
    
    return {value: valueArray[1], unit: typeof valueArray[2] == undefined ? 'px' : valueArray[2]};
    
}
