Pauan

Conductance Animation 1

Aug 23rd, 2014
254
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. @ = require(["mho:std", "mho:app"])
  2.  
  3. // TODO move all this into a separate module
  4. function ArrayDiff(x) {
  5.   @assert.ok(Array.isArray(x))
  6.   return @ObservableVar({
  7.     type: "init",
  8.     array: x
  9.   })
  10. }
  11.  
  12. function pushAt(stream, index, x) {
  13.   stream.modify(function (diff) {
  14.     var array = diff.array
  15.     @assert.ok(index >= 0)
  16.  
  17.     if (index === array.length) {
  18.       array.push(x)
  19.     } else {
  20.       array.splice(index, 0, x)
  21.     }
  22.  
  23.     return {
  24.       type: "add",
  25.       array: array,
  26.  
  27.       index: index,
  28.       value: x
  29.     }
  30.   })
  31.   return index
  32. }
  33.  
  34. function removeAt(stream, index) {
  35.   stream.modify(function (diff) {
  36.     var array = diff.array
  37.     @assert.ok(index >= 0)
  38.     @assert.ok(index < array.length)
  39.  
  40.     var value = array[index]
  41.     array.splice(index, 1)
  42.  
  43.     return {
  44.       type: "remove",
  45.       array: array,
  46.  
  47.       index: index,
  48.       value: value
  49.     }
  50.   })
  51.   return index
  52. }
  53.  
  54. function modifyAt(stream, index, f) {
  55.   stream.modify(function (diff) {
  56.     var array = diff.array
  57.     @assert.ok(index >= 0)
  58.     @assert.ok(index < array.length)
  59.  
  60.     var value_old = array[index]
  61.     var value_new = f(value_old)
  62.     array[index] = value_new
  63.  
  64.     return {
  65.       type: "modify",
  66.       array: array,
  67.  
  68.       index: index,
  69.       value_old: value_old,
  70.       value_new: value_new
  71.     }
  72.   })
  73.   return index
  74. }
  75.  
  76. function moveFromTo(stream, from, to) {
  77.   if (from !== to) {
  78.     stream.modify(function (diff) {
  79.       var array = diff.array
  80.       @assert.ok(from >= 0)
  81.       @assert.ok(from < array.length)
  82.       @assert.ok(to >= 0)
  83.  
  84.       var value = array[from]
  85.       array.splice(from, 1)
  86.       array.splice(to, 0, value)
  87.  
  88.       return {
  89.         type: "move",
  90.         array: array,
  91.  
  92.         index_from: from,
  93.         index_to: to,
  94.         value: value
  95.       }
  96.     })
  97.   }
  98.   return to
  99. }
  100.  
  101. function push(stream, x) {
  102.   var array = @current(stream).array
  103.   return pushAt(stream, array.length, x)
  104. }
  105.  
  106. // TODO allow for a custom comparison function
  107. function remove(stream, x) {
  108.   var array = @current(stream).array
  109.   var index = array.indexOf(x)
  110.   @assert.isNot(index, -1)
  111.   return removeAt(stream, index)
  112. }
  113.  
  114. function replaceAt(stream, index, x) {
  115.   return modifyAt(stream, index, function () {
  116.     return x
  117.   })
  118. }
  119.  
  120. // TODO allow for a custom comparison function
  121. function moveTo(stream, x, to) {
  122.   var array = @current(stream).array
  123.   var index = array.indexOf(x)
  124.   @assert.isNot(index, -1)
  125.   return moveFromTo(stream, index, to)
  126. }
  127.  
  128.  
  129.  
  130. // TODO this is a hack to work around the fact that Conductance requires a child node (it can't be null)
  131. function insertBefore(elem, child, value) {
  132.   if (child == null) {
  133.     return elem ..@appendContent(value)
  134.   } else {
  135.     return elem ..@insertBefore(child, value)
  136.   }
  137. }
  138.  
  139.   // TODO needs a better name: it takes a `from` and `to` index and returns a corrected `to` index
  140. function getIndex(from, to) {
  141.   if (from <= to) {
  142.     return to + 1
  143.   } else if (from > to) {
  144.     return to
  145.   }
  146. }
  147.  
  148. // TODO this is probably unnecessary
  149. function initAnimation(elem) {
  150.   elem.classList.remove("animation-start")
  151.   elem.classList.remove("animation-end")
  152. }
  153.  
  154. // TODO handling for if the animation never occurs
  155. function startAnimation(elem) {
  156.   elem.classList.add("animation-start")
  157.   elem.classList.remove("animation-end")
  158.   // Force relayout
  159.   getComputedStyle(elem).left
  160.   elem.classList.remove("animation-start")
  161.   elem.classList.add("animation-end")
  162.   elem ..@wait("transitionend")
  163.   elem.classList.remove("animation-end")
  164. }
  165.  
  166. // TODO handling for if the animation never occurs
  167. function reverseAnimation(elem) {
  168.   elem.classList.add("animation-start")
  169.   elem.classList.add("animation-end")
  170.   // Force relayout
  171.   getComputedStyle(elem).left
  172.   elem.classList.remove("animation-end")
  173.   elem ..@wait("transitionend")
  174.   elem.classList.remove("animation-start")
  175. }
  176.  
  177. function toDOM(stream, f) {
  178.   return @Mechanism(function (elem) {
  179.     var seen = false
  180.  
  181.     // TODO does this drop events if the block function is suspended?
  182.     // TODO it is necessary to use `each.par` or `each.track` to handle the situations where stuff changes in mid-animation
  183.     // TODO is `each.par` or `each.track` better?
  184.     stream ..@each.par(function (diff) {
  185.       // TODO this is really hacky, there has to be a better way to grab the proper element
  186.       var parent = elem.children[0]
  187.       var children = parent.children
  188.  
  189.       // TODO more assertions for all this stuff
  190.       if (seen) {
  191.         if (diff.type === "add") {
  192.           var child = children[diff.index]
  193.           // TODO animate these in parallel with each.par?
  194.           parent ..insertBefore(child, f(diff.value)) ..@each(startAnimation)
  195.  
  196.         } else if (diff.type === "remove") {
  197.           var child = children[diff.index]
  198.           @assert.ok(child)
  199.  
  200.           child ..reverseAnimation
  201.           child ..@removeNode
  202.  
  203.         // Do not animate when replacing one node with another
  204.         } else if (diff.type === "modify") {
  205.           var from = children[diff.index]
  206.           var to   = children[getIndex(diff.index, diff.index)]
  207.           @assert.ok(from)
  208.  
  209.           // TODO this waitfor is probably unnecessary, due to the lack of animation
  210.           // TODO does this waitfor cause any performance problems?
  211.           waitfor {
  212.             // TODO can't use DOM methods because this needs to de/attach mechanisms and such
  213.             from ..@removeNode
  214.           } and {
  215.             parent ..insertBefore(to, f(diff.value_new)) ..@each(initAnimation)
  216.           }
  217.  
  218.         } else if (diff.type === "move") {
  219.           var from = children[diff.index_from]
  220.           var to   = children[getIndex(diff.index_from, diff.index_to)]
  221.           @assert.ok(from)
  222.  
  223.           waitfor {
  224.             from ..reverseAnimation
  225.             from ..@removeNode
  226.           } and {
  227.             // TODO animate these in parallel with each.par?
  228.             parent ..insertBefore(to, f(diff.value)) ..@each(startAnimation)
  229.           }
  230.  
  231.         } else {
  232.           @assert.fail(diff.type)
  233.         }
  234.  
  235.       // Don't animate anything the first time we render the DOM
  236.       } else {
  237.         seen = true
  238.         // TODO do these in parallel with each.par?
  239.         parent ..@appendContent(diff.array ..@map(f)) ..@each(initAnimation)
  240.       }
  241.     })
  242.   })
  243. }
  244.  
  245.  
  246.  
  247. // The actual app begins here
  248. var elements = ArrayDiff([
  249.   { id: 1 },
  250.   { id: 2 }
  251. ])
  252.  
  253. var css = @CSS(`
  254.   {
  255.     box-sizing: border-box;
  256.     display: block;
  257.     transition: all 1s ease-in-out;
  258.     border: 1px solid gainsboro;
  259.     overflow: hidden;
  260.  
  261.     height: 35px;
  262.     padding: 5px;
  263.     margin: 0px;
  264.     margin-bottom: -1px;
  265.   }
  266.  
  267.   &.animation-start {
  268.     opacity: 0;
  269.     height: 0px;
  270.     border-top-width: 0px;
  271.     border-bottom-width: 0px;
  272.     padding-top: 0px;
  273.     padding-bottom: 0px;
  274.     margin-top: 0px;
  275.     margin-bottom: 0px;
  276.     margin-left: 20px;
  277.   }
  278.  
  279.   /* This is just to remove the circle from the left of <li> items */
  280.   li {
  281.     display: block;
  282.   }
  283. `)
  284.  
  285. var dom_elements = elements ..toDOM(function (x) {
  286.   return `
  287.     <li>${x.id}</li>
  288.   ` ..css
  289. })
  290.  
  291. waitfor {
  292.   hold(500)
  293.   var index = elements ..push({ id: 3 })
  294.   hold(1100)
  295.   elements ..replaceAt(index, { id: 4 })
  296.   hold(500)
  297.   elements ..push({ id: 5 })
  298.   hold(500)
  299.   elements ..moveFromTo(index, index + 1)
  300.   hold(500)
  301.   elements ..removeAt(index + 1)
  302. } and {
  303.   @mainContent ..@appendContent(`<ul></ul>` ..dom_elements) { || hold() }
  304. }
RAW Paste Data