Skip to content

Instantly share code, notes, and snippets.

@mythmon
Last active August 29, 2015 14:04
Show Gist options
  • Save mythmon/987737f757cd893d3e65 to your computer and use it in GitHub Desktop.
Save mythmon/987737f757cd893d3e65 to your computer and use it in GitHub Desktop.
Comic Knife
<!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>
(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();
})();
/**
* 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));
/**
* 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