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/.travis.yml b/.travis.yml index 35048b6..cf003a3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ language: node_js node_js: - '0.10' +install: PHANTOMJS_CDNURL=http://cnpmjs.org/downloads npm install 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 c0e8f94..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 } } }, @@ -42,7 +53,8 @@ module.exports = function(grunt) { qunit: { all: ["test/*.html"], options: { - timeout: 100000 + timeout: 100000, + noGlobals: true } }, release: { 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 3613692..9008141 100644
--- a/jquery-turtle.js
+++ b/jquery-turtle.js
@@ -4,11 +4,12 @@
jQuery-turtle
=============
-version 2.0.8
+version 2.0.9
jQuery-turtle is a jQuery plugin for turtle graphics.
With jQuery-turtle, every DOM element is a turtle that can be
+
moved using turtle graphics methods like fd (forward), bk (back),
rt (right turn), and lt (left turn). The pen function allows
a turtle to draw on a full-document canvas as it moves.
@@ -61,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).slide(x, y) // Slide right by x while sliding forward by y.
-$(q).jump(x, y) // Like slide, 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.
@@ -216,6 +217,7 @@ $.turtle() are as follows:
lastclick // Event object of the last click event in the doc.
+lastdblclick // The last double-click event.
lastmousemove // The last mousemove event.
lastmouseup // The last mouseup event.
lastmousedown // The last mousedown event.
@@ -231,6 +233,7 @@ timer(secs, fn) // Calls back fn once after secs seconds.
tick([perSec,] fn) // Repeatedly calls fn at the given rate (null clears).
done(fn) // Calls back fn after all turtle animation is complete.
random(n) // Returns a random number [0..n-1].
+random(n,m) // Returns a random number [n..m-1].
random(list) // Returns a random element of the list.
random('normal') // Returns a gaussian random (mean 0 stdev 1).
random('uniform') // Returns a uniform random [0...1).
@@ -258,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
@@ -365,11 +368,28 @@ THE SOFTWARE.
//////////////////////////////////////////////////////////////////////////
var undefined = void 0,
+ global = this,
__hasProp = {}.hasOwnProperty,
rootjQuery = jQuery(function() {}),
interrupted = false,
async_pending = 0,
- global_plan_counter = 0;
+ global_plan_depth = 0,
+ global_plan_queue = [];
+
+function nonrecursive_dequeue(elem, qname) {
+ if (global_plan_depth > 5) {
+ global_plan_queue.push({elem: elem, qname: qname});
+ } else {
+ global_plan_depth += 1;
+ $.dequeue(elem, qname);
+ while (global_plan_queue.length > 0) {
+ var task = global_plan_queue.shift();
+ $.dequeue(task.elem, task.qname);
+ checkForHungLoop()
+ }
+ global_plan_depth -= 1;
+ }
+}
function __extends(child, parent) {
for (var key in parent) {
@@ -427,13 +447,15 @@ if (!transform || !hasGetBoundingClientRect()) {
// and that first value will be interpreted as defaultProp:value1.
// Some rudimentary quoting can be done, e.g., value:"prop", etc.
function parseOptionString(str, defaultProp) {
- if (str == null) {
- return {};
- }
- if ($.isPlainObject(str)) {
- return str;
+ if (typeof(str) != 'string') {
+ if (str == null) {
+ return {};
+ }
+ if ($.isPlainObject(str)) {
+ return str;
+ }
+ str = '' + str;
}
- str = '' + str;
// Each token is an identifier, a quoted or parenthesized string,
// a run of whitespace, or any other non-matching character.
var token = str.match(/[-a-zA-Z_][-\w]*|"[^"]*"|'[^']'|\([^()]*\)|\s+|./g),
@@ -731,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;
@@ -750,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.
@@ -761,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);
}
@@ -929,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'),
@@ -937,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];
@@ -958,23 +981,14 @@ function getTurtleOrigin(elem, inverseParent, extra) {
return result;
}
-function unattached(elt) {
- // Unattached if not part of a document.
- while (elt) {
- if (elt.nodeType === 9) return false;
- elt = elt.parentNode;
- }
- return true;
-}
-
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() {
@@ -985,6 +999,10 @@ function dw() {
return document.body ? $(document).width() : document.width;
}
+function invisible(elem) {
+ return elem.offsetHeight <= 0 && elem.offsetWidth <= 0;
+}
+
function makeGbcrLTWH(left, top, width, height) {
return {
left: left, top: top, right: left + width, bottom: top + height,
@@ -997,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)) {
@@ -1040,23 +1058,11 @@ function polyMatchesGbcr(poly, gbcr) {
function readPageGbcr() {
var raw = this.getBoundingClientRect();
- if (raw.width === 0 && raw.height === 0 &&
- raw.top === 0 && raw.left === 0 && unattached(this)) {
- // Prentend unattached images have a size.
- return {
- top: 0,
- bottom: this.height || 0,
- left: 0,
- right: this.width || 0,
- width: this.width || 0,
- height: this.height || 0
- }
- }
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
};
@@ -1084,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;
}
@@ -1113,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];
}
@@ -1122,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] });
}
@@ -1141,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());
}
@@ -1156,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;
@@ -1176,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());
}
@@ -1223,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);
}
@@ -1366,6 +1388,7 @@ function convexHull(points) {
function parseTurtleHull(text) {
if (!text) return null;
+ if ($.isArray(text)) return text;
var nums = $.map(text.trim().split(/\s+/), parseFloat), points = [], j = 0;
while (j + 1 < nums.length) {
points.push({ pageX: nums[j], pageY: nums[j + 1] });
@@ -1450,7 +1473,7 @@ function readTurtleTransform(elem, computed) {
function cssNum(n) {
var r = n.toString();
- if (r.indexOf('e') >= 0) {
+ if (~r.indexOf('e')) {
r = Number(n).toFixed(17);
}
return r;
@@ -1481,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; }
@@ -1488,7 +1513,7 @@ function radiansToDegrees(r) {
}
function convertToRadians(d) {
- return d * Math.PI / 180;
+ return d / 180 * Math.PI;
}
function normalizeRotation(x) {
@@ -1521,6 +1546,8 @@ var globalDrawing = {
ctx: null,
canvas: null,
timer: null,
+ fieldMouse: false, // if a child of the field listens to mouse events.
+ fieldHook: false, // once the body-event-forwarding logic is set up.
subpixel: 1
};
@@ -1558,50 +1585,43 @@ function createSurfaceAndField() {
// fixes a "center" point in page coordinates that
// will not change even if the document resizes.
transformOrigin: cw + "px " + ch + "px",
+ pointerEvents: 'none',
overflow: 'hidden'
- });
- $(field).attr('id', 'field')
+ }).addClass('turtlefield');
+ $(field).attr('id', 'origin')
.css({
position: 'absolute',
display: 'inline-block',
- top: ch, left: cw, width: '100%', height: '100%',
+ // Setting with to 100% allows label text to not wrap.
+ top: ch, left: cw, width: '100%', height: '0',
font: 'inherit',
// Setting transform origin for the turtle field
// fixes a "center" point in page coordinates that
// will not change even if the document resizes.
transformOrigin: "0px 0px",
+ 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;
attachClipSurface();
+ // Now that we have a surface, the upward-center cartesian coordinate
+ // system based on that exists, so we can hook mouse events to add x, y.
+ addMouseEventHooks();
}
function attachClipSurface() {
if (document.body) {
+ if ($('html').attr('style') == null) {
+ // This prevents the body from shrinking.
+ $('html').css('min-height', '100%');
+ }
$(globalDrawing.surface).prependTo('body');
// Attach an event handler to forward mouse events from the body
// to turtles in the turtle field layer.
- $('body').on('click.turtle ' +
- 'mouseup.turtle mousedown.turtle mousemove.turtle', function(e) {
- if (e.target === this && !e.isTrigger) {
- // Only forward events directly on the body that (geometrically)
- // touch a turtle directly within the turtlefield.
- var warn = $.turtle.nowarn;
- $.turtle.nowarn = true;
- var sel = $(globalDrawing.surface)
- .find('.turtle').within('touch', e).eq(0);
- $.turtle.nowarn = warn;
- if (sel.length === 1) {
- // Erase portions of the event that are wrong for the turtle.
- e.target = null;
- e.relatedTarget = null;
- e.fromElement = null;
- e.toElement = null;
- sel.trigger(e);
- return false;
- }
- }
- });
+ forwardBodyMouseEventsIfNeeded();
} else {
$(document).ready(attachClipSurface);
}
@@ -1637,7 +1657,7 @@ function getTurtleDrawingCanvas() {
surface.insertBefore(globalDrawing.canvas, surface.firstChild);
resizecanvas();
pollbodysize(resizecanvas);
- $(window).resize(resizecanvas);
+ $(global).resize(resizecanvas);
return globalDrawing.canvas;
}
@@ -1685,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())
];
}
@@ -1769,14 +1789,16 @@ function getTurtleData(elem) {
if (!state) {
state = $.data(elem, 'turtleData', {
style: null,
+ corners: [[]],
path: [[]],
- down: true,
+ down: false,
speed: 'turtle',
easing: 'swing',
turningRadius: 0,
drawOnCanvas: null,
quickpagexy: null,
quickhomeorigin: null,
+ oldscale: 1,
instrument: null,
stream: null
});
@@ -1822,10 +1844,16 @@ function makePenStyleHook() {
return writePenStyle(getTurtleData(elem).style);
},
set: function(elem, value) {
- var style = parsePenStyle(value, 'strokeStyle');
- getTurtleData(elem).style = style;
+ var style = parsePenStyle(value, 'strokeStyle'),
+ state = getTurtleData(elem);
+ if (state.style) {
+ // Switch to an empty pen first, to terminate paths.
+ state.style = null;
+ flushPenState(elem, state, true);
+ }
+ state.style = style;
elem.style.turtlePenStyle = writePenStyle(style);
- flushPenState(elem);
+ flushPenState(elem, state, true);
}
};
}
@@ -1844,7 +1872,7 @@ function makePenDownHook() {
state.quickpagexy = null;
state.quickhomeorigin = null;
elem.style.turtlePenDown = writePenDown(style);
- flushPenState(elem);
+ flushPenState(elem, state, true);
}
}
};
@@ -1855,6 +1883,11 @@ function isPointNearby(a, b) {
Math.round(a.pageY - b.pageY) === 0;
}
+function isPointVeryNearby(a, b) {
+ return Math.round(1000 * (a.pageX - b.pageX)) === 0 &&
+ Math.round(1000 * (a.pageY - b.pageY)) === 0;
+}
+
function isBezierTiny(a, b) {
return isPointNearby(a, b) &&
Math.round(a.pageX - b.pageX1) === 0 &&
@@ -1875,13 +1908,18 @@ function applyPenStyle(ctx, ps, scale) {
scale = scale || 1;
var extraWidth = ps.eraseMode ? 1 : 0;
if (!ps || !('strokeStyle' in ps)) { ctx.strokeStyle = 'black'; }
- if (!ps || !('lineWidth' in ps)) { ctx.lineWidth = 1.62 * scale + extraWidth; }
+ if (!ps || !('lineWidth' in ps)) {
+ ctx.lineWidth = 1.62 * scale + extraWidth;
+ }
if (!ps || !('lineCap' in ps)) { ctx.lineCap = 'round'; }
+ if (!ps || !('lineJoin' in ps)) { ctx.lineJoin = 'round'; }
if (ps) {
for (var a in ps) {
if (a === 'savePath' || a === 'eraseMode') { continue; }
if (scale && a === 'lineWidth') {
ctx[a] = scale * ps[a] + extraWidth;
+ } else if (a === 'lineDash') {
+ ctx.setLineDash(('' + ps[a]).split(/[,\s]/g));
} else {
ctx[a] = ps[a];
}
@@ -1939,7 +1977,9 @@ function setCanvasPageTransform(ctx, canvas) {
}
}
-function drawAndClearPath(drawOnCanvas, path, style, scale) {
+var buttOverlap = 0.67;
+
+function drawAndClearPath(drawOnCanvas, path, style, scale, truncateTo) {
var ctx = drawOnCanvas.getContext('2d'),
isClosed, skipLast,
j = path.length,
@@ -1952,10 +1992,13 @@ function drawAndClearPath(drawOnCanvas, path, style, scale) {
while (j--) {
if (path[j].length > 1) {
segment = path[j];
- isClosed = segment.length > 2 && isPointNearby(
- segment[0], segment[segment.length - 1]);
+ isClosed = segment.length > 2 &&
+ isPointNearby(segment[0], segment[segment.length - 1]) &&
+ !isPointNearby(segment[0], segment[Math.floor(segment.length / 2)]);
skipLast = isClosed && (!('pageX2' in segment[segment.length - 1]));
- ctx.moveTo(segment[0].pageX, segment[0].pageY);
+ var startx = segment[0].pageX;
+ var starty = segment[0].pageY;
+ ctx.moveTo(startx, starty);
for (var k = 1; k < segment.length - (skipLast ? 1 : 0); ++k) {
if ('pageX2' in segment[k] &&
!isBezierTiny(segment[k - 1], segment[k])) {
@@ -1974,7 +2017,7 @@ function drawAndClearPath(drawOnCanvas, path, style, scale) {
if ('strokeStyle' in style) { ctx.stroke(); }
ctx.restore();
path.length = 1;
- path[0].splice(0, path[0].length - 1);
+ path[0].splice(0, Math.max(0, path[0].length - truncateTo));
}
function addBezierToPath(path, start, triples) {
@@ -1989,38 +2032,70 @@ function addBezierToPath(path, start, triples) {
}
}
-function flushPenState(elem) {
- var state = getTurtleData(elem);
- if (!state.style || (!state.down && !state.style.savePath)) {
- if (state.path.length > 1) { state.path.length = 1; }
- if (state.path[0].length) { state.path[0].length = 0; }
+function addToPathList(pathList, point) {
+ if (pathList.length &&
+ (point.corner ? isPointVeryNearby(point, pathList[pathList.length - 1]) :
+ isPointNearby(point, pathList[pathList.length - 1]))) {
+ return;
+ }
+ pathList.push(point);
+}
+
+function flushPenState(elem, state, corner) {
+ clearChildQuickLocations(elem);
+ if (!state) {
+ // Default is no pen and no path, so nothing to do.
return;
}
- if (!state.down) {
- // Penup when saving path will start a new segment if one isn't started.
- if (state.path.length && state.path[0].length) {
- state.path.unshift([]);
+ var path = state.path,
+ style = state.style,
+ corners = state.corners;
+ if (!style || !state.down) {
+ // pen up or pen null will clear the tracing path.
+ if (path.length > 1) { path.length = 1; }
+ if (path[0].length) { path[0].length = 0; }
+ if (corner) {
+ if (!style) {
+ // pen null will clear the retracing path too.
+ if (corners.length > 1) corners.length = 1;
+ if (corners[0].length) corners[0].length = 0;
+ } else {
+ // pen up with a non-null pen will start a new discontinuous segment.
+ if (corners.length && corners[0].length) {
+ if (corners[0].length == 1) {
+ corners[0].length = 0;
+ } else {
+ corners.unshift([]);
+ }
+ }
+ }
}
return;
}
+ if (!corner && style.savePath) return;
+ // Accumulate retracing path using only corners.
var center = getCenterInPageCoordinates(elem);
- if (!state.path[0].length ||
- !isPointNearby(center, state.path[0][state.path[0].length - 1])) {
- state.path[0].push(center);
- }
- if (!state.style.savePath) {
- var ts = readTurtleTransform(elem, true);
- drawAndClearPath(getDrawOnCanvas(state), state.path, state.style, ts.sx);
+ if (corner) {
+ center.corner = true;
+ addToPathList(corners[0], center);
}
+ if (style.savePath) return;
+ // Add to tracing path, and trace it right away.
+ addToPathList(path[0], center);
+ 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, scale, 2);
}
function endAndFillPenPath(elem, style) {
- var ts = readTurtleTransform(elem, true),
- state = getTurtleData(elem);
- drawAndClearPath(getDrawOnCanvas(state), state.path, style);
- if (state.style && state.style.savePath) {
- $.style(elem, 'turtlePenStyle', 'none');
+ var state = getTurtleData(elem);
+ if (state.style) {
+ // Apply a default style.
+ style = $.extend({}, state.style, style);
}
+ var scale = drawingScale(elem);
+ drawAndClearPath(getDrawOnCanvas(state), state.corners, style, scale, 1);
}
function clearField(arg) {
@@ -2034,21 +2109,24 @@ function clearField(arg) {
}
if (!arg || /\bturtles\b/.test(arg)) {
if (globalDrawing.surface) {
- var sel = $(globalDrawing.surface).find('.turtle');
+ var sel = $(globalDrawing.surface).find('.turtle').not('.turtlefield');
if (global_turtle) {
sel = sel.not(global_turtle);
}
sel.remove();
}
}
- if (!arg || /\btext\b/.test(arg)) {
- // "turtlefield" is a CSS class to use to mark top-level
- // elements that should not be deleted by clearscreen.
- var keep = $('.turtlefield');
+ if (!arg || /\blabels\b/.test(arg)) {
if (globalDrawing.surface) {
- keep = keep.add(globalDrawing.surface);
+ var sel = $(globalDrawing.surface).find('.turtlelabel')
+ .not('.turtlefield');
+ sel.remove();
}
- $('body').contents().not(keep).remove();
+ }
+ if (!arg || /\btext\b/.test(arg)) {
+ // "turtlefield" is a CSS class to use to mark elements that
+ // should not be deleted by clearscreen.
+ $('body').contents().not('.turtlefield').remove();
}
}
@@ -2100,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);
@@ -2129,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;
@@ -2156,13 +2233,14 @@ function touchesPixel(elem, color) {
// Functions in direct support of exported methods.
//////////////////////////////////////////////////////////////////////////
-function applyImg(sel, img) {
+function applyImg(sel, img, cb) {
if (img.img) {
if (sel[0].tagName == 'CANVAS' || sel[0].tagName == img.img.tagName) {
applyLoadedImage(img.img, sel[0], img.css);
}
} else if (sel[0].tagName == 'IMG' || sel[0].tagName == 'CANVAS') {
- setImageWithStableOrigin(sel[0], img.url, img.css);
+ setImageWithStableOrigin(sel[0], img.url, img.css, cb);
+ cb = null;
} else {
var props = {
backgroundImage: 'url(' + img.url + ')',
@@ -2174,13 +2252,17 @@ function applyImg(sel, img) {
}
sel.css(props);
}
+ if (cb) {
+ 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'),
@@ -2199,7 +2281,7 @@ function doQuickMove(elem, distance, sideways) {
ts.tx += dx;
ts.ty += dy;
elem.style[transform] = writeTurtleTransform(ts);
- flushPenState(elem);
+ flushPenState(elem, state, true);
}
function doQuickMoveXY(elem, dx, dy) {
@@ -2216,7 +2298,7 @@ function doQuickMoveXY(elem, dx, dy) {
ts.tx += dx;
ts.ty -= dy;
elem.style[transform] = writeTurtleTransform(ts);
- flushPenState(elem);
+ flushPenState(elem, state, true);
}
function doQuickRotate(elem, degrees) {
@@ -2227,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;
@@ -2274,12 +2357,14 @@ function makeTurtleEasingHook() {
}
}
-function animTime(elem) {
+function animTime(elem, intick) {
var state = $.data(elem, 'turtleData');
- if (!state) return 'turtle';
+ intick = intick || insidetick;
+ if (!state) return (intick ? 0 : 'turtle');
if ($.isNumeric(state.speed) || state.speed == 'Infinity') {
return 1000 / state.speed;
}
+ if (state.speed == 'turtle' && intick) return 0;
return state.speed;
}
@@ -2302,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),
@@ -2329,7 +2416,7 @@ function makeTurtleForwardHook() {
ts.tx = ntx;
ts.ty = nty;
elem.style[transform] = writeTurtleTransform(ts);
- flushPenState(elem);
+ flushPenState(elem, state);
}
};
}
@@ -2356,82 +2443,129 @@ function makeTurtleHook(prop, normalize, unit, displace) {
pageY: qpxy.pageY + (ts.ty - oty)
};
}
- flushPenState(elem);
+ flushPenState(elem, state);
+ } else {
+ clearChildQuickLocations(elem);
}
}
};
}
+// Given a starting direction, angle change, and turning radius,
+// this computes the side-radius (with a sign flip indicating
+// the other side), the coordinates of the center dc, and the dx/dy
+// displacement of the final location after the arc.
+function setupArc(
+ r0, // starting direction in local coordinates
+ r1, // ending direction local coordinates
+ turnradius // turning radius in local coordinates
+) {
+ var delta = normalizeRotationDelta(r1 - r0),
+ sradius = delta > 0 ? turnradius : -turnradius,
+ r0r = convertToRadians(r0),
+ dc = [Math.cos(r0r) * sradius, Math.sin(r0r) * sradius],
+ r1r = convertToRadians(r1);
+ return {
+ delta: delta,
+ sradius: sradius,
+ dc: dc,
+ dx: dc[0] - Math.cos(r1r) * sradius,
+ dy: dc[1] - Math.sin(r1r) * sradius
+ };
+}
+
+// Given a path array, a pageX/pageY starting position,
+// arc information in local coordinates, and a 2d transform
+// between page and local coordinates, this function adds to
+// the path scaled page-coorindate beziers following the arc.
+function addArcBezierPaths(
+ path, // path to add on to in page coordinates
+ start, // starting location in page coordinates
+ r0, // starting direction in local coordinates
+ end, // ending direction in local coordinates
+ turnradius, // turning radius in local coordinates
+ transform // linear distortion between page and local
+) {
+ var a = setupArc(r0, end, turnradius),
+ sradius = a.sradius, dc = a.dc,
+ r1, a1r, a2r, j, r, pts, triples,
+ splits, splita, absang, relative, points;
+ // Decompose an arc into equal arcs, all 45 degrees or less.
+ splits = 1;
+ splita = a.delta;
+ absang = Math.abs(a.delta);
+ if (absang > 45) {
+ splits = Math.ceil(absang / 45);
+ splita = a.delta / splits;
+ }
+ // Relative traces out the unit-radius arc centered at the origin.
+ relative = [];
+ while (--splits >= 0) {
+ r1 = splits === 0 ? end : r0 + splita;
+ a1r = convertToRadians(r0 + 180);
+ a2r = convertToRadians(r1 + 180);
+ relative.push.apply(relative, approxBezierUnitArc(a1r, a2r));
+ r0 = r1;
+ }
+ points = [];
+ for (j = 0; j < relative.length; j++) {
+ // Multiply each coordinate by radius scale up to the right
+ // turning radius and add to dc to center the turning radius
+ // at the right local coordinate position; then apply parent
+ // distortions to get page-coordinate relative offsets to the
+ // turtle's original position.
+ r = matrixVectorProduct(transform,
+ addVector(scaleVector(relative[j], sradius), dc));
+ // Finally add these to the turtle's actual original position
+ // to get page-coordinate control points for the bezier curves.
+ // (start is the starting position in absolute coordinates,
+ // and dc is the local coordinate offset from the starting
+ // position to the center of the turning radius.)
+ points.push({
+ pageX: r[0] + start.pageX,
+ pageY: r[1] + start.pageY});
+ }
+ // Divide control points into triples again to form bezier curves.
+ triples = [];
+ for (j = 0; j < points.length; j += 3) {
+ triples.push(points.slice(j, j + 3));
+ }
+ addBezierToPath(path, start, triples);
+ return a;
+}
+
+// An animation driver for rotation, including the possibility of
+// tracing out an arc. Reads an element's turningRadius to see if
+// changing ts.rot should also sweep out an arc. If so, calls
+// addArcBezierPath to directly add that arc to the drawing path.
function maybeArcRotation(end, elem, ts, opt) {
end = parseFloat(end);
var state = $.data(elem, 'turtleData'),
tradius = state ? state.turningRadius : 0;
- if (tradius === 0) {
+ if (tradius === 0 || ts.rot == end) {
// Avoid drawing a line if zero turning radius.
opt.displace = false;
- return normalizeRotation(end);
+ return tradius === 0 ? normalizeRotation(end) : end;
}
var tracing = (state && state.style && state.down),
- r0 = ts.rot, r1, r1r, a1r, a2r, j, r, pts, triples,
- r0r = convertToRadians(ts.rot),
- delta = normalizeRotationDelta(end - r0),
- radius = (delta > 0 ? tradius : -tradius) * ts.sy,
- dc = [Math.cos(r0r) * radius, Math.sin(r0r) * radius],
- splits, splita, absang, dx, dy, qpxy,
- path, totalParentTransform, start, relative, points;
+ sy = (state && state.oldscale) ? ts.sy : 1,
+ turnradius = tradius * sy, a;
if (tracing) {
- // Decompose an arc into equal arcs, all 45 degrees or less.
- splits = 1;
- splita = delta;
- absang = Math.abs(delta);
- if (absang > 45) {
- splits = Math.ceil(absang / 45);
- splita = delta / splits;
- }
- path = state.path[0];
- totalParentTransform = totalTransform2x2(elem.parentElement);
- // Relative traces out the unit-radius arc centered at the origin.
- relative = [];
- while (--splits >= 0) {
- r1 = splits === 0 ? end : r0 + splita;
- a1r = convertToRadians(r0 + 180);
- a2r = convertToRadians(r1 + 180);
- relative.push.apply(relative, approxBezierUnitArc(a1r, a2r));
- r0 = r1;
- }
- points = [];
- // start is the starting position in absolute coordinates,
- // and dc is the local coordinate offset from the starting
- // position to the center of the turning radius.
- start = getCenterInPageCoordinates(elem);
- for (j = 0; j < relative.length; j++) {
- // Multiply each coordinate by radius scale up to the right
- // turning radius and add to dc to center the turning radius
- // at the right local coordinate position; then apply parent
- // distortions to get page-coordinate relative offsets to the
- // turtle's original position.
- r = matrixVectorProduct(totalParentTransform,
- addVector(scaleVector(relative[j], radius), dc));
- // Finally add these to the turtle's actual original position
- // to get page-coordinate control points for the bezier curves.
- points.push({
- pageX: r[0] + start.pageX,
- pageY: r[1] + start.pageY});
- }
- // Divide control points into triples again to form bezier curves.
- triples = [];
- for (j = 0; j < points.length; j += 3) {
- triples.push(points.slice(j, j + 3));
- }
- addBezierToPath(path, start, triples);
- }
- // Now move turtle to its final position: in local coordinates,
- // translate to the turning center plus the vector to the arc end.
- r1r = convertToRadians(end);
- dx = dc[0] - Math.cos(r1r) * radius;
- dy = dc[1] - Math.sin(r1r) * radius;
- ts.tx += dx;
- ts.ty += dy;
+ a = addArcBezierPaths(
+ state.path[0], // path to add to
+ getCenterInPageCoordinates(elem), // starting location
+ ts.rot, // starting direction
+ end, // ending direction
+ turnradius, // scaled turning radius
+ totalTransform2x2(elem.parentElement)); // totalParentTransform
+ } else {
+ a = setupArc(
+ ts.rot, // starting direction
+ end, // degrees change
+ turnradius); // scaled turning radius
+ }
+ ts.tx += a.dx;
+ ts.ty += a.dy;
opt.displace = true;
return end;
}
@@ -2504,12 +2638,73 @@ function makeTurtleXYHook(publicname, propx, propy, displace) {
pageY: qpxy.pageY + (ts.ty - oty)
};
}
- flushPenState(elem);
+ flushPenState(elem, state);
+ } else {
+ clearChildQuickLocations(elem);
}
}
};
}
+var absoluteUrlAnchor = document.createElement('a');
+function absoluteUrlObject(url) {
+ absoluteUrlAnchor.href = url;
+ return absoluteUrlAnchor;
+}
+function absoluteUrl(url) {
+ return absoluteUrlObject(url).href;
+}
+
+// Pencil-code specific function: detects whether a domain appears to
+// be a pencilcode site.
+function isPencilHost(hostname) {
+ return /(?:^|\.)pencil(?:code)?\./i.test(hostname);
+}
+// Returns a pencilcode username from the URL, if any.
+function pencilUserFromUrl(url) {
+ var hostname = absoluteUrlObject(url == null ? '' : url).hostname,
+ match = /^(\w+)\.pencil(?:code)?\./i.exec(hostname);
+ if (match) return match[1];
+ return null;
+}
+// Rewrites a url to have the top directory name given.
+function apiUrl(url, topdir) {
+ var link = absoluteUrlObject(url == null ? '' : url),
+ result = link.href;
+ if (isPencilHost(link.hostname)) {
+ if (/^\/(?:edit|home|code|load|save)(?:\/|$)/.test(link.pathname)) {
+ // Replace a special topdir name.
+ result = link.protocol + '//' + link.host + '/' + topdir + '/' +
+ link.pathname.replace(/\/[^\/]*(?:\/|$)/, '') + link.search + link.hash;
+ }
+ } else if (isPencilHost(global.location.hostname)) {
+ // Proxy offdomain requests to avoid CORS issues.
+ result = '/proxy/' + result;
+ }
+ return result;
+}
+// Creates an image url from a potentially short name.
+function imgUrl(url) {
+ if (/\//.test(url)) { return url; }
+ url = '/img/' + url;
+ if (isPencilHost(global.location.hostname)) { return url; }
+ return '//pencilcode.net' + url;
+}
+// Retrieves the pencil code login cookie, if there is one.
+function loginCookie() {
+ if (!document.cookie) return null;
+ var cookies = document.cookie.split(/;\s*/);
+ for (var j = 0; j < cookies.length; ++j) {
+ if (/^login=/.test(cookies[j])) {
+ var val = unescape(cookies[j].substr(6)).split(':');
+ if (val && val.length == 2) {
+ return { user: val[0], key: val[1] };
+ }
+ }
+ }
+ return null;
+}
+
// A map of url to {img: Image, queue: [{elem: elem, css: css, cb: cb}]}.
var stablyLoadedImages = {};
@@ -2530,13 +2725,14 @@ 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;
+ 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) {
// Already requested this image?
record = stablyLoadedImages[url];
- if (record.img.complete) {
+ if (record.queue === null) {
// If already complete, then flip the image right away.
finishSet(record.img, elem, css, cb);
} else {
@@ -2551,18 +2747,15 @@ function setImageWithStableOrigin(elem, url, css, cb) {
img: new Image(),
queue: [{elem: elem, css: css, cb: cb}]
};
+ if (isPencilHost(urlobj.hostname)) {
+ // When requesting through pencilcode, always make a
+ // cross-origin request.
+ record.img.crossOrigin = 'Anonymous';
+ }
// Pop the element to the right dimensions early if possible.
resizeEarlyIfPossible(url, elem, css);
// First set up the onload callback, then start loading.
- function poll() {
- if (!record.img.complete) {
- // Guard against browsers that may fire onload too early or never.
- setTimeout(poll, 100);
- return;
- }
- record.img.removeEventListener('load', poll);
- record.img.removeEventListener('error', poll);
- // TODO: compute the convex hull of the image.
+ afterImageLoadOrError(record.img, url, function() {
var j, queue = record.queue;
record.queue = null;
if (queue) {
@@ -2571,12 +2764,7 @@ function setImageWithStableOrigin(elem, url, css, cb) {
finishSet(record.img, queue[j].elem, queue[j].css, queue[j].cb);
}
}
- }
- record.img.addEventListener('load', poll);
- record.img.addEventListener('error', poll);
- record.img.src = url;
- // Start polling immediately, because some browser may never fire onload.
- poll();
+ });
}
// This is the second step, done after the async load is complete:
// the parameter "loaded" contains the fully loaded Image.
@@ -2595,6 +2783,34 @@ function setImageWithStableOrigin(elem, url, css, cb) {
}
}
+function afterImageLoadOrError(img, url, fn) {
+ if (url == null) { url = img.src; }
+ // If already loaded, then just call fn.
+ if (url == img.src && (!url || img.complete)) {
+ fn();
+ return;
+ }
+ // Otherwise, set up listeners and wait.
+ var timeout = null;
+ function poll(e) {
+ // If we get a load or error event, notice img.complete
+ // or see that the src was changed, we're done here.
+ if (e || img.complete || img.src != url) {
+ img.removeEventListener('load', poll);
+ img.removeEventListener('error', poll);
+ clearTimeout(timeout);
+ fn();
+ } else {
+ // Otherwise, continue waiting and also polling.
+ timeout = setTimeout(poll, 100);
+ }
+ }
+ img.addEventListener('load', poll);
+ img.addEventListener('error', poll);
+ img.src = url;
+ poll();
+}
+
// In the special case of loading a data: URL onto an element
// where we also have an explicit css width and height to apply,
// we go ahead and synchronously apply the CSS properties even if
@@ -2645,10 +2861,12 @@ function applyLoadedImage(loaded, elem, css) {
elem.height = loaded.height;
if (!isCanvas) {
elem.src = loaded.src;
- } else {
- ctx = elem.getContext('2d');
- ctx.clearRect(0, 0, loaded.width, loaded.height);
- ctx.drawImage(loaded, 0, 0);
+ } else if (loaded.width > 0 && loaded.height > 0) {
+ try {
+ ctx = elem.getContext('2d');
+ ctx.clearRect(0, 0, loaded.width, loaded.height);
+ ctx.drawImage(loaded, 0, 0);
+ } catch (e) { }
}
}
}
@@ -2656,19 +2874,24 @@ function applyLoadedImage(loaded, elem, css) {
sel.css(css);
}
var newOrigin = readTransformOrigin(elem);
- if(loaded){
- var hull = cutTransparent(loaded);
- var turtleHull = '';
- for(var i = 0; i < hull.length; i++){
- if(i > 0) turtleHull += ' ';
- // Scale coordinates to the size of elem
- hull[i].pageX = Math.floor(hull[i].pageX * sel.css('height').slice(0, -2) / loaded.height);
- hull[i].pageY = Math.floor(hull[i].pageY * sel.css('width').slice(0, -2) / loaded.width);
- turtleHull += (hull[i].pageX - newOrigin[0]) + ' ' + (hull[i].pageY - newOrigin[1]);
- }
- sel.css('turtleHull', turtleHull);
- console.log(turtleHull);
+ if (loaded && !css.turtleHull) {
+ try {
+ var hull = transparentHull(loaded);
+ scalePolygon(hull,
+ parseFloat(sel.css('width')) / loaded.width,
+ parseFloat(sel.css('height')) / loaded.height,
+ -newOrigin[0], -newOrigin[1]);
+ sel.css('turtleHull', hull);
+ } catch (e) {
+ // Do not do this if the image can't be loaded.
+ }
}
+ moveToPreserveOrigin(elem, oldOrigin, newOrigin);
+}
+
+function moveToPreserveOrigin(elem, oldOrigin, newOrigin) {
+ var sel = $(elem);
+ if (!sel.hasClass('turtle')) return;
// If there was a change, then translate the element to keep the origin
// in the same location on the screen.
if (newOrigin[0] != oldOrigin[0] || newOrigin[1] != oldOrigin[1]) {
@@ -2689,7 +2912,6 @@ function applyLoadedImage(loaded, elem, css) {
}
}
-
function withinOrNot(obj, within, distance, x, y) {
var sel, elem, gbcr, pos, d2;
if (x === undefined && y === undefined) {
@@ -2791,6 +3013,46 @@ var Sprite = (function(_super) {
})(jQuery.fn.init);
+// Pencil extends Sprite, and is invisible and fast by default.
+var Pencil = (function(_super) {
+ __extends(Pencil, _super);
+
+ function Pencil(canvas) {
+ // A pencil draws on a canvas. Allow a selector or element.
+ if (canvas && canvas.jquery && $.isFunction(canvas.canvas)) {
+ canvas = canvas.canvas();
+ }
+ if (canvas && (canvas.tagName != 'CANVAS' ||
+ typeof canvas.getContext != 'function')) {
+ canvas = $(canvas).filter('canvas').get(0);
+ }
+ if (!canvas || canvas.tagName != 'CANVAS' ||
+ typeof canvas.getContext != 'function') {
+ canvas = null;
+ }
+ // 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' };
+ Pencil.__super__.constructor.call(this, settings, context);
+ // Set the pencil to hidden, infinite speed,
+ // and drawing on the specifed canvas.
+ this.each(function() {
+ var state = getTurtleData(this);
+ state.speed = Infinity;
+ state.drawOnCanvas = canvas;
+ this.style.display = 'none';
+ if (canvas) {
+ this.style[transform] = writeTurtleTransform(
+ readTurtleTransform(canvas, true));
+ }
+ });
+ }
+
+ return Pencil;
+
+})(Sprite);
+
+
// Turtle extends Sprite, and draws a turtle by default.
var Turtle = (function(_super) {
__extends(Turtle, _super);
@@ -2810,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);
@@ -2853,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();
@@ -3055,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.
@@ -3162,20 +3424,21 @@ var Piano = (function(_super) {
// Implementation of the "pressed" function
//////////////////////////////////////////////////////////////////////////
-// The implementation of the "pressed" function is captured in a closure.
-var pressedKey = (function() {
- var focusTakenOnce = false;
- function focusWindowIfFirst() {
- if (focusTakenOnce) return;
- focusTakenOnce = true;
- 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();
- } catch (e) {}
- window.focus();
- }
- var ua = typeof window !== 'undefined' ? window.navigator.userAgent : '',
+var focusTakenOnce = false;
+function focusWindowIfFirst() {
+ if (focusTakenOnce) return;
+ focusTakenOnce = true;
+ 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).
+ global.parent.document.activeElement.blur();
+ } catch (e) {}
+ global.focus();
+}
+
+// Construction of keyCode names.
+var keyCodeName = (function() {
+ var ua = typeof global !== 'undefined' ? global.navigator.userAgent : '',
isOSX = /OS X/.test(ua),
isOpera = /Opera/.test(ua),
maybeFirefox = !/like Gecko/.test(ua) && !isOpera,
@@ -3183,6 +3446,7 @@ var pressedKey = (function() {
preventable = 'contextmenu',
events = 'mousedown mouseup keydown keyup blur ' + preventable,
keyCodeName = {
+ 0: 'null',
1: 'mouse1',
2: 'mouse2',
3: 'break',
@@ -3290,48 +3554,50 @@ var pressedKey = (function() {
254: 'clear'
};
// :-@, 0-9, a-z(lowercased)
- for (i = 48; i < 91; ++i) {
+ for (var i = 48; i < 91; ++i) {
keyCodeName[i] = String.fromCharCode(i).toLowerCase();
}
// num-0-9 numeric keypad
for (i = 96; i < 106; ++i) {
- keyCodeName[i] = 'num ' + (i - 96);
+ keyCodeName[i] = 'numpad' + (i - 96);
}
// f1-f24
for (i = 112; i < 136; ++i) {
keyCodeName[i] = 'f' + (i-111);
}
+ return keyCodeName;
+})();
+
+var pressedKey = (function() {
// Listener for keyboard, mouse, and focus events that updates pressedState.
- function pressListener(event) {
- var name, simplified, down;
- if (event.type == 'mousedown' || event.type == 'mouseup') {
- name = 'mouse ' + event.which;
- down = (event.type == 'mousedown');
- } else if (event.type == 'keydown' || event.type == 'keyup') {
- name = keyCodeName[event.which];
- down = (event.type == 'keydown');
- if (event.which >= 160 && event.which <= 165) {
- // For "shift left", also trigger "shift"; same for control and alt.
- simplified = name.replace(/(?:left|right)$/, '');
- }
- } else if (event.type == 'blur' || event.type == 'contextmenu') {
- // When losing focus, clear all keyboard state.
- if (!event.isDefaultPrevented() || preventable != event.type) {
- resetPressedState();
+ function makeEventListener(mouse, down) {
+ return (function(event) {
+ var name, simplified, which = event.which;
+ if (mouse) {
+ name = 'mouse' + which;
+ } else {
+ // For testability, support whichSynth when which is zero, because
+ // it is impossible to simulate .which on phantom.
+ if (!which && event.whichSynth) { which = event.whichSynth; }
+ name = keyCodeName[which];
+ if (which >= 160 && which <= 165) {
+ // For "shift left", also trigger "shift"; same for control and alt.
+ updatePressedState(name.replace(/(?:left|right)$/, ''), down);
+ }
}
- return;
- }
- updatePressedState(name, down);
- updatePressedState(simplified, down);
- if (down) {
- // After any down event, unlisten and relisten to contextmenu,
- // to put oursleves last. This allows us to test isDefaultPrevented.
- $(window).off(preventable, pressListener);
- $(window).on(preventable, pressListener);
- }
- }
+ updatePressedState(name, down);
+ });
+ };
+ var eventMap = {
+ 'mousedown': makeEventListener(1, 1),
+ 'mouseup': makeEventListener(1, 0),
+ 'keydown': makeEventListener(0, 1),
+ 'keyup': makeEventListener(0, 0),
+ 'blur': resetPressedState
+ };
// The pressedState map just has an entry for each pressed key.
// Unpressing a key will delete the actual key from the map.
+ var pressedState = {};
function updatePressedState(name, down) {
if (name != null) {
if (!down) {
@@ -3350,10 +3616,12 @@ var pressedKey = (function() {
// The pressed listener can be turned on and off using pressed.enable(flag).
function enablePressListener(turnon) {
resetPressedState();
- if (turnon) {
- $(window).on(events, pressListener);
- } else {
- $(window).off(events, pressListener);
+ for (var name in eventMap) {
+ if (turnon) {
+ global.addEventListener(name, eventMap[name], true);
+ } else {
+ global.removeEventListener(name, eventMap[name]);
+ }
}
}
// All pressed keys known can be listed using pressed.list().
@@ -3367,16 +3635,172 @@ var pressedKey = (function() {
// The pressed function just polls the given keyname.
function pressed(keyname) {
focusWindowIfFirst();
- // Canonical names are lowercase and have no spaces.
- keyname = keyname.replace(/\s/g, '').toLowerCase();
- if (pressedState[keyname]) return true;
- return false;
+ if (keyname) {
+ // Canonical names are lowercase and have no spaces.
+ keyname = keyname.replace(/\s/g, '').toLowerCase();
+ if (pressedState[keyname]) return true;
+ return false;
+ } else {
+ return listPressedKeys();
+ }
}
pressed.enable = enablePressListener;
pressed.list = listPressedKeys;
return pressed;
})();
+
+//////////////////////////////////////////////////////////////////////////
+// JQUERY EVENT ENHANCEMENT
+// - Keyboard events get the .key property.
+// - Keyboard event listening with a string first (data) arg
+// automatically filter out events that don't match the keyname.
+// - Mouse events get .x and .y (center-up) if there is a turtle field.
+// - If a turtle in the field is listening to mouse events, unhandled
+// body mouse events are manually forwarded to turtles.
+//////////////////////////////////////////////////////////////////////////
+
+function addEventHook(hookobj, field, defobj, name, fn) {
+ var names = name.split(/\s+/);
+ for (var j = 0; j < names.length; ++j) {
+ name = names[j];
+ var hooks = hookobj[name];
+ if (!hooks) {
+ hooks = hookobj[name] = $.extend({}, defobj);
+ }
+ if (typeof hooks[field] != 'function') {
+ hooks[field] = fn;
+ } else if (hooks[field] != fn) {
+ // Multiple event hooks just listed in an array.
+ if (hooks[field].hooklist) {
+ if (hooks[field].hooklist.indexOf(fn) < 0) {
+ hooks[field].hooklist.push(fn);
+ }
+ } else {
+ (function() {
+ var hooklist = [hooks[field], fn];
+ (hooks[field] = function(event, original) {
+ var current = event;
+ for (var j = 0; j < hooklist.length; ++j) {
+ current = hooklist[j](current, original) || current;
+ }
+ return current;
+ }).hooklist = hooklist;
+ })();
+ }
+ }
+ }
+}
+
+function mouseFilterHook(event, original) {
+ if (globalDrawing.field && 'pageX' in event && 'pageY' in event) {
+ var origin = $(globalDrawing.field).offset();
+ if (origin) {
+ event.x = event.pageX - origin.left;
+ event.y = origin.top - event.pageY;
+ }
+ }
+ return event;
+}
+
+function mouseSetupHook(data, ns, fn) {
+ if (globalDrawing.field && !globalDrawing.fieldMouse &&
+ this.parentElement === globalDrawing.field ||
+ /(?:^|\s)turtle(?:\s|$)/.test(this.class)) {
+ globalDrawing.fieldMouse = true;
+ forwardBodyMouseEventsIfNeeded();
+ }
+ return false;
+}
+
+function forwardBodyMouseEventsIfNeeded() {
+ if (globalDrawing.fieldHook) return;
+ if (globalDrawing.surface && globalDrawing.fieldMouse) {
+ globalDrawing.fieldHook = true;
+ setTimeout(function() {
+ // TODO: check both globalDrawing.surface and
+ // globalDrawing.turtleMouseListener
+ $('body').on('click.turtle dblclick.turtle ' +
+ 'mouseup.turtle mousedown.turtle mousemove.turtle', function(e) {
+ if (e.target === this && !e.isTrigger) {
+ // Only forward events directly on the body that (geometrically)
+ // touch a turtle directly within the turtlefield.
+ var warn = $.turtle.nowarn;
+ $.turtle.nowarn = true;
+ var sel = $(globalDrawing.surface)
+ .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.
+ e.target = null;
+ e.relatedTarget = null;
+ e.fromElement = null;
+ e.toElement = null;
+ sel.trigger(e);
+ return false;
+ }
+ }
+ });
+ }, 0);
+ }
+}
+
+function addMouseEventHooks() {
+ var hookedEvents = 'mousedown mouseup mousemove click dblclick';
+ addEventHook($.event.fixHooks, 'filter', $.event.mouseHooks,
+ hookedEvents, mouseFilterHook);
+ addEventHook($.event.special, 'setup', {}, hookedEvents, mouseSetupHook);
+}
+
+function keyFilterHook(event, original) {
+ var which = event.which;
+ if (!which) {
+ which = (original || event.originalEvent).whichSynth;
+ }
+ var name = keyCodeName[which];
+ if (!name && which) {
+ name = String.fromCharCode(which);
+ }
+ event.key = name;
+ return event;
+}
+
+// Add .key to each keyboard event.
+function keypressFilterHook(event, original) {
+ if (event.charCode != null) {
+ event.key = String.fromCharCode(event.charCode);
+ }
+}
+
+// Intercept on('keydown/keyup/keypress')
+function keyAddHook(handleObj) {
+ if (typeof(handleObj.data) != 'string') return;
+ var choices = handleObj.data.replace(/\s/g, '').toLowerCase().split(',');
+ var original = handleObj.handler;
+ var wrapped = function(event) {
+ if (choices.indexOf(event.key) < 0) return;
+ return original.apply(this, arguments);
+ }
+ if (original.guid) { wrapped.guid = original.guid; }
+ handleObj.handler = wrapped;
+}
+
+function addKeyEventHooks() {
+ // Add the "key" field to keydown and keyup events - this uses
+ // the lowercase key names listed in the pressedKey utility.
+ addEventHook($.event.fixHooks, 'filter', $.event.keyHooks,
+ 'keydown keyup', keyFilterHook);
+ // Add "key" to keypress also. This is just the unicode character
+ // corresponding to event.charCode.
+ addEventHook($.event.fixHooks, 'filter', $.event.keyHooks,
+ 'keypress', keypressFilterHook);
+ // Finally, add special forms for the keyup/keydown/keypress events
+ // where the first argument can be the comma-separated name of keys
+ // to target (instead of just data)
+ addEventHook($.event.special, 'add', {},
+ 'keydown keyup keypress', keyAddHook);
+}
+
//////////////////////////////////////////////////////////////////////////
// WEB AUDIO SUPPORT
// Definition of play("ABC") - uses ABC music note syntax.
@@ -3438,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
@@ -3451,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),
@@ -3473,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;
+ }
}
}
@@ -3496,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
@@ -3535,9 +4008,10 @@ 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 = 10; // Default duration of a tone.
+ Instrument.toneLength = 1; // Default duration of a tone.
Instrument.cleanupDelay = 0.1; // Silent time before disconnecting nodes.
// Sets the default timbre for the instrument. See defaultTimbre.
@@ -3671,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 *
@@ -3748,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),
@@ -3770,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) *
@@ -3824,10 +4300,14 @@ var Instrument = (function() {
this.silence();
return;
}
- var now = this._atop.ac.currentTime, callbacks = [],
+ // The shortest time we can delay is 1 / 1000 secs, so if an event
+ // is within the next 0.5 ms, now is the closest moment, and we go
+ // ahead and process it.
+ var instant = this._atop.ac.currentTime + (1 / 2000),
+ callbacks = [],
j, work, when, freq, record, conflict, save, cb;
// Schedule a batch of notes
- if (this._minQueueTime - now <= Instrument.bufferSecs) {
+ if (this._minQueueTime - instant <= Instrument.bufferSecs) {
if (this._unsortedQueue) {
this._queue.sort(function(a, b) {
if (a.time != b.time) { return a.time - b.time; }
@@ -3837,7 +4317,7 @@ var Instrument = (function() {
this._unsortedQueue = false;
}
for (j = 0; j < this._queue.length; ++j) {
- if (this._queue[j].time - now > Instrument.bufferSecs) { break; }
+ if (this._queue[j].time - instant > Instrument.bufferSecs) { break; }
}
if (j > 0) {
work = this._queue.splice(0, j);
@@ -3851,7 +4331,7 @@ var Instrument = (function() {
// Disconnect notes from the cleanup set.
for (j = 0; j < this._cleanupSet.length; ++j) {
record = this._cleanupSet[j];
- if (record.cleanuptime < now) {
+ if (record.cleanuptime < instant) {
if (record.gainNode) {
// This explicit disconnect is needed or else Chrome's WebAudio
// starts getting overloaded after a couple thousand notes.
@@ -3866,7 +4346,7 @@ var Instrument = (function() {
for (freq in this._finishSet) {
record = this._finishSet[freq];
when = record.time + record.duration;
- if (when <= now) {
+ if (when <= instant) {
callbacks.push({
order: [when, 0],
f: this._trigger, t: this, a: ['noteoff', record]});
@@ -3880,7 +4360,7 @@ var Instrument = (function() {
for (j = 0; j < this._callbackSet.length; ++j) {
cb = this._callbackSet[j];
when = cb.time;
- if (when <= now) {
+ if (when <= instant) {
callbacks.push({
order: [when, 1],
f: cb.callback, t: null, a: []});
@@ -3890,7 +4370,7 @@ var Instrument = (function() {
}
// Notify about any notes starting.
for (j = 0; j < this._startSet.length; ++j) {
- if (this._startSet[j].time <= now) {
+ if (this._startSet[j].time <= instant) {
save = record = this._startSet[j];
freq = record.frequency;
conflict = null;
@@ -3986,7 +4466,7 @@ var Instrument = (function() {
earliest = Math.min(
earliest, this._minQueueTime - Instrument.dequeueTime);
- delay = Math.max(0, earliest - this._atop.ac.currentTime);
+ delay = Math.max(0.001, earliest - this._atop.ac.currentTime);
// If there are no future events, then we do not need a timer.
if (isNaN(delay) || delay == Infinity) { return; }
@@ -3997,7 +4477,7 @@ var Instrument = (function() {
// The low-level tone function.
Instrument.prototype.tone =
- function(pitch, velocity, duration, delay, timbre) {
+ function(pitch, duration, velocity, delay, timbre, origin) {
// If audio is not present, this is a no-op.
if (!this._atop) { return; }
@@ -4007,6 +4487,7 @@ var Instrument = (function() {
if (duration == null) duration = pitch.duration;
if (delay == null) delay = pitch.delay;
if (timbre == null) timbre = pitch.timbre;
+ if (origin == null) origin = pitch.origin;
pitch = pitch.pitch;
}
@@ -4037,7 +4518,7 @@ var Instrument = (function() {
if (key in given) {
timbre[key] = given[key];
} else {
- timbre[key] = defaulTimbre[key];
+ timbre[key] = defaultTimbre[key];
}
}
}
@@ -4057,7 +4538,8 @@ var Instrument = (function() {
instrument: this,
gainNode: null,
oscillators: null,
- cleanuptime: Infinity
+ cleanuptime: Infinity,
+ origin: origin // save the origin of the tone for visible feedback
};
if (time < now + Instrument.bufferSecs) {
@@ -4166,10 +4648,12 @@ var Instrument = (function() {
// This is innsermost part of the inner loop!
this.tone( // Play the tone:
note.pitch, // at the given pitch
- v, // with the given volume
secs, // for the given duration
+ v, // with the given volume
delay, // starting at the proper time
- timbre); // with the selected timbre
+ timbre, // with the selected timbre
+ note // the origin object for visual feedback
+ );
}
delay += stem.time * beatsecs; // Advance the sequenced time.
}
@@ -4184,295 +4668,269 @@ 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;
- }
- }
- // For picking a default voice, looks for the first voice name.
- function firstVoiceName() {
- if (result.V) {
- return result.V.split(/\s+/)[0];
- } else {
- return '';
- }
+
+ // 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 = {};
}
- // 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]);
- }
- 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];
- } 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());
- }
- } 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());
- }
- }
- // 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);
- }
- }
+ if (typeof(options) == 'string') {
+ // Abbreviation: name a wave to get a default timbre for that wave.
+ options = { wave: options };
}
- 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]];
- }
- }
- } else {
- out.push(j);
- }
- }
- // Delete any voices that had no stems.
- for (j = 0; j < out.length; ++j) {
- delete result.voice[out[j]];
+ 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];
}
}
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);
- if (!d) { return; }
- if (!beatinfo.unitnote) {
- if (d < 0.75) {
- beatinfo.unitnote = 1/16;
+
+ 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];
+ }
+ }
+ }
+ 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 {
- beatinfo.unitnote = 1/8;
+ 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;
}
- // Parse L: lines, e.g., "1/8".
- function parseUnitNote(lline, beatinfo) {
- var d = durationToTime(lline);
- if (!d) { return; }
- beatinfo.unitnote = d;
+
+ // 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;
}
- // Parse Q: line, e.g., "1/4=66".
- function parseTempo(qline, beatinfo) {
- var parts = qline.split(/\s+|=/), j, unit = null, tempo = null;
- for (j = 0; j < parts.length; ++j) {
- // It could be reversed, like "66=1/4", or just "120", so
- // determine what is going on by looking for a slash etc.
- if (parts[j].indexOf('/') >= 0 || /^[1-4]$/.test(parts[j])) {
- // The note-unit (e.g., 1/4).
- unit = unit || durationToTime(parts[j]);
+
+ // 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]];
+ }
+ }
+ // Remove this internal state variable;
+ delete result.voice[j].accent;
} else {
- // The tempo-number (e.g., 120)
- tempo = tempo || Number(parts[j]);
+ out.push(j);
}
}
- if (unit) {
- beatinfo.unitbeat = unit;
- }
- if (tempo) {
- beatinfo.tempo = tempo;
+ // Delete any voices that had no stems.
+ for (j = 0; j < out.length; ++j) {
+ delete result.voice[out[j]];
}
}
- // Run through all the notes, adding up time for tied notes,
- // and marking notes that were held over with holdover = true.
- function processTies(stems) {
- var tied = {}, nextTied, j, k, note, firstNote;
- for (j = 0; j < stems.length; ++j) {
- nextTied = {};
- for (k = 0; k < stems[j].notes.length; ++k) {
- firstNote = note = stems[j].notes[k];
- if (tied.hasOwnProperty(note.pitch)) { // Pitch was tied from before.
- firstNote = tied[note.pitch]; // Get the earliest note in the tie.
- firstNote.time += note.time; // Extend its time.
- note.holdover = true; // Silence this note as a holdover.
- }
- if (note.tie) { // This note is tied with the next.
- nextTied[note.pitch] = firstNote; // Save it away.
+ 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]);
}
- }
- tied = nextTied;
+ break;
+ case 'M':
+ parseMeter(value, context);
+ break;
+ case 'L':
+ parseUnitNote(value, context);
+ break;
+ case 'Q':
+ parseTempo(value, context);
+ break;
}
- }
- // Returns a map of A-G -> accidentals, according to the key signature.
- // When n is zero, there are no accidentals (e.g., C major or A minor).
- // When n is positive, there are n sharps (e.g., for G major, n = 1).
- // When n is negative, there are -n flats (e.g., for F major, n = -1).
- function accidentals(n) {
- var sharps = 'FCGDAEB',
- result = {}, j;
- if (!n) {
- return result;
+ // 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;
}
- if (n > 0) { // Handle sharps.
- for (j = 0; j < n && j < 7; ++j) {
- result[sharps.charAt(j)] = '^';
- }
- } else { // Flats are in the opposite order.
- for (j = 0; j > n && j > -7; --j) {
- result[sharps.charAt(6 + j)] = '_';
+ // 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());
}
}
- return result;
}
- // Decodes the key signature line (e.g., K: C#m) at the front of an ABC tune.
- // Supports the whole range of scale systems listed in the ABC spec.
- function keysig(keyname) {
- if (!keyname) { return {}; }
- var key, 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,
- // Minor
- '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,
- // Mixolydian
- '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,
- // Dorian
- '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,
- // Phrygian
- '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,
- // Lydian
- '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,
- // Locrian
- '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
- };
- var k = keyname.replace(/\s+/g, '').toLowerCase().substr(0, 5);
- var scale = k.match(/maj|min|mix|dor|phr|lyd|loc|m/);
- if (scale) {
- if (scale == 'maj') {
- key = k.substr(0, scale.index);
- } else if (scale == 'min') {
- key = k.substr(0, scale.index + 1);
- } else {
- key = k.substr(0, scale.index + scale[0].length);
- }
- } else {
- key = /^[a-g][#b]?/.exec(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;
}
- var result = accidentals(sigcodes[key]);
- var extras = keyname.substr(key.length).match(/(_+|=|\^+)[a-g]/ig);
- if (extras) {
- for (j = 0; j < extras.length; ++j) {
- var note = extras[j].charAt(extras[j].length - 1).toUpperCase();
- if (extras[j].charAt(0) == '=') {
- delete result[note];
- } else {
- result[note] = extras[j].substr(0, extras[j].length - 1);
- }
- }
+ 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;
}
- 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];
+
+ // 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
@@ -4494,9 +4952,8 @@ var Instrument = (function() {
//
// 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,
+ function parseABCNotes(str) {
+ var tokens = str.match(ABCtoken), parsed = null,
index = 0, dotted = 0, beatlet = null, t;
if (!tokens) {
return null;
@@ -4504,8 +4961,12 @@ var Instrument = (function() {
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.
+ // 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;
}
@@ -4538,9 +4999,9 @@ var Instrument = (function() {
accent.slurred -= 1;
if (accent.slurred <= 0) {
accent.slurred = 0;
- if (result.length >= 1) {
+ if (context.stems && context.stems.length >= 1) {
// The last notes in a slur are not slurred.
- slurStem(result[result.length - 1], false);
+ slurStem(context.stems[context.stems.length - 1], false);
}
}
}
@@ -4571,26 +5032,171 @@ var Instrument = (function() {
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;
+ // 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 {
+ 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);
+ }
+ // 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;
+ }
+ }
+
+ // 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);
+ if (!d) { return; }
+ if (!beatinfo.unitnote) {
+ if (d < 0.75) {
+ beatinfo.unitnote = 1/16;
+ } else {
+ beatinfo.unitnote = 1/8;
+ }
+ }
+ }
+ // Parse L: lines, e.g., "1/8".
+ function parseUnitNote(lline, beatinfo) {
+ var d = durationToTime(lline);
+ if (!d) { return; }
+ beatinfo.unitnote = d;
+ }
+ // Parse Q: line, e.g., "1/4=66".
+ function parseTempo(qline, beatinfo) {
+ var parts = qline.split(/\s+|=/), j, unit = null, tempo = null;
+ for (j = 0; j < parts.length; ++j) {
+ // It could be reversed, like "66=1/4", or just "120", so
+ // determine what is going on by looking for a slash etc.
+ if (parts[j].indexOf('/') >= 0 || /^[1-4]$/.test(parts[j])) {
+ // The note-unit (e.g., 1/4).
+ unit = unit || durationToTime(parts[j]);
+ } else {
+ // The tempo-number (e.g., 120)
+ tempo = tempo || Number(parts[j]);
+ }
+ }
+ if (unit) {
+ beatinfo.unitbeat = unit;
+ }
+ if (tempo) {
+ beatinfo.tempo = tempo;
+ }
+ }
+ // Run through all the notes, adding up time for tied notes,
+ // and marking notes that were held over with holdover = true.
+ function processTies(stems) {
+ var tied = {}, nextTied, j, k, note, firstNote;
+ for (j = 0; j < stems.length; ++j) {
+ nextTied = {};
+ for (k = 0; k < stems[j].notes.length; ++k) {
+ firstNote = note = stems[j].notes[k];
+ if (tied.hasOwnProperty(note.pitch)) { // Pitch was tied from before.
+ firstNote = tied[note.pitch]; // Get the earliest note in the tie.
+ firstNote.time += note.time; // Extend its time.
+ note.holdover = true; // Silence this note as a holdover.
+ }
+ if (note.tie) { // This note is tied with the next.
+ nextTied[note.pitch] = firstNote; // Save it away.
+ }
+ }
+ tied = nextTied;
+ }
+ }
+ // Returns a map of A-G -> accidentals, according to the key signature.
+ // When n is zero, there are no accidentals (e.g., C major or A minor).
+ // When n is positive, there are n sharps (e.g., for G major, n = 1).
+ // When n is negative, there are -n flats (e.g., for F major, n = -1).
+ function accidentals(n) {
+ var sharps = 'FCGDAEB',
+ result = {}, j;
+ if (!n) {
+ return result;
+ }
+ if (n > 0) { // Handle sharps.
+ for (j = 0; j < n && j < 7; ++j) {
+ result[sharps.charAt(j)] = '^';
+ }
+ } else { // Flats are in the opposite order.
+ for (j = 0; j > n && j > -7; --j) {
+ result[sharps.charAt(6 + j)] = '_';
+ }
+ }
+ return result;
+ }
+ // Decodes the key signature line (e.g., K: C#m) at the front of an ABC tune.
+ // Supports the whole range of scale systems listed in the ABC spec.
+ function keysig(keyname) {
+ if (!keyname) { return {}; }
+ 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,
+ // Minor
+ '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,
+ // Mixolydian
+ '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,
+ // Dorian
+ '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,
+ // Phrygian
+ '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,
+ // Lydian
+ '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,
+ // Locrian
+ '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
+ };
+ var k = keyname.replace(/\s+/g, '').toLowerCase().substr(0, 5);
+ var scale = k.match(/maj|min|mix|dor|phr|lyd|loc|m/);
+ if (scale) {
+ if (scale == 'maj') {
+ kkey = k.substr(0, scale.index);
+ } else if (scale == 'min') {
+ kkey = k.substr(0, scale.index + 1);
+ } else {
+ kkey = k.substr(0, scale.index + scale[0].length);
+ }
+ } else {
+ kkey = /^[a-g][#b]?/.exec(k) || '';
+ }
+ var result = accidentals(sigcodes[kkey]);
+ var extras = keyname.substr(kkey.length).match(/(_+|=|\^+)[a-g]/ig);
+ if (extras) {
+ 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];
} else {
- t = (Math.pow(0.5, -dotted) - 1) * result[result.length - 1].time;
+ result[note] = extras[j].substr(0, extras[j].length - 1);
}
- 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;
}
@@ -4598,7 +5204,6 @@ var Instrument = (function() {
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.
@@ -4826,47 +5431,6 @@ var Instrument = (function() {
}
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;
@@ -4885,123 +5449,7 @@ var Instrument = (function() {
}
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 };
- }
- 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];
- }
- }
- 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;
- }
- }
- 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];
- }
- }
- }
- 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;
- }
- } 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;
-})();
+}
// wavetable is a table of names for nonstandard waveforms.
// The table maps names to objects that have wave: and freq:
@@ -5094,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
@@ -5115,7 +5565,9 @@ function gatherelts(args) {
}
// Gather elements passed as arguments.
for (j = 0; j < argcount; ++j) {
- if (args[j].constructor === $) {
+ if (!args[j]) {
+ continue; // Skip null args.
+ } else if (args[j].constructor === $) {
elts.push.apply(elts, args[j].toArray()); // Unpack jQuery.
} else if ($.isArray(args[j])) {
elts.push.apply(elts, args[j]); // Accept an array.
@@ -5140,7 +5592,8 @@ function sync() {
// Unblock all animation queues.
for (j = 0; j < cb.length; ++j) { cb[j](); }
}
- for (j = 0; j < elts.length; ++j) {
+ if (elts.length > 1) for (j = 0; j < elts.length; ++j) {
+ queueWaitIfLoadingImg(elts[j]);
$(elts[j]).queue(function(next) {
if (ready) {
ready.push(next);
@@ -5259,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;
}
@@ -5279,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);
}
}
@@ -5310,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));
}
@@ -5324,28 +5778,8 @@ function visiblePause(elem, seconds) {
ms = seconds * 1000;
}
var thissel = $(elem);
- if (ms) {
- if (thissel.is(':visible')) {
- // Generic indication of taking some time for an action
- // A visual indicator of a pen color change.
- var circle = new Turtle('gray radius');
- circle.css({
- zIndex: 1,
- turtlePosition: thissel.css('turtlePosition'),
- turtleRotation: thissel.css('turtleRotation')
- });
- circle.animate({
- turtleRotation: '+=360'
- }, ms, 'linear');
- thissel.queue(function(next) {
- circle.done(function() {
- circle.remove();
- next();
- });
- });
- } else {
- thissel.delay(ms);
- }
+ if (ms > 0) {
+ thissel.delay(ms);
}
}
@@ -5361,10 +5795,10 @@ function doNothing() {}
// if the argument list is longer than argcount, or null otherwise.
function continuationArg(args, argcount) {
argcount = argcount || 0;
- if (args.length <= argcount || typeof(args[args.length - 1]) != 'function') {
- return null;
- }
- return args[args.length - 1];
+ if (args.length <= argcount) { return null; }
+ var lastarg = args[args.length - 1];
+ if (typeof(lastarg) === 'function' && !lastarg.helpname) { return lastarg; }
+ return null;
}
// This function helps implement the continuation-passing-style
@@ -5380,6 +5814,7 @@ function continuationArg(args, argcount) {
// as each of the elements' animations completes; when the jth
// element completes, resolve(j) should be called. The last time
// it is called, it will trigger the continuation callback, if any.
+// Call resolve(j, true) if a corner pen state should be marked.
// resolver: a function that returns a closure that calls resolve(j).
// start: a function to be called once to enable triggering of the callback.
// the last argument in an argument list if it is a function, and if the
@@ -5392,10 +5827,13 @@ function setupContinuation(thissel, name, args, argcount) {
countdown = length + 1,
sync = true,
debugId = debug.nextId();
- function resolve(j) {
+ function resolve(j, corner) {
if (j != null) {
- debug.reportEvent('resolve',
- [name, debugId, length, j, thissel && thissel[j]]);
+ var elem = thissel && thissel[j];
+ if (corner && elem) {
+ flushPenState(elem, $.data(elem, 'turtleData'), true);
+ }
+ debug.reportEvent('resolve', [name, debugId, length, j, elem]);
}
if ((--countdown) == 0) {
// A subtlety: if we still have not yet finished setting things up
@@ -5428,7 +5866,7 @@ function setupContinuation(thissel, name, args, argcount) {
args: mainargs,
appear: appear,
resolve: resolve,
- resolver: function(j) { return function() { resolve(j); }; },
+ resolver: function(j, c) { return function() { resolve(j, c); }; },
exit: function exit() {
debug.reportEvent('exit', [name, debugId, length, mainargs]);
// Resolve one extra countdown; this is needed for a done callback
@@ -5481,29 +5919,72 @@ function wrappredicate(name, helptext, fn) {
// Wrapglobalcommand does boilerplate setup for global commands that should
// queue on the main turtle queue when there is a main turtle, but that
// should execute immediately otherwise.
-function wrapglobalcommand(name, helptext, fn) {
+function wrapglobalcommand(name, helptext, fn, fnfilter) {
var wrapper = function globalcommandwrapper() {
checkForHungLoop(name);
if (interrupted) { throw new Error(name + ' interrupted'); }
- if (global_turtle) {
+ var early = null;
+ var argcount = 0;
+ var animate = global_turtle;
+ if (fnfilter) {
+ early = fnfilter.apply(null, arguments);
+ argcount = arguments.length;
+ animate = global_turtle_animating();
+ }
+ if (animate) {
var thissel = $(global_turtle).eq(0),
args = arguments,
- cc = setupContinuation(thissel, name, arguments, 0);
+ cc = setupContinuation(thissel, name, arguments, argcount);
thissel.plan(function(j, elem) {
cc.appear(j);
- fn.apply(null, args);
+ fn.apply(early, args);
this.plan(cc.resolver(j));
});
cc.exit();
} else {
- cc = setupContinuation(null, name, arguments, 0);
- fn.apply(null, arguments);
+ cc = setupContinuation(null, name, arguments, argcount);
+ fn.apply(early, arguments);
cc.exit();
}
+ if (early) {
+ if (early.result && early.result.constructor === jQuery && global_turtle) {
+ sync(global_turtle, early.result);
+ }
+ return early.result;
+ }
};
return wrapraw(name, helptext, wrapper);
}
+function wrapwindowevent(name, helptext) {
+ return wrapraw(name, helptext, function(d, fn) {
+ var forKey = /^key/.test(name),
+ forMouse = /^mouse|click$/.test(name),
+ filter = forMouse ? 'input,button' : forKey ?
+ 'textarea,input:not([type]),input[type=text],input[type=password]'
+ : null;
+ if (forKey) { focusWindowIfFirst(); }
+ if (fn == null && typeof(d) == 'function') { fn = d; d = null; }
+ $(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);
+ });
+ });
+}
+
+function windowhasturtleevent() {
+ var events = $._data(global, 'events');
+ if (!events) return false;
+ for (var type in events) {
+ var entries = events[type];
+ for (var j = 0; j < entries.length; ++j) {
+ if (entries[j].namespace == 'turtleevent') return true;
+ }
+ }
+ return false;
+}
+
// Wrapraw sets up help text for a function (such as "sqrt") that does
// not need any other setup.
function wrapraw(name, helptext, fn) {
@@ -5521,8 +6002,10 @@ function wrapraw(name, helptext, fn) {
function rtlt(cc, degrees, radius) {
if (degrees == null) {
degrees = 90; // zero-argument default.
+ } else {
+ degrees = normalizeRotationDelta(degrees);
}
- var elem, left = (cc.name === 'lt');
+ var elem, left = (cc.name === 'lt'), intick = insidetick;
if ((elem = canMoveInstantly(this)) &&
(radius === 0 || (radius == null && getTurningRadius(elem) === 0))) {
cc.appear(0);
@@ -5535,44 +6018,215 @@ function rtlt(cc, degrees, radius) {
this.plan(function(j, elem) {
cc.appear(j);
this.animate({turtleRotation: operator + cssNum(degrees || 0) + 'deg'},
- animTime(elem), animEasing(elem), cc.resolver(j));
+ animTime(elem, intick), animEasing(elem), cc.resolver(j));
});
return this;
} else {
this.plan(function(j, elem) {
cc.appear(j);
- var oldRadius = this.css('turtleTurningRadius');
- this.css({turtleTurningRadius: (degrees < 0) ? -radius : radius});
+ var state = getTurtleData(elem),
+ oldRadius = state.turningRadius,
+ newRadius = (degrees < 0) ? -radius : radius,
+ addCorner = null;
+ if (state.style && state.down) {
+ addCorner = (function() {
+ var oldPos = getCenterInPageCoordinates(elem),
+ oldTs = readTurtleTransform(elem, true),
+ oldTransform = totalTransform2x2(elem.parentElement);
+ return (function() {
+ addArcBezierPaths(
+ state.corners[0],
+ oldPos,
+ oldTs.rot,
+ oldTs.rot + (left ? -degrees : degrees),
+ newRadius * (state.oldscale ? oldTs.sy : 1),
+ oldTransform);
+ });
+ })();
+ }
+ state.turningRadius = newRadius;
this.animate({turtleRotation: operator + cssNum(degrees) + 'deg'},
- animTime(elem), animEasing(elem));
+ animTime(elem, intick), animEasing(elem));
+ this.plan(function() {
+ if (addCorner) addCorner();
+ state.turningRadius = oldRadius;
+ cc.resolve(j, true);
+ });
+ });
+ return this;
+ }
+}
+
+// Deals with both fd and bk by negating amount if cc.name is 'bk'.
+function fdbk(cc, amount) {
+ if (amount == null) {
+ amount = 100; // zero-argument default.
+ }
+ if (cc.name === 'bk') {
+ amount = -amount;
+ }
+ var elem, intick = insidetick;
+ if ((elem = canMoveInstantly(this))) {
+ cc.appear(0);
+ doQuickMove(elem, amount, 0);
+ cc.resolve(0, true);
+ return this;
+ }
+ this.plan(function(j, elem) {
+ cc.appear(j);
+ this.animate({turtleForward: '+=' + cssNum(amount || 0) + 'px'},
+ animTime(elem, intick), animEasing(elem), cc.resolver(j, true));
+ });
+ return this;
+}
+
+//////////////////////////////////////////////////////////////////////////
+// CARTESIAN MOVEMENT FUNCTIONS
+//////////////////////////////////////////////////////////////////////////
+
+function slide(cc, x, y) {
+ if ($.isArray(x)) {
+ y = x[1];
+ x = x[0];
+ }
+ if (!y) { y = 0; }
+ if (!x) { x = 0; }
+ var intick = insidetick;
+ this.plan(function(j, elem) {
+ cc && cc.appear(j);
+ this.animate({turtlePosition: displacedPosition(elem, y, x)},
+ animTime(elem, intick), animEasing(elem), cc && cc.resolver(j, true));
+ });
+ return this;
+}
+
+function movexy(cc, x, y) {
+ if ($.isArray(x)) {
+ y = x[1];
+ x = x[0];
+ }
+ if (!y) { y = 0; }
+ if (!x) { x = 0; }
+ var elem, intick = insidetick;
+ if ((elem = canMoveInstantly(this))) {
+ cc && cc.appear(0);
+ doQuickMoveXY(elem, x, y);
+ cc && cc.resolve(0);
+ return this;
+ }
+ this.plan(function(j, elem) {
+ cc && cc.appear(j);
+ var tr = getElementTranslation(elem);
+ this.animate(
+ { turtlePosition: cssNum(tr[0] + x) + ' ' + cssNum(tr[1] - y) },
+ animTime(elem, intick), animEasing(elem), cc && cc.resolver(j, true));
+ });
+ return this;
+}
+
+function moveto(cc, x, y) {
+ var position = x, localx = 0, localy = 0, limit = null, intick = insidetick;
+ if ($.isNumeric(position) && $.isNumeric(y)) {
+ // moveto x, y: use local coordinates.
+ localx = parseFloat(position);
+ localy = parseFloat(y);
+ position = null;
+ limit = null;
+ } else if ($.isArray(position)) {
+ // moveto [x, y], limit: use local coordinates (limit optional).
+ localx = position[0];
+ localy = position[1];
+ position = null;
+ limit = y;
+ } else if ($.isNumeric(y)) {
+ // moveto obj, limit: limited motion in the direction of obj.
+ limit = y;
+ }
+ // Otherwise moveto {pos}, limit: absolute motion with optional limit.
+ this.plan(function(j, elem) {
+ var pos = position;
+ if (pos === null) {
+ pos = $(homeContainer(elem)).pagexy();
+ }
+ if (pos && !isPageCoordinate(pos)) {
+ try {
+ pos = $(pos).pagexy();
+ } catch (e) {
+ return;
+ }
+ }
+ if (!pos || !isPageCoordinate(pos)) return;
+ if ($.isWindow(elem)) {
+ cc && cc.appear(j);
+ scrollWindowToDocumentPosition(pos, limit);
+ cc && cc.resolve(j);
+ return;
+ } else if (elem.nodeType === 9) {
+ return;
+ }
+ cc && cc.appear(j);
+ this.animate({turtlePosition:
+ computeTargetAsTurtlePosition(elem, pos, limit, localx, localy)},
+ animTime(elem, intick), animEasing(elem), cc && cc.resolver(j, true));
+ });
+ return this;
+}
+
+// Deals with jump, jumpxy, and jumpto functions
+function makejump(move) {
+ return (function(cc, x, y) {
+ this.plan(function(j, elem) {
+ cc.appear(j);
+ var down = this.css('turtlePenDown');
+ this.css({turtlePenDown: 'up'});
+ move.call(this, null, x, y);
this.plan(function() {
- this.css({turtleTurningRadius: oldRadius});
- cc.resolve(j);
+ this.css({turtlePenDown: down});
+ cc.resolve(j, true);
});
});
return this;
- }
+ });
}
-// Deals with both fd and bk by negating amount if cc.name is 'bk'.
-function fdbk(cc, amount) {
- if (amount == null) {
- amount = 100; // zero-argument default.
- }
- if (cc.name === 'bk') {
- amount = -amount;
- }
- var elem;
- if ((elem = canMoveInstantly(this))) {
- cc.appear(0);
- doQuickMove(elem, amount, 0);
- cc.resolve(0);
- return this;
- }
+//////////////////////////////////////////////////////////////////////////
+// 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) {
- cc.appear(0);
- this.animate({turtleForward: '+=' + cssNum(amount || 0) + 'px'},
- animTime(elem), animEasing(elem), cc.resolver(0));
+ 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;
}
@@ -5582,7 +6236,15 @@ function fdbk(cc, amount) {
// 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) {
if ($.isNumeric(style)) {
// Allow for parameters in either order.
@@ -5591,33 +6253,41 @@ function animatedDotCommand(fillShape) {
diameter = t;
}
if (diameter == null) { diameter = 8.8; }
- if (!style) { style = 'black'; }
- var ps = parsePenStyle(style, 'fillStyle');
this.plan(function(j, elem) {
+ var state = getTurtleData(elem),
+ penStyle = state.style;
+ if (!style) {
+ // If no color is specified, default to pen color, or black if no pen.
+ style = (penStyle && (penStyle.fillStyle || penStyle.strokeStyle)) ||
+ 'black';
+ }
cc.appear(j);
var c = this.pagexy(),
ts = readTurtleTransform(elem, true),
- state = getTurtleData(elem),
+ 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);
+ if (null == ps.lineWidth && penStyle && penStyle.lineWidth) {
+ ps.lineWidth = penStyle.lineWidth;
+ }
if (canMoveInstantly(this)) {
- fillShape(drawOnCanvas, c, finalDiam, ts.rot, ps);
+ fillShape(drawOnCanvas, c, finalDiam, ts.rot, ps, true);
cc.resolve(j);
} else {
this.queue(function(next) {
$({radius: 0}).animate({radius: animDiam}, {
- duration: animTime(elem),
+ duration: animTime(elem, intick),
step: function() {
if (!hasAlpha) {
- fillShape(drawOnCanvas, c, this.radius, ts.rot, ps);
+ fillShape(drawOnCanvas, c, this.radius, ts.rot, ps, false);
}
},
complete: function() {
- fillShape(drawOnCanvas, c, finalDiam, ts.rot, ps);
+ fillShape(drawOnCanvas, c, finalDiam, ts.rot, ps, true);
cc.resolve(j);
next();
}
@@ -5676,6 +6346,117 @@ function fillBox(drawOnCanvas, position, diameter, rot, style) {
ctx.restore();
}
+function fillArrow(drawOnCanvas, position, diameter, rot, style, drawhead) {
+ var ctx = drawOnCanvas.getContext('2d');
+ ctx.save();
+ applyPenStyle(ctx, style);
+ if (!style.strokeStyle && style.fillStyle) {
+ ctx.strokeStyle = style.fillStyle;
+ }
+ if (diameter !== Infinity) {
+ var c = Math.sin(rot / 180 * Math.PI),
+ s = -Math.cos(rot / 180 * Math.PI),
+ w = style.lineWidth || 1.62,
+ hx = position.pageX + diameter * c,
+ hy = position.pageY + diameter * s,
+ m = calcArrow(w, hx, hy, c, s),
+ ds = diameter - m.hs,
+ dx = ds * c,
+ dy = ds * s;
+ setCanvasPageTransform(ctx, drawOnCanvas);
+ if (ds > 0) {
+ ctx.beginPath();
+ ctx.moveTo(position.pageX, position.pageY);
+ ctx.lineTo(position.pageX + dx, position.pageY + dy);
+ ctx.stroke();
+ }
+ if (drawhead) {
+ drawArrowHead(ctx, m);
+ }
+ }
+ ctx.restore();
+}
+
+//////////////////////////////////////////////////////////////////////////
+// ARROW GEOMETRY
+//////////////////////////////////////////////////////////////////////////
+function calcArrow(w, x1, y1, cc, ss) {
+ var hw = Math.max(w * 1.25, w + 2),
+ hh = hw * 2,
+ hs = hh - hw / 2;
+ return {
+ hs: hs,
+ x1: x1,
+ y1: y1,
+ xm: x1 - cc * hs,
+ ym: y1 - ss * hs,
+ x2: x1 - ss * hw - cc * hh,
+ y2: y1 + cc * hw - ss * hh,
+ x3: x1 + ss * hw - cc * hh,
+ y3: y1 - cc * hw - ss * hh
+ };
+}
+
+function drawArrowHead(c, m) {
+ c.beginPath();
+ c.moveTo(m.x2, m.y2);
+ c.lineTo(m.x1, m.y1);
+ c.lineTo(m.x3, m.y3);
+ c.quadraticCurveTo(m.xm, m.ym, m.x2, m.y2);
+ c.closePath();
+ c.fill();
+}
+
+function drawArrowLine(c, w, x0, y0, x1, y1) {
+ var dx = x1 - x0,
+ dy = y1 - y0,
+ dd = Math.sqrt(dx * dx + dy * dy),
+ cc = dx / dd,
+ ss = dy / dd;
+ var m = calcArrow(w, x1, y1, cc, ss);
+ if (dd > m.hs) {
+ c.beginPath();
+ c.moveTo(x0, y0);
+ c.lineTo(m.xm, m.ym);
+ c.lineWidth = w;
+ c.stroke();
+ }
+ 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.
@@ -5699,133 +6480,26 @@ var turtlefn = {
["bk(pixels) Back. Moves in reverse by some pixels: " +
"bk 100"], fdbk),
slide: wrapcommand('slide', 1,
- ["slide(x, y) Slides right x and forward y pixels without turning: " +
- "slide 50, 100"],
- function slide(cc, x, y) {
- if ($.isArray(x)) {
- y = x[1];
- x = x[0];
- }
- if (!y) { y = 0; }
- if (!x) { x = 0; }
- this.plan(function(j, elem) {
- cc.appear(j);
- this.animate({turtlePosition: displacedPosition(elem, y, x)},
- animTime(elem), animEasing(elem), cc.resolver(j));
- });
- return this;
- }),
+ ["move(x, y) Slides right x and forward y pixels without turning: " +
+ "slide 50, 100"], slide),
movexy: wrapcommand('movexy', 1,
["movexy(x, y) Changes graphing coordinates by x and y: " +
- "movexy 50, 100"],
- function movexy(cc, x, y) {
- if ($.isArray(x)) {
- y = x[1];
- x = x[0];
- }
- if (!y) { y = 0; }
- if (!x) { x = 0; }
- var elem;
- if ((elem = canMoveInstantly(this))) {
- cc.appear(0);
- doQuickMoveXY(elem, x, y);
- cc.resolve(0);
- return this;
- }
- this.plan(function(j, elem) {
- cc.appear(j);
- var tr = getElementTranslation(elem);
- this.animate(
- { turtlePosition: cssNum(tr[0] + x) + ' ' + cssNum(tr[1] - y) },
- animTime(elem), animEasing(elem), cc.resolver(j));
- });
- return this;
- }),
+ "movexy 50, 100"], movexy),
moveto: wrapcommand('moveto', 1,
["moveto(x, y) Move to graphing coordinates (see getxy): " +
"moveto 50, 100",
"moveto(obj) Move to page coordinates " +
"or an object on the page (see pagexy): " +
- "moveto lastmousemove"],
- function moveto(cc, x, y) {
- var position = x, localx = 0, localy = 0, limit = null;
- if ($.isNumeric(position) && $.isNumeric(y)) {
- // moveto x, y: use local coordinates.
- localx = parseFloat(position);
- localy = parseFloat(y);
- position = null;
- limit = null;
- } else if ($.isArray(position)) {
- // moveto [x, y], limit: use local coordinates (limit optional).
- localx = position[0];
- localy = position[1];
- position = null;
- limit = y;
- } else if ($.isNumeric(y)) {
- // moveto obj, limit: limited motion in the direction of obj.
- limit = y;
- }
- // Otherwise moveto {pos}, limit: absolute motion with optional limit.
- this.plan(function(j, elem) {
- var pos = position;
- if (pos === null) {
- pos = $(homeContainer(elem)).pagexy();
- }
- if (pos && !isPageCoordinate(pos)) {
- try {
- pos = $(pos).pagexy();
- } catch (e) {
- return;
- }
- }
- if (!pos || !isPageCoordinate(pos)) return;
- if ($.isWindow(elem)) {
- cc.appear(j);
- scrollWindowToDocumentPosition(pos, limit);
- cc.resolve(j);
- return;
- } else if (elem.nodeType === 9) {
- return;
- }
- cc.appear(j);
- this.animate({turtlePosition:
- computeTargetAsTurtlePosition(elem, pos, limit, localx, localy)},
- animTime(elem), animEasing(elem), cc.resolver(j));
- });
- return this;
- }),
+ "moveto lastmousemove"], moveto),
jump: wrapcommand('jump', 1,
["jump(x, y) Move without drawing (compare to slide): " +
- "jump 0, 50"],
- function jump(cc, x, y) {
- this.plan(function(j, elem) {
- cc.appear(j);
- var down = this.css('turtlePenDown');
- this.css({turtlePenDown: 'up'});
- this.slide.apply(this, cc.args);
- this.plan(function() {
- this.css({turtlePenDown: down});
- cc.resolve(j);
- });
- });
- return this;
- }),
+ "jump 0, 50"], makejump(slide)),
+ jumpxy: wrapcommand('jumpxy', 1,
+ ["jumpxy(x, y) Move without drawing (compare to movexy): " +
+ "jump 0, 50"], makejump(movexy)),
jumpto: wrapcommand('jumpto', 1,
["jumpto(x, y) Move without drawing (compare to moveto): " +
- "jumpto 50, 100"],
- function jumpto(cc, x, y) {
- this.plan(function(j, elem) {
- cc.appear(j);
- var down = this.css('turtlePenDown');
- this.css({turtlePenDown: 'up'});
- this.moveto.apply(this, cc.args);
- this.plan(function() {
- this.css({turtlePenDown: down});
- cc.resolve(j);
- });
- });
- return this;
- }),
+ "jumpto 50, 100"], makejump(moveto)),
turnto: wrapcommand('turnto', 1,
["turnto(degrees) Turn to a direction. " +
"North is 0, East is 90: turnto 270",
@@ -5839,6 +6513,7 @@ var turtlefn = {
bearing = [bearing, y];
y = null;
}
+ var intick = insidetick;
this.plan(function(j, elem) {
cc.appear(j);
if ($.isWindow(elem) || elem.nodeType === 9) {
@@ -5873,15 +6548,21 @@ 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);
dir = limitRotation(ts.rot, dir, limit === null ? 360 : limit);
}
dir = ts.rot + normalizeRotation(dir - ts.rot);
+ var oldRadius = this.css('turtleTurningRadius');
+ this.css({turtleTurningRadius: 0});
this.animate({turtleRotation: dir},
- animTime(elem), animEasing(elem), cc.resolver(j));
+ animTime(elem, intick), animEasing(elem));
+ this.plan(function() {
+ this.css({turtleTurningRadius: oldRadius});
+ cc.resolve(j);
+ });
});
return this;
}),
@@ -5906,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: " +
@@ -5917,66 +6644,74 @@ var turtlefn = {
"pen off; pen on."
],
function pen(cc, penstyle, lineWidth) {
+ var args = autoArgs(arguments, 1, {
+ lineCap: /^(?:butt|square|round)$/,
+ lineJoin: /^(?:bevel|round|miter)$/,
+ lineWidth: $.isNumeric,
+ penStyle: '*'
+ });
+ penstyle = args.penStyle;
if (penstyle && (typeof(penstyle) == "function") && (
penstyle.helpname || penstyle.name)) {
// Deal with "tan" and "fill".
penstyle = (penstyle.helpname || penstyle.name);
}
- if (typeof(penstyle) == "number" && typeof(lineWidth) != "number") {
- // Deal with swapped argument order.
- var swap = penstyle;
- penstyle = lineWidth;
- lineWidth = swap;
- }
- if (lineWidth === 0) {
+ if (args.lineWidth === 0 || penstyle === null) {
penstyle = "none";
- }
- if (penstyle === undefined) {
+ } else if (penstyle === undefined) {
penstyle = 'black';
- } else if (penstyle === null) {
- penstyle = 'none';
} else if ($.isPlainObject(penstyle)) {
penstyle = writePenStyle(penstyle);
}
+ var intick = insidetick;
this.plan(function(j, elem) {
cc.appear(j);
- var animate = !canMoveInstantly(this) && this.is(':visible'),
+ var animate = !invisible(elem) && !canMoveInstantly(this),
oldstyle = animate && parsePenStyle(this.css('turtlePenStyle')),
- olddown = animate && this.css('turtlePenDown'),
+ olddown = oldstyle && ('down' == this.css('turtlePenDown')),
moved = false;
if (penstyle === false || penstyle === true ||
penstyle == 'down' || penstyle == 'up') {
this.css('turtlePenDown', penstyle);
moved = true;
} else {
- if (lineWidth) {
- penstyle += ";lineWidth:" + lineWidth;
+ if (args.lineWidth) {
+ penstyle += ";lineWidth:" + args.lineWidth;
+ }
+ if (args.lineCap) {
+ penstyle += ";lineCap:" + args.lineCap;
+ }
+ if (args.lineJoin) {
+ penstyle += ";lineJoin:" + args.lineJoin;
}
this.css('turtlePenStyle', penstyle);
+ this.css('turtlePenDown', penstyle == 'none' ? 'up' : 'down');
}
if (animate) {
// A visual indicator of a pen color change.
var style = parsePenStyle(this.css('turtlePenStyle')),
- color = (style && style.strokeStyle) ||
+ color = (style && (style.strokeStyle ||
+ (style.savePath && 'gray'))) ||
(oldstyle && oldstyle.strokeStyle) || 'gray',
target = {},
- newdown = this.css('turtlePenDown'),
- pencil = new Turtle(color + ' pencil'),
+ newdown = style && 'down' == this.css('turtlePenDown'),
+ pencil = new Turtle(color + ' pencil', this.parent()),
distance = this.height();
pencil.css({
zIndex: 1,
- turtlePosition: this.css('turtlePosition'),
+ turtlePosition: computeTargetAsTurtlePosition(
+ pencil.get(0), this.pagexy(), null, 0, 0),
turtleRotation: this.css('turtleRotation'),
turtleSpeed: Infinity
});
- if (olddown == "up") {
+ if (!olddown) {
pencil.css({ turtleForward: "+=" + distance, opacity: 0 });
- if (newdown == "down") {
+ if (newdown) {
target.turtleForward = "-=" + distance;
target.opacity = 1;
}
} else {
- if (newdown == "up") {
+ if (!newdown) {
target.turtleForward = "+=" + distance;
target.opacity = 0;
}
@@ -5989,7 +6724,7 @@ var turtlefn = {
pencil.css({ opacity: 0 });
target.opacity = 1;
}
- pencil.animate(target, animTime(elem));
+ pencil.animate(target, animTime(elem, intick));
this.queue(function(next) {
pencil.done(function() {
pencil.remove();
@@ -6008,7 +6743,7 @@ var turtlefn = {
"pen path: " +
"pen path; rt 100, 90; fill blue"],
function fill(cc, style) {
- if (!style) { style = 'black'; }
+ if (!style) { style = 'none'; }
else if ($.isPlainObject(style)) {
style = writePenStyle(style);
}
@@ -6028,6 +6763,9 @@ var turtlefn = {
["box(color, size) Draws a box. " +
"Color and size are optional: " +
"dot blue"], animatedDotCommand(fillBox)),
+ arrow: wrapcommand('arrow', 0,
+ ["arrow(color, size) Draws an arrow. " +
+ "arrow red, 100"], animatedDotCommand(fillArrow)),
mirror: wrapcommand('mirror', 1,
["mirror(flipped) Mirrors the turtle across its main axis, or " +
"unmirrors if flipped if false. " +
@@ -6060,26 +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; }
- 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), 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",
@@ -6154,6 +6876,44 @@ var turtlefn = {
function pf() {
return this.pen('path', continuationArg(arguments, 0));
},
+ clip: wrapcommand('clip', 1,
+ ["Clips tranparent bits out of the image of the sprite, " +
+ "and sets the hit region."],
+ function clip(cc, threshold) {
+ if (threshold == null) {
+ threshold = 0.125;
+ }
+ return this.plan(function(j, elem) {
+ cc.appear(j);
+ if (elem.tagName == 'CANVAS') {
+ var hull = transparentHull(elem, threshold),
+ sel = $(elem),
+ origin = readTransformOrigin(elem);
+ eraseOutsideHull(elem, hull);
+ scalePolygon(hull,
+ parseFloat(sel.css('width')) / elem.width,
+ parseFloat(sel.css('height')) / elem.height,
+ -origin[0], -origin[1]);
+ sel.css('turtleHull', hull);
+ }
+ cc.resolve(j);
+ });
+ }),
+ say: wrapcommand('say', 1,
+ ["say(words) Say something. Use English words." +
+ "say \"Let's go!\""],
+ function say(cc, words) {
+ this.plan(function(j, elem) {
+ cc.appear(j);
+ this.queue(function(next) {
+ utterSpeech(words, function() {
+ cc.resolve(j);
+ next();
+ });
+ });
+ });
+ return this;
+ }),
play: wrapcommand('play', 1,
["play(notes) Play notes. Notes are specified in " +
"" +
@@ -6219,12 +6979,16 @@ var turtlefn = {
"wear(url) Sets the turtle image url: " +
"wear 'http://bit.ly/1bgrQ0p'"],
function wear(cc, name, css) {
- if (typeof(name) == 'object' && typeof(css) == 'string') {
+ if ((typeof(name) == 'object' || typeof(name) == 'number') &&
+ typeof(css) == 'string') {
var t = css;
css = name;
name = t;
}
- var img = nameToImg(name, 'turtle');
+ if (typeof(css) == 'number') {
+ css = { height: css };
+ }
+ var img = nameToImg(name, 'turtle'), intick = insidetick;
if (!img) return this;
if (css) {
$.extend(img.css, css);
@@ -6237,9 +7001,19 @@ var turtlefn = {
this.css({
backgroundImage: 'none',
});
- applyImg(this, img);
+ var loaded = false, waiting = null;
+ applyImg(this, img, function() {
+ loaded = true;
+ var callback = waiting;
+ if (callback) { waiting = null; callback(); }
+ });
if (!canMoveInstantly(this)) {
- this.delay(animTime(elem));
+ this.delay(animTime(elem, intick));
+ }
+ if (!loaded) {
+ this.pause({done: function(cb) {
+ if (loaded) { cb(); } else { waiting = cb; }
+ }});
}
this.plan(function() {
cc.resolve(j);
@@ -6247,20 +7021,62 @@ var turtlefn = {
});
return this;
}),
+ saveimg: wrapcommand('saveimg', 1,
+ ["saveimg(filename) Saves the turtle's image as a file. " +
+ "t.saveimg 'mypicture.png'"],
+ function saveimg(cc, filename) {
+ return this.plan(function(j, elem) {
+ cc.appear(j);
+ var ok = false;
+ if (!filename) { filename = 'img'; }
+ var canvas = this.canvas();
+ if (!canvas) {
+ see.html('Cannot saveimg: not a canvas');
+ } else {
+ var dataurl = canvas.toDataURL();
+ var dparts = /^data:image\/(\w+);base64,(.*)$/i.exec(dataurl);
+ if (!dparts) {
+ see.html('Cannot saveimg: ' +
+ 'canvas toDataURL did not work as expected.');
+ } else {
+ if (dparts[1] && filename.toLowerCase().lastIndexOf(
+ '.' + dparts[1].toLowerCase()) !=
+ Math.max(0, filename.length - dparts[1].length - 1)) {
+ filename += '.' + dparts[1];
+ }
+ ok = true;
+ dollar_turtle_methods.save(filename, atob(dparts[2]), function() {
+ cc.resolve(j);
+ });
+ }
+ }
+ if (!ok) {
+ cc.resolve(j);
+ }
+ });
+ }),
drawon: wrapcommand('drawon', 1,
["drawon(canvas) Switches to drawing on the specified canvas. " +
"A = new Sprite('100x100'); " +
"drawon A; pen red; fd 50; done -> A.rt 360"],
function drawon(cc, canvas) {
+ this.each(function() {
+ var state = getTurtleData(this);
+ if (state.drawOnCanvasSync) sync(this, state.drawOnCanvasSync);
+ state.drawOnCanvasSync = canvas;
+ });
+ sync(canvas, this);
return this.plan(function(j, elem) {
cc.appear(j);
var state = getTurtleData(elem);
- if (!canvas) {
+ if (!canvas || canvas === global) {
state.drawOnCanvas = null;
} else if (canvas.jquery && $.isFunction(canvas.canvas)) {
state.drawOnCanvas = canvas.canvas();
} else if (canvas.tagName && canvas.tagName == 'CANVAS') {
state.drawOnCanvas = canvas;
+ } else if (canvas.nodeType == 1 || canvas.nodeType == 9) {
+ state.drawOnCanvas = $(canvas).canvas();
}
cc.resolve(j);
});
@@ -6268,20 +7084,37 @@ var turtlefn = {
label: wrapcommand('label', 1,
["label(text) Labels the current position with HTML: " +
"label 'remember'",
- "label(text, styles) Apply CSS styles to the label: " +
- "label 'big', { fontSize: 100 }"],
- function label(cc, html, styles) {
+ "label(text, styles, labelsite) Optional position specifies " +
+ "'top', 'bottom', 'left', 'right', and optional styles is a size " +
+ "or CSS object: " +
+ "label 'big', { color: red, fontSize: 100 }, 'bottom'"],
+ function label(cc, html, side, styles) {
+ if ((!styles || typeof(styles) == 'string') &&
+ ($.isNumeric(side) || $.isPlainObject(side))) {
+ // Handle switched second and third argument order.
+ var t = styles;
+ styles = side;
+ side = t;
+ }
if ($.isNumeric(styles)) {
styles = { fontSize: styles };
}
+ if (side == null) {
+ side =
+ styles && 'labelSide' in styles ? styles.labelSide :
+ styles && 'label-side' in styles ? styles['label-side'] :
+ side = 'rotated scaled';
+ }
+ var intick = insidetick;
return this.plan(function(j, elem) {
cc.appear(j);
- var applyStyles = {}, currentStyles = this.prop('style');
+ 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;
@@ -6298,20 +7131,42 @@ var turtlefn = {
left: 0
}, styles);
// Place the label on the screen using the figured styles.
- var out = output(html, 'label').css(applyStyles)
- .addClass('turtle').appendTo(getTurtleField());
+ 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);
+ }
+ if (styles && 'class' in styles) {
+ out.addClass(styles.class);
+ }
+ var rotated = /\brotated\b/.test(side),
+ scaled = /\bscaled\b/.test(side);
// Mimic the current position and rotation and scale of the turtle.
out.css({
turtlePosition: computeTargetAsTurtlePosition(
out.get(0), this.pagexy(), null, 0, 0),
- turtleRotation: this.css('turtleRotation'),
- turtleScale: this.css('turtleScale')
+ turtleRotation: rotated ? this.css('turtleRotation') : 0,
+ turtleScale: scaled ? this.css('turtleScale') : 1
});
+ var gbcr = out.get(0).getBoundingClientRect();
+ // Modify top-left to slide to the given corner, if requested.
+ if (/\b(?:top|bottom)\b/.test(side)) {
+ applyStyles.top =
+ (/\btop\b/.test(side) ? -1 : 1) * gbcr.height / 2;
+ }
+ if (/\b(?:left|right)\b/.test(side)) {
+ applyStyles.left =
+ (/\bleft\b/.test(side) ? -1 : 1) * gbcr.width / 2;
+ }
// Then finally apply styles (turtle styles may be overridden here).
out.css(applyStyles);
// Add a delay.
if (!canMoveInstantly(this)) {
- this.delay(animTime(elem));
+ this.delay(animTime(elem, intick));
}
this.plan(function() {
cc.resolve(j);
@@ -6325,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;
}
@@ -6379,7 +7234,7 @@ var turtlefn = {
}),
getxy: wrappredicate('getxy',
["getxy() Graphing coordinates [x, y], center-based: " +
- "v = getxy(); slide -v[0], -v[1]"],
+ "v = getxy(); move -v[0], -v[1]"],
function getxy() {
if (!this.length) return;
return computePositionAsLocalOffset(this[0]);
@@ -6438,7 +7293,58 @@ var turtlefn = {
"c = turtle.canvas().getContext('2d'); c.fillStyle = red; " +
"c.fillRect(10, 10, 30, 30)"],
function canvas() {
- return this.filter('canvas').get(0);
+ return this.filter('canvas').get(0) || this.find('canvas').get(0);
+ }),
+ imagedata: wrapraw('imagedata',
+ ["imagedata() Returns the image data for the turtle. " +
+ "imdat = imagedata(); write imdat.data.length, 'bytes'",
+ "imagedata(imdat) Sets the image data for the turtle. " +
+ "imagedata({width: 1, height:1, data:[255,0,0,255]});",
+ ],
+ function imagedata(val) {
+ var canvas = this.canvas();
+ if (!canvas) {
+ if (val) throw new Error(
+ 'can only set imagedata on a canvas like a Sprite');
+ var img = this.filter('img').get(0);
+ if (!img) return;
+ canvas = getOffscreenCanvas(img.naturalWidth, img.naturalHeight);
+ canvas.getContext('2d').drawImage(img, 0, 0);
+ }
+ var ctx = canvas.getContext('2d');
+ if (!val) {
+ // The read case: return the image data for the whole canvas.
+ return ctx.getImageData(0, 0, canvas.width, canvas.height);
+ }
+ // The write case: if it's not an ImageData, convert it to one.
+ if (!(val instanceof ImageData)) {
+ if (typeof val != 'object' ||
+ !$.isNumeric(val.width) || !$.isNumeric(val.height) ||
+ !($.isArray(val.data) || val.data instanceof Uint8ClampedArray ||
+ val.data instanceof Uint8Array)) {
+ return;
+ }
+ var imdat = ctx.createImageData(
+ Math.round(val.width), Math.round(val.height));
+ var minlen = Math.min(val.data.length, imdat.data.length);
+ for (var j = 0; j < minlen; ++j) { imdat.data[j] = val.data[j]; }
+ val = imdat;
+ }
+ // If the size must be changed, resize it.
+ if (val.width != canvas.width ||
+ val.height != canvas.height) {
+ var oldOrigin = readTransformOrigin(canvas);
+ canvas.width = val.width;
+ canvas.height = val.height;
+ var newOrigin = readTransformOrigin(canvas);
+ // Preserve the origin if it's a turtle.
+ moveToPreserveOrigin(canvas, oldOrigin, newOrigin);
+ // Drop the turtle hull, if any.
+ $(canvas).css('turtleHull', 'auto');
+ ctx = canvas.getContext('2d');
+ }
+ // Finally put the image data into the canvas.
+ ctx.putImageData(val, 0, 0);
}),
cell: wrapraw('cell',
["cell(r, c) Row r and column c in a table. " +
@@ -6454,13 +7360,15 @@ var turtlefn = {
["shown() True if turtle is shown, false if hidden: " +
"do ht; write shown()"],
function shown() {
- return this.is(':visible');
+ var elem = this.get(0);
+ return elem && !invisible(elem);
}),
hidden: wrappredicate('hidden',
["hidden() True if turtle is hidden: " +
"do ht; write hidden()"],
function hidden() {
- return !this.is(':visible');
+ var elem = this.get(0);
+ return !elem || invisible(elem);
}),
inside: wrappredicate('inside',
["inside(obj) True if the turtle is encircled by obj: " +
@@ -6471,7 +7379,7 @@ var turtlefn = {
elem = $(elem);
}
if (elem.jquery) {
- if (!elem.length || !elem.is(':visible')) return false;
+ if (!elem.length || invisible(elem[0])) return false;
elem = elem[0];
}
var gbcr0 = getPageGbcr(elem),
@@ -6506,7 +7414,10 @@ var turtlefn = {
"touches(color) True if the turtle touches a drawn color: " +
"touches red"],
function touches(arg, y) {
- if (!this.is(':visible') || !this.length) { return false; }
+ if (!this.length || invisible(this[0])) { return false; }
+ if (typeof(arg) == "function" && isCSSColor(arg.helpname)) {
+ arg = arg.helpname;
+ }
if (arg == 'color' || isCSSColor(arg)) {
return touchesPixel(this[0], arg == 'color' ? null : arg);
}
@@ -6520,15 +7431,23 @@ var turtlefn = {
if (!arg) return false;
if (typeof arg === 'string') { arg = $(arg); }
if (!arg.jquery && !$.isArray(arg)) { arg = [arg]; }
- var anyok = false, k = 0, j, obj, elem, gbcr0, toucher;
+ var anyok = false, k = 0, j, obj, elem, gbcr0, toucher, gbcr1;
for (;!anyok && k < this.length; ++k) {
elem = this[k];
gbcr0 = getPageGbcr(elem);
+ // hidden elements do not touch anything
+ if (gbcr0.width == 0) { continue; }
toucher = null;
for (j = 0; !anyok && j < arg.length; ++j) {
obj = arg[j];
// Optimize the outside-bounding-box case.
- if (isDisjointGbcr(gbcr0, getPageGbcr(obj))) {
+ gbcr1 = getPageGbcr(obj);
+ if (isDisjointGbcr(gbcr0, gbcr1)) {
+ continue;
+ }
+ // Do not touch removed or hidden elements, or points without
+ // a pageX/pageY coordinate.
+ if (gbcr1.width == 0 && (obj.pageX == null || obj.pageY == null)) {
continue;
}
if (!toucher) {
@@ -6613,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: " +
@@ -6624,7 +7542,7 @@ var turtlefn = {
callback = qname;
qname = 'fx';
}
- // If animation is active, then direct will queue the callback.
+ // If animation is active, then plan will queue the callback.
// It will also arrange things so that if the callback enqueues
// further animations, they are inserted at the same location,
// so that the callback can expand into several animations,
@@ -6635,34 +7553,26 @@ var turtlefn = {
(function() { callback.call($(elem), index, elem); })),
lastanim = elemqueue.length && elemqueue[elemqueue.length - 1],
animation = (function() {
- var saved = $.queue(this, qname),
- subst = [], inserted;
- if (saved[0] === 'inprogress') {
- subst.unshift(saved.shift());
- }
- $.queue(elem, qname, subst);
- action();
- // The Array.prototype.push is faster.
- // $.merge($.queue(elem, qname), saved);
- Array.prototype.push.apply($.queue(elem, qname), saved);
- if (global_plan_counter++ % 64) {
- $.dequeue(elem, qname);
- } else {
- // Insert a timeout after executing a batch of plans,
- // to avoid deep recursion.
- async_pending += 1;
- setTimeout(function() {
- async_pending -= 1;
- $.dequeue(elem, qname);
- }, 0);
- }
- });
+ var saved = $.queue(this, qname),
+ subst = [], inserted;
+ if (saved[0] === 'inprogress') {
+ subst.unshift(saved.shift());
+ }
+ $.queue(elem, qname, subst);
+ action();
+ // The Array.prototype.push is faster.
+ // $.merge($.queue(elem, qname), saved);
+ Array.prototype.push.apply($.queue(elem, qname), saved);
+ nonrecursive_dequeue(elem, qname);
+ });
animation.finish = action;
$.queue(elem, qname, animation);
}
var elem, sel, length = this.length, j = 0;
for (; j < length; ++j) {
elem = this[j];
+ // Special case: first wait for an unloaded image to load.
+ queueWaitIfLoadingImg(elem, qname);
// Queue an animation if there is a queue.
var elemqueue = $.queue(elem, qname);
if (elemqueue.length) {
@@ -6677,6 +7587,51 @@ 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';
+ if (img.tagName == 'IMG' && img.src && !img.complete) {
+ var queue = $.queue(img, qname);
+ if (queue.length == 0) {
+ $.queue(img, qname, function(next) {
+ afterImageLoadOrError(img, null, next);
+ });
+ nonrecursive_dequeue(img, qname);
+ }
+ }
+}
+
+//////////////////////////////////////////////////////////////////////////
+// HUNG LOOP DETECTION
+//////////////////////////////////////////////////////////////////////////
+
var warning_shown = {},
loopCounter = 0,
hungTimer = null,
@@ -6689,7 +7644,7 @@ var warning_shown = {},
// 100th turtle motion. If it takes more than a few seconds to receive it,
// our script is blocking message dispatch, and an interrupt is triggered.
function checkForHungLoop(fname) {
- if ($.turtle.hungtimeout == Infinity || loopCounter++ < 100) {
+ if ($.turtle.hangtime == Infinity || loopCounter++ < 100) {
return;
}
loopCounter = 0;
@@ -6704,8 +7659,8 @@ function checkForHungLoop(fname) {
}, 0);
return;
}
- // Timeout after which we interrupt the program: 6 seconds.
- if (now - hangStartTime > $.turtle.hungtimeout) {
+ // Timeout after which we interrupt the program.
+ if (now - hangStartTime > $.turtle.hangtime) {
if (see.visible()) {
see.html('Oops: program ' +
'interrupted because it was hanging the browser. ' +
@@ -6714,7 +7669,7 @@ function checkForHungLoop(fname) {
'tick ' +
'to make an animation.');
}
- $.turtle.interrupt();
+ $.turtle.interrupt('hung');
}
}
@@ -6765,6 +7720,7 @@ function deprecate(map, oldname, newname) {
__extends(map[oldname], map[newname]);
}
}
+deprecate(turtlefn, 'move', 'slide');
deprecate(turtlefn, 'direct', 'plan');
deprecate(turtlefn, 'enclosedby', 'inside');
deprecate(turtlefn, 'bearing', 'direction');
@@ -6784,10 +7740,13 @@ $.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 };
-var eventfn = { click:1, mouseup:1, mousedown:1, mousemove:1,
- keydown:1, keypress:1, keyup:1 };
+function global_turtle_animating() {
+ return (global_turtle && $.queue(global_turtle).length > 0);
+}
var global_turtle = null;
var global_turtle_methods = [];
@@ -6810,21 +7769,34 @@ var dollar_turtle_methods = {
if (interrupted) return false;
if (tickinterval) return true;
if ($.timers.length) return true;
+ if (forever_timers.length) return true;
if (async_pending) return true;
- if (global_turtle && $.queue(global_turtle).length > 0) return true;
+ if (global_turtle_animating()) return true;
if ($(':animated').length) return true;
- return ($('.turtle').filter(function() {
- return $.queue(this).length > 0;
- }).length > 0);
+ if ($('.turtle').filter(function() {
+ return $.queue(this).length > 0; }).length > 0) return true;
+ if ($('.turtleinput').filter(function() {
+ return !$(this).prop('disabled')}).length > 0) return true;
+ if (windowhasturtleevent()) return true;
+ return false;
}
// Stop all animations.
$(':animated,.turtle').clearQueue().stop();
// Stop our audio.
resetAudio();
+ // Disable all input.
+ $('.turtleinput').prop('disabled', true);
+ // Detach all event handlers on the window.
+ $(global).off('.turtleevent');
+ // Low-level detach all jQuery events
+ $('*').not('#_testpanel *').map(
+ function(i, e) { $._data(e, 'events', null) });
// Set a flag that will cause all commands to throw.
interrupted = true;
// Turn off the global tick interval timer.
globaltick(null, null);
+ // Turn off timers for 'forever'
+ clearForever();
// Run through any remaining timers, stopping each one.
// This handles the case of animations (like "dot") that
// are not attached to an HTML element.
@@ -6834,7 +7806,8 @@ var dollar_turtle_methods = {
}
}
// Throw an interrupt exception.
- throw new Error('interrupt() called');
+ var msg = option ? "'" + option + "'" : '';
+ throw new Error('interrupt(' + msg + ') called');
}),
cs: wrapglobalcommand('cs',
["cs() Clear screen. Erases both graphics canvas and " +
@@ -6846,7 +7819,7 @@ var dollar_turtle_methods = {
["cg() Clear graphics. Does not alter body text: " +
"do cg"],
function cg() {
- clearField('canvas');
+ clearField('canvas labels');
}),
ct: wrapglobalcommand('ct',
["ct() Clear text. Does not alter graphics canvas: " +
@@ -6865,12 +7838,24 @@ var dollar_turtle_methods = {
["sizexy() Get the document pixel [width, height]. " +
"[w, h] = sizexy(); canvas('2d').fillRect(0, 0, w, h)"],
sizexy),
+ forever: wrapraw('forever',
+ ["forever(fn) Calls fn repeatedly, forever. " +
+ "forever -> fd 2; rt 2",
+ "forever(fps, fn) Calls fn repeating fps per second. " +
+ "forever 2, -> fd 25; dot blue"],
+ forever),
+ stop: wrapraw('stop',
+ ["stop() stops the current forever loop. " +
+ "forever -> fd 10; if not inside window then stop()",
+ "stop(fn) stops the forever loop corresponding to fn.",
+ "Use break to stop a for or while loop."],
+ stop),
tick: wrapraw('tick',
["tick(fps, fn) Calls fn fps times per second until " +
"tick is called again: " +
"c = 10; tick 1, -> c and write(c--) or tick()"],
function tick(tps, fn) {
- if (global_turtle) {
+ if (global_turtle_animating()) {
var sel = $(global_turtle);
sel.plan(function() {
globaltick(tps, fn);
@@ -6885,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 " +
"" +
@@ -6916,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',
@@ -6955,44 +7954,150 @@ var dollar_turtle_methods = {
callback.apply(this, arguments);
}
});
- sync = null;
}),
- append: wrapraw('append',
+ load: wrapraw('load',
+ ["load(url, cb) Loads data from the url and passes it to cb. " +
+ "load 'intro', (t) -> write 'intro contains', t"],
+ function(url, cb) {
+ var val;
+ $.ajax(apiUrl(url, 'load'), { async: !!cb, complete: function(xhr) {
+ try {
+ 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) {
+ val = xhr.responseText;
+ }
+ }
+ if (cb) {
+ cb(val, xhr);
+ }
+ }});
+ return val;
+ }),
+ save: wrapraw('save',
+ ["save(url, data, cb) Posts data to the url and calls when done. " +
+ "save 'intro', 'pen gold, 20\\nfd 100\\n'"],
+ function(url, data, cb) {
+ if (!url) throw new Error('Missing url for save');
+ 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 {
+ for (key in data) if (data.hasOwnProperty(key)) {
+ if (typeof data[key] == 'string') {
+ payload[key] = data[key];
+ } else {
+ payload[key] = JSON.stringify(data[key]);
+ }
+ }
+ }
+ if (payload && !payload.key) {
+ var login = loginCookie();
+ if (login && login.key && login.user == pencilUserFromUrl(url)) {
+ payload.key = login.key;
+ }
+ }
+ $.ajax(apiUrl(url, 'save'), {
+ type: 'POST',
+ data: payload,
+ complete: function(xhr) {
+ var val
+ try {
+ val = JSON.parse(xhr.responseText);
+ } catch(e) {
+ if (val == null && xhr && xhr.responseText) {
+ val = xhr.responseText;
+ }
+ }
+ if (cb) {
+ cb(val, xhr);
+ }
+ }
+ });
+ }),
+ append: wrapglobalcommand('append',
["append(html) Appends text to the document without a new line. " +
"append 'try this twice...'"],
function append(html) {
$.fn.append.apply($('body'), arguments);
}),
- write: wrapraw('write',
- ["write(html) Writes a line of text. Arbitrary HTML may be written: " +
- "write 'Hello, world!'"],
- function write(html) {
- return output(Array.prototype.join.call(arguments, ' '), 'div');
- }),
- type: wrapraw('type',
+ type: wrapglobalcommand('type',
["type(text) Types preformatted text like a typewriter. " +
"type 'Hello!\n'"], plainTextPrint),
- read: wrapraw('read',
+ 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() {
+ return prepareOutput(Array.prototype.join.call(arguments, ' '), 'div');
+ }),
+ read: wrapglobalcommand('read',
["read(fn) Reads text or numeric input. " +
"Calls fn once: " +
"read (x) -> write x",
"read(html, fn) Prompts for input: " +
"read 'Your name?', (v) -> write 'Hello ' + v"],
- function read(a, b) { return input(a, b, 0); }),
- readnum: wrapraw('readnum',
+ doOutput, function read(a, b) { return prepareInput(a, b, 0); }),
+ readnum: wrapglobalcommand('readnum',
["readnum(html, fn) Reads numeric input. Only numbers allowed: " +
"readnum 'Amount?', (v) -> write 'Tip: ' + (0.15 * v)"],
- function readnum(a, b) { return input(a, b, 1); }),
- readstr: wrapraw('readstr',
+ 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'"],
- function readstr(a, b) { return input(a, b, -1); }),
- menu: wrapraw('menu',
+ 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: " +
"menu {A: (-> write 'chose A'), B: (-> write 'chose B')}"],
- menu),
+ doOutput, prepareMenu),
+ button: wrapglobalcommand('button',
+ ["button(text, fn) Writes a button. Calls " +
+ "fn whenever the button is clicked: " +
+ "button 'GO', -> fd 100"],
+ doOutput, prepareButton),
+ table: wrapglobalcommand('table',
+ ["table(m, n) Writes m rows and c columns. " +
+ "Access cells using cell: " +
+ "g = table 8, 8; g.cell(2,3).text 'hello'",
+ "table(array) Writes tabular data. " +
+ "Each nested array is a row: " +
+ "table [[1,2,3],[4,5,6]]"],
+ doOutput, prepareTable),
+ img: wrapglobalcommand('img',
+ ["img(url) Writes an image with the given address. " +
+ "Any URL can be provided. A name without slashes will be " +
+ "treated as '/img/name'." +
+ "t = img 'tree'"],
+ doOutput, prepareImage),
random: wrapraw('random',
["random(n) Random non-negative integer less than n: " +
"write random 10",
@@ -7003,23 +8108,6 @@ var dollar_turtle_methods = {
"random('color') Random color: " +
"pen random 'color'"],
random),
- hatch:
- function hatch(count, spec) {
- return $(document).hatch(count, spec);
- },
- button: wrapraw('button',
- ["button(text, fn) Writes a button. Calls " +
- "fn whenever the button is clicked: " +
- "button 'GO', -> fd 100"],
- button),
- table: wrapraw('table',
- ["table(m, n) Writes m rows and c columns. " +
- "Access cells using cell: " +
- "g = table 8, 8; g.cell(2,3).text 'hello'",
- "table(array) Writes tabular data. " +
- "Each nested array is a row: " +
- "table [[1,2,3],[4,5,6]]"],
- table),
rgb: wrapraw('rgb',
["rgb(r,g,b) Makes a color out of red, green, and blue parts. " +
"pen rgb(150,88,255)"],
@@ -7027,6 +8115,10 @@ var dollar_turtle_methods = {
Math.max(0, Math.min(255, Math.floor(r))),
Math.max(0, Math.min(255, Math.floor(g))),
Math.max(0, Math.min(255, Math.floor(b))) ]); }),
+ hatch: // Deprecated - no docs.
+ function hatch(count, spec) {
+ return $(document).hatch(count, spec);
+ },
rgba: wrapraw('rgba',
["rgba(r,g,b,a) Makes a color out of red, green, blue, and alpha. " +
"pen rgba(150,88,255,0.5)"],
@@ -7050,48 +8142,30 @@ var dollar_turtle_methods = {
(s * 100).toFixed(0) + '%',
(l * 100).toFixed(0) + '%',
a]); }),
- click: wrapraw('click',
+ click: wrapwindowevent('click',
["click(fn) Calls fn(event) whenever the mouse is clicked. " +
- "click (e) -> moveto e; label 'clicked'"],
- function(fn) {
- $(window).click(fn);
- }),
- mouseup: wrapraw('mouseup',
+ "click (e) -> moveto e; label 'clicked'"]),
+ dblclick: wrapwindowevent('dblclick',
+ ["dblclick(fn) Calls fn(event) whenever the mouse is double-clicked. " +
+ "dblclick (e) -> moveto e; label 'double'"]),
+ mouseup: wrapwindowevent('mouseup',
["mouseup(fn) Calls fn(event) whenever the mouse is released. " +
- "mouseup (e) -> moveto e; label 'up'"],
- function(fn) {
- $(window).mouseup(fn);
- }),
- mousedown: wrapraw('mousedown',
+ "mouseup (e) -> moveto e; label 'up'"]),
+ mousedown: wrapwindowevent('mousedown',
["mousedown(fn) Calls fn(event) whenever the mouse is pressed. " +
- "mousedown (e) -> moveto e; label 'down'"],
- function(fn) {
- $(window).mousedown(fn);
- }),
- mousemove: wrapraw('mousemove',
- ["mousedown(fn) Calls fn(event) whenever the mouse is moved. " +
- "mousemove (e) -> moveto e"],
- function(fn) {
- $(window).mousemove(fn);
- }),
- keydown: wrapraw('keydown',
+ "mousedown (e) -> moveto e; label 'down'"]),
+ mousemove: wrapwindowevent('mousemove',
+ ["mousemove(fn) Calls fn(event) whenever the mouse is moved. " +
+ "mousemove (e) -> write 'at ', e.x, ',', e.y"]),
+ keydown: wrapwindowevent('keydown',
["keydown(fn) Calls fn(event) whenever a key is pushed down. " +
- "keydown (e) -> write 'down ' + e.which"],
- function(fn) {
- $(window).keydown(fn);
- }),
- keyup: wrapraw('keyup',
+ "keydown (e) -> write 'down ' + e.key"]),
+ keyup: wrapwindowevent('keyup',
["keyup(fn) Calls fn(event) whenever a key is released. " +
- "keyup (e) -> write 'up ' + e.which"],
- function(fn) {
- $(window).keyup(fn);
- }),
- keypress: wrapraw('keypress',
- ["keypress(fn) Calls fn(event) whenever a letter is typed. " +
- "keypress (e) -> write 'press ' + e.which"],
- function(fn) {
- $(window).keypress(fn);
- }),
+ "keyup (e) -> write 'up ' + e.key"]),
+ keypress: wrapwindowevent('keypress',
+ ["keypress(fn) Calls fn(event) whenever a character key is pressed. " +
+ "keypress (e) -> write 'press ' + e.key"]),
send: wrapraw('send',
["send(name) Sends a message to be received by recv. " +
"send 'go'; recv 'go', -> fd 100"],
@@ -7116,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),
@@ -7183,6 +8345,12 @@ var dollar_turtle_methods = {
min: wrapraw('min',
["min(x, y, ...) The minimum of a set of values. " +
"see min 2, -5, 1"], Math.min),
+ Pencil: wrapraw('Pencil',
+ ["new Pencil(canvas) " +
+ "Make an invisble pencil for drawing on a canvas. " +
+ "s = new Sprite; p = new Pencil(s); " +
+ "p.pen red; p.fd 100; remove p"],
+ Pencil),
Turtle: wrapraw('Turtle',
["new Turtle(color) Make a new turtle. " +
"t = new Turtle; t.fd 100"], Turtle),
@@ -7202,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);
@@ -7249,6 +8417,8 @@ function pollSendRecv(name) {
deprecate(dollar_turtle_methods, 'defaultspeed', 'speed');
+dollar_turtle_methods.save.loginCookie = loginCookie;
+
var helpok = {};
var colors = [
@@ -7256,29 +8426,31 @@ var colors = [
"bisque", "black", "blanchedalmond", "blue", "blueviolet", "brown",
"burlywood", "cadetblue", "chartreuse", "chocolate", "coral",
"cornflowerblue", "cornsilk", "crimson", "cyan", "darkblue", "darkcyan",
- "darkgoldenrod", "darkgray", "darkgreen", "darkkhaki", "darkmagenta",
- "darkolivegreen", "darkorange", "darkorchid", "darkred", "darksalmon",
- "darkseagreen", "darkslateblue", "darkslategray", "darkturquoise",
- "darkviolet", "deeppink", "deepskyblue", "dimgray", "dodgerblue",
- "firebrick", "floralwhite", "forestgreen", "fuchsia", "gainsboro",
- "ghostwhite", "gold", "goldenrod", "gray", "green", "greenyellow",
- "honeydew", "hotpink", "indianred", "indigo", "ivory", "khaki",
- "lavender", "lavenderblush", "lawngreen", "lemonchiffon", "lightblue",
- "lightcoral", "lightcyan", "lightgoldenrodyellow", "lightgray",
- "lightgreen", "lightpink", "lightsalmon", "lightseagreen", "lightskyblue",
- "lightslategray", "lightsteelblue", "lightyellow", "lime", "limegreen",
- "linen", "magenta", "maroon", "mediumaquamarine", "mediumblue",
- "mediumorchid", "mediumpurple", "mediumseagreen", "mediumslateblue",
- "mediumspringgreen", "mediumturquoise", "mediumvioletred", "midnightblue",
- "mintcream", "mistyrose", "moccasin", "navajowhite", "navy", "oldlace",
- "olive", "olivedrab", "orange", "orangered", "orchid", "palegoldenrod",
- "palegreen", "paleturquoise", "palevioletred", "papayawhip", "peachpuff",
- "peru", "pink", "plum", "powderblue", "purple", "red", "rosybrown",
- "royalblue", "saddlebrown", "salmon", "sandybrown", "seagreen",
- "seashell", "sienna", "silver", "skyblue", "slateblue", "slategray",
- "snow", "springgreen", "steelblue", "tan", "teal", "thistle", "tomato",
+ "darkgoldenrod", "darkgray", "darkgrey", "darkgreen", "darkkhaki",
+ "darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darkred",
+ "darksalmon", "darkseagreen", "darkslateblue", "darkslategray",
+ "darkslategrey", "darkturquoise", "darkviolet", "deeppink", "deepskyblue",
+ "dimgray", "dimgrey", "dodgerblue", "firebrick", "floralwhite",
+ "forestgreen", "fuchsia", "gainsboro", "ghostwhite", "gold", "goldenrod",
+ "gray", "grey", "green", "greenyellow", "honeydew", "hotpink",
+ "indianred", "indigo", "ivory", "khaki", "lavender", "lavenderblush",
+ "lawngreen", "lemonchiffon", "lightblue", "lightcoral", "lightcyan",
+ "lightgoldenrodyellow", "lightgray", "lightgrey", "lightgreen",
+ "lightpink", "lightsalmon", "lightseagreen", "lightskyblue",
+ "lightslategray", "lightslategrey", "lightsteelblue", "lightyellow",
+ "lime", "limegreen", "linen", "magenta", "maroon", "mediumaquamarine",
+ "mediumblue", "mediumorchid", "mediumpurple", "mediumseagreen",
+ "mediumslateblue", "mediumspringgreen", "mediumturquoise",
+ "mediumvioletred", "midnightblue", "mintcream", "mistyrose", "moccasin",
+ "navajowhite", "navy", "oldlace", "olive", "olivedrab", "orange",
+ "orangered", "orchid", "palegoldenrod", "palegreen", "paleturquoise",
+ "palevioletred", "papayawhip", "peachpuff", "peru", "pink", "plum",
+ "powderblue", "purple", "rebeccapurple", "red", "rosybrown", "royalblue",
+ "saddlebrown", "salmon", "sandybrown", "seagreen", "seashell", "sienna",
+ "silver", "skyblue", "slateblue", "slategray", "slategrey", "snow",
+ "springgreen", "steelblue", "tan", "teal", "thistle", "tomato",
"turquoise", "violet", "wheat", "white", "whitesmoke", "yellow",
- "yellowgreen", "transparent",
+ "yellowgreen", "transparent"
];
(function() {
@@ -7295,6 +8467,7 @@ var colors = [
}
dollar_turtle_methods.PI = Math.PI;
dollar_turtle_methods.E = Math.E;
+ dollar_turtle_methods.print = dollar_turtle_methods.write
extrahelp.colors = {helptext:
["Defined colors: " + colors.join(" ")]};
extrahelp.see = {helptext:
@@ -7314,15 +8487,16 @@ 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;
+ }
// Clear any previous turtle methods.
clearGlobalTurtle();
// Expand any
+
+
+
+
+
+
diff --git a/test/autoscroll.html b/test/autoscroll.html
index 81ff91b..f9cf865 100644
--- a/test/autoscroll.html
+++ b/test/autoscroll.html
@@ -26,6 +26,7 @@
if (/phantom/i.test(navigator.userAgent)) {
// PhantomJS doesn't correctly deal with full-window scrolling.
// https://github.com/ariya/phantomjs/issues/10619
+ delete parent.window.dotest;
start();
} else {
var urect = fw.$('.last').get(0).getBoundingClientRect();
diff --git a/test/boxfill.html b/test/boxfill.html
index cc07f56..8289c36 100644
--- a/test/boxfill.html
+++ b/test/boxfill.html
@@ -6,7 +6,7 @@
+
+
+
+
+
+
+
+
+
diff --git a/test/continuations.html b/test/continuations.html
index 5f2d440..2dbf83d 100644
--- a/test/continuations.html
+++ b/test/continuations.html
@@ -28,7 +28,7 @@
ok(!touches(red));
turnto(0, function() {
ok(!touches(red));
- slide(0, -100, function() {
+ move(0, -100, function() {
ok(touches(red));
moveto(0, 0, function() {
ok(touches(red));
diff --git a/test/copy.html b/test/copy.html
new file mode 100644
index 0000000..d659aea
--- /dev/null
+++ b/test/copy.html
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
diff --git a/test/debugevents.html b/test/debugevents.html
index 64d3a57..b63142f 100644
--- a/test/debugevents.html
+++ b/test/debugevents.html
@@ -64,6 +64,7 @@
window.ide = {
bindframe: function() {
verify('bindframe');
+ return true;
},
reportEvent: function(eventname, args) {
verify([eventname, args[0]]);
diff --git a/test/dotfill.html b/test/dotfill.html
index 44e40d2..6f4a89b 100644
--- a/test/dotfill.html
+++ b/test/dotfill.html
@@ -52,7 +52,7 @@
ok(!touches(green));
ok(touches(red));
ok(touches(transparent));
- slide(50);
+ move(50);
ok(!touches('tan'));
ok(!touches(green));
ok(touches(red));
@@ -72,7 +72,7 @@
ok(touches(green));
ok(!touches(red));
ok(!touches(transparent));
- slide(50);
+ move(50);
ok(!touches('tan'));
ok(touches(green));
ok(!touches(red));
diff --git a/test/drawon.html b/test/drawon.html
index 8e8fbec..bf68802 100644
--- a/test/drawon.html
+++ b/test/drawon.html
@@ -10,7 +10,7 @@
asyncTest("Draws on another turtle.", function() {
speed(Infinity);
css({zIndex:1});
- var B = hatch('1000x1000/5');
+ var B = hatch({width: 1000, height: 1000, subpixel: 5});
drawon(B);
ok(!touches(blue));
ok(touches(transparent));
@@ -61,13 +61,15 @@
ok(!touches(transparent));
ok(!touches(red));
ok(touches(yellow));
- drawon(null);
+ drawon(window);
ok(!touches(blue));
ok(touches(transparent));
ok(!touches(red));
ok(!touches(yellow));
pen(red);
- fd(50);
+ fd(25);
+ drawon(null)
+ fd(25);
drawon(canvas());
fd(50);
ok(!touches(blue));
@@ -79,4 +81,18 @@
start();
});
});
+
+asyncTest("Drawon does not deadlock with sync.", function() {
+ speed(100);
+ var trt = new Sprite();
+ drawon(trt);
+ dot(red, 100);
+ drawon();
+ sync(turtle, trt);
+ trt.bk(100);
+ trt.done(function() {
+ ok(touches(red));
+ start();
+ });
+});
diff --git a/test/fern.html b/test/fern.html
index 37592f9..d3d505a 100644
--- a/test/fern.html
+++ b/test/fern.html
@@ -6,9 +6,9 @@
+
+
+
+
+
+
diff --git a/test/globals.html b/test/globals.html
new file mode 100644
index 0000000..a19cb14
--- /dev/null
+++ b/test/globals.html
@@ -0,0 +1,348 @@
+
+
+
+
+
+
+
+
diff --git a/test/hide.html b/test/hide.html
new file mode 100644
index 0000000..7af0f92
--- /dev/null
+++ b/test/hide.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
diff --git a/test/img.html b/test/img.html
new file mode 100644
index 0000000..3f3d734
--- /dev/null
+++ b/test/img.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
diff --git a/test/inputoutput.html b/test/inputoutput.html
new file mode 100644
index 0000000..b3b33b4
--- /dev/null
+++ b/test/inputoutput.html
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
diff --git a/test/inside.html b/test/inside.html
index ff1df20..513402d 100644
--- a/test/inside.html
+++ b/test/inside.html
@@ -12,7 +12,7 @@
ok(inside(window));
var p = write('hello');
ok(p.inside(window));
- p.slide(-25, 0);
+ p.move(-25, 0);
ok(!p.inside(window));
fd(2000);
ok(!inside(window));
diff --git a/test/instrument.html b/test/instrument.html
index d6a52a7..c0b9ff4 100644
--- a/test/instrument.html
+++ b/test/instrument.html
@@ -361,12 +361,12 @@
ins.on('noteoff', function(e) {
notelog.push('off' + e.midi + '-' + (currentTick() - startTick));
});
- ins.tone('C');
+ ins.tone('C', 10);
setTimeout(function() {
- ins.tone('G');
- ins.tone('E');
+ ins.tone('G', 10);
+ ins.tone('E', 10);
setTimeout(function() {
- ins.tone('C');
+ ins.tone('C', 10);
setTimeout(function() {
ins.tone('G', 0);
setTimeout(function() {
@@ -467,64 +467,64 @@
"on57-3845",
"on60-3845",
"on63-3845",
- "off54-5805",
- "off57-5805",
- "off60-5805",
- "off63-5805",
- "on55-5805",
- "on59-5805",
- "on62-5805",
- "off55-6743",
- "off59-6743",
- "off62-6743",
- "on44-7227",
- "off44-7680",
- "on56-7696",
- "on59-7696",
- "on62-7696",
- "on65-7696",
- "off56-10358",
- "off59-10358",
- "off62-10358",
- "off65-10358",
- "on56-10358",
- "on59-10358",
- "on62-10358",
- "on65-10358",
- "off56-10546",
- "off59-10546",
- "off62-10546",
- "off65-10546",
- "on55-10562",
- "on59-10562",
- "on62-10562",
- "on67-10562",
- "off55-11281",
- "off59-11281",
- "off62-11281",
- "off67-11281",
- "on53-11281",
- "on59-11281",
- "on62-11281",
- "on68-11281",
- "off53-11500",
- "off59-11500",
- "off62-11500",
- "off68-11500",
- "on51-11531",
- "on59-11531",
- "on62-11531",
- "on68-11531",
- "off59-13491",
- "off62-13491",
- "off68-13491",
- "on60-13491",
- "on63-13491",
- "on67-13491",
- "off51-14413",
- "off60-14413",
- "off63-14413",
- "off67-14413"
+ "off54-5804",
+ "off57-5804",
+ "off60-5804",
+ "off63-5804",
+ "on55-5804",
+ "on59-5804",
+ "on62-5804",
+ "off55-6742",
+ "off59-6742",
+ "off62-6742",
+ "on44-7226",
+ "off44-7679",
+ "on56-7695",
+ "on59-7695",
+ "on62-7695",
+ "on65-7695",
+ "off56-10357",
+ "off59-10357",
+ "off62-10357",
+ "off65-10357",
+ "on56-10357",
+ "on59-10357",
+ "on62-10357",
+ "on65-10357",
+ "off56-10545",
+ "off59-10545",
+ "off62-10545",
+ "off65-10545",
+ "on55-10576",
+ "on59-10576",
+ "on62-10576",
+ "on67-10576",
+ "off55-11279",
+ "off59-11279",
+ "off62-11279",
+ "off67-11279",
+ "on53-11295",
+ "on59-11295",
+ "on62-11295",
+ "on68-11295",
+ "off53-11514",
+ "off59-11514",
+ "off62-11514",
+ "off68-11514",
+ "on51-11530",
+ "on59-11530",
+ "on62-11530",
+ "on68-11530",
+ "off59-13474",
+ "off62-13474",
+ "off68-13474",
+ "on60-13474",
+ "on63-13474",
+ "on67-13474",
+ "off51-14412",
+ "off60-14412",
+ "off63-14412",
+ "off67-14412"
]);
ins.remove();
start();
diff --git a/test/interrupt.html b/test/interrupt.html
index 3ddf2b0..19f018c 100644
--- a/test/interrupt.html
+++ b/test/interrupt.html
@@ -5,7 +5,7 @@
diff --git a/test/label.html b/test/label.html
index 66ec7ae..927b318 100644
--- a/test/label.html
+++ b/test/label.html
@@ -36,25 +36,36 @@
label('big');
bk(100)
rt(90);
- label('mid', { fontSize: 50 });
+ label('mid', { fontSize: 50, class: "mid" });
fd(150)
label('giant', 150);
- label('upside down', { turtleRotation: 180, fontSize: 8 });
+ label('upside down', { turtleRotation: 180, fontSize: 8, id: 'upside' });
+ label('bottomleft', 'bottom-left');
done(function() {
ok($('label:contains(small)').width() < $('label:contains(mid)').width());
ok($('label:contains(mid)').width() < $('label:contains(big)').width());
ok($('label:contains(big)').width() < $('label:contains(giant)').width());
- ok($('label:contains(upside)').width() < $('label:contains(mid)').width());
+ ok($('label:contains(upside)').width() <$('label:contains(mid)').width());
+ equal($('.mid').length, 1);
+ equal($('.mid').text(), 'mid');
+ equal($('#upside').text(), 'upside down');
deepEqual(rounded($('label:contains(small)').getxy()), [0, 0]);
deepEqual(rounded($('label:contains(big)').getxy()), [0, 50]);
deepEqual(rounded($('label:contains(mid)').getxy()), [0, -50]);
deepEqual(rounded($('label:contains(giant)').getxy()), [150, -50]);
+ ok($('label:contains(bottomleft)').getxy()[0] < -50);
+ ok($('label:contains(bottomleft)').getxy()[1] < -100);
equal(rounded($('label:contains(small)').direction()), 0);
equal(rounded($('label:contains(big)').direction()), 0);
equal(rounded($('label:contains(mid)').direction()), 90);
equal(rounded($('label:contains(giant)').direction()), 90);
+ equal(rounded($('label:contains(bottomleft)').direction()), 0);
equal(direction(), 90);
equal(rounded($('label:contains(upside down)').direction()), 180);
+ var b;
+ label(b = button('hello'));
+ ok(touches(b));
+ equal(b.direction(), 90);
start();
});
});
diff --git a/test/newturtle.html b/test/newturtle.html
index 0832a38..832212d 100644
--- a/test/newturtle.html
+++ b/test/newturtle.html
@@ -50,8 +50,19 @@
}
done(function() {
- $('#radius').remove();
equal(expected, 4);
+ $('#radius').remove();
+ speed(Infinity);
+ ok(touches(targets[0]));
+ fd(100);
+ ok(touches(targets[1]));
+ remove(targets);
+ ok(!touches(targets[1]));
+ rt(90)
+ fd(100);
+ ok(!touches(targets[2]));
+ moveto({pageX: 0, pageY: 0});
+ ok(!touches(targets[2]));
start();
});
});
diff --git a/test/numeric_functions.html b/test/numeric_functions.html
new file mode 100644
index 0000000..49ee2df
--- /dev/null
+++ b/test/numeric_functions.html
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
diff --git a/test/field.html b/test/origin.html
similarity index 85%
rename from test/field.html
rename to test/origin.html
index 1674406..0f0534e 100644
--- a/test/field.html
+++ b/test/origin.html
@@ -7,7 +7,7 @@
-
+
diff --git a/test/piano.html b/test/piano.html
new file mode 100644
index 0000000..c85681b
--- /dev/null
+++ b/test/piano.html
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/test/pressed.html b/test/pressed.html
index e523b54..0829718 100644
--- a/test/pressed.html
+++ b/test/pressed.html
@@ -7,15 +7,50 @@
+
+
+
+
+
+
+
diff --git a/test/remove.html b/test/remove.html
index 6050a37..98e7166 100644
--- a/test/remove.html
+++ b/test/remove.html
@@ -33,6 +33,10 @@
ok(completed);
equal(t.parent().length, 0);
equal(typeof(window.turtle), 'undefined');
+ // Restore globals to an initial state.
+ $.turtle({ids: false});
+ window.turtle = $('#turtle');
+ equal(typeof(window.turtle), 'object');
start();
});
});
diff --git a/test/revolve.html b/test/revolve.html
new file mode 100644
index 0000000..49f6bbc
--- /dev/null
+++ b/test/revolve.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
diff --git a/test/say.html b/test/say.html
new file mode 100644
index 0000000..a8aed1c
--- /dev/null
+++ b/test/say.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/test/stop.html b/test/stop.html
new file mode 100644
index 0000000..3e77d67
--- /dev/null
+++ b/test/stop.html
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
diff --git a/test/sync.html b/test/sync.html
index 87e4d1a..4808c51 100644
--- a/test/sync.html
+++ b/test/sync.html
@@ -46,4 +46,22 @@
start();
});
});
+
+asyncTest("Moves a written object and verifies that sync works.", function() {
+ speed(10);
+ var w = write('hello');
+ w.bk(100);
+ sync(w, turtle);
+ w.plan(function() {
+ // When w is done moving, w2 should not have moved yet.
+ var xy2 = w2.getxy();
+ var xy = w.getxy();
+ ok(xy2[1] > xy[1]);
+ });
+ var w2 = write('hello2');
+ w2.bk(100);
+ done(function() {
+ start();
+ });
+});
diff --git a/test/touches.html b/test/touches.html
new file mode 100644
index 0000000..659887d
--- /dev/null
+++ b/test/touches.html
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
diff --git a/test/twist.html b/test/twist.html
index f62ff23..b3e3f21 100644
--- a/test/twist.html
+++ b/test/twist.html
@@ -9,16 +9,18 @@
module("Twist test.");
asyncTest("Test of twisting a skinny shape.", function() {
speed(Infinity);
- slide(100);
+ move(100);
dot(yellow);
- slide(-200);
+ move(-200);
dot(pink);
- slide(100);
+ move(100);
fd(100);
dot(red);
bk(200)
dot(blue);
- var s = new Sprite({ width: 10, height: 200, color: green });
+ var s = new Sprite({ width: 10, height: 200, color: green, id: "s" });
+ equal($('#s').length, 1);
+ equal(s.filter('#s').length, 1);
ok(s.touches(red));
ok(s.touches(blue));
ok(!s.touches(pink));
diff --git a/test/wear.html b/test/wear.html
index 37a581c..12a9d3f 100644
--- a/test/wear.html
+++ b/test/wear.html
@@ -3,10 +3,41 @@
-
+
diff --git a/test/write.html b/test/write.html
new file mode 100644
index 0000000..248e0f5
--- /dev/null
+++ b/test/write.html
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
diff --git a/turtle.jquery.json b/turtle.jquery.json
index be5c732..cd8b8a3 100644
--- a/turtle.jquery.json
+++ b/turtle.jquery.json
@@ -14,7 +14,7 @@
"audio",
"collision"
],
- "version": "2.0.8",
+ "version": "2.0.9",
"author": {
"name": "David Bau",
"url": "http://davidbau.com/"