summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.bowerrc3
-rw-r--r--.gitignore9
-rw-r--r--1376516658960-dumpfm-DoritoWitch-TimeFLyTrans0001.pngbin0 -> 163827 bytes
-rw-r--r--FileSaver.js232
-rw-r--r--Gruntfile.js48
-rw-r--r--abyss.pngbin0 -> 193659 bytes
-rw-r--r--bower.json14
-rw-r--r--building.pngbin0 -> 408192 bytes
-rw-r--r--canvasquery.dither.js217
-rw-r--r--canvasquery.js1767
-rw-r--r--dataUriToBlob.js47
-rw-r--r--gif-animate.html108
-rw-r--r--gif-dither.html195
-rwxr-xr-xgif-encode/GIFEncoder.js513
-rw-r--r--gif-encode/LZWEncoder.js328
-rw-r--r--gif-encode/NeuQuant.js538
-rw-r--r--gif-encode/client.js260
-rw-r--r--gif-encode/tube.js323
-rw-r--r--gif-encode/util.js15
-rw-r--r--gif-encode/worker.js88
-rw-r--r--gif.html149
-rw-r--r--gif.js1690
-rw-r--r--gradient.jpgbin0 -> 19198 bytes
-rw-r--r--halftone.html48
-rw-r--r--index.html61
-rw-r--r--package.json10
-rw-r--r--pattern.html61
-rw-r--r--proxy.py22
-rw-r--r--shader.html35
-rw-r--r--threshold.html46
-rw-r--r--vendor/FileSaver/.bower.json15
-rw-r--r--vendor/FileSaver/FileSaver.js232
-rw-r--r--vendor/FileSaver/LICENSE.md30
-rw-r--r--vendor/FileSaver/README.md78
-rw-r--r--vendor/FileSaver/bower.json7
-rw-r--r--vendor/FileSaver/demo/demo.css55
-rw-r--r--vendor/FileSaver/demo/demo.js213
-rw-r--r--vendor/FileSaver/demo/demo.min.js2
-rw-r--r--vendor/FileSaver/demo/index.xhtml57
-rw-r--r--vendor/FileSaver/package.json23
40 files changed, 7539 insertions, 0 deletions
diff --git a/.bowerrc b/.bowerrc
new file mode 100644
index 0000000..69eea5f
--- /dev/null
+++ b/.bowerrc
@@ -0,0 +1,3 @@
+{
+ "directory": "vendor"
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0c8a721
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+*~
+v1
+.DS_Store
+*.orig
+.sass-cache/
+.#*
+*.swp
+node_modules
+jquery
diff --git a/1376516658960-dumpfm-DoritoWitch-TimeFLyTrans0001.png b/1376516658960-dumpfm-DoritoWitch-TimeFLyTrans0001.png
new file mode 100644
index 0000000..019c209
--- /dev/null
+++ b/1376516658960-dumpfm-DoritoWitch-TimeFLyTrans0001.png
Binary files differ
diff --git a/FileSaver.js b/FileSaver.js
new file mode 100644
index 0000000..378a9dc
--- /dev/null
+++ b/FileSaver.js
@@ -0,0 +1,232 @@
+/* FileSaver.js
+ * A saveAs() FileSaver implementation.
+ * 2013-10-21
+ *
+ * By Eli Grey, http://eligrey.com
+ * License: X11/MIT
+ * See LICENSE.md
+ */
+
+/*global self */
+/*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true,
+ plusplus: true */
+
+/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */
+
+var saveAs = saveAs
+ || (typeof navigator !== 'undefined' && navigator.msSaveOrOpenBlob && navigator.msSaveOrOpenBlob.bind(navigator))
+ || (function(view) {
+ "use strict";
+ var
+ doc = view.document
+ // only get URL when necessary in case BlobBuilder.js hasn't overridden it yet
+ , get_URL = function() {
+ return view.URL || view.webkitURL || view;
+ }
+ , URL = view.URL || view.webkitURL || view
+ , save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a")
+ , can_use_save_link = !view.externalHost && "download" in save_link
+ , click = function(node) {
+ var event = doc.createEvent("MouseEvents");
+ event.initMouseEvent(
+ "click", true, false, view, 0, 0, 0, 0, 0
+ , false, false, false, false, 0, null
+ );
+ node.dispatchEvent(event);
+ }
+ , webkit_req_fs = view.webkitRequestFileSystem
+ , req_fs = view.requestFileSystem || webkit_req_fs || view.mozRequestFileSystem
+ , throw_outside = function (ex) {
+ (view.setImmediate || view.setTimeout)(function() {
+ throw ex;
+ }, 0);
+ }
+ , force_saveable_type = "application/octet-stream"
+ , fs_min_size = 0
+ , deletion_queue = []
+ , process_deletion_queue = function() {
+ var i = deletion_queue.length;
+ while (i--) {
+ var file = deletion_queue[i];
+ if (typeof file === "string") { // file is an object URL
+ URL.revokeObjectURL(file);
+ } else { // file is a File
+ file.remove();
+ }
+ }
+ deletion_queue.length = 0; // clear queue
+ }
+ , dispatch = function(filesaver, event_types, event) {
+ event_types = [].concat(event_types);
+ var i = event_types.length;
+ while (i--) {
+ var listener = filesaver["on" + event_types[i]];
+ if (typeof listener === "function") {
+ try {
+ listener.call(filesaver, event || filesaver);
+ } catch (ex) {
+ throw_outside(ex);
+ }
+ }
+ }
+ }
+ , FileSaver = function(blob, name) {
+ // First try a.download, then web filesystem, then object URLs
+ var
+ filesaver = this
+ , type = blob.type
+ , blob_changed = false
+ , object_url
+ , target_view
+ , get_object_url = function() {
+ var object_url = get_URL().createObjectURL(blob);
+ deletion_queue.push(object_url);
+ return object_url;
+ }
+ , dispatch_all = function() {
+ dispatch(filesaver, "writestart progress write writeend".split(" "));
+ }
+ // on any filesys errors revert to saving with object URLs
+ , fs_error = function() {
+ // don't create more object URLs than needed
+ if (blob_changed || !object_url) {
+ object_url = get_object_url(blob);
+ }
+ if (target_view) {
+ target_view.location.href = object_url;
+ } else {
+ window.open(object_url, "_blank");
+ }
+ filesaver.readyState = filesaver.DONE;
+ dispatch_all();
+ }
+ , abortable = function(func) {
+ return function() {
+ if (filesaver.readyState !== filesaver.DONE) {
+ return func.apply(this, arguments);
+ }
+ };
+ }
+ , create_if_not_found = {create: true, exclusive: false}
+ , slice
+ ;
+ filesaver.readyState = filesaver.INIT;
+ if (!name) {
+ name = "download";
+ }
+ if (can_use_save_link) {
+ object_url = get_object_url(blob);
+ // FF for Android has a nasty garbage collection mechanism
+ // that turns all objects that are not pure javascript into 'deadObject'
+ // this means `doc` and `save_link` are unusable and need to be recreated
+ // `view` is usable though:
+ doc = view.document;
+ save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a");
+ save_link.href = object_url;
+ save_link.download = name;
+ var event = doc.createEvent("MouseEvents");
+ event.initMouseEvent(
+ "click", true, false, view, 0, 0, 0, 0, 0
+ , false, false, false, false, 0, null
+ );
+ save_link.dispatchEvent(event);
+ filesaver.readyState = filesaver.DONE;
+ dispatch_all();
+ return;
+ }
+ // Object and web filesystem URLs have a problem saving in Google Chrome when
+ // viewed in a tab, so I force save with application/octet-stream
+ // http://code.google.com/p/chromium/issues/detail?id=91158
+ if (view.chrome && type && type !== force_saveable_type) {
+ slice = blob.slice || blob.webkitSlice;
+ blob = slice.call(blob, 0, blob.size, force_saveable_type);
+ blob_changed = true;
+ }
+ // Since I can't be sure that the guessed media type will trigger a download
+ // in WebKit, I append .download to the filename.
+ // https://bugs.webkit.org/show_bug.cgi?id=65440
+ if (webkit_req_fs && name !== "download") {
+ name += ".download";
+ }
+ if (type === force_saveable_type || webkit_req_fs) {
+ target_view = view;
+ }
+ if (!req_fs) {
+ fs_error();
+ return;
+ }
+ fs_min_size += blob.size;
+ req_fs(view.TEMPORARY, fs_min_size, abortable(function(fs) {
+ fs.root.getDirectory("saved", create_if_not_found, abortable(function(dir) {
+ var save = function() {
+ dir.getFile(name, create_if_not_found, abortable(function(file) {
+ file.createWriter(abortable(function(writer) {
+ writer.onwriteend = function(event) {
+ target_view.location.href = file.toURL();
+ deletion_queue.push(file);
+ filesaver.readyState = filesaver.DONE;
+ dispatch(filesaver, "writeend", event);
+ };
+ writer.onerror = function() {
+ var error = writer.error;
+ if (error.code !== error.ABORT_ERR) {
+ fs_error();
+ }
+ };
+ "writestart progress write abort".split(" ").forEach(function(event) {
+ writer["on" + event] = filesaver["on" + event];
+ });
+ writer.write(blob);
+ filesaver.abort = function() {
+ writer.abort();
+ filesaver.readyState = filesaver.DONE;
+ };
+ filesaver.readyState = filesaver.WRITING;
+ }), fs_error);
+ }), fs_error);
+ };
+ dir.getFile(name, {create: false}, abortable(function(file) {
+ // delete file if it already exists
+ file.remove();
+ save();
+ }), abortable(function(ex) {
+ if (ex.code === ex.NOT_FOUND_ERR) {
+ save();
+ } else {
+ fs_error();
+ }
+ }));
+ }), fs_error);
+ }), fs_error);
+ }
+ , FS_proto = FileSaver.prototype
+ , saveAs = function(blob, name) {
+ return new FileSaver(blob, name);
+ }
+ ;
+ FS_proto.abort = function() {
+ var filesaver = this;
+ filesaver.readyState = filesaver.DONE;
+ dispatch(filesaver, "abort");
+ };
+ FS_proto.readyState = FS_proto.INIT = 0;
+ FS_proto.WRITING = 1;
+ FS_proto.DONE = 2;
+
+ FS_proto.error =
+ FS_proto.onwritestart =
+ FS_proto.onprogress =
+ FS_proto.onwrite =
+ FS_proto.onabort =
+ FS_proto.onerror =
+ FS_proto.onwriteend =
+ null;
+
+ view.addEventListener("unload", process_deletion_queue, false);
+ return saveAs;
+}(this.self || this.window || this.content));
+// `self` is undefined in Firefox for Android content script context
+// while `this` is nsIContentFrameMessageManager
+// with an attribute `content` that corresponds to the window
+
+if (typeof module !== 'undefined') module.exports = saveAs;
diff --git a/Gruntfile.js b/Gruntfile.js
new file mode 100644
index 0000000..814a1c8
--- /dev/null
+++ b/Gruntfile.js
@@ -0,0 +1,48 @@
+module.exports = function(grunt) {
+
+ // Project configuration.
+ grunt.initConfig({
+ pkg: grunt.file.readJSON('package.json'),
+ concat: {
+ dist: {
+ src: [
+ 'js/vendor/jquery/jquery.js',
+ 'js/vendor/loader.js',
+ 'js/vendor/okhover.js',
+ 'js/vendor/tweenjs/Tween.js',
+ 'js/vendor/nodoubletapzoom/jquery.nodoubletapzoom.js',
+ 'js/vendor/tweenjs/src/Tween.js',
+ 'js/vendor/spin.js/spin.js',
+ 'js/mx/mx.js',
+ 'js/mx/mx.*.js',
+ 'js/spinner.js',
+ 'js/pano.js'
+
+ ],
+ dest: 'js/live.concat.js',
+ }
+ },
+ uglify: {
+ options: {
+ banner: '/* okfocus 2013 internet legends ~ https://github.com/okfocus/okfocus.github.io */\n'
+ },
+ build: {
+ src: 'js/live.concat.js',
+ dest: 'js/live.min.js'
+ }
+ },
+ watch: {
+ files: ['js/!(live.min|live.concat).js','js/vendor/*'],
+ tasks: ['default']
+ }
+ });
+
+ // Load tasks that we'll be using
+ grunt.loadNpmTasks('grunt-contrib-concat');
+ grunt.loadNpmTasks('grunt-contrib-uglify');
+ grunt.loadNpmTasks('grunt-contrib-watch');
+
+
+ // Default task(s).
+ grunt.registerTask('default', ['concat', 'uglify']);
+};
diff --git a/abyss.png b/abyss.png
new file mode 100644
index 0000000..4ce8d1d
--- /dev/null
+++ b/abyss.png
Binary files differ
diff --git a/bower.json b/bower.json
new file mode 100644
index 0000000..31aa3d2
--- /dev/null
+++ b/bower.json
@@ -0,0 +1,14 @@
+{
+ "name": "dither",
+ "version": "0.0.4",
+ "homepage": "http://asdf.us",
+ "authors": [
+ "julie"
+ ],
+ "description": "dither fx",
+ "private": true,
+ "dependencies": {
+ "jquery": "~2.0.3",
+ "FileSaver": "*"
+ }
+}
diff --git a/building.png b/building.png
new file mode 100644
index 0000000..4032700
--- /dev/null
+++ b/building.png
Binary files differ
diff --git a/canvasquery.dither.js b/canvasquery.dither.js
new file mode 100644
index 0000000..a76ad12
--- /dev/null
+++ b/canvasquery.dither.js
@@ -0,0 +1,217 @@
+var patterns = {}
+patterns[2] = [ 0, 2,
+ 3, 1 ]
+
+patterns[3] = [ 0, 5, 3,
+ 8, 1, 6,
+ 4, 7, 2, ]
+
+patterns[4] = [ 0, 8, 2, 10,
+ 6, 14, 4, 12,
+ 3, 11, 1, 9,
+ 5, 13, 7, 15 ]
+
+CanvasQuery.Wrapper.prototype.threshold = function(factor) {
+ var bitmap = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height)
+ var bitmapData = bitmap.data
+
+ var width = this.canvas.width
+ var height = this.canvas.height
+
+ if (factor < 1) factor *= 255
+
+ for (var i = 0; i < height; i++) {
+ for (var j = 0; j < width; j++) {
+ var a = 4 * (i*width + j)
+ var val = (bitmapData[a] + bitmapData[a+1] + bitmapData[a+2]) / 3
+ var lum = val > factor ? 255 : 0
+ bitmapData[a] = bitmapData[a+1] = bitmapData[a+2] = lum
+ }
+ }
+ this.context.putImageData(bitmap, 0, 0);
+ return this;
+}
+
+CanvasQuery.Wrapper.prototype.randomDither = function() {
+ var bitmap = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height)
+ var bitmapData = bitmap.data
+
+ var width = this.canvas.width
+ var height = this.canvas.height
+
+ for (var i = 0; i < height; i++) {
+ for (var j = 0; j < width; j++) {
+ var a = 4 * (i*width + j)
+ var val = (bitmapData[a] + bitmapData[a+1] + bitmapData[a+2]) / (3*255)
+ var lum = val > Math.random() ? 255 : 0
+ bitmapData[a] = bitmapData[a+1] = bitmapData[a+2] = lum
+ }
+ }
+ this.context.putImageData(bitmap, 0, 0);
+ return this;
+}
+
+CanvasQuery.Wrapper.prototype.pattern2Dither = function() {
+ this.patternDither(2)
+}
+CanvasQuery.Wrapper.prototype.pattern3Dither = function() {
+ this.patternDither(3)
+}
+CanvasQuery.Wrapper.prototype.pattern4Dither = function() {
+ this.patternDither(4)
+}
+CanvasQuery.Wrapper.prototype.patternDither = function(n) {
+ var bitmap = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height)
+ var bitmapData = bitmap.data
+
+ var width = this.canvas.width
+ var height = this.canvas.height
+
+ var pat = patterns[n]
+ var len = pat.length - 1
+
+ for (var i = 0; i < height; i++) {
+ for (var j = 0; j < width; j++) {
+ var p = ((i % n) * n) + (j % n)
+ var a = 4 * (i*width + j)
+ var val = (bitmapData[a] + bitmapData[a+1] + bitmapData[a+2]) / (3*255)
+ var lum = val > pat[p]/len ? 255 : 0
+ bitmapData[a] = bitmapData[a+1] = bitmapData[a+2] = lum
+ }
+ }
+ this.context.putImageData(bitmap, 0, 0);
+ return this;
+}
+
+CanvasQuery.Wrapper.prototype.pattern2LiteDither = function() {
+ this.patternLiteDither(2)
+}
+CanvasQuery.Wrapper.prototype.pattern3LiteDither = function() {
+ this.patternLiteDither(3)
+}
+CanvasQuery.Wrapper.prototype.pattern4LiteDither = function() {
+ this.patternLiteDither(4)
+}
+CanvasQuery.Wrapper.prototype.patternLiteDither = function(n) {
+ var bitmap = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height)
+ var bitmapData = bitmap.data
+
+ var width = this.canvas.width
+ var height = this.canvas.height
+
+ var pat = patterns[n]
+ var len = pat.length
+
+ for (var i = 0; i < height; i++) {
+ for (var j = 0; j < width; j++) {
+ var p = ((i % n) * n) + (j % n)
+ var a = 4 * (i*width + j)
+ var val = (bitmapData[a] + bitmapData[a+1] + bitmapData[a+2]) / (3*255)
+ var lum = val > pat[p]/len ? 255 : 0
+ bitmapData[a] = bitmapData[a+1] = bitmapData[a+2] = lum
+ }
+ }
+ this.context.putImageData(bitmap, 0, 0);
+ return this;
+}
+
+CanvasQuery.Wrapper.prototype.floydSteinbergDither = function(n) {
+ var bitmap = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height)
+ var bitmapData = bitmap.data
+
+ var mask = this.grayscaleToMask()
+
+ var width = this.canvas.width
+ var height = this.canvas.height
+
+ for (var y = 0; y < height; y++) {
+ for (var x = 0; x < width; x++) {
+ var p = (y*width + x)
+ var a = 4 * p
+ var val = mask[p]
+ var lum = val > 127 ? 255 : 0
+ var error = val - lum
+ if (x < width-1)
+ mask[ (y*width) + x+1 ] += 7/16 * error
+ if (y < height-1 && x > 0)
+ mask[ ((y+1)*width) + x-1 ] += 5/16 * error
+ if (y < height-1)
+ mask[ ((y+1)*width) + x ] += 3/16 * error
+ if (y < height-1 && x < width-1)
+ mask[ ((y+1)*width) + x+1 ] += 1/16 * error
+ bitmapData[a] = bitmapData[a+1] = bitmapData[a+2] = lum
+ }
+ }
+ this.context.putImageData(bitmap, 0, 0);
+ return this;
+}
+
+CanvasQuery.Wrapper.prototype.rightDither = function(n) {
+ var bitmap = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height)
+ var bitmapData = bitmap.data
+
+ var mask = this.grayscaleToMask()
+
+ var width = this.canvas.width
+ var height = this.canvas.height
+
+ for (var y = 0; y < height; y++) {
+ for (var x = 0; x < width; x++) {
+ var p = (y*width + x)
+ var a = 4 * p
+ var val = mask[p]
+ var lum = val > 127 ? 255 : 0
+ var error = val - lum
+ if (x < width-1)
+ mask[ (y*width) + x+1 ] += 7/16 * error
+ bitmapData[a] = bitmapData[a+1] = bitmapData[a+2] = lum
+ }
+ }
+ this.context.putImageData(bitmap, 0, 0);
+ return this;
+}
+
+CanvasQuery.Wrapper.prototype.halftone = function(radius, angle) {
+ var mask = this.grayscaleToMask()
+
+ this.fillStyle("#fff")
+ this.fillRect(0, 0, this.canvas.width, this.canvas.height)
+
+ var diameter = radius*2
+ var TWO_PI = Math.PI*2
+
+ var angle = angle / 180 * Math.PI
+
+ var cos = Math.cos(angle)
+ var sin = Math.sin(angle)
+
+ var xstep = cos * radius
+ var ystep = sin * radius
+
+ var w = this.canvas.width
+ var h = this.canvas.height
+
+ this.fillStyle("#000")
+
+ for (var i = -w; i < w; i++) {
+ for (var j = -h; j < h; j++) {
+ var x = i * ystep - j * xstep
+ var y = i * xstep + j * ystep
+ if (x > -diameter && y > -diameter && x < w+diameter && y < h+diameter) {
+ circle(this,x,y)
+ }
+ }
+ }
+
+ function circle(cq,x,y) {
+ var xx = x < 0 ? 0 : x > w ? w - 1 : x;
+ var yy = y < 0 ? 0 : y > h ? h - 1 : y;
+ var r = (1 - Math.pow( mask[ ~~(~~yy*w+(xx)) ] / 255, Math.E/4 )) * radius
+ cq.beginPath()
+ .arc(x,y,r,0,TWO_PI)
+ .closePath()
+ .fill();
+ }
+ return this;
+}
+
diff --git a/canvasquery.js b/canvasquery.js
new file mode 100644
index 0000000..a5b36f3
--- /dev/null
+++ b/canvasquery.js
@@ -0,0 +1,1767 @@
+/*
+ Canvas Query 0.8.4
+ http://canvasquery.org
+ (c) 2012-2013 http://rezoner.net
+ Canvas Query may be freely distributed under the MIT license.
+*/
+
+(function(window, undefined) {
+
+ var MOBILE = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
+
+
+ window.requestAnimationFrame = (function() {
+ return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function(callback) {
+ window.setTimeout(callback, 1000 / 60);
+ };
+ })();
+
+
+ var $ = function(selector) {
+ if (arguments.length === 0) {
+ var canvas = $.createCanvas(window.innerWidth, window.innerHeight);
+ window.addEventListener("resize", function() {
+ // canvas.width = window.innerWidth;
+ // canvas.height = window.innerHeight;
+ });
+ } else if (typeof selector === "string") {
+ var canvas = document.querySelector(selector);
+ } else if (typeof selector === "number") {
+ var canvas = $.createCanvas(arguments[0], arguments[1]);
+ } else if (selector instanceof Image || selector instanceof HTMLImageElement) {
+ var canvas = $.createCanvas(selector);
+ } else if (selector instanceof $.Wrapper) {
+ return selector;
+ } else {
+ var canvas = selector;
+ }
+
+ return new $.Wrapper(canvas);
+ }
+
+ $.extend = function() {
+ for (var i = 1; i < arguments.length; i++) {
+ for (var j in arguments[i]) {
+ arguments[0][j] = arguments[i][j];
+ }
+ }
+
+ return arguments[0];
+ };
+
+ $.augment = function() {
+ for (var i = 1; i < arguments.length; i++) {
+ _.extend(arguments[0], arguments[i]);
+ arguments[i](arguments[0]);
+ }
+ };
+
+ $.distance = function(x1, y1, x2, y2) {
+ if (arguments.length > 2) {
+ var dx = x1 - x2;
+ var dy = y1 - y2;
+
+ return Math.sqrt(dx * dx + dy * dy);
+ } else {
+ return Math.abs(x1 - y1);
+ }
+ };
+
+ $.extend($, {
+
+ keycodes: {
+ 37: "left",
+ 38: "up",
+ 39: "right",
+ 40: "down",
+ 45: "insert",
+ 46: "delete",
+ 8: "backspace",
+ 9: "tab",
+ 13: "enter",
+ 16: "shift",
+ 17: "ctrl",
+ 18: "alt",
+ 19: "pause",
+ 20: "capslock",
+ 27: "escape",
+ 32: "space",
+ 33: "pageup",
+ 34: "pagedown",
+ 35: "end",
+ 112: "f1",
+ 113: "f2",
+ 114: "f3",
+ 115: "f4",
+ 116: "f5",
+ 117: "f6",
+ 118: "f7",
+ 119: "f8",
+ 120: "f9",
+ 121: "f10",
+ 122: "f11",
+ 123: "f12",
+ 144: "numlock",
+ 145: "scrolllock",
+ 186: "semicolon",
+ 187: "equal",
+ 188: "comma",
+ 189: "dash",
+ 190: "period",
+ 191: "slash",
+ 192: "graveaccent",
+ 219: "openbracket",
+ 220: "backslash",
+ 221: "closebraket",
+ 222: "singlequote"
+ },
+
+ cleanArray: function(array, property) {
+
+ var lastArgument = arguments[arguments.length - 1];
+ var isLastArgumentFunction = typeof lastArgument === "function";
+
+ for (var i = 0, len = array.length; i < len; i++) {
+ if (array[i] === null || (property && array[i][property])) {
+ if (isLastArgumentFunction) {
+ lastArgument(array[i]);
+ }
+ array.splice(i--, 1);
+ len--;
+ }
+ }
+ },
+
+ specialBlendFunctions: ["color", "value", "hue", "saturation"],
+
+ blendFunctions: {
+ normal: function(a, b) {
+ return b;
+ },
+
+ overlay: function(a, b) {
+ a /= 255;
+ b /= 255;
+ var result = 0;
+
+ if (a < 0.5) result = 2 * a * b;
+ else result = 1 - 2 * (1 - a) * (1 - b);
+
+ return Math.min(255, Math.max(0, result * 255 | 0));
+ },
+
+ hardLight: function(a, b) {
+ return $.blendFunctions.overlay(b, a);
+ },
+
+ softLight: function(a, b) {
+ a /= 255;
+ b /= 255;
+
+ var v = (1 - 2 * b) * (a * a) + 2 * b * a;
+ return $.limitValue(v * 255, 0, 255);
+ },
+
+ dodge: function(a, b) {
+ return Math.min(256 * a / (255 - b + 1), 255);
+ },
+
+ burn: function(a, b) {
+ return 255 - Math.min(256 * (255 - a) / (b + 1), 255);
+ },
+
+ multiply: function(a, b) {
+ return b * a / 255;
+ },
+
+ divide: function(a, b) {
+ return Math.min(256 * a / (b + 1), 255);
+ },
+
+ screen: function(a, b) {
+ return 255 - (255 - b) * (255 - a) / 255;
+ },
+
+ grainExtract: function(a, b) {
+ return $.limitValue(a - b + 128, 0, 255);
+ },
+
+ grainMerge: function(a, b) {
+ return $.limitValue(a + b - 128, 0, 255);
+ },
+
+ difference: function(a, b) {
+ return Math.abs(a - b);
+ },
+
+ addition: function(a, b) {
+ return Math.min(a + b, 255);
+ },
+
+ substract: function(a, b) {
+ return Math.max(a - b, 0);
+ },
+
+ darkenOnly: function(a, b) {
+ return Math.min(a, b);
+ },
+
+ lightenOnly: function(a, b) {
+ return Math.max(a, b);
+ },
+
+ color: function(a, b) {
+ var aHSL = $.rgbToHsl(a);
+ var bHSL = $.rgbToHsl(b);
+
+ return $.hslToRgb(bHSL[0], bHSL[1], aHSL[2]);
+ },
+
+ hue: function(a, b) {
+ var aHSV = $.rgbToHsv(a);
+ var bHSV = $.rgbToHsv(b);
+
+ if (!bHSV[1]) return $.hsvToRgb(aHSV[0], aHSV[1], aHSV[2]);
+ else return $.hsvToRgb(bHSV[0], aHSV[1], aHSV[2]);
+ },
+
+ value: function(a, b) {
+ var aHSV = $.rgbToHsv(a);
+ var bHSV = $.rgbToHsv(b);
+
+ return $.hsvToRgb(aHSV[0], aHSV[1], bHSV[2]);
+ },
+
+ saturation: function(a, b) {
+ var aHSV = $.rgbToHsv(a);
+ var bHSV = $.rgbToHsv(b);
+
+ return $.hsvToRgb(aHSV[0], bHSV[1], aHSV[2]);
+ }
+ },
+
+ blend: function(below, above, mode, mix) {
+ if (typeof mix === "undefined") mix = 1;
+
+ var below = $(below);
+ var above = $(above);
+
+ var belowCtx = below.context;
+ var aboveCtx = above.context;
+
+ var belowData = belowCtx.getImageData(0, 0, below.canvas.width, below.canvas.height);
+ var aboveData = aboveCtx.getImageData(0, 0, above.canvas.width, above.canvas.height);
+
+ var belowPixels = belowData.data;
+ var abovePixels = aboveData.data;
+
+ var imageData = this.createImageData(below.canvas.width, below.canvas.height);
+ var pixels = imageData.data;
+
+ var blendingFunction = $.blendFunctions[mode];
+
+ if ($.specialBlendFunctions.indexOf(mode) !== -1) {
+ for (var i = 0, len = belowPixels.length; i < len; i += 4) {
+ var rgb = blendingFunction([belowPixels[i + 0], belowPixels[i + 1], belowPixels[i + 2]], [abovePixels[i + 0], abovePixels[i + 1], abovePixels[i + 2]]);
+
+ pixels[i + 0] = belowPixels[i + 0] + (rgb[0] - belowPixels[i + 0]) * mix;
+ pixels[i + 1] = belowPixels[i + 1] + (rgb[1] - belowPixels[i + 1]) * mix;
+ pixels[i + 2] = belowPixels[i + 2] + (rgb[2] - belowPixels[i + 2]) * mix;
+
+ pixels[i + 3] = belowPixels[i + 3];
+ }
+ } else {
+
+ for (var i = 0, len = belowPixels.length; i < len; i += 4) {
+ var r = blendingFunction(belowPixels[i + 0], abovePixels[i + 0]);
+ var g = blendingFunction(belowPixels[i + 1], abovePixels[i + 1]);
+ var b = blendingFunction(belowPixels[i + 2], abovePixels[i + 2]);
+
+ pixels[i + 0] = belowPixels[i + 0] + (r - belowPixels[i + 0]) * mix;
+ pixels[i + 1] = belowPixels[i + 1] + (g - belowPixels[i + 1]) * mix;
+ pixels[i + 2] = belowPixels[i + 2] + (b - belowPixels[i + 2]) * mix;
+
+ pixels[i + 3] = belowPixels[i + 3];
+ }
+ }
+
+ below.context.putImageData(imageData, 0, 0);
+
+ return below;
+ },
+
+ wrapValue: function(value, min, max) {
+ var d = Math.abs(max - min);
+ return min + (value - min) % d;
+ },
+
+ limitValue: function(value, min, max) {
+ return value < min ? min : value > max ? max : value;
+ },
+
+ mix: function(a, b, ammount) {
+ return a + (b - a) * ammount;
+ },
+
+ hexToRgb: function(hex) {
+ if (hex.length === 7) return ['0x' + hex[1] + hex[2] | 0, '0x' + hex[3] + hex[4] | 0, '0x' + hex[5] + hex[6] | 0];
+ else return ['0x' + hex[1] | 0, '0x' + hex[2], '0x' + hex[3] | 0];
+ },
+
+ rgbToHex: function(r, g, b) {
+ return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1, 7);
+ },
+
+ /* author: http://mjijackson.com/ */
+
+ rgbToHsl: function(r, g, b) {
+
+ if (r instanceof Array) {
+ b = r[2];
+ g = r[1];
+ r = r[0];
+ }
+
+ r /= 255, g /= 255, b /= 255;
+ var max = Math.max(r, g, b),
+ min = Math.min(r, g, b);
+ var h, s, l = (max + min) / 2;
+
+ if (max == min) {
+ h = s = 0; // achromatic
+ } else {
+ var d = max - min;
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+ switch (max) {
+ case r:
+ h = (g - b) / d + (g < b ? 6 : 0);
+ break;
+ case g:
+ h = (b - r) / d + 2;
+ break;
+ case b:
+ h = (r - g) / d + 4;
+ break;
+ }
+ h /= 6;
+ }
+
+ return [h, s, l];
+ },
+
+ /* author: http://mjijackson.com/ */
+
+ hslToRgb: function(h, s, l) {
+ var r, g, b;
+
+ if (s == 0) {
+ r = g = b = l; // achromatic
+ } else {
+ function hue2rgb(p, q, t) {
+ if (t < 0) t += 1;
+ if (t > 1) t -= 1;
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
+ if (t < 1 / 2) return q;
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
+ return p;
+ }
+
+ var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+ var p = 2 * l - q;
+ r = hue2rgb(p, q, h + 1 / 3);
+ g = hue2rgb(p, q, h);
+ b = hue2rgb(p, q, h - 1 / 3);
+ }
+
+ return [r * 255 | 0, g * 255 | 0, b * 255 | 0];
+ },
+
+ rgbToHsv: function(r, g, b) {
+ if (r instanceof Array) {
+ b = r[2];
+ g = r[1];
+ r = r[0];
+ }
+
+ r = r / 255, g = g / 255, b = b / 255;
+ var max = Math.max(r, g, b),
+ min = Math.min(r, g, b);
+ var h, s, v = max;
+
+ var d = max - min;
+ s = max == 0 ? 0 : d / max;
+
+ if (max == min) {
+ h = 0; // achromatic
+ } else {
+ switch (max) {
+ case r:
+ h = (g - b) / d + (g < b ? 6 : 0);
+ break;
+ case g:
+ h = (b - r) / d + 2;
+ break;
+ case b:
+ h = (r - g) / d + 4;
+ break;
+ }
+ h /= 6;
+ }
+
+ return [h, s, v];
+ },
+
+ hsvToRgb: function(h, s, v) {
+ var r, g, b;
+
+ var i = Math.floor(h * 6);
+ var f = h * 6 - i;
+ var p = v * (1 - s);
+ var q = v * (1 - f * s);
+ var t = v * (1 - (1 - f) * s);
+
+ switch (i % 6) {
+ case 0:
+ r = v, g = t, b = p;
+ break;
+ case 1:
+ r = q, g = v, b = p;
+ break;
+ case 2:
+ r = p, g = v, b = t;
+ break;
+ case 3:
+ r = p, g = q, b = v;
+ break;
+ case 4:
+ r = t, g = p, b = v;
+ break;
+ case 5:
+ r = v, g = p, b = q;
+ break;
+ }
+
+ return [r * 255 | 0, g * 255 | 0, b * 255 | 0];
+ },
+
+ color: function() {
+ var result = new $.Color();
+ result.parse(arguments[0], arguments[1]);
+ return result;
+ },
+
+ createCanvas: function(width, height) {
+ var result = document.createElement("canvas");
+
+ if (arguments[0] instanceof Image || arguments[0] instanceof HTMLImageElement) {
+ var image = arguments[0];
+ result.width = image.width;
+ result.height = image.height;
+ result.getContext("2d").drawImage(image, 0, 0);
+ } else {
+ result.width = width;
+ result.height = height;
+ }
+
+ return result;
+ },
+
+ createImageData: function(width, height) {
+ return document.createElement("Canvas").getContext("2d").createImageData(width, height);
+ },
+
+
+ /* https://gist.github.com/3781251 */
+
+ mousePosition: function(event) {
+ var totalOffsetX = 0,
+ totalOffsetY = 0,
+ coordX = 0,
+ coordY = 0,
+ currentElement = event.target || event.srcElement,
+ mouseX = 0,
+ mouseY = 0;
+
+ // Traversing the parents to get the total offset
+ do {
+ totalOffsetX += currentElement.offsetLeft;
+ totalOffsetY += currentElement.offsetTop;
+ }
+ while ((currentElement = currentElement.offsetParent));
+ // Set the event to first touch if using touch-input
+ if (event.changedTouches && event.changedTouches[0] !== undefined) {
+ event = event.changedTouches[0];
+ }
+ // Use pageX to get the mouse coordinates
+ if (event.pageX || event.pageY) {
+ mouseX = event.pageX;
+ mouseY = event.pageY;
+ }
+ // IE8 and below doesn't support event.pageX
+ else if (event.clientX || event.clientY) {
+ mouseX = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
+ mouseY = event.clientY + document.body.scrollTop + document.documentElement.scrollTop;
+ }
+ // Subtract the offset from the mouse coordinates
+ coordX = mouseX - totalOffsetX;
+ coordY = mouseY - totalOffsetY;
+
+ return {
+ x: coordX,
+ y: coordY
+ };
+ }
+ });
+
+ $.Wrapper = function(canvas) {
+ this.context = canvas.getContext("2d");
+ this.canvas = canvas;
+ }
+
+ $.Wrapper.prototype = {
+ appendTo: function(selector) {
+ if (typeof selector === "object") {
+ var element = selector;
+ } else {
+ var element = document.querySelector(selector);
+ }
+
+ element.appendChild(this.canvas);
+
+ return this;
+ },
+
+ blendOn: function(what, mode, mix) {
+ $.blend(what, this, mode, mix);
+
+ return this;
+ },
+
+ blend: function(what, mode, mix) {
+ if (typeof what === "string") {
+ var color = what;
+ what = $($.createCanvas(this.canvas.width, this.canvas.height));
+ what.fillStyle(color).fillRect(0, 0, this.canvas.width, this.canvas.height);
+ }
+
+ $.blend(this, what, mode, mix);
+
+ return this;
+ },
+
+ circle: function(x, y, r) {
+ this.context.arc(x, y, r, 0, Math.PI * 2);
+ return this;
+ },
+
+ crop: function(x, y, w, h) {
+
+ var canvas = $.createCanvas(w, h);
+ var context = canvas.getContext("2d");
+
+ context.drawImage(this.canvas, x, y, w, h, 0, 0, w, h);
+ this.canvas.width = w;
+ this.canvas.height = h;
+ this.clear();
+ this.context.drawImage(canvas, 0, 0);
+
+ return this;
+ },
+
+ set: function(properties) {
+ $.extend(this.context, properties);
+ },
+
+ resize: function(width, height) {
+ var w = width,
+ h = height;
+
+ if (arguments.length === 1) {
+ w = arguments[0] * this.canvas.width | 0;
+ h = arguments[0] * this.canvas.height | 0;
+ } else {
+
+ if (height === null) {
+ if (this.canvas.width > width) {
+ h = this.canvas.height * (width / this.canvas.width) | 0;
+ w = width;
+ } else {
+ w = this.canvas.width;
+ h = this.canvas.height;
+ }
+ } else if (width === null) {
+ if (this.canvas.width > width) {
+ w = this.canvas.width * (height / this.canvas.height) | 0;
+ h = height;
+ } else {
+ w = this.canvas.width;
+ h = this.canvas.height;
+ }
+ }
+ }
+
+ var $resized = $(w, h).drawImage(this.canvas, 0, 0, this.canvas.width, this.canvas.height, 0, 0, w, h);
+ this.canvas = $resized.canvas;
+ this.context = $resized.context;
+
+ return this;
+ },
+
+
+ trim: function(color, changes) {
+ var transparent;
+
+ if (color) {
+ color = $.color(color).toArray();
+ transparent = !color[3];
+ } else transparent = true;
+
+ var sourceData = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
+ var sourcePixels = sourceData.data;
+
+ var bound = [this.canvas.width, this.canvas.height, 0, 0];
+
+ for (var i = 0, len = sourcePixels.length; i < len; i += 4) {
+ if (transparent) {
+ if (!sourcePixels[i + 3]) continue;
+ } else if (sourcePixels[i + 0] === color[0] && sourcePixels[i + 1] === color[1] && sourcePixels[i + 2] === color[2]) continue;
+ var x = (i / 4 | 0) % this.canvas.width | 0;
+ var y = (i / 4 | 0) / this.canvas.width | 0;
+
+ if (x < bound[0]) bound[0] = x;
+ if (x > bound[2]) bound[2] = x;
+
+ if (y < bound[1]) bound[1] = y;
+ if (y > bound[3]) bound[3] = y;
+ }
+
+ if (bound[2] === 0 || bound[3] === 0) {
+
+ } else {
+ if (changes) {
+ changes.left = bound[0];
+ changes.top = bound[1];
+ changes.width = bound[2] - bound[0];
+ changes.height = bound[3] - bound[1];
+ }
+
+ this.crop(bound[0], bound[1], bound[2] - bound[0] + 1, bound[3] - bound[1] + 1);
+ }
+
+ return this;
+ },
+
+ resizePixel: function(pixelSize) {
+
+ var sourceData = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
+ var sourcePixels = sourceData.data;
+ var canvas = document.createElement("canvas");
+ var context = canvas.context = canvas.getContext("2d");
+
+ canvas.width = this.canvas.width * pixelSize | 0;
+ canvas.height = this.canvas.height * pixelSize | 0;
+
+ for (var i = 0, len = sourcePixels.length; i < len; i += 4) {
+ if (!sourcePixels[i + 3]) continue;
+ context.fillStyle = $.rgbToHex(sourcePixels[i + 0], sourcePixels[i + 1], sourcePixels[i + 2]);
+
+ var x = (i / 4) % this.canvas.width;
+ var y = (i / 4) / this.canvas.width | 0;
+
+ context.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize);
+ }
+
+ this.canvas.width = canvas.width;
+ this.canvas.height = canvas.height;
+ this.clear().drawImage(canvas, 0, 0);
+
+ return this;
+
+ /* this very clever method is working only under Chrome */
+
+ var x = 0,
+ y = 0;
+
+ var canvas = document.createElement("canvas");
+ var context = canvas.context = canvas.getContext("2d");
+
+ canvas.width = this.canvas.width * pixelSize | 0;
+ canvas.height = this.canvas.height * pixelSize | 0;
+
+ while (x < this.canvas.width) {
+ y = 0;
+ while (y < this.canvas.height) {
+ context.drawImage(this.canvas, x, y, 1, 1, x * pixelSize, y * pixelSize, pixelSize, pixelSize);
+ y++;
+ }
+ x++;
+ }
+
+ this.canvas = canvas;
+ this.context = context;
+
+ return this;
+ },
+
+
+ matchPalette: function(palette) {
+ var imgData = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
+
+ var rgbPalette = [];
+ for (var i = 0; i < palette.length; i++) {
+ rgbPalette.push($.color(palette[i]));
+ }
+
+
+ for (var i = 0; i < imgData.data.length; i += 4) {
+ var difList = [];
+ for (var j = 0; j < rgbPalette.length; j++) {
+ var rgbVal = rgbPalette[j];
+ var rDif = Math.abs(imgData.data[i] - rgbVal[0]),
+ gDif = Math.abs(imgData.data[i + 1] - rgbVal[1]),
+ bDif = Math.abs(imgData.data[i + 2] - rgbVal[2]);
+ difList.push(rDif + gDif + bDif);
+ }
+
+ var closestMatch = 0;
+ for (var j = 0; j < palette.length; j++) {
+ if (difList[j] < difList[closestMatch]) {
+ closestMatch = j;
+ }
+ }
+
+ var paletteRgb = cq.hexToRgb(palette[closestMatch]);
+ imgData.data[i] = paletteRgb[0];
+ imgData.data[i + 1] = paletteRgb[1];
+ imgData.data[i + 2] = paletteRgb[2];
+ }
+
+ this.context.putImageData(imgData, 0, 0);
+
+ return this;
+ },
+
+ getPalette: function() {
+ var palette = [];
+ var sourceData = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
+ var sourcePixels = sourceData.data;
+
+ for (var i = 0, len = sourcePixels.length; i < len; i += 4) {
+ if (sourcePixels[i + 3]) {
+ var hex = $.rgbToHex(sourcePixels[i + 0], sourcePixels[i + 1], sourcePixels[i + 2]);
+ if (palette.indexOf(hex) === -1) palette.push(hex);
+ }
+ }
+
+ return palette;
+ },
+
+ pixelize: function(size) {
+ if (!size) return this;
+ size = size || 4;
+
+ var mozImageSmoothingEnabled = this.context.mozImageSmoothingEnabled;
+ var webkitImageSmoothingEnabled = this.context.webkitImageSmoothingEnabled;
+
+ this.context.mozImageSmoothingEnabled = false;
+ this.context.webkitImageSmoothingEnabled = false;
+
+ var scale = (this.canvas.width / size) / this.canvas.width;
+
+ var temp = cq(this.canvas.width, this.canvas.height);
+
+ temp.drawImage(this.canvas, 0, 0, this.canvas.width, this.canvas.height, 0, 0, this.canvas.width * scale | 0, this.canvas.height * scale | 0);
+ this.clear().drawImage(temp.canvas, 0, 0, this.canvas.width * scale | 0, this.canvas.height * scale | 0, 0, 0, this.canvas.width, this.canvas.height);
+
+ this.context.mozImageSmoothingEnabled = mozImageSmoothingEnabled;
+ this.context.webkitImageSmoothingEnabled = webkitImageSmoothingEnabled;
+
+ return this;
+ },
+
+ colorToMask: function(color, inverted) {
+ color = $.color(color).toArray();
+ var sourceData = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
+ var sourcePixels = sourceData.data;
+
+ var mask = [];
+
+ for (var i = 0, len = sourcePixels.length; i < len; i += 4) {
+ if (sourcePixels[i + 0] == color[0] && sourcePixels[i + 1] == color[1] && sourcePixels[i + 2] == color[2]) mask.push(inverted || false);
+ else mask.push(!inverted);
+ }
+
+ return mask;
+ },
+
+ grayscaleToMask: function(color) {
+ var sourceData = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
+ var sourcePixels = sourceData.data;
+
+ var mask = [];
+
+ for (var i = 0, len = sourcePixels.length; i < len; i += 4) {
+ mask.push((sourcePixels[i + 0] + sourcePixels[i + 1] + sourcePixels[i + 2]) / 3 | 0);
+ }
+
+ return mask;
+ },
+
+ grayscaleToAlpha: function() {
+ var sourceData = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
+ var sourcePixels = sourceData.data;
+
+ var mask = [];
+
+ for (var i = 0, len = sourcePixels.length; i < len; i += 4) {
+ sourcePixels[i + 3] = (sourcePixels[i + 0] + sourcePixels[i + 1] + sourcePixels[i + 2]) / 3 | 0;
+
+ sourcePixels[i + 0] = sourcePixels[i + 1] = sourcePixels[i + 2] = 255;
+ }
+
+ this.context.putImageData(sourceData, 0, 0);
+
+ return this;
+ },
+
+ applyMask: function(mask) {
+ var sourceData = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
+ var sourcePixels = sourceData.data;
+
+ var mode = typeof mask[0] === "boolean" ? "bool" : "byte";
+
+ for (var i = 0, len = sourcePixels.length; i < len; i += 4) {
+ var value = mask[i / 4];
+
+ if (mode === "bool") sourcePixels[i + 3] = 255 * value | 0;
+ else {
+ sourcePixels[i + 3] = value | 0;
+ }
+ }
+
+
+ this.context.putImageData(sourceData, 0, 0);
+ return this;
+ },
+
+ fillMask: function(mask) {
+
+ var sourceData = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
+ var sourcePixels = sourceData.data;
+
+ var maskType = typeof mask[0] === "boolean" ? "bool" : "byte";
+ var colorMode = arguments.length === 2 ? "normal" : "gradient";
+
+ var color = $.color(arguments[1]);
+ if (colorMode === "gradient") colorB = $.color(arguments[2]);
+
+ for (var i = 0, len = sourcePixels.length; i < len; i += 4) {
+ var value = mask[i / 4];
+
+ if (maskType === "byte") value /= 255;
+
+ if (colorMode === "normal") {
+ if (value) {
+ sourcePixels[i + 0] = color[0] | 0;
+ sourcePixels[i + 1] = color[1] | 0;
+ sourcePixels[i + 2] = color[2] | 0;
+ sourcePixels[i + 3] = value * 255 | 0;
+ }
+ } else {
+ sourcePixels[i + 0] = color[0] + (colorB[0] - color[0]) * value | 0;
+ sourcePixels[i + 1] = color[1] + (colorB[1] - color[1]) * value | 0;
+ sourcePixels[i + 2] = color[2] + (colorB[2] - color[2]) * value | 0;
+ sourcePixels[i + 3] = 255;
+ }
+ }
+
+ this.context.putImageData(sourceData, 0, 0);
+ return this;
+ },
+
+ clear: function(color) {
+ if (color) {
+ this.context.fillStyle = color;
+ this.context.fillRect(0, 0, this.canvas.width, this.canvas.height);
+ } else {
+ this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
+ }
+
+ return this;
+ },
+
+ clone: function() {
+ var result = $.createCanvas(this.canvas.width, this.canvas.height);
+ result.getContext("2d").drawImage(this.canvas, 0, 0);
+ return $(result);
+ },
+
+ fillStyle: function(fillStyle) {
+ this.context.fillStyle = fillStyle;
+ return this;
+ },
+
+ strokeStyle: function(strokeStyle) {
+ this.context.strokeStyle = strokeStyle;
+ return this;
+ },
+
+ gradientText: function(text, x, y, maxWidth, gradient) {
+
+ var words = text.split(" ");
+
+ var h = this.font().match(/\d+/g)[0] * 2;
+
+ var ox = 0;
+ var oy = 0;
+
+ if (maxWidth) {
+ var line = 0;
+ var lines = [""];
+
+ for (var i = 0; i < words.length; i++) {
+ var word = words[i] + " ";
+ var wordWidth = this.context.measureText(word).width;
+
+ if (ox + wordWidth > maxWidth) {
+ lines[++line] = "";
+ ox = 0;
+ }
+
+ lines[line] += word;
+
+ ox += wordWidth;
+ }
+ } else var lines = [text];
+
+ for (var i = 0; i < lines.length; i++) {
+ var oy = y + i * h * 0.6 | 0;
+ var lingrad = this.context.createLinearGradient(0, oy, 0, oy + h * 0.6 | 0);
+
+ for (var j = 0; j < gradient.length; j += 2) {
+ lingrad.addColorStop(gradient[j], gradient[j + 1]);
+ }
+
+ var text = lines[i];
+
+ this.fillStyle(lingrad).fillText(text, x, oy);
+ }
+
+ return this;
+ },
+
+ setHsl: function() {
+
+ if (arguments.length === 1) {
+ var args = arguments[0];
+ } else {
+ var args = arguments;
+ }
+
+ var data = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
+ var pixels = data.data;
+ var r, g, b, a, h, s, l, hsl = [],
+ newPixel = [];
+
+ for (var i = 0, len = pixels.length; i < len; i += 4) {
+ hsl = $.rgbToHsl(pixels[i + 0], pixels[i + 1], pixels[i + 2]);
+
+ h = args[0] === null ? hsl[0] : $.limitValue(args[0], 0, 1);
+ s = args[1] === null ? hsl[1] : $.limitValue(args[1], 0, 1);
+ l = args[2] === null ? hsl[2] : $.limitValue(args[2], 0, 1);
+
+ newPixel = $.hslToRgb(h, s, l);
+
+ pixels[i + 0] = newPixel[0];
+ pixels[i + 1] = newPixel[1];
+ pixels[i + 2] = newPixel[2];
+ }
+
+ this.context.putImageData(data, 0, 0);
+
+ return this;
+ },
+
+ shiftHsl: function() {
+
+ if (arguments.length === 1) {
+ var args = arguments[0];
+ } else {
+ var args = arguments;
+ }
+
+ var data = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
+ var pixels = data.data;
+ var r, g, b, a, h, s, l, hsl = [],
+ newPixel = [];
+
+ for (var i = 0, len = pixels.length; i < len; i += 4) {
+ hsl = $.rgbToHsl(pixels[i + 0], pixels[i + 1], pixels[i + 2]);
+
+ h = args[0] === null ? hsl[0] : $.wrapValue(hsl[0] + args[0], 0, 1);
+ s = args[1] === null ? hsl[1] : $.limitValue(hsl[1] + args[1], 0, 1);
+ l = args[2] === null ? hsl[2] : $.limitValue(hsl[2] + args[2], 0, 1);
+
+ newPixel = $.hslToRgb(h, s, l);
+
+ pixels[i + 0] = newPixel[0];
+ pixels[i + 1] = newPixel[1];
+ pixels[i + 2] = newPixel[2];
+ }
+
+
+ this.context.putImageData(data, 0, 0);
+
+ return this;
+ },
+
+ replaceHue: function(src, dst) {
+
+ var data = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
+ var pixels = data.data;
+ var r, g, b, a, h, s, l, hsl = [],
+ newPixel = [];
+
+ for (var i = 0, len = pixels.length; i < len; i += 4) {
+ hsl = $.rgbToHsl(pixels[i + 0], pixels[i + 1], pixels[i + 2]);
+
+ if (Math.abs(hsl[0] - src) < 0.05) h = $.wrapValue(dst, 0, 1);
+ else h = hsl[0];
+
+ newPixel = $.hslToRgb(h, hsl[1], hsl[2]);
+
+ pixels[i + 0] = newPixel[0];
+ pixels[i + 1] = newPixel[1];
+ pixels[i + 2] = newPixel[2];
+ }
+
+ this.context.putImageData(data, 0, 0);
+
+ return this;
+ },
+
+ invert: function(src, dst) {
+
+ var data = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
+ var pixels = data.data;
+ var r, g, b, a, h, s, l, hsl = [],
+ newPixel = [];
+
+ for (var i = 0, len = pixels.length; i < len; i += 4) {
+ pixels[i + 0] = 255 - pixels[i + 0];
+ pixels[i + 1] = 255 - pixels[i + 1];
+ pixels[i + 2] = 255 - pixels[i + 2];
+ }
+
+ this.context.putImageData(data, 0, 0);
+
+ return this;
+ },
+
+ roundRect: function(x, y, width, height, radius) {
+
+ this.beginPath();
+ this.moveTo(x + radius, y);
+ this.lineTo(x + width - radius, y);
+ this.quadraticCurveTo(x + width, y, x + width, y + radius);
+ this.lineTo(x + width, y + height - radius);
+ this.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
+ this.lineTo(x + radius, y + height);
+ this.quadraticCurveTo(x, y + height, x, y + height - radius);
+ this.lineTo(x, y + radius);
+ this.quadraticCurveTo(x, y, x + radius, y);
+ this.closePath();
+
+ return this;
+ },
+
+ wrappedText: function(text, x, y, maxWidth, newlineCallback) {
+
+ var lines = text instanceof Array ? text : this.flowText(text, maxWidth);
+ var h = this.font().match(/\d+/g)[0] * 2;
+
+ var ox = 0;
+ var oy = 0;
+
+ if (maxWidth) {
+ var line = 0;
+ var lines = [""];
+
+ for (var i = 0; i < words.length; i++) {
+ var word = words[i] + " ";
+ var wordWidth = this.context.measureText(word).width;
+
+ if (ox + wordWidth > maxWidth) {
+ lines[++line] = "";
+ ox = 0;
+ }
+
+ lines[line] += word;
+
+ ox += wordWidth;
+ }
+ } else {
+ var lines = [text];
+ }
+
+ for (var i = 0; i < lines.length; i++) {
+ var oy = y + i * h * 0.6 | 0;
+
+ var text = lines[i];
+
+ if (newlineCallback) newlineCallback.call(this, x, y + oy);
+
+ this.fillText(text, x, oy);
+ }
+
+ return this;
+
+ },
+
+ flowText: function(text, maxWidth) {
+ var words = text.split(/(\s)/);
+
+ var ox = 0;
+ var oy = 0;
+
+ if (maxWidth) {
+ var line = 0;
+ var lines = [""];
+
+ var spaceWidth = this.context.measureText(" ").width;
+
+ for(var i = 0; i < words.length; i++) {
+
+ var word = words[i];
+
+ if (word == "\n") {
+ lines[line] = lines[line].replace(/\s+$/,"");
+ lines[++line] = "";
+ ox = 0;
+ continue;
+ }
+
+ else if (word == " ") {
+ ox += spaceWidth;
+ if (ox > maxWidth) {
+ lines[line] = lines[line].replace(/\s+$/,"");
+ lines[++line] = "";
+ ox = 0;
+ }
+ else {
+ lines[line] += " ";
+ }
+ }
+
+ else {
+ var wordWidth = this.context.measureText(word).width;
+
+ ox += wordWidth;
+
+ if(ox > maxWidth && lines[line].length) {
+ lines[line] = lines[line].replace(/\s+$/,"");
+ lines[++line] = "";
+ ox = wordWidth;
+ }
+
+ lines[line] += word;
+ }
+ }
+ } else {
+ var lines = [text];
+ }
+
+ return lines;
+
+ },
+
+ textBoundaries: function(text, maxWidth) {
+
+ var lines = this.flowText(text, maxWidth || Infinity);
+
+ var h = this.font().match(/\d+/g)[0] * 2;
+
+ var w = 0;
+ for (var i = 0; i < lines.length; i++) {
+ w = Math.max( w, this.measureText(lines[i]).width );
+ }
+
+ return {
+ height: lines.length * h * 0.6 | 0,
+ width: w,
+ lines: lines
+ }
+ },
+
+ paperBag: function(x, y, width, height, blowX, blowY) {
+ var lx, ly;
+ this.beginPath();
+ this.moveTo(x, y);
+ this.quadraticCurveTo(x + width / 2 | 0, y + height * blowY | 0, x + width, y);
+ this.quadraticCurveTo(x + width - width * blowX | 0, y + height / 2 | 0, x + width, y + height);
+ this.quadraticCurveTo(x + width / 2 | 0, y + height - height * blowY | 0, x, y + height);
+ this.quadraticCurveTo(x + width * blowX | 0, y + height / 2 | 0, x, y);
+ },
+
+ borderImage: function(image, x, y, w, h, t, r, b, l, fill) {
+
+ /* top */
+ this.drawImage(image, l, 0, image.width - l - r, t, x + l, y, w - l - r, t);
+
+ /* bottom */
+ this.drawImage(image, l, image.height - b, image.width - l - r, b, x + l, y + h - b, w - l - r, b);
+
+ /* left */
+ this.drawImage(image, 0, t, l, image.height - b - t, x, y + t, l, h - b - t);
+
+ /* right */
+ this.drawImage(image, image.width - r, t, r, image.height - b - t, x + w - r, y + t, r, h - b - t);
+
+ /* top-left */
+ this.drawImage(image, 0, 0, l, t, x, y, l, t);
+
+ /* top-right */
+ this.drawImage(image, image.width - r, 0, r, t, x + w - r, y, r, t);
+
+ /* bottom-right */
+ this.drawImage(image, image.width - r, image.height - b, r, b, x + w - r, y + h - b, r, b);
+
+ /* bottom-left */
+ this.drawImage(image, 0, image.height - b, l, b, x, y + h - b, l, b);
+
+ if (fill) {
+ if (typeof fill === "string") {
+ this.fillStyle(fill).fillRect(x + l, y + t, w - l - r, h - t - b);
+ } else {
+ this.drawImage(image, l, t, image.width - r - l, image.height - b - t, x + l, y + t, w - l - r, h - t - b);
+ }
+ }
+ },
+
+ /* www.html5rocks.com/en/tutorials/canvas/imagefilters/ */
+
+ convolve: function(matrix, mix, divide) {
+
+ if (typeof divide === "undefined") divide = 1;
+ if (typeof mix === "undefined") mix = 1;
+
+ var sourceData = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
+ var matrixSize = Math.sqrt(matrix.length) + 0.5 | 0;
+ var halfMatrixSize = matrixSize / 2 | 0;
+ var src = sourceData.data;
+ var sw = sourceData.width;
+ var sh = sourceData.height;
+ var w = sw;
+ var h = sh;
+ var output = $.createImageData(this.canvas.width, this.canvas.height);
+ var dst = output.data;
+
+ for (var y = 1; y < h - 1; y++) {
+ for (var x = 1; x < w - 1; x++) {
+
+ var dstOff = (y * w + x) * 4;
+ var r = 0,
+ g = 0,
+ b = 0,
+ a = 0;
+
+ for (var cy = 0; cy < matrixSize; cy++) {
+ for (var cx = 0; cx < matrixSize; cx++) {
+ var scy = y + cy - halfMatrixSize;
+ var scx = x + cx - halfMatrixSize;
+ if (scy >= 0 && scy < sh && scx >= 0 && scx < sw) {
+ var srcOff = (scy * sw + scx) * 4;
+
+ var wt = matrix[cy * matrixSize + cx] / divide;
+
+ r += src[srcOff + 0] * wt;
+ g += src[srcOff + 1] * wt;
+ b += src[srcOff + 2] * wt;
+ a += src[srcOff + 3] * wt;
+ }
+ }
+ }
+
+ dst[dstOff + 0] = $.mix(src[dstOff + 0], r, mix);
+ dst[dstOff + 1] = $.mix(src[dstOff + 1], g, mix);
+ dst[dstOff + 2] = $.mix(src[dstOff + 2], b, mix);
+ dst[dstOff + 3] = a;
+ // src[dstOff + 3];
+ }
+ }
+ },
+
+ blur: function(mix) {
+ return this.convolve([1, 1, 1, 1, 1, 1, 1, 1, 1], mix, 9);
+ },
+
+ gaussianBlur: function(mix) {
+ return this.convolve([0.00000067, 0.00002292, 0.00019117, 0.00038771, 0.00019117, 0.00002292, 0.00000067, 0.00002292, 0.00078633, 0.00655965, 0.01330373, 0.00655965, 0.00078633, 0.00002292, 0.00019117, 0.00655965, 0.05472157, 0.11098164, 0.05472157, 0.00655965, 0.00019117, 0.00038771, 0.01330373, 0.11098164, 0.22508352, 0.11098164, 0.01330373, 0.00038771, 0.00019117, 0.00655965, 0.05472157, 0.11098164, 0.05472157, 0.00655965, 0.00019117, 0.00002292, 0.00078633, 0.00655965, 0.01330373, 0.00655965, 0.00078633, 0.00002292, 0.00000067, 0.00002292, 0.00019117, 0.00038771, 0.00019117, 0.00002292, 0.00000067], mix, 1);
+ },
+
+ sharpen: function(mix) {
+ return this.convolve([0, -1, 0, -1, 5, -1, 0, -1, 0], mix);
+ },
+
+ threshold: function(threshold) {
+ var data = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
+ var pixels = data.data;
+ var r, g, b;
+
+ for (var i = 0; i < pixels.length; i += 4) {
+ var r = pixels[i];
+ var g = pixels[i + 1];
+ var b = pixels[i + 2];
+ var v = (0.2126 * r + 0.7152 * g + 0.0722 * b >= threshold) ? 255 : 0;
+ pixels[i] = pixels[i + 1] = pixels[i + 2] = v
+ }
+
+ this.context.putImageData(data, 0, 0);
+
+ return this;
+ },
+
+ sepia: function() {
+ var data = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
+ var pixels = data.data;
+ var r, g, b;
+
+ for (var i = 0; i < pixels.length; i += 4) {
+ pixels[i + 0] = $.limitValue((pixels[i + 0] * .393) + (pixels[i + 1] * .769) + (pixels[i + 2] * .189), 0, 255);
+ pixels[i + 1] = $.limitValue((pixels[i + 0] * .349) + (pixels[i + 1] * .686) + (pixels[i + 2] * .168), 0, 255);
+ pixels[i + 2] = $.limitValue((pixels[i + 0] * .272) + (pixels[i + 1] * .534) + (pixels[i + 2] * .131), 0, 255);
+ }
+
+ this.context.putImageData(data, 0, 0);
+
+ return this;
+ },
+
+ measureText: function() {
+ return this.context.measureText.apply(this.context, arguments);
+ },
+
+ createRadialGradient: function() {
+ return this.context.createRadialGradient.apply(this.context, arguments);
+ },
+
+ createLinearGradient: function() {
+ return this.context.createLinearGradient.apply(this.context, arguments);
+ },
+
+ getImageData: function() {
+ return this.context.getImageData.apply(this.context, arguments);
+ },
+
+ /* framework */
+
+ framework: function(args, context) {
+ if (context) {
+ this.tempContext = context === true ? args : context;
+ }
+
+ for (var name in args) {
+ if (this[name]) this[name](args[name], undefined, undefined);
+ }
+
+ this.tempContext = null;
+
+ return this;
+ },
+
+ onStep: function(callback, interval) {
+ var self = this.tempContext || this;
+ var lastTick = Date.now();
+ interval = interval || 25;
+
+ this.timer = setInterval(function() {
+ var delta = Date.now() - lastTick;
+ lastTick = Date.now();
+ callback.call(self, delta, lastTick);
+ }, interval);
+
+ return this;
+ },
+
+ onRender: function(callback) {
+ var self = this.tempContext || this;
+
+ var lastTick = Date.now();
+
+ function step() {
+ var delta = Date.now() - lastTick;
+ lastTick = Date.now();
+ requestAnimationFrame(step)
+ callback.call(self, delta, lastTick);
+ };
+
+ requestAnimationFrame(step);
+
+ return this;
+ },
+
+ onMouseMove: function(callback) {
+ var self = this.tempContext || this;
+
+ if (!MOBILE) this.canvas.addEventListener("mousemove", function(e) {
+ var pos = $.mousePosition(e);
+ callback.call(self, pos.x, pos.y);
+ });
+
+ else this.canvas.addEventListener("touchmove", function(e) {
+ e.preventDefault();
+ var pos = $.mousePosition(e);
+ callback.call(self, pos.x, pos.y);
+ });
+
+ return this;
+ },
+
+ onMouseDown: function(callback) {
+ var self = this.tempContext || this;
+
+ if (!MOBILE) {
+ this.canvas.addEventListener("mousedown", function(e) {
+ var pos = $.mousePosition(e);
+ callback.call(self, pos.x, pos.y, e.button);
+ });
+ } else {
+ this.canvas.addEventListener("touchstart", function(e) {
+ var pos = $.mousePosition(e);
+ callback.call(self, pos.x, pos.y, e.button);
+ });
+ }
+
+ return this;
+ },
+
+ onMouseUp: function(callback) {
+ var self = this.tempContext || this;
+
+ if (!MOBILE) {
+ this.canvas.addEventListener("mouseup", function(e) {
+ var pos = $.mousePosition(e);
+ callback.call(self, pos.x, pos.y, e.button);
+ });
+ } else {
+ this.canvas.addEventListener("touchend", function(e) {
+ var pos = $.mousePosition(e);
+ callback.call(self, pos.x, pos.y, e.button);
+ });
+ }
+
+ return this;
+ },
+
+
+ onSwipe: function(callback, threshold, timeout) {
+ var self = this.tempContext || this;
+
+ var swipeThr = threshold || 35;
+ var swipeTim = timeout || 350;
+
+ var swipeSP = 0;
+ var swipeST = 0;
+ var swipeEP = 0;
+ var swipeET = 0;
+
+ function swipeStart(e) {
+ e.preventDefault();
+ swipeSP = $.mousePosition(e);
+ swipeST = Date.now();
+ }
+
+ function swipeUpdate(e) {
+ e.preventDefault();
+ swipeEP = $.mousePosition(e);
+ swipeET = Date.now();
+ }
+
+ function swipeEnd(e) {
+ e.preventDefault();
+
+ var xDif = (swipeSP.x - swipeEP.x);
+ var yDif = (swipeSP.y - swipeEP.y);
+ var x = (xDif * xDif);
+ var y = (yDif * yDif);
+ var swipeDist = Math.sqrt(x + y);
+ var swipeTime = (swipeET - swipeST);
+ var swipeDir = undefined;
+
+ if (swipeDist > swipeThr && swipeTime < swipeTim) {
+ if (Math.abs(xDif) > Math.abs(yDif)) {
+ if (xDif > 0) {
+ swipeDir = "left";
+ } else {
+ swipeDir = "right";
+ }
+ } else {
+ if (yDif > 0) {
+ swipeDir = "up";
+ } else {
+ swipeDir = "down";
+ }
+ }
+ callback.call(self, swipeDir);
+ }
+ }
+
+ this.canvas.addEventListener("touchstart", function(e) {
+ swipeStart(e);
+ });
+ this.canvas.addEventListener("touchmove", function(e) {
+ swipeUpdate(e);
+ });
+ this.canvas.addEventListener("touchend", function(e) {
+ swipeEnd(e);
+ });
+ this.canvas.addEventListener("mousedown", function(e) {
+ swipeStart(e);
+ });
+ this.canvas.addEventListener("mousemove", function(e) {
+ swipeUpdate(e);
+ });
+ this.canvas.addEventListener("mouseup", function(e) {
+ swipeEnd(e);
+ });
+
+ return this;
+ },
+
+ onKeyDown: function(callback) {
+ var self = this.tempContext || this;
+
+ document.addEventListener("keydown", function(e) {
+ if (e.which >= 48 && e.which <= 90) var keyName = String.fromCharCode(e.which).toLowerCase();
+ else var keyName = $.keycodes[e.which];
+ callback.call(self, keyName);
+ });
+ return this;
+ },
+
+ onKeyUp: function(callback) {
+ var self = this.tempContext || this;
+
+ document.addEventListener("keyup", function(e) {
+ if (e.which >= 48 && e.which <= 90) var keyName = String.fromCharCode(e.which).toLowerCase();
+ else var keyName = $.keycodes[e.which];
+ callback.call(self, keyName);
+ });
+ return this;
+ },
+
+
+ onResize: function(callback) {
+ var self = this.tempContext || this;
+
+ window.addEventListener("resize", function() {
+ callback.call(self, window.innerWidth, window.innerHeight);
+ });
+
+ callback.call(self, window.innerWidth, window.innerHeight);
+
+ return this;
+ },
+
+ onDropImage: function(callback) {
+ var self = this.tempContext || this;
+
+ document.addEventListener('drop', function(e) {
+ e.stopPropagation();
+ e.preventDefault();
+
+ var file = e.dataTransfer.files[0];
+
+ if (!(/image/i).test(file.type)) return false;
+ var reader = new FileReader();
+
+ reader.onload = function(e) {
+ var image = new Image;
+
+ image.onload = function() {
+ callback.call(self, this);
+ };
+
+ image.src = e.target.result;
+ };
+
+ reader.readAsDataURL(file);
+
+ });
+
+ document.addEventListener("dragover", function(e) {
+ e.preventDefault();
+ });
+
+ return this;
+ }
+
+ };
+
+ /* extend wrapper with drawing context methods */
+
+ var methods = ["arc", "arcTo", "beginPath", "bezierCurveTo", "clearRect", "clip", "closePath", "createImageData", "createLinearGradient", "createRadialGradient", "createPattern", "drawFocusRing", "drawImage", "fill", "fillRect", "fillText", "getImageData", "isPointInPath", "lineTo", "measureText", "moveTo", "putImageData", "quadraticCurveTo", "rect", "restore", "rotate", "save", "scale", "setTransform", "stroke", "strokeRect", "strokeText", "transform", "translate"];
+ for (var i = 0; i < methods.length; i++) {
+ var name = methods[i];
+ if (!$.Wrapper.prototype[name]) $.Wrapper.prototype[name] = Function("this.context." + name + ".apply(this.context, arguments); return this;");
+ };
+
+ /* create setters and getters */
+
+ var properties = ["canvas", "fillStyle", "font", "globalAlpha", "globalCompositeOperation", "lineCap", "lineJoin", "lineWidth", "miterLimit", "shadowOffsetX", "shadowOffsetY", "shadowBlur", "shadowColor", "strokeStyle", "textAlign", "textBaseline"];
+ for (var i = 0; i < properties.length; i++) {
+ var name = properties[i];
+ if (!$.Wrapper.prototype[name]) $.Wrapper.prototype[name] = Function("if(arguments.length) { this.context." + name + " = arguments[0]; return this; } else { return this.context." + name + "; }");
+ };
+
+ /* color */
+
+ $.Color = function(data, type) {
+ if (arguments.length) this.parse(data);
+ }
+
+ $.Color.prototype = {
+ parse: function(args, type) {
+ if (args[0] instanceof $.Color) {
+ this[0] = args[0][0];
+ this[1] = args[0][1];
+ this[2] = args[0][2];
+ this[3] = args[0][3];
+ return;
+ }
+
+ if (typeof args === "string") {
+ var match = null;
+
+ if (args[0] === "#") {
+ var rgb = $.hexToRgb(args);
+ this[0] = rgb[0];
+ this[1] = rgb[1];
+ this[2] = rgb[2];
+ this[3] = 1.0;
+ } else if (match = args.match(/rgb\((.*),(.*),(.*)\)/)) {
+ this[0] = match[1] | 0;
+ this[1] = match[2] | 0;
+ this[2] = match[3] | 0;
+ this[3] = 1.0;
+ } else if (match = args.match(/rgba\((.*),(.*),(.*)\)/)) {
+ this[0] = match[1] | 0;
+ this[1] = match[2] | 0;
+ this[2] = match[3] | 0;
+ this[3] = match[4] | 0;
+ } else if (match = args.match(/hsl\((.*),(.*),(.*)\)/)) {
+ this.fromHsl(match[1], match[2], match[3]);
+ } else if (match = args.match(/hsv\((.*),(.*),(.*)\)/)) {
+ this.fromHsv(match[1], match[2], match[3]);
+ }
+ } else {
+ switch (type) {
+ case "hsl":
+ case "hsla":
+
+ this.fromHsl(args[0], args[1], args[2], args[3]);
+ break;
+
+ case "hsv":
+ case "hsva":
+
+ this.fromHsv(args[0], args[1], args[2], args[3]);
+ break;
+
+ default:
+ this[0] = args[0];
+ this[1] = args[1];
+ this[2] = args[2];
+ this[3] = typeof args[3] === "undefined" ? 1.0 : args[3];
+ break;
+ }
+ }
+ },
+
+ fromHsl: function() {
+ var components = arguments[0] instanceof Array ? arguments[0] : arguments;
+ var color = $.hslToRgb(components[0], components[1], components[2]);
+
+ this[0] = color[0];
+ this[1] = color[1];
+ this[2] = color[2];
+ this[3] = typeof arguments[3] === "undefined" ? 1.0 : arguments[3];
+ },
+
+ fromHsv: function() {
+ var components = arguments[0] instanceof Array ? arguments[0] : arguments;
+ var color = $.hsvToRgb(components[0], components[1], components[2]);
+
+ this[0] = color[0];
+ this[1] = color[1];
+ this[2] = color[2];
+ this[3] = typeof arguments[3] === "undefined" ? 1.0 : arguments[3];
+ },
+
+ toArray: function() {
+ return [this[0], this[1], this[2], this[3]];
+ },
+
+ toRgb: function() {
+ return "rgb(" + this[0] + ", " + this[1] + ", " + this[2] + ")";
+ },
+
+ toRgba: function() {
+ return "rgba(" + this[0] + ", " + this[1] + ", " + this[2] + ", " + this[3] + ")";
+ },
+
+ toHex: function() {
+ return $.rgbToHex(this[0], this[1], this[2]);
+ },
+
+ toHsl: function() {
+ var c = $.rgbToHsl(this[0], this[1], this[2]);
+ c[3] = this[3];
+ return c;
+ },
+
+ toHsv: function() {
+ var c = $.rgbToHsv(this[0], this[1], this[2]);
+ c[3] = this[3];
+ return c;
+ },
+
+ gradient: function(target, steps) {
+ var targetColor = $.color(target);
+ },
+
+ shiftHsl: function() {
+ var hsl = this.toHsl();
+
+ var h = arguments[0] === null ? hsl[0] : $.wrapValue(hsl[0] + arguments[0], 0, 1);
+ var s = arguments[1] === null ? hsl[1] : $.limitValue(hsl[1] + arguments[1], 0, 1);
+ var l = arguments[2] === null ? hsl[2] : $.limitValue(hsl[2] + arguments[2], 0, 1);
+
+ this.fromHsl(h, s, l);
+
+ return this;
+ },
+
+ setHsl: function() {
+ var hsl = this.toHsl();
+
+ var h = arguments[0] === null ? hsl[0] : $.limitValue(arguments[0], 0, 1);
+ var s = arguments[1] === null ? hsl[1] : $.limitValue(arguments[1], 0, 1);
+ var l = arguments[2] === null ? hsl[2] : $.limitValue(arguments[2], 0, 1);
+
+ this.fromHsl(h, s, l);
+
+ return this;
+ }
+
+ };
+
+ window["cq"] = window["CanvasQuery"] = $;
+
+ if (typeof define === "function" && define.amd) {
+ define([], function() {
+ return $;
+ });
+ }
+
+})(window);
diff --git a/dataUriToBlob.js b/dataUriToBlob.js
new file mode 100644
index 0000000..582aecb
--- /dev/null
+++ b/dataUriToBlob.js
@@ -0,0 +1,47 @@
+window.dataUriToBlob = (function(){
+/**
+ * Blob constructor.
+ */
+
+var Blob = window.Blob;
+
+/**
+ * ArrayBufferView support.
+ */
+
+var hasArrayBufferView = new Blob([new Uint8Array(100)]).size == 100;
+
+/**
+ * Return a `Blob` for the given data `uri`.
+ *
+ * @param {String} uri
+ * @return {Blob}
+ * @api public
+ */
+
+var dataUriToBlob = function(uri){
+ var data = uri.split(',')[1];
+ var bytes = atob(data);
+ var buf = new ArrayBuffer(bytes.length);
+ var arr = new Uint8Array(buf);
+ for (var i = 0; i < bytes.length; i++) {
+ arr[i] = bytes.charCodeAt(i);
+ }
+
+ if (!hasArrayBufferView) arr = buf;
+ var blob = new Blob([arr], { type: mime(uri) });
+ blob.slice = blob.slice || blob.webkitSlice;
+ return blob;
+};
+
+/**
+ * Return data uri mime type.
+ */
+
+function mime(uri) {
+ return uri.split(';')[0].slice(5);
+}
+
+return dataUriToBlob;
+
+})()
diff --git a/gif-animate.html b/gif-animate.html
new file mode 100644
index 0000000..6681aff
--- /dev/null
+++ b/gif-animate.html
@@ -0,0 +1,108 @@
+<!doctype html>
+<html>
+<head>
+<title>Gifs</title>
+</head>
+<body>
+<div>
+<button id="random">random</button>
+<button id="pattern2">pattern2</button>
+<button id="pattern3">pattern3</button>
+<button id="pattern4">pattern4</button>
+<button id="pattern2Lite">pattern2lite</button>
+<button id="pattern3Lite">pattern3lite</button>
+<button id="pattern4Lite">pattern4lite</button>
+<button id="floydSteinberg">floyd-steinberg</button>
+<button id="right">right</button>
+<input type="text" id="url" value="http://s3.amazonaws.com/feellondon/uploads/1.gif">
+<div id="loading">loading...</div>
+</div>
+<div id="images"></div>
+</body>
+<script type="text/javascript" src="gif.js"></script>
+<script type="text/javascript" src="canvasquery.js"></script>
+<script type="text/javascript" src="canvasquery.dither.js"></script>
+<script type="text/javascript">
+
+document.getElementById("url").onchange = load
+load()
+
+function load(imageURL) {
+ document.getElementById("loading").style.display = "inline"
+ var imageURL = proxify( document.getElementById("url").value );
+
+ if (imageURL.substr(-3) === "gif") {
+ window.gif = GIF(imageURL);
+ // gif.on("error", tryToLoadNextImage);
+ // gif.on("rendered", trackLoadTime);
+ gif.on("rendered", ready);
+ return gif.render();
+ } else {
+ window.img = new Image();
+ // img.addEventListener("error", tryToLoadNextImage);
+ img.addEventListener("load", ready);
+ img.crossOrigin = "anonymous";
+ return img.src = imageURL;
+ }
+}
+
+function giveImage() {
+ if (imageURL.substr(-3) === "gif") {
+ return gif.frames[gif.currentFrame()].ctx.canvas;
+ } else {
+ return img;
+ }
+}
+
+function proxify (url) {
+ return "/cgi-bin/proxy?" + url // .replace(/^https?:\/\//, "");
+};
+
+
+function ready(){
+ document.getElementById("loading").style.display = "none"
+ var buttons = document.getElementsByTagName("button")
+ for (var i = 0; i < buttons.length; i++) {
+ (function(n){
+ buttons[n].onclick = function(){
+ algo = buttons[n].id;
+ build()
+ }
+ })(i)
+ }
+ build()
+ animate()
+}
+
+var algo = "random"
+var frames;
+var viewer;
+function build(){
+ frames = []
+
+ document.getElementById("images").innerHTML = ""
+ gif.frames.forEach(function(frame){ dither(frame) })
+
+ canvas = gif.frames[0].ctx.canvas
+ var w = canvas.width, h = canvas.height;
+ viewer = cq(w, h)
+ viewer.appendTo("#images")
+}
+function dither(frame){
+ var canvas = frame.ctx.canvas
+ var w = canvas.width, h = canvas.height;
+ var cc = cq(w, h)
+ cc.drawImage(canvas, 0, 0, w, h);
+ cc[algo + "Dither"]( )
+ frames.push(cc)
+}
+
+var raf_id;
+function animate(){
+ raf_id = requestAnimationFrame(animate)
+ var canvas = frames[gif.currentFrame()].canvas
+ viewer.drawImage(canvas, 0, 0, canvas.width, canvas.height)
+}
+</script>
+</html>
+
diff --git a/gif-dither.html b/gif-dither.html
new file mode 100644
index 0000000..3421588
--- /dev/null
+++ b/gif-dither.html
@@ -0,0 +1,195 @@
+<!doctype html>
+<html>
+<head>
+<title>Gif Dither</title>
+</head>
+<body>
+<div>
+<!-- <button id="random">random</button> -->
+<button id="pattern2Lite">pattern2lite</button>
+<button id="pattern3Lite">pattern3lite</button>
+<button id="pattern4Lite">pattern4lite</button>
+<button id="floydSteinberg">floyd-steinberg</button>
+<button id="pattern4">pattern4</button>
+<button id="pattern3">pattern3</button>
+<!-- <button id="right">right</button> -->
+<input type="text" id="url" value="1376516658960-dumpfm-DoritoWitch-TimeFLyTrans0001.png">
+<button id="encode">ENCODE</button>
+<button id="save">SAVE</button>
+<span id="loading"></span>
+</div>
+<div id="images"></div>
+</body>
+<script type="text/javascript" src="gif-encode/util.js"></script>
+<script type="text/javascript" src="gif-encode/tube.js"></script>
+<script type="text/javascript" src="gif-encode/client.js"></script>
+<script type="text/javascript" src="gif.js"></script>
+<script type="text/javascript" src="FileSaver.js"></script>
+<script type="text/javascript" src="dataUriToBlob.js"></script>
+<script type="text/javascript" src="canvasquery.js"></script>
+<script type="text/javascript" src="canvasquery.dither.js"></script>
+<script type="text/javascript">
+
+document.getElementById("url").onchange = load
+setTimeout(init)
+
+function init () {
+ bind()
+// load()
+}
+
+function status (s){
+ var el = document.getElementById("loading")
+ el.innerHTML = s + "..."
+}
+
+function load(imageURL) {
+ document.getElementById("save").style.display = "none"
+ document.getElementById("encode").style.display = "none"
+ status("loading")
+ var imageURL = proxify( document.getElementById("url").value );
+
+ window.gif = window.img = null
+
+ if (imageURL.substr(-3) === "gif") {
+ window.gif = GIF(imageURL);
+ // gif.on("error", tryToLoadNextImage);
+ // gif.on("rendered", trackLoadTime);
+ gif.on("rendered", ready);
+ return gif.render();
+ } else {
+ window.img = new Image();
+ // img.addEventListener("error", tryToLoadNextImage);
+ img.addEventListener("load", ready);
+ img.crossOrigin = "anonymous";
+ return img.src = imageURL;
+ }
+}
+
+function giveImage() {
+ if (imageURL.substr(-3) === "gif") {
+ return gif.frames[gif.currentFrame()].ctx.canvas;
+ } else {
+ return img;
+ }
+}
+
+function proxify (url) {
+ if (url.indexOf("http") == 0)
+ return "/cgi-bin/proxy?" + url // .replace(/^https?:\/\//, "");
+ else
+ return url
+};
+
+
+function bind(){
+ var buttons = document.getElementsByTagName("button")
+ for (var i = 0; i < buttons.length; i++) {
+ (function(n){
+ buttons[n].onclick = function(){
+ algo = buttons[n].id;
+ build()
+ }
+ })(i)
+ }
+ document.getElementById("save").onclick = save
+ document.getElementById("encode").onclick = encode
+}
+
+var encoder = new GifEncoder()
+
+encoder.on("quantized", function(url){
+ status("encoding")
+ encoder.encode()
+})
+
+encoder.on("rendered-url", function(url){
+ status("")
+ var image = new Image ()
+ image.src = url
+ document.body.appendChild(image)
+ lastGif = url
+ document.getElementById("save").style.display = "inline"
+})
+
+/*
+var lastGif;
+encoder.on("rendered", function(bits){
+ status("")
+ lastGif = bits
+ document.getElementById("save").style.display = "inline"
+})
+*/
+
+function save (){
+ if (!lastGif) return;
+ var filename = document.getElementById("url").value.replace(/^.*\//,"").replace(/\.gif.*$/,"")
+ var blob = dataUriToBlob(lastGif); // new Blob ([lastGif], {type: "image/gif"});
+ saveAs(blob, filename + "-" + algo + "-" + (+new Date() % 1000) + ".gif");
+}
+
+function ready(){
+ if (window.gif) gif.off("rendered", ready)
+ status("")
+ document.getElementById("save").style.display = "none"
+ document.getElementById("encode").style.display = "inline"
+ build()
+}
+
+var algo = "random"
+var frames = []
+function build(){
+ status("dithering")
+ encoder.resetFrames()
+
+ document.getElementById("images").innerHTML = ""
+ if (window.gif) {
+ var buttons = document.getElementsByTagName("button")
+ var bz = []
+ for (var i = 0; i < buttons.length; i++) {
+ if (buttons[i].id != "save" && buttons[i].id != "encode") bz.push(buttons[i].id)
+ }
+ zz = bz
+ gif.frames.forEach(function(frame, i){
+ var algo = bz[i % bz.length]
+ var cc = dither(frame)
+ encoder.addFrame(cc.canvas)
+ })
+ }
+ else {
+ var frame = { ctx: { canvas: window.img } };
+ var buttons = document.getElementsByTagName("button")
+ for (var i = 0; i < buttons.length; i++) {
+ if (buttons[i].id != "save" && buttons[i].id != "encode") {
+ algo = buttons[i].id;
+ var cc = dither(frame)
+ encoder.addFrame(cc.canvas)
+ }
+ }
+ }
+ encode()
+}
+
+function encode(){
+ if (! encoder.quantized) {
+ status("quantizing")
+ encoder.quantize()
+ }
+ else {
+ status("encoding")
+ encoder.encode()
+ }
+}
+function dither(frame){
+ var canvas = frame.ctx.canvas
+ var w = canvas.width, h = canvas.height;
+ var cc = cq(w, h)
+ cc.drawImage(canvas, 0, 0, w, h);
+ cc[algo + "Dither"]( )
+ return cc
+ // cc.appendTo("#images")
+}
+
+</script>
+</html>
+
diff --git a/gif-encode/GIFEncoder.js b/gif-encode/GIFEncoder.js
new file mode 100755
index 0000000..01d3618
--- /dev/null
+++ b/gif-encode/GIFEncoder.js
@@ -0,0 +1,513 @@
+/**
+* This class lets you encode animated GIF files
+* Base class : http://www.java2s.com/Code/Java/2D-Graphics-GUI/AnimatedGifEncoder.htm
+* @author Kevin Weiner (original Java version - kweiner@fmsware.com)
+* @author Thibault Imbert (AS3 version - bytearray.org)
+* @version 0.1 AS3 implementation
+*/
+
+//import flash.utils.ByteArray;
+//import flash.display.BitmapData;
+//import flash.display.Bitmap;
+//import org.bytearray.gif.encoder.NeuQuant
+//import flash.net.URLRequestHeader;
+//import flash.net.URLRequestMethod;
+//import flash.net.URLRequest;
+//import flash.net.navigateToURL;
+
+GIFEncoder = function() {
+ for(var i = 0, chr = {}; i < 256; i++) {
+ chr[i] = String.fromCharCode(i);
+ }
+
+ function ByteArray(){
+ this.bin = [];
+ }
+
+ ByteArray.prototype.getData = function(){
+ for(var v = '', l = this.bin.length, i = 0; i < l; i++) {
+ v += chr[this.bin[i]];
+ }
+ return v;
+ }
+ ByteArray.prototype.writeByte = function(val){
+ this.bin.push(val);
+ }
+ ByteArray.prototype.writeUTFBytes = function(string) {
+ for(var l = string.length, i = 0; i < l; i++) {
+ this.writeByte(string.charCodeAt(i));
+ }
+ }
+ ByteArray.prototype.writeBytes = function(array, offset, length) {
+ for(var l = length || array.length, i = offset||0; i < l; i++) {
+ this.writeByte(array[i]);
+ }
+ }
+
+ var exports = {};
+ var width/*int*/ // image size
+ var height/*int*/;
+ var transparent/***/ = null; // transparent color if given
+ var transIndex/*int*/; // transparent index in color table
+ var repeat/*int*/ = -1; // no repeat
+ var delay/*int*/ = 0; // frame delay (hundredths)
+ var started/*Boolean*/ = false; // ready to output frames
+ var out/*ByteArray*/;
+ var image/*Bitmap*/; // current frame
+ var pixels/*ByteArray*/; // BGR byte array from frame
+ var indexedPixels/*ByteArray*/ // converted frame indexed to palette
+ var colorDepth/*int*/; // number of bit planes
+ var colorTab/*ByteArray*/; // RGB palette
+ var usedEntry/*Array*/ = new Array; // active palette entries
+ var palSize/*int*/ = 7; // color table size (bits-1)
+ var dispose/*int*/ = -1; // disposal code (-1 = use default)
+ var closeStream/*Boolean*/ = false; // close stream when finished
+ var firstFrame/*Boolean*/ = true;
+ var sizeSet/*Boolean*/ = false; // if false, get size from first frame
+ var sample/*int*/ = 1; // default sample interval for quantizer
+ var neuquantBrain = null; // allow loading in a prefab neural net
+
+ /**
+ * Sets the delay time between each frame, or changes it for subsequent frames
+ * (applies to last frame added)
+ * int delay time in milliseconds
+ * @param ms
+ */
+
+ var setDelay = exports.setDelay = function setDelay(ms/*int*/) {
+ delay = Math.round(ms / 10);
+ }
+
+ /**
+ * Sets the GIF frame disposal code for the last added frame and any
+ *
+ * subsequent frames. Default is 0 if no transparent color has been set,
+ * otherwise 2.
+ * @param code
+ * int disposal code.
+ */
+
+ var setDispose = exports.setDispose = function setDispose(code/*int*/) {
+ if (code >= 0) dispose = code;
+ }
+
+ /**
+ * Sets the number of times the set of GIF frames should be played. Default is
+ * 1; 0 means play indefinitely. Must be invoked before the first image is
+ * added.
+ *
+ * @param iter
+ * int number of iterations.
+ * @return
+ */
+
+ var setRepeat = exports.setRepeat = function setRepeat(iter/*int*/) {
+ if (iter >= 0) repeat = iter;
+ }
+
+ /**
+ * Sets the transparent color for the last added frame and any subsequent
+ * frames. Since all colors are subject to modification in the quantization
+ * process, the color in the final palette for each frame closest to the given
+ * color becomes the transparent color for that frame. May be set to null to
+ * indicate no transparent color.
+ * @param
+ * Color to be treated as transparent on display.
+ */
+
+ var setTransparent = exports.setTransparent = function setTransparent(c/*Number*/) {
+ transparent = c;
+ }
+
+ /**
+ * The addFrame method takes an incoming BitmapData object to create each frames
+ * @param
+ * BitmapData object to be treated as a GIF's frame
+ */
+
+ /*Boolean*/
+ var addFrame = exports.addFrame = function addFrame(im/*BitmapData*/, is_imageData) {
+ if ((im == null) || ! started || out == null) {
+ throw new Error ("Please call start method before calling addFrame");
+ return false;
+ }
+
+ var ok/*Boolean*/ = true;
+
+ try {
+ if ( ! is_imageData) {
+ image = im.getImageData(0,0, im.canvas.width, im.canvas.height).data;
+ if ( ! sizeSet) {
+ setSize(im.canvas.width, im.canvas.height);
+ }
+ }
+ else {
+ image = im;
+ }
+ getImagePixels(); // convert to correct format if necessary
+ analyzePixels(); // build color table & map pixels
+
+ if (firstFrame) {
+ writeLSD(); // logical screen descriptior
+ writePalette(); // global color table
+ if (repeat >= 0) {
+ // use NS app extension to indicate reps
+ writeNetscapeExt();
+ }
+ }
+
+ writeGraphicCtrlExt(); // write graphic control extension
+ writeImageDesc(); // image descriptor
+ if (!firstFrame) {
+ writePalette(); // local color table
+ }
+ writePixels(); // encode and write pixel data
+ firstFrame = false;
+ }
+ catch (e/*Error*/) {
+ ok = false;
+ }
+ return ok;
+ }
+
+ /**
+ * Adds final trailer to the GIF stream, if you don't call the finish method
+ * the GIF stream will not be valid.
+ */
+
+ /*Boolean*/
+ var finish = exports.finish = function finish() {
+ if ( ! started) {
+ return false;
+ }
+
+ var ok/*Boolean*/ = true;
+ started = false;
+ try {
+ out.writeByte(0x3b); // gif trailer
+ }
+ catch (e/*Error*/) {
+ ok = false;
+ }
+ return ok;
+ }
+
+ /**
+ * Resets some members so that a new stream can be started.
+ * This method is actually called by the start method
+ */
+
+ var reset = function reset () {
+ // reset for subsequent use
+ transIndex = 0;
+ image = null;
+ pixels = null;
+ indexedPixels = null;
+ colorTab = null;
+ closeStream = false;
+ firstFrame = true;
+ }
+
+ /**
+ * * Sets frame rate in frames per second. Equivalent to
+ * <code>setDelay(1000/fps)</code>.
+ * @param fps
+ * float frame rate (frames per second)
+ */
+
+ var setFrameRate = exports.setFrameRate = function setFrameRate(fps/*Number*/) {
+ if (fps != 0xf) {
+ delay = Math.round(100/fps);
+ }
+ }
+
+ /**
+ * Sets quality of color quantization (conversion of images to the maximum 256
+ * colors allowed by the GIF specification). Lower values (minimum = 1)
+ * produce better colors, but slow processing significantly. 10 is the
+ * default, and produces good color mapping at reasonable speeds. Values
+ * greater than 20 do not yield significant improvements in speed.
+ * @param quality
+ * int greater than 0.
+ * @return
+ */
+
+ var setQuality = exports.setQuality = function setQuality(quality/*int*/) {
+ sample = Math.max(1, quality);
+ }
+
+ /**
+ * Sets the GIF frame size. The default size is the size of the first frame
+ * added if this method is not invoked.
+ * @param w
+ * int frame width.
+ * @param h
+ * int frame width.
+ */
+
+ var setSize = exports.setSize = function setSize(w/*int*/, h/*int*/) {
+ if (started && !firstFrame) {
+ return;
+ }
+ width = w;
+ height = h;
+ if (width < 1) width = 320;
+ if (height < 1) height = 240;
+ sizeSet = true;
+ }
+
+ /**
+ * After running the Neuquant on some test frames, it can be exported and then loaded
+ * into an uninitialized NQ instance on another worker and used accordingly.
+ */
+ var setNeuquant = exports.setNeuquant = function setNeuquant(neuquant, colors){
+ neuquantBrain = neuquant;
+ colorTab = colors;
+ }
+
+ /**
+ * Initiates GIF file creation on the given stream.
+ * @param os
+ * OutputStream on which GIF images are written.
+ * @return false if initial write failed.
+ */
+
+ var start = exports.start = function start() {
+ reset();
+ var ok/*Boolean*/ = true;
+ closeStream = false;
+ out = new ByteArray;
+ try {
+ out.writeUTFBytes("GIF89a"); // header
+ } catch (e/*Error*/) {
+ ok = false;
+ }
+
+ return started = ok;
+ }
+
+ var cont = exports.cont = function cont() {
+ reset();
+ var ok = true;
+ closeStream = false;
+ out = new ByteArray ();
+ return started = ok;
+ }
+
+ /**
+ * Analyzes image colors and creates color map.
+ */
+
+ var analyzePixels = function analyzePixels() {
+ var len = pixels.length;
+ var nPix = len / 3;
+ indexedPixels = [];
+ // initialize quantizer
+
+ var nq;
+ if (neuquantBrain && colorTab) {
+ nq = new NeuQuant();
+ nq.load(neuquantBrain);
+ }
+ else {
+ nq = new NeuQuant (pixels, len, sample);
+ colorTab = nq.process(); // create reduced palette
+ }
+
+ // map image pixels to new palette
+ var k = 0;
+ for (var j = 0; j < nPix; j++) {
+ var index = nq.map(pixels[k++] & 0xff, pixels[k++] & 0xff, pixels[k++] & 0xff);
+ usedEntry[index] = true;
+ indexedPixels[j] = index;
+ }
+ pixels = null;
+ colorDepth = 8;
+ palSize = 7;
+
+ // get closest match to transparent color if specified
+ if (transparent != null) {
+ transIndex = findClosest(transparent);
+ }
+ }
+
+ /**
+ * Returns index of palette color closest to c
+ */
+
+ var findClosest = function findClosest(c/*Number*/) {
+ if (colorTab == null) return -1;
+ var r = (c & 0xFF0000) >> 16;
+ var g = (c & 0x00FF00) >> 8;
+ var b = (c & 0x0000FF);
+ var minpos = 0;
+ var dmin = 256 * 256 * 256;
+ var len = colorTab.length;
+
+ for (var i = 0; i < len;) {
+ var dr = r - (colorTab[i++] & 0xff);
+ var dg = g - (colorTab[i++] & 0xff);
+ var db = b - (colorTab[i] & 0xff);
+ var d = dr * dr + dg * dg + db * db;
+ var index = i / 3;
+ if (usedEntry[index] && (d < dmin)) {
+ dmin = d;
+ minpos = index;
+ }
+ i++;
+ }
+ return minpos;
+ }
+
+ /**
+ * Extracts image pixels into byte array "pixels
+ */
+
+ var getImagePixels = function getImagePixels() {
+ var w = width;
+ var h = height;
+ pixels = [];
+ var data = image;
+ var count/*int*/ = 0;
+
+ for ( var i/*int*/ = 0; i < h; i++ ) {
+ for (var j/*int*/ = 0; j < w; j++ ) {
+ var b = (i*w*4)+j*4;
+ pixels[count++] = data[b];
+ pixels[count++] = data[b+1];
+ pixels[count++] = data[b+2];
+ }
+ }
+ }
+
+ /**
+ * Writes Graphic Control Extension
+ */
+
+ var writeGraphicCtrlExt = function writeGraphicCtrlExt() {
+ out.writeByte(0x21); // extension introducer
+ out.writeByte(0xf9); // GCE label
+ out.writeByte(4); // data block size
+ var transp/*int*/
+ var disp/*int*/;
+ if (transparent == null) {
+ transp = 0;
+ disp = 0; // dispose = no action
+ }
+ else {
+ transp = 1;
+ disp = 2; // force clear if using transparent color
+ }
+ if (dispose >= 0) {
+ disp = dispose & 7; // user override
+ }
+ disp <<= 2;
+ // packed fields
+ out.writeByte(0 | // 1:3 reserved
+ disp | // 4:6 disposal
+ 0 | // 7 user input - 0 = none
+ transp); // 8 transparency flag
+
+ WriteShort(delay); // delay x 1/100 sec
+ out.writeByte(transIndex); // transparent color index
+ out.writeByte(0); // block terminator
+ }
+
+ /**
+ * Writes Image Descriptor
+ */
+
+ var writeImageDesc = function writeImageDesc() {
+ out.writeByte(0x2c); // image separator
+ WriteShort(0); // image position x,y = 0,0
+ WriteShort(0);
+ WriteShort(width); // image size
+ WriteShort(height);
+
+ // packed fields
+ if (firstFrame) {
+ // no LCT - GCT is used for first (or only) frame
+ out.writeByte(0);
+ }
+ else {
+ // specify normal LCT
+ out.writeByte(0x80 | // 1 local color table 1=yes
+ 0 | // 2 interlace - 0=no
+ 0 | // 3 sorted - 0=no
+ 0 | // 4-5 reserved
+ palSize); // 6-8 size of color table
+ }
+ }
+
+ /**
+ * Writes Logical Screen Descriptor
+ */
+
+ var writeLSD = function writeLSD() {
+ // logical screen size
+ WriteShort(width);
+ WriteShort(height);
+ // packed fields
+ out.writeByte((0x80 | // 1 : global color table flag = 1 (gct used)
+ 0x70 | // 2-4 : color resolution = 7
+ 0x00 | // 5 : gct sort flag = 0
+ palSize)); // 6-8 : gct size
+
+ out.writeByte(0); // background color index
+ out.writeByte(0); // pixel aspect ratio - assume 1:1
+ }
+
+ /**
+ * Writes Netscape application extension to define repeat count.
+ */
+
+ var writeNetscapeExt = function writeNetscapeExt() {
+ out.writeByte(0x21); // extension introducer
+ out.writeByte(0xff); // app extension label
+ out.writeByte(11); // block size
+ out.writeUTFBytes("NETSCAPE" + "2.0"); // app id + auth code
+ out.writeByte(3); // sub-block size
+ out.writeByte(1); // loop sub-block id
+ WriteShort(repeat); // loop count (extra iterations, 0=repeat forever)
+ out.writeByte(0); // block terminator
+ }
+
+ /**
+ * Writes color table
+ */
+ var writePalette = function writePalette() {
+ out.writeBytes(colorTab);
+ var n/*int*/ = (3 * 256) - colorTab.length;
+ for (var i/*int*/ = 0; i < n; i++) {
+ out.writeByte(0);
+ }
+ }
+
+ var WriteShort = function WriteShort (pValue/*int*/) {
+ out.writeByte( pValue & 0xFF );
+ out.writeByte( (pValue >> 8) & 0xFF);
+ }
+
+ /**
+ * Encodes and writes pixel data
+ */
+ var writePixels = function writePixels() {
+ var myencoder = new LZWEncoder(width, height, indexedPixels, colorDepth);
+ myencoder.encode(out);
+ }
+
+ /**
+ * retrieves the GIF stream
+ */
+ var stream = exports.stream = function stream () {
+ return out;
+ }
+
+ var setProperties = exports.setProperties = function setProperties(has_start, is_first) {
+ started = has_start;
+ firstFrame = is_first;
+ }
+
+ return exports;
+}
+
diff --git a/gif-encode/LZWEncoder.js b/gif-encode/LZWEncoder.js
new file mode 100644
index 0000000..e3c512a
--- /dev/null
+++ b/gif-encode/LZWEncoder.js
@@ -0,0 +1,328 @@
+/**
+* This class handles LZW encoding
+* Adapted from Jef Poskanzer's Java port by way of J. M. G. Elliott.
+* @author Kevin Weiner (original Java version - kweiner@fmsware.com)
+* @author Thibault Imbert (AS3 version - bytearray.org)
+* @version 0.1 AS3 implementation
+*/
+
+//import flash.utils.ByteArray;
+
+LZWEncoder = function()
+{
+ var exports = {};
+ /*private_static*/ var EOF/*int*/ = -1;
+ /*private*/ var imgW/*int*/;
+ /*private*/ var imgH/*int*/
+ /*private*/ var pixAry/*ByteArray*/;
+ /*private*/ var initCodeSize/*int*/;
+ /*private*/ var remaining/*int*/;
+ /*private*/ var curPixel/*int*/;
+
+ // GIFCOMPR.C - GIF Image compression routines
+ // Lempel-Ziv compression based on 'compress'. GIF modifications by
+ // David Rowley (mgardi@watdcsu.waterloo.edu)
+ // General DEFINEs
+
+ /*private_static*/ var BITS/*int*/ = 12;
+ /*private_static*/ var HSIZE/*int*/ = 5003; // 80% occupancy
+
+ // GIF Image compression - modified 'compress'
+ // Based on: compress.c - File compression ala IEEE Computer, June 1984.
+ // By Authors: Spencer W. Thomas (decvax!harpo!utah-cs!utah-gr!thomas)
+ // Jim McKie (decvax!mcvax!jim)
+ // Steve Davies (decvax!vax135!petsd!peora!srd)
+ // Ken Turkowski (decvax!decwrl!turtlevax!ken)
+ // James A. Woods (decvax!ihnp4!ames!jaw)
+ // Joe Orost (decvax!vax135!petsd!joe)
+
+ /*private*/ var n_bits/*int*/ // number of bits/code
+ /*private*/ var maxbits/*int*/ = BITS; // user settable max # bits/code
+ /*private*/ var maxcode/*int*/ // maximum code, given n_bits
+ /*private*/ var maxmaxcode/*int*/ = 1 << BITS; // should NEVER generate this code
+ /*private*/ var htab/*Array*/ = new Array;
+ /*private*/ var codetab/*Array*/ = new Array;
+ /*private*/ var hsize/*int*/ = HSIZE; // for dynamic table sizing
+ /*private*/ var free_ent/*int*/ = 0; // first unused entry
+
+ // block compression parameters -- after all codes are used up,
+ // and compression rate changes, start over.
+
+ /*private*/ var clear_flg/*Boolean*/ = false;
+
+ // Algorithm: use open addressing double hashing (no chaining) on the
+ // prefix code / next character combination. We do a variant of Knuth's
+ // algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime
+ // secondary probe. Here, the modular division first probe is gives way
+ // to a faster exclusive-or manipulation. Also do block compression with
+ // an adaptive reset, whereby the code table is cleared when the compression
+ // ratio decreases, but after the table fills. The variable-length output
+ // codes are re-sized at this point, and a special CLEAR code is generated
+ // for the decompressor. Late addition: construct the table according to
+ // file size for noticeable speed improvement on small files. Please direct
+ // questions about this implementation to ames!jaw.
+
+ /*private*/ var g_init_bits/*int*/;
+ /*private*/ var ClearCode/*int*/;
+ /*private*/ var EOFCode/*int*/;
+
+ // output
+ // Output the given code.
+ // Inputs:
+ // code: A n_bits-bit integer. If == -1, then EOF. This assumes
+ // that n_bits =< wordsize - 1.
+ // Outputs:
+ // Outputs code to the file.
+ // Assumptions:
+ // Chars are 8 bits long.
+ // Algorithm:
+ // Maintain a BITS character long buffer (so that 8 codes will
+ // fit in it exactly). Use the VAX insv instruction to insert each
+ // code in turn. When the buffer fills up empty it and start over.
+
+ /*private*/ var cur_accum/*int*/ = 0;
+ /*private*/ var cur_bits/*int*/ = 0;
+ /*private*/ var masks/*Array*/ = [ 0x0000, 0x0001, 0x0003, 0x0007, 0x000F, 0x001F, 0x003F, 0x007F, 0x00FF, 0x01FF, 0x03FF, 0x07FF, 0x0FFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF ];
+
+ // Number of characters so far in this 'packet'
+ /*private*/ var a_count/*int*/;
+
+ // Define the storage for the packet accumulator
+ /*private*/ var accum/*ByteArray*/ = [];
+
+ var LZWEncoder = exports.LZWEncoder = function LZWEncoder (width/*int*/, height/*int*/, pixels/*ByteArray*/, color_depth/*int*/)
+ {
+
+ imgW = width;
+ imgH = height;
+ pixAry = pixels;
+ initCodeSize = Math.max(2, color_depth);
+
+ }
+
+ // Add a character to the end of the current packet, and if it is 254
+ // characters, flush the packet to disk.
+ var char_out = function char_out(c/*Number*/, outs/*ByteArray*/)/*void*/
+ {
+ accum[a_count++] = c;
+ if (a_count >= 254) flush_char(outs);
+
+ }
+
+ // Clear out the hash table
+ // table clear for block compress
+
+ var cl_block = function cl_block(outs/*ByteArray*/)/*void*/
+ {
+
+ cl_hash(hsize);
+ free_ent = ClearCode + 2;
+ clear_flg = true;
+ output(ClearCode, outs);
+
+ }
+
+ // reset code table
+ var cl_hash = function cl_hash(hsize/*int*/)/*void*/
+ {
+
+ for (var i/*int*/ = 0; i < hsize; ++i) htab[i] = -1;
+
+ }
+
+ var compress = exports.compress = function compress(init_bits/*int*/, outs/*ByteArray*/)/*void*/
+
+ {
+ var fcode/*int*/;
+ var i/*int*/ /* = 0 */;
+ var c/*int*/;
+ var ent/*int*/;
+ var disp/*int*/;
+ var hsize_reg/*int*/;
+ var hshift/*int*/;
+
+ // Set up the globals: g_init_bits - initial number of bits
+ g_init_bits = init_bits;
+
+ // Set up the necessary values
+ clear_flg = false;
+ n_bits = g_init_bits;
+ maxcode = MAXCODE(n_bits);
+
+ ClearCode = 1 << (init_bits - 1);
+ EOFCode = ClearCode + 1;
+ free_ent = ClearCode + 2;
+
+ a_count = 0; // clear packet
+
+ ent = nextPixel();
+
+ hshift = 0;
+ for (fcode = hsize; fcode < 65536; fcode *= 2)
+ ++hshift;
+ hshift = 8 - hshift; // set hash code range bound
+
+ hsize_reg = hsize;
+ cl_hash(hsize_reg); // clear hash table
+
+ output(ClearCode, outs);
+
+ outer_loop: while ((c = nextPixel()) != EOF)
+
+ {
+
+ fcode = (c << maxbits) + ent;
+ i = (c << hshift) ^ ent; // xor hashing
+
+ if (htab[i] == fcode)
+ {
+ ent = codetab[i];
+ continue;
+ } else if (htab[i] >= 0) // non-empty slot
+ {
+ disp = hsize_reg - i; // secondary hash (after G. Knott)
+ if (i == 0)
+ disp = 1;
+ do
+ {
+
+ if ((i -= disp) < 0) i += hsize_reg;
+
+ if (htab[i] == fcode)
+ {
+ ent = codetab[i];
+ continue outer_loop;
+ }
+ } while (htab[i] >= 0);
+ }
+
+ output(ent, outs);
+ ent = c;
+ if (free_ent < maxmaxcode)
+ {
+ codetab[i] = free_ent++; // code -> hashtable
+ htab[i] = fcode;
+ } else cl_block(outs);
+ }
+
+ // Put out the final code.
+ output(ent, outs);
+ output(EOFCode, outs);
+
+ }
+
+ // ----------------------------------------------------------------------------
+ var encode = exports.encode = function encode(os/*ByteArray*/)/*void*/
+ {
+ os.writeByte(initCodeSize); // write "initial code size" byte
+ remaining = imgW * imgH; // reset navigation variables
+ curPixel = 0;
+ compress(initCodeSize + 1, os); // compress and write the pixel data
+ os.writeByte(0); // write block terminator
+
+ }
+
+ // Flush the packet to disk, and reset the accumulator
+ var flush_char = function flush_char(outs/*ByteArray*/)/*void*/
+ {
+
+ if (a_count > 0)
+ {
+ outs.writeByte(a_count);
+ outs.writeBytes(accum, 0, a_count);
+ a_count = 0;
+ }
+
+ }
+
+ var MAXCODE = function MAXCODE(n_bits/*int*/)/*int*/
+ {
+
+ return (1 << n_bits) - 1;
+
+ }
+
+ // ----------------------------------------------------------------------------
+ // Return the next pixel from the image
+ // ----------------------------------------------------------------------------
+
+ var nextPixel = function nextPixel()/*int*/
+ {
+
+ if (remaining == 0) return EOF;
+
+ --remaining;
+
+ var pix/*Number*/ = pixAry[curPixel++];
+
+ return pix & 0xff;
+
+ }
+
+ var output = function output(code/*int*/, outs/*ByteArray*/)/*void*/
+
+ {
+ cur_accum &= masks[cur_bits];
+
+ if (cur_bits > 0) cur_accum |= (code << cur_bits);
+ else cur_accum = code;
+
+ cur_bits += n_bits;
+
+ while (cur_bits >= 8)
+
+ {
+
+ char_out((cur_accum & 0xff), outs);
+ cur_accum >>= 8;
+ cur_bits -= 8;
+
+ }
+
+ // If the next entry is going to be too big for the code size,
+ // then increase it, if possible.
+
+ if (free_ent > maxcode || clear_flg)
+ {
+
+ if (clear_flg)
+ {
+
+ maxcode = MAXCODE(n_bits = g_init_bits);
+ clear_flg = false;
+
+ } else
+ {
+
+ ++n_bits;
+
+ if (n_bits == maxbits) maxcode = maxmaxcode;
+
+ else maxcode = MAXCODE(n_bits);
+
+ }
+
+ }
+
+ if (code == EOFCode)
+ {
+
+ // At EOF, write the rest of the buffer.
+ while (cur_bits > 0)
+ {
+
+ char_out((cur_accum & 0xff), outs);
+ cur_accum >>= 8;
+ cur_bits -= 8;
+ }
+
+
+ flush_char(outs);
+
+ }
+
+ }
+ LZWEncoder.apply(this, arguments);
+ return exports;
+}
+
diff --git a/gif-encode/NeuQuant.js b/gif-encode/NeuQuant.js
new file mode 100644
index 0000000..91424ba
--- /dev/null
+++ b/gif-encode/NeuQuant.js
@@ -0,0 +1,538 @@
+/*
+* NeuQuant Neural-Net Quantization Algorithm
+* ------------------------------------------
+*
+* Copyright (c) 1994 Anthony Dekker
+*
+* NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994. See
+* "Kohonen neural networks for optimal colour quantization" in "Network:
+* Computation in Neural Systems" Vol. 5 (1994) pp 351-367. for a discussion of
+* the algorithm.
+*
+* Any party obtaining a copy of these files from the author, directly or
+* indirectly, is granted, free of charge, a full and unrestricted irrevocable,
+* world-wide, paid up, royalty-free, nonexclusive right and license to deal in
+* this software and documentation files (the "Software"), including without
+* limitation the rights to use, copy, modify, merge, publish, distribute,
+* sublicense, and/or sell copies of the Software, and to permit persons who
+* receive copies from any such party to do so, with the only requirement being
+* that this copyright notice remain intact.
+*/
+
+/*
+* This class handles Neural-Net quantization algorithm
+* @author Kevin Weiner (original Java version - kweiner@fmsware.com)
+* @author Thibault Imbert (AS3 version - bytearray.org)
+* @version 0.1 AS3 implementation
+*/
+
+//import flash.utils.ByteArray;
+
+NeuQuant = function() {
+ var exports = {};
+ var netsize = 128; /* number of colours used */
+
+ /* four primes near 500 - assume no image has a length so large */
+ /* that it is divisible by all four primes */
+
+ var prime1 = 499;
+ var prime2 = 491;
+ var prime3 = 487;
+ var prime4 = 503;
+ var minpicturebytes = (3 * prime4);
+
+ /* minimum size for input image */
+ /*
+ * Program Skeleton ---------------- [select samplefac in range 1..30] [read
+ * image from input file] pic = (unsigned char*) malloc(3*width*height);
+ * initnet(pic,3*width*height,samplefac); learn(); unbiasnet(); [write output
+ * image header, using writecolourmap(f)] inxbuild(); write output image using
+ * inxsearch(b,g,r)
+ */
+
+ /*
+ * Network Definitions -------------------
+ */
+
+ var maxnetpos = (netsize - 1);
+ var netbiasshift = 4; /* bias for colour values */
+ var ncycles = 100; /* no. of learning cycles */
+
+ /* defs for freq and bias */
+ var intbiasshift = 16; /* bias for fractions */
+ var intbias = (1 << intbiasshift);
+ var gammashift = 10; /* gamma = 1024 */
+ var gamma = (1 << gammashift);
+ var betashift = 10;
+ var beta = (intbias >> betashift); /* beta = 1/1024 */
+ var betagamma = (intbias << (gammashift - betashift));
+
+ /* defs for decreasing radius factor */
+ var initrad = (netsize >> 3); /* for 256 cols, radius starts */
+ var radiusbiasshift = 6; /* at 32.0 biased by 6 bits */
+ var radiusbias = (1 << radiusbiasshift);
+ var initradius = (initrad * radiusbias); /* and decreases by a */
+ var radiusdec = 30; /* factor of 1/30 each cycle */
+
+ /* defs for decreasing alpha factor */
+ var alphabiasshift = 10; /* alpha starts at 1.0 */
+ var initalpha = (1 << alphabiasshift);
+ var alphadec /* biased by 10 bits */
+
+ /* radbias and alpharadbias used for radpower calculation */
+ var radbiasshift = 8;
+ var radbias = (1 << radbiasshift);
+ var alpharadbshift = (alphabiasshift + radbiasshift);
+
+ var alpharadbias = (1 << alpharadbshift);
+
+ /*
+ * Types and Global Variables --------------------------
+ */
+
+ var thepicture/*ByteArray*//* the input image itself */
+ var lengthcount; /* lengthcount = H*W*3 */
+ var samplefac; /* sampling factor 1..30 */
+
+ // typedef int pixel[4]; /* BGRc */
+ var network; /* the network itself - [netsize][4] */
+ var netindex = new Array();
+
+ /* for network lookup - really 256 */
+ var bias = new Array();
+
+ /* bias and freq arrays for learning */
+ var freq = new Array();
+ var radpower = new Array();
+
+ var NeuQuant = exports.NeuQuant = function NeuQuant(thepic, len, sample) {
+
+ // with no input, assume we'll load in a lobotomized neuquant later.
+ // otherwise, initialize the neural net stuff
+
+ if (thepic && len && sample) {
+ var i;
+ var p;
+
+ thepicture = thepic;
+ lengthcount = len;
+ samplefac = sample;
+
+ network = new Array(netsize);
+
+ for (i = 0; i < netsize; i++) {
+ network[i] = new Array(4);
+ p = network[i];
+ p[0] = p[1] = p[2] = (i << (netbiasshift + 8)) / netsize;
+ freq[i] = intbias / netsize; /* 1/netsize */
+ bias[i] = 0;
+ }
+ }
+ }
+
+ var colorMap = function colorMap() {
+ var map/*ByteArray*/ = [];
+ var index = new Array(netsize);
+ for (var i = 0; i < netsize; i++) {
+ index[network[i][3]] = i;
+ }
+ var k = 0;
+ for (var l = 0; l < netsize; l++) {
+ var j = index[l];
+ map[k++] = (network[j][0]);
+ map[k++] = (network[j][1]);
+ map[k++] = (network[j][2]);
+ }
+ return map;
+ }
+
+ /*
+ * Insertion sort of network and building of netindex[0..255] (to do after
+ * unbias)
+ * -------------------------------------------------------------------------------
+ */
+
+ var inxbuild = function inxbuild() {
+ var i;
+ var j;
+ var smallpos;
+ var smallval;
+ var p;
+ var q;
+ var previouscol
+ var startpos
+
+ previouscol = 0;
+ startpos = 0;
+ for (i = 0; i < netsize; i++) {
+ p = network[i];
+ smallpos = i;
+ smallval = p[1]; /* index on g */
+ /* find smallest in i..netsize-1 */
+ for (j = i + 1; j < netsize; j++) {
+ q = network[j];
+ if (q[1] < smallval) { /* index on g */
+ smallpos = j;
+ smallval = q[1]; /* index on g */
+ }
+ }
+
+ q = network[smallpos];
+ /* swap p (i) and q (smallpos) entries */
+
+ if (i != smallpos) {
+ j = q[0];
+ q[0] = p[0];
+ p[0] = j;
+ j = q[1];
+ q[1] = p[1];
+ p[1] = j;
+ j = q[2];
+ q[2] = p[2];
+ p[2] = j;
+ j = q[3];
+ q[3] = p[3];
+ p[3] = j;
+ }
+
+ /* smallval entry is now in position i */
+
+ if (smallval != previouscol) {
+ netindex[previouscol] = (startpos + i) >> 1;
+
+ for (j = previouscol + 1; j < smallval; j++) netindex[j] = i;
+
+ previouscol = smallval;
+ startpos = i;
+ }
+ }
+
+ netindex[previouscol] = (startpos + maxnetpos) >> 1;
+ for (j = previouscol + 1; j < 256; j++) netindex[j] = maxnetpos; /* really 256 */
+ }
+
+ /*
+ * Main Learning Loop ------------------
+ */
+
+ var learn = function learn() {
+ var i;
+ var j;
+ var b;
+ var g
+ var r;
+ var radius;
+ var rad;
+ var alpha;
+ var step;
+ var delta;
+ var samplepixels;
+ var p/*ByteArray*/;
+ var pix;
+ var lim;
+
+ if (lengthcount < minpicturebytes) samplefac = 1;
+
+ alphadec = 30 + ((samplefac - 1) / 3);
+ p = thepicture;
+ pix = 0;
+ lim = lengthcount;
+ samplepixels = lengthcount / (3 * samplefac);
+ delta = samplepixels / ncycles;
+ alpha = initalpha;
+ radius = initradius;
+
+ rad = radius >> radiusbiasshift;
+ if (rad <= 1) rad = 0;
+
+ for (i = 0; i < rad; i++) radpower[i] = alpha * (((rad * rad - i * i) * radbias) / (rad * rad));
+
+ if (lengthcount < minpicturebytes) step = 3;
+ else if ((lengthcount % prime1) != 0) step = 3 * prime1;
+ else if ((lengthcount % prime2) != 0) step = 3 * prime2;
+ else if ((lengthcount % prime3) != 0) step = 3 * prime3;
+ else step = 3 * prime4;
+
+ i = 0;
+
+ while (i < samplepixels) {
+ b = (p[pix + 0] & 0xff) << netbiasshift;
+ g = (p[pix + 1] & 0xff) << netbiasshift;
+ r = (p[pix + 2] & 0xff) << netbiasshift;
+ j = contest(b, g, r);
+
+ altersingle(alpha, j, b, g, r);
+
+ if (rad != 0) alterneigh(rad, j, b, g, r); /* alter neighbours */
+
+ pix += step;
+
+ if (pix >= lim) pix -= lengthcount;
+
+ i++;
+
+ if (delta == 0) delta = 1;
+
+ if (i % delta == 0) {
+ alpha -= alpha / alphadec;
+ radius -= radius / radiusdec;
+ rad = radius >> radiusbiasshift;
+
+ if (rad <= 1) rad = 0;
+
+ for (j = 0; j < rad; j++) radpower[j] = alpha * (((rad * rad - j * j) * radbias) / (rad * rad));
+ }
+ }
+ }
+
+
+ /* Save the neural network so we can load it back in on another worker.
+ */
+ var save = exports.save = function(){
+ var data = {
+ netindex: netindex,
+ netsize: netsize,
+ network: network
+ };
+ return data;
+ }
+ var load = exports.load = function(data){
+ netindex = data.netindex;
+ netsize = data.netsize;
+ network = data.network;
+ }
+
+
+ /*
+ ** Search for BGR values 0..255 (after net is unbiased) and return colour
+ * index
+ * ----------------------------------------------------------------------------
+ */
+
+ var map = exports.map = function map(b, g, r) {
+ var i;
+ var j;
+ var dist
+ var a;
+ var bestd;
+ var p;
+ var best;
+
+ bestd = 1000; /* biggest possible dist is 256*3 */
+ best = -1;
+ i = netindex[g]; /* index on g */
+ j = i - 1; /* start at netindex[g] and work outwards */
+
+ while ((i < netsize) || (j >= 0)) {
+ if (i < netsize) {
+ p = network[i];
+ dist = p[1] - g; /* inx key */
+ if (dist >= bestd) i = netsize; /* stop iter */
+ else {
+ i++;
+
+ if (dist < 0) dist = -dist;
+
+ a = p[0] - b;
+
+ if (a < 0) a = -a;
+
+ dist += a;
+
+ if (dist < bestd) {
+ a = p[2] - r;
+
+ if (a < 0) a = -a;
+
+ dist += a;
+
+ if (dist < bestd) {
+ bestd = dist;
+ best = p[3];
+ }
+ }
+ }
+ }
+ if (j >= 0) {
+ p = network[j];
+
+ dist = g - p[1]; /* inx key - reverse dif */
+
+ if (dist >= bestd) j = -1; /* stop iter */
+ else {
+ j--;
+ if (dist < 0) dist = -dist;
+ a = p[0] - b;
+ if (a < 0) a = -a;
+ dist += a;
+
+ if (dist < bestd) {
+ a = p[2] - r;
+ if (a < 0)a = -a;
+ dist += a;
+ if (dist < bestd) {
+ bestd = dist;
+ best = p[3];
+ }
+ }
+ }
+ }
+ }
+ return best;
+ }
+
+ var process = exports.process = function process() {
+ learn();
+ unbiasnet();
+ inxbuild();
+ return colorMap();
+ }
+
+ /*
+ * Unbias network to give byte values 0..255 and record position i to prepare
+ * for sort
+ * -----------------------------------------------------------------------------------
+ */
+
+ var unbiasnet = function unbiasnet() {
+ var i;
+ var j;
+ for (i = 0; i < netsize; i++) {
+ network[i][0] >>= netbiasshift;
+ network[i][1] >>= netbiasshift;
+ network[i][2] >>= netbiasshift;
+ network[i][3] = i; /* record colour no */
+ }
+ }
+
+ /*
+ * Move adjacent neurons by precomputed alpha*(1-((i-j)^2/[r]^2)) in
+ * radpower[|i-j|]
+ * ---------------------------------------------------------------------------------
+ */
+
+ var alterneigh = function alterneigh(rad, i, b, g, r) {
+ var j;
+ var k;
+ var lo;
+ var hi;
+ var a;
+ var m;
+ var p;
+
+ lo = i - rad;
+ if (lo < -1) lo = -1;
+
+ hi = i + rad;
+
+ if (hi > netsize) hi = netsize;
+
+ j = i + 1;
+ k = i - 1;
+ m = 1;
+
+ while ((j < hi) || (k > lo)) {
+ a = radpower[m++];
+ if (j < hi) {
+ p = network[j++];
+
+ try {
+ p[0] -= (a * (p[0] - b)) / alpharadbias;
+ p[1] -= (a * (p[1] - g)) / alpharadbias;
+ p[2] -= (a * (p[2] - r)) / alpharadbias;
+ } catch (e/*Error*/) {} // prevents 1.3 miscompilation
+ }
+
+ if (k > lo) {
+ p = network[k--];
+ try {
+ p[0] -= (a * (p[0] - b)) / alpharadbias;
+ p[1] -= (a * (p[1] - g)) / alpharadbias;
+ p[2] -= (a * (p[2] - r)) / alpharadbias;
+ } catch (e/*Error*/) {}
+ }
+ }
+ }
+
+ /*
+ * Move neuron i towards biased (b,g,r) by factor alpha
+ * ----------------------------------------------------
+ */
+
+ var altersingle = function altersingle(alpha, i, b, g, r) {
+ /* alter hit neuron */
+ var n = network[i];
+ n[0] -= (alpha * (n[0] - b)) / initalpha;
+ n[1] -= (alpha * (n[1] - g)) / initalpha;
+ n[2] -= (alpha * (n[2] - r)) / initalpha;
+ }
+
+ /*
+ * Search for biased BGR values ----------------------------
+ */
+
+ var contest = function contest(b, g, r) {
+ /* finds closest neuron (min dist) and updates freq */
+ /* finds best neuron (min dist-bias) and returns position */
+ /* for frequently chosen neurons, freq[i] is high and bias[i] is negative */
+ /* bias[i] = gamma*((1/netsize)-freq[i]) */
+
+ var i;
+ var dist;
+ var a;
+ var biasdist;
+ var betafreq;
+ var bestpos;
+ var bestbiaspos;
+ var bestd;
+ var bestbiasd;
+ var n;
+
+ bestd = ~(1 << 31);
+ bestbiasd = bestd;
+ bestpos = -1;
+ bestbiaspos = bestpos;
+
+ for (i = 0; i < netsize; i++) {
+ n = network[i];
+ dist = n[0] - b;
+
+ if (dist < 0) dist = -dist;
+
+ a = n[1] - g;
+
+ if (a < 0) a = -a;
+
+ dist += a;
+
+ a = n[2] - r;
+
+ if (a < 0) a = -a;
+
+ dist += a;
+
+ if (dist < bestd) {
+ bestd = dist;
+ bestpos = i;
+ }
+
+ biasdist = dist - ((bias[i]) >> (intbiasshift - netbiasshift));
+
+ if (biasdist < bestbiasd) {
+ bestbiasd = biasdist;
+ bestbiaspos = i;
+ }
+
+ betafreq = (freq[i] >> betashift);
+ freq[i] -= betafreq;
+ bias[i] += (betafreq << gammashift);
+ }
+
+ freq[bestpos] += beta;
+ bias[bestpos] -= betagamma;
+ return (bestbiaspos);
+ }
+
+ NeuQuant.apply(this, arguments);
+ return exports;
+}
diff --git a/gif-encode/client.js b/gif-encode/client.js
new file mode 100644
index 0000000..a6c09ec
--- /dev/null
+++ b/gif-encode/client.js
@@ -0,0 +1,260 @@
+// Total frames to record
+var FRAMES_PER_GIF = 36;
+
+// Frames per second to read from the video
+var FPS = 12;
+
+// Per-frame delay in milliseconds
+var DELAY = Math.floor( 1000 / FPS );
+
+// Number of WebWorkers to create
+var WORKERS = 4;
+
+// Number of frames to use to build the gif palette (takes longest)
+var FRAMES_TO_QUANTIZE = 4;
+
+// Upload these gifs when finished??
+var DO_UPLOAD = true;
+
+function GifEncoder(){
+ var base = this;
+ this.working = false;
+ var canvases = [];
+ var contexts = [];
+ var frames = [];
+ var delays = [];
+
+ var initted = Date.now();
+ var started = Date.now();
+ var tube = base.tube = new Tube ()
+
+ var workers = new Factory ();
+
+ var width, height;
+ var neuquant, colortab;
+
+ workers.hire("message", receiveMessage);
+ workers.hire("quantize", receiveQuantize);
+ workers.hire("encode", receiveEncode);
+
+ var reset = this.reset = function(){
+ canvases = [];
+ contexts = [];
+ frames = [];
+ delays = [];
+ width = 0;
+ height = 0;
+ neuquant = null;
+ colortab = null;
+ base.quantized = false
+ }
+ var resetFrames = this.resetFrames = function(){
+ canvases = [];
+ contexts = [];
+ frames = [];
+ delays = [];
+ width = 0;
+ height = 0;
+ }
+
+ this.on = function(){
+ base.tube.on.apply(base.tube, arguments)
+ };
+
+ this.off = function(){
+ base.tube.off.apply(base.tube, arguments)
+ };
+
+ var addFrame = this.addFrame = function(canvas, delay) {
+ var ctx = canvas.getContext('2d');
+ canvases.push(canvas);
+ contexts.push(ctx);
+ delays.push(delay);
+
+ if (canvases.length == 1) {
+ width = canvas.width;
+ height = canvas.height;
+ }
+ }
+
+ var copyFrame = this.copyFrame = function(canvas, delay) {
+ var newCanvas = document.createElement("canvas");
+ var ctx = newCanvas.getContext('2d');
+
+ ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height);
+
+ canvases.push(newCanvas);
+ contexts.push(ctx);
+ delays.push(delay);
+
+ if (canvases.length == 1) {
+ width = canvas.width;
+ height = canvas.height;
+ }
+ }
+
+ function Factory () {
+ var base = this;
+ var w = 0; // which worker to work next
+ var ww = [];
+ base.init = function(){
+ for (var i = 0; i < WORKERS; i++) {
+ var worker = new Worker('gif-encode/worker.js');
+ worker.onmessage = base.receiveWork;
+ ww.push(worker);
+ }
+ }
+ var tasks = {};
+ base.hire = function(task, cb){
+ tasks[task] = cb;
+ }
+ base.work = function(job){
+ ww[++w % ww.length].postMessage(job);
+ }
+ base.receiveWork = function(e){
+ e.data.task in tasks && tasks[e.data.task](e);
+ }
+ base.init();
+ }
+
+ function receiveMessage(e){
+ console.log("[WORKER]", e.data.message);
+ }
+
+ var neuquant, colortab;
+ var quantize = this.quantize = function () {
+ initted = Date.now();
+ started = Date.now();
+ var spritedata = spriteSheet(FRAMES_TO_QUANTIZE);
+
+ workers.work({
+ task: 'quantize',
+ imageData: spritedata
+ });
+ }
+
+ function receiveQuantize(e) {
+ console.log(Date.now() - started, "quantization done");
+ neuquant = e.data.neuquant;
+ colortab = e.data.colortab;
+ base.quantized = true
+ base.tube("quantized")
+ }
+
+ var encode = this.encode = function (nq, ct) {
+ nq = nq || neuquant
+ ct = ct || colortab
+
+ started = Date.now();
+
+ console.log('working .... ');
+ var i = 0;
+
+ function sendWork () {
+ if (i == canvases.length) return doneSending();
+
+ var ctx = contexts[i];
+ var imdata = ctx.getImageData(0, 0, width, height).data;
+ var delay = delays[i];
+
+ workers.work({
+ task: 'encode',
+ frame_index: i,
+ frame_length: contexts.length-1,
+ height: height,
+ width: width,
+ delay: delay,
+ imageData: imdata,
+ neuquant: neuquant,
+ colortab: colortab
+ });
+
+ i++;
+ setTimeout(sendWork, 16);
+ }
+ function doneSending(){
+ base.tube("doneSending")
+ // ui.doneEncodingPicture();
+ }
+ sendWork();
+ }
+
+ function receiveEncode(e){
+ var frame_index = e.data["frame_index"];
+ var frame_data = e.data["frame_data"];
+
+ frames[frame_index] = frame_data;
+ for (var j = 0; j < canvases.length; j++) {
+ if (frames[j] == null) {
+ return;
+ }
+ }
+ console.log("FINISHED " + canvases.length);
+ var binary_gif = frames.join('');
+ var base64_gif = window.btoa(binary_gif);
+ var data_url = 'data:image/gif;base64,'+base64_gif;
+
+ base.working = false;
+
+ // photo.setAttribute('src', data_url);
+ // ui.doneEncodingPicture();
+ base.tube("rendered", binary_gif)
+ base.tube("rendered-url", data_url)
+// if (DO_UPLOAD) upload( base64_gif );
+
+ console.log((Date.now() - started), "processed frames");
+ console.log((Date.now() - initted), "done");
+ }
+
+// function upload (base64_gif) {
+// $("#working").html("UPLOADING")
+//
+// console.log("starting upload")
+// var params = {
+// url: base64_gif
+// }
+// $.ajax({
+// 'url': "/photos.json",
+// 'type': 'post',
+// 'data': csrf(params),
+// 'success': function(data){
+//
+// // $("#share").data("href", "/photos/" + data.hash)
+// // $("#share, #make-another").fadeIn(400);
+// console.log(data);
+// console.log((Date.now() - started), "uploaded");
+// // $("#photo").attr("src", data.url);
+// // window.location.href = "/photos/" + data.hash
+// localStorage.setItem('hash', data.hash)
+// window.location.href = "/"
+// // data.hash
+// }
+// });
+// console.log("ok");
+// }
+
+ function spriteSheet (frameCount) {
+ var start = Date.now();
+ frameCount = Math.min(contexts.length, frameCount);
+ var sprites = document.createElement("canvas");
+ var spriteContext = sprites.getContext('2d');
+ sprites.width = width;
+ sprites.height = height * frameCount;
+ var spritedata = spriteContext.getImageData(0, 0, sprites.width, sprites.height)
+ var spritedatadata = spritedata.data
+ var j = 0;
+ var ctxz = sample(contexts, 4);
+ while (frameCount--) {
+ var ctx = ctxz[frameCount];
+ var imdata = ctx.getImageData(0, 0, width, height).data;
+ for (var n = 0; n < imdata.length; j++, n++) {
+ spritedatadata[j] = imdata[n];
+ }
+ }
+ // spriteContext.putImageData(spritedata, 0, 0, 0, 0, sprites.width, sprites.height);
+ // upload( sprites.toDataURL("image/png").split(",")[1]
+ console.log(Date.now() - start, "built spritesheet");
+ return spritedata;
+ }
+
+}
diff --git a/gif-encode/tube.js b/gif-encode/tube.js
new file mode 100644
index 0000000..17d3bfd
--- /dev/null
+++ b/gif-encode/tube.js
@@ -0,0 +1,323 @@
+var nextTick = (function(){
+ // postMessage behaves badly on IE8
+ if (window.ActiveXObject || !window.postMessage) {
+ var nextTick = function(fn) {
+ setTimeout(fn, 0);
+ }
+ } else {
+ // based on setZeroTimeout by David Baron
+ // - http://dbaron.org/log/20100309-faster-timeouts
+ var timeouts = []
+ , name = 'next-tick-zero-timeout'
+
+ window.addEventListener('message', function(e){
+ if (e.source == window && e.data == name) {
+ if (e.stopPropagation) e.stopPropagation();
+ if (timeouts.length) timeouts.shift()();
+ }
+ }, true);
+
+ var nextTick = function(fn){
+ timeouts.push(fn);
+ window.postMessage(name, '*');
+ }
+ }
+
+ return nextTick;
+})()
+
+var Uid = (function(){
+ var id = 0
+ return function(){ return id++ + "" }
+})()
+
+
+var tokenize = (function(){
+ var tokenize = function(str, splitOn){
+ return str
+ .trim()
+ .split(splitOn || tokenize.default);
+ };
+
+ tokenize.default = /\s+/g;
+
+ return tokenize;
+})()
+
+// globber("*".split(":"), "a:b:c".split(":")) => true
+// globber("*:c".split(":"), "a:b:c".split(":")) => true
+// globber("a:*".split(":"), "a:b:c".split(":")) => true
+// globber("a:*:c".split(":"), "a:b:c".split(":")) => true
+
+// based on codegolf.stackexchange.com/questions/467/implement-glob-matcher
+var globber = function(patterns, strings) {
+ // console.log("globber called with: " + patterns.join(":"), strings.join(":"))
+ var first = patterns[0],
+ rest = patterns.slice(1),
+ len = strings.length,
+ matchFound;
+
+ if(first === '*') {
+ for(var i = 0; i <= len; ++i) {
+ // console.log("* " + i + " trying " + rest.join(":") + " with " + strings.slice(i).join(":"))
+ if(globber(rest, strings.slice(i))) return true;
+ }
+ return false;
+ } else {
+ matchFound = (first === strings[0]);
+ // console.log ("literal matching " + first + " " + strings[0] + " " + !!matched)
+ }
+
+ return matchFound && ((!rest.length && !len) || globber(rest, strings.slice(1)));
+};
+
+var setproto = function(obj, proto){
+ if (obj.__proto__)
+ obj.__proto__ = proto;
+ else
+ for (var key in proto)
+ obj[key] = proto[key];
+};
+
+
+var Tube = (function(){
+ var globcache = {};
+ var Tube = function(opts){
+ opts = opts || {};
+ if (opts.queue){
+ var c = function(){
+ var args = arguments;
+ // queueOrNextTick (function(){ c.send.apply(c, args) });
+ nextTick (function(){ c.send.apply(c, args) });
+ return c;
+ };
+ } else {
+ var c = function(){
+ c.send.apply(c, arguments);
+ return c;
+ };
+ }
+
+ setproto(c, Tube.proto);
+ c.listeners = {};
+ c.globListeners = {};
+
+ return c;
+ };
+
+ Tube.total = {};
+ Tube.proto = {};
+
+ /*
+ adds fns as listeners to a channel
+
+ on("msg", fn, {opts})
+ on("msg", [fn, fn2], {opts})
+ on("msg msg2 msg3", fn, {opts})
+ on({"msg": fn, "msg2": fn2}, {opts})
+ */
+
+ Tube.proto.on = function(){
+ var chan = this;
+ if (typeof arguments[0] === "string") {
+ //if (arguments.length > 1) { // on("msg", f)
+ var msgMap = {};
+ msgMap[arguments[0]] = arguments[1];
+ var opts = arguments[2] || {};
+ } else { // on({"msg": f, ...})
+ var msgMap = arguments[0];
+ var opts = arguments[1] || {};
+ }
+
+ for (var string in msgMap){
+ var msgs = string.split(" ");
+ var fs = msgMap[string];
+ if (!Array.isArray(fs)) fs = [fs];
+
+ for(var i=0, f; f=fs[i]; i++){
+ if (!f.uid) f.uid = Uid();
+ }
+
+ for(var i=0, msg; msg=msgs[i]; i++){
+ var listeners = (msg.indexOf("*") === -1) ?
+ chan.listeners :
+ chan.globListeners;
+
+ // todo: this probably wastes a lot of memory?
+ // make a copy of the listener, add to it, and replace the listener
+ // why not just push directly?
+ // send might be iterating over it... and that will fuck up the iteration
+
+ listeners[msg] = (msg in listeners) ?
+ listeners[msg].concat(fs) :
+ fs.concat();
+ }
+ }
+
+ return chan;
+ };
+
+ /*
+ off()
+ off("a:b:c")
+ off(f)
+ off("a:b:c", f)
+ off("a:b:c d:e:f")
+ off([f, f2])
+ off({"a": f, "b": f2})
+ */
+
+ Tube.proto.off = function(){ var chan = this;
+
+ var listeners, i, msgs, msg;
+
+ // off() : delete all listeners. but replace, instead of delete
+ if (arguments.length === 0) {
+ chan.listeners = {};
+ chan.globListeners = {};
+ return chan;
+ }
+
+ // off("a:b:c d:e:f")
+ // remove all matching listeners
+ if (arguments.length === 1 && typeof arguments[0] === "string"){
+ // question... will this fuck up send if we delete in the middle of it dispatching?
+ msgs = arguments[0].split(" ");
+
+ for (i=0; msg=msgs[i]; i++){
+ delete chan.listeners[msg];
+ delete chan.globListeners[msg];
+ }
+ return chan;
+ }
+
+ // off(f) or off([f, f2])
+ // remove all matching functions
+ if (typeof arguments[0] === "function" || Array.isArray(arguments[0])) {
+ var fs = (typeof arguments[0] === "function") ?
+ [arguments[0]] :
+ arguments[0];
+ // TODO
+ return chan;
+ }
+
+ // off("a:b:c", f) or off({"a": f, "b": f2})
+ if (arguments.length > 1) { // off("msg", f)
+ var msgMap = {};
+ msgMap[arguments[0]] = arguments[1];
+ } else { // off({"msg": f, ...})
+ var msgMap = arguments[0];
+ }
+
+ for (var string in msgMap){
+ msgs = string.split(" ");
+
+ var fs = msgMap[string];
+ if (typeof fs === "function") fs = [fs];
+
+ for(var i=0; msg=msgs[i]; i++){
+ if (msg in chan.listeners)
+ listeners = chan.listeners;
+ else if (msg in chan.globListeners)
+ listeners = chan.globListeners;
+ else
+ continue;
+
+ // gotta do this carefully in case we are still iterating through the listener in send
+ // build a new array and assign it to the property, instead of mutating it.
+
+ // console.log(" length of listeners[" + msg + "]: " + listeners[msg].length)
+ // console.log(listeners[msg].join(","));
+ // console.log(fs.join(","));
+
+ listeners[msg] = listeners[msg].filter(
+ function(f){ return fs.indexOf(f) === -1 }
+ );
+
+ // console.log(" length of listeners[" + msg + "]: " + listeners[msg].length)
+
+ }
+ }
+
+ return chan;
+
+ };
+
+ /*
+ c = Tube()
+ c.on("foo", fn)
+ c("foo", "bar", [])
+
+ will call fn("bar", [], "foo")
+ */
+
+ Tube.proto.send = function(msgString /*, data... */){
+ // todo: don't do this?
+ if (!Tube.total[msgString]) Tube.total[msgString] = 0
+ Tube.total[msgString]+=1;
+
+ var listener,
+ listeners = this.listeners,
+ globListeners = this.globListeners,
+ //args = Array.prototype.splice.call(arguments, 1),
+ msgs = tokenize(msgString),
+ msg, f;
+
+ if (arguments.length) {
+ var args = Array.prototype.splice.call(arguments, 1);
+ args.push(msgString);
+
+ } else {
+ var args = [];
+ }
+
+ for (var m=0; msg=msgs[m]; m++){
+
+ var fsToRun = [];
+ var uidKeyFnValue = {};
+ var uidKeyMsgStringValue = {};
+
+ // note this will die on errors
+ // todo: implement http://dean.edwards.name/weblog/2009/03/callbacks-vs-events/
+ // exact matches
+ if (listener = listeners[msg]) {
+ for (var i=0; f=listener[i]; i++){
+ // fsToRun.push([f, msg]);
+ uidKeyFnValue[f.uid] = f;
+ uidKeyMsgStringValue[f.uid] = msg;
+ }
+ }
+
+ // glob matches
+ var msgSplit = msg.split(":");
+
+ for (var pattern in globListeners){
+
+ if (pattern !== "*") { // * always matches
+ var patternSplit = globcache[pattern] || (globcache[pattern] = pattern.split(":"));
+ if (!globber(patternSplit, msgSplit)) continue;
+ }
+
+ listener = globListeners[pattern];
+
+ for (var i=0; f=listener[i]; i++){
+ //f.apply(window, args); // hm possibly pass the actual message to the func
+ // fsToRun.push([f, msg]);
+ uidKeyFnValue[f.uid] = f;
+ uidKeyMsgStringValue[f.uid] = msg;
+ }
+ }
+
+ var fns = [];
+ for (var f in uidKeyFnValue) fns.push(uidKeyFnValue[f]);
+
+ for (var i=0, f; f=fns[i]; i++)
+ f.apply(f, args);
+
+ }
+ return this;
+ };
+
+ return Tube;
+})()
+
diff --git a/gif-encode/util.js b/gif-encode/util.js
new file mode 100644
index 0000000..92d8129
--- /dev/null
+++ b/gif-encode/util.js
@@ -0,0 +1,15 @@
+function shuffle(a){
+ var aa = new Array(a.length);
+ aa[0] = a[0];
+
+ for (var i = 1; i < a.length; i++) {
+ var j = Math.floor( Math.random() * i );
+ aa[i] = aa[j];
+ aa[j] = a[i];
+ }
+ return aa;
+}
+function sample(a, n) {
+ var aa = shuffle(a);
+ return aa.slice(0,n);
+}
diff --git a/gif-encode/worker.js b/gif-encode/worker.js
new file mode 100644
index 0000000..b2f32d4
--- /dev/null
+++ b/gif-encode/worker.js
@@ -0,0 +1,88 @@
+importScripts('LZWEncoder.js', 'NeuQuant.js', 'GIFEncoder.js');
+
+self.onmessage = function(event) {
+ var data = event.data;
+ var task = data['task'];
+ switch (task) {
+ case 'encode':
+ encode (data);
+ break;
+ case 'quantize':
+ quantize(data);
+ break;
+ }
+}
+
+function log(msg) {
+ self.postMessage({
+ task: 'message',
+ message: msg
+ });
+}
+
+function quantize (data) {
+ var imageData = data["imageData"];
+ var pixels = discardAlphaChannel( imageData.data );
+
+ var nq = new NeuQuant (pixels, pixels.length, 1);
+ var colortab = nq.process();
+
+ self.postMessage({
+ task: 'quantize',
+ neuquant: nq.save(),
+ colortab: colortab
+ });
+}
+
+function discardAlphaChannel( imageData ) {
+ var pixels = [];
+
+ for ( var i = 0, b = 0, _len = imageData.length; i < _len; b += 4 ) {
+ pixels[i++] = imageData[b];
+ pixels[i++] = imageData[b+1];
+ pixels[i++] = imageData[b+2];
+ }
+ return pixels;
+}
+
+function encode (data) {
+ var frame_index = data["frame_index"];
+ var frame_length = data["frame_length"];
+ var height = data["height"];
+ var width = data["width"];
+ var imageData = data["imageData"];
+ var delay = data["delay"];
+ var neuquant = data["neuquant"];
+ var colortab = data["colortab"];
+
+ // Create a new GIFEncoder for every new worker
+ var encoder = new GIFEncoder();
+ encoder.setRepeat(0); // loop forever
+ encoder.setQuality(1);
+ encoder.setSize(width, height);
+ encoder.setDelay(delay);
+
+ if (frame_index == 0) {
+ encoder.start();
+ }
+ else {
+ encoder.cont();
+ encoder.setProperties(true, false); //started, firstFrame
+ }
+
+ // Load the neural net here because the color table gets clobbered by encoder.start();
+ encoder.setNeuquant(neuquant, colortab);
+ encoder.addFrame(imageData, true);
+
+ if(frame_length == frame_index) {
+ encoder.finish();
+ }
+
+ self.postMessage({
+ task: 'encode',
+ frame_index: frame_index,
+ frame_data: encoder.stream().getData()
+ });
+ // on the page, search for the GIF89a to see the frame_index
+};
+
diff --git a/gif.html b/gif.html
new file mode 100644
index 0000000..a9090cc
--- /dev/null
+++ b/gif.html
@@ -0,0 +1,149 @@
+<!doctype html>
+<html>
+<head>
+<title>Gifs</title>
+</head>
+<body>
+<div>
+<button id="random">random</button>
+<button id="pattern2">pattern2</button>
+<button id="pattern3">pattern3</button>
+<button id="pattern4">pattern4</button>
+<button id="pattern2Lite">pattern2lite</button>
+<button id="pattern3Lite">pattern3lite</button>
+<button id="pattern4Lite">pattern4lite</button>
+<button id="floydSteinberg">floyd-steinberg</button>
+<button id="right">right</button>
+<input type="text" id="url" value="http://i.imgur.com/BmGm7sU.gif">
+<button id="save">SAVE</button>
+<span id="loading"></span>
+</div>
+<div id="images"></div>
+</body>
+<script type="text/javascript" src="gif-encode/util.js"></script>
+<script type="text/javascript" src="gif-encode/tube.js"></script>
+<script type="text/javascript" src="gif-encode/client.js"></script>
+<script type="text/javascript" src="gif.js"></script>
+<script type="text/javascript" src="FileSaver.js"></script>
+<script type="text/javascript" src="canvasquery.js"></script>
+<script type="text/javascript" src="canvasquery.dither.js"></script>
+<script type="text/javascript">
+
+document.getElementById("url").onchange = load
+setTimeout(load)
+
+function status (s){
+ var el = document.getElementById("loading")
+ el.innerHTML = s + "..."
+}
+
+function load(imageURL) {
+ document.getElementById("save").style.display = "none"
+ status("loading")
+ var imageURL = proxify( document.getElementById("url").value );
+
+ if (imageURL.substr(-3) === "gif") {
+ window.gif = GIF(imageURL);
+ // gif.on("error", tryToLoadNextImage);
+ // gif.on("rendered", trackLoadTime);
+ gif.on("rendered", ready);
+ return gif.render();
+ } else {
+ window.img = new Image();
+ // img.addEventListener("error", tryToLoadNextImage);
+ img.addEventListener("load", ready);
+ img.crossOrigin = "anonymous";
+ return img.src = imageURL;
+ }
+}
+
+function giveImage() {
+ if (imageURL.substr(-3) === "gif") {
+ return gif.frames[gif.currentFrame()].ctx.canvas;
+ } else {
+ return img;
+ }
+}
+
+function proxify (url) {
+ return "/cgi-bin/proxy?" + url // .replace(/^https?:\/\//, "");
+};
+
+
+function ready(){
+ status("")
+ var buttons = document.getElementsByTagName("button")
+ for (var i = 0; i < buttons.length; i++) {
+ (function(n){
+ buttons[n].onclick = function(){
+ algo = buttons[n].id;
+ build()
+ }
+ })(i)
+ }
+ document.getElementById("save").onclick = save
+ build()
+}
+
+var encoder = new GifEncoder()
+
+encoder.on("quantized", function(url){
+ status("encoding")
+ encoder.encode()
+})
+
+encoder.on("rendered-url", function(url){
+ status("")
+ var image = new Image ()
+ image.src = url
+ document.body.appendChild(image)
+})
+
+var lastGif;
+encoder.on("rendered", function(bits){
+ status("")
+ lastGif = bits
+ document.getElementById("save").style.display = "inline"
+})
+
+function save (){
+ if (!lastGif) return;
+ var filename = document.getElementById("url").value.replace(/^.*\//,"").replace(/\.gif.*$/,"")
+ var blob = new Blob ([lastGif], {type: "image/gif"});
+ saveAs(blob, filename + "-" + algo + "-" + (+new Date() % 1000) + ".gif");
+}
+
+var algo = "random"
+var frames = []
+function build(){
+ document.getElementById("save").style.display = "none"
+ status("dithering")
+ encoder.resetFrames()
+
+ document.getElementById("images").innerHTML = ""
+ gif.frames.forEach(function(frame){
+ var cc = dither(frame)
+ encoder.addFrame(cc.canvas)
+ })
+
+ if (! encoder.quantized) {
+ status("quantizing")
+ encoder.quantize()
+ }
+ else {
+ status("encoding")
+ encoder.encode()
+ }
+}
+function dither(frame){
+ var canvas = frame.ctx.canvas
+ var w = canvas.width, h = canvas.height;
+ var cc = cq(w, h)
+ cc.drawImage(canvas, 0, 0, w, h);
+ cc[algo + "Dither"]( )
+ return cc
+ // cc.appendTo("#images")
+}
+</script>
+</html>
+
diff --git a/gif.js b/gif.js
new file mode 100644
index 0000000..a2c22fa
--- /dev/null
+++ b/gif.js
@@ -0,0 +1,1690 @@
+// gif.js by @timb :)
+;(function(e,t,n,r){function i(r){if(!n[r]){if(!t[r]){if(e)return e(r);throw new Error("Cannot find module '"+r+"'")}var s=n[r]={exports:{}};t[r][0](function(e){var n=t[r][1][e];return i(n?n:e)},s,s.exports)}return n[r].exports}for(var s=0;s<r.length;s++)i(r[s]);return i})(typeof require!=="undefined"&&require,{1:[function(require,module,exports){var Benchmark = require('../../benchmark');
+var Tube = require('../../tube');
+var BufferLoader = require('../../bufferloader');
+
+var decode = require('./decode');
+var render = require('./render-canvas');
+
+var GIF = function(src, opts){
+
+ var gif = Object.create(GIF.proto);
+
+ gif.buf = {}; // buffers used to store binary data
+ gif.tube = Tube();
+ gif.benchmark = Benchmark();
+
+ if (src instanceof File){
+ gif.src = src;
+ }
+ else if (typeof src === 'string'){ // url
+ gif.src = src;
+ }
+
+ return gif;
+
+};
+
+
+/*
+states:
+empty
+buffering: fetching a resource (File, url) into an ArrayBuffer
+decoding: decode binary data into different data chunks
+rendering: render data into frames as canvas contexts, or into a texture for webgl, etc
+*/
+
+GIF.proto = {};
+
+GIF.setupBuffers = function(gif, abuf){
+ gif.buf.cursor = 0;
+ gif.buf.abuf = abuf;
+ gif.buf.u8 = new Uint8Array(abuf);
+ gif.buf.dv = new DataView(abuf);
+};
+
+GIF.proto.on = function(){ var gif = this;
+ gif.tube.on.apply(gif.tube, arguments)
+};
+GIF.proto.off = function(){ var gif = this;
+ gif.tube.off.apply(gif.tube, arguments)
+};
+
+GIF.proto.load = function(){ var gif = this;
+ // if (!gif.src) throw new Error('gif needs a src');
+
+ gif.loader = BufferLoader(gif.src, {benchmark: gif.benchmark});
+ gif.loader.on("load", function(abuf){
+ GIF.setupBuffers(gif, abuf)
+ gif.loaded = true;
+ gif.tube("loaded")
+ });
+ gif.loader.on("error", function(e, xhr){ gif.tube("error", e, xhr)} );
+ gif.loader.load();
+
+};
+
+GIF.proto.decode = function(){ var gif = this;
+
+ if (!gif.loaded) {
+ gif.on("loaded", gif.decode.bind(gif)) // ugh
+ gif.load();
+ return gif
+ }
+
+ // TODO: deal with
+ gif.extensions = [];
+ gif.frames = [];
+ //gif.cursor = 0;
+
+ decode(gif);
+
+};
+
+GIF.proto.render = function(){ var gif = this;
+ if (!gif.decoded){
+ gif.on("decoded", gif.render.bind(gif)) // ugh
+ gif.decode();
+ return gif
+ }
+
+ render(gif);
+
+};
+
+
+if (typeof modules !== "undefined") modules.exports = GIF;
+if (typeof window !== "undefined") window.GIF = GIF;
+},{"../../benchmark":2,"../../tube":3,"../../bufferloader":4,"./decode":5,"./render-canvas":6}],2:[function(require,module,exports){// var b = Benchmark();
+// b.start("something") // start a timer named "something"
+// b.stop("something") // stop it
+// b.something <-- time it took between start and stop calls in ms
+// b.total <--- total ms of all benchmarks
+
+var Benchmark = function(){
+ var b = Object.create(Benchmark.proto);
+ b.total = 0;
+ return b;
+};
+Benchmark.proto = {};
+Benchmark.proto.start = function(name){
+ this[name+"_start_timestamp"] = Date.now();
+};
+Benchmark.proto.stop = function(name){
+ if (!this[name+"_start_timestamp"]) return;
+ var val = Date.now() - this[name+"_start_timestamp"];
+ delete this[name+"_start_timestamp"];
+ this[name] = val + (this[name] || 0);
+ this.total += val;
+};
+
+module.exports = Benchmark;
+
+// export Benchmark;
+},{}],3:[function(require,module,exports){var setproto = require('./object/setproto');
+var tokenize = require('./string/tokenize');
+var globber = require('./string/globber');
+var Uid = require('./uid');
+var nextTick = require('./nexttick');
+
+ // import queueOrNextTick from 'lib/nexttick4';
+
+var globcache = {};
+
+var Tube = function(opts){
+
+ opts = opts || {};
+
+ if (opts.queue){
+ var c = function(){
+ var args = arguments;
+ // queueOrNextTick (function(){ c.send.apply(c, args) });
+ nextTick (function(){ c.send.apply(c, args) });
+ return c;
+ };
+ } else {
+ var c = function(){
+ c.send.apply(c, arguments);
+ return c;
+ };
+ }
+
+ setproto(c, Tube.proto);
+
+ c.listeners = {};
+ c.globListeners = {};
+
+ return c;
+};
+
+Tube.total = {};
+
+Tube.proto = {};
+
+
+/*
+adds fns as listeners to a channel
+
+on("msg", fn, {opts})
+on("msg", [fn, fn2], {opts})
+on("msg msg2 msg3", fn, {opts})
+on({"msg": fn, "msg2": fn2}, {opts})
+*/
+Tube.proto.on = function(){ var chan = this;
+
+ if (typeof arguments[0] === "string") {
+ //if (arguments.length > 1) { // on("msg", f)
+ var msgMap = {};
+ msgMap[arguments[0]] = arguments[1];
+ var opts = arguments[2] || {};
+ } else { // on({"msg": f, ...})
+ var msgMap = arguments[0];
+ var opts = arguments[1] || {};
+ }
+
+ for (var string in msgMap){
+
+ var msgs = string.split(" ");
+
+ var fs = msgMap[string];
+ if (!Array.isArray(fs)) fs = [fs];
+
+ for(var i=0, f; f=fs[i]; i++){
+ if (!f.uid) f.uid = Uid();
+ }
+
+ for(var i=0, msg; msg=msgs[i]; i++){
+
+ var listeners = (msg.indexOf("*") === -1) ?
+ chan.listeners :
+ chan.globListeners;
+
+ // todo: this probably wastes a lot of memory?
+ // make a copy of the listener, add to it, and replace the listener
+ // why not just push directly?
+ // send might be iterating over it... and that will fuck up the iteration
+ listeners[msg] = (msg in listeners) ?
+ listeners[msg].concat(fs) :
+ fs.concat();
+ }
+ }
+
+ return chan;
+
+};
+
+
+
+/*
+off()
+off("a:b:c")
+off(f)
+off("a:b:c", f)
+off("a:b:c d:e:f")
+off([f, f2])
+off({"a": f, "b": f2})
+*/
+Tube.proto.off = function(){ var chan = this;
+
+ var listeners, i, msgs, msg;
+
+ // off() : delete all listeners. but replace, instead of delete
+ if (arguments.length === 0) {
+ chan.listeners = {};
+ chan.globListeners = {};
+ return chan;
+ }
+
+ // off("a:b:c d:e:f")
+ // remove all matching listeners
+ if (arguments.length === 1 && typeof arguments[0] === "string"){
+ // question... will this fuck up send if we delete in the middle of it dispatching?
+ msgs = arguments[0].split(" ");
+
+ for (i=0; msg=msgs[i]; i++){
+ delete chan.listeners[msg];
+ delete chan.globListeners[msg];
+ }
+ return chan;
+ }
+
+ // off(f) or off([f, f2])
+ // remove all matching functions
+ if (typeof arguments[0] === "function" || Array.isArray(arguments[0])) {
+ var fs = (typeof arguments[0] === "function") ?
+ [arguments[0]] :
+ arguments[0];
+ // TODO
+ return chan;
+ }
+
+ // off("a:b:c", f) or off({"a": f, "b": f2})
+ if (arguments.length > 1) { // off("msg", f)
+ var msgMap = {};
+ msgMap[arguments[0]] = arguments[1];
+ } else { // off({"msg": f, ...})
+ var msgMap = arguments[0];
+ }
+
+ for (var string in msgMap){
+ msgs = string.split(" ");
+
+ var fs = msgMap[string];
+ if (typeof fs === "function") fs = [fs];
+
+ for(var i=0; msg=msgs[i]; i++){
+
+ if (msg in chan.listeners)
+ listeners = chan.listeners;
+ else if (msg in chan.globListeners)
+ listeners = chan.globListeners;
+ else
+ continue;
+
+ // gotta do this carefully in case we are still iterating through the listener in send
+ // build a new array and assign it to the property, instead of mutating it.
+
+ // console.log(" length of listeners[" + msg + "]: " + listeners[msg].length)
+ // console.log(listeners[msg].join(","));
+ // console.log(fs.join(","));
+
+ listeners[msg] = listeners[msg].filter(
+ function(f){ return fs.indexOf(f) === -1 }
+ );
+ // console.log(" length of listeners[" + msg + "]: " + listeners[msg].length)
+
+ }
+ }
+
+ return chan;
+
+};
+
+
+/*
+
+c = Tube()
+c.on("foo", fn)
+c("foo", "bar", [])
+
+will call fn("bar", [], "foo")
+
+*/
+Tube.proto.send = function(msgString /*, data... */){
+
+ // todo: don't do this?
+ if (!Tube.total[msgString]) Tube.total[msgString] = 0
+ Tube.total[msgString]+=1;
+
+ var listener,
+ listeners = this.listeners,
+ globListeners = this.globListeners,
+ //args = Array.prototype.splice.call(arguments, 1),
+ msgs = tokenize(msgString),
+ msg, f;
+
+ if (arguments.length) {
+ var args = Array.prototype.splice.call(arguments, 1);
+ args.push(msgString);
+
+ } else {
+ var args = [];
+ }
+
+ for (var m=0; msg=msgs[m]; m++){
+
+ var fsToRun = [];
+ var uidKeyFnValue = {};
+ var uidKeyMsgStringValue = {};
+
+ // note this will die on errors
+ // todo: implement http://dean.edwards.name/weblog/2009/03/callbacks-vs-events/
+ // exact matches
+ if (listener = listeners[msg]) {
+ for (var i=0; f=listener[i]; i++){
+ // fsToRun.push([f, msg]);
+ uidKeyFnValue[f.uid] = f;
+ uidKeyMsgStringValue[f.uid] = msg;
+ }
+ }
+
+ // glob matches
+ var msgSplit = msg.split(":");
+
+ for (var pattern in globListeners){
+
+ if (pattern !== "*") { // * always matches
+ var patternSplit = globcache[pattern] || (globcache[pattern] = pattern.split(":"));
+ if (!globber(patternSplit, msgSplit)) continue;
+ }
+
+ listener = globListeners[pattern];
+
+ for (var i=0; f=listener[i]; i++){
+ //f.apply(window, args); // hm possibly pass the actual message to the func
+ // fsToRun.push([f, msg]);
+ uidKeyFnValue[f.uid] = f;
+ uidKeyMsgStringValue[f.uid] = msg;
+ }
+ }
+
+ var fns = [];
+ for (var f in uidKeyFnValue) fns.push(uidKeyFnValue[f]);
+
+ for (var i=0, f; f=fns[i]; i++)
+ f.apply(f, args);
+
+ }
+
+ return this;
+
+};
+
+module.exports = Tube;
+},{"./object/setproto":7,"./string/tokenize":8,"./string/globber":9,"./uid":10,"./nexttick":11}],7:[function(require,module,exports){// TODO: replace all uses of
+// setproto(foo, proto)
+// with
+// foo.__proto__ = proto
+// when IE is no longer shit
+
+var setproto = function(obj, proto){
+ if (obj.__proto__)
+ obj.__proto__ = proto;
+ else
+ for (var key in proto)
+ obj[key] = proto[key];
+};
+module.exports = setproto;
+},{}],8:[function(require,module,exports){// trimmed string into array of strings
+var tokenize = function(str, splitOn){
+ return str
+ .trim()
+ .split(splitOn || tokenize.default);
+};
+tokenize.default = /\s+/g;
+
+module.exports = tokenize;
+},{}],9:[function(require,module,exports){// can use * to match 0 or more : separated msgs
+// globber("*".split(":"), "a:b:c".split(":")) => true
+// globber("*:c".split(":"), "a:b:c".split(":")) => true
+// globber("a:*".split(":"), "a:b:c".split(":")) => true
+// globber("a:*:c".split(":"), "a:b:c".split(":")) => true
+
+// based on codegolf.stackexchange.com/questions/467/implement-glob-matcher
+var globber = function(patterns, strings) {
+ // console.log("globber called with: " + patterns.join(":"), strings.join(":"))
+ var first = patterns[0],
+ rest = patterns.slice(1),
+ len = strings.length,
+ matchFound;
+
+ if(first === '*') {
+ for(var i = 0; i <= len; ++i) {
+ // console.log("* " + i + " trying " + rest.join(":") + " with " + strings.slice(i).join(":"))
+ if(globber(rest, strings.slice(i))) return true;
+ }
+ return false;
+ } else {
+ matchFound = (first === strings[0]);
+ // console.log ("literal matching " + first + " " + strings[0] + " " + !!matched)
+ }
+
+ return matchFound && ((!rest.length && !len) || globber(rest, strings.slice(1)));
+};
+module.exports = globber;
+},{}],10:[function(require,module,exports){var Uid = function(){
+ return (Uid.counter++ + "");
+}
+
+Uid.counter = 1;
+
+module.exports = Uid;
+},{}],11:[function(require,module,exports){// based on https://github.com/timoxley/next-tick/blob/master/index.js
+
+// postMessage behaves badly on IE8
+if (window.ActiveXObject || !window.postMessage) {
+
+ var nextTick = function(fn) {
+ setTimeout(fn, 0);
+ }
+
+} else {
+
+ // based on setZeroTimeout by David Baron
+ // - http://dbaron.org/log/20100309-faster-timeouts
+ var timeouts = []
+ , name = 'next-tick-zero-timeout'
+
+ window.addEventListener('message', function(e){
+ if (e.source == window && e.data == name) {
+ if (e.stopPropagation) e.stopPropagation();
+ if (timeouts.length) timeouts.shift()();
+ }
+ }, true);
+
+ var nextTick = function(fn){
+ timeouts.push(fn);
+ window.postMessage(name, '*');
+ }
+
+}
+
+module.exports = nextTick;
+},{}],4:[function(require,module,exports){var Benchmark = require('./benchmark');
+var setproto = require('./object/setproto');
+var extend = require('./object/extend');
+var Tube = require('./tube');
+
+// usage: Buffer(url || File || ArrayBuffer)
+var BufferLoader = function(src, opts){
+
+ var loader = Tube();
+ setproto(loader, BufferLoader.proto);
+
+ if (opts && opts.benchmark) loader.benchmark = opts.benchmark;
+
+ //buf.remember("load error")
+ // loader.on("load", function(abuf){ setupBuffer(loader, abuf) }); // <-- hm, needs ref to buf
+
+ loader.src = src;
+
+ return loader;
+}
+
+BufferLoader.proto = {};
+extend(BufferLoader.proto, Tube.proto);
+
+BufferLoader.proto.load = function(){ var loader = this;
+ var src = loader.src;
+ if (typeof src === "string") loader.loadFromUrl(src)
+ else if (src instanceof File) loader.loadFromFile(src);
+ else if (src instanceof ArrayBuffer) loader("load", src);
+}
+
+BufferLoader.proto.loadFromFile = function(file){ var loader = this;
+
+ var r = new FileReader();
+
+ r.addEventListener('load', function(e){
+ if (loader.benchmark) loader.benchmark.stop("fetch-from-disk");
+ loader('load', r.result, e);
+ // console.log(r.file.name + " 100%: " + r.result.byteLength + " bytes")
+ });
+
+ r.addEventListener('error', function(e){
+ // console.log(arguments);
+ loader('error', e, r)
+ });
+
+ r.addEventListener('progress', function(e){
+ //console.log(r.file.name + " " + (e.loaded / e.total * 100) + "%: " + e.loaded + " bytes")
+ loader('progress', e)
+ });
+
+ if (loader.benchmark) loader.benchmark.start("fetch-from-disk");
+ r.readAsArrayBuffer(file);
+
+};
+
+
+BufferLoader.proto.loadFromUrl = function(url){ var loader=this;
+
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", url);
+ xhr.responseType = "arraybuffer";
+
+ xhr.addEventListener('load', function(e){
+ if (loader.benchmark) loader.benchmark.stop("fetch-from-network");
+ // console.log(r.file.name + " 100%: " + r.result.byteLength + " bytes")
+ loader('load', xhr.response, e);
+ });
+
+ xhr.addEventListener('error', function(e){
+ // console.log(arguments);
+ loader('error', e, xhr);
+ });
+
+ xhr.addEventListener('progress', function(e){
+ //console.log(r.file.name + " " + (e.loaded / e.total * 100) + "%: " + e.loaded + " bytes")
+ loader('progress', e)
+ });
+
+ if (loader.benchmark) loader.benchmark.start("fetch-from-network");
+ // xhr.open("GET", url);
+
+ xhr.send();
+
+};
+
+module.exports = BufferLoader;
+},{"./benchmark":2,"./object/setproto":7,"./object/extend":12,"./tube":3}],12:[function(require,module,exports){module.exports = function(destination, source) {
+ for (var property in source)
+ destination[property] = source[property];
+ return destination;
+};
+},{}],5:[function(require,module,exports){(function(){var blockSigs = require('./spec').blockSigs;
+var extSigs = require('./spec').extSigs;
+var palette = require('./palette');
+var makeCurrentFrame = require('./animate').makeCurrentFrame;
+
+var BinarySpecReader = require('../../binaryspec');
+var spec = require('./spec').spec;
+var specReader = BinarySpecReader(spec);
+
+var decode = function(gif, opts){
+ decodeNextPart(gif);
+};
+
+
+// this gets called when more binary data is ready to process
+// it gets set as BufferedReader's onload function
+/*
+var moreDataInBufferEvent = function(gif){
+ if (gif.waitingForFileBuffer) decodeNextPart(gif);
+};
+*/
+
+/*
+loads the next part of the gif, eventually calling the gif's onload function when every part has been loaded
+
+this function gets called when a previous part has finished loading,
+and when there's more data in the buffered reader
+
+if the next part has a fixed size according to spec, we know whether we have to wait on the buffer to fill or not
+if the next part has a variable size, call the howToDecode function which will try to load it
+*/
+var decodeNextPart = function(gif, nextPart){
+ //gif.waitingForFileBuffer = false;
+
+ nextPart = nextPart || "header";
+ var buf = gif.buf;
+
+ while (nextPart !== "done" && nextPart !== "error"){
+
+ if (nextPart in howToDecode && typeof howToDecode[nextPart] === "function") { // dont know size
+ nextPart = howToDecode[nextPart](gif);
+ } else { // we know exact size of next part
+
+ var partSize = specReader.parts[nextPart].byteSize;
+
+ if (buf.abuf.byteLength < partSize + buf.cursor){
+ gif.waitingForFileBuffer = true;
+ return;
+ } else {
+ var fields = specReader.decodeBinaryFieldsToJSON(nextPart, buf.cursor, buf);
+ buf.cursor += specReader.parts[nextPart].byteSize;
+ nextPart = handleLoadedPart[nextPart](gif, fields);
+ }
+ }
+ // todo: maybe do this when and return unknown from dataBlock read
+ // if (nextPart === "unknown") nextPart = "dataBlock"
+
+ }
+
+ if (nextPart === "done"){
+ // create palette from all frames' palettes
+
+ if (gif.benchmark) gif.benchmark.start("palette");
+ gif.paletteTotal = palette.create(gif);
+ if (gif.benchmark) gif.benchmark.stop("palette");
+
+ // todo: move elsewhere
+ makeCurrentFrame.bind(gif)();
+// GIF.decode.doneDecodingCleanup(gif);
+ //gif.tube("progress", "done");
+ gif.decoded = true;
+ gif.tube("decoded");
+ return;
+ }
+
+};
+
+
+var howToDecode = {
+ "globalPalette": function(gif){ var buf = gif.buf;
+ var paletteSize = gif.paletteSize * 3; // r,g,b bytes
+
+ if (buf.abuf.byteLength < buf.cursor + paletteSize) {
+ gif.waitingForFileBuffer = true;
+ return;
+ }
+
+ //gif.palette = GIF.palette.binary2rgba(gif.reader.buffer.slice(gif.cursor, gif.cursor + paletteSize));
+ gif.palette = palette.binary2rgba(new Uint8Array(buf.abuf, buf.cursor, paletteSize));
+ buf.cursor += paletteSize;
+
+ return "dataBlock";
+ },
+ "localPalette": function(gif){ var buf = gif.buf;
+ var paletteSize = gif.frames[gif.frames.length - 1].paletteSize * 3; // r,g,b bytes
+
+ if (buf.abuf.byteLength < buf.cursor + paletteSize) {
+ gif.waitingForFileBuffer = true;
+ return;
+ }
+
+ gif.frames[gif.frames.length - 1].palette = palette.binary2rgba(new Uint8Array(buf.abuf, buf.cursor, paletteSize));
+ buf.cursor += paletteSize;
+ return "imageData";
+ },
+ "dataBlock": function(gif){ var buf = gif.buf;
+
+ if (buf.abuf.byteLength < buf.cursor + 1) {
+ gif.waitingForFileBuffer = true;
+ return;
+ }
+
+ var blockType = "unknown",
+ extType = "unknown",
+ nextPart;
+
+ var blockSig = buf.u8[buf.cursor];
+
+ if (blockSig in blockSigs) {
+ blockType = blockSigs[blockSig];
+ } else {
+ //console.log("unknown data block type ("+ Number(blockSig).toString(16) +")!");
+ nextPart = "done";
+ }
+
+ if (blockType === "extension"){ // we need to determine what kind of extension the block is
+ // need next two bytes to find out what kind of extension the data block is
+ if (buf.abuf.byteLength < buf.cursor + 2) {
+ gif.waitingForFileBuffer = true;
+ return;
+ console.log("ran out of buffer")
+ }
+ var extSig = buf.u8[buf.cursor+1];
+ if (extSig in extSigs) {
+ extType = extSigs[extSig];
+ nextPart = extType;
+ } else {
+ //console.log("unknown extension type ("+ Number(extSig).toString(16) +")")
+ nextPart = "done";
+ }
+ } else {
+ if (blockType === "unknown") { blockType = "dataBlock"; buf.cursor += 1 }
+ nextPart = blockType;
+ }
+
+ return nextPart;
+
+ },
+ "trailer": function(gif){
+ return "done";
+ }
+};
+
+var handleLoadedPart = {
+ "header": function(gif, fields){
+
+ if (fields.signature != "GIF"){
+ gif.tube("error", "file doesn't seem to be a gif (file signature: '"+fields.signature+"')");
+ return "error";
+ }
+
+ // gif.version = fields.version
+
+ return "screenDesc";
+ },
+ "screenDesc": function(gif, fields){
+ for (var field in fields) gif[field] = fields[field];
+ // todo: make nicer
+ // this should be a u3 not this bit[3] bit-shifted horseshit
+ gif.paletteSize = (gif.paletteSize[0] << 2) + (gif.paletteSize[1] << 1) + (gif.paletteSize[2]);
+ gif.paletteSize = Math.pow(2, gif.paletteSize + 1)
+
+ if (gif.paletteExists) return "globalPalette";
+ else return "dataBlock";
+ },
+ "imageDesc": function(gif, fields){
+ // make a blank image frame if none exists or the last frame already has image data
+ // we don't know if a blank frame has already been created because a graphic control block might
+ // have come before this block and made one
+ if (!gif.frames.length || ("w" in gif.frames[gif.frames.length - 1]))
+ gif.frames.push({})
+
+ var frame = gif.frames[gif.frames.length - 1]
+
+ for (var field in fields) frame[field] = fields[field];
+ frame.paletteSize = (frame.paletteSize[0] << 2) + (frame.paletteSize[1] << 1) + (frame.paletteSize[2]);
+ frame.paletteSize = Math.pow(2, frame.paletteSize + 1)
+
+ if (frame.paletteExists) return "localPalette";
+ else return "imageData";
+ },
+ // if sublocks was able to read:
+ // increase cursor from subblocks
+ // increase cursor from fields
+ // else
+ // set waiting
+ // subtract 2 from cursor (for the 2 that was read to get the data block)
+ // subtract part.bytesize from cursor (not needed in this case?)
+ "applicationExtension": function(gif, fields){
+ var blockinfo = readSubBlocks(gif);
+ if (blockinfo === false) {
+ gif.waitingForFileBuffer = true
+ gif.buf.cursor -= specReader.parts.applicationExtension.byteSize
+ return;
+ } else {
+ var extension = {"data": blockinfo};
+ for(var field in fields)
+ extension[field] = fields[field]
+ gif.extensions.push(extension)
+ return "dataBlock";
+ }
+ },
+ "comment": function(gif, fields){
+ var blockinfo = readSubBlocks(gif);
+ if (blockinfo === false) {
+ gif.waitingForFileBuffer = true;
+ gif.buf.cursor -= specReader.parts.comment.byteSize;
+ return;
+ } else {
+ var extension = {"comment": blockinfo};
+ for(var field in fields)
+ extension[field] = fields[field];
+ gif.extensions.push(extension);
+ return "dataBlock";
+ }
+ },
+ "plainText": function(gif, fields){
+ var blockinfo = readSubBlocks(gif);
+ if (blockinfo === false) {
+ gif.waitingForFileBuffer = true;
+ gif.buf.cursor -= specReader.parts.plainText.byteSize;
+ return;
+ } else {
+ var extension = {"plainText": blockinfo};
+ for(var field in fields)
+ extension[field] = fields[field];
+ gif.extensions.push(extension);
+ return "dataBlock";
+ }
+ },
+ "graphicControl": function(gif, fields){
+ var dm = (fields.disposalMethod[0] << 2) + (fields.disposalMethod[1] << 1) + (fields.disposalMethod[2]);
+ gif.frames.push({"delay": fields.delay,
+ "transparentIndex": fields.transparentColor ? fields.transparentIndex : -1,
+ "disposalMethod": dm})
+ return "dataBlock";
+ },
+ "imageData": function(gif, fields){
+ var blockinfo = readSubBlocks(gif);
+ if (blockinfo === false) {
+ gif.waitingForFileBuffer = true
+ gif.buf.cursor -= specReader.parts.imageData.byteSize
+ console.log("fucked")
+ return;
+ } else {
+
+// TODO: ENABLE THIS!
+// gif.tube("progress", "found " + gif.frames.length + " frames");
+
+ var frame = gif.frames[gif.frames.length - 1];
+ //var palette = ("palette" in frame) ? frame.palette : gif.palette // local palette otherwise global palette
+ frame.lzwCodeSize = fields.lzwCodeSize;
+ frame.blockinfo = blockinfo;
+// var transparentIndex = ("transparentIndex" in frame) ? frame.transparentIndex : -1
+ return "dataBlock";
+ }
+
+ }
+};
+
+// read subblocks out of a gif's buffer...
+// reads block sizes and returns an object with a start cursor and an array of block ends
+// returns false if there's not enough data
+var readSubBlocks = function(gif){
+
+ if (gif.benchmark) gif.benchmark.start("read-subblocks");
+
+ var blockEnds = [];
+
+ var buf = gif.buf,
+ u8 = buf.u8,
+ byteLength = u8.byteLength;
+
+ // gif.cursor is for whole file... pos is a cursor for just this blob
+ var startBlockCursor = buf.cursor;
+ var pos = buf.cursor;
+ startBlockCursor += 1;
+ var byteSize = 0;
+ var outOfData = false
+
+ // only actually advance cursor if we can read in all the sub blocks from the buffer
+ var cursorTemp = 0;
+
+ while(!outOfData){
+ // get block size
+ if (byteLength < pos + 1) { outOfData = true; break;}
+ byteSize = u8[pos];
+ pos += 1;
+
+ // a sub block with size 0 indicates end of sub blocks
+ if (byteSize === 0) { cursorTemp += 1; break;}
+
+ // read block
+ if (byteLength < pos + byteSize) { outOfData = true; break;}
+ blockEnds.push(pos + byteSize);
+
+ pos += byteSize
+ cursorTemp += byteSize + 1
+ // gif.subBlocksRead += 1
+ }
+
+ // TODO? CLEAN UP!
+ if (outOfData) {
+ // gif.bufferMisses += 1
+ // gif.benchmark.wasted += (Date.now() - start) / 1000
+ console.log("out of data")
+ return false
+
+ } else { // end of sub blocks happened
+ buf.cursor += cursorTemp
+ //if ("onprogress" in gif) gif.onprogress(gif, Math.floor(gif.cursor / gif.file.size * 100))
+// gif.benchmark.subblocks += (Date.now() - start) / 1000;
+ if (gif.benchmark) gif.benchmark.stop("read-subblocks");
+ return {start: startBlockCursor, blockEnds: blockEnds};
+
+ }
+};
+
+module.exports = decode;
+})()
+},{"./spec":13,"./palette":14,"./animate":15,"../../binaryspec":16}],13:[function(require,module,exports){var blockSigs = {
+ 0x21: "extension",
+ 0x2c: "imageDesc",
+ 0x3b: "trailer"
+};
+
+exports.blockSigs = blockSigs;
+
+var extSigs = {
+ 0xf9: "graphicControl",
+ 0xfe: "comment",
+ 0x01: "plainText",
+ 0xff: "applicationExtension"
+};
+
+exports.extSigs = extSigs;
+
+/*
+GIF.getGeneralBlockType = function(sig){
+ if (sig == 0x3b)
+ return "trailer"
+ else if (sig < 0x7F)
+ return "graphic rendering block"
+ else if (sig < 0xF9)
+ return "control block"
+ else return "special purpose block"
+}
+
+*/
+
+var spec = {
+ "header": [
+ "str[3] signature",
+ "str[3] version"
+ ],
+ "screenDesc": [
+ "u16 w",
+ "u16 h",
+ "bit paletteExists",
+ "bit[3] resolution ignore",
+ "bit sortFlag ignore",
+ "bit[3] paletteSize",
+ "u8 bgColorIndex",
+ "u8 aspectRatio ignore"
+ ],
+ "imageDesc": [
+ "u8 sig ignore",
+ "u16 x",
+ "u16 y",
+ "u16 w",
+ "u16 h",
+ "bit paletteExists",
+ "bit interlaced",
+ "bit sortFlag",
+ "bit[2] reserved ignore",
+ "bit[3] paletteSize"
+ ],
+ "applicationExtension": [
+ "u8 sig ignore",
+ "u8 extSig ignore",
+ "u8 blockSize ignore",
+ "str[8] identifier",
+ "str[3] authCode ignore"
+ ],
+ "graphicControl": [
+ "u8 sig ignore",
+ "u8 extSig ignore",
+ "u8 blockSize ignore",
+ "bit[3] reserved ignore",
+ "bit[3] disposalMethod",
+ "bit userInput ignore",
+ "bit transparentColor",
+ "u16 delay",
+ "u8 transparentIndex",
+ "u8 blockTerminator ignore"
+ ],
+ "comment": [
+ "u8 sig ignore",
+ "u8 extSig ignore"
+ ],
+ "plainText": [
+ "u8 sig ignore",
+ "u8 extSig ignore",
+ "u8 blockSize",
+ "u16 textGridLeft",
+ "u16 textGridTop",
+ "u16 textGridWidth",
+ "u16 textGridHeight",
+ "u8 charCellWidth",
+ "u8 charCellHeight",
+ "u8 fgColorIndex",
+ "u8 bgColorIndex"
+ ],
+ "imageData": [
+ "u8 lzwCodeSize"
+ ]
+};
+
+exports.spec = spec;
+},{}],14:[function(require,module,exports){(function(){var rgba2css = require('../../color/rgba2css');
+var create2d = require('../../create/2d');
+var createImageData = require('../../create/imagedata');
+
+var palette = {};
+
+// flat typed array of r g b a values from binary GIF palette
+palette.binary2rgba = function(abuf /*, transparentIndex */){
+ var table = new Uint8Array(abuf.byteLength/3*4);
+ var counter = 0
+ for(var i = 0, length = abuf.byteLength/3*4; i<length; i+=4){
+ table[i] = abuf[counter];
+ table[i+1] = abuf[counter+1];
+ table[i+2] = abuf[counter+2];
+ table[i+3] = 255;
+ counter += 3;
+ }
+/*
+ if (transparentIndex !== undefined)
+ table[transparentIndex*4 + 3] = 0;
+*/
+ return table;
+};
+
+
+// TODO. break this up, make less confusing, etc.
+// horrible monolithic palette mappings constructor
+// builds a hash lookup to map rgba value to palette index
+// builds an array to map indexes to rgba values
+// builds an array to map indexes to css values
+// builds an imagedata to map indexes to colors
+//
+// gif palette index numbers are not preserved...
+// this makes a palette from all local palettes (frames) plus global palette
+// only ONE transparent value is included... 0,0,0,0, others are ignored
+palette.create = function(gif){
+
+ // boot up palette with a transparent color to start with
+ var rgba2Index = {"0":0},
+ index2Css = ["rgba(0,0,0,0)"],
+ index2Rgba = [[0,0,0,0]];
+
+ var addPalette = function(palette){
+ for(var i=0, size=palette.length; i<size; i+=4){
+ var a = palette[i+3]
+ if (a === 0) continue; // skip transparent values... just use first palette index for them
+ var r = palette[i],
+ g = palette[i+1],
+ b = palette[i+2],
+ index = (r | (g << 8) | (b << 16) | (a << 24)).toString();
+ // http://jsperf.com/rgba-hash-lookup
+ // also... this overflows and flips sign... is that safe over diff. js implementations?
+ if (index in rgba2Index) continue;
+ rgba2Index[index] = index2Rgba.length
+ var rgba = [r,g,b,a]
+ index2Rgba.push(rgba)
+ index2Css.push(rgba2css(rgba))
+ }
+ };
+
+ if ("palette" in gif)
+ addPalette(gif.palette)
+
+ for(var f=0; f<gif.frames.length; f++){
+ var frame = gif.frames[f];
+ if (!("palette" in frame)) continue;
+ addPalette(frame.palette)
+ }
+
+ // create this last, as we don't know palette size until it is built
+ var imagedata = createImageData(index2Rgba.length, 1)
+ for(var i=0, size=index2Rgba.length*4, data=imagedata.data; i<size; i+=4){
+ var rgba = index2Rgba[i/4]
+ data[i] = rgba[0]
+ data[i+1] = rgba[1]
+ data[i+2] = rgba[2]
+ data[i+3] = rgba[3]
+ }
+
+ return {"rgba2Index": rgba2Index,
+ "index2Rgba": index2Rgba,
+ "index2Css": index2Css,
+ "imagedata": imagedata,
+ "length": index2Rgba.length}
+};
+
+module.exports = palette;
+})()
+},{"../../color/rgba2css":17,"../../create/2d":18,"../../create/imagedata":19}],17:[function(require,module,exports){var rgba2css = function(rgba){
+ //return "rgba(" + rgba.join(",") + ")"
+ // arraybuffers have no join...
+ return "rgba(" + rgba[0] + "," + rgba[1] + "," + rgba[2] + "," + rgba[3] + ")"
+}
+module.exports = rgba2css;
+},{}],18:[function(require,module,exports){var create2d = function(w, h){
+ var canvas = document.createElement("canvas");
+ canvas.width = w || 0;
+ canvas.height = h || 0;
+ return canvas.getContext("2d");
+};
+
+if (typeof module !== "undefined") module.exports = create2d;
+},{}],19:[function(require,module,exports){var createImageData = function createImageData(w, h){
+ return createImageData.ctx.createImageData(w, h);
+};
+createImageData.ctx = document.createElement("canvas").getContext("2d");
+
+if (typeof module !== "undefined") module.exports = createImageData;
+},{}],15:[function(require,module,exports){//make a function on a gif that determines which frame to show given a timestamp.
+// this allows all gifs to be synced no matter when they are loaded
+// however, this is undesirable if you want to start an animation on frame 0 when it is displayed.
+var makeCurrentFrame = function(){
+ var defaultDelay = 100; // msecs
+
+ if (this.frames.length === 1){ // shortcut for static gifs
+ this.currentFrame = function(){ return 0 };
+ }
+
+ var totalDelay = 0;
+ var delays = [];
+ for(var i=0; i<this.frames.length; i++){
+ var frame = this.frames[i];
+ var delay = ("delay" in frame && frame.delay > 0) ? frame.delay * 10 : defaultDelay;
+ totalDelay += delay;
+ delays.push(totalDelay);
+ }
+
+ this.currentFrame = makeCurrentFrameFunction(delays);
+};
+
+var makeCurrentFrameFunction = function(delays){
+ var totalTime = delays[delays.length - 1];
+ return function(timestamp){
+ var r = (timestamp || Date.now()) % totalTime;
+ for(var i=0; i<delays.length; i++)
+ if (r < delays[i])
+ return i;
+ return i;
+ }
+};
+
+exports.makeCurrentFrame = makeCurrentFrame;
+},{}],16:[function(require,module,exports){var tokenize = require('./string/tokenize');
+var BitView = require('../deps/bitview/bitview.timb.js');
+
+var BinarySpecReader = function(spec){
+ var reader = Object.create(BinarySpecReader.proto);
+ var parts = reader.parts = {};
+
+ for (var partName in spec)
+ parts[partName] = decodeSpecPart(spec[partName]);
+
+ return reader;
+};
+
+BinarySpecReader.proto = {};
+
+// decodes GIF parsing info from the specification
+var decodeSpecPart = function(part){
+
+ var bitSizes = {
+ "bit": 1,
+ "str": 8,
+ "i8": 8,
+ "u8": 8,
+ "i16": 16,
+ "u16": 16,
+ "i32": 32,
+ "u32": 32
+ };
+
+ var parsedSpec = {"fields": []};
+ var size = 0;
+
+ for (var i=0; i<part.length; i++){
+
+ var instruction = tokenize(part[i]);
+ var ignore = /(^|\s)ignore($|\s)/.test(part[i]);
+ var typeInfo = instruction[0].split("["); // eg "uint(16)"
+ var fieldType = typeInfo[0];
+ var fieldLength = parseInt(typeInfo[1] || 1);
+ var isArray = (typeInfo.length > 1);
+ // char(2) is 16 bits. uint(16) is 16 bits
+ var bitSize = bitSizes[fieldType] * fieldLength;
+
+ parsedSpec.fields.push({"name": instruction[1],
+ "type": fieldType,
+ "ignore": ignore,
+ "bitSize": bitSize,
+ "isArray": isArray});
+ size += bitSize;
+ }
+
+ parsedSpec.bitSize = size;
+ parsedSpec.byteSize = size / 8;
+
+ return parsedSpec;
+};
+
+// decodes a chunk according to data types in gif.spec.js
+// todo: rewrite the binary decoding stuff to not be so shit
+BinarySpecReader.proto.decodeBinaryFieldsToJSON = function(partName, cursor, buf){ var reader = this;
+
+ var part = reader.parts[partName];
+
+ var fields = {}, numFields = part.fields.length, bitPos = 0;
+
+ for(var i = 0; i < numFields; i++){
+
+ var field = part.fields[i];
+ if (!field.ignore) {
+ var bitOffset = bitPos % 8;
+ var decodeByteStart = Math.floor((bitPos - bitOffset) / 8);
+ var decodeByteEnd = decodeByteStart + Math.ceil(field.bitSize / 8);
+
+ switch(field.type){
+ case "u8":
+ fields[field.name] = buf.u8[cursor + decodeByteStart]; break;
+ case "i8":
+ fields[field.name] = buf.dv.getInt8(cursor + decodeByteStart); break;
+ case "u16":
+ fields[field.name] = buf.dv.getUint16(cursor + decodeByteStart, true); break;
+ case "i16":
+ fields[field.name] = buf.dv.getInt16(cursor + decodeByteStart, true); break;
+ case "u32":
+ fields[field.name] = buf.dv.getUint32(cursor + decodeByteStart, true); break;
+ case "i32":
+ fields[field.name] = buf.dv.getInt32(cursor + decodeByteStart, true); break;
+ case "str":
+ fields[field.name] = abuf2str(buf.abuf, cursor + decodeByteStart, field.bitSize >> 3); break;
+ case "bit":
+ if (!field.isArray) {
+ fields[field.name] = new BitView(buf.abuf, cursor + decodeByteStart).getBit(bitOffset);
+ } else {
+ var bv = new BitView(buf.abuf, cursor + decodeByteStart);
+ var bits = [];
+ for (var bb=bitOffset; bb<bitOffset+field.bitSize; bb++){
+ bits.push(bv.getBit(bb))
+ }
+ fields[field.name] = bits;
+ }
+ break;
+ // fields[field.name] = abuf2str(buf.abuf, cursor + decodeByteStart, field.bitSize / 8); break;
+ default:
+ console.log("please implement: " + field.type + ", " + partName);
+ }
+ }
+ bitPos += field.bitSize;
+ }
+ //gif.cursor += part.byteSize;
+ //GIF.decode.handleLoadedPart[gif.nextPart](gif, fields);
+ // console.log(fields);
+ return fields
+}
+
+// todo: wasteful of space?
+var abuf2str = function(abuf, offset, len) {
+ return String.fromCharCode.apply(null, new Uint8Array(abuf, offset, len));
+}
+
+
+module.exports = BinarySpecReader;
+},{"./string/tokenize":8,"../deps/bitview/bitview.timb.js":20}],20:[function(require,module,exports){// @timb: edit of https://github.com/kig/bitview.js by Ilmari Heikkinen
+var BitView = function(buf, offset) {
+ this.buffer = buf;
+ this.u8 = new Uint8Array(buf, offset);
+};
+
+BitView.prototype.getBit = function(idx) {
+ var v = this.u8[idx >> 3];
+ var off = idx & 0x7;
+ return (v >> (7-off)) & 1;
+};
+
+module.exports = BitView;
+},{}],6:[function(require,module,exports){var create2d = require('../../create/2d');
+var createImageData = require('../../create/imagedata');
+//import queueOrNextTick from 'lib/nexttick4';
+var nextTick = require('../../nexttick');
+
+var lzwImageData = require('./decode-lzw');
+
+/*
+ this has two rendering types... canvas and webgl
+ both build gif image frames from binary data that was decoded when the gif loaded
+
+ canvas method will build full-frame canvas objects and place them into each frame of the gif.
+ eg, render.canvas(gif) will create gif.frames[0].ctx and so on
+
+ webgl method builds imagedata textures and tries to pack multiple frames efficiently into channels if it can.
+
+ in gifs, each frame stored might just be a rectangle that changed from the previous frame.
+ these are referred to as "raw frames" in this code
+*/
+
+var render = function(gif, config){
+
+ config = config || {};
+
+ var bench = gif.benchmark || false;
+
+ var frameNum = config.frameNum || 0;
+
+ if (frameNum === 0){ // preallocate all frames
+ for (var i=0; i<gif.frames.length; i++){
+ gif.frames[i].ctx = create2d(gif.w, gif.h);
+ gif.buf.pixeldata = new Uint8Array(gif.w * gif.h);
+ }
+ }
+
+ if (frameNum >= gif.frames.length) { // done making frames
+ //gif.tube("progress", "done")
+ gif.rendered = true;
+ gif.tube("rendered");
+ return
+ }
+
+ var frame = gif.frames[frameNum];
+ var pixeldata = gif.buf.pixeldata;
+
+ // gif.percentLoaded = frameNum / gif.frames.length
+ //gif.tube("decompressing frame " + (frameNum+1))
+
+
+
+ // lzw
+ if (bench) bench.start("decompress-lzw");
+ // var pixeldata = new Uint8Array(frame.w * frame.h);
+ //pixeldata.area = frame.w * frame.h;
+ lzwImageData(frame.blockinfo, gif.buf.u8, frame.lzwCodeSize, frame.w, frame.h, pixeldata);
+ if (bench) bench.stop("decompress-lzw");
+
+ // deinterlace
+ if (frame.interlaced) {
+ if (bench) bench.start("deinterlace");
+ pixeldata = deinterlacePixels(pixeldata, frame.w, frame.h)
+ if (bench) bench.stop("deinterlace");
+ }
+
+ // canvas-ize
+ if (bench) bench.start("pixeldata-to-canvas");
+ makeFullFrame(pixeldata, gif, frameNum);
+ if (bench) bench.stop("pixeldata-to-canvas");
+
+ // todo: queue this better
+ var func = render.bind(undefined, gif, {"frameNum": frameNum+1});
+ // queueOrNextTick(func);
+ nextTick(func);
+ // setZeroTimeout(func);
+ //setTimeout(func, 1) // otherwise progress won't show in chrome
+
+};
+
+var makeFullFrame = function(pixeldata, gif, frameNum){
+
+ var frame = gif.frames[frameNum],
+ ctx = frame.ctx;
+
+ if (frameNum === 0){ // don't need previous frame info to do disposal if it's the first frame
+ ctx.putImageData(pixelDataToImageData(pixeldata, gif, frame), frame.x, frame.y,
+ 0,0,frame.w,frame.h);
+ return;
+ }
+
+ var prevFrameNum = frameNum-1,
+ prevFrame = gif.frames[prevFrameNum],
+ prevCanvas = prevFrame.ctx.canvas,
+ rawCtx;
+
+ // disposal method is 0 (unspecified) or 1 (do not dispose)
+ // do nothing, paste new frame image over old one
+ if (prevFrame.disposalMethod === 0 || prevFrame.disposalMethod === 1){
+ rawCtx = makeRawFrameAsContext(gif, frameNum, pixeldata);
+ ctx.drawImage(prevCanvas, 0, 0)
+ ctx.drawImage(rawCtx.canvas, 0,0,frame.w,frame.h, frame.x,frame.y,frame.w,frame.h)
+ }
+
+ // disposal method is 2 (restore to background color)
+ // but everyone just restores to transparency
+ // see notes on http://www.imagemagick.org/Usage/anim_basics/#background
+ if (prevFrame.disposalMethod === 2){
+ // fast path... whole frame cleared
+ if (prevFrame.x === 0 && prevFrame.y === 0 && prevFrame.w === gif.w && prevFrame.h === gif.h) {
+ // var rawContext = makeRawFrameAsContext(gif, frameNum)
+ // frame.context.drawImage(rawContext.canvas, frame.x, frame.y);
+ ctx.putImageData(makeRawFrameAsImageData(gif, frameNum, pixeldata), frame.x, frame.y,
+ 0,0,frame.w,frame.h);
+ } else { // draw the edges of the previous frame and then draw the current frame overtop
+ /*
+ .__________.
+ |__________|
+ | | | |
+ | | | |
+ |_|______|_|
+ |__________|
+ */
+ //top
+ if (prevFrame.y > 0)
+ ctx.drawImage(prevCanvas, 0,0, gif.w,prevFrame.y,
+ 0,0, gif.w,prevFrame.y);
+ //left
+ if (prevFrame.x > 0)
+ ctx.drawImage(prevCanvas, 0,prevFrame.y, prevFrame.x,prevFrame.h,
+ 0,prevFrame.y, prevFrame.x,prevFrame.h);
+ // right
+ if (prevFrame.x+prevFrame.w < gif.w)
+ ctx.drawImage(prevCanvas, prevFrame.x+prevFrame.w, prevFrame.y, (gif.w-prevFrame.x-prevFrame.w),prevFrame.h,
+ prevFrame.x+prevFrame.w, prevFrame.y, (gif.w-prevFrame.x-prevFrame.w),prevFrame.h);
+ // bottom
+ if (prevFrame.y+prevFrame.h < gif.h)
+ ctx.drawImage(prevCanvas, 0,prevFrame.y+prevFrame.h, gif.w,(gif.h-prevFrame.y-prevFrame.h),
+ 0,prevFrame.y+prevFrame.h, gif.w,(gif.h-prevFrame.y-prevFrame.h))
+
+ rawCtx = makeRawFrameAsContext(gif, frameNum, pixeldata);
+ ctx.drawImage(rawCtx.canvas, 0,0,frame.w,frame.h, frame.x,frame.y,frame.w,frame.h);
+
+ }
+ }
+
+ // disposal method is 3 (restore to previous)
+ if (prevFrame.disposalMethod === 3){
+ // look for last previous frame that doesn't have "previous" disposal method
+ while(prevFrameNum > 0 && gif.frames[prevFrameNum].disposalMethod === 3) prevFrameNum -= 1;
+ prevFrame = gif.frames[prevFrameNum]
+ // console.log(prevFrameNum)
+ if (prevFrame.disposalMethod != 3) ctx.drawImage(prevFrame.ctx.canvas, 0, 0)
+ rawCtx = makeRawFrameAsContext(gif, frameNum, pixeldata);
+ ctx.drawImage(rawCtx.canvas, 0,0,frame.w,frame.h, frame.x, frame.y,frame.w,frame.h)
+ }
+
+}
+
+var makeRawFrameAsContext = function(gif, frameNum, pixeldata){
+ // cache a context to reuse
+ if (makeRawFrameAsContext.ctx &&
+ makeRawFrameAsContext.ctx.canvas.width === gif.w &&
+ makeRawFrameAsContext.ctx.canvas.height === gif.h) {
+ var ctx = makeRawFrameAsContext.ctx;
+ } else {
+ var ctx = makeRawFrameAsContext.ctx = create2d(gif.w, gif.h);
+ }
+
+ var frame = gif.frames[frameNum];
+ pixeldata = pixeldata || frame.pixelData;
+ var palette = ("palette" in frame) ? frame.palette : gif.palette;
+ var transparentIndex = ("transparentIndex" in frame) ? frame.transparentIndex : -1;
+
+ if (transparentIndex > -1) palette[(transparentIndex*4)+3] = 0;
+
+ var rawImageData = pixelData2imageData(gif, palette, pixeldata, frame.w, frame.h, transparentIndex);
+
+ ctx.putImageData(rawImageData, 0, 0, 0,0,frame.w,frame.h)
+
+ return ctx
+
+ // return imageData2contextDirty(rawImageData, 0,0,frame.w,frame.h);
+};
+
+var makeRawFrameAsImageData = function(gif, frameNum, pixeldata){
+ var frame = gif.frames[frameNum];
+ pixeldata = pixeldata || frame.pixelData;
+ var palette = ("palette" in frame) ? frame.palette : gif.palette;
+ var transparentIndex = ("transparentIndex" in frame) ? frame.transparentIndex : -1;
+
+ if (transparentIndex > -1) palette[(transparentIndex*4)+3] = 0;
+
+ return pixelData2imageData(gif, palette, pixeldata, frame.w, frame.h, transparentIndex)
+};
+
+var pixelDataToImageData = function(pixeldata, gif, frame){
+ // var frame = gif.frames[frameNum];
+ var palette = ("palette" in frame) ? frame.palette : gif.palette;
+ var transparentIndex = ("transparentIndex" in frame) ? frame.transparentIndex : -1;
+
+ if (transparentIndex > -1) palette[(transparentIndex*4)+3] = 0;
+
+ return pixelData2imageData(gif, palette, pixeldata, frame.w, frame.h, transparentIndex)
+};
+
+
+var imageData2context = function(imageData){
+ var ctx = create2d(imageData.width, imageData.height)
+ ctx.putImageData(imageData, 0, 0)
+ return ctx
+};
+var imageData2contextDirty = function(imageData, dx,dy,dw,dh){
+ var ctx = create2d(imageData.width, imageData.height)
+ ctx.putImageData(imageData, 0, 0, dx,dy,dw,dh)
+ return ctx
+};
+
+var pixelData2imageData = function(gif, palette, pixeldata, w, h, transparentIndex){
+ if (pixelData2imageData.imagedata &&
+ pixelData2imageData.imagedata.width === gif.w &&
+ pixelData2imageData.imagedata.height === gif.h) {
+ var imagedata = pixelData2imageData.imagedata;
+ } else {
+ var imagedata = pixelData2imageData.imagedata = createImageData(gif.w, gif.h);
+ }
+
+ var data = imagedata.data
+ // var i = pixeldata.length;
+ var i = 0;
+
+ for (var y=0; y<h; y++){ var yinc = y*gif.w;
+ for (var x=0; x<w; x++){ //var i = y*gif.w+x;
+ // var cp = i*4
+ var cp = (x + yinc)*4
+ var ct = pixeldata[i]*4 //palette[pixeldata[i]]
+ data[cp] = palette[ct]
+ data[cp+1] = palette[ct+1]
+ data[cp+2] = palette[ct+2]
+ data[cp+3] = palette[ct+3]
+ i += 1;
+ }
+ }
+
+ return imagedata;
+};
+
+// deinterlace the 4 chunks from one imageData blob in one pass
+// see appendix e of gif spec
+//
+// ideas: canvasize first, then deinterlace...
+// and do canvas drawImage calls or putImageData calls to move the rows around?
+var deinterlacePixels = function(oldpixels, w, h){
+
+ var pixels = new Uint8Array(oldpixels.length)
+ var stripes2 = Math.ceil(h/8) // the row where the 2nd interlaced chunk starts
+ var stripes3 = Math.ceil(h/4)
+ var stripes4 = Math.ceil(h/2)
+
+ // pixels[(w*y) +x] = oldpixels[(w*y) +x] <- base calculation
+
+ for(var y=0; y<h; y++){
+ var interlacedRowOffset
+ var rowOffset = w * y
+ if (y % 8 === 0)
+ interlacedRowOffset = w* (y/8)
+ else if ((y + 4) % 8 === 0)
+ interlacedRowOffset = w* ((y-4)/8+stripes2)
+ else if (y % 2 === 0)
+ interlacedRowOffset = w* ((y-2)/4+stripes3)
+ else
+ interlacedRowOffset = w* ((y-1)/2+stripes4)
+ for(var x=0; x<w; x++)
+ pixels[rowOffset+x] = oldpixels[interlacedRowOffset+x]
+ }
+
+ return pixels;
+};
+
+module.exports = render;
+},{"../../create/2d":18,"../../create/imagedata":19,"../../nexttick":11,"./decode-lzw":21}],21:[function(require,module,exports){// looked at imagemagick implementation instead of trying to figure out my buggy one
+
+/*
+ decodes an lzw compressed gif frame
+ mostly based on imagemagick's implementation
+
+ data, binary string
+ data_size, lzw compression code size
+ w, h, width and height of image
+ paletteRemap is an optional array of different palette indexes to use
+
+ returns typed array of palette index values
+
+ this assumes we have the entire data for the frame buffered...
+ its possible to modify this so that a resumable state could be passed in, but prob not necessary
+*/
+// TODO: cache stacks?
+var lzwImageData = function(blockinfo, u8buf, data_size, w, h, pixels, paletteRemap){
+ var MaxStack = 4096;
+ var NullCode = -1;
+ var cursor = 0
+ var npix = w * h;
+ var code, in_code, old_code;
+ var first = 0;
+ var top = 0;
+ var pi = 0
+ var pixels = pixels || new Uint8Array(npix);
+ var prefix = new Uint16Array(MaxStack*2) //Uint16Array(MaxStack*1.5) <-- this makes a nice glitch
+ var suffix = new Uint8Array(MaxStack)
+ // var prefix = lzwImageData.prefix;
+ // var suffix = lzwImageData.suffix;
+ var pixelStack = new Uint8Array(MaxStack+1)
+
+ // Initialize GIF data stream decoder.
+ var clear = 1 << data_size;
+ var end_of_information = clear + 1;
+ var available = clear + 2;
+ old_code = NullCode;
+ var code_size = data_size + 1;
+ var code_mask = (1 << code_size) - 1;
+ for (code = 0; code < clear; code++){
+ prefix[code] = 0;
+ suffix[code] = code;
+ }
+
+ var bits = 0; var datum = 0;
+ var dvcursor = blockinfo.start;
+ var blockEnds = blockinfo.blockEnds;
+ var blockEnd = blockEnds.shift();
+
+ // Decode GIF pixel stream.
+ for (var i = 0; i < npix;){
+ if (top === 0){
+
+ if (bits < code_size){
+
+ datum += u8buf[dvcursor] << bits;
+
+ bits += 8;
+
+ dvcursor++;
+ if (dvcursor === blockEnd){
+ dvcursor += 1;
+ blockEnd = blockEnds.shift();
+ }
+
+ continue;
+
+ }
+ code = datum & code_mask;
+ datum >>= code_size;
+ bits -= code_size;
+
+ if (code > available) {
+ console.log(":(");
+ // console.log(num2bin(code));
+ // break;
+ } // if we get here something bad happened ;(
+ if (code === end_of_information) { console.log("fuck"); break};
+ if (code === clear) {
+ code_size = data_size + 1;
+ code_mask = (1 << code_size) - 1;
+ available = clear + 2;
+ old_code = NullCode;
+ continue;
+ }
+ if (old_code === NullCode){
+ pixelStack[top++] = suffix[code];
+ old_code = code;
+ first = code;
+ continue;
+ }
+ in_code = code;
+ if (code === available){
+ pixelStack[top++] = first;
+ code = old_code;
+ }
+ while (code > clear){
+ pixelStack[top++] = suffix[code];
+ code = prefix[code];
+ }
+ first = (suffix[code]) // & 0xff;
+ // ^^ timb: not needed?
+ // Add a new string to the string table,
+ if (available >= MaxStack) { /*console.log("maxstack!");*/ /*break;*/ }
+ pixelStack[top++] = first;
+ prefix[available] = old_code;
+ suffix[available] = first;
+ available++;
+ /* why does == have higher precedence than & ?
+ i just looked it up and i think it is because javascript
+ has basically the same precedence as C and
+ "once upon a time, C didn't have the logical operators && and ||,
+ and the bitwise operators & and | did double duty."
+ */
+ if ((available & code_mask) === 0 && available < MaxStack){
+ code_size++;
+ code_mask += available;
+ }
+ old_code = in_code;
+ }
+
+ top--;
+ if (paletteRemap)
+ pixels[pi++] = paletteRemap[pixelStack[top]];
+ else
+ pixels[pi++] = pixelStack[top];
+ i++;
+ }
+
+ // not needed: typed arrays init'd to 0 already
+ // for (i = pi, len=pixels.length; i < len; i++)
+ // pixels[i] = 0; // clear missing pixels
+
+
+ return pixels
+};
+
+var MaxStack = 4096;
+// lzwImageData.prefix = new Uint16Array(MaxStack*2)
+// lzwImageData.suffix = new Uint8Array(MaxStack)
+
+module.exports = lzwImageData;
+},{}]},{},[1]);
diff --git a/gradient.jpg b/gradient.jpg
new file mode 100644
index 0000000..8aad001
--- /dev/null
+++ b/gradient.jpg
Binary files differ
diff --git a/halftone.html b/halftone.html
new file mode 100644
index 0000000..17f7ca8
--- /dev/null
+++ b/halftone.html
@@ -0,0 +1,48 @@
+<!doctype html>
+<html>
+<head>
+<title>Halftone</title>
+</head>
+<body>
+<div>
+radius <input type="range" min="4" max="20" step="1" value="5" id="radius">
+angle <input type="range" min="0" max="90" step="1" value="45" id="angle">
+</div>
+<div id="images"></div>
+</body>
+<script type="text/javascript" src="canvasquery.js"></script>
+<script type="text/javascript" src="canvasquery.dither.js"></script>
+<script type="text/javascript">
+var urls = ["abyss.png","building.png","gradient.jpg"]
+var imgs = []
+var complete = 0
+urls.forEach(function(src){
+ var img = new Image ()
+ img.onload = ready
+ img.src = src
+ imgs.push(img)
+ if (img.complete) ready()
+})
+function ready(){
+ complete += 1
+ if (complete < imgs.length) return;
+ document.getElementById("radius").onchange = build
+ document.getElementById("angle").onchange = build
+ build()
+}
+function build(){
+ document.getElementById("images").innerHTML = ""
+ imgs.forEach(dither)
+}
+function dither(img){
+ var w = img.naturalWidth, h = img.naturalHeight;
+ var cc = cq(w, h)
+ var radius = parseFloat(document.getElementById("radius").value)
+ var angle = parseFloat(document.getElementById("angle").value)
+ cc.drawImage(img, 0, 0, w, h);
+ cc.halftone( radius, angle )
+ cc.appendTo("#images")
+}
+</script>
+</html>
+
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..18dfc3a
--- /dev/null
+++ b/index.html
@@ -0,0 +1,61 @@
+<!doctype html>
+<html>
+<head>
+<title>Dither</title>
+</head>
+<body>
+<div>
+<button id="random">random</button>
+<button id="pattern2">pattern2</button>
+<button id="pattern3">pattern3</button>
+<button id="pattern4">pattern4</button>
+<button id="pattern2Lite">pattern2lite</button>
+<button id="pattern3Lite">pattern3lite</button>
+<button id="pattern4Lite">pattern4lite</button>
+<button id="floydSteinberg">floyd-steinberg</button>
+<button id="right">right</button>
+</div>
+<div id="images"></div>
+</body>
+<script type="text/javascript" src="canvasquery.js"></script>
+<script type="text/javascript" src="canvasquery.dither.js"></script>
+<script type="text/javascript">
+var algo = 'random';
+var urls = ["abyss.png","building.png","gradient.jpg"]
+var imgs = []
+var complete = 0
+urls.forEach(function(src){
+ var img = new Image ()
+ img.onload = ready
+ img.src = src
+ imgs.push(img)
+ if (img.complete) ready()
+})
+function ready(){
+ complete += 1
+ if (complete < imgs.length) return;
+ var buttons = document.getElementsByTagName("button")
+ for (var i = 0; i < buttons.length; i++) {
+ (function(n){
+ buttons[n].onclick = function(){
+ algo = buttons[n].id;
+ build()
+ }
+ })(i)
+ }
+ build()
+}
+function build(){
+ document.getElementById("images").innerHTML = ""
+ imgs.forEach(dither)
+}
+function dither(img){
+ var w = img.naturalWidth, h = img.naturalHeight;
+ var cc = cq(w, h)
+ cc.drawImage(img, 0, 0, w, h);
+ cc[algo + "Dither"]( )
+ cc.appendTo("#images")
+}
+</script>
+</html>
+
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..ccaf174
--- /dev/null
+++ b/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "okfocus.github.io",
+ "version": "0.0.4",
+ "devDependencies": {
+ "grunt": "~0.4.1",
+ "grunt-contrib-concat": "~0.3.0",
+ "grunt-contrib-uglify": "~0.2.5",
+ "grunt-contrib-watch": "~0.5.3"
+ }
+}
diff --git a/pattern.html b/pattern.html
new file mode 100644
index 0000000..18dfc3a
--- /dev/null
+++ b/pattern.html
@@ -0,0 +1,61 @@
+<!doctype html>
+<html>
+<head>
+<title>Dither</title>
+</head>
+<body>
+<div>
+<button id="random">random</button>
+<button id="pattern2">pattern2</button>
+<button id="pattern3">pattern3</button>
+<button id="pattern4">pattern4</button>
+<button id="pattern2Lite">pattern2lite</button>
+<button id="pattern3Lite">pattern3lite</button>
+<button id="pattern4Lite">pattern4lite</button>
+<button id="floydSteinberg">floyd-steinberg</button>
+<button id="right">right</button>
+</div>
+<div id="images"></div>
+</body>
+<script type="text/javascript" src="canvasquery.js"></script>
+<script type="text/javascript" src="canvasquery.dither.js"></script>
+<script type="text/javascript">
+var algo = 'random';
+var urls = ["abyss.png","building.png","gradient.jpg"]
+var imgs = []
+var complete = 0
+urls.forEach(function(src){
+ var img = new Image ()
+ img.onload = ready
+ img.src = src
+ imgs.push(img)
+ if (img.complete) ready()
+})
+function ready(){
+ complete += 1
+ if (complete < imgs.length) return;
+ var buttons = document.getElementsByTagName("button")
+ for (var i = 0; i < buttons.length; i++) {
+ (function(n){
+ buttons[n].onclick = function(){
+ algo = buttons[n].id;
+ build()
+ }
+ })(i)
+ }
+ build()
+}
+function build(){
+ document.getElementById("images").innerHTML = ""
+ imgs.forEach(dither)
+}
+function dither(img){
+ var w = img.naturalWidth, h = img.naturalHeight;
+ var cc = cq(w, h)
+ cc.drawImage(img, 0, 0, w, h);
+ cc[algo + "Dither"]( )
+ cc.appendTo("#images")
+}
+</script>
+</html>
+
diff --git a/proxy.py b/proxy.py
new file mode 100644
index 0000000..ff9b0ea
--- /dev/null
+++ b/proxy.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+import http.server
+import urllib.request
+
+class MyHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
+ def end_headers(self):
+ self.send_my_headers()
+
+ http.server.SimpleHTTPRequestHandler.end_headers(self)
+
+ def send_my_headers(self):
+ self.send_header("Access-Control-Allow-Origin", "*")
+
+ def do_GET(self):
+ if self.path[0:14] == "/cgi-bin/proxy":
+ self.copyfile(urllib.request.urlopen(self.path[15:]), self.wfile)
+ else:
+ super().do_GET()
+
+if __name__ == '__main__':
+ http.server.test(HandlerClass=MyHTTPRequestHandler)
+
diff --git a/shader.html b/shader.html
new file mode 100644
index 0000000..bc6b9a2
--- /dev/null
+++ b/shader.html
@@ -0,0 +1,35 @@
+<!doctype html>
+<html>
+<head>
+<title>Shader</title>
+</head>
+<body>
+<div>
+<button id="random">random</button>
+<button id="pattern2">pattern2</button>
+<button id="pattern3">pattern3</button>
+<button id="pattern4">pattern4</button>
+<button id="pattern2Lite">pattern2lite</button>
+<button id="pattern3Lite">pattern3lite</button>
+<button id="pattern4Lite">pattern4lite</button>
+<button id="floydSteinberg">floyd-steinberg</button>
+<button id="right">right</button>
+</div>
+w/h <input type="text" id="width">x<input type="text" id="height">
+<textarea name="shader">
+</textarea>
+</body>
+<script type="text/javascript" src="canvasquery.js"></script>
+<script type="text/javascript" src="canvasquery.dither.js"></script>
+<script type="text/javascript">
+
+function dither(img){
+ var w = img.naturalWidth, h = img.naturalHeight;
+ var cc = cq(w, h)
+ cc.drawImage(img, 0, 0, w, h);
+ cc[algo + "Dither"]( )
+ cc.appendTo("#images")
+}
+</script>
+</html>
+
diff --git a/threshold.html b/threshold.html
new file mode 100644
index 0000000..f3cdc76
--- /dev/null
+++ b/threshold.html
@@ -0,0 +1,46 @@
+<!doctype html>
+<html>
+<head>
+<title>Threshold</title>
+</head>
+<body>
+<div>
+threshold <input type="range" min="0" max="1" step="0.01" value="0.5" id="threshold">
+</div>
+<div id="images"></div>
+</body>
+<script type="text/javascript" src="canvasquery.js"></script>
+<script type="text/javascript" src="canvasquery.dither.js"></script>
+<script type="text/javascript">
+var urls = ["abyss.png","building.png","gradient.jpg"]
+var imgs = []
+var complete = 0
+urls.forEach(function(src){
+ var img = new Image ()
+ img.onload = ready
+ img.src = src
+ imgs.push(img)
+ if (img.complete) ready()
+})
+function ready(){
+ complete += 1
+ if (complete < imgs.length) return;
+ document.getElementById("threshold").onchange = build
+ build()
+}
+function build(){
+ document.getElementById("images").innerHTML = ""
+ imgs.forEach(dither)
+}
+function dither(img){
+ var w = img.naturalWidth, h = img.naturalHeight;
+ var cc = cq(w, h)
+ var thresh = parseFloat(document.getElementById("threshold").value)
+ console.log(thresh)
+ cc.drawImage(img, 0, 0, w, h);
+ cc.threshold( thresh )
+ cc.appendTo("#images")
+}
+</script>
+</html>
+
diff --git a/vendor/FileSaver/.bower.json b/vendor/FileSaver/.bower.json
new file mode 100644
index 0000000..586a935
--- /dev/null
+++ b/vendor/FileSaver/.bower.json
@@ -0,0 +1,15 @@
+{
+ "name": "FileSaver",
+ "main": "./FileSaver.js",
+ "dependencies": {},
+ "homepage": "https://github.com/eligrey/FileSaver.js",
+ "_release": "1f4b2f1724",
+ "_resolution": {
+ "type": "branch",
+ "branch": "master",
+ "commit": "1f4b2f1724b19ddaf5eae3d1523f7a8ae9cbf812"
+ },
+ "_source": "git://github.com/eligrey/FileSaver.js.git",
+ "_target": "*",
+ "_originalSource": "FileSaver"
+} \ No newline at end of file
diff --git a/vendor/FileSaver/FileSaver.js b/vendor/FileSaver/FileSaver.js
new file mode 100644
index 0000000..378a9dc
--- /dev/null
+++ b/vendor/FileSaver/FileSaver.js
@@ -0,0 +1,232 @@
+/* FileSaver.js
+ * A saveAs() FileSaver implementation.
+ * 2013-10-21
+ *
+ * By Eli Grey, http://eligrey.com
+ * License: X11/MIT
+ * See LICENSE.md
+ */
+
+/*global self */
+/*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true,
+ plusplus: true */
+
+/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */
+
+var saveAs = saveAs
+ || (typeof navigator !== 'undefined' && navigator.msSaveOrOpenBlob && navigator.msSaveOrOpenBlob.bind(navigator))
+ || (function(view) {
+ "use strict";
+ var
+ doc = view.document
+ // only get URL when necessary in case BlobBuilder.js hasn't overridden it yet
+ , get_URL = function() {
+ return view.URL || view.webkitURL || view;
+ }
+ , URL = view.URL || view.webkitURL || view
+ , save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a")
+ , can_use_save_link = !view.externalHost && "download" in save_link
+ , click = function(node) {
+ var event = doc.createEvent("MouseEvents");
+ event.initMouseEvent(
+ "click", true, false, view, 0, 0, 0, 0, 0
+ , false, false, false, false, 0, null
+ );
+ node.dispatchEvent(event);
+ }
+ , webkit_req_fs = view.webkitRequestFileSystem
+ , req_fs = view.requestFileSystem || webkit_req_fs || view.mozRequestFileSystem
+ , throw_outside = function (ex) {
+ (view.setImmediate || view.setTimeout)(function() {
+ throw ex;
+ }, 0);
+ }
+ , force_saveable_type = "application/octet-stream"
+ , fs_min_size = 0
+ , deletion_queue = []
+ , process_deletion_queue = function() {
+ var i = deletion_queue.length;
+ while (i--) {
+ var file = deletion_queue[i];
+ if (typeof file === "string") { // file is an object URL
+ URL.revokeObjectURL(file);
+ } else { // file is a File
+ file.remove();
+ }
+ }
+ deletion_queue.length = 0; // clear queue
+ }
+ , dispatch = function(filesaver, event_types, event) {
+ event_types = [].concat(event_types);
+ var i = event_types.length;
+ while (i--) {
+ var listener = filesaver["on" + event_types[i]];
+ if (typeof listener === "function") {
+ try {
+ listener.call(filesaver, event || filesaver);
+ } catch (ex) {
+ throw_outside(ex);
+ }
+ }
+ }
+ }
+ , FileSaver = function(blob, name) {
+ // First try a.download, then web filesystem, then object URLs
+ var
+ filesaver = this
+ , type = blob.type
+ , blob_changed = false
+ , object_url
+ , target_view
+ , get_object_url = function() {
+ var object_url = get_URL().createObjectURL(blob);
+ deletion_queue.push(object_url);
+ return object_url;
+ }
+ , dispatch_all = function() {
+ dispatch(filesaver, "writestart progress write writeend".split(" "));
+ }
+ // on any filesys errors revert to saving with object URLs
+ , fs_error = function() {
+ // don't create more object URLs than needed
+ if (blob_changed || !object_url) {
+ object_url = get_object_url(blob);
+ }
+ if (target_view) {
+ target_view.location.href = object_url;
+ } else {
+ window.open(object_url, "_blank");
+ }
+ filesaver.readyState = filesaver.DONE;
+ dispatch_all();
+ }
+ , abortable = function(func) {
+ return function() {
+ if (filesaver.readyState !== filesaver.DONE) {
+ return func.apply(this, arguments);
+ }
+ };
+ }
+ , create_if_not_found = {create: true, exclusive: false}
+ , slice
+ ;
+ filesaver.readyState = filesaver.INIT;
+ if (!name) {
+ name = "download";
+ }
+ if (can_use_save_link) {
+ object_url = get_object_url(blob);
+ // FF for Android has a nasty garbage collection mechanism
+ // that turns all objects that are not pure javascript into 'deadObject'
+ // this means `doc` and `save_link` are unusable and need to be recreated
+ // `view` is usable though:
+ doc = view.document;
+ save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a");
+ save_link.href = object_url;
+ save_link.download = name;
+ var event = doc.createEvent("MouseEvents");
+ event.initMouseEvent(
+ "click", true, false, view, 0, 0, 0, 0, 0
+ , false, false, false, false, 0, null
+ );
+ save_link.dispatchEvent(event);
+ filesaver.readyState = filesaver.DONE;
+ dispatch_all();
+ return;
+ }
+ // Object and web filesystem URLs have a problem saving in Google Chrome when
+ // viewed in a tab, so I force save with application/octet-stream
+ // http://code.google.com/p/chromium/issues/detail?id=91158
+ if (view.chrome && type && type !== force_saveable_type) {
+ slice = blob.slice || blob.webkitSlice;
+ blob = slice.call(blob, 0, blob.size, force_saveable_type);
+ blob_changed = true;
+ }
+ // Since I can't be sure that the guessed media type will trigger a download
+ // in WebKit, I append .download to the filename.
+ // https://bugs.webkit.org/show_bug.cgi?id=65440
+ if (webkit_req_fs && name !== "download") {
+ name += ".download";
+ }
+ if (type === force_saveable_type || webkit_req_fs) {
+ target_view = view;
+ }
+ if (!req_fs) {
+ fs_error();
+ return;
+ }
+ fs_min_size += blob.size;
+ req_fs(view.TEMPORARY, fs_min_size, abortable(function(fs) {
+ fs.root.getDirectory("saved", create_if_not_found, abortable(function(dir) {
+ var save = function() {
+ dir.getFile(name, create_if_not_found, abortable(function(file) {
+ file.createWriter(abortable(function(writer) {
+ writer.onwriteend = function(event) {
+ target_view.location.href = file.toURL();
+ deletion_queue.push(file);
+ filesaver.readyState = filesaver.DONE;
+ dispatch(filesaver, "writeend", event);
+ };
+ writer.onerror = function() {
+ var error = writer.error;
+ if (error.code !== error.ABORT_ERR) {
+ fs_error();
+ }
+ };
+ "writestart progress write abort".split(" ").forEach(function(event) {
+ writer["on" + event] = filesaver["on" + event];
+ });
+ writer.write(blob);
+ filesaver.abort = function() {
+ writer.abort();
+ filesaver.readyState = filesaver.DONE;
+ };
+ filesaver.readyState = filesaver.WRITING;
+ }), fs_error);
+ }), fs_error);
+ };
+ dir.getFile(name, {create: false}, abortable(function(file) {
+ // delete file if it already exists
+ file.remove();
+ save();
+ }), abortable(function(ex) {
+ if (ex.code === ex.NOT_FOUND_ERR) {
+ save();
+ } else {
+ fs_error();
+ }
+ }));
+ }), fs_error);
+ }), fs_error);
+ }
+ , FS_proto = FileSaver.prototype
+ , saveAs = function(blob, name) {
+ return new FileSaver(blob, name);
+ }
+ ;
+ FS_proto.abort = function() {
+ var filesaver = this;
+ filesaver.readyState = filesaver.DONE;
+ dispatch(filesaver, "abort");
+ };
+ FS_proto.readyState = FS_proto.INIT = 0;
+ FS_proto.WRITING = 1;
+ FS_proto.DONE = 2;
+
+ FS_proto.error =
+ FS_proto.onwritestart =
+ FS_proto.onprogress =
+ FS_proto.onwrite =
+ FS_proto.onabort =
+ FS_proto.onerror =
+ FS_proto.onwriteend =
+ null;
+
+ view.addEventListener("unload", process_deletion_queue, false);
+ return saveAs;
+}(this.self || this.window || this.content));
+// `self` is undefined in Firefox for Android content script context
+// while `this` is nsIContentFrameMessageManager
+// with an attribute `content` that corresponds to the window
+
+if (typeof module !== 'undefined') module.exports = saveAs;
diff --git a/vendor/FileSaver/LICENSE.md b/vendor/FileSaver/LICENSE.md
new file mode 100644
index 0000000..7eb56b9
--- /dev/null
+++ b/vendor/FileSaver/LICENSE.md
@@ -0,0 +1,30 @@
+This software is licensed under the MIT/X11 license.
+
+MIT/X11 license
+---------------
+
+Copyright &copy; 2011 [Eli Grey][1].
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+
+ [1]: http://eligrey.com \ No newline at end of file
diff --git a/vendor/FileSaver/README.md b/vendor/FileSaver/README.md
new file mode 100644
index 0000000..49aff62
--- /dev/null
+++ b/vendor/FileSaver/README.md
@@ -0,0 +1,78 @@
+FileSaver.js
+============
+
+FileSaver.js implements the HTML5 W3C `saveAs()` [FileSaver][1] interface in browsers that do
+not natively support it. There is a [FileSaver.js demo][2] that demonstrates saving
+various media types.
+
+FileSaver.js is the solution to saving files on the client-side, and is perfect for
+webapps that need to generate files, or for saving sensitive information that shouldn't be
+sent to an external server.
+
+Looking for `canvas.toBlob()` for saving canvases? Check out
+[canvas-toBlob.js](https://github.com/eligrey/canvas-toBlob.js) for a cross-browser implementation.
+
+Supported Browsers
+------------------
+
+| Browser | Constructs as | Filenames | Max Blob Size | Dependencies |
+| -------------- | ------------- | ------------ | ------------- | ------------ |
+| Firefox 20+ | Blob | Yes | 800MiB | None |
+| Firefox ≤ 19 | data: URI | No | n/a | [Blob.js](https://github.com/eligrey/Blob.js) |
+| Chrome | Blob | Yes | 345MiB | None |
+| Chrome for Android | Blob | Yes | ? | None |
+| IE 10+ | Blob | Yes | 600MiB | None |
+| Opera Next | Blob | Yes | ? | None |
+| Opera < 15 | data: URI | No | n/a | [Blob.js](https://github.com/eligrey/Blob.js) |
+| Safari 6.1+ | Blob | No | ? | None |
+| Safari < 6 | data: URI | No | n/a | [Blob.js](https://github.com/eligrey/Blob.js) |
+
+Feature detection is possible:
+
+ try { var isFileSaverSupported = !!new Blob(); } catch(e){}
+
+Syntax
+------
+
+ FileSaver saveAs(in Blob data, in DOMString filename)
+
+Examples
+--------
+
+### Saving text
+
+ var blob = new Blob(["Hello, world!"], {type: "text/plain;charset=utf-8"});
+ saveAs(blob, "hello world.txt");
+
+The standard W3C File API [`Blob`][3] interface is not available in all browsers.
+[Blob.js][4] is a cross-browser `Blob` implementation that solves this.
+
+### Saving a canvas
+
+ var canvas = document.getElementById("my-canvas"), ctx = canvas.getContext("2d");
+ // draw to canvas...
+ canvas.toBlob(function(blob) {
+ saveAs(blob, "pretty image.png");
+ });
+
+Note: The standard HTML5 `canvas.toBlob()` method is not available in all browsers.
+[canvas-toBlob.js][5] is a cross-browser `canvas.toBlob()` that polyfills this.
+
+### Aborting a save
+
+ var filesaver = saveAs(blob, "whatever");
+ cancel_button.addEventListener("click", function() {
+ if (filesaver.abort) {
+ filesaver.abort();
+ }
+ }, false);
+
+This isn't that useful unless you're saving very large files (e.g. generated video).
+
+![Tracking image](https://in.getclicky.com/212712ns.gif)
+
+ [1]: http://www.w3.org/TR/file-writer-api/#the-filesaver-interface
+ [2]: http://eligrey.com/demos/FileSaver.js/
+ [3]: https://developer.mozilla.org/en-US/docs/DOM/Blob
+ [4]: https://github.com/eligrey/Blob.js
+ [5]: https://github.com/eligrey/canvas-toBlob.js
diff --git a/vendor/FileSaver/bower.json b/vendor/FileSaver/bower.json
new file mode 100644
index 0000000..0e0d7e0
--- /dev/null
+++ b/vendor/FileSaver/bower.json
@@ -0,0 +1,7 @@
+{
+ "name": "FileSaver",
+ "version": "1.0.0",
+ "main": "./FileSaver.js",
+ "dependencies": {
+ }
+}
diff --git a/vendor/FileSaver/demo/demo.css b/vendor/FileSaver/demo/demo.css
new file mode 100644
index 0000000..fe03ca5
--- /dev/null
+++ b/vendor/FileSaver/demo/demo.css
@@ -0,0 +1,55 @@
+html {
+ background-color: #DDD;
+}
+body {
+ width: 900px;
+ margin: 0 auto;
+ font-family: Verdana, Helvetica, Arial, sans-serif;
+ box-shadow: 0 0 5px #000;
+ box-shadow: 0 0 10px 2px rgba(0, 0, 0, .5);
+ padding: 7px 25px 70px;
+ background-color: #FFF;
+}
+h1, h2, h3, h4, h5, h6 {
+ font-family: Georgia, "Times New Roman", serif;
+}
+h2, form {
+ text-align: center;
+}
+form {
+ margin-top: 5px;
+}
+.input {
+ width: 500px;
+ height: 300px;
+ margin: 0 auto;
+ display: block;
+}
+section {
+ margin-top: 40px;
+}
+dt {
+ font-weight: bold;
+ font-size: larger;
+}
+#canvas {
+ cursor: crosshair;
+}
+#canvas, #html {
+ border: 1px solid black;
+}
+.filename {
+ text-align: right;
+}
+#html {
+ box-sizing: border-box;
+ ms-box-sizing: border-box;
+ webkit-box-sizing: border-box;
+ moz-box-sizing: border-box;
+ overflow: auto;
+ padding: 1em;
+}
+dt:target {
+ background-color: Highlight;
+ color: HighlightText;
+}
diff --git a/vendor/FileSaver/demo/demo.js b/vendor/FileSaver/demo/demo.js
new file mode 100644
index 0000000..e08497b
--- /dev/null
+++ b/vendor/FileSaver/demo/demo.js
@@ -0,0 +1,213 @@
+/* FileSaver.js demo script
+ * 2012-01-23
+ *
+ * By Eli Grey, http://eligrey.com
+ * License: X11/MIT
+ * See LICENSE.md
+ */
+
+/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/demo/demo.js */
+
+(function(view) {
+"use strict";
+// The canvas drawing portion of the demo is based off the demo at
+// http://www.williammalone.com/articles/create-html5-canvas-javascript-drawing-app/
+var
+ document = view.document
+ , $ = function(id) {
+ return document.getElementById(id);
+ }
+ , session = view.sessionStorage
+ // only get URL when necessary in case Blob.js hasn't defined it yet
+ , get_blob = function() {
+ return view.Blob;
+ }
+
+ , canvas = $("canvas")
+ , canvas_options_form = $("canvas-options")
+ , canvas_filename = $("canvas-filename")
+ , canvas_clear_button = $("canvas-clear")
+
+ , text = $("text")
+ , text_options_form = $("text-options")
+ , text_filename = $("text-filename")
+
+ , html = $("html")
+ , html_options_form = $("html-options")
+ , html_filename = $("html-filename")
+
+ , ctx = canvas.getContext("2d")
+ , drawing = false
+ , x_points = session.x_points || []
+ , y_points = session.y_points || []
+ , drag_points = session.drag_points || []
+ , add_point = function(x, y, dragging) {
+ x_points.push(x);
+ y_points.push(y);
+ drag_points.push(dragging);
+ }
+ , draw = function(){
+ canvas.width = canvas.width;
+ ctx.lineWidth = 6;
+ ctx.lineJoin = "round";
+ ctx.strokeStyle = "#000000";
+ var
+ i = 0
+ , len = x_points.length
+ ;
+ for(; i < len; i++) {
+ ctx.beginPath();
+ if (i && drag_points[i]) {
+ ctx.moveTo(x_points[i-1], y_points[i-1]);
+ } else {
+ ctx.moveTo(x_points[i]-1, y_points[i]);
+ }
+ ctx.lineTo(x_points[i], y_points[i]);
+ ctx.closePath();
+ ctx.stroke();
+ }
+ }
+ , stop_drawing = function() {
+ drawing = false;
+ }
+
+ // Title guesser and document creator available at https://gist.github.com/1059648
+ , guess_title = function(doc) {
+ var
+ h = "h6 h5 h4 h3 h2 h1".split(" ")
+ , i = h.length
+ , headers
+ , header_text
+ ;
+ while (i--) {
+ headers = doc.getElementsByTagName(h[i]);
+ for (var j = 0, len = headers.length; j < len; j++) {
+ header_text = headers[j].textContent.trim();
+ if (header_text) {
+ return header_text;
+ }
+ }
+ }
+ }
+ , doc_impl = document.implementation
+ , create_html_doc = function(html) {
+ var
+ dt = doc_impl.createDocumentType('html', null, null)
+ , doc = doc_impl.createDocument("http://www.w3.org/1999/xhtml", "html", dt)
+ , doc_el = doc.documentElement
+ , head = doc_el.appendChild(doc.createElement("head"))
+ , charset_meta = head.appendChild(doc.createElement("meta"))
+ , title = head.appendChild(doc.createElement("title"))
+ , body = doc_el.appendChild(doc.createElement("body"))
+ , i = 0
+ , len = html.childNodes.length
+ ;
+ charset_meta.setAttribute("charset", html.ownerDocument.characterSet);
+ for (; i < len; i++) {
+ body.appendChild(doc.importNode(html.childNodes.item(i), true));
+ }
+ var title_text = guess_title(doc);
+ if (title_text) {
+ title.appendChild(doc.createTextNode(title_text));
+ }
+ return doc;
+ }
+;
+canvas.width = 500;
+canvas.height = 300;
+
+ if (typeof x_points === "string") {
+ x_points = JSON.parse(x_points);
+} if (typeof y_points === "string") {
+ y_points = JSON.parse(y_points);
+} if (typeof drag_points === "string") {
+ drag_points = JSON.parse(drag_points);
+} if (session.canvas_filename) {
+ canvas_filename.value = session.canvas_filename;
+} if (session.text) {
+ text.value = session.text;
+} if (session.text_filename) {
+ text_filename.value = session.text_filename;
+} if (session.html) {
+ html.innerHTML = session.html;
+} if (session.html_filename) {
+ html_filename.value = session.html_filename;
+}
+
+drawing = true;
+draw();
+drawing = false;
+
+canvas_clear_button.addEventListener("click", function() {
+ canvas.width = canvas.width;
+ x_points.length =
+ y_points.length =
+ drag_points.length =
+ 0;
+}, false);
+canvas.addEventListener("mousedown", function(event) {
+ event.preventDefault();
+ drawing = true;
+ add_point(event.pageX - canvas.offsetLeft, event.pageY - canvas.offsetTop, false);
+ draw();
+}, false);
+canvas.addEventListener("mousemove", function(event) {
+ if (drawing) {
+ add_point(event.pageX - canvas.offsetLeft, event.pageY - canvas.offsetTop, true);
+ draw();
+ }
+}, false);
+canvas.addEventListener("mouseup", stop_drawing, false);
+canvas.addEventListener("mouseout", stop_drawing, false);
+
+canvas_options_form.addEventListener("submit", function(event) {
+ event.preventDefault();
+ canvas.toBlob(function(blob) {
+ saveAs(
+ blob
+ , (canvas_filename.value || canvas_filename.placeholder) + ".png"
+ );
+ }, "image/png");
+}, false);
+
+text_options_form.addEventListener("submit", function(event) {
+ event.preventDefault();
+ var BB = get_blob();
+ saveAs(
+ new BB(
+ [text.value || text.placeholder]
+ , {type: "text/plain;charset=" + document.characterSet}
+ )
+ , (text_filename.value || text_filename.placeholder) + ".txt"
+ );
+}, false);
+
+html_options_form.addEventListener("submit", function(event) {
+ event.preventDefault();
+ var
+ BB = get_blob()
+ , xml_serializer = new XMLSerializer
+ , doc = create_html_doc(html)
+ ;
+ saveAs(
+ new BB(
+ [xml_serializer.serializeToString(doc)]
+ , {type: "application/xhtml+xml;charset=" + document.characterSet}
+ )
+ , (html_filename.value || html_filename.placeholder) + ".xhtml"
+ );
+}, false);
+
+view.addEventListener("unload", function() {
+ session.x_points = JSON.stringify(x_points);
+ session.y_points = JSON.stringify(y_points);
+ session.drag_points = JSON.stringify(drag_points);
+ session.canvas_filename = canvas_filename.value;
+
+ session.text = text.value;
+ session.text_filename = text_filename.value;
+
+ session.html = html.innerHTML;
+ session.html_filename = html_filename.value;
+}, false);
+}(self));
diff --git a/vendor/FileSaver/demo/demo.min.js b/vendor/FileSaver/demo/demo.min.js
new file mode 100644
index 0000000..77f9ed1
--- /dev/null
+++ b/vendor/FileSaver/demo/demo.min.js
@@ -0,0 +1,2 @@
+/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/demo/demo.js */
+(function(n){"use strict";var s=n.document,g=function(A){return s.getElementById(A)},b=n.sessionStorage,x=function(){return n.Blob},f=g("canvas"),r=g("canvas-options"),y=g("canvas-filename"),p=g("canvas-clear"),q=g("text"),t=g("text-options"),h=g("text-filename"),m=g("html"),e=g("html-options"),i=g("html-filename"),u=f.getContext("2d"),z=false,a=b.x_points||[],o=b.y_points||[],d=b.drag_points||[],j=function(A,C,B){a.push(A);o.push(C);d.push(B)},l=function(){f.width=f.width;u.lineWidth=6;u.lineJoin="round";u.strokeStyle="#000000";var B=0,A=a.length;for(;B<A;B++){u.beginPath();if(B&&d[B]){u.moveTo(a[B-1],o[B-1])}else{u.moveTo(a[B]-1,o[B])}u.lineTo(a[B],o[B]);u.closePath();u.stroke()}},c=function(){z=false},w=function(E){var D="h6 h5 h4 h3 h2 h1".split(" "),C=D.length,F,G;while(C--){F=E.getElementsByTagName(D[C]);for(var B=0,A=F.length;B<A;B++){G=F[B].textContent.trim();if(G){return G}}}},v=s.implementation,k=function(D){var B=v.createDocumentType("html",null,null),J=v.createDocument("http://www.w3.org/1999/xhtml","html",B),A=J.documentElement,H=A.appendChild(J.createElement("head")),K=H.appendChild(J.createElement("meta")),I=H.appendChild(J.createElement("title")),E=A.appendChild(J.createElement("body")),C=0,G=D.childNodes.length;K.setAttribute("charset",D.ownerDocument.characterSet);for(;C<G;C++){E.appendChild(J.importNode(D.childNodes.item(C),true))}var F=w(J);if(F){I.appendChild(J.createTextNode(F))}return J};f.width=500;f.height=300;if(typeof a==="string"){a=JSON.parse(a)}if(typeof o==="string"){o=JSON.parse(o)}if(typeof d==="string"){d=JSON.parse(d)}if(b.canvas_filename){y.value=b.canvas_filename}if(b.text){q.value=b.text}if(b.text_filename){h.value=b.text_filename}if(b.html){m.innerHTML=b.html}if(b.html_filename){i.value=b.html_filename}z=true;l();z=false;p.addEventListener("click",function(){f.width=f.width;a.length=o.length=d.length=0},false);f.addEventListener("mousedown",function(A){A.preventDefault();z=true;j(A.pageX-f.offsetLeft,A.pageY-f.offsetTop,false);l()},false);f.addEventListener("mousemove",function(A){if(z){j(A.pageX-f.offsetLeft,A.pageY-f.offsetTop,true);l()}},false);f.addEventListener("mouseup",c,false);f.addEventListener("mouseout",c,false);r.addEventListener("submit",function(A){A.preventDefault();f.toBlob(function(B){saveAs(B,(y.value||y.placeholder)+".png")},"image/png")},false);t.addEventListener("submit",function(A){A.preventDefault();var B=x();saveAs(new B([q.value||q.placeholder],{type:"text/plain;charset="+s.characterSet}),(h.value||h.placeholder)+".txt")},false);e.addEventListener("submit",function(B){B.preventDefault();var D=x(),A=new XMLSerializer,C=k(m);saveAs(new D([A.serializeToString(C)],{type:"application/xhtml+xml;charset="+s.characterSet}),(i.value||i.placeholder)+".xhtml")},false);n.addEventListener("unload",function(){b.x_points=JSON.stringify(a);b.y_points=JSON.stringify(o);b.drag_points=JSON.stringify(d);b.canvas_filename=y.value;b.text=q.value;b.text_filename=h.value;b.html=m.innerHTML;b.html_filename=i.value},false)}(self)); \ No newline at end of file
diff --git a/vendor/FileSaver/demo/index.xhtml b/vendor/FileSaver/demo/index.xhtml
new file mode 100644
index 0000000..abc8813
--- /dev/null
+++ b/vendor/FileSaver/demo/index.xhtml
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" dir="ltr" lang="en-US-x-Hixie">
+<head>
+<meta charset="utf-8"/>
+<title>FileSaver.js demo</title>
+<link rel="stylesheet" type="text/css" href="demo.css"/>
+</head>
+<body>
+<h1><a href="https://github.com/eligrey/FileSaver.js">FileSaver.js</a> demo</h1>
+<p>
+The following examples demonstrate how it is possible to generate and save any type of data right in the browser using the W3C <code>saveAs()</code> <a href="http://www.w3.org/TR/file-writer-api/#the-filesaver-interface">FileSaver</a> interface, without contacting any servers.
+</p>
+<section id="image-demo">
+ <h2>Saving an image</h2>
+ <canvas class="input" id="canvas" width="500" height="300"/>
+ <form id="canvas-options">
+ <label>Filename: <input type="text" class="filename" id="canvas-filename" placeholder="doodle"/>.png</label>
+ <input type="submit" value="Save"/>
+ <input type="button" id="canvas-clear" value="Clear"/>
+ </form>
+</section>
+<section id="text-demo">
+ <h2>Saving text</h2>
+ <textarea class="input" id="text" placeholder="Once upon a time..."/>
+ <form id="text-options">
+ <label>Filename: <input type="text" class="filename" id="text-filename" placeholder="a plain document"/>.txt</label>
+ <input type="submit" value="Save"/>
+ </form>
+</section>
+<section id="html-demo">
+ <h2>Saving rich text</h2>
+ <div class="input" id="html" contenteditable="">
+ <h3>Some example rich text</h3>
+ <ul>
+ <li><del>Plain</del> <ins>Boring</ins> text.</li>
+ <li><em>Emphasized text!</em></li>
+ <li><strong>Strong text!</strong></li>
+ <li>
+ <svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="70" height="70">
+ <circle cx="35" cy="35" r="35" fill="red"/>
+ <text x="10" y="40">image</text>
+ </svg>
+ </li>
+ <li><a href="https://github.com/eligrey/FileSaver.js">A link.</a></li>
+ </ul>
+ </div>
+ <form id="html-options">
+ <label>Filename: <input type="text" class="filename" id="html-filename" placeholder="a rich document"/>.xhtml</label>
+ <input type="submit" value="Save"/>
+ </form>
+</section>
+<script type="application/ecmascript" async="" src="https://raw.github.com/eligrey/Blob.js/master/Blob.min.js"/>
+<script type="application/ecmascript" async="" src="https://raw.github.com/eligrey/canvas-toBlob.js/master/canvas-toBlob.js"/>
+<script type="application/ecmascript" async="" src="https://raw.github.com/eligrey/FileSaver.js/master/FileSaver.js"/>
+<script type="application/ecmascript" async="" src="https://raw.github.com/eligrey/FileSaver.js/master/demo/demo.js"/>
+</body>
+</html>
diff --git a/vendor/FileSaver/package.json b/vendor/FileSaver/package.json
new file mode 100644
index 0000000..3ddf184
--- /dev/null
+++ b/vendor/FileSaver/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "filesaver.js",
+ "version": "2013.1.23",
+ "description": "A saveAs() FileSaver implementation",
+ "main": "FileSaver.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/eligrey/FileSaver.js.git"
+ },
+ "keywords": [
+ "filesaver",
+ "saveas",
+ "blob"
+ ],
+ "author": "Eli Grey",
+ "license": "MIT/X11",
+ "bugs": {
+ "url": "https://github.com/eligrey/FileSaver.js/issues"
+ }
+}