From 5c7f7accf2bf9d1231464a27ff3712bbb4a8460d Mon Sep 17 00:00:00 2001 From: David Bau Date: Fri, 8 Aug 2014 01:51:23 -0400 Subject: [PATCH 1/4] Add .clip() method for sprites. Also makes sprites not-perfectly-transparent by default. Also refactors and fixes some small issues with the new hull-computing code (thanks for the code alex), and adds a test. --- jquery-turtle.js | 130 ++++++++++++++++++++++++++++++++++------------- test/clip.html | 45 ++++++++++++++++ 2 files changed, 141 insertions(+), 34 deletions(-) create mode 100644 test/clip.html diff --git a/jquery-turtle.js b/jquery-turtle.js index 3613692..38912ab 100644 --- a/jquery-turtle.js +++ b/jquery-turtle.js @@ -1366,6 +1366,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] }); @@ -2656,18 +2657,13 @@ 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) { + 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); } // If there was a change, then translate the element to keep the origin // in the same location on the screen. @@ -5115,7 +5111,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. @@ -6154,6 +6152,29 @@ 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); + }); + }), play: wrapcommand('play', 1, ["play(notes) Play notes. Notes are specified in " + "" + @@ -7796,7 +7817,10 @@ function createRectangleShape(width, height, subpixels) { return (function(color) { var c = getOffscreenCanvas(width, height); var ctx = c.getContext('2d'); - if (color && color != 'transparent') { + if (!color) { + color = "rgba(128,128,128,0.125)"; + } + if (color != 'transparent') { ctx.fillStyle = color; ctx.fillRect(0, 0, width, height); } @@ -9413,39 +9437,77 @@ function tryinitpanel() { eval("scope('jquery-turtle', " + seejs + ", this)"); -function cutTransparent(image){ - // We transform image into a 2D array of 0s and 1s, with 0 and 1 representing - // transparent and non-transparent pixels in image, respectively. +function transparentHull(image, threshold) { var c = document.createElement('canvas'); + if (!threshold) threshold = 0; c.width = image.width; c.height = image.height; ctx = c.getContext('2d'); ctx.drawImage(image, 0, 0); - data = ctx.getImageData(0, 0, c.width, c.height).data; - alphaData = []; - for(var i = 0; i < data.length; i += 4){ - var row = Math.floor(i / 4 / c.width); - var col = (i / 4) - row * c.width; - if(!alphaData[row]) alphaData[row] = []; - alphaData[row][col] = (data[i + 3] == 0 ? 0 : 1); - } + return transparentCanvasHull(c, threshold); +} + +function transparentCanvasHull(canvas, threshold) { + var ctx = canvas.getContext('2d'); + data = ctx.getImageData(0, 0, canvas.width, canvas.height).data; var hull = []; - for(var i = 0; i < c.height; i++){ + var intthresh = 256 * threshold; + var first, last, prevfirst = Infinity, prevlast = -1; + for (var row = 0; row < canvas.height; ++row) { // We only take the first/last hull in a row to reduce the number of // possible points from O(n^2) to O(n). - var first = -1, last = -1; - for(var j = 0; j < c.width; j++){ - if(alphaData[i][j] == 1){ - if(first < 0) first = j; - last = j; + first = Infinity; + last = -1; + for (var col = 0; col < canvas.width; ++col) { + if (data[row * 4 * canvas.width + col * 4 + 3] > intthresh) { + if (last < 0) first = col; + last = col; } } - if(first >= 0){ - hull.push({ pageX: first, pageY: i}); - hull.push({ pageX: last, pageY: i}); + if (last >= 0 || prevlast >= 0) { + hull.push({ pageX: Math.min(first, prevfirst), pageY: row}); + hull.push({ pageX: Math.max(last, prevlast), pageY: row}); } + prevfirst = first; + prevlast = last; + } + if (prevlast >= 0) { + hull.push({ pageX: prevfirst, pageY: canvas.height}); + hull.push({ pageX: prevlast, pageY: canvas.height}); } return convexHull(hull); } +function eraseOutsideHull(canvas, hull) { + var ctx = canvas.getContext('2d'), + w = canvas.width, + h = canvas.height + j = 0; + ctx.save(); + // Erase everything outside clipping region. + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(w, 0); + ctx.lineTo(w, h); + ctx.lineTo(0, h); + ctx.closePath(); + if (hull.length) { + ctx.moveTo(hull[0].pageX, hull[0].pageY); + for (; j < hull.length; j += 1) { + ctx.lineTo(hull[j].pageX, hull[j].pageY); + } + } + ctx.closePath(); + ctx.clip(); + ctx.clearRect(0, 0, w, h); + ctx.restore(); +} + +function scalePolygon(poly, sx, sy, tx, ty) { + for (var i = 0; i < poly.length; i++){ + poly[i].pageX = poly[i].pageX * sx + tx; + poly[i].pageY = poly[i].pageY * sy + ty; + } +} + })(jQuery); diff --git a/test/clip.html b/test/clip.html new file mode 100644 index 0000000..c5b2271 --- /dev/null +++ b/test/clip.html @@ -0,0 +1,45 @@ + + + + + +
+ + + + From 2b818acd68dadf3d025ddeda8ca934ad6ece723a Mon Sep 17 00:00:00 2001 From: David Bau Date: Fri, 8 Aug 2014 19:41:11 -0400 Subject: [PATCH 2/4] Deal with cross-origin restrictions on bit access, and remove openicon urls. --- jquery-turtle.js | 57 ++++++++++++------------------------------------ 1 file changed, 14 insertions(+), 43 deletions(-) diff --git a/jquery-turtle.js b/jquery-turtle.js index 38912ab..3841875 100644 --- a/jquery-turtle.js +++ b/jquery-turtle.js @@ -2658,12 +2658,16 @@ function applyLoadedImage(loaded, elem, css) { } var newOrigin = readTransformOrigin(elem); if (loaded && !css.turtleHull) { - 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); + 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. + } } // If there was a change, then translate the element to keep the origin // in the same location on the screen. @@ -7907,45 +7911,12 @@ function nameToImg(name, defaultshape) { if (shape) { return shape(color); } - // Parse openicon patterns. - // TODO: add more built-in shapes and remove this. - var openicon = - /^openicon:\/?\/?([^@\/][^@]*)(?:@(?:(\d+):)?(\d+))?$/.exec(name); - if (openicon) { - var openiconName = openicon[1], - sourceSize = parseInt(openicon[3]), - targetSize = parseInt(openicon[2]), - dotloc = openiconName.lastIndexOf('.'), - openiconType = 'png'; - if (openiconName.indexOf('/') == -1) { - openiconName = 'others/' + openiconName; - } - if (dotloc > 0 && dotloc <= openiconName.length - 4 && - dotloc >= openiconName.length - 5) { - openiconType = openiconName.substring(dotloc + 1); - openiconName = openiconName.substring(0, dotloc); - } - if (!targetSize) { - targetSize = sourceSize || 24; - } - if (!sourceSize) { - sourceSize = 48; - } - return { - url: 'http://openiconlibrary.sourceforge.net/gallery2/' + - 'open_icon_library-full/icons/' + openiconType + '/' + - sourceSize + 'x' + sourceSize + '/' + - openiconName + '.' + openiconType, - css: { - width: targetSize, - height: targetSize, - transformOrigin: '50% 50%', - opacity: 1 - } - } - } // Parse URLs. if (/^(?:(?:https?|data):)?\//i.exec(name)) { + if (/^https?:/i.test(name) && !/^https?:\/\/[^/]*pencilcode.net/.test(name) + && /(?:^|\.)pencilcode\.net$/.test(window.location.hostname)) { + name = '/proxy/' + name; + } return { url: name, css: { From f8b82cd8e59c78df53e70fec8bdcd07dec09306a Mon Sep 17 00:00:00 2001 From: David Bau Date: Fri, 8 Aug 2014 19:57:18 -0400 Subject: [PATCH 3/4] Slight optimization. --- jquery-turtle.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/jquery-turtle.js b/jquery-turtle.js index 3841875..81dc0f5 100644 --- a/jquery-turtle.js +++ b/jquery-turtle.js @@ -427,13 +427,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), From 8273770e066b733dcdf1ea69349fa0df3461a067 Mon Sep 17 00:00:00 2001 From: David Bau Date: Mon, 11 Aug 2014 12:10:54 -0400 Subject: [PATCH 4/4] Fix clip off-by-one error. --- jquery-turtle.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jquery-turtle.js b/jquery-turtle.js index 81dc0f5..2c58de9 100644 --- a/jquery-turtle.js +++ b/jquery-turtle.js @@ -9439,14 +9439,14 @@ function transparentCanvasHull(canvas, threshold) { } if (last >= 0 || prevlast >= 0) { hull.push({ pageX: Math.min(first, prevfirst), pageY: row}); - hull.push({ pageX: Math.max(last, prevlast), pageY: row}); + hull.push({ pageX: Math.max(last, prevlast) + 1, pageY: row}); } prevfirst = first; prevlast = last; } if (prevlast >= 0) { hull.push({ pageX: prevfirst, pageY: canvas.height}); - hull.push({ pageX: prevlast, pageY: canvas.height}); + hull.push({ pageX: prevlast + 1, pageY: canvas.height}); } return convexHull(hull); }