Source: scrollable-movie-clip.js

/**
 * @function scrollableMovieClip
 * @summary
 * Add a drag handler to a movie clip, so it appears scrollable by mouse and touch events.
 * @description
 * The mousewheel also scrolls the movie clip on all desktop browsers except Microsoft Edge.
 *
 * The scrollable movie clip's endpoints for movement are the most inward of either:
 *   1. the edge of the initial position of the scrollable movie clip, or
 *   2. the edge of the mask
 *
 * You must include the Initialize Gestures and Constrained Drag snippets in your FLA.
 *
 * Required Parameters:
 * * draggable the Movie Clip instance to be made scrollable
 *
 * Optional Parameters:
 * * clickableArray
 *   * is an optional array of MovieClips that will be repositioned as the draggable is scrolled.
 *   * You need to add the event handlers outside of this snippet, to handle actions (clicks, mouseover,
 *     mouseleave, etc) on the clickables.
 *   * The clickables must in a layer or layers LOWER than the draggable.
 *   * If the goal is to make certain regions of an image clickable, then the clickables can simply be movie clips
 *     that contain a rectangle.  The rectangle can have an alpha of 0.
 *
 * * mask
 *   * If mask is specified, this Movie Clip will be used to create a mask.
 *   * The mask Movie Clip will be made transparent, so you can use any color you want in the Animate CC editor.
 *   * The mask is also used to create a shape under the draggable which will capture clicks and not transmit the
 *     click events to lower layers.
 *
 * * margin
 *   * If options.margin is provided, additional padding will be added (or subtracted if negative)
 *     from the scrolling boundaries.
 *   * The units of the margin are those of the parent MovieClip.
 *   * If the margins cause the current position of the draggable to be outside the boundaries,
 *     it will be immediately positioned to the closest edge.
 *
 * * dragUpdate
 *   function will be called at each dragUpdate and passed an object with a key "fraction"
 *
 * Requires snippets:
 * *  Initialize Gestures
 * *  Constrained Drag
 * *  Get Movie Clip Bounds (if there is a ClickableArray)
 * *  Point In Bounds (if there is a ClickableArray)
 *
 * Returns
 * ```
 * {
 *   removeEventHandlers<function>,
 *   restoreEventHandlers<function>,
 *   moveToFraction<function({fraction: number}>,
 *   moveByDelta<function({deltaX: number, deltaY: number}>,
 *   startAutoScroll<function>,
 *   stopAutoScroll<function>
 * }
 * ```
 * The function moveToFraction allows the position of the draggable to be easily changed outside this snippet.
 *
 * @param {createjs.MovieClip} options.draggable
 * @param {createjs.MovieClip=} options.mask optional
 * @param {boolean=} options.doVerticalScroll optional, defaults to false
 * @param {Array<createjs.MovieClip>=} options.clickableArray optional
 * @param {number=} clickMsAfterDrag options.clickMsAfterDrag
 *          optional value for number of milliseconds that activates a click after a drag (default = 200)
 * @param {Object<number, number, number, number>=} options.margin
 *          optional values for added inward margin (can be negative) in units of the
 *          parent of the draggable
 * @param {number} options.margin.left left margin
 * @param {number} options.margin.right right margin
 * @param {number} options.margin.top top margin
 * @param {number} options.margin.bottom bottom margin
 * @param {number=} options.mouseWheelSpeed defaults to 1.0 (100% of actual speed)
 * @param {number=} options.releaseTolerance passed to constrained drag snippet, default = 0
 * @param {function=} options.dragUpdate Optional function called at each drag update
 *        See constrained drag snippet for details.
 * @param {function=} options.dragEnd Optional function called at the end of the drag
 *        See constrained drag snippet for details.
 * @param {number=} options.clickMsAfterDrag (default 200) Number of milliseconds to wait before a click event will be
 *        forwarded an item in the ClickableArray.  If this value is zero, every drag will send a click.
 * @param {number=} options.autoScrollIntervalMs (default 100) Number of milliseconds that autoScroll is repeated
 * @param {number=} options.waitTimeToRestartAutoScrollMs (default 2000) Number of milliseconds to automatically restart
 *        autoscroll after the user interacts (manually moves) the draggable
 * @param {number=} options.startAutoScrollDelayMs (default 0) After calling startAutoScroll, the first movement is not
 *        until after startAutoScrollDelayMs.  The same delay time applies to automatic restarting of autoscroll.
 * @param {number=} options.autoScrollDx (default 1) Number of pixels to move a horizontal scrollable per autoScroll.
 *        Positive values move the scrollable to the right.
 * @param {number=} options.autoScrollDy (default 1) Number of pixels to move a vertical scrollable per autoScroll
 *        Positive values move the scrollable downward.  Most ads with scrollable text will use negative Dy.
 * @param {number=} options.startFraction (default auto calculated) Position draggable will be reset after auto scroll loop
 * @param {number=} options.endFraction (default auto calculated) Position at which draggable loops in auto scroll
 * @param {boolean=} options.loop (default false) if true, draggable will loop back to the other end when it reaches either edge
 *
 * @returns {Object} same object as returned from constrained drag + moveToFraction
 */
function scrollableMovieClip (options) {
  var addConstrainedDrag = snippets.addConstrainedDrag
  if (typeof addConstrainedDrag !== 'function') {
    console.error('Scrollable Movie Clip snippet requires Constrained Drag snippet')
    return
  }
  var scrollableOptions = options
  var doVerticalScroll = !!options.doVerticalScroll
  console.log('doVerticalScroll = ', doVerticalScroll)
  var draggable = options.draggable
  var mask = options.mask
  var doLogging = !!options.doLogging
  if (!(draggable instanceof createjs.MovieClip)) {
    console.error('scrollableMovieClip must be passed options.draggable')
    return
  }
  var forceDragRelease = !(options.forceDragRelease === false)
  // var allowDraggableClick = !(options.allowDraggableClick === false)
  var margin = options.margin || {}
  margin = Object.assign({ left: 0, right: 0, bottom: 0, top: 0 }, margin)
  var optionalCallback = typeof options.dragUpdate === 'function'
    ? options.dragUpdate : null
  var clickableArray = options.clickableArray || []
  var clickMsAfterDrag = parseInt(options.clickMsAfterDrag)
  clickMsAfterDrag = isNaN(clickMsAfterDrag) ? 200 : clickMsAfterDrag
  clickableArray = clickableArray.filter(function (item) {
    return (item instanceof createjs.MovieClip)
  })
  /*
   * These 3 snippets are also required if a clickableArray is used
   */
  var getMovieClipBounds = snippets.getMovieClipBounds
  var pointInBounds = snippets.pointInBounds
  if (clickableArray.length) {
    if (typeof pointInBounds !== 'function') {
      console.error('Scrollable Movie Clip snippet requires snippet Point In Bounds')
      return
    }
    if (typeof getMovieClipBounds !== 'function') {
      console.error('Scrollable Movie Clip snippet requires snippet Get MovieClip Bounds')
      return
    }
  }
  var allowDraggableClick = !!clickableArray.length
  var contentArray = [draggable].concat(clickableArray)
  var useMask = mask instanceof createjs.MovieClip
  var isNumber = function (x) {
    return typeof x === 'number'
  }
  var mouseWheelSpeed = isNumber(options.mouseWheelSpeed) ? options.mouseWheelSpeed : 1.0
  mouseWheelSpeed = Math.min(mouseWheelSpeed, 1)
  mouseWheelSpeed = Math.max(mouseWheelSpeed, 0)
  var releaseTolerance = isNumber(options.releaseTolerance) ? options.releaseTolerance : 0.0
  releaseTolerance = Math.min(releaseTolerance, 1)
  releaseTolerance = Math.max(releaseTolerance, 0)

  var loop = options.loop === true

  clickableArray.forEach(function (clickable) {
    clickable.draggableOffset = {
      x: clickable.x - draggable.x,
      y: clickable.y - draggable.y
    }
  })
  var updateClickableArray = function () {
    clickableArray.forEach(function (clickable) {
      clickable.x = draggable.x + clickable.draggableOffset.x
      clickable.y = draggable.y + clickable.draggableOffset.y
    })
  }
  /*
   * This wrapper function ensures the clickable MovieClip array is moved in sync
   * with the draggable.
   */
  var dragUpdateTime = new Date()
  var dragUpdate = function (options) {
    /*
     * The dragUpdateTime is used to determine if a drag event should activate a click handler,
     * for the option clickMsAfterDrag.
     */
    if (!isAutoScrolling) {
      dragUpdateTime = new Date()
    }
    optionalCallback && optionalCallback(options)
    updateClickableArray()
  }

  var container = draggable.parent
  var x0, x1, y0, y1
  var containerX0, containerX1, containerY0, containerY1

  var draggableUpperLeft = draggable.localToLocal(
    draggable.nominalBounds.x,
    draggable.nominalBounds.y,
    container
  )
  var draggableLowerRight = draggable.localToLocal(
    (draggable.nominalBounds.x + draggable.nominalBounds.width),
    (draggable.nominalBounds.y + draggable.nominalBounds.height),
    container
  )
  if (doLogging) {
    console.log('draggable upper left: ', draggableUpperLeft)
    console.log('draggable lower right: ', draggableLowerRight)
  }
  /*
   * Autocalculate the Constrained Drag Line Endpoints.
   *
   * For horizontal scrolling:
   *   containerX0 = location of left edge of draggable
   *   containerX1 = location of right edge of draggable
   * For vertical scrolling:
   *   containerY0 = location of top edge of draggable
   *   containerY1 = location of bottom edge of draggable
   */
  if (useMask) {
    /*
     * Create a new shape to intercept clicks and add under the draggable
     */
    var clickableShape = createMaskShape({ maskMC: mask, container: container, color: '#000000' })
    var clickableHitArea = createMaskShape({ maskMC: mask, container: container, color: '#000000' })
    clickableHitArea.x = 0
    clickableHitArea.y = 0
    clickableHitArea.alpha = 1
    clickableShape.hitArea = clickableHitArea
    clickableShape._off = false
    /*
     * The array container.children is not populated until each object's tween has completed
     */
    setTimeout(function () {
      var draggableIndex = container.getChildIndex(draggable)
      container.addChildAt(clickableShape, draggableIndex)
    }, 100)
    clickableShape.on('click', function (evt) {
      evt.preventDefault()
    })
    /*
     * Use the mask movie clip's nominal bounds to create a mask shape and add to each item
     * in the contentArray.
     */
    convertToMask({
      maskMC: mask,
      maskableArray: contentArray
    })
    var maskUpperLeft = mask.localToLocal(
      mask.nominalBounds.x,
      mask.nominalBounds.y,
      container
    )
    var maskLowerRight = mask.localToLocal(
      (mask.nominalBounds.x + mask.nominalBounds.width),
      (mask.nominalBounds.y + mask.nominalBounds.height),
      container
    )
    if (doLogging) {
      console.log('mask upper left: ', maskUpperLeft)
      console.log('mask lower right: ', maskLowerRight)
    }
    containerX0 = Math.min(maskLowerRight.x, draggableLowerRight.x)
    containerX1 = Math.max(maskUpperLeft.x, draggableUpperLeft.x)
    containerY0 = Math.max(maskUpperLeft.y, draggableUpperLeft.y)
    containerY1 = Math.min(maskLowerRight.y, draggableLowerRight.y)
  } else {
    containerX0 = draggableLowerRight.x
    containerX1 = draggableUpperLeft.x
    containerY0 = draggableUpperLeft.y
    containerY1 = draggableLowerRight.y
    var containerUpperLeft = {
      x: container.nominalBounds.x,
      y: container.nominalBounds.y
    }
    var containerLowerRight = {
      x: container.nominalBounds.x + container.nominalBounds.width,
      y: container.nominalBounds.y + container.nominalBounds.height
    }
    if (doLogging) {
      console.log('container upper left: ', containerUpperLeft)
      console.log('container lower right: ', containerLowerRight)
    }
    containerX0 = Math.max(containerLowerRight.x, draggableLowerRight.x)
    containerX1 = Math.min(containerUpperLeft.x, draggableUpperLeft.x)
    containerY0 = Math.min(containerUpperLeft.y, draggableUpperLeft.y)
    containerY1 = Math.max(containerLowerRight.y, draggableLowerRight.y)
  }

  /*
   * Use the scaled nominalBounds coordinates to account for an arbitrary registration point.
   *
   * Subtract the scaled width or height to keep the far edge of the draggable
   * within the boundaries.
   *
   * If draggable is the same size as the container, add epsilon so addContrainedDrag uses right direction
   */
  if (doVerticalScroll) {
    y0 = containerY0 - draggable.nominalBounds.y * draggable.scaleY +
         margin.top
    y1 = containerY1 - (draggable.nominalBounds.height + draggable.nominalBounds.y) * draggable.scaleY -
         margin.bottom
    y1 = y0 === y1 ? y1 + 1.e-6 : y1
    x0 = draggable.x
    x1 = x0
  } else {
    x0 = containerX0 - (draggable.nominalBounds.width + draggable.nominalBounds.x) * draggable.scaleX -
         margin.right
    x1 = containerX1 - draggable.nominalBounds.x * draggable.scaleX +
         margin.left
    x1 = x0 === x1 ? x1 + 1.e-6 : x1
    y0 = draggable.y
    y1 = y0
  }
  var dragReturn = addConstrainedDrag(Object.assign(options, {
    draggable: draggable,
    line: {
      a: { x: x0, y: y0 },
      b: { x: x1, y: y1 }
    },
    dragUpdate: dragUpdate,
    dragEnd: options.dragEnd,
    // updateClickableArray: updateClickableArray,
    forceDragRelease: forceDragRelease,
    allowDraggableClick: allowDraggableClick,
    mouseWheelSpeed: mouseWheelSpeed,
    releaseTolerance: releaseTolerance,
    naturalScrolling: true,
    loop: loop,
    doLogging: doLogging
  }))
  var baseMoveToFraction = dragReturn.moveToFraction
  /*
   * The snippet scrollbar-movie-clip, passes in a callback function moveToFractionCallback and
   * moveByDeltaCallback, so the scroll bar moves in sync with the scrollable
   */
  var moveToFraction = function (options) {
    var deltaObj = baseMoveToFraction(options)
    updateClickableArray()
    scrollableOptions.moveToFractionCallback && scrollableOptions.moveToFractionCallback(options)
    return deltaObj
  }
  var baseMoveByDelta = dragReturn.moveByDelta
  var moveByDelta = function (options) {
    var deltaObj = baseMoveByDelta(options)
    updateClickableArray()
    scrollableOptions.moveByDeltaCallback && scrollableOptions.moveByDeltaCallback()
    return deltaObj
  }
  dragReturn.moveToFraction = moveToFraction
  dragReturn.moveByDelta = moveByDelta
  dragReturn.startAutoScroll = startAutoScroll
  dragReturn.stopAutoScroll = stopAutoScroll
  dragReturn.restartAutoScroll = restartAutoScroll
  /*
   * Forward click event on draggable to item in clickableArray
   */
  if (clickableArray.length) {
    /*
     * The bounds of each clickable movie clip is used to determine if the event should dispatched.
     */
    var boundsArray = clickableArray.map(function (clickable) {
      /*
       * The target coordinate system is the parent parameter in getMousePoint
       */
      return getMovieClipBounds({ movieClip: clickable, target: draggable })
    })
    /*
     * Store a reference to all children of the clickableArray, to filter for the proper target of
     * a click event outside the mask.
     */
    var clickableChildren = [draggable]
    var addChildren = function (parent) {
      if (!parent.children) return
      parent.children.forEach(function (child) {
        clickableChildren.push(child)
        addChildren(child)
      })
    }
    /*
     * Wait for children to be added to the parents
     */
    setTimeout(function () {
      clickableArray.forEach(function (clickable) {
        addChildren(clickable)
      })
      // doLogging && console.log(clickableChildren)
    }, 100)
    /*
     * CreateJS activates a click event at the end of drag on the mouseUp event.  The distance between the mouseup and
     * mousedown event is used to determine if the click event was a regular click or a drag mouseup.
     */
    var lastMouseDownEvent = null
    draggable.on('mousedown', function (evt) {
      lastMouseDownEvent = evt
    })
    draggable.on('click', function (evt) {
      evt.stopPropagation()
      evt.preventDefault()
      var eventDispatched = false
      var mousePoint = dragReturn.getMousePoint({ event: evt, parent: draggable })
      var maskBounds
      var pointInMask
      if (mask) {
        maskBounds = getMovieClipBounds({ movieClip: mask, target: draggable })
        pointInMask = pointInBounds({ pt: mousePoint, bounds: maskBounds })
      } else {
        /*
         * If there is no mask, every click on the draggable is valid.
         */
        pointInMask = true
      }
      /*
       * The deltaTime filters drag events from activating a click handler unintentionally.
       */
      if (pointInMask) {
        var deltaTime = new Date() - dragUpdateTime
        if (deltaTime > clickMsAfterDrag) {
          var boundsFound = boundsArray.find(function (element) {
            return pointInBounds({ pt: mousePoint, bounds: element })
          })
          if (boundsFound) {
            var index = boundsArray.indexOf(boundsFound)
            // console.log('index = ', index)
            clickableArray[index].dispatchEvent(evt)
            eventDispatched = true
          }
        }
      } else {
        doLogging && console.log('click point not within mask')
      }
      /*
       * Since the draggable can be larger than the mask, it might intercept click events intended
       * for underlying elements.  Dispatch the event to the highest element with a click handler that
       * is not in the clickableArray.
       */
      if (!eventDispatched && !pointInMask && lastMouseDownEvent) {
        /*
         * Ignore the event if it was the duplicate click event sent after a drag.
         */
        var dx = evt.stageX - lastMouseDownEvent.stageX
        var dy = evt.stageY - lastMouseDownEvent.stageY
        var dist = Math.sqrt(dx * dx + dy * dy)
        /*
         * Arbitrary distance of 10 pixels indicates it was not a simple click.
         */
        if (dist > 10) {
          console.log('Event was a drag and not a click')
          return
        }
        var x = evt.stageX / window.$b.stage.scaleX
        var y = evt.stageY / window.$b.stage.scaleY
        var objs = window.$b.stage.getObjectsUnderPoint(x, y)
        objs.shift()
        var obj
        while (objs.length) {
          obj = objs.shift()
          if (obj.hasEventListener('click')) {
            if (clickableChildren.indexOf(obj) < 0) {
              doLogging && console.log('dispatch event to:', obj)
              obj.dispatchEvent(evt)
              evt.stopPropagation()
              break
            }
          }
        }
      }
    })
  }
  var waitTimeToRestartAutoScrollMs = isNumber(options.waitTimeToRestartAutoScrollMs) ? options.waitTimeToRestartAutoScrollMs : 2000
  var autoScrollIntervalMs = isNumber(options.autoScrollIntervalMs) ? options.autoScrollIntervalMs : 100
  var startAutoScrollDelayMs = isNumber(options.startAutoScrollDelayMs) ? options.startAutoScrollDelayMs : 0
  var autoScrollDx = 0
  var autoScrollDy = 0
  /*
   * If autoScroll Dx or Dy is not specified, choose value so that endFraction is farther away than startFraction
   */
  var startFraction = dragReturn.getFraction()
  if (doVerticalScroll) {
    // autoScrollDy = isNumber(options.autoScrollDy) ? options.autoScrollDy : 1
    if (isNumber(options.autoScrollDy)) {
      autoScrollDy = options.autoScrollDy
    } else {
      autoScrollDy = startFraction > 0.5 ? 1 : -1
    }
  } else {
    // autoScrollDx = isNumber(options.autoScrollDx) ? options.autoScrollDx : 1
    if (isNumber(options.autoScrollDx)) {
      autoScrollDx = options.autoScrollDx
    } else {
      autoScrollDx = startFraction < 0.5 ? 1 : -1
    }
  }
  var isAutoScrolling = false
  var autoScrollWasStarted = false
  var cancelAutoScroll = false
  var restartAutoScrollTimerId
  var intervalId = null
  var endFraction
  if (isNumber(options.endFraction)) {
    endFraction = options.endFraction
  } else {
    if (doVerticalScroll) {
      endFraction = autoScrollDy > 0 ? 0 : 1
    } else {
      endFraction = autoScrollDx < 0 ? 0 : 1
    }
  }
  if (isNumber(options.startFraction)) {
    startFraction = options.startFraction
  } else {
    startFraction = 1 - endFraction
  }

  /*
   * Since original options object is used inside this function, use a new variable name for the input options.
   */
  function startAutoScroll (startAutoScrollOptions) {
    autoScrollWasStarted = true
    startAutoScrollOptions = typeof startAutoScrollOptions === 'object' ? startAutoScrollOptions : {}
    cancelAutoScroll = false
    if (isAutoScrolling) return
    var lastFraction = dragReturn.getFraction()
    isAutoScrolling = true
    var lastTimeStamp = 0
    /*
     * Using requestAnimationFrame instead of a setTimeout or setInterval, allows the maximum frame rate
     * that the browser can deliver.  This wrapper function returns if the next animation frame is less than
     * the autoScrollInterval.
     */
    var animationWrapper = function (timeStamp) {
      if (cancelAutoScroll) return
      intervalId = requestAnimationFrame(animationWrapper)
      if (!timeStamp || (timeStamp - lastTimeStamp < autoScrollIntervalMs)) return
      lastTimeStamp = timeStamp
      moveDrag()
    }
    var moveDrag = function () {
      var newFraction = dragReturn.getFraction()
      // console.log('fraction: ', newFraction)
      if (newFraction !== lastFraction) {
        lastFraction = newFraction
        window.cancelAnimationFrame(intervalId)
        intervalId = null
        isAutoScrolling = false
        restartAutoScroll()
      } else {
        if (((endFraction === 0) && (newFraction > 0)) || ((endFraction === 1) && (newFraction < 1))) {
          moveByDelta({ dx: autoScrollDx, dy: autoScrollDy })
        } else {
          loop && moveToFraction({ fraction: startFraction })
        }
        lastFraction = dragReturn.getFraction()
      }
    }
    if (!isNumber(startAutoScrollDelayMs) || startAutoScrollOptions.delay === false) {
      animationWrapper(null)
    } else {
      setTimeout(function () {
        animationWrapper(null)
      }, startAutoScrollDelayMs)
    }
  }

  function stopAutoScroll () {
    cancelAutoScroll = true
    if (intervalId) {
      // window.clearInterval(intervalId)
      window.cancelAnimationFrame(intervalId)
      intervalId = null
    }
    isAutoScrolling = false
  }

  function restartAutoScroll (options) {
    if (cancelAutoScroll && !options.overrideCancel) return
    if (!autoScrollWasStarted) return
    if (restartAutoScrollTimerId) {
      window.clearTimeout(restartAutoScrollTimerId)
    }
    restartAutoScrollTimerId = setTimeout(function () {
      restartAutoScrollTimerId = null
      startAutoScroll({ delay: false })
    }, waitTimeToRestartAutoScrollMs)
  }

  return dragReturn
}

/**
 * @function convertToMask
 * @summary
 * Use a mask movie clip to create a mask shape and add that to the contents movieClip.
 * @description
 * Typically, all maskableMovieClips will have the same parent, so they can be masked with
 * the same object.
 *
 * @param options.maskMC {createjs.MovieClip} Movie Clip to use as a template for a mask
 * @param options.maskableArray {Array<createjs.MovieClip>} Array of MovieClips to be masked
 */
function convertToMask (options) {
  var maskMC = options.maskMC
  if (!(maskMC instanceof createjs.MovieClip)) {
    console.error('convertToMask must be passed a Movie Cip options.maskMC')
  }
  var maskableArray = options.maskableArray || []
  maskableArray = maskableArray.filter(function (item) {
    return item instanceof createjs.MovieClip
  })
  if (!maskableArray.length) {
    console.error('convertToMask must be passed an array of Movie Clips to be masked options.maskableArray')
    return
  }
  var container = maskableArray[0].parent
  /*
   * If all the maskables have the same parent, this mask will be used for all of them
   */
  var mask = createMaskShape({
    maskMC: maskMC,
    container: container
  })
  maskMC.visible = false
  if (maskableArray && maskableArray.length) {
    maskableArray.forEach(function (maskable) {
      /*
       * Only create a separate mask shape if it is different than the first one
       */
      if (maskable.parent !== container) {
        maskable.mask = createMaskShape({
          maskMC: maskMC,
          container: maskable.parent
        })
      } else {
        maskable.mask = mask
      }
    })
  }
}

/**
 * @function createMaskShape
 * @summary
 * Create and return a shape from a Movie Clip in coordinates of the container.
 * @description
 * Used by function convertToMask or can also be used independently
 * Returned shape has alpha set to zero
 *
 * @param {Object} options
 * @param {createjs.MovieClip} options.maskMC  Movie Clip to use as a template for the mask.
 * @param {createjs.MovieClip} options.container Container of the Movie Clip to be masked.
 * @param options.color {String=} optional hex color, ex #000000
 * @returns {createjs.Shape}
 */
function createMaskShape (options) {
  var maskMC = options.maskMC
  var container = options.container
  var color = options.color || '#00ff00'
  var width = maskMC.nominalBounds.width * maskMC.scaleX
  var height = maskMC.nominalBounds.height * maskMC.scaleY
  var mask = new window.createjs.Shape()
  mask
    .graphics
    .beginFill(color)
    .drawRect(0, 0, width, height)
  mask._off = true
  mask.alpha = 0

  /*
   * The offset accounts for an abitrary registration point.
   * Use the unscaled coordinates, since localToLocal takes scaling into account.
   */
  var maskX = maskMC.nominalBounds.x
  var maskY = maskMC.nominalBounds.y
  var offset = maskMC.localToLocal(maskX, maskY, container)
  mask.x = offset.x
  mask.y = offset.y
  // console.log('mask x,y: ', mask.x, mask.y)
  return mask
}

/**
 * @function useMovieClipAsHitArea
 * @summary
 * Create a hitArea on the targetMovieClip based on the current relative coordinates of the hitAreaMovieClip
 *
 * @param {Object} options
 * @param {createjs.MovieClip} options.targetMovieClip Movie Clip to add a hitArea to
 * @param {createjs.MovieClip} options.hitAreaMovieClip Movie Clip to base the hitArea on
 */
function useMovieClipAsHitArea (options) {
  var targetMovieClip = options.targetMovieClip
  var hitAreaMovieClip = options.hitAreaMovieClip
  if (!(targetMovieClip instanceof createjs.MovieClip)) {
    console.error('useMovieClipAsHitArea must be passed options.targetMovieClip')
    return
  }
  if (!(hitAreaMovieClip instanceof createjs.MovieClip)) {
    console.error('useMovieClipAsHitArea must be passed options.hitAreaMovieClip')
    return
  }
  var nb = hitAreaMovieClip.nominalBounds
  var upperLeft = hitAreaMovieClip.localToLocal(nb.x, nb.y, targetMovieClip)
  var lowerRight = hitAreaMovieClip.localToLocal(nb.x + nb.width, nb.y + nb.height, targetMovieClip)
  var hitArea = new window.createjs.Shape()
  hitArea
    .graphics
    .beginFill('#000')
    .drawRect(upperLeft.x, upperLeft.y, lowerRight.x, lowerRight.y)
  targetMovieClip.hitArea = hitArea
  targetMovieClip.mouseChildren = false
  return ({ upperLeft: upperLeft, lowerRight: lowerRight })
}

window.$b = window.$b || {}
window.$b.snippets = window.$b.snippets || {}
var snippets = window.$b.snippets
snippets.scrollableMovieClip = scrollableMovieClip
snippets.convertToMask = convertToMask
snippets.createMaskShape = createMaskShape
snippets.useMovieClipAsHitArea = useMovieClipAsHitArea