From 037a0ae8072217f7821549bbdfe030e73289330d Mon Sep 17 00:00:00 2001 From: Julie Lala Date: Tue, 10 Dec 2013 00:47:36 -0500 Subject: dither stuff --- .bowerrc | 3 + .gitignore | 9 + ...6658960-dumpfm-DoritoWitch-TimeFLyTrans0001.png | Bin 0 -> 163827 bytes FileSaver.js | 232 +++ Gruntfile.js | 48 + abyss.png | Bin 0 -> 193659 bytes bower.json | 14 + building.png | Bin 0 -> 408192 bytes canvasquery.dither.js | 217 +++ canvasquery.js | 1767 ++++++++++++++++++++ dataUriToBlob.js | 47 + gif-animate.html | 108 ++ gif-dither.html | 195 +++ gif-encode/GIFEncoder.js | 513 ++++++ gif-encode/LZWEncoder.js | 328 ++++ gif-encode/NeuQuant.js | 538 ++++++ gif-encode/client.js | 260 +++ gif-encode/tube.js | 323 ++++ gif-encode/util.js | 15 + gif-encode/worker.js | 88 + gif.html | 149 ++ gif.js | 1690 +++++++++++++++++++ gradient.jpg | Bin 0 -> 19198 bytes halftone.html | 48 + index.html | 61 + package.json | 10 + pattern.html | 61 + proxy.py | 22 + shader.html | 35 + threshold.html | 46 + vendor/FileSaver/.bower.json | 15 + vendor/FileSaver/FileSaver.js | 232 +++ vendor/FileSaver/LICENSE.md | 30 + vendor/FileSaver/README.md | 78 + vendor/FileSaver/bower.json | 7 + vendor/FileSaver/demo/demo.css | 55 + vendor/FileSaver/demo/demo.js | 213 +++ vendor/FileSaver/demo/demo.min.js | 2 + vendor/FileSaver/demo/index.xhtml | 57 + vendor/FileSaver/package.json | 23 + 40 files changed, 7539 insertions(+) create mode 100644 .bowerrc create mode 100644 .gitignore create mode 100644 1376516658960-dumpfm-DoritoWitch-TimeFLyTrans0001.png create mode 100644 FileSaver.js create mode 100644 Gruntfile.js create mode 100644 abyss.png create mode 100644 bower.json create mode 100644 building.png create mode 100644 canvasquery.dither.js create mode 100644 canvasquery.js create mode 100644 dataUriToBlob.js create mode 100644 gif-animate.html create mode 100644 gif-dither.html create mode 100755 gif-encode/GIFEncoder.js create mode 100644 gif-encode/LZWEncoder.js create mode 100644 gif-encode/NeuQuant.js create mode 100644 gif-encode/client.js create mode 100644 gif-encode/tube.js create mode 100644 gif-encode/util.js create mode 100644 gif-encode/worker.js create mode 100644 gif.html create mode 100644 gif.js create mode 100644 gradient.jpg create mode 100644 halftone.html create mode 100644 index.html create mode 100644 package.json create mode 100644 pattern.html create mode 100644 proxy.py create mode 100644 shader.html create mode 100644 threshold.html create mode 100644 vendor/FileSaver/.bower.json create mode 100644 vendor/FileSaver/FileSaver.js create mode 100644 vendor/FileSaver/LICENSE.md create mode 100644 vendor/FileSaver/README.md create mode 100644 vendor/FileSaver/bower.json create mode 100644 vendor/FileSaver/demo/demo.css create mode 100644 vendor/FileSaver/demo/demo.js create mode 100644 vendor/FileSaver/demo/demo.min.js create mode 100644 vendor/FileSaver/demo/index.xhtml create mode 100644 vendor/FileSaver/package.json 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 Binary files /dev/null and b/1376516658960-dumpfm-DoritoWitch-TimeFLyTrans0001.png 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 Binary files /dev/null and b/abyss.png 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 Binary files /dev/null and b/building.png 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 @@ + + + +Gifs + + +
+ + + + + + + + + + +
loading...
+
+
+ + + + + + + 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 @@ + + + +Gif Dither + + +
+ + + + + + + + + + + + +
+
+ + + + + + + + + + + + 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 + * setDelay(1000/fps). + * @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 @@ + + + +Gifs + + +
+ + + + + + + + + + + + +
+
+ + + + + + + + + + + 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 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 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 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> 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) { // 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>= 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 Binary files /dev/null and b/gradient.jpg 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 @@ + + + +Halftone + + +
+radius +angle +
+
+ + + + + + diff --git a/index.html b/index.html new file mode 100644 index 0000000..18dfc3a --- /dev/null +++ b/index.html @@ -0,0 +1,61 @@ + + + +Dither + + +
+ + + + + + + + + +
+
+ + + + + + 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 @@ + + + +Dither + + +
+ + + + + + + + + +
+
+ + + + + + 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 @@ + + + +Shader + + +
+ + + + + + + + + +
+w/h x + + + + + + + diff --git a/threshold.html b/threshold.html new file mode 100644 index 0000000..f3cdc76 --- /dev/null +++ b/threshold.html @@ -0,0 +1,46 @@ + + + +Threshold + + +
+threshold +
+
+ + + + + + 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 © 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 + + + +FileSaver.js demo + + + +

FileSaver.js demo

+

+The following examples demonstrate how it is possible to generate and save any type of data right in the browser using the W3C saveAs() FileSaver interface, without contacting any servers. +

+
+

Saving an image

+ +
+ + + +
+
+
+

Saving text

+