summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjules <jules@okfoc.us>2014-01-21 13:51:57 -0500
committerjules <jules@okfoc.us>2014-01-21 13:51:57 -0500
commit08333b714e73034ca9098692950051d5d9d78220 (patch)
tree8774aa0b7c82fb1cad43a6cc3f50c85d3d8944e8
parent7500866755ac545294821ccf2b166ff84db685d8 (diff)
split off shader localstorage version
-rw-r--r--dither-localstorage.html132
-rw-r--r--js/api/localstorage.js106
-rw-r--r--js/api/set.js71
-rw-r--r--js/user.js106
-rw-r--r--shader-api.html9
-rw-r--r--shader-localstorage.html211
-rw-r--r--shader-picker.html1
7 files changed, 359 insertions, 277 deletions
diff --git a/dither-localstorage.html b/dither-localstorage.html
deleted file mode 100644
index 5e9bff9..0000000
--- a/dither-localstorage.html
+++ /dev/null
@@ -1,132 +0,0 @@
-<!doctype html>
-<html>
-<head>
-<title>Dither</title>
-<style>
- form { display: inline-block; }
- #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; }
-</style>
-</head>
-<body>
-
-<div id="gallery">
- <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>
- <span id="status"></span>
- <div id="gallery-images"></div>
-</div>
-
-<div id="dither">
-<button id="random">random</button>
-<button id="pattern2">pattern2</button>
-<button id="pattern3">pattern3</button>
-<button id="pattern4">pattern4</button>
-<button id="pattern2Lite">pattern2lite</button>
-<button id="pattern3Lite">pattern3lite</button>
-<button id="pattern4Lite">pattern4lite</button>
-<button id="floydSteinberg">floyd-steinberg</button>
-<button id="right">right</button>
-</div>
-<div id="images"></div>
-
-
-</body>
-<script type="text/javascript" src="js/vendor/jquery/jquery.min.js"></script>
-<script type="text/javascript" src="js/vendor/canvasquery.js"></script>
-<script type="text/javascript" src="js/vendor/gif.js"></script>
-<script type="text/javascript" src="js/canvasquery.dither.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/gallery.js"></script>
-<script type="text/javascript">
-
-var algo = 'random';
-var url = "img/abyss.png";
-var imgs = []
-var anim = []
-var complete = 0
-var viewport = cq(100, 100)
-$("#images").append(viewport.canvas)
-
-var ui = function(){}
-
-ui.init = function(){
- gallery.choose = ui.choose
- gallery.init()
- ui.bind()
- ui.animate()
- asdf.random()
-}
-
-ui.bind = function(){
- var buttons = $("#dither button")
- for (var i = 0; i < buttons.length; i++) {
- (function(n){
- buttons[n].onclick = function(){
- algo = buttons[n].id;
- ui.build()
- }
- })(i)
- }
-}
-
-ui.choose = function(){
- status("loading image..")
- loadImage( this.src, ui.ready );
-}
-
-ui.ready = function(){
- status("ready")
- loading = false
- if (window.gif) {
- frames = gif.frames
- }
- else {
- fc = cq(img.width, img.height)
- fc.drawImage(img, 0, 0)
- frames = [ { ctx: fc.context } ]
- }
- w = viewport.canvas.width = frames[0].ctx.canvas.width
- h = viewport.canvas.height = frames[0].ctx.canvas.height
- ui.build()
-}
-
-ui.build = function (){
- anim = []
- for (var i in frames) {
- anim.push( ui.dither(frames[i].ctx).context )
- }
-}
-
-ui.animate = function(t){
- requestAnimationFrame(ui.animate)
- if (! anim.length) return;
-
- if (window.gif && window.gif.currentFrame) {
- var idx = gif.currentFrame(t)
- var frame = anim[idx]
- }
- else {
- var frame = anim[0]
- }
- viewport.clearRect(0,0,w,h).drawImage(frame.canvas, 0, 0, w, h);
-}
-
-ui.dither = function (frame){
- var cc = cq(w, h)
- cc.drawImage(frame.canvas, 0, 0, w, h);
- cc[algo + "Dither"]( )
- return cc
-}
-
-function status(s){ $("#status").html(s); console.log(s) }
-
-$(ui.init)
-
-</script>
-</html>
-
diff --git a/js/api/localstorage.js b/js/api/localstorage.js
new file mode 100644
index 0000000..0ec7cad
--- /dev/null
+++ b/js/api/localstorage.js
@@ -0,0 +1,106 @@
+
+user.shaders = new function(){}
+user.shaders.init = function(){
+ user.shaders.bind()
+}
+user.shaders.bind = function(){
+ user.shaders.buttons = {}
+ if ('SHADERS' in window) {
+ user.shaders.bindButtons( window.SHADERS )
+ }
+ user.shaders.bindButtons( user.shaders.getShaders() )
+ document.getElementById("add-shader").addEventListener("click", user.shaders.save, false)
+ document.getElementById("remove-shader").addEventListener("click", user.shaders.remove, false)
+}
+user.shaders.bindButtons = function(shaders){
+ for (var i = 0; i < shaders.length; i++){
+ user.shaders.bindButton(shaders[i])
+ }
+}
+user.shaders.bindButton = function(data){
+// console.log("button >> " + data.name)
+ var button = document.createElement("button")
+ button.innerHTML = data.name
+ button.addEventListener("click", user.shaders.load.bind(this, data), false)
+ document.getElementById("shaders").appendChild(button)
+ user.shaders.buttons[data.name] = button
+}
+user.shaders.load = function(data){
+ var shader = data.shader
+ if (shader && shader.length > 0) {
+ user.shaders.name = data.name
+ $("#shader").val(shader)
+ // pause here?
+ shader_build()
+ $(".active").removeClass("active")
+ $(user.shaders.buttons[data.name]).addClass("active")
+ user.shaders.setLastAccessed(data.name)
+ }
+}
+user.shaders.loadLastAccessed = function(){
+ var name = localStorage.getItem("im.lastShader") || "colorcycle"
+ if (name in user.shaders.buttons) {
+ $(user.shaders.buttons[name]).trigger("click")
+ }
+}
+user.shaders.setLastAccessed = function(name){
+ localStorage.setItem("im.lastShader", name)
+}
+
+user.shaders.getShaderByName = function(name){
+ return { name: name, shader: user.shaders.getShaderBody(name) }
+}
+user.shaders.getShaderIndex = function(){
+ return JSON.parse( localStorage.getItem("im.shaders") ) || []
+}
+user.shaders.getShaderBody = function(name){
+ return localStorage.getItem("im.shaders."+name) || null
+}
+user.shaders.getShaders = function(){
+ var shaders = user.shaders.getShaderIndex()
+ return shaders.map(function(name){ return { name: name, shader: user.shaders.getShaderBody(name) } })
+}
+user.shaders.setShaderIndex = function(shaders){
+ localStorage.setItem("im.shaders", JSON.stringify(shaders))
+}
+user.shaders.setShader = function(name, shader){
+ if (! name.length || ! shader.length || !name.length || !shader.length) return
+ localStorage.setItem("im.shaders." + name, shader)
+}
+user.shaders.removeShader = function(name){
+ localStorage.removeItem("im.shaders." + name)
+}
+
+user.shaders.save = function(){
+ var name = prompt("enter shader name")
+ var shader = $("#shader").val()
+
+ if (! name) return;
+ if (shade !== shade_no_error_handling) return;
+
+ user.shaders.setShader(name, shader)
+
+ var shaders = user.shaders.getShaderIndex()
+ shaders.push(name)
+ user.shaders.setShaderIndex(shaders)
+
+ user.shaders.name = name
+ user.shaders.bindButton({ name: name, shader: shader })
+
+ $(".active").removeClass("active")
+ $(user.shaders.buttons[name]).addClass("active")
+ user.shaders.setLastAccessed(name)
+}
+
+user.shaders.remove = function(){
+ var name = user.shaders.name
+ if (! name || ! name.length) return
+
+ var shaders = user.shaders.getShaderIndex()
+ if (shaders.splice( shaders.indexOf(name), 1 )) {
+ user.shaders.setShaderIndex(shaders)
+ user.shaders.removeShader(name)
+ $(user.shaders.buttons[name]).remove()
+ user.shaders.name = ""
+ }
+}
diff --git a/js/api/set.js b/js/api/set.js
index 1e7e31f..834eaa8 100644
--- a/js/api/set.js
+++ b/js/api/set.js
@@ -1,39 +1,40 @@
function save_shader(){
- typeof shader_id_root == 'undefined' ? shader_id_root = "" : shader_id_root
- var params = {
- script : $("#shader").val(),
- image_url : $("#url").val(),
- username : user.username,
- shader_id : shader_id_root
- }
- console.log(params)
- $.post("/cgi-bin/im/shader/save", params, function(resp){
- console.log(resp);
- data = JSON.parse(resp)
- if (data.ERROR ){
- alert(data.ERROR)
- return false
- }
- if (! shader_id_root) {
- shader_id_root = data.id;
- }
+ typeof shader_id_root == 'undefined' ? shader_id_root = "" : shader_id_root
+ var params = {
+ script: $("#shader").val(),
+ image_url: $("#url").val(),
+ username: user.username,
+ name: $("#shader-name").val() || "",
+ shader_id: shader_id_root
+ }
+ console.log(params)
+ $.post("/cgi-bin/im/shader/save", params, function(resp){
+ console.log(resp);
+ data = JSON.parse(resp)
+ if (data.ERROR ){
+ alert(data.ERROR)
+ return false
+ }
+ if (! shader_id_root) {
+ shader_id_root = data.id;
+ }
- var blob = dataUriToBlob(cc.clone().resize(200,200).canvas.toDataURL("image/png"))
- var form = new FormData();
-
- form.append("id", data.id);
- form.append("qqfile", blob);
- $.ajax({
- url: "/cgi-bin/im/shader/thumbnail_upload",
- type: "POST",
- data: form,
- processData: false,
- contentType: false,
- }).done(function(resp){
- console.log(resp);
- });
+ var blob = dataUriToBlob(cc.clone().resize(200,200).canvas.toDataURL("image/png"))
+ var form = new FormData();
- })
- //maintain the shader_id_root...
- return shader_id_root;
+ form.append("id", data.id);
+ form.append("qqfile", blob);
+ $.ajax({
+ url: "/cgi-bin/im/shader/thumbnail_upload",
+ type: "POST",
+ data: form,
+ processData: false,
+ contentType: false,
+ }).done(function(resp){
+ console.log(resp);
+ });
+
+ })
+ //maintain the shader_id_root...
+ return shader_id_root;
}
diff --git a/js/user.js b/js/user.js
index 296974e..a1ac53e 100644
--- a/js/user.js
+++ b/js/user.js
@@ -37,109 +37,3 @@ user.setCookie = function(username){
document.cookie = "imname="+username+";path=/;domain=.asdf.us;max-age=1086400"
localStorage.setItem("im.name", username);
}
-
-user.shaders = new function(){}
-user.shaders.init = function(){
- user.shaders.bind()
-}
-user.shaders.bind = function(){
- user.shaders.buttons = {}
- if ('SHADERS' in window) {
- user.shaders.bindButtons( window.SHADERS )
- }
- user.shaders.bindButtons( user.shaders.getShaders() )
- document.getElementById("add-shader").addEventListener("click", user.shaders.save, false)
- document.getElementById("remove-shader").addEventListener("click", user.shaders.remove, false)
-}
-user.shaders.bindButtons = function(shaders){
- for (var i = 0; i < shaders.length; i++){
- user.shaders.bindButton(shaders[i])
- }
-}
-user.shaders.bindButton = function(data){
-// console.log("button >> " + data.name)
- var button = document.createElement("button")
- button.innerHTML = data.name
- button.addEventListener("click", user.shaders.load.bind(this, data), false)
- document.getElementById("shaders").appendChild(button)
- user.shaders.buttons[data.name] = button
-}
-user.shaders.load = function(data){
- var shader = data.shader
- if (shader && shader.length > 0) {
- user.shaders.name = data.name
- $("#shader").val(shader)
- // pause here?
- shader_build()
- $(".active").removeClass("active")
- $(user.shaders.buttons[data.name]).addClass("active")
- user.shaders.setLastAccessed(data.name)
- }
-}
-user.shaders.loadLastAccessed = function(){
- var name = localStorage.getItem("im.lastShader") || "colorcycle"
- if (name in user.shaders.buttons) {
- $(user.shaders.buttons[name]).trigger("click")
- }
-}
-user.shaders.setLastAccessed = function(name){
- localStorage.setItem("im.lastShader", name)
-}
-
-user.shaders.getShaderByName = function(name){
- return { name: name, shader: user.shaders.getShaderBody(name) }
-}
-user.shaders.getShaderIndex = function(){
- return JSON.parse( localStorage.getItem("im.shaders") ) || []
-}
-user.shaders.getShaderBody = function(name){
- return localStorage.getItem("im.shaders."+name) || null
-}
-user.shaders.getShaders = function(){
- var shaders = user.shaders.getShaderIndex()
- return shaders.map(function(name){ return { name: name, shader: user.shaders.getShaderBody(name) } })
-}
-user.shaders.setShaderIndex = function(shaders){
- localStorage.setItem("im.shaders", JSON.stringify(shaders))
-}
-user.shaders.setShader = function(name, shader){
- if (! name.length || ! shader.length || !name.length || !shader.length) return
- localStorage.setItem("im.shaders." + name, shader)
-}
-user.shaders.removeShader = function(name){
- localStorage.removeItem("im.shaders." + name)
-}
-
-user.shaders.save = function(){
- var name = prompt("enter shader name")
- var shader = $("#shader").val()
-
- if (! name) return;
- if (shade !== shade_no_error_handling) return;
-
- user.shaders.setShader(name, shader)
-
- var shaders = user.shaders.getShaderIndex()
- shaders.push(name)
- user.shaders.setShaderIndex(shaders)
-
- user.shaders.name = name
- user.shaders.bindButton({ name: name, shader: shader })
-
- $(".active").removeClass("active")
- $(user.shaders.buttons[name]).addClass("active")
- user.shaders.setLastAccessed(name)
-}
-
-user.shaders.remove = function(){
- var name = user.shaders.name
- if (! name || ! name.length) return
-
- var shaders = user.shaders.getShaderIndex()
- if (shaders.splice( shaders.indexOf(name), 1 )) {
- user.shaders.setShaderIndex(shaders)
- user.shaders.removeShader(name)
- $(user.shaders.buttons[name]).remove()
- user.shaders.name = ""
- }
-} \ No newline at end of file
diff --git a/shader-api.html b/shader-api.html
index 0aed936..8a4bc86 100644
--- a/shader-api.html
+++ b/shader-api.html
@@ -10,10 +10,9 @@ form { display: inline-block; }
#shader-api,#gallery { clear: right; width:100%; padding: 0; }
#shader-gallery,#gallery-images { display: block; min-height: 152px; max-height: 210px; overflow-y: auto; width: 100%; }
#shader-gallery img,#shader-gallery canvas,#gallery-images img, #gallery-images canvas { max-width: 200px; height: 100px; margin: 5px; cursor: pointer; }
-.shader {
- width: 100px;
-}
+.shader { width: 100px; }
#username { width: 40px; }
+#shader-name { width: 40px; }
a { color: #00f; }
#shader-id { width: 40px; }
</style>
@@ -52,6 +51,8 @@ a { color: #00f; }
<div id="controls">
<input type="text" id="url" value="img/1376516658960-dumpfm-DoritoWitch-TimeFLyTrans0001.png">
+ <input type="text" id="username" placeholder="username">
+ <input type="text" id="shader-name" placeholder="shader name">
<br>
<br>
@@ -76,7 +77,7 @@ a { color: #00f; }
<script type="text/javascript" src="js/api/get.js"></script>
<script type="text/javascript" src="js/api/set.js"></script>
<script type="text/html" id="shader-gallery-template">
-<img src="{thumbnail_url}">
+<span><img src="{thumbnail_url}"></span>
{username}
</script>
<script type="text/javascript">
diff --git a/shader-localstorage.html b/shader-localstorage.html
new file mode 100644
index 0000000..cc4bd18
--- /dev/null
+++ b/shader-localstorage.html
@@ -0,0 +1,211 @@
+<!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; }
+#instructions { 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; }
+#instructions iframe {width: 100%;height:100%;margin:0;padding:0;border:0;}
+#instructions.dragging iframe { pointer-events: none; }
+#instructions .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>
+ <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="1">
+ interval <input type="text" id="frameinterval" value="0.1s">
+ <button id="add-frame">+add frame</button>
+ <button id="remove-all-frames">clear</button>
+ <button id="render" disabled>render</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="help">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="instructions"><iframe src="instructions.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/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/asdf.js"></script>
+<script type="text/javascript" src="js/image.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/util.js"></script>
+<script type="text/javascript" src="shaders.json"></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
+ })
+
+ $("#add-frame").click(add_frame)
+ $("#frames").sortable({
+ start: drag_start,
+ stop: drag_stop
+ });
+ $(document).on("click","#frames .remove",remove_frame)
+ $("#framecount").change(function(){
+ var val = $(this).int()
+ if (val < 1 || isNaN(val)) $(this).val(val = 1)
+ if (val == 1) $("#add-frame").html("+add frame")
+ else $("#add-frame").html("+add frames")
+ })
+ $("#background").change(function(){
+ document.body.style.backgroundColor = $("#background").string()
+ })
+
+ $("#frames").disableSelection();
+ $("#remove-all-frames").click(remove_all_frames)
+ $("#weave-frames").click(weave_frames)
+ $("#shuffle-frames").click(shuffle_frames)
+ $("#reverse-frames").click(reverse_frames)
+ $("#sort-frames").click(sort_frames)
+
+ $("#render").click(render)
+ $("#save").click(save)
+ $("#upload").click(upload)
+ $("#help,#instructions .close").click(function(){ $("#instructions").toggle() })
+ $("#instructions").draggable({
+ start: drag_start,
+ stop: drag_stop
+ })
+ $("#instructions").disableSelection();
+
+ load()
+ $(window).on("scroll DOMMouseScroll mousewheel", function(){ scrolling = true })
+
+ gallery.choose = choose
+ gallery.init()
+
+ user.init()
+ if (user.username.length) {
+ console.log("signed in as", user.username)
+ }
+
+ user.shaders.init()
+ user.shaders.loadLastAccessed()
+
+ document.getElementById('shader').addEventListener('input', shader_build);
+ shader_build()
+
+ requestAnimationFrame(animate)
+}
+function drag_start(){ dragging = true; $(this).addClass("dragging") }
+function drag_stop(){ dragging = false; $(".dragging").removeClass("dragging") }
+
+</script>
+<script type="text/html" id="frame-template">
+<button class="remove">x</button>
+<span class="frame"></span>
+</script>
+</html>
+
diff --git a/shader-picker.html b/shader-picker.html
index d482a6b..cc4bd18 100644
--- a/shader-picker.html
+++ b/shader-picker.html
@@ -115,6 +115,7 @@ a { color: #00f; }
<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/asdf.js"></script>
<script type="text/javascript" src="js/image.js"></script>
<script type="text/javascript" src="js/gallery.js"></script>