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 = $('