• ¶

    OriDomi

    Fold up the DOM like paper.

    1.1.5

  • ¶

    oridomi.com

    by Dan Motzenbecker

  • ¶

    Copyright 2014, MIT License

    
    libName = 'OriDomi'
  • ¶

    This variable is set to true and negated later if the browser does not support OriDomi.

    isSupported = true
  • ¶

    Utility Functions

  • ¶
  • ¶

    Used for informing the developer which required feature the browser lacks.

    supportWarning = (prop) ->
      console?.warn "#{ libName }: Missing support for `#{ prop }`."
      isSupported = false
  • ¶

    Checks for the presence of CSS properties on a test element.

    testProp = (prop) ->
  • ¶

    Loop through the vendor prefix list and return a match is found.

      for prefix in prefixList
        return full if (full = prefix + capitalize prop) of testEl.style
  • ¶

    If the unprefixed property is present, return it.

      return prop if prop of testEl.style
  • ¶

    If no matches are found, return false to denote that the browser is missing this property.

      false
  • ¶

    Generates CSS text based on a selector string and a map of styling rules.

    addStyle = (selector, rules) ->
      style = ".#{ selector }{"
      for prop, val of rules
  • ¶

    If the CSS property is among special properties defined later, prefix it.

        if prop of css
          prop = css[prop]
          prop = '-' + prop if prop.match /^(webkit|moz|ms)/i
  • ¶

    Convert camel case to hyphenated.

        style += "#{ prop.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() }:#{ val };"
    
      styleBuffer += style + '}'
  • ¶

    Defines gradient directions based on a given anchor.

    getGradient = (anchor) ->
      "#{ css.gradientProp }(#{ anchor }, rgba(0, 0, 0, .5) 0%, rgba(255, 255, 255, .35) 100%)"
  • ¶

    Used mainly when creating camel cased strings.

    capitalize = (s) ->
      s[0].toUpperCase() + s[1...]
  • ¶

    Create an element and look up the canonical class name.

    createEl = (className) ->
      el = document.createElement 'div'
      el.className = elClasses[className]
      el
  • ¶

    Clone an element, add an additional class, and return it.

    cloneEl = (parent, deep, className) ->
      el = parent.cloneNode deep
      el.classList.add elClasses[className]
      el
  • ¶

    GPU efficient ways of hiding and showing elements:

    hideEl = (el) ->
      el.style[css.transform] = 'translate3d(-99999px, 0, 0)'
    
    
    showEl = (el) ->
      el.style[css.transform] = 'translate3d(0, 0, 0)'
  • ¶

    This decorator is used on public effect methods to invoke preliminary tasks before the effect is applied.

    prep = (fn) ->
      ->
  • ¶

    If the method has been initiated by a touch handler, skip this process.

        if @_touchStarted
          fn.apply @, arguments
        else
          [a0, a1, a2] = arguments
          opt          = {}
          angle        = anchor = null
  • ¶

    This switch is used to derive the intended order of arguments. This keeps argument requirements flexible, allowing most to be left out. By putting this logic in a decorator, it doesn’t have to exist in any of the individual methods.

  • ¶

    Methods are inferred by their arity.

          switch fn.length
            when 1
              opt.callback = a0
            when 2
              if typeof a0 is 'function'
                opt.callback = a0
              else
                anchor       = a0
                opt.callback = a1
            when 3
              angle = a0
              if arguments.length is 2
                if typeof a1 is 'object'
                  opt = a1
                else if typeof a1 is 'function'
                  opt.callback = a1
                else
                  anchor = a1
              else if arguments.length is 3
                anchor = a1
                if typeof a2 is 'object'
                  opt = a2
                else if typeof a2 is 'function'
                  opt.callback = a2
    
          angle   ?= @_lastOp.angle or 0
          anchor or= @_lastOp.anchor
  • ¶

    Here we add the called function and its normalized arguments to the instance’s queue.

          @_queue.push [fn, @_normalizeAngle(angle), @_getLonghandAnchor(anchor), opt]
  • ¶

    _step() manages the queue and decides whether the action will occur now or be deferred.

          @_step()
  • ¶

    This decorator also returns the instance so effect methods are chainable.

          @
  • ¶

    It’s necessary to defer many DOM manipulations to a subsequent event loop tick.

    defer = (fn) ->
      setTimeout fn, 0
  • ¶

    Empty function to be used as placeholder for callback defaults (instead of creating separate empty functions).

    noOp = ->
  • ¶

    Setup

  • ¶
  • ¶

    Set a reference to jQuery (or another $-aliased DOM library). If it doesn’t exist, set to null so OriDomi knows we are working without jQuery. OriDomi doesn’t require it to work, but offers a useful plugin bridge if present.

    $ = if window?.$?.data then window.$ else null
  • ¶

    List of anchors and their corresponding axis pairs.

    anchorList  = ['left', 'right', 'top', 'bottom']
    anchorListV = anchorList[..1]
    anchorListH = anchorList[2..]
  • ¶

    Create a div for testing CSS3 properties.

    testEl = document.createElement 'div'
  • ¶

    The style buffer is later populated with CSS rules and appended to the document.

    styleBuffer = ''
  • ¶

    List of browser prefixes for testing CSS3 properties.

    prefixList = ['Webkit', 'Moz', 'ms']
    baseName   = libName.toLowerCase()
  • ¶

    CSS classes used by style rules.

    elClasses  =
      active:       'active'
      clone:        'clone'
      holder:       'holder'
      stage:        'stage'
      stageLeft:    'stage-left'
      stageRight:   'stage-right'
      stageTop:     'stage-top'
      stageBottom:  'stage-bottom'
      content:      'content'
      mask:         'mask'
      maskH:        'mask-h'
      maskV:        'mask-v'
      panel:        'panel'
      panelH:       'panel-h'
      panelV:       'panel-v'
      shader:       'shader'
      shaderLeft:   'shader-left'
      shaderRight:  'shader-right'
      shaderTop:    'shader-top'
      shaderBottom: 'shader-bottom'
  • ¶

    Each class is namespaced to prevent styling collisions.

    elClasses[k] = "#{ baseName }-#{ v }" for k, v of elClasses
  • ¶

    Map of the CSS3 properties needed to support OriDomi, with shorthand names as keys. The keys and values are initialized as identical pairs to start with and prefixed subsequently when necessary.

    css = new ->
      @[key] = key for key in [
        'transform'
        'transformOrigin'
        'transformStyle'
        'transitionProperty'
        'transitionDuration'
        'transitionDelay'
        'transitionTimingFunction'
        'perspective'
        'perspectiveOrigin'
        'backfaceVisibility'
        'boxSizing'
        'mask'
      ]
      @
  • ¶

    This section is wrapped in a function call so that it can exit early when discovering a lack of browser support to prevent unnecessary work.

    do ->
  • ¶

    Loop through the CSS map and replace each value with the result of testProp().

      for key, value of css
        css[key] = testProp value
  • ¶

    If the returned value is false, warn the user that the browser doesn’t support OriDomi, set isSupported to false, and break out of the loop.

        return supportWarning value unless css[key]
  • ¶

    Test for preserve-3d as a transform style. This is particularly important since it’s necessary for nested 3D transforms and recent versions of IE that support 3D transforms lack it.

      p3d = 'preserve-3d'
      testEl.style[css.transformStyle] = p3d
  • ¶

    Failure is indicated when querying the style lacks the correct string.

      unless testEl.style[css.transformStyle] is p3d
        return supportWarning p3d
  • ¶

    CSS3 linear gradients are used for shading. Testing for them is different because they are prefixed values, not properties. This invokes an anonymous function to loop through vendor-prefixed linear gradients.

      css.gradientProp = do ->
        for prefix in prefixList
          hyphenated = "-#{ prefix.toLowerCase() }-linear-gradient"
          testEl.style.backgroundImage = "#{ hyphenated }(left, #000, #fff)"
  • ¶

    After setting a gradient background on the test div, attempt to retrieve it.

          return hyphenated unless testEl.style.backgroundImage.indexOf('gradient') is -1
  • ¶

    If none of the hyphenated values worked, return the unprefixed version.

        'linear-gradient'
  • ¶

    The default cursor style is set to grab to prompt the user to interact with the element. grab as a value isn’t supported in all browsers so it has to be detected.

      [css.grab, css.grabbing] = do ->
        for prefix in prefixList
          plainGrab = 'grab'
          testEl.style.cursor = (grabValue = "-#{ prefix.toLowerCase() }-#{ plainGrab }")
  • ¶

    If the cursor was set correctly, return the prefixed pair.

          return [grabValue, "-#{ prefix.toLowerCase() }-grabbing"] if testEl.style.cursor is grabValue
  • ¶

    Otherwise try the unprefixed version.

        testEl.style.cursor = plainGrab
        if testEl.style.cursor is plainGrab
          [plainGrab, 'grabbing']
        else
  • ¶

    Fallback to move.

          ['move', 'move']
  • ¶

    Like gradients, transform (as a transition value) needs to be detected and prefixed.

      css.transformProp =
  • ¶

    Use a regular expression to pluck the prefix testProp found.

        if prefix = css.transform.match /(\w+)Transform/i
          "-#{ prefix[1].toLowerCase() }-transform"
        else
          'transform'
  • ¶

    Set a transitionEnd property based on the browser’s prefix for transitionProperty.

      css.transitionEnd =
        switch css.transitionProperty.toLowerCase()
          when 'transitionproperty'       then 'transitionEnd'
          when 'webkittransitionproperty' then 'webkitTransitionEnd'
          when 'moztransitionproperty'    then 'transitionend'
          when 'mstransitionproperty'     then 'msTransitionEnd'
  • ¶

    These calls generate OriDomi’s stylesheet.

      do (i = (s) -> s + ' !important') ->
        addStyle elClasses.active,
          backgroundColor: i 'transparent'
          backgroundImage: i 'none'
          boxSizing:       i 'border-box'
          border:          i 'none'
          outline:         i 'none'
          padding:         i '0'
          transformStyle:  i p3d
          mask:            i 'none'
          position:          'relative'
    
        addStyle elClasses.clone,
          margin:    i '0'
          boxSizing: i 'border-box'
          overflow:  i 'hidden'
          display:   i 'block'
    
        addStyle elClasses.holder,
          width:          '100%'
          position:       'absolute'
          top:            '0'
          bottom:         '0'
          transformStyle: p3d
    
        addStyle elClasses.stage,
          width:          '100%'
          height:         '100%'
          position:       'absolute'
          transform:      'translate3d(-9999px, 0, 0)'
          margin:         '0'
          padding:        '0'
          transformStyle: p3d
  • ¶

    Each anchor needs a particular perspective origin.

        for k, v of {Left: '0% 50%', Right: '100% 50%', Top: '50% 0%', Bottom: '50% 100%'}
          addStyle elClasses['stage' + k], perspectiveOrigin: v
    
        addStyle elClasses.shader,
          width:              '100%'
          height:             '100%'
          position:           'absolute'
          opacity:            '0'
          top:                '0'
          left:               '0'
          pointerEvents:      'none'
          transitionProperty: 'opacity'
  • ¶

    Linear gradient directions depend on their anchor.

        for anchor in anchorList
          addStyle elClasses['shader' + capitalize anchor], background: getGradient anchor
    
        addStyle elClasses.content,
          margin:    i '0'
          position:  i 'relative'
          float:     i 'none'
          boxSizing: i 'border-box'
          overflow:  i 'hidden'
    
        addStyle elClasses.mask,
          width:              '100%'
          height:             '100%'
          position:           'absolute'
          overflow:           'hidden'
          transform:          'translate3d(0, 0, 0)'
          outline:            '1px solid transparent'
    
        addStyle elClasses.panel,
          width:              '100%'
          height:             '100%'
          padding:            '0'
          position:           'absolute'
          transitionProperty: css.transformProp
          transformOrigin:    'left'
          transformStyle:     p3d
    
        addStyle elClasses.panelH, transformOrigin: 'top'
        addStyle "#{ elClasses.stageRight } .#{ elClasses.panel }", transformOrigin: 'right'
        addStyle "#{ elClasses.stageBottom } .#{ elClasses.panel }", transformOrigin: 'bottom'
    
      styleEl      = document.createElement 'style'
      styleEl.type = 'text/css'
  • ¶

    Once the style buffer is ready, it’s appended to the document as a stylesheet.

      if styleEl.styleSheet
        styleEl.styleSheet.cssText = styleBuffer
      else
        styleEl.appendChild document.createTextNode styleBuffer
    
      document.head.appendChild styleEl
  • ¶

    Defaults

  • ¶
  • ¶

    These defaults are used by all OriDomi instances unless overridden.

    defaults =
  • ¶

    The number of vertical panels (for folding left or right). You can use either an integer, or an array of percentages if you want custom panel widths, e.g. [20, 10, 10, 20, 10, 20, 10]. The numbers must add up to 100 (or near it, so you can use values like [33, 33, 33]).

      vPanels: 3
  • ¶

    The number of horizontal panels (for folding top or bottom) or an array of percentages.

      hPanels: 3
  • ¶

    The determines the distance in pixels (z axis) of the camera/viewer to the paper. The smaller the value, the more distorted and exaggerated the effects will appear.

      perspective: 1000
  • ¶

    The default shading style is hard, which shows distinct creases in the paper. Other options include 'soft' — for a smoother, more rounded look — or false to disable shading altogether for a flat look.

      shading: 'hard'
  • ¶

    Determines the duration of all animations in milliseconds.

      speed: 700
  • ¶

    Configurable maximum angle for effects. With most effects, exceeding 90/-90 usually makes the element wrap around and pass through itself leading to some glitchy visuals.

      maxAngle: 90
  • ¶

    Ripple mode causes effects to fold in a staggered, cascading manner. 1 indicates a forward cascade, 2 is backwards. It is disabled by default.

      ripple: 0
  • ¶

    This CSS class is applied to OriDomi elements so they can be easily targeted later.

      oriDomiClass: libName.toLowerCase()
  • ¶

    This is a multiplier that determines the darkness of shading. If you need subtler shading, set this to a value below 1.

      shadingIntensity: 1
  • ¶

    This option allows you to supply the name of a CSS easing method or a cubic bezier formula for customized animation easing.

      easingMethod: ''
  • ¶

    Number of pixels to offset each panel to prevent small gaps from appearing between them. This is configurable if you have a need for precision.

      gapNudge: 1.5
  • ¶

    Allows the user to fold the element via touch or mouse.

      touchEnabled: true
  • ¶

    Coefficient of touch/drag action’s distance delta. Higher numbers cause more movement.

      touchSensitivity: .25
  • ¶

    Custom callbacks for touch/drag events. Each one is invoked with a relevant value so they can be used to manipulate objects outside of the OriDomi instance (e.g. sliding panels). x values are returned when folding left and right, y values for top and bottom. The second argument passed is the original touch or mouse event. These are empty functions by default. Invoked with starting coordinate as first argument.

      touchStartCallback: noOp
  • ¶

    Invoked with the folded angle.

      touchMoveCallback: noOp
  • ¶

    Invoked with ending point.

      touchEndCallback: noOp
  • ¶

    Constructor

  • ¶
    class OriDomi
    
      constructor: (@el, options = {}) ->
        return unless isSupported
  • ¶

    Fix constructor calls made without new.

        return new OriDomi @el, options unless @ instanceof OriDomi
  • ¶

    Support selector strings as well as elements.

        @el = document.querySelector @el if typeof @el is 'string'
  • ¶

    Make sure element is valid.

        unless @el and @el.nodeType is 1
          console?.warn "#{ libName }: First argument must be a DOM element"
          return
  • ¶

    Fill in passed options with defaults.

        @_config = new ->
          for k, v of defaults
            if k of options
              @[k] = options[k]
            else
              @[k] = v
          @
  • ¶

    The ripple setting is converted to a number to allow boolean settings.

        @_config.ripple = Number @_config.ripple
  • ¶

    The queue holds animation sequences.

        @_queue   = []
        @_panels  = {}
        @_stages  = {}
  • ¶

    Set the starting anchor to left.

        @_lastOp  = anchor: anchorList[0]
        @_shading = @_config.shading
  • ¶

    Alias shading: true as hard shading.

        @_shading = 'hard' if @_shading is true
  • ¶

    The shader elements are constructed in a conditional so the process can be skipped if shading is disabled.

        if @_shading
          @_shaders    = {}
          shaderProtos = {}
          shaderProto  = createEl 'shader'
          shaderProto.style[css.transitionDuration]       = @_config.speed + 'ms'
          shaderProto.style[css.transitionTimingFunction] = @_config.easingMethod
    
        stageProto = createEl 'stage'
        stageProto.style[css.perspective] = @_config.perspective + 'px'
    
        for anchor in anchorList
  • ¶

    Each anchor has a unique set of panels.

          @_panels[anchor] = []
          @_stages[anchor] = cloneEl stageProto, false, 'stage' + capitalize anchor
          if @_shading
            @_shaders[anchor] = {}
            if anchor in anchorListV
              @_shaders[anchor][side] = [] for side in anchorListV
            else
              @_shaders[anchor][side] = [] for side in anchorListH
    
            shaderProtos[anchor] = cloneEl shaderProto, false, 'shader' + capitalize anchor
    
        contentHolder = cloneEl @el, true, 'content'
    
        maskProto = createEl 'mask'
        maskProto.appendChild contentHolder
    
        panelProto = createEl 'panel'
        panelProto.style[css.transitionDuration]       = @_config.speed + 'ms'
        panelProto.style[css.transitionTimingFunction] = @_config.easingMethod
  • ¶

    These arrays store panel offsets so they don’t have to be computed twice for each axis.

        offsets = left: [], top: []
  • ¶

    This loop builds all of the panels.

        for axis in ['x', 'y']
          if axis is 'x'
            anchorSet   = anchorListV
            metric      = 'width'
            classSuffix = 'V'
          else
            anchorSet   = anchorListH
            metric      = 'height'
            classSuffix = 'H'
    
          panelConfig = @_config[panelKey = classSuffix.toLowerCase() + 'Panels']
  • ¶

    If the panel set configuration is an integer (as it is by default), an array is filled with equal percentages.

          if typeof panelConfig is 'number'
            count       = Math.abs parseInt panelConfig, 10
            percent     = 100 / count
            panelConfig = @_config[panelKey] = (percent for [0...count])
          else
            count = panelConfig.length
            unless 99 <= panelConfig.reduce((p, c) -> p + c) <= 100.1
              throw new Error "#{ libName }: Panel percentages do not sum to 100"
  • ¶

    Clone a new mask element and append it to a panel element prototype.

          mask = cloneEl maskProto, true, 'mask' + classSuffix
    
          if @_shading
            mask.appendChild shaderProtos[anchor] for anchor in anchorSet
    
          proto = cloneEl panelProto, false, 'panel' + classSuffix
          proto.appendChild mask
    
          for anchor, rightOrBottom in anchorSet
            for panelN in [0...count]
              panel   = proto.cloneNode true
              content = panel.children[0].children[0]
              content.style.width = content.style.height = '100%'
    
              if rightOrBottom
                panel.style[css.origin] = anchor
  • ¶

    Panels on the right and bottom axes are placed backwards.

                index = panelConfig.length - panelN - 1
                prev  = index + 1
              else
                index = panelN
                prev  = index - 1
  • ¶

    The inner content of each panel is offset relative to the panel index to display a contiguous composition.

                if panelN is 0
                  offsets[anchor].push 0
                else
                  offsets[anchor].push (offsets[anchor][prev] - 100) * (panelConfig[prev] / panelConfig[index])
    
              if panelN is 0
                panel.style[anchor] = '0'
  • ¶

    Only the first panel has its size set to the nominal target percentage.

                panel.style[metric] = panelConfig[index] + '%'
              else
  • ¶

    Each subsequent panel is offset by its predecessor/parent’s size.

                panel.style[anchor] = '100%'
  • ¶

    Subsequent panels have their percentages set relative to their parent panel’s percentage to counteract it in an absolute sense.

                panel.style[metric] = panelConfig[index] / panelConfig[prev] * 100 + '%'
    
              if @_shading
                for a, i in anchorSet
                  @_shaders[anchor][a][panelN] = panel.children[0].children[i + 1]
  • ¶

    The inner content retains the original dimensions of the element while being inside a small slice. By manipulating the number based on the total number of panels and the absolute percentage, the size reduction of the parent is undone and sizing flexibility is achieved.

              content.style[metric] =
                content.style['max' + capitalize metric] =
                  (count / panelConfig[index] * 10000 / count) + '%'
    
              content.style[anchorSet[0]] = offsets[anchorSet[0]][index] + '%'
    
              @_transformPanel panel, 0, anchor
              @_panels[anchor][panelN] = panel
  • ¶

    Panels are nested inside each other.

              @_panels[anchor][panelN - 1].appendChild panel unless panelN is 0
  • ¶

    Append the first panel to each stage.

            @_stages[anchor].appendChild @_panels[anchor][0]
    
        @_stageHolder = createEl 'holder'
        @_stageHolder.setAttribute 'aria-hidden', 'true'
        @_stageHolder.appendChild @_stages[anchor] for anchor in anchorList
  • ¶

    Override default styling if original positioning is absolute.

        if window.getComputedStyle(@el).position is 'absolute'
          @el.style.position = 'absolute'
    
        @el.classList.add elClasses.active
        showEl @_stages.left
  • ¶

    The original element is cloned and hidden via transforms so the dimensions of the OriDomi content are maintained by it.

        @_cloneEl = cloneEl @el, true, 'clone'
        @_cloneEl.classList.remove elClasses.active
        hideEl @_cloneEl
  • ¶

    Once the clone is stored the original element is emptied and appended with the clone and the OriDomi content.

        @el.innerHTML = ''
        @el.appendChild @_cloneEl
        @el.appendChild @_stageHolder
  • ¶

    This ensures mouse events work correctly when panels are transformed away from the viewer.

        @el.parentNode.style[css.transformStyle] = 'preserve-3d'
  • ¶

    An effect method is called since touch events rely on using the last method called.

        @accordion 0
        @setRipple @_config.ripple if @_config.ripple
        @enableTouch() if @_config.touchEnabled
  • ¶

    Internal Methods

  • ¶
  • ¶

    This method is called for the action shifted off the queue.

      _step: =>
  • ¶

    Return if the composition is currently in transition or the queue is empty.

        return if @_inTrans or !@_queue.length
        @_inTrans = true
  • ¶

    Destructure action arguments from the front of the queue.

        [fn, angle, anchor, options] = @_queue.shift()
        @unfreeze() if @isFrozen
  • ¶

    A local function for the next action is created should the call need to be deferred (if the stage is folded up or on the wrong anchor).

        next = =>
          @_setCallback {angle, anchor, options, fn}
          args = [angle, anchor, options]
          args.shift() if fn.length < 3
          fn.apply @, args
    
        if @isFoldedUp
          if fn.length is 2
            next()
          else
            @_unfold next
        else if anchor isnt @_lastOp.anchor
          @_stageReset anchor, next
        else
          next()
  • ¶

    This method tests if the called action is identical to the previous one. If two identical operations were called in a row, the transition callback wouldn’t be called due to no animation taking place. This method reasons if movement has taken place, avoiding this pitfall of transition listeners.

      _isIdenticalOperation: (op) ->
        return true unless @_lastOp.fn
        return false if @_lastOp.reset
        (return false if @_lastOp[key] isnt op[key]) for key in ['angle', 'anchor', 'fn']
        (return false if v isnt @_lastOp.options[k] and k isnt 'callback') for k, v of op.options
        true
  • ¶

    This method normalizes callback handling for all public methods.

      _setCallback: (operation) ->
  • ¶

    If there was no transformation, invoke the callback immediately.

        if !@_config.speed or @_isIdenticalOperation operation
          @_conclude operation.options.callback
  • ¶

    Otherwise, attach an event listener to be called on the transition’s end.

        else
          @_panels[@_lastOp.anchor][0].addEventListener css.transitionEnd, @_onTransitionEnd, false
    
        (@_lastOp = operation).reset = false
  • ¶

    Handler called when a CSS transition ends.

      _onTransitionEnd: (e) =>
  • ¶

    Remove the event listener immediately to prevent bubbling.

        e.currentTarget.removeEventListener css.transitionEnd, @_onTransitionEnd, false
  • ¶

    Initialize the transition teardown process.

        @_conclude @_lastOp.options.callback, e
  • ¶

    Used to handle the end process of transitions and to initialize queued operations.

      _conclude: (cb, event) =>
        defer =>
          @_inTrans = false
          @_step()
          cb? event, @
  • ¶

    Transforms a given element based on angle, anchor, and fracture boolean.

      _transformPanel: (el, angle, anchor, fracture) ->
        x = y = z = 0
        switch anchor
          when 'left'
            y = angle
            transPrefix = 'X(-'
          when 'right'
            y = -angle
            transPrefix = 'X('
          when 'top'
            x = -angle
            transPrefix = 'Y(-'
          when 'bottom'
            x = angle
            transPrefix = 'Y('
  • ¶

    Rotate on every axis in fracture mode.

        x = y = z = angle if fracture
    
        el.style[css.transform] = "
                                  rotateX(#{ x }deg)
                                  rotateY(#{ y }deg)
                                  rotateZ(#{ z }deg)
                                  translate#{ transPrefix }#{ @_config.gapNudge }px)
                                  "
  • ¶

    This validates a given angle by making sure it’s a float and by keeping it within the maximum range specified in the instance settings.

      _normalizeAngle: (angle) ->
        angle = parseFloat angle, 10
        max   = @_config.maxAngle
        if isNaN angle
          0
        else if angle > max
          max
        else if angle < -max
          -max
        else
          angle
  • ¶

    Allows other methods to change the transition duration/delay or disable it altogether.

      _setTrans: (duration, delay, anchor = @_lastOp.anchor) ->
        @_iterate anchor, (panel, i, len) => @_setPanelTrans anchor, arguments..., duration, delay
  • ¶

    This method changes the transition duration and delay of panels and shaders.

      _setPanelTrans: (anchor, panel, i, len, duration, delay) ->
        delayMs =
  • ¶

    Delay is a ripple value. The milliseconds are derived based on the speed setting and the number of panels.

          switch delay
            when 0 then 0
            when 1 then @_config.speed / len * i
            when 2 then @_config.speed / len * (len - i - 1)
    
        panel.style[css.transitionDuration] = duration + 'ms'
        panel.style[css.transitionDelay]    = delayMs  + 'ms'
        if @_shading
          for side in (if anchor in anchorListV then anchorListV else anchorListH)
            shader = @_shaders[anchor][side][i]
            shader.style[css.transitionDuration] = duration + 'ms'
            shader.style[css.transitionDelay]    = delayMs  + 'ms'
    
        delayMs
  • ¶

    Determines a shader’s opacity based upon panel position, anchor, and angle.

      _setShader: (n, anchor, angle) ->
  • ¶

    Store the angle’s absolute value and generate an opacity based on shadingIntensity.

        abs     = Math.abs angle
        opacity = abs / 90 * @_config.shadingIntensity
  • ¶

    With hard shading, opacity is reduced and angle is based on the global lastAngle so all panels’ shaders share the same direction. Soft shaders have alternating directions.

        if @_shading is 'hard'
          opacity *= .15
          if @_lastOp.angle < 0
            angle = abs
          else
            angle = -abs
        else
          opacity *= .4
  • ¶

    This block makes sure left and top shaders appear for negative angles and right and bottom shaders appear for positive ones.

        if anchor in anchorListV
          if angle < 0
            a = opacity
            b = 0
          else
            a = 0
            b = opacity
          @_shaders[anchor].left[n].style.opacity  = a
          @_shaders[anchor].right[n].style.opacity = b
        else
          if angle < 0
            a = 0
            b = opacity
          else
            a = opacity
            b = 0
          @_shaders[anchor].top[n].style.opacity    = a
          @_shaders[anchor].bottom[n].style.opacity = b
  • ¶

    This method shows the requested stage element and sets a reference to it as the current stage.

      _showStage: (anchor) ->
        if anchor isnt @_lastOp.anchor
          hideEl @_stages[@_lastOp.anchor]
          @_lastOp.anchor = anchor
          @_lastOp.reset  = true
          @_stages[anchor].style[css.transform] = 'translate3d(' +
            switch anchor
              when 'left'
                '0, 0, 0)'
              when 'right'
                "-#{ @_config.vPanels.length }px, 0, 0)"
              when 'top'
                '0, 0, 0)'
              when 'bottom'
                "0, -#{ @_config.hPanels.length }px, 0)"
  • ¶

    If the composition needs to switch stages or fold up, it must first unfold all panels to 0 degrees.

      _stageReset: (anchor, cb) =>
        fn = (e) =>
          e.currentTarget.removeEventListener css.transitionEnd, fn, false if e
          @_showStage anchor
          defer cb
  • ¶

    If already unfolded to 0, immediately invoke the change function.

        return fn() if @_lastOp.angle is 0
        @_panels[@_lastOp.anchor][0].addEventListener css.transitionEnd, fn, false
    
        @_iterate @_lastOp.anchor, (panel, i) =>
          @_transformPanel panel, 0, @_lastOp.anchor
          @_setShader i, @_lastOp.anchor, 0 if @_shading
  • ¶

    Converts a shorthand anchor name to a full one. Numerical shorthands are based on CSS shorthand ordering.

      _getLonghandAnchor: (shorthand) ->
        switch shorthand.toString()
          when 'left',   'l', '4'
            'left'
          when 'right',  'r', '2'
            'right'
          when 'top',    't', '1'
            'top'
          when 'bottom', 'b', '3'
            'bottom'
          else
  • ¶

    Left is always default.

            'left'
  • ¶

    Gives the element a resize cursor to prompt the user to drag the mouse.

      _setCursor: (bool = @_touchEnabled) ->
        if bool
          @el.style.cursor = css.grab
        else
          @el.style.cursor = 'default'
  • ¶

    Touch / Drag Event Handlers

  • ¶
  • ¶

    Adds or removes handlers from the element based on the boolean argument given.

      _setTouch: (toggle) ->
        if toggle
          return @ if @_touchEnabled
          listenFn = 'addEventListener'
        else
          return @ unless @_touchEnabled
          listenFn = 'removeEventListener'
    
        @_touchEnabled = toggle
        @_setCursor()
  • ¶

    Array of event type pairs.

        eventPairs = [['TouchStart', 'MouseDown'], ['TouchEnd', 'MouseUp'],
                      ['TouchMove', 'MouseMove'], ['TouchLeave', 'MouseLeave']]
  • ¶

    Detect native mouseleave support.

        mouseLeaveSupport = 'onmouseleave' of window
  • ¶

    Attach touch/drag event listeners in related pairs.

        for eventPair in eventPairs
          for eString in eventPair
            unless eString is 'TouchLeave' and !mouseLeaveSupport
              @el[listenFn] eString.toLowerCase(), @['_on' + eventPair[0]], false
            else
              @el[listenFn] 'mouseout', @_onMouseOut, false
              break
        @
  • ¶

    This method is called when a finger or mouse button is pressed on the element.

      _onTouchStart: (e) =>
        return if !@_touchEnabled or @isFoldedUp
        e.preventDefault()
  • ¶

    Clear queued animations.

        @emptyQueue()
  • ¶

    Set a property to track touch starts.

        @_touchStarted = true
  • ¶

    Change the cursor to the active grabbing state.

        @el.style.cursor = css.grabbing
  • ¶

    Disable tweening to enable instant 1 to 1 movement.

        @_setTrans 0, 0
  • ¶

    Derive the axis to fold on.

        @_touchAxis = if @_lastOp.anchor in anchorListV then 'x' else 'y'
  • ¶

    Set a reference to the last folded angle to accurately derive deltas.

        @["_#{ @_touchAxis }Last"] = @_lastOp.angle
        axis1 = "_#{ @_touchAxis }1"
  • ¶

    Determine the starting tap’s coordinate for touch and mouse events.

        if e.type is 'mousedown'
          @[axis1] = e["page#{ @_touchAxis.toUpperCase() }"]
        else
          @[axis1] = e.targetTouches[0]["page#{ @_touchAxis.toUpperCase() }"]
  • ¶

    Return that value to an external listener.

        @_config.touchStartCallback @[axis1], e
  • ¶

    Called on touch/mouse movement.

      _onTouchMove: (e) =>
        return unless @_touchEnabled and @_touchStarted
        e.preventDefault()
  • ¶

    Set a reference to the current x or y position.

        if e.type is 'mousemove'
          current = e["page#{ @_touchAxis.toUpperCase() }"]
        else
          current = e.targetTouches[0]["page#{ @_touchAxis.toUpperCase() }"]
  • ¶

    Calculate distance and multiply by touchSensitivity.

        distance = (current - @["_#{ @_touchAxis }1"]) * @_config.touchSensitivity
  • ¶

    Calculate final delta based on starting angle, anchor, and what side of zero the last operation was on.

        if @_lastOp.angle < 0
          if @_lastOp.anchor is 'right' or @_lastOp.anchor is 'bottom'
            delta = @["_#{ @_touchAxis }Last"] - distance
          else
            delta = @["_#{ @_touchAxis }Last"] + distance
          delta = 0 if delta > 0
        else
          if @_lastOp.anchor is 'right' or @_lastOp.anchor is 'bottom'
            delta = @["_#{ @_touchAxis }Last"] + distance
          else
            delta = @["_#{ @_touchAxis }Last"] - distance
          delta = 0 if delta < 0
    
    
        @_lastOp.angle = delta = @_normalizeAngle delta
        @_lastOp.fn.call @, delta, @_lastOp.anchor, @_lastOp.options
        @_config.touchMoveCallback delta, e
  • ¶

    Teardown process when touch/drag event ends.

      _onTouchEnd: (e) =>
        return unless @_touchEnabled
  • ¶

    Restore the initial touch status and cursor.

        @_touchStarted = @_inTrans = false
        @el.style.cursor = css.grab
  • ¶

    Enable transitions again.

        @_setTrans @_config.speed, @_config.ripple
  • ¶

    Pass callback final coordinate.

        @_config.touchEndCallback @["_#{ @_touchAxis }Last"], e
  • ¶

    End folding when the mouse or finger leaves the composition.

      _onTouchLeave: (e) =>
        return unless @_touchEnabled and @_touchStarted
        @_onTouchEnd e
  • ¶

    A fallback for browsers that don’t support mouseleave.

      _onMouseOut: (e) =>
        return unless @_touchEnabled and @_touchStarted
        @_onTouchEnd e if e.toElement and !@el.contains e.toElement
  • ¶

    This method unfolds the composition after it’s been folded up. It’s private and doesn’t use the decorator because it’s used internally by other methods and skips the queue. Its public counterpart is a queued alias.

      _unfold: (callback) ->
        @_inTrans = true
        {anchor}  = @_lastOp
        @_iterate anchor, (panel, i, len) =>
          delay = @_setPanelTrans anchor, arguments..., @_config.speed, 1
    
          do (panel, i, delay) =>
            defer =>
              @_transformPanel panel, 0, anchor
              @_setShader i, anchor, 0 if @_shading
    
              setTimeout =>
                showEl panel.children[0]
                if i is len - 1
                  @_inTrans = @isFoldedUp = false
                  callback?()
                  @_lastOp.fn    = @accordion
                  @_lastOp.angle = 0
    
                defer => panel.style[css.transitionDuration] = @_config.speed
    
              , delay + @_config.speed * .25
  • ¶

    This method is used by many others to iterate among panels within a given anchor.

      _iterate: (anchor, fn) ->
        fn.call @, panel, i, panels.length for panel, i in panels = @_panels[anchor]
  • ¶

    Public Methods

  • ¶
  • ¶

    Enables touch events.

      enableTouch: ->
        @_setTouch true
  • ¶

    Disables touch events.

      disableTouch: ->
        @_setTouch false
  • ¶

    Public setter for transition durations.

      setSpeed: (speed) ->
        for anchor in anchorList
          @_setTrans (@_config.speed = speed), @_config.ripple, anchor
        @
  • ¶

    Disables OriDomi slicing by showing the original, untouched target element. This is useful for certain user interactions on the inner content.

      freeze: (callback) ->
  • ¶

    Return if already frozen.

        if @isFrozen
          callback?()
        else
  • ¶

    Make sure to reset folding first.

          @_stageReset @_lastOp.anchor, =>
            @isFrozen = true
  • ¶

    Swap the visibility of the elements.

            hideEl @_stageHolder
            showEl @_cloneEl
            @_setCursor false
            callback?()
        @
  • ¶

    Restores the OriDomi version of the element for folding purposes.

      unfreeze: ->
  • ¶

    Only unfreeze if already frozen.

        if @isFrozen
          @isFrozen = false
  • ¶

    Swap the visibility of the elements.

          hideEl @_cloneEl
          showEl @_stageHolder
          @_setCursor()
  • ¶

    Set lastAngle to 0 so an immediately subsequent call to freeze triggers the callback.

          @_lastOp.angle = 0
        @
  • ¶

    Removes the OriDomi element and restores the original element.

      destroy: (callback) ->
  • ¶

    First restore the original element.

        @freeze =>
  • ¶

    Remove event listeners.

          @_setTouch false
  • ¶

    Remove the data reference if using jQuery.

          $.data @el, baseName, null if $
  • ¶

    Remove the OriDomi element from the DOM.

          @el.innerHTML = @_cloneEl.innerHTML
  • ¶

    Reset original styling.

          @el.classList.remove elClasses.active
          callback?()
        null
  • ¶

    Empties the queue should you want to cancel scheduled animations.

      emptyQueue: ->
        @_queue = []
        defer => @_inTrans = false
        @
  • ¶

    Enable or disable ripple. 1 is forwards, 2 is backwards, 0 is disabled.

      setRipple: (dir = 1) ->
        @_config.ripple = Number dir
        @setSpeed @_config.speed
        @
  • ¶

    Setter method for maxAngle.

      constrainAngle: (angle) ->
        @_config.maxAngle = parseFloat(angle, 10) or defaults.maxAngle
        @
  • ¶

    Pause in the midst of an animation sequence, in milliseconds. E.g.: el.reveal(20).wait(5000).accordion(-33)

      wait: (ms) ->
        fn = => setTimeout @_conclude, ms
        if @_inTrans
          @_queue.push [fn, @_lastOp.angle, @_lastOp.anchor, @_lastOp.options]
        else
          fn()
        @
  • ¶

    This method is used to externally manipulate the styling or contents of the composition. Manipulation instructions can be supplied via a function (invoked with each panel element), or a map of selectors with instructions. Instruction values can be text to implicitly update innerHTML content or objects with style and/or content keys. Style keys should contain object literals with camel-cased CSS properties as keys.

      modifyContent: (fn) ->
        if typeof fn isnt 'function'
          selectors = fn
    
          set = (el, content, style) ->
            el.innerHTML = content if content
            if style
              el.style[key] = value for key, value of style
              null
    
          fn = (el) ->
            for selector, value of selectors
              content = style = null
              if typeof value is 'string'
                content = value
              else
                {content, style} = value
    
              if selector is ''
                set el, content, style
                continue
    
              set match, content, style for match in el.querySelectorAll selector
    
            null
    
        for anchor in anchorList
          for panel, i in @_panels[anchor]
            fn panel.children[0].children[0], i, anchor
        @
  • ¶

    Effect Methods

  • ¶
  • ¶

    Base effect with alternating peaks and valleys. reveal relies on it by calling it with sticky: true to keep the first panel flat.

      accordion: prep (angle, anchor, options) ->
        @_iterate anchor, (panel, i) =>
  • ¶

    With an odd-numbered panel, reverse the angle.

          if i % 2 isnt 0 and !options.twist
            deg = -angle
          else
            deg = angle
  • ¶

    If sticky, keep the first panel flat.

          if options.sticky
            if i is 0
              deg = 0
            else if i > 1 or options.stairs
              deg *= 2
          else
  • ¶

    Double the angle to counteract the angle of the parent panel.

            deg *= 2 unless i is 0
  • ¶

    In stairs mode, keep all the angles on the same side of 0.

          deg *= -1 if options.stairs
  • ¶

    Set the CSS transformation.

          @_transformPanel panel, deg, anchor, options.fracture
    
          if @_shading
            if options.twist or options.fracture or (i is 0 and options.sticky)
              @_setShader i, anchor, 0
            else if Math.abs(deg) isnt 180
              @_setShader i, anchor, deg
  • ¶

    This effect appears to bend rather than fold the paper. Its curves can appear smoother with higher panel counts.

      curl: prep (angle, anchor, options) ->
  • ¶

    Reduce the angle based on the number of panels in this axis.

        angle /= if anchor in anchorListV then @_config.vPanels.length else @_config.hPanels.length
    
        @_iterate anchor, (panel, i) =>
          @_transformPanel panel, angle, anchor
          @_setShader i, anchor, 0 if @_shading
  • ¶

    Lifts up all panels after the first one.

      ramp: prep (angle, anchor, options) ->
  • ¶

    Rotate the second panel for the lift up.

        @_transformPanel @_panels[anchor][1], angle, anchor
  • ¶

    For all but the second panel, set the angle to 0.

        @_iterate anchor, (panel, i) =>
          @_transformPanel panel, 0, anchor if i isnt 1
          @_setShader i, anchor, 0 if @_shading
  • ¶

    Hides the element by folding each panel in a cascade of animations.

      foldUp: prep (anchor, callback) ->
        return callback?() if @isFoldedUp
        @_stageReset anchor, =>
          @_inTrans = @isFoldedUp = true
    
          @_iterate anchor, (panel, i, len) =>
            duration  = @_config.speed
            duration /= 2 if i is 0
            delay     = @_setPanelTrans anchor, arguments..., duration, 2
    
            do (panel, i, delay) =>
              defer =>
                @_transformPanel panel, (if i is 0 then 90 else 170), anchor
                setTimeout =>
                  if i is 0
                    @_inTrans = false
                    callback?()
                  else
                    hideEl panel.children[0]
    
                , delay + @_config.speed * .25
  • ¶

    This is the queued version of _unfold.

      unfold: prep @::_unfold
  • ¶

    For custom folding behavior, you can pass a function to map() that will determine the folding angle applied to each panel. The passed function is supplied with the input angle, the panel index, and the number of panels in the active anchor. Calling map returns a new function bound to the instance and the lambda, e.g. oridomi.map(randomFn)(30).reveal(20).

      map: (fn) ->
        prep (angle, anchor, options) =>
          @_iterate anchor, (panel, i, len) =>
            @_transformPanel panel, fn(angle, i, len), anchor, options.fracture
        .bind @
  • ¶

    Convenience Methods

  • ¶
  • ¶

    Resets all panels back to zero degrees.

      reset: (callback) ->
        @accordion 0, {callback}
  • ¶

    Simply proxy for calling accordion with sticky enabled. Keeps first panel flat on page.

      reveal: (angle, anchor, options = {}) ->
        options.sticky = true
        @accordion angle, anchor, options
  • ¶

    Proxy to enable stairs mode on accordion.

      stairs: (angle, anchor, options = {}) ->
        options.stairs = options.sticky = true
        @accordion angle, anchor, options
  • ¶

    The composition is split apart by its panels rather than folded.

      fracture: (angle, anchor, options = {}) ->
        options.fracture = true
        @accordion angle, anchor, options
  • ¶

    Similar to fracture, but the panels are twisted as well.

      twist: (angle, anchor, options = {}) ->
        options.fracture = options.twist = true
        @accordion angle / 10, anchor, options
  • ¶

    Convenience proxy to accordion-fold instance to maximum angle.

      collapse: (anchor, options = {}) ->
        options.sticky = false
        @accordion -@_config.maxAngle, anchor, options
  • ¶

    Same as collapse, but uses positive angle for slightly different effect.

      collapseAlt: (anchor, options = {}) ->
        options.sticky = false
        @accordion @_config.maxAngle, anchor, options
  • ¶

    Statics

  • ¶
  • ¶

    Set a version flag for easy external retrieval.

      @VERSION = '1.1.5'
  • ¶

    Externally reveal if OriDomi is supported by the browser.

      @isSupported = isSupported
  • ¶

    Expose the OriDomi constructor via CommonJS, AMD, or the window object.

    if module?.exports
      module.exports = OriDomi
    else if define?.amd
      define -> OriDomi
    else
      window.OriDomi = OriDomi
  • ¶

    Plugin Bridge

  • ¶
  • ¶

    Only create bridge if jQuery (or an imitation supporting data()) exists.

    return unless $
  • ¶

    Attach an OriDomi method to $‘s prototype.

    $::oriDomi = (options) ->
  • ¶

    Return selection if OriDomi is unsupported by the browser.

      return @ unless isSupported
      return $.data @[0], baseName if options is true
  • ¶

    If options is a string, assume it’s a method call.

      if typeof options is 'string'
        methodName = options
  • ¶

    Check if method exists and warn if it doesn’t.

        unless typeof (method = OriDomi::[methodName]) is 'function'
          console?.warn "#{ libName }: No such method `#{ methodName }`"
          return @
    
        for el in @
    
          unless instance = $.data el, baseName
            instance = $.data el, baseName, new OriDomi el, options
  • ¶

    Call the requested method with arguments.

          method.apply instance, Array::slice.call(arguments)[1...]
  • ¶

    If not calling a method, initialize OriDomi on the selection.

      else
        for el in @
  • ¶

    If the element in the selection already has an instance of OriDomi attached to it, return the instance.

          if instance = $.data el, baseName
            continue
          else
  • ¶

    Create an instance of OriDomi and attach it to the element.

            $.data el, baseName, new OriDomi el, options
  • ¶

    Return the selection.

      @