From e81c251c9cfa405a43e47651649dacb693a05794 Mon Sep 17 00:00:00 2001
From: Neil Gershenfeld <gersh@cba.mit.edu>
Date: Thu, 11 Jan 2018 17:52:00 -0500
Subject: [PATCH] dt motion detection

---
 modules/image/motion detect  | 29 ++++++++++++-----------------
 programs/image/motion detect |  2 +-
 2 files changed, 13 insertions(+), 18 deletions(-)

diff --git a/modules/image/motion detect b/modules/image/motion detect
index f2a7499..2c08459 100755
--- a/modules/image/motion detect	
+++ b/modules/image/motion detect	
@@ -33,6 +33,7 @@ var init = function() {
    mod.delay.value = 1
    mod.dpi = 100
    mod.win = null
+   mod.time = 0
    //
    // trigger image after latency (for start-up)
    //
@@ -182,13 +183,13 @@ function compare_images() {
       window.URL.revokeObjectURL(url)
       mod.changetext.nodeValue = 'relative change: '+evt.data.change.toFixed(3)
       mod.change = evt.data.change
+      var time = Date.now()
+      var dt = (time-mod.time)/1000
       //
-      // check if change > threshold
+      // check whether to save image
       //
-      if (mod.change > parseFloat(mod.threshold.value)) {
-         //
-         // yes, output image
-         //
+      if ((mod.change > parseFloat(mod.threshold.value))
+         && (dt > parseFloat(mod.latency.value))) {
          var obj = {}
          var date = new Date()
          var year = date.getFullYear()
@@ -204,18 +205,7 @@ function compare_images() {
          obj.height = mod.img.height
          outputs.imageInfo.event(obj)
          outputs.image.event()
-         //
-         // trigger next image after latency
-         //
-         setTimeout(outputs.trigger.event,
-            parseFloat(mod.latency.value)*1000)
-         }
-      else {
-         //
-         // no, trigger next image after delay
-         //
-         setTimeout(outputs.trigger.event,
-            parseFloat(mod.delay.value)*1000)
+         mod.time = time
          }
       //
       // update canvas
@@ -252,6 +242,11 @@ function compare_images() {
       // terminate worker
       //
       webworker.terminate()
+      //
+      // trigger next image
+      //
+      setTimeout(outputs.trigger.event,
+         parseFloat(mod.delay.value)*1000)
       })
    //
    // call worker
diff --git a/programs/image/motion detect b/programs/image/motion detect
index ac24ddb..f6eb580 100644
--- a/programs/image/motion detect	
+++ b/programs/image/motion detect	
@@ -1 +1 @@
-{"modules":{"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   mod.flip.checked = false\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":{}},"0.8685400002482915":{"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   //\n   // UI settings\n   //\n   mod.threshold.value = 0.025\n   mod.latency.value = 15\n   mod.delay.value = 1\n   mod.dpi = 100\n   mod.win = null\n   //\n   // trigger image after latency (for start-up)\n   //\n   setTimeout(outputs.trigger.event,\n      parseFloat(mod.latency.value)*1000)\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 canvases\n   //\n   var canvas = document.createElement('canvas')\n      mod.img = canvas\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 last image'))\n      btn.addEventListener('click',function(){\n         mod.win = window.open('')\n         mod.win.document.title = 'last image'\n         mod.win.document.body.style.overflow = 'hidden'\n         mod.win.document.body.style.border = 0\n         mod.win.document.body.style.padding = 0\n         mod.win.document.body.style.margin = 0\n         mod.win.addEventListener('unload',function() {\n            mod.win = null\n            })\n         var canvas = document.createElement('canvas')\n            canvas.width = mod.img.width\n            canvas.height = mod.img.height\n            canvas.setAttribute('id',mod.div.id+'canvas')\n            mod.win.document.body.appendChild(canvas)\n         var ctx = canvas.getContext(\"2d\")\n            ctx.drawImage(mod.img,0,0)\n         })\n      div.appendChild(btn)\n   //\n   // info div\n   //\n   var info = document.createElement('div')\n      var text = document.createTextNode('relative change: ')\n         info.appendChild(text)\n         mod.changetext = 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.latency = input\n      info.appendChild(document.createTextNode(' (s)'))\n      div.appendChild(info)\n      info.appendChild(document.createElement('br'))\n      info.appendChild(document.createTextNode('delay: '))\n      var input = document.createElement('input')\n         input.type = 'text'\n         input.size = 6\n         info.appendChild(input)\n         mod.delay = input\n      info.appendChild(document.createTextNode(' (s)'))\n      div.appendChild(info)\n   }\n//\n// local functions\n//\nfunction open_window() {\n   mod.win = window.open('')\n   mod.win.document.title = 'motion detect last image'\n   mod.win.document.body.style.overflow = 'hidden'\n   mod.win.document.body.style.border = 0\n   mod.win.document.body.style.padding = 0\n   mod.win.document.body.style.margin = 0\n   var canvas = document.createElement('canvas')\n      canvas.width = mod.img.width\n      canvas.height = mod.img.height\n      canvas.setAttribute('id',mod.div.id+'canvas')\n      mod.win.document.body.appendChild(canvas)\n   }\nfunction compare_images() {\n   //\n   // create worker\n   //\n   var blob = new Blob(['('+worker.toString()+'())'])\n   var url = window.URL.createObjectURL(blob)\n   var webworker = new Worker(url)\n   //\n   // worker handler\n   //\n   webworker.addEventListener('message',function(evt) {\n      window.URL.revokeObjectURL(url)\n      mod.changetext.nodeValue = 'relative change: '+evt.data.change.toFixed(3)\n      mod.change = evt.data.change\n      //\n      // check if change > threshold\n      //\n      if (mod.change > parseFloat(mod.threshold.value)) {\n         //\n         // yes, output image\n         //\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         // trigger next image after latency\n         //\n         setTimeout(outputs.trigger.event,\n            parseFloat(mod.latency.value)*1000)\n         }\n      else {\n         //\n         // no, trigger next image after delay\n         //\n         setTimeout(outputs.trigger.event,\n            parseFloat(mod.delay.value)*1000)\n         }\n      //\n      // update canvas\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      // update view window\n      //\n      if (mod.win != null) {\n         var canvas = mod.win.document.getElementById(mod.div.id+'canvas')\n         canvas.width = mod.img.width\n         canvas.height = mod.img.height\n         var ctx = canvas.getContext(\"2d\")\n         ctx.clearRect(0,0,mod.img.width,mod.img.height)\n         ctx.drawImage(mod.img,0,0)\n         }\n      //\n      // terminate worker\n      //\n      webworker.terminate()\n      })\n   //\n   // call worker\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":"225","inputs":{},"outputs":{}}},"links":["{\"source\":\"{\\\"id\\\":\\\"0.8685400002482915\\\",\\\"type\\\":\\\"outputs\\\",\\\"name\\\":\\\"image\\\"}\",\"dest\":\"{\\\"id\\\":\\\"0.10092185293872713\\\",\\\"type\\\":\\\"inputs\\\",\\\"name\\\":\\\"image\\\"}\"}","{\"source\":\"{\\\"id\\\":\\\"0.8685400002482915\\\",\\\"type\\\":\\\"outputs\\\",\\\"name\\\":\\\"imageInfo\\\"}\",\"dest\":\"{\\\"id\\\":\\\"0.10092185293872713\\\",\\\"type\\\":\\\"inputs\\\",\\\"name\\\":\\\"imageInfo\\\"}\"}","{\"source\":\"{\\\"id\\\":\\\"0.8685400002482915\\\",\\\"type\\\":\\\"outputs\\\",\\\"name\\\":\\\"trigger\\\"}\",\"dest\":\"{\\\"id\\\":\\\"0.4997564076516918\\\",\\\"type\\\":\\\"inputs\\\",\\\"name\\\":\\\"capture\\\"}\"}","{\"source\":\"{\\\"id\\\":\\\"0.4997564076516918\\\",\\\"type\\\":\\\"outputs\\\",\\\"name\\\":\\\"image\\\"}\",\"dest\":\"{\\\"id\\\":\\\"0.8685400002482915\\\",\\\"type\\\":\\\"inputs\\\",\\\"name\\\":\\\"image\\\"}\"}"]}
\ No newline at end of file
+{"modules":{"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   mod.flip.checked = false\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":{}},"0.9721041935677535":{"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   //\n   // UI settings\n   //\n   mod.threshold.value = 0.025\n   mod.latency.value = 15\n   mod.delay.value = 1\n   mod.dpi = 100\n   mod.win = null\n   mod.time = 0\n   //\n   // trigger image after latency (for start-up)\n   //\n   setTimeout(outputs.trigger.event,\n      parseFloat(mod.latency.value)*1000)\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 canvases\n   //\n   var canvas = document.createElement('canvas')\n      mod.img = canvas\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 last image'))\n      btn.addEventListener('click',function(){\n         mod.win = window.open('')\n         mod.win.document.title = 'last image'\n         mod.win.document.body.style.overflow = 'hidden'\n         mod.win.document.body.style.border = 0\n         mod.win.document.body.style.padding = 0\n         mod.win.document.body.style.margin = 0\n         mod.win.addEventListener('unload',function() {\n            mod.win = null\n            })\n         var canvas = document.createElement('canvas')\n            canvas.width = mod.img.width\n            canvas.height = mod.img.height\n            canvas.setAttribute('id',mod.div.id+'canvas')\n            mod.win.document.body.appendChild(canvas)\n         var ctx = canvas.getContext(\"2d\")\n            ctx.drawImage(mod.img,0,0)\n         })\n      div.appendChild(btn)\n   //\n   // info div\n   //\n   var info = document.createElement('div')\n      var text = document.createTextNode('relative change: ')\n         info.appendChild(text)\n         mod.changetext = 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.latency = input\n      info.appendChild(document.createTextNode(' (s)'))\n      div.appendChild(info)\n      info.appendChild(document.createElement('br'))\n      info.appendChild(document.createTextNode('delay: '))\n      var input = document.createElement('input')\n         input.type = 'text'\n         input.size = 6\n         info.appendChild(input)\n         mod.delay = input\n      info.appendChild(document.createTextNode(' (s)'))\n      div.appendChild(info)\n   }\n//\n// local functions\n//\nfunction open_window() {\n   mod.win = window.open('')\n   mod.win.document.title = 'motion detect last image'\n   mod.win.document.body.style.overflow = 'hidden'\n   mod.win.document.body.style.border = 0\n   mod.win.document.body.style.padding = 0\n   mod.win.document.body.style.margin = 0\n   var canvas = document.createElement('canvas')\n      canvas.width = mod.img.width\n      canvas.height = mod.img.height\n      canvas.setAttribute('id',mod.div.id+'canvas')\n      mod.win.document.body.appendChild(canvas)\n   }\nfunction compare_images() {\n   //\n   // create worker\n   //\n   var blob = new Blob(['('+worker.toString()+'())'])\n   var url = window.URL.createObjectURL(blob)\n   var webworker = new Worker(url)\n   //\n   // worker handler\n   //\n   webworker.addEventListener('message',function(evt) {\n      window.URL.revokeObjectURL(url)\n      mod.changetext.nodeValue = 'relative change: '+evt.data.change.toFixed(3)\n      mod.change = evt.data.change\n      var time = Date.now()\n      var dt = (time-mod.time)/1000\n      //\n      // check whether to save image\n      //\n      if ((mod.change > parseFloat(mod.threshold.value))\n         && (dt > parseFloat(mod.latency.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         mod.time = time\n         }\n      //\n      // update canvas\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      // update view window\n      //\n      if (mod.win != null) {\n         var canvas = mod.win.document.getElementById(mod.div.id+'canvas')\n         canvas.width = mod.img.width\n         canvas.height = mod.img.height\n         var ctx = canvas.getContext(\"2d\")\n         ctx.clearRect(0,0,mod.img.width,mod.img.height)\n         ctx.drawImage(mod.img,0,0)\n         }\n      //\n      // terminate worker\n      //\n      webworker.terminate()\n      //\n      // trigger next image\n      //\n      setTimeout(outputs.trigger.event,\n         parseFloat(mod.delay.value)*1000)\n      })\n   //\n   // call worker\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":"137","left":"230","inputs":{},"outputs":{}}},"links":["{\"source\":\"{\\\"id\\\":\\\"0.9721041935677535\\\",\\\"type\\\":\\\"outputs\\\",\\\"name\\\":\\\"image\\\"}\",\"dest\":\"{\\\"id\\\":\\\"0.10092185293872713\\\",\\\"type\\\":\\\"inputs\\\",\\\"name\\\":\\\"image\\\"}\"}","{\"source\":\"{\\\"id\\\":\\\"0.9721041935677535\\\",\\\"type\\\":\\\"outputs\\\",\\\"name\\\":\\\"imageInfo\\\"}\",\"dest\":\"{\\\"id\\\":\\\"0.10092185293872713\\\",\\\"type\\\":\\\"inputs\\\",\\\"name\\\":\\\"imageInfo\\\"}\"}","{\"source\":\"{\\\"id\\\":\\\"0.9721041935677535\\\",\\\"type\\\":\\\"outputs\\\",\\\"name\\\":\\\"trigger\\\"}\",\"dest\":\"{\\\"id\\\":\\\"0.4997564076516918\\\",\\\"type\\\":\\\"inputs\\\",\\\"name\\\":\\\"capture\\\"}\"}","{\"source\":\"{\\\"id\\\":\\\"0.4997564076516918\\\",\\\"type\\\":\\\"outputs\\\",\\\"name\\\":\\\"image\\\"}\",\"dest\":\"{\\\"id\\\":\\\"0.9721041935677535\\\",\\\"type\\\":\\\"inputs\\\",\\\"name\\\":\\\"image\\\"}\"}"]}
\ No newline at end of file
-- 
GitLab