1405 lines
		
	
	
		
			52 KiB
		
	
	
	
		
			JavaScript
		
	
	
			
		
		
	
	
			1405 lines
		
	
	
		
			52 KiB
		
	
	
	
		
			JavaScript
		
	
	
/** @preserve
 | 
						|
jSignature v2 "${buildDate}" "${commitID}"
 | 
						|
Copyright (c) 2012 Willow Systems Corp http://willow-systems.com
 | 
						|
Copyright (c) 2010 Brinley Ang http://www.unbolt.net
 | 
						|
MIT License <http://www.opensource.org/licenses/mit-license.php>
 | 
						|
 | 
						|
*/
 | 
						|
 | 
						|
;(function() {
 | 
						|
 | 
						|
var apinamespace = 'jSignature'
 | 
						|
 | 
						|
/**
 | 
						|
Allows one to delay certain eventual action by setting up a timer for it and allowing one to delay it
 | 
						|
by "kick"ing it. Sorta like "kick the can down the road"
 | 
						|
 | 
						|
@public
 | 
						|
@class
 | 
						|
@param
 | 
						|
@returns {Type}
 | 
						|
*/
 | 
						|
var KickTimerClass = function(time, callback) {
 | 
						|
    var timer
 | 
						|
    this.kick = function() {
 | 
						|
        clearTimeout(timer)
 | 
						|
        timer = setTimeout(
 | 
						|
            callback
 | 
						|
            , time
 | 
						|
        )
 | 
						|
    }
 | 
						|
    this.clear = function() {
 | 
						|
        clearTimeout(timer)
 | 
						|
    }
 | 
						|
    return this
 | 
						|
}
 | 
						|
 | 
						|
var PubSubClass = function(context){
 | 
						|
    'use strict'
 | 
						|
    /*  @preserve
 | 
						|
    -----------------------------------------------------------------------------------------------
 | 
						|
    JavaScript PubSub library
 | 
						|
    2012 (c) Willow Systems Corp (www.willow-systems.com)
 | 
						|
    based on Peter Higgins (dante@dojotoolkit.org)
 | 
						|
    Loosely based on Dojo publish/subscribe API, limited in scope. Rewritten blindly.
 | 
						|
    Original is (c) Dojo Foundation 2004-2010. Released under either AFL or new BSD, see:
 | 
						|
    http://dojofoundation.org/license for more information.
 | 
						|
    -----------------------------------------------------------------------------------------------
 | 
						|
    */
 | 
						|
    this.topics = {}
 | 
						|
    // here we choose what will be "this" for the called events.
 | 
						|
    // if context is defined, it's context. Else, 'this' is this instance of PubSub
 | 
						|
    this.context = context ? context : this
 | 
						|
    /**
 | 
						|
     * Allows caller to emit an event and pass arguments to event listeners.
 | 
						|
     * @public
 | 
						|
     * @function
 | 
						|
     * @param topic {String} Name of the channel on which to voice this event
 | 
						|
     * @param **arguments Any number of arguments you want to pass to the listeners of this event.
 | 
						|
     */
 | 
						|
    this.publish = function(topic, arg1, arg2, etc) {
 | 
						|
        'use strict'
 | 
						|
        if (this.topics[topic]) {
 | 
						|
            var currentTopic = this.topics[topic]
 | 
						|
            , args = Array.prototype.slice.call(arguments, 1)
 | 
						|
            , toremove = []
 | 
						|
            , fn
 | 
						|
            , i, l
 | 
						|
            , pair
 | 
						|
 | 
						|
            for (i = 0, l = currentTopic.length; i < l; i++) {
 | 
						|
                pair = currentTopic[i] // this is a [function, once_flag] array
 | 
						|
                fn = pair[0]
 | 
						|
                if (pair[1] /* 'run once' flag set */){
 | 
						|
                  pair[0] = function(){}
 | 
						|
                  toremove.push(i)
 | 
						|
                }
 | 
						|
                fn.apply(this.context, args)
 | 
						|
            }
 | 
						|
            for (i = 0, l = toremove.length; i < l; i++) {
 | 
						|
              currentTopic.splice(toremove[i], 1)
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
    /**
 | 
						|
     * Allows listener code to subscribe to channel and be called when data is available
 | 
						|
     * @public
 | 
						|
     * @function
 | 
						|
     * @param topic {String} Name of the channel on which to voice this event
 | 
						|
     * @param callback {Function} Executable (function pointer) that will be ran when event is voiced on this channel.
 | 
						|
     * @param once {Boolean} (optional. False by default) Flag indicating if the function is to be triggered only once.
 | 
						|
     * @returns {Object} A token object that cen be used for unsubscribing.
 | 
						|
     */
 | 
						|
    this.subscribe = function(topic, callback, once) {
 | 
						|
        'use strict'
 | 
						|
        if (!this.topics[topic]) {
 | 
						|
            this.topics[topic] = [[callback, once]];
 | 
						|
        } else {
 | 
						|
            this.topics[topic].push([callback,once]);
 | 
						|
        }
 | 
						|
        return {
 | 
						|
            "topic": topic,
 | 
						|
            "callback": callback
 | 
						|
        };
 | 
						|
    };
 | 
						|
    /**
 | 
						|
     * Allows listener code to unsubscribe from a channel
 | 
						|
     * @public
 | 
						|
     * @function
 | 
						|
     * @param token {Object} A token object that was returned by `subscribe` method
 | 
						|
     */
 | 
						|
    this.unsubscribe = function(token) {
 | 
						|
        if (this.topics[token.topic]) {
 | 
						|
            var currentTopic = this.topics[token.topic]
 | 
						|
 | 
						|
            for (var i = 0, l = currentTopic.length; i < l; i++) {
 | 
						|
                if (currentTopic[i][0] === token.callback) {
 | 
						|
                    currentTopic.splice(i, 1)
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/// Returns front, back and "decor" colors derived from element (as jQuery obj)
 | 
						|
function getColors($e){
 | 
						|
    var tmp
 | 
						|
    , undef
 | 
						|
    , frontcolor = $e.css('color')
 | 
						|
    , backcolor
 | 
						|
    , e = $e[0]
 | 
						|
 | 
						|
    var toOfDOM = false
 | 
						|
    while(e && !backcolor && !toOfDOM){
 | 
						|
        try{
 | 
						|
            tmp = $(e).css('background-color')
 | 
						|
        } catch (ex) {
 | 
						|
            tmp = 'transparent'
 | 
						|
        }
 | 
						|
        if (tmp !== 'transparent' && tmp !== 'rgba(0, 0, 0, 0)'){
 | 
						|
            backcolor = tmp
 | 
						|
        }
 | 
						|
        toOfDOM = e.body
 | 
						|
        e = e.parentNode
 | 
						|
    }
 | 
						|
 | 
						|
    var rgbaregex = /rgb[a]*\((\d+),\s*(\d+),\s*(\d+)/ // modern browsers
 | 
						|
    , hexregex = /#([AaBbCcDdEeFf\d]{2})([AaBbCcDdEeFf\d]{2})([AaBbCcDdEeFf\d]{2})/ // IE 8 and less.
 | 
						|
    , frontcolorcomponents
 | 
						|
 | 
						|
    // Decomposing Front color into R, G, B ints
 | 
						|
    tmp = undef
 | 
						|
    tmp = frontcolor.match(rgbaregex)
 | 
						|
    if (tmp){
 | 
						|
        frontcolorcomponents = {'r':parseInt(tmp[1],10),'g':parseInt(tmp[2],10),'b':parseInt(tmp[3],10)}
 | 
						|
    } else {
 | 
						|
        tmp = frontcolor.match(hexregex)
 | 
						|
        if (tmp) {
 | 
						|
            frontcolorcomponents = {'r':parseInt(tmp[1],16),'g':parseInt(tmp[2],16),'b':parseInt(tmp[3],16)}
 | 
						|
        }
 | 
						|
    }
 | 
						|
//      if(!frontcolorcomponents){
 | 
						|
//          frontcolorcomponents = {'r':255,'g':255,'b':255}
 | 
						|
//      }
 | 
						|
 | 
						|
    var backcolorcomponents
 | 
						|
    // Decomposing back color into R, G, B ints
 | 
						|
    if(!backcolor){
 | 
						|
        // HIghly unlikely since this means that no background styling was applied to any element from here to top of dom.
 | 
						|
        // we'll pick up back color from front color
 | 
						|
        if(frontcolorcomponents){
 | 
						|
            if (Math.max.apply(null, [frontcolorcomponents.r, frontcolorcomponents.g, frontcolorcomponents.b]) > 127){
 | 
						|
                backcolorcomponents = {'r':0,'g':0,'b':0}
 | 
						|
            } else {
 | 
						|
                backcolorcomponents = {'r':255,'g':255,'b':255}
 | 
						|
            }
 | 
						|
        } else {
 | 
						|
            // arg!!! front color is in format we don't understand (hsl, named colors)
 | 
						|
            // Let's just go with white background.
 | 
						|
            backcolorcomponents = {'r':255,'g':255,'b':255}
 | 
						|
        }
 | 
						|
    } else {
 | 
						|
        tmp = undef
 | 
						|
        tmp = backcolor.match(rgbaregex)
 | 
						|
        if (tmp){
 | 
						|
            backcolorcomponents = {'r':parseInt(tmp[1],10),'g':parseInt(tmp[2],10),'b':parseInt(tmp[3],10)}
 | 
						|
        } else {
 | 
						|
            tmp = backcolor.match(hexregex)
 | 
						|
            if (tmp) {
 | 
						|
                backcolorcomponents = {'r':parseInt(tmp[1],16),'g':parseInt(tmp[2],16),'b':parseInt(tmp[3],16)}
 | 
						|
            }
 | 
						|
        }
 | 
						|
//          if(!backcolorcomponents){
 | 
						|
//              backcolorcomponents = {'r':0,'g':0,'b':0}
 | 
						|
//          }
 | 
						|
    }
 | 
						|
 | 
						|
    // Deriving Decor color
 | 
						|
    // THis is LAZY!!!! Better way would be to use HSL and adjust luminocity. However, that could be an overkill.
 | 
						|
 | 
						|
    var toRGBfn = function(o){return 'rgb(' + [o.r, o.g, o.b].join(', ') + ')'}
 | 
						|
    , decorcolorcomponents
 | 
						|
    , frontcolorbrightness
 | 
						|
    , adjusted
 | 
						|
 | 
						|
    if (frontcolorcomponents && backcolorcomponents){
 | 
						|
        var backcolorbrightness = Math.max.apply(null, [frontcolorcomponents.r, frontcolorcomponents.g, frontcolorcomponents.b])
 | 
						|
 | 
						|
        frontcolorbrightness = Math.max.apply(null, [backcolorcomponents.r, backcolorcomponents.g, backcolorcomponents.b])
 | 
						|
        adjusted = Math.round(frontcolorbrightness + (-1 * (frontcolorbrightness - backcolorbrightness) * 0.75)) // "dimming" the difference between pen and back.
 | 
						|
        decorcolorcomponents = {'r':adjusted,'g':adjusted,'b':adjusted} // always shade of gray
 | 
						|
    } else if (frontcolorcomponents) {
 | 
						|
        frontcolorbrightness = Math.max.apply(null, [frontcolorcomponents.r, frontcolorcomponents.g, frontcolorcomponents.b])
 | 
						|
        var polarity = +1
 | 
						|
        if (frontcolorbrightness > 127){
 | 
						|
            polarity = -1
 | 
						|
        }
 | 
						|
        // shifting by 25% (64 points on RGB scale)
 | 
						|
        adjusted = Math.round(frontcolorbrightness + (polarity * 96)) // "dimming" the pen's color by 75% to get decor color.
 | 
						|
        decorcolorcomponents = {'r':adjusted,'g':adjusted,'b':adjusted} // always shade of gray
 | 
						|
    } else {
 | 
						|
        decorcolorcomponents = {'r':191,'g':191,'b':191} // always shade of gray
 | 
						|
    }
 | 
						|
 | 
						|
    return {
 | 
						|
        'color': frontcolor
 | 
						|
        , 'background-color': backcolorcomponents? toRGBfn(backcolorcomponents) : backcolor
 | 
						|
        , 'decor-color': toRGBfn(decorcolorcomponents)
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
function Vector(x,y){
 | 
						|
    this.x = x
 | 
						|
    this.y = y
 | 
						|
    this.reverse = function(){
 | 
						|
        return new this.constructor(
 | 
						|
            this.x * -1
 | 
						|
            , this.y * -1
 | 
						|
        )
 | 
						|
    }
 | 
						|
    this._length = null
 | 
						|
    this.getLength = function(){
 | 
						|
        if (!this._length){
 | 
						|
            this._length = Math.sqrt( Math.pow(this.x, 2) + Math.pow(this.y, 2) )
 | 
						|
        }
 | 
						|
        return this._length
 | 
						|
    }
 | 
						|
 | 
						|
    var polarity = function (e){
 | 
						|
        return Math.round(e / Math.abs(e))
 | 
						|
    }
 | 
						|
    this.resizeTo = function(length){
 | 
						|
        // proportionally changes x,y such that the hypotenuse (vector length) is = new length
 | 
						|
        if (this.x === 0 && this.y === 0){
 | 
						|
            this._length = 0
 | 
						|
        } else if (this.x === 0){
 | 
						|
            this._length = length
 | 
						|
            this.y = length * polarity(this.y)
 | 
						|
        } else if(this.y === 0){
 | 
						|
            this._length = length
 | 
						|
            this.x = length * polarity(this.x)
 | 
						|
        } else {
 | 
						|
            var proportion = Math.abs(this.y / this.x)
 | 
						|
                , x = Math.sqrt(Math.pow(length, 2) / (1 + Math.pow(proportion, 2)))
 | 
						|
                , y = proportion * x
 | 
						|
            this._length = length
 | 
						|
            this.x = x * polarity(this.x)
 | 
						|
            this.y = y * polarity(this.y)
 | 
						|
        }
 | 
						|
        return this
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Calculates the angle between 'this' vector and another.
 | 
						|
     * @public
 | 
						|
     * @function
 | 
						|
     * @returns {Number} The angle between the two vectors as measured in PI.
 | 
						|
     */
 | 
						|
    this.angleTo = function(vectorB) {
 | 
						|
        var divisor = this.getLength() * vectorB.getLength()
 | 
						|
        if (divisor === 0) {
 | 
						|
            return 0
 | 
						|
        } else {
 | 
						|
            // JavaScript floating point math is screwed up.
 | 
						|
            // because of it, the core of the formula can, on occasion, have values
 | 
						|
            // over 1.0 and below -1.0.
 | 
						|
            return Math.acos(
 | 
						|
                Math.min(
 | 
						|
                    Math.max(
 | 
						|
                        ( this.x * vectorB.x + this.y * vectorB.y ) / divisor
 | 
						|
                        , -1.0
 | 
						|
                    )
 | 
						|
                    , 1.0
 | 
						|
                )
 | 
						|
            ) / Math.PI
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
function Point(x,y){
 | 
						|
    this.x = x
 | 
						|
    this.y = y
 | 
						|
 | 
						|
    this.getVectorToCoordinates = function (x, y) {
 | 
						|
        return new Vector(x - this.x, y - this.y)
 | 
						|
    }
 | 
						|
    this.getVectorFromCoordinates = function (x, y) {
 | 
						|
        return this.getVectorToCoordinates(x, y).reverse()
 | 
						|
    }
 | 
						|
    this.getVectorToPoint = function (point) {
 | 
						|
        return new Vector(point.x - this.x, point.y - this.y)
 | 
						|
    }
 | 
						|
    this.getVectorFromPoint = function (point) {
 | 
						|
        return this.getVectorToPoint(point).reverse()
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/*
 | 
						|
 * About data structure:
 | 
						|
 * We don't store / deal with "pictures" this signature capture code captures "vectors"
 | 
						|
 *
 | 
						|
 * We don't store bitmaps. We store "strokes" as arrays of arrays. (Actually, arrays of objects containing arrays of coordinates.
 | 
						|
 *
 | 
						|
 * Stroke = mousedown + mousemoved * n (+ mouseup but we don't record that as that was the "end / lack of movement" indicator)
 | 
						|
 *
 | 
						|
 * Vectors = not classical vectors where numbers indicated shift relative last position. Our vectors are actually coordinates against top left of canvas.
 | 
						|
 *          we could calc the classical vectors, but keeping the the actual coordinates allows us (through Math.max / min)
 | 
						|
 *          to calc the size of resulting drawing very quickly. If we want classical vectors later, we can always get them in backend code.
 | 
						|
 *
 | 
						|
 * So, the data structure:
 | 
						|
 *
 | 
						|
 * var data = [
 | 
						|
 *  { // stroke starts
 | 
						|
 *      x : [101, 98, 57, 43] // x points
 | 
						|
 *      , y : [1, 23, 65, 87] // y points
 | 
						|
 *  } // stroke ends
 | 
						|
 *  , { // stroke starts
 | 
						|
 *      x : [55, 56, 57, 58] // x points
 | 
						|
 *      , y : [101, 97, 54, 4] // y points
 | 
						|
 *  } // stroke ends
 | 
						|
 *  , { // stroke consisting of just a dot
 | 
						|
 *      x : [53] // x points
 | 
						|
 *      , y : [151] // y points
 | 
						|
 *  } // stroke ends
 | 
						|
 * ]
 | 
						|
 *
 | 
						|
 * we don't care or store stroke width (it's canvas-size-relative), color, shadow values. These can be added / changed on whim post-capture.
 | 
						|
 *
 | 
						|
 */
 | 
						|
function DataEngine(storageObject, context, startStrokeFn, addToStrokeFn, endStrokeFn){
 | 
						|
    this.data = storageObject // we expect this to be an instance of Array
 | 
						|
    this.context = context
 | 
						|
 | 
						|
    if (storageObject.length){
 | 
						|
        // we have data to render
 | 
						|
        var numofstrokes = storageObject.length
 | 
						|
        , stroke
 | 
						|
        , numofpoints
 | 
						|
 | 
						|
        for (var i = 0; i < numofstrokes; i++){
 | 
						|
            stroke = storageObject[i]
 | 
						|
            numofpoints = stroke.x.length
 | 
						|
            startStrokeFn.call(context, stroke)
 | 
						|
            for(var j = 1; j < numofpoints; j++){
 | 
						|
                addToStrokeFn.call(context, stroke, j)
 | 
						|
            }
 | 
						|
            endStrokeFn.call(context, stroke)
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    this.changed = function(){}
 | 
						|
 | 
						|
    this.startStrokeFn = startStrokeFn
 | 
						|
    this.addToStrokeFn = addToStrokeFn
 | 
						|
    this.endStrokeFn = endStrokeFn
 | 
						|
 | 
						|
    this.inStroke = false
 | 
						|
 | 
						|
    this._lastPoint = null
 | 
						|
    this._stroke = null
 | 
						|
    this.startStroke = function(point){
 | 
						|
        if(point && typeof(point.x) == "number" && typeof(point.y) == "number"){
 | 
						|
            this._stroke = {'x':[point.x], 'y':[point.y]}
 | 
						|
            this.data.push(this._stroke)
 | 
						|
            this._lastPoint = point
 | 
						|
            this.inStroke = true
 | 
						|
            // 'this' does not work same inside setTimeout(
 | 
						|
            var stroke = this._stroke
 | 
						|
            , fn = this.startStrokeFn
 | 
						|
            , context = this.context
 | 
						|
            setTimeout(
 | 
						|
                // some IE's don't support passing args per setTimeout API. Have to create closure every time instead.
 | 
						|
                function() {fn.call(context, stroke)}
 | 
						|
                , 3
 | 
						|
            )
 | 
						|
            return point
 | 
						|
        } else {
 | 
						|
            return null
 | 
						|
        }
 | 
						|
    }
 | 
						|
    // that "5" at the very end of this if is important to explain.
 | 
						|
    // we do NOT render links between two captured points (in the middle of the stroke) if the distance is shorter than that number.
 | 
						|
    // not only do we NOT render it, we also do NOT capture (add) these intermediate points to storage.
 | 
						|
    // when clustering of these is too tight, it produces noise on the line, which, because of smoothing, makes lines too curvy.
 | 
						|
    // maybe, later, we can expose this as a configurable setting of some sort.
 | 
						|
    this.addToStroke = function(point){
 | 
						|
        if (this.inStroke &&
 | 
						|
            typeof(point.x) === "number" &&
 | 
						|
            typeof(point.y) === "number" &&
 | 
						|
            // calculates absolute shift in diagonal pixels away from original point
 | 
						|
            (Math.abs(point.x - this._lastPoint.x) + Math.abs(point.y - this._lastPoint.y)) > 4
 | 
						|
        ){
 | 
						|
            var positionInStroke = this._stroke.x.length
 | 
						|
            this._stroke.x.push(point.x)
 | 
						|
            this._stroke.y.push(point.y)
 | 
						|
            this._lastPoint = point
 | 
						|
 | 
						|
            var stroke = this._stroke
 | 
						|
            , fn = this.addToStrokeFn
 | 
						|
            , context = this.context
 | 
						|
            setTimeout(
 | 
						|
                // some IE's don't support passing args per setTimeout API. Have to create closure every time instead.
 | 
						|
                function() {fn.call(context, stroke, positionInStroke)}
 | 
						|
                , 3
 | 
						|
            )
 | 
						|
            return point
 | 
						|
        } else {
 | 
						|
            return null
 | 
						|
        }
 | 
						|
    }
 | 
						|
    this.endStroke = function(){
 | 
						|
        var c = this.inStroke
 | 
						|
        this.inStroke = false
 | 
						|
        this._lastPoint = null
 | 
						|
        if (c){
 | 
						|
            var stroke = this._stroke
 | 
						|
            , fn = this.endStrokeFn // 'this' does not work same inside setTimeout(
 | 
						|
            , context = this.context
 | 
						|
            , changedfn = this.changed
 | 
						|
            setTimeout(
 | 
						|
                // some IE's don't support passing args per setTimeout API. Have to create closure every time instead.
 | 
						|
                function(){
 | 
						|
                    fn.call(context, stroke)
 | 
						|
                    changedfn.call(context)
 | 
						|
                }
 | 
						|
                , 3
 | 
						|
            )
 | 
						|
            return true
 | 
						|
        } else {
 | 
						|
            return null
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
var basicDot = function(ctx, x, y, size){
 | 
						|
    var fillStyle = ctx.fillStyle
 | 
						|
    ctx.fillStyle = ctx.strokeStyle
 | 
						|
    ctx.fillRect(x + size / -2 , y + size / -2, size, size)
 | 
						|
    ctx.fillStyle = fillStyle
 | 
						|
}
 | 
						|
, basicLine = function(ctx, startx, starty, endx, endy){
 | 
						|
    ctx.beginPath()
 | 
						|
    ctx.moveTo(startx, starty)
 | 
						|
    ctx.lineTo(endx, endy)
 | 
						|
    ctx.stroke()
 | 
						|
}
 | 
						|
, basicCurve = function(ctx, startx, starty, endx, endy, cp1x, cp1y, cp2x, cp2y){
 | 
						|
    ctx.beginPath()
 | 
						|
    ctx.moveTo(startx, starty)
 | 
						|
    ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, endx, endy)
 | 
						|
    ctx.stroke()
 | 
						|
}
 | 
						|
, strokeStartCallback = function(stroke) {
 | 
						|
    // this = jSignatureClass instance
 | 
						|
    basicDot(this.canvasContext, stroke.x[0], stroke.y[0], this.settings.lineWidth)
 | 
						|
}
 | 
						|
, strokeAddCallback = function(stroke, positionInStroke){
 | 
						|
    // this = jSignatureClass instance
 | 
						|
 | 
						|
    // Because we are funky this way, here we draw TWO curves.
 | 
						|
    // 1. POSSIBLY "this line" - spanning from point right before us, to this latest point.
 | 
						|
    // 2. POSSIBLY "prior curve" - spanning from "latest point" to the one before it.
 | 
						|
 | 
						|
    // Why you ask?
 | 
						|
    // long lines (ones with many pixels between them) do not look good when they are part of a large curvy stroke.
 | 
						|
    // You know, the jaggedy crocodile spine instead of a pretty, smooth curve. Yuck!
 | 
						|
    // We want to approximate pretty curves in-place of those ugly lines.
 | 
						|
    // To approximate a very nice curve we need to know the direction of line before and after.
 | 
						|
    // Hence, on long lines we actually wait for another point beyond it to come back from
 | 
						|
    // mousemoved before we draw this curve.
 | 
						|
 | 
						|
    // So for "prior curve" to be calc'ed we need 4 points
 | 
						|
    //  A, B, C, D (we are on D now, A is 3 points in the past.)
 | 
						|
    // and 3 lines:
 | 
						|
    //  pre-line (from points A to B),
 | 
						|
    //  this line (from points B to C), (we call it "this" because if it was not yet, it's the only one we can draw for sure.)
 | 
						|
    //  post-line (from points C to D) (even through D point is 'current' we don't know how we can draw it yet)
 | 
						|
    //
 | 
						|
    // Well, actually, we don't need to *know* the point A, just the vector A->B
 | 
						|
    var Cpoint = new Point(stroke.x[positionInStroke-1], stroke.y[positionInStroke-1])
 | 
						|
        , Dpoint = new Point(stroke.x[positionInStroke], stroke.y[positionInStroke])
 | 
						|
        , CDvector = Cpoint.getVectorToPoint(Dpoint)
 | 
						|
 | 
						|
    // Again, we have a chance here to draw TWO things:
 | 
						|
    //  BC Curve (only if it's long, because if it was short, it was drawn by previous callback) and
 | 
						|
    //  CD Line (only if it's short)
 | 
						|
 | 
						|
    // So, let's start with BC curve.
 | 
						|
    // if there is only 2 points in stroke array, we don't have "history" long enough to have point B, let alone point A.
 | 
						|
    // Falling through to drawing line CD is proper, as that's the only line we have points for.
 | 
						|
    if(positionInStroke > 1) {
 | 
						|
        // we are here when there are at least 3 points in stroke array.
 | 
						|
        var Bpoint = new Point(stroke.x[positionInStroke-2], stroke.y[positionInStroke-2])
 | 
						|
        , BCvector = Bpoint.getVectorToPoint(Cpoint)
 | 
						|
        , ABvector
 | 
						|
        if(BCvector.getLength() > this.lineCurveThreshold){
 | 
						|
            // Yey! Pretty curves, here we come!
 | 
						|
            if(positionInStroke > 2) {
 | 
						|
                // we are here when at least 4 points in stroke array.
 | 
						|
                ABvector = (new Point(stroke.x[positionInStroke-3], stroke.y[positionInStroke-3])).getVectorToPoint(Bpoint)
 | 
						|
            } else {
 | 
						|
                ABvector = new Vector(0,0)
 | 
						|
            }
 | 
						|
 | 
						|
            var minlenfraction = 0.05
 | 
						|
            , maxlen = BCvector.getLength() * 0.35
 | 
						|
            , ABCangle = BCvector.angleTo(ABvector.reverse())
 | 
						|
            , BCDangle = CDvector.angleTo(BCvector.reverse())
 | 
						|
            , BCP1vector = new Vector(ABvector.x + BCvector.x, ABvector.y + BCvector.y).resizeTo(
 | 
						|
                Math.max(minlenfraction, ABCangle) * maxlen
 | 
						|
            )
 | 
						|
            , CCP2vector = (new Vector(BCvector.x + CDvector.x, BCvector.y + CDvector.y)).reverse().resizeTo(
 | 
						|
                Math.max(minlenfraction, BCDangle) * maxlen
 | 
						|
            )
 | 
						|
 | 
						|
            basicCurve(
 | 
						|
                this.canvasContext
 | 
						|
                , Bpoint.x
 | 
						|
                , Bpoint.y
 | 
						|
                , Cpoint.x
 | 
						|
                , Cpoint.y
 | 
						|
                , Bpoint.x + BCP1vector.x
 | 
						|
                , Bpoint.y + BCP1vector.y
 | 
						|
                , Cpoint.x + CCP2vector.x
 | 
						|
                , Cpoint.y + CCP2vector.y
 | 
						|
            )
 | 
						|
        }
 | 
						|
    }
 | 
						|
    if(CDvector.getLength() <= this.lineCurveThreshold){
 | 
						|
        basicLine(
 | 
						|
            this.canvasContext
 | 
						|
            , Cpoint.x
 | 
						|
            , Cpoint.y
 | 
						|
            , Dpoint.x
 | 
						|
            , Dpoint.y
 | 
						|
        )
 | 
						|
    }
 | 
						|
}
 | 
						|
, strokeEndCallback = function(stroke){
 | 
						|
    // this = jSignatureClass instance
 | 
						|
 | 
						|
    // Here we tidy up things left unfinished in last strokeAddCallback run.
 | 
						|
 | 
						|
    // What's POTENTIALLY left unfinished there is the curve between the last points
 | 
						|
    // in the stroke, if the len of that line is more than lineCurveThreshold
 | 
						|
    // If the last line was shorter than lineCurveThreshold, it was drawn there, and there
 | 
						|
    // is nothing for us here to do.
 | 
						|
    // We can also be called when there is only one point in the stroke (meaning, the
 | 
						|
    // stroke was just a dot), in which case, again, there is nothing for us to do.
 | 
						|
 | 
						|
    // So for "this curve" to be calc'ed we need 3 points
 | 
						|
    //  A, B, C
 | 
						|
    // and 2 lines:
 | 
						|
    //  pre-line (from points A to B),
 | 
						|
    //  this line (from points B to C)
 | 
						|
    // Well, actually, we don't need to *know* the point A, just the vector A->B
 | 
						|
    // so, we really need points B, C and AB vector.
 | 
						|
    var positionInStroke = stroke.x.length - 1
 | 
						|
 | 
						|
    if (positionInStroke > 0){
 | 
						|
        // there are at least 2 points in the stroke.we are in business.
 | 
						|
        var Cpoint = new Point(stroke.x[positionInStroke], stroke.y[positionInStroke])
 | 
						|
            , Bpoint = new Point(stroke.x[positionInStroke-1], stroke.y[positionInStroke-1])
 | 
						|
            , BCvector = Bpoint.getVectorToPoint(Cpoint)
 | 
						|
            , ABvector
 | 
						|
        if (BCvector.getLength() > this.lineCurveThreshold){
 | 
						|
            // yep. This one was left undrawn in prior callback. Have to draw it now.
 | 
						|
            if (positionInStroke > 1){
 | 
						|
                // we have at least 3 elems in stroke
 | 
						|
                ABvector = (new Point(stroke.x[positionInStroke-2], stroke.y[positionInStroke-2])).getVectorToPoint(Bpoint)
 | 
						|
                var BCP1vector = new Vector(ABvector.x + BCvector.x, ABvector.y + BCvector.y).resizeTo(BCvector.getLength() / 2)
 | 
						|
                basicCurve(
 | 
						|
                    this.canvasContext
 | 
						|
                    , Bpoint.x
 | 
						|
                    , Bpoint.y
 | 
						|
                    , Cpoint.x
 | 
						|
                    , Cpoint.y
 | 
						|
                    , Bpoint.x + BCP1vector.x
 | 
						|
                    , Bpoint.y + BCP1vector.y
 | 
						|
                    , Cpoint.x
 | 
						|
                    , Cpoint.y
 | 
						|
                )
 | 
						|
            } else {
 | 
						|
                // Since there is no AB leg, there is no curve to draw. This line is still "long" but no curve.
 | 
						|
                basicLine(
 | 
						|
                    this.canvasContext
 | 
						|
                    , Bpoint.x
 | 
						|
                    , Bpoint.y
 | 
						|
                    , Cpoint.x
 | 
						|
                    , Cpoint.y
 | 
						|
                )
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
/*
 | 
						|
var getDataStats = function(){
 | 
						|
    var strokecnt = strokes.length
 | 
						|
        , stroke
 | 
						|
        , pointid
 | 
						|
        , pointcnt
 | 
						|
        , x, y
 | 
						|
        , maxX = Number.NEGATIVE_INFINITY
 | 
						|
        , maxY = Number.NEGATIVE_INFINITY
 | 
						|
        , minX = Number.POSITIVE_INFINITY
 | 
						|
        , minY = Number.POSITIVE_INFINITY
 | 
						|
    for(strokeid = 0; strokeid < strokecnt; strokeid++){
 | 
						|
        stroke = strokes[strokeid]
 | 
						|
        pointcnt = stroke.length
 | 
						|
        for(pointid = 0; pointid < pointcnt; pointid++){
 | 
						|
            x = stroke.x[pointid]
 | 
						|
            y = stroke.y[pointid]
 | 
						|
            if (x > maxX){
 | 
						|
                maxX = x
 | 
						|
            } else if (x < minX) {
 | 
						|
                minX = x
 | 
						|
            }
 | 
						|
            if (y > maxY){
 | 
						|
                maxY = y
 | 
						|
            } else if (y < minY) {
 | 
						|
                minY = y
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
    return {'maxX': maxX, 'minX': minX, 'maxY': maxY, 'minY': minY}
 | 
						|
}
 | 
						|
*/
 | 
						|
 | 
						|
function conditionallyLinkCanvasResizeToWindowResize(jSignatureInstance, settingsWidth, apinamespace, globalEvents){
 | 
						|
    'use strict'
 | 
						|
    if ( settingsWidth === 'ratio' || settingsWidth.split('')[settingsWidth.length - 1] === '%' ) {
 | 
						|
 | 
						|
        this.eventTokens[apinamespace + '.parentresized'] = globalEvents.subscribe(
 | 
						|
            apinamespace + '.parentresized'
 | 
						|
            , (function(eventTokens, $parent, originalParentWidth, sizeRatio){
 | 
						|
                'use strict'
 | 
						|
 | 
						|
                return function(){
 | 
						|
                    'use strict'
 | 
						|
 | 
						|
                    var w = $parent.width()
 | 
						|
                    if (w !== originalParentWidth) {
 | 
						|
 | 
						|
                        // UNsubscribing this particular instance of signature pad only.
 | 
						|
                        // there is a separate `eventTokens` per each instance of signature pad
 | 
						|
                        for (var key in eventTokens){
 | 
						|
                            if (eventTokens.hasOwnProperty(key)) {
 | 
						|
                                globalEvents.unsubscribe(eventTokens[key])
 | 
						|
                                delete eventTokens[key]
 | 
						|
                            }
 | 
						|
                        }
 | 
						|
 | 
						|
                        var settings = jSignatureInstance.settings
 | 
						|
                        jSignatureInstance.$parent.children().remove()
 | 
						|
                        for (var key in jSignatureInstance){
 | 
						|
                            if (jSignatureInstance.hasOwnProperty(key)) {
 | 
						|
                                delete jSignatureInstance[key]
 | 
						|
                            }
 | 
						|
                        }
 | 
						|
 | 
						|
                        // scale data to new signature pad size
 | 
						|
                        settings.data = (function(data, scale){
 | 
						|
                            var newData = []
 | 
						|
                            var o, i, l, j, m, stroke
 | 
						|
                            for ( i = 0, l = data.length; i < l; i++) {
 | 
						|
                                stroke = data[i]
 | 
						|
 | 
						|
                                o = {'x':[],'y':[]}
 | 
						|
 | 
						|
                                for ( j = 0, m = stroke.x.length; j < m; j++) {
 | 
						|
                                    o.x.push(stroke.x[j] * scale)
 | 
						|
                                    o.y.push(stroke.y[j] * scale)
 | 
						|
                                }
 | 
						|
 | 
						|
                                newData.push(o)
 | 
						|
                            }
 | 
						|
                            return newData
 | 
						|
                        })(
 | 
						|
                            settings.data
 | 
						|
                            , w * 1.0 / originalParentWidth
 | 
						|
                        )
 | 
						|
 | 
						|
                        $parent[apinamespace](settings)
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            })(
 | 
						|
                this.eventTokens
 | 
						|
                , this.$parent
 | 
						|
                , this.$parent.width()
 | 
						|
                , this.canvas.width * 1.0 / this.canvas.height
 | 
						|
            )
 | 
						|
        )
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
function jSignatureClass(parent, options, instanceExtensions) {
 | 
						|
 | 
						|
    var $parent = this.$parent = $(parent)
 | 
						|
    , eventTokens = this.eventTokens = {}
 | 
						|
    , events = this.events = new PubSubClass(this)
 | 
						|
    , globalEvents = $.fn[apinamespace]('globalEvents')
 | 
						|
    , settings = {
 | 
						|
        'width' : 'ratio'
 | 
						|
        ,'height' : 'ratio'
 | 
						|
        ,'sizeRatio': 4 // only used when height = 'ratio'
 | 
						|
        ,'color' : '#000'
 | 
						|
        ,'background-color': '#fff'
 | 
						|
        ,'decor-color': '#eee'
 | 
						|
        ,'lineWidth' : 0
 | 
						|
        ,'minFatFingerCompensation' : -10
 | 
						|
        ,'showUndoButton': false
 | 
						|
        ,'data': []
 | 
						|
    }
 | 
						|
    $.extend(settings, getColors($parent))
 | 
						|
    if (options) {
 | 
						|
        $.extend(settings, options)
 | 
						|
    }
 | 
						|
    this.settings = settings
 | 
						|
 | 
						|
    for (var extensionName in instanceExtensions){
 | 
						|
        if (instanceExtensions.hasOwnProperty(extensionName)) {
 | 
						|
            instanceExtensions[extensionName].call(this, extensionName)
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    this.events.publish(apinamespace+'.initializing')
 | 
						|
 | 
						|
    // these, when enabled, will hover above the sig area. Hence we append them to DOM before canvas.
 | 
						|
    this.$controlbarUpper = (function(){
 | 
						|
        var controlbarstyle = 'padding:0 !important;margin:0 !important;'+
 | 
						|
            'width: 100% !important; height: 0 !important;'+
 | 
						|
            'margin-top:-1em !important;margin-bottom:1em !important;'
 | 
						|
        return $('<div style="'+controlbarstyle+'"></div>').appendTo($parent)
 | 
						|
    })();
 | 
						|
 | 
						|
    this.isCanvasEmulator = false // will be flipped by initializer when needed.
 | 
						|
    var canvas = this.canvas = this.initializeCanvas(settings)
 | 
						|
    , $canvas = $(canvas)
 | 
						|
 | 
						|
    this.$controlbarLower = (function(){
 | 
						|
        var controlbarstyle = 'padding:0 !important;margin:0 !important;'+
 | 
						|
            'width: 100% !important; height: 0 !important;'+
 | 
						|
            'margin-top:-1.5em !important;margin-bottom:1.5em !important;'
 | 
						|
        return $('<div style="'+controlbarstyle+'"></div>').appendTo($parent)
 | 
						|
    })();
 | 
						|
 | 
						|
    this.canvasContext = canvas.getContext("2d")
 | 
						|
 | 
						|
    // Most of our exposed API will be looking for this:
 | 
						|
    $canvas.data(apinamespace + '.this', this)
 | 
						|
 | 
						|
 | 
						|
    settings.lineWidth = (function(defaultLineWidth, canvasWidth){
 | 
						|
        if (!defaultLineWidth){
 | 
						|
            return Math.max(
 | 
						|
                Math.round(canvasWidth / 400) /*+1 pixel for every extra 300px of width.*/
 | 
						|
                , 2 /* minimum line width */
 | 
						|
            )
 | 
						|
        } else {
 | 
						|
            return defaultLineWidth
 | 
						|
        }
 | 
						|
    })(settings.lineWidth, canvas.width);
 | 
						|
 | 
						|
    this.lineCurveThreshold = settings.lineWidth * 3
 | 
						|
 | 
						|
    // Add custom class if defined
 | 
						|
    if(settings.cssclass && $.trim(settings.cssclass) != "") {
 | 
						|
        $canvas.addClass(settings.cssclass)
 | 
						|
    }
 | 
						|
 | 
						|
    // used for shifting the drawing point up on touch devices, so one can see the drawing above the finger.
 | 
						|
    this.fatFingerCompensation = 0
 | 
						|
 | 
						|
    var movementHandlers = (function(jSignatureInstance) {
 | 
						|
 | 
						|
        //================================
 | 
						|
        // mouse down, move, up handlers:
 | 
						|
 | 
						|
        // shifts - adjustment values in viewport pixels drived from position of canvas on the page
 | 
						|
        var shiftX
 | 
						|
        , shiftY
 | 
						|
        , setStartValues = function(){
 | 
						|
            var tos = $(jSignatureInstance.canvas).offset()
 | 
						|
            shiftX = tos.left * -1
 | 
						|
            shiftY = tos.top * -1
 | 
						|
        }
 | 
						|
        , getPointFromEvent = function(e) {
 | 
						|
            var firstEvent = (e.changedTouches && e.changedTouches.length > 0 ? e.changedTouches[0] : e)
 | 
						|
            // All devices i tried report correct coordinates in pageX,Y
 | 
						|
            // Android Chrome 2.3.x, 3.1, 3.2., Opera Mobile,  safari iOS 4.x,
 | 
						|
            // Windows: Chrome, FF, IE9, Safari
 | 
						|
            // None of that scroll shift calc vs screenXY other sigs do is needed.
 | 
						|
            // ... oh, yeah, the "fatFinger.." is for tablets so that people see what they draw.
 | 
						|
            return new Point(
 | 
						|
                Math.round(firstEvent.pageX + shiftX)
 | 
						|
                , Math.round(firstEvent.pageY + shiftY) + jSignatureInstance.fatFingerCompensation
 | 
						|
            )
 | 
						|
        }
 | 
						|
        , timer = new KickTimerClass(
 | 
						|
            750
 | 
						|
            , function() { jSignatureInstance.dataEngine.endStroke() }
 | 
						|
        )
 | 
						|
 | 
						|
        this.drawEndHandler = function(e) {
 | 
						|
            try { e.preventDefault() } catch (ex) {}
 | 
						|
            timer.clear()
 | 
						|
            jSignatureInstance.dataEngine.endStroke()
 | 
						|
        }
 | 
						|
        this.drawStartHandler = function(e) {
 | 
						|
            e.preventDefault()
 | 
						|
            // for performance we cache the offsets
 | 
						|
            // we recalc these only at the beginning the stroke
 | 
						|
            setStartValues()
 | 
						|
            jSignatureInstance.dataEngine.startStroke( getPointFromEvent(e) )
 | 
						|
            timer.kick()
 | 
						|
        }
 | 
						|
        this.drawMoveHandler = function(e) {
 | 
						|
            e.preventDefault()
 | 
						|
            if (!jSignatureInstance.dataEngine.inStroke){
 | 
						|
                return
 | 
						|
            }
 | 
						|
            jSignatureInstance.dataEngine.addToStroke( getPointFromEvent(e) )
 | 
						|
            timer.kick()
 | 
						|
        }
 | 
						|
 | 
						|
        return this
 | 
						|
 | 
						|
    }).call( {}, this )
 | 
						|
 | 
						|
    //
 | 
						|
    //================================
 | 
						|
 | 
						|
    ;(function(drawEndHandler, drawStartHandler, drawMoveHandler) {
 | 
						|
        var canvas = this.canvas
 | 
						|
        , $canvas = $(canvas)
 | 
						|
        , undef
 | 
						|
        if (this.isCanvasEmulator){
 | 
						|
            $canvas.bind('mousemove.'+apinamespace, drawMoveHandler)
 | 
						|
            $canvas.bind('mouseup.'+apinamespace, drawEndHandler)
 | 
						|
            $canvas.bind('mousedown.'+apinamespace, drawStartHandler)
 | 
						|
        } else {
 | 
						|
            canvas.ontouchstart = function(e) {
 | 
						|
                canvas.onmousedown = undef
 | 
						|
                canvas.onmouseup = undef
 | 
						|
                canvas.onmousemove = undef
 | 
						|
 | 
						|
                this.fatFingerCompensation = (
 | 
						|
                    settings.minFatFingerCompensation &&
 | 
						|
                    settings.lineWidth * -3 > settings.minFatFingerCompensation
 | 
						|
                ) ? settings.lineWidth * -3 : settings.minFatFingerCompensation
 | 
						|
 | 
						|
                drawStartHandler(e)
 | 
						|
 | 
						|
                canvas.ontouchend = drawEndHandler
 | 
						|
                canvas.ontouchstart = drawStartHandler
 | 
						|
                canvas.ontouchmove = drawMoveHandler
 | 
						|
            }
 | 
						|
            canvas.onmousedown = function(e) {
 | 
						|
                canvas.ontouchstart = undef
 | 
						|
                canvas.ontouchend = undef
 | 
						|
                canvas.ontouchmove = undef
 | 
						|
 | 
						|
                drawStartHandler(e)
 | 
						|
 | 
						|
                canvas.onmousedown = drawStartHandler
 | 
						|
                canvas.onmouseup = drawEndHandler
 | 
						|
                canvas.onmousemove = drawMoveHandler
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }).call(
 | 
						|
        this
 | 
						|
        , movementHandlers.drawEndHandler
 | 
						|
        , movementHandlers.drawStartHandler
 | 
						|
        , movementHandlers.drawMoveHandler
 | 
						|
    )
 | 
						|
 | 
						|
    //=========================================
 | 
						|
    // various event handlers
 | 
						|
 | 
						|
    // on mouseout + mouseup canvas did not know that mouseUP fired. Continued to draw despite mouse UP.
 | 
						|
    // it is bettr than
 | 
						|
    // $canvas.bind('mouseout', drawEndHandler)
 | 
						|
    // because we don't want to break the stroke where user accidentally gets ouside and wants to get back in quickly.
 | 
						|
    eventTokens[apinamespace + '.windowmouseup'] = globalEvents.subscribe(
 | 
						|
        apinamespace + '.windowmouseup'
 | 
						|
        , movementHandlers.drawEndHandler
 | 
						|
    )
 | 
						|
 | 
						|
    this.events.publish(apinamespace+'.attachingEventHandlers')
 | 
						|
 | 
						|
    // If we have proportional width, we sign up to events broadcasting "window resized" and checking if
 | 
						|
    // parent's width changed. If so, we (1) extract settings + data from current signature pad,
 | 
						|
    // (2) remove signature pad from parent, and (3) reinit new signature pad at new size with same settings, (rescaled) data.
 | 
						|
    conditionallyLinkCanvasResizeToWindowResize.call(
 | 
						|
        this
 | 
						|
        , this
 | 
						|
        , settings.width.toString(10)
 | 
						|
        , apinamespace, globalEvents
 | 
						|
    )
 | 
						|
 | 
						|
    // end of event handlers.
 | 
						|
    // ===============================
 | 
						|
 | 
						|
    this.resetCanvas(settings.data)
 | 
						|
 | 
						|
    // resetCanvas renders the data on the screen and fires ONE "change" event
 | 
						|
    // if there is data. If you have controls that rely on "change" firing
 | 
						|
    // attach them to something that runs before this.resetCanvas, like
 | 
						|
    // apinamespace+'.attachingEventHandlers' that fires a bit higher.
 | 
						|
    this.events.publish(apinamespace+'.initialized')
 | 
						|
 | 
						|
    return this
 | 
						|
} // end of initBase
 | 
						|
 | 
						|
//=========================================================================
 | 
						|
// jSignatureClass's methods and supporting fn's
 | 
						|
 | 
						|
jSignatureClass.prototype.resetCanvas = function(data){
 | 
						|
    var canvas = this.canvas
 | 
						|
    , settings = this.settings
 | 
						|
    , ctx = this.canvasContext
 | 
						|
    , isCanvasEmulator = this.isCanvasEmulator
 | 
						|
 | 
						|
    , cw = canvas.width
 | 
						|
    , ch = canvas.height
 | 
						|
 | 
						|
    // preparing colors, drawing area
 | 
						|
 | 
						|
    ctx.clearRect(0, 0, cw + 30, ch + 30)
 | 
						|
 | 
						|
    ctx.shadowColor = ctx.fillStyle = settings['background-color']
 | 
						|
    if (isCanvasEmulator){
 | 
						|
        // FLashCanvas fills with Black by default, covering up the parent div's background
 | 
						|
        // hence we refill
 | 
						|
        ctx.fillRect(0,0,cw + 30, ch + 30)
 | 
						|
    }
 | 
						|
 | 
						|
    ctx.lineWidth = Math.ceil(parseInt(settings.lineWidth, 10))
 | 
						|
    ctx.lineCap = ctx.lineJoin = "round"
 | 
						|
 | 
						|
    // signature line
 | 
						|
    ctx.strokeStyle = settings['decor-color']
 | 
						|
    ctx.shadowOffsetX = 0
 | 
						|
    ctx.shadowOffsetY = 0
 | 
						|
    var lineoffset = Math.round( ch / 5 )
 | 
						|
    basicLine(ctx, lineoffset * 1.5, ch - lineoffset, cw - (lineoffset * 1.5), ch - lineoffset)
 | 
						|
    ctx.strokeStyle = settings.color
 | 
						|
 | 
						|
    if (!isCanvasEmulator){
 | 
						|
        ctx.shadowColor = ctx.strokeStyle
 | 
						|
        ctx.shadowOffsetX = ctx.lineWidth * 0.5
 | 
						|
        ctx.shadowOffsetY = ctx.lineWidth * -0.6
 | 
						|
        ctx.shadowBlur = 0
 | 
						|
    }
 | 
						|
 | 
						|
    // setting up new dataEngine
 | 
						|
 | 
						|
    if (!data) { data = [] }
 | 
						|
 | 
						|
    var dataEngine = this.dataEngine = new DataEngine(
 | 
						|
        data
 | 
						|
        , this
 | 
						|
        , strokeStartCallback
 | 
						|
        , strokeAddCallback
 | 
						|
        , strokeEndCallback
 | 
						|
    )
 | 
						|
 | 
						|
    settings.data = data  // onwindowresize handler uses it, i think.
 | 
						|
    $(canvas).data(apinamespace+'.data', data)
 | 
						|
        .data(apinamespace+'.settings', settings)
 | 
						|
 | 
						|
    // we fire "change" event on every change in data.
 | 
						|
    // setting this up:
 | 
						|
    dataEngine.changed = (function(target, events, apinamespace) {
 | 
						|
        'use strict'
 | 
						|
        return function() {
 | 
						|
            events.publish(apinamespace+'.change')
 | 
						|
            target.trigger('change')
 | 
						|
        }
 | 
						|
    })(this.$parent, this.events, apinamespace)
 | 
						|
    // let's trigger change on all data reloads
 | 
						|
    dataEngine.changed()
 | 
						|
 | 
						|
    // import filters will be passing this back as indication of "we rendered"
 | 
						|
    return true
 | 
						|
}
 | 
						|
 | 
						|
function initializeCanvasEmulator(canvas){
 | 
						|
    if (canvas.getContext){
 | 
						|
        return false
 | 
						|
    } else {
 | 
						|
        // for cases when jSignature, FlashCanvas is inserted
 | 
						|
        // from one window into another (child iframe)
 | 
						|
        // 'window' and 'FlashCanvas' may be stuck behind
 | 
						|
        // in that other parent window.
 | 
						|
        // we need to find it
 | 
						|
        var window = canvas.ownerDocument.parentWindow
 | 
						|
        var FC = window.FlashCanvas ?
 | 
						|
            canvas.ownerDocument.parentWindow.FlashCanvas :
 | 
						|
            (
 | 
						|
                typeof FlashCanvas === "undefined" ?
 | 
						|
                undefined :
 | 
						|
                FlashCanvas
 | 
						|
            )
 | 
						|
 | 
						|
        if (FC) {
 | 
						|
            canvas = FC.initElement(canvas)
 | 
						|
 | 
						|
            var zoom = 1
 | 
						|
            // FlashCanvas uses flash which has this annoying habit of NOT scaling with page zoom.
 | 
						|
            // It matches pixel-to-pixel to screen instead.
 | 
						|
            // Since we are targeting ONLY IE 7, 8 with FlashCanvas, we will test the zoom only the IE8, IE7 way
 | 
						|
            if (window && window.screen && window.screen.deviceXDPI && window.screen.logicalXDPI){
 | 
						|
                zoom = window.screen.deviceXDPI * 1.0 / window.screen.logicalXDPI
 | 
						|
            }
 | 
						|
            if (zoom !== 1){
 | 
						|
                try {
 | 
						|
                    // We effectively abuse the brokenness of FlashCanvas and force the flash rendering surface to
 | 
						|
                    // occupy larger pixel dimensions than the wrapping, scaled up DIV and Canvas elems.
 | 
						|
                    $(canvas).children('object').get(0).resize(Math.ceil(canvas.width * zoom), Math.ceil(canvas.height * zoom))
 | 
						|
                    // And by applying "scale" transformation we can talk "browser pixels" to FlashCanvas
 | 
						|
                    // and have it translate the "browser pixels" to "screen pixels"
 | 
						|
                    canvas.getContext('2d').scale(zoom, zoom)
 | 
						|
                    // Note to self: don't reuse Canvas element. Repeated "scale" are cumulative.
 | 
						|
                } catch (ex) {}
 | 
						|
            }
 | 
						|
            return true
 | 
						|
        } else {
 | 
						|
            throw new Error("Canvas element does not support 2d context. jSignature cannot proceed.")
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
}
 | 
						|
 | 
						|
jSignatureClass.prototype.initializeCanvas = function(settings) {
 | 
						|
    // ===========
 | 
						|
    // Init + Sizing code
 | 
						|
 | 
						|
    var canvas = document.createElement('canvas')
 | 
						|
    , $canvas = $(canvas)
 | 
						|
 | 
						|
    // We cannot work with circular dependency
 | 
						|
    if (settings.width === settings.height && settings.height === 'ratio') {
 | 
						|
        settings.width = '100%'
 | 
						|
    }
 | 
						|
 | 
						|
    $canvas.css(
 | 
						|
        'margin'
 | 
						|
        , 0
 | 
						|
    ).css(
 | 
						|
        'padding'
 | 
						|
        , 0
 | 
						|
    ).css(
 | 
						|
        'border'
 | 
						|
        , 'none'
 | 
						|
    ).css(
 | 
						|
        'height'
 | 
						|
        , settings.height === 'ratio' || !settings.height ? 1 : settings.height.toString(10)
 | 
						|
    ).css(
 | 
						|
        'width'
 | 
						|
        , settings.width === 'ratio' || !settings.width ? 1 : settings.width.toString(10)
 | 
						|
    )
 | 
						|
 | 
						|
    $canvas.appendTo(this.$parent)
 | 
						|
 | 
						|
    // we could not do this until canvas is rendered (appended to DOM)
 | 
						|
    if (settings.height === 'ratio') {
 | 
						|
        $canvas.css(
 | 
						|
            'height'
 | 
						|
            , Math.round( $canvas.width() / settings.sizeRatio )
 | 
						|
        )
 | 
						|
    } else if (settings.width === 'ratio') {
 | 
						|
        $canvas.css(
 | 
						|
            'width'
 | 
						|
            , Math.round( $canvas.height() * settings.sizeRatio )
 | 
						|
        )
 | 
						|
    }
 | 
						|
 | 
						|
    $canvas.addClass(apinamespace)
 | 
						|
 | 
						|
    // canvas's drawing area resolution is independent from canvas's size.
 | 
						|
    // pixels are just scaled up or down when internal resolution does not
 | 
						|
    // match external size. So...
 | 
						|
 | 
						|
    canvas.width = $canvas.width()
 | 
						|
    canvas.height = $canvas.height()
 | 
						|
 | 
						|
    // Special case Sizing code
 | 
						|
 | 
						|
    this.isCanvasEmulator = initializeCanvasEmulator(canvas)
 | 
						|
 | 
						|
    // End of Sizing Code
 | 
						|
    // ===========
 | 
						|
 | 
						|
    // normally select preventer would be short, but
 | 
						|
    // Canvas emulator on IE does NOT provide value for Event. Hence this convoluted line.
 | 
						|
    canvas.onselectstart = function(e){if(e && e.preventDefault){e.preventDefault()}; if(e && e.stopPropagation){e.stopPropagation()}; return false;}
 | 
						|
 | 
						|
    return canvas
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
var GlobalJSignatureObjectInitializer = function(window){
 | 
						|
 | 
						|
    var globalEvents = new PubSubClass()
 | 
						|
 | 
						|
    // common "window resized" event listener.
 | 
						|
    // jSignature instances will subscribe to this chanel.
 | 
						|
    // to resize themselves when needed.
 | 
						|
    ;(function(globalEvents, apinamespace, $, window){
 | 
						|
        'use strict'
 | 
						|
 | 
						|
        var resizetimer
 | 
						|
        , runner = function(){
 | 
						|
            globalEvents.publish(
 | 
						|
                apinamespace + '.parentresized'
 | 
						|
            )
 | 
						|
        }
 | 
						|
 | 
						|
        // jSignature knows how to resize its content when its parent is resized
 | 
						|
        // window resize is the only way we can catch resize events though...
 | 
						|
        $(window).bind('resize.'+apinamespace, function(){
 | 
						|
            if (resizetimer) {
 | 
						|
                clearTimeout(resizetimer)
 | 
						|
            }
 | 
						|
            resizetimer = setTimeout(
 | 
						|
                runner
 | 
						|
                , 500
 | 
						|
            )
 | 
						|
        })
 | 
						|
        // when mouse exists canvas element and "up"s outside, we cannot catch it with
 | 
						|
        // callbacks attached to canvas. This catches it outside.
 | 
						|
        .bind('mouseup.'+apinamespace, function(e){
 | 
						|
            globalEvents.publish(
 | 
						|
                apinamespace + '.windowmouseup'
 | 
						|
            )
 | 
						|
        })
 | 
						|
 | 
						|
    })(globalEvents, apinamespace, $, window)
 | 
						|
 | 
						|
    var jSignatureInstanceExtensions = {
 | 
						|
 | 
						|
        'exampleExtension':function(extensionName){
 | 
						|
            // we are called very early in instance's life.
 | 
						|
            // right after the settings are resolved and
 | 
						|
            // jSignatureInstance.events is created
 | 
						|
            // and right before first ("jSignature.initializing") event is called.
 | 
						|
            // You don't really need to manupilate
 | 
						|
            // jSignatureInstance directly, just attach
 | 
						|
            // a bunch of events to jSignatureInstance.events
 | 
						|
            // (look at the source of jSignatureClass to see when these fire)
 | 
						|
            // and your special pieces of code will attach by themselves.
 | 
						|
 | 
						|
            // this function runs every time a new instance is set up.
 | 
						|
            // this means every var you create will live only for one instance
 | 
						|
            // unless you attach it to something outside, like "window."
 | 
						|
            // and pick it up later from there.
 | 
						|
 | 
						|
            // when globalEvents' events fire, 'this' is globalEvents object
 | 
						|
            // when jSignatureInstance's events fire, 'this' is jSignatureInstance
 | 
						|
 | 
						|
            // Here,
 | 
						|
            // this = is new jSignatureClass's instance.
 | 
						|
 | 
						|
            // The way you COULD approch setting this up is:
 | 
						|
            // if you have multistep set up, attach event to "jSignature.initializing"
 | 
						|
            // that attaches other events to be fired further lower the init stream.
 | 
						|
            // Or, if you know for sure you rely on only one jSignatureInstance's event,
 | 
						|
            // just attach to it directly
 | 
						|
 | 
						|
            this.events.subscribe(
 | 
						|
                // name of the event
 | 
						|
                apinamespace + '.initializing'
 | 
						|
                // event handlers, can pass args too, but in majority of cases,
 | 
						|
                // 'this' which is jSignatureClass object instance pointer is enough to get by.
 | 
						|
                , function(){
 | 
						|
                    if (this.settings.hasOwnProperty('non-existent setting category?')) {
 | 
						|
                        console.log(extensionName + ' is here')
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            )
 | 
						|
        }
 | 
						|
 | 
						|
    }
 | 
						|
 | 
						|
    var exportplugins = {
 | 
						|
        'default':function(data){return this.toDataURL()}
 | 
						|
        , 'native':function(data){return data}
 | 
						|
        , 'image':function(data){
 | 
						|
            /*this = canvas elem */
 | 
						|
            var imagestring = this.toDataURL()
 | 
						|
 | 
						|
            if (typeof imagestring === 'string' &&
 | 
						|
                imagestring.length > 4 &&
 | 
						|
                imagestring.slice(0,5) === 'data:' &&
 | 
						|
                imagestring.indexOf(',') !== -1){
 | 
						|
 | 
						|
                var splitterpos = imagestring.indexOf(',')
 | 
						|
 | 
						|
                return [
 | 
						|
                    imagestring.slice(5, splitterpos)
 | 
						|
                    , imagestring.substr(splitterpos + 1)
 | 
						|
                ]
 | 
						|
            }
 | 
						|
            return []
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    // will be part of "importplugins"
 | 
						|
    function _renderImageOnCanvas( data, formattype, rerendercallable ) {
 | 
						|
        'use strict'
 | 
						|
        // #1. Do NOT rely on this. No worky on IE
 | 
						|
        //   (url max len + lack of base64 decoder + possibly other issues)
 | 
						|
        // #2. This does NOT affect what is captured as "signature" as far as vector data is
 | 
						|
        // concerned. This is treated same as "signature line" - i.e. completely ignored
 | 
						|
        // the only time you see imported image data exported is if you export as image.
 | 
						|
 | 
						|
        // we do NOT call rerendercallable here (unlike in other import plugins)
 | 
						|
        // because importing image does absolutely nothing to the underlying vector data storage
 | 
						|
        // This could be a way to "import" old signatures stored as images
 | 
						|
        // This could also be a way to import extra decor into signature area.
 | 
						|
 | 
						|
//        var img = new Image()
 | 
						|
//        // this = Canvas DOM elem. Not jQuery object. Not Canvas's parent div.
 | 
						|
//        , c = this
 | 
						|
//
 | 
						|
//        img.onload = function() {
 | 
						|
//            var ctx = c.getContext("2d").drawImage(
 | 
						|
//                img, 0, 0
 | 
						|
//                , ( img.width < c.width) ? img.width : c.width
 | 
						|
//                , ( img.height < c.height) ? img.height : c.height
 | 
						|
//            )
 | 
						|
//        }
 | 
						|
//
 | 
						|
//        img.src = 'data:' + formattype + ',' + data
 | 
						|
 | 
						|
        var c = new Image,
 | 
						|
            e = this;
 | 
						|
        c.onload = function() {
 | 
						|
            var a = e.getContext("2d"),
 | 
						|
                b = a.shadowColor;
 | 
						|
            a.shadowColor = "transparent";
 | 
						|
            a.drawImage(c, 0, 0, c.width < e.width ? c.width : e.width, c.height <
 | 
						|
                e.height ? c.height : e.height);
 | 
						|
            a.shadowColor = b
 | 
						|
        };
 | 
						|
        c.src = "data:" + formattype + "," + data
 | 
						|
    }
 | 
						|
 | 
						|
    var importplugins = {
 | 
						|
        'native':function(data, formattype, rerendercallable){
 | 
						|
            // we expect data as Array of objects of arrays here - whatever 'default' EXPORT plugin spits out.
 | 
						|
            // returning Truthy to indicate we are good, all updated.
 | 
						|
            rerendercallable( data )
 | 
						|
        }
 | 
						|
        , 'image': _renderImageOnCanvas
 | 
						|
        , 'image/png;base64': _renderImageOnCanvas
 | 
						|
        , 'image/jpeg;base64': _renderImageOnCanvas
 | 
						|
        , 'image/jpg;base64': _renderImageOnCanvas
 | 
						|
    }
 | 
						|
 | 
						|
    function _clearDrawingArea( data ) {
 | 
						|
        this.find('canvas.'+apinamespace)
 | 
						|
            .add(this.filter('canvas.'+apinamespace))
 | 
						|
            .data(apinamespace+'.this').resetCanvas( data )
 | 
						|
        return this
 | 
						|
    }
 | 
						|
 | 
						|
    function _setDrawingData( data, formattype ) {
 | 
						|
        var undef
 | 
						|
 | 
						|
        if (formattype === undef && typeof data === 'string' && data.substr(0,5) === 'data:') {
 | 
						|
            formattype = data.slice(5).split(',')[0]
 | 
						|
            // 5 chars of "data:" + mimetype len + 1 "," char = all skipped.
 | 
						|
            data = data.slice(6 + formattype.length)
 | 
						|
            if (formattype === data) return
 | 
						|
        }
 | 
						|
 | 
						|
        var $canvas = this.find('canvas.'+apinamespace).add(this.filter('canvas.'+apinamespace))
 | 
						|
 | 
						|
        if (!importplugins.hasOwnProperty(formattype)){
 | 
						|
            throw new Error(apinamespace + " is unable to find import plugin with for format '"+ String(formattype) +"'")
 | 
						|
        } else if ($canvas.length !== 0){
 | 
						|
            importplugins[formattype].call(
 | 
						|
                $canvas[0]
 | 
						|
                , data
 | 
						|
                , formattype
 | 
						|
                , (function(jSignatureInstance){
 | 
						|
                    return function(){ return jSignatureInstance.resetCanvas.apply(jSignatureInstance, arguments) }
 | 
						|
                })($canvas.data(apinamespace+'.this'))
 | 
						|
            )
 | 
						|
        }
 | 
						|
 | 
						|
        return this
 | 
						|
    }
 | 
						|
 | 
						|
    var elementIsOrphan = function(e){
 | 
						|
        var topOfDOM = false
 | 
						|
        e = e.parentNode
 | 
						|
        while (e && !topOfDOM){
 | 
						|
            topOfDOM = $(e).find(".o_form_view")
 | 
						|
            e = e.parentNode
 | 
						|
        }
 | 
						|
        return !topOfDOM
 | 
						|
    }
 | 
						|
 | 
						|
    //These are exposed as methods under $obj.jSignature('methodname', *args)
 | 
						|
    var plugins = {'export':exportplugins, 'import':importplugins, 'instance': jSignatureInstanceExtensions}
 | 
						|
    , methods = {
 | 
						|
        'init' : function( options ) {
 | 
						|
            return this.each( function() {
 | 
						|
                if (!elementIsOrphan(this)) {
 | 
						|
                    new jSignatureClass(this, options, jSignatureInstanceExtensions)
 | 
						|
                }
 | 
						|
            })
 | 
						|
        }
 | 
						|
        , 'getSettings' : function() {
 | 
						|
            return this.find('canvas.'+apinamespace)
 | 
						|
                .add(this.filter('canvas.'+apinamespace))
 | 
						|
                .data(apinamespace+'.this').settings
 | 
						|
        }
 | 
						|
        // around since v1
 | 
						|
        , 'clear' : _clearDrawingArea
 | 
						|
        // was mistakenly introduced instead of 'clear' in v2
 | 
						|
        , 'reset' : _clearDrawingArea
 | 
						|
        , 'addPlugin' : function(pluginType, pluginName, callable){
 | 
						|
            if (plugins.hasOwnProperty(pluginType)){
 | 
						|
                plugins[pluginType][pluginName] = callable
 | 
						|
            }
 | 
						|
            return this
 | 
						|
        }
 | 
						|
        , 'listPlugins' : function(pluginType){
 | 
						|
            var answer = []
 | 
						|
            if (plugins.hasOwnProperty(pluginType)){
 | 
						|
                var o = plugins[pluginType]
 | 
						|
                for (var k in o){
 | 
						|
                    if (o.hasOwnProperty(k)){
 | 
						|
                        answer.push(k)
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            }
 | 
						|
            return answer
 | 
						|
        }
 | 
						|
        , 'getData' : function( formattype ) {
 | 
						|
            var undef, $canvas=this.find('canvas.'+apinamespace).add(this.filter('canvas.'+apinamespace))
 | 
						|
            if (formattype === undef) formattype = 'default'
 | 
						|
            if ($canvas.length !== 0 && exportplugins.hasOwnProperty(formattype)){
 | 
						|
                return exportplugins[formattype].call(
 | 
						|
                    $canvas.get(0) // canvas dom elem
 | 
						|
                    , $canvas.data(apinamespace+'.data') // raw signature data as array of objects of arrays
 | 
						|
                )
 | 
						|
            }
 | 
						|
        }
 | 
						|
        // around since v1. Took only one arg - data-url-formatted string with (likely png of) signature image
 | 
						|
        , 'importData' : _setDrawingData
 | 
						|
        // was mistakenly introduced instead of 'importData' in v2
 | 
						|
        , 'setData' : _setDrawingData
 | 
						|
        // this is one and same instance for all jSignature.
 | 
						|
        , 'globalEvents' : function(){return globalEvents}
 | 
						|
        // there will be a separate one for each jSignature instance.
 | 
						|
        , 'events' : function() {
 | 
						|
            return this.find('canvas.'+apinamespace)
 | 
						|
                    .add(this.filter('canvas.'+apinamespace))
 | 
						|
                    .data(apinamespace+'.this').events
 | 
						|
        }
 | 
						|
    } // end of methods declaration.
 | 
						|
 | 
						|
    $.fn[apinamespace] = function(method) {
 | 
						|
        'use strict'
 | 
						|
        if ( !method || typeof method === 'object' ) {
 | 
						|
            return methods.init.apply( this, arguments )
 | 
						|
        } else if ( typeof method === 'string' && methods[method] ) {
 | 
						|
            return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 ))
 | 
						|
        } else {
 | 
						|
            $.error( 'Method ' +  String(method) + ' does not exist on jQuery.' + apinamespace )
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
} // end of GlobalJSignatureObjectInitializer
 | 
						|
 | 
						|
GlobalJSignatureObjectInitializer(window)
 | 
						|
 | 
						|
})(); |