diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a349cbb
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+.svn
+node_modules
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 d05b587..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: {
@@ -66,6 +78,14 @@ module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-release');
+ grunt.task.registerTask('test', 'Run unit tests, or just one test.',
+ function(testname) {
+ if (!!testname) {
+ grunt.config('qunit.all', ['test/' + testname + '.html']);
+ }
+ grunt.task.run('qunit:all');
+ });
+
grunt.registerTask("testserver", ["watch:testserver"]);
grunt.registerTask("default", ["uglify", "qunit"]);
};
diff --git a/LICENSE.txt b/LICENSE.txt
index 29597fe..f997494 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -2,7 +2,7 @@ jQuery-turtle version 2.0
LICENSE (MIT):
-Copyright (c) 2013 David Bau
+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 a9c2369..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) 2013 David Bau
+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 43356a4..4eca85a 100644
--- a/bower.json
+++ b/bower.json
@@ -1,6 +1,8 @@
{
"name": "jquery-turtle",
- "version": "2.0.8",
+ "main": "jquery-turtle.js",
+ "ignore": [],
+ "version": "2.0.9",
"description": "Turtle graphics plugin for jQuery.",
"devDependencies": {
"jquery": "latest",
diff --git a/jquery-turtle.js b/jquery-turtle.js
index fec405d..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.
@@ -102,7 +103,6 @@ $(q).nearest(pos) // Filters to item (or items if tied) nearest pos.
$(q).within(d, t) // Filters to items with centers within d of t.pagexy().
$(q).notwithin() // The negation of within.
$(q).cell(y, x) // Selects the yth row and xth column cell in a table.
-$(q).hatch([n,] [img]) // Creates and returns n turtles with the given img.
@@ -124,9 +124,9 @@ Pen and Fill Styles
-------------------
The turtle pen respects canvas styling: any valid strokeStyle is
-accepted; and also using a space-separated syntax, lineWidth, lineCap,
+accepted; and also using a css-like syntax, lineWidth, lineCap,
lineJoin, miterLimit, and fillStyle can be specified, e.g.,
-pen('red lineWidth 5 lineCap square'). The same syntax applies for
+pen('red;lineWidth:5;lineCap:square'). The same syntax applies for
styling dot and fill (except that the default interpretation for the
first value is fillStyle instead of strokeStyle).
@@ -144,11 +144,11 @@ apostrophes, carets, underscores, digits, and slashes as in the
standard. Enclosing letters in square brackets represents a chord,
and z represents a rest. The default tempo is 120, but can be changed
by passing a options object as the first parameter setting tempo, e.g.,
-{ tempo: 200 }. Other options include volume: 0.5, type: 'sine' or
-'square' or 'sawtooth' or 'triangle', and envelope: which defines
-an ADSR envelope e.g., { a: 0.01, d: 0.2, s: 0.1, r: 0.1 }.
+{ tempo: 200 }.
-The turtle's motion will pause while it is playing notes.
+The turtle's motion will pause while it is playing notes. A single
+tone can be played immediately (without participating in the
+turtle animation queue) by using the "tone" method.
Planning Logic in the Animation Queue
-------------------------------------
@@ -217,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.
@@ -232,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).
@@ -241,10 +243,12 @@ random('gray') // Returns a random hsl(0, 0, *) gray.
remove() // Removes default turtle and its globals (fd, etc).
see(a, b, c...) // Logs tree-expandable data into debugging panel.
write(html) // Appends html into the document body.
+type(plaintext) // Appends preformatted text into a pre in the document.
read([label,] fn) // Makes a one-time input field, calls fn after entry.
readnum([label,] fn) // Like read, but restricted to numeric input.
readstr([label,] fn) // Like read, but never converts input to a number.
button([label,] fn) // Makes a clickable button, calls fn when clicked.
+menu(choices, fn) // Makes a clickable choice, calls fn when chosen.
table(m, n) // Outputs a table with m rows and n columns.
play('[DFG][EGc]') // Plays musical notes.
send(m, arg) // Sends an async message to be received by recv(m, fn).
@@ -257,11 +261,11 @@ 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
-b = hatch blue
+r = new Turtle red
+b = new Turtle blue
tick 10, ->
turnto lastmousemove
fd 6
@@ -305,6 +309,8 @@ $(q).css('turtleTurningRadius, '50px');// arc turning radius for rotation.
$(q).css('turtlePenStyle', 'red'); // or 'red lineWidth 2px' etc.
$(q).css('turtlePenDown', 'up'); // default 'down' to draw with pen.
$(q).css('turtleHull', '5 0 0 5 0 -5');// fine-tune shape for collisions.
+$(q).css('turtleTimbre', 'square'); // quality of the sound.
+$(q).css('turtleVolume', '0.3'); // volume of the sound.
Arbitrary 2d transforms are supported, including transforms of elements
@@ -362,10 +368,28 @@ THE SOFTWARE.
//////////////////////////////////////////////////////////////////////////
var undefined = void 0,
+ global = this,
__hasProp = {}.hasOwnProperty,
rootjQuery = jQuery(function() {}),
- Pencil, Turtle,
- global_plan_counter = 0;
+ interrupted = false,
+ async_pending = 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) {
@@ -417,6 +441,112 @@ if (!transform || !hasGetBoundingClientRect()) {
return;
}
+// An options string looks like a (simplified) CSS properties string,
+// of the form prop:value;prop:value; etc. If defaultProp is supplied
+// then the string can begin with "value" (i.e., value1;prop:value2)
+// 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 (typeof(str) != 'string') {
+ if (str == null) {
+ return {};
+ }
+ if ($.isPlainObject(str)) {
+ return 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),
+ result = {}, j, t, key = null, value, arg,
+ seencolon = false, vlist = [], firstval = true;
+
+ // While parsing, commitvalue() validates and unquotes a prop:value
+ // pair and commits it to the result.
+ function commitvalue() {
+ // Trim whitespace
+ while (vlist.length && /^\s/.test(vlist[vlist.length - 1])) { vlist.pop(); }
+ while (vlist.length && /^\s/.test(vlist[0])) { vlist.shift(); }
+ if (vlist.length == 1 && (
+ /^".*"$/.test(vlist[0]) || /^'.*'$/.test(vlist[0]))) {
+ // Unquote quoted string.
+ value = vlist[0].substr(1, vlist[0].length - 2);
+ } else if (vlist.length == 2 && vlist[0] == 'url' &&
+ /^(.*)$/.test(vlist[1])) {
+ // Remove url(....) from around a string.
+ value = vlist[1].substr(1, vlist[1].length - 2);
+ } else {
+ // Form the string for the value.
+ arg = vlist.join('');
+ // Convert the value to a number if it looks like a number.
+ if (arg == "") {
+ value = arg;
+ } else if (isNaN(arg)) {
+ value = arg;
+ } else {
+ value = Number(arg);
+ }
+ }
+ // Deal with a keyless first value.
+ if (!seencolon && firstval && defaultProp && vlist.length) {
+ // value will already have been formed.
+ key = defaultProp;
+ }
+ if (key) {
+ result[key] = value;
+ }
+ }
+ // Now the parsing: just iterate through all the tokens.
+ for (j = 0; j < token.length; ++j) {
+ t = token[j];
+ if (!seencolon) {
+ // Before a colon, remember the first identifier as the key.
+ if (!key && /^[a-zA-Z_-]/.test(t)) {
+ key = t;
+ }
+ // And also look for the colon.
+ if (t == ':') {
+ seencolon = true;
+ vlist.length = 0;
+ continue;
+ }
+ }
+ if (t == ';') {
+ // When a semicolon is seen, form the value and save it.
+ commitvalue();
+ // Then reset the parsing state.
+ key = null;
+ vlist.length = 0;
+ seencolon = false;
+ firstval = false;
+ continue;
+ }
+ // Accumulate all tokens into the vlist.
+ vlist.push(t);
+ }
+ commitvalue();
+ return result;
+}
+// Prints a map of options as a parsable string.
+// The inverse of parseOptionString.
+function printOptionAsString(obj) {
+ var result = [];
+ function quoted(s) {
+ if (/[\s;]/.test(s)) {
+ if (s.indexOf('"') < 0) {
+ return '"' + s + '"';
+ }
+ return "'" + s + "'";
+ }
+ return s;
+ }
+ for (var k in obj) if (obj.hasOwnProperty(k)) {
+ result.push(k + ':' + quoted(obj[k]) + ';');
+ }
+ return result.join(' ');
+}
+
//////////////////////////////////////////////////////////////////////////
// MATH
// 2d matrix support functions.
@@ -474,6 +604,31 @@ function inverse2x2(a) {
rotation(-(d[0]))));
}
+// By convention, a 2x3 transformation matrix has a 2x2 transform
+// in the first four slots and a 1x2 translation in the last two slots.
+// The array [a, b, c, d, e, f] is shorthand for the following 3x3
+// matrix where the upper-left 2x2 is an in-place transform, and the
+// upper-right vector [e, f] is the translation.
+//
+// [a c e] The inverse of this 3x3 matrix can be formed by
+// [b d f] figuring the inverse of the 2x2 upper-left corner
+// [0 0 1] (call that Ai), and then negating the upper-right vector
+// (call that -t) and then forming Ai * (-t).
+//
+// [ A t] (if Ai is the inverse of A) [ Ai Ai*(-t) ]
+// [0 0 1] --- invert ---> [ 0 0 1 ]
+//
+// The result is of the same form, and can be represented as an array
+// of six numbers.
+function inverse2x3(a) {
+ var ai = inverse2x2(a);
+ if (a.length == 4) return ai;
+ var nait = matrixVectorProduct(ai, [-a[4], -a[5]]);
+ ai.push(nait[0]);
+ ai.push(nait[1]);
+ return ai;
+}
+
function rotation(theta) {
var c = Math.cos(theta),
s = Math.sin(theta);
@@ -598,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;
@@ -616,8 +771,25 @@ function readTransformMatrix(elem) {
// Reads out the css transformOrigin property, if present.
function readTransformOrigin(elem, wh) {
- var gcs = (window.getComputedStyle ? window.getComputedStyle(elem) : null),
- origin = (gcs && gcs[transformOrigin] || $.css(elem, 'transformOrigin'));
+ var hidden = ($.css(elem, 'display') === 'none'),
+ swapout, old, name;
+ if (hidden) {
+ // IE GetComputedStyle doesn't give pixel values for transformOrigin
+ // unless the element is unhidden.
+ swapout = { position: "absolute", visibility: "hidden", display: "block" };
+ old = {};
+ for (name in swapout) {
+ old[name] = elem.style[name];
+ elem.style[name] = swapout[name];
+ }
+ }
+ 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);
}
@@ -775,46 +947,48 @@ function cleanedStyle(trans) {
return result;
}
-function getTurtleOrigin(elem, inverseParent, corners) {
+// Returns the turtle's origin (the absolute location of its pen and
+// 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
+ && elem.classList && elem.classList.contains('turtle')) {
+ return state.quickhomeorigin;
+ }
var hidden = ($.css(elem, 'display') === 'none'),
swapout = hidden ?
{ position: "absolute", visibility: "hidden", display: "block" } : {},
substTransform = swapout[transform] = (inverseParent ? 'matrix(' +
$.map(inverseParent, cssNum).join(', ') + ', 0, 0)' : 'none'),
- old = {}, name, gbcr;
+ old = {}, name, gbcr, transformOrigin;
for (name in swapout) {
old[name] = elem.style[name];
elem.style[name] = swapout[name];
}
gbcr = getPageGbcr(elem);
+ transformOrigin = readTransformOrigin(elem, [gbcr.width, gbcr.height]);
for (name in swapout) {
elem.style[name] = cleanedStyle(old[name]);
}
- if (corners) {
- corners.gbcr = gbcr;
+ if (extra) {
+ extra.gbcr = gbcr;
+ extra.localorigin = transformOrigin;
}
- return addVector(
- [gbcr.left, gbcr.top],
- readTransformOrigin(elem, [gbcr.width, gbcr.height]));
-}
-
-function unattached(elt) {
- // Unattached if not part of a document.
- while (elt) {
- if (elt.nodeType === 9) return false;
- elt = elt.parentNode;
+ var result = addVector([gbcr.left, gbcr.top], transformOrigin);
+ if (state && state.down && state.style) {
+ state.quickhomeorigin = result;
}
- return true;
+ return result;
}
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() {
@@ -825,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,
@@ -837,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)) {
@@ -880,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
};
@@ -924,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;
}
@@ -953,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];
}
@@ -962,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] });
}
@@ -981,12 +1147,12 @@ 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());
}
var state = getTurtleData(elem);
- if (state && state.quickpagexy && state.down) {
+ if (state && state.quickpagexy && state.down && state.style) {
return state.quickpagexy;
}
var tr = getElementTranslation(elem),
@@ -996,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) {
+ 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;
@@ -1016,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());
}
@@ -1025,7 +1207,7 @@ function getCornersInPageCoordinates(elem, untransformed) {
totalTransform = matrixProduct(totalParentTransform, currentTransform),
inverseParent = inverse2x2(totalParentTransform),
out = {},
- origin = getTurtleOrigin(elem, inverseParent),
+ origin = getTurtleOrigin(elem, inverseParent, out),
gbcr = out.gbcr,
hull = polyToVectorsOffset(getTurtleData(elem).hull, origin) || [
[gbcr.left, gbcr.top],
@@ -1050,7 +1232,7 @@ function getDirectionOnPage(elem) {
r = convertToRadians(normalizeRotation(ts.rot)),
ux = Math.sin(r), uy = Math.cos(r),
totalParentTransform = totalTransform2x2(elem.parentElement),
- up = matrixVectorProduct(totalParentTransform, [ux, uy]);
+ up = matrixVectorProduct(totalParentTransform, [ux, uy]),
dp = Math.atan2(up[0], up[1]);
return radiansToDegrees(dp);
}
@@ -1063,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);
}
@@ -1206,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] });
@@ -1226,6 +1409,7 @@ function writeTurtleHull(hull) {
}
function makeHullHook() {
+ // jQuery CSS hook for turtleHull property.
return {
get: function(elem, computed, extra) {
var hull = getTurtleData(elem).hull;
@@ -1289,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;
@@ -1320,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; }
@@ -1327,7 +1513,7 @@ function radiansToDegrees(r) {
}
function convertToRadians(d) {
- return d * Math.PI / 180;
+ return d / 180 * Math.PI;
}
function normalizeRotation(x) {
@@ -1353,28 +1539,30 @@ function normalizeRotationDelta(x) {
//////////////////////////////////////////////////////////////////////////
// drawing state.
-var drawing = {
+var globalDrawing = {
attached: false,
surface: null,
field: null,
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
};
function getTurtleField() {
- if (!drawing.field) {
+ if (!globalDrawing.field) {
createSurfaceAndField();
}
- return drawing.field;
+ return globalDrawing.field;
}
function getTurtleClipSurface() {
- if (!drawing.surface) {
+ if (!globalDrawing.surface) {
createSurfaceAndField();
}
- return drawing.surface;
+ return globalDrawing.surface;
}
@@ -1388,88 +1576,110 @@ function createSurfaceAndField() {
position: 'absolute',
display: 'inline-block',
top: 0, left: 0, width: '100%', height: '100%',
- zIndex: -1,
font: 'inherit',
+ // z-index: -1 is required to keep the turtle
+ // surface behind document text and buttons, so
+ // the canvas does not block interaction
+ zIndex: -1,
// Setting transform origin for the turtle field
// 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);
- drawing.surface = surface;
- drawing.field = field;
+ 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) {
- $(drawing.surface).prependTo('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 = $(drawing.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);
}
}
-function getTurtleDrawingCtx() {
- if (drawing.ctx) {
- return drawing.ctx;
+// Given a $.data(elem, 'turtleData') state object, return or create
+// the drawing canvas that this turtle should be drawing on.
+function getDrawOnCanvas(state) {
+ if (!state.drawOnCanvas) {
+ state.drawOnCanvas = getTurtleDrawingCanvas();
+ }
+ return state.drawOnCanvas;
+}
+
+// Similar to getDrawOnCanvas, but for the read-only case: it avoids
+// creating turtleData if none exists, and it avoid creating the global
+// canvas if one doesn't already exist. If there is no global canvas,
+// this returns null.
+function getCanvasForReading(elem) {
+ var state = $.data(elem, 'turtleData');
+ if (!state) return null;
+ if (state.drawOnCanvas) return state.drawOnCanvas;
+ return globalDrawing.canvas;
+}
+
+function getTurtleDrawingCanvas() {
+ if (globalDrawing.canvas) {
+ return globalDrawing.canvas;
}
var surface = getTurtleClipSurface();
- drawing.canvas = document.createElement('canvas');
- $(drawing.canvas).css({'z-index': -1});
- surface.insertBefore(drawing.canvas, surface.firstChild);
- drawing.ctx = drawing.canvas.getContext('2d');
+ globalDrawing.canvas = document.createElement('canvas');
+ $(globalDrawing.canvas).css({'z-index': -1});
+ surface.insertBefore(globalDrawing.canvas, surface.firstChild);
resizecanvas();
pollbodysize(resizecanvas);
- $(window).resize(resizecanvas);
- drawing.ctx.scale(drawing.subpixel, drawing.subpixel);
- return drawing.ctx;
+ $(global).resize(resizecanvas);
+ return globalDrawing.canvas;
}
function getOffscreenCanvas(width, height) {
- if (drawing.offscreen &&
- drawing.offscreen.width === width &&
- drawing.offscreen.height === height) {
- return drawing.offscreen;
- }
- if (!drawing.offscreen) {
- drawing.offscreen = document.createElement('canvas');
- }
- drawing.offscreen.width = width;
- drawing.offscreen.height = height;
- return drawing.offscreen;
+ if (globalDrawing.offscreen &&
+ globalDrawing.offscreen.width === width &&
+ globalDrawing.offscreen.height === height) {
+ // Return a clean canvas.
+ globalDrawing.offscreen.getContext('2d').clearRect(0, 0, width, height);
+ return globalDrawing.offscreen;
+ }
+ if (!globalDrawing.offscreen) {
+ globalDrawing.offscreen = document.createElement('canvas');
+ /* for debugging "touches": make offscreen canvas visisble.
+ $(globalDrawing.offscreen)
+ .css({position:'absolute',top:0,left:0,zIndex:1})
+ .appendTo('body');
+ */
+ }
+ globalDrawing.offscreen.width = width;
+ globalDrawing.offscreen.height = height;
+ return globalDrawing.offscreen;
}
function pollbodysize(callback) {
@@ -1483,40 +1693,65 @@ function pollbodysize(callback) {
lastheight = b.height();
}
});
- if (drawing.timer) {
- clearInterval(drawing.timer);
+ if (globalDrawing.timer) {
+ clearInterval(globalDrawing.timer);
}
- drawing.timer = setInterval(poller, 250);
+ globalDrawing.timer = setInterval(poller, 250);
+}
+
+function sizexy() {
+ // Notice that before the body exists, we cannot get its size; so
+ // we fall back to the window size.
+ // Using innerHeight || $(window).height() deals with quirks-mode.
+ var b = $('body');
+ return [
+ Math.max(b.outerWidth(true), global.innerWidth || $(global).width()),
+ Math.max(b.outerHeight(true), global.innerHeight || $(global).height())
+ ];
}
function resizecanvas() {
- if (!drawing.canvas) return;
- var b = $('body'),
- wh = Math.max(b.outerHeight(true),
- window.innerHeight || $(window).height()),
- bw = Math.max(200, Math.ceil(b.outerWidth(true) / 100) * 100),
- bh = Math.max(200, Math.ceil(wh / 100) * 100),
- cw = drawing.canvas.width,
- ch = drawing.canvas.height,
+ if (!globalDrawing.canvas) return;
+ var sxy = sizexy(),
+ ww = sxy[0],
+ wh = sxy[1],
+ cw = globalDrawing.canvas.width,
+ ch = globalDrawing.canvas.height,
+ // Logic: minimum size 200; only shrink if larger than 2000;
+ // and only resize if changed more than 100 pixels.
+ bw = Math.max(Math.min(2000, Math.max(200, cw)),
+ Math.ceil(ww / 100) * 100) * globalDrawing.subpixel,
+ bh = Math.max(Math.min(2000, Math.max(200, cw)),
+ Math.ceil(wh / 100) * 100) * globalDrawing.subpixel,
tc;
- $(drawing.surface).css({ width: b.outerWidth(true) + 'px',
- height: wh + 'px'});
- if (cw != bw * drawing.subpixel || ch != bh * drawing.subpixel) {
+ $(globalDrawing.surface).css({
+ width: ww + 'px',
+ height: wh + 'px'
+ });
+ if (cw != bw || ch != bh) {
// Transfer canvas out to tc and back again after resize.
tc = document.createElement('canvas');
- tc.width = Math.min(cw, bw * drawing.subpixel);
- tc.height = Math.min(ch, bh * drawing.subpixel);
- tc.getContext('2d').drawImage(drawing.canvas, 0, 0);
- drawing.canvas.width = bw * drawing.subpixel;
- drawing.canvas.height = bh * drawing.subpixel;
- drawing.canvas.getContext('2d').drawImage(tc, 0, 0);
- $(drawing.canvas).css({ width: bw, height: bh });
+ tc.width = Math.min(cw, bw);
+ tc.height = Math.min(ch, bh);
+ tc.getContext('2d').drawImage(globalDrawing.canvas, 0, 0);
+ globalDrawing.canvas.width = bw;
+ globalDrawing.canvas.height = bh;
+ globalDrawing.canvas.getContext('2d').drawImage(tc, 0, 0);
+ $(globalDrawing.canvas).css({
+ width: bw / globalDrawing.subpixel,
+ height: bh / globalDrawing.subpixel
+ });
}
}
// turtlePenStyle style syntax
function parsePenStyle(text, defaultProp) {
if (!text) { return null; }
+ if (text && (typeof(text) == "function") && (
+ text.helpname || text.name)) {
+ // Deal with "tan" and "fill".
+ text = (text.helpname || text.name);
+ }
text = String(text);
if (text.trim) { text = text.trim(); }
if (!text || text === 'none') { return null; }
@@ -1526,43 +1761,17 @@ function parsePenStyle(text, defaultProp) {
var eraseMode = false;
if (/^erase\b/.test(text)) {
text = text.replace(
- /^erase\b/, 'white globalCompositeOperation destination-out');
+ /^erase\b/, 'white; globalCompositeOperation:destination-out');
eraseMode = true;
}
- var words = text.split(/\s+/),
- mapping = {
- strokeStyle: identity,
- lineWidth: parseFloat,
- lineCap: identity,
- lineJoin: identity,
- miterLimit: parseFloat,
- fillStyle: identity,
- globalCompositeOperation: identity
- },
- result = {}, j, end = words.length;
+ var result = parseOptionString(text, defaultProp);
if (eraseMode) { result.eraseMode = true; }
- for (j = words.length - 1; j >= 0; --j) {
- if (mapping.hasOwnProperty(words[j])) {
- var key = words[j],
- param = words.slice(j + 1, end).join(' ');
- result[key] = mapping[key](param);
- end = j;
- }
- }
- if (end > 0 && !result[defaultProp]) {
- result[defaultProp] = words.slice(0, end).join(' ');
- }
return result;
}
function writePenStyle(style) {
if (!style) { return 'none'; }
- var result = [];
- $.each(style, function(k, v) {
- result.push(k);
- result.push(v);
- });
- return result.join(' ');
+ return printOptionAsString(style);
}
function parsePenDown(style) {
@@ -1580,12 +1789,18 @@ function getTurtleData(elem) {
if (!state) {
state = $.data(elem, 'turtleData', {
style: null,
+ corners: [[]],
path: [[]],
- down: true,
+ down: false,
speed: 'turtle',
easing: 'swing',
turningRadius: 0,
- quickpagexy: null
+ drawOnCanvas: null,
+ quickpagexy: null,
+ quickhomeorigin: null,
+ oldscale: 1,
+ instrument: null,
+ stream: null
});
}
return state;
@@ -1629,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);
}
};
}
@@ -1649,8 +1870,9 @@ function makePenDownHook() {
if (style != state.down) {
state.down = style;
state.quickpagexy = null;
+ state.quickhomeorigin = null;
elem.style.turtlePenDown = writePenDown(style);
- flushPenState(elem);
+ flushPenState(elem, state, true);
}
}
};
@@ -1661,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 &&
@@ -1681,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];
}
@@ -1695,22 +1927,78 @@ function applyPenStyle(ctx, ps, scale) {
}
}
-function drawAndClearPath(path, style, scale) {
- var ctx = getTurtleDrawingCtx(),
+// Computes a matrix that transforms page coordinates to the local
+// canvas coordinates. Applying this matrix as the canvas transform
+// allows us to draw on a canvas using page coordinates; and the bits
+// will show up on the canvas in the corresponding location on the
+// physical page, even if the canvas has been moved by absolute
+// position and CSS 2d transforms.
+function computeCanvasPageTransform(canvas) {
+ if (!canvas) { return; }
+ if (canvas === globalDrawing.canvas) {
+ return [globalDrawing.subpixel, 0, 0, globalDrawing.subpixel];
+ }
+ var totalParentTransform = totalTransform2x2(canvas.parentElement),
+ inverseParent = inverse2x2(totalParentTransform),
+ out = {},
+ origin = getTurtleOrigin(canvas, inverseParent, out),
+ gbcr = out.gbcr,
+ originTranslate = [1, 0, 0, 1, -origin[0], -origin[1]],
+ finalScale = gbcr.width && gbcr.height &&
+ [canvas.width / gbcr.width, 0, 0, canvas.height / gbcr.height],
+ localTransform = readTransformMatrix(canvas) || [1, 0, 0, 1],
+ inverseTransform = inverse2x3(localTransform),
+ totalInverse;
+ if (!inverseParent || !inverseTransform || !finalScale) {
+ return;
+ }
+ totalInverse =
+ matrixProduct(
+ matrixProduct(
+ matrixProduct(
+ finalScale,
+ inverseTransform),
+ inverseParent),
+ originTranslate);
+ totalInverse[4] += out.localorigin[0] * finalScale[0];
+ totalInverse[5] += out.localorigin[1] * finalScale[3];
+ return totalInverse;
+}
+
+function setCanvasPageTransform(ctx, canvas) {
+ if (canvas === globalDrawing.canvas) {
+ ctx.setTransform(
+ globalDrawing.subpixel, 0, 0, globalDrawing.subpixel, 0, 0);
+ } else {
+ var pageToCanvas = computeCanvasPageTransform(canvas);
+ if (pageToCanvas) {
+ ctx.setTransform.apply(ctx, pageToCanvas);
+ }
+ }
+}
+
+var buttOverlap = 0.67;
+
+function drawAndClearPath(drawOnCanvas, path, style, scale, truncateTo) {
+ var ctx = drawOnCanvas.getContext('2d'),
isClosed, skipLast,
j = path.length,
segment;
ctx.save();
+ setCanvasPageTransform(ctx, drawOnCanvas);
ctx.beginPath();
// Scale up lineWidth by sx. (TODO: consider parent transforms.)
applyPenStyle(ctx, 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])) {
@@ -1729,7 +2017,7 @@ function drawAndClearPath(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) {
@@ -1744,102 +2032,105 @@ 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.shift([]);
+ 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(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(state.path, style);
- if (state.style && state.style.savePath) {
- $.style(elem, 'turtlePenStyle', 'none');
- }
-}
-
-function fillDot(position, diameter, style) {
- var ctx = getTurtleDrawingCtx();
- ctx.save();
- applyPenStyle(ctx, style);
- if (diameter === Infinity && drawing.canvas) {
- ctx.fillRect(0, 0, drawing.canvas.width, drawing.canvas.height);
- } else {
- ctx.beginPath();
- ctx.arc(position.pageX, position.pageY, diameter / 2, 0, 2*Math.PI, false);
- ctx.closePath();
- ctx.fill();
- if (style.strokeStyle) {
- ctx.stroke();
- }
+ var state = getTurtleData(elem);
+ if (state.style) {
+ // Apply a default style.
+ style = $.extend({}, state.style, style);
}
- ctx.restore();
+ var scale = drawingScale(elem);
+ drawAndClearPath(getDrawOnCanvas(state), state.corners, style, scale, 1);
}
function clearField(arg) {
- if (!arg || /\bcanvas\b/.test(arg)) {
- eraseBox(document, {fillStyle: 'transparent'});
+ if ((!arg || /\bcanvas\b/.test(arg)) && globalDrawing.canvas) {
+ var ctx = globalDrawing.canvas.getContext('2d');
+ ctx.save();
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
+ ctx.clearRect(
+ 0, 0, globalDrawing.canvas.width, globalDrawing.canvas.height);
+ ctx.restore();
}
if (!arg || /\bturtles\b/.test(arg)) {
- if (drawing.surface) {
- var sel = $(drawing.surface).find('.turtle');
+ if (globalDrawing.surface) {
+ var sel = $(globalDrawing.surface).find('.turtle').not('.turtlefield');
if (global_turtle) {
sel = sel.not(global_turtle);
}
sel.remove();
}
}
- if (!arg || /\btext\b/.test(arg)) {
- var keep = $('samp#_testpanel');
- if (drawing.surface) {
- keep = keep.add(drawing.surface);
+ if (!arg || /\blabels\b/.test(arg)) {
+ if (globalDrawing.surface) {
+ var sel = $(globalDrawing.surface).find('.turtlelabel')
+ .not('.turtlefield');
+ sel.remove();
}
- $('body').contents().not(keep).remove();
}
-}
-
-function eraseBox(elem, style) {
- var c = getCornersInPageCoordinates(elem),
- ctx = getTurtleDrawingCtx(),
- j = 1;
- if (!c || c.length < 3) { return; }
- ctx.save();
- // Clip to box and use 'copy' mode so that 'transparent' can be
- // written into the canvas - that's better erasing than 'white'.
- ctx.globalCompositeOperation = 'copy';
- applyPenStyle(ctx, style);
- ctx.beginPath();
- ctx.moveTo(c[0].pageX, c[0].pageY);
- for (; j < c.length; j += 1) {
- ctx.lineTo(c[j].pageX, c[j].pageY);
+ 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();
}
- ctx.closePath();
- ctx.clip();
- ctx.fill();
- ctx.restore();
}
-function getBoundingBoxOfCorners(c, clipToDoc) {
+function getBoundingBoxOfCorners(c, clip) {
if (!c || c.length < 1) return null;
var j = 1, result = {
left: Math.floor(c[0].pageX),
@@ -1853,30 +2144,44 @@ function getBoundingBoxOfCorners(c, clipToDoc) {
result.right = Math.max(result.right, Math.ceil(c[j].pageX));
result.bottom = Math.max(result.bottom, Math.ceil(c[j].pageY));
}
- if (clipToDoc) {
- result.left = Math.max(0, result.left);
- result.top = Math.max(0, result.top);
- result.right = Math.min(dw(), result.right);
- result.bottom = Math.min(dh(), result.bottom);
+ if (clip) {
+ result.left = Math.max(clip.left, result.left);
+ result.top = Math.max(clip.top, result.top);
+ result.right = Math.min(clip.right, result.right);
+ result.bottom = Math.min(clip.bottom, result.bottom);
+ }
+ return result;
+}
+
+function transformPoints(m, points) {
+ if (!m || !points) return null;
+ if (m.length == 4 && isone2x2(m)) return points;
+ var result = [], j, prod;
+ for (j = 0; j < points.length; ++j) {
+ prod = matrixVectorProduct(m, [points[j].pageX, points[j].pageY]);
+ result.push({pageX: prod[0], pageY: prod[1]});
}
return result;
}
function touchesPixel(elem, color) {
- if (!elem || !drawing.canvas) { return false; }
- var c = getCornersInPageCoordinates(elem),
- canvas = drawing.canvas,
- bb = getBoundingBoxOfCorners(c, true),
- w = (bb.right - bb.left) * drawing.subpixel,
- h = (bb.bottom - bb.top) * drawing.subpixel,
+ if (!elem) { return false; }
+ var rgba = rgbaForColor(color),
+ canvas = getCanvasForReading(elem);
+ if (!canvas) { return rgba && rgba[3] == 0; }
+ var trans = computeCanvasPageTransform(canvas),
+ originalc = getCornersInPageCoordinates(elem),
+ c = transformPoints(trans, originalc),
+ bb = getBoundingBoxOfCorners(c,
+ {left:0, top:0, right:canvas.width, bottom:canvas.height}),
+ w = (bb.right - bb.left),
+ 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.clearRect(0, 0, w, h);
octx.drawImage(canvas,
- bb.left * drawing.subpixel, bb.top * drawing.subpixel, w, h, 0, 0, w, h);
+ bb.left, bb.top, w, h, 0, 0, w, h);
octx.save();
// Erase everything outside clipping region.
octx.beginPath();
@@ -1885,21 +2190,28 @@ function touchesPixel(elem, color) {
octx.lineTo(w, h);
octx.lineTo(0, h);
octx.closePath();
- octx.moveTo((c[0].pageX - bb.left) * drawing.subpixel,
- (c[0].pageY - bb.top) * drawing.subpixel);
+ octx.moveTo((c[0].pageX - bb.left),
+ (c[0].pageY - bb.top));
for (; j < c.length; j += 1) {
- octx.lineTo((c[j].pageX - bb.left) * drawing.subpixel,
- (c[j].pageY - bb.top) * drawing.subpixel);
+ octx.lineTo((c[j].pageX - bb.left),
+ (c[j].pageY - bb.top));
}
octx.closePath();
octx.clip();
- octx.clearRect(0, 0, w, h);
+ if (rgba && rgba[3] == 0) {
+ // If testing for transparent, should clip with black, not transparent.
+ octx.fillRect(0, 0, w, h);
+ } else {
+ octx.clearRect(0, 0, w, h);
+ }
octx.restore();
// Now examine the results and look for alpha > 0%.
- data = octx.getImageData(0, 0, w, h).data;
- if (!rgba) {
+ 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;
for (j = 0; j < data.length; j += 4) {
- if (data[j + 3] > 0) return true;
+ if ((data[j + 3] > 0) == wantcolor) return true;
}
} else {
for (j = 0; j < data.length; j += 4) {
@@ -1921,9 +2233,14 @@ function touchesPixel(elem, color) {
// Functions in direct support of exported methods.
//////////////////////////////////////////////////////////////////////////
-function applyImg(sel, img) {
- if (sel[0].tagName == 'IMG') {
- setImageWithStableOrigin(sel[0], img.url, img.css);
+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, cb);
+ cb = null;
} else {
var props = {
backgroundImage: 'url(' + img.url + ')',
@@ -1935,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'),
@@ -1960,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) {
@@ -1977,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) {
@@ -1988,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;
@@ -2035,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;
}
@@ -2063,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),
@@ -2090,7 +2416,7 @@ function makeTurtleForwardHook() {
ts.tx = ntx;
ts.ty = nty;
elem.style[transform] = writeTurtleTransform(ts);
- flushPenState(elem);
+ flushPenState(elem, state);
}
};
}
@@ -2117,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;
}
@@ -2253,8 +2626,8 @@ function makeTurtleXYHook(publicname, propx, propy, displace) {
state = $.data(elem, 'turtleData'),
otx = ts.tx, oty = ts.ty, qpxy;
if (parts.length < 1 || parts.length > 2) { return; }
- if (parts.length >= 1) { ts[propx] = parts[0]; }
- if (parts.length >= 2) { ts[propy] = parts[1]; }
+ if (parts.length >= 1) { ts[propx] = parseFloat(parts[0]); }
+ if (parts.length >= 2) { ts[propy] = parseFloat(parts[1]); }
else if (!displace) { ts[propy] = ts[propx]; }
else { ts[propy] = 0; }
elem.style[transform] = writeTurtleTransform(ts);
@@ -2265,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 = {};
@@ -2291,18 +2725,21 @@ 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 {
// If not yet complete, then add the target element to the queue.
record.queue.push({elem: elem, css: css, cb: cb});
+ // Pop the element to the right dimensions early if possible.
+ resizeEarlyIfPossible(url, elem, css);
}
} else {
// Set up a new image load.
@@ -2310,19 +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.
- record.img.addEventListener('load', poll);
- record.img.addEventListener('error', poll);
- record.img.src = url;
- 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) {
@@ -2331,9 +2764,7 @@ function setImageWithStableOrigin(elem, url, css, cb) {
finishSet(record.img, queue[j].elem, queue[j].css, queue[j].cb);
}
}
- }
- // Start polling immediatey, 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.
@@ -2343,36 +2774,7 @@ function setImageWithStableOrigin(elem, url, css, cb) {
// some subsequent load that has now superceded ours.
if (elem.getAttribute('data-loading') == loaded.src) {
elem.removeAttribute('data-loading');
- // Read the element's origin before setting the image src.
- var oldOrigin = readTransformOrigin(elem);
- // Set the image to a 1x1 transparent GIF, and clear the transform origin.
- // (This "reset" code was original added in an effort to avoid browser
- // bugs, but it is not clear if it is still needed.)
- elem.src = 'data:image/gif;base64,R0lGODlhAQABAIAAA' +
- 'AAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
- var sel = $(elem);
- sel.css({
- backgroundImage: 'none',
- height: '',
- width: '',
- transformOrigin: ''
- });
- // Now set the source, and then apply any css requested.
- sel[0].width = loaded.width;
- sel[0].height = loaded.height;
- sel[0].src = loaded.src;
- if (css) {
- sel.css(css);
- }
- var newOrigin = readTransformOrigin(elem);
- // 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]) {
- var ts = readTurtleTransform(elem, true);
- ts.tx += oldOrigin[0] - newOrigin[0];
- ts.ty += oldOrigin[1] - newOrigin[1];
- elem.style[transform] = writeTurtleTransform(ts);
- }
+ applyLoadedImage(loaded, elem, css);
}
// Call the callback, if any.
if (cb) {
@@ -2381,6 +2783,135 @@ 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
+// the URL isn't yet marked as loaded. This is needed to allow
+// "wear pointer" to act synchronously for tests even though
+// PhantomJS asynchronously loads data: url images. (Note that
+// Chrome syncrhonously loads data: url images, so this is a
+// dead code path on Chrome.)
+function resizeEarlyIfPossible(url, elem, css) {
+ if (/^data:/.test(url) && css.width && css.height) {
+ applyLoadedImage(null, elem, css);
+ }
+}
+
+function applyLoadedImage(loaded, elem, css) {
+ // Read the element's origin before setting the image src.
+ var oldOrigin = readTransformOrigin(elem),
+ sel = $(elem),
+ isCanvas = (elem.tagName == 'CANVAS'),
+ ctx;
+ if (!isCanvas) {
+ // Set the image to a 1x1 transparent GIF, and clear the transform origin.
+ // (This "reset" code was original added in an effort to avoid browser
+ // bugs, but it is not clear if it is still needed.)
+ elem.src = 'data:image/gif;base64,R0lGODlhAQABAIAAA' +
+ 'AAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
+ }
+ sel.css({
+ backgroundImage: 'none',
+ height: '',
+ width: '',
+ turtleHull: '',
+ transformOrigin: ''
+ });
+ if (loaded) {
+ // Now set the source, and then apply any css requested.
+ if (loaded.tagName == 'VIDEO') {
+ elem.width = $(loaded).width();
+ elem.height = $(loaded).height();
+ if (isCanvas) {
+ ctx = elem.getContext('2d');
+ ctx.clearRect(0, 0, elem.width, elem.height);
+ ctx.drawImage(loaded, 0, 0, loaded.videoWidth, loaded.videoHeight,
+ 0, 0, elem.width, elem.height);
+ }
+ } else {
+ elem.width = loaded.width;
+ elem.height = loaded.height;
+ if (!isCanvas) {
+ elem.src = loaded.src;
+ } 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) { }
+ }
+ }
+ }
+ if (css) {
+ sel.css(css);
+ }
+ var newOrigin = readTransformOrigin(elem);
+ 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]) {
+ if (sel.css('position') == 'absolute' &&
+ /px$/.test(sel.css('left')) && /px$/.test(sel.css('top'))) {
+ // Do the translation using absolute positioning, if absolute.
+ sel.css('left',
+ parseFloat(sel.css('left')) + oldOrigin[0] - newOrigin[0]);
+ sel.css('top',
+ parseFloat(sel.css('top')) + oldOrigin[1] - newOrigin[1]);
+ } else {
+ // Do the translation using CSS transforms otherwise.
+ var ts = readTurtleTransform(elem, true);
+ ts.tx += oldOrigin[0] - newOrigin[0];
+ ts.ty += oldOrigin[1] - newOrigin[1];
+ elem.style[transform] = writeTurtleTransform(ts);
+ }
+ }
+}
+
function withinOrNot(obj, within, distance, x, y) {
var sel, elem, gbcr, pos, d2;
if (x === undefined && y === undefined) {
@@ -2448,19 +2979,22 @@ function withinOrNot(obj, within, distance, x, y) {
// Classes to allow jQuery to be subclassed.
//////////////////////////////////////////////////////////////////////////
-// A class to wrap jQuery
-Pencil = (function(_super) {
- __extends(Pencil, _super);
+// Sprite extends the jQuery object prototype.
+var Sprite = (function(_super) {
+ __extends(Sprite, _super);
- function Pencil(selector, context) {
+ function Sprite(selector, context) {
this.constructor = jQuery;
this.constructor.prototype = Object.getPrototypeOf(this);
- if ('function' !== typeof selector) {
- jQuery.fn.init.call(this, selector, context, rootjQuery);
+ if (!selector || typeof(selector) == 'string' ||
+ $.isPlainObject(selector) || typeof(selector) == 'number') {
+ // Use hatchone to create an element.
+ selector = hatchone(selector, context, '256x256');
}
+ jQuery.fn.init.call(this, selector, context, rootjQuery);
}
- Pencil.prototype.pushStack = function() {
+ Sprite.prototype.pushStack = function() {
var count, ret, same;
ret = jQuery.fn.pushStack.apply(this, arguments);
count = ret.length;
@@ -2475,47 +3009,2643 @@ Pencil = (function(_super) {
}
};
- return Pencil;
+ return Sprite;
})(jQuery.fn.init);
-Turtle = (function(_super) {
+// 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);
function Turtle(arg, context) {
- Turtle.__super__.constructor.call(this, hatchone(arg, context));
+ // The turtle is a sprite that just defaults to the turtle shape.
+ Turtle.__super__.constructor.call(this, hatchone(arg, context, 'turtle'));
}
return Turtle;
-})(Pencil);
-
-//////////////////////////////////////////////////////////////////////////
-// JQUERY REGISTRATION
-// Register all our hooks.
-//////////////////////////////////////////////////////////////////////////
+})(Sprite);
-$.extend(true, $, {
- cssHooks: {
- turtlePenStyle: makePenStyleHook(),
- turtlePenDown: makePenDownHook(),
- turtleSpeed: makeTurtleSpeedHook(),
- turtleEasing: makeTurtleEasingHook(),
- turtleForward: makeTurtleForwardHook(),
- turtleTurningRadius: makeTurningRadiusHook(),
- turtlePosition: makeTurtleXYHook('turtlePosition', 'tx', 'ty', true),
- turtlePositionX: makeTurtleHook('tx', identity, 'px', true),
- turtlePositionY: makeTurtleHook('ty', identity, 'px', true),
- turtleRotation: makeTurtleHook('rot', maybeArcRotation, 'deg', true),
- turtleScale: makeTurtleXYHook('turtleScale', 'sx', 'sy', false),
- turtleScaleX: makeTurtleHook('sx', identity, '', false),
- turtleScaleY: makeTurtleHook('sy', identity, '', false),
- turtleTwist: makeTurtleHook('twi', normalizeRotation, 'deg', false),
- turtleHull: makeHullHook()
- },
- cssNumber: {
- turtleRotation: true,
- turtleSpeed: true,
+// Webcam extends Sprite, and draws a live video camera by default.
+var Webcam = (function(_super) {
+ __extends(Webcam, _super);
+ function Webcam(opts, context) {
+ var attrs = "", hassrc = false, hasautoplay = false, hasdims = false;
+ if ($.isPlainObject(opts)) {
+ for (var key in opts) {
+ attrs += ' ' + key + '="' + escapeHtml(opts[key]) + '"';
+ }
+ hassrc = ('src' in opts);
+ hasautoplay = ('autoplay' in opts);
+ hasdims = ('width' in opts || 'height' in opts);
+ if (hasdims && !('height' in opts)) {
+ attrs += ' height=' + Math.round(opts.width * 3/4);
+ }
+ if (hasdims && !('width' in opts)) {
+ attrs += ' width=' + Math.round(opts.height * 4/3);
+ }
+ }
+ if (!hasautoplay) {
+ attrs += ' autoplay';
+ }
+ if (!hasdims) {
+ attrs += ' width=320 height=240';
+ }
+ Webcam.__super__.constructor.call(this, '