libName = 'OriDomi'
Copyright 2014, MIT License
libName = 'OriDomi'
This variable is set to true and negated later if the browser does not support OriDomi.
isSupported = true
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 = ->
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
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
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
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'
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]
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
@
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 @
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
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
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.
@