PageSourceSearch

https://www.studiofeixen.ch/wp-content/themes/studiofeixen/js/wiggle.js

js studiofeixen.ch collected 2026-10-03 23:28:10 UTC 47,744 bytes, 1,340 lines download raw bytes

1/* Universal Module Definition */
2(function (root, factory) {
3  if (typeof define === 'function' && define.amd) {
4    // AMD. Register as a named AMD module.
5    define('baselineRatio', [], function () {
6      return (root.baselineRatio = factory());
7    });
8  } else if (typeof exports === 'object') {
9    // Node. Does not work with strict CommonJS, but
10    // only CommonJS-like enviroments that support module.exports,
11    // like Node.
12    module.exports = factory();
13  } else {
14    // Browser globals
15    root.baselineRatio = factory();
16  }
17}(this, function () {
18  var baselineRatio = function(elem) {
19    // Get the baseline in the context of whatever element is passed in.
20    elem = elem || document.body;
21
22    // The container is a little defenseive.
23    var container = document.createElement('div');
24    container.style.display = "block";
25    container.style.position = "absolute";
26    container.style.bottom = "0";
27    container.style.right = "0";
28    container.style.width = "0px";
29    container.style.height = "0px";
30    container.style.margin = "0";
31    container.style.padding = "0";
32    container.style.visibility = "hidden";
33    container.style.overflow = "hidden";
34
35    // Intentionally unprotected style definition.
36    var small = document.createElement('span');
37    var large = document.createElement('span');
38
39    // Large numbers help improve accuracy.
40    small.style.fontSize = "0px";
41    large.style.fontSize = "2000px";
42
43    small.innerHTML = "X";
44    large.innerHTML = "X";
45
46    container.appendChild(small);
47    container.appendChild(large);
48
49    // Put the element in the DOM for a split second.
50    elem.appendChild(container);
51    var smalldims = small.getBoundingClientRect();
52    var largedims = large.getBoundingClientRect();
53    elem.removeChild(container);
54
55    // Calculate where the baseline was, percentage-wise.
56    var baselineposition = smalldims.top - largedims.top;
57    var height = largedims.height;
58
59    return 1 - (baselineposition / height);
60  }
61
62  return baselineRatio;
63}));
64
65/*!
66 * classie v1.0.1
67 * class helper functions
68 * from bonzo https://github.com/ded/bonzo
69 * MIT license
70 * 
71 * classie.has( elem, 'my-class' ) -> true/false
72 * classie.add( elem, 'my-new-class' )
73 * classie.remove( elem, 'my-unwanted-class' )
74 * classie.toggle( elem, 'my-class' )
75 */
76
77/*jshint browser: true, strict: true, undef: true, unused: true */
78/*global define: false, module: false */
79
80( function( window ) {
81
82'use strict';
83
84// class helper functions from bonzo https://github.com/ded/bonzo
85
86function classReg( className ) {
87  return new RegExp("(^|\\s+)" + className + "(\\s+|$)");
88}
89
90// classList support for class management
91// altho to be fair, the api sucks because it won't accept multiple classes at once
92var hasClass, addClass, removeClass;
93
94if ( 'classList' in document.documentElement ) {
95  hasClass = function( elem, c ) {
96    return elem.classList.contains( c );
97  };
98  addClass = function( elem, c ) {
99    elem.classList.add( c );
100  };
101  removeClass = function( elem, c ) {
102    elem.classList.remove( c );
103  };
104}
105else {
106  hasClass = function( elem, c ) {
107    return classReg( c ).test( elem.className );
108  };
109  addClass = function( elem, c ) {
110    if ( !hasClass( elem, c ) ) {
111      elem.className = elem.className + ' ' + c;
112    }
113  };
114  removeClass = function( elem, c ) {
115    elem.className = elem.className.replace( classReg( c ), ' ' );
116  };
117}
118
119function toggleClass( elem, c ) {
120  var fn = hasClass( elem, c ) ? removeClass : addClass;
121  fn( elem, c );
122}
123
124var classie = {
125  // full names
126  hasClass: hasClass,
127  addClass: addClass,
128  removeClass: removeClass,
129  toggleClass: toggleClass,
130  // short names
131  has: hasClass,
132  add: addClass,
133  remove: removeClass,
134  toggle: toggleClass
135};
136
137// transport
138if ( typeof define === 'function' && define.amd ) {
139  // AMD
140  define( classie );
141} else if ( typeof exports === 'object' ) {
142  // CommonJS
143  module.exports = classie;
144} else {
145  // browser global
146  window.classie = classie;
147}
148
149})( window );
150
151function MultipleUnderline(element, underlineStyles, elementStyles) {
152    //ctor
153    this.element = element;
154
155    this.text = this.element.textContent;
156
157    this.underlineStyles = underlineStyles;
158
159    // this.elementStyles = getElementStyles(element);
160    this.elementStyles = elementStyles;
161
162    this.canvas = document.createElement("canvas");
163    this.ctx = this.canvas.getContext('2d');
164
165
166    this.ratio = window.devicePixelRatio;
167        this.canvas.width = this.elementStyles.width*this.ratio;
168        this.canvas.height = this.elementStyles.height*this.ratio;
169        // this.canvas.height = this.canvas.clientHeight + this.elementStyles.lineHeight;
170        this.canvas.style.left = this.elementStyles.canvasLeft + 'px';
171        this.element.appendChild(this.canvas);
172        this.canvas.style.width =  this.elementStyles.width + 'px';
173
174        this.ctx.font = this.font = this.elementStyles.fontStyle + ' ' 
175                        + multiplyValue(this.elementStyles.fontSize, this.ratio) + ' ' 
176                        + this.elementStyles.fontFamily;
177
178    this.multipleRedrawActive = false;
179    if (is_chrome) {
180        // chrome floor the lineheight when it is not a whole number
181        // this.elementStyles.lineHeight = Math.floor(this.elementStyles.lineHeight * this.ratio);
182        this.elementStyles.lineHeight = this.elementStyles.lineHeight * this.ratio;
183    } else {
184        this.elementStyles.lineHeight = this.elementStyles.lineHeight * this.ratio;
185    }
186
187    // determine the text-underline-width / strokeWidth
188    var dotWidth = this.ctx.measureText('.')['width'];
189    if (this.underlineStyles['text-underline-width'] == "auto") {
190        // if set to auto, calculate the optimized width based on font
191        this.strokeWidth = dotWidth/12;
192    } else {
193        //if set to px value, todo: other unit such as em?
194        this.strokeWidth = this.underlineStyles['text-underline-width'];
195        //get number value
196        this.strokeWidth = parseFloat(this.strokeWidth)*this.ratio;
197    }
198
199    // determine the text-underline-position / underlinePosition
200    // text-underline-position in ratio, todo: default and user set position ratio
201    if (this.underlineStyles['text-underline-position'] == "auto") {
202        // if set to auto, calculate the optimized width based on font
203        this.underlinePosition = parseFloat(this.elementStyles.fontSize) * this.ratio 
204                * ( 1 - this.elementStyles.baselinePositionRatio + 
205                    this.elementStyles.baselinePositionRatio * 0.4)
206                + this.strokeWidth/2;
207    } else {
208        //if set to ratio value, todo: other unit such as em, px?
209        var userUnderlinePosition = parseFloat(this.underlineStyles['text-underline-position']);
210        // console.log(userUnderlinePosition);
211        this.underlinePosition = parseFloat(this.elementStyles.fontSize) * this.ratio * 
212                ( 1 - this.elementStyles.baselinePositionRatio + 
213                    this.elementStyles.baselinePositionRatio * userUnderlinePosition)
214                + this.strokeWidth/2;
215    }
216
217
218    var adjustValue = optimalStrokeWidthPos(this.strokeWidth, this.underlinePosition);
219    this.strokeWidth = adjustValue.strokeWidth;
220    this.underlinePosition = adjustValue.posY;
221
222    this.lines = [];
223    this.myStrings = [];
224
225    var words = this.text.match(/[^\s-]+-?\s?/g);
226    var line = '';
227
228    var linePositionY = 0;
229    var firstLineCount = 0;
230    for (var n = 0; n < words.length; n++) {
231        // add the whitespace after getting the width measurement
232        if (words[n].match(/\s+$/)) {
233            // the last character of words[n] is whitespace
234            var newWord = words[n].replace(/\s+$/, '');
235            var testLine = line + newWord;
236            var testLineMetrics = this.ctx.measureText(testLine);
237            var testLineWidth = testLineMetrics.width;
238            testLine = testLine + ' ';
239        } else {
240            var testLine = line + words[n];
241            var testLineMetrics = this.ctx.measureText(testLine);
242            var testLineWidth = testLineMetrics.width;
243        }
244
245        if (!firstLineCount) {
246            //the first line, should consider startingPointX
247            if (testLineWidth + this.elementStyles.textIndent * this.ratio > this.elementStyles.parentWidth * this.ratio && n > 0) {
248                //  draw the underline
249                if (line.match(/\s+$/)) {
250                    // the last character of line is whitespace               
251                    var lineMetrics = this.ctx.measureText(line.replace(/\s+$/, ''));
252                    var lineWidth = lineMetrics.width;
253                } else {
254                    var lineMetrics = this.ctx.measureText(line);
255                    var lineWidth = lineMetrics.width;
256                }
257
258                var tempLine = {
259                    lineText: line,
260                    lineTextIndent: this.elementStyles.textIndent * this.ratio - 0.2,
261                    linePositionY: linePositionY,
262                    lineMeasureWidth: lineWidth
263                }
264                this.lines.push(tempLine)
265
266                line = words[n];
267                linePositionY += this.elementStyles.lineHeight;
268                firstLineCount++;
269            } else {
270                line = testLine;
271            }
272        } else {
273            if (testLineWidth > this.elementStyles.parentWidth * this.ratio && n > 0) {
274                //  draw the underline
275                if (line.match(/\s+$/)) {
276                    // the last character of line is whitespace               
277                    var lineMetrics = this.ctx.measureText(line.replace(/\s+$/, ''));
278                    var lineWidth = lineMetrics.width;
279                } else {
280                    var lineMetrics = this.ctx.measureText(line);
281                    var lineWidth = lineMetrics.width;
282                }
283
284                var tempLine = {
285                    lineText: line,
286                    lineTextIndent: -0.2,
287                    linePositionY: linePositionY,
288                    lineMeasureWidth: lineWidth
289                }
290                this.lines.push(tempLine);
291
292                line = words[n];
293                linePositionY += this.elementStyles.lineHeight;
294            } else {
295                line = testLine;
296            }
297        }
298    }
299    // draw the last line
300    //  draw the underline
301    if (line.match(/\s+$/)) {
302        // the last character of line is whitespace               
303        var lineMetrics = this.ctx.measureText(line.replace(/\s+$/, ''));
304        var lineWidth = lineMetrics.width;
305    } else {
306        var lineMetrics = this.ctx.measureText(line);
307        var lineWidth = lineMetrics.width;
308    }
309
310    var tempLine = {
311        lineText: line,
312        lineTextIndent: -0.2,
313        linePositionY: linePositionY,
314        lineMeasureWidth: lineWidth
315    }
316    this.lines.push(tempLine);
317    for(var i = 0; i < this.lines.length; i++) {
318        var tempLine = this.lines[i];
319        var myString = new GuitarString(
320                this.ctx, 
321                new Point(tempLine.lineTextIndent, tempLine.linePositionY + this.underlinePosition), 
322                new Point(tempLine.lineTextIndent + tempLine.lineMeasureWidth, tempLine.linePositionY + this.underlinePosition), 
323                this.strokeWidth, this.underlineStyles['text-underline-color'], this.ratio);
324        this.myStrings.push(myString);
325    }
326
327    this.drawUnderline();
328    this.drawHoles();
329
330}
331
332MultipleUnderline.prototype.drawUnderline = function(){
333    // draw the underline
334    for(var i = 0; i < this.myStrings.length; i++) {
335        var tempString = this.myStrings[i];
336        // tempString.clear();
337            tempString.update();
338            tempString.draw();
339    }
340
341};
342
343
344MultipleUnderline.prototype.drawHoles = function(){
345    // draw the font stroke
346    for(var i = 0; i < this.lines.length; i++) {
347        var tempLine = this.lines[i];
348
349        this.ctx.globalCompositeOperation = "destination-out";
350        this.ctx.font = this.font;
351
352        this.ctx.fillStyle = 'green';
353        this.ctx.textBaseline = 'top';
354        this.ctx.fillText(tempLine.lineText, tempLine.lineTextIndent, tempLine.linePositionY);
355
356        this.ctx.lineWidth = 2*this.ratio + this.strokeWidth*3.6;
357        this.ctx.strokeStyle = 'blue';
358        this.ctx.strokeText(tempLine.lineText, tempLine.lineTextIndent, tempLine.linePositionY);
359
360    }
361}
362
363MultipleUnderline.prototype.clear = function(){
364    // clear
365    var lastMultipleRedrawActive = this.multipleRedrawActive;
366    this.multipleRedrawActive = false;
367    for(var i = 0; i < this.myStrings.length; i++) {
368        var tempString = this.myStrings[i];
369        // this.myString.clear();
370        // console.log(tempString.redrawActive);
371        if(tempString.redrawActive) {
372            this.multipleRedrawActive = true;
373        }
374    }
375    // console.log(this.multipleRedrawActive);
376    if (this.multipleRedrawActive) {
377        console.log('clear now!')
378        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
379    }
380    // if (!lastMultipleRedrawActive && this.multipleRedrawActive) {
381    //     for(var i = 0; i < this.myStrings.length; i++) {
382    //         var tempString = this.myStrings[i];
383    //         tempString.drawLine();
384    //     }
385    // }
386
387};
388
389MultipleUnderline.prototype.update = function(){
390    //update
391};
392
393
394MultipleUnderline.prototype.draw = function(){
395    // draw
396    if (this.multipleRedrawActive) {
397        this.drawUnderline();
398        this.drawHoles();
399    }
400};
401
402function MultipleUnderline(element, underlineStyles, elementStyles) {
403    //ctor
404    this.element = element;
405
406    this.text = this.element.textContent;
407
408    this.underlineStyles = underlineStyles;
409
410    // this.elementStyles = getElementStyles(element);
411    this.elementStyles = elementStyles;
412
413    this.canvas = document.createElement("canvas");
414        this.canvas.width = this.elementStyles.width;
415        this.canvas.height = this.elementStyles.height;
416        this.canvas.style.left = this.elementStyles.canvasLeft + 'px';
417        this.element.appendChild(this.canvas);
418        this.canvas.width = this.canvas.clientWidth;
419        this.canvas.height = this.canvas.clientHeight + this.elementStyles.lineHeight;
420
421    this.ctx = this.canvas.getContext('2d');
422    this.ctx.font = this.font = this.elementStyles.fontStyle + ' ' + this.elementStyles.fontSize + ' ' + this.elementStyles.fontFamily;
423
424    this.multipleRedrawActive = false;
425    if (is_chrome) {
426        // chrome floor the lineheight when it is not a whole number
427        this.elementStyles.lineHeight = Math.floor(this.elementStyles.lineHeight);
428    }
429
430
431    // determine the text-underline-width / strokeWidth
432    this.dotWidth = this.ctx.measureText('.')['width'];
433    if (this.underlineStyles['text-underline-width'] == "auto") {
434        // if set to auto, calculate the optimized width based on font
435        if (this.dotWidth / 6 <= 2) {
436            this.strokeWidth = Math.round(this.dotWidth / 3) / 2;
437        } else {
438            this.strokeWidth = Math.round(this.dotWidth / 6);
439        }
440    } else {
441        //if set to px value
442        this.strokeWidth = this.underlineStyles['text-underline-width'];
443        //get number value
444        this.strokeWidth = parseFloat(this.strokeWidth);
445    }
446
447    // determine the text-underline-position / underlinePosition
448    // text-underline-position in ratio
449    this.underlinePosition = parseFloat(this.elementStyles.fontSize) * 0.89;
450    if (this.strokeWidth <= 1 || (this.strokeWidth % 2 && this.strokeWidth > 2)) {
451        this.underlinePosition = Math.round(this.underlinePosition - 0.5) + 0.5;
452    } else {
453        this.underlinePosition = Math.round(this.underlinePosition);
454    }
455
456    this.lines = [];
457    this.myStrings = [];
458
459    var words = this.text.match(/[^\s-]+-?\s?/g);
460    var line = '';
461
462    var linePositionY = 0;
463    var firstLineCount = 0;
464    for (var n = 0; n < words.length; n++) {
465        // add the whitespace after getting the width measurement
466        if (words[n].match(/\s+$/)) {
467            // the last character of words[n] is whitespace
468            var newWord = words[n].replace(/\s+$/, '');
469            var testLine = line + newWord;
470            var testLineMetrics = this.ctx.measureText(testLine);
471            var testLineWidth = testLineMetrics.width;
472            testLine = testLine + ' ';
473        } else {
474            var testLine = line + words[n];
475            var testLineMetrics = this.ctx.measureText(testLine);
476            var testLineWidth = testLineMetrics.width;
477        }
478
479        if (!firstLineCount) {
480            //the first line, should consider startingPointX
481            if (testLineWidth + this.elementStyles.textIndent > this.elementStyles.parentWidth && n > 0) {
482                //  draw the underline
483                if (line.match(/\s+$/)) {
484                    // the last character of line is whitespace               
485                    var lineMetrics = this.ctx.measureText(line.replace(/\s+$/, ''));
486                    var lineWidth = lineMetrics.width;
487                } else {
488                    var lineMetrics = this.ctx.measureText(line);
489                    var lineWidth = lineMetrics.width;
490                }
491
492                var tempLine = {
493                    lineText: line,
494                    lineTextIndent: this.elementStyles.textIndent,
495                    linePositionY: linePositionY,
496                    lineMeasureWidth: lineWidth
497                }
498                this.lines.push(tempLine)
499
500                line = words[n];
501                linePositionY += this.elementStyles.lineHeight;
502                firstLineCount++;
503            } else {
504                line = testLine;
505            }
506        } else {
507            if (testLineWidth > this.elementStyles.parentWidth && n > 0) {
508                //  draw the underline
509                if (line.match(/\s+$/)) {
510                    // the last character of line is whitespace               
511                    var lineMetrics = this.ctx.measureText(line.replace(/\s+$/, ''));
512                    var lineWidth = lineMetrics.width;
513                } else {
514                    var lineMetrics = this.ctx.measureText(line);
515                    var lineWidth = lineMetrics.width;
516                }
517
518                var tempLine = {
519                    lineText: line,
520                    lineTextIndent: 0,
521                    linePositionY: linePositionY,
522                    lineMeasureWidth: lineWidth
523                }
524                this.lines.push(tempLine);
525
526                line = words[n];
527                linePositionY += this.elementStyles.lineHeight;
528            } else {
529                line = testLine;
530            }
531        }
532    }
533    // draw the last line
534    //  draw the underline
535    if (line.match(/\s+$/)) {
536        // the last character of line is whitespace               
537        var lineMetrics = this.ctx.measureText(line.replace(/\s+$/, ''));
538        var lineWidth = lineMetrics.width;
539    } else {
540        var lineMetrics = this.ctx.measureText(line);
541        var lineWidth = lineMetrics.width;
542    }
543
544    var tempLine = {
545        lineText: line,
546        lineTextIndent: 0,
547        linePositionY: linePositionY,
548        lineMeasureWidth: lineWidth
549    }
550    this.lines.push(tempLine);
551
552
553
554    for(var i = 0; i < this.lines.length; i++) {
555        var tempLine = this.lines[i];
556        var myString = new GuitarString(
557                this.ctx, 
558                new Point(tempLine.lineTextIndent, tempLine.linePositionY + this.underlinePosition), 
559                new Point(tempLine.lineTextIndent + tempLine.lineMeasureWidth, tempLine.linePositionY + this.underlinePosition), 
560                this.strokeWidth, this.underlineStyles['text-underline-color'], 1);
561        this.myStrings.push(myString);
562    }
563
564    this.drawUnderline();
565    this.drawHoles();
566
567}
568
569
570MultipleUnderline.prototype.clear = function(){
571    // clear
572    var lastMultipleRedrawActive = this.multipleRedrawActive;
573    this.multipleRedrawActive = false;
574    for(var i = 0; i < this.myStrings.length; i++) {
575        var tempString = this.myStrings[i];
576        // this.myString.clear();
577        // console.log(tempString.redrawActive);
578        if(tempString.redrawActive) {
579            this.multipleRedrawActive = true;
580        }
581    }
582    // console.log(this.multipleRedrawActive);
583    if (this.multipleRedrawActive) {
584        console.log('clear now!')
585        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
586    }
587    // if (!lastMultipleRedrawActive && this.multipleRedrawActive) {
588    //     for(var i = 0; i < this.myStrings.length; i++) {
589    //         var tempString = this.myStrings[i];
590    //         tempString.drawLine();
591    //     }
592    // }
593
594};
595
596MultipleUnderline.prototype.update = function(){
597    //update
598};
599
600
601MultipleUnderline.prototype.draw = function(){
602    // draw
603    if (this.multipleRedrawActive) {
604        this.drawUnderline();
605        this.drawHoles();
606    }
607};
608
609
610MultipleUnderline.prototype.drawUnderline = function(){
611    // draw the underline
612    for(var i = 0; i < this.myStrings.length; i++) {
613        var tempString = this.myStrings[i];
614        // tempString.clear();
615            tempString.update();
616            tempString.draw();
617    }
618
619};
620
621
622MultipleUnderline.prototype.drawHoles = function(){
623    // draw the font stroke
624    for(var i = 0; i < this.lines.length; i++) {
625        var tempLine = this.lines[i];
626
627        this.ctx.globalCompositeOperation = "destination-out";
628        this.ctx.font = this.font;
629        this.ctx.fillStyle = 'green';
630        this.ctx.textBaseline = 'top';
631        this.ctx.fillText(tempLine.lineText, tempLine.lineTextIndent, tempLine.linePositionY);
632        this.ctx.lineWidth = 3 + this.strokeWidth;
633        this.ctx.strokeStyle = 'blue';
634        this.ctx.strokeText(tempLine.lineText, tempLine.lineTextIndent, tempLine.linePositionY);
635
636    }
637}
638
639var multiplyValue = function(value, multiplier){
640    var str = value;
641    var m = multiplier;
642    var result = str.match(/(\d*\.?\d*)(.*)/);
643    //http://stackoverflow.com/questions/2868947/split1px-into-1px-1-px-in-javascript
644    return result[1] * m + result[2];
645}
646
647var optimalStrokeWidthPos = function(strokeWidth, posY){
648    if ( strokeWidth < 1) {
649        posY = Math.round(posY - 0.5) + 0.5;
650    } else if ( strokeWidth >= 1 ) {
651        strokeWidth = Math.round( strokeWidth );
652        if ( strokeWidth % 2 ){
653            // odd, posY -> 0.5
654            posY = Math.round(posY - 0.5) + 0.5;
655        } else {
656            // even, posY -> 1
657            posY = Math.round(posY);
658        }
659    }
660    return {
661        strokeWidth: strokeWidth,
662        posY: posY
663    }
664}
665
666function SingleUnderline(element, underlineStyles, elementStyles) {
667    //ctor
668    this.element = element;
669
670    this.text = this.element.textContent;
671
672    this.underlineStyles = underlineStyles;
673
674    this.elementStyles = elementStyles;
675    this.redrawActive = false;
676
677    this.canvas = document.createElement("canvas");
678    this.ctx = this.canvas.getContext('2d');
679    
680    this.ratio = window.devicePixelRatio;
681        this.canvas.width = this.elementStyles.width*this.ratio;
682        this.canvas.height = this.elementStyles.height*this.ratio;
683        this.element.appendChild(this.canvas);
684        this.canvas.style.width =  this.elementStyles.width + 'px';
685
686        this.ctx.font = this.font = this.elementStyles.fontStyle + ' ' 
687                        + multiplyValue(this.elementStyles.fontSize, this.ratio) + ' ' 
688                        + this.elementStyles.fontFamily;
689
690    // determine the text-underline-width / strokeWidth
691    var dotWidth = this.ctx.measureText('.')['width'];
692    if (this.underlineStyles['text-underline-width'] == "auto") {
693        // if set to auto, calculate the optimized width based on font
694        this.strokeWidth = dotWidth/12;
695    } else {
696        //if set to px value, todo: other unit such as em?
697        this.strokeWidth = this.underlineStyles['text-underline-width'];
698        //get number value
699        this.strokeWidth = parseFloat(this.strokeWidth)*this.ratio;
700    }
701
702
703    // determine the text-underline-position / underlinePosition
704    // text-underline-position in ratio, todo: default and user set position ratio
705    if (this.underlineStyles['text-underline-position'] == "auto") {
706        // if set to auto, calculate the optimized width based on font
707        // console.log(this.elementStyles.baselinePositionRatio);
708        this.underlinePosition = parseFloat(this.elementStyles.height) * this.ratio 
709                * ( 1 - this.elementStyles.baselinePositionRatio + 
710                    this.elementStyles.baselinePositionRatio * 0.4)
711                + this.strokeWidth/2;
712    } else {
713        //if set to ratio value, todo: other unit such as em, px?
714        var userUnderlinePosition = parseFloat(this.underlineStyles['text-underline-position']);
715        // console.log(this.elementStyles.baselinePositionRatio);
716        this.underlinePosition = parseFloat(this.elementStyles.height) * this.ratio * 
717                ( 1 - this.elementStyles.baselinePositionRatio + 
718                    this.elementStyles.baselinePositionRatio * userUnderlinePosition)
719                + this.strokeWidth/2;
720    }
721
722    var adjustValue = optimalStrokeWidthPos(this.strokeWidth, this.underlinePosition);
723    this.strokeWidth = adjustValue.strokeWidth;
724    this.underlinePosition = adjustValue.posY;
725
726    // todo: if last character is a space, remove the space
727    textWidth = this.ctx.measureText(this.text).width;
728
729    this.myString = new GuitarString(this.ctx, 
730        new Point(0, this.underlinePosition), 
731        new Point(textWidth, this.underlinePosition), 
732        this.strokeWidth, this.underlineStyles['text-underline-color'], this.ratio);
733    this.drawHoles();
734
735}
736
737SingleUnderline.prototype.drawUnderline = function(){
738    //  draw the underline
739    this.myString.draw();
740}
741
742SingleUnderline.prototype.drawHoles = function(){
743    // draw the font stroke             
744    this.ctx.font = this.font;
745    this.ctx.textBaseline = 'top';
746
747    this.ctx.globalCompositeOperation = "destination-out";   
748
749    this.ctx.lineWidth = 2*this.ratio + this.strokeWidth*3.6;
750    this.ctx.strokeStyle = 'blue';
751    this.ctx.beginPath();
752    this.ctx.strokeText(this.text, -0.2, 0);  
753
754    this.ctx.fillStyle = 'green';
755    this.ctx.beginPath();
756    this.ctx.fillText(this.text, -0.2, 0);
757}
758
759SingleUnderline.prototype.clear = function(){
760    this.redrawActive = this.myString.redrawActive;
761    // clear
762    if(this.myString.redrawActive) {
763        // this.myString.clear();
764        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
765    }
766};
767
768
769SingleUnderline.prototype.update = function(){
770    // update
771    if(this.myString.redrawActive) {
772        this.myString.update();
773    }
774};
775
776
777SingleUnderline.prototype.draw = function(){
778    // draw
779    if(this.redrawActive) {
780        this.drawUnderline();
781        this.drawHoles();
782    }
783};
784
785var getElementStyles = function(element){
786 // lineHeight, height, ratio, fontFamily, fontSize, fontStyle
787    var $this = element;
788
789    var baselinePositionRatio = baselineRatio(element);
790    var lineHeight = parseFloat(window.getComputedStyle($this, null)
791            .getPropertyValue("line-height"));
792    var fontFamily = window.getComputedStyle($this, null)
793            .getPropertyValue("font-family");
794    var fontSize = window.getComputedStyle($this, null)
795            .getPropertyValue("font-size");
796    var fontStyle = window.getComputedStyle($this, null)
797            .getPropertyValue("font-style");
798    var width = $this.getBoundingClientRect().width;
799    var height = $this.getBoundingClientRect().height;
800    var parentWidth = $this.parentNode.getBoundingClientRect().width;
801
802
803    var offsetLeft = $this.offsetLeft;
804    var parentOffsetLeft = $this.parentNode.offsetLeft;
805    var canvasLeft = parentOffsetLeft - offsetLeft;
806    var textIndent = offsetLeft - parentOffsetLeft;
807
808    // canvas.style.left= canvasLeft + 'px';   
809    return {
810        lineHeight: lineHeight,
811        width: width,
812        height: height,
813        parentWidth: parentWidth,
814        fontFamily: fontFamily,
815        fontSize: fontSize,
816        fontStyle: fontStyle,
817        baselinePositionRatio: baselinePositionRatio,
818        canvasLeft: canvasLeft,
819        textIndent: textIndent
820    }
821};
822
823window.requestAnimFrame = (function(callback) {
824	return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame ||
825	function(callback) {
826	  window.setTimeout(callback, 1000 / 60);
827	};
828})();
829
830var myUnderlines = [];
831var myMultipleUnderlines = [];
832window.onload = function() {
833	var underlineElements = document.querySelectorAll(".underline");
834    for(var n = 0; n < underlineElements.length; n++) {
835
836    	var element = underlineElements[n];
837
838    	var underlineStyles = {
839    		'text-underline-color': '#000',
840    		'text-underline-position': 'auto', // could be ratio or todo: px 
841    		'text-underline-skip': true,
842    		'text-underline-width': 'auto' // could be auto or px or ratio
843    	}
844
845
846    	var elementStyles = getElementStyles(element);
847    	// single line or multiple line?
848		if (elementStyles.height > elementStyles.lineHeight) {
849			// multiple lines
850			// console.log('multiple lines');
851    		var myUnderline = new MultipleUnderline(element, underlineStyles, elementStyles);
852    		// myUnderline.update();
853    		// myUnderline.draw();
854    		myUnderlines.push(myUnderline);
855		} else {
856			// single line
857    		var myUnderline = new SingleUnderline(element, underlineStyles, elementStyles);
858    		myUnderlines.push(myUnderline);
859		}
860
861		// if(window.device)
862    }
863}
864
865
866function animate() {	
867
868	for(var i = 0; i < myUnderlines.length; i++) {
869	    var myUnderline = myUnderlines[i];
870
871		// clear
872	    myUnderline.clear();
873	    
874		// update
875		myUnderline.update();
876		
877		// draw stuff
878	    myUnderline.draw();
879	}
880
881	
882	// request new frame
883	requestAnimFrame(function() {
884	  animate();
885	});
886}
887animate();
888
889//audio play multiple channels at the same time: http://www.storiesinflight.com/html5/audio.html
890var channel_max = 10; // number of channels
891audiochannels = new Array();
892for (a = 0; a < channel_max; a++) { // prepare the channels
893    audiochannels[a] = new Array();
894    audiochannels[a]['channel'] = new Audio(); // create a new audio object
895    audiochannels[a]['finished'] = -1; // expected end time for this channel
896}
897
898function play_multi_sound(s) {
899    for (a = 0; a < audiochannels.length; a++) {
900        thistime = new Date();
901        if (audiochannels[a]['finished'] < thistime.getTime()) { // is this channel finished?
902            audiochannels[a]['finished'] = thistime.getTime() + document.getElementById(s).duration * 1000;
903            audiochannels[a]['channel'].src = document.getElementById(s).src;
904            audiochannels[a]['channel'].load();
905            audiochannels[a]['channel'].play();
906            break;
907        }
908    }
909}
910
911var is_chrome = navigator.userAgent.toLowerCase().indexOf('chrome') > -1;
912
913
914var circleCenter = function(startPoint, thirdPoint, endPoint){
915    var dy1 = thirdPoint.y - startPoint.y;
916    var dx1 = thirdPoint.x - startPoint.x;
917    var dy2 = endPoint.y - thirdPoint.y;
918    var dx2 = endPoint.x - thirdPoint.x;
919
920    var aSlope = dy1/dx1;
921    var bSlope = dy2/dx2;  
922
923
924    var centerX = (aSlope*bSlope*(startPoint.y - endPoint.y) + bSlope*(startPoint.x + thirdPoint.x)
925        - aSlope*(thirdPoint.x+endPoint.x) )/( 2* (bSlope-aSlope) );
926    var centerY = -1*(centerX - (startPoint.x+thirdPoint.x)/2)/aSlope +  (startPoint.y+thirdPoint.y)/2;
927    var r = dist(centerX, centerY, startPoint.x, startPoint.y)
928
929    return {
930        x: centerX,
931        y: centerY,
932        r: r
933    };
934}
935
936var dist = function(x, y, x0, y0){
937    return Math.sqrt((x -= x0) * x + (y -= y0) * y);
938};
939
940var Point = function (x,y){
941    this.x=x;
942    this.y=y;
943}
944
945
946var intersects = function(a, b, c, d, p, q, r, s) {
947    // returns true if the line from (a,b)->(c,d) intersects with (p,q)->(r,s)
948    var det, gamma, lambda;
949    det = (c - a) * (s - q) - (r - p) * (d - b);
950    if (det === 0) {
951        return false;                    
952    } else {
953        lambda = ((s - q) * (r - a) + (p - r) * (s - b)) / det;
954        gamma = ((b - d) * (r - a) + (c - a) * (s - b)) / det;
955        return (0 < lambda && lambda < 1) && (0 < gamma && gamma < 1);
956    }
957};
958
959var musicLevel = function(startPoint, endPoint, ratio){
960    var length = dist(startPoint.x, startPoint.y, endPoint.x, endPoint.y)/ratio;
961
962    level = Math.floor(length/30);
963    if (level > 19 ) {
964        level = 19;
965    }
966    level = 19 - level 
967    if (level < 10) {
968        level = '0' + level 
969    }
970    return level;
971};
972function GuitarString(ctx, startPoint, endPoint, strokeWidth, strokeColor, ratio) {
973	//ctor
974    this.ctx = ctx;
975    this.canvas = ctx.canvas;
976	this.startPoint = startPoint;
977	this.endPoint = endPoint;
978    this.strokeWidth = strokeWidth;
979    this.strokeColor = strokeColor;
980    this.ratio = ratio;
981
982    this.level = musicLevel(this.startPoint, this.endPoint, this.ratio);
983
984    // this.canvas.width = this.canvas.clientWidth;
985    // this.canvas.height = this.canvas.clientHeight*1.2;
986    
987    this.maxGrabDistance = this.strokeWidth * 2;
988    this.maxControlDistance = this.strokeWidth * 6;
989
990    this.ctx.lineWidth = this.strokeWidth;
991    this.ctx.strokeStyle = this.strokeColor;
992        this.ctx.beginPath();
993        this.ctx.moveTo(this.startPoint.x, this.startPoint.y);
994        this.ctx.lineTo(this.endPoint.x, this.endPoint.y);
995        this.ctx.globalCompositeOperation = "source-over";
996        this.ctx.stroke();
997
998    this.currentMouseX;
999    this.currentMouseY;
1000    this.lastMouseX;
1001    this.lastMouseY;
1002    this.waveInitX = (this.startPoint.x + this.endPoint.x)/2;
1003    this.waveInitY = this.startPoint.y - this.maxControlDistance;
1004    this.waveCount = 0;
1005    this.damping = 0.9;
1006
1007	this.thirdPoint = new Point((this.startPoint.x + this.endPoint.x)/2, this.startPoint.y);
1008    
1009
1010    // state flags
1011	this.userInControl = false;
1012	this.userPlucked = false;
1013	this.waveInControl = false;
1014	this.waveFinished = false;
1015	this.initState = true;
1016    this.lastRedraw = false;
1017    this.redrawActive = false;
1018
1019    //add event listener
1020	var self = this;
1021    this.canvas.addEventListener('mouseover',  function(event) { 
1022        self.mouseOver(self, event);
1023    }, false);
1024
1025	this.canvas.addEventListener('mousemove',  function(event) { 
1026		self.mouseMove(self, event);
1027	}, false);
1028
1029    this.canvas.addEventListener ("mouseleave", function(event){
1030        self.mouseLeave(self, event);
1031    }, false);
1032
1033    this.canvas.addEventListener ("mouseout", function(event){
1034        self.mouseOut(self, event);
1035    }, false);
1036
1037    this.canvas.addEventListener("touchstart", function(event) { 
1038        self.touchDown(self, event);
1039    }, false);
1040    this.canvas.addEventListener("touchmove", function(event) { 
1041        self.touchXY(self, event);
1042    }, false);
1043    this.canvas.addEventListener("touchend", function(event) { 
1044        self.touchUp(self, event);
1045    }, false);
1046
1047}
1048
1049GuitarString.prototype.mouseOver = function(self, event){
1050    // console.log('mouseOver');
1051    this.currentMouseX = event.layerX*this.ratio;
1052    this.currentMouseX = event.layerY*this.ratio;
1053};
1054GuitarString.prototype.mouseMove = function (self, event){
1055    // console.log('mouseMove');
1056    this.lastMouseX = this.currentMouseX;
1057    this.lastMouseY = this.currentMouseY;
1058    this.currentMouseX = event.layerX * this.ratio;
1059    this.currentMouseY = event.layerY * this.ratio; 
1060
1061    var radius = circleCenter(  new Point(this.startPoint.x, this.startPoint.y), 
1062                                new Point(this.currentMouseX, this.currentMouseY), 
1063                                new Point(this.endPoint.x, this.endPoint.y) ).r;
1064    var currentWaveDistance = radius - Math.sqrt( Math.pow(radius, 2) - Math.pow((Math.abs(this.endPoint.x - this.startPoint.x))/2, 2) );
1065    var lastRadius = circleCenter(  new Point(this.startPoint.x, this.startPoint.y), 
1066                                	new Point(this.lastMouseX, this.lastMouseY), 
1067                                	new Point(this.endPoint.x, this.endPoint.y) ).r;
1068    var lastWaveDistance = lastRadius - Math.sqrt( Math.pow(lastRadius, 2) 
1069                                        - Math.pow((Math.abs(this.endPoint.x - this.startPoint.x))/2, 2) );
1070    
1071
1072
1073    var mouseInGrabRange = currentWaveDistance < this.maxGrabDistance 
1074        && this.currentMouseX > this.startPoint.x
1075        && this.currentMouseX < this.endPoint.x;
1076
1077    var lastMouseOutGrabRange = !(lastWaveDistance < this.maxGrabDistance
1078        && this.lastMouseX > this.startPoint.x
1079        && this.lastMouseX < this.endPoint.x);
1080
1081    var mouseOutControlRange = !(currentWaveDistance < this.maxControlDistance 
1082        && this.currentMouseX > this.startPoint.x
1083        && this.currentMouseX < this.endPoint.x);
1084
1085    var lastMouseInControlRange = lastWaveDistance < this.maxControlDistance
1086        && this.lastMouseX > this.startPoint.x
1087        && this.lastMouseY < this.endPoint.x;
1088    
1089    var mouseCrossed = intersects(this.lastMouseX, this.lastMouseY, 
1090                            this.currentMouseX, this.currentMouseY,
1091                            this.startPoint.x, this.startPoint.y,
1092                            this.endPoint.x, this.endPoint.y);
1093
1094    if( mouseInGrabRange && lastMouseOutGrabRange && (!this.userInControl) ){
1095        // console.log('grab!');
1096        this.initState = false;
1097        this.userInControl = true;
1098        this.waveInControl = false;
1099        this.waveFinished = false;
1100
1101        this.redrawActive = true;
1102    } else if ( mouseOutControlRange && lastMouseInControlRange && this.userInControl){
1103        // console.log('boing!');
1104        this.initState = false;
1105        this.userInControl = false;
1106        this.waveInControl = true;
1107        this.waveFinished = false;
1108        this.waveCount = 0;
1109        // this.waveInitX = this.lastMouseX;
1110        // this.waveInitY = this.lastMouseY;
1111        this.waveInitX = (this.startPoint.x + this.endPoint.x)/2;
1112        this.waveInitY = this.endPoint.y + this.maxControlDistance;
1113        // play audio
1114        play_multi_sound('audio' + this.level);
1115        // createjs.Sound.play('cello_' + this.level);
1116
1117    } 
1118
1119    if( (!this.userInControl)&&mouseCrossed ) {
1120    	// console.log('i just plucked!');
1121        this.initState = false;
1122        this.userInControl = false;
1123        this.waveInControl = true;
1124        this.waveFinished = false;
1125        this.redrawActive = true;
1126        this.waveCount = 0;
1127        this.waveInitX = (this.startPoint.x + this.endPoint.y)/2;
1128        this.waveInitY = this.endPoint.y + this.maxGrabDistance * 2 / 3;
1129    }
1130};
1131GuitarString.prototype.mouseLeave = function(self, event){
1132    // console.log('mouseLeave');
1133    if( this.userInControl ) {
1134        this.initState = false;
1135        this.userInControl = false;
1136        this.waveInControl = true;
1137        this.waveFinished = false;
1138        this.redrawActive = true;
1139        this.waveCount = 0;
1140
1141        this.waveInitX = event.layerX*this.ratio;
1142        this.waveInitY = event.layerY*this.ratio;
1143
1144    }
1145};
1146GuitarString.prototype.mouseOut = function(self, event){
1147    // console.log('mouseOut');
1148};
1149GuitarString.prototype.touchDown = function(self, event){
1150    // console.log('touchDown');
1151    this.currentMouseX = event.layerX*this.ratio;
1152    this.currentMouseX = event.layerY*this.ratio;
1153};
1154GuitarString.prototype.touchXY = function (self, event){
1155    // console.log('touchMove');
1156    this.lastMouseX = this.currentMouseX;
1157    this.lastMouseY = this.currentMouseY;
1158    this.currentMouseX = event.layerX * this.ratio;
1159    this.currentMouseY = event.layerY * this.ratio; 
1160
1161    var radius = circleCenter(  new Point(this.startPoint.x, this.startPoint.y), 
1162                                new Point(this.currentMouseX, this.currentMouseY), 
1163                                new Point(this.endPoint.x, this.endPoint.y) ).r;
1164    var currentWaveDistance = radius - Math.sqrt( Math.pow(radius, 2) - Math.pow((Math.abs(this.endPoint.x - this.startPoint.x))/2, 2) );
1165    var lastRadius = circleCenter(  new Point(this.startPoint.x, this.startPoint.y), 
1166                                    new Point(this.lastMouseX, this.lastMouseY), 
1167                                    new Point(this.endPoint.x, this.endPoint.y) ).r;
1168    var lastWaveDistance = lastRadius - Math.sqrt( Math.pow(lastRadius, 2) 
1169                                        - Math.pow((Math.abs(this.endPoint.x - this.startPoint.x))/2, 2) );
1170    
1171
1172
1173    var mouseInGrabRange = currentWaveDistance < this.maxGrabDistance 
1174        && this.currentMouseX > this.startPoint.x
1175        && this.currentMouseX < this.endPoint.x;
1176
1177    var lastMouseOutGrabRange = !(lastWaveDistance < this.maxGrabDistance
1178        && this.lastMouseX > this.startPoint.x
1179        && this.lastMouseX < this.endPoint.x);
1180
1181    var mouseOutControlRange = !(currentWaveDistance < this.maxControlDistance 
1182        && this.currentMouseX > this.startPoint.x
1183        && this.currentMouseX < this.endPoint.x);
1184
1185    var lastMouseInControlRange = lastWaveDistance < this.maxControlDistance
1186        && this.lastMouseX > this.startPoint.x
1187        && this.lastMouseY < this.endPoint.x;
1188    
1189    var mouseCrossed = intersects(this.lastMouseX, this.lastMouseY, 
1190                            this.currentMouseX, this.currentMouseY,
1191                            this.startPoint.x, this.startPoint.y,
1192                            this.endPoint.x, this.endPoint.y);
1193
1194    if( mouseInGrabRange && lastMouseOutGrabRange && (!this.userInControl) ){
1195        // console.log('grab!');
1196        this.initState = false;
1197        this.userInControl = true;
1198        this.waveInControl = false;
1199        this.waveFinished = false;
1200
1201        this.redrawActive = true;
1202    } else if ( mouseOutControlRange && lastMouseInControlRange && this.userInControl){
1203        // console.log('boing!');
1204        this.initState = false;
1205        this.userInControl = false;
1206        this.waveInControl = true;
1207        this.waveFinished = false;
1208        this.waveCount = 0;
1209        // this.waveInitX = this.lastMouseX;
1210        // this.waveInitY = this.lastMouseY;
1211        this.waveInitX = (this.startPoint.x + this.endPoint.x)/2;
1212        this.waveInitY = this.endPoint.y + this.maxControlDistance;
1213        // play audio
1214        play_multi_sound('audio' + this.level);
1215
1216    } 
1217
1218    if( (!this.userInControl)&&mouseCrossed ) {
1219        // console.log('i just plucked!');
1220        this.initState = false;
1221        this.userInControl = false;
1222        this.waveInControl = true;
1223        this.waveFinished = false;
1224        this.redrawActive = true;
1225        this.waveCount = 0;
1226        this.waveInitX = (this.startPoint.x + this.endPoint.y)/2;
1227        this.waveInitY = this.endPoint.y + this.maxGrabDistance * 2 / 3;
1228        // play audio
1229        // play_multi_sound('audio' + this.level);
1230
1231    }
1232};
1233GuitarString.prototype.touchUp = function(self, event){
1234    // console.log('touchUp');
1235    if( this.userInControl ) {
1236        this.initState = false;
1237        this.userInControl = false;
1238        this.waveInControl = true;
1239        this.waveFinished = false;
1240        this.redrawActive = true;
1241        this.waveCount = 0;
1242
1243        this.waveInitX = event.layerX*this.ratio;
1244        this.waveInitY = event.layerY*this.ratio;
1245
1246    }
1247};
1248
1249
1250GuitarString.prototype.clear = function(){
1251	// clear
1252    // if(this.redrawActive){
1253    	// this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
1254    // }
1255};
1256
1257GuitarString.prototype.update = function(){
1258	// console.log(this.redrawActive);
1259	// if(this.redrawActive){
1260		if ( this.userInControl ){
1261            this.thirdPoint = new Point(this.currentMouseX, this.currentMouseY);
1262        }
1263        if ( this.waveInControl ){
1264            var waveX = this.waveInitX;
1265            var waveY = this.startPoint.y + 
1266                    (this.waveInitY-this.startPoint.y)
1267                    *Math.cos(this.waveCount/5*Math.PI)
1268                    *Math.pow(this.damping, this.waveCount);
1269    
1270            if ( Math.pow(this.damping, this.waveCount) > 0.03) {
1271                // still waving ....
1272                this.thirdPoint = new Point(waveX, waveY);
1273                this.waveCount++;
1274            } else {
1275                // wave damped to a straight line, wave is finished
1276                this.waveInControl = false;
1277                this.waveFinished = true;
1278                this.lastRedraw = true;
1279                this.thirdPoint = new Point(waveX, waveY);
1280            }
1281        }
1282	// }
1283}
1284
1285GuitarString.prototype.draw = function(){
1286    // if(this.redrawActive){   
1287        // draw stuff
1288    // }
1289    if(this.lastRedraw) {
1290        this.drawLine();
1291        this.lastRedraw = false;
1292        this.redrawActive = false;
1293    } else {
1294        this.drawArc(this.startPoint, this.thirdPoint, this.endPoint);
1295    }
1296};
1297GuitarString.prototype.drawLine = function(){
1298    // draw a line instead of a flat curve when it stops redraw, pixel-perfect  
1299    this.ctx.lineWidth = this.strokeWidth;
1300    this.ctx.strokeStyle = this.strokeColor;
1301        this.ctx.beginPath();
1302        this.ctx.moveTo(this.startPoint.x, this.startPoint.y);
1303        this.ctx.lineTo(this.endPoint.x, this.endPoint.y);
1304        this.ctx.globalCompositeOperation = "source-over";
1305        this.ctx.stroke();
1306};
1307
1308
1309GuitarString.prototype.drawArc = function(startPoint, thirdPoint, endPoint){
1310    var ctx = this.ctx;
1311    ctx.lineWidth = this.strokeWidth;
1312    ctx.strokeStyle = this.strokeColor;
1313
1314    var centerObject = circleCenter( new Point(startPoint.x, startPoint.y), 
1315                                     new Point(thirdPoint.x, thirdPoint.y), 
1316                                     new Point(endPoint.x, endPoint.y) );
1317    var centerX = centerObject.x;
1318    var centerY = centerObject.y;
1319    var r = centerObject.r
1320
1321    var angle = Math.atan2(centerX-startPoint.x, centerY-startPoint.y);
1322    // console.log(centerObject);
1323    if (!angle){
1324        ctx.beginPath();
1325        ctx.moveTo(startPoint.x, startPoint.y);
1326        ctx.lineTo(endPoint.x, endPoint.y);
1327    } else {
1328    	if( angle > Math.PI/2) {
1329	        ctx.beginPath();
1330	        ctx.arc(centerX, centerY, r, Math.PI * 1.5-angle, Math.PI * 1.5 + angle, true);
1331	    } else {
1332	        ctx.beginPath();
1333	        ctx.arc(centerX, centerY, r, Math.PI * 1.5-angle, Math.PI * 1.5 + angle, false);
1334	    }
1335    }
1336    ctx.globalCompositeOperation = "source-over";
1337    ctx.stroke();
1338
1339}
1340

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.