diff --git a/.gitignore b/.gitignore index c40cd56..a349cbb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ .svn node_modules -jquery-turtle.min.js diff --git a/BUILD.txt b/BUILD.txt index 6208a91..c22f4af 100644 --- a/BUILD.txt +++ b/BUILD.txt @@ -29,5 +29,3 @@ Testing Unit tests use a headless webkit. When setting it up on a system without GUI, you may find that you need to install font support. On debian. "apt-get libfontconfig1" provides enough font support. - - diff --git a/Gruntfile.js b/Gruntfile.js index b2f1735..12f17ea 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,3 +1,13 @@ +var serveNoDottedFiles = function(connect, options, middlewares) { + // Avoid leaking .git/.svn or other dotted files from test servers. + middlewares.unshift(function(req, res, next) { + if (req.url.indexOf('/.') < 0) { return next(); } + res.statusCode = 404 + res.end("Cannot GET " + req.url) + }); + return middlewares; +}; + module.exports = function(grunt) { "use strict"; @@ -21,7 +31,8 @@ module.exports = function(grunt) { connect: { testserver: { options: { - hostname: '0.0.0.0' + hostname: '0.0.0.0', + middleware: serveNoDottedFiles } } }, diff --git a/LICENSE.txt b/LICENSE.txt index fca7187..f997494 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -2,7 +2,7 @@ jQuery-turtle version 2.0 LICENSE (MIT): -Copyright (c) 2013 Pencil Code Foundation, Google, and other contributors. +Copyright (c) 2013 Pencil Code Foundation, Google Inc., and other contributors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 91bace6..fe5db8e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ jQuery-turtle ============= -version 2.0.8 +version 2.0.9 jQuery-turtle is a jQuery plugin for turtle graphics. @@ -254,7 +254,7 @@ the functions:
 eval $.turtle()  # Create the default turtle and global functions.
 
-defaultspeed Infinity
+speed Infinity
 write "Catch blue before red gets you."
 bk 100
 r = hatch red
@@ -264,7 +264,7 @@ tick 10, ->
   fd 6
   r.turnto turtle
   r.fd 4
-  b.turnto bearing b
+  b.turnto direction b
   b.fd 3
   if b.touches(turtle)
     write "You win!"
@@ -331,7 +331,7 @@ element.
 License (MIT)
 -------------
 
-Copyright (c) 2014 Pencil Code, Google, and other contributors
+Copyright (c) 2014 Pencil Code, Google Inc., and other contributors
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/bower.json b/bower.json
index f2e71d7..4eca85a 100644
--- a/bower.json
+++ b/bower.json
@@ -2,7 +2,7 @@
   "name": "jquery-turtle",
   "main": "jquery-turtle.js",
   "ignore": [],
-  "version": "2.0.8",
+  "version": "2.0.9",
   "description": "Turtle graphics plugin for jQuery.",
   "devDependencies": {
     "jquery": "latest",
diff --git a/jquery-turtle.js b/jquery-turtle.js
index f31c0b5..9008141 100644
--- a/jquery-turtle.js
+++ b/jquery-turtle.js
@@ -4,7 +4,7 @@
 jQuery-turtle
 =============
 
-version 2.0.8
+version 2.0.9
 
 jQuery-turtle is a jQuery plugin for turtle graphics.
 
@@ -43,7 +43,7 @@ for color in [red, gold, green, blue]
       lt 360 / sides
     pen null
     fd 40
-  move 40, -160
+  slide 40, -160
 
[Try an interactive demo (CoffeeScript syntax) here.]( @@ -62,8 +62,8 @@ $(q).fd(100) // Forward relative motion in local coordinates. $(q).bk(50) // Back. $(q).rt(90) // Right turn. Optional second arg is turning radius. $(q).lt(45) // Left turn. Optional second arg is turning radius. -$(q).move(x, y) // Move right by x while moving forward by y. -$(q).jump(x, y) // Like move, but without drawing. +$(q).slide(x, y) // Move right by x while moving forward by y. +$(q).leap(x, y) // Like slide, but without drawing. $(q).moveto({pageX:x,pageY:y} | [x,y]) // Absolute motion on page. $(q).jumpto({pageX:x,pageY:y} | [x,y]) // Like moveto, without drawing. $(q).turnto(direction || position) // Absolute direction adjustment. @@ -261,7 +261,7 @@ the functions:
 eval $.turtle()  # Create the default turtle and global functions.
 
-defaultspeed Infinity
+speed Infinity
 write "Catch blue before red gets you."
 bk 100
 r = new Turtle red
@@ -368,6 +368,7 @@ THE SOFTWARE.
 //////////////////////////////////////////////////////////////////////////
 
 var undefined = void 0,
+    global = this,
     __hasProp = {}.hasOwnProperty,
     rootjQuery = jQuery(function() {}),
     interrupted = false,
@@ -752,8 +753,8 @@ function getElementTranslation(elem) {
 
 // Reads out the 2x3 transform matrix of the given element.
 function readTransformMatrix(elem) {
-  var ts = (window.getComputedStyle ?
-      window.getComputedStyle(elem)[transform] :
+  var ts = (global.getComputedStyle ?
+      global.getComputedStyle(elem)[transform] :
       $.css(elem, 'transform'));
   if (!ts || ts === 'none') {
     return null;
@@ -771,7 +772,7 @@ function readTransformMatrix(elem) {
 // Reads out the css transformOrigin property, if present.
 function readTransformOrigin(elem, wh) {
   var hidden = ($.css(elem, 'display') === 'none'),
-      swapout, old, name, gcs, origin;
+      swapout, old, name;
   if (hidden) {
     // IE GetComputedStyle doesn't give pixel values for transformOrigin
     // unless the element is unhidden.
@@ -782,13 +783,13 @@ function readTransformOrigin(elem, wh) {
       elem.style[name] = swapout[name];
     }
   }
-  var gcs = (window.getComputedStyle ?  window.getComputedStyle(elem) : null),
-      origin = (gcs && gcs[transformOrigin] || $.css(elem, 'transformOrigin'));
+  var gcs = (global.getComputedStyle ?  global.getComputedStyle(elem) : null);
   if (hidden) {
     for (name in swapout) {
       elem.style[name] = old[name];
     }
   }
+  var origin = (gcs && gcs[transformOrigin] || $.css(elem, 'transformOrigin'));
   if (origin && origin.indexOf('%') < 0) {
     return $.map(origin.split(' '), parseFloat);
   }
@@ -950,7 +951,8 @@ function cleanedStyle(trans) {
 // center of rotation when no transforms are applied) in page coordinates.
 function getTurtleOrigin(elem, inverseParent, extra) {
   var state = $.data(elem, 'turtleData');
-  if (state && state.quickhomeorigin && state.down && state.style && !extra) {
+  if (state && state.quickhomeorigin && state.down && state.style && !extra
+      && elem.classList && elem.classList.contains('turtle')) {
     return state.quickhomeorigin;
   }
   var hidden = ($.css(elem, 'display') === 'none'),
@@ -958,7 +960,7 @@ function getTurtleOrigin(elem, inverseParent, extra) {
         { position: "absolute", visibility: "hidden", display: "block" } : {},
       substTransform = swapout[transform] = (inverseParent ? 'matrix(' +
           $.map(inverseParent, cssNum).join(', ') + ', 0, 0)' : 'none'),
-      old = {}, name, gbcr, transformOrigin, result;
+      old = {}, name, gbcr, transformOrigin;
   for (name in swapout) {
     old[name] = elem.style[name];
     elem.style[name] = swapout[name];
@@ -981,12 +983,12 @@ function getTurtleOrigin(elem, inverseParent, extra) {
 
 function wh() {
   // Quirks-mode compatible window height.
-  return window.innerHeight || $(window).height();
+  return global.innerHeight || $(global).height();
 }
 
 function ww() {
   // Quirks-mode compatible window width.
-  return window.innerWidth || $(window).width();
+  return global.innerWidth || $(global).width();
 }
 
 function dh() {
@@ -1013,7 +1015,7 @@ function getPageGbcr(elem) {
     return makeGbcrLTWH(elem.pageX, elem.pageY, 0, 0);
   } else if ($.isWindow(elem)) {
     return makeGbcrLTWH(
-        $(window).scrollLeft(), $(window).scrollTop(), ww(), wh());
+        $(global).scrollLeft(), $(global).scrollTop(), ww(), wh());
   } else if (elem.nodeType === 9) {
     return makeGbcrLTWH(0, 0, dw(), dh());
   } else if (!('getBoundingClientRect' in elem)) {
@@ -1057,10 +1059,10 @@ function polyMatchesGbcr(poly, gbcr) {
 function readPageGbcr() {
   var raw = this.getBoundingClientRect();
   return {
-    top: raw.top + window.pageYOffset,
-    bottom: raw.bottom + window.pageYOffset,
-    left: raw.left + window.pageXOffset,
-    right: raw.right + window.pageXOffset,
+    top: raw.top + global.pageYOffset,
+    bottom: raw.bottom + global.pageYOffset,
+    left: raw.left + global.pageXOffset,
+    right: raw.right + global.pageXOffset,
     width: raw.width,
     height: raw.height
   };
@@ -1088,8 +1090,7 @@ function computeTargetAsTurtlePosition(elem, target, limit, localx, localy) {
   localTarget = matrixVectorProduct(inverseParent,
       subtractVector([target.pageX, target.pageY], origin));
   if (localx || localy) {
-    var ts = readTurtleTransform(elem, true),
-        sy = ts ? ts.sy : 1;
+    var sy = elemOldScale(elem);
     localTarget[0] += localx * sy;
     localTarget[1] -= localy * sy;
   }
@@ -1117,7 +1118,7 @@ function computePositionAsLocalOffset(elem, home) {
       ts = readTurtleTransform(elem, true),
       localHome = inverseParent && matrixVectorProduct(inverseParent,
           subtractVector([home.pageX, home.pageY], origin)),
-      isy = ts && 1 / ts.sy;
+      isy = 1 / elemOldScale(elem);
   if (!inverseParent) { return; }
   return [(ts.tx - localHome[0]) * isy, (localHome[1] - ts.ty) * isy];
 }
@@ -1126,11 +1127,12 @@ function convertLocalXyToPageCoordinates(elem, localxy) {
   var totalParentTransform = totalTransform2x2(elem.parentElement),
       ts = readTurtleTransform(elem, true),
       center = $(homeContainer(elem)).pagexy(),
+      sy = elemOldScale(elem),
       result = [],
       pageOffset, j;
   for (j = 0; j < localxy.length; j++) {
     pageOffset = matrixVectorProduct(
-        totalParentTransform, [localxy[j][0] * ts.sy, -localxy[j][1] * ts.sy]);
+        totalParentTransform, [localxy[j][0] * sy, -localxy[j][1] * sy]);
     result.push({ pageX: center.pageX + pageOffset[0],
                   pageY: center.pageY + pageOffset[1] });
   }
@@ -1145,7 +1147,7 @@ function convertLocalXyToPageCoordinates(elem, localxy) {
 function getCenterInPageCoordinates(elem) {
   if ($.isWindow(elem)) {
     return getRoundedCenterLTWH(
-        $(window).scrollLeft(), $(window).scrollTop(), ww(), wh());
+        $(global).scrollLeft(), $(global).scrollTop(), ww(), wh());
   } else if (elem.nodeType === 9 || elem == document.body) {
     return getRoundedCenterLTWH(0, 0, dw(), dh());
   }
@@ -1160,12 +1162,28 @@ function getCenterInPageCoordinates(elem) {
       origin = getTurtleOrigin(elem, inverseParent),
       pos = addVector(matrixVectorProduct(totalParentTransform, tr), origin),
       result = { pageX: pos[0], pageY: pos[1] };
-  if (state && simple && state.down && state.style) {
+  if (state && simple && state.down && state.style && elem.classList &&
+      elem.classList.contains('turtle')) {
     state.quickpagexy = result;
   }
   return result;
 }
 
+// The quickpagexy variable is an optimization that assumes
+// parent coordinates do not change.  This function will clear
+// the cache, and is used when we have a container that is moving.
+function clearChildQuickLocations(elem) {
+  if (elem.tagName != 'CANVAS' && elem.tagName != 'IMG') {
+    $(elem).find('.turtle').each(function(j, e) {
+      var s = $.data(e, 'turtleData');
+      if (s) {
+        s.quickpagexy = null;
+        s.quickhomeorigin = null;
+      }
+    });
+  }
+}
+
 function polyToVectorsOffset(poly, offset) {
   if (!poly) { return null; }
   var result = [], j = 0;
@@ -1180,7 +1198,7 @@ function polyToVectorsOffset(poly, offset) {
 function getCornersInPageCoordinates(elem, untransformed) {
   if ($.isWindow(elem)) {
     return getStraightRectLTWH(
-        $(window).scrollLeft(), $(window).scrollTop(), ww(), wh());
+        $(global).scrollLeft(), $(global).scrollTop(), ww(), wh());
   } else if (elem.nodeType === 9) {
     return getStraightRectLTWH(0, 0, dw(), dh());
   }
@@ -1227,12 +1245,12 @@ function scrollWindowToDocumentPosition(pos, limit) {
       b = $('body'),
       dw = b.width(),
       dh = b.height(),
-      w = $(window);
+      w = $(global);
   if (tx > dw - ww2) { tx = dw - ww2; }
   if (tx < ww2) { tx = ww2; }
   if (ty > dh - wh2) { ty = dh - wh2; }
   if (ty < wh2) { ty = wh2; }
-  targ = { pageX: tx, pageY: ty };
+  var targ = { pageX: tx, pageY: ty };
   if ($.isNumeric(limit)) {
     targ = limitMovement(w.origin(), targ, limit);
   }
@@ -1486,6 +1504,8 @@ function writeTurtleTransform(ts) {
   return result.join(' ');
 }
 
+function modulo(n, m) { return (+n % (m = +m) + m) % m; }
+
 function radiansToDegrees(r) {
   var d = r * 180 / Math.PI;
   if (d > 180) { d -= 360; }
@@ -1493,7 +1513,7 @@ function radiansToDegrees(r) {
 }
 
 function convertToRadians(d) {
-  return d * Math.PI / 180;
+  return d / 180 * Math.PI;
 }
 
 function normalizeRotation(x) {
@@ -1568,7 +1588,7 @@ function createSurfaceAndField() {
       pointerEvents: 'none',
       overflow: 'hidden'
     }).addClass('turtlefield');
-  $(field).attr('id', 'field')
+  $(field).attr('id', 'origin')
     .css({
       position: 'absolute',
       display: 'inline-block',
@@ -1579,7 +1599,10 @@ function createSurfaceAndField() {
       // fixes a "center" point in page coordinates that
       // will not change even if the document resizes.
       transformOrigin: "0px 0px",
-      pointerEvents: 'all'
+      pointerEvents: 'all',
+      // Setting turtleSpeed to Infinity by default allows
+      // moving the origin instantly without sync.
+      turtleSpeed: Infinity
     }).appendTo(surface);
   globalDrawing.surface = surface;
   globalDrawing.field = field;
@@ -1634,7 +1657,7 @@ function getTurtleDrawingCanvas() {
   surface.insertBefore(globalDrawing.canvas, surface.firstChild);
   resizecanvas();
   pollbodysize(resizecanvas);
-  $(window).resize(resizecanvas);
+  $(global).resize(resizecanvas);
   return globalDrawing.canvas;
 }
 
@@ -1682,8 +1705,8 @@ function sizexy() {
   // Using innerHeight || $(window).height() deals with quirks-mode.
   var b = $('body');
   return [
-    Math.max(b.outerWidth(true), window.innerWidth || $(window).width()),
-    Math.max(b.outerHeight(true), window.innerHeight || $(window).height())
+    Math.max(b.outerWidth(true), global.innerWidth || $(global).width()),
+    Math.max(b.outerHeight(true), global.innerHeight || $(global).height())
   ];
 }
 
@@ -1775,6 +1798,7 @@ function getTurtleData(elem) {
       drawOnCanvas: null,
       quickpagexy: null,
       quickhomeorigin: null,
+      oldscale: 1,
       instrument: null,
       stream: null
     });
@@ -2018,6 +2042,7 @@ function addToPathList(pathList, point) {
 }
 
 function flushPenState(elem, state, corner) {
+  clearChildQuickLocations(elem);
   if (!state) {
     // Default is no pen and no path, so nothing to do.
     return;
@@ -2031,7 +2056,6 @@ function flushPenState(elem, state, corner) {
     if (path[0].length) { path[0].length = 0; }
     if (corner) {
       if (!style) {
-        if (window.buggy) console.trace('clearing the retracing path');
         // pen null will clear the retracing path too.
         if (corners.length > 1) corners.length = 1;
         if (corners[0].length) corners[0].length = 0;
@@ -2056,22 +2080,22 @@ function flushPenState(elem, state, corner) {
     addToPathList(corners[0], center);
   }
   if (style.savePath) return;
-  // Add to tracing path, and trace it righ away.
+  // Add to tracing path, and trace it right away.
   addToPathList(path[0], center);
-  var ts = readTurtleTransform(elem, true);
+  var scale = drawingScale(elem);
   // Last argument 2 means that the last two points are saved, which
   // allows us to draw corner miters and also avoid 'butt' lineCap gaps.
-  drawAndClearPath(getDrawOnCanvas(state), state.path, style, ts.sx, 2);
+  drawAndClearPath(getDrawOnCanvas(state), state.path, style, scale, 2);
 }
 
 function endAndFillPenPath(elem, style) {
-  var ts = readTurtleTransform(elem, true),
-      state = getTurtleData(elem);
+  var state = getTurtleData(elem);
   if (state.style) {
     // Apply a default style.
     style = $.extend({}, state.style, style);
   }
-  drawAndClearPath(getDrawOnCanvas(state), state.corners, style, ts.sx, 1);
+  var scale = drawingScale(elem);
+  drawAndClearPath(getDrawOnCanvas(state), state.corners, style, scale, 1);
 }
 
 function clearField(arg) {
@@ -2154,8 +2178,7 @@ function touchesPixel(elem, color) {
       h = (bb.bottom - bb.top),
       osc = getOffscreenCanvas(w, h),
       octx = osc.getContext('2d'),
-      rgba = rgbaForColor(color),
-      j = 1, k, data;
+      j = 1, k;
   if (!c || c.length < 3 || !w || !h) { return false; }
   octx.drawImage(canvas,
       bb.left, bb.top, w, h, 0, 0, w, h);
@@ -2183,7 +2206,7 @@ function touchesPixel(elem, color) {
   }
   octx.restore();
   // Now examine the results and look for alpha > 0%.
-  data = octx.getImageData(0, 0, w, h).data;
+  var data = octx.getImageData(0, 0, w, h).data;
   if (!rgba || rgba[3] == 0) {
     // Handle the "looking for any color" and "transparent" cases.
     var wantcolor = !rgba;
@@ -2237,8 +2260,9 @@ function applyImg(sel, img, cb) {
 function doQuickMove(elem, distance, sideways) {
   var ts = readTurtleTransform(elem, true),
       r = ts && convertToRadians(ts.rot),
-      scaledDistance = ts && (distance * ts.sy),
-      scaledSideways = ts && ((sideways || 0) * ts.sy),
+      sy = elemOldScale(elem),
+      scaledDistance = ts && (distance * sy),
+      scaledSideways = ts && ((sideways || 0) * sy),
       dy = -Math.cos(r) * scaledDistance,
       dx = Math.sin(r) * scaledDistance,
       state = $.data(elem, 'turtleData'),
@@ -2285,13 +2309,14 @@ function doQuickRotate(elem, degrees) {
 }
 
 function displacedPosition(elem, distance, sideways) {
-  var ts = readTurtleTransform(elem, true),
-      r = ts && convertToRadians(ts.rot),
-      scaledDistance = ts && (distance * ts.sy),
-      scaledSideways = ts && ((sideways || 0) * ts.sy),
+  var ts = readTurtleTransform(elem, true);
+  if (!ts) { return; }
+  var s = elemOldScale(elem),
+      r = convertToRadians(ts.rot),
+      scaledDistance = distance * s,
+      scaledSideways = (sideways || 0) * s,
       dy = -Math.cos(r) * scaledDistance,
       dx = Math.sin(r) * scaledDistance;
-  if (!ts) { return; }
   if (scaledSideways) {
     dy += Math.sin(r) * scaledSideways;
     dx += Math.cos(r) * scaledSideways;
@@ -2362,16 +2387,18 @@ function makeTurtleForwardHook() {
       if (ts) {
         var r = convertToRadians(ts.rot),
             c = Math.cos(r),
-            s = Math.sin(r);
+            s = Math.sin(r),
+            sy = elemOldScale(elem);
         return cssNum(((ts.tx + middle[0]) * s - (ts.ty + middle[1]) * c)
-            / ts.sy) + 'px';
+            / sy) + 'px';
       }
     },
     set: function(elem, value) {
       var ts = readTurtleTransform(elem, true) ||
               {tx: 0, ty: 0, rot: 0, sx: 1, sy: 1, twi: 0},
           middle = readTransformOrigin(elem),
-          v = parseFloat(value) * ts.sy,
+          sy = elemOldScale(elem),
+          v = parseFloat(value) * sy,
           r = convertToRadians(ts.rot),
           c = Math.cos(r),
           s = Math.sin(r),
@@ -2417,6 +2444,8 @@ function makeTurtleHook(prop, normalize, unit, displace) {
           };
         }
         flushPenState(elem, state);
+      } else {
+        clearChildQuickLocations(elem);
       }
     }
   };
@@ -2519,7 +2548,8 @@ function maybeArcRotation(end, elem, ts, opt) {
     return tradius === 0 ? normalizeRotation(end) : end;
   }
   var tracing = (state && state.style && state.down),
-      turnradius = tradius * ts.sy, a;
+      sy = (state && state.oldscale) ? ts.sy : 1,
+      turnradius = tradius * sy, a;
   if (tracing) {
     a = addArcBezierPaths(
       state.path[0],                            // path to add to
@@ -2609,6 +2639,8 @@ function makeTurtleXYHook(publicname, propx, propy, displace) {
           };
         }
         flushPenState(elem, state);
+      } else {
+        clearChildQuickLocations(elem);
       }
     }
   };
@@ -2645,7 +2677,7 @@ function apiUrl(url, topdir) {
       result = link.protocol + '//' + link.host + '/' + topdir + '/' +
         link.pathname.replace(/\/[^\/]*(?:\/|$)/, '') + link.search + link.hash;
     }
-  } else if (isPencilHost(window.location.hostname)) {
+  } else if (isPencilHost(global.location.hostname)) {
     // Proxy offdomain requests to avoid CORS issues.
     result = '/proxy/' + result;
   }
@@ -2655,8 +2687,8 @@ function apiUrl(url, topdir) {
 function imgUrl(url) {
   if (/\//.test(url)) { return url; }
   url = '/img/' + url;
-  if (isPencilHost(window.location.hostname)) { return url; }
-  return '//pencil.io' + url;
+  if (isPencilHost(global.location.hostname)) { return url; }
+  return '//pencilcode.net' + url;
 }
 // Retrieves the pencil code login cookie, if there is one.
 function loginCookie() {
@@ -2693,7 +2725,8 @@ var stablyLoadedImages = {};
 // @param css is a dictionary of css props to set when the image is loaded.
 // @param cb is an optional callback, called after the loading is done.
 function setImageWithStableOrigin(elem, url, css, cb) {
-  var record, urlobj = absoluteUrlObject(url), url = urlobj.href;
+  var record, urlobj = absoluteUrlObject(url);
+  url = urlobj.href;
   // The data-loading attr will always reflect the last URL requested.
   elem.setAttribute('data-loading', url);
   if (url in stablyLoadedImages) {
@@ -2999,7 +3032,7 @@ var Pencil = (function(_super) {
     }
     // The pencil is a sprite that just defaults to zero size.
     var context = canvas ? canvas.parentElement : null;
-    var settings = { width: 0, height: 0, color: transparent };
+    var settings = { width: 0, height: 0, color: 'transparent' };
     Pencil.__super__.constructor.call(this, settings, context);
     // Set the pencil to hidden, infinite speed,
     // and drawing on the specifed canvas.
@@ -3039,7 +3072,7 @@ var Webcam = (function(_super) {
   function Webcam(opts, context) {
     var attrs = "", hassrc = false, hasautoplay = false, hasdims = false;
     if ($.isPlainObject(opts)) {
-      for (key in opts) {
+      for (var key in opts) {
         attrs += ' ' + key + '="' + escapeHtml(opts[key]) + '"';
       }
       hassrc = ('src' in opts);
@@ -3082,7 +3115,7 @@ var Webcam = (function(_super) {
             $(v).off('play.capture' + k);
             next();
           });
-          v.src = window.URL.createObjectURL(stream);
+          v.src = global.URL.createObjectURL(stream);
         }
       }, function() {
         next();
@@ -3284,12 +3317,12 @@ var Piano = (function(_super) {
 
   // Converts a midi number to a white key position (black keys round left).
   function wcp(n) {
-    return floor((n + 7) / 12 * 7);
+    return Math.floor((n + 7) / 12 * 7);
   };
 
   // Converts from a white key position to a midi number.
   function mcp(n) {
-    return ceil(n / 7 * 12) - 7;
+    return Math.ceil(n / 7 * 12) - 7;
   };
 
   // True if midi #n is a black key.
@@ -3398,14 +3431,14 @@ function focusWindowIfFirst() {
   try {
     // If we are in a frame with access to a parent with an activeElement,
     // then try to blur it (as is common inside the pencilcode IDE).
-    window.parent.document.activeElement.blur();
+    global.parent.document.activeElement.blur();
   } catch (e) {}
-  window.focus();
+  global.focus();
 }
 
 // Construction of keyCode names.
 var keyCodeName = (function() {
-  var ua = typeof window !== 'undefined' ? window.navigator.userAgent : '',
+  var ua = typeof global !== 'undefined' ? global.navigator.userAgent : '',
       isOSX = /OS X/.test(ua),
       isOpera = /Opera/.test(ua),
       maybeFirefox = !/like Gecko/.test(ua) && !isOpera,
@@ -3585,9 +3618,9 @@ var pressedKey = (function() {
     resetPressedState();
     for (var name in eventMap) {
       if (turnon) {
-        window.addEventListener(name, eventMap[name], true);
+        global.addEventListener(name, eventMap[name], true);
       } else {
-        window.removeEventListener(name, eventMap[name]);
+        global.removeEventListener(name, eventMap[name]);
       }
     }
   }
@@ -3695,7 +3728,7 @@ function forwardBodyMouseEventsIfNeeded() {
           var warn = $.turtle.nowarn;
           $.turtle.nowarn = true;
           var sel = $(globalDrawing.surface)
-              .find('.turtle').within('touch', e).eq(0);
+              .find('.turtle,.turtlelabel').within('touch', e).eq(0);
           $.turtle.nowarn = warn;
           if (sel.length === 1) {
             // Erase portions of the event that are wrong for the turtle.
@@ -3829,9 +3862,11 @@ function getGlobalInstrument() {
   return global_instrument;
 }
 
+// Beginning of musical.js copy
+
 // Tests for the presence of HTML5 Web Audio (or webkit's version).
 function isAudioPresent() {
-  return !!(window.AudioContext || window.webkitAudioContext);
+  return !!(global.AudioContext || global.webkitAudioContext);
 }
 
 // All our audio funnels through the same AudioContext with a
@@ -3842,7 +3877,7 @@ function getAudioTop() {
   if (!isAudioPresent()) {
     return null;
   }
-  var ac = new (window.AudioContext || window.webkitAudioContext);
+  var ac = new (global.AudioContext || global.webkitAudioContext);
   getAudioTop.audioTop = {
     ac: ac,
     wavetable: makeWavetable(ac),
@@ -3864,11 +3899,16 @@ function resetAudio() {
       atop.out = null;
       atop.currentStart = null;
     }
-    var dcn = atop.ac.createDynamicsCompressor();
-    dcn.ratio = 16;
-    dcn.attack = 0.0005;
-    dcn.connect(atop.ac.destination);
-    atop.out = dcn;
+    // If resetting due to interrupt after AudioContext closed, this can fail.
+    try {
+      var dcn = atop.ac.createDynamicsCompressor();
+      dcn.ratio = 16;
+      dcn.attack = 0.0005;
+      dcn.connect(atop.ac.destination);
+      atop.out = dcn;
+    } catch (e) {
+      getAudioTop.audioTop = null;
+    }
   }
 }
 
@@ -3887,6 +3927,48 @@ function audioCurrentStartTime() {
   return atop.currentStart;
 }
 
+// Converts a midi note number to a frequency in Hz.
+function midiToFrequency(midi) {
+  return 440 * Math.pow(2, (midi - 69) / 12);
+}
+// Some constants.
+var noteNum =
+    {C:0,D:2,E:4,F:5,G:7,A:9,B:11,c:12,d:14,e:16,f:17,g:19,a:21,b:23};
+var accSym =
+    { '^':1, '': 0, '=':0, '_':-1 };
+var noteName =
+    ['C', '^C', 'D', '_E', 'E', 'F', '^F', 'G', '_A', 'A', '_B', 'B',
+     'c', '^c', 'd', '_e', 'e', 'f', '^f', 'g', '_a', 'a', '_b', 'b'];
+// Converts a frequency in Hz to the closest midi number.
+function frequencyToMidi(freq) {
+  return Math.round(69 + Math.log(freq / 440) * 12 / Math.LN2);
+}
+// Converts an ABC pitch (such as "^G,,") to a midi note number.
+function pitchToMidi(pitch) {
+  var m = /^(\^+|_+|=|)([A-Ga-g])([,']*)$/.exec(pitch);
+  if (!m) { return null; }
+  var octave = m[3].replace(/,/g, '').length - m[3].replace(/'/g, '').length;
+  var semitone =
+      noteNum[m[2]] + accSym[m[1].charAt(0)] * m[1].length + 12 * octave;
+  return semitone + 60; // 60 = midi code middle "C".
+}
+// Converts a midi number to an ABC notation pitch.
+function midiToPitch(midi) {
+  var index = ((midi - 72) % 12);
+  if (midi > 60 || index != 0) { index += 12; }
+  var octaves = Math.round((midi - index - 60) / 12),
+      result = noteName[index];
+  while (octaves != 0) {
+    result += octaves > 0 ? "'" : ",";
+    octaves += octaves > 0 ? -1 : 1;
+  }
+  return result;
+}
+// Converts an ABC pitch to a frequency in Hz.
+function pitchToFrequency(pitch) {
+  return midiToFrequency(pitchToMidi(pitch));
+}
+
 // All further details of audio handling are encapsulated in the Instrument
 // class, which knows how to synthesize a basic timbre; how to play and
 // schedule a tone; and how to parse and sequence a song written in ABC
@@ -3926,6 +4008,7 @@ var Instrument = (function() {
     }
   }
 
+  Instrument.timeOffset = 0.0625;// Seconds to delay all audiable timing.
   Instrument.dequeueTime = 0.5;  // Seconds before an event to reexamine queue.
   Instrument.bufferSecs = 2;     // Seconds ahead to put notes in WebAudio.
   Instrument.toneLength = 1;     // Default duration of a tone.
@@ -4062,7 +4145,7 @@ var Instrument = (function() {
   // node graph for the tone generators and filters for the tone.
   Instrument.prototype._makeSound = function(record) {
     var timbre = record.timbre || this._timbre,
-        starttime = record.time,
+        starttime = record.time + Instrument.timeOffset,
         releasetime = starttime + record.duration,
         attacktime = Math.min(releasetime, starttime + timbre.attack),
         decaytime = timbre.decay *
@@ -4139,12 +4222,13 @@ var Instrument = (function() {
   // Truncates a sound previously scheduled by _makeSound by using
   // cancelScheduledValues and directly ramping down to zero.
   // Can only be used to shorten a sound.
-  Instrument.prototype._truncateSound = function(record, releasetime) {
-    if (releasetime < record.time + record.duration) {
-      record.duration = Math.max(0, releasetime - record.time);
+  Instrument.prototype._truncateSound = function(record, truncatetime) {
+    if (truncatetime < record.time + record.duration) {
+      record.duration = Math.max(0, truncatetime - record.time);
       if (record.gainNode) {
         var timbre = record.timbre || this._timbre,
-            starttime = record.time,
+            starttime = record.time + Instrument.timeOffset,
+            releasetime = truncatetime + Instrument.timeOffset,
             attacktime = Math.min(releasetime, starttime + timbre.attack),
             decaytime = timbre.decay *
                 Math.pow(440 / record.frequency, timbre.decayfollow),
@@ -4161,7 +4245,8 @@ var Instrument = (function() {
         } else if (releasetime <= attacktime) {
           // Release before attack is done?  Interrupt ramp up.
           g.gain.linearRampToValueAtTime(
-            amp * (releasetime - starttime) / (attacktime - starttime));
+            amp * (releasetime - starttime) / (attacktime - starttime),
+            releasetime);
         } else {
           // Release during decay?  Interrupt decay down.
           g.gain.setValueAtTime(amp * (timbre.sustain + (1 - timbre.sustain) *
@@ -4433,7 +4518,7 @@ var Instrument = (function() {
         if (key in given) {
           timbre[key] = given[key];
         } else {
-          timbre[key] = defaulTimbre[key];
+          timbre[key] = defaultTimbre[key];
         }
       }
     }
@@ -4583,150 +4668,399 @@ var Instrument = (function() {
     }
   };
 
-  // Parses an ABC file to an object with the following structure:
-  // {
-  //   X: value from the X: lines in header (\n separated for multiple values)
-  //   V: value from the V:myname lines that appear before K:
-  //   (etc): for all the one-letter header-names.
-  //   K: value from the K: lines in header.
-  //   tempo: Q: line parsed as beatsecs
-  //   timbre: ... I:timbre line as parsed by makeTimbre
-  //   voice: {
-  //     myname: { // voice with id "myname"
-  //       V: value from the V:myname lines (from the body)
-  //       stems: [...] as parsed by parseABCstems
-  //    }
-  //  }
-  // }
-  // ABC files are idiosyncratic to parse: the written specifications
-  // do not necessarily reflect the defacto standard implemented by
-  // ABC content on the web.  This implementation is designed to be
-  // practical, working on content as it appears on the web, and only
-  // using the written standard as a guideline.
-  var ABCheader = /^([A-Za-z]):\s*(.*)$/;
-  function parseABCFile(str) {
-    var lines = str.split('\n'),
-        result = {
-          voice: {}
-        },
-        context = result, timbre,
-        j, k, header, stems, key = {}, accent = {}, voiceid, out;
-    // Shifts context to a voice with the given id given.  If no id
-    // given, then just sticks with the current voice.  If the current
-    // voice is unnamed and empty, renames the current voice.
-    function startVoiceContext(id) {
-      id = id || '';
-      if (!id && context !== result) {
-        return;
-      }
-      if (result.voice.hasOwnProperty(id)) {
-        // Resume a named voice.
-        context = result.voice[id];
-        accent = context.accent;
-      } else {
-        // Start a new voice.
-        context = { id: id, accent: { slurred: 0 } };
-        result.voice[id] = context;
-        accent = context.accent;
-      }
+
+  // The default sound is a square wave with a pretty quick decay to zero.
+  var defaultTimbre = Instrument.defaultTimbre = {
+    wave: 'square',   // Oscillator type.
+    gain: 0.1,        // Overall gain at maximum attack.
+    attack: 0.002,    // Attack time at the beginning of a tone.
+    decay: 0.4,       // Rate of exponential decay after attack.
+    decayfollow: 0,   // Amount of decay shortening for higher notes.
+    sustain: 0,       // Portion of gain to sustain indefinitely.
+    release: 0.1,     // Release time after a tone is done.
+    cutoff: 0,        // Low-pass filter cutoff frequency.
+    cutfollow: 0,     // Cutoff adjustment, a multiple of oscillator freq.
+    resonance: 0,     // Low-pass filter resonance.
+    detune: 0         // Detune factor for a second oscillator.
+  };
+
+  // Norrmalizes a timbre object by making a copy that has exactly
+  // the right set of timbre fields, defaulting when needed.
+  // A timbre can specify any of the fields of defaultTimbre; any
+  // unspecified fields are treated as they are set in defaultTimbre.
+  function makeTimbre(options, atop) {
+    if (!options) {
+      options = {};
     }
-    // For picking a default voice, looks for the first voice name.
-    function firstVoiceName() {
-      if (result.V) {
-        return result.V.split(/\s+/)[0];
-      } else {
-        return '';
+    if (typeof(options) == 'string') {
+      // Abbreviation: name a wave to get a default timbre for that wave.
+      options = { wave: options };
+    }
+    var result = {}, key,
+        wt = atop && atop.wavetable && atop.wavetable[options.wave];
+    for (key in defaultTimbre) {
+      if (options.hasOwnProperty(key)) {
+        result[key] = options[key];
+      } else if (wt && wt.defs && wt.defs.hasOwnProperty(key)) {
+        result[key] = wt.defs[key];
+      } else{
+        result[key] = defaultTimbre[key];
       }
     }
-    // ABC files are parsed one line at a time.
-    for (j = 0; j < lines.length; ++j) {
-      // First, check to see if the line is a header line.
-      header = ABCheader.exec(lines[j]);
-      if (header) {
-        // The following headers are recognized and processed.
-        switch(header[1]) {
-          case 'V':
-            // A V: header switches voices if in the body.
-            // If in the header, then it is just advisory.
-            if (context !== result) {
-              startVoiceContext(header[2].split(' ')[0]);
+    return result;
+  }
+
+  function getWhiteNoiseBuf() {
+    var ac = getAudioTop().ac,
+      bufferSize = 2 * ac.sampleRate,
+      whiteNoiseBuf = ac.createBuffer(1, bufferSize, ac.sampleRate),
+      output = whiteNoiseBuf.getChannelData(0);
+    for (var i = 0; i < bufferSize; i++) {
+      output[i] = Math.random() * 2 - 1;
+    }
+    return whiteNoiseBuf;
+  }
+
+  // This utility function creates an oscillator at the given frequency
+  // and the given wavename.  It supports lookups in a static wavetable,
+  // defined right below.
+  function makeOscillator(atop, wavename, freq) {
+    if (wavename == 'noise') {
+      var whiteNoise = atop.ac.createBufferSource();
+      whiteNoise.buffer = getWhiteNoiseBuf();
+      whiteNoise.loop = true;
+      return whiteNoise;
+    }
+    var wavetable = atop.wavetable, o = atop.ac.createOscillator(),
+        k, pwave, bwf, wf;
+    try {
+      if (wavetable.hasOwnProperty(wavename)) {
+        // Use a customized wavetable.
+        pwave = wavetable[wavename].wave;
+        if (wavetable[wavename].freq) {
+          bwf = 0;
+          // Look for a higher-frequency variant.
+          for (k in wavetable[wavename].freq) {
+            wf = Number(k);
+            if (freq > wf && wf > bwf) {
+              bwf = wf;
+              pwave = wavetable[wavename].freq[bwf];
             }
-            break;
-          case 'M':
-            parseMeter(header[2], context);
-            break;
-          case 'L':
-            parseUnitNote(header[2], context);
-            break;
-          case 'Q':
-            parseTempo(header[2], context);
-            break;
+          }
         }
-        // All headers (including unrecognized ones) are
-        // just accumulated as properties. Repeated header
-        // lines are accumulated as multiline properties.
-        if (context.hasOwnProperty(header[1])) {
-          context[header[1]] += '\n' + header[2];
+        if (!o.setPeriodicWave && o.setWaveTable) {
+          // The old API name: Safari 7 still uses this.
+          o.setWaveTable(pwave);
         } else {
-          context[header[1]] = header[2];
-        }
-        // The K header is special: it should be the last one
-        // before the voices and notes begin.
-        if (header[1] == 'K' && context === result) {
-          key = keysig(header[2]);
-          startVoiceContext(firstVoiceName());
+          // The new API name.
+          o.setPeriodicWave(pwave);
         }
-      } else if (/^\s*(?:%.*)?$/.test(lines[j])) {
-        // Skip blank and comment lines.
-        continue;
       } else {
-        // A non-blank non-header line should have notes.
-        voiceid = peekABCVoice(lines[j]);
-        if (voiceid) {
-          // If it declares a voice id, respect it.
-          startVoiceContext(voiceid);
-        } else {
-          // Otherwise, start a default voice.
-          if (context === result) {
-            startVoiceContext(firstVoiceName());
+        o.type = wavename;
+      }
+    } catch(e) {
+      if (window.console) { window.console.log(e); }
+      // If unrecognized, just use square.
+      // TODO: support "noise" or other wave shapes.
+      o.type = 'square';
+    }
+    o.frequency.value = freq;
+    return o;
+  }
+
+  // Accepts either an ABC pitch or a midi number and converts to midi.
+  Instrument.pitchToMidi = function(n) {
+    if (typeof(n) == 'string') { return pitchToMidi(n); }
+    return n;
+  }
+
+  // Accepts either an ABC pitch or a midi number and converts to ABC pitch.
+  Instrument.midiToPitch = function(n) {
+    if (typeof(n) == 'number') { return midiToPitch(n); }
+    return n;
+  }
+
+  return Instrument;
+})();
+
+// Parses an ABC file to an object with the following structure:
+// {
+//   X: value from the X: lines in header (\n separated for multiple values)
+//   V: value from the V:myname lines that appear before K:
+//   (etc): for all the one-letter header-names.
+//   K: value from the K: lines in header.
+//   tempo: Q: line parsed as beatsecs
+//   timbre: ... I:timbre line as parsed by makeTimbre
+//   voice: {
+//     myname: { // voice with id "myname"
+//       V: value from the V:myname lines (from the body)
+//       stems: [...] as parsed by parseABCstems
+//    }
+//  }
+// }
+// ABC files are idiosyncratic to parse: the written specifications
+// do not necessarily reflect the defacto standard implemented by
+// ABC content on the web.  This implementation is designed to be
+// practical, working on content as it appears on the web, and only
+// using the written standard as a guideline.
+var ABCheader = /^([A-Za-z]):\s*(.*)$/;
+var ABCtoken = /(?:\[[A-Za-z]:[^\]]*\])|\s+|%[^\n]*|![^\s!:|\[\]]*!|\+[^+|!]*\+|[_<>@^]?"[^"]*"|\[|\]|>+|<+|(?:(?:\^+|_+|=|)[A-Ga-g](?:,+|'+|))|\(\d+(?::\d+){0,2}|\d*\/\d+|\d+\/?|\/+|[xzXZ]|\[?\|\]?|:?\|:?|::|./g;
+function parseABCFile(str) {
+  var lines = str.split('\n'),
+      result = {},
+      context = result, timbre,
+      j, k, header, stems, key = {}, accent = { slurred: 0 }, voiceid, out;
+  // ABC files are parsed one line at a time.
+  for (j = 0; j < lines.length; ++j) {
+    // First, check to see if the line is a header line.
+    header = ABCheader.exec(lines[j]);
+    if (header) {
+      handleInformation(header[1], header[2].trim());
+    } else if (/^\s*(?:%.*)?$/.test(lines[j])) {
+      // Skip blank and comment lines.
+      continue;
+    } else {
+      // Parse the notes.
+      parseABCNotes(lines[j]);
+    }
+  }
+  var infer = ['unitnote', 'unitbeat', 'tempo'];
+  if (result.voice) {
+    out = [];
+    for (j in result.voice) {
+      if (result.voice[j].stems && result.voice[j].stems.length) {
+        // Calculate times for all the tied notes.  This happens at the end
+        // because in principle, the first note of a song could be tied all
+        // the way through to the last note.
+        processTies(result.voice[j].stems);
+        // Bring up inferred tempo values from voices if not specified
+        // in the header.
+        for (k = 0; k < infer.length; ++k) {
+          if (!(infer[k] in result) && (infer[k] in result.voice[j])) {
+            result[infer[k]] = result.voice[j][infer[k]];
           }
         }
-        // Parse the notes.
-        stems = parseABCNotes(lines[j], key, accent);
-        if (stems && stems.length) {
-          // Push the line of stems into the voice.
-          if (!('stems' in context)) { context.stems = []; }
-          context.stems.push.apply(context.stems, stems);
+        // Remove this internal state variable;
+        delete result.voice[j].accent;
+      } else {
+        out.push(j);
+      }
+    }
+    // Delete any voices that had no stems.
+    for (j = 0; j < out.length; ++j) {
+      delete result.voice[out[j]];
+    }
+  }
+  return result;
+
+
+  ////////////////////////////////////////////////////////////////////////
+  // Parsing helper functions below.
+  ////////////////////////////////////////////////////////////////////////
+
+
+  // Processes header fields such as V: voice, which may appear at the
+  // top of the ABC file, or in the ABC body in a [V:voice] directive.
+  function handleInformation(field, value) {
+    // The following headers are recognized and processed.
+    switch(field) {
+      case 'V':
+        // A V: header switches voices if in the body.
+        // If in the header, then it is just advisory.
+        if (context !== result) {
+          startVoiceContext(value.split(' ')[0]);
         }
+        break;
+      case 'M':
+        parseMeter(value, context);
+        break;
+      case 'L':
+        parseUnitNote(value, context);
+        break;
+      case 'Q':
+        parseTempo(value, context);
+        break;
+    }
+    // All headers (including unrecognized ones) are
+    // just accumulated as properties. Repeated header
+    // lines are accumulated as multiline properties.
+    if (context.hasOwnProperty(field)) {
+      context[field] += '\n' + value;
+    } else {
+      context[field] = value;
+    }
+    // The K header is special: it should be the last one
+    // before the voices and notes begin.
+    if (field == 'K') {
+      key = keysig(value);
+      if (context === result) {
+        startVoiceContext(firstVoiceName());
       }
     }
-    var infer = ['unitnote', 'unitbeat', 'tempo'];
-    if (result.voice) {
-      out = [];
-      for (j in result.voice) {
-        if (result.voice[j].stems && result.voice[j].stems.length) {
-          // Calculate times for all the tied notes.  This happens at the end
-          // because in principle, the first note of a song could be tied all
-          // the way through to the last note.
-          processTies(result.voice[j].stems);
-          // Bring up inferred tempo values from voices if not specified
-          // in the header.
-          for (k = 0; k < infer.length; ++k) {
-            if (!(infer[k] in result) && (infer[k] in result.voice[j])) {
-              result[infer[k]] = result.voice[j][infer[k]];
+  }
+
+  // Shifts context to a voice with the given id given.  If no id
+  // given, then just sticks with the current voice.  If the current
+  // voice is unnamed and empty, renames the current voice.
+  function startVoiceContext(id) {
+    id = id || '';
+    if (!id && context !== result) {
+      return;
+    }
+    if (!result.voice) {
+      result.voice = {};
+    }
+    if (result.voice.hasOwnProperty(id)) {
+      // Resume a named voice.
+      context = result.voice[id];
+      accent = context.accent;
+    } else {
+      // Start a new voice.
+      context = { id: id, accent: { slurred: 0 } };
+      result.voice[id] = context;
+      accent = context.accent;
+    }
+  }
+
+  // For picking a default voice, looks for the first voice name.
+  function firstVoiceName() {
+    if (result.V) {
+      return result.V.split(/\s+/)[0];
+    } else {
+      return '';
+    }
+  }
+
+  // Parses a single line of ABC notes (i.e., not a header line).
+  //
+  // We process an ABC song stream by dividing it into tokens, each of
+  // which is a pitch, duration, or special decoration symbol; then
+  // we process each decoration individually, and we process each
+  // stem as a group using parseStem.
+  // The structure of a single ABC note is something like this:
+  //
+  // NOTE -> STACCATO? PITCH DURATION? TIE?
+  //
+  // I.e., it always has a pitch, and it is prefixed by some optional
+  // decorations such as a (.) staccato marking, and it is suffixed by
+  // an optional duration and an optional tie (-) marking.
+  //
+  // A stem is either a note or a bracketed series of notes, followed
+  // by duration and tie.
+  //
+  // STEM -> NOTE   OR    '[' NOTE * ']' DURAITON? TIE?
+  //
+  // Then a song is just a sequence of stems interleaved with other
+  // decorations such as dynamics markings and measure delimiters.
+  function parseABCNotes(str) {
+    var tokens = str.match(ABCtoken), parsed = null,
+        index = 0, dotted = 0, beatlet = null, t;
+    if (!tokens) {
+      return null;
+    }
+    while (index < tokens.length) {
+      // Ignore %comments and !markings!
+      if (/^[\s%]/.test(tokens[index])) { index++; continue; }
+      // Handle inline [X:...] information fields
+      if (/^\[[A-Za-z]:[^\]]*\]$/.test(tokens[index])) {
+        handleInformation(
+          tokens[index].substring(1, 2),
+          tokens[index].substring(3, tokens[index].length - 1).trim()
+        );
+        index++;
+        continue;
+      }
+      // Handled dotted notation abbreviations.
+      if (//.test(tokens[index])) {
+        dotted = tokens[index++].length;
+        continue;
+      }
+      if (/^\(\d+(?::\d+)*/.test(tokens[index])) {
+        beatlet = parseBeatlet(tokens[index++]);
+        continue;
+      }
+      if (/^[!+].*[!+]$/.test(tokens[index])) {
+        parseDecoration(tokens[index++], accent);
+        continue;
+      }
+      if (/^.?".*"$/.test(tokens[index])) {
+        // Ignore double-quoted tokens (chords and general text annotations).
+        index++;
+        continue;
+      }
+      if (/^[()]$/.test(tokens[index])) {
+        if (tokens[index++] == '(') {
+          accent.slurred += 1;
+        } else {
+          accent.slurred -= 1;
+          if (accent.slurred <= 0) {
+            accent.slurred = 0;
+            if (context.stems && context.stems.length >= 1) {
+              // The last notes in a slur are not slurred.
+              slurStem(context.stems[context.stems.length - 1], false);
             }
           }
+        }
+        continue;
+      }
+      // Handle measure markings by clearing accidentals.
+      if (/\|/.test(tokens[index])) {
+        for (t in accent) {
+          if (t.length == 1) {
+            // Single-letter accent properties are note accidentals.
+            delete accent[t];
+          }
+        }
+        index++;
+        continue;
+      }
+      parsed = parseStem(tokens, index, key, accent);
+      // Skip unparsable bits
+      if (parsed === null) {
+        index++;
+        continue;
+      }
+      // Process a parsed stem.
+      if (beatlet) {
+        scaleStem(parsed.stem, beatlet.time);
+        beatlet.count -= 1;
+        if (!beatlet.count) {
+          beatlet = null;
+        }
+      }
+      // If syncopated with > or < notation, shift part of a beat
+      // between this stem and the previous one.
+      if (dotted && context.stems && context.stems.length) {
+        if (dotted > 0) {
+          t = (1 - Math.pow(0.5, dotted)) * parsed.stem.time;
         } else {
-          out.push(j);
+          t = (Math.pow(0.5, -dotted) - 1) *
+              context.stems[context.stems.length - 1].time;
         }
+        syncopateStem(context.stems[context.stems.length - 1], t);
+        syncopateStem(parsed.stem, -t);
+      }
+      dotted = 0;
+      // Slur all the notes contained within a strem.
+      if (accent.slurred) {
+        slurStem(parsed.stem, true);
       }
-      // Delete any voices that had no stems.
-      for (j = 0; j < out.length; ++j) {
-        delete result.voice[out[j]];
+      // Start a default voice if we're not in a voice yet.
+      if (context === result) {
+        startVoiceContext(firstVoiceName());
       }
+      if (!('stems' in context)) { context.stems = []; }
+      // Add the stem to the sequence of stems for this voice.
+      context.stems.push(parsed.stem);
+      // Advance the parsing index since a stem is multiple tokens.
+      index = parsed.index;
     }
-    return result;
   }
+
   // Parse M: lines.  "3/4" is 3/4 time and "C" is 4/4 (common) time.
   function parseMeter(mline, beatinfo) {
     var d = /^C/.test(mline) ? 4/4 : durationToTime(mline);
@@ -4811,7 +5145,7 @@ var Instrument = (function() {
   // Supports the whole range of scale systems listed in the ABC spec.
   function keysig(keyname) {
     if (!keyname) { return {}; }
-    var key, sigcodes = {
+    var kkey, sigcodes = {
       // Major
       'c#':7, 'f#':6, 'b':5, 'e':4, 'a':3, 'd':2, 'g':1, 'c':0,
       'f':-1, 'bb':-2, 'eb':-3, 'ab':-4, 'db':-5, 'gb':-6, 'cb':-7,
@@ -4843,19 +5177,19 @@ var Instrument = (function() {
     var scale = k.match(/maj|min|mix|dor|phr|lyd|loc|m/);
     if (scale) {
       if (scale == 'maj') {
-        key = k.substr(0, scale.index);
+        kkey = k.substr(0, scale.index);
       } else if (scale == 'min') {
-        key = k.substr(0, scale.index + 1);
+        kkey = k.substr(0, scale.index + 1);
       } else {
-        key = k.substr(0, scale.index + scale[0].length);
+        kkey = k.substr(0, scale.index + scale[0].length);
       }
     } else {
-      key = /^[a-g][#b]?/.exec(k) || '';
+      kkey = /^[a-g][#b]?/.exec(k) || '';
     }
-    var result = accidentals(sigcodes[key]);
-    var extras = keyname.substr(key.length).match(/(_+|=|\^+)[a-g]/ig);
+    var result = accidentals(sigcodes[kkey]);
+    var extras = keyname.substr(kkey.length).match(/(_+|=|\^+)[a-g]/ig);
     if (extras) {
-      for (j = 0; j < extras.length; ++j) {
+      for (var j = 0; j < extras.length; ++j) {
         var note = extras[j].charAt(extras[j].length - 1).toUpperCase();
         if (extras[j].charAt(0) == '=') {
           delete result[note];
@@ -4866,138 +5200,10 @@ var Instrument = (function() {
     }
     return result;
   }
-  // Peeks and looks for a prefix of the form [V:voiceid].
-  function peekABCVoice(line) {
-    var match = /^\[V:([^\]\s]*)\]/.exec(line);
-    if (!match) return null;
-    return match[1];
-  }
-  // Parses a single line of ABC notes (i.e., not a header line).
-  //
-  // We process an ABC song stream by dividing it into tokens, each of
-  // which is a pitch, duration, or special decoration symbol; then
-  // we process each decoration individually, and we process each
-  // stem as a group using parseStem.
-  // The structure of a single ABC note is something like this:
-  //
-  // NOTE -> STACCATO? PITCH DURATION? TIE?
-  //
-  // I.e., it always has a pitch, and it is prefixed by some optional
-  // decorations such as a (.) staccato marking, and it is suffixed by
-  // an optional duration and an optional tie (-) marking.
-  //
-  // A stem is either a note or a bracketed series of notes, followed
-  // by duration and tie.
-  //
-  // STEM -> NOTE   OR    '[' NOTE * ']' DURAITON? TIE?
-  //
-  // Then a song is just a sequence of stems interleaved with other
-  // decorations such as dynamics markings and measure delimiters.
-  var ABCtoken = /(?:^\[V:[^\]\s]*\])|\s+|%[^\n]*|![^\s!:|\[\]]*!|\+[^+|!]*\+|[_<>@^]?"[^"]*"|\[|\]|>+|<+|(?:(?:\^+|_+|=|)[A-Ga-g](?:,+|'+|))|\(\d+(?::\d+){0,2}|\d*\/\d+|\d+\/?|\/+|[xzXZ]|\[?\|\]?|:?\|:?|::|./g;
-  function parseABCNotes(str, key, accent) {
-    var tokens = str.match(ABCtoken), result = [], parsed = null,
-        index = 0, dotted = 0, beatlet = null, t;
-    if (!tokens) {
-      return null;
-    }
-    while (index < tokens.length) {
-      // Ignore %comments and !markings!
-      if (/^[\s%]/.test(tokens[index])) { index++; continue; }
-      if (/^\[V:\S*\]$/.test(tokens[index])) {
-        // Voice id from [V:id] is handled in peekABCVoice.
-        index++;
-        continue;
-      }
-      // Handled dotted notation abbreviations.
-      if (//.test(tokens[index])) {
-        dotted = tokens[index++].length;
-        continue;
-      }
-      if (/^\(\d+(?::\d+)*/.test(tokens[index])) {
-        beatlet = parseBeatlet(tokens[index++]);
-        continue;
-      }
-      if (/^[!+].*[!+]$/.test(tokens[index])) {
-        parseDecoration(tokens[index++], accent);
-        continue;
-      }
-      if (/^.?".*"$/.test(tokens[index])) {
-        // Ignore double-quoted tokens (chords and general text annotations).
-        index++;
-        continue;
-      }
-      if (/^[()]$/.test(tokens[index])) {
-        if (tokens[index++] == '(') {
-          accent.slurred += 1;
-        } else {
-          accent.slurred -= 1;
-          if (accent.slurred <= 0) {
-            accent.slurred = 0;
-            if (result.length >= 1) {
-              // The last notes in a slur are not slurred.
-              slurStem(result[result.length - 1], false);
-            }
-          }
-        }
-        continue;
-      }
-      // Handle measure markings by clearing accidentals.
-      if (/\|/.test(tokens[index])) {
-        for (t in accent) {
-          if (t.length == 1) {
-            // Single-letter accent properties are note accidentals.
-            delete accent[t];
-          }
-        }
-        index++;
-        continue;
-      }
-      parsed = parseStem(tokens, index, key, accent);
-      // Skip unparsable bits
-      if (parsed === null) {
-        index++;
-        continue;
-      }
-      // Process a parsed stem.
-      if (beatlet) {
-        scaleStem(parsed.stem, beatlet.time);
-        beatlet.count -= 1;
-        if (!beatlet.count) {
-          beatlet = null;
-        }
-      }
-      // If syncopated with > or < notation, shift part of a beat
-      // between this stem and the previous one.
-      if (dotted && result.length) {
-        if (dotted > 0) {
-          t = (1 - Math.pow(0.5, dotted)) * parsed.stem.time;
-        } else {
-          t = (Math.pow(0.5, -dotted) - 1) * result[result.length - 1].time;
-        }
-        syncopateStem(result[result.length - 1], t);
-        syncopateStem(parsed.stem, -t);
-      }
-      dotted = 0;
-      // Slur all the notes contained within a strem.
-      if (accent.slurred) {
-        slurStem(parsed.stem, true);
-      }
-      // Add the stem to the sequence of stems for this voice.
-      result.push(parsed.stem);
-      // Advance the parsing index since a stem is multiple tokens.
-      index = parsed.index;
-    }
-    return result;
-  }
   // Additively adjusts the beats for a stem and the contained notes.
   function syncopateStem(stem, t) {
     var j, note, stemtime = stem.time, newtime = stemtime + t;
     stem.time = newtime;
-    syncopateStem
     for (j = 0; j < stem.notes.length; ++j) {
       note = stem.notes[j];
       // Only adjust a note's duration if it matched the stem's duration.
@@ -5212,195 +5418,38 @@ var Instrument = (function() {
     if (m[1].length > 0) {
       // When there is an explicit accidental, then remember it for
       // the rest of the measure.
-      accent[letter] = m[1];
-      return stripNatural(pitch);
-    }
-    if (accent.hasOwnProperty(letter)) {
-      // Accidentals from this measure apply to unaccented notes.
-      return stripNatural(accent[letter] + m[2] + m[3]);
-    }
-    if (key.hasOwnProperty(letter)) {
-      // Key signatures apply by default.
-      return stripNatural(key[letter] + m[2] + m[3]);
-    }
-    return stripNatural(pitch);
-  }
-  // Converts a midi note number to a frequency in Hz.
-  function midiToFrequency(midi) {
-    return 440 * Math.pow(2, (midi - 69) / 12);
-  }
-  // Some constants.
-  var noteNum =
-      {C:0,D:2,E:4,F:5,G:7,A:9,B:11,c:12,d:14,e:16,f:17,g:19,a:21,b:23};
-  var accSym =
-      { '^':1, '': 0, '=':0, '_':-1 };
-  var noteName =
-      ['C', '^C', 'D', '_E', 'E', 'F', '^F', 'G', '_A', 'A', '_B', 'B',
-       'c', '^c', 'd', '_e', 'e', 'f', '^f', 'g', '_a', 'a', '_b', 'b'];
-  // Converts a frequency in Hz to the closest midi number.
-  function frequencyToMidi(freq) {
-    return Math.round(69 + Math.log(freq / 440) * 12 / Math.LN2);
-  }
-  // Converts an ABC pitch (such as "^G,,") to a midi note number.
-  function pitchToMidi(pitch) {
-    var m = /^(\^+|_+|=|)([A-Ga-g])([,']*)$/.exec(pitch);
-    if (!m) { return null; }
-    var octave = m[3].replace(/,/g, '').length - m[3].replace(/'/g, '').length;
-    var semitone =
-        noteNum[m[2]] + accSym[m[1].charAt(0)] * m[1].length + 12 * octave;
-    return semitone + 60; // 60 = midi code middle "C".
-  }
-  // Converts a midi number to an ABC notation pitch.
-  function midiToPitch(midi) {
-    var index = ((midi - 72) % 12);
-    if (midi > 60 || index != 0) { index += 12; }
-    var octaves = Math.round((midi - index - 60) / 12),
-        result = noteName[index];
-    while (octaves != 0) {
-      result += octaves > 0 ? "'" : ",";
-      octaves += octaves > 0 ? -1 : 1;
-    }
-    return result;
-  }
-  // Converts an ABC pitch to a frequency in Hz.
-  function pitchToFrequency(pitch) {
-    return midiToFrequency(pitchToMidi(pitch));
-  }
-  // Converts an ABC duration to a number (e.g., "/3"->0.333 or "11/2"->1.5).
-  function durationToTime(duration) {
-    var m = /^(\d*)(?:\/(\d*))?$|^(\/+)$/.exec(duration), n, d, i = 0, ilen;
-    if (!m) return;
-    if (m[3]) return Math.pow(0.5, m[3].length);
-    d = (m[2] ? parseFloat(m[2]) : /\//.test(duration) ? 2 : 1);
-    // Handle mixed frations:
-    ilen = 0;
-    n = (m[1] ? parseFloat(m[1]) : 1);
-    if (m[2]) {
-      while (ilen + 1 < m[1].length && n > d) {
-        ilen += 1
-        i = parseFloat(m[1].substring(0, ilen))
-        n = parseFloat(m[1].substring(ilen))
-      }
-    }
-    return i + (n / d);
-  }
-
-  // The default sound is a square wave with a pretty quick decay to zero.
-  var defaultTimbre = Instrument.defaultTimbre = {
-    wave: 'square',   // Oscillator type.
-    gain: 0.1,        // Overall gain at maximum attack.
-    attack: 0.002,    // Attack time at the beginning of a tone.
-    decay: 0.4,       // Rate of exponential decay after attack.
-    decayfollow: 0,   // Amount of decay shortening for higher notes.
-    sustain: 0,       // Portion of gain to sustain indefinitely.
-    release: 0.1,     // Release time after a tone is done.
-    cutoff: 0,        // Low-pass filter cutoff frequency.
-    cutfollow: 0,     // Cutoff adjustment, a multiple of oscillator freq.
-    resonance: 0,     // Low-pass filter resonance.
-    detune: 0         // Detune factor for a second oscillator.
-  };
-
-  // Norrmalizes a timbre object by making a copy that has exactly
-  // the right set of timbre fields, defaulting when needed.
-  // A timbre can specify any of the fields of defaultTimbre; any
-  // unspecified fields are treated as they are set in defaultTimbre.
-  function makeTimbre(options, atop) {
-    if (!options) {
-      options = {};
-    }
-    if (typeof(options) == 'string') {
-      // Abbreviation: name a wave to get a default timbre for that wave.
-      options = { wave: options };
+      accent[letter] = m[1];
+      return stripNatural(pitch);
     }
-    var result = {}, key,
-        wt = atop && atop.wavetable && atop.wavetable[options.wave];
-    for (key in defaultTimbre) {
-      if (options.hasOwnProperty(key)) {
-        result[key] = options[key];
-      } else if (wt && wt.defs && wt.defs.hasOwnProperty(key)) {
-        result[key] = wt.defs[key];
-      } else{
-        result[key] = defaultTimbre[key];
-      }
+    if (accent.hasOwnProperty(letter)) {
+      // Accidentals from this measure apply to unaccented notes.
+      return stripNatural(accent[letter] + m[2] + m[3]);
     }
-    return result;
-  }
-
-  var whiteNoiseBuf = null;
-  function getWhiteNoiseBuf() {
-    if (whiteNoiseBuf == null) {
-      var ac = getAudioTop().ac,
-          bufferSize = 2 * ac.sampleRate,
-          whiteNoiseBuf = ac.createBuffer(1, bufferSize, ac.sampleRate),
-          output = whiteNoiseBuf.getChannelData(0);
-      for (var i = 0; i < bufferSize; i++) {
-        output[i] = Math.random() * 2 - 1;
-      }
+    if (key.hasOwnProperty(letter)) {
+      // Key signatures apply by default.
+      return stripNatural(key[letter] + m[2] + m[3]);
     }
-    return whiteNoiseBuf;
+    return stripNatural(pitch);
   }
-
-  // This utility function creates an oscillator at the given frequency
-  // and the given wavename.  It supports lookups in a static wavetable,
-  // defined right below.
-  function makeOscillator(atop, wavename, freq) {
-    if (wavename == 'noise') {
-      var whiteNoise = atop.ac.createBufferSource();
-      whiteNoise.buffer = getWhiteNoiseBuf();
-      whiteNoise.loop = true;
-      return whiteNoise;
-    }
-    var wavetable = atop.wavetable, o = atop.ac.createOscillator(),
-        k, pwave, bwf, wf;
-    try {
-      if (wavetable.hasOwnProperty(wavename)) {
-        // Use a customized wavetable.
-        pwave = wavetable[wavename].wave;
-        if (wavetable[wavename].freq) {
-          bwf = 0;
-          // Look for a higher-frequency variant.
-          for (k in wavetable[wavename].freq) {
-            wf = Number(k);
-            if (freq > wf && wf > bwf) {
-              bwf = wf;
-              pwave = wavetable[wavename].freq[bwf];
-            }
-          }
-        }
-        if (!o.setPeriodicWave && o.setWaveTable) {
-          // The old API name: Safari 7 still uses this.
-          o.setWaveTable(pwave);
-        } else {
-          // The new API name.
-          o.setPeriodicWave(pwave);
-        }
-      } else {
-        o.type = wavename;
+  // Converts an ABC duration to a number (e.g., "/3"->0.333 or "11/2"->1.5).
+  function durationToTime(duration) {
+    var m = /^(\d*)(?:\/(\d*))?$|^(\/+)$/.exec(duration), n, d, i = 0, ilen;
+    if (!m) return;
+    if (m[3]) return Math.pow(0.5, m[3].length);
+    d = (m[2] ? parseFloat(m[2]) : /\//.test(duration) ? 2 : 1);
+    // Handle mixed frations:
+    ilen = 0;
+    n = (m[1] ? parseFloat(m[1]) : 1);
+    if (m[2]) {
+      while (ilen + 1 < m[1].length && n > d) {
+        ilen += 1
+        i = parseFloat(m[1].substring(0, ilen))
+        n = parseFloat(m[1].substring(ilen))
       }
-    } catch(e) {
-      if (window.console) { window.console.log(e); }
-      // If unrecognized, just use square.
-      // TODO: support "noise" or other wave shapes.
-      o.type = 'square';
     }
-    o.frequency.value = freq;
-    return o;
-  }
-
-  // Accepts either an ABC pitch or a midi number and converts to midi.
-  Instrument.pitchToMidi = function(n) {
-    if (typeof(n) == 'string') { return pitchToMidi(n); }
-    return n;
-  }
-
-  // Accepts either an ABC pitch or a midi number and converts to ABC pitch.
-  Instrument.midiToPitch = function(n) {
-    if (typeof(n) == 'number') { return midiToPitch(n); }
-    return n;
+    return i + (n / d);
   }
-
-  return Instrument;
-})();
+}
 
 // wavetable is a table of names for nonstandard waveforms.
 // The table maps names to objects that have wave: and freq:
@@ -5493,13 +5542,15 @@ function makeWavetable(ac) {
       // TODO: this approach attenuates low notes too much -
       // this should be fixed.
       defs: { wave: 'piano', gain: 0.5,
-              attack: 0.002, decay: 0.4, sustain: 0.005, release: 0.1,
+              attack: 0.002, decay: 0.25, sustain: 0.03, release: 0.1,
               decayfollow: 0.7,
-              cutoff: 800, cutfollow: 0.1, resonance: 1, detune: 1.001 }
+              cutoff: 800, cutfollow: 0.1, resonance: 1, detune: 0.9994 }
     }
   });
 }
 
+// End of musical.js copy.
+
 
 //////////////////////////////////////////////////////////////////////////
 // SYNC, REMOVE SUPPORT
@@ -5661,7 +5712,7 @@ function globalhelp(obj) {
     helpwrite('This is an unassigned value.');
     return helpok;
   }
-  if (obj === window) {
+  if (obj === global) {
     helpwrite('The global window object represents the browser window.');
     return helpok;
   }
@@ -5681,7 +5732,7 @@ function globalhelp(obj) {
   helplist = [];
   for (var name in helptable) {
     if (helptable[name].helptext && helptable[name].helptext.length &&
-        (!(name in window) || typeof(window[name]) == 'function')) {
+        (!(name in global) || typeof(global[name]) == 'function')) {
       helplist.push(name);
     }
   }
@@ -5712,6 +5763,7 @@ function canElementMoveInstantly(elem) {
   // moving at speed Infinity.
   var atime;
   return (elem && $.queue(elem).length == 0 &&
+      (!elem.parentElement || !elem.parentElement.style.transform) &&
       ((atime = animTime(elem)) === 0 || $.fx.speeds[atime] === 0));
 }
 
@@ -5895,6 +5947,9 @@ function wrapglobalcommand(name, helptext, fn, fnfilter) {
       cc.exit();
     }
     if (early) {
+      if (early.result && early.result.constructor === jQuery && global_turtle) {
+        sync(global_turtle, early.result);
+      }
       return early.result;
     }
   };
@@ -5910,7 +5965,7 @@ function wrapwindowevent(name, helptext) {
             : null;
     if (forKey) { focusWindowIfFirst(); }
     if (fn == null && typeof(d) == 'function') { fn = d; d = null; }
-    $(window).on(name + '.turtleevent', null, d, !filter ? fn : function(e) {
+    $(global).on(name + '.turtleevent', null, d, !filter ? fn : function(e) {
       if (interrupted) return;
       if ($(e.target).closest(filter).length) { return; }
       return fn.apply(this, arguments);
@@ -5919,7 +5974,7 @@ function wrapwindowevent(name, helptext) {
 }
 
 function windowhasturtleevent() {
-  var events = $._data(window, 'events');
+  var events = $._data(global, 'events');
   if (!events) return false;
   for (var type in events) {
     var entries = events[type];
@@ -5984,7 +6039,7 @@ function rtlt(cc, degrees, radius) {
               oldPos,
               oldTs.rot,
               oldTs.rot + (left ? -degrees : degrees),
-              newRadius * oldTs.sy,
+              newRadius * (state.oldscale ? oldTs.sy : 1),
               oldTransform);
           });
         })();
@@ -6029,7 +6084,7 @@ function fdbk(cc, amount) {
 // CARTESIAN MOVEMENT FUNCTIONS
 //////////////////////////////////////////////////////////////////////////
 
-function move(cc, x, y) {
+function slide(cc, x, y) {
   if ($.isArray(x)) {
     y = x[1];
     x = x[0];
@@ -6134,11 +6189,60 @@ function makejump(move) {
   });
 }
 
+//////////////////////////////////////////////////////////////////////////
+// SCALING FUNCTIONS
+// Support for old-fashioned scaling and new.
+//////////////////////////////////////////////////////////////////////////
+
+function elemOldScale(elem) {
+  var state = $.data(elem, 'turtleData');
+  return state && (state.oldscale != null) ? state.oldscale : 1;
+}
+
+function scaleCmd(cc, valx, valy) {
+  growImpl.call(this, true, cc, valx, valy);
+}
+
+function grow(cc, valx, valy) {
+  growImpl.call(this, false, cc, valx, valy);
+}
+
+function growImpl(oldscale, cc, valx, valy) {
+  if (valy === undefined) { valy = valx; }
+  // Disallow scaling to zero using this method.
+  if (!valx || !valy) { valx = valy = 1; }
+  var intick = insidetick;
+  this.plan(function(j, elem) {
+    if (oldscale) {
+      getTurtleData(elem).oldscale *= valy;
+    }
+    cc.appear(j);
+    if ($.isWindow(elem) || elem.nodeType === 9) {
+      cc.resolve(j);
+      return;
+    }
+    var c = $.map($.css(elem, 'turtleScale').split(' '), parseFloat);
+    if (c.length === 1) { c.push(c[0]); }
+    c[0] *= valx;
+    c[1] *= valy;
+    this.animate({turtleScale: $.map(c, cssNum).join(' ')},
+          animTime(elem, intick), animEasing(elem), cc.resolver(j));
+  });
+  return this;
+}
+
 //////////////////////////////////////////////////////////////////////////
 // DOT AND BOX FUNCTIONS
 // Support for animated drawing of dots and boxes.
 //////////////////////////////////////////////////////////////////////////
 
+function drawingScale(elem, oldscale) {
+  var totalParentTransform = totalTransform2x2(elem.parentElement),
+      simple = isone2x2(totalParentTransform),
+      scale = simple ? 1 : decomposeSVD(totalParentTransform)[1];
+  return scale * elemOldScale(elem);
+}
+
 function animatedDotCommand(fillShape) {
   var intick = insidetick;
   return (function(cc, style, diameter) {
@@ -6162,8 +6266,8 @@ function animatedDotCommand(fillShape) {
           ts = readTurtleTransform(elem, true),
           ps = parsePenStyle(style, 'fillStyle'),
           drawOnCanvas = getDrawOnCanvas(state),
-          // Scale by sx.  (TODO: consider parent transforms.)
-          targetDiam = diameter * ts.sx,
+          sx = drawingScale(elem),
+          targetDiam = diameter * sx,
           animDiam = Math.max(0, targetDiam - 2),
           finalDiam = targetDiam + (ps.eraseMode ? 2 : 0),
           hasAlpha = /rgba|hsla/.test(ps.fillStyle);
@@ -6320,6 +6424,39 @@ function drawArrowLine(c, w, x0, y0, x1, y1) {
   drawArrowHead(c, m);
 }
 
+//////////////////////////////////////////////////////////////////////////
+// VOICE SYNTHESIS
+// Method for uttering words.
+//////////////////////////////////////////////////////////////////////////
+function utterSpeech(words, cb) {
+  var pollTimer = null;
+  function complete() {
+    if (pollTimer) { clearInterval(pollTimer); }
+    if (cb) { cb(); }
+  }
+  if (!global.speechSynthesis) {
+    console.log('No speech synthesis: ' + words);
+    complete();
+    return;
+  }
+  try {
+    var msg = new global.SpeechSynthesisUtterance(words);
+    msg.addEventListener('end', complete);
+    msg.addEventListener('error', complete);
+    msg.lang = navigator.language || 'en-GB';
+    global.speechSynthesis.speak(msg);
+    pollTimer = setInterval(function() {
+      // Chrome speech synthesis fails to deliver an 'end' event
+      // sometimes, so we also poll every 250ms.
+      if (global.speechSynthesis.pending || global.speechSynthesis.speaking) return;
+      complete();
+    }, 250);
+  } catch (e) {
+    if (global.console) { global.console.log(e); }
+    complete();
+  }
+}
+
 //////////////////////////////////////////////////////////////////////////
 // TURTLE FUNCTIONS
 // Turtle methods to be registered as jquery instance methods.
@@ -6342,9 +6479,9 @@ var turtlefn = {
   bk: wrapcommand('bk', 1,
   ["bk(pixels) Back. Moves in reverse by some pixels: " +
       "bk 100"], fdbk),
-  move: wrapcommand('move', 1,
+  slide: wrapcommand('slide', 1,
   ["move(x, y) Slides right x and forward y pixels without turning: " +
-      "move 50, 100"], move),
+      "slide 50, 100"], slide),
   movexy: wrapcommand('movexy', 1,
   ["movexy(x, y) Changes graphing coordinates by x and y: " +
       "movexy 50, 100"], movexy),
@@ -6355,8 +6492,8 @@ var turtlefn = {
       "or an object on the page (see pagexy): " +
       "moveto lastmousemove"], moveto),
   jump: wrapcommand('jump', 1,
-  ["jump(x, y) Move without drawing (compare to move): " +
-      "jump 0, 50"], makejump(move)),
+  ["jump(x, y) Move without drawing (compare to slide): " +
+      "jump 0, 50"], makejump(slide)),
   jumpxy: wrapcommand('jumpxy', 1,
   ["jumpxy(x, y) Move without drawing (compare to movexy): " +
       "jump 0, 50"], makejump(movexy)),
@@ -6411,7 +6548,7 @@ var turtlefn = {
       if (!nlocalxy) {
         nlocalxy = computePositionAsLocalOffset(elem, targetpos);
       }
-      dir = radiansToDegrees(Math.atan2(-nlocalxy[0], -nlocalxy[1]));
+      var dir = radiansToDegrees(Math.atan2(-nlocalxy[0], -nlocalxy[1]));
       ts = readTurtleTransform(elem, true);
       if (!(limit === null)) {
         r = convertToRadians(ts.rot);
@@ -6450,6 +6587,52 @@ var turtlefn = {
     });
     return this;
   }),
+  copy: wrapcommand('copy', 0,
+  ["copy() makes a new turtle that is a copy of this turtle."],
+  function copy(cc) {
+    var t2 =  this.clone().insertAfter(this);
+    t2.hide();
+    // t2.plan doesn't work here.
+    this.plan(function(j, elem) {
+      cc.appear(j);
+
+      //copy over turtle data:
+      var olddata = getTurtleData(this);
+      var newdata = getTurtleData(t2);
+      for (var k in olddata) { newdata[k] = olddata[k]; }
+
+      // copy over style attributes:
+      t2.attr('style', this.attr('style'));
+
+      // copy each thing listed in css hooks:
+      for (var property in $.cssHooks) {
+        var value = this.css(property);
+        t2.css(property, value);
+      }
+
+      // copy attributes, just in case:
+      var attrs = this.prop("attributes");
+      for (var i in attrs) {
+        t2.attr(attrs[i].name, attrs[i].value);
+      }
+
+      // copy the canvas:
+      var t2canvas = t2.canvas();
+      var tcanvas = this.canvas();
+      if (t2canvas && tcanvas) {
+        t2canvas.width = tcanvas.width;
+        t2canvas.height = tcanvas.height;
+        var newCanvasContext = t2canvas.getContext('2d');
+        newCanvasContext.drawImage(tcanvas, 0, 0)
+      }
+
+      t2.show();
+
+      cc.resolve(j);
+    }); // pass in our current clone, otherwise things apply to the wrong clone
+    sync(t2, this);
+    return t2;
+  }),
   pen: wrapcommand('pen', 1,
   ["pen(color, size) Selects a pen. " +
       "Chooses a color and/or size for the pen: " +
@@ -6615,27 +6798,10 @@ var turtlefn = {
   }),
   scale: wrapcommand('scale', 1,
   ["scale(factor) Scales all motion up or down by a factor. " +
-      "To double all drawing: scale(2)"],
-  function scale(cc, valx, valy) {
-    if (valy === undefined) { valy = valx; }
-    // Disallow scaling to zero using this method.
-    if (!valx || !valy) { valx = valy = 1; }
-    var intick = insidetick;
-    this.plan(function(j, elem) {
-      cc.appear(j);
-      if ($.isWindow(elem) || elem.nodeType === 9) {
-        cc.resolve(j);
-        return;
-      }
-      var c = $.map($.css(elem, 'turtleScale').split(' '), parseFloat);
-      if (c.length === 1) { c.push(c[0]); }
-      c[0] *= valx;
-      c[1] *= valy;
-      this.animate({turtleScale: $.map(c, cssNum).join(' ')},
-            animTime(elem, intick), animEasing(elem), cc.resolver(j));
-    });
-    return this;
-  }),
+      "To double all drawing: scale(2)"], scaleCmd),
+  grow: wrapcommand('grow', 1,
+  ["grow(factor) Changes the size of the element by a factor. " +
+      "To double the size: grow(2)"], grow),
   pause: wrapcommand('pause', 1,
   ["pause(seconds) Pauses some seconds before proceeding. " +
       "fd 100; pause 2.5; bk 100",
@@ -6740,34 +6906,13 @@ var turtlefn = {
     this.plan(function(j, elem) {
       cc.appear(j);
       this.queue(function(next) {
-        var finished = false,
-            pollTimer = null,
-            complete = function() {
-          if (finished) return;
-          clearInterval(pollTimer);
-          finished = true;
+        utterSpeech(words, function() {
           cc.resolve(j);
           next();
-        };
-        try {
-          var msg = new SpeechSynthesisUtterance(words);
-          msg.addEventListener('end', complete);
-          msg.addEventListener('error', complete);
-          speechSynthesis.speak(msg);
-          pollTimer = setInterval(function() {
-            // Chrome speech synthesis fails to deliver an 'end' event
-            // sometimes, so we also poll every 250ms.
-            if (speechSynthesis.pending || speechSynthesis.speaking) return;
-            complete();
-          }, 250);
-        } catch (e) {
-          console.log(e);
-          complete();
-        }
+        });
       });
     });
     return this;
-
   }),
   play: wrapcommand('play', 1,
   ["play(notes) Play notes. Notes are specified in " +
@@ -6924,10 +7069,7 @@ var turtlefn = {
     return this.plan(function(j, elem) {
       cc.appear(j);
       var state = getTurtleData(elem);
-      if (state.drawOnCanvas) {
-        sync(elem, state.drawOnCanvas);
-      }
-      if (!canvas || canvas === window) {
+      if (!canvas || canvas === global) {
         state.drawOnCanvas = null;
       } else if (canvas.jquery && $.isFunction(canvas.canvas)) {
         state.drawOnCanvas = canvas.canvas();
@@ -6966,13 +7108,13 @@ var turtlefn = {
     var intick = insidetick;
     return this.plan(function(j, elem) {
       cc.appear(j);
-      var applyStyles = {padding: 8},
+      var applyStyles = {},
           currentStyles = this.prop('style');
       // For defaults, copy inline styles of the turtle itself except for
       // properties in the following list (these are the properties used to
       // make the turtle look like a turtle).
-      for (var j = 0; j < currentStyles.length; ++j) {
-        var styleProperty = currentStyles[j];
+      for (var j2 = 0; j2 < currentStyles.length; ++j2) {
+        var styleProperty = currentStyles[j2];
         if (/^(?:width|height|opacity|background-image|background-size)$/.test(
           styleProperty) || /transform/.test(styleProperty)) {
           continue;
@@ -6991,6 +7133,10 @@ var turtlefn = {
       // Place the label on the screen using the figured styles.
       var out = prepareOutput(html, 'label').result.css(applyStyles)
           .addClass('turtlelabel').appendTo(getTurtleField());
+      // If the output has a turtleinput, then forward mouse events.
+      if (out.hasClass('turtleinput') || out.find('.turtleinput').length) {
+        mouseSetupHook.apply(out.get(0));
+      }
       if (styles && 'id' in styles) {
         out.attr('id', styles.id);
       }
@@ -7034,7 +7180,7 @@ var turtlefn = {
     this.plan(function(j, elem) {
       cc.appear(j);
       if ($.isWindow(elem) || elem.nodeType === 9) {
-        window.location.reload();
+        global.location.reload();
         cc.resolve(j);
         return;
       }
@@ -7175,7 +7321,7 @@ var turtlefn = {
       if (typeof val != 'object' ||
           !$.isNumeric(val.width) || !$.isNumeric(val.height) ||
           !($.isArray(val.data) || val.data instanceof Uint8ClampedArray ||
-            val.data instanceof Unit8Array)) {
+            val.data instanceof Uint8Array)) {
         return;
       }
       var imdat = ctx.createImageData(
@@ -7386,7 +7532,6 @@ var turtlefn = {
         callback.apply(this, arguments);
       }
     });
-    sync = null;
   }),
   plan: wrapraw('plan',
   ["plan(fn) Runs fn in the animation queue. For planning logic: " +
@@ -7442,6 +7587,33 @@ var turtlefn = {
   })
 };
 
+//////////////////////////////////////////////////////////////////////////
+// QUEUING SUPPORT
+//////////////////////////////////////////////////////////////////////////
+
+function queueShowHideToggle() {
+  $.each(['toggle', 'show', 'hide'], function(i, name) {
+
+
+    var builtInFn = $.fn[name];
+    // Change show/hide/toggle to queue their behavior by default.
+    // Since animating show/hide will call the zero-argument
+    // form synchronously at the end of animation, we avoid
+    // infinite recursion by examining jQuery's internal fxshow
+    // state and avoiding the recursion if the animation is calling
+    // show/hide.
+    $.fn[name] = function(speed, easing, callback) {
+      var a = arguments;
+      // TODO: file a bug in jQuery to allow solving this without _data.
+      if (!a.length && this.hasClass('turtle') &&
+          (this.length > 1 || !$._data(this[0], 'fxshow'))) {
+        a = [0];
+      }
+      builtInFn.apply(this, a);
+    }
+  });
+}
+
 // If the queue for an image is empty, starts by queuing a wait-for-load.
 function queueWaitIfLoadingImg(img, qname) {
   if (!qname) qname = 'fx';
@@ -7456,6 +7628,10 @@ function queueWaitIfLoadingImg(img, qname) {
   }
 }
 
+//////////////////////////////////////////////////////////////////////////
+// HUNG LOOP DETECTION
+//////////////////////////////////////////////////////////////////////////
+
 var warning_shown = {},
     loopCounter = 0,
     hungTimer = null,
@@ -7483,7 +7659,7 @@ function checkForHungLoop(fname) {
     }, 0);
     return;
   }
-  // Timeout after which we interrupt the program: 6 seconds.
+  // Timeout after which we interrupt the program.
   if (now - hangStartTime > $.turtle.hangtime) {
     if (see.visible()) {
       see.html('Oops: program ' +
@@ -7544,7 +7720,7 @@ function deprecate(map, oldname, newname) {
     __extends(map[oldname], map[newname]);
   }
 }
-deprecate(turtlefn, 'slide', 'move');
+deprecate(turtlefn, 'move', 'slide');
 deprecate(turtlefn, 'direct', 'plan');
 deprecate(turtlefn, 'enclosedby', 'inside');
 deprecate(turtlefn, 'bearing', 'direction');
@@ -7564,7 +7740,7 @@ $.fn.extend(turtlefn);
 // * Sets up a global "hatch" function to make a new turtle.
 //////////////////////////////////////////////////////////////////////////
 
-var turtleGIFUrl = "data:image/gif;base64,R0lGODlhKAAwAPIFAAAAAAFsOACSRTCuSICAgP///wAAAAAAACH5BAlkAAYAIf8LTkVUU0NBUEUyLjADAQAAACwAAAAAKAAwAAAD72i6zATEgBCAebHpzUnxhDAMAvhxKOoV3ziuZyo3RO26dTbvgXj/gsCO9ysOhENZz+gKJmcUkmA6PSKfSqrWieVtuU+KGNXbXofLEZgR/VHCgdua4isGz9mbmM6U7/94BmlyfUZ1fhqDhYuGgYqMkCOBgo+RfWsNlZZ3ewIpcZaIYaF6XaCkR6aokqqrk0qrqVinpK+fsbZkuK2ouRy0ob4bwJbCibthh6GYebGcY7/EsWqTbdNG1dd9jnXPyk2d38y0Z9Yub2yA6AvWPYk+zEnkv6xdCoPuw/X2gLqy9vJIGAN4b8pAgpQOIlzI8EkCACH5BAlkAAYALAAAAAAoADAAAAPuaLrMBMSAEIB5senNSfGEMAwC+HEo6hXfOK5nKjdE7bp1Nu+BeP+CwI73Kw6EQ1nP6AomZxSSYDo9Ip9KqtaJ5W25Xej3qqGYsdEfZbMcgZXtYpActzLMeLOP6c7f3nVNfEZ7TXSFg4lyZAYBio+LZYiQfHMbc3iTlG9ilGpdjp4ujESiI6RQpqegqkesqqhKrbEpoaa0KLaiuBy6nrxss6+3w7tomo+cDXmBnsoLza2nsb7SN2tl1nyozVOZTJhxysxnd9XYCrrAtT7KQaPruavBo2HQ8xrvffaN+GV5/JbE45fOG8Ek5Q4qXHgwAQA7"
+var turtleGIFUrl = "data:image/gif;base64,R0lGODlhKAAwAPIFAAAAAAFsOACSRTCuSICAgP///wAAAAAAACH5BAlkAAYAIf8LTkVUU0NBUEUyLjADAQAAACwAAAAAKAAwAAAD72i6zATEgBCAebHpzUnxhDAMAvhxKOoV3ziuZyo3RO26dTbvgXj/gsCO9ysOhENZz+gKJmcUkmA6PSKfSqrWieVtuU+KGNXbXofLEZgR/VHCgdua4isGz9mbmM6U7/94BmlyfUZ1fhqDhYuGgYqMkCOBgo+RfWsNlZZ3ewIpcZaIYaF6XaCkR6aokqqrk0qrqVinpK+fsbZkuK2ouRy0ob4bwJbCibthh6GYebGcY7/EsWqTbdNG1dd9jnXPyk2d38y0Z9Yub2yA6AvWPYk+zEnkv6xdCoPuw/X2gLqy9vJIGAN4b8pAgpQOIlzI8EkCACH5BAlkAAYALAAAAAAoADAAAAPuaLrMBMSAEIB5senNSfGEMAwC+HEo6hXfOK5nKjdE7bp1Nu+BeP+CwI73Kw6EQ1nP6AomZxSSYDo9Ip9KqtaJ5W25Xej3qqGYsdEfZbMcgZXtYpActzLMeLOP6c7f3nVNfEZ7TXSFg4lyZAYBio+LZYiQfHMbc3iTlG9ilGpdjp4ujESiI6RQpqegqkesqqhKrbEpoaa0KLaiuBy6nrxss6+3w7tomo+cDXmBnsoLza2nsb7SN2tl1nyozVOZTJhxysxnd9XYCrrAtT7KQaPruavBo2HQ8xrvffaN+GV5/JbE45fOG8Ek5Q4qXHgwAQA7";
 
 var eventfn = { click:1, dblclick:1, mouseup:1, mousedown:1, mousemove:1 };
 
@@ -7611,7 +7787,7 @@ var dollar_turtle_methods = {
     // Disable all input.
     $('.turtleinput').prop('disabled', true);
     // Detach all event handlers on the window.
-    $(window).off('.turtleevent');
+    $(global).off('.turtleevent');
     // Low-level detach all jQuery events
     $('*').not('#_testpanel *').map(
        function(i, e) { $._data(e, 'events', null) });
@@ -7694,6 +7870,20 @@ var dollar_turtle_methods = {
   function globalspeed(mps) {
     globaldefaultspeed(mps);
   }),
+  say: wrapraw('say',
+  ["say(words) Say something. Use English words." +
+      "say \"Let's go!\""],
+  function say(words) {
+    if (global_turtle) {
+      var sel = $(global_turtle);
+      sel.say.call(sel, words);
+    } else {
+      var cc = setupContinuation(null, 'say', arguments, 0);
+      cc.appear(null);
+      utterSpeech(words, function() { cc.resolve(null); });
+      cc.exit();
+    }
+  }),
   play: wrapraw('play',
   ["play(notes) Play notes. Notes are specified in " +
       "" +
@@ -7725,7 +7915,7 @@ var dollar_turtle_methods = {
       sel.tone.apply(sel, arguments);
     } else {
       var instrument = getGlobalInstrument();
-      instrument.play.apply(instrument, args);
+      instrument.play.apply(instrument);
     }
   }),
   silence: wrapraw('silence',
@@ -7764,7 +7954,6 @@ var dollar_turtle_methods = {
         callback.apply(this, arguments);
       }
     });
-    sync = null;
   }),
   load: wrapraw('load',
   ["load(url, cb) Loads data from the url and passes it to cb. " +
@@ -7773,11 +7962,16 @@ var dollar_turtle_methods = {
     var val;
     $.ajax(apiUrl(url, 'load'), { async: !!cb, complete: function(xhr) {
       try {
-        val = JSON.parse(xhr.responseText);
+        val = xhr.responseObject = JSON.parse(xhr.responseText);
         if (typeof(val.data) == 'string' && typeof(val.file) == 'string') {
           val = val.data;
+          if (/\.json(?:$|\?|\#)/.test(url)) {
+            try { val = JSON.parse(val); } catch(e) {}
+          }
         } else if ($.isArray(val.list) && typeof(val.directory) == 'string') {
           val = val.list;
+        } else if (val.error) {
+          val = null;
         }
       } catch(e) {
         if (val == null && xhr && xhr.responseText) {
@@ -7795,7 +7989,11 @@ var dollar_turtle_methods = {
       "save 'intro', 'pen gold, 20\\nfd 100\\n'"],
   function(url, data, cb) {
     if (!url) throw new Error('Missing url for save');
-    var payload = { }, url = apiUrl(url, 'save'), key;
+    var payload = { }, key;
+    url = apiUrl(url, 'save');
+    if (/\.json(?:$|\?|\#)/.test(url)) {
+      data = JSON.stringify(data, null, 2);
+    }
     if (typeof(data) == 'string' || typeof(data) == 'number') {
       payload.data = data;
     } else {
@@ -7840,6 +8038,17 @@ var dollar_turtle_methods = {
   type: wrapglobalcommand('type',
   ["type(text) Types preformatted text like a typewriter. " +
       "type 'Hello!\n'"], plainTextPrint),
+  typebox: wrapglobalcommand('typebox',
+  ["typebox(clr) Draws a colored box as typewriter output. " +
+      "typebox red"], function(c, t) {
+    if (t == null && c != null && !isCSSColor(c)) { t = c; c = null; }
+    plainBoxPrint(c, t);
+  }),
+  typeline: wrapglobalcommand('typebox',
+  ["typeline() Same as type '\\n'. " +
+      "typeline()"], function(t) {
+    plainTextPrint((t || '') + '\n');
+  }),
   write: wrapglobalcommand('write',
   ["write(html) Writes a line of text. Arbitrary HTML may be written: " +
       "write 'Hello, world!'"], doOutput, function() {
@@ -7855,12 +8064,16 @@ var dollar_turtle_methods = {
   readnum: wrapglobalcommand('readnum',
   ["readnum(html, fn) Reads numeric input. Only numbers allowed: " +
       "readnum 'Amount?', (v) -> write 'Tip: ' + (0.15 * v)"],
-  doOutput, function readnum(a, b) { return prepareInput(a, b, 1); }),
+  doOutput, function readnum(a, b) { return prepareInput(a, b, 'number'); }),
   readstr: wrapglobalcommand('readstr',
   ["readstr(html, fn) Reads text input. Never " +
       "converts input to a number: " +
       "readstr 'Enter code', (v) -> write v.length + ' long'"],
-  doOutput, function readstr(a, b) { return prepareInput(a, b, -1); }),
+  doOutput, function readstr(a, b) { return prepareInput(a, b, 'text'); }),
+  listen: wrapglobalcommand('listen',
+  ["listen(html, fn) Reads voice input, if the browser supports it:" +
+      "listen 'Say something', (v) -> write v"],
+  doOutput, function readstr(a, b) { return prepareInput(a, b, 'voice'); }),
   menu: wrapglobalcommand('menu',
   ["menu(map) shows a menu of choices and calls a function " +
       "based on the user's choice: " +
@@ -7977,41 +8190,129 @@ var dollar_turtle_methods = {
   ["abs(x) The absolute value of x. " +
       "see abs -5"], Math.abs),
   acos: wrapraw('acos',
-  ["acos(degreees) Trigonometric arccosine, in degrees. " +
-      "see acos 0.5"],
-  function acos(x) { return roundEpsilon(Math.acos(x) * 180 / Math.PI); }
-  ),
+  ["acos(x) Trigonometric arccosine, in radians. " +
+      "see acos 0.5"], Math.acos),
   asin: wrapraw('asin',
-  ["asin(degreees) Trigonometric arcsine, in degrees. " +
-      "see asin 0.5"],
-  function asin(x) { return roundEpsilon(Math.asin(x) * 180 / Math.PI); }
-  ),
+  ["asin(y) Trigonometric arcsine, in radians. " +
+      "see asin 0.5"], Math.asin),
   atan: wrapraw('atan',
-  ["atan(degreees) Trigonometric arctangent, in degrees. " +
+  ["atan(y, x = 1) Trigonometric arctangent, in radians. " +
       "see atan 0.5"],
-  function atan(x) { return roundEpsilon(Math.atan(x) * 180 / Math.PI); }
+  function atan(y, x) { return Math.atan2(y, (x == undefined) ? 1 : x); }
   ),
-  atan2: wrapraw('atan2',
-  ["atan2(degreees) Trigonometric two-argument arctangent, " +
-      "in degrees. see atan -1, 0"],
-  function atan2(x, y) {
-    return roundEpsilon(Math.atan2(x, y) * 180 / Math.PI);
-  }),
   cos: wrapraw('cos',
-  ["cos(degreees) Trigonometric cosine, in degrees. " +
-      "see cos 45"],
-  function cos(x) { return roundEpsilon(Math.cos((x % 360) * Math.PI / 180)); }
-  ),
+  ["cos(radians) Trigonometric cosine, in radians. " +
+      "see cos 0"], Math.cos),
   sin: wrapraw('sin',
-  ["sin(degreees) Trigonometric sine, in degrees. " +
-      "see sin 45"],
-  function sin(x) { return roundEpsilon(Math.sin((x % 360) * Math.PI / 180)); }
-  ),
+  ["sin(radians) Trigonometric sine, in radians. " +
+      "see sin 0"], Math.sin),
   tan: wrapraw('tan',
-  ["tan(degreees) Trigonometric tangent, in degrees. " +
-      "see tan 45"],
-  function tan(x) { return roundEpsilon(Math.tan((x % 360) * Math.PI / 180)); }
-  ),
+  ["tan(radians) Trigonometric tangent, in radians. " +
+      "see tan 0"], Math.tan),
+
+  // For degree versions of trig functions, make sure we return exact
+  // results when possible. The set of values we have to consider is
+  // fortunately very limited. See "Rational Values of Trigonometric
+  // Functions." http://www.jstor.org/stable/2304540
+
+  acosd: wrapraw('acosd',
+  ["acosd(x) Trigonometric arccosine, in degrees. " +
+      "see acosd 0.5"],
+   function acosd(x) {
+     switch (x) {
+       case   1: return   0;
+       case  .5: return  60;
+       case   0: return  90;
+       case -.5: return 120;
+       case  -1: return 180;
+     }
+     return Math.acos(x) * 180 / Math.PI;
+  }),
+  asind: wrapraw('asind',
+  ["asind(x) Trigonometric arcsine, in degrees. " +
+      "see asind 0.5"],
+  function asind(x) {
+    switch (x) {
+      case   1: return  90;
+      case  .5: return  30;
+      case   0: return   0;
+      case -.5: return -30;
+      case  -1: return -90;
+    }
+    return Math.asin(x) * 180 / Math.PI;
+  }),
+  atand: wrapraw('atand',
+  ["atand(y, x = 1) Trigonometric arctangent, " +
+      "in degrees. see atand -1, 0/mark>"],
+  function atand(y, x) {
+    if (x == undefined) { x = 1; }
+    if (y == 0) {
+      return (x == 0) ? NaN : ((x > 0) ? 0 : 180);
+    } else if (x == 0) {
+      return (y > 0) ? Infinity : -Infinity;
+    } else if (Math.abs(y) == Math.abs(x)) {
+      return (y > 0) ? ((x > 0) ? 45 : 135) :
+                       ((x > 0) ? -45 : -135);
+    }
+    return Math.atan2(y, x) * 180 / Math.PI;
+  }),
+  cosd: wrapraw('cosd',
+  ["cosd(degrees) Trigonometric cosine, in degrees. " +
+      "see cosd 45"],
+  function cosd(x) {
+    x = modulo(x, 360);
+    if (x % 30 === 0) {
+      switch ((x < 0) ? x + 360 : x) {
+        case   0: return   1;
+        case  60: return  .5;
+        case  90: return   0;
+        case 120: return -.5;
+        case 180: return  -1;
+        case 240: return -.5;
+        case 270: return   0;
+        case 300: return  .5;
+      }
+    }
+    return Math.cos(x / 180 * Math.PI);
+  }),
+  sind: wrapraw('sind',
+  ["sind(degrees) Trigonometric sine, in degrees. " +
+      "see sind 45"],
+  function sind(x) {
+    x = modulo(x, 360);
+    if (x % 30 === 0) {
+      switch ((x < 0) ? x + 360 : x) {
+        case   0: return   0;
+        case  30: return  .5;
+        case  90: return   1;
+        case 150: return  .5;
+        case 180: return   0;
+        case 210: return -.5;
+        case 270: return  -1;
+        case 330: return -.5;
+      }
+    }
+    return Math.sin(x / 180 * Math.PI);
+  }),
+  tand: wrapraw('tand',
+  ["tand(degrees) Trigonometric tangent, in degrees. " +
+      "see tand 45"],
+  function tand(x) {
+    x = modulo(x, 360);
+    if (x % 45 === 0) {
+      switch ((x < 0) ? x + 360 : x) {
+        case   0: return 0;
+        case  45: return 1;
+        case  90: return Infinity;
+        case 135: return -1;
+        case 180: return 0;
+        case 225: return 1;
+        case 270: return -Infinity;
+        case 315: return -1
+      }
+    }
+    return Math.tan(x / 180 * Math.PI);
+  }),
   ceil: wrapraw('ceil',
   ["ceil(x) Round up. " +
       "see ceil 1.9"], Math.ceil),
@@ -8069,7 +8370,7 @@ var dollar_turtle_methods = {
   ["loadscript(url, callback) Loads Javascript or Coffeescript from " +
        "the given URL, calling callback when done."],
   function loadscript(url, callback) {
-    if (window.CoffeeScript && /\.(?:coffee|cs)$/.test(url)) {
+    if (global.CoffeeScript && /\.(?:coffee|cs)$/.test(url)) {
       CoffeeScript.load(url, callback);
     } else {
       $.getScript(url, callback);
@@ -8116,6 +8417,8 @@ function pollSendRecv(name) {
 
 deprecate(dollar_turtle_methods, 'defaultspeed', 'speed');
 
+dollar_turtle_methods.save.loginCookie = loginCookie;
+
 var helpok = {};
 
 var colors = [
@@ -8184,14 +8487,12 @@ var colors = [
 
 $.turtle = function turtle(id, options) {
   var exportedsee = false;
-  if (!arguments.length) {
-    id = 'turtle';
-  }
   if (arguments.length == 1 && typeof(id) == 'object' && id &&
       !id.hasOwnProperty('length')) {
     options = id;
     id = 'turtle';
   }
+  id = id || 'turtle';
   options = options || {};
   if ('turtle' in options) {
     id = options.turtle;
@@ -8209,34 +8510,40 @@ $.turtle = function turtle(id, options) {
   if (!globalDrawing.ctx && ('subpixel' in options)) {
     globalDrawing.subpixel = parseInt(options.subpixel);
   }
-  // Set up hung-browser timeout, default 10 seconds.
+  // Set up hung-browser timeout, default 20 seconds.
   $.turtle.hangtime = ('hangtime' in options) ?
-      parseFloat(options.hangtime) : 10000;
+      parseFloat(options.hangtime) : 20000;
 
   // Set up global events.
-  if (!('events' in options) || options.events) {
+  if (options.events !== false) {
     turtleevents(options.eventprefix);
   }
-  if (!('pressed' in options) || options.pressed) {
+  if (options.pressed !== false) {
     addKeyEventHooks();
     pressedKey.enable(true);
   }
   // Set up global log function.
-  if (!('see' in options) || options.see) {
+  if (options.see !== false) {
     exportsee();
     exportedsee = true;
-    if (window.addEventListener) {
-      window.addEventListener('error', see);
+    if (global.addEventListener) {
+      global.addEventListener('error', see);
     } else {
-      window.onerror = see;
+      global.onerror = see;
     }
     // Set up an alias.
-    window.log = see;
+    global.debug = see;
+    // 'debug' should be used now instead of log
+    deprecate(global, 'log', 'debug');
   }
+  if (options.queuehide !== false) {
+    queueShowHideToggle();
+  }
+
   // Copy $.turtle.* functions into global namespace.
-  if (!('functions' in options) || options.functions) {
-    window.printpage = window.print;
-    $.extend(window, dollar_turtle_methods);
+  if (options.functions !== false) {
+    global.printpage = global.print;
+    $.extend(global, dollar_turtle_methods);
   }
   // Set default turtle speed
   globaldefaultspeed(('defaultspeed' in options) ?
@@ -8261,11 +8568,11 @@ $.turtle = function turtle(id, options) {
   }
   if (selector && !selector.length) { selector = null; }
   // Globalize selected jQuery methods of a singleton turtle.
-  if (selector && selector.length === 1 &&
-      (!('global' in options) || options.global)) {
+  if (selector && selector.length === 1 && (options.global !== false)) {
     var extraturtlefn = {
       css:1, fadeIn:1, fadeOut:1, fadeTo:1, fadeToggle:1,
-      animate:1, toggle:1, finish:1, promise:1, direct:1 };
+      animate:1, toggle:1, finish:1, promise:1, direct:1,
+      show:1, hide:1 };
     var globalfn = $.extend({}, turtlefn, extraturtlefn);
     global_turtle_methods.push.apply(global_turtle_methods,
        globalizeMethods(selector, globalfn));
@@ -8274,14 +8581,14 @@ $.turtle = function turtle(id, options) {
     selector.css({zIndex: 1});
   }
   // Set up global objects by id.
-  if (!('ids' in options) || options.ids) {
+  if (options.ids !== false) {
     turtleids(options.idprefix);
     if (selector && id) {
-      window[id] = selector;
+      global[id] = selector;
     }
   }
   // Set up test console.
-  if (!('panel' in options) || options.panel) {
+  if (options.panel !== false) {
     var seeopt = {
       title: 'test panel (type help for help)',
       abbreviate: [undefined, helpok],
@@ -8295,24 +8602,20 @@ $.turtle = function turtle(id, options) {
       seeopt.height = options.panelheight;
     }
     see.init(seeopt);
-    if (wrotebody) {
-       /*
-       see.html('Turtle script should be inside body ' +
-                '- wrote a <body>');
-       */
-    }
     // Return an eval loop hook string if 'see' is exported.
     if (exportedsee) {
-      if (window.CoffeeScript) {
+      if (global.CoffeeScript) {
         return "see.init(eval(see.cs))";
       } else {
         return see.here;
       }
     }
   }
+  return $('#' + id);
 };
 
 $.extend($.turtle, dollar_turtle_methods);
+$.turtle.colors = colors;
 
 function seehelphook(text, result) {
   // Also, check the command to look for (non-CoffeeScript) help requests.
@@ -8346,9 +8649,9 @@ function copyhelp(method, fname, extrahelp, globalfn) {
 function globalizeMethods(thisobj, fnames) {
   var replaced = [];
   for (var fname in fnames) {
-    if (fnames.hasOwnProperty(fname) && !(fname in window)) {
+    if (fnames.hasOwnProperty(fname) && !(fname in global)) {
       replaced.push(fname);
-      window[fname] = (function(fname) {
+      global[fname] = (function(fname) {
         var method = thisobj[fname], target = thisobj;
         return copyhelp(method, fname, extrahelp,
             (function globalized() { /* Use parentheses to call a function */
@@ -8362,7 +8665,7 @@ function globalizeMethods(thisobj, fnames) {
 function clearGlobalTurtle() {
   global_turtle = null;
   for (var j = 0; j < global_turtle_methods.length; ++j) {
-    delete window[global_turtle_methods[j]];
+    delete global[global_turtle_methods[j]];
   }
   global_turtle_methods.length = 0;
 }
@@ -8377,10 +8680,10 @@ $.cleanData = function(elems) {
       state.stream.stop();
     }
     // Undefine global variablelem.
-    if (elem.id && window[elem.id] && window[elem.id].jquery &&
-        window[elem.id].length === 1 &&
-        window[elem.id][0] === elem) {
-      delete window[elem.id];
+    if (elem.id && global[elem.id] && global[elem.id].jquery &&
+        global[elem.id].length === 1 &&
+        global[elem.id][0] === elem) {
+      delete global[elem.id];
     }
     // Clear global turtlelem.
     if (elem === global_turtle) {
@@ -8672,7 +8975,9 @@ function createRectangleShape(width, height, subpixels) {
     subpixels = 1;
   }
   return (function(color) {
-    var c = getOffscreenCanvas(width, height);
+    var c = document.createElement('canvas');
+    c.width = width;
+    c.height = height;
     var ctx = c.getContext('2d');
     if (!color) {
       color = "rgba(128,128,128,0.125)";
@@ -8694,7 +8999,7 @@ function createRectangleShape(width, height, subpixels) {
       css.imageRendering = 'pixelated';
     }
     return {
-      url: c.toDataURL(),
+      img: c,
       css: css
     };
   });
@@ -8739,7 +9044,7 @@ function nameToImg(name, defaultshape) {
     // Deal with unquoted "tan" and "dot".
     name = name.helpname || name.name;
   }
-  if (name.constructor === $) {
+  if (name.constructor === jQuery) {
     // Unwrap jquery objects.
     if (!name.length) { return null; }
     name = name.get(0);
@@ -8770,14 +9075,18 @@ function nameToImg(name, defaultshape) {
   if (shape) {
     return shape(color);
   }
+  // Default to '/img/' URLs if it doesn't match a well-known name.
+  if (!/\//.test(name)) {
+    name = imgUrl(name);
+  }
   // Parse URLs.
   if (/\//.test(name)) {
     var hostname = absoluteUrlObject(name).hostname;
     // Use proxy to load image if the image is offdomain but the page is on
     // a pencil host (with a proxy).
-    if (!isPencilHost(hostname) && isPencilHost(window.location.hostname)) {
-      name = window.location.protocol + '//' +
-             window.location.host + '/proxy/' + absoluteUrl(name);
+    if (!isPencilHost(hostname) && isPencilHost(global.location.hostname)) {
+      name = global.location.protocol + '//' +
+             global.location.host + '/proxy/' + absoluteUrl(name);
     }
     return {
       url: name,
@@ -8813,11 +9122,11 @@ function hatchone(name, container, defaultshape) {
 
   // Create an image element with the requested name.
   var result;
-  if (img) {
+  if (isTag) {
+    result = $(name);
+  } else if (img) {
     result = $('');
     applyImg(result, img);
-  } else if (isTag) {
-    result = $(name);
   } else {
     result = $('
' + escapeHtml(name) + '
'); } @@ -8861,8 +9170,8 @@ function hatchone(name, container, defaultshape) { if (isID) { result.attr('id', name); // Update global variable unless there is a conflict. - if (attaching_ids && !window.hasOwnProperty(name)) { - window[name] = result; + if (attaching_ids && !global.hasOwnProperty(name)) { + global[name] = result; } } // Move it to the center of the document and export the name as a global. @@ -8879,7 +9188,7 @@ function random(arg, arg2) { } return Math.floor(Math.random() * arg); } - if (typeof(arg) == 'object' && arg.length && arg.slice) { + if (typeof(arg) == 'object' && arg && arg.length && arg.slice) { return arg[Math.floor(Math.random() * arg.length)]; } if (arg == 'normal') { @@ -8906,6 +9215,9 @@ function random(arg, arg2) { if (arg == 'gray') { return 'hsl(0,0,' + Math.floor(Math.random() * 100) + '%)'; } + if (arg === true) { + return Math.random() >= 0.5; + } return Math.random(); } @@ -8980,11 +9292,11 @@ function globaltick(rps, fn) { rps = 1; } if (tickinterval) { - window.clearInterval(tickinterval); + global.clearInterval(tickinterval); tickinterval = null; } if (fn && rps) { - tickinterval = window.setInterval( + tickinterval = global.setInterval( function() { // Set default speed to Infinity within tick(). try { @@ -9013,7 +9325,7 @@ function turtleids(prefix) { prefix = ''; } $('[id]').each(function(j, item) { - window[prefix + item.id] = $('#' + item.id); + global[prefix + item.id] = $('#' + item.id); }); attaching_ids = true; } @@ -9026,32 +9338,31 @@ function turtleevents(prefix) { prefix = 'last'; } if (eventsaver) { - $(window).off($.map(eventfn, function(x,k) { return k; }).join(' '), + $(global).off($.map(eventfn, function(x,k) { return k; }).join(' '), eventsaver); } if (prefix || prefix === '') { eventsaver = (function(e) { // Keep the old instance if possible. - var names = [prefix + e.type], j, old, name, prop; + var names = [prefix + e.type], j; if ((e.originalEvent || e) instanceof MouseEvent) { names.push(prefix + 'mouse'); } for (j = 0; j < names.length; ++j) { - var name = names[j]; - old = window[name], prop; + var name = names[j], old = global[name], prop; if (old && old.__proto__ === e.__proto__) { for (prop in old) { if (old.hasOwnProperty(prop)) delete old[prop]; } for (prop in e) { if (e.hasOwnProperty(prop)) old[prop] = e[prop]; } } else { - window[name] = e; + global[name] = e; } } }); - window[prefix + 'mouse'] = new $.Event(); + global[prefix + 'mouse'] = new $.Event(); for (var k in eventfn) { - window[prefix + k] = new $.Event(); + global[prefix + k] = new $.Event(); } - $(window).on($.map(eventfn, function(x,k) { return k; }).join(' '), + $(global).on($.map(eventfn, function(x,k) { return k; }).join(' '), eventsaver); } } @@ -9063,15 +9374,15 @@ function turtleevents(prefix) { function autoScrollAfter(f) { var slop = 10, seen = autoScrollBottomSeen(), - stick = ($(window).height() + $(window).scrollTop() + slop >= + stick = ($(global).height() + $(global).scrollTop() + slop >= $('html').outerHeight(true)); f(); if (stick) { - var scrollPos = $(window).scrollTop(), + var scrollPos = $(global).scrollTop(), advancedScrollPos = Math.min(seen, - $('html').outerHeight(true) - $(window).height()); + $('html').outerHeight(true) - $(global).height()); if (advancedScrollPos > scrollPos) { - $(window).scrollTop(advancedScrollPos); + $(global).scrollTop(advancedScrollPos); } } } @@ -9090,7 +9401,7 @@ function autoScrollBottomSeen() { var offset = $('body').offset(); var doctop = offset ? offset.top : 8; autoScrollState.bottomSeen = Math.min( - $(window).height() + $(window).scrollTop(), + $(global).height() + $(global).scrollTop(), $('body').height() + doctop); } return autoScrollState.bottomSeen; @@ -9099,9 +9410,9 @@ function autoScrollBottomSeen() { // location after running the passed function. (E.g., to allow focusing // a control without autoscrolling.) function undoScrollAfter(f) { - var scrollPos = $(window).scrollTop(); + var scrollPos = $(global).scrollTop(); f(); - $(window).scrollTop(scrollPos); + $(global).scrollTop(scrollPos); } ////////////////////////////////////////////////////////////////////////// @@ -9111,20 +9422,47 @@ function undoScrollAfter(f) { ////////////////////////////////////////////////////////////////////////// // Simplify output of preformatted text inside a
.
+function getTrailingPre() {
+  var pre = document.body.lastChild;
+  if (!pre || pre.tagName != 'PRE') {
+    pre = document.createElement('pre');
+    document.body.appendChild(pre);
+  }
+  return pre;
+}
+
 function plainTextPrint() {
   var args = arguments;
   autoScrollAfter(function() {
-    var pre = document.body.lastChild;
-    if (!pre || pre.tagName != 'PRE') {
-      pre = document.createElement('pre');
-      document.body.appendChild(pre);
-    }
+    var pre = getTrailingPre();
     for (var j = 0; j < args.length; j++) {
       pre.appendChild(document.createTextNode(String(args[j])));
     }
   });
 }
 
+function plainBoxPrint(clr, text) {
+  var elem = $("
").css({ + display: 'inline-block', + verticalAlign: 'top', + textAlign: 'center', + height: '1.2em', + width: '1.2em', + maxWidth: '1.2em', + overflow: 'hidden' + }).appendTo(getTrailingPre()), finish = function() { + if (clr != null) { elem.css({background: clr}); } + if (text != null) { elem.text(text); } + }; + if (!global_turtle) { + finish(); + } else { + var turtle = $(global_turtle); + moveto.call(turtle, null, elem); + turtle.eq(0).plan(finish); + } +} + // Put this output on the screen. Called some time after prepareOutput // if the turtle is animating. function doOutput() { @@ -9144,6 +9482,8 @@ function prepareOutput(html, tag) { if (html === undefined || html === null) { // Make empty line when no arguments. return {result: $(prefix + '
' + suffix)}; + } else if (html.jquery || (html instanceof Element && (html = $(html)))) { + return {result: html}; } else { var wrapped = false, result = null; html = '' + html; @@ -9321,18 +9661,25 @@ function prepareButton(name, callback) { // for creating an input box with a label, for one-shot input ////////////////////////////////////////////////////////////////////////// +var microphoneSvg = "data:image/svg+xml," + // Simplify $('body').append('' + label) and onchange hookup. -function prepareInput(name, callback, numeric) { +// Type can be 'auto', 'number', 'text', or 'voice' for slightly +// different interfaces. +function prepareInput(name, callback, type) { if ($.isFunction(name) && !callback) { callback = name; name = null; } + if (!type) { type = 'auto'; } name = $.isNumeric(name) || name ? name : '⇒'; var textbox = $('').css({margin:0, padding:0}), + button = $('').css({marginLeft:4}), label = $('').append(textbox), + '').append(textbox).append(button), debounce = null, - lastseen = textbox.val(); + lastseen = textbox.val(), + recognition = null; function dodebounce() { if (!debounce) { debounce = setTimeout(function() { debounce = null; }, 1000); @@ -9345,16 +9692,17 @@ function prepareInput(name, callback, numeric) { dodebounce(); lastseen = val; textbox.remove(); + button.remove(); label.append(val).css({display: 'table'}); - if (numeric > 0 || ( - numeric >= 0 && $.isNumeric(val) && ('' + parseFloat(val) == val))) { + if (type == 'number' || (type == 'auto' && + $.isNumeric(val) && ('' + parseFloat(val) == val))) { val = parseFloat(val); } label.prop('value', val); if (callback) { setTimeout(function() {callback.call(label, val); }, 0); } } function validate() { - if (numeric <= 0) return true; + if (type != 'numeric') return true; var val = textbox.val(), nval = val.replace(/[^0-9\.]/g, ''); if (val != nval || !$.isNumeric(nval)) { @@ -9368,19 +9716,23 @@ function prepareInput(name, callback, numeric) { if (!validate()) { return false; } newval(); } - if (numeric > 0 && (e.which >= 32 && e.which <= 127) && + if (type == 'voice' && recognition) { + recognition.abort(); + recognition = null; + } + if (type == 'numeric' && (e.which >= 32 && e.which <= 127) && (e.which < '0'.charCodeAt(0) || e.which > '9'.charCodeAt(0)) && (e.which != '.'.charCodeAt(0) || ~textbox.val().indexOf('.'))) { return false; } } textbox.on('keypress keydown', key); - textbox.on('change', newval); + button.on('click', newval); return { result: label, setup: function() { dodebounce(); - if (numeric < 0) { + if (type == 'text' || type == 'voice') { // Widen a "readstr" textbox to make it fill the line. var availwidth = label.parent().width(), freewidth = availwidth + label.offset().left - textbox.offset().left, @@ -9389,6 +9741,48 @@ function prepareInput(name, callback, numeric) { marginwidth = textbox.outerWidth(true) - textbox.width(); textbox.width(desiredwidth - marginwidth); } + if (type == 'number') { + textbox.attr('type', 'number'); + } + if (type == 'voice') { + button.css({display: 'none'}); + var SR = global.SpeechRecognition || global.webkitSpeechRecognition; + if ('function' == typeof(SR)) { + try { + recognition = new SR(); + recognition.continuous = false; + recognition.interimResults = true; + textbox.css({backgroundColor: 'lightyellow', + color: 'gray', + backgroundImage: "url(" + microphoneSvg + ")", + backgroundRepeat: 'no-repeat', + backgroundPosition: 'center'}); + recognition.onspeechstart = function() { + textbox.css({background: 'lightgreen'}); + }; + recognition.onend = function() { + textbox.css({color: '', backgroundColor: '', backgroundImage: '', + backgroundRepeat: '', backgroundPosition: ''}); + textbox.val(lastseen); + newval(); + }; + recognition.onresult = function(event) { + var text = event.results[0][0].transcript; + var confidence = event.results[0][0].confidence; + var shade = 128 - 128 * confidence; + if (event.results[0].isFinal) { + shade = 0; + lastseen = text; + } + textbox.css({color: componentColor('rgb', shade, shade, shade)}); + textbox.val(text); + }; + recognition.start(); + } catch (e) { + console.log(e); + } + } + } // Focus, but don't cause autoscroll to occur due to focus. undoScrollAfter(function() { textbox.focus(); }); } @@ -9486,58 +9880,57 @@ function componentColor(t, args) { return t + '(' + Array.prototype.join.call(args, ',') + ')'; } -function autoArgs(arguments, start, map) { +function autoArgs(args, start, map) { var j = 0; var taken = []; var result = {}; for (var key in map) { var pattern = map[key]; - for (j = start; j < arguments.length; ++j) { + for (j = start; j < args.length; ++j) { if (~taken.indexOf(j)) continue; if (pattern == '*') { break; - } else if (pattern instanceof RegExp && pattern.test(arguments[j])) { + } else if (pattern instanceof RegExp && pattern.test(args[j])) { break; - } else if (pattern instanceof Function && pattern(arguments[j])) { + } else if (pattern instanceof Function && pattern(args[j])) { break; - } else if (pattern == typeof arguments[j]) { + } else if (pattern == typeof args[j]) { break; } } - if (j < arguments.length) { + if (j < args.length) { taken.push(j); - result[key] = arguments[j]; + result[key] = args[j]; } } - if (taken.length + start < arguments.length) { + if (taken.length + start < args.length) { var extra = []; - for (j = start; j < arguments.length; ++j) { + for (j = start; j < args.length; ++j) { if (~taken.indexOf(j)) continue; - extra.push(arguments[j]); + extra.push(args[j]); } result.extra = extra; } return result; } + ////////////////////////////////////////////////////////////////////////// // DEBUGGING SUPPORT ////////////////////////////////////////////////////////////////////////// var debug = { init: function initdebug() { + if (this.ide) return; // Don't re-initialize debug. try { - if (parent && parent.ide) { + if (parent && parent.ide && parent.ide.bindframe && + parent.ide.bindframe(global, parent)) { this.ide = parent.ide; - this.ide.bindframe(window); this.attached = true; } - } catch(e) { - this.ide = null; - this.attached = false; - } + } catch(e) { } if (this.attached) { - if (window.addEventListener) { - window.addEventListener('error', function(event) { + if (global.addEventListener) { + global.addEventListener('error', function(event) { // An error event will highlight the error line. debug.reportEvent('error', [event]); }); @@ -9564,7 +9957,7 @@ debug.init(); // X Y coordinate showing support ////////////////////////////////////////////////////////////////////////// (function() { - if (!debug.ide || (debug.ide.getOptions && !debug.ide.getOptions().panel)) { + if (!debug.ide) { // Only show the X-Y tip if inside a debugging IDE. return; } @@ -9591,7 +9984,7 @@ debug.init(); } var linestart = null, linecanvas = null, lineend = null, xa = 0, ya = 0, xb = 0, yb = 0, xt, yt, dr, ar; - $(window).on('mousedown mouseup mousemove keydown', function(e) { + $(global).on('mousedown mouseup mousemove keydown', function(e) { if (e.type == 'keydown') { if (e.which < 27) return; if (linecanvas) linecanvas.remove(); @@ -9632,7 +10025,7 @@ debug.init(); c = cnv.getContext('2d'), relative = false, p = lineend || e, - s, html, dx, dy, dd, dir, ang; + html, dx, dy, dd, dir, ang; if (linestart && 'function' == typeof(linestart.pagexy)) { var xy = linestart.getxy(), s = linestart.pagexy(); s.x = xy[0]; @@ -9661,7 +10054,7 @@ debug.init(); ang = Math.atan2(dx, dy) / Math.PI * 180; if (linestart) { c.save(); - c.clearRect(xa - 10, ya - 10, xb + 10, yb + 10); + c.clearRect(xa - 10, ya - 10, xb - xa + 20, yb - ya + 20); xa = xb = s.pageX; ya = yb = s.pageY; // Draw a dot @@ -9718,16 +10111,16 @@ debug.init(); var pos = { left: '', top: '', right: '', bottom: '' }; if (p.pageX + 5 < s.pageX) { pos.left = Math.max( - p.pageX - $(window).scrollLeft() - location.outerWidth() - 5, 2); + p.pageX - $(global).scrollLeft() - location.outerWidth() - 5, 2); } else { - pos.left = Math.min(p.pageX - $(window).scrollLeft() + 5, + pos.left = Math.min(p.pageX - $(global).scrollLeft() + 5, $(document).width() - location.outerWidth() - 2); } if (p.pageY + 5 < s.pageY) { pos.top = Math.max( - p.pageY - $(window).scrollTop() - location.outerHeight() - 5, 2); + p.pageY - $(global).scrollTop() - location.outerHeight() - 5, 2); } else { - pos.top = Math.min(p.pageY - $(window).scrollTop() + 5, + pos.top = Math.min(p.pageY - $(global).scrollTop() + 5, $(document).height() - location.outerHeight() - 2); } location.css(pos); @@ -9769,14 +10162,15 @@ var linestyle = 'position:relative;display:block;font-family:monospace;' + var logdepth = 5; var autoscroll = false; var logelement = 'body'; -var panel = false; +var panel = 'auto'; try { // show panel by default if framed inside a an ide, // and if the screen is big enough (i.e., omit mobile clients). - panel = (window.self !== window.top && - screen.width >= 800 && screen.height >= 600 && - parent && parent.ide && parent.ide.getOptions().panel); + if (global.self !== global.top && + screen.width >= 800 && screen.height >= 600 && + parent && parent.ide) { panel = parent.ide.getOptions().panel; } } catch(e) {} +var consolelog = panel; var see; // defined below. var paneltitle = ''; var logconsole = null; @@ -9784,10 +10178,10 @@ var uselocalstorage = '_loghistory'; var panelheight = 50; var currentscope = ''; var scopes = { - '': { e: window.eval, t: window }, - top: { e: window.eval, t: window } + '': { e: global.eval, t: global }, + top: { e: global.eval, t: global } }; -var coffeescript = window.CoffeeScript; +var coffeescript = global.CoffeeScript; var seejs = '(function(){return eval(arguments[0]);})'; function init(options) { @@ -9815,13 +10209,17 @@ function init(options) { if ('coffee' in options) { coffeescript = options.coffee; } if ('abbreviate' in options) { abbreviate = options.abbreviate; } if ('consolehook' in options) { consolehook = options.consolehook; } + if ('consolelog' in options) { consolelog = options.consolelog; } if ('noconflict' in options) { noconflict(options.noconflict); } if (panel) { // panel overrides element and autoscroll. logelement = '#_testlog'; autoscroll = '#_testscroll'; - pulljQuery(tryinitpanel); + if (panel === true) { + startinitpanel(); + } } + if (consolelog === true) { initconsolelog(); } return scope(); } @@ -9845,6 +10243,7 @@ function seeeval(scope, code) { if (scopes[scope].e) { ef = scopes[scope].e; } if (scopes[scope].t) { et = scopes[scope].t; } } + debug.reportEvent("seeeval", [scope, code]); return ef.call(et, code); } @@ -9854,7 +10253,7 @@ var initialvardecl = new RegExp( function barecs(s) { // Compile coffeescript in bare mode. - var compiler = coffeescript || window.CoffeeScript; + var compiler = coffeescript || global.CoffeeScript; var compiled = compiler.compile(s, {bare:1}); if (compiled) { // Further strip top-level var decls out of the coffeescript so @@ -9881,22 +10280,22 @@ function exportsee() { see.js = seejs; see.cs = '(function(){return eval(' + seepkg + '.barecs(arguments[0]));})'; see.version = version; - window[seepkg] = see; + global[seepkg] = see; } function noteoldvalue(name) { return { name: name, - has: window.hasOwnProperty(name), - value: window[name] + has: global.hasOwnProperty(name), + value: global[name] }; } function restoreoldvalue(old) { if (!old.has) { - delete window[old.name]; + delete global[old.name]; } else { - window[old.name] = old.value; + global[old.name] = old.value; } } @@ -9960,7 +10359,7 @@ var queue = []; see = function see() { if (logconsole && typeof(logconsole.log) == 'function') { - logconsole.log.apply(window.console, arguments); + logconsole.log.apply(global.console, arguments); } var args = Array.prototype.slice.call(arguments); queue.push(''); @@ -10014,7 +10413,8 @@ function isprimitive(vt) { } function isdom(obj) { - return (obj.nodeType && obj.nodeName && typeof(obj.cloneNode) == 'function'); + return (obj && obj.nodeType && obj.nodeName && + typeof(obj.cloneNode) == 'function'); } function midtruncate(s, maxlen) { @@ -10309,12 +10709,12 @@ function expand(prefix, obj, depth, output) { } } function initlogcss() { - if (!addedcss && !window.document.getElementById('_logcss')) { - var style = window.document.createElement('style'); + if (!addedcss && !global.document.getElementById('_logcss')) { + var style = global.document.createElement('style'); style.id = '_logcss'; style.innerHTML = (linestyle ? 'samp._log{' + linestyle + '}' : '') + logcss; - window.document.head.appendChild(style); + global.document.head.appendChild(style); addedcss = true; } } @@ -10352,7 +10752,6 @@ function aselement(s, def) { default: return s; } - return null; } function stickscroll() { var stick = false, a = aselement(autoscroll, null); @@ -10368,10 +10767,10 @@ function stickscroll() { } } function flushqueue() { - var elt = aselement(logelement, null); + var elt = aselement(logelement, null), child; if (elt && elt.appendChild && queue.length) { initlogcss(); - var temp = window.document.createElement('samp'); + var temp = global.document.createElement('samp'); temp.innerHTML = queue.join(''); queue.length = 0; var complete = stickscroll(); @@ -10381,7 +10780,10 @@ function flushqueue() { complete(); } if (!retrying && queue.length) { - retrying = setTimeout(function() { timer = null; flushqueue(); }, 100); + if (panel == 'auto') { + startinitpanel(); + } + retrying = setTimeout(function() { retrying = null; flushqueue(); }, 100); } else if (retrying && !queue.length) { clearTimeout(retrying); retrying = null; @@ -10392,6 +10794,7 @@ function flushqueue() { // TEST PANEL SUPPORT // --------------------------------------------------------------------- var addedpanel = false; +var initpanelstarted = false; var inittesttimer = null; var abbreviate = [{}.undefined]; var consolehook = null; @@ -10416,7 +10819,7 @@ function promptcaret(color) { return ''; } function getSelectedText(){ - if(window.getSelection) { return window.getSelection().toString(); } + if(global.getSelection) { return global.getSelection().toString(); } else if(document.getSelection) { return document.getSelection(); } else if(document.selection) { return document.selection.createRange().text; } @@ -10432,7 +10835,7 @@ function readlocalstorage() { } var state = { height: panelheight, history: [] }, result; try { - result = window.JSON.parse(window.localStorage[uselocalstorage]); + result = global.JSON.parse(global.localStorage[uselocalstorage]); } catch(e) { result = noLocalStorage || {}; } @@ -10463,14 +10866,37 @@ function updatelocalstorage(state) { } if (changed) { try { - window.localStorage[uselocalstorage] = window.JSON.stringify(stored); + global.localStorage[uselocalstorage] = global.JSON.stringify(stored); } catch(e) { noLocalStorage = stored; } } } function wheight() { - return window.innerHeight || $(window).height(); + return global.innerHeight || $(global).height(); +} +function initconsolelog() { + try { + if (consolelog && global.console && !global.console._log && + 'function' == typeof global.console.log) { + var _log = global.console._log = global.console.log; + global.console.log = function log() { + _log.apply(this, arguments); + see.apply(this, arguments); + } + var _debug = global.console._debug = global.console.debug; + global.console.debug = function debug() { + _debug.apply(this, arguments); + see.apply(this, arguments); + } + } + } catch(e) { } +} +function startinitpanel() { + if (!initpanelstarted) { + initpanelstarted = true; + pulljQuery(tryinitpanel); + } } function tryinitpanel() { if (addedpanel) { @@ -10483,7 +10909,8 @@ function tryinitpanel() { } $('#_testpanel').show(); } else { - if (!window.document.getElementById('_testlog') && window.document.body) { + if (!global.document.getElementById('_testlog') && global.document.body) { + initconsolelog(); initlogcss(); var state = readlocalstorage(); var titlehtml = (paneltitle ? formattitle(paneltitle) : ''); @@ -10613,7 +11040,7 @@ function tryinitpanel() { } if (e.type == 'mouseup' || e.type == 'blur' || e.type == 'mousemove' && e.which != dragwhich) { - $(window).off('mousemove mouseup blur', dragfunc); + $(global).off('mousemove mouseup blur', dragfunc); if (document.releaseCapture) { document.releaseCapture(); } if ($('#_testpanel').height() != state.height) { state.height = $('#_testpanel').height(); @@ -10621,7 +11048,7 @@ function tryinitpanel() { } } }; - $(window).on('mousemove mouseup blur', dragfunc); + $(global).on('mousemove mouseup blur', dragfunc); return false; }); $('#_testpanel').on('mouseup', function(e) { @@ -10648,14 +11075,14 @@ function transparentHull(image, threshold) { if (!threshold) threshold = 0; c.width = image.width; c.height = image.height; - ctx = c.getContext('2d'); + var ctx = c.getContext('2d'); ctx.drawImage(image, 0, 0); return transparentCanvasHull(c, threshold); } function transparentCanvasHull(canvas, threshold) { var ctx = canvas.getContext('2d'); - data = ctx.getImageData(0, 0, canvas.width, canvas.height).data; + var data = ctx.getImageData(0, 0, canvas.width, canvas.height).data; var hull = []; var intthresh = 256 * threshold; var first, last, prevfirst = Infinity, prevlast = -1; @@ -10687,7 +11114,7 @@ function transparentCanvasHull(canvas, threshold) { function eraseOutsideHull(canvas, hull) { var ctx = canvas.getContext('2d'), w = canvas.width, - h = canvas.height + h = canvas.height, j = 0; ctx.save(); // Erase everything outside clipping region. @@ -10716,4 +11143,4 @@ function scalePolygon(poly, sx, sy, tx, ty) { } } -})(jQuery); +}).call(this, this.jQuery); diff --git a/jquery-turtle.min.js b/jquery-turtle.min.js new file mode 100644 index 0000000..387546f --- /dev/null +++ b/jquery-turtle.min.js @@ -0,0 +1,5 @@ +(function(a){function b(b,c){if(Ne>5)Oe.push({elem:b,qname:c});else{for(Ne+=1,a.dequeue(b,c);Oe.length>0;){var d=Oe.shift();a.dequeue(d.elem,d.qname),Vc()}Ne-=1}}function c(a,b){function c(){this.constructor=a}for(var d in b)Je.call(b,d)&&(a[d]=b[d]);return c.prototype=b.prototype,a.prototype=new c,a.__super__=b.prototype,a}function d(b){var c,d,e=b.charAt(0).toUpperCase()+b.slice(1),f=["Moz","Webkit","O","ms"],g=document.createElement("div");if(b in g.style)d=b;else for(var h=0;h<0?'"'+a+'"':"'"+a+"'":a}var c=[];for(var d in a)a.hasOwnProperty(d)&&c.push(d+":"+b(a[d])+";");return c.join(" ")}function h(a){return a}function i(a,b){var c=[a[0]*b[0]+a[2]*b[1],a[1]*b[0]+a[3]*b[1]];return 6==a.length&&(c[0]+=a[4],c[1]+=a[5]),c}function j(a,b){var c=[a[0]*b[0]+a[2]*b[1],a[1]*b[0]+a[3]*b[1],a[0]*b[2]+a[2]*b[3],a[1]*b[2]+a[3]*b[3]],d=6==a.length;return 6==b.length?(c.push(a[0]*b[4]+a[2]*b[5]+(d?a[4]:0)),c.push(a[1]*b[4]+a[3]*b[5]+(d?a[5]:0))):d&&(c.push(a[4]),c.push(a[5])),c}function k(a){return Math.abs(a)>1e-12}function l(a){return!(k(a[1])||k(a[2])||k(1-a[0])||k(1-a[3]))}function m(a){if(l(a))return[1,0,0,1];var b=u(a);return k(b[2])?j(o(-b[3]),j(p(1/b[1],1/b[2]),o(-b[0]))):null}function n(a){var b=m(a);if(4==a.length)return b;var c=i(b,[-a[4],-a[5]]);return b.push(c[0]),b.push(c[1]),b}function o(a){var b=Math.cos(a),c=Math.sin(a);return[b,c,-c,b]}function p(a,b){return 1==arguments.length&&(a=b),[a,0,0,b]}function q(a,b){return[a[0]+b[0],a[1]+b[1]]}function r(a,b){return[a[0]-b[0],a[1]-b[1]]}function s(a,b){return[a[0]*b,a[1]*b]}function t(a,b,c){return q(i(a,r(b,c)),c)}function u(a){var b,c,d=a[0]*a[0]+a[1]*a[1],e=a[0]*a[2]+a[1]*a[3],f=a[2]*a[2]+a[3]*a[3],g=-.5*Math.atan2(2*e,d-f),h=Math.cos(g),i=Math.sin(g),j=a[0]*h-a[2]*i,k=a[1]*h-a[3]*i,l=Math.atan2(k,j),m=Math.cos(l),n=Math.sin(l),o=(a[1]*i+a[3]*h)*m-(a[0]*i+a[2]*h)*n,p=(a[0]*h-a[2]*i)*m+(a[1]*h-a[3]*i)*n;return g<-Math.PI/4?(g+=Math.PI/2,c=p,b=o,l-=Math.PI/2):(b=p,c=o),l>Math.PI&&(l-=2*Math.PI),[l,b,c,g]}function v(a,b){var c=(b-a)/2,d=Math.cos(c),e=Math.sin(c),f=d,g=-e,h=1+f*d+g*e,i=f*e-g*d,j=i&&4/3*(Math.sqrt(2*h)-h)/i,k=f-j*g,l=g+j*f,m=k,n=-l,o=c+a,p=Math.cos(o),q=Math.sin(o);return[[k*p-l*q,k*q+l*p],[m*p-n*q,m*q+n*p],[Math.cos(b),Math.sin(b)]]}function w(a){var b=ra(a,!1);if(b)return[b.tx,b.ty];var c=x(a);return c?[c[4],c[5]]:[0,0]}function x(b){var c=Ie.getComputedStyle?Ie.getComputedStyle(b)[Pe]:a.css(b,"transform");if(!c||"none"===c)return null;var d=/^matrix\(([\-+.\de]+),\s*([\-+.\de]+),\s*([\-+.\de]+),\s*([\-+.\de]+),\s*([\-+.\de]+)(?:px)?,\s*([\-+.\de]+)(?:px)?\)$/.exec(c);return d?[parseFloat(d[1]),parseFloat(d[2]),parseFloat(d[3]),parseFloat(d[4]),parseFloat(d[5]),parseFloat(d[6])]:A(c)}function y(b,c){var d,e,f,g="none"===a.css(b,"display");if(g){d={position:"absolute",visibility:"hidden",display:"block"},e={};for(f in d)e[f]=b.style[f],b.style[f]=d[f]}var h=Ie.getComputedStyle?Ie.getComputedStyle(b):null;if(g)for(f in d)b.style[f]=e[f];var i=h&&h[Qe]||a.css(b,"transformOrigin");if(i&&i.indexOf("%")<0)return a.map(i.split(" "),parseFloat);if(c)return[c[0]/2,c[1]/2];var j=a(b);return[j.width()/2,j.height()/2]}function z(a){for(var b,c=[1,0,0,1];null!==a;)b=x(a),b&&!l(b)&&(c=j(b,c)),a=a.parentElement;return c.slice(0,4)}function A(b){var c=[1,0,0,1],d=[],e=[],f=/(?:^\s*|)(\w*)\s*\(([^)]*)\)\s*/g,g=b.replace(f,function(b){return d.push(b[1].toLowerCase()),e.push(a.map(b[2].split(","),function(a){var b=a.trim().toLowerCase();return{num:parseFloat(b),unit:b.replace(/^[+-.\de]*/,"")}})),""});if(g)return null;for(var h=d.length-1;h>=0;--h){var i,k,l,m=null,n=d[h],o=e[h];if("matrix"==n)o.length>=6&&(m=[o[0].num,o[1].num,o[2].num,o[3].num,o[4].num,o[5].num]);else if("rotate"==n)1==o.length&&(i=wa(o[0]),k=Math.cos(i),l=Math.sin(i),m=[k,-l,k,l]);else if("translate"==n||"translatex"==n||"translatey"==n){var p=0,q=0;if(o.length>=1){if(o[0].unit&&"px"!=o[0].unit)return null;if("translate"==n||"translatex"==n?p=o[0].num:"translatey"==n&&(q=o[0].num),"translate"==n&&o.length>=2){if(o[1].unit&&"px"!=o[1].unit)return null;q=o[1].num}m=[0,0,0,0,p,q]}}else if("scale"==n||"scalex"==n||"scaley"==n){var r=1,s=1;o.length>=1&&("scale"==n||"scalex"==n?r=o[0].num:"scaley"==n&&(s=o[0].num),"scale"==n&&o.length>=2&&(s=o[1].num),m=[r,0,0,s,0,0])}else{if("skew"!=n&&"skewx"!=n&&"skewy"!=n)return null;var t=0,u=0;o.length>=1&&("skew"==n||"skewx"==n?t=Math.tan(wa(o[0])):"skewy"==n&&(u=Math.tan(wa(o[0]))),"skew"==n&&o.length>=2&&(u=Math.tan(wa(o[0]))),m=[1,u,t,1,0,0])}c=j(c,m)}return c}function B(a,b,c){if(0>=c)return a;var d=b.pageX-a.pageX,e=b.pageY-a.pageY,f=d*d+e*e;if(c*c>=f)return b;var g=c/Math.sqrt(f);return{pageX:a.pageX+g*d,pageY:a.pageY+g*e}}function C(a,b,c){if(0>=c)b=a;else if(180>c){var d=xa(b-a);d>c?b=a+c:-c>d&&(b=a-c)}return xa(b)}function D(a,b,c,d){return{pageX:Math.floor(a+c/2),pageY:Math.floor(b+d/2)}}function E(a,b,c,d){var e=a+c,f=b+d;return[{pageX:a,pageY:b},{pageX:a,pageY:f},{pageX:e,pageY:f},{pageX:e,pageY:b}]}function F(a){if(!/e[\-+]/.exec(a))return a;var b=a.replace(/(?:\d+(?:\.\d*)?|\.\d+)e[\-+]\d+/g,function(a){return sa(parseFloat(a))});return b}function G(b,c,d){var e=a.data(b,"turtleData");if(e&&e.quickhomeorigin&&e.down&&e.style&&!d&&b.classList&&b.classList.contains("turtle"))return e.quickhomeorigin;var f,g,h,i="none"===a.css(b,"display"),j=i?{position:"absolute",visibility:"hidden",display:"block"}:{},k=(j[Pe]=c?"matrix("+a.map(c,sa).join(", ")+", 0, 0)":"none",{});for(f in j)k[f]=b.style[f],b.style[f]=j[f];g=N(b),h=y(b,[g.width,g.height]);for(f in j)b.style[f]=F(k[f]);d&&(d.gbcr=g,d.localorigin=h);var l=q([g.left,g.top],h);return e&&e.down&&e.style&&(e.quickhomeorigin=l),l}function H(){return Ie.innerHeight||a(Ie).height()}function I(){return Ie.innerWidth||a(Ie).width()}function J(){return document.body?a(document).height():document.height}function K(){return document.body?a(document).width():document.width}function L(a){return a.offsetHeight<=0&&a.offsetWidth<=0}function M(a,b,c,d){return{left:a,top:b,right:a+c,bottom:b+d,width:c,height:d}}function N(b){return mb(b)?M(b.pageX,b.pageY,0,0):a.isWindow(b)?M(a(Ie).scrollLeft(),a(Ie).scrollTop(),I(),H()):9===b.nodeType?M(0,0,K(),J()):"getBoundingClientRect"in b?T.apply(b):M(0,0,0,0)}function O(a,b,c){var d=Math.max(0,Math.max(c.top-a.pageY,a.pageY-c.bottom)),e=Math.max(0,Math.max(c.left-a.pageX,a.pageX-c.right));return e*e+d*d>b}function P(a,b,c){var d=Math.max(c.bottom-a.pageY,a.pageY-c.top),e=Math.max(c.right-a.pageX,a.pageX-c.left);return b>e*e+d*d}function Q(a,b){return b.right=a.top&&b.bottom<=a.bottom&&b.left>=a.left&&b.right<=a.right}function S(a,b){return 4===a.length&&a[0].pageX===b.left&&a[0].pageY===b.top&&a[1].pageX===b.left&&a[1].pageY===b.bottom&&a[2].pageX===b.right&&a[2].pageY===b.bottom&&a[3].pageX===b.right&&a[3].pageY===b.top}function T(){var a=this.getBoundingClientRect();return{top:a.top+Ie.pageYOffset,bottom:a.bottom+Ie.pageYOffset,left:a.left+Ie.pageXOffset,right:a.right+Ie.pageXOffset,width:a.width,height:a.height}}function U(b,c,d,e,f){var g,h,j,k,l=z(b.parentElement),n=m(l),o=G(b,n);if(n){if(a.isNumeric(d)&&(j=w(b),g=q(i(l,j),o),h={pageX:g[0],pageY:g[1]},c=B(h,c,d)),k=i(n,r([c.pageX,c.pageY],o)),e||f){var p=Gc(b);k[0]+=e*p,k[1]-=f*p}return sa(k[0])+" "+sa(k[1])}}function V(a){var b=a.offsetParent;return b?b:document}function W(b,c){c||(c=a(V(b)).pagexy());var d=z(b.parentElement),e=m(d),f=G(b,e),g=ra(b,!0),h=e&&i(e,r([c.pageX,c.pageY],f)),j=1/Gc(b);if(e)return[(g.tx-h[0])*j,(h[1]-g.ty)*j]}function X(b,c){var d,e,f=z(b.parentElement),g=(ra(b,!0),a(V(b)).pagexy()),h=Gc(b),j=[];for(e=0;ei-f&&(d=i-f),f>d&&(d=f),e>j-g&&(e=j-g),g>e&&(e=g);var l={pageX:d,pageY:e};a.isNumeric(c)&&(l=B(k.origin(),l,c)),k.scrollLeft(l.pageX-f),k.scrollTop(l.pageY-g)}function ca(a,b,c){var d=b.pageX-a.pageX,e=b.pageY-a.pageY,f=c.pageX-a.pageX,g=c.pageY-a.pageY;return f*e-d*g}function da(a,b,c){var d=c.pageX-a.pageX,e=c.pageY-a.pageY;return d*b.pageY-b.pageX*e}function ea(a,b){if(b.length<=0)return!1;if(1==b.length)return b[0].pageX==a.pageX&&b[0].pageY==a.pageY;var c=ca(a,b[b.length-1],b[0]);if(0===c)return!0;var d=c>0;if(2==b.length)return!1;for(var e=1;e0!=d)return!1}return!0}function fa(a,b){return{pageX:a.pageX-b.pageX,pageY:a.pageY-b.pageY}}function ga(a,b,c,d){var e,f,g=fa(c,b);for(e=0;e0?1:0>a?-1:0}function ia(a){if(a.length<=2)return 0;var b=ca(a[a.length-1],a[0],a[1]);if(0!==b)return ha(b);for(var c=1;c1&&1!=ha(ca(a[a.length-2],a[a.length-1],b));)a.pop();return a.length&&d(a[a.length-1],b)||a.push(b),a}function c(a,b,c){for(var d=0;db.pageX?1:a.pageYb.pageY?1:0}a.sort(e);var f=c(a,[],b),g=c(a.reverse(),[],b);return f.concat(g.slice(1,-1))}function ma(b){if(!b)return null;if(a.isArray(b))return b;for(var c=a.map(b.trim().split(/\s+/),parseFloat),d=[],e=0;e+1180&&(b-=360),b}function wa(a){return a/180*Math.PI}function xa(a){return Math.abs(a)>180&&(a%=360,a>180?a-=360:-180>=a&&(a+=360)),a}function ya(a){return Math.abs(a)>=720&&(a=a%360+(a>0?360:-360)),a}function za(){return Re.field||Ba(),Re.field}function Aa(){return Re.surface||Ba(),Re.surface}function Ba(){var b=document.createElement("samp"),c=document.createElement("samp"),d=Math.floor(I()/2),e=Math.floor(H()/2);a(b).css({position:"absolute",display:"inline-block",top:0,left:0,width:"100%",height:"100%",font:"inherit",zIndex:-1,transformOrigin:d+"px "+e+"px",pointerEvents:"none",overflow:"hidden"}).addClass("turtlefield"),a(c).attr("id","origin").css({position:"absolute",display:"inline-block",top:e,left:d,width:"100%",height:"0",font:"inherit",transformOrigin:"0px 0px",pointerEvents:"all",turtleSpeed:1/0}).appendTo(b),Re.surface=b,Re.field=c,Ca(),Sb()}function Ca(){document.body?(null==a("html").attr("style")&&a("html").css("min-height","100%"),a(Re.surface).prependTo("body"),Rb()):a(document).ready(Ca)}function Da(a){return a.drawOnCanvas||(a.drawOnCanvas=Fa()),a.drawOnCanvas}function Ea(b){var c=a.data(b,"turtleData");return c?c.drawOnCanvas?c.drawOnCanvas:Re.canvas:null}function Fa(){if(Re.canvas)return Re.canvas;var b=Aa();return Re.canvas=document.createElement("canvas"),a(Re.canvas).css({"z-index":-1}),b.insertBefore(Re.canvas,b.firstChild),Ja(),Ha(Ja),a(Ie).resize(Ja),Re.canvas}function Ga(a,b){return Re.offscreen&&Re.offscreen.width===a&&Re.offscreen.height===b?(Re.offscreen.getContext("2d").clearRect(0,0,a,b),Re.offscreen):(Re.offscreen||(Re.offscreen=document.createElement("canvas")),Re.offscreen.width=a,Re.offscreen.height=b,Re.offscreen)}function Ha(b){var c=a("body"),d=c.width(),e=c.height(),f=function(){(c.width()!=d||c.height()!=e)&&(b(),d=c.width(),e=c.height())};Re.timer&&clearInterval(Re.timer),Re.timer=setInterval(f,250)}function Ia(){var b=a("body");return[Math.max(b.outerWidth(!0),Ie.innerWidth||a(Ie).width()),Math.max(b.outerHeight(!0),Ie.innerHeight||a(Ie).height())]}function Ja(){if(Re.canvas){var b,c=Ia(),d=c[0],e=c[1],f=Re.canvas.width,g=Re.canvas.height,h=Math.max(Math.min(2e3,Math.max(200,f)),100*Math.ceil(d/100))*Re.subpixel,i=Math.max(Math.min(2e3,Math.max(200,f)),100*Math.ceil(e/100))*Re.subpixel;a(Re.surface).css({width:d+"px",height:e+"px"}),(f!=h||g!=i)&&(b=document.createElement("canvas"),b.width=Math.min(f,h),b.height=Math.min(g,i),b.getContext("2d").drawImage(Re.canvas,0,0),Re.canvas.width=h,Re.canvas.height=i,Re.canvas.getContext("2d").drawImage(b,0,0),a(Re.canvas).css({width:h/Re.subpixel,height:i/Re.subpixel}))}}function Ka(a,b){if(!a)return null;if(a&&"function"==typeof a&&(a.helpname||a.name)&&(a=a.helpname||a.name),a=String(a),a.trim&&(a=a.trim()),!a||"none"===a)return null;if("path"===a||"fill"===a)return{savePath:!0};var c=!1;/^erase\b/.test(a)&&(a=a.replace(/^erase\b/,"white; globalCompositeOperation:destination-out"),c=!0);var d=f(a,b);return c&&(d.eraseMode=!0),d}function La(a){return a?g(a):"none"}function Ma(a){return"down"==a||a===!0?!0:"up"==a||a===!1?!1:He}function Na(a){return a?"down":"up"}function Oa(b){var c=a.data(b,"turtleData");return c||(c=a.data(b,"turtleData",{style:null,corners:[[]],path:[[]],down:!1,speed:"turtle",easing:"swing",turningRadius:0,drawOnCanvas:null,quickpagexy:null,quickhomeorigin:null,oldscale:1,instrument:null,stream:null})),c}function Pa(b){var c=a.data(b,"turtleData");return c?c.turningRadius:0}function Qa(){return{get:function(a,b,c){return sa(Pa(a))+"px"},set:function(a,b){var c=parseFloat(b);if(!isNaN(c)&&(Oa(a).turningRadius=c,a.style.turtleTurningRadius=""+sa(c)+"px",0===c)){var d=ra(a,!1);d&&(d.rot>180||d.rot<=-180)&&(d.rot=xa(d.rot),a.style[Pe]=ta(d))}}}}function Ra(){return{get:function(a,b,c){return La(Oa(a).style)},set:function(a,b){var c=Ka(b,"strokeStyle"),d=Oa(a);d.style&&(d.style=null,bb(a,d,!0)),d.style=c,a.style.turtlePenStyle=La(c),bb(a,d,!0)}}}function Sa(){return{get:function(a,b,c){return Na(Oa(a).down)},set:function(a,b){var c=Ma(b);if(c!==He){var d=Oa(a);c!=d.down&&(d.down=c,d.quickpagexy=null,d.quickhomeorigin=null,a.style.turtlePenDown=Na(c),bb(a,d,!0))}}}}function Ta(a,b){return 0===Math.round(a.pageX-b.pageX)&&0===Math.round(a.pageY-b.pageY)}function Ua(a,b){return 0===Math.round(1e3*(a.pageX-b.pageX))&&0===Math.round(1e3*(a.pageY-b.pageY))}function Va(a,b){return Ta(a,b)&&0===Math.round(a.pageX-b.pageX1)&&0===Math.round(a.pageY-b.pageY1)&&0===Math.round(b.pageX2-b.pageX)&&0===Math.round(b.pageY2-b.pageY)}function Wa(a){var b=1e3*a,c=Math.round(b);return Math.abs(c-b)1){h=b[j],f=h.length>2&&Ta(h[0],h[h.length-1])&&!Ta(h[0],h[Math.floor(h.length/2)]),g=f&&!("pageX2"in h[h.length-1]);var k=h[0].pageX,l=h[0].pageY;i.moveTo(k,l);for(var m=1;m1&&(d.length=1),d[0].length&&(d[0].length=0),void(c&&(e?f.length&&f[0].length&&(1==f[0].length?f[0].length=0:f.unshift([])):(f.length>1&&(f.length=1),f[0].length&&(f[0].length=0))));if(c||!e.savePath){var g=Y(a);if(c&&(g.corner=!0,ab(f[0],g)),!e.savePath){ab(d[0],g);var h=Kc(a);$a(Da(b),b.path,e,h,2)}}}}function cb(b,c){var d=Oa(b);d.style&&(c=a.extend({},d.style,c));var e=Kc(b);$a(Da(d),d.corners,c,e,1)}function db(b){if((!b||/\bcanvas\b/.test(b))&&Re.canvas){var c=Re.canvas.getContext("2d");c.save(),c.setTransform(1,0,0,1,0,0),c.clearRect(0,0,Re.canvas.width,Re.canvas.height),c.restore()}if((!b||/\bturtles\b/.test(b))&&Re.surface){var d=a(Re.surface).find(".turtle").not(".turtlefield");pf&&(d=d.not(pf)),d.remove()}if((!b||/\blabels\b/.test(b))&&Re.surface){var d=a(Re.surface).find(".turtlelabel").not(".turtlefield");d.remove()}(!b||/\btext\b/.test(b))&&a("body").contents().not(".turtlefield").remove()}function eb(a,b){if(!a||a.length<1)return null;for(var c=1,d={left:Math.floor(a[0].pageX),top:Math.floor(a[0].pageY),right:Math.ceil(a[0].pageX),bottom:Math.ceil(a[0].pageY)};c=c[3]/2)return!0}else{var o=!c;for(m=0;m0==o)return!0}return!1}function hb(a,b,c){if(b.img)("CANVAS"==a[0].tagName||a[0].tagName==b.img.tagName)&&Kb(b.img,a[0],b.css);else if("IMG"==a[0].tagName||"CANVAS"==a[0].tagName)Hb(a[0],b.url,b.css,c),c=null;else{var d={backgroundImage:"url("+b.url+")",backgroundRepeat:"no-repeat",backgroundPosition:"center"};b.css.width&&b.css.height&&(d.backgroundSize=b.css.width+"px "+b.css.height+"px"),a.css(d)}c&&c()}function ib(b,c,d){var e,f=ra(b,!0),g=f&&wa(f.rot),h=Gc(b),i=f&&c*h,j=f&&(d||0)*h,k=-Math.cos(g)*i,l=Math.sin(g)*i,m=a.data(b,"turtleData");f&&(d&&(k+=Math.sin(g)*j,l+=Math.cos(g)*j),m&&(e=m.quickpagexy)&&(m.quickpagexy={pageX:e.pageX+l,pageY:e.pageY+k}),f.tx+=l,f.ty+=k,b.style[Pe]=ta(f),bb(b,m,!0))}function jb(b,c,d){var e,f=ra(b,!0),g=a.data(b,"turtleData");f&&(g&&(e=g.quickpagexy)&&(g.quickpagexy={pageX:e.pageX+c,pageY:e.pageY-d}),f.tx+=c,f.ty-=d,b.style[Pe]=ta(f),bb(b,g,!0))}function kb(a,b){var c=ra(a,!0);c&&(c.rot+=b,a.style[Pe]=ta(c))}function lb(a,b,c){var d=ra(a,!0);if(d){var e=Gc(a),f=wa(d.rot),g=b*e,h=(c||0)*e,i=-Math.cos(f)*g,j=Math.sin(f)*g;return h&&(i+=Math.sin(f)*h,j+=Math.cos(f)*h),sa(d.tx+j)+" "+sa(d.ty+i)}}function mb(b){return b&&a.isNumeric(b.pageX)&&a.isNumeric(b.pageY)}function nb(){return{get:function(a,b,c){return Oa(a).speed},set:function(b,c){(a.isNumeric(c)&&!(0>=c)||c in a.fx.speeds||""+c=="Infinity")&&(Oa(b).speed=""+c)}}}function ob(){return{get:function(a,b,c){return Oa(a).easing},set:function(b,c){c in a.easing&&(Oa(b).easing=c)}}}function pb(b,c){var d=a.data(b,"turtleData");return c=c||Df,d?a.isNumeric(d.speed)||"Infinity"==d.speed?1e3/d.speed:"turtle"==d.speed&&c?0:d.speed:c?0:"turtle"}function qb(b){var c=a.data(b,"turtleData");return c?c.easing:null}function rb(){return{get:function(a,b,c){var d=ra(a,b),e=y(a);if(d){var f=wa(d.rot),g=Math.cos(f),h=Math.sin(f),i=Gc(a);return sa(((d.tx+e[0])*h-(d.ty+e[1])*g)/i)+"px"}},set:function(b,c){var d,e=ra(b,!0)||{tx:0,ty:0,rot:0,sx:1,sy:1,twi:0},f=y(b),g=Gc(b),h=parseFloat(c)*g,i=wa(e.rot),j=Math.cos(i),k=Math.sin(i),l=(e.tx+f[0])*j+(e.ty+f[1])*k,m=l*j+h*k-f[0],n=l*k-h*j-f[1],o=a.data(b,"turtleData");o&&(d=o.quickpagexy)&&(o.quickpagexy={pageX:d.pageX+(m-e.tx),pageY:d.pageY+(n-e.ty)}),e.tx=m,e.ty=n,b.style[Pe]=ta(e),bb(b,o)}}}function sb(b,c,d,e){return{get:function(a,c,e){var f=ra(a,c);return f?f[b]+d:void 0},set:function(d,f){var g,h=ra(d,!0)||{tx:0,ty:0,rot:0,sx:1,sy:1,twi:0},i={displace:e},j=a.data(d,"turtleData"),k=h.tx,l=h.ty;h[b]=c(f,d,h,i),d.style[Pe]=ta(h),i.displace?(j&&(g=j.quickpagexy)&&(j.quickpagexy={pageX:g.pageX+(h.tx-k),pageY:g.pageY+(h.ty-l)}),bb(d,j)):Z(d)}}}function tb(a,b,c){var d=ya(b-a),e=d>0?c:-c,f=wa(a),g=[Math.cos(f)*e,Math.sin(f)*e],h=wa(b);return{delta:d,sradius:e,dc:g,dx:g[0]-Math.cos(h)*e,dy:g[1]-Math.sin(h)*e}}function ub(a,b,c,d,e,f){var g,h,j,k,l,m,n,o,p,r,t,u=tb(c,d,e),w=u.sradius,x=u.dc;for(n=1,o=u.delta,p=Math.abs(u.delta),p>45&&(n=Math.ceil(p/45),o=u.delta/n),r=[];--n>=0;)g=0===n?d:c+o,h=wa(c+180),j=wa(g+180),r.push.apply(r,v(h,j)),c=g;for(t=[],k=0;k2||(j.length>=1&&(i[c]=parseFloat(j[0])),j.length>=2?i[d]=parseFloat(j[1]):e?i[d]=0:i[d]=i[c],b.style[Pe]=ta(i),e?(k&&(h=k.quickpagexy)&&(k.quickpagexy={pageX:h.pageX+(i.tx-l),pageY:h.pageY+(i.ty-m)}),bb(b,k)):Z(b))}}}function Ab(a){return Se.href=a,Se}function Bb(a){return Ab(a).href}function Cb(a){return/(?:^|\.)pencil(?:code)?\./i.test(a)}function Db(a){var b=Ab(null==a?"":a).hostname,c=/^(\w+)\.pencil(?:code)?\./i.exec(b);return c?c[1]:null}function Eb(a,b){var c=Ab(null==a?"":a),d=c.href;return Cb(c.hostname)?/^\/(?:edit|home|code|load|save)(?:\/|$)/.test(c.pathname)&&(d=c.protocol+"//"+c.host+"/"+b+"/"+c.pathname.replace(/\/[^\/]*(?:\/|$)/,"")+c.search+c.hash):Cb(Ie.location.hostname)&&(d="/proxy/"+d),d}function Fb(a){return/\//.test(a)?a:(a="/img/"+a,Cb(Ie.location.hostname)?a:"//pencilcode.net"+a)}function Gb(){if(!document.cookie)return null;for(var a=document.cookie.split(/;\s*/),b=0;b0&&b.height>0)try{e=c.getContext("2d"),e.clearRect(0,0,b.width,b.height),e.drawImage(b,0,0)}catch(i){}}else c.src=b.src;d&&g.css(d);var j=y(c);if(b&&!d.turtleHull)try{var k=De(b);Ge(k,parseFloat(g.css("width"))/b.width,parseFloat(g.css("height"))/b.height,-j[0],-j[1]),g.css("turtleHull",k)}catch(i){}Lb(c,f,j)}function Lb(b,c,d){var e=a(b);if(e.hasClass("turtle")&&(d[0]!=c[0]||d[1]!=c[1]))if("absolute"==e.css("position")&&/px$/.test(e.css("left"))&&/px$/.test(e.css("top")))e.css("left",parseFloat(e.css("left"))+c[0]-d[0]),e.css("top",parseFloat(e.css("top"))+c[1]-d[1]);else{var f=ra(b,!0);f.tx+=c[0]-d[0],f.ty+=c[1]-d[1],b.style[Pe]=ta(f)}}function Mb(b,c,d,e,f){var g,h,i,j,k;return e===He&&f===He?(g=a(d),g.length?(h=g[0],i=N(h),S(_(h),i)?b.filter(function(){var b=N(this);return c===(R(i,b)||!Q(i,b)&&a(this).inside(h))}):b.filter(function(){return c===a(this).inside(h)})):[]):(j=a.isNumeric(e)&&a.isNumeric(f)?[e,f]:e,a.isArray(j)&&(j=X(b[0]||document.body,[j])[0]),"touch"===d?mb(j)?b.filter(function(){return c===a(this).touches(j)}):(g=a(j),i=N(g[0]),S(_(g[0]),i)?b.filter(function(){var a=N(this);return c===(!Q(i,a)&&(R(i,a)||g.touches(this)))}):b.filter(function(){return c===g.touches(this)})):(k=d*d,b.filter(function(){var a=N(this);if(O(j,k,a))return!c;if(P(j,k,a))return c;var b=Y(this),d=j.pageX-b.pageX,e=j.pageY-b.pageY;return c===k>=d*d+e*e})))}function Nb(){if(!Ze){Ze=!0;try{Ie.parent.document.activeElement.blur()}catch(a){}Ie.focus()}}function Ob(b,c,d,e,f){for(var g=e.split(/\s+/),h=0;h<0?void 0:c.apply(this,arguments)};c.guid&&(d.guid=c.guid),a.handler=d}}function Wb(){Ob(a.event.fixHooks,"filter",a.event.keyHooks,"keydown keyup",Tb),Ob(a.event.fixHooks,"filter",a.event.keyHooks,"keypress",Ub),Ob(a.event.special,"add",{},"keydown keyup keypress",Vb)}function Xb(){return{get:function(a,b,c){return g(Zb(a).getTimbre())},set:function(a,b){Zb(a).setTimbre(f(b,"wave"))}}}function Yb(){return{get:function(a,b,c){return Zb(a).getVolume()},set:function(a,b){Zb(a).setVolume(parseFloat(b))}}}function Zb(b){var c=Oa(b);if(c.instrument)return c.instrument;c.instrument=new ef("piano");var d=a(b);return c.instrument.on("noteon",function(b){var c=a.Event("noteon");c.midi=b.midi,d.trigger(c)}),c.instrument.on("noteoff",function(b){var c=a.Event("noteoff");c.midi=b.midi,d.trigger(c)}),c.instrument}function $b(){return af||(af=new ef),af}function _b(){return!(!Ie.AudioContext&&!Ie.webkitAudioContext)}function ac(){if(ac.audioTop)return ac.audioTop;if(!_b())return null;var a=new(Ie.AudioContext||Ie.webkitAudioContext);return ac.audioTop={ac:a,wavetable:jc(a),out:null,currentStart:null},bc(),ac.audioTop}function bc(){if(ac.audioTop){var a=ac.audioTop;a.out&&(a.out.disconnect(),a.out=null,a.currentStart=null);try{var b=a.ac.createDynamicsCompressor();b.ratio=16,b.attack=5e-4,b.connect(a.ac.destination),a.out=b}catch(c){ac.audioTop=null}}}function cc(){var a=ac();return null!=a.currentStart?a.currentStart:(a.currentStart=Math.max(.25,a.ac.currentTime),setTimeout(function(){a.currentStart=null},0),a.currentStart)}function dc(a){return 440*Math.pow(2,(a-69)/12)}function ec(a){return Math.round(69+12*Math.log(a/440)/Math.LN2)}function fc(a){var b=/^(\^+|_+|=|)([A-Ga-g])([,']*)$/.exec(a);if(!b)return null;var c=b[3].replace(/,/g,"").length-b[3].replace(/'/g,"").length,d=bf[b[2]]+cf[b[1].charAt(0)]*b[1].length+12*c;return d+60}function gc(a){var b=(a-72)%12;(a>60||0!=b)&&(b+=12);for(var c=Math.round((a-b-60)/12),d=df[b];0!=c;)d+=c>0?"'":",",c+=c>0?-1:1;return d}function hc(a){return dc(fc(a))}function ic(a){function b(a,b){switch(a){case"V":A!==z&&c(b.split(" ")[0]);break;case"M":f(b,A);break;case"L":g(b,A);break;case"Q":h(b,A)}A.hasOwnProperty(a)?A[a]+="\n"+b:A[a]=b,"K"==a&&(B=k(b),A===z&&c(d()))}function c(a){a=a||"",(a||A===z)&&(z.voice||(z.voice={}),z.voice.hasOwnProperty(a)?(A=z.voice[a],C=A.accent):(A={id:a,accent:{slurred:0}},z.voice[a]=A,C=A.accent))}function d(){return z.V?z.V.split(/\s+/)[0]:""}function e(a){var e,f=a.match(gf),g=null,h=0,i=0,j=null;if(!f)return null;for(;h/.test(f[h]))i=f[h++].length;else if(/^\(\d+(?::\d+)*/.test(f[h]))j=o(f[h++]);else if(/^[!+].*[!+]$/.test(f[h]))p(f[h++],C);else if(/^.?".*"$/.test(f[h]))h++;else if(/^[()]$/.test(f[h]))"("==f[h++]?C.slurred+=1:(C.slurred-=1,C.slurred<=0&&(C.slurred=0,A.stems&&A.stems.length>=1&&m(A.stems[A.stems.length-1],!1)));else if(/\|/.test(f[h])){for(e in C)1==e.length&&delete C[e];h++}else g=q(f,h,B,C),null!==g?(j&&(n(g.stem,j.time),j.count-=1,j.count||(j=null)),i&&A.stems&&A.stems.length&&(e=i>0?(1-Math.pow(.5,i))*g.stem.time:(Math.pow(.5,-i)-1)*A.stems[A.stems.length-1].time,l(A.stems[A.stems.length-1],e),l(g.stem,-e)),i=0,C.slurred&&m(g.stem,!0),A===z&&c(d()),"stems"in A||(A.stems=[]),A.stems.push(g.stem),h=g.index):h++}function f(a,b){var c=/^C/.test(a)?1:t(a);c&&(b.unitnote||(.75>c?b.unitnote=1/16:b.unitnote=1/8))}function g(a,b){var c=t(a);c&&(b.unitnote=c)}function h(a,b){var c,d=a.split(/\s+|=/),e=null,f=null;for(c=0;c=0||/^[1-4]$/.test(d[c])?e=e||t(d[c]):f=f||Number(d[c]);e&&(b.unitbeat=e),f&&(b.tempo=f)}function i(a){var b,c,d,e,f,g={};for(c=0;c0)for(b=0;a>b&&7>b;++b)d[c.charAt(b)]="^";else for(b=0;b>a&&b>-7;--b)d[c.charAt(6+b)]="_";return d}function k(a){if(!a)return{};var b,c={"c#":7,"f#":6,b:5,e:4,a:3,d:2,g:1,c:0,f:-1,bb:-2,eb:-3,ab:-4,db:-5,gb:-6,cb:-7,"a#m":7,"d#m":6,"g#m":5,"c#m":4,"f#m":3,bm:2,em:1,am:0,dm:-1,gm:-2,cm:-3,fm:-4,bbm:-5,ebm:-6,abm:-7,"g#mix":7,"c#mix":6,"f#mix":5,bmix:4,emix:3,amix:2,dmix:1,gmix:0,cmix:-1,fmix:-2,bbmix:-3,ebmix:-4,abmix:-5,dbmix:-6,gbmix:-7,"d#dor":7,"g#dor":6,"c#dor":5,"f#dor":4,bdor:3,edor:2,ador:1,ddor:0,gdor:-1,cdor:-2,fdor:-3,bbdor:-4,ebdor:-5,abdor:-6,dbdor:-7,"e#phr":7,"a#phr":6,"d#phr":5,"g#phr":4,"c#phr":3,"f#phr":2,bphr:1,ephr:0,aphr:-1,dphr:-2,gphr:-3,cphr:-4,fphr:-5,bbphr:-6,ebphr:-7,"f#lyd":7,blyd:6,elyd:5,alyd:4,dlyd:3,glyd:2,clyd:1,flyd:0,bblyd:-1,eblyd:-2,ablyd:-3,dblyd:-4,gblyd:-5,cblyd:-6,fblyd:-7,"b#loc":7,"e#loc":6,"a#loc":5,"d#loc":4,"g#loc":3,"c#loc":2,"f#loc":1,bloc:0,eloc:-1,aloc:-2,dloc:-3,gloc:-4,cloc:-5,floc:-6,bbloc:-7},d=a.replace(/\s+/g,"").toLowerCase().substr(0,5),e=d.match(/maj|min|mix|dor|phr|lyd|loc|m/);b=e?"maj"==e?d.substr(0,e.index):"min"==e?d.substr(0,e.index+1):d.substr(0,e.index+e[0].length):/^[a-g][#b]?/.exec(d)||"";var f=j(c[b]),g=a.substr(b.length).match(/(_+|=|\^+)[a-g]/gi);if(g)for(var h=0;h<2))switch(a=a.substring(1,a.length-1)){case"pppp":case"ppp":b.dynamics=.2;break;case"pp":b.dynamics=.4;break;case"p":b.dynamics=.6;break;case"mp":b.dynamics=.8;break;case"mf":b.dynamics=1;break;case"f":b.dynamics=1.2;break;case"ff":b.dynamics=1.4;break;case"fff":case"ffff":b.dynamics=1.5}}function q(a,b,c,d){var e,f,g,h,i=[],j="",k=!1,l=null,m=1/0;if(bf&&(j=e,m=f),b0&&"="==a.charAt(0)?a.substr(1):a}function s(a,b,c){var d,e=/^(\^+|_+|=|)([A-Ga-g])(.*)$/.exec(a);return e?(d=e[2].toUpperCase(),e[1].length>0?(c[d]=e[1],r(a)):r(c.hasOwnProperty(d)?c[d]+e[2]+e[3]:b.hasOwnProperty(d)?b[d]+e[2]+e[3]:a)):a}function t(a){var b,c,d,e=/^(\d*)(?:\/(\d*))?$|^(\/+)$/.exec(a),f=0;if(e){if(e[3])return Math.pow(.5,e[3].length);if(c=e[2]?parseFloat(e[2]):/\//.test(a)?2:1,d=0,b=e[1]?parseFloat(e[1]):1,e[2])for(;d+1c;)d+=1,f=parseFloat(e[1].substring(0,d)),b=parseFloat(e[1].substring(d));return f+b/c}}var u,v,w,x,y=a.split("\n"),z={},A=z,B={},C={slurred:0};for(u=0;uc;++c)e[c]=b.real[c],f[c]=b.imag[c];try{return a.createPeriodicWave(e,f)}catch(g){}try{return a.createWaveTable(e,f)}catch(g){}return null}function d(a,b,c){var d,e,f={real:[],imag:[]},g=a.real.length;for(d=0;g>d;++d)e=Math.log(b[Math.min(d,b.length-1)]),f.real.push(a.real[d]*Math.exp(c*e)),f.imag.push(a.imag[d]*Math.exp(c*e));return f}var e,f,g,h,i,j,k={};for(e in b)if(f=b[e],j=c(f)){if(i={wave:j},f.mult)for(h=b[e].freq,i.freq={},g=0;gc;++c)b[c]&&(b[c].constructor===a?e.push.apply(e,b[c].toArray()):a.isArray(b[c])?e.push.apply(e,b[c]):e.push(b[c]));return{elts:a.unique(e),completion:d}}function lc(){function b(){var a,b=g;for(g=null,f&&f(),a=0;a1)for(c=0;c")}function oc(b){var c,d,e=a.extend({},sf,hf,tf);if(b&&!a.isArray(b.helptext)&&b in e&&(b=e[b]),b&&a.isArray(b.helptext)&&b.helptext.length){for(d=0;d/g,'<$1 style="border:1px solid black;text-decoration:none;word-break:keep-all;white-space:nowrap">').replace(/<(mark)>/g,'<$1 style="border:1px solid blue;color:blue;text-decoration:none;word-break:keep-all;white-space:nowrap;cursor:pointer;" onclick="see.enter($(this).text())">'))}return vf}if("number"==typeof b)return nc("Equal to the number "+b+"."),vf;if("boolean"==typeof b)return nc("Equal to the boolean value "+b+"."),vf;if(null===b)return nc("The special null value represents the absence of a value."),vf;if(b===He)return nc("This is an unassigned value."),vf;if(b===Ie)return nc("The global window object represents the browser window."),vf;if(b===document)return nc("The HTML document running the program."),vf;if(b===jQuery)return nc('The jQuery function. Read about it at
jquery.com.'),vf;if(b&&b!=oc)return nc("No help available for "+b),vf;c=[];for(var g in e)!e[g].helptext||!e[g].helptext.length||g in Ie&&"function"!=typeof Ie[g]||c.push(g);return c.sort(function(a,b){return a.length!=b.length?a.length-b.length:b>a?-1:a>b?1:0}),nc("help available for: "+c.map(function(a){return''+a+""}).join(" ")),vf}function pc(a){return 1==a.length&&qc(a[0])&&a[0]}function qc(b){var c;return!(!b||0!=a.queue(b).length||b.parentElement&&b.parentElement.style.transform||0!==(c=pb(b))&&0!==a.fx.speeds[c])}function rc(b,c){var d;if(null==c){if(qc(b))return;d=pb(b)}else d=1e3*c;var e=a(b);d>0&&e.delay(d)}function sc(a,b){if(b=b||0,a.length<=b)return null;var c=a[a.length-1];return"function"!=typeof c||c.helpname?null:c}function tc(b,c,d,e){function f(d,e){if(null!=d){var f=b&&b[d];e&&f&&bb(f,a.data(f,"turtleData"),!0),Hf.reportEvent("resolve",[c,m,j,d,f])}0==--k&&h&&(l?(Me+=1,setTimeout(function(){Me-=1,h()},0)):h())}function g(a){null!=a&&Hf.reportEvent("appear",[c,m,j,a,b&&b[a],i])}var h=sc(d,e),i=h?Array.prototype.slice.call(d,0,d.length-1):d,j=b?b.length||0:0,k=j+1,l=!0,m=Hf.nextId();return Hf.reportEvent("enter",[c,m,j,i]),{name:c,args:i,appear:g,resolve:f,resolver:function(a,b){return function(){f(a,b)}},exit:function(){Hf.reportEvent("exit",[c,m,j,i]),f(null),l=!1}}}function uc(b,c,d,e){var f=function(){if(Vc(b),Le)throw new Error(b+" interrupted");var d,f=tc(this,b,arguments,c),g=[f].concat(a.makeArray(f.args));try{d=e.apply(this,g)}finally{f.exit()}return d};return zc(b,d,f)}function vc(a,b,c){var d=function(){if(Vc(a),Le)throw new Error(a+" interrupted");return Wc(a,this),c.apply(this,arguments)};return zc(a,b,d)}function wc(b,c,d,e){var f=function(){if(Vc(b),Le)throw new Error(b+" interrupted");var c=null,f=0,g=pf;if(e&&(c=e.apply(null,arguments),f=arguments.length,g=Yc()),g){var h=a(pf).eq(0),i=arguments,j=tc(h,b,arguments,f);h.plan(function(a,b){j.appear(a),d.apply(c,i),this.plan(j.resolver(a))}),j.exit()}else j=tc(null,b,arguments,f),d.apply(c,arguments),j.exit();return c?(c.result&&c.result.constructor===jQuery&&pf&&lc(pf,c.result),c.result):void 0};return zc(b,c,f)}function xc(b,c){return zc(b,c,function(c,d){var e=/^key/.test(b),f=/^mouse|click$/.test(b),g=f?"input,button":e?"textarea,input:not([type]),input[type=text],input[type=password]":null;e&&Nb(),null==d&&"function"==typeof c&&(d=c,c=null),a(Ie).on(b+".turtleevent",null,c,g?function(b){return Le||a(b.target).closest(g).length?void 0:d.apply(this,arguments)}:d)})}function yc(){var b=a._data(Ie,"events");if(!b)return!1;for(var c in b)for(var d=b[c],e=0;eb?-c:c,l=null;i.style&&i.down&&(l=function(){var a=Y(h),c=ra(h,!0),d=z(h.parentElement);return function(){ub(i.corners[0],a,c.rot,c.rot+(e?-b:b),k*(i.oldscale?c.sy:1),d)}}()),i.turningRadius=k,this.animate({turtleRotation:g+sa(b)+"deg"},pb(h,f),qb(h)),this.plan(function(){l&&l(),i.turningRadius=j,a.resolve(d,!0)})}),this)}function Bc(a,b){null==b&&(b=100),"bk"===a.name&&(b=-b);var c,d=Df;return(c=pc(this))?(a.appear(0),ib(c,b,0),a.resolve(0,!0),this):(this.plan(function(c,e){a.appear(c),this.animate({turtleForward:"+="+sa(b||0)+"px"},pb(e,d),qb(e),a.resolver(c,!0))}),this)}function Cc(b,c,d){a.isArray(c)&&(d=c[1],c=c[0]),d||(d=0),c||(c=0);var e=Df;return this.plan(function(a,f){b&&b.appear(a),this.animate({turtlePosition:lb(f,d,c)},pb(f,e),qb(f),b&&b.resolver(a,!0))}),this}function Dc(b,c,d){a.isArray(c)&&(d=c[1],c=c[0]),d||(d=0),c||(c=0);var e,f=Df;return(e=pc(this))?(b&&b.appear(0),jb(e,c,d),b&&b.resolve(0),this):(this.plan(function(a,e){b&&b.appear(a);var g=w(e);this.animate({turtlePosition:sa(g[0]+c)+" "+sa(g[1]-d)},pb(e,f),qb(e),b&&b.resolver(a,!0))}),this)}function Ec(b,c,d){var e=c,f=0,g=0,h=null,i=Df;return a.isNumeric(e)&&a.isNumeric(d)?(f=parseFloat(e),g=parseFloat(d),e=null,h=null):a.isArray(e)?(f=e[0],g=e[1],e=null,h=d):a.isNumeric(d)&&(h=d),this.plan(function(c,d){var j=e;if(null===j&&(j=a(V(d)).pagexy()),j&&!mb(j))try{j=a(j).pagexy()}catch(k){return}return j&&mb(j)?a.isWindow(d)?(b&&b.appear(c),ba(j,h),void(b&&b.resolve(c))):void(9!==d.nodeType&&(b&&b.appear(c),this.animate({turtlePosition:U(d,j,h,f,g)},pb(d,i),qb(d),b&&b.resolver(c,!0)))):void 0}),this}function Fc(a){return function(b,c,d){return this.plan(function(e,f){b.appear(e);var g=this.css("turtlePenDown");this.css({turtlePenDown:"up"}),a.call(this,null,c,d),this.plan(function(){this.css({turtlePenDown:g}),b.resolve(e,!0)})}),this}}function Gc(b){var c=a.data(b,"turtleData");return c&&null!=c.oldscale?c.oldscale:1}function Hc(a,b,c){Jc.call(this,!0,a,b,c)}function Ic(a,b,c){Jc.call(this,!1,a,b,c)}function Jc(b,c,d,e){e===He&&(e=d),d&&e||(d=e=1);var f=Df;return this.plan(function(g,h){if(b&&(Oa(h).oldscale*=e),c.appear(g),a.isWindow(h)||9===h.nodeType)return void c.resolve(g);var i=a.map(a.css(h,"turtleScale").split(" "),parseFloat);1===i.length&&i.push(i[0]),i[0]*=d,i[1]*=e,this.animate({turtleScale:a.map(i,sa).join(" ")},pb(h,f),qb(h),c.resolver(g))}),this}function Kc(a,b){var c=z(a.parentElement),d=l(c),e=d?1:u(c)[1];return e*Gc(a)}function Lc(b){var c=Df;return function(d,e,f){if(a.isNumeric(e)){var g=e;e=f,f=g}return null==f&&(f=8.8),this.plan(function(g,h){var i=Oa(h),j=i.style;e||(e=j&&(j.fillStyle||j.strokeStyle)||"black"),d.appear(g);var k=this.pagexy(),l=ra(h,!0),m=Ka(e,"fillStyle"),n=Da(i),o=Kc(h),p=f*o,q=Math.max(0,p-2),r=p+(m.eraseMode?2:0),s=/rgba|hsla/.test(m.fillStyle);null==m.lineWidth&&j&&j.lineWidth&&(m.lineWidth=j.lineWidth),pc(this)?(b(n,k,r,l.rot,m,!0),d.resolve(g)):this.queue(function(e){a({radius:0}).animate({radius:q},{duration:pb(h,c),step:function(){s||b(n,k,this.radius,l.rot,m,!1)},complete:function(){b(n,k,r,l.rot,m,!0),d.resolve(g),e()}})})}),this}}function Mc(a,b,c,d,e){var f=a.getContext("2d");f.save(),Xa(f,e),c===1/0?(f.setTransform(1,0,0,1,0,0),f.fillRect(0,0,a.width,a.height)):(Za(f,a),f.beginPath(),f.arc(b.pageX,b.pageY,c/2,0,2*Math.PI,!1),f.closePath(),f.fill(),e.strokeStyle&&f.stroke()),f.restore()}function Nc(a,b,c,d,e){var f=a.getContext("2d");if(f.save(),Xa(f,e),c===1/0)f.setTransform(1,0,0,1,0,0),f.fillRect(0,0,a.width,a.height);else{var g=Math.sin((d+45)/180*Math.PI),h=Math.cos((d+45)/180*Math.PI),i=c*h/Math.SQRT2,j=c*g/Math.SQRT2;Za(f,a),f.beginPath(),f.moveTo(b.pageX-i,b.pageY-j),f.lineTo(b.pageX-j,b.pageY+i),f.lineTo(b.pageX+i,b.pageY+j),f.lineTo(b.pageX+j,b.pageY-i),f.closePath(),f.fill(),e.strokeStyle&&f.stroke()}f.restore()}function Oc(a,b,c,d,e,f){var g=a.getContext("2d");if(g.save(),Xa(g,e),!e.strokeStyle&&e.fillStyle&&(g.strokeStyle=e.fillStyle),c!==1/0){var h=Math.sin(d/180*Math.PI),i=-Math.cos(d/180*Math.PI),j=e.lineWidth||1.62,k=b.pageX+c*h,l=b.pageY+c*i,m=Pc(j,k,l,h,i),n=c-m.hs,o=n*h,p=n*i;Za(g,a),n>0&&(g.beginPath(),g.moveTo(b.pageX,b.pageY),g.lineTo(b.pageX+o,b.pageY+p),g.stroke()),f&&Qc(g,m)}g.restore()}function Pc(a,b,c,d,e){var f=Math.max(1.25*a,a+2),g=2*f,h=g-f/2;return{hs:h,x1:b,y1:c,xm:b-d*h,ym:c-e*h,x2:b-e*f-d*g,y2:c+d*f-e*g,x3:b+e*f-d*g,y3:c-d*f-e*g}}function Qc(a,b){a.beginPath(),a.moveTo(b.x2,b.y2),a.lineTo(b.x1,b.y1),a.lineTo(b.x3,b.y3),a.quadraticCurveTo(b.xm,b.ym,b.x2,b.y2),a.closePath(),a.fill()}function Rc(a,b,c,d,e,f){var g=e-c,h=f-d,i=Math.sqrt(g*g+h*h),j=g/i,k=h/i,l=Pc(b,e,f,j,k);i>l.hs&&(a.beginPath(),a.moveTo(c,d),a.lineTo(l.xm,l.ym),a.lineWidth=b,a.stroke()),Qc(a,l)}function Sc(a,b){function c(){d&&clearInterval(d),b&&b()}var d=null;if(!Ie.speechSynthesis)return console.log("No speech synthesis: "+a),void c();try{var e=new Ie.SpeechSynthesisUtterance(a);e.addEventListener("end",c),e.addEventListener("error",c),e.lang=navigator.language||"en-GB",Ie.speechSynthesis.speak(e),d=setInterval(function(){Ie.speechSynthesis.pending||Ie.speechSynthesis.speaking||c()},250)}catch(f){Ie.console&&Ie.console.log(f),c()}}function Tc(){a.each(["toggle","show","hide"],function(b,c){var d=a.fn[c];a.fn[c]=function(b,c,e){var f=arguments;!f.length&&this.hasClass("turtle")&&(this.length>1||!a._data(this[0],"fxshow"))&&(f=[0]),d.apply(this,f)}})}function Uc(c,d){if(d||(d="fx"),"IMG"==c.tagName&&c.src&&!c.complete){var e=a.queue(c,d);0==e.length&&(a.queue(c,d,function(a){Ib(c,null,a)}),b(c,d))}}function Vc(b){if(!(a.turtle.hangtime==1/0||kf++<100)){kf=0;var c=(new Date).getTime();return mf?void(c-mf>a.turtle.hangtime&&(Sf.visible()&&Sf.html('Oops: program interrupted because it was hanging the browser. Try reducing the number of repetitions. Or try using await done defer() or tick to make an animation.'),a.turtle.interrupt("hung"))):(mf=c,clearTimeout(lf),void(lf=setTimeout(function(){clearTimeout(lf),lf=null,mf=null},0)))}}function Wc(b,c){if(!a.turtle.nowarn){var d,e=!0;for(d=0;e&&d=100&&(e=!1);e||(jf[b]||(jf[b]=1,Sf.visible()?Sf.html('Oops: '+b+' may not return useful results when motion is queued. Try speed Infinity or await done defer() first.'):console.warn(b+' may not return useful results when motion is queued. Try "speed Infinity" or "await done defer()".')),c.finish())}}function Xc(a,b,d){a[b]=function(){return b in jf||(Sf.html(''+b+" deprecated. Use "+d+"."),jf[b]=1),a[d].apply(this,arguments)},a[d].__super__&&c(a[b],a[d])}function Yc(){return pf&&a.queue(pf).length>0}function Zc(a){if(null===uf.pollTimer){var b=uf.sent[a],c=uf.waiting[a];c&&c.length&&b&&b.length&&(uf.pollTimer=setTimeout(function(){uf.pollTimer=null,c&&c.length&&b&&b.length&&(c.shift().apply(null,b.shift()),Zc(a))},0))}}function $c(a,b){if("function"!=typeof b&&"undefined"!=typeof b||!/^\w+\s*$/.test(a)){if("undefined"==typeof b&&/^help\s+\S+$/.test(a))return oc(/^help\s+(\S+)$/.exec(a)[1]),!0}else{if(b&&b.helptext)return oc(b),!0;if(a in tf)return oc(a),!0}return!1}function _c(a,b,c,d){return a.helptext?d.helptext=a.helptext:b in c&&(d.helptext=c[b].helptext),d.method=a,d.helpname=b,d}function ad(a,b){var c=[];for(var d in b)!b.hasOwnProperty(d)||d in Ie||(c.push(d),Ie[d]=function(b){var c=a[b],d=a;return _c(c,b,tf,function(){return c.apply(d,arguments)})}(d));return c}function bd(){pf=null;for(var a=0;a=0;i--)null===h[i]?(i--,c.moveTo(d-h[i][0],e+h[i][1])):c.lineTo(d-h[i][0],e+h[i][1])}return c.lineWidth=1.1,c.strokeStyle="rgba(255,255,255,0.75)",c.stroke(),c.beginPath(),c.arc(d,e,15.5,0,2*Math.PI,!1),c.closePath(),c.strokeStyle="rgba(0,0,0,0.4)",c.stroke(),b.toDataURL()}function gd(a){var b=Ga(40,48),c=b.getContext("2d");return c.beginPath(),c.moveTo(0,48),c.lineTo(20,0),c.lineTo(40,48),c.lineTo(20,36),c.closePath(),c.fillStyle=a,c.fill(),b.toDataURL()}function hd(a){var b=Ga(40,40),c=b.getContext("2d");return c.beginPath(),c.arc(20,20,18,-5*Math.PI/2,-Math.PI/2),c.closePath(),c.lineTo(20,20),c.stroke(),c.strokeStyle=a,c.lineWidth=4,c.stroke(),b.toDataURL()}function id(a,b){b=b||12;var c=Ga(b,b),d=c.getContext("2d"),e=b/2;return d.beginPath(),d.arc(e,e,e,0,2*Math.PI),d.closePath(),d.fillStyle=a,d.fill(),c.toDataURL()}function jd(a){function b(){e.moveTo(19.5,43),e.lineTo(20.5,43),e.lineTo(21.5,43.5),e.lineTo(25.5,36.2),e.lineTo(24,35.5),e.lineTo(23,35.5),e.lineTo(20.5,36.5),e.lineTo(19.5,36.5),e.lineTo(17,35.5),e.lineTo(16,35.5),e.lineTo(14.5,36.2),e.lineTo(18.5,43.5),e.closePath()}function c(){e.moveTo(25.5,12),e.lineTo(25.5,8),e.lineTo(14.5,8),e.lineTo(14.5,12),e.closePath()}var d=Ga(40,48),e=d.getContext("2d");return e.beginPath(),b(),e.fillStyle="#ffcb6b",e.fill(),e.beginPath(),c(),e.fillStyle="#d1ebff",e.fill(),e.beginPath(),e.moveTo(19.5,48),e.lineTo(13,36),e.lineTo(13,1),e.lineTo(14,0),e.lineTo(26,0),e.lineTo(27,1),e.lineTo(27,36),e.lineTo(20.5,48),e.closePath(),b(),e.moveTo(25.5,12),e.lineTo(25.5,8),e.lineTo(14.5,8),e.lineTo(14.5,12),e.closePath(),e.fillStyle=a,e.fill(),d.toDataURL()}function kd(a,b,c){return c||(c=1),function(d){var e=document.createElement("canvas");e.width=a,e.height=b;var f=e.getContext("2d");d||(d="rgba(128,128,128,0.125)"),"transparent"!=d&&(f.fillStyle=d,f.fillRect(0,0,a,b));var g=a/c,h=b/c,i={width:g,height:h,transformOrigin:g/2+"px + "+h/2+"px",opacity:1};return 1>c&&(i.imageRendering="pixelated"),{img:e,css:i}}}function ld(a){if(!a)return null;if(a in yf)return yf[a];var b=a.match(/^(\d+)x(\d+)(?:\/(\d+))?$/);return b?kd(parseFloat(b[1]),parseFloat(b[2]),b[3]&&parseFloat(b[3])):null}function md(a,b){var c=a.width||a.height||256,d=a.height||a.width||256,e=a.subpixel||1/(a.scale||1),f=a.color||"transparent",g=kd(c,d,e);return g(f)}function nd(b,c){if(!b)return null;if(a.isPlainObject(b))return md(b,c);if(a.isFunction(b)&&(b.helpname||b.name)&&(b=b.helpname||b.name),b.constructor===jQuery){if(!b.length)return null;b=b.get(0)}if(b.tagName)return"CANVAS"!=b.tagName&&"IMG"!=b.tagName&&"VIDEO"!=b.tagName?null:{img:b,css:{opacity:1}};var d=b.toString().trim().split(/\s+/),e=null,f=null;if(d.length&&(f=ld(d[d.length-1]),f&&d.pop()),d.length&&cd(d.join(" "))&&(e=d.join(" "),d.length=0),!f&&e&&(f=ld(c)),f)return f(e);if(/\//.test(b)||(b=Fb(b)),/\//.test(b)){var g=Ab(b).hostname;return!Cb(g)&&Cb(Ie.location.hostname)&&(b=Ie.location.protocol+"//"+Ie.location.host+"/proxy/"+Bb(b)),{url:b,css:{transformOrigin:"50% 50%",opacity:1}}}return null}function od(a){return null==a?"":String(a).replace(/[&<>"]/g,function(a){return zf[a]})}function pd(b,c,d){var e=b&&/^[a-zA-Z]\w*$/.exec(b),f=b&&/^<.*>$/.exec(b),g=nd(b,d)||null==b&&nd(d);e&&a("#"+b).length&&(e=!1);var h;f?h=a(b):g?(h=a(""),hb(h,g)):h=a("
"+od(b)+"
"),b&&"object"==typeof b&&("id"in b&&h.attr("id",b.id),"class"in b&&h.addClass(b["class"])),h.css({position:"absolute",display:"table",top:0,left:0}),(!c||9==c.nodeType||a.isWindow(c))&&(c=za()),h.appendTo(c);var i=y(h[0]);return h.css({top:-i[1],left:-i[0]}),h.css({turtlePosition:U(h[0],a(c).pagexy(),null,0,0),turtleRotation:0,turtleScale:1}),h.addClass("turtle"),e&&(h.attr("id",b),rf&&!Ie.hasOwnProperty(b)&&(Ie[b]=h)),h}function qd(a,b){if("number"==typeof a)return a=Math.ceil(a),"number"==typeof b?(b=Math.ceil(b),Math.floor(Math.random()*(b-a)+a)):Math.floor(Math.random()*a);if("object"==typeof a&&a&&a.length&&a.slice)return a[Math.floor(Math.random()*a.length)];if("normal"==a){var c,d,e,f,g;do c=Math.random(),d=1.7156*(Math.random()-.5),e=c-.449871,f=Math.abs(d)+.386595,g=e*e+f*(.196*f-.25472*e);while(g>.27597&&(g>.27846||d*d>-4*Math.log(c)*c*c));return d/c}return"position"==a?{pageX:qd(K()+1),pageY:qd(J()+1)}:"color"==a?"hsl("+Math.floor(360*Math.random())+",100%,50%)":"gray"==a?"hsl(0,0,"+Math.floor(100*Math.random())+"%)":a===!0?Math.random()>=.5:Math.random()}function rd(b,c){c||"function"!=typeof b||(c=b,b=30);var d=null,e=Math.max(Math.floor(1e3/Math.max(1/86400,b)),0);if(Yc()){var f=a(pf);f.plan(function(){d=c})}else d=c;var g={fn:c,timer:setInterval(function(){if(d)try{Df++,Bf.push(g),d()}finally{Df--,Bf.pop(g)}},e)};return Af.push(g),g.timer}function sd(a){for(var b=[],c=0;c0?1e3/b:0)}function wd(b){b===He&&(b=""),a("[id]").each(function(c,d){Ie[b+d.id]=a("#"+d.id)}),rf=!0}function xd(b){if(b===He&&(b="last"),Ef&&a(Ie).off(a.map(of,function(a,b){return b}).join(" "),Ef),b||""===b){Ef=function(a){var c,d=[b+a.type];for((a.originalEvent||a)instanceof MouseEvent&&d.push(b+"mouse"),c=0;c=a("html").outerHeight(!0);if(b(),e){var f=a(Ie).scrollTop(),g=Math.min(d,a("html").outerHeight(!0)-a(Ie).height());g>f&&a(Ie).scrollTop(g)}}function zd(){if(!Ff.timer){Ff.timer=setTimeout(function(){Ff.timer=null},0);var b=a("body").offset(),c=b?b.top:8;Ff.bottomSeen=Math.min(a(Ie).height()+a(Ie).scrollTop(),a("body").height()+c)}return Ff.bottomSeen}function Ad(b){var c=a(Ie).scrollTop();b(),a(Ie).scrollTop(c)}function Bd(){var a=document.body.lastChild;return a&&"PRE"==a.tagName||(a=document.createElement("pre"),document.body.appendChild(a)),a}function Cd(){var a=arguments;yd(function(){for(var b=Bd(),c=0;c").css({display:"inline-block",verticalAlign:"top",textAlign:"center",height:"1.2em",width:"1.2em",maxWidth:"1.2em",overflow:"hidden"}).appendTo(Bd()),e=function(){null!=b&&d.css({background:b}),null!=c&&d.text(c)};if(pf){var f=a(pf);Ec.call(f,null,d),f.eq(0).plan(e)}else e()}function Ed(){var a=this;yd(function(){a.result.appendTo("body"),a.setup&&a.setup()})}function Fd(b,c){var d="<"+c+' style="display:table">',e="";if(b===He||null===b)return{result:a(d+"
"+e)};if(b.jquery||b instanceof Element&&(b=a(b)))return{result:b};var f=null;return b=""+b,/^\s*<.*>\s*$/.test(b)&&(f=a(b)),(null==f||1!=f.length||1!=f[0].nodeType)&&(f=a(d+b+e)),{result:f}}function Gd(b,c){function d(b){return function(d){j||m&&"change"==d.type||(j=!0,a(this).prop("checked",!0),i.find("input[type=radio]").prop("disabled",!0),c(b))}}function e(a){return function(){j||(l=a,f())}}function f(a){a||(m+=1,setTimeout(function(){m-=1},0),i.find("input").eq(l).prop("checked",!0)),i.find("input").eq(l).focus()}function g(b,c){a.isFunction(b)&&(b=(k+1).toString());var f=a.isFunction(c)||null==c?b:c,g=a('').attr("value",f).on("change click",d(c)),h=a('