diff --git a/files.html b/files.html
index 2078f41bdf04e7f9a48c316c2d9425fc8e105594..b9ad79c6fb81b7752a4ebb8e14e94ed4aa1b1b6c 100644
--- a/files.html
+++ b/files.html
@@ -42,6 +42,7 @@
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./modules/control/slider'>slider</a><br>
 <i>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;convert</i><br>
 <i>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;rgba</i><br>
+&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./modules/convert/rgba/jpg'>jpg</a><br>
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./modules/convert/rgba/png'>png</a><br>
 <i>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;svg</i><br>
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./modules/convert/svg/array'>array</a><br>
diff --git a/modules/convert/rgba/jpg b/modules/convert/rgba/jpg
new file mode 100755
index 0000000000000000000000000000000000000000..ce1c443a223c45910fe84b2ed4a8e7b3435be310
--- /dev/null
+++ b/modules/convert/rgba/jpg
@@ -0,0 +1,173 @@
+//
+// convert rgba jpg
+//
+// Neil Gershenfeld 
+// (c) Massachusetts Institute of Technology 2017
+// 
+// This work may be reproduced, modified, distributed, performed, and 
+// displayed for any purpose, but must acknowledge the mods
+// project. Copyright is retained and must be preserved. The work is 
+// provided as is; no warranty is provided, and users accept all 
+// liability.
+//
+// closure
+//
+(function(){
+//
+// module globals
+//
+var mod = {}
+//
+// name
+//
+var name = 'convert RGBA to JPG'
+//
+// initialization
+//
+var init = function() {
+   mod.name.value = "file.jpg"
+   mod.compress.value = .75
+   }
+//
+// inputs
+//
+var inputs = {
+   image:{type:'RGBA',
+      event:function(evt){
+         var ctx = mod.img.getContext("2d")
+         ctx.canvas.width = evt.detail.width
+         ctx.canvas.height = evt.detail.height 
+         ctx.putImageData(evt.detail,0,0)
+         mod.pxtext.nodeValue = evt.detail.width+' x '+evt.detail.height+' px'
+         convert_image()
+         }},
+   imageInfo:{type:'object',
+      event:function(evt){
+         mod.name.value = evt.detail.name+'.jpg'
+         }}
+   }
+//
+// outputs
+//
+var outputs = {
+   }
+//
+// interface
+//
+var interface = function(div){
+   mod.div = div
+   //
+   // on-screen drawing canvas
+   //
+   var canvas = document.createElement('canvas')
+      canvas.width = mods.ui.canvas
+      canvas.height = mods.ui.canvas
+      canvas.style.backgroundColor = 'rgb(255,255,255)'
+      div.appendChild(canvas)
+      mod.canvas = canvas
+   div.appendChild(document.createElement('br'))
+   //
+   // off-screen image canvas
+   //
+   var canvas = document.createElement('canvas')
+      mod.img = canvas
+   //
+   // view button
+   //
+   var btn = document.createElement('button')
+      btn.style.padding = mods.ui.padding
+      btn.style.margin = 1
+      btn.appendChild(document.createTextNode('view'))
+      btn.addEventListener('click',function(){
+         var win = window.open('')
+         var btn = document.createElement('button')
+            btn.appendChild(document.createTextNode('close'))
+            btn.style.padding = mods.ui.padding
+            btn.style.margin = 1
+            btn.addEventListener('click',function(){
+               win.close()
+               })
+            win.document.body.appendChild(btn)
+         win.document.body.appendChild(document.createElement('br'))
+         var canvas = document.createElement('canvas')
+            canvas.width = mod.img.width
+            canvas.height = mod.img.height
+            win.document.body.appendChild(canvas)
+         var ctx = canvas.getContext("2d")
+            ctx.drawImage(mod.img,0,0)
+         })
+      div.appendChild(btn)
+   div.appendChild(document.createTextNode(' '))
+   //
+   // info div
+   //
+   var info = document.createElement('div')
+      info.appendChild(document.createTextNode('file name: '))
+      var input = document.createElement('input')
+         input.type = 'text'
+         input.size = 6
+         info.appendChild(input)
+         mod.name = input
+      info.appendChild(document.createElement('br'))
+      info.appendChild(document.createTextNode('compression: '))
+      var input = document.createElement('input')
+         input.type = 'text'
+         input.size = 6
+         info.appendChild(input)
+         mod.compress = input
+      info.appendChild(document.createTextNode(' (0-1)'))
+      info.appendChild(document.createElement('br'))
+      var text = document.createTextNode('px: ')
+         info.appendChild(text)
+         mod.pxtext = text
+      div.appendChild(info)
+   }
+//
+// local functions
+//
+function convert_image() {
+   //
+   // preview
+   //
+   var h = mod.img.height
+   var w = mod.img.width
+   if (w > h) {
+      var x0 = 0
+      var y0 = mod.canvas.height*.5*(1-h/w)
+      var wd = mod.canvas.width
+      var hd = mod.canvas.width*h/w
+      }
+   else {
+      var x0 = mod.canvas.width*.5*(1-w/h)
+      var y0 = 0
+      var wd = mod.canvas.height*w/h
+      var hd = mod.canvas.height
+      }
+   var ctx = mod.canvas.getContext("2d")
+   ctx.clearRect(0,0,mod.canvas.width,mod.canvas.height)
+   ctx.drawImage(mod.img,x0,y0,wd,hd)
+   //
+   // convert and save
+   //
+   mod.img.toBlob(function(blob){
+      var url = URL.createObjectURL(blob)
+      var link = document.createElement('a')
+      link.download = mod.name.value
+      link.href = url
+      document.body.appendChild(link)
+      link.click()
+      document.body.removeChild(link)
+      URL.revokeObjectURL(url)
+      },'image/jpeg',parseFloat(mod.compress.value))
+   }
+//
+// return values
+//
+return ({
+   name:name,
+   init:init,
+   inputs:inputs,
+   outputs:outputs,
+   interface:interface
+   })
+}())
diff --git a/modules/convert/rgba/png b/modules/convert/rgba/png
index d5ff0ed2c86314cf93a29bae5afd3aa15de97ac6..acaec9b6cdd5087a64e735858898b50f1a5afd7e 100755
--- a/modules/convert/rgba/png
+++ b/modules/convert/rgba/png
@@ -25,8 +25,7 @@ var name = 'convert RGBA to PNG'
 // initialization
 //
 var init = function() {
-   mod.nametext.value = "file.png"
-   mod.dpitext.value = 100
+   mod.name.value = "file.png"
    }
 //
 // inputs
@@ -43,9 +42,7 @@ var inputs = {
          }},
    imageInfo:{type:'object',
       event:function(evt){
-         mod.nametext.value = evt.detail.name
-         mod.dpitext.value = evt.detail.dpi
-         update_info()
+         mod.name.value = evt.detail.name+'.png'
          }}
    }
 //
@@ -109,35 +106,11 @@ var interface = function(div){
          input.type = 'text'
          input.size = 6
          info.appendChild(input)
-         mod.nametext = input
-      info.appendChild(document.createElement('br'))
-      info.appendChild(document.createTextNode('dpi: '))
-      var input = document.createElement('input')
-         input.type = 'text'
-         input.size = 6
-         input.addEventListener('input',function(){
-            mod.dpi = parseFloat(mod.dpitext.value)
-            mod.mmtext.nodeValue = (25.4*mod.img.width/mod.dpi).toFixed(3)
-               +' x '+(25.4*mod.img.height/mod.dpi).toFixed(3)+' mm'
-            mod.intext.nodeValue = (mod.img.width/mod.dpi).toFixed(3)
-               +' x '+(mod.img.height/mod.dpi).toFixed(3)+' in'
-            outputs.imageInfo.event()
-            })
-         info.appendChild(input)
-         mod.dpitext = input
+         mod.name = input
       info.appendChild(document.createElement('br'))
       var text = document.createTextNode('px: ')
          info.appendChild(text)
          mod.pxtext = text
-      info.appendChild(document.createElement('br'))
-      var text = document.createTextNode('mm: ')
-         info.appendChild(text)
-         mod.mmtext = text
-      info.appendChild(document.createElement('br'))
-      var text = document.createTextNode('in: ')
-         info.appendChild(text)
-         mod.intext = text
-      info.appendChild(document.createElement('br'))
       div.appendChild(info)
    }
 //
@@ -170,7 +143,7 @@ function convert_image() {
    mod.img.toBlob(function(blob){
       var url = URL.createObjectURL(blob)
       var link = document.createElement('a')
-      link.download = mod.nametext.value
+      link.download = mod.name.value
       link.href = url
       document.body.appendChild(link)
       link.click()
@@ -178,13 +151,7 @@ function convert_image() {
       URL.revokeObjectURL(url)
       },'image/png')
    }
-function update_info() {
-   mod.dpi = parseFloat(mod.dpitext.value)
-   mod.mmtext.nodeValue = (25.4*mod.img.width/mod.dpi).toFixed(3)
-      +' x '+(25.4*mod.img.height/mod.dpi).toFixed(3)+' mm'
-   mod.intext.nodeValue = (mod.img.width/mod.dpi).toFixed(3)
-      +' x '+(mod.img.height/mod.dpi).toFixed(3)+' in'
-   }
+//
 // return values
 //
 return ({
diff --git a/modules/image/motion detect b/modules/image/motion detect
index fb161a60b1ce7ce61f40773e15259ce257ad9f0e..2fe373e4e84bd72957918846a6a94da7793dbc43 100755
--- a/modules/image/motion detect	
+++ b/modules/image/motion detect	
@@ -25,7 +25,7 @@ var name = 'motion detect'
 // initialization
 //
 var init = function() {
-   mod.threshold.value = 0.1
+   mod.threshold.value = 0.01
    mod.time.value = 15
    mod.dpi = 100
    timeout()
@@ -146,7 +146,6 @@ function timeout() {
    setTimeout(timeout,parseFloat(mod.time.value)*1000)
    }
 function compare_images() {
-   //mod.change.nodeValue = Date.now()
    var blob = new Blob(['('+worker.toString()+'())'])
    var url = window.URL.createObjectURL(blob)
    var webworker = new Worker(url)
@@ -162,7 +161,7 @@ function compare_images() {
          var hour = ('0'+date.getHours()).slice(-2)
          var minute = ('0'+date.getMinutes()).slice(-2)
          var second = ('0'+date.getSeconds()).slice(-2)
-         var name = year+'-'+month+'-'+day+'-'+hour+'-'+minute+'-'+second+'.png'
+         var name = year+'-'+month+'-'+day+'-'+hour+'-'+minute+'-'+second
          obj.name = name
          obj.dpi = mod.dpi
          obj.width = mod.img.width
@@ -223,6 +222,7 @@ function worker() {
       self.postMessage({change:change})
       })
    }
+//
 // return values
 //
 return ({
diff --git a/modules/index.html b/modules/index.html
index b06b2bb5158b788faf09c2308f48755db4734a91..1dbe4ae1bf9b71e64d441db52b97cfbd10e68638 100644
--- a/modules/index.html
+++ b/modules/index.html
@@ -25,6 +25,7 @@
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="javascript:handler('modules/control/slider')">slider</a><br>
 <i>convert</i><br>
 <i>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;rgba</i><br>
+&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="javascript:handler('modules/convert/rgba/jpg')">jpg</a><br>
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="javascript:handler('modules/convert/rgba/png')">png</a><br>
 <i>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;svg</i><br>
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="javascript:handler('modules/convert/svg/array')">array</a><br>
diff --git a/modules/input/video b/modules/input/video
index 1cb52e68fe19cef7f62106d3495b68012c72303c..fb80006a96be73cfcf670290d9fa2d5ca707e03f 100644
--- a/modules/input/video
+++ b/modules/input/video
@@ -131,10 +131,18 @@ var interface = function(div){
       mod.height = input
    div.appendChild(document.createElement('br'))
    //
+   // flip
+   //
+   div.appendChild(document.createTextNode('flip image: '))
+   var input = document.createElement('input')
+      input.type = 'checkbox'
+      div.appendChild(input)
+      mod.flip = input
+   div.appendChild(document.createElement('br'))
+   //
    // video element
    //
    var video = document.createElement('video')
-      //div.appendChild(video)
       mod.video = video
    }
 //
@@ -175,22 +183,67 @@ function capture_video() {
    var h = parseInt(mod.height.value)
    var ctx = mod.img.getContext("2d")
    ctx.drawImage(mod.video,0,0,w,h)
-   if (w > h) {
-      var x0 = 0
-      var y0 = mod.canvas.height*.5*(1-h/w)
-      var wd = mod.canvas.width
-      var hd = mod.canvas.width*h/w
-      }
-   else {
-      var x0 = mod.canvas.width*.5*(1-w/h)
-      var y0 = 0
-      var wd = mod.canvas.height*w/h
-      var hd = mod.canvas.height
-      }
-   var ctx = mod.canvas.getContext("2d")
-   ctx.clearRect(0,0,mod.canvas.width,mod.canvas.height)
-   ctx.drawImage(mod.img,x0,y0,wd,hd)
-   outputs.image.event()
+   var blob = new Blob(['('+worker.toString()+'())'])
+   var url = window.URL.createObjectURL(blob)
+   var webworker = new Worker(url)
+   webworker.addEventListener('message',function(evt) {
+      window.URL.revokeObjectURL(url)
+      var h = mod.img.height
+      var w = mod.img.width
+      var buf = new Uint8ClampedArray(evt.data.buffer)
+      var imgdata = new ImageData(buf,w,h)
+      var ctx = mod.img.getContext("2d")
+      ctx.putImageData(imgdata,0,0)
+      if (w > h) {
+         var x0 = 0
+         var y0 = mod.canvas.height*.5*(1-h/w)
+         var wd = mod.canvas.width
+         var hd = mod.canvas.width*h/w
+         }
+      else {
+         var x0 = mod.canvas.width*.5*(1-w/h)
+         var y0 = 0
+         var wd = mod.canvas.height*w/h
+         var hd = mod.canvas.height
+         }
+      var ctx = mod.canvas.getContext("2d")
+      ctx.clearRect(0,0,mod.canvas.width,mod.canvas.height)
+      ctx.drawImage(mod.img,x0,y0,wd,hd)
+      outputs.image.event()
+      webworker.terminate()
+      })
+   var ctx = mod.img.getContext("2d")
+   var img = ctx.getImageData(0,0,mod.img.width,mod.img.height)
+   webworker.postMessage({
+      height:mod.img.height,width:mod.img.width,
+      checked:mod.flip.checked,
+      buffer:img.data.buffer},[img.data.buffer])
+   }
+function worker() {
+   self.addEventListener('message',function(evt) {
+      var h = evt.data.height
+      var w = evt.data.width
+      var checked = evt.data.checked
+      var buf = new Uint8ClampedArray(evt.data.buffer)
+      if (checked == true) {
+         var newbuf = new Uint8ClampedArray(buf.length)
+         for (var row = 0; row < h; ++row) {
+            for (var col = 0; col < w; ++col) {
+               newbuf[(h-1-row)*w*4+col*4+0] = 
+                  buf[row*w*4+(w-1-col)*4+0] 
+               newbuf[(h-1-row)*w*4+col*4+1] = 
+                  buf[row*w*4+(w-1-col)*4+1]
+               newbuf[(h-1-row)*w*4+col*4+2] = 
+                  buf[row*w*4+(w-1-col)*4+2]
+               newbuf[(h-1-row)*w*4+col*4+3] = 
+                  buf[row*w*4+(w-1-col)*4+3]
+               }
+            }
+         self.postMessage({buffer:newbuf.buffer},[newbuf.buffer])
+         }
+      else
+         self.postMessage({buffer:buf.buffer},[buf.buffer])
+      })
    }
 //
 // return values
diff --git a/programs/image/motion detect b/programs/image/motion detect
index 0eadde01cb5123cb73eee59485b3a8410c641f44..8c4bfb3f2ab1167386d619876569c97f2c71e143 100644
--- a/programs/image/motion detect	
+++ b/programs/image/motion detect	
@@ -1 +1 @@
-{"modules":{"0.022552610085642244":{"definition":"//\n// motion detect\n//\n// Neil Gershenfeld \n// (c) Massachusetts Institute of Technology 2017\n// \n// This work may be reproduced, modified, distributed, performed, and \n// displayed for any purpose, but must acknowledge the mods\n// project. Copyright is retained and must be preserved. The work is \n// provided as is; no warranty is provided, and users accept all \n// liability.\n//\n// closure\n//\n(function(){\n//\n// module globals\n//\nvar mod = {}\n//\n// name\n//\nvar name = 'motion detect'\n//\n// initialization\n//\nvar init = function() {\n   mod.threshold.value = 0.1\n   mod.time.value = 15\n   mod.dpi = 100\n   timeout()\n   }\n//\n// inputs\n//\nvar inputs = {\n   image:{type:'RGBA',\n      event:function(evt){\n         var ctx = mod.img.getContext(\"2d\")\n         var lastctx = mod.lastimg.getContext(\"2d\")\n         lastctx.canvas.width = ctx.canvas.width\n         lastctx.canvas.height = ctx.canvas.height\n         lastctx.drawImage(mod.img,0,0)\n         ctx.canvas.width = evt.detail.width\n         ctx.canvas.height = evt.detail.height \n         ctx.putImageData(evt.detail,0,0)\n         compare_images()\n         }}}\n//\n// outputs\n//\nvar outputs = {\n   image:{type:'RGBA',\n      event:function(){\n         var ctx = mod.img.getContext(\"2d\")\n         var img = ctx.getImageData(0,0,mod.img.width,mod.img.height)\n         mods.output(mod,'image',img)}},\n   imageInfo:{type:'object',\n      event:function(obj){\n         mods.output(mod,'imageInfo',obj)}},\n   trigger:{type:'event',\n      event:function(){\n         mods.output(mod,'trigger',null)}}}\n//\n// interface\n//\nvar interface = function(div){\n   mod.div = div\n   //\n   // on-screen drawing canvas\n   //\n   var canvas = document.createElement('canvas')\n      canvas.width = mods.ui.canvas\n      canvas.height = mods.ui.canvas\n      canvas.style.backgroundColor = 'rgb(255,255,255)'\n      div.appendChild(canvas)\n      mod.canvas = canvas\n   div.appendChild(document.createElement('br'))\n   //\n   // off-screen image canvas\n   //\n   var canvas = document.createElement('canvas')\n      mod.img = canvas\n   //\n   // off-screen last image canvas\n   //\n   var canvas = document.createElement('canvas')\n      mod.lastimg = canvas\n   //\n   // view button\n   //\n   var btn = document.createElement('button')\n      btn.style.padding = mods.ui.padding\n      btn.style.margin = 1\n      btn.appendChild(document.createTextNode('view'))\n      btn.addEventListener('click',function(){\n         var win = window.open('')\n         var btn = document.createElement('button')\n            btn.appendChild(document.createTextNode('close'))\n            btn.style.padding = mods.ui.padding\n            btn.style.margin = 1\n            btn.addEventListener('click',function(){\n               win.close()\n               })\n            win.document.body.appendChild(btn)\n         win.document.body.appendChild(document.createElement('br'))\n         var canvas = document.createElement('canvas')\n            canvas.width = mod.img.width\n            canvas.height = mod.img.height\n            win.document.body.appendChild(canvas)\n         var ctx = canvas.getContext(\"2d\")\n            ctx.drawImage(mod.img,0,0)\n         })\n      div.appendChild(btn)\n   div.appendChild(document.createTextNode(' '))\n   //\n   // info div\n   //\n   var info = document.createElement('div')\n      var text = document.createTextNode('relative change: ')\n         info.appendChild(text)\n         mod.change = text\n      info.appendChild(document.createElement('br'))\n      info.appendChild(document.createTextNode('threshold: '))\n      var input = document.createElement('input')\n         input.type = 'text'\n         input.size = 6\n         info.appendChild(input)\n         mod.threshold = input\n      info.appendChild(document.createTextNode(' (0-1)'))\n      info.appendChild(document.createElement('br'))\n      info.appendChild(document.createTextNode('latency: '))\n      var input = document.createElement('input')\n         input.type = 'text'\n         input.size = 6\n         info.appendChild(input)\n         mod.time = input\n      info.appendChild(document.createTextNode(' (s)'))\n      div.appendChild(info)\n   }\n//\n// local functions\n//\nfunction timeout() {\n   outputs.trigger.event()\n   setTimeout(timeout,parseFloat(mod.time.value)*1000)\n   }\nfunction compare_images() {\n   //mod.change.nodeValue = Date.now()\n   var blob = new Blob(['('+worker.toString()+'())'])\n   var url = window.URL.createObjectURL(blob)\n   var webworker = new Worker(url)\n   webworker.addEventListener('message',function(evt) {\n      window.URL.revokeObjectURL(url)\n      mod.change.nodeValue = 'relative change: '+evt.data.change.toFixed(3)\n      if (evt.data.change > parseFloat(mod.threshold.value)) {\n         var obj = {}\n         var date = new Date()\n         var year = date.getFullYear()\n         var month = ('0'+(1+parseInt(date.getMonth()))).slice(-2)\n         var day = ('0'+date.getDate()).slice(-2)\n         var hour = ('0'+date.getHours()).slice(-2)\n         var minute = ('0'+date.getMinutes()).slice(-2)\n         var second = ('0'+date.getSeconds()).slice(-2)\n         var name = year+'-'+month+'-'+day+'-'+hour+'-'+minute+'-'+second+'.png'\n         obj.name = name\n         obj.dpi = mod.dpi\n         obj.width = mod.img.width\n         obj.height = mod.img.height\n         outputs.imageInfo.event(obj)\n         outputs.image.event()\n         }\n      var h = mod.img.height\n      var w = mod.img.width\n      if (w > h) {\n         var x0 = 0\n         var y0 = mod.canvas.height*.5*(1-h/w)\n         var wd = mod.canvas.width\n         var hd = mod.canvas.width*h/w\n         }\n      else {\n         var x0 = mod.canvas.width*.5*(1-w/h)\n         var y0 = 0\n         var wd = mod.canvas.height*w/h\n         var hd = mod.canvas.height\n         }\n      var ctx = mod.canvas.getContext(\"2d\")\n      ctx.clearRect(0,0,mod.canvas.width,mod.canvas.height)\n      ctx.drawImage(mod.img,x0,y0,wd,hd)\n      webworker.terminate()\n      })\n   var ctx = mod.img.getContext(\"2d\")\n   var img = ctx.getImageData(0,0,mod.img.width,mod.img.height)\n   var ctx = mod.lastimg.getContext(\"2d\")\n   var lastimg = ctx.getImageData(0,0,mod.img.width,mod.img.height)\n   var t = parseFloat(mod.threshold.value)\n   webworker.postMessage({\n      height:mod.img.height,width:mod.img.width,threshold:t,\n      buffer:img.data.buffer,lastbuffer:lastimg.data.buffer})\n   }\nfunction worker() {\n   self.addEventListener('message',function(evt) {\n      var h = evt.data.height\n      var w = evt.data.width\n      var t = evt.data.threshold\n      var buf = new Uint8ClampedArray(evt.data.buffer)\n      var lastbuf = new Uint8ClampedArray(evt.data.lastbuffer)\n      var change = 0\n      for (var row = 0; row < h; ++row) {\n         for (var col = 0; col < w; ++col) {\n            r = buf[(h-1-row)*w*4+col*4+0] \n            g = buf[(h-1-row)*w*4+col*4+1] \n            b = buf[(h-1-row)*w*4+col*4+2] \n            rl = lastbuf[(h-1-row)*w*4+col*4+0] \n            gl = lastbuf[(h-1-row)*w*4+col*4+1] \n            bl = lastbuf[(h-1-row)*w*4+col*4+2] \n            change += (Math.abs(r-rl)/255 \n               +Math.abs(g-gl)/255\n               +Math.abs(b-bl)/255)/3\n            }\n         }\n      change = change/(w*h)\n      self.postMessage({change:change})\n      })\n   }\n// return values\n//\nreturn ({\n   name:name,\n   init:init,\n   inputs:inputs,\n   outputs:outputs,\n   interface:interface\n   })\n}())\n","top":"102","left":"204","inputs":{},"outputs":{}},"0.6300838506360679":{"definition":"//\n// convert rgba png\n//\n// Neil Gershenfeld \n// (c) Massachusetts Institute of Technology 2017\n// \n// This work may be reproduced, modified, distributed, performed, and \n// displayed for any purpose, but must acknowledge the mods\n// project. Copyright is retained and must be preserved. The work is \n// provided as is; no warranty is provided, and users accept all \n// liability.\n//\n// closure\n//\n(function(){\n//\n// module globals\n//\nvar mod = {}\n//\n// name\n//\nvar name = 'convert RGBA to PNG'\n//\n// initialization\n//\nvar init = function() {\n   mod.nametext.value = \"file.png\"\n   mod.dpitext.value = 100\n   }\n//\n// inputs\n//\nvar inputs = {\n   image:{type:'RGBA',\n      event:function(evt){\n         var ctx = mod.img.getContext(\"2d\")\n         ctx.canvas.width = evt.detail.width\n         ctx.canvas.height = evt.detail.height \n         ctx.putImageData(evt.detail,0,0)\n         mod.pxtext.nodeValue = evt.detail.width+' x '+evt.detail.height+' px'\n         convert_image()\n         }},\n   imageInfo:{type:'object',\n      event:function(evt){\n         mod.nametext.value = evt.detail.name\n         mod.dpitext.value = evt.detail.dpi\n         update_info()\n         }}\n   }\n//\n// outputs\n//\nvar outputs = {\n   }\n//\n// interface\n//\nvar interface = function(div){\n   mod.div = div\n   //\n   // on-screen drawing canvas\n   //\n   var canvas = document.createElement('canvas')\n      canvas.width = mods.ui.canvas\n      canvas.height = mods.ui.canvas\n      canvas.style.backgroundColor = 'rgb(255,255,255)'\n      div.appendChild(canvas)\n      mod.canvas = canvas\n   div.appendChild(document.createElement('br'))\n   //\n   // off-screen image canvas\n   //\n   var canvas = document.createElement('canvas')\n      mod.img = canvas\n   //\n   // view button\n   //\n   var btn = document.createElement('button')\n      btn.style.padding = mods.ui.padding\n      btn.style.margin = 1\n      btn.appendChild(document.createTextNode('view'))\n      btn.addEventListener('click',function(){\n         var win = window.open('')\n         var btn = document.createElement('button')\n            btn.appendChild(document.createTextNode('close'))\n            btn.style.padding = mods.ui.padding\n            btn.style.margin = 1\n            btn.addEventListener('click',function(){\n               win.close()\n               })\n            win.document.body.appendChild(btn)\n         win.document.body.appendChild(document.createElement('br'))\n         var canvas = document.createElement('canvas')\n            canvas.width = mod.img.width\n            canvas.height = mod.img.height\n            win.document.body.appendChild(canvas)\n         var ctx = canvas.getContext(\"2d\")\n            ctx.drawImage(mod.img,0,0)\n         })\n      div.appendChild(btn)\n   div.appendChild(document.createTextNode(' '))\n   //\n   // info div\n   //\n   var info = document.createElement('div')\n      info.appendChild(document.createTextNode('file name: '))\n      var input = document.createElement('input')\n         input.type = 'text'\n         input.size = 6\n         info.appendChild(input)\n         mod.nametext = input\n      info.appendChild(document.createElement('br'))\n      info.appendChild(document.createTextNode('dpi: '))\n      var input = document.createElement('input')\n         input.type = 'text'\n         input.size = 6\n         input.addEventListener('input',function(){\n            mod.dpi = parseFloat(mod.dpitext.value)\n            mod.mmtext.nodeValue = (25.4*mod.img.width/mod.dpi).toFixed(3)\n               +' x '+(25.4*mod.img.height/mod.dpi).toFixed(3)+' mm'\n            mod.intext.nodeValue = (mod.img.width/mod.dpi).toFixed(3)\n               +' x '+(mod.img.height/mod.dpi).toFixed(3)+' in'\n            outputs.imageInfo.event()\n            })\n         info.appendChild(input)\n         mod.dpitext = input\n      info.appendChild(document.createElement('br'))\n      var text = document.createTextNode('px: ')\n         info.appendChild(text)\n         mod.pxtext = text\n      info.appendChild(document.createElement('br'))\n      var text = document.createTextNode('mm: ')\n         info.appendChild(text)\n         mod.mmtext = text\n      info.appendChild(document.createElement('br'))\n      var text = document.createTextNode('in: ')\n         info.appendChild(text)\n         mod.intext = text\n      info.appendChild(document.createElement('br'))\n      div.appendChild(info)\n   }\n//\n// local functions\n//\nfunction convert_image() {\n   //\n   // preview\n   //\n   var h = mod.img.height\n   var w = mod.img.width\n   if (w > h) {\n      var x0 = 0\n      var y0 = mod.canvas.height*.5*(1-h/w)\n      var wd = mod.canvas.width\n      var hd = mod.canvas.width*h/w\n      }\n   else {\n      var x0 = mod.canvas.width*.5*(1-w/h)\n      var y0 = 0\n      var wd = mod.canvas.height*w/h\n      var hd = mod.canvas.height\n      }\n   var ctx = mod.canvas.getContext(\"2d\")\n   ctx.clearRect(0,0,mod.canvas.width,mod.canvas.height)\n   ctx.drawImage(mod.img,x0,y0,wd,hd)\n   //\n   // convert and save\n   //\n   mod.img.toBlob(function(blob){\n      var url = URL.createObjectURL(blob)\n      var link = document.createElement('a')\n      link.download = mod.nametext.value\n      link.href = url\n      document.body.appendChild(link)\n      link.click()\n      document.body.removeChild(link)\n      URL.revokeObjectURL(url)\n      },'image/png')\n   }\nfunction update_info() {\n   mod.dpi = parseFloat(mod.dpitext.value)\n   mod.mmtext.nodeValue = (25.4*mod.img.width/mod.dpi).toFixed(3)\n      +' x '+(25.4*mod.img.height/mod.dpi).toFixed(3)+' mm'\n   mod.intext.nodeValue = (mod.img.width/mod.dpi).toFixed(3)\n      +' x '+(mod.img.height/mod.dpi).toFixed(3)+' in'\n   }\n// return values\n//\nreturn ({\n   name:name,\n   init:init,\n   inputs:inputs,\n   outputs:outputs,\n   interface:interface\n   })\n}())\n","top":"103","left":"1034","inputs":{},"outputs":{}},"0.9649518313148912":{"definition":"//\n// video\n//\n// Neil Gershenfeld \n// (c) Massachusetts Institute of Technology 2015,6\n// \n// This work may be reproduced, modified, distributed, performed, and \n// displayed for any purpose, but must acknowledge the mods\n// project. Copyright is retained and must be preserved. The work is \n// provided as is; no warranty is provided, and users accept all \n// liability.\n//\n// closure\n//\n(function(){\n//\n// module globals\n//\nvar mod = {}\n//\n// name\n//\nvar name = 'video'\n//\n// initialization\n//\nvar init = function() {\n   mod.width.value = 1280\n   mod.height.value = 720\n   start_video()\n   }\n//\n// inputs\n//\nvar inputs = {\n   capture:{type:'event',\n      event:function(evt){\n         capture_video()}}}\n//\n// outputs\n//\nvar outputs = {\n   image:{type:'RGBA',\n      event:function(){\n         var ctx = mod.img.getContext(\"2d\")\n         var img = ctx.getImageData(0,0,mod.img.width,mod.img.height)\n         mods.output(mod,'image',img)}}}\n//\n// interface\n//\nvar interface = function(div){\n   mod.div = div\n   //\n   // on-screen drawing canvas\n   //\n   var canvas = document.createElement('canvas')\n      canvas.width = mods.ui.canvas\n      canvas.height = mods.ui.canvas\n      canvas.style.backgroundColor = 'rgb(255,255,255)'\n      div.appendChild(canvas)\n      mod.canvas = canvas\n   div.appendChild(document.createElement('br'))\n   //\n   // off-screen image canvas\n   //\n   var canvas = document.createElement('canvas')\n      mod.img = canvas\n   //\n   // capture button\n   //\n   var btn = document.createElement('button')\n      btn.style.padding = mods.ui.padding\n      btn.style.margin = 1\n      btn.appendChild(document.createTextNode('capture'))\n      btn.addEventListener('click',function() {\n         capture_video()\n         })\n      div.appendChild(btn)\n   div.appendChild(document.createTextNode(' '))\n   //\n   // view button\n   //\n   var btn = document.createElement('button')\n      btn.style.padding = mods.ui.padding\n      btn.style.margin = 1\n      btn.appendChild(document.createTextNode('view'))\n      btn.addEventListener('click',function(){\n         var win = window.open('')\n         var btn = document.createElement('button')\n            btn.appendChild(document.createTextNode('close'))\n            btn.style.padding = mods.ui.padding\n            btn.style.margin = 1\n            btn.addEventListener('click',function(){\n               win.close()\n               })\n            win.document.body.appendChild(btn)\n         win.document.body.appendChild(document.createElement('br'))\n         var canvas = document.createElement('canvas')\n            canvas.width = mod.img.width\n            canvas.height = mod.img.height\n            win.document.body.appendChild(canvas)\n         var ctx = canvas.getContext(\"2d\")\n            ctx.drawImage(mod.img,0,0)\n         })\n      div.appendChild(btn)\n   div.appendChild(document.createElement('br'))\n   //\n   // width\n   //\n   div.appendChild(document.createTextNode('width: '))\n   var input = document.createElement('input')\n      input.type = 'text'\n      input.size = 6\n      input.addEventListener('change',function() {\n         update_video()\n         })\n      div.appendChild(input)\n      mod.width = input\n   div.appendChild(document.createElement('br'))\n   //\n   // height\n   //\n   div.appendChild(document.createTextNode(' height: '))\n   var input = document.createElement('input')\n      input.type = 'text'\n      input.size = 6\n      input.addEventListener('change',function() {\n         update_video()\n         })\n      div.appendChild(input)\n      mod.height = input\n   div.appendChild(document.createElement('br'))\n   //\n   // video element\n   //\n   var video = document.createElement('video')\n      //div.appendChild(video)\n      mod.video = video\n   }\n//\n// local functions\n//\nfunction start_video() {\n   var w = parseInt(mod.width.value)\n   var h = parseInt(mod.height.value)\n   var ctx = mod.img.getContext(\"2d\")\n   ctx.canvas.width = w\n   ctx.canvas.height = h\n   var constraints = {\n      audio:false,\n      video:{width:w,height:h}\n      }\n   navigator.mediaDevices.getUserMedia(constraints)\n      .then(function(stream) {\n         mod.video.srcObject = stream\n         mod.video.onloadedmetadata = function(e) {\n            mod.video.play()\n            }\n         })\n      .catch(function(err) {\n         console.log(err.name + \": \"+err.message)\n         })\n   }\nfunction update_video() {\n   var w = parseInt(mod.width.value)\n   var h = parseInt(mod.height.value)\n   mod.video.setAttribute('width',w)\n   mod.video.setAttribute('height',h)\n   var ctx = mod.img.getContext(\"2d\")\n   ctx.canvas.width = w\n   ctx.canvas.height = h\n   }\nfunction capture_video() {\n   var w = parseInt(mod.width.value)\n   var h = parseInt(mod.height.value)\n   var ctx = mod.img.getContext(\"2d\")\n   ctx.drawImage(mod.video,0,0,w,h)\n   if (w > h) {\n      var x0 = 0\n      var y0 = mod.canvas.height*.5*(1-h/w)\n      var wd = mod.canvas.width\n      var hd = mod.canvas.width*h/w\n      }\n   else {\n      var x0 = mod.canvas.width*.5*(1-w/h)\n      var y0 = 0\n      var wd = mod.canvas.height*w/h\n      var hd = mod.canvas.height\n      }\n   var ctx = mod.canvas.getContext(\"2d\")\n   ctx.clearRect(0,0,mod.canvas.width,mod.canvas.height)\n   ctx.drawImage(mod.img,x0,y0,wd,hd)\n   outputs.image.event()\n   }\n//\n// return values\n//\nreturn ({\n   name:name,\n   init:init,\n   inputs:inputs,\n   outputs:outputs,\n   interface:interface\n   })\n}())\n","top":"262","left":"681","inputs":{},"outputs":{}}},"links":["{\"source\":\"{\\\"id\\\":\\\"0.022552610085642244\\\",\\\"type\\\":\\\"outputs\\\",\\\"name\\\":\\\"imageInfo\\\"}\",\"dest\":\"{\\\"id\\\":\\\"0.6300838506360679\\\",\\\"type\\\":\\\"inputs\\\",\\\"name\\\":\\\"imageInfo\\\"}\"}","{\"source\":\"{\\\"id\\\":\\\"0.022552610085642244\\\",\\\"type\\\":\\\"outputs\\\",\\\"name\\\":\\\"image\\\"}\",\"dest\":\"{\\\"id\\\":\\\"0.6300838506360679\\\",\\\"type\\\":\\\"inputs\\\",\\\"name\\\":\\\"image\\\"}\"}","{\"source\":\"{\\\"id\\\":\\\"0.022552610085642244\\\",\\\"type\\\":\\\"outputs\\\",\\\"name\\\":\\\"trigger\\\"}\",\"dest\":\"{\\\"id\\\":\\\"0.9649518313148912\\\",\\\"type\\\":\\\"inputs\\\",\\\"name\\\":\\\"capture\\\"}\"}","{\"source\":\"{\\\"id\\\":\\\"0.9649518313148912\\\",\\\"type\\\":\\\"outputs\\\",\\\"name\\\":\\\"image\\\"}\",\"dest\":\"{\\\"id\\\":\\\"0.022552610085642244\\\",\\\"type\\\":\\\"inputs\\\",\\\"name\\\":\\\"image\\\"}\"}"]}
\ No newline at end of file
+{"modules":{"0.8218876344311344":{"definition":"//\n// motion detect\n//\n// Neil Gershenfeld \n// (c) Massachusetts Institute of Technology 2017\n// \n// This work may be reproduced, modified, distributed, performed, and \n// displayed for any purpose, but must acknowledge the mods\n// project. Copyright is retained and must be preserved. The work is \n// provided as is; no warranty is provided, and users accept all \n// liability.\n//\n// closure\n//\n(function(){\n//\n// module globals\n//\nvar mod = {}\n//\n// name\n//\nvar name = 'motion detect'\n//\n// initialization\n//\nvar init = function() {\n   mod.threshold.value = 0.01\n   mod.time.value = 15\n   mod.dpi = 100\n   timeout()\n   }\n//\n// inputs\n//\nvar inputs = {\n   image:{type:'RGBA',\n      event:function(evt){\n         var ctx = mod.img.getContext(\"2d\")\n         var lastctx = mod.lastimg.getContext(\"2d\")\n         lastctx.canvas.width = ctx.canvas.width\n         lastctx.canvas.height = ctx.canvas.height\n         lastctx.drawImage(mod.img,0,0)\n         ctx.canvas.width = evt.detail.width\n         ctx.canvas.height = evt.detail.height \n         ctx.putImageData(evt.detail,0,0)\n         compare_images()\n         }}}\n//\n// outputs\n//\nvar outputs = {\n   image:{type:'RGBA',\n      event:function(){\n         var ctx = mod.img.getContext(\"2d\")\n         var img = ctx.getImageData(0,0,mod.img.width,mod.img.height)\n         mods.output(mod,'image',img)}},\n   imageInfo:{type:'object',\n      event:function(obj){\n         mods.output(mod,'imageInfo',obj)}},\n   trigger:{type:'event',\n      event:function(){\n         mods.output(mod,'trigger',null)}}}\n//\n// interface\n//\nvar interface = function(div){\n   mod.div = div\n   //\n   // on-screen drawing canvas\n   //\n   var canvas = document.createElement('canvas')\n      canvas.width = mods.ui.canvas\n      canvas.height = mods.ui.canvas\n      canvas.style.backgroundColor = 'rgb(255,255,255)'\n      div.appendChild(canvas)\n      mod.canvas = canvas\n   div.appendChild(document.createElement('br'))\n   //\n   // off-screen image canvas\n   //\n   var canvas = document.createElement('canvas')\n      mod.img = canvas\n   //\n   // off-screen last image canvas\n   //\n   var canvas = document.createElement('canvas')\n      mod.lastimg = canvas\n   //\n   // view button\n   //\n   var btn = document.createElement('button')\n      btn.style.padding = mods.ui.padding\n      btn.style.margin = 1\n      btn.appendChild(document.createTextNode('view'))\n      btn.addEventListener('click',function(){\n         var win = window.open('')\n         var btn = document.createElement('button')\n            btn.appendChild(document.createTextNode('close'))\n            btn.style.padding = mods.ui.padding\n            btn.style.margin = 1\n            btn.addEventListener('click',function(){\n               win.close()\n               })\n            win.document.body.appendChild(btn)\n         win.document.body.appendChild(document.createElement('br'))\n         var canvas = document.createElement('canvas')\n            canvas.width = mod.img.width\n            canvas.height = mod.img.height\n            win.document.body.appendChild(canvas)\n         var ctx = canvas.getContext(\"2d\")\n            ctx.drawImage(mod.img,0,0)\n         })\n      div.appendChild(btn)\n   div.appendChild(document.createTextNode(' '))\n   //\n   // info div\n   //\n   var info = document.createElement('div')\n      var text = document.createTextNode('relative change: ')\n         info.appendChild(text)\n         mod.change = text\n      info.appendChild(document.createElement('br'))\n      info.appendChild(document.createTextNode('threshold: '))\n      var input = document.createElement('input')\n         input.type = 'text'\n         input.size = 6\n         info.appendChild(input)\n         mod.threshold = input\n      info.appendChild(document.createTextNode(' (0-1)'))\n      info.appendChild(document.createElement('br'))\n      info.appendChild(document.createTextNode('latency: '))\n      var input = document.createElement('input')\n         input.type = 'text'\n         input.size = 6\n         info.appendChild(input)\n         mod.time = input\n      info.appendChild(document.createTextNode(' (s)'))\n      div.appendChild(info)\n   }\n//\n// local functions\n//\nfunction timeout() {\n   outputs.trigger.event()\n   setTimeout(timeout,parseFloat(mod.time.value)*1000)\n   }\nfunction compare_images() {\n   var blob = new Blob(['('+worker.toString()+'())'])\n   var url = window.URL.createObjectURL(blob)\n   var webworker = new Worker(url)\n   webworker.addEventListener('message',function(evt) {\n      window.URL.revokeObjectURL(url)\n      mod.change.nodeValue = 'relative change: '+evt.data.change.toFixed(3)\n      if (evt.data.change > parseFloat(mod.threshold.value)) {\n         var obj = {}\n         var date = new Date()\n         var year = date.getFullYear()\n         var month = ('0'+(1+parseInt(date.getMonth()))).slice(-2)\n         var day = ('0'+date.getDate()).slice(-2)\n         var hour = ('0'+date.getHours()).slice(-2)\n         var minute = ('0'+date.getMinutes()).slice(-2)\n         var second = ('0'+date.getSeconds()).slice(-2)\n         var name = year+'-'+month+'-'+day+'-'+hour+'-'+minute+'-'+second\n         obj.name = name\n         obj.dpi = mod.dpi\n         obj.width = mod.img.width\n         obj.height = mod.img.height\n         outputs.imageInfo.event(obj)\n         outputs.image.event()\n         }\n      var h = mod.img.height\n      var w = mod.img.width\n      if (w > h) {\n         var x0 = 0\n         var y0 = mod.canvas.height*.5*(1-h/w)\n         var wd = mod.canvas.width\n         var hd = mod.canvas.width*h/w\n         }\n      else {\n         var x0 = mod.canvas.width*.5*(1-w/h)\n         var y0 = 0\n         var wd = mod.canvas.height*w/h\n         var hd = mod.canvas.height\n         }\n      var ctx = mod.canvas.getContext(\"2d\")\n      ctx.clearRect(0,0,mod.canvas.width,mod.canvas.height)\n      ctx.drawImage(mod.img,x0,y0,wd,hd)\n      webworker.terminate()\n      })\n   var ctx = mod.img.getContext(\"2d\")\n   var img = ctx.getImageData(0,0,mod.img.width,mod.img.height)\n   var ctx = mod.lastimg.getContext(\"2d\")\n   var lastimg = ctx.getImageData(0,0,mod.img.width,mod.img.height)\n   var t = parseFloat(mod.threshold.value)\n   webworker.postMessage({\n      height:mod.img.height,width:mod.img.width,threshold:t,\n      buffer:img.data.buffer,lastbuffer:lastimg.data.buffer})\n   }\nfunction worker() {\n   self.addEventListener('message',function(evt) {\n      var h = evt.data.height\n      var w = evt.data.width\n      var t = evt.data.threshold\n      var buf = new Uint8ClampedArray(evt.data.buffer)\n      var lastbuf = new Uint8ClampedArray(evt.data.lastbuffer)\n      var change = 0\n      for (var row = 0; row < h; ++row) {\n         for (var col = 0; col < w; ++col) {\n            r = buf[(h-1-row)*w*4+col*4+0] \n            g = buf[(h-1-row)*w*4+col*4+1] \n            b = buf[(h-1-row)*w*4+col*4+2] \n            rl = lastbuf[(h-1-row)*w*4+col*4+0] \n            gl = lastbuf[(h-1-row)*w*4+col*4+1] \n            bl = lastbuf[(h-1-row)*w*4+col*4+2] \n            change += (Math.abs(r-rl)/255 \n               +Math.abs(g-gl)/255\n               +Math.abs(b-bl)/255)/3\n            }\n         }\n      change = change/(w*h)\n      self.postMessage({change:change})\n      })\n   }\n//\n// return values\n//\nreturn ({\n   name:name,\n   init:init,\n   inputs:inputs,\n   outputs:outputs,\n   interface:interface\n   })\n}())\n","top":"140","left":"233","inputs":{},"outputs":{}},"0.10092185293872713":{"definition":"//\n// convert rgba jpg\n//\n// Neil Gershenfeld \n// (c) Massachusetts Institute of Technology 2017\n// \n// This work may be reproduced, modified, distributed, performed, and \n// displayed for any purpose, but must acknowledge the mods\n// project. Copyright is retained and must be preserved. The work is \n// provided as is; no warranty is provided, and users accept all \n// liability.\n//\n// closure\n//\n(function(){\n//\n// module globals\n//\nvar mod = {}\n//\n// name\n//\nvar name = 'convert RGBA to JPG'\n//\n// initialization\n//\nvar init = function() {\n   mod.name.value = \"file.jpg\"\n   mod.compress.value = .75\n   }\n//\n// inputs\n//\nvar inputs = {\n   image:{type:'RGBA',\n      event:function(evt){\n         var ctx = mod.img.getContext(\"2d\")\n         ctx.canvas.width = evt.detail.width\n         ctx.canvas.height = evt.detail.height \n         ctx.putImageData(evt.detail,0,0)\n         mod.pxtext.nodeValue = evt.detail.width+' x '+evt.detail.height+' px'\n         convert_image()\n         }},\n   imageInfo:{type:'object',\n      event:function(evt){\n         mod.name.value = evt.detail.name+'.jpg'\n         }}\n   }\n//\n// outputs\n//\nvar outputs = {\n   }\n//\n// interface\n//\nvar interface = function(div){\n   mod.div = div\n   //\n   // on-screen drawing canvas\n   //\n   var canvas = document.createElement('canvas')\n      canvas.width = mods.ui.canvas\n      canvas.height = mods.ui.canvas\n      canvas.style.backgroundColor = 'rgb(255,255,255)'\n      div.appendChild(canvas)\n      mod.canvas = canvas\n   div.appendChild(document.createElement('br'))\n   //\n   // off-screen image canvas\n   //\n   var canvas = document.createElement('canvas')\n      mod.img = canvas\n   //\n   // view button\n   //\n   var btn = document.createElement('button')\n      btn.style.padding = mods.ui.padding\n      btn.style.margin = 1\n      btn.appendChild(document.createTextNode('view'))\n      btn.addEventListener('click',function(){\n         var win = window.open('')\n         var btn = document.createElement('button')\n            btn.appendChild(document.createTextNode('close'))\n            btn.style.padding = mods.ui.padding\n            btn.style.margin = 1\n            btn.addEventListener('click',function(){\n               win.close()\n               })\n            win.document.body.appendChild(btn)\n         win.document.body.appendChild(document.createElement('br'))\n         var canvas = document.createElement('canvas')\n            canvas.width = mod.img.width\n            canvas.height = mod.img.height\n            win.document.body.appendChild(canvas)\n         var ctx = canvas.getContext(\"2d\")\n            ctx.drawImage(mod.img,0,0)\n         })\n      div.appendChild(btn)\n   div.appendChild(document.createTextNode(' '))\n   //\n   // info div\n   //\n   var info = document.createElement('div')\n      info.appendChild(document.createTextNode('file name: '))\n      var input = document.createElement('input')\n         input.type = 'text'\n         input.size = 6\n         info.appendChild(input)\n         mod.name = input\n      info.appendChild(document.createElement('br'))\n      info.appendChild(document.createTextNode('compression: '))\n      var input = document.createElement('input')\n         input.type = 'text'\n         input.size = 6\n         info.appendChild(input)\n         mod.compress = input\n      info.appendChild(document.createTextNode(' (0-1)'))\n      info.appendChild(document.createElement('br'))\n      var text = document.createTextNode('px: ')\n         info.appendChild(text)\n         mod.pxtext = text\n      div.appendChild(info)\n   }\n//\n// local functions\n//\nfunction convert_image() {\n   //\n   // preview\n   //\n   var h = mod.img.height\n   var w = mod.img.width\n   if (w > h) {\n      var x0 = 0\n      var y0 = mod.canvas.height*.5*(1-h/w)\n      var wd = mod.canvas.width\n      var hd = mod.canvas.width*h/w\n      }\n   else {\n      var x0 = mod.canvas.width*.5*(1-w/h)\n      var y0 = 0\n      var wd = mod.canvas.height*w/h\n      var hd = mod.canvas.height\n      }\n   var ctx = mod.canvas.getContext(\"2d\")\n   ctx.clearRect(0,0,mod.canvas.width,mod.canvas.height)\n   ctx.drawImage(mod.img,x0,y0,wd,hd)\n   //\n   // convert and save\n   //\n   mod.img.toBlob(function(blob){\n      var url = URL.createObjectURL(blob)\n      var link = document.createElement('a')\n      link.download = mod.name.value\n      link.href = url\n      document.body.appendChild(link)\n      link.click()\n      document.body.removeChild(link)\n      URL.revokeObjectURL(url)\n      },'image/jpeg',parseFloat(mod.compress.value))\n   }\n//\n// return values\n//\nreturn ({\n   name:name,\n   init:init,\n   inputs:inputs,\n   outputs:outputs,\n   interface:interface\n   })\n}())\n","top":"142","left":"1098","inputs":{},"outputs":{}},"0.4997564076516918":{"definition":"//\n// video\n//\n// Neil Gershenfeld \n// (c) Massachusetts Institute of Technology 2015,6\n// \n// This work may be reproduced, modified, distributed, performed, and \n// displayed for any purpose, but must acknowledge the mods\n// project. Copyright is retained and must be preserved. The work is \n// provided as is; no warranty is provided, and users accept all \n// liability.\n//\n// closure\n//\n(function(){\n//\n// module globals\n//\nvar mod = {}\n//\n// name\n//\nvar name = 'video'\n//\n// initialization\n//\nvar init = function() {\n   mod.width.value = 1280 \n   mod.height.value = 720\n   start_video()\n   }\n//\n// inputs\n//\nvar inputs = {\n   capture:{type:'event',\n      event:function(evt){\n         capture_video()}}}\n//\n// outputs\n//\nvar outputs = {\n   image:{type:'RGBA',\n      event:function(){\n         var ctx = mod.img.getContext(\"2d\")\n         var img = ctx.getImageData(0,0,mod.img.width,mod.img.height)\n         mods.output(mod,'image',img)}}}\n//\n// interface\n//\nvar interface = function(div){\n   mod.div = div\n   //\n   // on-screen drawing canvas\n   //\n   var canvas = document.createElement('canvas')\n      canvas.width = mods.ui.canvas\n      canvas.height = mods.ui.canvas\n      canvas.style.backgroundColor = 'rgb(255,255,255)'\n      div.appendChild(canvas)\n      mod.canvas = canvas\n   div.appendChild(document.createElement('br'))\n   //\n   // off-screen image canvas\n   //\n   var canvas = document.createElement('canvas')\n      mod.img = canvas\n   //\n   // capture button\n   //\n   var btn = document.createElement('button')\n      btn.style.padding = mods.ui.padding\n      btn.style.margin = 1\n      btn.appendChild(document.createTextNode('capture'))\n      btn.addEventListener('click',function() {\n         capture_video()\n         })\n      div.appendChild(btn)\n   div.appendChild(document.createTextNode(' '))\n   //\n   // view button\n   //\n   var btn = document.createElement('button')\n      btn.style.padding = mods.ui.padding\n      btn.style.margin = 1\n      btn.appendChild(document.createTextNode('view'))\n      btn.addEventListener('click',function(){\n         var win = window.open('')\n         var btn = document.createElement('button')\n            btn.appendChild(document.createTextNode('close'))\n            btn.style.padding = mods.ui.padding\n            btn.style.margin = 1\n            btn.addEventListener('click',function(){\n               win.close()\n               })\n            win.document.body.appendChild(btn)\n         win.document.body.appendChild(document.createElement('br'))\n         var canvas = document.createElement('canvas')\n            canvas.width = mod.img.width\n            canvas.height = mod.img.height\n            win.document.body.appendChild(canvas)\n         var ctx = canvas.getContext(\"2d\")\n            ctx.drawImage(mod.img,0,0)\n         })\n      div.appendChild(btn)\n   div.appendChild(document.createElement('br'))\n   //\n   // width\n   //\n   div.appendChild(document.createTextNode('width: '))\n   var input = document.createElement('input')\n      input.type = 'text'\n      input.size = 6\n      input.addEventListener('change',function() {\n         update_video()\n         })\n      div.appendChild(input)\n      mod.width = input\n   div.appendChild(document.createElement('br'))\n   //\n   // height\n   //\n   div.appendChild(document.createTextNode(' height: '))\n   var input = document.createElement('input')\n      input.type = 'text'\n      input.size = 6\n      input.addEventListener('change',function() {\n         update_video()\n         })\n      div.appendChild(input)\n      mod.height = input\n   div.appendChild(document.createElement('br'))\n   //\n   // flip\n   //\n   div.appendChild(document.createTextNode('flip image: '))\n   var input = document.createElement('input')\n      input.type = 'checkbox'\n      div.appendChild(input)\n      mod.flip = input\n   div.appendChild(document.createElement('br'))\n   //\n   // video element\n   //\n   var video = document.createElement('video')\n      mod.video = video\n   }\n//\n// local functions\n//\nfunction start_video() {\n   var w = parseInt(mod.width.value)\n   var h = parseInt(mod.height.value)\n   var ctx = mod.img.getContext(\"2d\")\n   ctx.canvas.width = w\n   ctx.canvas.height = h\n   var constraints = {\n      audio:false,\n      video:{width:w,height:h}\n      }\n   navigator.mediaDevices.getUserMedia(constraints)\n      .then(function(stream) {\n         mod.video.srcObject = stream\n         mod.video.onloadedmetadata = function(e) {\n            mod.video.play()\n            }\n         })\n      .catch(function(err) {\n         console.log(err.name + \": \"+err.message)\n         })\n   }\nfunction update_video() {\n   var w = parseInt(mod.width.value)\n   var h = parseInt(mod.height.value)\n   mod.video.setAttribute('width',w)\n   mod.video.setAttribute('height',h)\n   var ctx = mod.img.getContext(\"2d\")\n   ctx.canvas.width = w\n   ctx.canvas.height = h\n   }\nfunction capture_video() {\n   var w = parseInt(mod.width.value)\n   var h = parseInt(mod.height.value)\n   var ctx = mod.img.getContext(\"2d\")\n   ctx.drawImage(mod.video,0,0,w,h)\n   var blob = new Blob(['('+worker.toString()+'())'])\n   var url = window.URL.createObjectURL(blob)\n   var webworker = new Worker(url)\n   webworker.addEventListener('message',function(evt) {\n      window.URL.revokeObjectURL(url)\n      var h = mod.img.height\n      var w = mod.img.width\n      var buf = new Uint8ClampedArray(evt.data.buffer)\n      var imgdata = new ImageData(buf,w,h)\n      var ctx = mod.img.getContext(\"2d\")\n      ctx.putImageData(imgdata,0,0)\n      if (w > h) {\n         var x0 = 0\n         var y0 = mod.canvas.height*.5*(1-h/w)\n         var wd = mod.canvas.width\n         var hd = mod.canvas.width*h/w\n         }\n      else {\n         var x0 = mod.canvas.width*.5*(1-w/h)\n         var y0 = 0\n         var wd = mod.canvas.height*w/h\n         var hd = mod.canvas.height\n         }\n      var ctx = mod.canvas.getContext(\"2d\")\n      ctx.clearRect(0,0,mod.canvas.width,mod.canvas.height)\n      ctx.drawImage(mod.img,x0,y0,wd,hd)\n      outputs.image.event()\n      webworker.terminate()\n      })\n   var ctx = mod.img.getContext(\"2d\")\n   var img = ctx.getImageData(0,0,mod.img.width,mod.img.height)\n   webworker.postMessage({\n      height:mod.img.height,width:mod.img.width,\n      checked:mod.flip.checked,\n      buffer:img.data.buffer},[img.data.buffer])\n   }\nfunction worker() {\n   self.addEventListener('message',function(evt) {\n      var h = evt.data.height\n      var w = evt.data.width\n      var checked = evt.data.checked\n      var buf = new Uint8ClampedArray(evt.data.buffer)\n      if (checked == true) {\n         var newbuf = new Uint8ClampedArray(buf.length)\n         for (var row = 0; row < h; ++row) {\n            for (var col = 0; col < w; ++col) {\n               newbuf[(h-1-row)*w*4+col*4+0] = \n                  buf[row*w*4+(w-1-col)*4+0] \n               newbuf[(h-1-row)*w*4+col*4+1] = \n                  buf[row*w*4+(w-1-col)*4+1]\n               newbuf[(h-1-row)*w*4+col*4+2] = \n                  buf[row*w*4+(w-1-col)*4+2]\n               newbuf[(h-1-row)*w*4+col*4+3] = \n                  buf[row*w*4+(w-1-col)*4+3]\n               }\n            }\n         self.postMessage({buffer:newbuf.buffer},[newbuf.buffer])\n         }\n      else\n         self.postMessage({buffer:buf.buffer},[buf.buffer])\n      })\n   }\n//\n// return values\n//\nreturn ({\n   name:name,\n   init:init,\n   inputs:inputs,\n   outputs:outputs,\n   interface:interface\n   })\n}())\n","top":"302","left":"738","inputs":{},"outputs":{}}},"links":["{\"source\":\"{\\\"id\\\":\\\"0.8218876344311344\\\",\\\"type\\\":\\\"outputs\\\",\\\"name\\\":\\\"imageInfo\\\"}\",\"dest\":\"{\\\"id\\\":\\\"0.10092185293872713\\\",\\\"type\\\":\\\"inputs\\\",\\\"name\\\":\\\"imageInfo\\\"}\"}","{\"source\":\"{\\\"id\\\":\\\"0.8218876344311344\\\",\\\"type\\\":\\\"outputs\\\",\\\"name\\\":\\\"image\\\"}\",\"dest\":\"{\\\"id\\\":\\\"0.10092185293872713\\\",\\\"type\\\":\\\"inputs\\\",\\\"name\\\":\\\"image\\\"}\"}","{\"source\":\"{\\\"id\\\":\\\"0.8218876344311344\\\",\\\"type\\\":\\\"outputs\\\",\\\"name\\\":\\\"trigger\\\"}\",\"dest\":\"{\\\"id\\\":\\\"0.4997564076516918\\\",\\\"type\\\":\\\"inputs\\\",\\\"name\\\":\\\"capture\\\"}\"}","{\"source\":\"{\\\"id\\\":\\\"0.4997564076516918\\\",\\\"type\\\":\\\"outputs\\\",\\\"name\\\":\\\"image\\\"}\",\"dest\":\"{\\\"id\\\":\\\"0.8218876344311344\\\",\\\"type\\\":\\\"inputs\\\",\\\"name\\\":\\\"image\\\"}\"}"]}
\ No newline at end of file