diff --git a/programs/image/motion detect.html b/programs/image/motion detect.html
new file mode 100644
index 0000000000000000000000000000000000000000..ca3c35be1ea709903c7debcaf27a5902963c171d
--- /dev/null
+++ b/programs/image/motion detect.html	
@@ -0,0 +1,1400 @@
+<html>
+<head><meta charset='utf-8'>
+<title>mods</title>
+</head>
+<body link='black' alink='black' vlink='black'>
+<script>
+//
+// mods.js
+//
+// Neil Gershenfeld
+// (c) Massachusetts Institute of Technology 2015,6,7
+//
+// 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(){
+//
+// globals
+//
+var mods = {}
+mods.ui = {source:null,
+   progname:'',
+   padding:7,
+   bezier:100,
+   canvas:250,
+   rows:5,
+   cols:20,
+   link:'rgb(0,0,128)',
+   link_highlight:'rgb(255,0,0)'
+   }
+mods.globals = {}
+//
+// set up UI
+//
+function optest(opt,link) {
+   if (document.location.href.slice(0,4) == 'http') {
+      var req = new XMLHttpRequest()
+      req.responseType = 'text'
+      req.onreadystatechange = function() {
+         if (req.readyState == XMLHttpRequest.DONE) {
+            if (req.status == 404)
+               opt.disabled = true
+            }
+         }
+      req.open('GET',link+'?rnd='+Math.random()) // random to prevent caching
+      req.send()
+      }
+   else
+      opt.disabled = true
+   }
+//
+// programs
+//
+document.body.appendChild(document.createTextNode(' '))
+var sel = document.createElement('select')
+   sel.style.padding = mods.ui.padding
+   sel.addEventListener(('change'),function(evt){ // click?
+      switch (evt.target.value) {
+         case 'open server program':
+            window.callback = function(msg) {
+               if (location.port == 80)
+                  var uri = encodeURI('http://'+location.hostname
+                     +'?program='+msg)
+               else
+                  var uri = encodeURI('http://'+location.hostname+':'
+                     +location.port+'?program='+msg)
+               set_prompt('<a href='+uri+'>program link</a>')
+               prog_message_handler(msg)
+               }
+            var win = window.open('programs/index.html')
+            break
+         case 'open local program':
+            var file = document.getElementById('prog_input')
+            file.value = null
+            file.click()
+            break
+         case 'open remote program':
+            alert('remotes not yet implemented')
+            break
+         case 'save local program':
+            save_program()
+            break
+         case 'save local page':
+            save_page()
+            break
+         }
+      evt.target.value = 'programs'
+      })
+   var opt = document.createElement('option')
+      opt.text = 'programs'
+      opt.value = opt.text
+      sel.add(opt)
+   var opt = document.createElement('option')
+      opt.text = 'open server program'
+      opt.value = opt.text
+      sel.add(opt)
+      optest(opt,'programs/index.html')
+   var opt = document.createElement('option')
+      opt.text = 'open local program'
+      opt.value = opt.text
+      sel.add(opt)
+   var opt = document.createElement('option')
+      opt.text = 'open remote program'
+      opt.value = opt.text
+      sel.add(opt)
+   var opt = document.createElement('option')
+      opt.text = 'save local program'
+      opt.value = opt.text
+      sel.add(opt)
+   var opt = document.createElement('option')
+      opt.text = 'save local page'
+      opt.value = opt.text
+      sel.add(opt)
+   document.body.appendChild(sel)
+mods.ui.header = 2*sel.clientHeight
+//
+// modules
+//
+document.body.appendChild(document.createTextNode(' '))
+var sel = document.createElement('select')
+   sel.style.padding = mods.ui.padding
+   sel.addEventListener(('change'),function(evt){ // click?
+      switch (evt.target.value) {
+         case 'add server module':
+            window.callback = function(msg) {
+               mod_message_handler(msg)
+               }
+            var win = window.open('modules/index.html')
+            break
+         case 'add local module':
+            var file = document.getElementById('mod_input')
+            file.value = null
+            file.click()
+            break
+         case 'add remote module':
+            alert('remotes not yet implemented')
+            break
+         }
+      evt.target.value = 'modules'
+      })
+   var opt = document.createElement('option')
+      opt.text = 'modules'
+      opt.value = 'modules'
+      sel.add(opt)
+   var opt = document.createElement('option')
+      opt.text = 'add server module'
+      opt.value = opt.text
+      sel.add(opt)
+      optest(opt,'modules/index.html')
+   var opt = document.createElement('option')
+      opt.text = 'add local module'
+      opt.value = opt.text
+      sel.add(opt)
+   var opt = document.createElement('option')
+      opt.text = 'add remote module'
+      opt.value = opt.text
+      sel.add(opt)
+   document.body.appendChild(sel)
+//
+// edit
+//
+document.body.appendChild(document.createTextNode(' '))
+document.body.appendChild(document.createTextNode(' '))
+var sel = document.createElement('select')
+   sel.style.padding = mods.ui.padding
+   sel.addEventListener(('change'),function(evt){ // click?
+      evt.target.value = 'edit'
+      })
+   var opt = document.createElement('option')
+      opt.text = 'edit'
+      opt.value = opt.text
+      sel.add(opt)
+   var opt = document.createElement('option')
+      opt.text = 'cut'
+      opt.value = opt.text
+      opt.disabled = true
+      sel.add(opt)
+   var opt = document.createElement('option')
+      opt.text = 'copy'
+      opt.value = opt.text
+      opt.disabled = true
+      sel.add(opt)
+   var opt = document.createElement('option')
+      opt.text = 'paste'
+      opt.value = opt.text
+      opt.disabled = true
+      sel.add(opt)
+   var opt = document.createElement('option')
+      opt.text = 'nest'
+      opt.value = opt.text
+      opt.disabled = true
+      sel.add(opt)
+   document.body.appendChild(sel)
+//
+// options
+//
+document.body.appendChild(document.createTextNode(' '))
+var sel = document.createElement('select')
+   sel.style.padding = mods.ui.padding
+   sel.addEventListener(('change'),function(evt){ // click
+      switch (evt.target.value) {
+         case 'list all files':
+            var win = window.open('files.html')
+            break
+         case 'save all files':
+            var win = window.open('https://gitlab.cba.mit.edu/pub/mods')
+            break
+         }
+      evt.target.value = 'options'
+      })
+   var opt = document.createElement('option')
+      opt.text = 'options'
+      opt.value = 'options'
+      sel.add(opt)
+   var opt = document.createElement('option')
+      opt.text = 'preferences'
+      opt.value = opt.text
+      opt.disabled = true
+      sel.add(opt)
+   var opt = document.createElement('option')
+      opt.text = 'list all files'
+      opt.value = opt.text
+      sel.add(opt)
+      optest(opt,'files.html')
+   var opt = document.createElement('option')
+      opt.text = 'save all files'
+      opt.value = opt.text
+      sel.add(opt)
+   var opt = document.createElement('option')
+      opt.text = 'about'
+      opt.value = opt.text
+      opt.disabled = true
+      sel.add(opt)
+   document.body.appendChild(sel)
+//
+// prompt
+//
+document.body.appendChild(document.createTextNode(' '))
+var span = document.createElement('span')
+   span.setAttribute('id','logo')
+   span.style.display = 'inline-block'
+   span.style.verticalAlign = 'middle'
+   span.style.width = 20
+   span.style.height = 20
+   span.style.padding = mods.ui.padding
+   span.appendChild(logo(1))
+   document.body.appendChild(span)
+document.body.appendChild(document.createTextNode(' '))
+var span = document.createElement('span')
+   span.setAttribute('id','prompt')
+   span.style.display = 'inline-block'
+   span.style.verticalAlign = 'middle'
+   var innerspan = document.createElement('span')
+      span.appendChild(innerspan)
+   document.body.appendChild(span)
+function logo(size) {
+   var x = 0
+   var y = 2.8*size/3.8
+   var svgNS = "http://www.w3.org/2000/svg"
+   var logo = document.createElementNS(svgNS,"svg")
+   logo.setAttributeNS("http://www.w3.org/2000/xmlns/",
+      "xmlns:xlink","http://www.w3.org/1999/xlink")
+   logo.setAttributeNS(null,'viewBox',"0 0 "+size+" "+size)
+   var new_rect = document.createElementNS(svgNS,"rect");
+   new_rect.setAttribute("width",size/3.8)
+   new_rect.setAttribute("height",size/3.8)
+   new_rect.setAttribute("x",x)
+   new_rect.setAttribute("y",y)
+   new_rect.setAttribute("fill","blue")
+   logo.appendChild(new_rect)
+   var new_rect = document.createElementNS(svgNS,"rect");
+   new_rect.setAttribute("width",size/3.8)
+   new_rect.setAttribute("height",size/3.8)
+   new_rect.setAttribute("x",x+1.4*size/3.8)
+   new_rect.setAttribute("y",y)
+   new_rect.setAttribute("fill","blue")
+   logo.appendChild(new_rect)
+   var new_rect = document.createElementNS(svgNS,"rect");
+   new_rect.setAttribute("width",size/3.8)
+   new_rect.setAttribute("height",size/3.8)
+   new_rect.setAttribute("x",x+2.8*size/3.8)
+   new_rect.setAttribute("y",y)
+   new_rect.setAttribute("fill","blue")
+   logo.appendChild(new_rect)
+   var new_rect = document.createElementNS(svgNS, "rect");
+   new_rect.setAttribute("width",size/3.8)
+   new_rect.setAttribute("height",size/3.8)
+   new_rect.setAttribute("x",x)
+   new_rect.setAttribute("y",y-1.4*size/3.8)
+   new_rect.setAttribute("fill","blue")
+   logo.appendChild(new_rect)
+   var new_rect = document.createElementNS(svgNS, "rect");
+   new_rect.setAttribute("width", size / 3.8)
+   new_rect.setAttribute("height", size / 3.8)
+   new_rect.setAttribute("x", x + 2.8 * size / 3.8)
+   new_rect.setAttribute("y", y - 1.4 * size / 3.8)
+   new_rect.setAttribute("fill", "blue")
+   logo.appendChild(new_rect)
+   var new_rect = document.createElementNS(svgNS, "rect");
+   new_rect.setAttribute("width", size / 3.8)
+   new_rect.setAttribute("height", size / 3.8)
+   new_rect.setAttribute("x", x + 1.4 * size / 3.8)
+   new_rect.setAttribute("y", y - 2.8 * size / 3.8)
+   new_rect.setAttribute("fill", "blue")
+   logo.appendChild(new_rect)
+   var new_rect = document.createElementNS(svgNS, "rect");
+   new_rect.setAttribute("width", size / 3.8)
+   new_rect.setAttribute("height", size / 3.8)
+   new_rect.setAttribute("x", x + 2.8 * size / 3.8)
+   new_rect.setAttribute("y", y - 2.8 * size / 3.8)
+   new_rect.setAttribute("fill", "blue")
+   logo.appendChild(new_rect)
+   var new_circ = document.createElementNS(svgNS, "circle");
+   new_circ.setAttribute("r", size / (2 * 3.8))
+   new_circ.setAttribute("cx", x + size / (2 * 3.8))
+   new_circ.setAttribute("cy", y + size / (2 * 3.8) - 2.8 * size / 3.8)
+   new_circ.setAttribute("fill", "red")
+   logo.appendChild(new_circ)
+   var new_circ = document.createElementNS(svgNS, "circle");
+   new_circ.setAttribute("r", size / (2 * 3.8))
+   new_circ.setAttribute("cx", x + size / (2 * 3.8) + 1.4 * size / 3.8)
+   new_circ.setAttribute("cy", y + size / (2 * 3.8) - 1.4 * size / 3.8)
+   new_circ.setAttribute("fill", "red")
+   logo.appendChild(new_circ)
+   return logo
+   }
+//
+// SVG canvas for drawing
+//
+var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg")
+   svg.style.position = 'absolute'
+   svg.style.backgroundColor = 'rgb(255,255,255)'
+   svg.style.top = mods.ui.header
+   svg.style.left = 0
+   svg.style.zIndex = 0
+   svg.style.overflow = 'visible'
+   svg.setAttribute('id','svg')
+   svg.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink")
+   document.body.appendChild(svg)
+//
+// link container
+//
+var svg = document.getElementById('svg')
+var g = document.createElementNS('http://www.w3.org/2000/svg','g')
+   g.setAttribute('id','links')
+   svg.appendChild(g)
+//
+// file reading controls
+//
+var file = document.createElement('input')
+   file.setAttribute('type','file')
+   file.setAttribute('id','mod_input')
+   file.style.position = 'absolute'
+   file.style.left = 0
+   file.style.top = 0
+   file.style.width = 0
+   file.style.height = 0
+   file.style.opacity = 0
+   file.addEventListener('change',function() {
+      mod_read_handler()
+      })
+   document.body.appendChild(file)
+var file = document.createElement('input')
+   file.setAttribute('type','file')
+   file.setAttribute('id','prog_input')
+   file.style.position = 'absolute'
+   file.style.left = 0
+   file.style.top = 0
+   file.style.width = 0
+   file.style.height = 0
+   file.style.opacity = 0
+   file.addEventListener('change',function() {
+      prog_read_handler()
+      })
+   document.body.appendChild(file)
+//
+// module container
+//
+var div = document.createElement('div')
+   div.setAttribute('id','modules')
+   document.body.appendChild(div)
+//
+// check for program load query
+//
+if (location.search.length > 0) {
+   var args = location.search.slice(1).split('&')
+   for (var a in args) {
+      var arg = args[a].split('=')
+      if (arg[0] == 'program')
+      prog_message_handler(arg[1])
+      }
+   }
+//
+// program routines
+//
+function prog_read_handler(event) {
+   var file = document.getElementById('prog_input')
+   var file_reader = new FileReader()
+   file_reader.onload = prog_load_handler
+   file_reader.readAsText(file.files[0])
+   mods.ui.progname = file.files[0].name
+   }
+function prog_message_handler(filename) {
+   var req = new XMLHttpRequest()
+   req.responseType = 'text'
+   req.onreadystatechange = function() {
+      if (req.readyState == XMLHttpRequest.DONE) {
+         prog = JSON.parse(req.response)
+         prog_load(prog)
+         }
+      }
+   var index = filename.lastIndexOf('/')
+   if (index != -1) {
+      mods.ui.progname = filename.slice(index+1)
+      }
+   else
+      mods.ui.progname = filename
+   //
+   // send request, with random query to prevent caching
+   //
+   req.open('GET',filename+'?rnd='+Math.random())
+   req.send()
+   }
+function prog_load_handler(event) {
+   prog = JSON.parse(event.target.result)
+   prog_load(prog)
+   }
+function prog_load(prog) {
+   //
+   // load modules
+   //
+   for (var idnumber in prog.modules) {
+      var module = prog.modules[idnumber]
+      var str = module.definition
+      eval('var args = '+str)
+      args.definition = str
+      args.id = idnumber
+      args.top = module.top
+      args.left = module.left
+      add_module(args)
+      }
+   //
+   // load links
+   //
+   for (var linkid in prog.links) {
+      var str = prog.links[linkid]
+      eval('var link = '+str)
+      eval('var linksrc = '+link.source)
+      eval('var linkdst = '+link.dest)
+      var src = document.getElementById(
+         JSON.stringify({id:linksrc.id,type:linksrc.type,name:linksrc.name}))
+      var dst = document.getElementById(
+         JSON.stringify({id:linkdst.id,type:linkdst.type,name:linkdst.name}))
+      add_link(src,dst)
+      }
+   }
+function save_program() {
+   set_prompt('program name? ')
+   get_prompt(mods.ui.progname,function(filename){
+      mods.ui.progname = filename
+      var prog = {modules:{},links:[]}
+      var modules = document.getElementById('modules')
+      //
+      // save modules
+      //
+      for (var c = 0; c < modules.childNodes.length; ++c) {
+         var module = modules.childNodes[c]
+         var idnumber = module.id
+         prog.modules[idnumber] = {
+            definition:module.dataset.definition,
+            top:module.dataset.top,
+            left:module.dataset.left,
+            inputs:{},
+            outputs:{}
+            }
+         }
+      //
+      // save links
+      //
+      var svg = document.getElementById('svg')
+      var links = svg.getElementById('links')
+         for (var l = 0; l < links.childNodes.length; ++l) {
+            var link = links.childNodes[l]
+            var linkid = link.id
+            prog.links.push(linkid)
+            }
+      //
+      // download
+      //
+      var text = JSON.stringify(prog)
+      var a = document.createElement('a')
+      a.setAttribute('href','data:text/plain;charset=utf-8,'+
+         encodeURIComponent(text))
+      a.setAttribute('download',filename)
+      a.style.display = 'none'
+      document.body.appendChild(a)
+      a.click()
+      document.body.removeChild(a)
+      })
+   }
+function save_page() {
+   set_prompt('page name? ')
+   get_prompt(mods.ui.progname+".html",function(filename){
+      mods.ui.progname = filename
+      var prog = {modules:{},links:[]}
+      var modules = document.getElementById('modules')
+      //
+      // save modules
+      //
+      for (var c = 0; c < modules.childNodes.length; ++c) {
+         var module = modules.childNodes[c]
+         var idnumber = module.id
+         prog.modules[idnumber] = {
+            definition:module.dataset.definition,
+            top:module.dataset.top,
+            left:module.dataset.left,
+            inputs:{},
+            outputs:{}
+            }
+         }
+      //
+      // save links
+      //
+      var svg = document.getElementById('svg')
+      var links = svg.getElementById('links')
+         for (var l = 0; l < links.childNodes.length; ++l) {
+            var link = links.childNodes[l]
+            var linkid = link.id
+            prog.links.push(linkid)
+            }
+      //
+      // read mods.js
+      //
+      var req = new XMLHttpRequest()
+      req.responseType = 'text'
+      req.onreadystatechange = function() {
+         if (req.readyState == XMLHttpRequest.DONE) {
+            //
+            // construct page
+            //
+            var str = req.response
+            var text ="<html>\n"
+            text += "<head><meta charset='utf-8'>\n"
+            text += "<title>mods</title>\n"
+            text += "</head>\n"
+            text += "<body link='black' alink='black' vlink='black'>\n"
+            text += "<"+"script"+">\n"
+            text += str
+            text += "var prog = JSON.parse(JSON.stringify("+JSON.stringify(prog)+"))\n"
+            text += "window.mods_prog_load(prog)\n"
+            text += "</"+"script"+">\n"
+            text += "</body>\n"
+            text += "</html>\n"
+            //
+            // download page
+            //
+            var a = document.createElement('a')
+            a.setAttribute('href','data:text/plain;charset=utf-8,'+
+               encodeURIComponent(text))
+            a.setAttribute('download',filename)
+            a.style.display = 'none'
+            document.body.appendChild(a)
+            a.click()
+            document.body.removeChild(a)
+            }
+         }
+      //
+      // send request, with random query to prevent caching
+      //
+      req.open('GET','js/mods.js'+'?rnd='+Math.random())
+      req.send()
+      })
+   }
+//
+// add program load to window
+//
+window.mods_prog_load = function(prog) {
+   prog_load(prog)
+   }
+//
+// module routines
+//
+function mod_read_handler(event) {
+   var file = document.getElementById('mod_input')
+   var file_reader = new FileReader()
+   file_reader.onload = mod_load_handler
+   file_reader.readAsText(file.files[0])
+   }
+function mod_message_handler(filename) {
+   var req = new XMLHttpRequest()
+   req.responseType = 'text'
+   req.onreadystatechange = function() {
+      if (req.readyState == XMLHttpRequest.DONE) {
+         var str = req.response
+         eval('var args = '+str)
+         args.definition = str
+         args.id = String(Math.random())
+         args.top = 1.5*mods.ui.header
+         args.left = 3*mods.ui.header
+         add_module(args)
+         }
+      }
+   //
+   // send request, with random query to prevent caching
+   //
+   req.open('GET',filename+'?rnd='+Math.random())
+   req.send()
+   }
+function mod_load_handler(event) {
+   str = event.target.result
+   eval('var args = '+str)
+   args.definition = str
+   args.id = String(Math.random())
+   args.top = 1.5*mods.ui.header
+   args.left = 3*mods.ui.header
+   add_module(args)
+   }
+function add_module(args) {
+   var idnumber = args.id
+   var modules = document.getElementById('modules')
+   //
+   // container
+   //
+   var container = document.createElement('div')
+      container.setAttribute("id",idnumber)
+      container.style.position = "absolute"
+      container.style.top = args.top
+      container.style.left = args.left
+      container.dataset.top = args.top
+      container.dataset.left = args.left
+      container.dataset.name = args.name
+      container.style.zIndex = 0
+      container.style.width = window.innerWidth
+      container.dataset.definition = args.definition
+      modules.appendChild(container)
+   //
+   // name
+   //
+   var divname = document.createElement('div')
+      divname.appendChild(document.createTextNode(args.name))
+      divname.addEventListener('mouseover',name_over)
+      divname.addEventListener('mouseout',name_out)
+      divname.addEventListener('mousedown',name_mousedown)
+      divname.addEventListener('touchstart',name_touchdown)
+      divname.style.backgroundColor = "rgb(210,240,210)"
+      divname.style.padding = 1.5*mods.ui.padding
+      divname.style.position = "absolute"
+      divname.style.cursor = 'default'
+      divname.style.top = 0
+      divname.style.left = 0
+      divname.style.textAlign = 'center'
+      divname.style.border = '2px solid'
+      divname.style.borderRadius = '10px'
+      container.appendChild(divname)
+   //
+   // controls
+   //
+   var divctrl = document.createElement('div')
+      var editspan = document.createElement('span')
+         editspan.innerHTML = 'edit'
+         editspan.style.fontWeight = 'normal'
+         editspan.addEventListener('mouseover',function(event){
+            set_prompt('click to edit')
+            editspan.style.fontWeight = 'bold'})
+         editspan.addEventListener('mouseout',function(event){
+            set_prompt('')
+            editspan.style.fontWeight = 'normal'})
+         editspan.addEventListener('mousedown',edit_module)
+         divctrl.appendChild(editspan)
+      var delspan = document.createElement('span')
+         delspan.innerHTML = ' delete '
+         delspan.addEventListener('mouseover',function(event){
+            set_prompt('click to delete')
+            delspan.style.fontWeight = 'bold'})
+         delspan.addEventListener('mouseout',function(event){
+            set_prompt('')
+            delspan.style.fontWeight = 'normal'})
+         delspan.addEventListener('mousedown',function(event){
+            delete_module(event.target.parentNode.parentNode.id)})
+         divctrl.appendChild(delspan)
+      divctrl.style.backgroundColor = "rgb(240,220,220)"
+      divctrl.style.padding = mods.ui.padding
+      divctrl.style.position = "absolute"
+      divctrl.style.cursor = 'default'
+      divctrl.style.top = divname.clientHeight
+      divctrl.style.left = 0
+      divctrl.style.textAlign = 'center'
+      divctrl.style.border = '2px solid'
+      divctrl.style.borderRadius = '10px'
+      container.appendChild(divctrl)
+      divctrl.style.left = divname.clientWidth/2-divctrl.clientWidth/2
+   //
+   // interface
+   //
+   var divint = document.createElement('div')
+      divint.style.backgroundColor = "rgb(240,240,240)"
+      divint.style.padding = mods.ui.padding
+      divint.style.position = "absolute"
+      divint.style.top = divname.clientHeight+divctrl.clientHeight
+      divint.style.textAlign = 'center'
+      divint.style.border = '2px solid'
+      divint.style.borderRadius = '10px'
+      divint.setAttribute('id',JSON.stringify({id:idnumber,type:'interface'}))
+      divint.dataset.id = idnumber
+      divint.dataset.divNameSize = divname.clientWidth
+      args.interface(divint)
+      container.appendChild(divint)
+      divint.style.left = divname.clientWidth/2-divint.clientWidth/2
+   //
+   // inputs
+   //
+   var divin = document.createElement('div')
+      divin.setAttribute('id',JSON.stringify({id:idnumber,type:'inputs'}))
+      var b = document.createElement('b')
+         b.appendChild(document.createTextNode('inputs'))
+         divin.appendChild(b)
+      divin.style.backgroundColor = "rgb(240,240,210)"
+      divin.style.padding = mods.ui.padding
+      divin.style.paddingLeft = '2px'
+      divin.style.position= "absolute"
+      divin.style.top = divname.clientHeight+divctrl.clientHeight
+      divin.style.textAlign = 'left'
+      divin.style.border = '2px solid'
+      divin.style.borderRadius = '10px'
+      divin.setAttribute('id',JSON.stringify({id:idnumber,type:'inputs'}))
+      divin.style.cursor = 'default'
+      divin.addEventListener('mousedown',nothing)
+      divin.addEventListener('touchstart',nothing)
+      divin.addEventListener('mouseup',nothing)
+      divin.addEventListener('touchend',nothing)
+      for (var v in args.inputs) {
+         var div = document.createElement('div')
+         if (args.inputs[v].label != undefined)
+            div.innerHTML += args.inputs[v].label
+         else
+            div.innerHTML += v
+         if (args.inputs[v].type != '')
+            div.innerHTML += ' ('+args.inputs[v].type+')'
+         div.setAttribute('id',JSON.stringify({id:idnumber,type:'inputs',name:v}))
+         div.addEventListener('mouseover',input_over)
+         div.addEventListener('mouseout',input_out)
+         div.addEventListener('mousedown',input_mousedown)
+         div.addEventListener('touchstart',input_touchdown)
+         div.dataset.links = JSON.stringify([])
+         div.dataset.name = v
+         divin.appendChild(div)
+         var evtid = JSON.stringify(
+            {id:idnumber,type:'input',name:v})
+         window.addEventListener(evtid,args.inputs[v].event)
+         }
+      container.appendChild(divin)
+      if ((Object.keys(args.inputs).length) == 0)
+         divin.style.visibility = 'hidden'
+      divin.style.left = divname.clientWidth/2
+         -divint.clientWidth/2-divin.clientWidth
+      for (var i = 1; i < divin.childNodes.length; ++i) {
+         divin.childNodes[i].dataset.dx = divin.offsetLeft
+            +divin.childNodes[i].offsetLeft
+         divin.childNodes[i].dataset.dy = divin.offsetTop
+            +divin.childNodes[i].offsetTop
+            +divin.childNodes[i].offsetHeight/2
+         }
+   //
+   // outputs
+   //
+   var divout = document.createElement('div')
+      divout.setAttribute('id',JSON.stringify({id:idnumber,type:'outputs'}))
+      var b = document.createElement('b')
+         b.appendChild(document.createTextNode('outputs'))
+      divout.appendChild(b)
+      divout.style.backgroundColor = "rgb(240,240,210)"
+      divout.style.padding = mods.ui.padding
+      divout.style.paddingRight = '2px'
+      divout.style.position = "absolute"
+      divout.style.top = divname.clientHeight+divctrl.clientHeight
+      divout.style.textAlign = 'right'
+      divout.addEventListener('mousedown',nothing)
+      divout.style.border = '2px solid'
+      divout.style.borderRadius = '10px'
+      divout.setAttribute('id',JSON.stringify({id:idnumber,type:'outputs'}))
+      divout.style.cursor = 'default'
+      divout.addEventListener('touchstart',nothing)
+      divout.addEventListener('mouseup',nothing)
+      divout.addEventListener('touchend',nothing)
+      for (var v in args.outputs) {
+         var div = document.createElement('div')
+         if (args.outputs[v].label != undefined)
+            div.innerHTML += args.outputs[v].label
+         else
+            div.innerHTML += v
+         if (args.outputs[v].type != '')
+            div.innerHTML += ' ('+args.outputs[v].type+')'
+         div.setAttribute('id',JSON.stringify({id:idnumber,type:'outputs',name:v}))
+         div.addEventListener('mouseover',output_over)
+         div.addEventListener('mouseout',output_out)
+         div.addEventListener('mousedown',output_mousedown)
+         div.addEventListener('touchstart',output_touchdown)
+         div.dataset.links = JSON.stringify([])
+         div.dataset.name = v
+         divout.appendChild(div)
+         var evtid = JSON.stringify(
+            {id:idnumber,type:'output',name:v})
+         window.addEventListener(evtid,args.outputs[v].event)
+         }
+      container.appendChild(divout)
+      if ((Object.keys(args.outputs).length) == 0)
+         divout.style.visibility = 'hidden'
+      divout.style.left = divname.clientWidth/2
+         +divint.clientWidth/2
+      for (var i = 1; i < divout.childNodes.length; ++i) {
+         divout.childNodes[i].dataset.dx = divout.offsetLeft
+            +divout.childNodes[i].offsetLeft
+            +divout.childNodes[i].offsetWidth
+         divout.childNodes[i].dataset.dy = divout.offsetTop
+            +divout.childNodes[i].offsetTop
+            +divout.childNodes[i].offsetHeight/2
+         }
+      //
+      // initialization
+      //
+      args.init()
+      //
+      // resize to contents
+      //
+      container.style.width = divint.clientWidth+divin.clientWidth+divout.clientWidth
+      mods.fit(divint)
+   }
+function delete_module(idnumber) {
+   //
+   // delete links
+   //
+   var ins = document.getElementById(
+      JSON.stringify({id:idnumber,type:'inputs'}))
+   var outs = document.getElementById(
+      JSON.stringify({id:idnumber,type:'outputs'}))
+   for (var i = 1; i < ins.childNodes.length; ++i) {
+      var links = JSON.parse(ins.childNodes[i].dataset.links)
+      for (var l in links)
+         delete_link(links[l])
+      }
+   for (var i = 1; i < outs.childNodes.length; ++i) {
+      var links = JSON.parse(outs.childNodes[i].dataset.links)
+      for (var l in links)
+         delete_link(links[l])
+      }
+   //
+   // delete container
+   //
+   var modules = document.getElementById('modules')
+   var container = document.getElementById(idnumber)
+   modules.removeChild(container)
+   //
+   // clear prompt
+   //
+   set_prompt('')
+   }
+function edit_module(evt) {
+   var mod = evt.target.parentNode.parentNode
+   var idnumber = mod.id
+   var def = mod.dataset.definition
+   var top = mod.dataset.top
+   var left = mod.dataset.left
+   var name = mod.dataset.name
+   var fontsize = 100
+   var win = window.open('')
+   var file = document.createElement('input')
+      file.setAttribute('type','file')
+      file.setAttribute('id','edit_module_file')
+      file.style.position = 'absolute'
+      file.style.left = 0
+      file.style.top = 0
+      file.style.width = 0
+      file.style.height = 0
+      file.style.opacity = 0
+      file.addEventListener('change',function() {
+         edit_module_read_handler()
+         })
+      win.document.body.appendChild(file)
+   function edit_module_read_handler() {
+      var file = win.document.getElementById('edit_module_file')
+      var file_reader = new FileReader()
+      file_reader.onload = edit_module_load_handler
+      file_reader.readAsText(file.files[0])
+      }
+   function edit_module_load_handler(event) {
+      str = event.target.result
+      var text = win.document.getElementById('edit_module_text')
+      text.value = str
+      update_module(idnumber)
+      win.close()
+      }
+   var btn = document.createElement('button')
+      btn.appendChild(document.createTextNode('update and close'))
+      btn.style.padding = mods.ui.padding
+      btn.style.margin = 1
+      btn.addEventListener('click',function(){
+         update_module(idnumber)
+         win.close()
+         })
+      win.document.body.appendChild(btn)
+   var btn = document.createElement('button')
+      btn.appendChild(document.createTextNode('update'))
+      btn.style.padding = mods.ui.padding
+      btn.style.margin = 1
+      btn.addEventListener('click',function(){
+         update_module(idnumber)
+         })
+      win.document.body.appendChild(btn)
+   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)
+   var btn = document.createElement('button')
+      btn.appendChild(document.createTextNode('save'))
+      btn.style.padding = mods.ui.padding
+      btn.style.margin = 1
+      btn.addEventListener('click',function(){
+         var a = document.createElement('a')
+         a.setAttribute('href','data:text/plain;charset=utf-8,'+
+            encodeURIComponent(text.value))
+         a.setAttribute('download',name)
+         a.style.display = 'none'
+         document.body.appendChild(a)
+         a.click()
+         document.body.removeChild(a)
+         })
+      win.document.body.appendChild(btn)
+   var btn = document.createElement('button')
+      btn.appendChild(document.createTextNode('reload'))
+      btn.style.padding = mods.ui.padding
+      btn.style.margin = 1
+      btn.addEventListener('click',function(){
+         var file = win.document.getElementById('edit_module_file')
+         file.value = null
+         file.click()
+         })
+      win.document.body.appendChild(btn)
+   var btn = document.createElement('button')
+      btn.appendChild(document.createTextNode('increase font'))
+      btn.style.padding = mods.ui.padding
+      btn.style.margin = 1
+      btn.addEventListener('click',function(){
+         fontsize *= 1.2
+         text.style.fontSize = fontsize+'%'
+         })
+      win.document.body.appendChild(btn)
+   var btn = document.createElement('button')
+      btn.appendChild(document.createTextNode('decrease font'))
+      btn.style.padding = mods.ui.padding
+      btn.style.margin = 1
+      btn.addEventListener('click',function(){
+         fontsize /= 1.2
+         text.style.fontSize = fontsize+'%'
+         })
+      win.document.body.appendChild(btn)
+   win.document.body.appendChild(document.createElement('br'))
+   var text = document.createElement('textarea')
+      text.setAttribute('id','edit_module_text')
+      text.style.width = '100%'
+      text.style.height= '100%'
+      text.value = def
+      win.document.body.appendChild(text)
+   function update_module(idnumber) {
+      //
+      // save links
+      //
+      var ins = document.getElementById(
+         JSON.stringify({id:idnumber,type:'inputs'}))
+      var inlinks = []
+      for (var i = 1; i < ins.childNodes.length; ++i) {
+         var links = JSON.parse(ins.childNodes[i].dataset.links)
+         for (var l in links)
+            inlinks.push(links[l])
+         }
+      var outs = document.getElementById(
+         JSON.stringify({id:idnumber,type:'outputs'}))
+      var outlinks = []
+      for (var i = 1; i < outs.childNodes.length; ++i) {
+         var links = JSON.parse(outs.childNodes[i].dataset.links)
+         for (var l in links)
+            outlinks.push(links[l])
+         }
+      //
+      // delete module
+      //
+      delete_module(idnumber)
+      //
+      // add module
+      //
+      var def = text.value
+      eval('var args = '+def)
+      args.definition = def
+      args.id = idnumber
+      args.top = top
+      args.left = left
+      add_module(args)
+      //
+      // add links
+      //
+      for (var l in inlinks) {
+         eval('var link = '+inlinks[l])
+         eval('var linksrc = '+link.source)
+         eval('var linkdst = '+link.dest)
+         var src = document.getElementById(
+            JSON.stringify(
+               {id:linksrc.id,type:linksrc.type,name:linksrc.name}))
+         var dst = document.getElementById(
+            JSON.stringify(
+               {id:linkdst.id,type:linkdst.type,name:linkdst.name}))
+         add_link(src,dst)
+         }
+      for (var l in outlinks) {
+         eval('var link = '+outlinks[l])
+         eval('var linksrc = '+link.source)
+         eval('var linkdst = '+link.dest)
+         var src = document.getElementById(
+            JSON.stringify(
+               {id:linksrc.id,type:linksrc.type,name:linksrc.name}))
+         var dst = document.getElementById(
+            JSON.stringify(
+               {id:linkdst.id,type:linkdst.type,name:linkdst.name}))
+         add_link(src,dst)
+         }
+      }
+   }
+//
+// UI routines
+//
+function set_prompt(txt) {
+   var span = document.getElementById('prompt')
+   span.childNodes[0].innerHTML = ' '+txt
+   }
+function get_prompt(txt,fn) {
+   var div = document.getElementById('prompt')
+   if (div.childNodes.length > 2)
+      //
+      // already getting a prompt
+      //
+      return
+   var text = document.createElement('input')
+      text.type = 'text'
+      text.size = 20
+      text.value = txt
+      text.addEventListener('keydown',function(evt){
+         if (evt.key == 'Enter') {
+            var div = document.getElementById('prompt')
+            div.removeChild(div.childNodes[1])
+            div.removeChild(div.childNodes[1])
+            set_prompt('')
+            fn(text.value)
+            }
+         })
+      div.appendChild(text)
+      div.appendChild(document.createTextNode(' (enter)'))
+      text.focus()
+   }
+function nothing(evt) {
+   evt.preventDefault()
+   evt.stopPropagation()
+   }
+//
+// link routines
+//
+function add_link(src,dst) {
+   //
+   // link order from out to in
+   //
+   if (src.id.indexOf('outputs') == -1) {
+      var tmp = src
+      src = dst
+      dst = tmp
+      }
+   //
+   // check if link exists
+   //
+   var id = JSON.stringify({source:src.id,dest:dst.id})
+   var link = document.getElementById(id)
+   if (link != null) {
+      //
+      // yes, remove it and return
+      //
+      delete_link(id)
+      return
+      }
+   //
+   // no, add link
+   //
+   var links = JSON.parse(src.dataset.links)
+      links.push(id)
+      src.dataset.links = JSON.stringify(links)
+   var links = JSON.parse(dst.dataset.links)
+      links.push(id)
+      dst.dataset.links = JSON.stringify(links)
+   //
+   // draw link
+   //
+   xsrc = src.parentNode.parentNode.offsetLeft
+      +parseFloat(src.dataset.dx)
+   ysrc = src.parentNode.parentNode.offsetTop
+      +parseFloat(src.dataset.dy)
+      -mods.ui.header
+   xdst = dst.parentNode.parentNode.offsetLeft
+      +parseFloat(dst.dataset.dx)
+   ydst = dst.parentNode.parentNode.offsetTop
+      +parseFloat(dst.dataset.dy)
+      -mods.ui.header
+   var links = document.getElementById('links')
+   var path = document.createElementNS('http://www.w3.org/2000/svg','path')
+      path.setAttribute('id',id)
+      path.setAttribute('d','M'+xsrc+','+ysrc+' C'+(xsrc+mods.ui.bezier)+','
+         +ysrc+' '+(xdst-mods.ui.bezier)+','+ydst+' '+xdst+','+ydst)
+      path.setAttribute('fill','none')
+      path.setAttribute('stroke','rgb(0,0,128)')
+      path.setAttribute('stroke-width',3)
+      links.appendChild(path)
+   /*
+   //
+   // don't trigger link
+   //
+   eval('var evtid = '+src.id)
+   evtid.type = 'output'
+   var evtstr = JSON.stringify(evtid)
+   var evt = new CustomEvent(evtstr)
+   window.dispatchEvent(evt)
+   */
+   }
+function delete_link(linkid) {
+   var links = document.getElementById('links')
+   links.removeChild(document.getElementById(linkid))
+   link = JSON.parse(linkid)
+   src = document.getElementById(link.source)
+   dst = document.getElementById(link.dest)
+   var links = JSON.parse(src.dataset.links)
+      var index = links.indexOf(linkid)
+      links.splice(index,1)
+      src.dataset.links = JSON.stringify(links)
+   var links = JSON.parse(dst.dataset.links)
+      var index = links.indexOf(linkid)
+      links.splice(index,1)
+      dst.dataset.links = JSON.stringify(links)
+   }
+function draw_links(idnumber,color) {
+   var ins = document.getElementById(
+      JSON.stringify({id:idnumber,type:'inputs'}))
+   var outs = document.getElementById(
+      JSON.stringify({id:idnumber,type:'outputs'}))
+   for (var i = 1; i < ins.childNodes.length; ++i) {
+      var links = JSON.parse(ins.childNodes[i].dataset.links)
+      for (var l in links)
+         draw_link(links[l],color)
+      }
+   for (var i = 1; i < outs.childNodes.length; ++i) {
+      var links = JSON.parse(outs.childNodes[i].dataset.links)
+      for (var l in links)
+         draw_link(links[l],color)
+      }
+   }
+function draw_link(id,color) {
+   var link = JSON.parse(id)
+   src = document.getElementById(link.source)
+   dst = document.getElementById(link.dest)
+   var path = document.getElementById(id)
+   xsrc = src.parentNode.parentNode.offsetLeft
+      +parseFloat(src.dataset.dx)
+   ysrc = src.parentNode.parentNode.offsetTop
+      +parseFloat(src.dataset.dy)
+      -mods.ui.header
+   xdst = dst.parentNode.parentNode.offsetLeft
+      +parseFloat(dst.dataset.dx)
+   ydst = dst.parentNode.parentNode.offsetTop
+      +parseFloat(dst.dataset.dy)
+      -mods.ui.header
+   path.setAttribute('d','M'+xsrc+','+ysrc+' C'+(xsrc+mods.ui.bezier)+','
+      +ysrc+' '+(xdst-mods.ui.bezier)+','+ydst+' '+xdst+','+ydst)
+   path.setAttribute('stroke',color)
+   }
+//
+// module fit call
+//
+mods.fit = function(div) {
+   div.style.left = div.dataset.divNameSize/2-div.clientWidth/2
+   var divin = document.getElementById(
+      JSON.stringify(
+         {id:div.dataset.id,type:'inputs'}))
+   divin.style.left = div.dataset.divNameSize/2-div.clientWidth/2-divin.clientWidth
+   var divout = document.getElementById(
+      JSON.stringify(
+         {id:div.dataset.id,type:'outputs'}))
+   divout.style.left = div.dataset.divNameSize/2+div.clientWidth/2
+   }
+//
+// module output call
+//
+mods.output = function(mod,varname,val) {
+   var div = mod.div
+   var key = JSON.parse(div.id)
+   var idnumber = key.id
+   var out = document.getElementById(
+      JSON.stringify(
+         {id:idnumber,type:'outputs',name:varname}))
+   var links = JSON.parse(out.dataset.links)
+   for (var l in links) {
+      var link = JSON.parse(links[l])
+      var dest = JSON.parse(link.dest)
+      var evtid = JSON.stringify(
+         {id:dest.id,type:'input',name:dest.name})
+      var evt = new CustomEvent(evtid,{detail:val})
+      window.dispatchEvent(evt)
+      }
+   }
+//
+// module create call
+//
+mods.create = function(args) {
+   var event = {target:{result:args}}
+   mod_load_handler(event)
+   }
+//
+// input event handlers
+//
+function input_over(evt) {
+   evt.target.style.fontWeight = 'bold'
+   var links = JSON.parse(evt.target.dataset.links)
+   for (var l in links)
+      draw_link(links[l],mods.ui.link_highlight)
+   if (mods.ui.source == null)
+      set_prompt('click to link')
+   }
+function input_out(evt) {
+   evt.target.style.fontWeight = 'normal'
+   var links = JSON.parse(evt.target.dataset.links)
+   for (var l in links)
+      draw_link(links[l],mods.ui.link)
+   if (mods.ui.source == null)
+      set_prompt('')
+   }
+function input_mousedown(evt) {
+   if (mods.ui.source == null) {
+      mods.ui.source = evt.target
+      set_prompt('variable to link/unlink to?')
+      }
+   else {
+      add_link(mods.ui.source,evt.target)
+      set_prompt('')
+      mods.ui.source = null
+      }
+   }
+function input_touchdown(evt) {
+   if (mods.ui.source == null) {
+      mods.ui.source = evt.target
+      set_prompt('variable to link/unlink to?')
+      }
+   else {
+      add_link(mods.ui.source,evt.target)
+      set_prompt('')
+      mods.ui.source = null
+      }
+   }
+//
+// output event handlers
+//
+function output_over(evt) {
+   evt.target.style.fontWeight = 'bold'
+   var links = JSON.parse(evt.target.dataset.links)
+   for (var l in links)
+      draw_link(links[l],mods.ui.link_highlight)
+   if (mods.ui.source == null)
+      set_prompt('click to link')
+   }
+function output_out(evt) {
+   evt.target.style.fontWeight = 'normal'
+   var links = JSON.parse(evt.target.dataset.links)
+   for (var l in links)
+      draw_link(links[l],mods.ui.link)
+   if (mods.ui.source == null)
+      set_prompt('')
+   }
+function output_mousedown(evt) {
+   if (mods.ui.source == null) {
+      mods.ui.source = evt.target
+      set_prompt('variable to link/unlink to?')
+      }
+   else {
+      add_link(mods.ui.source,evt.target)
+      set_prompt('')
+      mods.ui.source = null
+      }
+   }
+function output_touchdown(evt) {
+   if (mods.ui.source == null) {
+      mods.ui.source = evt.target
+      set_prompt('variable to link/unlink to?')
+      }
+   else {
+      add_link(mods.ui.source,evt.target)
+      set_prompt('')
+      mods.ui.source = null
+      }
+   }
+//
+// name event handlers
+//
+function name_over(evt) {
+   evt.target.style.fontWeight = 'bold'
+   if (mods.ui.source == null)
+      set_prompt('click and drag to move')
+   }
+function name_out(evt) {
+   evt.target.style.fontWeight = 'normal'
+   if (mods.ui.source == null)
+      set_prompt('')
+   }
+function name_mousedown(evt) {
+   evt.preventDefault()
+   evt.stopPropagation()
+   var div = document.getElementById(evt.target.parentNode.id)
+      div.style.zIndex = 1
+      div.dataset.xdown = evt.clientX
+      div.dataset.ydown = evt.clientY
+      mods.id = evt.target.parentNode.id
+      window.addEventListener('mousemove',window_mousemove)
+      window.addEventListener('mouseup',window_mouseup)
+   }
+function name_touchdown(evt) {
+   evt.preventDefault()
+   evt.stopPropagation()
+   var div = document.getElementById(evt.target.parentNode.id)
+      div.style.zIndex = 1
+      div.dataset.xdown = evt.changedTouches[0].pageX
+      div.dataset.ydown = evt.changedTouches[0].pageY
+      mods.id = evt.target.parentNode.id
+      window.addEventListener('touchmove',window_touchmove)
+      window.addEventListener('touchend',window_touchup)
+   }
+//
+// window event handlers
+//
+function window_mousemove(evt) {
+   evt.preventDefault()
+   evt.stopPropagation()
+   var div = document.getElementById(mods.id)
+      var dx = evt.clientX - div.dataset.xdown
+      var dy = evt.clientY - div.dataset.ydown
+      var newleft = parseFloat(div.dataset.left) + dx
+      var newtop = parseFloat(div.dataset.top) + dy
+      div.style.left = newleft+'px'
+      div.style.top = newtop+'px'
+   draw_links(mods.id,mods.ui.link)
+   }
+function window_mouseup(evt) {
+   evt.preventDefault()
+   evt.stopPropagation()
+   var div = document.getElementById(mods.id)
+      div.style.zIndex = 0
+      var dx = evt.clientX - div.dataset.xdown
+      var dy = evt.clientY - div.dataset.ydown
+      div.dataset.left = parseFloat(div.dataset.left) + dx
+      div.dataset.top = parseFloat(div.dataset.top) + dy
+      window.removeEventListener('mousemove',window_mousemove)
+      window.removeEventListener('mouseup',window_mouseup)
+   }
+function window_touchmove(evt) {
+   evt.preventDefault()
+   evt.stopPropagation()
+   var div = document.getElementById(mods.id)
+      var dx = evt.changedTouches[0].pageX - div.dataset.xdown
+      var dy = evt.changedTouches[0].pageY - div.dataset.ydown
+      var newleft = parseFloat(div.dataset.left) + dx
+      var newtop = parseFloat(div.dataset.top) + dy
+      div.style.left = newleft+'px'
+      div.style.top = newtop+'px'
+   draw_links(mods.id,mods.ui.link)
+   }
+function window_touchup(evt) {
+   evt.preventDefault()
+   evt.stopPropagation()
+   var div = document.getElementById(mods.id)
+      div.style.zIndex = 0
+      var dx = evt.changedTouches[0].pageX- div.dataset.xdown
+      var dy = evt.changedTouches[0].pageY - div.dataset.ydown
+      div.dataset.left = parseFloat(div.dataset.left) + dx
+      div.dataset.top = parseFloat(div.dataset.top) + dy
+      window.removeEventListener('touchmove',window_touchmove)
+      window.removeEventListener('touchend',window_touchup)
+   }
+})()
+var prog = JSON.parse(JSON.stringify({"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\\\"}\"}"]}))
+window.mods_prog_load(prog)
+</script>
+</body>
+</html>