summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--js/frames.js56
-rw-r--r--js/save.js13
-rw-r--r--js/vendor/whammy.js531
-rw-r--r--shader-webm.html192
4 files changed, 776 insertions, 16 deletions
diff --git a/js/frames.js b/js/frames.js
index 163e899..dd8759c 100644
--- a/js/frames.js
+++ b/js/frames.js
@@ -1,9 +1,10 @@
var frame_thumb_size = 93
var frame_editor = {}
+var lastGif, lastWebm
frame_editor.init = function(){
- frame_editor.bind()
+ frame_editor.bind()
}
frame_editor.bind = function(){
@@ -28,16 +29,18 @@ frame_editor.bind = function(){
$("#sort-frames").click(sort_frames)
$("#render").click(render)
+ $("#render-webm").click(render_webm)
$("#save").click(save)
$("#upload").click(upload)
- $("#background").change(function(){
- document.body.style.backgroundColor = $("#background").string()
- })
+ $("#background").change(function(){
+ document.body.style.backgroundColor = $("#background").string()
+ })
}
function add_frame(){
- $("#render").enable()
+ $("#render").enable()
+ $("#render-webm").enable()
var frame_count = $("#framecount").int()
if (frame_count < 2) {
add_single_frame()
@@ -99,7 +102,7 @@ function sort_frames(){
function render (){
if (rendering) return
if ($("#frames canvas.fullsize").length == 0) {
- add_frame()
+ add_frame()
}
rendering = true
@@ -109,7 +112,7 @@ function render (){
var frame = cq(this.width, this.height).fillStyle($("#background").string()).fillRect(0,0,this.width, this.height).drawImage(this,0,0)
encoder.addFrame(frame.canvas, delay)
})
- $("#pause,#render,#add-frame").disable()
+ $("#pause,#render,#render-webm,#add-frame").disable()
$("#workspace").find("img").remove()
$("#rendered").show()
// really bad results with neuquant?
@@ -119,7 +122,7 @@ function render (){
try {
encoder.encode()
} catch (e) {
- $("#pause,#render,#add-frame").enable()
+ $("#pause,#render,#render-webm,#add-frame").enable()
rendering = false
status(e)
throw e
@@ -127,6 +130,36 @@ function render (){
$("#render").html("rendering")
}
+function render_webm (){
+ if (rendering) return
+ if ($("#frames canvas.fullsize").length == 0) {
+ add_frame()
+ }
+ rendering = true
+ var $frames = $("#frames canvas.fullsize")
+ var webm_encoder = new Whammy.Video($frames.length)
+ $frames.each(function(){
+ var frame = cq(this.width, this.height).fillStyle($("#background").string()).fillRect(0,0,this.width, this.height).drawImage(this,0,0)
+ webm_encoder.add(frame.canvas)
+ })
+ lastGif = null
+ var output = lastWebm = webm_encoder.compile();
+ var video = document.createElement("video")
+ video.src = (window.webkitURL || window.URL).createObjectURL(output);
+ video.setAttribute("loop", true)
+ video.setAttribute("autoplay", true)
+ video.play()
+ $("#workspace canvas").hide()
+ $("#workspace").append(video)
+ $("#uploaded-url").hide().val("")
+ $("#uploaded-url + br").hide()
+ $("#save,#upload,#rendered").show()
+ $("#pause,#render,#render-webm,#add-frame,#save,#upload").enable()
+ $("#render").html("render gif")
+ rendering = false
+ pause(true)
+}
+
var encoder = new GifEncoder()
encoder.on("quantized", function(url){
@@ -145,12 +178,13 @@ encoder.on("rendered", function(bytes){
encoder.on("rendered-url", function(url){
var image = new Image ()
lastGif = image.src = url
- $("#workspace canvas").hide()
+ lastWebm = null
+ $("#workspace canvas").hide()
$("#workspace").append(image)
$("#uploaded-url").hide().val("")
- $("#uploaded-url + br").hide()
+ $("#uploaded-url + br").hide()
$("#save,#upload,#rendered").show()
- $("#pause,#render,#add-frame,#save,#upload").enable()
+ $("#pause,#render,#render-webm,#add-frame,#save,#upload").enable()
$("#render").html("render gif")
rendering = false
pause(true)
diff --git a/js/save.js b/js/save.js
index ec2fc6b..92f8fd6 100644
--- a/js/save.js
+++ b/js/save.js
@@ -1,14 +1,17 @@
function get_filename(){
var basename = $("#url").val().replace(/^.*\//,"").replace(/\..*$/,"").replace(/[^-_ a-zA-Z0-9]/g,"")
var username = user.username
- var filename = basename + "-" + username + "-" + (+new Date()) + ".gif"
+ var filename = basename + "-" + username + "-" + (+new Date()) + "." + (lastWebm ? "webm" : "gif")
return filename.replace(/ /g,"_").replace(/-+/g,"-")
}
function save (){
- if (! lastGif) return;
- var filename = get_filename()
- var blob = dataUriToBlob(lastGif)
- saveAs(blob, filename);
+ if (lastGif) {
+ var filename = get_filename()
+ var blob = dataUriToBlob(lastGif)
+ saveAs(blob, filename);
+ } else if (lastWebm) {
+ saveAs(lastWebm, filename);
+ }
}
function saveJSON (data, filename) {
var bytes = JSON.stringify(data)
diff --git a/js/vendor/whammy.js b/js/vendor/whammy.js
new file mode 100644
index 0000000..0448d3e
--- /dev/null
+++ b/js/vendor/whammy.js
@@ -0,0 +1,531 @@
+/*
+ var vid = new Whammy.Video();
+ vid.add(canvas or data url)
+ vid.compile()
+*/
+
+window.Whammy = (function(){
+ // in this case, frames has a very specific meaning, which will be
+ // detailed once i finish writing the code
+
+ function toWebM(frames, outputAsArray){
+ var info = checkFrames(frames);
+
+ //max duration by cluster in milliseconds
+ var CLUSTER_MAX_DURATION = 30000;
+
+ var EBML = [
+ {
+ "id": 0x1a45dfa3, // EBML
+ "data": [
+ {
+ "data": 1,
+ "id": 0x4286 // EBMLVersion
+ },
+ {
+ "data": 1,
+ "id": 0x42f7 // EBMLReadVersion
+ },
+ {
+ "data": 4,
+ "id": 0x42f2 // EBMLMaxIDLength
+ },
+ {
+ "data": 8,
+ "id": 0x42f3 // EBMLMaxSizeLength
+ },
+ {
+ "data": "webm",
+ "id": 0x4282 // DocType
+ },
+ {
+ "data": 2,
+ "id": 0x4287 // DocTypeVersion
+ },
+ {
+ "data": 2,
+ "id": 0x4285 // DocTypeReadVersion
+ }
+ ]
+ },
+ {
+ "id": 0x18538067, // Segment
+ "data": [
+ {
+ "id": 0x1549a966, // Info
+ "data": [
+ {
+ "data": 1e6, //do things in millisecs (num of nanosecs for duration scale)
+ "id": 0x2ad7b1 // TimecodeScale
+ },
+ {
+ "data": "whammy",
+ "id": 0x4d80 // MuxingApp
+ },
+ {
+ "data": "whammy",
+ "id": 0x5741 // WritingApp
+ },
+ {
+ "data": doubleToString(info.duration),
+ "id": 0x4489 // Duration
+ }
+ ]
+ },
+ {
+ "id": 0x1654ae6b, // Tracks
+ "data": [
+ {
+ "id": 0xae, // TrackEntry
+ "data": [
+ {
+ "data": 1,
+ "id": 0xd7 // TrackNumber
+ },
+ {
+ "data": 1,
+ "id": 0x73c5 // TrackUID
+ },
+ {
+ "data": 0,
+ "id": 0x9c // FlagLacing
+ },
+ {
+ "data": "und",
+ "id": 0x22b59c // Language
+ },
+ {
+ "data": "V_VP8",
+ "id": 0x86 // CodecID
+ },
+ {
+ "data": "VP8",
+ "id": 0x258688 // CodecName
+ },
+ {
+ "data": 1,
+ "id": 0x83 // TrackType
+ },
+ {
+ "id": 0xe0, // Video
+ "data": [
+ {
+ "data": info.width,
+ "id": 0xb0 // PixelWidth
+ },
+ {
+ "data": info.height,
+ "id": 0xba // PixelHeight
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": 0x1c53bb6b, // Cues
+ "data": [
+ //cue insertion point
+ ]
+ }
+
+ //cluster insertion point
+ ]
+ }
+ ];
+
+
+ var segment = EBML[1];
+ var cues = segment.data[2];
+
+ //Generate clusters (max duration)
+ var frameNumber = 0;
+ var clusterTimecode = 0;
+ while(frameNumber < frames.length){
+
+ var cuePoint = {
+ "id": 0xbb, // CuePoint
+ "data": [
+ {
+ "data": Math.round(clusterTimecode),
+ "id": 0xb3 // CueTime
+ },
+ {
+ "id": 0xb7, // CueTrackPositions
+ "data": [
+ {
+ "data": 1,
+ "id": 0xf7 // CueTrack
+ },
+ {
+ "data": 0, // to be filled in when we know it
+ "size": 8,
+ "id": 0xf1 // CueClusterPosition
+ }
+ ]
+ }
+ ]
+ };
+
+ cues.data.push(cuePoint);
+
+ var clusterFrames = [];
+ var clusterDuration = 0;
+ do {
+ clusterFrames.push(frames[frameNumber]);
+ clusterDuration += frames[frameNumber].duration;
+ frameNumber++;
+ }while(frameNumber < frames.length && clusterDuration < CLUSTER_MAX_DURATION);
+
+ var clusterCounter = 0;
+ var cluster = {
+ "id": 0x1f43b675, // Cluster
+ "data": [
+ {
+ "data": Math.round(clusterTimecode),
+ "id": 0xe7 // Timecode
+ }
+ ].concat(clusterFrames.map(function(webp){
+ var block = makeSimpleBlock({
+ discardable: 0,
+ frame: webp.data.slice(4),
+ invisible: 0,
+ keyframe: 1,
+ lacing: 0,
+ trackNum: 1,
+ timecode: Math.round(clusterCounter)
+ });
+ clusterCounter += webp.duration;
+ return {
+ data: block,
+ id: 0xa3
+ };
+ }))
+ }
+
+ //Add cluster to segment
+ segment.data.push(cluster);
+ clusterTimecode += clusterDuration;
+ }
+
+ //First pass to compute cluster positions
+ var position = 0;
+ for(var i = 0; i < segment.data.length; i++){
+ if (i >= 3) {
+ cues.data[i-3].data[1].data[1].data = position;
+ }
+ var data = generateEBML([segment.data[i]], outputAsArray);
+ position += data.size || data.byteLength || data.length;
+ if (i != 2) { // not cues
+ //Save results to avoid having to encode everything twice
+ segment.data[i] = data;
+ }
+ }
+
+ return generateEBML(EBML, outputAsArray)
+ }
+
+ // sums the lengths of all the frames and gets the duration, woo
+
+ function checkFrames(frames){
+ var width = frames[0].width,
+ height = frames[0].height,
+ duration = frames[0].duration;
+ for(var i = 1; i < frames.length; i++){
+ if(frames[i].width != width) throw "Frame " + (i + 1) + " has a different width";
+ if(frames[i].height != height) throw "Frame " + (i + 1) + " has a different height";
+ if(frames[i].duration < 0 || frames[i].duration > 0x7fff) throw "Frame " + (i + 1) + " has a weird duration (must be between 0 and 32767)";
+ duration += frames[i].duration;
+ }
+ return {
+ duration: duration,
+ width: width,
+ height: height
+ };
+ }
+
+
+ function numToBuffer(num){
+ var parts = [];
+ while(num > 0){
+ parts.push(num & 0xff)
+ num = num >> 8
+ }
+ return new Uint8Array(parts.reverse());
+ }
+
+ function numToFixedBuffer(num, size){
+ var parts = new Uint8Array(size);
+ for(var i = size - 1; i >= 0; i--){
+ parts[i] = num & 0xff;
+ num = num >> 8;
+ }
+ return parts;
+ }
+
+ function strToBuffer(str){
+ // return new Blob([str]);
+
+ var arr = new Uint8Array(str.length);
+ for(var i = 0; i < str.length; i++){
+ arr[i] = str.charCodeAt(i)
+ }
+ return arr;
+ // this is slower
+ // return new Uint8Array(str.split('').map(function(e){
+ // return e.charCodeAt(0)
+ // }))
+ }
+
+
+ //sorry this is ugly, and sort of hard to understand exactly why this was done
+ // at all really, but the reason is that there's some code below that i dont really
+ // feel like understanding, and this is easier than using my brain.
+
+ function bitsToBuffer(bits){
+ var data = [];
+ var pad = (bits.length % 8) ? (new Array(1 + 8 - (bits.length % 8))).join('0') : '';
+ bits = pad + bits;
+ for(var i = 0; i < bits.length; i+= 8){
+ data.push(parseInt(bits.substr(i,8),2))
+ }
+ return new Uint8Array(data);
+ }
+
+ function generateEBML(json, outputAsArray){
+ var ebml = [];
+ for(var i = 0; i < json.length; i++){
+ if (!('id' in json[i])){
+ //already encoded blob or byteArray
+ ebml.push(json[i]);
+ continue;
+ }
+
+ var data = json[i].data;
+ if(typeof data == 'object') data = generateEBML(data, outputAsArray);
+ if(typeof data == 'number') data = ('size' in json[i]) ? numToFixedBuffer(data, json[i].size) : bitsToBuffer(data.toString(2));
+ if(typeof data == 'string') data = strToBuffer(data);
+
+ if(data.length){
+ var z = z;
+ }
+
+ var len = data.size || data.byteLength || data.length;
+ var zeroes = Math.ceil(Math.ceil(Math.log(len)/Math.log(2))/8);
+ var size_str = len.toString(2);
+ var padded = (new Array((zeroes * 7 + 7 + 1) - size_str.length)).join('0') + size_str;
+ var size = (new Array(zeroes)).join('0') + '1' + padded;
+
+ //i actually dont quite understand what went on up there, so I'm not really
+ //going to fix this, i'm probably just going to write some hacky thing which
+ //converts that string into a buffer-esque thing
+
+ ebml.push(numToBuffer(json[i].id));
+ ebml.push(bitsToBuffer(size));
+ ebml.push(data)
+
+
+ }
+
+ //output as blob or byteArray
+ if(outputAsArray){
+ //convert ebml to an array
+ var buffer = toFlatArray(ebml)
+ return new Uint8Array(buffer);
+ }else{
+ return new Blob(ebml, {type: "video/webm"});
+ }
+ }
+
+ function toFlatArray(arr, outBuffer){
+ if(outBuffer == null){
+ outBuffer = [];
+ }
+ for(var i = 0; i < arr.length; i++){
+ if(typeof arr[i] == 'object'){
+ //an array
+ toFlatArray(arr[i], outBuffer)
+ }else{
+ //a simple element
+ outBuffer.push(arr[i]);
+ }
+ }
+ return outBuffer;
+ }
+
+ //OKAY, so the following two functions are the string-based old stuff, the reason they're
+ //still sort of in here, is that they're actually faster than the new blob stuff because
+ //getAsFile isn't widely implemented, or at least, it doesn't work in chrome, which is the
+ // only browser which supports get as webp
+
+ //Converting between a string of 0010101001's and binary back and forth is probably inefficient
+ //TODO: get rid of this function
+ function toBinStr_old(bits){
+ var data = '';
+ var pad = (bits.length % 8) ? (new Array(1 + 8 - (bits.length % 8))).join('0') : '';
+ bits = pad + bits;
+ for(var i = 0; i < bits.length; i+= 8){
+ data += String.fromCharCode(parseInt(bits.substr(i,8),2))
+ }
+ return data;
+ }
+
+ function generateEBML_old(json){
+ var ebml = '';
+ for(var i = 0; i < json.length; i++){
+ var data = json[i].data;
+ if(typeof data == 'object') data = generateEBML_old(data);
+ if(typeof data == 'number') data = toBinStr_old(data.toString(2));
+
+ var len = data.length;
+ var zeroes = Math.ceil(Math.ceil(Math.log(len)/Math.log(2))/8);
+ var size_str = len.toString(2);
+ var padded = (new Array((zeroes * 7 + 7 + 1) - size_str.length)).join('0') + size_str;
+ var size = (new Array(zeroes)).join('0') + '1' + padded;
+
+ ebml += toBinStr_old(json[i].id.toString(2)) + toBinStr_old(size) + data;
+
+ }
+ return ebml;
+ }
+
+ //woot, a function that's actually written for this project!
+ //this parses some json markup and makes it into that binary magic
+ //which can then get shoved into the matroska comtainer (peaceably)
+
+ function makeSimpleBlock(data){
+ var flags = 0;
+ if (data.keyframe) flags |= 128;
+ if (data.invisible) flags |= 8;
+ if (data.lacing) flags |= (data.lacing << 1);
+ if (data.discardable) flags |= 1;
+ if (data.trackNum > 127) {
+ throw "TrackNumber > 127 not supported";
+ }
+ var out = [data.trackNum | 0x80, data.timecode >> 8, data.timecode & 0xff, flags].map(function(e){
+ return String.fromCharCode(e)
+ }).join('') + data.frame;
+
+ return out;
+ }
+
+ // here's something else taken verbatim from weppy, awesome rite?
+
+ function parseWebP(riff){
+ var VP8 = riff.RIFF[0].WEBP[0];
+
+ var frame_start = VP8.indexOf('\x9d\x01\x2a'); //A VP8 keyframe starts with the 0x9d012a header
+ for(var i = 0, c = []; i < 4; i++) c[i] = VP8.charCodeAt(frame_start + 3 + i);
+
+ var width, horizontal_scale, height, vertical_scale, tmp;
+
+ //the code below is literally copied verbatim from the bitstream spec
+ tmp = (c[1] << 8) | c[0];
+ width = tmp & 0x3FFF;
+ horizontal_scale = tmp >> 14;
+ tmp = (c[3] << 8) | c[2];
+ height = tmp & 0x3FFF;
+ vertical_scale = tmp >> 14;
+ return {
+ width: width,
+ height: height,
+ data: VP8,
+ riff: riff
+ }
+ }
+
+ // i think i'm going off on a riff by pretending this is some known
+ // idiom which i'm making a casual and brilliant pun about, but since
+ // i can't find anything on google which conforms to this idiomatic
+ // usage, I'm assuming this is just a consequence of some psychotic
+ // break which makes me make up puns. well, enough riff-raff (aha a
+ // rescue of sorts), this function was ripped wholesale from weppy
+
+ function parseRIFF(string){
+ var offset = 0;
+ var chunks = {};
+
+ while (offset < string.length) {
+ var id = string.substr(offset, 4);
+ var len = parseInt(string.substr(offset + 4, 4).split('').map(function(i){
+ var unpadded = i.charCodeAt(0).toString(2);
+ return (new Array(8 - unpadded.length + 1)).join('0') + unpadded
+ }).join(''),2);
+ var data = string.substr(offset + 4 + 4, len);
+ offset += 4 + 4 + len;
+ chunks[id] = chunks[id] || [];
+
+ if (id == 'RIFF' || id == 'LIST') {
+ chunks[id].push(parseRIFF(data));
+ } else {
+ chunks[id].push(data);
+ }
+ }
+ return chunks;
+ }
+
+ // here's a little utility function that acts as a utility for other functions
+ // basically, the only purpose is for encoding "Duration", which is encoded as
+ // a double (considerably more difficult to encode than an integer)
+ function doubleToString(num){
+ return [].slice.call(
+ new Uint8Array(
+ (
+ new Float64Array([num]) //create a float64 array
+ ).buffer) //extract the array buffer
+ , 0) // convert the Uint8Array into a regular array
+ .map(function(e){ //since it's a regular array, we can now use map
+ return String.fromCharCode(e) // encode all the bytes individually
+ })
+ .reverse() //correct the byte endianness (assume it's little endian for now)
+ .join('') // join the bytes in holy matrimony as a string
+ }
+
+ function WhammyVideo(speed, quality){ // a more abstract-ish API
+ this.frames = [];
+ this.duration = 1000 / speed;
+ this.quality = quality || 0.8;
+ }
+
+ WhammyVideo.prototype.add = function(frame, duration){
+ if(typeof duration != 'undefined' && this.duration) throw "you can't pass a duration if the fps is set";
+ if(typeof duration == 'undefined' && !this.duration) throw "if you don't have the fps set, you ned to have durations here.";
+ if(frame.canvas){ //CanvasRenderingContext2D
+ frame = frame.canvas;
+ }
+ if(frame.toDataURL){
+ frame = frame.toDataURL('image/webp', this.quality)
+ }else if(typeof frame != "string"){
+ throw "frame must be a a HTMLCanvasElement, a CanvasRenderingContext2D or a DataURI formatted string"
+ }
+ if (!(/^data:image\/webp;base64,/ig).test(frame)) {
+ throw "Input must be formatted properly as a base64 encoded DataURI of type image/webp";
+ }
+ this.frames.push({
+ image: frame,
+ duration: duration || this.duration
+ })
+ }
+
+ WhammyVideo.prototype.compile = function(outputAsArray){
+ return new toWebM(this.frames.map(function(frame){
+ var webp = parseWebP(parseRIFF(atob(frame.image.slice(23))));
+ webp.duration = frame.duration;
+ return webp;
+ }), outputAsArray)
+ }
+
+ return {
+ Video: WhammyVideo,
+ fromImageArray: function(images, fps, outputAsArray){
+ return toWebM(images.map(function(image){
+ var webp = parseWebP(parseRIFF(atob(image.slice(23))))
+ webp.duration = 1000 / fps;
+ return webp;
+ }), outputAsArray)
+ },
+ toWebM: toWebM
+ // expose methods of madness
+ }
+})()
diff --git a/shader-webm.html b/shader-webm.html
new file mode 100644
index 0000000..e1c3c1d
--- /dev/null
+++ b/shader-webm.html
@@ -0,0 +1,192 @@
+<!doctype html>
+<html>
+<head>
+<title>Shader</title>
+<style type="text/css">
+html,body { margin: 0; padding: 0; }
+#url { width: 450px; }
+#width,#height,#framecount,#framedelay,#frameinterval,#background { width: 30px; }
+#shader { width: 100%; height: 247px; font-family: fixed; }
+#controls { width: 450px; }
+#frames { width: 435px; max-height: 150px; overflow: auto; border: 1px solid #ddd; line-height: 0; }
+#frames div { margin: 1px; padding: 0; position: relative; border: 1px solid #eee; cursor: -webkit-grab; }
+.dragging { cursor: -webkit-grabbing !important; }
+.ui-sortable-helper { cursor: -webkit-grabbing !important; }
+#frames canvas { display: block }
+#frames .remove { position: absolute; top: 5px; right: 5px; color: #f00; padding: 3px; border: 0;background: white; font-size: 10px; line-height: 10px; }
+.paused { background: black; color: white; border-width: 1px; padding: 1px 3px 2px 4px; outline: 0 !important; }
+.active { background: black; color: white; border-width: 1px; padding: 1px 4px 2px 4px; outline: 0 !important; }
+div { display: inline-block; padding: 10px;}
+#gallery,#controls,#workspace,#rendered{ float: left; }
+#rendered img { display: block; }
+#render,#save { font-weight: bold; }
+#render { float: right; }
+#commands { position: absolute;top:20px;right:20px; width:190px;height:465px; box-shadow:5px 5px 10px rgba(0,0,0,0.3); background:rgba(255,255,255,0.8); display: none; cursor: -webkit-grab; }
+#commands iframe {width: 100%;height:100%;margin:0;padding:0;border:0;}
+#commands.dragging iframe { pointer-events: none; }
+#commands .close { position: absolute; top: 5px; right: 5px; color: #f00; padding: 3px; border: 0;background: white; font-size: 10px; line-height: 10px; }
+.close,.remove { cursor: pointer; }
+#uploaded-url { display: none; width: 300px; }
+form { display: inline-block; }
+#gallery { clear: right; width:100%; padding: 0; }
+#gallery-images { display: block; max-height: 210px; overflow-y: auto; }
+#gallery-images img, #gallery-images canvas { max-width: 200px; height: 100px; margin: 5px; cursor: pointer; }
+#username { width: 40px; }
+a { color: #00f; }
+</style>
+</head>
+<body>
+<div id="gallery">
+ <div id="gallery-form">
+ <form id="gallery-search">
+ <input type="text" id="dumpfm-search-query" value="duck bill">
+ <button id="gallery-search">DUMP SEARCH</button>
+ </form>
+ <button id="gallery-random">IM RANDOM</button>
+ <button id="camera">CAMERA</button>
+ <span class="status"></span>
+ </div>
+ <div id="gallery-images"></div>
+</div>
+
+<div id="controls">
+
+ <input type="text" id="url" value="img/1376516658960-dumpfm-DoritoWitch-TimeFLyTrans0001.png">
+ <br>
+ <br>
+
+ <textarea id="shader"></textarea>
+ <br>
+ <br>
+
+ frames <input type="text" id="framecount" value="120">
+ interval <input type="text" id="frameinterval" value="0.05s">
+ <button id="add-frame">+add frame</button>
+ <button id="remove-all-frames">clear</button>
+ <button id="render" disabled>render gif</button>
+ <button id="render-webm" disabled>render webm</button>
+ <br>
+ reorder:
+ <button id="weave-frames">weave</button>
+ <button id="shuffle-frames">shuffle</button>
+ <button id="reverse-frames">reverse</button>
+ <button id="sort-frames">sort</button>
+
+ <div id="frames"></div>
+ <br>
+ <br>
+
+ gif delay <input type="text" id="framedelay" value="0.06s">
+ background <input type="text" id="background" value="#fff">
+ your name here &rarr; <input type="text" id="username" value="">
+ <br>
+ <br>
+ <button id="show-commands">help</button>
+ &nbsp;
+ <button id="add-shader">+</button>
+ <button id="remove-shader">&times;</button>
+ &nbsp;
+ <span id="shaders"></span>
+ <br>
+ <br>
+ <a href="http://asdf.us/im/gallery/?tag=shader" target="_blank">Photoblaster Gallery</a>
+</div>
+
+<div id="workspace"></div>
+
+<div id="rendered">
+ <button id="reset">reset</button>
+ <button id="pause">pause</button>
+ <button id="step-forward">&gt;&gt;</button>
+ <span class="status"></span>
+ <button id="save" disabled>save</button>
+ <button id="upload" disabled>upload</button>
+ <br>
+ <input type="text" id="uploaded-url">
+</div>
+
+<div id="commands"><iframe src="commands.html"></iframe><button class="close">&times;</button></div>
+</body>
+<script type="text/javascript" src="js/vendor/gif-encode/util.js"></script>
+<script type="text/javascript" src="js/vendor/gif-encode/tube.js"></script>
+<script type="text/javascript" src="js/vendor/gif-encode/client.js"></script>
+<script type="text/javascript" src="js/vendor/gif.js"></script>
+<script type="text/javascript" src="js/vendor/FileSaver/FileSaver.js"></script>
+<script type="text/javascript" src="js/vendor/dataUriToBlob.js"></script>
+<script type="text/javascript" src="js/vendor/acorn.js"></script>
+<script type="text/javascript" src="js/vendor/whammy.js"></script>
+<script type="text/javascript" src="js/vendor/jquery/jquery.min.js"></script>
+<script type="text/javascript" src="js/vendor/jquery-ui-1.10.3.custom.min.js"></script>
+<script type="text/javascript" src="js/vendor/canvasquery.js"></script>
+<script type="text/javascript" src="js/canvasquery.dither.js"></script>
+<script type="text/javascript" src="js/api/localstorage.js"></script>
+<script type="text/javascript" src="js/error.highlight.js"></script>
+<script type="text/javascript" src="js/asdf.js"></script>
+<script type="text/javascript" src="js/image.js"></script>
+<script type="text/javascript" src="js/camera.js"></script>
+<script type="text/javascript" src="js/gallery.js"></script>
+<script type="text/javascript" src="js/color.js"></script>
+<script type="text/javascript" src="js/frames.js"></script>
+<script type="text/javascript" src="js/image.js"></script>
+<script type="text/javascript" src="js/user.js"></script>
+<script type="text/javascript" src="js/render.js"></script>
+<script type="text/javascript" src="js/shader.js"></script>
+<script type="text/javascript" src="js/save.js"></script>
+<script type="text/javascript" src="js/util.js"></script>
+<script type="text/javascript" src="js/help.js"></script>
+<script type="text/javascript" src="shaders.json"></script>
+
+<script type="text/html" id="frame-template">
+<button class="remove">x</button>
+<span class="frame"></span>
+</script>
+
+<script type="text/javascript">
+
+var cc = cq(0,0).appendTo("#workspace")
+var w, h
+var lastGif
+
+$(init)
+
+var mousex, mousey
+
+function init(){
+ $("#url").change(load)
+ $("#reset").click(reset)
+ $("#pause").click(pause)
+ $("#step-forward").click(step_forward)
+ $(document).on("mousemove", function(e) {
+ mousex = e.pageX
+ mousey = e.pageY
+ })
+
+ $("#background").change(function(){
+ document.body.style.backgroundColor = $("#background").string()
+ })
+
+ frame_editor.init()
+ help.init()
+ load()
+
+ gallery.init(choose)
+
+ user.init()
+ if (user.username.length) {
+ console.log("signed in as", user.username)
+ }
+
+ local_shaders.init()
+ local_shaders.loadLastAccessed()
+
+ document.getElementById('shader').addEventListener('input', shader_build);
+ shader_build()
+
+ requestAnimationFrame(animate)
+
+ // $("#camera").trigger("click")
+}
+
+</script>
+</html>
+