Last active
August 29, 2015 14:04
-
-
Save mythmon/987737f757cd893d3e65 to your computer and use it in GitHub Desktop.
Comic Knife
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8" /> | |
<style> | |
canvas { | |
margin: 20px; | |
border: 1px dotted black; | |
padding: 1px; | |
} | |
</style> | |
</head> | |
<body> | |
<p> | |
Split a comic with simple horizontal panels based on white gaps in the image. | |
The display will re-calculate as you change values. Press enter or tab to update. | |
</p> | |
<form> | |
<ul> | |
<li> | |
<label>Image URL</label> | |
<input name="url"> | |
</li> | |
<li> | |
<label>Minimum panel area</label> | |
<input name="minArea"> | |
</li> | |
<li> | |
<label>Border threshold</label> | |
<input name="panelThreshold"> | |
</li> | |
</ul> | |
</form> | |
<script src="setimmediate.js"></script> | |
<script src="promise.js"></script> | |
<script src="main.js"></script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function() { | |
"use strict"; | |
var config = { | |
panelThreshold: 0.99, | |
minArea: 3000, | |
url: 'http://imgs.xkcd.com/comics/tar.png', | |
} | |
function geomToString(x, y, w, h) { | |
return x + '+' + y + ':' + w + 'x' + h; | |
} | |
function Comic(canvas, x, y, w, h, transpose) { | |
this.canvas = canvas; | |
this.x = x || 0; | |
this.y = y || 0; | |
this.w = w || (canvas.width - this.x); | |
this.h = h || (canvas.height - this.y); | |
this.transpose = !!transpose; | |
if (this.transpose) { | |
this._x = this.y; | |
this._y = this.x; | |
this._w = this.h; | |
this._h = this.w; | |
} else { | |
this._x = this.x; | |
this._y = this.y; | |
this._w = this.w; | |
this._h = this.h; | |
} | |
this.imageData = canvas.getContext('2d').getImageData(this._x, this._y, this._w, this._h); | |
this.children = []; | |
} | |
Comic.prototype.crop = function(x, y, w, h) { | |
var cr = new Comic(this.canvas, this.x + x, this.y + y, w, h, this.transpose); | |
this.children.push(cr); | |
return cr; | |
}; | |
Comic.prototype.rotate = function() { | |
this.transpose = !this.transpose; | |
var tmp; | |
tmp = this.w; | |
this.w = this.h; | |
this.h = tmp; | |
tmp = this.x; | |
this.x = this.y; | |
this.y = tmp; | |
}; | |
Comic.prototype.at = function(x, y) { | |
if (this.transpose) { | |
var tmp = x; | |
x = y; | |
y = tmp; | |
} | |
var p = y * this._w * 4 + x * 4; | |
var data = this.imageData.data; | |
return Array.prototype.slice.call(this.imageData.data, p, p + 4); | |
// return this.imageData.data.slice(p, p + 4); | |
// return [data[p+0], data[p+1], data[p+2], data[p+3]]; | |
}; | |
Comic.prototype.drawTo = function(canvas) { | |
canvas.getContext('2d').drawImage(this.canvas, this._x, this._y, this._w, this._h, 0, 0, this._w, this._h); | |
}; | |
Comic.prototype.walk = function(cb) { | |
if (this.children.length === 0) { | |
cb(this); | |
} else { | |
for (var i=0; i < this.children.length; i++) { | |
this.children[i].walk(cb); | |
} | |
} | |
}; | |
var theComic; | |
function init() { | |
var input; | |
window.location.search.slice(1).split('&').forEach(function(pair) { | |
var key = pair.split('=')[0]; | |
var val = pair.split('=').slice(1).join('='); | |
config[key] = decodeURIComponent(val); | |
}); | |
for (var key in config) { | |
input = document.querySelector('input[name="' + key + '"]'); | |
if (input) { | |
input.value = config[key]; | |
} | |
} | |
var form = document.querySelector('form'); | |
form.addEventListener('change', function(e) { | |
var elem = e.target; | |
if (elem.tagName.toLowerCase() === 'input') { | |
var key = elem.getAttribute('name'); | |
var val = elem.value; | |
config[key] = val; | |
if (key === 'url') { | |
clearCanvases(); | |
load() | |
.then(function(canvas) { | |
theComic = new Comic(canvas); | |
detect(theComic); | |
output(theComic); | |
}) | |
.catch(function(err) { | |
console.error(err); | |
}); | |
} else { | |
theComic.children = []; | |
clearCanvases(); | |
detect(theComic); | |
output(theComic); | |
} | |
var qs = '?'; | |
for (var key in config) { | |
qs += key + '=' + encodeURIComponent(config[key]) + '&'; | |
} | |
qs = qs.slice(0, -1); | |
window.history.pushState(config, null, qs); | |
} | |
}); | |
form.addEventListener('submit', function(e) { | |
e.preventDefault(); | |
return false; | |
}); | |
load() | |
.then(function(canvas) { | |
theComic = new Comic(canvas); | |
detect(theComic); | |
output(theComic); | |
}) | |
.catch(function(err) { | |
console.error(err); | |
}); | |
} | |
function makeCanvas(add, width, height) { | |
var canvas = document.createElement('canvas'); | |
if (add) { | |
document.body.appendChild(canvas); | |
} | |
if (width) { | |
canvas.width = width; | |
} | |
if (height) { | |
canvas.height = height; | |
} | |
return canvas; | |
} | |
function load() { | |
console.log('loading'); | |
return new Promise(function(resolve, reject) { | |
var url = config.url; | |
if (url.indexOf('http') === 0) { | |
url = url.replace(/^https?:\/\//, ''); | |
url = 'http://www.corsproxy.com/' + url; | |
} | |
var image = document.createElement('img'); | |
image.setAttribute('crossorigin', true); | |
image.onload = function() { | |
var canvas = makeCanvas(false, this.width, this.height); | |
var ctx = canvas.getContext('2d'); | |
ctx.drawImage(image, 0, 0); | |
resolve(canvas); | |
}; | |
image.onerror = function(err) { | |
reject(err); | |
} | |
image.src = url; | |
}); | |
} | |
function detect(comic) { | |
console.log('detecting'); | |
var start = new Date(); | |
_detect(comic, 0); | |
console.log('done detecting after', new Date() - start, 'ms'); | |
return comic; | |
} | |
function _detect(comic, depth) { | |
console.log('_detect', depth) | |
var seamScores = []; | |
var max = 3 * 256 * comic.h; | |
var sum; | |
var x, y; | |
for (x = 0; x < comic.w; x++) { | |
sum = 0; | |
for (y = 0; y < comic.h; y++) { | |
var p = comic.at(x, y); | |
sum += p[0] + p[1] + p[2]; | |
} | |
seamScores.push(sum / max); | |
} | |
var sections = []; | |
// State machine. | |
function noop() {} | |
function setStart() { | |
sections.push([x, null]); | |
} | |
function setEnd() { | |
var sq = sections.pop(); | |
sq[1] = x; | |
sections.push(sq); | |
} | |
function err() { | |
throw new Error('Invalid state!'); | |
} | |
var state = 'start'; | |
var transitions = { | |
start: { | |
bg: ['preborder', noop], | |
fg: ['panel', setStart], | |
end: [null, err], | |
}, | |
preborder: { | |
bg: ['preborder', noop], | |
fg: ['panel', setStart], | |
end: [null, err], | |
}, | |
panel: { | |
bg: ['border', setEnd], | |
fg: ['panel', noop], | |
end: ['end', setEnd], | |
}, | |
border: { | |
bg: ['border', noop], | |
fg: ['panel', setStart], | |
end: ['end', noop], | |
} | |
}; | |
var input; | |
for (x = 0; x < comic.w; x++) { | |
input = seamScores[x] > config.panelThreshold ? 'bg' : 'fg'; | |
transitions[state][input][1](); | |
state = transitions[state][input][0]; | |
} | |
transitions[state]['end'][1](); | |
// combine sections to make them big enough. | |
sections = sections.slice(1).reduce(function(soFar, next) { | |
var prev = soFar.slice(-1)[0]; | |
var prevWidth = prev[1] - prev[0]; | |
var area = prevWidth * comic.h; | |
if (prevWidth * comic.h < config.minArea) { | |
soFar.pop(); | |
soFar.push([prev[0], next[1]]); | |
} else { | |
soFar.push(next); | |
} | |
return soFar; | |
}, [sections[0]]); | |
var recurse = depth < 1 || sections.length > 1; | |
// var recurse = false; | |
// var recurse = depth < 1; | |
sections.forEach(function(section) { | |
var start = section[0]; | |
var width = section[1] - start; | |
var sectionComic = comic.crop(start, 0, width, comic.h); | |
sectionComic.rotate(); | |
if (recurse) { | |
_detect(sectionComic, depth + 1); | |
} | |
}); | |
} | |
function clearCanvases() { | |
var canvases = document.querySelectorAll('canvas'); | |
for (var i = 0; i < canvases.length; i++) { | |
document.body.removeChild(canvases[i]); | |
} | |
} | |
function output(comic) { | |
comic.walk(function(panel) { | |
var panelCanvas = makeCanvas(true, panel._w, panel._h); | |
panel.drawTo(panelCanvas); | |
}); | |
} | |
init(); | |
})(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Promise polyfill v1.0.8 | |
* requires setImmediate | |
* | |
* © 2014 Dmitry Korobkin | |
* Released under the MIT license | |
* github.com/Octane/Promise | |
*/ | |
(function (global) {'use strict'; | |
var setImmediate = global.setImmediate || require('timers').setImmediate; | |
function toPromise(thenable) { | |
if (isPromise(thenable)) { | |
return thenable; | |
} | |
return new Promise(function (resolve, reject) { | |
setImmediate(function () { | |
try { | |
thenable.then(resolve, reject); | |
} catch (error) { | |
reject(error); | |
} | |
}); | |
}); | |
} | |
function isCallable(anything) { | |
return 'function' == typeof anything; | |
} | |
function isPromise(anything) { | |
return anything instanceof Promise; | |
} | |
function isThenable(anything) { | |
return Object(anything) === anything && isCallable(anything.then); | |
} | |
function isSettled(promise) { | |
return promise._fulfilled || promise._rejected; | |
} | |
function identity(value) { | |
return value; | |
} | |
function thrower(reason) { | |
throw reason; | |
} | |
function call(callback) { | |
callback(); | |
} | |
function dive(thenable, onFulfilled, onRejected) { | |
function interimOnFulfilled(value) { | |
if (isThenable(value)) { | |
toPromise(value).then(interimOnFulfilled, interimOnRejected); | |
} else { | |
onFulfilled(value); | |
} | |
} | |
function interimOnRejected(reason) { | |
if (isThenable(reason)) { | |
toPromise(reason).then(interimOnFulfilled, interimOnRejected); | |
} else { | |
onRejected(reason); | |
} | |
} | |
toPromise(thenable).then(interimOnFulfilled, interimOnRejected); | |
} | |
function Promise(resolver) { | |
this._fulfilled = false; | |
this._rejected = false; | |
this._value = undefined; | |
this._reason = undefined; | |
this._onFulfilled = []; | |
this._onRejected = []; | |
this._resolve(resolver); | |
} | |
Promise.resolve = function (value) { | |
if (isThenable(value)) { | |
return toPromise(value); | |
} | |
return new Promise(function (resolve) { | |
resolve(value); | |
}); | |
}; | |
Promise.reject = function (reason) { | |
return new Promise(function (resolve, reject) { | |
reject(reason); | |
}); | |
}; | |
Promise.race = function (values) { | |
return new Promise(function (resolve, reject) { | |
var value, | |
length = values.length, | |
i = 0; | |
while (i < length) { | |
value = values[i]; | |
if (isThenable(value)) { | |
dive(value, resolve, reject); | |
} else { | |
resolve(value); | |
} | |
i++; | |
} | |
}); | |
}; | |
Promise.all = function (values) { | |
return new Promise(function (resolve, reject) { | |
var thenables = 0, | |
fulfilled = 0, | |
value, | |
length = values.length, | |
i = 0; | |
values = values.slice(0); | |
while (i < length) { | |
value = values[i]; | |
if (isThenable(value)) { | |
thenables++; | |
dive( | |
value, | |
function (index) { | |
return function (value) { | |
values[index] = value; | |
fulfilled++; | |
if (fulfilled == thenables) { | |
resolve(values); | |
} | |
}; | |
}(i), | |
reject | |
); | |
} else { | |
//[1, , 3] → [1, undefined, 3] | |
values[i] = value; | |
} | |
i++; | |
} | |
if (!thenables) { | |
resolve(values); | |
} | |
}); | |
}; | |
Promise.prototype = { | |
constructor: Promise, | |
_resolve: function (resolver) { | |
var promise = this; | |
function resolve(value) { | |
promise._fulfill(value); | |
} | |
function reject(reason) { | |
promise._reject(reason); | |
} | |
try { | |
resolver(resolve, reject); | |
} catch(error) { | |
if (!isSettled(promise)) { | |
reject(error); | |
} | |
} | |
}, | |
_fulfill: function (value) { | |
if (!isSettled(this)) { | |
this._fulfilled = true; | |
this._value = value; | |
this._onFulfilled.forEach(call); | |
this._clearQueue(); | |
} | |
}, | |
_reject: function (reason) { | |
if (!isSettled(this)) { | |
this._rejected = true; | |
this._reason = reason; | |
this._onRejected.forEach(call); | |
this._clearQueue(); | |
} | |
}, | |
_enqueue: function (onFulfilled, onRejected) { | |
this._onFulfilled.push(onFulfilled); | |
this._onRejected.push(onRejected); | |
}, | |
_clearQueue: function () { | |
this._onFulfilled = []; | |
this._onRejected = []; | |
}, | |
then: function (onFulfilled, onRejected) { | |
var promise = this; | |
onFulfilled = isCallable(onFulfilled) ? onFulfilled : identity; | |
onRejected = isCallable(onRejected) ? onRejected : thrower; | |
return new Promise(function (resolve, reject) { | |
function asyncOnFulfilled() { | |
setImmediate(function () { | |
var value; | |
try { | |
value = onFulfilled(promise._value); | |
} catch (error) { | |
reject(error); | |
return; | |
} | |
if (isThenable(value)) { | |
toPromise(value).then(resolve, reject); | |
} else { | |
resolve(value); | |
} | |
}); | |
} | |
function asyncOnRejected() { | |
setImmediate(function () { | |
var reason; | |
try { | |
reason = onRejected(promise._reason); | |
} catch (error) { | |
reject(error); | |
return; | |
} | |
if (isThenable(reason)) { | |
toPromise(reason).then(resolve, reject); | |
} else { | |
resolve(reason); | |
} | |
}); | |
} | |
if (promise._fulfilled) { | |
asyncOnFulfilled(); | |
} else if (promise._rejected) { | |
asyncOnRejected(); | |
} else { | |
promise._enqueue(asyncOnFulfilled, asyncOnRejected); | |
} | |
}); | |
}, | |
'catch': function (onRejected) { | |
return this.then(undefined, onRejected); | |
} | |
}; | |
if ('undefined' != typeof module && module.exports) { | |
module.exports = global.Promise || Promise; | |
} else if (!global.Promise) { | |
global.Promise = Promise; | |
} | |
}(this)); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* setImmediate polyfill v1.0.0, supports IE9+ | |
* © 2014 Dmitry Korobkin | |
* Released under the MIT license | |
* github.com/Octane/setImmediate | |
*/ | |
window.setImmediate || function () {'use strict'; | |
var uid = 0, | |
storage = {}, | |
firstCall = true, | |
slice = Array.prototype.slice, | |
message = 'setImmediatePolyfillMessage'; | |
function fastApply(args) { | |
var func = args[0]; | |
switch (args.length) { | |
case 1: | |
return func(); | |
case 2: | |
return func(args[1]); | |
case 3: | |
return func(args[1], args[2]); | |
} | |
return func.apply(window, slice.call(args, 1)); | |
} | |
function callback(event) { | |
var key = event.data, | |
data; | |
if ('string' == typeof key && 0 == key.indexOf(message)) { | |
data = storage[key]; | |
if (data) { | |
delete storage[key]; | |
fastApply(data); | |
} | |
} | |
} | |
window.setImmediate = function setImmediate() { | |
var id = uid++, | |
key = message + id; | |
storage[key] = arguments; | |
if (firstCall) { | |
firstCall = false; | |
window.addEventListener('message', callback); | |
} | |
window.postMessage(key, '*'); | |
return id; | |
}; | |
window.clearImmediate = function clearImmediate(id) { | |
delete storage[message + id]; | |
}; | |
}(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment