<!--   <div class="trigger"></div> -->
<!--   <article> -->
  
    <p>
      <a href="#" class="underline title">Underline</a>
    </p>
    <p class="text">
      <a href="#" class="underline">Testing descenders</a>
        <br>
      <a href="#" class="underline">to see g and y and p</a>
        <br>
      <a href="#" class="underline">with a nice underline</a>
        <br>
      <a href="#" class="underline">jump google quick yogurt</a>
        <br>
      <a href="#" class="underline">spanning multiple lines is still broken though...</a>
        <br>
    </p>
  
<!--   </article> -->
	
.underline {
	position: relative;
}
.underline span {
  pointer-events: none;
}
.underline canvas {
  	pointer-events: auto;
	position: absolute;
	top: 0;
	left: 0;
	/*background-color: rgba(222, 222, 222, 0.1);*/
	/*z-index: -1;*/
	-moz-user-select: none; 
	-khtml-user-select: none; 
	-webkit-user-select: none; 
	-o-user-select: none; 
	cursor: pointer;
}

body {
  font-size: 24px;
  line-height: 36px;
  color: #212121;
  margin: 0 auto;
  width: 500px;
  padding: 0;
  -moz-osx-font-smoothing: grayscale;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
   }

a {
  text-decoration: none;
  color: #212121; }

p {
  font-family: "EB Garamond";
  margin-top: 24px; }

p.text a {
  font-size: 48px;
  line-height: 1.4em;
  font-style: italic; }

p a.title {
  font-size: 126px;
  line-height: 1.4em; }

p em {
  font-style: italic; }

p:empty {
  margin-top: 0px; }

View Compiled
/* Universal Module Definition */
(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD. Register as a named AMD module.
    define('baselineRatio', [], function () {
      return (root.baselineRatio = factory());
    });
  } else if (typeof exports === 'object') {
    // Node. Does not work with strict CommonJS, but
    // only CommonJS-like enviroments that support module.exports,
    // like Node.
    module.exports = factory();
  } else {
    // Browser globals
    root.baselineRatio = factory();
  }
}(this, function () {
  var baselineRatio = function(elem) {
    // Get the baseline in the context of whatever element is passed in.
    elem = elem || document.body;

    // The container is a little defenseive.
    var container = document.createElement('div');
    container.style.display = "block";
    container.style.position = "absolute";
    container.style.bottom = "0";
    container.style.right = "0";
    container.style.width = "0px";
    container.style.height = "0px";
    container.style.margin = "0";
    container.style.padding = "0";
    container.style.visibility = "hidden";
    container.style.overflow = "hidden";

    // Intentionally unprotected style definition.
    var small = document.createElement('span');
    var large = document.createElement('span');

    // Large numbers help improve accuracy.
    small.style.fontSize = "0px";
    large.style.fontSize = "2000px";

    small.innerHTML = "X";
    large.innerHTML = "X";

    container.appendChild(small);
    container.appendChild(large);

    // Put the element in the DOM for a split second.
    elem.appendChild(container);
    var smalldims = small.getBoundingClientRect();
    var largedims = large.getBoundingClientRect();
    elem.removeChild(container);

    // Calculate where the baseline was, percentage-wise.
    var baselineposition = smalldims.top - largedims.top;
    var height = largedims.height;

    return 1 - (baselineposition / height);
  }

  return baselineRatio;
}));

/*!
 * classie v1.0.1
 * class helper functions
 * from bonzo https://github.com/ded/bonzo
 * MIT license
 * 
 * classie.has( elem, 'my-class' ) -> true/false
 * classie.add( elem, 'my-new-class' )
 * classie.remove( elem, 'my-unwanted-class' )
 * classie.toggle( elem, 'my-class' )
 */

/*jshint browser: true, strict: true, undef: true, unused: true */
/*global define: false, module: false */

( function( window ) {

'use strict';

// class helper functions from bonzo https://github.com/ded/bonzo

function classReg( className ) {
  return new RegExp("(^|\\s+)" + className + "(\\s+|$)");
}

// classList support for class management
// altho to be fair, the api sucks because it won't accept multiple classes at once
var hasClass, addClass, removeClass;

if ( 'classList' in document.documentElement ) {
  hasClass = function( elem, c ) {
    return elem.classList.contains( c );
  };
  addClass = function( elem, c ) {
    elem.classList.add( c );
  };
  removeClass = function( elem, c ) {
    elem.classList.remove( c );
  };
}
else {
  hasClass = function( elem, c ) {
    return classReg( c ).test( elem.className );
  };
  addClass = function( elem, c ) {
    if ( !hasClass( elem, c ) ) {
      elem.className = elem.className + ' ' + c;
    }
  };
  removeClass = function( elem, c ) {
    elem.className = elem.className.replace( classReg( c ), ' ' );
  };
}

function toggleClass( elem, c ) {
  var fn = hasClass( elem, c ) ? removeClass : addClass;
  fn( elem, c );
}

var classie = {
  // full names
  hasClass: hasClass,
  addClass: addClass,
  removeClass: removeClass,
  toggleClass: toggleClass,
  // short names
  has: hasClass,
  add: addClass,
  remove: removeClass,
  toggle: toggleClass
};

// transport
if ( typeof define === 'function' && define.amd ) {
  // AMD
  define( classie );
} else if ( typeof exports === 'object' ) {
  // CommonJS
  module.exports = classie;
} else {
  // browser global
  window.classie = classie;
}

})( window );

function MultipleUnderline(element, underlineStyles, elementStyles) {
    //ctor
    this.element = element;

    this.text = this.element.textContent;

    this.underlineStyles = underlineStyles;

    // this.elementStyles = getElementStyles(element);
    this.elementStyles = elementStyles;

    this.canvas = document.createElement("canvas");
    this.ctx = this.canvas.getContext('2d');


    this.ratio = window.devicePixelRatio;
        this.canvas.width = this.elementStyles.width*this.ratio;
        this.canvas.height = this.elementStyles.height*this.ratio;
        // this.canvas.height = this.canvas.clientHeight + this.elementStyles.lineHeight;
        this.canvas.style.left = this.elementStyles.canvasLeft + 'px';
        this.element.appendChild(this.canvas);
        this.canvas.style.width =  this.elementStyles.width + 'px';

        this.ctx.font = this.font = this.elementStyles.fontStyle + ' ' 
                        + multiplyValue(this.elementStyles.fontSize, this.ratio) + ' ' 
                        + this.elementStyles.fontFamily;

    this.multipleRedrawActive = false;
    if (is_chrome) {
        // chrome floor the lineheight when it is not a whole number
        // this.elementStyles.lineHeight = Math.floor(this.elementStyles.lineHeight * this.ratio);
        this.elementStyles.lineHeight = this.elementStyles.lineHeight * this.ratio;
    } else {
        this.elementStyles.lineHeight = this.elementStyles.lineHeight * this.ratio;
    }

    // determine the text-underline-width / strokeWidth
    var dotWidth = this.ctx.measureText('.')['width'];
    if (this.underlineStyles['text-underline-width'] == "auto") {
        // if set to auto, calculate the optimized width based on font
        this.strokeWidth = dotWidth/12;
    } else {
        //if set to px value, todo: other unit such as em?
        this.strokeWidth = this.underlineStyles['text-underline-width'];
        //get number value
        this.strokeWidth = parseFloat(this.strokeWidth)*this.ratio;
    }

    // determine the text-underline-position / underlinePosition
    // text-underline-position in ratio, todo: default and user set position ratio
    if (this.underlineStyles['text-underline-position'] == "auto") {
        // if set to auto, calculate the optimized width based on font
        this.underlinePosition = parseFloat(this.elementStyles.fontSize) * this.ratio 
                * ( 1 - this.elementStyles.baselinePositionRatio + 
                    this.elementStyles.baselinePositionRatio * 0.4)
                + this.strokeWidth/2;
    } else {
        //if set to ratio value, todo: other unit such as em, px?
        var userUnderlinePosition = parseFloat(this.underlineStyles['text-underline-position']);
        // console.log(userUnderlinePosition);
        this.underlinePosition = parseFloat(this.elementStyles.fontSize) * this.ratio * 
                ( 1 - this.elementStyles.baselinePositionRatio + 
                    this.elementStyles.baselinePositionRatio * userUnderlinePosition)
                + this.strokeWidth/2;
    }


    var adjustValue = optimalStrokeWidthPos(this.strokeWidth, this.underlinePosition);
    this.strokeWidth = adjustValue.strokeWidth;
    this.underlinePosition = adjustValue.posY;

    this.lines = [];
    this.myStrings = [];

    var words = this.text.match(/[^\s-]+-?\s?/g);
    var line = '';

    var linePositionY = 0;
    var firstLineCount = 0;
    for (var n = 0; n < words.length; n++) {
        // add the whitespace after getting the width measurement
        if (words[n].match(/\s+$/)) {
            // the last character of words[n] is whitespace
            var newWord = words[n].replace(/\s+$/, '');
            var testLine = line + newWord;
            var testLineMetrics = this.ctx.measureText(testLine);
            var testLineWidth = testLineMetrics.width;
            testLine = testLine + ' ';
        } else {
            var testLine = line + words[n];
            var testLineMetrics = this.ctx.measureText(testLine);
            var testLineWidth = testLineMetrics.width;
        }

        if (!firstLineCount) {
            //the first line, should consider startingPointX
            if (testLineWidth + this.elementStyles.textIndent * this.ratio > this.elementStyles.parentWidth * this.ratio && n > 0) {
                //  draw the underline
                if (line.match(/\s+$/)) {
                    // the last character of line is whitespace               
                    var lineMetrics = this.ctx.measureText(line.replace(/\s+$/, ''));
                    var lineWidth = lineMetrics.width;
                } else {
                    var lineMetrics = this.ctx.measureText(line);
                    var lineWidth = lineMetrics.width;
                }

                var tempLine = {
                    lineText: line,
                    lineTextIndent: this.elementStyles.textIndent * this.ratio - 0.2,
                    linePositionY: linePositionY,
                    lineMeasureWidth: lineWidth
                }
                this.lines.push(tempLine)

                line = words[n];
                linePositionY += this.elementStyles.lineHeight;
                firstLineCount++;
            } else {
                line = testLine;
            }
        } else {
            if (testLineWidth > this.elementStyles.parentWidth * this.ratio && n > 0) {
                //  draw the underline
                if (line.match(/\s+$/)) {
                    // the last character of line is whitespace               
                    var lineMetrics = this.ctx.measureText(line.replace(/\s+$/, ''));
                    var lineWidth = lineMetrics.width;
                } else {
                    var lineMetrics = this.ctx.measureText(line);
                    var lineWidth = lineMetrics.width;
                }

                var tempLine = {
                    lineText: line,
                    lineTextIndent: -0.2,
                    linePositionY: linePositionY,
                    lineMeasureWidth: lineWidth
                }
                this.lines.push(tempLine);

                line = words[n];
                linePositionY += this.elementStyles.lineHeight;
            } else {
                line = testLine;
            }
        }
    }
    // draw the last line
    //  draw the underline
    if (line.match(/\s+$/)) {
        // the last character of line is whitespace               
        var lineMetrics = this.ctx.measureText(line.replace(/\s+$/, ''));
        var lineWidth = lineMetrics.width;
    } else {
        var lineMetrics = this.ctx.measureText(line);
        var lineWidth = lineMetrics.width;
    }

    var tempLine = {
        lineText: line,
        lineTextIndent: -0.2,
        linePositionY: linePositionY,
        lineMeasureWidth: lineWidth
    }
    this.lines.push(tempLine);
    for(var i = 0; i < this.lines.length; i++) {
        var tempLine = this.lines[i];
        var myString = new GuitarString(
                this.ctx, 
                new Point(tempLine.lineTextIndent, tempLine.linePositionY + this.underlinePosition), 
                new Point(tempLine.lineTextIndent + tempLine.lineMeasureWidth, tempLine.linePositionY + this.underlinePosition), 
                this.strokeWidth, this.underlineStyles['text-underline-color'], this.ratio);
        this.myStrings.push(myString);
    }

    this.drawUnderline();
    this.drawHoles();

}

MultipleUnderline.prototype.drawUnderline = function(){
    // draw the underline
    for(var i = 0; i < this.myStrings.length; i++) {
        var tempString = this.myStrings[i];
        // tempString.clear();
            tempString.update();
            tempString.draw();
    }

};


MultipleUnderline.prototype.drawHoles = function(){
    // draw the font stroke
    for(var i = 0; i < this.lines.length; i++) {
        var tempLine = this.lines[i];

        this.ctx.globalCompositeOperation = "destination-out";
        this.ctx.font = this.font;

        this.ctx.fillStyle = 'green';
        this.ctx.textBaseline = 'top';
        this.ctx.fillText(tempLine.lineText, tempLine.lineTextIndent, tempLine.linePositionY);

        this.ctx.lineWidth = 2*this.ratio + this.strokeWidth*3.6;
        this.ctx.strokeStyle = 'blue';
        this.ctx.strokeText(tempLine.lineText, tempLine.lineTextIndent, tempLine.linePositionY);

    }
}

MultipleUnderline.prototype.clear = function(){
    // clear
    var lastMultipleRedrawActive = this.multipleRedrawActive;
    this.multipleRedrawActive = false;
    for(var i = 0; i < this.myStrings.length; i++) {
        var tempString = this.myStrings[i];
        // this.myString.clear();
        // console.log(tempString.redrawActive);
        if(tempString.redrawActive) {
            this.multipleRedrawActive = true;
        }
    }
    // console.log(this.multipleRedrawActive);
    if (this.multipleRedrawActive) {
        console.log('clear now!')
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    }
    // if (!lastMultipleRedrawActive && this.multipleRedrawActive) {
    //     for(var i = 0; i < this.myStrings.length; i++) {
    //         var tempString = this.myStrings[i];
    //         tempString.drawLine();
    //     }
    // }

};

MultipleUnderline.prototype.update = function(){
    //update
};


MultipleUnderline.prototype.draw = function(){
    // draw
    if (this.multipleRedrawActive) {
        this.drawUnderline();
        this.drawHoles();
    }
};

function MultipleUnderline(element, underlineStyles, elementStyles) {
    //ctor
    this.element = element;

    this.text = this.element.textContent;

    this.underlineStyles = underlineStyles;

    // this.elementStyles = getElementStyles(element);
    this.elementStyles = elementStyles;

    this.canvas = document.createElement("canvas");
        this.canvas.width = this.elementStyles.width;
        this.canvas.height = this.elementStyles.height;
        this.canvas.style.left = this.elementStyles.canvasLeft + 'px';
        this.element.appendChild(this.canvas);
        this.canvas.width = this.canvas.clientWidth;
        this.canvas.height = this.canvas.clientHeight + this.elementStyles.lineHeight;

    this.ctx = this.canvas.getContext('2d');
    this.ctx.font = this.font = this.elementStyles.fontStyle + ' ' + this.elementStyles.fontSize + ' ' + this.elementStyles.fontFamily;

    this.multipleRedrawActive = false;
    if (is_chrome) {
        // chrome floor the lineheight when it is not a whole number
        this.elementStyles.lineHeight = Math.floor(this.elementStyles.lineHeight);
    }


    // determine the text-underline-width / strokeWidth
    this.dotWidth = this.ctx.measureText('.')['width'];
    if (this.underlineStyles['text-underline-width'] == "auto") {
        // if set to auto, calculate the optimized width based on font
        if (this.dotWidth / 6 <= 2) {
            this.strokeWidth = Math.round(this.dotWidth / 3) / 2;
        } else {
            this.strokeWidth = Math.round(this.dotWidth / 6);
        }
    } else {
        //if set to px value
        this.strokeWidth = this.underlineStyles['text-underline-width'];
        //get number value
        this.strokeWidth = parseFloat(this.strokeWidth);
    }

    // determine the text-underline-position / underlinePosition
    // text-underline-position in ratio
    this.underlinePosition = parseFloat(this.elementStyles.fontSize) * 0.89;
    if (this.strokeWidth <= 1 || (this.strokeWidth % 2 && this.strokeWidth > 2)) {
        this.underlinePosition = Math.round(this.underlinePosition - 0.5) + 0.5;
    } else {
        this.underlinePosition = Math.round(this.underlinePosition);
    }

    this.lines = [];
    this.myStrings = [];

    var words = this.text.match(/[^\s-]+-?\s?/g);
    var line = '';

    var linePositionY = 0;
    var firstLineCount = 0;
    for (var n = 0; n < words.length; n++) {
        // add the whitespace after getting the width measurement
        if (words[n].match(/\s+$/)) {
            // the last character of words[n] is whitespace
            var newWord = words[n].replace(/\s+$/, '');
            var testLine = line + newWord;
            var testLineMetrics = this.ctx.measureText(testLine);
            var testLineWidth = testLineMetrics.width;
            testLine = testLine + ' ';
        } else {
            var testLine = line + words[n];
            var testLineMetrics = this.ctx.measureText(testLine);
            var testLineWidth = testLineMetrics.width;
        }

        if (!firstLineCount) {
            //the first line, should consider startingPointX
            if (testLineWidth + this.elementStyles.textIndent > this.elementStyles.parentWidth && n > 0) {
                //  draw the underline
                if (line.match(/\s+$/)) {
                    // the last character of line is whitespace               
                    var lineMetrics = this.ctx.measureText(line.replace(/\s+$/, ''));
                    var lineWidth = lineMetrics.width;
                } else {
                    var lineMetrics = this.ctx.measureText(line);
                    var lineWidth = lineMetrics.width;
                }

                var tempLine = {
                    lineText: line,
                    lineTextIndent: this.elementStyles.textIndent,
                    linePositionY: linePositionY,
                    lineMeasureWidth: lineWidth
                }
                this.lines.push(tempLine)

                line = words[n];
                linePositionY += this.elementStyles.lineHeight;
                firstLineCount++;
            } else {
                line = testLine;
            }
        } else {
            if (testLineWidth > this.elementStyles.parentWidth && n > 0) {
                //  draw the underline
                if (line.match(/\s+$/)) {
                    // the last character of line is whitespace               
                    var lineMetrics = this.ctx.measureText(line.replace(/\s+$/, ''));
                    var lineWidth = lineMetrics.width;
                } else {
                    var lineMetrics = this.ctx.measureText(line);
                    var lineWidth = lineMetrics.width;
                }

                var tempLine = {
                    lineText: line,
                    lineTextIndent: 0,
                    linePositionY: linePositionY,
                    lineMeasureWidth: lineWidth
                }
                this.lines.push(tempLine);

                line = words[n];
                linePositionY += this.elementStyles.lineHeight;
            } else {
                line = testLine;
            }
        }
    }
    // draw the last line
    //  draw the underline
    if (line.match(/\s+$/)) {
        // the last character of line is whitespace               
        var lineMetrics = this.ctx.measureText(line.replace(/\s+$/, ''));
        var lineWidth = lineMetrics.width;
    } else {
        var lineMetrics = this.ctx.measureText(line);
        var lineWidth = lineMetrics.width;
    }

    var tempLine = {
        lineText: line,
        lineTextIndent: 0,
        linePositionY: linePositionY,
        lineMeasureWidth: lineWidth
    }
    this.lines.push(tempLine);



    for(var i = 0; i < this.lines.length; i++) {
        var tempLine = this.lines[i];
        var myString = new GuitarString(
                this.ctx, 
                new Point(tempLine.lineTextIndent, tempLine.linePositionY + this.underlinePosition), 
                new Point(tempLine.lineTextIndent + tempLine.lineMeasureWidth, tempLine.linePositionY + this.underlinePosition), 
                this.strokeWidth, this.underlineStyles['text-underline-color'], 1);
        this.myStrings.push(myString);
    }

    this.drawUnderline();
    this.drawHoles();

}


MultipleUnderline.prototype.clear = function(){
    // clear
    var lastMultipleRedrawActive = this.multipleRedrawActive;
    this.multipleRedrawActive = false;
    for(var i = 0; i < this.myStrings.length; i++) {
        var tempString = this.myStrings[i];
        // this.myString.clear();
        // console.log(tempString.redrawActive);
        if(tempString.redrawActive) {
            this.multipleRedrawActive = true;
        }
    }
    // console.log(this.multipleRedrawActive);
    if (this.multipleRedrawActive) {
        console.log('clear now!')
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    }
    // if (!lastMultipleRedrawActive && this.multipleRedrawActive) {
    //     for(var i = 0; i < this.myStrings.length; i++) {
    //         var tempString = this.myStrings[i];
    //         tempString.drawLine();
    //     }
    // }

};

MultipleUnderline.prototype.update = function(){
    //update
};


MultipleUnderline.prototype.draw = function(){
    // draw
    if (this.multipleRedrawActive) {
        this.drawUnderline();
        this.drawHoles();
    }
};


MultipleUnderline.prototype.drawUnderline = function(){
    // draw the underline
    for(var i = 0; i < this.myStrings.length; i++) {
        var tempString = this.myStrings[i];
        // tempString.clear();
            tempString.update();
            tempString.draw();
    }

};


MultipleUnderline.prototype.drawHoles = function(){
    // draw the font stroke
    for(var i = 0; i < this.lines.length; i++) {
        var tempLine = this.lines[i];

        this.ctx.globalCompositeOperation = "destination-out";
        this.ctx.font = this.font;
        this.ctx.fillStyle = 'green';
        this.ctx.textBaseline = 'top';
        this.ctx.fillText(tempLine.lineText, tempLine.lineTextIndent, tempLine.linePositionY);
        this.ctx.lineWidth = 3 + this.strokeWidth;
        this.ctx.strokeStyle = 'blue';
        this.ctx.strokeText(tempLine.lineText, tempLine.lineTextIndent, tempLine.linePositionY);

    }
}

var multiplyValue = function(value, multiplier){
    var str = value;
    var m = multiplier;
    var result = str.match(/(\d*\.?\d*)(.*)/);
    //http://stackoverflow.com/questions/2868947/split1px-into-1px-1-px-in-javascript
    return result[1] * m + result[2];
}

var optimalStrokeWidthPos = function(strokeWidth, posY){
    if ( strokeWidth < 1) {
        posY = Math.round(posY - 0.5) + 0.5;
    } else if ( strokeWidth >= 1 ) {
        strokeWidth = Math.round( strokeWidth );
        if ( strokeWidth % 2 ){
            // odd, posY -> 0.5
            posY = Math.round(posY - 0.5) + 0.5;
        } else {
            // even, posY -> 1
            posY = Math.round(posY);
        }
    }
    return {
        strokeWidth: strokeWidth,
        posY: posY
    }
}

function SingleUnderline(element, underlineStyles, elementStyles) {
    //ctor
    this.element = element;

    this.text = this.element.textContent;

    this.underlineStyles = underlineStyles;

    this.elementStyles = elementStyles;
    this.redrawActive = false;

    this.canvas = document.createElement("canvas");
    this.ctx = this.canvas.getContext('2d');
    
    this.ratio = window.devicePixelRatio;
        this.canvas.width = this.elementStyles.width*this.ratio;
        this.canvas.height = this.elementStyles.height*this.ratio;
        this.element.appendChild(this.canvas);
        this.canvas.style.width =  this.elementStyles.width + 'px';

        this.ctx.font = this.font = this.elementStyles.fontStyle + ' ' 
                        + multiplyValue(this.elementStyles.fontSize, this.ratio) + ' ' 
                        + this.elementStyles.fontFamily;

    // determine the text-underline-width / strokeWidth
    var dotWidth = this.ctx.measureText('.')['width'];
    if (this.underlineStyles['text-underline-width'] == "auto") {
        // if set to auto, calculate the optimized width based on font
        this.strokeWidth = dotWidth/12;
    } else {
        //if set to px value, todo: other unit such as em?
        this.strokeWidth = this.underlineStyles['text-underline-width'];
        //get number value
        this.strokeWidth = parseFloat(this.strokeWidth)*this.ratio;
    }


    // determine the text-underline-position / underlinePosition
    // text-underline-position in ratio, todo: default and user set position ratio
    if (this.underlineStyles['text-underline-position'] == "auto") {
        // if set to auto, calculate the optimized width based on font
        // console.log(this.elementStyles.baselinePositionRatio);
        this.underlinePosition = parseFloat(this.elementStyles.height) * this.ratio 
                * ( 1 - this.elementStyles.baselinePositionRatio + 
                    this.elementStyles.baselinePositionRatio * 0.4)
                + this.strokeWidth/2;
    } else {
        //if set to ratio value, todo: other unit such as em, px?
        var userUnderlinePosition = parseFloat(this.underlineStyles['text-underline-position']);
        // console.log(this.elementStyles.baselinePositionRatio);
        this.underlinePosition = parseFloat(this.elementStyles.height) * this.ratio * 
                ( 1 - this.elementStyles.baselinePositionRatio + 
                    this.elementStyles.baselinePositionRatio * userUnderlinePosition)
                + this.strokeWidth/2;
    }

    var adjustValue = optimalStrokeWidthPos(this.strokeWidth, this.underlinePosition);
    this.strokeWidth = adjustValue.strokeWidth;
    this.underlinePosition = adjustValue.posY;

    // todo: if last character is a space, remove the space
    textWidth = this.ctx.measureText(this.text).width;

    this.myString = new GuitarString(this.ctx, 
        new Point(0, this.underlinePosition), 
        new Point(textWidth, this.underlinePosition), 
        this.strokeWidth, this.underlineStyles['text-underline-color'], this.ratio);
    this.drawHoles();

}

SingleUnderline.prototype.drawUnderline = function(){
    //  draw the underline
    this.myString.draw();
}

SingleUnderline.prototype.drawHoles = function(){
    // draw the font stroke             
    this.ctx.font = this.font;
    this.ctx.textBaseline = 'top';

    this.ctx.globalCompositeOperation = "destination-out";   

    this.ctx.lineWidth = 2*this.ratio + this.strokeWidth*3.6;
    this.ctx.strokeStyle = 'blue';
    this.ctx.beginPath();
    this.ctx.strokeText(this.text, -0.2, 0);  

    this.ctx.fillStyle = 'green';
    this.ctx.beginPath();
    this.ctx.fillText(this.text, -0.2, 0);
}

SingleUnderline.prototype.clear = function(){
    this.redrawActive = this.myString.redrawActive;
    // clear
    if(this.myString.redrawActive) {
        // this.myString.clear();
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    }
};


SingleUnderline.prototype.update = function(){
    // update
    if(this.myString.redrawActive) {
        this.myString.update();
    }
};


SingleUnderline.prototype.draw = function(){
    // draw
    if(this.redrawActive) {
        this.drawUnderline();
        this.drawHoles();
    }
};

var getElementStyles = function(element){
 // lineHeight, height, ratio, fontFamily, fontSize, fontStyle
    var $this = element;

    var baselinePositionRatio = baselineRatio(element);
    var lineHeight = parseFloat(window.getComputedStyle($this, null)
            .getPropertyValue("line-height"));
    var fontFamily = window.getComputedStyle($this, null)
            .getPropertyValue("font-family");
    var fontSize = window.getComputedStyle($this, null)
            .getPropertyValue("font-size");
    var fontStyle = window.getComputedStyle($this, null)
            .getPropertyValue("font-style");
    var width = $this.getBoundingClientRect().width;
    var height = $this.getBoundingClientRect().height;
    var parentWidth = $this.parentNode.getBoundingClientRect().width;


    var offsetLeft = $this.offsetLeft;
    var parentOffsetLeft = $this.parentNode.offsetLeft;
    var canvasLeft = parentOffsetLeft - offsetLeft;
    var textIndent = offsetLeft - parentOffsetLeft;

    // canvas.style.left= canvasLeft + 'px';   
    return {
        lineHeight: lineHeight,
        width: width,
        height: height,
        parentWidth: parentWidth,
        fontFamily: fontFamily,
        fontSize: fontSize,
        fontStyle: fontStyle,
        baselinePositionRatio: baselinePositionRatio,
        canvasLeft: canvasLeft,
        textIndent: textIndent
    }
};

window.requestAnimFrame = (function(callback) {
	return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame ||
	function(callback) {
	  window.setTimeout(callback, 1000 / 60);
	};
})();

var myUnderlines = [];
var myMultipleUnderlines = [];
window.onload = function() {
	var underlineElements = document.querySelectorAll(".underline");
    for(var n = 0; n < underlineElements.length; n++) {

    	var element = underlineElements[n];

    	var underlineStyles = {
    		'text-underline-color': '#000',
    		'text-underline-position': 'auto', // could be ratio or todo: px 
    		'text-underline-skip': true,
    		'text-underline-width': 'auto' // could be auto or px or ratio
    	}


    	var elementStyles = getElementStyles(element);
    	// single line or multiple line?
		if (elementStyles.height > elementStyles.lineHeight) {
			// multiple lines
			// console.log('multiple lines');
    		var myUnderline = new MultipleUnderline(element, underlineStyles, elementStyles);
    		// myUnderline.update();
    		// myUnderline.draw();
    		myUnderlines.push(myUnderline);
		} else {
			// single line
    		var myUnderline = new SingleUnderline(element, underlineStyles, elementStyles);
    		myUnderlines.push(myUnderline);
		}

		// if(window.device)
    }
}


function animate() {	

	for(var i = 0; i < myUnderlines.length; i++) {
	    var myUnderline = myUnderlines[i];

		// clear
	    myUnderline.clear();
	    
		// update
		myUnderline.update();
		
		// draw stuff
	    myUnderline.draw();
	}

	
	// request new frame
	requestAnimFrame(function() {
	  animate();
	});
}
animate();

//audio play multiple channels at the same time: http://www.storiesinflight.com/html5/audio.html
var channel_max = 10; // number of channels
audiochannels = new Array();
for (a = 0; a < channel_max; a++) { // prepare the channels
    audiochannels[a] = new Array();
    audiochannels[a]['channel'] = new Audio(); // create a new audio object
    audiochannels[a]['finished'] = -1; // expected end time for this channel
}

function play_multi_sound(s) {
    for (a = 0; a < audiochannels.length; a++) {
        thistime = new Date();
        if (audiochannels[a]['finished'] < thistime.getTime()) { // is this channel finished?
            audiochannels[a]['finished'] = thistime.getTime() + document.getElementById(s).duration * 1000;
            audiochannels[a]['channel'].src = document.getElementById(s).src;
            audiochannels[a]['channel'].load();
            audiochannels[a]['channel'].play();
            break;
        }
    }
}

var is_chrome = navigator.userAgent.toLowerCase().indexOf('chrome') > -1;


var circleCenter = function(startPoint, thirdPoint, endPoint){
    var dy1 = thirdPoint.y - startPoint.y;
    var dx1 = thirdPoint.x - startPoint.x;
    var dy2 = endPoint.y - thirdPoint.y;
    var dx2 = endPoint.x - thirdPoint.x;

    var aSlope = dy1/dx1;
    var bSlope = dy2/dx2;  


    var centerX = (aSlope*bSlope*(startPoint.y - endPoint.y) + bSlope*(startPoint.x + thirdPoint.x)
        - aSlope*(thirdPoint.x+endPoint.x) )/( 2* (bSlope-aSlope) );
    var centerY = -1*(centerX - (startPoint.x+thirdPoint.x)/2)/aSlope +  (startPoint.y+thirdPoint.y)/2;
    var r = dist(centerX, centerY, startPoint.x, startPoint.y)

    return {
        x: centerX,
        y: centerY,
        r: r
    };
}

var dist = function(x, y, x0, y0){
    return Math.sqrt((x -= x0) * x + (y -= y0) * y);
};

var Point = function (x,y){
    this.x=x;
    this.y=y;
}


var intersects = function(a, b, c, d, p, q, r, s) {
    // returns true if the line from (a,b)->(c,d) intersects with (p,q)->(r,s)
    var det, gamma, lambda;
    det = (c - a) * (s - q) - (r - p) * (d - b);
    if (det === 0) {
        return false;                    
    } else {
        lambda = ((s - q) * (r - a) + (p - r) * (s - b)) / det;
        gamma = ((b - d) * (r - a) + (c - a) * (s - b)) / det;
        return (0 < lambda && lambda < 1) && (0 < gamma && gamma < 1);
    }
};

var musicLevel = function(startPoint, endPoint, ratio){
    var length = dist(startPoint.x, startPoint.y, endPoint.x, endPoint.y)/ratio;

    level = Math.floor(length/30);
    if (level > 19 ) {
        level = 19;
    }
    level = 19 - level 
    if (level < 10) {
        level = '0' + level 
    }
    return level;
};
function GuitarString(ctx, startPoint, endPoint, strokeWidth, strokeColor, ratio) {
	//ctor
    this.ctx = ctx;
    this.canvas = ctx.canvas;
	this.startPoint = startPoint;
	this.endPoint = endPoint;
    this.strokeWidth = strokeWidth;
    this.strokeColor = strokeColor;
    this.ratio = ratio;

    this.level = musicLevel(this.startPoint, this.endPoint, this.ratio);

    // this.canvas.width = this.canvas.clientWidth;
    // this.canvas.height = this.canvas.clientHeight*1.2;
    
    this.maxGrabDistance = this.strokeWidth * 2;
    this.maxControlDistance = this.strokeWidth * 6;

    this.ctx.lineWidth = this.strokeWidth;
    this.ctx.strokeStyle = this.strokeColor;
        this.ctx.beginPath();
        this.ctx.moveTo(this.startPoint.x, this.startPoint.y);
        this.ctx.lineTo(this.endPoint.x, this.endPoint.y);
        this.ctx.globalCompositeOperation = "source-over";
        this.ctx.stroke();

    this.currentMouseX;
    this.currentMouseY;
    this.lastMouseX;
    this.lastMouseY;
    this.waveInitX = (this.startPoint.x + this.endPoint.x)/2;
    this.waveInitY = this.startPoint.y - this.maxControlDistance;
    this.waveCount = 0;
    this.damping = 0.9;

	this.thirdPoint = new Point((this.startPoint.x + this.endPoint.x)/2, this.startPoint.y);
    

    // state flags
	this.userInControl = false;
	this.userPlucked = false;
	this.waveInControl = false;
	this.waveFinished = false;
	this.initState = true;
    this.lastRedraw = false;
    this.redrawActive = false;

    //add event listener
	var self = this;
    this.canvas.addEventListener('mouseover',  function(event) { 
        self.mouseOver(self, event);
    }, false);

	this.canvas.addEventListener('mousemove',  function(event) { 
		self.mouseMove(self, event);
	}, false);

    this.canvas.addEventListener ("mouseleave", function(event){
        self.mouseLeave(self, event);
    }, false);

    this.canvas.addEventListener ("mouseout", function(event){
        self.mouseOut(self, event);
    }, false);

    this.canvas.addEventListener("touchstart", function(event) { 
        self.touchDown(self, event);
    }, false);
    this.canvas.addEventListener("touchmove", function(event) { 
        self.touchXY(self, event);
    }, false);
    this.canvas.addEventListener("touchend", function(event) { 
        self.touchUp(self, event);
    }, false);

}

GuitarString.prototype.mouseOver = function(self, event){
    // console.log('mouseOver');
    this.currentMouseX = event.layerX*this.ratio;
    this.currentMouseX = event.layerY*this.ratio;
};
GuitarString.prototype.mouseMove = function (self, event){
    // console.log('mouseMove');
    this.lastMouseX = this.currentMouseX;
    this.lastMouseY = this.currentMouseY;
    this.currentMouseX = event.layerX * this.ratio;
    this.currentMouseY = event.layerY * this.ratio; 

    var radius = circleCenter(  new Point(this.startPoint.x, this.startPoint.y), 
                                new Point(this.currentMouseX, this.currentMouseY), 
                                new Point(this.endPoint.x, this.endPoint.y) ).r;
    var currentWaveDistance = radius - Math.sqrt( Math.pow(radius, 2) - Math.pow((Math.abs(this.endPoint.x - this.startPoint.x))/2, 2) );
    var lastRadius = circleCenter(  new Point(this.startPoint.x, this.startPoint.y), 
                                	new Point(this.lastMouseX, this.lastMouseY), 
                                	new Point(this.endPoint.x, this.endPoint.y) ).r;
    var lastWaveDistance = lastRadius - Math.sqrt( Math.pow(lastRadius, 2) 
                                        - Math.pow((Math.abs(this.endPoint.x - this.startPoint.x))/2, 2) );
    


    var mouseInGrabRange = currentWaveDistance < this.maxGrabDistance 
        && this.currentMouseX > this.startPoint.x
        && this.currentMouseX < this.endPoint.x;

    var lastMouseOutGrabRange = !(lastWaveDistance < this.maxGrabDistance
        && this.lastMouseX > this.startPoint.x
        && this.lastMouseX < this.endPoint.x);

    var mouseOutControlRange = !(currentWaveDistance < this.maxControlDistance 
        && this.currentMouseX > this.startPoint.x
        && this.currentMouseX < this.endPoint.x);

    var lastMouseInControlRange = lastWaveDistance < this.maxControlDistance
        && this.lastMouseX > this.startPoint.x
        && this.lastMouseY < this.endPoint.x;
    
    var mouseCrossed = intersects(this.lastMouseX, this.lastMouseY, 
                            this.currentMouseX, this.currentMouseY,
                            this.startPoint.x, this.startPoint.y,
                            this.endPoint.x, this.endPoint.y);

    if( mouseInGrabRange && lastMouseOutGrabRange && (!this.userInControl) ){
        // console.log('grab!');
        this.initState = false;
        this.userInControl = true;
        this.waveInControl = false;
        this.waveFinished = false;

        this.redrawActive = true;
    } else if ( mouseOutControlRange && lastMouseInControlRange && this.userInControl){
        // console.log('boing!');
        this.initState = false;
        this.userInControl = false;
        this.waveInControl = true;
        this.waveFinished = false;
        this.waveCount = 0;
        // this.waveInitX = this.lastMouseX;
        // this.waveInitY = this.lastMouseY;
        this.waveInitX = (this.startPoint.x + this.endPoint.x)/2;
        this.waveInitY = this.endPoint.y + this.maxControlDistance;
        // play audio
        play_multi_sound('audio' + this.level);
        // createjs.Sound.play('cello_' + this.level);

    } 

    if( (!this.userInControl)&&mouseCrossed ) {
    	// console.log('i just plucked!');
        this.initState = false;
        this.userInControl = false;
        this.waveInControl = true;
        this.waveFinished = false;
        this.redrawActive = true;
        this.waveCount = 0;
        this.waveInitX = (this.startPoint.x + this.endPoint.y)/2;
        this.waveInitY = this.endPoint.y + this.maxGrabDistance * 2 / 3;
    }
};
GuitarString.prototype.mouseLeave = function(self, event){
    // console.log('mouseLeave');
    if( this.userInControl ) {
        this.initState = false;
        this.userInControl = false;
        this.waveInControl = true;
        this.waveFinished = false;
        this.redrawActive = true;
        this.waveCount = 0;

        this.waveInitX = event.layerX*this.ratio;
        this.waveInitY = event.layerY*this.ratio;

    }
};
GuitarString.prototype.mouseOut = function(self, event){
    // console.log('mouseOut');
};
GuitarString.prototype.touchDown = function(self, event){
    // console.log('touchDown');
    this.currentMouseX = event.layerX*this.ratio;
    this.currentMouseX = event.layerY*this.ratio;
};
GuitarString.prototype.touchXY = function (self, event){
    // console.log('touchMove');
    this.lastMouseX = this.currentMouseX;
    this.lastMouseY = this.currentMouseY;
    this.currentMouseX = event.layerX * this.ratio;
    this.currentMouseY = event.layerY * this.ratio; 

    var radius = circleCenter(  new Point(this.startPoint.x, this.startPoint.y), 
                                new Point(this.currentMouseX, this.currentMouseY), 
                                new Point(this.endPoint.x, this.endPoint.y) ).r;
    var currentWaveDistance = radius - Math.sqrt( Math.pow(radius, 2) - Math.pow((Math.abs(this.endPoint.x - this.startPoint.x))/2, 2) );
    var lastRadius = circleCenter(  new Point(this.startPoint.x, this.startPoint.y), 
                                    new Point(this.lastMouseX, this.lastMouseY), 
                                    new Point(this.endPoint.x, this.endPoint.y) ).r;
    var lastWaveDistance = lastRadius - Math.sqrt( Math.pow(lastRadius, 2) 
                                        - Math.pow((Math.abs(this.endPoint.x - this.startPoint.x))/2, 2) );
    


    var mouseInGrabRange = currentWaveDistance < this.maxGrabDistance 
        && this.currentMouseX > this.startPoint.x
        && this.currentMouseX < this.endPoint.x;

    var lastMouseOutGrabRange = !(lastWaveDistance < this.maxGrabDistance
        && this.lastMouseX > this.startPoint.x
        && this.lastMouseX < this.endPoint.x);

    var mouseOutControlRange = !(currentWaveDistance < this.maxControlDistance 
        && this.currentMouseX > this.startPoint.x
        && this.currentMouseX < this.endPoint.x);

    var lastMouseInControlRange = lastWaveDistance < this.maxControlDistance
        && this.lastMouseX > this.startPoint.x
        && this.lastMouseY < this.endPoint.x;
    
    var mouseCrossed = intersects(this.lastMouseX, this.lastMouseY, 
                            this.currentMouseX, this.currentMouseY,
                            this.startPoint.x, this.startPoint.y,
                            this.endPoint.x, this.endPoint.y);

    if( mouseInGrabRange && lastMouseOutGrabRange && (!this.userInControl) ){
        // console.log('grab!');
        this.initState = false;
        this.userInControl = true;
        this.waveInControl = false;
        this.waveFinished = false;

        this.redrawActive = true;
    } else if ( mouseOutControlRange && lastMouseInControlRange && this.userInControl){
        // console.log('boing!');
        this.initState = false;
        this.userInControl = false;
        this.waveInControl = true;
        this.waveFinished = false;
        this.waveCount = 0;
        // this.waveInitX = this.lastMouseX;
        // this.waveInitY = this.lastMouseY;
        this.waveInitX = (this.startPoint.x + this.endPoint.x)/2;
        this.waveInitY = this.endPoint.y + this.maxControlDistance;
        // play audio
        play_multi_sound('audio' + this.level);

    } 

    if( (!this.userInControl)&&mouseCrossed ) {
        // console.log('i just plucked!');
        this.initState = false;
        this.userInControl = false;
        this.waveInControl = true;
        this.waveFinished = false;
        this.redrawActive = true;
        this.waveCount = 0;
        this.waveInitX = (this.startPoint.x + this.endPoint.y)/2;
        this.waveInitY = this.endPoint.y + this.maxGrabDistance * 2 / 3;
        // play audio
        // play_multi_sound('audio' + this.level);

    }
};
GuitarString.prototype.touchUp = function(self, event){
    // console.log('touchUp');
    if( this.userInControl ) {
        this.initState = false;
        this.userInControl = false;
        this.waveInControl = true;
        this.waveFinished = false;
        this.redrawActive = true;
        this.waveCount = 0;

        this.waveInitX = event.layerX*this.ratio;
        this.waveInitY = event.layerY*this.ratio;

    }
};


GuitarString.prototype.clear = function(){
	// clear
    // if(this.redrawActive){
    	// this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    // }
};

GuitarString.prototype.update = function(){
	// console.log(this.redrawActive);
	// if(this.redrawActive){
		if ( this.userInControl ){
            this.thirdPoint = new Point(this.currentMouseX, this.currentMouseY);
        }
        if ( this.waveInControl ){
            var waveX = this.waveInitX;
            var waveY = this.startPoint.y + 
                    (this.waveInitY-this.startPoint.y)
                    *Math.cos(this.waveCount/5*Math.PI)
                    *Math.pow(this.damping, this.waveCount);
    
            if ( Math.pow(this.damping, this.waveCount) > 0.03) {
                // still waving ....
                this.thirdPoint = new Point(waveX, waveY);
                this.waveCount++;
            } else {
                // wave damped to a straight line, wave is finished
                this.waveInControl = false;
                this.waveFinished = true;
                this.lastRedraw = true;
                this.thirdPoint = new Point(waveX, waveY);
            }
        }
	// }
}

GuitarString.prototype.draw = function(){
    // if(this.redrawActive){   
        // draw stuff
    // }
    if(this.lastRedraw) {
        this.drawLine();
        this.lastRedraw = false;
        this.redrawActive = false;
    } else {
        this.drawArc(this.startPoint, this.thirdPoint, this.endPoint);
    }
};
GuitarString.prototype.drawLine = function(){
    // draw a line instead of a flat curve when it stops redraw, pixel-perfect  
    this.ctx.lineWidth = this.strokeWidth;
    this.ctx.strokeStyle = this.strokeColor;
        this.ctx.beginPath();
        this.ctx.moveTo(this.startPoint.x, this.startPoint.y);
        this.ctx.lineTo(this.endPoint.x, this.endPoint.y);
        this.ctx.globalCompositeOperation = "source-over";
        this.ctx.stroke();
};


GuitarString.prototype.drawArc = function(startPoint, thirdPoint, endPoint){
    var ctx = this.ctx;
    ctx.lineWidth = this.strokeWidth;
    ctx.strokeStyle = this.strokeColor;

    var centerObject = circleCenter( new Point(startPoint.x, startPoint.y), 
                                     new Point(thirdPoint.x, thirdPoint.y), 
                                     new Point(endPoint.x, endPoint.y) );
    var centerX = centerObject.x;
    var centerY = centerObject.y;
    var r = centerObject.r

    var angle = Math.atan2(centerX-startPoint.x, centerY-startPoint.y);
    // console.log(centerObject);
    if (!angle){
        ctx.beginPath();
        ctx.moveTo(startPoint.x, startPoint.y);
        ctx.lineTo(endPoint.x, endPoint.y);
    } else {
    	if( angle > Math.PI/2) {
	        ctx.beginPath();
	        ctx.arc(centerX, centerY, r, Math.PI * 1.5-angle, Math.PI * 1.5 + angle, true);
	    } else {
	        ctx.beginPath();
	        ctx.arc(centerX, centerY, r, Math.PI * 1.5-angle, Math.PI * 1.5 + angle, false);
	    }
    }
    ctx.globalCompositeOperation = "source-over";
    ctx.stroke();

}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.